Files
9router/src/lib/headroom/detect.js
T
Carmelo CamposandCursor 50ed79fe9e fix(headroom): support Docker sidecar proxy
Treat configured Headroom proxy as running when its /health endpoint
responds, even if local headroom CLI is not installed. Dashboard
Start/Stop stays limited to local loopback proxies while external
Docker sidecars can be enabled via HEADROOM_URL.

Closes #1948

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 17:45:27 +07:00

103 lines
3.6 KiB
JavaScript

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 };
}