import { spawn } from "node:child_process"; import { createServer } from "node:net"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ParsedDaemonBaseUrl { readonly hostname: string; readonly port: number; } export interface DaemonSpawnSpec { readonly command: string; readonly args: ReadonlyArray; } export interface ExecutorServerReachabilityInput { readonly baseUrl: string; } type ProbeServer = ReturnType & { removeAllListeners: () => void; once: (event: "error" | "listening", listener: (...args: unknown[]) => void) => void; }; // --------------------------------------------------------------------------- // Base URL parsing // --------------------------------------------------------------------------- export const parseDaemonBaseUrl = (baseUrl: string, defaultPort: number): ParsedDaemonBaseUrl => { const parsed = new URL(baseUrl); if (parsed.protocol !== "http:") { throw new Error(`Only http:// base URLs are supported for daemon auto-start: ${baseUrl}`); } const port = Number(parsed.port) || defaultPort; if (!Number.isFinite(port) || port <= 0 || port > 65535) { throw new Error(`Invalid daemon port in base URL: ${baseUrl}`); } return { hostname: parsed.hostname || "localhost", port, }; }; // --------------------------------------------------------------------------- // Local-host checks // --------------------------------------------------------------------------- const LOCAL_DAEMON_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); export const canAutoStartLocalDaemonForHost = (hostname: string): boolean => LOCAL_DAEMON_HOSTNAMES.has(hostname.toLowerCase()); /** * Bun's compiled-binary embedded filesystem root, drive-rooted on Windows * (`B:\~BUN\root\...`, argv normalized to `B:/~BUN/root/...`). Anchored to a * drive prefix so a dev checkout that merely *contains* a `~BUN` directory * isn't misread as a compiled binary. */ const WINDOWS_BUNFS_ENTRYPOINT = /^[a-z]:\/~BUN\//i; /** * Whether the process is running from the dev source (`bun run src/main.ts`) * rather than a compiled single-file binary. A compiled binary runs from Bun's * embedded filesystem, whose entrypoint is `/$bunfs/root/main.js` on Unix but * `B:\~BUN\root\main.js` (argv like `B:/~BUN/root/main.js`) on Windows — match * BOTH. Missing the Windows form made a real `executor.exe` look like a dev * checkout, so `service install` refused on Windows. (Found by a real EC2 * Windows test.) */ export const isDevCliEntrypoint = (scriptPath: string | undefined): boolean => { if (!scriptPath) return false; const normalized = scriptPath.replaceAll("\\", "/"); if (normalized.startsWith("/$bunfs/") || WINDOWS_BUNFS_ENTRYPOINT.test(normalized)) return false; return normalized.endsWith(".ts") || normalized.endsWith(".js"); }; export const isExecutorServerReachable = ( input: ExecutorServerReachabilityInput, ): Effect.Effect => Effect.tryPromise(async () => { // The unauthenticated liveness probe — never forwards a credential, so a // misconfigured base URL can't leak the bearer token to a third-party host. const url = new URL("/api/health", input.baseUrl); const response = await fetch(url, { signal: AbortSignal.timeout(2000) }); const body = await response.text(); return response.ok && body.trim() === "ok"; }).pipe(Effect.catchCause(() => Effect.succeed(false))); // --------------------------------------------------------------------------- // Process spec // --------------------------------------------------------------------------- export const buildDaemonSpawnSpec = (input: { readonly port: number; readonly hostname: string; readonly isDevMode: boolean; readonly scriptPath: string | undefined; readonly executablePath: string; readonly allowedHosts?: ReadonlyArray; }): DaemonSpawnSpec => { const daemonArgs = [ "daemon", "run", "--port", String(input.port), "--hostname", input.hostname, "--foreground", ...(input.allowedHosts ?? []).flatMap((h) => ["--allowed-host", h]), ]; if (input.isDevMode) { if (!input.scriptPath) { throw new Error("Cannot auto-start daemon in dev mode without a CLI script path"); } return { command: "bun", args: ["run", input.scriptPath, ...daemonArgs], }; } return { command: input.executablePath, args: daemonArgs, }; }; // --------------------------------------------------------------------------- // Spawn + wait // --------------------------------------------------------------------------- export const spawnDetached = (input: { readonly command: string; readonly args: ReadonlyArray; readonly env: Record; }): Effect.Effect => Effect.try({ try: () => { const child = spawn(input.command, [...input.args], { detached: true, stdio: "ignore", env: input.env, }); child.unref(); }, catch: (cause) => cause instanceof Error ? cause : new Error(`Failed to spawn daemon process: ${String(cause)}`), }); const waitForCondition = (input: { readonly check: Effect.Effect; readonly expected: boolean; readonly timeoutMs: number; readonly intervalMs: number; }): Effect.Effect => Effect.gen(function* () { const startedAt = yield* Clock.currentTimeMillis; while (true) { const reachable = yield* input.check; if (reachable === input.expected) return true; const now = yield* Clock.currentTimeMillis; if (now - startedAt >= input.timeoutMs) return false; yield* Effect.sleep(input.intervalMs); } }); export const waitForReachable = (input: { readonly check: Effect.Effect; readonly timeoutMs: number; readonly intervalMs: number; }): Effect.Effect => waitForCondition({ check: input.check, expected: true, timeoutMs: input.timeoutMs, intervalMs: input.intervalMs, }); export const waitForUnreachable = (input: { readonly check: Effect.Effect; readonly timeoutMs: number; readonly intervalMs: number; }): Effect.Effect => waitForCondition({ check: input.check, expected: false, timeoutMs: input.timeoutMs, intervalMs: input.intervalMs, }); const toProbeHost = (hostname: string): string => { const normalized = hostname.trim().toLowerCase(); if (normalized === "localhost" || normalized === "0.0.0.0") { return "127.0.0.1"; } return hostname; }; const isPortAvailable = (input: { hostname: string; port: number; }): Effect.Effect => Effect.tryPromise({ try: () => new Promise((resolve) => { const server = createServer() as ProbeServer; const cleanup = () => { if (typeof server.removeAllListeners === "function") { server.removeAllListeners(); } }; server.once("error", () => { cleanup(); resolve(false); }); server.once("listening", () => { cleanup(); server.close(() => resolve(true)); }); server.listen({ port: input.port, host: toProbeHost(input.hostname) }); }), catch: (cause) => cause instanceof Error ? cause : new Error(`Failed probing port availability: ${String(cause)}`), }); const pickEphemeralPort = (hostname: string): Effect.Effect => Effect.tryPromise({ try: () => new Promise((resolve, reject) => { const server = createServer() as ProbeServer; server.once("error", (error: unknown) => { reject(error); }); server.once("listening", () => { const address = server.address(); const port = typeof address === "object" && address !== null ? address.port : 0; server.close(() => resolve(port)); }); server.listen({ port: 0, host: toProbeHost(hostname) }); }), catch: (cause) => cause instanceof Error ? cause : new Error(`Failed selecting ephemeral port: ${String(cause)}`), }); export const chooseDaemonPort = (input: { preferredPort: number; hostname: string; }): Effect.Effect => Effect.gen(function* () { const preferredAvailable = yield* isPortAvailable({ hostname: input.hostname, port: input.preferredPort, }); if (preferredAvailable) return input.preferredPort; const fallbackPort = yield* pickEphemeralPort(input.hostname); if (!Number.isFinite(fallbackPort) || fallbackPort <= 0 || fallbackPort > 65535) { return yield* Effect.fail( new Error(`Could not find an available daemon port for host ${input.hostname}`), ); } return fallbackPort; }); // --------------------------------------------------------------------------- // Service-install planning (pure) // --------------------------------------------------------------------------- export type ServiceInstallPlan = "noop" | "reinstall" | "takeover-then-install"; export const planServiceInstall = (input: { readonly registered: boolean; readonly running: boolean; readonly activeKind: "cli-daemon" | "desktop-sidecar" | "foreground" | null; readonly activePid?: number | null; readonly servicePid?: number | null; readonly activeVersion: string | null; readonly activeExecutablePath?: string | null; readonly activePort: number | null; readonly requestedPort: number; readonly currentVersion: string; readonly currentExecutablePath?: string | null; }): ServiceInstallPlan => { if (input.activeKind !== null && input.activeKind !== "cli-daemon") { return "takeover-then-install"; } if (input.registered && input.running) { if ( input.activeKind === "cli-daemon" && input.activePid !== undefined && input.activePid !== null && input.servicePid !== undefined && input.servicePid !== null && input.activePid !== input.servicePid ) { return "takeover-then-install"; } const executableMatches = !input.activeExecutablePath || !input.currentExecutablePath || input.activeExecutablePath === input.currentExecutablePath; return input.activeVersion === input.currentVersion && input.activePort === input.requestedPort && executableMatches ? "noop" : "reinstall"; } return "takeover-then-install"; };