diff --git a/CHANGELOG.md b/CHANGELOG.md index d24610311a1..520784d029f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ Docs: https://docs.openclaw.ai - Release/Windows: run release-check npm pack/install/root probes through the shared npm runner so native Windows avoids bare `npm` lookup and `.cmd` shell-argv handling. - Release/Windows: run cross-OS release check `.cmd` shims through explicit `cmd.exe` wrapping so native Windows install and gateway probes avoid Node shell-argv handling. - Control UI/Windows: run i18n Pi, npm, and pnpm helper commands through explicit Windows runners so native Windows translation sync avoids brittle `.cmd` launches. +- Scripts/Windows: run the Z.AI fallback repro through the shared pnpm runner so native Windows avoids raw `.cmd` launches. - Plugins/Windows: run plugin npm package staging through the shared npm runner so native Windows release checks avoid bare `npm` lookup and `.cmd` shell-argv handling. - Checks/Windows: route full `pnpm check` stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too. - Agents/fs: allow workspace-only host write/edit tools to write through in-workspace symlink directory parents while preserving outside-workspace symlink rejection. Fixes #84696. Thanks @garbagenetwork. diff --git a/scripts/zai-fallback-repro.ts b/scripts/zai-fallback-repro.ts index e0279f0c494..0585764e173 100644 --- a/scripts/zai-fallback-repro.ts +++ b/scripts/zai-fallback-repro.ts @@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { resolvePnpmRunner } from "./pnpm-runner.mjs"; type RunResult = { code: number | null; @@ -11,6 +13,47 @@ type RunResult = { stderr: string; }; +type PnpmCommand = { + args: string[]; + command: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + windowsVerbatimArguments?: boolean; +}; + +type ResolvePnpmCommandOptions = { + comSpec?: string; + env?: NodeJS.ProcessEnv; + execPath?: string; + npmExecPath?: string; + 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 resolveZaiFallbackPnpmCommand( + args: string[], + options: ResolvePnpmCommandOptions = {}, +): PnpmCommand { + const env = options.env ?? process.env; + const command = resolvePnpmRunner({ + comSpec: options.comSpec ?? resolveEnvValue(env, "ComSpec"), + npmExecPath: options.npmExecPath ?? env.npm_execpath, + nodeExecPath: options.execPath ?? process.execPath, + platform: options.platform, + pnpmArgs: args, + }); + if (command.env === undefined) { + const invocation = { ...command }; + delete invocation.env; + return invocation; + } + return command; +} + function pickAnthropicEnv(): { type: "oauth" | "api"; value: string } | null { const oauth = process.env.ANTHROPIC_OAUTH_TOKEN?.trim(); if (oauth) { @@ -33,9 +76,12 @@ async function runCommand( env: NodeJS.ProcessEnv, ): Promise { return await new Promise((resolve, reject) => { - const child = spawn("pnpm", args, { - env, + const command = resolveZaiFallbackPnpmCommand(args, { env }); + const child = spawn(command.command, command.args, { + env: command.env ?? env, + shell: command.shell, stdio: ["ignore", "pipe", "pipe"], + windowsVerbatimArguments: command.windowsVerbatimArguments, }); let stdout = ""; let stderr = ""; @@ -157,7 +203,14 @@ async function main() { process.exit(run2.code ?? 1); } -main().catch((err) => { - console.error(err); - process.exit(1); -}); +function isCliEntrypoint() { + const entrypoint = process.argv[1]; + return Boolean(entrypoint && import.meta.url === pathToFileURL(path.resolve(entrypoint)).href); +} + +if (isCliEntrypoint()) { + await main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/test/scripts/zai-fallback-repro.test.ts b/test/scripts/zai-fallback-repro.test.ts new file mode 100644 index 00000000000..c56c8d7a010 --- /dev/null +++ b/test/scripts/zai-fallback-repro.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { resolveZaiFallbackPnpmCommand } from "../../scripts/zai-fallback-repro.ts"; + +describe("zai fallback repro command resolution", () => { + it("wraps Windows pnpm.cmd without Node shell argv", () => { + expect( + resolveZaiFallbackPnpmCommand( + ["openclaw", "agent", "--message", "hello world"], + { + comSpec: String.raw`C:\Windows\System32\cmd.exe`, + npmExecPath: String.raw`C:\Program Files\nodejs\pnpm.cmd`, + platform: "win32", + }, + ), + ).toEqual({ + args: [ + "/d", + "/s", + "/c", + String.raw`""C:\Program Files\nodejs\pnpm.cmd" openclaw agent --message "hello world""`, + ], + command: String.raw`C:\Windows\System32\cmd.exe`, + shell: false, + windowsVerbatimArguments: true, + }); + }); +});