mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 15:04:33 +00:00
fix(daemon): resolve Homebrew Cellar path to stable symlink for gateway install
When `openclaw gateway install` runs under Homebrew Node, `process.execPath` resolves to the versioned Cellar path (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node). This path breaks when Homebrew upgrades Node, silently killing the gateway daemon. Resolve Cellar paths to the stable Homebrew symlink (/opt/homebrew/bin/node) which Homebrew updates automatically during upgrades. Closes #32182 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
29dde80c3e
commit
8950c59581
@@ -12,6 +12,7 @@ vi.mock("node:fs/promises", () => ({
|
|||||||
import {
|
import {
|
||||||
renderSystemNodeWarning,
|
renderSystemNodeWarning,
|
||||||
resolvePreferredNodePath,
|
resolvePreferredNodePath,
|
||||||
|
resolveStableNodePath,
|
||||||
resolveSystemNodeInfo,
|
resolveSystemNodeInfo,
|
||||||
} from "./runtime-paths.js";
|
} from "./runtime-paths.js";
|
||||||
|
|
||||||
@@ -19,9 +20,9 @@ afterEach(() => {
|
|||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockNodePathPresent(nodePath: string) {
|
function mockNodePathPresent(...nodePaths: string[]) {
|
||||||
fsMocks.access.mockImplementation(async (target: string) => {
|
fsMocks.access.mockImplementation(async (target: string) => {
|
||||||
if (target === nodePath) {
|
if (nodePaths.includes(target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("missing");
|
throw new Error("missing");
|
||||||
@@ -142,6 +143,61 @@ describe("resolvePreferredNodePath", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveStableNodePath", () => {
|
||||||
|
it("resolves Homebrew Cellar path to stable symlink", async () => {
|
||||||
|
mockNodePathPresent("/opt/homebrew/bin/node");
|
||||||
|
|
||||||
|
const result = await resolveStableNodePath("/opt/homebrew/Cellar/node/25.7.0/bin/node");
|
||||||
|
expect(result).toBe("/opt/homebrew/bin/node");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves Intel Mac Cellar path to stable symlink", async () => {
|
||||||
|
mockNodePathPresent("/usr/local/bin/node");
|
||||||
|
|
||||||
|
const result = await resolveStableNodePath("/usr/local/Cellar/node/25.7.0/bin/node");
|
||||||
|
expect(result).toBe("/usr/local/bin/node");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns original path when symlink does not exist", async () => {
|
||||||
|
fsMocks.access.mockRejectedValue(new Error("missing"));
|
||||||
|
|
||||||
|
const cellarPath = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
||||||
|
const result = await resolveStableNodePath(cellarPath);
|
||||||
|
expect(result).toBe(cellarPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns non-Cellar paths unchanged", async () => {
|
||||||
|
const fnmPath = "/Users/test/.fnm/node-versions/v24.11.1/installation/bin/node";
|
||||||
|
const result = await resolveStableNodePath(fnmPath);
|
||||||
|
expect(result).toBe(fnmPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns system paths unchanged", async () => {
|
||||||
|
const result = await resolveStableNodePath("/opt/homebrew/bin/node");
|
||||||
|
expect(result).toBe("/opt/homebrew/bin/node");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolvePreferredNodePath — Homebrew Cellar", () => {
|
||||||
|
it("resolves Cellar execPath to stable Homebrew symlink", async () => {
|
||||||
|
const cellarNode = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
||||||
|
const stableNode = "/opt/homebrew/bin/node";
|
||||||
|
mockNodePathPresent(stableNode);
|
||||||
|
|
||||||
|
const execFile = vi.fn().mockResolvedValue({ stdout: "25.7.0\n", stderr: "" });
|
||||||
|
|
||||||
|
const result = await resolvePreferredNodePath({
|
||||||
|
env: {},
|
||||||
|
runtime: "node",
|
||||||
|
platform: "darwin",
|
||||||
|
execFile,
|
||||||
|
execPath: cellarNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(stableNode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveSystemNodeInfo", () => {
|
describe("resolveSystemNodeInfo", () => {
|
||||||
const darwinNode = "/opt/homebrew/bin/node";
|
const darwinNode = "/opt/homebrew/bin/node";
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,26 @@ export function renderSystemNodeWarning(
|
|||||||
return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22+.${selectedLabel} Install Node 22+ from nodejs.org or Homebrew.`;
|
return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22+.${selectedLabel} Install Node 22+ from nodejs.org or Homebrew.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 the stable Homebrew symlink path (/opt/homebrew/bin/node)
|
||||||
|
* which Homebrew updates automatically during upgrades.
|
||||||
|
*/
|
||||||
|
export async function resolveStableNodePath(nodePath: string): Promise<string> {
|
||||||
|
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/);
|
||||||
|
if (!cellarMatch) {
|
||||||
|
return nodePath;
|
||||||
|
}
|
||||||
|
const stablePath = `${cellarMatch[1]}/bin/node`;
|
||||||
|
try {
|
||||||
|
await fs.access(stablePath);
|
||||||
|
return stablePath;
|
||||||
|
} catch {
|
||||||
|
return nodePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolvePreferredNodePath(params: {
|
export async function resolvePreferredNodePath(params: {
|
||||||
env?: Record<string, string | undefined>;
|
env?: Record<string, string | undefined>;
|
||||||
runtime?: string;
|
runtime?: string;
|
||||||
@@ -172,7 +192,7 @@ export async function resolvePreferredNodePath(params: {
|
|||||||
const execFileImpl = params.execFile ?? execFileAsync;
|
const execFileImpl = params.execFile ?? execFileAsync;
|
||||||
const version = await resolveNodeVersion(currentExecPath, execFileImpl);
|
const version = await resolveNodeVersion(currentExecPath, execFileImpl);
|
||||||
if (isSupportedNodeVersion(version)) {
|
if (isSupportedNodeVersion(version)) {
|
||||||
return currentExecPath;
|
return resolveStableNodePath(currentExecPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user