refactor: make executable resolution async and centralize spawn

- Rename findExecutable → findExecutableSync, add async findExecutable
  that uses promisified execFile instead of execFileSync
- Simplify Windows PATH resolution: rely on inherited env from Explorer
  instead of shelling out to PowerShell for registry PATH entries
- Add spawnProcess() helper that centralizes Windows shell/quoting concerns
- Update all agent providers and consumers to use async executable resolution
- Add windowsHide: true across all Windows child process calls
This commit is contained in:
Mohamed Boudra
2026-04-08 23:24:43 +07:00
parent fc667d6312
commit dace4f862f
25 changed files with 449 additions and 522 deletions
+9 -8
View File
@@ -1,4 +1,5 @@
import { exec, execFile, spawn } from "child_process";
import { exec, execFile } from "child_process";
import { spawnProcess } from "./spawn.js";
import { promisify } from "util";
import { resolve, dirname, basename } from "path";
import { realpathSync } from "fs";
@@ -87,7 +88,7 @@ async function spawnLimitedText(params: {
const accept = new Set(params.acceptExitCodes ?? [0]);
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(params.cmd, params.args, {
const child = spawnProcess(params.cmd, params.args, {
cwd: params.cwd,
env: params.env,
stdio: ["ignore", "pipe", "pipe"],
@@ -106,7 +107,7 @@ async function spawnLimitedText(params: {
}
};
child.stdout.on("data", (chunk: Buffer) => {
child.stdout!.on("data", (chunk: Buffer) => {
if (truncated) return;
stdoutBytes += chunk.length;
if (stdoutBytes > params.maxBytes) {
@@ -119,7 +120,7 @@ async function spawnLimitedText(params: {
// We don't buffer stderr (it can be large too). Keep it minimal for debugging.
let stderrPreview = "";
child.stderr.on("data", (chunk: Buffer) => {
child.stderr!.on("data", (chunk: Buffer) => {
if (stderrPreview.length > 2048) return;
stderrPreview += chunk.toString("utf8");
});
@@ -1783,9 +1784,9 @@ export interface PullRequestStatusResult {
githubFeaturesEnabled: boolean;
}
function resolveGhPath(): string {
async function resolveGhPath(): Promise<string> {
if (cachedGhPath === undefined) {
cachedGhPath = findExecutable("gh");
cachedGhPath = await findExecutable("gh");
}
if (cachedGhPath === null) {
throw new Error("GitHub CLI (gh) is not installed or not in PATH");
@@ -1859,7 +1860,7 @@ export async function createPullRequest(
options: CreatePullRequestOptions,
): Promise<{ url: string; number: number }> {
await requireGitRepo(cwd);
const ghPath = resolveGhPath();
const ghPath = await resolveGhPath();
const repo = await resolveGitHubRepo(cwd);
if (!repo) {
throw new Error("Unable to determine GitHub repo from git remote");
@@ -1932,7 +1933,7 @@ async function getPullRequestStatusUncached(cwd: string): Promise<PullRequestSta
}
let ghPath: string;
try {
ghPath = resolveGhPath();
ghPath = await resolveGhPath();
} catch {
return {
status: null,