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; 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 { if (process.platform !== "win32" && existsSync("/usr/bin/which")) { return enumerateCandidatesViaSystemWhich(name); } return enumerateCandidatesViaLibrary(name); } async function enumerateCandidatesViaSystemWhich(name: string): Promise { 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 { 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(); 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 { 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 { 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 { return (await findExecutable(command)) !== null; }