diff --git a/docs/plans/state.json b/docs/plans/state.json index 48e022f..39a8b44 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1379,6 +1379,15 @@ "src/cli/skills.test.ts" ], "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" + }, + "install_preflight_only_mode": { + "status": "completed", + "description": "Added --preflight-only mode to skills install for plan-only previews without performing installation, including JSON output path", + "files_modified": [ + "src/cli/skills.ts", + "src/cli/skills.test.ts" + ], + "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" } } } @@ -1407,7 +1416,7 @@ }, "overall_progress": { - "total_test_count": 1528, + "total_test_count": 1529, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -1427,7 +1436,7 @@ "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback", "remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 2/2 (100%) — component registry, confidence routing. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening", - "next_up": "Skills infrastructure Phase 3: add install preflight-only mode to print plan without performing install" + "next_up": "Skills infrastructure Phase 3: add installer execution stub command that consumes plan output but does not run package manager commands yet" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/cli/skills.test.ts b/src/cli/skills.test.ts index 2b0d5cd..ad0788d 100644 --- a/src/cli/skills.test.ts +++ b/src/cli/skills.test.ts @@ -187,6 +187,17 @@ describe('skills CLI helpers', () => { rmSync(root, { recursive: true, force: true }); }); + it('returns null install preflight view when source is invalid', () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const sourceDir = join(root, 'invalid-source-skill'); + mkdirSync(sourceDir, { recursive: true }); + + const view = toSkillInstallPreflightView(sourceDir); + + expect(view).toBeNull(); + rmSync(root, { recursive: true, force: true }); + }); + it('renders install preflight output text', () => { const output = renderSkillInstallPreflight({ sourcePath: '/tmp/source-skill', diff --git a/src/cli/skills.ts b/src/cli/skills.ts index 2df9d95..1a2010d 100644 --- a/src/cli/skills.ts +++ b/src/cli/skills.ts @@ -340,8 +340,9 @@ export function registerSkillsCommand(program: Command): void { .command('install ') .description('Install a skill from a local directory') .option('--json', 'Output preflight and install result as JSON') + .option('--preflight-only', 'Show installer preflight without performing install') .option('-c, --config ', 'Config file path') - .action((pathArg: string, opts: { json?: boolean; config?: string }) => { + .action((pathArg: string, opts: { json?: boolean; preflightOnly?: boolean; config?: string }) => { const loaded = loadConfigSafe(opts.config); if (loaded.error || !loaded.config) { console.error(loaded.error ?? 'Failed to load config'); @@ -353,6 +354,22 @@ export function registerSkillsCommand(program: Command): void { const installer = new SkillInstaller(loaded.config.skills.managed_dir ?? defaultManagedDir); const preflight = toSkillInstallPreflightView(pathArg); + if (opts.preflightOnly) { + if (!preflight) { + console.error(`Failed to generate install preflight from '${resolve(pathArg)}'.`); + process.exitCode = 1; + return; + } + + if (opts.json) { + console.log(JSON.stringify({ preflight }, null, 2)); + return; + } + + console.log(renderSkillInstallPreflight(preflight)); + return; + } + if (preflight) { if (opts.json) { console.log(JSON.stringify({ preflight }, null, 2));