diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index d9faa981f..52e4b24f4 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -1,5 +1,3 @@ -import fs from "node:fs"; -import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; @@ -13,6 +11,7 @@ import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections. import { enableConsoleCapture } from "../logging.js"; import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js"; import { tryRouteCli } from "./route.js"; +import { normalizeWindowsArgv } from "./windows-argv.js"; export function rewriteUpdateFlagArgv(argv: string[]): string[] { const index = argv.indexOf("--update"); @@ -24,7 +23,7 @@ export function rewriteUpdateFlagArgv(argv: string[]): string[] { } export async function runCli(argv: string[] = process.argv) { - const normalizedArgv = stripWindowsNodeExec(argv); + const normalizedArgv = normalizeWindowsArgv(argv); loadDotEnv({ quiet: true }); normalizeEnv(); ensureClawdbotCliOnPath(); @@ -60,53 +59,6 @@ export async function runCli(argv: string[] = process.argv) { await program.parseAsync(parseArgv); } -function stripWindowsNodeExec(argv: string[]): string[] { - if (process.platform !== "win32") return argv; - const stripControlChars = (value: string): string => { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - const code = value.charCodeAt(i); - if (code >= 32 && code !== 127) { - out += value[i]; - } - } - return out; - }; - const normalizeArg = (value: string): string => - stripControlChars(value) - .replace(/^['"]+|['"]+$/g, "") - .trim(); - const normalizeCandidate = (value: string): string => - normalizeArg(value).replace(/^\\\\\\?\\/, ""); - const execPath = normalizeCandidate(process.execPath); - const execPathLower = execPath.toLowerCase(); - const execBase = path.basename(execPath).toLowerCase(); - const isExecPath = (value: string | undefined): boolean => { - if (!value) return false; - const normalized = normalizeCandidate(value); - if (!normalized) return false; - const lower = normalized.toLowerCase(); - return ( - lower === execPathLower || - path.basename(lower) === execBase || - lower.endsWith("\\node.exe") || - lower.endsWith("/node.exe") || - lower.includes("node.exe") || - (path.basename(lower) === "node.exe" && fs.existsSync(normalized)) - ); - }; - const filtered = argv.filter((arg, index) => index === 0 || !isExecPath(arg)); - if (filtered.length < 3) return filtered; - const cleaned = [...filtered]; - if (isExecPath(cleaned[1])) { - cleaned.splice(1, 1); - } - if (isExecPath(cleaned[2])) { - cleaned.splice(2, 1); - } - return cleaned; -} - export function isCliMainModule(): boolean { return isMainModule({ currentFile: fileURLToPath(import.meta.url) }); } diff --git a/src/cli/windows-argv.test.ts b/src/cli/windows-argv.test.ts new file mode 100644 index 000000000..a9e9f1ee5 --- /dev/null +++ b/src/cli/windows-argv.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeWindowsArgv } from "./windows-argv.js"; + +describe("normalizeWindowsArgv", () => { + const execPath = "C:\\Program Files\\nodejs\\node.exe"; + const scriptPath = "C:\\clawdbot\\dist\\entry.js"; + + it("returns argv unchanged on non-windows platforms", () => { + const argv = [execPath, scriptPath, "status"]; + expect(normalizeWindowsArgv(argv, { platform: "darwin", execPath })).toBe(argv); + }); + + it("removes duplicate node exec at argv[1]", () => { + const argv = [execPath, execPath, scriptPath, "status"]; + expect(normalizeWindowsArgv(argv, { platform: "win32", execPath })).toEqual([ + execPath, + scriptPath, + "status", + ]); + }); + + it("removes duplicate node exec at argv[2]", () => { + const argv = [execPath, scriptPath, execPath, "gateway", "run"]; + expect(normalizeWindowsArgv(argv, { platform: "win32", execPath })).toEqual([ + execPath, + scriptPath, + "gateway", + "run", + ]); + }); + + it("keeps url arguments that contain node.exe", () => { + const argv = [execPath, scriptPath, "send", "https://example.com/node.exe"]; + expect(normalizeWindowsArgv(argv, { platform: "win32", execPath })).toEqual(argv); + }); + + it("keeps node.exe paths after the command", () => { + const argv = [execPath, scriptPath, "send", "C:\\Program Files\\nodejs\\node.exe"]; + expect(normalizeWindowsArgv(argv, { platform: "win32", execPath })).toEqual(argv); + }); +}); diff --git a/src/cli/windows-argv.ts b/src/cli/windows-argv.ts new file mode 100644 index 000000000..a65aff688 --- /dev/null +++ b/src/cli/windows-argv.ts @@ -0,0 +1,55 @@ +import path from "node:path"; +import process from "node:process"; + +type WindowsArgvOptions = { + platform?: NodeJS.Platform; + execPath?: string; +}; + +export function normalizeWindowsArgv( + argv: string[], + { platform = process.platform, execPath = process.execPath }: WindowsArgvOptions = {}, +): string[] { + if (platform !== "win32") return argv; + if (argv.length < 2) return argv; + + const stripControlChars = (value: string): string => { + let out = ""; + for (let i = 0; i < value.length; i += 1) { + const code = value.charCodeAt(i); + if (code >= 32 && code !== 127) { + out += value[i]; + } + } + return out; + }; + const normalizeArg = (value: string): string => + stripControlChars(value) + .replace(/^['"]+|['"]+$/g, "") + .trim(); + const normalizeCandidate = (value: string): string => + normalizeArg(value).replace(/^\\\\\\?\\/, ""); + const execPathNormalized = normalizeCandidate(execPath); + const execPathLower = execPathNormalized.toLowerCase(); + const execBaseLower = path.basename(execPathLower); + const isNodeExecPath = (value: string | undefined): boolean => { + if (!value) return false; + const normalized = normalizeCandidate(value); + if (!normalized) return false; + if (normalized.includes("://")) return false; + const lower = normalized.toLowerCase(); + if (lower === execPathLower || lower === execBaseLower) return true; + if (!lower.endsWith("node.exe")) return false; + return path.isAbsolute(normalized) || normalized.includes("\\") || normalized.includes("/"); + }; + + const next = [...argv]; + for (let i = 1; i <= 2 && i < next.length; ) { + if (isNodeExecPath(next[i])) { + next.splice(i, 1); + continue; + } + i += 1; + } + return next; +} diff --git a/src/entry.ts b/src/entry.ts index b09922ff0..9500c7aa1 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; -import path from "node:path"; import process from "node:process"; import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js"; +import { normalizeWindowsArgv } from "./cli/windows-argv.js"; import { isTruthyEnvValue } from "./infra/env.js"; import { attachChildProcessBridge } from "./process/child-process-bridge.js"; @@ -57,65 +57,6 @@ function ensureExperimentalWarningSuppressed(): boolean { return true; } -function normalizeWindowsArgv(argv: string[]): string[] { - if (process.platform !== "win32") return argv; - if (argv.length < 2) return argv; - const stripControlChars = (value: string): string => { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - const code = value.charCodeAt(i); - if (code >= 32 && code !== 127) { - out += value[i]; - } - } - return out; - }; - const normalizeArg = (value: string): string => - stripControlChars(value) - .replace(/^['"]+|['"]+$/g, "") - .trim(); - const normalizeCandidate = (value: string): string => - normalizeArg(value).replace(/^\\\\\\?\\/, ""); - const execPath = normalizeCandidate(process.execPath); - const execPathLower = execPath.toLowerCase(); - const execBase = path.basename(execPath).toLowerCase(); - const isExecPath = (value: string | undefined): boolean => { - if (!value) return false; - const lower = normalizeCandidate(value).toLowerCase(); - return ( - lower === execPathLower || - path.basename(lower) === execBase || - lower.endsWith("\\node.exe") || - lower.endsWith("/node.exe") || - lower.includes("node.exe") - ); - }; - const next = [...argv]; - for (let i = 1; i <= 3 && i < next.length; ) { - if (isExecPath(next[i])) { - next.splice(i, 1); - continue; - } - i += 1; - } - const filtered = next.filter((arg, index) => index === 0 || !isExecPath(arg)); - if (filtered.length < 3) return filtered; - const cleaned = [...filtered]; - for (let i = 2; i < cleaned.length; ) { - const arg = cleaned[i]; - if (!arg || arg.startsWith("-")) { - i += 1; - continue; - } - if (isExecPath(arg)) { - cleaned.splice(i, 1); - continue; - } - break; - } - return cleaned; -} - process.argv = normalizeWindowsArgv(process.argv); if (!ensureExperimentalWarningSuppressed()) {