From a47979bb92c000f2b1f7557a0ee76a0fde1bc53c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 28 Jun 2026 15:22:13 -0400 Subject: [PATCH] =?UTF-8?q?refactor(#1679):=20ADR-1239=20Phase=20B=20?= =?UTF-8?q?=E2=80=94=20collapse=20program=20+=20command=20chains=20[AC2=20?= =?UTF-8?q?slice=204]=20(#1813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4] Phase 2 AC2 slice 4. Collapses two more duplicated runtime->string chains in bin/install.js's post-install next-step message: - program (14 branches): an EXACT duplicate of runtimeLabel -> getRuntimeLabel. - command (14 branches): the per-runtime /gsd-new-project invocation syntax (gemini '/gsd:', codex '$', cursor skill-mention, kimi '/skill:', default '/gsd-new-project') -> new getRuntimeNewProjectCommand(runtime) helper. - src/runtime-name-policy.cts: RUNTIME_NEW_PROJECT_COMMANDS table + getRuntimeNewProjectCommand(runtime) (sibling to runtimeFlags/getRuntimeLabel). - bin/install.js: import getRuntimeNewProjectCommand; replace the program + command chains with single lookups. - tests/runtime-label-policy.test.cjs: 2 new tests for getRuntimeNewProjectCommand (4 overrides + default for the other 12). runtime === count: 53 -> 25 (-28). Cumulative Phase 2 this session: 129 -> 25 (-104). golden-install-parity 16/16 (program/command are stdout-only so not parity-covered, but program matches RUNTIME_LABELS exactly and command values are preserved verbatim in the table). AC2 data-collapse now essentially exhausted; remaining 25 branches are the ADR-1235 agent-loop tail + per-runtime semantic behavior. * chore(changeset): add Changed fragment for program+command collapse (#1679) --- .changeset/agile-newts-roar.md | 7 ++++++ bin/install.js | 38 +++++------------------------ src/runtime-name-policy.cts | 21 ++++++++++++++++ tests/runtime-label-policy.test.cjs | 29 +++++++++++++++++++++- 4 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 .changeset/agile-newts-roar.md 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`); + } +});