diff --git a/src/cli/commands/__tests__/init.test.ts b/src/cli/commands/__tests__/init.test.ts index b1da49ea..6f7f82ee 100644 --- a/src/cli/commands/__tests__/init.test.ts +++ b/src/cli/commands/__tests__/init.test.ts @@ -159,6 +159,30 @@ describe('init command', () => { stdoutSpy.mockRestore(); }); + it('errors with skip reason when only Claude is detected for MCP auto-install', async () => { + const fakeHome = join(tempDir, 'home-only-claude'); + mkdirSync(join(fakeHome, '.claude'), { recursive: true }); + mockedHomedir.mockReturnValue(fakeHome); + + const yargs = (await import('yargs')).default; + const mod = await loadInitModule(); + + const app = yargs(['init', '--skill', 'mcp']).scriptName('').fail(false); + mod.registerInitCommand(app); + + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + await expect(app.parseAsync()).rejects.toThrow( + 'No eligible install targets after applying skill policy. Skipped: Claude Code: MCP skill is unnecessary because Claude Code already uses server instructions.', + ); + + const output = stdoutSpy.mock.calls.map((c) => String(c[0])).join(''); + expect(output).toContain( + 'Skipped Claude Code: MCP skill is unnecessary because Claude Code already uses server instructions.', + ); + + stdoutSpy.mockRestore(); + }); + it('allows explicit Claude MCP install with --client claude', async () => { const fakeHome = join(tempDir, 'home-explicit-claude'); mkdirSync(join(fakeHome, '.claude'), { recursive: true }); diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 96357730..2dab509c 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -100,6 +100,14 @@ interface InstallPolicyResult { skippedClients: Array<{ client: string; reason: string }>; } +function formatSkippedClients(skippedClients: Array<{ client: string; reason: string }>): string { + if (skippedClients.length === 0) { + return ''; + } + + return skippedClients.map((skipped) => `${skipped.client}: ${skipped.reason}`).join('; '); +} + async function installSkill( skillsDir: string, clientName: string, @@ -348,8 +356,14 @@ export function registerInitCommand(app: Argv, ctx?: { workspaceRoot: string }): const targets = resolveTargets(clientFlag, destFlag, 'install'); const policy = enforceInstallPolicy(targets, skillType, clientFlag, destFlag); + for (const skipped of policy.skippedClients) { + writeLine(`Skipped ${skipped.client}: ${skipped.reason}`); + } + if (policy.allowedTargets.length === 0) { - throw new Error('No eligible install targets after applying skill policy.'); + const skippedSummary = formatSkippedClients(policy.skippedClients); + const reasonSuffix = skippedSummary.length > 0 ? ` Skipped: ${skippedSummary}` : ''; + throw new Error(`No eligible install targets after applying skill policy.${reasonSuffix}`); } const results: InstallResult[] = []; @@ -361,10 +375,6 @@ export function registerInitCommand(app: Argv, ctx?: { workspaceRoot: string }): results.push(result); } - for (const skipped of policy.skippedClients) { - writeLine(`Skipped ${skipped.client}: ${skipped.reason}`); - } - writeLine(`Installed ${skillDisplayName(skillType)} skill`); for (const result of results) { writeLine(` Client: ${result.client}`); @@ -393,6 +403,10 @@ function enforceInstallPolicy( return { allowedTargets: targets, skippedClients: [] }; } + if (clientFlag === 'claude') { + return { allowedTargets: targets, skippedClients: [] }; + } + const allowedTargets: ClientInfo[] = []; const skippedClients: Array<{ client: string; reason: string }> = []; @@ -407,9 +421,5 @@ function enforceInstallPolicy( allowedTargets.push(target); } - if (clientFlag === 'claude') { - return { allowedTargets: targets, skippedClients: [] }; - } - return { allowedTargets, skippedClients }; }