diff --git a/packages/cli/__tests__/init.test.ts b/packages/cli/__tests__/init.test.ts index b1f17a7..eb11cb5 100644 --- a/packages/cli/__tests__/init.test.ts +++ b/packages/cli/__tests__/init.test.ts @@ -10,7 +10,15 @@ */ import { describe, it, expect } from 'vitest' -import { buildClients, runInit, renderTable, type ClientSpec, type FsLike } from '../src/init.js' +import { + buildClients, + runInit, + renderTable, + shouldPrintOpenSkillsFallback, + type ClientSpec, + type FsLike, + type InstallResult, +} from '../src/init.js' /** Build an in-memory fs-like surface for tests. */ function makeFs(initial: { dirs?: string[]; files?: string[] } = {}): FsLike & { @@ -215,6 +223,54 @@ describe('JSON summary shape', () => { }) }) +describe('shouldPrintOpenSkillsFallback', () => { + it('fires when no Tier-1 client is detected', () => { + const results: InstallResult[] = [ + { client: 'claude-code', status: 'skipped', reason: 'not detected' }, + { client: 'cursor', status: 'skipped', reason: 'not detected' }, + { client: 'windsurf', status: 'skipped', reason: 'not detected' }, + { client: 'cline', status: 'skipped', reason: 'not detected' }, + { client: 'continue', status: 'skipped', reason: 'not detected' }, + ] + expect(shouldPrintOpenSkillsFallback(results)).toBe(true) + }) + + it('fires when only an advisory client (Continue.dev) is detected', () => { + const results: InstallResult[] = [ + { client: 'claude-code', status: 'skipped', reason: 'not detected' }, + { client: 'continue', status: 'advisory', reason: 'no native skills surface' }, + ] + expect(shouldPrintOpenSkillsFallback(results)).toBe(true) + }) + + it('does NOT fire on re-run when a Tier-1 client is already installed', () => { + const results: InstallResult[] = [ + { + client: 'claude-code', + status: 'skipped', + reason: 'already installed (use --force to overwrite)', + }, + { client: 'cursor', status: 'skipped', reason: 'not detected' }, + ] + expect(shouldPrintOpenSkillsFallback(results)).toBe(false) + }) + + it('does NOT fire when at least one Tier-1 client was installed this run', () => { + const results: InstallResult[] = [ + { client: 'claude-code', status: 'installed', path: '/home/u/.claude/skills/openhop' }, + { client: 'cursor', status: 'skipped', reason: 'not detected' }, + ] + expect(shouldPrintOpenSkillsFallback(results)).toBe(false) + }) + + it('does NOT fire on dry-run when at least one Tier-1 would install', () => { + const results: InstallResult[] = [ + { client: 'claude-code', status: 'would-install', path: '/home/u/.claude/skills/openhop' }, + ] + expect(shouldPrintOpenSkillsFallback(results)).toBe(false) + }) +}) + describe('ClientSpec type sanity', () => { it('exposes detect+skills directories as functions', () => { const c: ClientSpec = buildClients(HOME)[0] diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index 661403d..267afb1 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -259,6 +259,24 @@ export function runInit( return { results, sourceMissing: false } } +/** + * True iff `init` should print the OpenSkills fallback hint pointing the user + * at Tier-2 clients (Codex / Gemini / Junie / ...). The hint is only useful + * when no Tier-1 client has the skill — either none were detected or the only + * detection was advisory-only (Continue.dev). On a re-run where Tier-1 is + * already installed, the user already has the skill and the hint is misleading. + * + * "Tier-1 received the skill" covers: installed this run, would-install (dry + * run), and skipped-as-already-installed. Failed installs early-exit before + * the caller reaches this check, so they're intentionally not represented. + */ +export function shouldPrintOpenSkillsFallback(results: InstallResult[]): boolean { + const hasTier1ReceivedSkill = results.some( + (r) => r.status !== 'advisory' && !(r.status === 'skipped' && r.reason === 'not detected') + ) + return !hasTier1ReceivedSkill +} + /** Render results as a small fixed-width table. Pure function; testable. */ export function renderTable(results: InstallResult[]): string { const rows = [ @@ -338,7 +356,7 @@ export function registerInit(program: Command): void { (r) => !(r.status === 'skipped' && r.reason === 'not detected') ).length - if (!opts.json && installed.length === 0 && wouldInstall.length === 0) { + if (!opts.json && shouldPrintOpenSkillsFallback(results)) { process.stderr.write( 'No Tier-1 client received the skill. For Codex CLI / Gemini CLI / Junie / Copilot / OpenCode / Goose / Antigravity, run:\n' + ' npx openskills install naorsabag/openhop\n'