fix: centralize Windows exec handling and async migrations (#318)

* Add Windows exec foundation helpers

* server: centralize windows exec handling phase 2

* migrate cli daemon status to execCommand

* refactor: complete phase 4 async exec migrations

* test: cover windows exec helpers

* fix: use beforeEach for context.skip in test setup
This commit is contained in:
Mohamed Boudra
2026-04-13 16:20:59 +08:00
committed by GitHub
parent f0c59ec534
commit bf76915884
44 changed files with 3048 additions and 2781 deletions
+57 -19
View File
@@ -1,15 +1,27 @@
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
interface ExecCommandOptions {
cwd?: string;
env?: NodeJS.ProcessEnv;
encoding?: BufferEncoding;
timeout?: number;
maxBuffer?: number;
}
interface ExecCommandResult {
stdout: string;
stderr: string;
}
function quoteForCmd(value: string): string {
if (!value.includes(" ")) return value;
if (value.startsWith('"') && value.endsWith('"')) return value;
return `"${value}"`;
}
/**
* Platform-aware spawn that centralizes Windows shell and quoting concerns.
*
* On Windows:
* - Enables `shell: true` (routes through cmd.exe) unless the caller explicitly sets `shell`
* - Quotes the command and arguments so paths with spaces survive cmd.exe parsing
* - Always sets `windowsHide: true` to prevent console window flashes
*
* On other platforms the call is passed through to `spawn` unchanged (with `windowsHide: true`).
*/
export function spawnProcess(
command: string,
args: string[],
@@ -27,12 +39,38 @@ export function spawnProcess(
});
}
/**
* Quote a string for cmd.exe if it contains spaces and isn't already quoted.
* No-op for strings without spaces or strings that are already double-quoted.
*/
function quoteForCmd(value: string): string {
if (!value.includes(" ")) return value;
if (value.startsWith('"') && value.endsWith('"')) return value;
return `"${value}"`;
export async function execCommand(
command: string,
args: string[],
options?: ExecCommandOptions,
): Promise<ExecCommandResult> {
const isWindows = process.platform === "win32";
const resolvedCommand = isWindows ? quoteForCmd(command) : command;
const resolvedArgs = isWindows ? args.map(quoteForCmd) : args;
return execFileAsync(resolvedCommand, resolvedArgs, {
cwd: options?.cwd,
env: options?.env,
encoding: options?.encoding ?? "utf8",
timeout: options?.timeout,
maxBuffer: options?.maxBuffer,
shell: isWindows,
windowsHide: true,
}) as Promise<ExecCommandResult>;
}
export function platformShell(): { command: string; flag: string[] } {
if (process.platform === "win32") {
return { command: "cmd.exe", flag: ["/c"] };
}
return { command: "/bin/sh", flag: ["-lc"] };
}
export function platformBash(): { command: string; flag: string[] } {
if (process.platform === "win32") {
return { command: "cmd.exe", flag: ["/c"] };
}
return { command: "/bin/bash", flag: ["-lc"] };
}