diff --git a/.changeset/agile-newts-roar.md b/.changeset/agile-newts-roar.md new file mode 100644 index 000000000..a767ec9c2 --- /dev/null +++ b/.changeset/agile-newts-roar.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1813 +--- +**Internal: the installer's `program` (display-name) + `command` (slash-invocation) chains are now single-source lookups** — the 14-line `program` chain (an exact duplicate of `runtimeLabel`) → `getRuntimeLabel`, and the 14-line `command` chain (the per-runtime `/gsd-new-project` syntax: gemini `/gsd:`, codex `$`, cursor skill-mention, kimi `/skill:`, default `/gsd-new-project`) → new `getRuntimeNewProjectCommand(runtime)` helper (ADR-1239 Phase B / #1679 AC2 slice 4). `runtime ===` count in `bin/install.js`: 53 → 25 (cumulative this session: 129 → 25). Stdout strings preserved byte-for-byte; no install-output change (golden-parity 16/16). No user-facing change. + + diff --git a/bin/install.js b/bin/install.js index 0e361f834..6ab82cf7b 100755 --- a/bin/install.js +++ b/bin/install.js @@ -37,7 +37,7 @@ const { // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the // conversion module's rewrite engine can consume it without importing // bin/install.js. Re-exported below for back-compat consumers/tests. -const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); +const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); const { applyWorktreeBaseRef, readBaseRefFromSettings, @@ -10519,37 +10519,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS } } - let program = 'Claude Code'; - if (runtime === 'opencode') program = 'OpenCode'; - if (runtime === 'gemini') program = 'Gemini'; - if (runtime === 'kilo') program = 'Kilo'; - if (runtime === 'codex') program = 'Codex'; - if (runtime === 'copilot') program = 'Copilot'; - if (runtime === 'antigravity') program = 'Antigravity'; - if (runtime === 'cursor') program = 'Cursor'; - if (runtime === 'windsurf') program = 'Windsurf'; - if (runtime === 'augment') program = 'Augment'; - if (runtime === 'trae') program = 'Trae'; - if (runtime === 'cline') program = 'Cline'; - if (runtime === 'qwen') program = 'Qwen Code'; - if (runtime === 'hermes') program = 'Hermes Agent'; - if (runtime === 'kimi') program = 'Kimi CLI'; - - let command = '/gsd-new-project'; - if (runtime === 'opencode') command = '/gsd-new-project'; - if (runtime === 'kilo') command = '/gsd-new-project'; - if (runtime === 'gemini') command = '/gsd:new-project'; - if (runtime === 'codex') command = '$gsd-new-project'; - if (runtime === 'copilot') command = '/gsd-new-project'; - if (runtime === 'antigravity') command = '/gsd-new-project'; - if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)'; - if (runtime === 'windsurf') command = '/gsd-new-project'; - if (runtime === 'augment') command = '/gsd-new-project'; - if (runtime === 'trae') command = '/gsd-new-project'; - if (runtime === 'cline') command = '/gsd-new-project'; - if (runtime === 'qwen') command = '/gsd-new-project'; - if (runtime === 'hermes') command = '/gsd-new-project'; - if (runtime === 'kimi') command = '/skill:gsd-new-project'; + // program + command are now single-source lookups (ADR-1239 Phase B / #1679): + // program is the runtime display label; command is the per-host /gsd-new-project + // invocation syntax. + const program = getRuntimeLabel(runtime); + const command = getRuntimeNewProjectCommand(runtime); // Claude Code global installs use the skills/ format (CC 2.1.88+). // Restart is required for CC to pick up newly-installed skills, and the diff --git a/src/runtime-name-policy.cts b/src/runtime-name-policy.cts index 94ddab70d..6df48adba 100644 --- a/src/runtime-name-policy.cts +++ b/src/runtime-name-policy.cts @@ -284,3 +284,24 @@ export function runtimeFlags(runtime: string): Readonly> } return Object.freeze(flags); } + +/** + * The `/gsd-new-project` invocation syntax per runtime — the post-install + * "next step" command string. Most runtimes use the default `/gsd-new-project`; + * a few hosts need a different surface syntax. Collapses the 14-line + * `if (runtime === 'x') command = ...` chain in bin/install.js's next-step + * message (ADR-1239 Phase B / #1679 AC2). Pure: no I/O. + */ +const DEFAULT_NEW_PROJECT_COMMAND = '/gsd-new-project'; +const RUNTIME_NEW_PROJECT_COMMANDS: Readonly> = { + gemini: '/gsd:new-project', + codex: '$gsd-new-project', + cursor: 'gsd-new-project (mention the skill name)', + kimi: '/skill:gsd-new-project', +}; + +export function getRuntimeNewProjectCommand(runtime: string): string { + if (!runtime) return DEFAULT_NEW_PROJECT_COMMAND; + const c = RUNTIME_NEW_PROJECT_COMMANDS[runtime]; + return typeof c === 'string' && c.length > 0 ? c : DEFAULT_NEW_PROJECT_COMMAND; +} diff --git a/tests/runtime-label-policy.test.cjs b/tests/runtime-label-policy.test.cjs index 119e02e45..953885eb3 100644 --- a/tests/runtime-label-policy.test.cjs +++ b/tests/runtime-label-policy.test.cjs @@ -33,7 +33,7 @@ const assert = require('node:assert/strict'); const runtimeNamePolicy = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); -const { getRuntimeLabel } = runtimeNamePolicy; +const { getRuntimeLabel, getRuntimeNewProjectCommand } = runtimeNamePolicy; // Golden oracle: hardcoded expected map of all 16 runtime ids to their short // install/uninstall display label. A pinned expected value in a TEST is correct @@ -101,3 +101,30 @@ test('getRuntimeLabel fallback: alias is NOT auto-expanded (raw id match only)', assert.strictEqual(getRuntimeLabel('claude-code'), 'Claude Code', 'getRuntimeLabel("claude-code") must return the default "Claude Code" (raw-id match only; aliases are not expanded)'); }); + +// --------------------------------------------------------------------------- +// getRuntimeNewProjectCommand (ADR-1239 Phase B / #1679 AC2) — the per-runtime +// /gsd-new-project invocation syntax for the post-install next-step message. +// --------------------------------------------------------------------------- + +const GOLDEN_COMMAND_MAP = { + // 4 real overrides (the rest use the default): + gemini: '/gsd:new-project', + codex: '$gsd-new-project', + cursor: 'gsd-new-project (mention the skill name)', + kimi: '/skill:gsd-new-project', +}; +const DEFAULT_CMD = '/gsd-new-project'; + +test('getRuntimeNewProjectCommand: the 4 overrides + the default for the other 12 runtimes', () => { + for (const [id, expected] of Object.entries(GOLDEN_COMMAND_MAP)) { + assert.strictEqual(getRuntimeLabel ? getRuntimeNewProjectCommand(id) : null, expected, `override ${id}`); + } + // sanity: getRuntimeNewProjectCommand is imported alongside getRuntimeLabel above +}); + +test('getRuntimeNewProjectCommand: claude/unknown/empty + the 12 non-override runtimes → default', () => { + for (const id of ['claude', 'opencode', 'kilo', 'copilot', 'antigravity', 'windsurf', 'augment', 'trae', 'cline', 'qwen', 'hermes', 'codebuddy', 'unknown', '']) { + assert.strictEqual(getRuntimeNewProjectCommand(id), DEFAULT_CMD, `runtime '${id}' must return the default command`); + } +});