fix(install): prefer stable Homebrew symlinks over versioned Cellar paths in node runner
process.execPath on Homebrew resolves symlinks and returns the versioned Cellar path (e.g. /usr/local/Cellar/node/25.8.1/bin/node). After brew upgrade node, the old Cellar binary fails with dyld: Library not loaded because shared libraries have changed SOVERSION. - Add normalizeNodePath() helper that maps Cellar paths to stable Homebrew symlinks (/usr/local/bin/node or /opt/homebrew/bin/node) - resolveNodeRunner() now calls normalizeNodePath() before quoting - rewriteLegacyManagedNodeHookCommands() also normalizes baked Cellar runner paths in existing hook commands so reinstall doesn't re-bake them - Export normalizeNodePath for testability - Add 22 tests covering all cases (Cellar paths, stable symlinks, NVM, system node, Windows, null/empty, both function surfaces) Closes #3181 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/gallant-badgers-bark.md
Normal file
5
.changeset/gallant-badgers-bark.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Fixed
|
||||||
|
pr: 3181
|
||||||
|
---
|
||||||
|
resolveNodeRunner() and rewriteLegacyManagedNodeHookCommands() now prefer stable Homebrew symlinks (/usr/local/bin/node, /opt/homebrew/bin/node) over versioned Cellar paths when a Cellar path is detected, preventing dyld: Library not loaded errors after brew upgrade node
|
||||||
@@ -8,7 +8,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Workstream resolution in `init.milestone-op` and `roadmap.analyze`** — both handlers now respect `--ws`, `GSD_WORKSTREAM`, and the `.planning/active-workstream` file; workstream-scoped repos no longer exit "All phases complete — Nothing left to do" due to `phase_count: 0` from reading the wrong root `.planning/`. (#3196)
|
- **Stable node path on Homebrew** — `resolveNodeRunner()` now maps versioned Homebrew Cellar paths (e.g. `/usr/local/Cellar/node/25.8.1/bin/node`) to the stable Homebrew symlinks (`/usr/local/bin/node` on Intel, `/opt/homebrew/bin/node` on Apple Silicon). `rewriteLegacyManagedNodeHookCommands()` applies the same normalization to baked Cellar paths in existing hook commands. This prevents `dyld: Library not loaded` errors after `brew upgrade node`. (#3181)
|
||||||
- **Milestone-archive layout support** — `validate consistency`, `validate health`, and `find-phase` now scan `.planning/milestones/v*-phases/` directories in addition to the flat `.planning/phases/` layout. Projects that have graduated to milestone-archive layout no longer receive spurious W006 "Phase N in ROADMAP.md but no directory on disk" warnings for every active phase. (#3164)
|
- **Milestone-archive layout support** — `validate consistency`, `validate health`, and `find-phase` now scan `.planning/milestones/v*-phases/` directories in addition to the flat `.planning/phases/` layout. Projects that have graduated to milestone-archive layout no longer receive spurious W006 "Phase N in ROADMAP.md but no directory on disk" warnings for every active phase. (#3164)
|
||||||
|
|
||||||
### Feature
|
### Feature
|
||||||
|
|||||||
@@ -525,6 +525,34 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
|
|||||||
return `${resolvedTarget}/`;
|
return `${resolvedTarget}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a raw `process.execPath` to a stable, upgrade-safe node binary
|
||||||
|
* path. On Homebrew installs, `process.execPath` resolves symlinks and returns
|
||||||
|
* the versioned Cellar path (e.g.
|
||||||
|
* `/usr/local/Cellar/node/25.8.1/bin/node`). Baking that path into hook
|
||||||
|
* commands causes `dyld: Library not loaded` errors after `brew upgrade node`
|
||||||
|
* because the shared libraries referenced by the Cellar binary have changed
|
||||||
|
* SOVERSION. (#3181)
|
||||||
|
*
|
||||||
|
* The stable Homebrew symlinks (`/usr/local/bin/node` for Intel,
|
||||||
|
* `/opt/homebrew/bin/node` for Apple Silicon) survive upgrades — Homebrew
|
||||||
|
* re-points them atomically. We prefer those when a Cellar path is detected.
|
||||||
|
*
|
||||||
|
* Non-Homebrew installs (NVM, system node, Windows, etc.) are returned as-is.
|
||||||
|
*/
|
||||||
|
function normalizeNodePath(execPath) {
|
||||||
|
if (!execPath) return execPath;
|
||||||
|
// Intel Homebrew: /usr/local/Cellar/node/<version>/bin/node
|
||||||
|
if (/^\/usr\/local\/Cellar\/node\/[^/]+\/bin\/node(\.exe)?$/.test(execPath)) {
|
||||||
|
return '/usr/local/bin/node';
|
||||||
|
}
|
||||||
|
// Apple Silicon Homebrew: /opt/homebrew/Cellar/node/<version>/bin/node
|
||||||
|
if (/^\/opt\/homebrew\/Cellar\/node\/[^/]+\/bin\/node(\.exe)?$/.test(execPath)) {
|
||||||
|
return '/opt/homebrew/bin/node';
|
||||||
|
}
|
||||||
|
return execPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the absolute path to the node binary running the installer.
|
* Resolve the absolute path to the node binary running the installer.
|
||||||
* Used as the runner for .js hooks so they execute in GUI/minimal-PATH
|
* Used as the runner for .js hooks so they execute in GUI/minimal-PATH
|
||||||
@@ -537,13 +565,17 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
|
|||||||
* gives the absolute path of the node binary actively running the
|
* gives the absolute path of the node binary actively running the
|
||||||
* installer — that is the version the user just installed under, and
|
* installer — that is the version the user just installed under, and
|
||||||
* the right default runtime for hooks invoked under the same install.
|
* the right default runtime for hooks invoked under the same install.
|
||||||
|
*
|
||||||
|
* When `process.execPath` is a versioned Homebrew Cellar path, the stable
|
||||||
|
* Homebrew symlink is returned instead to survive `brew upgrade node` (#3181).
|
||||||
*/
|
*/
|
||||||
function resolveNodeRunner() {
|
function resolveNodeRunner() {
|
||||||
const execPath = typeof process.execPath === 'string' ? process.execPath : '';
|
const execPath = typeof process.execPath === 'string' ? process.execPath : '';
|
||||||
if (!execPath) return null;
|
if (!execPath) return null;
|
||||||
|
const stablePath = normalizeNodePath(execPath);
|
||||||
// JSON.stringify produces a properly escaped double-quoted shell token,
|
// JSON.stringify produces a properly escaped double-quoted shell token,
|
||||||
// safe for paths containing spaces or unusual characters.
|
// safe for paths containing spaces or unusual characters.
|
||||||
return JSON.stringify(execPath.replace(/\\/g, '/'));
|
return JSON.stringify(stablePath.replace(/\\/g, '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -580,20 +612,49 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner) {
|
|||||||
for (const h of entry.hooks) {
|
for (const h of entry.hooks) {
|
||||||
if (!h || typeof h.command !== 'string') continue;
|
if (!h || typeof h.command !== 'string') continue;
|
||||||
const trimmed = h.command.trim();
|
const trimmed = h.command.trim();
|
||||||
// Match the EXACT legacy form: `node <script>` with optional quoting.
|
// Match two runner forms:
|
||||||
|
// 1. Legacy bare-node form: `node <script>` (#2979/#3002)
|
||||||
|
// 2. Cellar-path form: `"/usr/local/Cellar/node/<v>/bin/node" <script>`
|
||||||
|
// or `"/opt/homebrew/Cellar/node/<v>/bin/node" <script>` (#3181)
|
||||||
|
//
|
||||||
|
// Both patterns use the same script-token capture group so the rewrite
|
||||||
|
// is uniform. We detect the Cellar form by extracting the runner token
|
||||||
|
// and running it through normalizeNodePath.
|
||||||
|
//
|
||||||
// The previous shape used `trimmed.includes(<filename>)` which would
|
// The previous shape used `trimmed.includes(<filename>)` which would
|
||||||
// false-positive on user-authored hooks whose path merely contained
|
// false-positive on user-authored hooks whose path merely contained
|
||||||
// a managed filename as a substring (e.g.
|
// a managed filename as a substring (e.g.
|
||||||
// /home/me/scripts/wraps-gsd-check-update.js-and-more.js). #3002 CR.
|
// /home/me/scripts/wraps-gsd-check-update.js-and-more.js). #3002 CR.
|
||||||
const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
|
const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
|
||||||
|
trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
|
||||||
if (!m) continue;
|
if (!m) continue;
|
||||||
const scriptToken = m[1];
|
|
||||||
const scriptPath = m[2] || m[3] || m[4] || '';
|
let runnerToken, scriptToken, scriptPath;
|
||||||
|
if (/^node\s+/.test(trimmed)) {
|
||||||
|
// bare-node form
|
||||||
|
runnerToken = 'node';
|
||||||
|
scriptToken = m[1];
|
||||||
|
scriptPath = m[2] || m[3] || m[4] || '';
|
||||||
|
} else {
|
||||||
|
// quoted/unquoted runner form — check whether runner is a Cellar path
|
||||||
|
runnerToken = m[1];
|
||||||
|
const runnerPath = (m[2] || m[3] || m[4] || '').replace(/\\/g, '/');
|
||||||
|
const stableRunner = normalizeNodePath(runnerPath);
|
||||||
|
// Only process if the runner IS a Cellar path that normalizes to something different
|
||||||
|
if (stableRunner === runnerPath) continue;
|
||||||
|
scriptToken = m[5];
|
||||||
|
scriptPath = m[6] || m[7] || m[8] || '';
|
||||||
|
}
|
||||||
|
|
||||||
// Take the basename — match against MANAGED_HOOK_FILES by exact
|
// Take the basename — match against MANAGED_HOOK_FILES by exact
|
||||||
// equality, not substring containment. Handles both forward and
|
// equality, not substring containment. Handles both forward and
|
||||||
// backslash separators (Windows).
|
// backslash separators (Windows).
|
||||||
const scriptBase = scriptPath.split(/[\\/]/).pop() || '';
|
const scriptBase = scriptPath.split(/[\\/]/).pop() || '';
|
||||||
if (!MANAGED_HOOK_FILES.has(scriptBase)) continue;
|
if (!MANAGED_HOOK_FILES.has(scriptBase)) continue;
|
||||||
|
|
||||||
|
// Skip if already using the desired stable runner
|
||||||
|
if (runnerToken !== 'node' && runnerToken === absoluteRunner) continue;
|
||||||
|
|
||||||
h.command = `${absoluteRunner} ${scriptToken}`;
|
h.command = `${absoluteRunner} ${scriptToken}`;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -9989,6 +10050,7 @@ if (process.env.GSD_TEST_MODE) {
|
|||||||
parseUpdateBannerInput,
|
parseUpdateBannerInput,
|
||||||
buildUpdateBannerHookEntry,
|
buildUpdateBannerHookEntry,
|
||||||
buildHookCommand,
|
buildHookCommand,
|
||||||
|
normalizeNodePath,
|
||||||
resolveNodeRunner,
|
resolveNodeRunner,
|
||||||
rewriteLegacyManagedNodeHookCommands,
|
rewriteLegacyManagedNodeHookCommands,
|
||||||
buildCodexHookBlock,
|
buildCodexHookBlock,
|
||||||
|
|||||||
254
tests/bug-3181-node-cellar-path.test.cjs
Normal file
254
tests/bug-3181-node-cellar-path.test.cjs
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
process.env.GSD_TEST_MODE = '1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bug #3181: `resolveNodeRunner()` bakes versioned Homebrew Cellar paths
|
||||||
|
* (e.g. `/usr/local/Cellar/node/25.8.1/bin/node`) into hook commands in
|
||||||
|
* `~/.claude/settings.json`. After `brew upgrade node` the Cellar binary
|
||||||
|
* fails with `dyld: Library not loaded` because shared libraries have
|
||||||
|
* changed SOVERSION.
|
||||||
|
*
|
||||||
|
* Fix: prefer the stable Homebrew symlinks (`/usr/local/bin/node` for Intel
|
||||||
|
* Macs, `/opt/homebrew/bin/node` for Apple Silicon) when a Cellar path is
|
||||||
|
* detected. Non-Homebrew paths (NVM, system node, Windows, etc.) are
|
||||||
|
* returned unchanged.
|
||||||
|
*
|
||||||
|
* Also: `rewriteLegacyManagedNodeHookCommands()` must normalize Cellar paths
|
||||||
|
* baked into existing hook commands so reinstall doesn't re-bake them.
|
||||||
|
*
|
||||||
|
* All assertions go against exported function return values — no source-grep.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, describe } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const INSTALL = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
||||||
|
const { normalizeNodePath, resolveNodeRunner, rewriteLegacyManagedNodeHookCommands } = INSTALL;
|
||||||
|
|
||||||
|
// ─── normalizeNodePath ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Bug #3181: normalizeNodePath — exported as a function', () => {
|
||||||
|
test('normalizeNodePath is exported', () => {
|
||||||
|
assert.equal(typeof normalizeNodePath, 'function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bug #3181: normalizeNodePath — Intel Homebrew Cellar paths → /usr/local/bin/node', () => {
|
||||||
|
test('simple versioned Intel Cellar path', () => {
|
||||||
|
const result = normalizeNodePath('/usr/local/Cellar/node/25.8.1/bin/node');
|
||||||
|
assert.equal(result, '/usr/local/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Intel Cellar path with long semver', () => {
|
||||||
|
const result = normalizeNodePath('/usr/local/Cellar/node/20.11.0/bin/node');
|
||||||
|
assert.equal(result, '/usr/local/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Intel Cellar path with prerelease version segment', () => {
|
||||||
|
const result = normalizeNodePath('/usr/local/Cellar/node/22.0.0-rc.1/bin/node');
|
||||||
|
assert.equal(result, '/usr/local/bin/node');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bug #3181: normalizeNodePath — Apple Silicon Homebrew Cellar paths → /opt/homebrew/bin/node', () => {
|
||||||
|
test('simple versioned Apple Silicon Cellar path', () => {
|
||||||
|
const result = normalizeNodePath('/opt/homebrew/Cellar/node/25.8.1/bin/node');
|
||||||
|
assert.equal(result, '/opt/homebrew/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Apple Silicon Cellar path with another version', () => {
|
||||||
|
const result = normalizeNodePath('/opt/homebrew/Cellar/node/18.20.4/bin/node');
|
||||||
|
assert.equal(result, '/opt/homebrew/bin/node');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bug #3181: normalizeNodePath — non-Homebrew paths are returned unchanged', () => {
|
||||||
|
test('NVM path is unchanged', () => {
|
||||||
|
const nvm = '/Users/dev/.nvm/versions/node/v20.11.0/bin/node';
|
||||||
|
assert.equal(normalizeNodePath(nvm), nvm);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('already-stable Intel Homebrew symlink is unchanged', () => {
|
||||||
|
assert.equal(normalizeNodePath('/usr/local/bin/node'), '/usr/local/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('already-stable Apple Silicon Homebrew symlink is unchanged', () => {
|
||||||
|
assert.equal(normalizeNodePath('/opt/homebrew/bin/node'), '/opt/homebrew/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('system node (/usr/bin/node) is unchanged', () => {
|
||||||
|
assert.equal(normalizeNodePath('/usr/bin/node'), '/usr/bin/node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Windows path is unchanged', () => {
|
||||||
|
const win = 'C:\\Program Files\\nodejs\\node.exe';
|
||||||
|
assert.equal(normalizeNodePath(win), win);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty string is returned as-is', () => {
|
||||||
|
assert.equal(normalizeNodePath(''), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null is returned as-is', () => {
|
||||||
|
assert.equal(normalizeNodePath(null), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── resolveNodeRunner ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Bug #3181: resolveNodeRunner — maps Cellar execPath to stable symlink', () => {
|
||||||
|
test('Intel Cellar execPath → stable symlink quoted token', () => {
|
||||||
|
const orig = process.execPath;
|
||||||
|
try {
|
||||||
|
Object.defineProperty(process, 'execPath', {
|
||||||
|
value: '/usr/local/Cellar/node/25.8.1/bin/node',
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
const runner = resolveNodeRunner();
|
||||||
|
assert.equal(runner, '"/usr/local/bin/node"',
|
||||||
|
`expected stable Intel symlink, got: ${runner}`);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: orig, configurable: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Apple Silicon Cellar execPath → stable symlink quoted token', () => {
|
||||||
|
const orig = process.execPath;
|
||||||
|
try {
|
||||||
|
Object.defineProperty(process, 'execPath', {
|
||||||
|
value: '/opt/homebrew/Cellar/node/25.8.1/bin/node',
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
const runner = resolveNodeRunner();
|
||||||
|
assert.equal(runner, '"/opt/homebrew/bin/node"',
|
||||||
|
`expected stable Apple Silicon symlink, got: ${runner}`);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: orig, configurable: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-Homebrew execPath is returned as a quoted absolute path unchanged', () => {
|
||||||
|
const orig = process.execPath;
|
||||||
|
const nvmPath = '/Users/dev/.nvm/versions/node/v20.11.0/bin/node';
|
||||||
|
try {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: nvmPath, configurable: true });
|
||||||
|
const runner = resolveNodeRunner();
|
||||||
|
assert.equal(runner, JSON.stringify(nvmPath));
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: orig, configurable: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when execPath is empty (existing null-guard is preserved)', () => {
|
||||||
|
const orig = process.execPath;
|
||||||
|
try {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: '', configurable: true });
|
||||||
|
assert.equal(resolveNodeRunner(), null);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'execPath', { value: orig, configurable: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── rewriteLegacyManagedNodeHookCommands — Cellar runner rewrite ─────────────
|
||||||
|
|
||||||
|
describe('Bug #3181: rewriteLegacyManagedNodeHookCommands — rewrites baked Cellar runner to stable symlink', () => {
|
||||||
|
test('Intel Cellar runner in a managed hook is rewritten to the stable symlink', () => {
|
||||||
|
const settings = {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [{
|
||||||
|
hooks: [{
|
||||||
|
type: 'command',
|
||||||
|
command: '"/usr/local/Cellar/node/25.8.1/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const runner = '"/usr/local/bin/node"';
|
||||||
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner);
|
||||||
|
assert.equal(changed, true, 'expected rewrite to occur');
|
||||||
|
assert.equal(
|
||||||
|
settings.hooks.SessionStart[0].hooks[0].command,
|
||||||
|
'"/usr/local/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Apple Silicon Cellar runner in a managed hook is rewritten to the stable symlink', () => {
|
||||||
|
const settings = {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [{
|
||||||
|
hooks: [{
|
||||||
|
type: 'command',
|
||||||
|
command: '"/opt/homebrew/Cellar/node/25.8.1/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const runner = '"/opt/homebrew/bin/node"';
|
||||||
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner);
|
||||||
|
assert.equal(changed, true, 'expected rewrite to occur');
|
||||||
|
assert.equal(
|
||||||
|
settings.hooks.SessionStart[0].hooks[0].command,
|
||||||
|
'"/opt/homebrew/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a hook already using the stable runner is NOT rewritten (no churn)', () => {
|
||||||
|
const settings = {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [{
|
||||||
|
hooks: [{
|
||||||
|
type: 'command',
|
||||||
|
command: '"/usr/local/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const runner = '"/usr/local/bin/node"';
|
||||||
|
const before = settings.hooks.SessionStart[0].hooks[0].command;
|
||||||
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner);
|
||||||
|
assert.equal(changed, false, 'already-stable entry must not be touched');
|
||||||
|
assert.equal(settings.hooks.SessionStart[0].hooks[0].command, before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a user hook using a Cellar runner but an unmanaged filename is NOT rewritten', () => {
|
||||||
|
const settings = {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [{
|
||||||
|
hooks: [{
|
||||||
|
type: 'command',
|
||||||
|
command: '"/usr/local/Cellar/node/25.8.1/bin/node" "/Users/x/my-custom-hook.js"',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const runner = '"/usr/local/bin/node"';
|
||||||
|
const before = settings.hooks.SessionStart[0].hooks[0].command;
|
||||||
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner);
|
||||||
|
assert.equal(changed, false, 'unmanaged hooks with Cellar runner must not be touched');
|
||||||
|
assert.equal(settings.hooks.SessionStart[0].hooks[0].command, before);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Existing bare-node rewrite still works alongside the new Cellar rewrite
|
||||||
|
test('bare `node` managed hook is still rewritten (existing #2979 behaviour preserved)', () => {
|
||||||
|
const settings = {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [{
|
||||||
|
hooks: [{
|
||||||
|
type: 'command',
|
||||||
|
command: 'node "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const runner = '"/usr/local/bin/node"';
|
||||||
|
const changed = rewriteLegacyManagedNodeHookCommands(settings, runner);
|
||||||
|
assert.equal(changed, true);
|
||||||
|
assert.equal(
|
||||||
|
settings.hooks.SessionStart[0].hooks[0].command,
|
||||||
|
'"/usr/local/bin/node" "/Users/x/.gemini/hooks/gsd-check-update.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user