diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 132fe4876..d37194100 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,7 +1,12 @@ import type { Command } from "commander"; import { execFileSync } from "node:child_process"; import { createRequire } from "node:module"; -import { getOrCreateServerId, findExecutable, applyProviderEnv } from "@getpaseo/server"; +import { + getOrCreateServerId, + findExecutable, + quoteWindowsCommand, + applyProviderEnv, +} from "@getpaseo/server"; import { tryConnectToDaemon } from "../../utils/client.js"; import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js"; import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js"; @@ -170,7 +175,7 @@ function checkProviderBinary(binary: string): { path: string | null; version: st } const env = applyProviderEnv(process.env); try { - const output = execFileSync(binaryPath, ["--version"], { + const output = execFileSync(quoteWindowsCommand(binaryPath), ["--version"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index 6ce4ca983..962313963 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -1,7 +1,8 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { findExecutable, + quoteWindowsCommand, resolveProviderCommandPrefix, applyProviderEnv, type ProviderRuntimeSettings, @@ -218,3 +219,39 @@ describe("findExecutable", () => { expect(findExecutableDependencies.existsSync).toHaveBeenCalledWith("/usr/local/bin/codex"); }); }); + +describe("quoteWindowsCommand", () => { + const originalPlatform = process.platform; + + function setPlatform(value: string) { + Object.defineProperty(process, "platform", { value, writable: true }); + } + + afterEach(() => { + setPlatform(originalPlatform); + }); + + test("quotes a Windows path with spaces", () => { + setPlatform("win32"); + expect(quoteWindowsCommand("C:\\Program Files\\Anthropic\\claude.exe")).toBe( + '"C:\\Program Files\\Anthropic\\claude.exe"', + ); + }); + + test("does not double-quote an already-quoted path", () => { + setPlatform("win32"); + expect(quoteWindowsCommand('"C:\\Program Files\\Anthropic\\claude.exe"')).toBe( + '"C:\\Program Files\\Anthropic\\claude.exe"', + ); + }); + + test("returns the command unchanged when there are no spaces", () => { + setPlatform("win32"); + expect(quoteWindowsCommand("C:\\nvm4w\\nodejs\\codex")).toBe("C:\\nvm4w\\nodejs\\codex"); + }); + + test("returns the command unchanged on non-Windows platforms", () => { + setPlatform("darwin"); + expect(quoteWindowsCommand("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code"); + }); +}); diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index 267c3bd8d..ba5260082 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -273,6 +273,19 @@ export function findExecutable( } } +/** + * When spawning with `shell: true` on Windows, the command is passed to + * `cmd.exe /d /s /c "command args"`. The `/s` strips outer quotes, so a + * command path with spaces (e.g. `C:\Program Files\...`) is split at the + * space. Wrapping it in quotes produces the correct `"C:\Program Files\..." args`. + */ +export function quoteWindowsCommand(command: string): string { + if (process.platform !== "win32") return command; + if (!command.includes(" ")) return command; + if (command.startsWith('"') && command.endsWith('"')) return command; + return `"${command}"`; +} + export function isCommandAvailable(command: string): boolean { return findExecutable(command) !== null; } diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 3b05dc5db..00352f65d 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -70,6 +70,7 @@ import type { import { applyProviderEnv, findExecutable, + quoteWindowsCommand, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js"; @@ -213,7 +214,7 @@ function applyRuntimeSettingsToClaudeOptions( const isDefaultRuntime = resolved.command === "node" || resolved.command === "bun"; const command = isDefaultRuntime ? process.execPath : resolved.command; - const child = spawn(command, resolved.args, { + const child = spawn(quoteWindowsCommand(command), resolved.args, { cwd: spawnOptions.cwd, env: { ...applyProviderEnv(spawnOptions.env, runtimeSettings), diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index ccd83cf8c..dd96e74e0 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -42,6 +42,7 @@ import { import { applyProviderEnv, findExecutable, + quoteWindowsCommand, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -3419,7 +3420,7 @@ export class CodexAppServerAgentClient implements AgentClient { }, "Spawning Codex app server", ); - return spawn(launchPrefix.command, [...launchPrefix.args, "app-server"], { + return spawn(quoteWindowsCommand(launchPrefix.command), [...launchPrefix.args, "app-server"], { detached: process.platform !== "win32", shell: process.platform === "win32", stdio: ["pipe", "pipe", "pipe"], diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index a129675d4..8764b0df4 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -32,6 +32,7 @@ import type { import { applyProviderEnv, findExecutable, + quoteWindowsCommand, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -333,7 +334,7 @@ export class OpenCodeServerManager { return new Promise((resolve, reject) => { this.server = spawn( - launchPrefix.command, + quoteWindowsCommand(launchPrefix.command), [...launchPrefix.args, "serve", "--port", String(this.port)], { shell: process.platform === "win32", diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index a7e2bb8fc..3a4a110ea 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -30,7 +30,11 @@ export { } from "./speech/providers/local/sherpa/sherpa-runtime-env.js"; // Provider binary resolution -export { findExecutable, applyProviderEnv } from "./agent/provider-launch-config.js"; +export { + findExecutable, + applyProviderEnv, + quoteWindowsCommand, +} from "./agent/provider-launch-config.js"; // Agent SDK types for CLI commands export type {