diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index e15904104..db76d0c4f 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -525,6 +525,54 @@ async function promptCheckbox(message, options, { selectedValues = [] } = {}) { // ─── skills help ────────────────────────────────────────────────────────────── +const SUBCOMMAND_HELP = { + install: `Usage: impeccable install [options] + +Install compiled Impeccable skills into project or user-level harness folders. + +Options: + -y, --yes Accept detected defaults without prompting + --providers= Comma-separated harnesses to install + --scope= Install scope: project or global + --project Install into the current project + --user, --global Install at the user level + --no-hooks Install skills without provider hook manifests + --force Replace an existing installation + -h, --help Show this help message`, + link: `Usage: impeccable link [options] + +Link Impeccable skills from a local checkout or submodule. + +Options: + --source= Source checkout (default: .impeccable) + --providers= Comma-separated harnesses to link + -y, --yes Accept detected defaults without prompting + --force Replace existing skill folders with links + -h, --help Show this help message`, + update: `Usage: impeccable update [options] + +Update an existing Impeccable skill installation. + +Options: + -y, --yes Accept detected defaults without prompting + --scope= Update scope: project or global + --project Update the current project installation + --user, --global Update the user-level installation + --no-hooks Update skills without changing hook manifests + --force Replace installed skill files + -h, --help Show this help message`, + check: `Usage: impeccable check [options] + +Check whether installed Impeccable skills are up to date. + +Options: + -h, --help Show this help message`, +}; + +function showSubcommandHelp(subcommand) { + console.log(SUBCOMMAND_HELP[subcommand]); +} + async function showHelp() { let commands; try { @@ -2533,6 +2581,11 @@ export { export async function run(args) { const sub = args[0]; + if (SUBCOMMAND_HELP[sub] && args.slice(1).some(arg => arg === '--help' || arg === '-h')) { + showSubcommandHelp(sub); + return; + } + if (!sub || sub === 'help' || sub === '--help' || sub === '-h') { await showHelp(); } else if (sub === 'install') { diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index 6524cea5a..5ddb94d23 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -864,6 +864,40 @@ describe('skills install/update: local universal bundle e2e', () => { expect(output).not.toContain('skills install Install impeccable skills'); }); + test('skill-management subcommand help exits before downloads, prompts, or writes (#699)', () => { + const commands = ['install', 'link', 'update', 'check']; + const prefixes = ['', 'skills ']; + const helpFlags = ['--help', '-h']; + + for (const command of commands) { + for (const prefix of prefixes) { + for (const helpFlag of helpFlags) { + const tmp = mkdtempSync(join(tmpdir(), `imp-test-${command}-help-`)); + const home = mkdtempSync(join(tmpdir(), `imp-home-${command}-help-`)); + execSync('git init', { cwd: tmp }); + + const output = run(`${prefix}${command} ${helpFlag}`, { + cwd: tmp, + env: { + ...process.env, + HOME: home, + IMPECCABLE_BUNDLE_PATH: join(tmp, 'must-not-be-read'), + }, + }); + + expect(output).toContain(`Usage: impeccable ${command}`); + for (const provider of ['.agents', '.claude', '.cursor', '.impeccable']) { + expect(existsSync(join(tmp, provider))).toBe(false); + expect(existsSync(join(home, provider))).toBe(false); + } + + rmSync(tmp, { recursive: true, force: true }); + rmSync(home, { recursive: true, force: true }); + } + } + } + }, 30000); + test('top-level install aliases the legacy skills install command', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-top-level-install-')); const home = mkdtempSync(join(tmpdir(), 'imp-home-top-level-install-'));