feat(plugins): first-class plugin providers — status, launch resolution, config overrides (#5707)

* feat(plugins): support provider status, launch resolution, and config overrides

* fix(plugins): defer override warnings until provider startup settles
This commit is contained in:
Mohamed Boudra
2026-09-30 13:46:35 +01:00
committed by GitHub
parent 8cccc1f6fa
commit b0c9de0c90
21 changed files with 1033 additions and 139 deletions
@@ -1,12 +1,16 @@
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import type { ProcessEnvRecord } from "../server/paseo-env.js";
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[]>;
type Which = (
command: string,
options: { all: true; path?: string; pathExt?: string },
) => Promise<string[]>;
const require = createRequire(import.meta.url);
const which = require("which") as Which;
@@ -16,16 +20,20 @@ function hasPathSeparator(value: string): boolean {
return value.includes("/") || value.includes("\\");
}
async function enumerateCandidates(name: string): Promise<string[]> {
async function enumerateCandidates(name: string, env?: ProcessEnvRecord): Promise<string[]> {
if (process.platform !== "win32" && existsSync("/usr/bin/which")) {
return enumerateCandidatesViaSystemWhich(name);
return enumerateCandidatesViaSystemWhich(name, env);
}
return enumerateCandidatesViaLibrary(name);
return enumerateCandidatesViaLibrary(name, env);
}
async function enumerateCandidatesViaSystemWhich(name: string): Promise<string[]> {
async function enumerateCandidatesViaSystemWhich(
name: string,
env?: ProcessEnvRecord,
): Promise<string[]> {
try {
const { stdout } = await execCommand("/usr/bin/which", ["-a", name], {
baseEnv: env,
timeout: 3000,
killSignal: "SIGKILL",
});
@@ -37,10 +45,13 @@ async function enumerateCandidatesViaSystemWhich(name: string): Promise<string[]
}
}
async function enumerateCandidatesViaLibrary(name: string): Promise<string[]> {
async function enumerateCandidatesViaLibrary(
name: string,
env?: ProcessEnvRecord,
): Promise<string[]> {
let candidates: string[];
try {
candidates = await which(name, { all: true });
candidates = await which(name, { all: true, path: env?.PATH, pathExt: env?.PATHEXT });
} catch (error) {
// `which` throws ENOENT when the command is absent from PATH.
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
@@ -61,10 +72,13 @@ async function enumerateCandidatesViaLibrary(name: string): Promise<string[]> {
export async function probeExecutable(
executablePath: string,
timeoutMs = PROBE_TIMEOUT_MS,
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
): Promise<boolean> {
const { probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS, env } =
typeof options === "number" ? { probeTimeoutMs: options } : options;
try {
await execCommand(executablePath, ["--version"], {
baseEnv: env,
timeout: timeoutMs,
killSignal: "SIGKILL",
maxBuffer: 64 * 1024,
@@ -110,10 +124,17 @@ export function executableExists(
return exists(executablePath) ? executablePath : null;
}
export interface ExecutableResolutionOptions {
probeTimeoutMs?: number;
env?: ProcessEnvRecord;
}
export async function findExecutable(
name: string,
probeTimeoutMs = PROBE_TIMEOUT_MS,
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
): Promise<string | null> {
const { probeTimeoutMs = PROBE_TIMEOUT_MS, env } =
typeof options === "number" ? { probeTimeoutMs: options } : options;
const trimmed = name.trim();
if (!trimmed) {
return null;
@@ -121,20 +142,21 @@ export async function findExecutable(
if (process.platform === "win32") {
return windowsExecutableResolution.find(trimmed, {
enumeratePathCandidates: enumerateCandidates,
probeExecutable,
enumeratePathCandidates: (command) => enumerateCandidates(command, env),
probeExecutable: (command, timeout) =>
probeExecutable(command, { probeTimeoutMs: timeout, env }),
exists: existsSync,
probeTimeoutMs,
});
}
if (hasPathSeparator(trimmed)) {
return (await probeExecutable(trimmed, probeTimeoutMs)) ? trimmed : null;
return (await probeExecutable(trimmed, { probeTimeoutMs, env })) ? trimmed : null;
}
const candidates = await enumerateCandidates(trimmed);
const candidates = await enumerateCandidates(trimmed, env);
for (const candidate of candidates) {
if (await probeExecutable(candidate, probeTimeoutMs)) {
if (await probeExecutable(candidate, { probeTimeoutMs, env })) {
return candidate;
}
}