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
+16 -8
View File
@@ -105,6 +105,20 @@ export async function isCommandAvailable(command: string): Promise<boolean> {
return (await findExecutable(command)) !== null;
}
function escapeWindowsCmdValue(value: string): string {
if (process.platform !== "win32") return value;
const isQuoted = value.startsWith('"') && value.endsWith('"');
const unquoted = isQuoted ? value.slice(1, -1) : value;
const escaped = unquoted.replace(/%/g, "%%").replace(/([&|^<>()!])/g, "^$1");
if (isQuoted || escaped.includes(" ")) {
return `"${escaped}"`;
}
return escaped;
}
/**
* 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
@@ -112,10 +126,7 @@ export async function isCommandAvailable(command: string): Promise<boolean> {
* 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}"`;
return escapeWindowsCmdValue(command);
}
/**
@@ -124,8 +135,5 @@ export function quoteWindowsCommand(command: string): string {
* 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}"`;
return escapeWindowsCmdValue(argument);
}