UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

248 lines (245 loc) 7.58 kB
// @bun import { AdkError } from "./chunk-p0hjqn4r.js"; // src/server/lifecycle.ts import { existsSync, readFileSync, unlinkSync } from "fs"; import { promises as fsp } from "fs"; import { dirname, join } from "path"; import { homedir } from "os"; import { spawn } from "child_process"; var {connect } = globalThis.Bun; var __dirname = "/home/runner/work/agent-lack/agent-lack/packages/cli/src/server"; var ADK_DIR = join(homedir(), ".adk"); var CONSOLE_SOCKET_PATH = join(ADK_DIR, "console.sock"); var CONSOLE_PORT_FILE = join(ADK_DIR, "console.port"); var CONSOLE_LOCK_FILE = join(ADK_DIR, "console.lock"); var INTERNAL_UI_SERVER_ARG = "--adk-internal-ui-server"; var DEVCONSOLE_ID_HEADER = "X-ADK-DevConsole"; var SPAWN_TIMEOUT_MS = 1e4; var PORT_SCAN_RANGE_SIZE = 20; function probeUnixSocket(socketPath) { return new Promise((resolve) => { try { connect({ unix: socketPath, socket: { open: (s) => { resolve(true); s.end(); }, data: () => {}, close: () => resolve(false), error: () => resolve(false), connectError: () => resolve(false) } }); } catch { resolve(false); } }); } async function isSocketAlive() { if (!existsSync(CONSOLE_SOCKET_PATH)) return false; return probeUnixSocket(CONSOLE_SOCKET_PATH); } function readConsolePort() { if (!existsSync(CONSOLE_PORT_FILE)) return null; try { const port = parseInt(readFileSync(CONSOLE_PORT_FILE, "utf-8").trim(), 10); return port > 0 ? port : null; } catch { return null; } } var HEALTH_CHECK_TIMEOUT_MS = 500; async function isAdkDevConsolePort(port) { try { const res = await fetch(`http://localhost:${port}/api/agents`, { signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS) }); if (!res.ok) { await res.body?.cancel(); return false; } if (res.headers.get(DEVCONSOLE_ID_HEADER) !== "1") { await res.body?.cancel(); return false; } const body = await res.json().catch(() => null); return Array.isArray(body?.agents); } catch { return false; } } function unlinkConsolePortFile() { try { if (existsSync(CONSOLE_PORT_FILE)) unlinkSync(CONSOLE_PORT_FILE); } catch {} } function cleanupStaleFiles() { try { if (existsSync(CONSOLE_SOCKET_PATH)) unlinkSync(CONSOLE_SOCKET_PATH); } catch {} try { if (existsSync(CONSOLE_PORT_FILE)) unlinkSync(CONSOLE_PORT_FILE); } catch {} } function readLockPid() { try { const pid = parseInt(readFileSync(CONSOLE_LOCK_FILE, "utf-8").trim(), 10); return Number.isFinite(pid) && pid > 0 ? pid : null; } catch { return null; } } function isProcessAlive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } function errorCode(err) { return err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : undefined; } async function acquireLock() { await fsp.mkdir(ADK_DIR, { recursive: true }); const TIMEOUT_MS = 5000; const start = Date.now(); while (Date.now() - start < TIMEOUT_MS) { try { await fsp.writeFile(CONSOLE_LOCK_FILE, String(process.pid), { flag: "wx", encoding: "utf-8", mode: 384 }); return () => { try { unlinkSync(CONSOLE_LOCK_FILE); } catch {} }; } catch (err) { if (errorCode(err) !== "EEXIST") throw err; const pid = readLockPid(); if (pid === null || !isProcessAlive(pid)) { try { unlinkSync(CONSOLE_LOCK_FILE); } catch {} continue; } await Bun.sleep(100); } } const heldBy = readLockPid(); throw new AdkError({ code: "CONSOLE_LOCK_TIMEOUT", message: `Lock held by PID ${heldBy ?? "unknown"} for over 5s. Remove stale files with: rm ~/.adk/console.lock ~/.adk/console.port ~/.adk/console.sock` }); } async function spawnUiServer(options) { const scriptDir = process.argv[1] ? dirname(process.argv[1]) : __dirname; const isVirtualBunPath = (path) => path.includes("$bunfs"); const isCompiledExecutable = process.argv.some((arg) => isVirtualBunPath(arg)); const entryPath = !isCompiledExecutable ? [ join(__dirname, "ui-server-entry.ts"), join(__dirname, "ui-server-entry.js"), join(__dirname, "..", "ui-server-entry.js"), join(scriptDir, "ui-server-entry.js") ].find((candidate) => !isVirtualBunPath(candidate) && existsSync(candidate)) ?? null : null; const args = entryPath ? [entryPath] : [INTERNAL_UI_SERVER_ARG]; if (options.startPort) args.push("--port", String(options.startPort)); if (options.standalone) args.push("--standalone"); const child = spawn(process.execPath, args, { detached: true, stdio: "ignore" }); child.unref(); let childError = null; child.on("error", (err) => { childError = err; }); child.on("exit", (code, signal) => { if (childError) return; if (code !== null) childError = new Error(`DevConsoleUiServer child exited with code ${code}`); else if (signal !== null) childError = new Error(`DevConsoleUiServer child terminated by signal ${signal}`); }); const start = Date.now(); while (Date.now() - start < SPAWN_TIMEOUT_MS) { if (childError) throw childError; const port = readConsolePort(); if (port !== null) return { port, pid: child.pid }; await Bun.sleep(100); } throw new AdkError({ code: "CONSOLE_SPAWN_TIMEOUT", message: `DevConsoleUiServer failed to start within ${SPAWN_TIMEOUT_MS / 1000} seconds` }); } async function discoverAdkDevConsolePort(startPort) { const port = readConsolePort(); if (port !== null && await isAdkDevConsolePort(port)) return port; if (port !== null) unlinkConsolePortFile(); for (let p = startPort;p < startPort + PORT_SCAN_RANGE_SIZE; p++) { if (await isAdkDevConsolePort(p)) return p; } return null; } function liveSocketWithoutPortError(startPort) { return new AdkError({ code: "CONSOLE_PORT_UNVERIFIED", message: `DevConsole socket is alive, but its HTTP port could not be verified from ~/.adk/console.port or ports ${startPort}-${startPort + PORT_SCAN_RANGE_SIZE - 1}. Run \`adk kill --all\` if the dashboard is stuck.` }); } async function ensureUiServer(options) { const startPort = options?.startPort ?? 3001; if (await isSocketAlive()) { const port = await discoverAdkDevConsolePort(startPort); if (port !== null) { return { port, wasSpawned: false }; } } const releaseLock = await acquireLock(); try { if (await isSocketAlive()) { const port = await discoverAdkDevConsolePort(startPort); if (port !== null) { return { port, wasSpawned: false }; } throw liveSocketWithoutPortError(startPort); } cleanupStaleFiles(); try { const { port, pid } = await spawnUiServer({ startPort: options?.startPort, standalone: options?.standalone }); return { port, wasSpawned: true, pid }; } catch (err) { if (await isSocketAlive()) { const port = await discoverAdkDevConsolePort(startPort); if (port !== null) { return { port, wasSpawned: false }; } throw liveSocketWithoutPortError(startPort); } throw err; } } finally { releaseLock(); } } export { CONSOLE_SOCKET_PATH, CONSOLE_PORT_FILE, CONSOLE_LOCK_FILE, INTERNAL_UI_SERVER_ARG, DEVCONSOLE_ID_HEADER, probeUnixSocket, readConsolePort, isAdkDevConsolePort, ensureUiServer };