Skip to content

Commit a1a8ec6

Browse files
steipetecodertony
andcommitted
fix(windows): land openclaw#31147 plugin install spawn EINVAL (@codertony)
Landed from contributor PR openclaw#31147 by @codertony. Co-authored-by: codertony <[email protected]>
1 parent 00d2df4 commit a1a8ec6

File tree

3 files changed

+60
-7
lines changed

3 files changed

+60
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
2020

2121
### Fixes
2222

23+
- Windows/Plugin install: avoid `spawn EINVAL` on Windows npm/npx invocations by resolving to `node` + npm CLI scripts instead of spawning `.cmd` directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.
2324
- Agents/Thinking fallback: when providers reject unsupported thinking levels without enumerating alternatives, retry with `think=off` to avoid hard failure during model/provider fallback chains. Landed from contributor PR #31002 by @yfge. Thanks @yfge.
2425
- Agents/Failover reason classification: avoid false rate-limit classification from incidental `tpm` substrings by matching TPM as a standalone token/phrase and keeping auth-context errors on the auth path. Landed from contributor PR #31007 by @HOYALIM. Thanks @HOYALIM.
2526
- Slack/Announce target account routing: enable session-backed announce-target lookup for Slack so multi-account announces resolve the correct `accountId` instead of defaulting to bot-token context. Landed from contributor PR #31028 by @taw0002. Thanks @taw0002.

src/process/exec.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ describe("runCommandWithTimeout", () => {
133133
expect(result.noOutputTimedOut).toBe(false);
134134
expect(result.code).not.toBe(0);
135135
});
136+
137+
it.runIf(process.platform === "win32")(
138+
"on Windows spawns node + npm-cli.js for npm argv to avoid spawn EINVAL",
139+
async () => {
140+
const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 });
141+
expect(result.code).toBe(0);
142+
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
143+
},
144+
);
136145
});
137146

138147
describe("attachChildProcessBridge", () => {

src/process/exec.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
11
import { execFile, spawn } from "node:child_process";
2+
import fs from "node:fs";
23
import path from "node:path";
4+
import process from "node:process";
35
import { promisify } from "node:util";
46
import { danger, shouldLogVerbose } from "../globals.js";
57
import { logDebug, logError } from "../logger.js";
68
import { resolveCommandStdio } from "./spawn-utils.js";
79

810
const execFileAsync = promisify(execFile);
911

12+
/**
13+
* On Windows, Node 18.20.2+ (CVE-2024-27980) rejects spawning .cmd/.bat directly
14+
* without shell, causing EINVAL. Resolve npm/npx to node + cli script so we
15+
* spawn node.exe instead of npm.cmd.
16+
*/
17+
function resolveNpmArgvForWindows(argv: string[]): string[] | null {
18+
if (process.platform !== "win32" || argv.length === 0) {
19+
return null;
20+
}
21+
const basename = path
22+
.basename(argv[0])
23+
.toLowerCase()
24+
.replace(/\.(cmd|exe|bat)$/, "");
25+
const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null;
26+
if (!cliName) {
27+
return null;
28+
}
29+
const nodeDir = path.dirname(process.execPath);
30+
const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName);
31+
if (!fs.existsSync(cliPath)) {
32+
return null;
33+
}
34+
return [process.execPath, cliPath, ...argv.slice(1)];
35+
}
36+
1037
/**
1138
* Resolves a command for Windows compatibility.
12-
* On Windows, non-.exe commands (like npm, pnpm) require their .cmd extension.
39+
* On Windows, non-.exe commands (like pnpm, yarn) are resolved to .cmd; npm/npx
40+
* are handled by resolveNpmArgvForWindows to avoid spawn EINVAL (no direct .cmd).
1341
*/
1442
function resolveCommand(command: string): string {
1543
if (process.platform !== "win32") {
1644
return command;
1745
}
1846
const basename = path.basename(command).toLowerCase();
19-
// Skip if already has an extension (.cmd, .exe, .bat, etc.)
2047
const ext = path.extname(basename);
2148
if (ext) {
2249
return command;
2350
}
24-
// Common npm-related commands that need .cmd extension on Windows
25-
const cmdCommands = ["npm", "pnpm", "yarn", "npx"];
51+
const cmdCommands = ["pnpm", "yarn"];
2652
if (cmdCommands.includes(basename)) {
2753
return `${command}.cmd`;
2854
}
@@ -58,7 +84,23 @@ export async function runExec(
5884
encoding: "utf8" as const,
5985
};
6086
try {
61-
const { stdout, stderr } = await execFileAsync(resolveCommand(command), args, options);
87+
const argv = [command, ...args];
88+
let execCommand: string;
89+
let execArgs: string[];
90+
if (process.platform === "win32") {
91+
const resolved = resolveNpmArgvForWindows(argv);
92+
if (resolved) {
93+
execCommand = resolved[0] ?? "";
94+
execArgs = resolved.slice(1);
95+
} else {
96+
execCommand = resolveCommand(command);
97+
execArgs = args;
98+
}
99+
} else {
100+
execCommand = resolveCommand(command);
101+
execArgs = args;
102+
}
103+
const { stdout, stderr } = await execFileAsync(execCommand, execArgs, options);
62104
if (shouldLogVerbose()) {
63105
if (stdout.trim()) {
64106
logDebug(stdout.trim());
@@ -134,8 +176,9 @@ export async function runCommandWithTimeout(
134176
}
135177

136178
const stdio = resolveCommandStdio({ hasInput, preferInherit: true });
137-
const resolvedCommand = resolveCommand(argv[0] ?? "");
138-
const child = spawn(resolvedCommand, argv.slice(1), {
179+
const finalArgv = process.platform === "win32" ? (resolveNpmArgvForWindows(argv) ?? argv) : argv;
180+
const resolvedCommand = finalArgv !== argv ? (finalArgv[0] ?? "") : resolveCommand(argv[0] ?? "");
181+
const child = spawn(resolvedCommand, finalArgv.slice(1), {
139182
stdio,
140183
cwd,
141184
env: resolvedEnv,

0 commit comments

Comments
 (0)