fix: use PATH-aware resolution for provider availability and fix Windows cmd escaping

Provider isAvailable() was using executableExists() which only checks
filesystem paths, not PATH. Commands like ["claude", "--flag"] would
show as unavailable even though they'd launch fine. Switch to
isCommandAvailable() which uses findExecutable() for proper PATH
resolution. Un-export executableExists from the public API.

Fix Windows cmd.exe metacharacter escaping — &, |, ^, <, >, (, ), !
are now properly escaped with ^, and % is doubled. Add shared
escapeWindowsCmdValue helper used by both quoteWindowsCommand and
quoteWindowsArgument. Replace local quoteForCmd in spawn.ts.

Add server-tests-windows CI job to catch Windows-specific regressions.
This commit is contained in:
Mohamed Boudra
2026-04-13 20:14:33 +07:00
parent 6f28027687
commit 95e112c185
9 changed files with 138 additions and 25 deletions
+6 -10
View File
@@ -1,6 +1,8 @@
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { promisify } from "node:util";
import { quoteWindowsArgument, quoteWindowsCommand } from "./executable.js";
const execFileAsync = promisify(execFile);
interface ExecCommandOptions {
@@ -16,12 +18,6 @@ interface ExecCommandResult {
stderr: string;
}
function quoteForCmd(value: string): string {
if (!value.includes(" ")) return value;
if (value.startsWith('"') && value.endsWith('"')) return value;
return `"${value}"`;
}
export function spawnProcess(
command: string,
args: string[],
@@ -29,8 +25,8 @@ export function spawnProcess(
): ChildProcess {
const isWindows = process.platform === "win32";
const resolvedCommand = isWindows ? quoteForCmd(command) : command;
const resolvedArgs = isWindows ? args.map(quoteForCmd) : args;
const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command;
const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args;
return spawn(resolvedCommand, resolvedArgs, {
...options,
@@ -45,8 +41,8 @@ export async function execCommand(
options?: ExecCommandOptions,
): Promise<ExecCommandResult> {
const isWindows = process.platform === "win32";
const resolvedCommand = isWindows ? quoteForCmd(command) : command;
const resolvedArgs = isWindows ? args.map(quoteForCmd) : args;
const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command;
const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args;
return execFileAsync(resolvedCommand, resolvedArgs, {
cwd: options?.cwd,