fix(windows): quote shell args with spaces
This commit is contained in:
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
quoteWindowsCommand,
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
@@ -255,3 +256,39 @@ describe("quoteWindowsCommand", () => {
|
|||||||
expect(quoteWindowsCommand("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code");
|
expect(quoteWindowsCommand("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("quoteWindowsArgument", () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
function setPlatform(value: string) {
|
||||||
|
Object.defineProperty(process, "platform", { value, writable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setPlatform(originalPlatform);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quotes a Windows argument with spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument("C:\\Program Files\\Anthropic\\cli.js")).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\cli.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not double-quote an already-quoted argument", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument('"C:\\Program Files\\Anthropic\\cli.js"')).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\cli.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the argument unchanged when there are no spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument("--version")).toBe("--version");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the argument unchanged on non-Windows platforms", () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
expect(quoteWindowsArgument("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -286,6 +286,18 @@ export function quoteWindowsCommand(command: string): string {
|
|||||||
return `"${command}"`;
|
return `"${command}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `spawn(..., { shell: true })` on Windows also passes argv through `cmd.exe`.
|
||||||
|
* Any argument containing spaces must be quoted or it will be split before the
|
||||||
|
* child process sees it.
|
||||||
|
*/
|
||||||
|
export function quoteWindowsArgument(argument: string): string {
|
||||||
|
if (process.platform !== "win32") return argument;
|
||||||
|
if (!argument.includes(" ")) return argument;
|
||||||
|
if (argument.startsWith('"') && argument.endsWith('"')) return argument;
|
||||||
|
return `"${argument}"`;
|
||||||
|
}
|
||||||
|
|
||||||
export function isCommandAvailable(command: string): boolean {
|
export function isCommandAvailable(command: string): boolean {
|
||||||
return findExecutable(command) !== null;
|
return findExecutable(command) !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
quoteWindowsCommand,
|
quoteWindowsCommand,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
@@ -214,7 +215,10 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
const isDefaultRuntime =
|
const isDefaultRuntime =
|
||||||
resolved.command === "node" || resolved.command === "bun";
|
resolved.command === "node" || resolved.command === "bun";
|
||||||
const command = isDefaultRuntime ? process.execPath : resolved.command;
|
const command = isDefaultRuntime ? process.execPath : resolved.command;
|
||||||
const child = spawn(quoteWindowsCommand(command), resolved.args, {
|
const child = spawn(
|
||||||
|
quoteWindowsCommand(command),
|
||||||
|
resolved.args.map((argument) => quoteWindowsArgument(argument)),
|
||||||
|
{
|
||||||
cwd: spawnOptions.cwd,
|
cwd: spawnOptions.cwd,
|
||||||
env: {
|
env: {
|
||||||
...applyProviderEnv(spawnOptions.env, runtimeSettings),
|
...applyProviderEnv(spawnOptions.env, runtimeSettings),
|
||||||
@@ -223,7 +227,8 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
signal: spawnOptions.signal,
|
signal: spawnOptions.signal,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (typeof options.stderr === "function") {
|
if (typeof options.stderr === "function") {
|
||||||
child.stderr?.on("data", (chunk: Buffer | string) => {
|
child.stderr?.on("data", (chunk: Buffer | string) => {
|
||||||
options.stderr?.(chunk.toString());
|
options.stderr?.(chunk.toString());
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
quoteWindowsCommand,
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
@@ -3420,12 +3421,16 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
},
|
},
|
||||||
"Spawning Codex app server",
|
"Spawning Codex app server",
|
||||||
);
|
);
|
||||||
return spawn(quoteWindowsCommand(launchPrefix.command), [...launchPrefix.args, "app-server"], {
|
return spawn(
|
||||||
detached: process.platform !== "win32",
|
quoteWindowsCommand(launchPrefix.command),
|
||||||
shell: process.platform === "win32",
|
[...launchPrefix.args, "app-server"].map((argument) => quoteWindowsArgument(argument)),
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
{
|
||||||
env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv),
|
detached: process.platform !== "win32",
|
||||||
});
|
shell: process.platform === "win32",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
quoteWindowsCommand,
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
@@ -335,7 +336,9 @@ export class OpenCodeServerManager {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.server = spawn(
|
this.server = spawn(
|
||||||
quoteWindowsCommand(launchPrefix.command),
|
quoteWindowsCommand(launchPrefix.command),
|
||||||
[...launchPrefix.args, "serve", "--port", String(this.port)],
|
[...launchPrefix.args, "serve", "--port", String(this.port)].map((argument) =>
|
||||||
|
quoteWindowsArgument(argument),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
findExecutable,
|
findExecutable,
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
|
quoteWindowsArgument,
|
||||||
quoteWindowsCommand,
|
quoteWindowsCommand,
|
||||||
} from "./agent/provider-launch-config.js";
|
} from "./agent/provider-launch-config.js";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user