Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion packages/cli/__tests__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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]
Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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'
Expand Down
Loading