Skip to content

Commit 5dd264d

Browse files
committed
refactor(daemon): unify runtime binary detection
1 parent 58171c8 commit 5dd264d

File tree

3 files changed

+66
-22
lines changed

3 files changed

+66
-22
lines changed

src/cli/argv.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isBunRuntime, isNodeRuntime } from "../daemon/runtime-binary.js";
2+
13
const HELP_FLAGS = new Set(["-h", "--help"]);
24
const VERSION_FLAGS = new Set(["-V", "--version"]);
35
const ROOT_VERSION_ALIAS_FLAG = "-v";
@@ -163,31 +165,15 @@ export function buildParseArgv(params: {
163165
: baseArgv[0]?.endsWith("openclaw")
164166
? baseArgv.slice(1)
165167
: baseArgv;
166-
const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase();
167168
const looksLikeNode =
168-
normalizedArgv.length >= 2 && (isNodeExecutable(executable) || isBunExecutable(executable));
169+
normalizedArgv.length >= 2 &&
170+
(isNodeRuntime(normalizedArgv[0] ?? "") || isBunRuntime(normalizedArgv[0] ?? ""));
169171
if (looksLikeNode) {
170172
return normalizedArgv;
171173
}
172174
return ["node", programName || "openclaw", ...normalizedArgv];
173175
}
174176

175-
const nodeExecutablePattern = /^node(?:-\d+|\d+)(?:\.\d+)*(?:\.exe)?$/;
176-
177-
function isNodeExecutable(executable: string): boolean {
178-
return (
179-
executable === "node" ||
180-
executable === "node.exe" ||
181-
executable === "nodejs" ||
182-
executable === "nodejs.exe" ||
183-
nodeExecutablePattern.test(executable)
184-
);
185-
}
186-
187-
function isBunExecutable(executable: string): boolean {
188-
return executable === "bun" || executable === "bun.exe";
189-
}
190-
191177
export function shouldMigrateStateFromPath(path: string[]): boolean {
192178
if (path.length === 0) {
193179
return true;

src/daemon/runtime-binary.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isBunRuntime, isNodeRuntime } from "./runtime-binary.js";
3+
4+
describe("isNodeRuntime", () => {
5+
it("recognizes standard node binaries", () => {
6+
expect(isNodeRuntime("/usr/bin/node")).toBe(true);
7+
expect(isNodeRuntime("C:\\Program Files\\nodejs\\node.exe")).toBe(true);
8+
expect(isNodeRuntime("/usr/bin/nodejs")).toBe(true);
9+
expect(isNodeRuntime("C:\\nodejs.exe")).toBe(true);
10+
});
11+
12+
it("recognizes versioned node binaries with and without dashes", () => {
13+
expect(isNodeRuntime("/usr/bin/node24")).toBe(true);
14+
expect(isNodeRuntime("/usr/bin/node-24")).toBe(true);
15+
expect(isNodeRuntime("/usr/bin/node24.1")).toBe(true);
16+
expect(isNodeRuntime("/usr/bin/node-24.1")).toBe(true);
17+
expect(isNodeRuntime("C:\\node24.exe")).toBe(true);
18+
expect(isNodeRuntime("C:\\node-24.exe")).toBe(true);
19+
});
20+
21+
it("handles quotes and casing", () => {
22+
expect(isNodeRuntime('"/usr/bin/node24"')).toBe(true);
23+
expect(isNodeRuntime("'C:\\Program Files\\nodejs\\NODE.EXE'")).toBe(true);
24+
});
25+
26+
it("rejects non-node runtimes", () => {
27+
expect(isNodeRuntime("/usr/bin/bun")).toBe(false);
28+
expect(isNodeRuntime("/usr/bin/node-dev")).toBe(false);
29+
expect(isNodeRuntime("/usr/bin/nodeenv")).toBe(false);
30+
expect(isNodeRuntime("/usr/bin/nodemon")).toBe(false);
31+
});
32+
});
33+
34+
describe("isBunRuntime", () => {
35+
it("recognizes bun binaries", () => {
36+
expect(isBunRuntime("/usr/bin/bun")).toBe(true);
37+
expect(isBunRuntime("C:\\BUN.EXE")).toBe(true);
38+
expect(isBunRuntime('"/opt/homebrew/bin/bun"')).toBe(true);
39+
});
40+
41+
it("rejects non-bun runtimes", () => {
42+
expect(isBunRuntime("/usr/bin/node")).toBe(false);
43+
expect(isBunRuntime("/usr/bin/bunx")).toBe(false);
44+
});
45+
});

src/daemon/runtime-binary.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
import path from "node:path";
1+
const NODE_VERSIONED_PATTERN = /^node(?:-\d+|\d+)(?:\.\d+)*(?:\.exe)?$/;
2+
3+
function normalizeRuntimeBasename(execPath: string): string {
4+
const trimmed = execPath.trim().replace(/^["']|["']$/g, "");
5+
const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
6+
const basename = lastSlash === -1 ? trimmed : trimmed.slice(lastSlash + 1);
7+
return basename.toLowerCase();
8+
}
29

310
export function isNodeRuntime(execPath: string): boolean {
4-
const base = path.basename(execPath).toLowerCase();
5-
return base === "node" || base === "node.exe";
11+
const base = normalizeRuntimeBasename(execPath);
12+
return (
13+
base === "node" ||
14+
base === "node.exe" ||
15+
base === "nodejs" ||
16+
base === "nodejs.exe" ||
17+
NODE_VERSIONED_PATTERN.test(base)
18+
);
619
}
720

821
export function isBunRuntime(execPath: string): boolean {
9-
const base = path.basename(execPath).toLowerCase();
22+
const base = normalizeRuntimeBasename(execPath);
1023
return base === "bun" || base === "bun.exe";
1124
}

0 commit comments

Comments
 (0)