refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel [AC2 slice 1/6] (#1800)
* refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel Collapses the two duplicated runtimeLabel assignment chains in bin/install.js (uninstall() and install()) into a single getRuntimeLabel(runtime) lookup in src/runtime-name-policy.cts — a curated short-form label table, sibling to the registry-derived getDirName precedent. This is slice 1 of AC2 (regional residue-collapse in install.js) under ADR-1239 Phase B / #1679. The install/uninstall console label was the add-a-host tax poster child: a new runtime meant adding a label line to BOTH chains, and they had drifted out of sync: - kimi: install 'Kimi' / uninstall 'Kimi CLI' -> canonical 'Kimi CLI' - cline: install 'Cline' / uninstall (omitted) -> canonical 'Cline' Each canonical value matches the majority chain AND the descriptor title. Behavior: - 14 of 16 runtime labels unchanged in both sites (zero observable change). - 2 unifications (kimi-install, cline-uninstall) move toward consistency. - Unknown/empty runtime id fails closed to 'Claude Code'. - Raw-id lookup only (no alias expansion); callers pass canonicalized ids. Voice: these SHORT UI labels are intentionally distinct from the descriptor title (the long product name) which serves docs/registry display, not the console. A future slice may relocate this to a runtime.label descriptor field. Verification: - TDD: tests/runtime-label-policy.test.cjs (golden map + drift guard + fallbacks) - 16-runtime golden install parity: byte-identical (labels are stdout-only) - 162-test neighbor cluster green; eslint + test-file-count + regression-names clean - runtime === count in install.js: 129 -> 115 (-14, the uninstalled label chain) * chore(changeset): add Changed fragment for runtimeLabel collapse (#1679) PR #1800 touches bin/ → changeset-required gate. Mirrors the sibling ADR-1239 Phase B slice (eager-elks-frolic): type Changed + docs-exempt marker (internal refactor, no user-facing doc surface).
This commit is contained in:
7
.changeset/sturdy-voles-dart.md
Normal file
7
.changeset/sturdy-voles-dart.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1800
|
||||
---
|
||||
**Internal: install/uninstall runtime labels are now sourced from a single `getRuntimeLabel` lookup** — the two duplicated `runtimeLabel` assignment chains in `bin/install.js` (uninstall + install) are collapsed into one curated label table in `runtime-name-policy.cts`, sibling to the registry-derived `getDirName` (ADR-1239 Phase B, #1679). Install output is byte-identical for all 16 runtimes (golden-parity asserted). Two console-label inconsistencies are normalized as a side effect: `kimi` shows 'Kimi CLI' in both sites, and `cline` uninstall no longer falls through to 'Claude Code'.
|
||||
|
||||
<!-- docs-exempt: internal refactor; only cosmetic console-label normalization with no doc surface to update -->
|
||||
@@ -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 } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
const { getDirName, getRuntimeLabel } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
const {
|
||||
applyWorktreeBaseRef,
|
||||
readBaseRefFromSettings,
|
||||
@@ -6960,21 +6960,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
? targetDir.replace(os.homedir(), '~')
|
||||
: targetDir.replace(process.cwd(), '.');
|
||||
|
||||
let runtimeLabel = 'Claude Code';
|
||||
if (runtime === 'opencode') runtimeLabel = 'OpenCode';
|
||||
if (runtime === 'gemini') runtimeLabel = 'Gemini';
|
||||
if (runtime === 'kilo') runtimeLabel = 'Kilo';
|
||||
if (runtime === 'codex') runtimeLabel = 'Codex';
|
||||
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
||||
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
||||
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
||||
if (runtime === 'windsurf') runtimeLabel = 'Windsurf';
|
||||
if (runtime === 'augment') runtimeLabel = 'Augment';
|
||||
if (runtime === 'trae') runtimeLabel = 'Trae';
|
||||
if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
|
||||
if (runtime === 'hermes') runtimeLabel = 'Hermes Agent';
|
||||
if (runtime === 'kimi') runtimeLabel = 'Kimi CLI';
|
||||
if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy';
|
||||
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
||||
// Phase B / #1679) — collapses the prior 15-line assignment chain.
|
||||
const runtimeLabel = getRuntimeLabel(runtime);
|
||||
|
||||
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -8599,22 +8587,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
homeDir,
|
||||
});
|
||||
|
||||
let runtimeLabel = 'Claude Code';
|
||||
if (isOpencode) runtimeLabel = 'OpenCode';
|
||||
if (isGemini) runtimeLabel = 'Gemini';
|
||||
if (isKilo) runtimeLabel = 'Kilo';
|
||||
if (isCodex) runtimeLabel = 'Codex';
|
||||
if (isCopilot) runtimeLabel = 'Copilot';
|
||||
if (isAntigravity) runtimeLabel = 'Antigravity';
|
||||
if (isCursor) runtimeLabel = 'Cursor';
|
||||
if (isWindsurf) runtimeLabel = 'Windsurf';
|
||||
if (isAugment) runtimeLabel = 'Augment';
|
||||
if (isTrae) runtimeLabel = 'Trae';
|
||||
if (isQwen) runtimeLabel = 'Qwen Code';
|
||||
if (isHermes) runtimeLabel = 'Hermes Agent';
|
||||
if (isKimi) runtimeLabel = 'Kimi';
|
||||
if (isCodebuddy) runtimeLabel = 'CodeBuddy';
|
||||
if (isCline) runtimeLabel = 'Cline';
|
||||
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
||||
// Phase B / #1679) — collapses the prior 16-line assignment chain.
|
||||
const runtimeLabel = getRuntimeLabel(runtime);
|
||||
|
||||
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
|
||||
@@ -151,3 +151,60 @@ export function getDirName(runtime: string): string {
|
||||
if (typeof dir === 'string' && dir.length > 0) return dir;
|
||||
return '.claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Curated short display labels for the install/uninstall console output, keyed
|
||||
* by canonical runtime id. The SINGLE source of truth consumed by both
|
||||
* `install()` and `uninstall()` in bin/install.js via `getRuntimeLabel`.
|
||||
*
|
||||
* Collapses the two duplicated `runtimeLabel` assignment chains that previously
|
||||
* lived inline in bin/install.js (ADR-1239 Phase B, #1679) — the add-a-host tax:
|
||||
* a new runtime meant remembering to add a label line in BOTH chains, and they
|
||||
* had drifted out of sync (uninstall omitted `cline` and used a different
|
||||
* `kimi` value than install). This table is the curated canonical resolution:
|
||||
* - kimi: install 'Kimi' / uninstall 'Kimi CLI' → 'Kimi CLI' (majority + descriptor title)
|
||||
* - cline: install 'Cline' / uninstall (omitted) → 'Cline' (majority + descriptor title)
|
||||
*
|
||||
* Voice: these are the SHORT UI labels, intentionally distinct from the
|
||||
* descriptor `title` (the long product name — e.g. "OpenAI Codex CLI",
|
||||
* "GitHub Copilot", "Gemini CLI") which serves documentation/registry display,
|
||||
* not the install console. A future slice may relocate this to a
|
||||
* `runtime.label` descriptor field; until then this table is the source.
|
||||
*
|
||||
* Lookup is RAW-ID only (no alias expansion) — callers pass an already-
|
||||
* canonicalized runtime id, keeping the label surface explicit. Unknown/empty
|
||||
* ids fall back to 'Claude Code' (the always-safe default, fail-closed).
|
||||
*
|
||||
* The drift-guard test (tests/runtime-label-policy.test.cjs) pins this table's
|
||||
* id set to the capability-registry runtime id set, so adding/removing a runtime
|
||||
* forces a deliberate update here.
|
||||
*/
|
||||
const RUNTIME_LABELS: Readonly<Record<string, string>> = {
|
||||
claude: 'Claude Code',
|
||||
opencode: 'OpenCode',
|
||||
gemini: 'Gemini',
|
||||
kilo: 'Kilo',
|
||||
codex: 'Codex',
|
||||
copilot: 'Copilot',
|
||||
antigravity: 'Antigravity',
|
||||
cursor: 'Cursor',
|
||||
windsurf: 'Windsurf',
|
||||
augment: 'Augment',
|
||||
trae: 'Trae',
|
||||
qwen: 'Qwen Code',
|
||||
hermes: 'Hermes Agent',
|
||||
kimi: 'Kimi CLI',
|
||||
codebuddy: 'CodeBuddy',
|
||||
cline: 'Cline',
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a canonical runtime id to its short display label for the
|
||||
* install/uninstall console output. Unknown/empty inputs fall back to
|
||||
* 'Claude Code'. Sibling to `getDirName`; pure (no I/O).
|
||||
*/
|
||||
export function getRuntimeLabel(runtime: string): string {
|
||||
if (!runtime) return 'Claude Code';
|
||||
const label = RUNTIME_LABELS[runtime];
|
||||
return typeof label === 'string' && label.length > 0 ? label : 'Claude Code';
|
||||
}
|
||||
|
||||
103
tests/runtime-label-policy.test.cjs
Normal file
103
tests/runtime-label-policy.test.cjs
Normal file
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Drift-guard + collapse: getRuntimeLabel must be the SINGLE source of truth for
|
||||
* the install/uninstall console display label, replacing the two duplicated
|
||||
* `runtimeLabel` assignment chains that previously lived in bin/install.js
|
||||
* (uninstall() and install()) — the add-a-host tax ADR-1239 Phase B (#1679)
|
||||
* eliminates.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. For every known runtime id, getRuntimeLabel(id) equals the hardcoded
|
||||
* golden expected map — a pinned oracle that catches BOTH table bugs AND
|
||||
* unintended drift (adding/removing a runtime forces a deliberate
|
||||
* golden-map update here).
|
||||
* 2. getRuntimeLabel('unknown') and getRuntimeLabel('') fall back to
|
||||
* 'Claude Code' (the always-safe default).
|
||||
* 3. The golden id set EXACTLY equals the capability-registry runtime id set
|
||||
* (adding/removing a runtime forces a golden update here).
|
||||
*
|
||||
* Voice: these are the SHORT UI labels used in the install/uninstall console
|
||||
* output, intentionally distinct from the descriptor `title` (the long product
|
||||
* name, e.g. "OpenAI Codex CLI", "GitHub Copilot") which serves
|
||||
* documentation/registry display. Two prior-chain inconsistencies are resolved
|
||||
* by this canonical map (both move toward the majority + descriptor value):
|
||||
* - kimi: install said 'Kimi', uninstall said 'Kimi CLI' → canonical 'Kimi CLI'
|
||||
* - cline: install said 'Cline', uninstall omitted it (→ 'Claude Code') → canonical 'Cline'
|
||||
*
|
||||
* ADR-1239 Phase B (#1679).
|
||||
* Behavioral tests only: assert on returned values, no source-grep.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
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;
|
||||
|
||||
// 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
|
||||
// — the test IS the oracle (non-circular). Only PRODUCTION code should derive
|
||||
// dynamically. If this map diverges from getRuntimeLabel output, either the
|
||||
// table is wrong OR the registry changed — both require a deliberate golden-map
|
||||
// update here.
|
||||
const GOLDEN_LABEL_MAP = {
|
||||
claude: 'Claude Code',
|
||||
opencode: 'OpenCode',
|
||||
gemini: 'Gemini',
|
||||
kilo: 'Kilo',
|
||||
codex: 'Codex',
|
||||
copilot: 'Copilot',
|
||||
antigravity: 'Antigravity',
|
||||
cursor: 'Cursor',
|
||||
windsurf: 'Windsurf',
|
||||
augment: 'Augment',
|
||||
trae: 'Trae',
|
||||
qwen: 'Qwen Code',
|
||||
hermes: 'Hermes Agent',
|
||||
kimi: 'Kimi CLI',
|
||||
codebuddy: 'CodeBuddy',
|
||||
cline: 'Cline',
|
||||
};
|
||||
|
||||
test('getRuntimeLabel: golden map matches for all 16 known runtime ids', () => {
|
||||
for (const [id, expected] of Object.entries(GOLDEN_LABEL_MAP)) {
|
||||
const actual = getRuntimeLabel(id);
|
||||
assert.strictEqual(
|
||||
actual,
|
||||
expected,
|
||||
`getRuntimeLabel('${id}') diverged from golden.\n` +
|
||||
` actual: ${JSON.stringify(actual)}\n` +
|
||||
` expected: ${JSON.stringify(expected)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('drift guard: registry runtime id set EXACTLY equals the golden map (adding/removing a runtime forces a golden update)', () => {
|
||||
// Without this, a newly-added runtime would pass (its label never checked) and
|
||||
// removing `claude` could pass via the 'Claude Code' fallback. Pin the set
|
||||
// both ways — mirroring the getDirName drift guard.
|
||||
const registryIds = Object.keys(registry.runtimes).sort();
|
||||
const goldenIds = Object.keys(GOLDEN_LABEL_MAP).sort();
|
||||
assert.deepEqual(registryIds, goldenIds,
|
||||
'registry.runtimes id set must exactly match GOLDEN_LABEL_MAP — update the golden map when adding/removing a runtime');
|
||||
});
|
||||
|
||||
test('getRuntimeLabel fallback: unknown runtime returns "Claude Code"', () => {
|
||||
assert.strictEqual(getRuntimeLabel('unknown'), 'Claude Code',
|
||||
'getRuntimeLabel("unknown") must return "Claude Code" (default fallback)');
|
||||
});
|
||||
|
||||
test('getRuntimeLabel fallback: empty string returns "Claude Code"', () => {
|
||||
assert.strictEqual(getRuntimeLabel(''), 'Claude Code',
|
||||
'getRuntimeLabel("") must return "Claude Code" (empty-input fallback)');
|
||||
});
|
||||
|
||||
test('getRuntimeLabel fallback: alias is NOT auto-expanded (raw id match only)', () => {
|
||||
// getRuntimeLabel is a raw-id lookup, not alias-aware (unlike canonicalizeRuntimeName).
|
||||
// Callers pass an already-canonicalized runtime id. An alias must fall back to
|
||||
// the default rather than silently matching — this keeps the label surface
|
||||
// explicit and prevents a future alias from changing console output by accident.
|
||||
assert.strictEqual(getRuntimeLabel('claude-code'), 'Claude Code',
|
||||
'getRuntimeLabel("claude-code") must return the default "Claude Code" (raw-id match only; aliases are not expanded)');
|
||||
});
|
||||
Reference in New Issue
Block a user