From 7baabad146b143d1ad381aff6637ae56f9bef8d6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 12 Jul 2026 16:23:54 -0400 Subject: [PATCH] fix(#2185): normalize Linuxbrew + any Homebrew Cellar path to the stable symlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeNodePath had hardcoded branches for macOS Intel (/usr/local/Cellar) and Apple Silicon (/opt/homebrew/Cellar) but none for Linuxbrew (/home/linuxbrew/.linuxbrew/Cellar). After `brew upgrade node` on Linux, the version-pinned Cellar path baked into managed hook commands 404'd, and re-running the installer did not repair it — normalizeNodePath returned the pinned path unchanged, so the 'already normalized' skip-rewrite check no-op'd. Generalize to one branch: match /Cellar/node(<@ver>)?//bin/node and rewrite to /bin/node, deriving from the path itself. This covers macOS Intel, Apple Silicon, Linuxbrew, and any custom HOMEBREW_PREFIX — the same failure class as #977 (fnm) and #1619 (mise), now closed for Homebrew on every platform. Closes #2185 --- src/runtime-hooks-surface.cts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/runtime-hooks-surface.cts b/src/runtime-hooks-surface.cts index c9dddc57f..a6955df00 100644 --- a/src/runtime-hooks-surface.cts +++ b/src/runtime-hooks-surface.cts @@ -326,11 +326,18 @@ function normalizeNodePath(execPath: string, opts?: NodeNormOpts): string { return execPath; } - if (/^\/usr\/local\/Cellar\/node(@\d+)?\/[^/]+\/bin\/node(\.exe)?$/.test(execPath)) { - return '/usr/local/bin/node'; - } - if (/^\/opt\/homebrew\/Cellar\/node(@\d+)?\/[^/]+\/bin\/node(\.exe)?$/.test(execPath)) { - return '/opt/homebrew/bin/node'; + // Homebrew (macOS Intel /usr/local, Apple Silicon /opt/homebrew, Linuxbrew + // /home/linuxbrew/.linuxbrew, and any custom HOMEBREW_PREFIX) pins node at + // /Cellar/node(<@ver>)?//bin/node, then deletes prior versions on + // `brew upgrade node`. Rewrite to the stable /bin/node symlink, which + // survives the upgrade. Derive from the path itself (more reliable + // than HOMEBREW_PREFIX env — the path IS the install location) so every layout + // is covered by one branch instead of one per known prefix (#2185). + const homebrewMatch = normalizedForMatch.match( + /^(.+)\/Cellar\/node(@\d+)?\/[^/]+\/bin\/node(\.exe)?$/i, + ); + if (homebrewMatch) { + return `${homebrewMatch[1]}/bin/node${homebrewMatch[3] || ''}`; } // mise pins a concrete node version at /installs/node//bin/node