From d000316d1901a802f06a77bc8d1ddf52aa504012 Mon Sep 17 00:00:00 2001 From: 0xlin2023 Date: Fri, 6 Mar 2026 23:01:52 +0800 Subject: [PATCH] fix: Windows: openclaw plugins install fails with spawn EINVAL Fixes #7631 --- src/process/exec.test.ts | 15 +++++++++++++++ src/process/exec.ts | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index 6f2c3640c11..19937d6cb32 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -1,5 +1,6 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; +import fs from "node:fs"; import process from "node:process"; import { describe, expect, it, vi } from "vitest"; import { attachChildProcessBridge } from "./child-process-bridge.js"; @@ -77,6 +78,20 @@ describe("runCommandWithTimeout", () => { expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/); }, ); + + it.runIf(process.platform === "win32")( + "falls back to npm.cmd when npm-cli.js is unavailable", + async () => { + const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false); + try { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 }); + expect(result.code).toBe(0); + expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/); + } finally { + existsSpy.mockRestore(); + } + }, + ); }); describe("attachChildProcessBridge", () => { diff --git a/src/process/exec.ts b/src/process/exec.ts index ef6b707fbe6..ddc572092d8 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -58,7 +58,13 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null { const nodeDir = path.dirname(process.execPath); const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName); if (!fs.existsSync(cliPath)) { - return null; + // Bun-based runs don't ship npm-cli.js next to process.execPath. + // Fall back to npm.cmd/npx.cmd so we still route through cmd wrapper + // (avoids direct .cmd spawn EINVAL on patched Node). + const command = argv[0] ?? ""; + const ext = path.extname(command).toLowerCase(); + const shimmedCommand = ext ? command : `${command}.cmd`; + return [shimmedCommand, ...argv.slice(1)]; } return [process.execPath, cliPath, ...argv.slice(1)]; }