mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
fix(windows): land #31147 plugin install spawn EINVAL (@codertony)
Landed from contributor PR #31147 by @codertony. Co-authored-by: codertony <codertony@users.noreply.github.com>
This commit is contained in:
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -133,6 +133,15 @@ describe("runCommandWithTimeout", () => {
|
||||
expect(result.noOutputTimedOut).toBe(false);
|
||||
expect(result.code).not.toBe(0);
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "win32")(
|
||||
"on Windows spawns node + npm-cli.js for npm argv to avoid spawn EINVAL",
|
||||
async () => {
|
||||
const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 });
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("attachChildProcessBridge", () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { promisify } from "node:util";
|
||||
import { danger, shouldLogVerbose } from "../globals.js";
|
||||
import { logDebug, logError } from "../logger.js";
|
||||
@@ -7,22 +9,46 @@ import { resolveCommandStdio } from "./spawn-utils.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* On Windows, Node 18.20.2+ (CVE-2024-27980) rejects spawning .cmd/.bat directly
|
||||
* without shell, causing EINVAL. Resolve npm/npx to node + cli script so we
|
||||
* spawn node.exe instead of npm.cmd.
|
||||
*/
|
||||
function resolveNpmArgvForWindows(argv: string[]): string[] | null {
|
||||
if (process.platform !== "win32" || argv.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const basename = path
|
||||
.basename(argv[0])
|
||||
.toLowerCase()
|
||||
.replace(/\.(cmd|exe|bat)$/, "");
|
||||
const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null;
|
||||
if (!cliName) {
|
||||
return null;
|
||||
}
|
||||
const nodeDir = path.dirname(process.execPath);
|
||||
const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName);
|
||||
if (!fs.existsSync(cliPath)) {
|
||||
return null;
|
||||
}
|
||||
return [process.execPath, cliPath, ...argv.slice(1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a command for Windows compatibility.
|
||||
* On Windows, non-.exe commands (like npm, pnpm) require their .cmd extension.
|
||||
* On Windows, non-.exe commands (like pnpm, yarn) are resolved to .cmd; npm/npx
|
||||
* 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();
|
||||
// Skip if already has an extension (.cmd, .exe, .bat, etc.)
|
||||
const ext = path.extname(basename);
|
||||
if (ext) {
|
||||
return command;
|
||||
}
|
||||
// Common npm-related commands that need .cmd extension on Windows
|
||||
const cmdCommands = ["npm", "pnpm", "yarn", "npx"];
|
||||
const cmdCommands = ["pnpm", "yarn"];
|
||||
if (cmdCommands.includes(basename)) {
|
||||
return `${command}.cmd`;
|
||||
}
|
||||
@@ -58,7 +84,23 @@ export async function runExec(
|
||||
encoding: "utf8" as const,
|
||||
};
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(resolveCommand(command), args, options);
|
||||
const argv = [command, ...args];
|
||||
let execCommand: string;
|
||||
let execArgs: string[];
|
||||
if (process.platform === "win32") {
|
||||
const resolved = resolveNpmArgvForWindows(argv);
|
||||
if (resolved) {
|
||||
execCommand = resolved[0] ?? "";
|
||||
execArgs = resolved.slice(1);
|
||||
} else {
|
||||
execCommand = resolveCommand(command);
|
||||
execArgs = args;
|
||||
}
|
||||
} else {
|
||||
execCommand = resolveCommand(command);
|
||||
execArgs = args;
|
||||
}
|
||||
const { stdout, stderr } = await execFileAsync(execCommand, execArgs, options);
|
||||
if (shouldLogVerbose()) {
|
||||
if (stdout.trim()) {
|
||||
logDebug(stdout.trim());
|
||||
@@ -134,8 +176,9 @@ export async function runCommandWithTimeout(
|
||||
}
|
||||
|
||||
const stdio = resolveCommandStdio({ hasInput, preferInherit: true });
|
||||
const resolvedCommand = resolveCommand(argv[0] ?? "");
|
||||
const child = spawn(resolvedCommand, argv.slice(1), {
|
||||
const finalArgv = process.platform === "win32" ? (resolveNpmArgvForWindows(argv) ?? argv) : argv;
|
||||
const resolvedCommand = finalArgv !== argv ? (finalArgv[0] ?? "") : resolveCommand(argv[0] ?? "");
|
||||
const child = spawn(resolvedCommand, finalArgv.slice(1), {
|
||||
stdio,
|
||||
cwd,
|
||||
env: resolvedEnv,
|
||||
|
||||
Reference in New Issue
Block a user