refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3] (#1811)
* refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> 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<Runtime> 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)
This commit is contained in:
7
.changeset/bold-orcas-wander.md
Normal file
7
.changeset/bold-orcas-wander.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1811
|
||||
---
|
||||
**Internal: the installer's per-function `is<Runtime>` 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.
|
||||
|
||||
<!-- docs-exempt: internal refactor; no user-facing doc surface -->
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Runtime>` 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<Runtime>` 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<Record<string, boolean>> {
|
||||
const flags: Record<string, boolean> = {};
|
||||
for (const id of RUNTIME_FLAG_IDS) {
|
||||
flags['is' + id.charAt(0).toUpperCase() + id.slice(1)] = runtime === id;
|
||||
}
|
||||
return Object.freeze(flags);
|
||||
}
|
||||
|
||||
54
tests/runtime-flags.test.cjs
Normal file
54
tests/runtime-flags.test.cjs
Normal file
@@ -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`);
|
||||
});
|
||||
Reference in New Issue
Block a user