UNPKG

@botpress/adk-cli

Version:

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

362 lines (359 loc) 12.1 kB
// @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 };