import { execSync, execFileSync } from "node:child_process"; import { basename } from "node:path"; const commandAvailability = new Map(); export function hasCommand(command: string): boolean { if (commandAvailability.has(command)) { return commandAvailability.get(command)!; } let available: boolean; if (process.platform === "win32") { // `command -v` is not available in Windows shells, so prefer `where.exe`. try { execFileSync("where.exe", [command], { stdio: "ignore" }); available = true; } catch { try { execSync(`command -v ${command}`, { stdio: "ignore" }); available = true; } catch { available = false; } } } else { try { execSync(`command -v ${command}`, { stdio: "ignore" }); available = true; } catch { available = false; } } commandAvailability.set(command, available); return available; } export function isFishShell(): boolean { const shell = process.env.SHELL ?? ""; return basename(shell) === "fish"; } export function exitStatusVar(): string { return isFishShell() ? "$status" : "$?"; } export function shellEscape(s: string): string { return "'" + s.replace(/'/g, "'\\''") + "'"; } export function tailLines(text: string, lines: number): string { const split = text.split("\n"); if (split.length <= lines) return text; return split.slice(-lines).join("\n"); } export const __test__ = { clearCommandAvailability(): void { commandAvailability.clear(); }, };