UNPKG

@botpress/adk-cli

Version:

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

447 lines (444 loc) 14.8 kB
// @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 };