@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
248 lines (245 loc) • 7.58 kB
JavaScript
// @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 };