@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
622 lines (613 loc) • 21.3 kB
JavaScript
// @bun
import {
execAsync
} from "./chunk-bmbzs4s8.js";
import {
checkAgent0CapabilitiesStatus,
installAgent0Capabilities
} from "./chunk-tt572q4r.js";
import {
resolveCommandContext
} from "./chunk-3ahwp6fe.js";
import {
getRuntimeMajor,
getRuntimeMajorRelation,
getRuntimeVersionReport
} from "./chunk-ty7sdgd4.js";
import {
detectPackageManagers,
getPreferredPackageManager
} from "./chunk-nbasj5jm.js";
import {
EXPECTED_RUNTIME_VERSION
} from "./chunk-nxy2ya5r.js";
import {
AdkError,
ConfigWriter,
exports_dependencies
} from "./chunk-p0hjqn4r.js";
import {
require_semver
} from "./chunk-2a5b6azq.js";
import {
Uk
} from "./chunk-3xrpxgq4.js";
import {
__toESM
} from "./chunk-dhs2bg35.js";
// src/agent-project-patcher/types.ts
var UNKNOWN_AGENT_PROJECT_VERSION = "0.0.0";
// src/agent-project-patcher/agent-project-patcher.ts
var semver = __toESM(require_semver(), 1);
// src/agent-project-patcher/patches/agent0-capabilities-bootstrap.ts
function agent0Reason(status) {
if (status.state === "missing") {
return "Agent(0) capabilities manifest is missing or invalid";
}
if (status.state === "stale") {
return status.installedVersion ? `Agent(0) capabilities are stale (${status.installedVersion} -> ${status.currentVersion})` : "Agent(0) capabilities are stale";
}
return `Agent(0) capabilities are current (${status.installedVersion})`;
}
async function detectAgent0CapabilitiesBootstrapPatch(ctx) {
const status = (ctx.checkAgent0CapabilitiesStatus ?? checkAgent0CapabilitiesStatus)(ctx.projectPath);
if (status.state !== "missing") {
return {
state: "skipped",
reason: agent0Reason(status),
details: { status }
};
}
return {
state: "pending",
reason: agent0Reason(status),
details: { status }
};
}
async function applyAgent0CapabilitiesBootstrapPatch(ctx) {
await (ctx.installAgent0Capabilities ?? installAgent0Capabilities)(ctx.projectPath);
}
var agent0CapabilitiesBootstrapPatch = {
id: "agent0-capabilities-bootstrap",
introducedIn: "2.0.0",
title: "Agent(0) capabilities",
description: "Create the project-local Agent(0) capability bundle when it is missing.",
detect: detectAgent0CapabilitiesBootstrapPatch,
apply: applyAgent0CapabilitiesBootstrapPatch
};
// src/agent-project-patcher/patches/dependency-snapshots.ts
import * as fs from "fs/promises";
import * as path from "path";
import ts from "typescript";
async function fileExists(filePath) {
return fs.access(filePath).then(() => true).catch(() => false);
}
function propertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) {
return name.text;
}
return null;
}
function hasLegacyDependenciesField(content) {
const source = ts.createSourceFile("agent.config.ts", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
let found = false;
const visit = (node) => {
if (found) {
return;
}
if (ts.isPropertyAssignment(node) && propertyNameText(node.name) === "dependencies") {
found = true;
return;
}
if (ts.isShorthandPropertyAssignment(node) && node.name.text === "dependencies") {
found = true;
return;
}
ts.forEachChild(node, visit);
};
ts.forEachChild(source, visit);
return found;
}
async function readConfig(projectPath) {
try {
return await fs.readFile(path.join(projectPath, "agent.config.ts"), "utf8");
} catch (error) {
if (error.code === "ENOENT")
return null;
throw error;
}
}
async function detectDependencySnapshotPatch(ctx) {
const markerPath = path.join(ctx.projectPath, ".adk", "dependencies", "migration.json");
if (await fileExists(markerPath)) {
return {
state: "skipped",
reason: "migration marker exists",
details: { markerPath }
};
}
const config = await readConfig(ctx.projectPath);
const hasConfigDependencies = config ? hasLegacyDependenciesField(config) : false;
const legacyLockFiles = (await Promise.all(["dependencies.dev.lock.json", "dependencies.prod.lock.json"].map(async (name) => ({
name,
exists: await fileExists(path.join(ctx.projectPath, name))
})))).filter((item) => item.exists).map((item) => item.name);
if (!hasConfigDependencies && legacyLockFiles.length === 0) {
return {
state: "skipped",
reason: "no legacy dependency state detected",
details: { markerPath, legacyLockFiles }
};
}
return {
state: "pending",
reason: hasConfigDependencies ? "agent.config.ts contains a legacy dependencies field" : "legacy dependency lock file exists",
details: { markerPath, legacyLockFiles, hasConfigDependencies }
};
}
function createMigrationClient(opts) {
return new Uk({
token: opts.credentials.token,
apiUrl: opts.apiUrl,
workspaceId: opts.workspaceId,
headers: {
"x-multiple-integrations": "true"
}
});
}
async function getMigrationContext(ctx) {
if (ctx.migrationContext) {
return ctx.migrationContext;
}
const resolveContext = ctx.resolveContext ?? resolveCommandContext;
return resolveContext({
cwd: ctx.projectPath,
target: "dev",
require: ["project", "credentials", "workspace"],
createClient: false
});
}
async function applyDependencySnapshotPatch(ctx) {
const context = await getMigrationContext(ctx);
if (!context.workspaceId) {
throw new AdkError({
code: "WORKSPACE_ID_MISSING",
message: "No workspace ID found for dependency migration. Run `adk link` to link your agent first.",
expected: true,
suggestion: "Run `adk link` first."
});
}
const clientFactory = ctx.clientFactory ?? createMigrationClient;
const migrateFromConfig = ctx.migrateFromConfig ?? exports_dependencies.migrateFromConfig;
const client = clientFactory({
credentials: context.credentials,
apiUrl: context.apiUrl,
workspaceId: context.workspaceId
});
const result = await migrateFromConfig({
projectPath: ctx.projectPath,
client
});
for (const warning of result.warnings) {
ctx.logger?.warn(warning.message, { event: "agent-project-patch", patch: "dependency-snapshots" });
}
}
var dependencySnapshotsPatch = {
id: "dependency-snapshots",
introducedIn: "2.0.0",
title: "Dependency snapshots",
description: "Move legacy dependency state to Cloud-backed .adk dependency snapshots.",
detect: detectDependencySnapshotPatch,
apply: applyDependencySnapshotPatch
};
// src/agent-project-patcher/patches/integration-config-format.ts
function manualIntegrationConfigReason(ctx, aliases) {
return `Deprecated integration config entries require manual updates: ${aliases.join(", ")}. ` + "Update those entries in agent.config.ts to string shorthand, then verify integration settings in the Control Panel: " + `http://localhost:${ctx.adkDevConsolePortStr ?? "3001"}/integrations.`;
}
async function detectIntegrationConfigPatch(ctx) {
const configWriter = new ConfigWriter(ctx.projectPath);
const { deprecatedAliases, migratableAliases } = configWriter.getIntegrationConfigMigrationState();
if (deprecatedAliases.length === 0) {
return {
state: "skipped",
reason: "no deprecated integration object entries detected",
details: { deprecatedAliases, migratableAliases }
};
}
if (migratableAliases.length === 0) {
return {
state: "blocked",
reason: manualIntegrationConfigReason(ctx, deprecatedAliases),
details: { deprecatedAliases, migratableAliases }
};
}
return {
state: "pending",
reason: "agent.config.ts contains deprecated integration object entries that can be migrated automatically",
details: { deprecatedAliases, migratableAliases }
};
}
async function applyIntegrationConfigPatch(ctx) {
const configWriter = new ConfigWriter(ctx.projectPath);
const migratedAliases = await configWriter.migrateIntegrationsToStringFormat();
const { deprecatedAliases: remainingAliases } = configWriter.getIntegrationConfigMigrationState();
if (migratedAliases.length > 0) {
ctx.logger?.info(`Migrated ${migratedAliases.length} integration config entr${migratedAliases.length === 1 ? "y" : "ies"} to string shorthand`, {
event: "agent-project-patch",
patch: integrationConfigFormatPatch.id,
aliases: migratedAliases
});
}
if (remainingAliases.length > 0) {
ctx.logger?.warn(`Some deprecated integration config entries still need manual updates: ${remainingAliases.join(", ")}. ` + `Open http://localhost:${ctx.adkDevConsolePortStr ?? "3001"}/integrations to verify settings.`, { event: "agent-project-patch", patch: integrationConfigFormatPatch.id, aliases: remainingAliases });
}
}
var integrationConfigFormatPatch = {
id: "integration-config-format",
introducedIn: "1.18.0",
title: "Integration config format",
description: "Rewrite deprecated integration object entries to string shorthand when safe.",
detect: detectIntegrationConfigPatch,
apply: applyIntegrationConfigPatch
};
// src/agent-project-patcher/patches/runtime-package-versions.ts
import * as fs2 from "fs/promises";
import * as path2 from "path";
var ADK_PACKAGE_NAMES = ["@botpress/runtime", "@botpress/adk", "@botpress/evals"];
var PACKAGE_JSON_DEPENDENCY_SECTIONS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies"
];
async function readPackageJson(projectPath) {
const packageJsonPath = path2.join(projectPath, "package.json");
let raw;
try {
raw = await fs2.readFile(packageJsonPath, "utf8");
} catch (error) {
if (error.code === "ENOENT")
return null;
throw error;
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new AdkError({
code: "INVALID_PACKAGE_JSON",
message: "package.json must contain an object",
expected: true,
suggestion: "Ensure package.json contains a valid JSON object.",
details: { packageJsonPath }
});
}
return parsed;
}
function isPackageManagerManagedSpecifier(version) {
return /^(workspace|catalog|file|link|portal):/.test(version);
}
function findRuntimePackageChanges(packageJson, targetVersion) {
const desiredVersion = `^${targetVersion}`;
const changes = [];
let runtimeFound = false;
for (const sectionName of PACKAGE_JSON_DEPENDENCY_SECTIONS) {
const section = packageJson[sectionName];
if (!section || typeof section !== "object" || Array.isArray(section)) {
continue;
}
const dependencies = section;
for (const packageName of ADK_PACKAGE_NAMES) {
const currentVersion = dependencies[packageName];
if (typeof currentVersion !== "string") {
continue;
}
if (packageName === "@botpress/runtime") {
runtimeFound = true;
}
if (isPackageManagerManagedSpecifier(currentVersion)) {
continue;
}
if (currentVersion !== desiredVersion) {
changes.push({
packageName,
section: sectionName,
fromVersion: currentVersion,
toVersion: desiredVersion,
action: "update"
});
}
}
}
if (!runtimeFound) {
changes.push({
packageName: "@botpress/runtime",
section: "dependencies",
toVersion: desiredVersion,
action: "add"
});
}
return changes;
}
function getPackageInstallCommand(ctx) {
if (ctx.packageInstallCommand !== undefined) {
return ctx.packageInstallCommand;
}
const preferred = getPreferredPackageManager(ctx.projectPath, detectPackageManagers());
return preferred?.installCommand ?? null;
}
function shouldInstallRuntime(report) {
return report.status === "not_installed" || report.status === "invalid" || report.status === "major_mismatch" || report.status === "outdated";
}
function isInstalledRuntimeNewerThanTarget(report, targetVersion) {
return getRuntimeMajorRelation(report.installedVersion, targetVersion) === "newer";
}
function newerRuntimeBlockedReason(report, targetVersion) {
const targetMajor = getRuntimeMajor(targetVersion);
const supportedMajor = targetMajor !== null ? `${targetMajor}.x` : `v${targetVersion}`;
return `Project runtime v${report.installedVersion} is newer than this CLI's supported major ${supportedMajor}; run \`adk self-upgrade\` instead of patching the project.`;
}
async function detectRuntimePackageVersionsPatch(ctx) {
const packageJsonPath = path2.join(ctx.projectPath, "package.json");
let packageJson;
try {
packageJson = await readPackageJson(ctx.projectPath);
} catch (error) {
return {
state: "blocked",
reason: `Could not read package.json: ${error instanceof Error ? error.message : String(error)}`,
details: { packageJsonPath }
};
}
if (!packageJson) {
return {
state: "blocked",
reason: "package.json is missing",
details: { packageJsonPath, packageJsonMissing: true }
};
}
const runtimeReport = getRuntimeVersionReport(ctx.projectPath);
if (isInstalledRuntimeNewerThanTarget(runtimeReport, ctx.runtimePackageVersion)) {
return {
state: "blocked",
reason: newerRuntimeBlockedReason(runtimeReport, ctx.runtimePackageVersion),
details: {
packageJsonPath,
runtimeStatus: runtimeReport.status
}
};
}
const changes = findRuntimePackageChanges(packageJson, ctx.runtimePackageVersion);
const needsInstall = ctx.runPackageInstall && shouldInstallRuntime(runtimeReport);
if (changes.length === 0 && !needsInstall) {
return {
state: "skipped",
reason: "ADK package versions already match the target runtime version",
details: {
packageJsonPath,
changes,
runtimeStatus: runtimeReport.status
}
};
}
const installCommand = getPackageInstallCommand(ctx);
if (ctx.runPackageInstall && !installCommand) {
return {
state: "blocked",
reason: "No supported package manager was detected for installing updated packages",
details: {
packageJsonPath,
changes,
installCommand,
runtimeStatus: runtimeReport.status
}
};
}
return {
state: "pending",
reason: changes.length > 0 ? `ADK package versions need to align to ^${ctx.runtimePackageVersion}` : "@botpress/runtime needs to be installed from the current package.json",
details: {
packageJsonPath,
changes,
installCommand,
runtimeStatus: runtimeReport.status
}
};
}
async function applyRuntimePackageVersionsPatch(ctx) {
const packageJson = await readPackageJson(ctx.projectPath);
if (!packageJson) {
throw new AdkError({
code: "PACKAGE_JSON_MISSING",
message: "package.json is missing",
expected: true,
suggestion: "Run this command from an agent project that contains a package.json.",
details: { projectPath: ctx.projectPath }
});
}
const runtimeReport = getRuntimeVersionReport(ctx.projectPath);
if (isInstalledRuntimeNewerThanTarget(runtimeReport, ctx.runtimePackageVersion)) {
throw new AdkError({
code: "RUNTIME_NEWER_THAN_CLI",
message: newerRuntimeBlockedReason(runtimeReport, ctx.runtimePackageVersion),
expected: true,
suggestion: "Run `adk self-upgrade` to update the CLI instead of patching the project.",
details: {
installedVersion: runtimeReport.installedVersion,
targetVersion: ctx.runtimePackageVersion
}
});
}
const changes = findRuntimePackageChanges(packageJson, ctx.runtimePackageVersion);
const shouldInstall = ctx.runPackageInstall && (changes.length > 0 || shouldInstallRuntime(runtimeReport));
if (changes.length === 0 && !shouldInstall) {
return;
}
if (changes.length > 0) {
for (const change of changes) {
let section = packageJson[change.section];
if (!section || typeof section !== "object" || Array.isArray(section)) {
section = {};
packageJson[change.section] = section;
}
const dependencies = section;
dependencies[change.packageName] = change.toVersion;
}
await fs2.writeFile(path2.join(ctx.projectPath, "package.json"), JSON.stringify(packageJson, null, 2) + `
`, "utf8");
}
if (!shouldInstall) {
return;
}
const installCommand = getPackageInstallCommand(ctx);
if (!installCommand) {
throw new AdkError({
code: "NO_PACKAGE_MANAGER",
message: "No supported package manager was detected for installing updated packages",
expected: true,
suggestion: "Install a supported package manager (npm, pnpm, yarn, or bun) and retry."
});
}
if (ctx.runCommand) {
await ctx.runCommand(installCommand, ctx.projectPath);
} else {
await execAsync(installCommand, { cwd: ctx.projectPath, env: { ...process.env } });
}
}
var runtimePackageVersionsPatch = {
id: "runtime-package-versions",
introducedIn: "0.0.0",
title: "ADK package versions",
description: "Align @botpress runtime packages in package.json with the target runtime version.",
detect: detectRuntimePackageVersionsPatch,
apply: applyRuntimePackageVersionsPatch
};
// src/agent-project-patcher/patches.ts
var AGENT_PROJECT_PATCHES = [
runtimePackageVersionsPatch,
integrationConfigFormatPatch,
dependencySnapshotsPatch,
agent0CapabilitiesBootstrapPatch
];
// src/agent-project-patcher/agent-project-patcher.ts
class AgentProjectPatcher {
ctx;
patches;
constructor(options, patches = AGENT_PROJECT_PATCHES) {
this.ctx = {
...options,
fromVersion: options.fromVersion ?? UNKNOWN_AGENT_PROJECT_VERSION,
runtimePackageVersion: options.runtimePackageVersion ?? EXPECTED_RUNTIME_VERSION,
runPackageInstall: options.runPackageInstall ?? true
};
this.patches = patches;
}
async plan() {
const items = [];
for (const patch of this.getEligiblePatches()) {
items.push(await this.planPatch(patch));
}
return this.buildPlan(items);
}
async apply(plan) {
const sourcePlan = plan ?? await this.plan();
const items = [];
for (const plannedItem of sourcePlan.items) {
if (plannedItem.state === "blocked") {
items.push({ ...plannedItem, status: "blocked" });
continue;
}
if (!plannedItem.pending) {
items.push({ ...plannedItem, status: "skipped" });
continue;
}
const patch = this.getPatch(plannedItem.id);
const item = await this.planPatch(patch);
if (item.state === "blocked") {
items.push({ ...item, status: "blocked" });
continue;
}
if (!item.pending) {
items.push({ ...item, status: "skipped" });
continue;
}
try {
await patch.apply(this.ctx, item);
items.push({ ...item, status: "applied" });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
items.push({ ...item, status: "failed", error: message });
this.ctx.logger?.warn(`Project patch failed (${item.title}): ${message}`, {
event: "agent-project-patch",
patch: item.id
});
}
}
const applied = items.filter((item) => item.status === "applied");
const failed = items.filter((item) => item.status === "failed");
const blocked = items.filter((item) => item.status === "blocked");
return {
projectPath: this.ctx.projectPath,
fromVersion: this.ctx.fromVersion,
toVersion: this.ctx.toVersion,
items,
applied,
failed,
blocked
};
}
getEligiblePatches() {
const to = semver.parse(this.ctx.toVersion);
if (!to) {
throw new AdkError({
code: "INVALID_PATCH_TARGET_VERSION",
message: `Invalid patch target version: ${this.ctx.toVersion}`,
details: { toVersion: this.ctx.toVersion }
});
}
const targetReleaseVersion = `${to.major}.${to.minor}.${to.patch}`;
const patchIds = this.ctx.patchIds;
return this.patches.filter((patch) => {
if (patchIds && !patchIds.includes(patch.id)) {
return false;
}
const introduced = semver.parse(patch.introducedIn);
return Boolean(introduced && semver.lte(introduced, targetReleaseVersion));
});
}
getPatch(id) {
const patch = this.patches.find((item) => item.id === id);
if (!patch) {
throw new AdkError({
code: "UNKNOWN_AGENT_PROJECT_PATCH",
message: `Unknown agent project patch: ${id}`,
details: { id }
});
}
return patch;
}
async planPatch(patch) {
const detection = await patch.detect(this.ctx);
return {
id: patch.id,
title: patch.title,
description: patch.description,
introducedIn: patch.introducedIn,
...detection,
pending: detection.state === "pending"
};
}
buildPlan(items) {
const pending = items.filter((item) => item.pending);
const blocked = items.filter((item) => item.state === "blocked");
return {
projectPath: this.ctx.projectPath,
fromVersion: this.ctx.fromVersion,
toVersion: this.ctx.toVersion,
items,
pending,
blocked,
hasPending: pending.length > 0,
hasBlocked: blocked.length > 0
};
}
}
export { UNKNOWN_AGENT_PROJECT_VERSION, AgentProjectPatcher };