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
+30 -63
View File
@@ -1,15 +1,21 @@
import { execFile, execFileSync } from "node:child_process";
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { platform } from "node:os";
import path from "node:path";
import path, { extname } from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export interface FindExecutableDependencies {
execFileSync: typeof execFileSync;
existsSync: typeof existsSync;
platform: typeof platform;
function pickBestWindowsCandidate(lines: string[]): string | null {
const candidates = lines.filter((line) => line.length > 0);
if (candidates.length === 0) return null;
const extPriority = [".exe", ".cmd", ".ps1"];
for (const ext of extPriority) {
const match = candidates.find((candidate) => candidate.toLowerCase().endsWith(ext));
if (match) return match;
}
return candidates[0] ?? null;
}
function resolveExecutableFromWhichOutput(
@@ -44,58 +50,18 @@ function resolveExecutableFromWhichOutput(
* enriches it at startup via inheritLoginShellEnv(); on Windows, Electron
* inherits the full user environment from Explorer.
*/
export function findExecutableSync(
name: string,
dependencies?: FindExecutableDependencies,
export function executableExists(
executablePath: string,
exists: typeof existsSync = existsSync,
): string | null {
const trimmed = name.trim();
if (!trimmed) {
return null;
}
const deps: FindExecutableDependencies = {
execFileSync,
existsSync,
platform,
...dependencies,
};
if (trimmed.includes("/") || trimmed.includes("\\")) {
return deps.existsSync(trimmed) ? trimmed : null;
}
if (deps.platform() === "win32") {
try {
const out = deps
.execFileSync("where.exe", [trimmed], {
encoding: "utf8",
windowsHide: true,
})
.trim();
return (
out
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0) ?? null
);
} catch {
return null;
if (exists(executablePath)) return executablePath;
if (process.platform === "win32" && !extname(executablePath)) {
for (const ext of [".exe", ".cmd", ".ps1"]) {
const candidate = executablePath + ext;
if (exists(candidate)) return candidate;
}
}
try {
return resolveExecutableFromWhichOutput(
trimmed,
deps.execFileSync("which", [trimmed], { encoding: "utf8" }).trim(),
"which",
);
} catch {
return null;
}
}
export function isCommandAvailableSync(command: string): boolean {
return findExecutableSync(command) !== null;
return null;
}
export async function findExecutable(name: string): Promise<string | null> {
@@ -105,21 +71,22 @@ export async function findExecutable(name: string): Promise<string | null> {
}
if (trimmed.includes("/") || trimmed.includes("\\")) {
return existsSync(trimmed) ? trimmed : null;
return executableExists(trimmed);
}
if (platform() === "win32") {
if (process.platform === "win32") {
try {
const { stdout } = await execFileAsync("where.exe", [trimmed], {
encoding: "utf8",
windowsHide: true,
});
return (
stdout
.trim()
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0) ?? null
pickBestWindowsCandidate(
stdout
.trim()
.split(/\r?\n/)
.map((line) => line.trim()),
) ?? null
);
} catch {
return null;