diff --git a/src/auth/auth-commands.ts b/src/auth/auth-commands.ts index 86a3c710..85458288 100644 --- a/src/auth/auth-commands.ts +++ b/src/auth/auth-commands.ts @@ -91,6 +91,9 @@ class AuthCommands { ` ${color('ccs auth create backup --context-group sprint-a --deeper-continuity', 'command')}` ); console.log(''); + console.log(` ${dim('# Create clean profile without shared commands/skills/agents')}`); + console.log(` ${color('ccs auth create sandbox --bare', 'command')}`); + console.log(''); console.log(` ${dim('# Set work as default')}`); console.log(` ${color('ccs auth default work', 'command')}`); console.log(''); @@ -116,6 +119,9 @@ class AuthCommands { console.log( ` ${color('--deeper-continuity', 'command')} Advanced shared mode: sync additional continuity artifacts` ); + console.log( + ` ${color('--bare', 'command')} Create clean profile without shared symlinks (no CK/commands/skills)` + ); console.log( ` ${color('--yes, -y', 'command')} Skip confirmation prompts (remove)` ); diff --git a/src/auth/commands/create-command.ts b/src/auth/commands/create-command.ts index 93550a2e..79b76885 100644 --- a/src/auth/commands/create-command.ts +++ b/src/auth/commands/create-command.ts @@ -31,7 +31,7 @@ function sanitizeProfileNameForInstance(name: string): string { */ export async function handleCreate(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName, force, shareContext, contextGroup, deeperContinuity, unknownFlags } = + const { profileName, force, shareContext, contextGroup, deeperContinuity, bare, unknownFlags } = parseArgs(args); if (unknownFlags && unknownFlags.length > 0) { @@ -39,7 +39,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise console.log(fail(`Unknown option(s): ${unknownList}`)); console.log(''); console.log( - `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` + `Usage: ${color('ccs auth create [--force] [--bare] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` ); console.log(`Help: ${color('ccs auth --help', 'command')}`); console.log(''); @@ -50,7 +50,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise console.log(fail('Profile name is required')); console.log(''); console.log( - `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` + `Usage: ${color('ccs auth create [--force] [--bare] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` ); console.log(''); console.log('Example:'); @@ -179,7 +179,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise try { // Create instance directory console.log(info(`Creating profile: ${profileName}`)); - const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy); + const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy, { + bare: !!bare, + }); // Create/update profile entry based on config mode if (useUnifiedConfig) { @@ -188,10 +190,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise ctx.registry.updateAccountUnified(profileName, { context_mode: contextMetadata.context_mode, context_group: contextMetadata.context_group, + ...(bare ? { bare: true } : {}), }); ctx.registry.touchAccountUnified(profileName); } else { - ctx.registry.createAccountUnified(profileName, contextMetadata); + ctx.registry.createAccountUnified(profileName, { + ...contextMetadata, + ...(bare ? { bare: true } : {}), + }); } } else { // Use legacy profiles.json @@ -200,12 +206,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise type: 'account', context_mode: contextMetadata.context_mode, context_group: contextMetadata.context_group, + ...(bare ? { bare: true } : {}), }); } else { ctx.registry.createProfile(profileName, { type: 'account', context_mode: contextMetadata.context_mode, context_group: contextMetadata.context_group, + ...(bare ? { bare: true } : {}), }); } } @@ -262,7 +270,8 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise `Profile: ${profileName}\n` + `Instance: ${instancePath}\n` + `Type: account\n` + - `Context: ${formatAccountContextPolicy(contextPolicy)}`, + `Context: ${formatAccountContextPolicy(contextPolicy)}` + + (bare ? '\nMode: bare (no shared symlinks)' : ''), 'Profile Created' ) ); diff --git a/src/auth/commands/show-command.ts b/src/auth/commands/show-command.ts index f127f70c..63aab594 100644 --- a/src/auth/commands/show-command.ts +++ b/src/auth/commands/show-command.ts @@ -63,6 +63,7 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise', - 'Create account profile (supports shared groups + --deeper-continuity)', + 'Create account profile (supports --bare, shared groups, --deeper-continuity)', ], ['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'], [ diff --git a/src/commands/sync-command.ts b/src/commands/sync-command.ts index 2ceb5d16..86d58b6a 100644 --- a/src/commands/sync-command.ts +++ b/src/commands/sync-command.ts @@ -4,7 +4,7 @@ * Handle sync command for CCS. */ -import { initUI, header, ok } from '../utils/ui'; +import { initUI, header, ok, info } from '../utils/ui'; /** * Handle sync command @@ -41,6 +41,31 @@ export async function handleSyncCommand(): Promise { sharedManager.ensureSharedDirectories(); console.log(ok('Shared symlinks verified')); + // Sync MCP servers from global ~/.claude.json to all non-bare instances + const { InstanceManager } = await import('../management/instance-manager'); + const instanceMgr = new InstanceManager(); + const ProfileRegistry = (await import('../auth/profile-registry')).default; + const registry = new ProfileRegistry(); + const allProfiles = registry.getAllProfilesMerged(); + let mcpSynced = 0; + + for (const [name, profile] of Object.entries(allProfiles)) { + if (profile.bare) { + continue; // Skip bare profiles + } + const instancePath = instanceMgr.getInstancePath(name); + if (instancePath) { + instanceMgr.syncMcpServers(instancePath); + mcpSynced++; + } + } + + if (mcpSynced > 0) { + console.log(ok(`MCP servers synced to ${mcpSynced} instance(s)`)); + } else { + console.log(info('No instances to sync MCP servers')); + } + console.log(''); console.log(ok('Sync complete!')); console.log(''); diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 5ddc755a..89ed7d9f 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -46,6 +46,8 @@ export interface AccountConfig { context_group?: string; /** Shared continuity depth when context_mode='shared' */ continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; } /** diff --git a/src/management/instance-manager.ts b/src/management/instance-manager.ts index 8f49cf74..3ad89c16 100644 --- a/src/management/instance-manager.ts +++ b/src/management/instance-manager.ts @@ -11,7 +11,13 @@ import * as path from 'path'; import SharedManager from './shared-manager'; import ProfileContextSyncLock from './profile-context-sync-lock'; import { AccountContextPolicy, DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context'; -import { getCcsDir } from '../utils/config-manager'; +import { getCcsDir, getCcsHome } from '../utils/config-manager'; + +/** Options for instance creation */ +interface InstanceOptions { + /** Skip shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; +} /** * Instance Manager Class @@ -32,7 +38,8 @@ class InstanceManager { */ async ensureInstance( profileName: string, - contextPolicy: AccountContextPolicy = { mode: DEFAULT_ACCOUNT_CONTEXT_MODE } + contextPolicy: AccountContextPolicy = { mode: DEFAULT_ACCOUNT_CONTEXT_MODE }, + options: InstanceOptions = {} ): Promise { const instancePath = this.getInstancePath(profileName); @@ -40,7 +47,7 @@ class InstanceManager { await this.contextSyncLock.withLock(profileName, async () => { // Lazy initialization if (!fs.existsSync(instancePath)) { - this.initializeInstance(profileName, instancePath); + this.initializeInstance(profileName, instancePath, options); } // Validate structure (auto-fix missing dirs) @@ -51,6 +58,11 @@ class InstanceManager { await this.sharedManager.syncAdvancedContinuityArtifacts(instancePath, contextPolicy); }); + // Sync MCP servers from global ~/.claude.json (unless bare) + if (!options.bare) { + this.syncMcpServers(instancePath); + } + return instancePath; } @@ -65,7 +77,11 @@ class InstanceManager { /** * Initialize new instance directory */ - private initializeInstance(profileName: string, instancePath: string): void { + private initializeInstance( + profileName: string, + instancePath: string, + options: InstanceOptions = {} + ): void { try { // Create base directory fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 }); @@ -88,8 +104,10 @@ class InstanceManager { } }); - // Symlink shared directories (Phase 1: commands, skills) - this.sharedManager.linkSharedDirectories(instancePath); + // Bare profiles skip shared symlinks (commands, skills, agents, settings.json) + if (!options.bare) { + this.sharedManager.linkSharedDirectories(instancePath); + } // Copy global configs if exist (settings.json only) this.copyGlobalConfigs(instancePath); @@ -167,33 +185,49 @@ class InstanceManager { */ private copyGlobalConfigs(_instancePath: string): void { // No longer needed - settings.json now symlinked via SharedManager - // Keeping method for backward compatibility (empty implementation) - // Can be removed in future major version } /** - * Copy directory recursively - Currently unused + * Sync MCP servers from global ~/.claude.json to instance .claude.json. + * Selectively copies only mcpServers key (not OAuth sessions or caches). */ - /* - private copyDirectory(src: string, dest: string): void { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true, mode: 0o700 }); + syncMcpServers(instancePath: string): void { + const homeDir = getCcsHome(); + const globalClaudeJson = path.join(homeDir, '.claude.json'); + + if (!fs.existsSync(globalClaudeJson)) { + return; } - const entries = fs.readdirSync(src, { withFileTypes: true }); + try { + const globalContent = JSON.parse(fs.readFileSync(globalClaudeJson, 'utf8')); + const mcpServers = globalContent.mcpServers; - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - - if (entry.isDirectory()) { - this.copyDirectory(srcPath, destPath); - } else { - fs.copyFileSync(srcPath, destPath); + if (!mcpServers || Object.keys(mcpServers).length === 0) { + return; } + + const instanceClaudeJson = path.join(instancePath, '.claude.json'); + let instanceContent: Record = {}; + + if (fs.existsSync(instanceClaudeJson)) { + try { + instanceContent = JSON.parse(fs.readFileSync(instanceClaudeJson, 'utf8')); + } catch { + // Corrupted file, start fresh + instanceContent = {}; + } + } + + // Merge: global MCP servers as base, instance-specific overrides on top + const existingMcp = (instanceContent.mcpServers as Record | undefined) || {}; + instanceContent.mcpServers = { ...mcpServers, ...existingMcp }; + + fs.writeFileSync(instanceClaudeJson, JSON.stringify(instanceContent, null, 2), 'utf8'); + } catch { + // Best-effort: don't fail instance creation if MCP sync fails } } - */ /** * Sanitize profile name for filesystem diff --git a/src/types/config.ts b/src/types/config.ts index 754753dc..f3c2541a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -100,6 +100,8 @@ export interface ProfileMetadata { context_group?: string; /** Shared continuity depth when context_mode='shared' */ continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; } export interface ProfilesRegistry {