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