@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
447 lines (444 loc) • 14.8 kB
JavaScript
// @bun
import {
require_adm_zip
} from "./chunk-8n02v208.js";
import {
dependencyStateSchema,
sortKeysDeep
} from "./chunk-5zm2mgt9.js";
import {
PACKAGE_MANAGER_LOCK_FILES
} from "./chunk-nbasj5jm.js";
import {
AdkError
} from "./chunk-p0hjqn4r.js";
import {
__toESM
} from "./chunk-dhs2bg35.js";
// src/utils/adk-archive.ts
var import_adm_zip = __toESM(require_adm_zip(), 1);
import fs from "fs/promises";
import path from "path";
var ADK_ARCHIVE_SCHEMA_VERSION = 1;
var ADK_IMPORT_DEPENDENCIES_FILE = ".adk-import-dependencies.json";
var ADK_IMPORT_DEPENDENCIES_TYPE = "botpress-adk-dependencies-import";
var ADK_IMPORT_DEPENDENCIES_VERSION = 1;
var ENVIRONMENTS = ["dev", "prod"];
var EXCLUDED_DIR_NAMES = new Set([
".adk",
".agents",
".cache",
".claude",
".codex",
".cursor",
".git",
".idea",
".next",
".opencode",
".turbo",
".vite",
".vscode",
"coverage",
"dist",
"logs",
"node_modules",
"traces"
]);
var EXCLUDED_FILE_NAMES = new Set([
".DS_Store",
".mcp.json",
"Thumbs.db",
ADK_IMPORT_DEPENDENCIES_FILE,
"agent.json",
"agent.local.json",
"dependencies.dev.lock.json",
"dependencies.prod.lock.json"
]);
async function createAdkArchive(options) {
const projectPath = path.resolve(options.projectPath);
const outputPath = path.resolve(options.outputPath);
const dependencySnapshots = ENVIRONMENTS.filter((env) => options.dependencyStates[env]);
const manifest = {
schemaVersion: ADK_ARCHIVE_SCHEMA_VERSION,
projectName: options.projectName,
adkVersion: options.adkVersion,
exportedAt: new Date().toISOString(),
dependencySnapshots
};
const zip = new import_adm_zip.default;
zip.addFile("manifest.json", jsonBuffer(manifest));
const files = await collectProjectFiles(projectPath, outputPath);
for (const file of files) {
zip.addFile(`project/${file.relativePath}`, file.data);
}
if (dependencySnapshots.length > 0) {
zip.addFile(`project/${ADK_IMPORT_DEPENDENCIES_FILE}`, jsonBuffer(createImportDependenciesFile(options.dependencyStates)));
}
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, zip.toBuffer());
return { outputPath, manifest, projectFileCount: files.length };
}
async function readAdkArchive(archivePath) {
let archiveBuffer;
try {
archiveBuffer = await fs.readFile(archivePath);
} catch (error) {
throw new AdkError({
code: "ARCHIVE_READ_FAILED",
message: `Failed to read archive: ${error instanceof Error ? error.message : String(error)}`,
expected: true,
cause: error
});
}
let zip;
try {
zip = new import_adm_zip.default(archiveBuffer);
} catch (error) {
throw invalidArchive(`Invalid zip archive: ${error instanceof Error ? error.message : String(error)}`, error);
}
const files = new Map;
const seen = new Set;
for (const entry of zip.getEntries()) {
const name = normalizeArchivePath(entry.entryName);
validateAllowedArchivePath(name, entry.isDirectory);
if (entry.isDirectory) {
continue;
}
if (seen.has(name)) {
throw invalidArchive(`Archive contains duplicate entry: ${name}`);
}
seen.add(name);
files.set(name, entry.getData());
}
const manifestData = files.get("manifest.json");
if (!manifestData) {
throw invalidArchive("Archive is missing manifest.json");
}
const manifest = parseManifest(manifestData);
const projectFiles = [];
let dependencyStates = {};
let hasImportDependenciesFile = false;
for (const [name, data] of files) {
if (name === "manifest.json") {
continue;
}
if (name.startsWith("project/")) {
const relativePath = name.slice("project/".length);
if (!relativePath) {
throw invalidArchive("Archive contains an empty project path");
}
if (relativePath === ADK_IMPORT_DEPENDENCIES_FILE) {
if (hasImportDependenciesFile) {
throw invalidArchive(`Archive contains duplicate ${ADK_IMPORT_DEPENDENCIES_FILE}`);
}
hasImportDependenciesFile = true;
dependencyStates = parseImportDependenciesFile(data, name);
}
projectFiles.push({ relativePath, data });
continue;
}
throw invalidArchive(`Archive contains unsupported entry: ${name}`);
}
const manifestEnvs = new Set(manifest.dependencySnapshots);
for (const env of ENVIRONMENTS) {
const hasFile = !!dependencyStates[env];
if (manifestEnvs.has(env) && !hasFile) {
throw invalidArchive(`Archive manifest lists ${env} dependencies but the dependency file is missing`);
}
if (!manifestEnvs.has(env) && hasFile) {
throw invalidArchive(`Archive includes ${env} dependencies but manifest.json does not list them`);
}
}
projectFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
return { manifest, projectFiles, dependencyStates };
}
async function prepareImportDirectory(directory) {
const destination = path.resolve(directory);
try {
const stat = await fs.stat(destination);
if (!stat.isDirectory()) {
throw new AdkError({
code: "IMPORT_DESTINATION_INVALID",
message: `Import destination exists and is not a directory: ${destination}`,
expected: true
});
}
const entries = await fs.readdir(destination);
if (entries.length > 0) {
throw new AdkError({
code: "IMPORT_DESTINATION_NOT_EMPTY",
message: `Import destination must be empty: ${destination}`,
expected: true
});
}
} catch (error) {
if (error.code === "ENOENT") {
await fs.mkdir(destination, { recursive: true });
return destination;
}
throw error;
}
return destination;
}
async function extractProjectFiles(archive, destination) {
const root = path.resolve(destination);
for (const file of archive.projectFiles) {
const target = resolveInside(root, file.relativePath);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, file.data);
}
}
async function removeImportDependenciesFile(destination) {
await fs.rm(path.join(path.resolve(destination), ADK_IMPORT_DEPENDENCIES_FILE), { force: true });
}
function defaultArchiveFileName(projectName) {
const safeName = projectName.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
return `${safeName || "agent"}.adk`;
}
function archiveDefaultDirectoryName(projectName) {
const safeName = projectName.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
return safeName || "agent";
}
async function collectProjectFiles(projectPath, outputPath) {
const outputResolved = path.resolve(outputPath);
const files = [];
const walk = async (dir, relativeDir) => {
const entries = (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
for (const entry of entries) {
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
const absolutePath = path.join(dir, entry.name);
if (path.resolve(absolutePath) === outputResolved) {
continue;
}
if (entry.isSymbolicLink()) {
continue;
}
if (entry.isDirectory()) {
if (shouldExcludeDirectory(entry.name)) {
continue;
}
await walk(absolutePath, relativePath);
continue;
}
if (!entry.isFile() || shouldExcludeFile(entry.name)) {
continue;
}
files.push({
absolutePath,
relativePath: toArchivePath(relativePath),
data: await fs.readFile(absolutePath)
});
}
};
await walk(projectPath, "");
await addNearestPackageManagerLockFile(projectPath, outputResolved, files);
return files;
}
async function addNearestPackageManagerLockFile(projectPath, outputPath, files) {
const lockPath = await findNearestPackageManagerLockFile(projectPath);
if (!lockPath || path.resolve(lockPath) === outputPath) {
return;
}
const relativePath = toArchivePath(path.basename(lockPath));
if (files.some((file) => file.relativePath === relativePath)) {
return;
}
files.push({
absolutePath: lockPath,
relativePath,
data: await fs.readFile(lockPath)
});
}
async function findNearestPackageManagerLockFile(projectPath) {
let current = path.resolve(projectPath);
while (true) {
for (const lockFile of PACKAGE_MANAGER_LOCK_FILES) {
const candidate = path.join(current, lockFile.fileName);
if (await isFile(candidate)) {
return candidate;
}
}
const parent = path.dirname(current);
if (parent === current) {
return;
}
current = parent;
}
}
async function isFile(filePath) {
try {
const stat = await fs.stat(filePath);
return stat.isFile();
} catch (error) {
if (error.code === "ENOENT") {
return false;
}
throw error;
}
}
function shouldExcludeDirectory(name) {
return EXCLUDED_DIR_NAMES.has(name);
}
function shouldExcludeFile(name) {
if (EXCLUDED_FILE_NAMES.has(name)) {
return true;
}
if (name.endsWith(".adk") || name.endsWith(".adk.zip")) {
return true;
}
if (name === ".env" || name.startsWith(".env.")) {
return true;
}
if (name.endsWith(".log") || name.endsWith(".trace") || name.endsWith(".pid")) {
return true;
}
if (name.endsWith(".seed") || name.endsWith(".pid.lock") || name.endsWith(".tsbuildinfo")) {
return true;
}
return false;
}
function toArchivePath(filePath) {
return filePath.split(path.sep).join("/");
}
function jsonBuffer(value) {
return Buffer.from(JSON.stringify(sortKeysDeep(value), null, 2) + `
`);
}
function parseJsonObject(data, label) {
try {
const parsed = JSON.parse(data.toString("utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${label} must be a JSON object`);
}
return parsed;
} catch (error) {
throw invalidArchive(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`, error);
}
}
function parseManifest(data) {
const parsed = parseJsonObject(data, "manifest.json");
if (parsed.schemaVersion !== ADK_ARCHIVE_SCHEMA_VERSION) {
throw invalidArchive(`Unsupported archive schema version: ${String(parsed.schemaVersion)}`);
}
if (typeof parsed.projectName !== "string" || !parsed.projectName.trim()) {
throw invalidArchive("manifest.json projectName must be a non-empty string");
}
if (typeof parsed.adkVersion !== "string" || !parsed.adkVersion.trim()) {
throw invalidArchive("manifest.json adkVersion must be a non-empty string");
}
if (typeof parsed.exportedAt !== "string" || Number.isNaN(Date.parse(parsed.exportedAt))) {
throw invalidArchive("manifest.json exportedAt must be an ISO timestamp");
}
if (!Array.isArray(parsed.dependencySnapshots)) {
throw invalidArchive("manifest.json dependencySnapshots must be an array");
}
const dependencySnapshots = [];
for (const value of parsed.dependencySnapshots) {
if (value !== "dev" && value !== "prod") {
throw invalidArchive(`manifest.json dependencySnapshots contains unsupported value: ${String(value)}`);
}
if (dependencySnapshots.includes(value)) {
throw invalidArchive(`manifest.json dependencySnapshots contains duplicate value: ${value}`);
}
dependencySnapshots.push(value);
}
return {
schemaVersion: ADK_ARCHIVE_SCHEMA_VERSION,
projectName: parsed.projectName,
adkVersion: parsed.adkVersion,
exportedAt: parsed.exportedAt,
dependencySnapshots
};
}
function createImportDependenciesFile(states) {
const dependencies = {};
for (const env of ENVIRONMENTS) {
const state = states[env];
if (!state) {
continue;
}
dependencies[env] = dependencyStateSchema.parse({ ...state, env });
}
return {
type: ADK_IMPORT_DEPENDENCIES_TYPE,
version: ADK_IMPORT_DEPENDENCIES_VERSION,
dependencies
};
}
function parseImportDependenciesFile(data, label) {
const parsed = parseJsonObject(data, label);
if (parsed.type !== ADK_IMPORT_DEPENDENCIES_TYPE) {
throw invalidArchive(`${label} type must be ${ADK_IMPORT_DEPENDENCIES_TYPE}`);
}
if (parsed.version !== ADK_IMPORT_DEPENDENCIES_VERSION) {
throw invalidArchive(`Unsupported ${label} version: ${String(parsed.version)}`);
}
const dependencies = parsed.dependencies;
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
throw invalidArchive(`${label} dependencies must be an object`);
}
const output = {};
for (const env of ENVIRONMENTS) {
const state = dependencies[env];
if (state === undefined) {
continue;
}
try {
output[env] = dependencyStateSchema.parse({
...state,
env
});
} catch (error) {
throw invalidArchive(`Invalid ${label} ${env} dependencies: ${error instanceof Error ? error.message : String(error)}`, error);
}
}
for (const env of Object.keys(dependencies)) {
if (!ENVIRONMENTS.includes(env)) {
throw invalidArchive(`${label} dependencies contains unsupported environment: ${env}`);
}
}
return output;
}
function normalizeArchivePath(entryName) {
const withoutTrailingSlash = entryName.replace(/\/+$/g, "");
if (!withoutTrailingSlash || entryName.includes("\\") || path.posix.isAbsolute(withoutTrailingSlash)) {
throw invalidArchive(`Unsafe archive path: ${entryName}`);
}
if (withoutTrailingSlash === ".." || withoutTrailingSlash.startsWith("../") || withoutTrailingSlash.includes("/../")) {
throw invalidArchive(`Unsafe archive path: ${entryName}`);
}
const normalized = path.posix.normalize(withoutTrailingSlash);
if (normalized !== withoutTrailingSlash || normalized === ".") {
throw invalidArchive(`Unsafe archive path: ${entryName}`);
}
return normalized;
}
function validateAllowedArchivePath(name, isDirectory) {
if (name === "manifest.json" && !isDirectory) {
return;
}
if (name === "project" && isDirectory) {
return;
}
if (name.startsWith("project/")) {
return;
}
throw invalidArchive(`Archive contains unsupported entry: ${name}`);
}
function resolveInside(root, relativePath) {
const normalized = normalizeArchivePath(relativePath);
const target = path.resolve(root, normalized);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
throw invalidArchive(`Unsafe archive path: ${relativePath}`);
}
return target;
}
function invalidArchive(message, cause) {
return new AdkError({
code: "INVALID_ARCHIVE",
message,
expected: true,
cause
});
}
export { ADK_IMPORT_DEPENDENCIES_FILE, createAdkArchive, readAdkArchive, prepareImportDirectory, extractProjectFiles, removeImportDependenciesFile, defaultArchiveFileName, archiveDefaultDirectoryName };