mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user