import { spawn } from "node:child_process"; import net from "node:net"; export function isPortFree(host: string, port: number): Promise { return new Promise((resolve) => { const socket = net.connect({ host, port, timeout: 500 }); socket.once("connect", () => { socket.destroy(); resolve(false); }); socket.once("timeout", () => { socket.destroy(); resolve(true); }); socket.once("error", () => { resolve(true); }); }); } export async function waitUntilReady( url: string, timeoutMs: number, intervalMs = 400, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await fetch(url, { signal: AbortSignal.timeout(1500) }); if (response.status === 200) return true; } catch { // not ready yet } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } return false; } /** Single-shot check (not a poll loop, unlike `waitUntilReady`) for whether a triggershell server * is already listening at `url` - used to decide whether `run` can go through the REST/WS API. */ export async function isServerReachable(url: string): Promise { try { const response = await fetch(`${url}/api/healthz`, { signal: AbortSignal.timeout(1000), }); return response.status === 200; } catch { return false; } } export function openBrowser(url: string): void { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; const args = process.platform === "win32" ? ["", url] : [url]; try { spawn(command, args, { detached: true, stdio: "ignore", shell: process.platform === "win32", }).unref(); } catch { // best-effort - not fatal if no browser opener is available } }