diff --git a/src/process/exec.ts b/src/process/exec.ts index 3464a083894..7692fb9ad21 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -7,6 +7,7 @@ import { danger, shouldLogVerbose } from "../globals.js"; import { markOpenClawExecEnv } from "../infra/openclaw-exec-env.js"; import { logDebug, logError } from "../logger.js"; import { resolveCommandStdio } from "./spawn-utils.js"; +import { resolveWindowsCommandShim } from "./windows-command.js"; const execFileAsync = promisify(execFile); @@ -76,19 +77,10 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null { * are handled by resolveNpmArgvForWindows to avoid spawn EINVAL (no direct .cmd). */ function resolveCommand(command: string): string { - if (process.platform !== "win32") { - return command; - } - const basename = path.basename(command).toLowerCase(); - const ext = path.extname(basename); - if (ext) { - return command; - } - const cmdCommands = ["pnpm", "yarn"]; - if (cmdCommands.includes(basename)) { - return `${command}.cmd`; - } - return command; + return resolveWindowsCommandShim({ + command, + cmdCommands: ["pnpm", "yarn"], + }); } export function shouldSpawnWithShell(params: { diff --git a/src/process/supervisor/adapters/child.ts b/src/process/supervisor/adapters/child.ts index 44275df6e64..04d7e1d7aa1 100644 --- a/src/process/supervisor/adapters/child.ts +++ b/src/process/supervisor/adapters/child.ts @@ -1,22 +1,15 @@ import type { ChildProcessWithoutNullStreams, SpawnOptions } from "node:child_process"; import { killProcessTree } from "../../kill-tree.js"; import { spawnWithFallback } from "../../spawn-utils.js"; +import { resolveWindowsCommandShim } from "../../windows-command.js"; import type { ManagedRunStdin, SpawnProcessAdapter } from "../types.js"; import { toStringEnv } from "./env.js"; function resolveCommand(command: string): string { - if (process.platform !== "win32") { - return command; - } - const lower = command.toLowerCase(); - if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat")) { - return command; - } - const basename = lower.split(/[\\/]/).pop() ?? lower; - if (basename === "npm" || basename === "pnpm" || basename === "yarn" || basename === "npx") { - return `${command}.cmd`; - } - return command; + return resolveWindowsCommandShim({ + command, + cmdCommands: ["npm", "pnpm", "yarn", "npx"], + }); } export type ChildAdapter = SpawnProcessAdapter; diff --git a/src/process/windows-command.test.ts b/src/process/windows-command.test.ts new file mode 100644 index 00000000000..47b1907fbf0 --- /dev/null +++ b/src/process/windows-command.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { resolveWindowsCommandShim } from "./windows-command.js"; + +describe("resolveWindowsCommandShim", () => { + it("leaves commands unchanged outside Windows", () => { + expect( + resolveWindowsCommandShim({ + command: "pnpm", + cmdCommands: ["pnpm"], + platform: "linux", + }), + ).toBe("pnpm"); + }); + + it("appends .cmd for configured Windows shims", () => { + expect( + resolveWindowsCommandShim({ + command: "pnpm", + cmdCommands: ["pnpm", "yarn"], + platform: "win32", + }), + ).toBe("pnpm.cmd"); + }); + + it("keeps explicit extensions on Windows", () => { + expect( + resolveWindowsCommandShim({ + command: "npm.cmd", + cmdCommands: ["npm", "npx"], + platform: "win32", + }), + ).toBe("npm.cmd"); + }); +}); diff --git a/src/process/windows-command.ts b/src/process/windows-command.ts new file mode 100644 index 00000000000..c8e5981e2ef --- /dev/null +++ b/src/process/windows-command.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import process from "node:process"; + +export function resolveWindowsCommandShim(params: { + command: string; + cmdCommands: readonly string[]; + platform?: NodeJS.Platform; +}): string { + if ((params.platform ?? process.platform) !== "win32") { + return params.command; + } + const basename = path.basename(params.command).toLowerCase(); + if (path.extname(basename)) { + return params.command; + } + if (params.cmdCommands.includes(basename)) { + return `${params.command}.cmd`; + } + return params.command; +}