From f954bb4cacdacdba757e97b8ab2cecc31bd165f4 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 28 Jun 2026 14:59:29 -0400 Subject: [PATCH] =?UTF-8?q?refactor(#1679):=20ADR-1239=20Phase=20B=20?= =?UTF-8?q?=E2=80=94=20collapse=20is=20flag=20blocks=20into=20run?= =?UTF-8?q?timeFlags=20[AC2=20slice=203]=20(#1811)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(#1679): ADR-1239 Phase B — collapse is flag blocks into runtimeFlags [AC2 slice 3] Phase 2 AC2 slice 3. Collapses the four duplicated 'const isX = runtime === x' declaration blocks in bin/install.js (uninstall / writeManifest / install / a fourth helper — 48 of the 101 remaining runtime=== branches) into a single runtimeFlags(runtime) helper in src/runtime-name-policy.cts, sibling to getDirName / getRuntimeLabel / getGlobalConfigHomeFragment. The purest add-a-host tax: a new runtime meant remembering to add ~12 flag lines to each of four functions. Now it is one entry in RUNTIME_FLAG_IDS. - src/runtime-name-policy.cts: RUNTIME_FLAG_IDS + runtimeFlags(runtime) -> frozen map of is booleans (single runtime=== source, via loop). - bin/install.js: import runtimeFlags; replace the 4 declaration blocks with one destructure each. ZERO usage-site churn (flag names preserved; install.js's eslint block has no no-unused-vars rule so destructure-all is clean). - tests/runtime-flags.test.cjs: 4 tests (each runtime sets exactly its flag, claude/unknown/empty -> all false, all 15 flags present + frozen, drift guard). runtime === count: 101 -> 53 (-48). golden-install-parity 16/16 byte-identical (behavior-identical collapse). AC2 data-collapse now substantially complete; ADR-1235 agent-loop tail + per-runtime semantic residue remain (separate). * chore(changeset): add Changed fragment for runtimeFlags collapse (#1679) --- .changeset/bold-orcas-wander.md | 7 +++++ bin/install.js | 54 +++------------------------------ src/runtime-name-policy.cts | 27 +++++++++++++++++ tests/runtime-flags.test.cjs | 54 +++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 49 deletions(-) create mode 100644 .changeset/bold-orcas-wander.md create mode 100644 tests/runtime-flags.test.cjs diff --git a/.changeset/bold-orcas-wander.md b/.changeset/bold-orcas-wander.md new file mode 100644 index 000000000..dbdce1386 --- /dev/null +++ b/.changeset/bold-orcas-wander.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1811 +--- +**Internal: the installer's per-function `is` flag-declaration blocks are now a single `runtimeFlags` lookup** — the four duplicated `const isX = runtime === 'x'` blocks in `bin/install.js` (uninstall / writeManager / install / a fourth helper — 48 branches) are collapsed into one `runtimeFlags(runtime)` helper in `runtime-name-policy.cts` (ADR-1239 Phase B / #1679 AC2 slice 3). The add-a-host tax for flags is removed (one `RUNTIME_FLAG_IDS` entry, not four declaration blocks). Install output is byte-identical for all 16 runtimes (golden-parity asserted); `runtime ===` count in `bin/install.js`: 101 → 53. No user-facing change. + + diff --git a/bin/install.js b/bin/install.js index 7d62d5709..0e361f834 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 } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); +const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); const { applyWorktreeBaseRef, readBaseRefFromSettings, @@ -6918,19 +6918,7 @@ const GSD_UNINSTALL_HOOKS = [ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot') */ function uninstall(isGlobal, runtime = 'claude') { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const dirName = getDirName(runtime); // Get the target directory based on runtime and install type. Cline local @@ -7915,18 +7903,7 @@ function resolveInstallRelativePath(baseDir, relPath) { * Write file manifest after installation for future modification detection */ function writeManifest(configDir, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isTrae = runtime === 'trae'; - const isCline = runtime === 'cline'; - const isKimi = runtime === 'kimi'; - const isHermes = runtime === 'hermes'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const gsdDir = path.join(configDir, 'gsd-core'); // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/). // commandsDir points to the old location for Gemini (which still uses commands/gsd/). @@ -8413,21 +8390,7 @@ function reportInstallerMigrationResult(result) { } function install(isGlobal, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isGemini = runtime === 'gemini'; - const isKilo = runtime === 'kilo'; - const isKimi = runtime === 'kimi'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; - const isCline = runtime === 'cline'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -10433,14 +10396,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { * Apply statusline config, then print completion message */ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null, bannerOpts = {}) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isTrae = runtime === 'trae'; - const isCline = runtime === 'cline'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); if (shouldInstallStatusline && plan.writesSharedSettings && !isOpencode) { diff --git a/src/runtime-name-policy.cts b/src/runtime-name-policy.cts index 0ca9ef667..94ddab70d 100644 --- a/src/runtime-name-policy.cts +++ b/src/runtime-name-policy.cts @@ -257,3 +257,30 @@ export function getGlobalConfigHomeFragment(runtime: string): string { const frag = GLOBAL_CONFIG_HOME_FRAGMENTS[runtime]; return typeof frag === 'string' && frag.length > 0 ? frag : DEFAULT_CONFIG_HOME_FRAGMENT; } + +/** + * The runtime ids for which `bin/install.js` needs an `is` boolean + * predicate (every installed host that takes a non-claude install branch). + * Single source of truth — adding a runtime is one entry here, not a per- + * function declaration block (the add-a-host tax ADR-1239 Phase B / #1679 AC2 + * removes). + */ +const RUNTIME_FLAG_IDS = Object.freeze([ + 'opencode', 'kilo', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', + 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', +] as const); + +/** + * Return a frozen map of `is` boolean predicates for the given runtime + * id (e.g. `flags.isOpencode`). Collapses the four duplicated `const isX = + * runtime === 'x'` declaration blocks that lived in `bin/install.js`'s + * `uninstall`/`writeManifest`/`install`/etc. into one helper (sibling to + * `getDirName`/`getRuntimeLabel`). Pure: no I/O. + */ +export function runtimeFlags(runtime: string): Readonly> { + const flags: Record = {}; + for (const id of RUNTIME_FLAG_IDS) { + flags['is' + id.charAt(0).toUpperCase() + id.slice(1)] = runtime === id; + } + return Object.freeze(flags); +} diff --git a/tests/runtime-flags.test.cjs b/tests/runtime-flags.test.cjs new file mode 100644 index 000000000..a03a6e6f5 --- /dev/null +++ b/tests/runtime-flags.test.cjs @@ -0,0 +1,54 @@ +'use strict'; +/** + * Tests for runtimeFlags (ADR-1239 Phase B / #1679 AC2). Collapses the four + * duplicated `const isX = runtime === 'x'` declaration blocks in bin/install.js + * into one helper. Pins: all flags present, exactly one true per known runtime, + * claude/unknown/empty → all false, frozen. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { runtimeFlags } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); +const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); + +const EXPECTED_FLAGS = [ + 'isOpencode', 'isKilo', 'isGemini', 'isCodex', 'isCopilot', 'isAntigravity', + 'isCursor', 'isWindsurf', 'isAugment', 'isTrae', 'isQwen', 'isHermes', + 'isCodebuddy', 'isCline', 'isKimi', +]; + +test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => { + const ids = EXPECTED_FLAGS.map((f) => f.slice(2).toLowerCase()); + for (const id of ids) { + const flags = runtimeFlags(id); + const trues = EXPECTED_FLAGS.filter((f) => flags[f] === true); + assert.deepStrictEqual(trues, ['is' + id.charAt(0).toUpperCase() + id.slice(1)], `runtime '${id}' must set exactly its own flag`); + } +}); + +test('runtimeFlags: claude / unknown / empty → all flags false (fail-closed)', () => { + for (const id of ['claude', 'unknown', '', 'claude-code']) { + const flags = runtimeFlags(id); + for (const f of EXPECTED_FLAGS) { + assert.strictEqual(flags[f], false, `runtime '${id}': ${f} must be false`); + } + } +}); + +test('runtimeFlags: all 15 flags present + boolean + the object is frozen', () => { + const flags = runtimeFlags('opencode'); + for (const f of EXPECTED_FLAGS) { + assert.strictEqual(typeof flags[f], 'boolean', `${f} must be boolean`); + } + assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 15 flags'); + assert.ok(Object.isFrozen(flags), 'flags object must be frozen'); +}); + +test('runtimeFlags drift guard: covers every registry runtime except claude', () => { + // Adding a registry runtime that is not claude must get a flag or be added to + // RUNTIME_FLAG_IDS — pin the set so a new runtime forces a deliberate update. + const registryNonClaude = Object.keys(registry.runtimes).filter((r) => r !== 'claude').sort(); + const flagIds = EXPECTED_FLAGS.map((f) => f.slice(2).toLowerCase()).sort(); + const missing = registryNonClaude.filter((r) => !flagIds.includes(r)); + assert.deepEqual(missing, [], `registry runtimes missing a runtimeFlags entry: ${missing.join(', ')} — add to RUNTIME_FLAG_IDS`); +});