UNPKG

@botpress/adk-cli

Version:

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

343 lines (340 loc) 11.5 kB
// @bun import { userInput } from "./chunk-tefbm840.js"; import { resolveCommandContext } from "./chunk-3ahwp6fe.js"; import { ADK_LOGO_LINE1, ADK_LOGO_LINE2 } from "./chunk-9e2nksab.js"; import { bold, box, fg, getActiveTheme, mount, router, signal, t, text } from "./chunk-m2h26j5f.js"; import { createCliLogger } from "./chunk-gzwt1qdr.js"; import { AdkError, AgentProject, ConfigManager, coerceConfigValue } from "./chunk-p0hjqn4r.js"; // src/utils/config-validator.ts async function resolveConfigContext(options) { let project = options.project; let credentials = options.credentials; let client = options.client; if (!project || !credentials || options.createClient && !client) { const context = await resolveCommandContext({ cwd: options.projectPath, target: options.isProd ? "prod" : "dev", require: options.createClient ? ["workspace"] : [], createClient: options.createClient }); project ??= context.project; credentials ??= context.credentials; client ??= context.client; } if (!project || !credentials) { throw new AdkError({ code: "CONFIG_VALIDATION_FAILED", message: "Unable to resolve project credentials for configuration validation.", expected: true }); } return { project, credentials, ...client ? { client } : {} }; } async function validateRemoteConfig(projectPath, botId, configSchema, options = {}) { try { const { client } = await resolveConfigContext({ projectPath, isProd: options.isProd ?? true, project: options.project, credentials: options.credentials, client: options.client, createClient: true }); if (!client) { throw new AdkError({ code: "CONFIG_VALIDATION_FAILED", message: "Unable to create a client for remote configuration validation.", expected: true }); } const { bot } = await client.getBot({ id: botId }); const remoteConfig = bot.configuration?.data || {}; const result = configSchema.safeParse(remoteConfig); if (result.success) { return { valid: true, errors: [] }; } const errors = []; for (const issue of result.error.issues) { const key = issue.path.join("."); errors.push(`${key}: ${issue.message}`); } return { valid: false, errors }; } catch (error) { return { valid: false, errors: [`Failed to fetch remote configuration: ${error}`] }; } } async function validateAndPromptConfig(options) { const { projectPath, isProd, interactive = true, botId } = options; const project = options.project ?? await AgentProject.load(projectPath); const configSchema = project.config?.configuration?.schema; if (!configSchema) { return { valid: true, config: {}, errors: [] }; } if (!configSchema?._def?.typeName || configSchema._def.typeName !== "ZodObject") { throw new AdkError({ code: "INVALID_CONFIG_SCHEMA", message: "Configuration schema must be a z.object()", expected: true }); } const logger = createCliLogger(); const { credentials, client } = await resolveConfigContext({ projectPath, isProd, project, credentials: options.credentials, client: options.client, createClient: !!botId }); const currentBotId = botId || (isProd ? project.agentInfo?.botId : project.agentInfo?.devId); if (!currentBotId) { logger.warn(`\u26A0 No ${isProd ? "production" : "development"} bot ID found`); return { valid: false, config: {}, errors: ["No bot ID found"] }; } const configManager = new ConfigManager(currentBotId, { project, credentials }); if (botId) { const remoteValidation = await validateRemoteConfig(projectPath, botId, configSchema, { isProd, project, credentials, client }); if (!remoteValidation.valid) { const actualErrors = remoteValidation.errors.filter((error) => !error.includes("Required") && !error.includes("required")); if (actualErrors.length > 0) { logger.warn(` \u26A0 Remote bot configuration has invalid values:`); for (const error of actualErrors) { logger.error(` \u2022 ${error}`); } logger.info(` Please update the bot configuration in Botpress Studio. `, "gray"); } } } const validation = await configManager.validate(configSchema); if (validation.valid) { const config2 = await configManager.getAll(); if (interactive) { logger.info(`\u2713 Configuration validated successfully `, "green"); } return { valid: true, config: config2, errors: [] }; } if (!interactive) { return { valid: false, config: await configManager.getAll(), errors: validation.errors }; } const shape = configSchema.shape; const allKeys = Object.keys(shape); const currentConfig = await configManager.getAll(); const validKeys = []; const invalidKeys = []; const missingKeys = []; for (const key of allKeys) { const fieldSchema = shape[key]; const currentValue = currentConfig[key]; const description = fieldSchema.description; if (currentValue === undefined) { missingKeys.push({ key, description }); } else { const result = fieldSchema.safeParse(currentValue); if (result.success) { validKeys.push(key); } else { invalidKeys.push({ key, error: result.error.issues[0]?.message || "Invalid value", description }); } } } const keysToPrompt = [ ...missingKeys.map((m) => ({ ...m, error: undefined })), ...invalidKeys.map((i) => ({ key: i.key, description: i.description, error: i.error })) ]; if (keysToPrompt.length > 0) { await promptForAllConfigValues({ keys: keysToPrompt, validKeys, configManager, shape, schema: configSchema, isProd }); } const finalValidation = await configManager.validate(configSchema); const config = await configManager.getAll(); if (!finalValidation.valid) { return { valid: false, config, errors: finalValidation.errors }; } if (interactive) { logger.info(` \u2713 Configuration validated successfully `, "green"); } return { valid: true, config, errors: [] }; } function configPromptModal(renderer, scope, deps) { const { keys, validKeys, shape, isProd, configManager, onComplete, onError } = deps; const theme = getActiveTheme(); const pendingValues = {}; const index = signal(0); let saveStarted = false; const saveAll = async () => { try { const currentConfig = await configManager.getAll(); await configManager.save({ ...currentConfig, ...pendingValues }); onComplete(); } catch (error) { onError(error instanceof Error ? error : new Error(String(error))); } }; const logoLines = () => box(renderer, { flexDirection: "column", marginBottom: 1 }, [ text(renderer, t`${fg(theme.accent.purple)(bold(` ${ADK_LOGO_LINE1}`))}`), text(renderer, t`${fg(theme.accent.purple)(bold(` ${ADK_LOGO_LINE2}`))}`) ]); return router(renderer, scope, index, (i, viewScope) => { if (i >= keys.length) { if (!saveStarted) { saveStarted = true; saveAll(); } return box(renderer, { paddingX: 2, paddingY: 1 }, [ text(renderer, t`${fg(theme.text.dim)("Saving configuration...")}`) ]); } const currentKey = keys[i]; const fieldSchema = shape[currentKey.key]; const children = [logoLines()]; if (validKeys.length > 0) { children.push(box(renderer, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, [ text(renderer, t`${fg(theme.status.success)("\u2713 Valid configuration:")}`), ...validKeys.map((key) => box(renderer, { marginLeft: 2 }, [text(renderer, t`${fg(theme.status.success)(`\u2713 ${key}`)}`)])) ])); } children.push(box(renderer, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, [ text(renderer, t`${fg(theme.status.warning)(bold("\u26A0\uFE0F Configuration Validation Failed"))}`), text(renderer, t`${fg(theme.text.dim)(`Environment: ${isProd ? "production" : "development"}`)}`), text(renderer, t`${fg(theme.text.dim)(`Location: ${isProd ? ".adk/config.prod.json" : ".adk/config.dev.json"}`)}`) ])); children.push(box(renderer, { marginBottom: 1 }, [ text(renderer, t`${fg(theme.text.dim)("You can also set config non-interactively: ")}${fg(theme.accent.cyan)("adk config:set <key> <value>")}`) ])); children.push(box(renderer, { flexDirection: "column", marginTop: 1, marginBottom: 2 }, [ text(renderer, t`${fg(theme.status.warning)("\u26A0 Configuration to fix:")}`), ...keys.map((item, idx) => { const marker = idx < i ? t`${fg(theme.status.success)(`\u2713 ${item.key}`)}` : idx === i ? t`${fg(theme.accent.purple)(`\u25B6 ${item.key}`)}` : t`${fg(theme.text.dim)(`\u2022 ${item.key}`)}`; return box(renderer, { marginLeft: 2 }, [text(renderer, marker)]); }) ])); if (currentKey.error) { children.push(box(renderer, { marginBottom: 2 }, [ text(renderer, t`${fg(theme.status.error)(`Current value is invalid: ${currentKey.error}`)}`) ])); } children.push(box(renderer, { marginBottom: 2 }, [ text(renderer, t`${fg(theme.text.dim)(`Progress: ${i + 1}/${keys.length}`)}`) ])); const desc = currentKey.description ? ` (${currentKey.description})` : ""; children.push(userInput(renderer, viewScope, { prompt: `${currentKey.key}${desc}:`, validate: (raw) => { const trimmed = raw.trim(); if (!trimmed) return "Value cannot be empty"; const coerced = coerceConfigValue(trimmed, fieldSchema); const result = fieldSchema.safeParse(coerced); return result.success ? null : result.error.issues[0]?.message || "Invalid value"; }, onSubmit: (raw) => { const coerced = coerceConfigValue(raw.trim(), fieldSchema); const result = fieldSchema.safeParse(coerced); if (result.success) { pendingValues[currentKey.key] = result.data; index.set(index() + 1); } } })); return box(renderer, { flexDirection: "column", paddingX: 2, paddingY: 2 }, children); }); } async function promptForAllConfigValues(options) { if (options.keys.length === 0) return; return new Promise((resolve, reject) => { let appRef = null; const finish = (cb) => { setTimeout(() => { appRef?.unmount(); cb(); }, 80); }; mount((renderer, scope) => configPromptModal(renderer, scope, { ...options, onComplete: () => finish(resolve), onError: (error) => finish(() => reject(error)) }), { exitOnCtrlC: true }).then((app) => { appRef = app; }); }); } async function getDeploymentConfig(projectPath, isProd, options = {}) { const project = options.project ?? await AgentProject.load(projectPath); const botId = isProd ? project.agentInfo?.botId : project.agentInfo?.devId; if (!botId) { return {}; } const { credentials } = await resolveConfigContext({ projectPath, isProd, project, credentials: options.credentials }); const configManager = new ConfigManager(botId, { project, credentials }); return await configManager.getAll(); } export { validateRemoteConfig, validateAndPromptConfig, getDeploymentConfig };