mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(scripts): resolve Windows cmd shim launcher
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user