mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
140 lines
3.6 KiB
TypeScript
140 lines
3.6 KiB
TypeScript
import { createRequire } from "node:module";
|
|
import { existsSync } from "node:fs";
|
|
import { extname } from "node:path";
|
|
import { execCommand } from "./spawn.js";
|
|
import { isWindowsCommandScript } from "./windows-command.js";
|
|
|
|
export { quoteWindowsArgument, quoteWindowsCommand } from "./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 {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
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 (exists(executablePath)) return executablePath;
|
|
if (process.platform === "win32" && !extname(executablePath)) {
|
|
for (const ext of [".exe", ".cmd"]) {
|
|
const candidate = executablePath + ext;
|
|
if (exists(candidate)) return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function findExecutable(
|
|
name: string,
|
|
probeTimeoutMs = PROBE_TIMEOUT_MS,
|
|
): Promise<string | null> {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|