Files
paseo/packages/server/src/executable-resolution/executable-resolution.ts
T
Mohamed Boudra dd7671be20 Remember model selections across workspace creation (#4401)
* fix(providers): remember model selections across workspace creation

Keep saved model intent independent of catalogue readiness and use the caller-owned host and directory from the first render. Share discovery through provider-owned keys while preserving execution context.

Give each target one committed snapshot, publish actual transitions atomically, and share catalogue content across client directory associations. Regression coverage exercises successful repeated creation and provider lifecycle boundaries.

* fix(providers): preserve shared cache bodies during eviction

Admit directory references with their bodies, retain reference recency during eviction and recovery, and remove the unused persisted index. Refresh known workspace catalogues concurrently.

Make repeated-creation picker states explicit and seed mode tests from the installed provider catalogue, preserving assertions across Codex versions.

* fix(providers): bound discovery independently per provider

Settings refresh repairs all remembered targets, which otherwise starts an unbounded number of discoveries. Admit work after catalogue-key deduplication and keep each provider’s budget independent so a stalled provider cannot block another. Preserve active accounting across configuration changes and settle obsolete queued requests during replacement and shutdown.
2026-09-06 20:29:41 +02:00

147 lines
3.9 KiB
TypeScript

import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import { execCommand } from "../utils/spawn.js";
import { isWindowsCommandScript } from "../utils/windows-command.js";
import { windowsExecutableResolution } from "./windows.js";
export { quoteWindowsArgument, quoteWindowsCommand } from "../utils/windows-command.js";
type Which = (command: string, options: { all: true }) => Promise<string[]>;
const require = createRequire(import.meta.url);
const which = require("which") as Which;
const PROBE_TIMEOUT_MS = 2000;
function hasPathSeparator(value: string): boolean {
return value.includes("/") || value.includes("\\");
}
async function enumerateCandidates(name: string): Promise<string[]> {
if (process.platform !== "win32" && existsSync("/usr/bin/which")) {
return enumerateCandidatesViaSystemWhich(name);
}
return enumerateCandidatesViaLibrary(name);
}
async function enumerateCandidatesViaSystemWhich(name: string): Promise<string[]> {
try {
const { stdout } = await execCommand("/usr/bin/which", ["-a", name], {
timeout: 3000,
killSignal: "SIGKILL",
});
return Array.from(new Set(stdout.trim().split("\n").filter(Boolean)));
} catch (error) {
// which exits 1 for a missing command. A failed lookup is not evidence of absence.
if (error instanceof Error && "code" in error && error.code === 1) return [];
throw error;
}
}
async function enumerateCandidatesViaLibrary(name: string): Promise<string[]> {
let candidates: string[];
try {
candidates = await which(name, { all: true });
} catch (error) {
// `which` throws ENOENT when the command is absent from PATH.
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
const seen = new Set<string>();
return candidates.filter((candidate) => {
if (seen.has(candidate)) {
return false;
}
seen.add(candidate);
return true;
});
}
export async function probeExecutable(
executablePath: string,
timeoutMs = PROBE_TIMEOUT_MS,
): Promise<boolean> {
try {
await execCommand(executablePath, ["--version"], {
timeout: timeoutMs,
killSignal: "SIGKILL",
maxBuffer: 64 * 1024,
shell: isWindowsCommandScript(executablePath),
});
return true;
} catch (error) {
return classifyProbeError(error);
}
}
function classifyProbeError(error: unknown): boolean {
const err = error as NodeJS.ErrnoException & {
killed?: boolean;
};
if (err.killed) {
return true;
}
if (typeof err.code === "number") {
return true;
}
if (
err.code === "ENOENT" ||
err.code === "EACCES" ||
err.code === "ENOEXEC" ||
err.code === "UNKNOWN"
) {
return false;
}
return false;
}
/**
* Check a literal executable path. PATH search is handled by findExecutable().
*/
export function executableExists(
executablePath: string,
exists: typeof existsSync = existsSync,
): string | null {
if (process.platform === "win32") {
return windowsExecutableResolution.exists(executablePath, { exists });
}
return exists(executablePath) ? executablePath : null;
}
export async function findExecutable(
name: string,
probeTimeoutMs = PROBE_TIMEOUT_MS,
): Promise<string | null> {
const trimmed = name.trim();
if (!trimmed) {
return null;
}
if (process.platform === "win32") {
return windowsExecutableResolution.find(trimmed, {
enumeratePathCandidates: enumerateCandidates,
probeExecutable,
exists: existsSync,
probeTimeoutMs,
});
}
if (hasPathSeparator(trimmed)) {
return (await probeExecutable(trimmed, probeTimeoutMs)) ? trimmed : null;
}
const candidates = await enumerateCandidates(trimmed);
for (const candidate of candidates) {
if (await probeExecutable(candidate, probeTimeoutMs)) {
return candidate;
}
}
return null;
}
export async function isCommandAvailable(command: string): Promise<boolean> {
return (await findExecutable(command)) !== null;
}