import fs from "node:fs/promises"; import path from "node:path"; /** * Homebrew Cellar paths (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node) * break when Homebrew upgrades Node and removes the old version directory. * Resolve these to a stable Homebrew-managed path that survives upgrades: * - Default formula "node": /opt/node/bin/node or /bin/node * - Versioned formula "node@22": /opt/node@22/bin/node (keg-only) */ export async function resolveStableNodePath(nodePath: string): Promise { const cellarMatch = nodePath.match( /^(.+?)[\\/]Cellar[\\/]([^\\/]+)[\\/][^\\/]+[\\/]bin[\\/]node$/, ); if (!cellarMatch) { return nodePath; } const prefix = cellarMatch[1]; // e.g. /opt/homebrew const formula = cellarMatch[2]; // e.g. "node" or "node@22" const pathModule = nodePath.includes("\\") ? path.win32 : path.posix; // Try the Homebrew opt symlink first — works for both default and versioned formulas. const optPath = pathModule.join(prefix, "opt", formula, "bin", "node"); try { await fs.access(optPath); return optPath; } catch { // fall through } // For the default "node" formula, also try the direct bin symlink. if (formula === "node") { const binPath = pathModule.join(prefix, "bin", "node"); try { await fs.access(binPath); return binPath; } catch { // fall through } } return nodePath; }