UNPKG

@botpress/adk-cli

Version:

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

680 lines (675 loc) 25 kB
// @bun import { displayWorkspaceInfo } from "./chunk-5fy9y5qt.js"; import { buildAndUploadComponents, computeEvalManifestPlan, refreshDependencySnapshotOnce, runProdDeployPipeline } from "./chunk-xkcrb68a.js"; import { adkBuild } from "./chunk-ttvctaf1.js"; import"./chunk-wmec2w0t.js"; import { identifyUser } from "./chunk-6zha718h.js"; import { ensureJsonOnlyFormat } from "./chunk-7sfagm12.js"; import"./chunk-r72adjnh.js"; import { telemetry_default } from "./chunk-kwmsaz7n.js"; import"./chunk-26vqkz52.js"; import { resolveCommandContext } from "./chunk-3ahwp6fe.js"; import { ADK_LOGO_LINE1, ADK_LOGO_LINE2 } from "./chunk-9e2nksab.js"; import"./chunk-m2h26j5f.js"; import"./chunk-8gqzjqmb.js"; import"./chunk-kk3h6qaj.js"; import { createCliLogger, source_default } from "./chunk-gzwt1qdr.js"; import"./chunk-nxy2ya5r.js"; import { sanitizeErrorMessage } from "./chunk-wzj4dc7n.js"; import { AdkError, PreflightChecker, SecretsManager, assertNoBlockingDependencies, isAdkError, pluralize2, summarizeBlockingDependencies } from "./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 { __require } from "./chunk-dhs2bg35.js"; // src/utils/preflight-formatter.ts var purple = source_default.hex("#D2A6FF"); function formatValue(value) { if (value === undefined || value === null) { return source_default.gray("(not set)"); } if (typeof value === "string") { return `"${value}"`; } if (typeof value === "boolean") { return value ? source_default.green("true") : source_default.red("false"); } if (Array.isArray(value)) { return `[${value.map((v) => formatValue(v)).join(", ")}]`; } if (typeof value === "object") { return JSON.stringify(value); } return String(value); } function pushSecretWarnings(lines, warnings, env) { const required = warnings.filter((w) => !w.optional); const optional = warnings.filter((w) => w.optional); const flag = env === "prod" ? " --prod" : ""; const label = env === "prod" ? " (prod)" : ""; lines.push(source_default.bold.yellow(`Missing Secrets${label}`) + ` `); if (required.length > 0) { lines.push(source_default.red(" Required (must be set before use):") + ` `); for (const warning of required) { const desc = warning.description ? `: ${warning.description}` : ""; lines.push(` \u2014 ${warning.name}${desc}`); lines.push(` ${source_default.gray("Set with:")} ${source_default.cyan(`adk secret:set ${warning.name}${flag}`)} `); } } if (optional.length > 0) { lines.push(source_default.yellow(" Optional (can be set later):") + ` `); for (const warning of optional) { const desc = warning.description ? `: ${warning.description}` : ""; lines.push(` \u2014 ${warning.name}${desc}`); lines.push(` ${source_default.gray("Set with:")} ${source_default.cyan(`adk secret:set ${warning.name}${flag}`)} `); } } if (env === "dev") { lines.push(source_default.gray(" For local development, export SECRET_<NAME>=... in your environment.")); } lines.push(source_default.gray(" Never commit .env files containing secrets to version control.") + ` `); } class PreflightFormatter { static format(result) { const lines = []; lines.push(""); lines.push(purple(` ${ADK_LOGO_LINE1} Botpress ADK`)); lines.push(purple(` ${ADK_LOGO_LINE2} Preflight Check`)); lines.push(""); lines.push(purple("Running preflight checks...")); lines.push(""); if (result.agentConfig.length > 0) { lines.push(source_default.bold("Agent Configuration Differences")); lines.push(source_default.gray("(These are top-level agent settings)") + ` `); for (const diff of result.agentConfig) { lines.push(` agent.${diff.field}:`); lines.push(` ${source_default.red(`- Remote: ${formatValue(diff.oldValue)}`)}`); lines.push(` ${source_default.green(`+ Local: ${formatValue(diff.newValue)}`)} `); } } if (result.secretWarnings && result.secretWarnings.length > 0) { pushSecretWarnings(lines, result.secretWarnings, result.env); } lines.push(source_default.bold("Summary of Actions") + ` `); const agentConfig = result.agentConfig.length; if (agentConfig > 0) { lines.push(` \u2022 ${source_default.cyan("Agent config:")} ${pluralize2(agentConfig, "change")}`); } lines.push(""); return lines.join(` `); } static formatPrompt() { return source_default.bold.red(`\u2753 Apply these changes before continuing? `) + source_default.gray(`[y] apply \xB7 [n] deploy without applying \xB7 [Esc] cancel deployment `) + source_default.gray("Proceed? [y/N] "); } } // src/utils/configuration-size-warning.ts var CONFIGURATION_DATA_WARNING_THRESHOLD_BYTES = 9 * 1024; function getSerializedConfigurationDataSize(config) { return new TextEncoder().encode(JSON.stringify(config)).byteLength; } function getConfigurationDataSizeWarning(config) { const serializedBytes = getSerializedConfigurationDataSize(config); if (serializedBytes <= CONFIGURATION_DATA_WARNING_THRESHOLD_BYTES) { return null; } return { serializedBytes, thresholdBytes: CONFIGURATION_DATA_WARNING_THRESHOLD_BYTES }; } function formatConfigurationDataSizeWarning(warning) { return ` \u26A0\uFE0F Serialized configuration data is ${warning.serializedBytes.toLocaleString("en-US")} bytes. ` + ` Configurations above ${warning.thresholdBytes / 1024} KiB may exceed runtime request-header limits and make the deployed bot unresponsive. ` + " Move large values to tables, files, or another storage service before deploying."; } // src/commands/adk-deploy.ts var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); var PROGRESS_PREFIXES = ["\u25CB", "\u2714", "\u2713", "\u256D", "\u2502", "\u2570"]; function extractDeployErrorMessage(error) { if (!(error instanceof Error)) { return String(error); } const extra = error; if (typeof extra.stderr === "string" && extra.stderr.trim()) { const lines = extra.stderr.trim().split(` `); for (let i = lines.length - 1;i >= 0; i--) { const line = lines[i].replace(ANSI_RE, "").trim(); if (line && !PROGRESS_PREFIXES.some((p) => line.startsWith(p))) { return line; } } } return error.message; } function formatDeployPlan(plan, logger, options) { const hint = options?.showConfirmHints ? " (requires --confirm-storage-changes)" : ""; const integrationVersionMismatches = plan.dependencyPlan.integrationVersionMismatches; if (integrationVersionMismatches.length > 0) { logger.warn(` \u26A0\uFE0F Integration versions differ between dev and prod:`); for (const mismatch of integrationVersionMismatches) { logger.warn(` \u2022 ${mismatch.alias}: dev ${mismatch.devVersion} \u2192 prod ${mismatch.prodVersion}`); } logger.warn(" Run `adk integrations copy --from dev --to prod --dry-run` to preview, then `adk integrations copy --from dev --to prod --yes` to promote dev integrations to prod."); } if (plan.preflight.result.hasChanges) { logger.info(PreflightFormatter.format(plan.preflight.result)); } if (plan.tablePlan?.hasChanges) { logger.info(` \uD83D\uDCCA Tables:`, "cyan"); if (plan.tablePlan.totalCreate > 0) logger.info(` \u2022 ${plan.tablePlan.totalCreate} to create`, "green"); if (plan.tablePlan.totalUpdate > 0) logger.info(` \u2022 ${plan.tablePlan.totalUpdate} to update`, "cyan"); if (plan.tablePlan.totalDelete > 0) logger.warn(` \u2022 ${plan.tablePlan.totalDelete} to delete${hint}`); } if (plan.kbPlan?.hasChanges) { logger.info(` \uD83D\uDCDA Knowledge bases:`, "cyan"); logger.info(` \u2022 ${plan.kbPlan.toSync} to sync, ${plan.kbPlan.toSkip} up to date`); if (plan.kbPlan.orphanedSourcesToDelete > 0) { logger.warn(` \u2022 ${plan.kbPlan.orphanedSourcesToDelete} orphaned sources to remove${hint}`); } } if (plan.orphanedKBs.length > 0) { logger.warn(` \u26A0\uFE0F ${plan.orphanedKBs.length} remote KB(s) not defined locally:`); for (const kb of plan.orphanedKBs) { logger.warn(` \u2022 ${kb.name}`); } if (options?.showConfirmHints) { logger.warn(` Requires --confirm-storage-changes to delete`); } } if (plan.assetPlan?.hasChanges) { logger.info(` \uD83D\uDCC1 Assets:`, "cyan"); if (plan.assetPlan.totalCreate > 0) logger.info(` \u2022 ${plan.assetPlan.totalCreate} to upload`, "green"); if (plan.assetPlan.totalUpdate > 0) logger.info(` \u2022 ${plan.assetPlan.totalUpdate} to update`, "cyan"); if (plan.assetPlan.totalDelete > 0) logger.warn(` \u2022 ${plan.assetPlan.totalDelete} to delete${hint}`); } const evalCount = options?.evalManifestPlan?.evalCount ?? 0; if (evalCount > 0) { logger.info(` \uD83E\uDDEA Evals:`, "cyan"); logger.info(` \u2022 ${evalCount} eval manifest ${evalCount === 1 ? "entry" : "entries"} to publish`, "green"); } } function logPipelineStageStart(stage, logger) { switch (stage) { case "deploy": logger.info(` \uD83D\uDCE4 Deploying bot to Botpress...`, "cyan"); return; case "manifest": logger.info(` \uD83E\uDDFE Publishing prod metadata...`, "cyan"); return; case "kb-sync": logger.info(` \uD83D\uDCDA Syncing knowledge bases...`, "cyan"); return; case "tables": logger.info(` \uD83D\uDCCA Syncing tables...`, "cyan"); return; case "assets": logger.info(` \uD83D\uDCC1 Syncing assets...`, "cyan"); return; case "eval-manifest": logger.info(` \uD83E\uDDEA Publishing eval manifest...`, "cyan"); return; case "preflight": return; } } function logPipelineStageComplete(stage, detail, logger, deployment) { switch (stage) { case "deploy": logger.info(` \u2705 Agent deployed successfully!`, "green"); logger.info(`\uD83C\uDF10 Your agent is now live on Botpress`, "gray"); logger.info(`\uD83E\uDD16 Bot ID: ${deployment.botId}`, "gray"); logger.info(`\uD83C\uDFE2 Workspace ID: ${deployment.workspaceId}`, "gray"); return; case "manifest": if (typeof detail?.agentMapSnapshotWarning === "string") { logger.warn(`\u26A0\uFE0F Failed to publish Agent Map metadata: ${detail.agentMapSnapshotWarning}`); logger.warn(" Redeploy again to retry publishing Agent Map metadata."); } logger.info("\u2705 Prod metadata published", "green"); return; case "kb-sync": logger.info(` \u2705 Knowledge bases synced${formatCountDetail(detail, ["synced", "skipped"])}`, "green"); return; case "tables": logger.info(` \u2705 Tables synced${formatCountDetail(detail, ["created", "updated", "deleted", "skipped"])}`, "green"); return; case "assets": logger.info(` \u2705 Assets synced${formatCountDetail(detail, ["created", "updated", "deleted", "skipped"])}`, "green"); return; case "eval-manifest": { const uploaded = typeof detail?.uploaded === "number" ? detail.uploaded : 0; if (uploaded > 0) { logger.info(`\u2705 Eval manifest uploaded (${uploaded} eval${uploaded === 1 ? "" : "s"})`, "green"); } else { logger.info(" No evals to publish", "gray"); } return; } case "preflight": return; } } function logPipelineStageError(stage, error, logger, event) { const message = error instanceof Error ? error.message : String(error); if (stage === "manifest" && event?.nonFatal) { logger.warn(`\u26A0\uFE0F Failed to publish prod metadata: ${message}`); logger.warn(" The bot was deployed. Run deploy again to retry publishing prod metadata."); return; } if (stage === "deploy") { logger.error(`\u274C Failed to deploy bot: ${message}`); return; } if (stage === "preflight") { logger.error(`\u274C Failed to apply config changes: ${message}`); return; } if (stage === "eval-manifest") { logger.error(`\u274C Failed to publish eval manifest: ${message}`); return; } const label = stage === "kb-sync" ? "knowledge bases" : stage; logger.error(`\u274C Failed to sync ${label}: ${message}`); if (stage === "tables") { for (const failure of getTableSyncFailureDetails(event?.detail)) { logger.error(` \u2022 ${failure.table} (${failure.operation}, ${failure.errorType}): ${failure.message}`); } } } function getTableSyncFailureDetails(detail) { if (!Array.isArray(detail?.failures)) return []; return detail.failures.filter((failure) => { if (!failure || typeof failure !== "object") return false; const value = failure; return typeof value.table === "string" && typeof value.operation === "string" && typeof value.errorType === "string" && typeof value.message === "string"; }); } function formatCountDetail(detail, keys) { if (!detail) return ""; const parts = keys.map((key) => typeof detail[key] === "number" && detail[key] > 0 ? `${detail[key]} ${key}` : null).filter((part) => Boolean(part)); return parts.length > 0 ? `: ${parts.join(", ")}` : ""; } async function promptUserApproval() { const { promptApproval } = await import("./chunk-6afthdr4.js"); return promptApproval(PreflightFormatter.formatPrompt(), process.stdout); } async function adkDeploy(environment = "production", options = {}) { ensureJsonOnlyFormat(options.format); const isJson = options.format === "json"; const logger = createCliLogger({ format: options.format }); if (isJson && !options.autoApprove && !options.dryRun) { throw new AdkError({ code: "JSON_REQUIRES_YES", message: "--format json requires --yes or --dry-run flag", expected: true }); } logger.info("\uD83D\uDE80 Deploying ADK agent to Botpress...", "blue"); logger.info(`Environment: ${environment}`, "gray"); let stage = "auth"; try { stage = "load"; const context = await resolveCommandContext({ target: "prod", projectLoadOptions: { adkCommand: "adk-deploy" }, require: ["project", "credentials", "workspace", "bot"] }); const agentRoot = context.agentRoot; const project = context.project; const botId = context.botId; const workspaceId = context.workspaceId; const credentials = context.credentials; const client = context.client; await refreshDependencySnapshotOnce({ projectPath: agentRoot, env: "prod", botId, client, logger, event: "dependency-refresh", required: true }); await refreshDependencySnapshotOnce({ projectPath: agentRoot, env: "dev", botId: project.agentInfo?.devId, client, logger, event: "dependency-refresh", required: false }); await identifyUser(agentRoot).catch(() => {}); if (!isJson) { await displayWorkspaceInfo({ context }); } stage = "build"; logger.info(` \uD83C\uDFD7\uFE0F Building agent project...`, "cyan"); await adkBuild({ silent: true, adkCommand: "adk-deploy", beforeGenerate: project.customComponents.length > 0 ? async () => { logger.info(` \uD83E\uDDE9 Building and uploading custom components...`, "cyan"); await buildAndUploadComponents({ agentRoot, project, client, log: (msg) => logger.info(` ${msg}`, "gray") }); } : undefined }); stage = "config"; logger.info(` \uD83D\uDD27 Validating configuration...`, "cyan"); let configurationData = {}; try { const { validateAndPromptConfig } = await import("./chunk-jzzzp7t0.js"); const configResult = await validateAndPromptConfig({ projectPath: agentRoot, isProd: true, interactive: !isJson, botId, project, credentials, client }); configurationData = configResult.config ?? {}; if (!configResult.valid) { logger.error(` \u2717 Configuration validation failed:`); configResult.errors.forEach((err) => logger.error(` \u2022 ${err}`)); throw new AdkError({ code: "CONFIG_VALIDATION_FAILED", message: "Configuration validation failed", expected: true, details: { errors: configResult.errors } }); } } catch (error) { if (!(isAdkError(error) && error.code === "INVALID_CONFIG_SCHEMA")) { throw error; } const { bot } = await client.getBot({ id: botId }); configurationData = bot.configuration?.data ?? {}; } const configurationDataSizeWarning = getConfigurationDataSizeWarning(configurationData); if (configurationDataSizeWarning) { logger.warn(formatConfigurationDataSizeWarning(configurationDataSizeWarning)); } stage = "preflight"; logger.info("\uD83D\uDD0D Computing deploy plan...", "cyan"); const checker = new PreflightChecker(agentRoot, { credentials }); const plan = await checker.computeDeployPlan(botId); const evalManifestPlan = await computeEvalManifestPlan(agentRoot); if (!options.dryRun) { assertNoBlockingDependencies(plan, { allowUnconfigured: options.allowUnconfigured }); } const hasAnyChanges = plan.preflight.result.hasChanges || plan.tablePlan?.hasChanges || plan.kbPlan?.hasChanges || plan.orphanedKBs.length > 0 || plan.assetPlan?.hasChanges; if (options.dryRun) { if (isJson) { const jsonPlan = { preflight: { hasChanges: plan.preflight.result.hasChanges, agentConfig: plan.preflight.result.agentConfig, secretWarnings: plan.preflight.result.secretWarnings }, tables: plan.tablePlan ? { hasChanges: plan.tablePlan.hasChanges, totalCreate: plan.tablePlan.totalCreate, totalUpdate: plan.tablePlan.totalUpdate, totalDelete: plan.tablePlan.totalDelete } : null, knowledgeBases: plan.kbPlan ? { hasChanges: plan.kbPlan.hasChanges, toSync: plan.kbPlan.toSync, toSkip: plan.kbPlan.toSkip, orphanedKBs: plan.orphanedKBs.map((kb) => kb.name) } : null, assets: plan.assetPlan ? { hasChanges: plan.assetPlan.hasChanges, totalCreate: plan.assetPlan.totalCreate, totalUpdate: plan.assetPlan.totalUpdate, totalDelete: plan.assetPlan.totalDelete } : null, dependencies: { blocking: summarizeBlockingDependencies(plan.dependencyPlan.blocking), integrationVersionMismatches: plan.dependencyPlan.integrationVersionMismatches }, evalManifest: { evalCount: evalManifestPlan.evalCount }, hasDestructiveStorageChanges: plan.hasDestructiveStorageChanges }; logger.info("").result(jsonPlan); } else { formatDeployPlan(plan, logger, { showConfirmHints: true, evalManifestPlan }); if (plan.hasDestructiveStorageChanges) { logger.warn(` Destructive storage changes require: --confirm-storage-changes`); } logger.info(` \uD83D\uDD0D Dry run \u2014 no changes applied.`); } return; } let interactivelyApproved = false; let planApproved = true; if (hasAnyChanges) { formatDeployPlan(plan, logger, { evalManifestPlan }); if (options.autoApprove) { if (plan.hasDestructiveStorageChanges && !options.confirmStorageChanges) { logger.error(` \u2717 Destructive storage changes require explicit confirmation:`); logger.error(` --confirm-storage-changes`); throw new AdkError({ code: "STORAGE_CONFIRM_REQUIRED", message: "Destructive storage changes require --confirm-storage-changes", expected: true }); } logger.info(` \u2713 Auto-approved. Applying changes... `, "green"); } else { const choice = await promptUserApproval(); if (choice === "cancel") { logger.error(` \u2717 Deployment cancelled by user. `); return; } if (choice === "yes") { interactivelyApproved = true; } else { planApproved = false; logger.warn(` \u26A0\uFE0F Plan updates declined \u2014 deploying the bot code without applying these changes.`); logger.warn(" Your bot may not work as expected until the plan updates are applied."); logger.warn(" Re-run `adk deploy` and approve the plan to apply them.\n"); } } } else { formatDeployPlan(plan, logger, { evalManifestPlan }); if (evalManifestPlan.evalCount > 0) logger.info(""); logger.info(`\u2713 No resource changes detected. Proceeding... `, "gray"); } stage = "secrets"; const declaredSecrets = project.config?.secrets ?? {}; const secretsManager = new SecretsManager(project.path); const prodSecrets = await secretsManager.getAll("prod", declaredSecrets); const missingRequired = Object.entries(declaredSecrets).filter(([name, def]) => !def.optional && !(name in prodSecrets)).map(([name]) => name); if (missingRequired.length > 0) { throw new AdkError({ code: "MISSING_SECRETS", message: `Missing required prod secrets: ${missingRequired.join(", ")} Set them with: ${missingRequired.map((n) => `adk secret:set ${n} <value> --prod`).join(", ")}`, expected: true }); } const confirmStorage = interactivelyApproved || (options.confirmStorageChanges ?? false); let deployTelemetryTracked = false; const deployResult = await runProdDeployPipeline({ agentRoot, project, plan, botId, workspaceId, credentials, secrets: Object.keys(prodSecrets).length > 0 ? prodSecrets : undefined, client, confirmStorageChanges: confirmStorage, applyPlanUpdates: planApproved, evalManifestPlan, callbacks: { onStageStart: (nextStage) => { stage = nextStage; logPipelineStageStart(nextStage, logger); }, onStageComplete: (completedStage, detail) => { logPipelineStageComplete(completedStage, detail, logger, { botId, workspaceId }); if (completedStage === "deploy" && !deployTelemetryTracked) { deployTelemetryTracked = true; telemetry_default.track("deploy", { success: true, botId, workspaceId, environment, autoApproved: options.autoApprove, planDeclined: hasAnyChanges ? !planApproved : undefined, primitive_count: project.actions.length + project.workflows.length + project.conversations.length + project.triggers.length + project.tables.length }); } }, onStageError: (failedStage, error, event) => { logPipelineStageError(failedStage, error, logger, event); }, onDeployCommand: (deployCommand) => { if (!isJson) { deployCommand.on("stdout", (data) => logger.stdout(data)); deployCommand.on("stderr", (data) => logger.stderr(data)); } }, onPreflightProgress: (msg) => logger.info(`\uD83D\uDD04 ${msg}`, "cyan"), onPreflightSuccess: (msg) => logger.info(`\u2705 ${msg}`, "green"), onPreflightError: (msg) => logger.error(`\u274C ${msg}`) } }); if (!planApproved) { logger.warn("\n\u26A0\uFE0F Plan updates were not applied \u2014 run `adk deploy` again and approve to sync your config, tables, KBs, and assets."); } logger.info("\uD83C\uDF89 Deployment complete.").result({ success: deployResult.success, botId, workspaceId, environment, ...deployResult.tableFailures ? { tableFailures: deployResult.tableFailures } : {} }); } catch (error) { if (error instanceof Error) { const annotated = error; const errorStage = annotated.stage ?? stage; annotated.stage = errorStage; if (errorStage === "deploy") { annotated.detail = sanitizeErrorMessage(extractDeployErrorMessage(error)); } } throw error; } } export { adkDeploy };