





























@@ -8,6 +8,9 @@ import { createInterface } from "node:readline";
88import { fileURLToPath, pathToFileURL } from "node:url";
99import * as ts from "typescript";
1010import { formatErrorMessage } from "../src/infra/errors.ts";
11+import { resolveNpmRunner } from "./npm-runner.mjs";
12+import { resolvePnpmRunner } from "./pnpm-runner.mjs";
13+import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
11141215interface TranslationMap {
1316[key: string]: string | TranslationMap;
@@ -920,6 +923,128 @@ type PiCommand = {
920923executable: string;
921924};
922925926+type ProcessCommand = {
927+args: string[];
928+env?: NodeJS.ProcessEnv;
929+executable: string;
930+shell: boolean;
931+windowsVerbatimArguments?: boolean;
932+};
933+934+type ResolveProcessCommandOptions = {
935+comSpec?: string;
936+env?: NodeJS.ProcessEnv;
937+execPath?: string;
938+existsSync?: (path: string) => boolean;
939+npmExecPath?: string;
940+platform?: NodeJS.Platform;
941+};
942+943+function portableExtension(value: string): string {
944+return path.posix.extname(value.split(/[/\\]/u).at(-1) ?? value).toLowerCase();
945+}
946+947+function isWindowsCommandShim(value: string, platform = process.platform): boolean {
948+const extension = portableExtension(value);
949+return platform === "win32" && (extension === ".cmd" || extension === ".bat");
950+}
951+952+function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
953+const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
954+return key === undefined ? undefined : env[key];
955+}
956+957+function commandFromRunner(runner: {
958+args: string[];
959+command: string;
960+env?: NodeJS.ProcessEnv;
961+shell: boolean;
962+windowsVerbatimArguments?: boolean;
963+}): ProcessCommand {
964+const command: ProcessCommand = {
965+args: runner.args,
966+executable: runner.command,
967+shell: runner.shell,
968+windowsVerbatimArguments: runner.windowsVerbatimArguments,
969+};
970+if (runner.env !== undefined) {
971+command.env = runner.env;
972+}
973+return command;
974+}
975+976+export function resolveControlUiI18nProcessCommand(
977+executable: string,
978+args: string[],
979+options: ResolveProcessCommandOptions = {},
980+): ProcessCommand {
981+const env = options.env ?? process.env;
982+const platform = options.platform ?? process.platform;
983+const comSpec = options.comSpec ?? resolveEnvValue(env, "ComSpec") ?? "cmd.exe";
984+if (isWindowsCommandShim(executable, platform)) {
985+return {
986+args: ["/d", "/s", "/c", buildCmdExeCommandLine(executable, args)],
987+executable: comSpec,
988+shell: false,
989+windowsVerbatimArguments: true,
990+};
991+}
992+return { args, executable, shell: false };
993+}
994+995+export function resolveControlUiI18nNpmInstallCommand(
996+packageSpec: string,
997+options: ResolveProcessCommandOptions = {},
998+): ProcessCommand {
999+return commandFromRunner(
1000+resolveNpmRunner({
1001+comSpec: options.comSpec,
1002+env: options.env,
1003+execPath: options.execPath,
1004+existsSync: options.existsSync,
1005+npmArgs: ["install", "--silent", "--no-audit", "--no-fund", packageSpec],
1006+platform: options.platform,
1007+}),
1008+);
1009+}
1010+1011+export function resolveControlUiI18nPnpmCommand(
1012+args: string[],
1013+options: ResolveProcessCommandOptions = {},
1014+): ProcessCommand {
1015+return commandFromRunner(
1016+resolvePnpmRunner({
1017+comSpec: options.comSpec,
1018+npmExecPath: options.npmExecPath ?? process.env.npm_execpath,
1019+nodeExecPath: options.execPath ?? process.execPath,
1020+platform: options.platform,
1021+pnpmArgs: args,
1022+}),
1023+);
1024+}
1025+1026+export function resolvePiShimNodeCommand(
1027+shimPath: string,
1028+options: Pick<ResolveProcessCommandOptions, "existsSync" | "platform"> = {},
1029+): PiCommand | null {
1030+const platform = options.platform ?? process.platform;
1031+if (!isWindowsCommandShim(shimPath, platform)) {
1032+return null;
1033+}
1034+const cliPath = path.win32.join(
1035+path.win32.dirname(shimPath),
1036+"node_modules",
1037+ ...PI_PACKAGE_NAME.split("/"),
1038+"dist",
1039+"cli.js",
1040+);
1041+const exists = options.existsSync ?? existsSync;
1042+if (!exists(cliPath)) {
1043+return null;
1044+}
1045+return { executable: "node", args: [cliPath] };
1046+}
1047+9231048function resolvePiPackageVersion(): string {
9241049return process.env[ENV_PI_PACKAGE_VERSION]?.trim() || DEFAULT_PI_PACKAGE_VERSION;
9251050}
@@ -946,16 +1071,36 @@ export function resolveLocalPiCommand(root = ROOT): PiCommand | null {
9461071async function resolvePiCommand(): Promise<PiCommand> {
9471072const explicitExecutable = process.env[ENV_PI_EXECUTABLE]?.trim();
9481073if (explicitExecutable) {
1074+const explicitArgs = process.env[ENV_PI_ARGS]?.trim().split(/\s+/).filter(Boolean) ?? [];
1075+const shimCommand = resolvePiShimNodeCommand(explicitExecutable);
1076+if (shimCommand) {
1077+return {
1078+executable: shimCommand.executable,
1079+args: [...shimCommand.args, ...explicitArgs],
1080+};
1081+}
1082+if (isWindowsCommandShim(explicitExecutable)) {
1083+throw new Error(
1084+`${ENV_PI_EXECUTABLE} points to a Windows command shim that cannot safely carry the multiline i18n system prompt. Point it at node with ${ENV_PI_ARGS} set to the Pi package dist/cli.js path, or unset it so OpenClaw uses the managed Pi runtime.`,
1085+);
1086+}
9491087return {
9501088executable: explicitExecutable,
951-args: process.env[ENV_PI_ARGS]?.trim().split(/\s+/).filter(Boolean) ?? [],
1089+args: explicitArgs,
9521090};
9531091}
95410929551093const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
9561094for (const entry of pathEntries) {
9571095const candidate = path.join(entry, process.platform === "win32" ? "pi.cmd" : "pi");
9581096if (existsSync(candidate)) {
1097+const shimCommand = resolvePiShimNodeCommand(candidate);
1098+if (shimCommand) {
1099+return shimCommand;
1100+}
1101+if (process.platform === "win32") {
1102+continue;
1103+}
9591104return { executable: candidate, args: [] };
9601105}
9611106}
@@ -975,15 +1120,8 @@ async function resolvePiCommand(): Promise<PiCommand> {
9751120);
9761121if (!existsSync(cliPath)) {
9771122await mkdir(runtimeDir, { recursive: true });
978-await runProcess(
979-"npm",
980-[
981-"install",
982-"--silent",
983-"--no-audit",
984-"--no-fund",
985-`${PI_PACKAGE_NAME}@${resolvePiPackageVersion()}`,
986-],
1123+await runProcessCommand(
1124+resolveControlUiI18nNpmInstallCommand(`${PI_PACKAGE_NAME}@${resolvePiPackageVersion()}`),
9871125{
9881126cwd: runtimeDir,
9891127rejectOnFailure: true,
@@ -999,16 +1137,17 @@ type RunProcessOptions = {
9991137rejectOnFailure?: boolean;
10001138};
100111391002-async function runProcess(
1003-executable: string,
1004-args: string[],
1140+async function runProcessCommand(
1141+command: ProcessCommand,
10051142options: RunProcessOptions = {},
10061143): Promise<{ code: number; stderr: string; stdout: string }> {
10071144return await new Promise((resolve, reject) => {
1008-const child = spawn(executable, args, {
1145+const child = spawn(command.executable, command.args, {
10091146cwd: options.cwd ?? ROOT,
1010-env: process.env,
1147+env: command.env ?? process.env,
1148+shell: command.shell,
10111149stdio: ["pipe", "pipe", "pipe"],
1150+windowsVerbatimArguments: command.windowsVerbatimArguments,
10121151});
1013115210141153let stdout = "";
@@ -1028,7 +1167,11 @@ async function runProcess(
10281167child.once("close", (code) => {
10291168if ((code ?? 1) !== 0 && options.rejectOnFailure) {
10301169reject(
1031-new Error(`${executable} ${args.join(" ")} failed: ${stderr.trim() || stdout.trim()}`),
1170+new Error(
1171+`${command.executable} ${command.args.join(" ")} failed: ${
1172+ stderr.trim() || stdout.trim()
1173+ }`,
1174+),
10321175);
10331176return;
10341177}
@@ -1038,9 +1181,10 @@ async function runProcess(
10381181}
1039118210401183async function formatGeneratedTypeScript(filePath: string, source: string): Promise<string> {
1041-const result = await runProcess(
1042-"pnpm",
1043-["exec", "oxfmt", "--stdin-filepath", path.relative(ROOT, filePath)],
1184+const result = await runProcessCommand(
1185+resolveControlUiI18nPnpmCommand(
1186+["exec", "oxfmt", "--stdin-filepath", path.relative(ROOT, filePath)],
1187+),
10441188{
10451189input: source,
10461190rejectOnFailure: true,
@@ -1167,10 +1311,13 @@ class PiRpcClient {
11671311"--system-prompt",
11681312systemPrompt,
11691313];
1170-const child = spawn(command.executable, args, {
1314+const invocation = resolveControlUiI18nProcessCommand(command.executable, args);
1315+const child = spawn(invocation.executable, invocation.args, {
11711316cwd: ROOT,
1172-env: process.env,
1317+env: invocation.env ?? process.env,
1318+shell: invocation.shell,
11731319stdio: ["pipe", "pipe", "pipe"],
1320+windowsVerbatimArguments: invocation.windowsVerbatimArguments,
11741321});
1175132211761323const client = new PiRpcClient(child);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。