import { execSync } from "child_process"; import path from "path"; const IS_WIN = process.platform === "win32"; const WHICH_CMD = IS_WIN ? "where" : "which"; // Extra bin dirs often missing from a packaged/launchd PATH (Python installs headroom here). const EXTRA_BINS = IS_WIN ? [ `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python313\\Scripts`, `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python312\\Scripts`, `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python311\\Scripts`, `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python310\\Scripts`, `${process.env.APPDATA || ""}\\Python\\Python313\\Scripts`, ] : [ "/usr/local/bin", "/opt/homebrew/bin", "/Library/Frameworks/Python.framework/Versions/3.13/bin", "/Library/Frameworks/Python.framework/Versions/3.12/bin", "/Library/Frameworks/Python.framework/Versions/3.11/bin", "/Library/Frameworks/Python.framework/Versions/3.10/bin", `${process.env.HOME || ""}/.local/bin`, "/usr/bin", "/bin", ]; const EXTENDED_PATH = [...EXTRA_BINS, process.env.PATH || ""].filter(Boolean).join(path.delimiter); const PYTHON_CANDIDATES = ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"]; const MIN_VERSION = [3, 10]; const HEADROOM_HEALTH_TIMEOUT_MS = 1500; const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]); export const DEFAULT_HEADROOM_URL = process.env.HEADROOM_URL || "http://localhost:8787"; // Detect whether the headroom CLI is installed and where its binary lives. export function findHeadroomBinary() { try { const out = execSync(`${WHICH_CMD} headroom`, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, }).toString().trim(); // Windows `where` may return multiple lines — take the first. return out ? out.split(/\r?\n/)[0].trim() : null; } catch { return null; } } // Find a Python interpreter >= 3.10 (headroom-ai requires it). Returns null if none. export function findPython310() { for (const candidate of PYTHON_CANDIDATES) { try { const ver = execSync(`${candidate} --version`, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, }).toString().trim(); const match = ver.match(/(\d+)\.(\d+)/); if (!match) continue; const [major, minor] = [parseInt(match[1], 10), parseInt(match[2], 10)]; if (major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1])) { return candidate; } } catch { // candidate not present, try next } } return null; } // Probe whether a Headroom proxy is reachable at the given URL by hitting /health. export async function probeProxyRunning(url) { if (!url) return false; const base = String(url).replace(/\/$/, ""); try { const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(HEADROOM_HEALTH_TIMEOUT_MS) }); return res.ok; } catch { return false; } } export function isLoopbackHeadroomUrl(url) { try { const parsed = new URL(url); return LOOPBACK_HOSTS.has(parsed.hostname); } catch { return false; } } // Aggregate status for the dashboard: installed, running, python interpreter. export async function getHeadroomStatus(url) { const path = findHeadroomBinary(); const python = findPython310(); const installed = Boolean(path); const running = await probeProxyRunning(url); const localUrl = isLoopbackHeadroomUrl(url); return { installed, path, running, python, localUrl, canStart: installed && localUrl }; }