fix(scripts): resolve Windows cmd shim launcher

This commit is contained in:
Vincent Koc
2026-06-21 11:15:14 +02:00
parent 9242137ca7
commit 6c4028e073
24 changed files with 217 additions and 114 deletions
+1 -1
View File
@@ -341,7 +341,7 @@ export function resolveBuildAllStep(step, params = {}) {
pnpmArgs: step.pnpmArgs,
nodeExecPath: params.nodeExecPath ?? nodeBin,
npmExecPath: params.npmExecPath ?? env.npm_execpath,
comSpec: params.comSpec ?? env.ComSpec,
comSpec: params.comSpec,
platform,
});
return {
+2 -2
View File
@@ -19,7 +19,7 @@ import {
import { homedir, tmpdir } from "node:os";
import { delimiter, dirname, extname, isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
import { resolvePathEnvKey, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const ignoreRepoBinary = process.env.OPENCLAW_CRABBOX_WRAPPER_IGNORE_REPO_BINARY === "1";
@@ -119,7 +119,7 @@ function spawnInvocation(command, commandArgs, env, platform) {
const extension = extname(command).toLowerCase();
if (platform === "win32" && (extension === ".cmd" || extension === ".bat")) {
return {
command: env.ComSpec ?? "cmd.exe",
command: resolveWindowsCmdExePath(env),
args: ["/d", "/s", "/c", buildBatchCommandLine(command, commandArgs)],
windowsVerbatimArguments: true,
};
@@ -5,7 +5,7 @@ import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { parseReleaseVersion } from "../../../lib/npm-publish-plan.mjs";
import { buildCmdExeCommandLine } from "../../../windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../../../windows-cmd-helpers.mjs";
const args = process.argv.slice(2);
const command = args.shift();
@@ -215,7 +215,7 @@ function adaptStepForBaseline(step, baselineVersion, summary) {
export function resolveUpgradeSurvivorOpenClawCommand(argv, params = {}) {
const platform = params.platform ?? process.platform;
if (platform === "win32") {
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
const comSpec = params.comSpec ?? resolveWindowsCmdExePath(params.env ?? process.env);
return {
command: comSpec,
args: ["/d", "/s", "/c", buildCmdExeCommandLine("openclaw.cmd", argv)],
+2 -7
View File
@@ -6,7 +6,7 @@ import { finished } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { resolveNpmRunner } from "../../npm-runner.mjs";
import { resolvePnpmRunner } from "../../pnpm-runner.mjs";
import { buildCmdExeCommandLine } from "../../windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../../windows-cmd-helpers.mjs";
import type { CommandResult, RunOptions } from "./types.ts";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
@@ -338,11 +338,6 @@ function isBareCommand(command: string, name: "npm" | "pnpm"): boolean {
return portableBasename(command) === command && command.toLowerCase() === name;
}
function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
return key === undefined ? undefined : env[key];
}
export function resolveHostCommandInvocation(
command: string,
args: string[],
@@ -350,7 +345,7 @@ export function resolveHostCommandInvocation(
): HostCommandInvocation {
const env = options.env ?? process.env;
const platform = options.platform ?? process.platform;
const comSpec = options.comSpec ?? resolveEnvValue(env, "ComSpec") ?? "cmd.exe";
const comSpec = options.comSpec ?? resolveWindowsCmdExePath(env);
if (isBareCommand(command, "pnpm")) {
const runner = resolvePnpmRunner({
+1 -1
View File
@@ -52,7 +52,7 @@ export function resolvePlaywrightInstallRunner(options = {}) {
const env = options.env ?? process.env;
const targets = options.targets ?? ["chromium"];
return resolvePnpmRunner({
comSpec: options.comSpec ?? env.ComSpec ?? env.COMSPEC,
comSpec: options.comSpec,
env,
npmExecPath: env === process.env ? env.npm_execpath : (env.npm_execpath ?? ""),
platform: options.platform,
+2 -2
View File
@@ -7,7 +7,7 @@ import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const ROOT = path.resolve(import.meta.dirname, "..");
const CHECK = process.argv.includes("--check");
@@ -128,7 +128,7 @@ export function resolveOxfmtInvocation(args, params = {}) {
if (existsSync(shimPath)) {
if (platform === "win32") {
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
const comSpec = params.comSpec ?? resolveWindowsCmdExePath(params.env ?? process.env);
return {
command: comSpec,
args: ["/d", "/s", "/c", buildCmdExeCommandLine(shimPath, args)],
@@ -42,18 +42,13 @@ type ResolvePnpmCommandOptions = {
platform?: NodeJS.Platform;
};
function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
return key === undefined ? undefined : env[key];
}
export function resolveCodexProtocolPnpmCommand(
args: string[],
options: ResolvePnpmCommandOptions = {},
): PnpmCommand {
const env = options.env ?? process.env;
const command = resolvePnpmRunner({
comSpec: options.comSpec ?? resolveEnvValue(env, "ComSpec"),
comSpec: options.comSpec,
env,
npmExecPath: options.npmExecPath ?? env.npm_execpath,
nodeExecPath: options.execPath ?? process.execPath,
+2 -2
View File
@@ -1,7 +1,7 @@
// Runs child commands with process-group signal forwarding and Windows shell normalization.
import { spawn, spawnSync } from "node:child_process";
import { constants as osConstants } from "node:os";
import { buildCmdExeCommandLine } from "../windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../windows-cmd-helpers.mjs";
import { resolveWindowsTaskkillPath } from "./windows-taskkill.mjs";
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
@@ -276,7 +276,7 @@ export function createManagedCommandInvocation({
if (platform === "win32" && shell && args.length > 0) {
return {
args: ["/d", "/s", "/c", buildCmdExeCommandLine(bin, args)],
command: comSpec ?? env?.ComSpec ?? env?.COMSPEC ?? process.env.ComSpec ?? "cmd.exe",
command: comSpec ?? resolveWindowsCmdExePath(env ?? process.env),
shell: false,
windowsVerbatimArguments: true,
};
+6 -59
View File
@@ -1,64 +1,11 @@
// Resolves the Windows taskkill binary without trusting PATH.
import path from "node:path";
// Resolves Windows system binaries without trusting PATH.
import {
resolveWindowsPowerShellPath,
resolveWindowsSystem32Path,
} from "../windows-cmd-helpers.mjs";
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
function getEnvValueCaseInsensitive(env, expectedKey) {
const direct = env[expectedKey];
if (direct !== undefined) {
return direct;
}
const expected = expectedKey.toUpperCase();
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === expected);
return actualKey ? env[actualKey] : undefined;
}
function normalizeWindowsSystemRoot(raw) {
const trimmed = raw?.trim();
if (
!trimmed ||
trimmed.includes("\0") ||
trimmed.includes("\r") ||
trimmed.includes("\n") ||
trimmed.includes(";")
) {
return null;
}
const normalized = path.win32.normalize(trimmed);
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
return null;
}
const parsed = path.win32.parse(normalized);
if (!/^[A-Za-z]:\\$/.test(parsed.root) || normalized.length <= parsed.root.length) {
return null;
}
return normalized.replace(/[\\/]+$/, "");
}
export { resolveWindowsPowerShellPath, resolveWindowsSystem32Path };
export function resolveWindowsTaskkillPath(env = process.env) {
return resolveWindowsSystem32Path("taskkill.exe", env);
}
function resolveWindowsSystemRoot(env) {
return (
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
DEFAULT_WINDOWS_SYSTEM_ROOT
);
}
export function resolveWindowsSystem32Path(executableName, env = process.env) {
if (
path.win32.basename(executableName) !== executableName ||
!/^[A-Za-z0-9_.-]+\.exe$/u.test(executableName)
) {
throw new Error(`Invalid Windows System32 executable name: ${executableName}`);
}
const systemRoot = resolveWindowsSystemRoot(env);
return path.win32.join(systemRoot, "System32", executableName);
}
export function resolveWindowsPowerShellPath(env = process.env) {
const systemRoot = resolveWindowsSystemRoot(env);
return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
}
+6 -2
View File
@@ -1,7 +1,11 @@
// Resolves npm commands from the active Node toolchain, especially on Windows.
import fs from "node:fs";
import path from "node:path";
import { buildCmdExeCommandLine, resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
import {
buildCmdExeCommandLine,
resolvePathEnvKey,
resolveWindowsCmdExePath,
} from "./windows-cmd-helpers.mjs";
function resolveToolchainNpmRunner(params) {
const npmCliCandidates = [
@@ -48,7 +52,7 @@ export function resolveNpmRunner(params = {}) {
const existsSync = params.existsSync ?? fs.existsSync;
const env = params.env ?? process.env;
const platform = params.platform ?? process.platform;
const comSpec = params.comSpec ?? env.ComSpec ?? "cmd.exe";
const comSpec = params.comSpec ?? (platform === "win32" ? resolveWindowsCmdExePath(env) : "");
const pathImpl = platform === "win32" ? path.win32 : path.posix;
const nodeDir = pathImpl.dirname(execPath);
const npmToolchain = resolveToolchainNpmRunner({
+6 -7
View File
@@ -30,7 +30,7 @@ import { basename, dirname, join, relative, resolve, win32 as pathWin32 } from "
import { fileURLToPath, pathToFileURL } from "node:url";
import { isLocalBuildMetadataDistPath } from "./lib/local-build-metadata-paths.mjs";
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const PUBLISHED_INSTALLER_BASE_URL = "https://openclaw.ai";
@@ -1780,13 +1780,12 @@ export function resolveCommandSpawnInvocation(
args,
options = {
platform: process.platform,
comSpec: process.env.ComSpec,
},
) {
const platform = options.platform ?? process.platform;
if (platform === "win32" && /\.(cmd|bat)$/iu.test(command)) {
return {
command: options.comSpec ?? process.env.ComSpec ?? "cmd.exe",
command: options.comSpec ?? resolveWindowsCmdExePath(options.env ?? process.env),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(command, args)],
shell: false,
windowsVerbatimArguments: true,
@@ -1800,7 +1799,6 @@ export function resolveInstalledCliInvocation(
args = [],
options = {
platform: process.platform,
comSpec: process.env.ComSpec,
},
) {
const platform = options.platform ?? process.platform;
@@ -1823,6 +1821,7 @@ export function resolveInstalledCliInvocation(
}
return resolveCommandSpawnInvocation(normalizedCliPath, args, {
comSpec: options.comSpec,
env: options.env,
platform,
});
}
@@ -2029,7 +2028,7 @@ async function verifyFreshShellCommand(params) {
async function runInstalledCli(params) {
const invocation = resolveInstalledCliInvocation(params.cliPath, params.args, {
comSpec: params.env?.ComSpec ?? params.env?.COMSPEC,
env: params.env,
platform: process.platform,
});
return runCommandInvocation(invocation, {
@@ -2120,7 +2119,7 @@ async function startManualGatewayFromInstalledCli(params) {
params.cliPath,
["gateway", "run", "--bind", "loopback", "--port", String(params.lane.gatewayPort), "--force"],
{
comSpec: params.env?.ComSpec ?? params.env?.COMSPEC,
env: params.env,
platform: process.platform,
},
);
@@ -3897,7 +3896,7 @@ function appendBoundedCommandOutput(current, chunk, maxBytes) {
export async function runCommand(command, args, options) {
const invocation = resolveCommandSpawnInvocation(command, args, {
comSpec: options.env?.ComSpec ?? options.env?.COMSPEC,
env: options.env,
platform: process.platform,
});
return runCommandInvocation(invocation, options);
+2 -2
View File
@@ -35,7 +35,7 @@ import {
} from "./lib/plugin-package-dependencies.mjs";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
type InstalledPackageJson = {
version?: string;
@@ -853,7 +853,7 @@ export function resolveInstalledBinaryCommandInvocation(
const binaryPath = resolveInstalledBinaryPath(prefixDir, platform);
if (platform === "win32") {
return {
command: params.comSpec ?? process.env.ComSpec ?? "cmd.exe",
command: params.comSpec ?? resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, args)],
windowsVerbatimArguments: true,
};
+4 -4
View File
@@ -17,7 +17,7 @@ import {
parseReleaseVersion as parseReleaseVersionBase,
} from "./lib/npm-publish-plan.mjs";
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "./lib/workspace-bootstrap-smoke.mjs";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
type PackageJson = {
name?: string;
@@ -508,7 +508,7 @@ export function resolveNpmCommandInvocation(
const name = portableBasename(npmExecPath).toLowerCase();
if (platform === "win32" && (name.endsWith(".cmd") || name.endsWith(".bat"))) {
return {
command: params.comSpec ?? process.env.ComSpec ?? "cmd.exe",
command: params.comSpec ?? resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(npmExecPath, npmArgs)],
windowsVerbatimArguments: true,
};
@@ -518,7 +518,7 @@ export function resolveNpmCommandInvocation(
}
if (platform === "win32" && name === "npm") {
return {
command: params.comSpec ?? process.env.ComSpec ?? "cmd.exe",
command: params.comSpec ?? resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(`${npmExecPath}.cmd`, npmArgs)],
windowsVerbatimArguments: true,
};
@@ -531,7 +531,7 @@ export function resolveNpmCommandInvocation(
if (platform === "win32") {
return {
command: params.comSpec ?? process.env.ComSpec ?? "cmd.exe",
command: params.comSpec ?? resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine("npm.cmd", npmArgs)],
windowsVerbatimArguments: true,
};
+11 -5
View File
@@ -2,7 +2,11 @@
import { spawn } from "node:child_process";
import { accessSync, closeSync, constants, openSync, readSync, statSync } from "node:fs";
import path from "node:path";
import { buildCmdExeCommandLine, resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
import {
buildCmdExeCommandLine,
resolvePathEnvKey,
resolveWindowsCmdExePath,
} from "./windows-cmd-helpers.mjs";
function getPortableBasename(value) {
return value.split(/[/\\]/).at(-1) ?? value;
@@ -61,8 +65,10 @@ function findExecutableOnPath(command, envPath, platform, env, cwd) {
}
const extensions =
platform === "win32"
? (env[Object.keys(env).find((key) => key.toLowerCase() === "pathext") ?? "PATHEXT"] ??
".COM;.EXE;.BAT;.CMD")
? (
env[Object.keys(env).find((key) => key.toLowerCase() === "pathext") ?? "PATHEXT"] ??
".COM;.EXE;.BAT;.CMD"
)
.split(";")
.filter(Boolean)
.map((extension) => extension.toLowerCase())
@@ -75,7 +81,7 @@ function findExecutableOnPath(command, envPath, platform, env, cwd) {
const resolvedDirectory = path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
for (const extension of extensions) {
const candidate = path.join(resolvedDirectory, `${command}${extension}`);
if ((platform === "win32" ? isFile(candidate) : isExecutableFile(candidate))) {
if (platform === "win32" ? isFile(candidate) : isExecutableFile(candidate)) {
return candidate;
}
}
@@ -119,8 +125,8 @@ export function resolvePnpmRunner(params = {}) {
const npmExecPath = params.npmExecPath ?? process.env.npm_execpath;
const nodeExecPath = params.nodeExecPath ?? process.execPath;
const platform = params.platform ?? process.platform;
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
const env = params.env ?? process.env;
const comSpec = params.comSpec ?? (platform === "win32" ? resolveWindowsCmdExePath(env) : "");
const envPath = env[platform === "win32" ? resolvePathEnvKey(env) : "PATH"];
const cwd = params.cwd ?? process.cwd();
+1 -1
View File
@@ -579,7 +579,7 @@ export function resolveTsdownBuildInvocation(params = {}) {
pnpmArgs: ["exec", "tsdown", ...tsdownArgs],
nodeExecPath: params.nodeExecPath ?? process.execPath,
npmExecPath: params.npmExecPath ?? env.npm_execpath,
comSpec: params.comSpec ?? env.ComSpec,
comSpec: params.comSpec,
platform: params.platform ?? process.platform,
});
return {
+3 -3
View File
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "..");
@@ -35,7 +35,6 @@ export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
*/
export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
const platform = params.platform ?? process.platform;
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
const options = {
cwd: params.cwd ?? uiDir,
stdio: "inherit",
@@ -44,6 +43,7 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
};
if (shouldUseCmdExeForCommand(cmd, platform)) {
const comSpec = params.comSpec ?? resolveWindowsCmdExePath(options.env);
return {
command: comSpec,
args: ["/d", "/s", "/c", buildCmdExeCommandLine(cmd, args)],
@@ -74,7 +74,7 @@ export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
pnpmArgs,
nodeExecPath: params.nodeExecPath ?? process.execPath,
npmExecPath: params.npmExecPath ?? env.npm_execpath,
comSpec: params.comSpec ?? env.ComSpec,
comSpec: params.comSpec,
platform,
});
return {
+4
View File
@@ -1,2 +1,6 @@
export function resolvePathEnvKey(env: NodeJS.ProcessEnv): string;
export function resolveWindowsSystemRoot(env?: NodeJS.ProcessEnv): string;
export function resolveWindowsSystem32Path(executableName: string, env?: NodeJS.ProcessEnv): string;
export function resolveWindowsCmdExePath(env?: NodeJS.ProcessEnv): string;
export function resolveWindowsPowerShellPath(env?: NodeJS.ProcessEnv): string;
export function buildCmdExeCommandLine(command: string, args: string[]): string;
+67
View File
@@ -1,5 +1,40 @@
// Windows cmd.exe quoting helpers for npm/pnpm command shims.
import path from "node:path";
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/;
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
function getEnvValueCaseInsensitive(env, expectedKey) {
const direct = env[expectedKey];
if (direct !== undefined) {
return direct;
}
const expected = expectedKey.toUpperCase();
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === expected);
return actualKey ? env[actualKey] : undefined;
}
function normalizeWindowsSystemRoot(raw) {
const trimmed = raw?.trim();
if (
!trimmed ||
trimmed.includes("\0") ||
trimmed.includes("\r") ||
trimmed.includes("\n") ||
trimmed.includes(";")
) {
return null;
}
const normalized = path.win32.normalize(trimmed);
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
return null;
}
const parsed = path.win32.parse(normalized);
if (!/^[A-Za-z]:\\$/.test(parsed.root) || normalized.length <= parsed.root.length) {
return null;
}
return normalized.replace(/[\\/]+$/, "");
}
/**
* Resolves the correctly cased PATH key in a Windows-style env object.
@@ -8,6 +43,38 @@ export function resolvePathEnvKey(env) {
return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
}
export function resolveWindowsSystemRoot(env = process.env) {
return (
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
DEFAULT_WINDOWS_SYSTEM_ROOT
);
}
export function resolveWindowsSystem32Path(executableName, env = process.env) {
if (
path.win32.basename(executableName) !== executableName ||
!/^[A-Za-z0-9_.-]+\.exe$/u.test(executableName)
) {
throw new Error(`Invalid Windows System32 executable name: ${executableName}`);
}
return path.win32.join(resolveWindowsSystemRoot(env), "System32", executableName);
}
export function resolveWindowsCmdExePath(env = process.env) {
return resolveWindowsSystem32Path("cmd.exe", env);
}
export function resolveWindowsPowerShellPath(env = process.env) {
return path.win32.join(
resolveWindowsSystemRoot(env),
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
);
}
function escapeForCmdExe(arg) {
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) {
throw new Error(`unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}`);
+1 -6
View File
@@ -53,11 +53,6 @@ type RunZaiFallbackReproDeps = {
writeFile?: typeof fs.writeFile;
};
function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
return key === undefined ? undefined : env[key];
}
export function appendBoundedReproOutput(
capture: OutputCapture,
chunk: unknown,
@@ -124,7 +119,7 @@ export function resolveZaiFallbackPnpmCommand(
): PnpmCommand {
const env = options.env ?? process.env;
const command = resolvePnpmRunner({
comSpec: options.comSpec ?? resolveEnvValue(env, "ComSpec"),
comSpec: options.comSpec,
env,
npmExecPath: options.npmExecPath ?? env.npm_execpath,
nodeExecPath: options.execPath ?? process.execPath,
+18
View File
@@ -129,6 +129,24 @@ describe("resolveNpmRunner", () => {
});
});
it("ignores ambient ComSpec when wrapping an adjacent npm.cmd on Windows", () => {
const execPath = "C:\\nodejs\\node.exe";
const npmCmdPath = path.win32.resolve(path.win32.dirname(execPath), "npm.cmd");
const runner = resolveNpmRunner({
env: {
ComSpec: "C:\\Users\\test\\bin\\cmd.exe",
SystemRoot: "D:\\Windows",
},
execPath,
existsSync: (candidate) => candidate === npmCmdPath,
npmArgs: ["install"],
platform: "win32",
});
expect(runner.command).toBe("D:\\Windows\\System32\\cmd.exe");
});
it("prefixes PATH with the active node dir when falling back to bare npm", () => {
expect(
resolveNpmRunner({
@@ -1161,6 +1161,32 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
});
});
it("does not trust ambient ComSpec when wrapping Windows cmd shims", () => {
const originalComSpec = process.env.ComSpec;
const originalSystemRoot = process.env.SystemRoot;
try {
process.env.ComSpec = String.raw`C:\Users\test\bin\cmd.exe`;
process.env.SystemRoot = String.raw`D:\Windows`;
expect(
resolveCommandSpawnInvocation(String.raw`C:\Program Files\nodejs\npm.cmd`, ["--version"], {
platform: "win32",
}).command,
).toBe(String.raw`D:\Windows\System32\cmd.exe`);
} finally {
if (originalComSpec === undefined) {
delete process.env.ComSpec;
} else {
process.env.ComSpec = originalComSpec;
}
if (originalSystemRoot === undefined) {
delete process.env.SystemRoot;
} else {
process.env.SystemRoot = originalSystemRoot;
}
}
});
it("wraps installed Windows CLI cmd fallbacks without Node shell argv", () => {
expect(
resolveInstalledCliInvocation(
+15 -2
View File
@@ -1293,8 +1293,9 @@ if (isPrlctl) {
expect(windowsGit.indexOf('"MinGit-2.53.0.2-64-bit.zip"')).toBeLessThan(
windowsGit.indexOf('"MinGit-2.53.0.2-arm64.zip"'),
);
expect(combined.match(/curl\.exe -fsSL --connect-timeout 10 --max-time 120 --retry 2/g))
.toHaveLength(2);
expect(
combined.match(/curl\.exe -fsSL --connect-timeout 10 --max-time 120 --retry 2/g),
).toHaveLength(2);
expect(script).toContain("Invoke-RestMethod -Uri");
expect(script).toContain("-TimeoutSec 120");
expect(windowsGit).toContain('if "-64-bit." in name:');
@@ -1907,6 +1908,18 @@ setInterval(() => {}, 1000);
});
});
it("ignores ambient ComSpec for Windows host batch commands", () => {
expect(
resolveHostCommandInvocation("C:\\Tools\\helper.cmd", ["build"], {
env: {
ComSpec: "C:\\Users\\test\\bin\\cmd.exe",
SystemRoot: "D:\\Windows",
},
platform: "win32",
}).command,
).toBe("D:\\Windows\\System32\\cmd.exe");
});
it("runs the Windows agent turn through the detached done-file runner", () => {
const script = readFileSync(TS_PATHS.windows, "utf8");
+20
View File
@@ -328,6 +328,26 @@ describe("resolvePnpmRunner", () => {
});
});
it("ignores ambient ComSpec when defaulting the Windows cmd shim launcher", () => {
expect(
resolvePnpmRunner({
env: {
ComSpec: "C:\\Users\\test\\bin\\cmd.exe",
PATH: "",
SystemRoot: "D:\\Windows",
},
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: "D:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd exec vitest run"],
shell: false,
windowsVerbatimArguments: true,
});
});
it("uses Corepack on Windows when no pnpm shim is available", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-corepack-"));
const corepackPath = path.join(tempDir, "corepack.cmd");
+14
View File
@@ -111,6 +111,20 @@ describe("scripts/ui windows spawn behavior", () => {
).toThrow(/unsafe windows cmd\.exe argument/i);
});
it("uses a trusted cmd.exe path when no explicit Windows launcher is injected", () => {
expect(
resolveSpawnCall(
"C:\\tools\\pnpm.cmd",
["run", "build"],
{
ComSpec: "C:\\Users\\test\\bin\\cmd.exe",
SystemRoot: "D:\\Windows",
},
{ cwd: "C:\\repo\\ui", platform: "win32" },
).command,
).toBe("D:\\Windows\\System32\\cmd.exe");
});
it("routes Windows Corepack pnpm entrypoints through node", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pnpm-runner-"));
const npmExecPath = path.join(tempDir, "pnpm.mjs");