@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
362 lines (359 loc) • 12.1 kB
JavaScript
// @bun
import {
formatStatusLine,
remediationFor
} from "./chunk-3k2q12qe.js";
import {
resolveCommandContext,
resolveEnvironmentRemoteContext
} from "./chunk-3ahwp6fe.js";
import {
DEPENDENCY_ERROR_CODES,
DependencyError
} from "./chunk-5zm2mgt9.js";
import {
findAgentRootOrFail
} from "./chunk-kk3h6qaj.js";
import {
createCliLogger
} from "./chunk-gzwt1qdr.js";
import {
isAdkError
} from "./chunk-wzj4dc7n.js";
import {
__require
} from "./chunk-dhs2bg35.js";
// src/utils/dependency-cli.ts
import readline from "readline";
function isDependencyErrorLike(err) {
if (err instanceof DependencyError) {
return true;
}
if (!err || typeof err !== "object") {
return false;
}
const code = err.code;
const message = err.message;
return typeof code === "string" && DEPENDENCY_ERROR_CODES.includes(code) && typeof message === "string";
}
function formatJsonError(err) {
if (isDependencyErrorLike(err)) {
const body = {
ok: false,
error: {
code: err.code,
message: err.message,
...err.details !== undefined && { details: err.details },
...err.suggestion !== undefined && { suggestion: err.suggestion }
}
};
return JSON.stringify(body, null, 2);
}
if (isAdkError(err)) {
const body = {
ok: false,
error: {
code: err.code,
message: err.message,
...err.details !== undefined && { details: err.details },
...err.suggestion !== undefined && { suggestion: err.suggestion }
}
};
return JSON.stringify(body, null, 2);
}
const message = err instanceof Error ? err.message : String(err);
return JSON.stringify({ ok: false, error: { code: "SYSTEM_ERROR", message } }, null, 2);
}
var USER_ERROR_CODES = new Set([
"AUTH_REQUIRED",
"INTEGRATION_NOT_FOUND",
"PLUGIN_NOT_FOUND",
"INTERFACE_NOT_FOUND",
"VERSION_NOT_FOUND",
"MISSING_INPUT",
"MISSING_DEPENDENCY",
"AMBIGUOUS_DEPENDENCY",
"INTERFACE_NOT_IMPLEMENTED",
"SAME_SOURCE_TARGET",
"SOURCE_SNAPSHOT_MISSING",
"INVALID_CONFIG",
"BOT_NOT_FOUND",
"BUILTIN_INTERFACE_IMMUTABLE",
"UNCONFIGURED_DEPENDENCIES"
]);
var STATE_ERROR_CODES = new Set([
"SNAPSHOT_DRIFT",
"MIGRATION_CONFLICT",
"PROD_CONFIRMATION_REQUIRED",
"UNINSTALL_REQUIRES_CONFIRMATION"
]);
function exitCodeFor(err) {
if (isDependencyErrorLike(err)) {
if (STATE_ERROR_CODES.has(err.code))
return 3;
if (USER_ERROR_CODES.has(err.code))
return 1;
return 2;
}
return 2;
}
function parseTarget(raw) {
const value = raw ?? process.env.ADK_TARGET ?? "dev";
if (value !== "dev" && value !== "prod" && value !== "all") {
throw new DependencyError({
code: "MISSING_INPUT",
message: `Invalid --target value '${value}'. Expected 'dev', 'prod', or 'all'.`
});
}
return value;
}
function expandTarget(target) {
return target === "all" ? ["dev", "prod"] : [target];
}
function parseFormat(raw) {
const value = raw ?? "text";
if (value !== "text" && value !== "json") {
throw new DependencyError({
code: "MISSING_INPUT",
message: `Invalid --format value '${value}'. Expected 'text' or 'json'.`
});
}
return value;
}
function parseKeyValueOptions(values, optionName) {
const parsed = {};
for (const kv of values ?? []) {
const eq = kv.indexOf("=");
if (eq < 0) {
throw new DependencyError({ code: "MISSING_INPUT", message: `${optionName} expects key=value, got '${kv}'` });
}
const key = kv.slice(0, eq);
if (!key) {
throw new DependencyError({ code: "MISSING_INPUT", message: `${optionName} expects a non-empty key` });
}
parsed[key] = parseKeyValueValue(kv.slice(eq + 1), optionName);
}
return parsed;
}
function parseKeyValueValue(raw, optionName) {
const value = raw.trim();
if (value === "true")
return true;
if (value === "false")
return false;
if (value === "null")
return null;
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value))
return Number(value);
const first = value[0];
if (first === "{" || first === "[" || first === '"') {
try {
return JSON.parse(value);
} catch (err) {
throw new DependencyError({
code: "INVALID_CONFIG",
message: `${optionName} value '${raw}' looks like JSON but could not be parsed: ${err instanceof Error ? err.message : String(err)}`
});
}
}
return raw;
}
async function promptYesNo(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
});
});
}
async function confirmProdAndRetry(call, runWithYes, logger) {
try {
return await call();
} catch (err) {
if (!isDependencyErrorLike(err) || err.code !== "PROD_CONFIRMATION_REQUIRED" && err.code !== "UNINSTALL_REQUIRES_CONFIRMATION") {
throw err;
}
if (logger.format === "json" || !process.stderr.isTTY) {
throw err;
}
const planned = err.details?.planned ?? err.details?.destructive ?? [];
const header = err.code === "PROD_CONFIRMATION_REQUIRED" ? "Apply targets production." : "Apply would uninstall resources from cloud.";
if (planned.length === 0) {
logger.warn(header);
} else {
logger.warn(`${header} Planned changes:`);
for (const a of planned) {
logger.warn(` ${a.action} ${a.type}:${a.alias}`);
}
}
const confirmed = await promptYesNo("Proceed? [y/N] ");
if (!confirmed) {
throw new DependencyError({ code: "MISSING_INPUT", message: "Aborted by user." });
}
return runWithYes();
}
}
async function loadProjectDM(env) {
const context = await resolveCommandContext({
target: env,
require: ["project", "credentials", "workspace", "bot"]
});
const { dependencies } = await import("./chunk-ka3e16hs.js");
const dm = await dependencies.DependencyManager.fromProject({
projectPath: context.project.path,
env,
client: context.client,
botId: context.botId
});
return { dm, project: context.project, context };
}
async function resolveDependencyRegistryManagerOptions() {
const context = await resolveEnvironmentRemoteContext();
return {
credentials: context.credentials,
apiUrl: context.apiUrl
};
}
async function runCopyCommand(options) {
await runDependencyCommand({
options,
run: async (logger) => {
const format = parseFormat(options.format);
if (!options.from || !options.to) {
const { DependencyError: DE } = await import("./chunk-z6sc1k1g.js");
throw new DE({ code: "MISSING_INPUT", message: "--from and --to are required" });
}
const fromParsed = parseTarget(options.from);
const toParsed = parseTarget(options.to);
if (fromParsed === "all" || toParsed === "all") {
const { DependencyError: DE } = await import("./chunk-z6sc1k1g.js");
throw new DE({
code: "MISSING_INPUT",
message: `'all' is not a valid value for --from or --to on copy. Use 'dev' or 'prod'.`
});
}
const from = fromParsed;
const to = toParsed;
const { dm } = await loadProjectDM(to);
const { context: sourceContext } = await loadProjectDM(from);
const copyOpts = { from, to, dryRun: options.dryRun, yes: options.yes, sourceBotId: sourceContext.botId };
const result = await confirmProdAndRetry(() => dm.copy(copyOpts), () => dm.copy({ ...copyOpts, yes: true }), logger);
if (format === "text") {
const verb = result.dryRun ? "Plan" : "Applied";
logger.info(`${verb} ${from} \u2192 ${to}:`);
for (const a of result.applied.length ? result.applied : result.skipped) {
logger.info(` ${a.action} ${a.type}:${a.alias}`);
}
for (const e of result.errors) {
logger.info(` \u2717 ${e.action.action} ${e.action.type}:${e.action.alias} \u2014 ${e.code}: ${e.message}`, "red");
}
if (result.errors.length > 0) {
throw new DependencyError({
code: "INVALID_CONFIG",
message: `Copy completed with errors`
});
}
return {};
}
return { data: { result } };
}
});
}
async function runDiffCommand(options) {
await runDependencyCommand({
options,
run: async (logger) => {
const format = parseFormat(options.format);
const target = parseTarget(options.target);
const envs = expandTarget(target);
const results = [];
for (const env of envs) {
const { dm } = await loadProjectDM(env);
results.push({ env, ...await dm.diff() });
}
if (format === "text") {
for (const result of results) {
const prefix = envs.length > 1 ? `[${result.env}] ` : "";
if (result.snapshotReflectsCloud) {
logger.info(`${prefix}Snapshot matches cloud (${result.env}).`);
} else {
for (const a of result.delta.addedInSnapshot)
logger.info(`${prefix} + ${a.type}:${a.alias}`);
for (const a of result.delta.removedInSnapshot)
logger.info(`${prefix} - ${a.type}:${a.alias}`);
for (const a of result.delta.changedInSnapshot)
logger.info(`${prefix} ~ ${a.type}:${a.alias}`);
}
}
return {};
}
return { data: { results }, extras: { target } };
}
});
}
async function runStatusCommand(options, typeFilter) {
await runDependencyCommand({
options,
run: async (logger) => {
const format = parseFormat(options.format);
const parsed = parseTarget(options.target);
if (parsed === "all") {
throw new DependencyError({
code: "MISSING_INPUT",
message: `'all' is not valid for --target on this command. Use 'dev' or 'prod'.`
});
}
const target = parsed;
const agentRoot = await findAgentRootOrFail(process.cwd());
const { dependencies } = await import("./chunk-ka3e16hs.js");
const snapshot = await new dependencies.DependencySnapshotStore({ projectPath: agentRoot }).readOrEmpty(target, {
tolerant: true
});
const all = await dependencies.resolveDependencyStatuses({ snapshot });
const matching = all.filter((d) => d.type === typeFilter);
if (format === "text") {
if (matching.length === 0) {
logger.info(`No ${typeFilter}s installed in ${target}.`);
} else {
for (const d of matching) {
logger.info(formatStatusLine(d));
const hint = remediationFor(d);
if (hint)
logger.info(` \u21B3 ${hint}`);
}
}
return {};
}
return { data: { dependencies: matching }, extras: { target } };
}
});
}
async function runDependencyCommand(opts) {
const format = parseFormat(opts.options.format);
const logger = createCliLogger({ format });
try {
const result = await opts.run(logger);
if (format === "json") {
const body = { ok: true, ...result.extras ?? {} };
if (result.data !== undefined)
body.data = result.data;
if (result.warnings?.length)
body.warnings = result.warnings;
logger.info("").result(body);
} else if (result.data !== undefined) {
logger.info(JSON.stringify(result.data, null, 2));
}
} catch (err) {
const code = exitCodeFor(err);
const json = JSON.parse(formatJsonError(err));
const wrapped = err instanceof Error ? err : new Error(String(err));
wrapped.exitCode = code;
wrapped.json = json;
if (isDependencyErrorLike(err) && err.suggestion) {
wrapped.suggestion = err.suggestion;
}
throw wrapped;
}
}
export { parseTarget, parseFormat, parseKeyValueOptions, confirmProdAndRetry, loadProjectDM, resolveDependencyRegistryManagerOptions, runCopyCommand, runDiffCommand, runStatusCommand, runDependencyCommand };