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