From cc333deb4d2aa8d367b4c27520bb21287628e793 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Thu, 26 Mar 2026 17:16:52 -0500 Subject: [PATCH] fix: separate --yes from --force in bootstrap, adf, and setup commands (#65) --yes now means non-interactive (accept defaults, refuse to overwrite custom content). --force explicitly allows overwriting. Also: - Backup filenames include ISO timestamps (.ai/.backup/.) - Overwrite warnings include byte counts for visibility - Orphaned ADF modules are auto-registered in --yes mode - Package.json detection checks worker/, src/, app/ nested dirs Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 5 +++ packages/cli/src/__tests__/bootstrap.test.ts | 35 +++++++++++++++----- packages/cli/src/commands/adf.ts | 6 ++-- packages/cli/src/commands/bootstrap.ts | 26 +++++++++++---- packages/cli/src/commands/setup.ts | 4 +-- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 1670e0c..15e573b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,8 @@ C:* node_modules/ .pnpm-store/ __pycache__/ +# cc-taskrunner worktree protection +C:* +node_modules/ +.pnpm-store/ +__pycache__/ diff --git a/packages/cli/src/__tests__/bootstrap.test.ts b/packages/cli/src/__tests__/bootstrap.test.ts index c2f89f1..699127e 100644 --- a/packages/cli/src/__tests__/bootstrap.test.ts +++ b/packages/cli/src/__tests__/bootstrap.test.ts @@ -52,7 +52,9 @@ CONTEXT: expect(fs.existsSync(path.join('.ai', '.backup'))).toBe(false); expect(fs.existsSync(path.join('.ai', 'manifest.adf'))).toBe(true); expect(fs.existsSync(path.join('.ai', 'state.adf'))).toBe(true); - expect(logs).toContain(' Warning: .ai/core.adf has custom content; skipping scaffold overwrite'); + const skipWarning = logs.find(l => l.includes('.ai/core.adf has custom content') && l.includes('skipping scaffold overwrite')); + expect(skipWarning).toBeDefined(); + expect(skipWarning).toMatch(/\d+ bytes/); }); it('backs up and overwrites custom ADF files when run with --force', async () => { @@ -75,15 +77,26 @@ STATE: ); expect(exitCode).toBe(0); - expect(fs.readFileSync(path.join('.ai', '.backup', 'core.adf'), 'utf-8')).toBe(customCore); - expect(fs.readFileSync(path.join('.ai', '.backup', 'state.adf'), 'utf-8')).toBe(customState); + + // Backup files should exist with timestamp suffix + const backupDir = path.join('.ai', '.backup'); + expect(fs.existsSync(backupDir)).toBe(true); + const backupFiles = fs.readdirSync(backupDir); + const coreBackup = backupFiles.find(f => f.startsWith('core.adf.')); + const stateBackup = backupFiles.find(f => f.startsWith('state.adf.')); + expect(coreBackup).toBeDefined(); + expect(stateBackup).toBeDefined(); + expect(fs.readFileSync(path.join(backupDir, coreBackup!), 'utf-8')).toBe(customCore); + expect(fs.readFileSync(path.join(backupDir, stateBackup!), 'utf-8')).toBe(customState); + + // Originals should be overwritten with scaffold content expect(fs.readFileSync(path.join('.ai', 'core.adf'), 'utf-8')).not.toBe(customCore); expect(fs.readFileSync(path.join('.ai', 'state.adf'), 'utf-8')).not.toBe(customState); expect(logs).toContain(' Backed up 2 files to .ai/.backup/'); }); - it('detects orphaned .adf modules not in manifest (--yes mode prints warning)', async () => { - // Pre-create .ai/ with an extra module that won't be in the scaffold manifest + it('detects orphaned .adf modules and auto-registers them in --yes mode', async () => { + // Pre-create .ai/ with extra modules that won't be in the scaffold manifest fs.mkdirSync('.ai', { recursive: true }); fs.writeFileSync(path.join('.ai', 'agent.adf'), 'ADF: 0.1\n\nCONTEXT:\n - Agent rules\n'); fs.writeFileSync(path.join('.ai', 'persona.adf'), 'ADF: 0.1\n\nCONTEXT:\n - Persona rules\n'); @@ -99,8 +112,14 @@ STATE: expect(orphanWarning).toBeDefined(); expect(orphanWarning).toContain('agent.adf'); expect(orphanWarning).toContain('persona.adf'); - // Should suggest the register command - const registerHint = logs.find(l => l.includes('charter adf register')); - expect(registerHint).toBeDefined(); + + // In --yes mode, orphans should be auto-registered in manifest + const registerLog = logs.find(l => l.includes('Registered 2 module(s) as ON_DEMAND')); + expect(registerLog).toBeDefined(); + + // Verify manifest contains the orphan entries + const manifest = fs.readFileSync(path.join('.ai', 'manifest.adf'), 'utf-8'); + expect(manifest).toContain('agent.adf'); + expect(manifest).toContain('persona.adf'); }); }); diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts index 605aacf..3c014b7 100644 --- a/packages/cli/src/commands/adf.ts +++ b/packages/cli/src/commands/adf.ts @@ -299,7 +299,7 @@ See .ai/manifest.adf for the module routing manifest. `; function adfInit(options: CLIOptions, args: string[]): number { - const force = options.yes || args.includes('--force'); + const force = args.includes('--force'); const aiDir = getFlag(args, '--ai-dir') || '.ai'; const moduleFlag = getFlag(args, '--module'); const presetFlag = getFlag(args, '--preset'); @@ -323,7 +323,7 @@ function adfInit(options: CLIOptions, args: string[]): number { console.log(''); console.log(' .ai/ directory already exists. Run \'charter doctor\' to check for issues.'); console.log(''); - console.log(' Use --force (or --yes) to overwrite.'); + console.log(' Use --force to overwrite existing files.'); console.log(' To add a single module: charter adf init --module '); } return EXIT_CODE.SUCCESS; @@ -566,7 +566,7 @@ function adfCreate(options: CLIOptions, args: string[]): number { } const aiDir = getFlag(args, '--ai-dir') || '.ai'; - const force = options.yes || args.includes('--force'); + const force = args.includes('--force'); const load = (getFlag(args, '--load') || 'on-demand').toLowerCase(); if (load !== 'default' && load !== 'on-demand') { throw new CLIError(`Invalid --load value: ${load}. Use default or on-demand.`); diff --git a/packages/cli/src/commands/bootstrap.ts b/packages/cli/src/commands/bootstrap.ts index d7e3dba..53b38e5 100644 --- a/packages/cli/src/commands/bootstrap.ts +++ b/packages/cli/src/commands/bootstrap.ts @@ -165,15 +165,22 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro console.log(''); } - // Orphan registration prompt (interactive only) + // Orphan registration: auto-register in --yes mode, prompt interactively otherwise const orphans = adfResult.step.details.orphans as string[] || []; - if (orphans.length > 0 && !nonInteractive && options.format === 'text') { - const shouldRegister = await promptYesNo(' Register these modules now? (y/N) '); + if (orphans.length > 0) { + let shouldRegister = false; + if (nonInteractive) { + shouldRegister = true; + } else if (options.format === 'text') { + shouldRegister = await promptYesNo(' Register these modules now? (y/N) '); + } if (shouldRegister) { registerOrphansInManifest(path.join('.ai', 'manifest.adf'), orphans); updateModuleIndex('CLAUDE.md', '.ai'); - console.log(` Registered ${orphans.length} module(s) as ON_DEMAND in manifest.adf`); - console.log(''); + if (options.format === 'text') { + console.log(` Registered ${orphans.length} module(s) as ON_DEMAND in manifest.adf`); + console.log(''); + } } } @@ -514,6 +521,7 @@ function writeAdfScaffolds( const warnings: string[] = []; let backedUp = 0; let backupDir: string | undefined; + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); for (const scaffold of getAdfScaffolds(preset)) { const targetPath = path.join(aiDir, scaffold.name); @@ -530,14 +538,18 @@ function writeAdfScaffolds( continue; } + const byteCount = Buffer.byteLength(existing, 'utf-8'); + if (!force) { - warnings.push(`${label} has custom content; skipping scaffold overwrite`); + warnings.push(`${label} has custom content (${byteCount} bytes); skipping scaffold overwrite`); continue; } backupDir ||= path.join(aiDir, '.backup'); fs.mkdirSync(backupDir, { recursive: true }); - fs.copyFileSync(targetPath, path.join(backupDir, scaffold.name)); + const backupName = `${scaffold.name}.${timestamp}`; + fs.copyFileSync(targetPath, path.join(backupDir, backupName)); + warnings.push(`Backed up ${label} (${byteCount} bytes) → .ai/.backup/${backupName}`); backedUp++; fs.writeFileSync(targetPath, scaffold.content); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 26d3fd9..d0ad653 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -178,7 +178,7 @@ export async function setupCommand(options: CLIOptions, args: string[]): Promise const presetFlag = getFlag(args, '--preset'); const detectOnly = args.includes('--detect-only'); const explicitForce = args.includes('--force'); - const force = options.yes || explicitForce; + const force = explicitForce; const noDependencySync = args.includes('--no-dependency-sync'); if (ciMode && ciMode !== 'github') { @@ -645,7 +645,7 @@ export function detectStack(contexts: PackageContext[]): DetectionResult { export function loadPackageContexts(): PackageContext[] { const candidates = new Set(['package.json']); - for (const dir of ['client', 'frontend', 'web']) { + for (const dir of ['client', 'frontend', 'web', 'worker', 'src', 'app']) { candidates.add(path.join(dir, 'package.json')); } candidates.add(path.join('apps', 'web', 'package.json'));