UNPKG

@botpress/adk-cli

Version:

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

509 lines (506 loc) 18 kB
// @bun import { fetchProcesses } from "./chunk-72cmdww1.js"; import { cleanupConsoleFiles, forceKillConsole, shutdownConsole, waitForConsoleDead } from "./chunk-py8vpzzs.js"; import { ensureJsonOnlyFormat } from "./chunk-7sfagm12.js"; import { failEarly } from "./chunk-5mfrmhhc.js"; import { isAdkDevConsolePort, readConsolePort } from "./chunk-sgj6770p.js"; import { findAgentRoot } from "./chunk-kk3h6qaj.js"; import { createCliLogger } from "./chunk-gzwt1qdr.js"; import"./chunk-nxy2ya5r.js"; import"./chunk-wzj4dc7n.js"; import"./chunk-p0hjqn4r.js"; import"./chunk-np5wcwfv.js"; import"./chunk-dq2xpa24.js"; import"./chunk-6w0knnta.js"; import"./chunk-40x04ckt.js"; import"./chunk-t76d8fxx.js"; import"./chunk-nh2akp42.js"; import"./chunk-0fdvzjbh.js"; import"./chunk-2a5b6azq.js"; import"./chunk-vay209b5.js"; import"./chunk-3xrpxgq4.js"; import"./chunk-rfm3jr1m.js"; import"./chunk-w346ejn9.js"; import"./chunk-knvm2anf.js"; import"./chunk-65h5trb5.js"; import"./chunk-s2akeqpw.js"; import"./chunk-6771vrjp.js"; import"./chunk-g8mm42v1.js"; import"./chunk-50hzjdck.js"; import"./chunk-nn2jb0x0.js"; import"./chunk-v8xvth6j.js"; import"./chunk-kkk13rcb.js"; import"./chunk-ytpp1kam.js"; import"./chunk-na956zz3.js"; import"./chunk-f4bw8q7c.js"; import"./chunk-0v8vgrns.js"; import"./chunk-54qt5g7m.js"; import"./chunk-dhs2bg35.js"; // src/commands/adk-kill.ts import path from "path"; var CONSOLE_ALIASES = new Set(["console", "ui", "devconsole", "dev-console", "devConsole"]); var FETCH_TIMEOUT_MS = 1e4; function resolveAgentTarget(target, agents) { const normalizedTarget = target.toLowerCase(); const matches = []; const asPid = parseInt(target, 10); if (!isNaN(asPid) && String(asPid) === target) { for (const agent of agents) { if (agent.process?.adkPid === asPid || agent.process?.botPid === asPid) { if (!matches.includes(agent)) matches.push(agent); } } if (matches.length > 0) return matches; } for (const agent of agents) { const agentPathLower = agent.agentPath.toLowerCase(); const agentBasename = path.basename(agent.agentPath).toLowerCase(); const agentName = agent.name.toLowerCase(); if (agentPathLower === normalizedTarget || agent.agentPath === target) { if (!matches.includes(agent)) matches.push(agent); continue; } if (agentBasename === normalizedTarget || agentPathLower.includes(normalizedTarget)) { if (!matches.includes(agent)) matches.push(agent); continue; } if (agentName === normalizedTarget) { if (!matches.includes(agent)) matches.push(agent); continue; } } return matches; } async function terminateAgentGracefully(port, agentPath) { try { const res = await fetch(`http://localhost:${port}/api/agents/terminate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentPath }), signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!res.ok) { const body2 = await res.json().catch(() => null); return { status: "failed", error: body2?.message ?? `Terminate request returned HTTP ${res.status}` }; } const body = await res.json(); return { status: body.status ?? "stopped", graceful: body.graceful, forced: body.forced, signal: body.signal }; } catch (err) { return { status: "failed", error: err instanceof Error ? err.message : String(err) }; } } async function waitForAgentsGone(port, agentPaths, timeoutMs = 6000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const data2 = await fetchProcesses(port).catch(() => null); if (!data2) break; const remaining = data2.agents.filter((a) => agentPaths.has(a.agentPath) && a.consoleMode !== "cloud"); if (remaining.length === 0) return []; await new Promise((resolve) => setTimeout(resolve, 250)); } const data = await fetchProcesses(port).catch(() => null); if (!data) return []; return data.agents.filter((a) => agentPaths.has(a.agentPath) && a.consoleMode !== "cloud").map((a) => a.agentPath); } function forceKillPid(pid) { try { process.kill(pid, "SIGKILL"); return true; } catch { return false; } } async function adkKill(positionalTargets, options = {}) { ensureJsonOnlyFormat(options.format); const logger = createCliLogger({ format: options.format }); const hasTargets = positionalTargets.length > 0 || options.all || options.current || (options.pid?.length ?? 0) > 0; if (!hasTargets) { const msg = `No targets specified. Use --all, --current, --pid <pid>, or provide agent path/name arguments. ` + `Examples: ` + ` adk kill --current # stop the agent in the current directory ` + ` adk kill my-agent # stop by name or path ` + ` adk kill --all # stop all agents and the DevConsole ` + " adk kill --pid 12345 # stop by ADK process PID"; return failEarly(logger, options.format, msg, { targets: [], killed: [], failed: [], dryRun: options.dryRun ?? false, all: options.all ?? false, consoleCleanup: "not-applicable" }); } for (const t of positionalTargets) { if (CONSOLE_ALIASES.has(t.toLowerCase())) { const msg = `Cannot kill the DevConsole by name ('${t}' is a reserved alias). ` + "Use --all to stop all agents and then shut down the DevConsole."; return failEarly(logger, options.format, msg, { targets: positionalTargets, killed: [], failed: [{ agentPath: t, status: "failed", error: msg }], dryRun: options.dryRun ?? false, all: options.all ?? false, consoleCleanup: "not-applicable" }); } } const port = readConsolePort(); if (port === null) { const msg = "No DevConsole is running. Start one with `adk dev` or `adk dashboard`."; return failEarly(logger, options.format, msg, { targets: positionalTargets, killed: [], failed: [], dryRun: options.dryRun ?? false, all: options.all ?? false, consoleCleanup: "not-applicable" }); } if (!await isAdkDevConsolePort(port)) { const msg = `~/.adk/console.port points at port ${port}, but no ADK DevConsole is listening there. Remove stale files with: rm ~/.adk/console.lock ~/.adk/console.port ~/.adk/console.sock`; return failEarly(logger, options.format, msg, { targets: positionalTargets, killed: [], failed: [], dryRun: options.dryRun ?? false, all: options.all ?? false, consoleCleanup: "not-applicable" }); } const data = await fetchProcesses(port); if (!data) { const msg = `Failed to fetch process list from DevConsole at port ${port}`; return failEarly(logger, options.format, msg, { targets: positionalTargets, killed: [], failed: [], dryRun: options.dryRun ?? false, all: options.all ?? false, consoleCleanup: "not-applicable" }); } const localAgents = data.agents.filter((a) => a.consoleMode !== "cloud"); let resolvedAgents = []; const unresolvedTargets = []; if (options.all) { resolvedAgents = [...localAgents]; } else { if (options.current) { const agentRoot = await findAgentRoot(process.cwd()); if (!agentRoot) { const msg = "No ADK project found in the current directory or its parents. Cannot use --current."; return failEarly(logger, options.format, msg, { targets: ["--current"], killed: [], failed: [{ agentPath: "--current", status: "failed", error: msg }], dryRun: options.dryRun ?? false, all: false, consoleCleanup: "not-applicable" }); } const agent = localAgents.find((a) => a.agentPath === agentRoot); if (!agent) { const msg = `No running agent found for current project at ${agentRoot}`; return failEarly(logger, options.format, msg, { targets: [agentRoot], killed: [], failed: [{ agentPath: agentRoot, status: "failed", error: msg }], dryRun: options.dryRun ?? false, all: false, consoleCleanup: "not-applicable" }); } if (!resolvedAgents.includes(agent)) resolvedAgents.push(agent); } if (options.pid?.length) { for (const pid of options.pid) { const matched = localAgents.filter((a) => a.process?.adkPid === pid || a.process?.botPid === pid); if (matched.length === 0) { unresolvedTargets.push(`--pid ${pid}`); } else { for (const a of matched) { if (!resolvedAgents.includes(a)) resolvedAgents.push(a); } } } } for (const target of positionalTargets) { const matches = resolveAgentTarget(target, localAgents); if (matches.length === 0) { unresolvedTargets.push(target); } else if (matches.length > 1) { const matchPaths = matches.map((a) => a.agentPath).join(` - `); const msg = `Ambiguous target '${target}': matched multiple agents. Use a more specific path or name. ` + `Matched: - ${matchPaths}`; return failEarly(logger, options.format, msg, { targets: positionalTargets, killed: [], failed: [{ agentPath: target, status: "failed", error: msg }], dryRun: options.dryRun ?? false, all: false, consoleCleanup: "not-applicable" }); } else { const [agent] = matches; if (!resolvedAgents.includes(agent)) resolvedAgents.push(agent); } } if (unresolvedTargets.length > 0) { const msg = `No running agent found for target(s): ${unresolvedTargets.join(", ")}`; return failEarly(logger, options.format, msg, { targets: [...positionalTargets, ...options.pid?.map((p) => `--pid ${p}`) ?? []], killed: [], failed: unresolvedTargets.map((t) => ({ agentPath: t, status: "failed", error: msg })), dryRun: options.dryRun ?? false, all: false, consoleCleanup: "not-applicable" }); } } const targetPaths = resolvedAgents.map((a) => a.agentPath); if (options.dryRun) { const consoleCleanup2 = options.all ? "skipped" : "not-applicable"; if (options.format !== "json") { if (resolvedAgents.length === 0) { logger.info("No agents to stop (dry run)"); } else { logger.info("Dry run \u2014 the following agents would be stopped:"); for (const agent of resolvedAgents) { logger.info(` ${agent.name} (${agent.agentPath})`); } if (options.all) { logger.info(" DevConsole would also be shut down after all agents stop."); } } } const output2 = { targets: targetPaths, killed: resolvedAgents.map((a) => ({ agentPath: a.agentPath, status: "dry-run" })), failed: [], dryRun: true, all: options.all ?? false, consoleCleanup: consoleCleanup2 }; if (options.format === "json") { logger.info("Dry run").result(output2); } return output2; } const killed = []; const failed = []; for (const agent of resolvedAgents) { if (options.format !== "json") { logger.info(`Stopping ${agent.name} (${agent.agentPath})...`); } const result = await terminateAgentGracefully(port, agent.agentPath); if (result.status === "failed") { if (options.force && agent.process?.adkPid) { const killed_ok = forceKillPid(agent.process.adkPid); if (killed_ok) { killed.push({ agentPath: agent.agentPath, status: "forced", graceful: false, forced: true, signal: "SIGKILL" }); if (options.format !== "json") { logger.info(` \u2713 Force killed (SIGKILL, adkPid: ${agent.process.adkPid})`); } } else { failed.push({ agentPath: agent.agentPath, status: "failed", error: result.error }); if (options.format !== "json") { logger.error(` \u2717 Failed: ${result.error}`); } } } else if (options.force && !agent.process?.adkPid) { failed.push({ agentPath: agent.agentPath, status: "not-forceable", error: "No adkPid available for force kill (graceful also failed)" }); if (options.format !== "json") { logger.warn(` \u26A0 Not forceable: no PID metadata available. Graceful error: ${result.error ?? "unknown"}`); } } else { failed.push({ agentPath: agent.agentPath, status: "failed", error: result.error }); if (options.format !== "json") { logger.error(` \u2717 Failed: ${result.error ?? "unknown error"} (use --force to force kill)`); } } } else { const killedResult = { agentPath: agent.agentPath, status: result.status === "shutdown-requested" ? "shutdown-requested" : "stopped", graceful: result.graceful, forced: result.forced, signal: result.signal }; killed.push(killedResult); if (options.format !== "json") { if (result.status === "shutdown-requested") { logger.info(` \u2713 Shutdown requested (graceful signal sent, process exiting in background)`); } else if (result.forced) { logger.info(` \u2713 Stopped (forced, signal: ${result.signal ?? "SIGTERM/SIGKILL"})`); } else { logger.info(` \u2713 Stopped gracefully`); } } } } let consoleCleanup = options.all ? "skipped" : "not-applicable"; let consoleCleanupError; if (options.all) { const agentPathSet = new Set(targetPaths); if (agentPathSet.size > 0) { if (options.format !== "json") { logger.info("Waiting for agents to disconnect..."); } const remaining = await waitForAgentsGone(port, agentPathSet); if (remaining.length > 0 && options.format !== "json") { logger.warn(`Some agents may still be running: ${remaining.join(", ")}`); } } if (options.format !== "json") { logger.info("Requesting DevConsole shutdown..."); } const consolePid = data.console.pid; const shutdown = await shutdownConsole(port); if (shutdown.result === "success") { const dead = await waitForConsoleDead(port); if (dead) { consoleCleanup = "success"; if (options.format !== "json") { logger.info(" \u2713 DevConsole shut down"); } } else if (consolePid > 0) { if (options.format !== "json") { logger.warn(" \u26A0 DevConsole did not exit in time, force killing..."); } const fk = await forceKillConsole(consolePid); if (fk.killed) { consoleCleanup = "success"; if (options.format !== "json") { logger.info(` \u2713 DevConsole force killed (${fk.signal}, PID ${consolePid})`); } } else { consoleCleanup = "failed"; consoleCleanupError = `DevConsole PID ${consolePid} did not respond to SIGTERM/SIGKILL`; if (options.format !== "json") { logger.error(` \u2717 ${consoleCleanupError}`); } } } else { consoleCleanup = "success"; if (options.format !== "json") { logger.info(" \u2713 DevConsole shutdown requested (PID unknown, cannot verify exit)"); } } } else if (shutdown.result === "unavailable" || shutdown.result === "failed") { if (consolePid > 0) { if (options.format !== "json") { const reason = shutdown.result === "unavailable" ? "endpoint unavailable (older version)" : `endpoint failed: ${shutdown.error ?? "unknown"}`; logger.warn(` \u26A0 Graceful shutdown ${reason}, force killing PID ${consolePid}...`); } const fk = await forceKillConsole(consolePid); if (fk.killed) { consoleCleanup = "success"; if (options.format !== "json") { logger.info(` \u2713 DevConsole force killed (${fk.signal}, PID ${consolePid})`); } } else { consoleCleanup = "failed"; consoleCleanupError = `DevConsole PID ${consolePid} did not respond to SIGTERM/SIGKILL`; if (options.format !== "json") { logger.error(` \u2717 ${consoleCleanupError}`); } } } else { consoleCleanup = "failed"; consoleCleanupError = shutdown.result === "unavailable" ? "Shutdown endpoint unavailable and no PID known for force kill" : `Shutdown failed: ${shutdown.error ?? "unknown"}, and no PID known for force kill`; if (options.format !== "json") { logger.error(` \u2717 ${consoleCleanupError}`); } } } const removedFiles = cleanupConsoleFiles(); if (removedFiles.length > 0 && options.format !== "json") { logger.info(` Cleaned up: ${removedFiles.map((f) => `~/.adk/${f}`).join(", ")}`); } } const output = { targets: targetPaths, killed, failed, dryRun: false, all: options.all ?? false, consoleCleanup, ...consoleCleanupError ? { consoleCleanupError } : {} }; if (options.format === "json") { logger.info(`Kill complete`).result(output); } else { if (killed.length > 0 || failed.length > 0) { logger.newline(); logger.info(`Done: ${killed.length} stopped, ${failed.length} failed` + (consoleCleanup !== "not-applicable" ? `, console cleanup: ${consoleCleanup}` : "")); } else if (resolvedAgents.length === 0) { logger.info("No agents were running."); } } return output; } export { adkKill };