diff --git a/packages/commands/package.json b/packages/commands/package.json index d6ab50a..6e2f6ae 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -40,7 +40,7 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/local-git": "0.4.0", + "@openagentpack/project-versions": "0.4.0", "@openagentpack/sdk": "0.4.0", "bailian-cli-core": "workspace:*", "bailian-cli-runtime": "workspace:*", diff --git a/packages/commands/src/commands/managed-agent/_engine/git-project.ts b/packages/commands/src/commands/managed-agent/_engine/git-project.ts deleted file mode 100644 index eb96b5f..0000000 --- a/packages/commands/src/commands/managed-agent/_engine/git-project.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { execFile } from "node:child_process"; -import { existsSync } from "node:fs"; -import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; -import { basename, resolve } from "node:path"; -import { promisify } from "node:util"; -import { BailianError, ExitCode } from "bailian-cli-core"; - -const execFileAsync = promisify(execFile); - -const REPOSITORY_GITIGNORE = `# Dependencies -node_modules/ - -# Bailian CLI local runs -.openagentpack/state/ -.openagentpack/runs/ - -# Local credentials -.env -.env.* -!.env.example -`; - -const REPOSITORY_GITIGNORE_PATTERNS = [ - "node_modules/", - ".openagentpack/state/", - ".openagentpack/runs/", - ".env", - ".env.*", - "!.env.example", -] as const; - -const PROJECT_SCRIPTS = { - "agents:validate": "bl managed-agent validate --file agents.yaml", - "agents:plan": "bl managed-agent plan --file agents.yaml", - "agents:plan:ci": "bl managed-agent plan --file agents.yaml --output json", - "agents:apply:ci": "bl managed-agent apply --file agents.yaml --ci", - "agents:workbench": "bl managed-agent workbench --file agents.yaml", -} as const; - -const INITIAL_STATE = `${JSON.stringify({ resources: [] }, null, 2)}\n`; - -export interface GitProjectResult { - targetDirectory: string; - mode: "created" | "upgraded"; - initializedGit: boolean; - createdFiles: string[]; - updatedFiles: string[]; - preservedFiles: string[]; -} - -interface CreateGitProjectOptions { - config: string; - cliVersion: string; -} - -type ProjectTargetMode = "new" | "existing"; - -export async function inspectGitProjectTarget(targetDirectory: string): Promise { - if (!existsSync(targetDirectory)) return "new"; - const targetStat = await stat(targetDirectory); - if (!targetStat.isDirectory()) { - throw new BailianError(`Target '${targetDirectory}' is not a directory.`, ExitCode.USAGE); - } - const entries = await readdir(targetDirectory); - if (entries.length === 0) return "new"; - if (existsSync(resolve(targetDirectory, "agents.yaml"))) return "existing"; - throw new BailianError( - `Target directory '${targetDirectory}' is not empty and does not contain agents.yaml.`, - ExitCode.USAGE, - ); -} - -export async function createGitProject( - directory: string, - options: CreateGitProjectOptions, -): Promise { - const targetDirectory = resolve(directory); - const targetMode = await inspectGitProjectTarget(targetDirectory); - const shouldInitializeGit = !existsSync(resolve(targetDirectory, ".git")); - if (shouldInitializeGit) await assertGitAvailable(); - - const createdFiles: string[] = []; - const updatedFiles: string[] = []; - const preservedFiles: string[] = []; - await mkdir(resolve(targetDirectory, ".aoneci"), { recursive: true }); - - const config = - targetMode === "new" - ? options.config - : await readFile(resolve(targetDirectory, "agents.yaml"), "utf8"); - if (targetMode === "new") { - await writeFile(resolve(targetDirectory, "agents.yaml"), config, "utf8"); - createdFiles.push("agents.yaml"); - } else { - preservedFiles.push("agents.yaml"); - } - - await mergeOrCreateTextFile( - resolve(targetDirectory, ".gitignore"), - REPOSITORY_GITIGNORE, - mergeGitignore, - ".gitignore", - createdFiles, - updatedFiles, - ); - await mergeOrCreateTextFile( - resolve(targetDirectory, ".env.example"), - environmentExample(config), - (current) => mergeEnvironmentExample(current, config), - ".env.example", - createdFiles, - updatedFiles, - ); - - const packagePath = resolve(targetDirectory, "package.json"); - if (existsSync(packagePath)) { - const current = await readFile(packagePath, "utf8"); - const merged = mergePackageJson(current, basename(targetDirectory), options.cliVersion); - if (merged.content !== current) { - await writeFile(packagePath, merged.content, "utf8"); - updatedFiles.push("package.json"); - } - preservedFiles.push(...merged.preservedSettings); - } else { - await writeFile( - packagePath, - buildPackageJson(basename(targetDirectory), options.cliVersion), - "utf8", - ); - createdFiles.push("package.json"); - } - - await createIfMissing( - resolve(targetDirectory, "agents.state.json"), - INITIAL_STATE, - "agents.state.json", - createdFiles, - preservedFiles, - ); - await createIfMissing( - resolve(targetDirectory, ".aoneci/bailian-cli.yml"), - buildAoneWorkflow(config), - ".aoneci/bailian-cli.yml", - createdFiles, - preservedFiles, - ); - await createIfMissing( - resolve(targetDirectory, ".aoneci/bailian-cli-check.yml"), - buildAoneCheckWorkflow(config), - ".aoneci/bailian-cli-check.yml", - createdFiles, - preservedFiles, - ); - await createIfMissing( - resolve(targetDirectory, "README.md"), - buildReadme(basename(targetDirectory), config), - "README.md", - createdFiles, - preservedFiles, - ); - - if (shouldInitializeGit) await initializeGitRepository(targetDirectory); - return { - targetDirectory, - mode: targetMode === "new" ? "created" : "upgraded", - initializedGit: shouldInitializeGit, - createdFiles, - updatedFiles, - preservedFiles, - }; -} - -async function assertGitAvailable(): Promise { - try { - await execFileAsync("git", ["--version"]); - } catch { - throw new BailianError( - "Git is required to initialize a repository.", - ExitCode.USAGE, - "Install Git and retry.", - ); - } -} - -async function initializeGitRepository(targetDirectory: string): Promise { - try { - await execFileAsync("git", ["init", "--initial-branch", "main"], { - cwd: targetDirectory, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new BailianError( - `Could not initialize the local Git repository: ${message}`, - ExitCode.GENERAL, - ); - } -} - -async function mergeOrCreateTextFile( - path: string, - initialContent: string, - merge: (current: string) => string, - label: string, - createdFiles: string[], - updatedFiles: string[], -): Promise { - if (!existsSync(path)) { - await writeFile(path, initialContent, "utf8"); - createdFiles.push(label); - return; - } - const current = await readFile(path, "utf8"); - const next = merge(current); - if (next !== current) { - await writeFile(path, next, "utf8"); - updatedFiles.push(label); - } -} - -async function createIfMissing( - path: string, - content: string, - label: string, - createdFiles: string[], - preservedFiles: string[], -): Promise { - if (existsSync(path)) { - preservedFiles.push(label); - return; - } - await writeFile(path, content, "utf8"); - createdFiles.push(label); -} - -function mergeGitignore(content: string): string { - const repositoryContent = content - .split(/\r?\n/) - .filter((line) => line.trim() !== "agents.state.json") - .join("\n"); - const existingPatterns = new Set( - repositoryContent - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean), - ); - const missingPatterns = REPOSITORY_GITIGNORE_PATTERNS.filter( - (pattern) => !existingPatterns.has(pattern), - ); - if (missingPatterns.length === 0) return repositoryContent; - return appendBlock( - repositoryContent, - `# Bailian CLI local files\n${missingPatterns.join("\n")}\n`, - ); -} - -function environmentExample(config: string): string { - return `${extractEnvironmentVariables(config) - .map((variable) => `${variable}=replace-me`) - .join("\n")}\n`; -} - -function mergeEnvironmentExample(content: string, config: string): string { - const existingVariables = new Set(); - for (const line of content.split(/\r?\n/)) { - const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/); - if (match?.[1]) existingVariables.add(match[1]); - } - const missingVariables = extractEnvironmentVariables(config).filter( - (variable) => !existingVariables.has(variable), - ); - if (missingVariables.length === 0) return content; - return appendBlock( - content, - `${missingVariables.map((variable) => `${variable}=replace-me`).join("\n")}\n`, - ); -} - -function extractEnvironmentVariables(config: string): string[] { - const variables = new Set(); - for (const match of config.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/g)) { - if (match[1]) variables.add(match[1]); - } - return [...variables]; -} - -function appendBlock(content: string, block: string): string { - if (!content) return block; - if (content.endsWith("\n\n")) return `${content}${block}`; - if (content.endsWith("\n")) return `${content}\n${block}`; - return `${content}\n\n${block}`; -} - -function buildPackageJson(projectName: string, cliVersion: string): string { - return `${JSON.stringify( - { - name: npmPackageName(projectName), - private: true, - version: "0.0.0", - type: "module", - scripts: PROJECT_SCRIPTS, - devDependencies: { "bailian-cli": cliVersion }, - }, - null, - 2, - )}\n`; -} - -function mergePackageJson( - content: string, - projectName: string, - cliVersion: string, -): { content: string; preservedSettings: string[] } { - let manifest: unknown; - try { - manifest = JSON.parse(content); - } catch { - throw new BailianError( - "Cannot upgrade package.json because it is not valid JSON.", - ExitCode.USAGE, - ); - } - if (!isRecord(manifest)) { - throw new BailianError( - "Cannot upgrade package.json because its root is not an object.", - ExitCode.USAGE, - ); - } - const preservedSettings: string[] = []; - if (manifest.name === undefined) manifest.name = npmPackageName(projectName); - if (manifest.private === undefined) manifest.private = true; - - const scripts = manifest.scripts === undefined ? {} : manifest.scripts; - if (!isRecord(scripts)) { - throw new BailianError( - "Cannot upgrade package.json because 'scripts' is not an object.", - ExitCode.USAGE, - ); - } - manifest.scripts = scripts; - for (const [name, command] of Object.entries(PROJECT_SCRIPTS)) { - if (scripts[name] === undefined) scripts[name] = command; - else if (scripts[name] !== command) preservedSettings.push(`package.json scripts.${name}`); - } - - const developmentDependencies = - manifest.devDependencies === undefined ? {} : manifest.devDependencies; - if (!isRecord(developmentDependencies)) { - throw new BailianError( - "Cannot upgrade package.json because 'devDependencies' is not an object.", - ExitCode.USAGE, - ); - } - manifest.devDependencies = developmentDependencies; - if (developmentDependencies["bailian-cli"] === undefined) { - developmentDependencies["bailian-cli"] = cliVersion; - } else if (developmentDependencies["bailian-cli"] !== cliVersion) { - preservedSettings.push("package.json bailian-cli version"); - } - return { content: `${JSON.stringify(manifest, null, 2)}\n`, preservedSettings }; -} - -function buildAoneEnvironmentBlock(config: string): string { - const variables = extractEnvironmentVariables(config); - if (variables.length === 0) { - return " # Add provider variables referenced by agents.yaml in Aone Flow."; - } - return variables.map((variable) => ` ${variable}: \${{secrets.${variable}}}`).join("\n"); -} - -function buildAoneWorkflow(config: string): string { - const environmentBlock = buildAoneEnvironmentBlock(config); - return `name: Bailian CLI Managed Agent - -triggers: - push: - branches: - - main - -jobs: - apply: - name: Validate, plan, and apply Agent resources - image: alios-8u - timeout: 30m - steps: - - id: checkout - uses: checkout - - id: setup-env - uses: setup-env - inputs: - node-version: 22 - tnpm-version: 10 - tnpm-cache: true - - id: install - run: npm install --ignore-scripts --no-audit --no-fund - - id: validate-and-plan - envs: -${environmentBlock} - run: | - npm run agents:validate - npm run agents:plan:ci > bailian-cli-plan.json - - id: upload-plan - uses: upload-artifact - inputs: - name: bailian-cli-plan - path: bailian-cli-plan.json - - id: apply-and-persist-state - envs: -${environmentBlock} - run: | - set +e - npm run agents:apply:ci - apply_status=$? - set -e - if ! git diff --quiet -- agents.state.json; then - git config user.name "Bailian CLI CI" - git config user.email "bailian-cli-ci@alibaba-inc.com" - git add -- agents.state.json - git commit -m "chore: update Bailian CLI Agent state [skip ci]" - git push origin HEAD:main - fi - exit "$apply_status" -`; -} - -function buildAoneCheckWorkflow(config: string): string { - const environmentBlock = buildAoneEnvironmentBlock(config); - return `name: Bailian CLI Managed Agent Check - -# Bind this pipeline to Codeup merge-request new/update events in Aone Flow. -jobs: - check: - name: Validate and plan Agent resources - image: alios-8u - timeout: 20m - steps: - - id: checkout - uses: checkout - - id: setup-env - uses: setup-env - inputs: - node-version: 22 - tnpm-version: 10 - tnpm-cache: true - - id: install - run: npm install --ignore-scripts --no-audit --no-fund - - id: validate-and-plan - envs: -${environmentBlock} - run: | - npm run agents:validate - npm run agents:plan:ci > bailian-cli-plan.json - - id: upload-plan - uses: upload-artifact - inputs: - name: bailian-cli-plan - path: bailian-cli-plan.json -`; -} - -function buildReadme(projectName: string, config: string): string { - const variableList = extractEnvironmentVariables(config) - .map((variable) => `- \`${variable}\``) - .join("\n"); - return `# ${projectName} - -This repository declares cloud Agent resources with Bailian CLI. - -## Local Workbench - -1. Copy \`.env.example\` to \`.env\` and replace placeholder credentials. -2. Run \`npm install\`. -3. Run \`npm run agents:workbench\`. - -## Aone CI - -\`.aoneci/bailian-cli-check.yml\` validates and plans merge requests without applying. \`.aoneci/bailian-cli.yml\` applies non-destructive local changes after a push to \`main\` and commits the resulting \`agents.state.json\` back to \`main\`. - -Configure these values as secret variables in Aone Flow: - -${variableList || "- Add the provider variables referenced by agents.yaml."} - -Set pipeline concurrency to 1, protect the main branch, and require approval where appropriate. Workbench and CI should use isolated credentials, resource namespaces, and State scopes. - -Create the remote repository yourself, then push this local repository: - -\`\`\`bash -git add . -git commit -m "Initialize Bailian CLI Agent project" -git remote add origin -git push -u origin main -\`\`\` -`; -} - -function npmPackageName(projectName: string): string { - const normalized = projectName - .toLowerCase() - .replace(/[^a-z0-9._-]+/g, "-") - .replace(/^[._-]+|[._-]+$/g, ""); - return normalized || "bailian-agent-project"; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} diff --git a/packages/commands/src/commands/managed-agent/apply.ts b/packages/commands/src/commands/managed-agent/apply.ts index 320ef8a..884a2b8 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -6,12 +6,7 @@ import { type FlagsDef, } from "bailian-cli-core"; import { emitBare, emitResult } from "bailian-cli-runtime"; -import { - executePlannedProject, - planProjectContext, - type PlannedAction, - UserError, -} from "@openagentpack/sdk"; +import { executePlannedProject, planProjectContext } from "@openagentpack/sdk"; import { formatResourceLabel } from "./_engine/address-utils.ts"; import { assertProviderConfigured, @@ -22,11 +17,12 @@ import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; import { renderAgentFeedback } from "./_engine/feedback.ts"; import { - commitAutomaticVersion, - type PreparedAutomaticVersion, - prepareAutomaticVersion, - readVersionSource, -} from "@openagentpack/local-git"; + commitPreparedProjectVersion, + type PreparedProjectVersion, + prepareProjectVersion, + readProjectVersionSource, + releasePreparedProjectVersion, +} from "@openagentpack/project-versions"; const APPLY_FLAGS = { file: { @@ -52,13 +48,6 @@ const APPLY_FLAGS = { "zh-CN": "无需交互提示直接确认并应用(执行变更时必填)", }, }, - ci: { - type: "switch", - description: { - "en-US": "Run non-interactively while blocking deletes and remote drift", - "zh-CN": "以非交互模式运行,并阻止删除和远端漂移覆盖", - }, - }, noRefresh: { type: "switch", description: { @@ -90,17 +79,10 @@ export default defineCommand({ }, auth: "apiKey", usageArgs: - "[--file ] [--provider ] [--yes | --ci] [--no-refresh] [--refresh-only] [--concurrency ]", + "[--file ] [--provider ] [--yes] [--no-refresh] [--refresh-only] [--concurrency ]", flags: APPLY_FLAGS, - exampleArgs: ["--yes", "--provider bailian --yes", "--ci"], + exampleArgs: ["--yes", "--provider bailian --yes"], notes: CREDENTIALS_NOTE, - validate(flags) { - if (flags.ci && flags.yes) return "--ci cannot be combined with --yes."; - if (flags.ci && flags.noRefresh) { - return "--ci requires remote state refresh and cannot be combined with --no-refresh."; - } - return undefined; - }, async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); @@ -113,7 +95,6 @@ export default defineCommand({ provider: flags.provider ?? "all", refresh: !flags.noRefresh, concurrency: flags.concurrency, - ci: flags.ci, refresh_only: flags.refreshOnly, }, config_file: file, @@ -124,7 +105,7 @@ export default defineCommand({ return; } - const versionSource = await readVersionSource(file); + const versionSource = await readProjectVersionSource(file); const { planned, runtime } = await withAgentErrors(() => withStdoutProtected(async () => { @@ -157,7 +138,7 @@ export default defineCommand({ const actionable = plan.actions.filter((action) => action.action !== "no-op"); if (actionable.length === 0) { if (!flags.refreshOnly) { - const preparedVersion = await prepareAutomaticVersion( + const preparedVersion = await prepareProjectVersion( runtime.configPath, versionSource.source, ); @@ -172,8 +153,6 @@ export default defineCommand({ const creates = actionable.filter((action) => action.action === "create").length; const updates = actionable.filter((action) => action.action === "update").length; const deletes = planned.destructiveActions; - if (flags.ci) assertCiApplyPolicy(actionable); - for (const action of actionable) { const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; emitProgress(` ${icon} ${formatResourceLabel(action.address)}`); @@ -197,7 +176,7 @@ export default defineCommand({ return; } - if (!flags.yes && !flags.ci) { + if (!flags.yes) { throw new BailianError( `Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`, ExitCode.USAGE, @@ -205,62 +184,50 @@ export default defineCommand({ ); } - const preparedVersion = await prepareAutomaticVersion(runtime.configPath, versionSource.source); - - const result = await withAgentErrors(() => - withStdoutProtected(() => - executePlannedProject(planned, { - onFeedback: renderAgentFeedback, - policy: "force", - concurrency: flags.concurrency, - }), - ), - ); - - const succeeded = result.results.filter((entry) => entry.status === "success").length; - const failed = result.results.filter((entry) => entry.status === "failed").length; - const skipped = result.results.filter((entry) => entry.status === "skipped").length; - - if (format === "json") { - emitResult({ succeeded, failed, skipped, results: result.results }, format); - } else { - emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`); - } - - if (failed > 0 || skipped > 0) { - throw new BailianError( - failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.", - ExitCode.GENERAL, + const preparedVersion = await prepareProjectVersion(runtime.configPath, versionSource.source); + let versionCommitted = false; + try { + const result = await withAgentErrors(() => + withStdoutProtected(() => + executePlannedProject(planned, { + onFeedback: renderAgentFeedback, + policy: "force", + concurrency: flags.concurrency, + }), + ), ); + + const succeeded = result.results.filter((entry) => entry.status === "success").length; + const failed = result.results.filter((entry) => entry.status === "failed").length; + const skipped = result.results.filter((entry) => entry.status === "skipped").length; + + if (format === "json") { + emitResult({ succeeded, failed, skipped, results: result.results }, format); + } else { + emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`); + } + + if (failed > 0 || skipped > 0) { + throw new BailianError( + failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.", + ExitCode.GENERAL, + ); + } + await commitSuccessfulApplyVersion(preparedVersion, format); + versionCommitted = true; + } finally { + if (!versionCommitted) await releasePreparedProjectVersion(preparedVersion); } - await commitSuccessfulApplyVersion(preparedVersion, format); }, }); -export function assertCiApplyPolicy(actions: PlannedAction[]): void { - const deletes = actions.filter((action) => action.action === "delete"); - if (deletes.length > 0) { - throw new UserError( - `CI policy blocked ${deletes.length} delete action(s). Review the plan and apply this destructive change through an explicitly approved workflow.`, - ); - } - const drifted = actions.filter( - (action) => action.driftKind === "remote" || action.driftKind === "both", - ); - if (drifted.length > 0) { - throw new UserError( - `CI policy blocked ${drifted.length} action(s) with remote drift. Review the remote changes before deciding whether YAML should overwrite them.`, - ); - } -} - async function commitSuccessfulApplyVersion( - prepared: PreparedAutomaticVersion | null, + prepared: PreparedProjectVersion | null, format: "text" | "json", ): Promise { if (!prepared) return; - const version = await commitAutomaticVersion(prepared); + const version = await commitPreparedProjectVersion(prepared); if (version && format !== "json") { - emitBare(`Created local version ${version.short_commit} (${version.message}).`); + emitBare(`Created local version ${version.short_version} (${version.message}).`); } } diff --git a/packages/commands/src/commands/managed-agent/init.ts b/packages/commands/src/commands/managed-agent/init.ts index f2fc8ee..9805984 100644 --- a/packages/commands/src/commands/managed-agent/init.ts +++ b/packages/commands/src/commands/managed-agent/init.ts @@ -8,11 +8,11 @@ import { type FlagsDef, } from "bailian-cli-core"; import { emitBare, emitResult } from "bailian-cli-runtime"; -import { createGitProject, inspectGitProjectTarget } from "./_engine/git-project.ts"; const GITIGNORE_ADDITIONS = ` # agents agents.state.json +.openagentpack/versions/ .env `; @@ -101,14 +101,6 @@ const INIT_FLAGS = { "zh-CN": "输出配置路径(默认:agents.yaml)", }, }, - git: { - type: "string", - valueHint: "", - description: { - "en-US": "Create or add CI/Git scaffolding in this project directory", - "zh-CN": "在此项目目录中创建或补充 CI/Git 脚手架", - }, - }, force: { type: "switch", description: { "en-US": "Overwrite an existing config file", "zh-CN": "覆盖已有配置文件" }, @@ -117,19 +109,13 @@ const INIT_FLAGS = { export default defineCommand({ description: { - "en-US": "Create an agents.yaml template or a local CI/Git project", - "zh-CN": "创建 agents.yaml 模板或本地 CI/Git 项目", + "en-US": "Create an agents.yaml template", + "zh-CN": "创建 agents.yaml 模板", }, auth: "none", - usageArgs: - "[--provider ] [--agent-name ] [--file ] [--git ] [--force]", + usageArgs: "[--provider ] [--agent-name ] [--file ] [--force]", flags: INIT_FLAGS, - exampleArgs: ["", "--provider bailian --agent-name assistant", "--git ./my-agents", "--git ."], - validate(flags) { - if (flags.git && flags.file) return "--git cannot be combined with --file."; - if (flags.git && flags.force) return "--git cannot be combined with --force."; - return undefined; - }, + exampleArgs: ["", "--provider bailian --agent-name assistant"], async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); @@ -137,45 +123,6 @@ export default defineCommand({ const agentName = flags.agentName ?? "assistant"; const file = flags.file ?? "agents.yaml"; - if (flags.git) { - const targetMode = await inspectGitProjectTarget(flags.git); - if (settings.dryRun) { - emitResult( - { - would_initialize_git_project: flags.git, - mode: targetMode === "new" ? "create" : "upgrade", - provider, - agent: agentName, - }, - format, - ); - return; - } - const template = buildTemplate({ provider, agentName }); - const result = await createGitProject(flags.git, { - config: template, - cliVersion: ctx.identity.version, - }); - if (format === "json") { - emitResult(result, format); - } else { - const action = - result.mode === "created" ? "Created CI/Git project" : "Added CI/Git scaffolding"; - emitBare(`${action} at ${result.targetDirectory}`); - if (result.createdFiles.length > 0) { - emitBare(`Created: ${result.createdFiles.join(", ")}`); - } - if (result.updatedFiles.length > 0) { - emitBare(`Updated: ${result.updatedFiles.join(", ")}`); - } - if (result.preservedFiles.length > 0) { - emitBare(`Preserved: ${result.preservedFiles.join(", ")}`); - } - emitBare("Next: add credentials to .env, install dependencies, and open the Workbench."); - } - return; - } - if (existsSync(file) && !flags.force) { throw new BailianError( `${file} already exists.`, diff --git a/packages/commands/src/commands/managed-agent/version.ts b/packages/commands/src/commands/managed-agent/version.ts index e9caefc..5ca51e2 100644 --- a/packages/commands/src/commands/managed-agent/version.ts +++ b/packages/commands/src/commands/managed-agent/version.ts @@ -8,16 +8,16 @@ import { import { confirmDangerousAction, emitBare, emitResult } from "bailian-cli-runtime"; import chalk from "chalk"; import { - disableLocalVersioning, - enableLocalVersioning, - getLocalVersionStatus, - type LocalProjectVersion, - type LocalVersionPreview, - type LocalVersionStatus, - listLocalVersions, - previewLocalVersion, - restoreLocalVersion, -} from "@openagentpack/local-git"; + disableProjectVersioning, + enableProjectVersioning, + getProjectVersionStatus, + type ProjectVersion, + type ProjectVersionPreview, + type ProjectVersionStatus, + listProjectVersions, + previewProjectVersion, + restoreProjectVersion, +} from "@openagentpack/project-versions"; const FILE_FLAG = { file: { @@ -30,22 +30,22 @@ const FILE_FLAG = { }, } satisfies FlagsDef; -const COMMIT_FLAG = { - commit: { +const VERSION_FLAG = { + versionId: { type: "string", - valueHint: "", + valueHint: "", required: true, description: { - "en-US": "Full commit SHA from the current branch", - "zh-CN": "当前分支中的完整 Commit SHA", + "en-US": "Full local version ID", + "zh-CN": "完整的本地版本 ID", }, }, } satisfies FlagsDef; export const managedAgentVersionEnable = defineCommand({ description: { - "en-US": "Enable Apply-time Git versioning for agents.yaml", - "zh-CN": "为 agents.yaml 启用 Apply 后自动 Git 版本管理", + "en-US": "Enable Apply-time local snapshots for agents.yaml", + "zh-CN": "为 agents.yaml 启用 Apply 后自动本地快照", }, auth: "none", usageArgs: "[--file ]", @@ -55,28 +55,30 @@ export const managedAgentVersionEnable = defineCommand({ const file = ctx.flags.file ?? "agents.yaml"; const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult({ would_enable: file, git: await getLocalVersionStatus(file) }, format); + emitResult({ would_enable: file, versioning: await getProjectVersionStatus(file) }, format); return; } - const result = await enableLocalVersioning(file, "Enable Bailian CLI versioning"); + const result = await enableProjectVersioning(file, "Enable Bailian CLI versioning"); if (format === "json") { emitResult(result, format); return; } if (result.version) { - emitBare(`Created baseline version ${result.version.short_commit} ${result.version.message}`); + emitBare( + `Created baseline version ${result.version.short_version} ${result.version.message}`, + ); } else { - emitBare("Current agents.yaml is already versioned; no commit was created."); + emitBare("Current agents.yaml is already versioned; no snapshot was created."); } emitBare("Automatic versioning is enabled for this agents.yaml."); - renderStatus(result.git); + renderStatus(result.versioning); }, }); export const managedAgentVersionDisable = defineCommand({ description: { - "en-US": "Disable Apply-time Git versioning without removing history", - "zh-CN": "关闭 Apply 后自动 Git 版本管理,但保留历史", + "en-US": "Disable Apply-time local snapshots without removing history", + "zh-CN": "关闭 Apply 后自动本地快照,但保留历史", }, auth: "none", usageArgs: "[--file ]", @@ -86,10 +88,10 @@ export const managedAgentVersionDisable = defineCommand({ const file = ctx.flags.file ?? "agents.yaml"; const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult({ would_disable: file, git: await getLocalVersionStatus(file) }, format); + emitResult({ would_disable: file, versioning: await getProjectVersionStatus(file) }, format); return; } - const status = await disableLocalVersioning(file); + const status = await disableProjectVersioning(file); if (format === "json") { emitResult(status, format); return; @@ -101,15 +103,15 @@ export const managedAgentVersionDisable = defineCommand({ export const managedAgentVersionStatus = defineCommand({ description: { - "en-US": "Show local Git versioning status for agents.yaml", - "zh-CN": "显示 agents.yaml 的本地 Git 版本管理状态", + "en-US": "Show local snapshot versioning status for agents.yaml", + "zh-CN": "显示 agents.yaml 的本地快照版本管理状态", }, auth: "none", usageArgs: "[--file ]", flags: FILE_FLAG, exampleArgs: ["", "--file agents.yaml --output json"], async run(ctx) { - const status = await getLocalVersionStatus(ctx.flags.file ?? "agents.yaml"); + const status = await getProjectVersionStatus(ctx.flags.file ?? "agents.yaml"); const format = detectOutputFormat(ctx.settings.output); if (format === "json") emitResult(status, format); else renderStatus(status); @@ -138,15 +140,15 @@ const LIST_FLAGS = { export const managedAgentVersionList = defineCommand({ description: { - "en-US": "List current-branch commits that changed agents.yaml", - "zh-CN": "列出当前分支中修改过 agents.yaml 的 Commit", + "en-US": "List local snapshots of agents.yaml", + "zh-CN": "列出 agents.yaml 的本地快照", }, auth: "none", usageArgs: "[--file ] [--limit ] [--cursor ]", flags: LIST_FLAGS, exampleArgs: ["", "--limit 20 --output json"], async run(ctx) { - const page = await listLocalVersions(ctx.flags.file ?? "agents.yaml", { + const page = await listProjectVersions(ctx.flags.file ?? "agents.yaml", { limit: ctx.flags.limit, cursor: ctx.flags.cursor, }); @@ -156,7 +158,7 @@ export const managedAgentVersionList = defineCommand({ return; } if (page.versions.length === 0) { - emitBare("No versions of agents.yaml exist on the current branch."); + emitBare("No local versions of agents.yaml exist."); return; } for (const version of page.versions) emitBare(formatVersion(version)); @@ -166,7 +168,7 @@ export const managedAgentVersionList = defineCommand({ const PREVIEW_FLAGS = { ...FILE_FLAG, - ...COMMIT_FLAG, + ...VERSION_FLAG, } satisfies FlagsDef; export const managedAgentVersionPreview = defineCommand({ @@ -175,11 +177,14 @@ export const managedAgentVersionPreview = defineCommand({ "zh-CN": "预览 agents.yaml 的历史版本", }, auth: "none", - usageArgs: "--commit [--file ]", + usageArgs: "--version-id [--file ]", flags: PREVIEW_FLAGS, - exampleArgs: ["--commit ", "--commit --output json"], + exampleArgs: ["--version-id ", "--version-id --output json"], async run(ctx) { - const preview = await previewLocalVersion(ctx.flags.file ?? "agents.yaml", ctx.flags.commit); + const preview = await previewProjectVersion( + ctx.flags.file ?? "agents.yaml", + ctx.flags.versionId, + ); const format = detectOutputFormat(ctx.settings.output); if (format === "json") emitResult(preview, format); else renderPreview(preview); @@ -203,12 +208,12 @@ export const managedAgentVersionRestore = defineCommand({ "zh-CN": "将 agents.yaml 历史版本恢复到工作区", }, auth: "none", - usageArgs: "--commit [--file ] [--yes]", + usageArgs: "--version-id [--file ] [--yes]", flags: RESTORE_FLAGS, - exampleArgs: ["--commit ", "--commit --yes --output json"], + exampleArgs: ["--version-id ", "--version-id --yes --output json"], async run(ctx) { const file = ctx.flags.file ?? "agents.yaml"; - const preview = await previewLocalVersion(file, ctx.flags.commit); + const preview = await previewProjectVersion(file, ctx.flags.versionId); const format = detectOutputFormat(ctx.settings.output); if (format !== "json") renderPreview(preview); if (!preview.can_restore) { @@ -220,49 +225,47 @@ export const managedAgentVersionRestore = defineCommand({ ); } if (ctx.settings.dryRun) { - emitResult({ would_restore: ctx.flags.commit, preview }, format); + emitResult({ would_restore: ctx.flags.versionId, preview }, format); return; } await confirmDangerousAction( - "Restore this version to the agents.yaml working tree? HEAD and agents.state.json will not change.", + "Restore this version to the agents.yaml working tree? Version history and agents.state.json will not change.", ctx.flags.yes, ); - const restored = await restoreLocalVersion(file, ctx.flags.commit, { - head: preview.base_head, + const restored = await restoreProjectVersion(file, ctx.flags.versionId, { + headVersion: preview.base_head_version, sourceRevision: preview.base_source_revision, }); if (format === "json") { - emitResult({ restored: ctx.flags.commit, preview: restored }, format); + emitResult({ restored: ctx.flags.versionId, preview: restored }, format); } else { emitBare( - `Restored ${ctx.flags.commit.slice(0, 12)} to the working tree. HEAD was not changed.`, + `Restored ${ctx.flags.versionId.slice(0, 12)} to the working tree. Version history was not changed.`, ); } }, }); -function renderStatus(status: LocalVersionStatus): void { - emitBare(`Git available: ${status.git_available ? "yes" : "no"}`); +function renderStatus(status: ProjectVersionStatus): void { emitBare(`Automatic versioning: ${status.enabled ? "enabled" : "disabled"}`); - emitBare(`Repository: ${status.repository_root ?? "none"}`); - emitBare(`Config path: ${status.config_path ?? "none"}`); - emitBare(`Branch: ${status.branch ?? "none"}`); - emitBare(`HEAD: ${status.head ?? "none"}`); + emitBare(`Version store: ${status.initialized ? status.store_root : "not initialized"}`); + emitBare(`Config path: ${status.config_path}`); + emitBare(`Current version: ${status.head_version ?? "none"}`); emitBare( - `agents.yaml: ${status.config_status}${status.config_versioned ? ", versioned" : ", unversioned"}`, + `agents.yaml: ${status.source_status}${status.source_versioned ? ", versioned" : ", unversioned"}`, ); - const blockers = [...new Set([...status.commit_blockers, ...status.restore_blockers])]; + const blockers = [...new Set([...status.write_blockers, ...status.restore_blockers])]; for (const blocker of blockers) emitBare(chalk.yellow(`Blocker: ${blocker}`)); } -function formatVersion(version: LocalProjectVersion): string { - return `${chalk.yellow(version.short_commit)} ${version.authored_at} ${version.message} ${chalk.dim(`(${version.author_name})`)}`; +function formatVersion(version: ProjectVersion): string { + return `${chalk.yellow(version.short_version)} ${version.created_at} ${version.message} ${chalk.dim(`(${version.created_by})`)}`; } -function renderPreview(preview: LocalVersionPreview): void { - emitBare(chalk.bold(`Version ${preview.commit}`)); +function renderPreview(preview: ProjectVersionPreview): void { + emitBare(chalk.bold(`Version ${preview.version_id}`)); emitBare(chalk.red("--- working tree")); - emitBare(chalk.green(`+++ ${preview.commit}`)); + emitBare(chalk.green(`+++ ${preview.version_id}`)); for (const line of buildLineDiff(preview.before_yaml, preview.after_yaml)) { if (line.kind === "deletion") emitBare(chalk.red(`-${line.text}`)); else if (line.kind === "addition") emitBare(chalk.green(`+${line.text}`)); diff --git a/packages/commands/src/commands/managed-agent/workbench.ts b/packages/commands/src/commands/managed-agent/workbench.ts index 059925c..08f806c 100644 --- a/packages/commands/src/commands/managed-agent/workbench.ts +++ b/packages/commands/src/commands/managed-agent/workbench.ts @@ -45,9 +45,9 @@ const WORKBENCH_NOTES = [ ...CREDENTIALS_NOTE, { "en-US": - "Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.", + "Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.", "zh-CN": - "Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;不会推送 Git Commit 或切换分支。", + "Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;本地版本使用共享的 .openagentpack/versions 项目版本存储,不依赖 Git。", }, ]; diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index 4034509..fffd535 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -130,6 +130,17 @@ describe("e2e: managed-agent", () => { ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--file|--provider|--yes/i); + expect(stderr).not.toContain("--ci"); + }); + + test("managed-agent init 不再暴露 Git 仓库脚手架", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "init", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).not.toContain("--git"); }); test("managed-agent version 暴露共享版本管理子命令", async () => { @@ -142,7 +153,7 @@ describe("e2e: managed-agent", () => { expect(stderr).toMatch(/enable|disable|status|list|preview|restore/i); }); - test("managed-agent version preview 缺少 --commit 时退出为用法错误 (2)", async () => { + test("managed-agent version preview 缺少 --version-id 时退出为用法错误 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", "version", @@ -150,7 +161,7 @@ describe("e2e: managed-agent", () => { "--quiet", ]); expect(exitCode).toBe(2); - expect(stderr).toMatch(/--commit|Missing required/i); + expect(stderr).toMatch(/--version-id|Missing required/i); }); test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { @@ -239,29 +250,6 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(data.provider).toBe("bailian"); }); - test("init --git --dry-run 仅输出仓库脚手架计划", async () => { - const targetDirectory = join( - process.cwd(), - `.managed-agent-git-dry-run-${process.pid}-${Date.now()}`, - ); - const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ - "managed-agent", - "init", - "--git", - targetDirectory, - "--dry-run", - "--output", - "json", - ]); - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson<{ - would_initialize_git_project?: string; - mode?: string; - }>(stdout); - expect(data.would_initialize_git_project).toBe(targetDirectory); - expect(data.mode).toBe("create"); - }); - test("workbench --dry-run 仅输出启动计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/packages/commands/tests/managed-agent-git-project.test.ts b/packages/commands/tests/managed-agent-git-project.test.ts deleted file mode 100644 index 4fe6305..0000000 --- a/packages/commands/tests/managed-agent-git-project.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, test } from "vite-plus/test"; -import { createGitProject } from "../src/commands/managed-agent/_engine/git-project.ts"; - -const temporaryDirectories: string[] = []; - -afterEach(async () => { - for (const directory of temporaryDirectories.splice(0)) { - await rm(directory, { recursive: true, force: true }); - } -}); - -describe("managed-agent init --git project scaffolding", () => { - test("creates a main-branch Git project without committing or configuring a remote", async () => { - const parentDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-project-")); - temporaryDirectories.push(parentDirectory); - const targetDirectory = join(parentDirectory, "agent-project"); - - const result = await createGitProject(targetDirectory, { - config: projectYaml(), - cliVersion: "1.17.1", - }); - - expect(result.mode).toBe("created"); - expect(result.initializedGit).toBe(true); - expect(result.createdFiles).toContain("agents.yaml"); - expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toContain("assistant:"); - expect(await readFile(join(targetDirectory, ".aoneci/bailian-cli.yml"), "utf8")).toContain( - "agents:apply:ci", - ); - expect(await readFile(join(targetDirectory, "README.md"), "utf8")).toContain( - "Create the remote repository yourself", - ); - }); - - test("upgrades an initialized config directory without overwriting agents.yaml", async () => { - const targetDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-upgrade-")); - temporaryDirectories.push(targetDirectory); - const originalSource = projectYaml().replace("assistant", "reviewer"); - await writeFile(join(targetDirectory, "agents.yaml"), originalSource); - - const result = await createGitProject(targetDirectory, { - config: projectYaml(), - cliVersion: "1.17.1", - }); - - expect(result.mode).toBe("upgraded"); - expect(result.preservedFiles).toContain("agents.yaml"); - expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toBe(originalSource); - }); -}); - -function projectYaml(): string { - return `version: "1" -providers: - bailian: - api_key: \${DASHSCOPE_API_KEY} -defaults: - provider: bailian -agents: - assistant: - model: qwen3.8-max - instructions: You are helpful. -`; -} diff --git a/packages/commands/tests/managed-agent-local-git.test.ts b/packages/commands/tests/managed-agent-local-git.test.ts deleted file mode 100644 index dfaa25a..0000000 --- a/packages/commands/tests/managed-agent-local-git.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { execFile } from "node:child_process"; -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { promisify } from "node:util"; -import type { PlannedAction } from "@openagentpack/sdk"; -import { afterEach, describe, expect, test } from "vite-plus/test"; -import { - commitAutomaticVersion, - disableLocalVersioning, - enableLocalVersioning, - getLocalVersionStatus, - prepareAutomaticVersion, - previewLocalVersion, - restoreLocalVersion, -} from "@openagentpack/local-git"; -import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts"; -import { assertCiApplyPolicy } from "../src/commands/managed-agent/apply.ts"; - -const execFileAsync = promisify(execFile); -const temporaryDirectories: string[] = []; -const gitIdentity = { - GIT_AUTHOR_NAME: "Bailian CLI Test", - GIT_AUTHOR_EMAIL: "bailian-cli@example.com", - GIT_COMMITTER_NAME: "Bailian CLI Test", - GIT_COMMITTER_EMAIL: "bailian-cli@example.com", -}; - -afterEach(async () => { - for (const directory of temporaryDirectories.splice(0)) { - await rm(directory, { recursive: true, force: true }); - } -}); - -describe("managed-agent local Git versions", () => { - test("uses the shared path-scoped switch and commits only agents.yaml", async () => { - const root = await temporaryDirectory(); - const configPath = join(root, "agents.yaml"); - const nestedDirectory = join(root, "nested"); - const nestedConfigPath = join(nestedDirectory, "agents.yaml"); - await mkdir(nestedDirectory); - await writeFile(configPath, projectYaml("First")); - await writeFile(nestedConfigPath, projectYaml("Second")); - await git(root, ["init", "--initial-branch", "main"]); - await writeFile(join(root, "staged.txt"), "staged\n"); - await git(root, ["add", "staged.txt"]); - const stagedBefore = await git(root, ["status", "--porcelain=v1", "--", "staged.txt"]); - - const enabled = await withGitIdentity(() => - enableLocalVersioning(configPath, "Enable Bailian CLI versioning"), - ); - - expect(enabled.git.enabled).toBe(true); - expect((await getLocalVersionStatus(nestedConfigPath)).enabled).toBe(false); - expect((await git(root, ["show", "--pretty=", "--name-only", "HEAD"])).trim()).toBe( - "agents.yaml", - ); - expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore); - expect( - await git(root, ["rev-parse", "--git-path", "openagentpack/local-git/versions"]), - ).toContain("openagentpack/local-git/versions"); - - await writeFile(configPath, projectYaml("First updated")); - const repeated = await withGitIdentity(() => - enableLocalVersioning(configPath, "Enable Bailian CLI versioning"), - ); - expect(repeated.version?.message).toBe("Enable Bailian CLI versioning"); - expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore); - - const disabled = await disableLocalVersioning(configPath); - expect(disabled.enabled).toBe(false); - }); - - test("auto-commits after success and restores without moving HEAD or changing permissions", async () => { - const root = await temporaryDirectory(); - const configPath = join(root, "agents.yaml"); - await writeFile(configPath, projectYaml("Version one")); - await chmod(configPath, 0o640); - const enabled = await withGitIdentity(() => - enableLocalVersioning(configPath, "Enable Bailian CLI versioning"), - ); - const firstCommit = enabled.version!.commit; - const secondSource = projectYaml("Version two"); - await writeFile(configPath, secondSource); - - const prepared = await withGitIdentity(() => prepareAutomaticVersion(configPath, secondSource)); - const version = await withGitIdentity(() => commitAutomaticVersion(prepared!)); - const headBeforeRestore = (await git(root, ["rev-parse", "HEAD"])).trim(); - expect(version?.message).toBe("Apply agents.yaml"); - - const preview = await previewLocalVersion(configPath, firstCommit); - expect(preview.can_restore).toBe(true); - expect(preview.after_yaml).toContain("Version one"); - await restoreLocalVersion(configPath, firstCommit, { - head: preview.base_head, - sourceRevision: preview.base_source_revision, - }); - - expect(await readFile(configPath, "utf8")).toContain("Version one"); - expect((await git(root, ["rev-parse", "HEAD"])).trim()).toBe(headBeforeRestore); - expect((await stat(configPath)).mode & 0o777).toBe(0o640); - }); - - test("rejects short SHAs and plaintext credentials", async () => { - const root = await temporaryDirectory(); - const configPath = join(root, "agents.yaml"); - await writeFile(configPath, projectYaml("Safe")); - const enabled = await withGitIdentity(() => - enableLocalVersioning(configPath, "Enable Bailian CLI versioning"), - ); - await expect(previewLocalVersion(configPath, enabled.version!.short_commit)).rejects.toThrow( - /full hexadecimal commit SHA/i, - ); - - await disableLocalVersioning(configPath); - await writeFile( - configPath, - projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"), - ); - await expect( - withGitIdentity(() => enableLocalVersioning(configPath, "Enable Bailian CLI versioning")), - ).rejects.toThrow(/environment variable reference/i); - }); -}); - -describe("managed-agent CI and Workbench policies", () => { - test("CI blocks delete actions and remote drift", () => { - expect(() => assertCiApplyPolicy([plannedAction("delete")])).toThrow(/blocked.*delete/i); - expect(() => assertCiApplyPolicy([plannedAction("update", "remote")])).toThrow( - /blocked.*remote drift/i, - ); - expect(() => assertCiApplyPolicy([plannedAction("update", "local")])).not.toThrow(); - }); - - test("Session Preview opens the requested Agent or falls back to Workbench", () => { - const summary = { - status: "valid", - agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }], - }; - expect( - playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"), - ).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" }); - expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual( - expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }), - ); - }); -}); - -async function temporaryDirectory(): Promise { - const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-git-")); - temporaryDirectories.push(directory); - return directory; -} - -function projectYaml(instructions: string): string { - return `version: "1" -providers: - qoder: {} -defaults: - provider: qoder -agents: - assistant: - model: ultimate - instructions: ${instructions} -`; -} - -function plannedAction( - action: "create" | "update" | "delete", - driftKind: "none" | "local" | "remote" | "both" = "none", -): PlannedAction { - return { - action, - driftKind, - address: { provider: "bailian", type: "agent", name: "assistant" }, - } as PlannedAction; -} - -async function git(workingDirectory: string, arguments_: string[]): Promise { - const result = await execFileAsync("git", arguments_, { - cwd: workingDirectory, - encoding: "utf8", - env: { ...process.env, ...gitIdentity }, - }); - return result.stdout; -} - -async function withGitIdentity(operation: () => Promise): Promise { - const previousEnvironment = Object.fromEntries( - Object.keys(gitIdentity).map((key) => [key, process.env[key]]), - ); - Object.assign(process.env, gitIdentity); - try { - return await operation(); - } finally { - for (const key of Object.keys(gitIdentity)) { - const value = previousEnvironment[key]; - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - } -} diff --git a/packages/commands/tests/managed-agent-local-versions.test.ts b/packages/commands/tests/managed-agent-local-versions.test.ts new file mode 100644 index 0000000..dae7d8f --- /dev/null +++ b/packages/commands/tests/managed-agent-local-versions.test.ts @@ -0,0 +1,139 @@ +import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + commitPreparedProjectVersion, + disableProjectVersioning, + enableProjectVersioning, + getProjectVersionStatus, + listProjectVersions, + prepareProjectVersion, + previewProjectVersion, + restoreProjectVersion, +} from "@openagentpack/project-versions"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + for (const directory of temporaryDirectories.splice(0)) { + await rm(directory, { recursive: true, force: true }); + } +}); + +describe("managed-agent local snapshot versions", () => { + test("uses the shared path-scoped switch and stores full YAML outside store.json", async () => { + const root = await temporaryDirectory(); + const configPath = join(root, "agents.yaml"); + const siblingDirectory = join(root, "nested"); + const siblingConfigPath = join(siblingDirectory, "agents.yaml"); + await mkdir(siblingDirectory); + await writeFile(configPath, projectYaml("First")); + await writeFile(siblingConfigPath, projectYaml("Second")); + + const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning"); + + expect(enabled.versioning.enabled).toBe(true); + expect(enabled.version?.message).toBe("Enable Bailian CLI versioning"); + expect((await getProjectVersionStatus(siblingConfigPath)).enabled).toBe(false); + const storeSource = await readFile(join(root, ".openagentpack/versions/store.json"), "utf8"); + expect(storeSource).not.toContain("instructions: First"); + const snapshotSource = await readFile( + join(root, ".openagentpack/versions/blobs", `${enabled.version!.source_hash}.yaml`), + "utf8", + ); + expect(snapshotSource).toContain("instructions: First"); + + await writeFile(configPath, projectYaml("First updated")); + const repeated = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning"); + expect(repeated.version?.message).toBe("Enable Bailian CLI versioning"); + expect((await listProjectVersions(configPath)).versions).toHaveLength(2); + + const disabled = await disableProjectVersioning(configPath); + expect(disabled.enabled).toBe(false); + }); + + test("auto-snapshots after success and restores without changing history or permissions", async () => { + const root = await temporaryDirectory(); + const configPath = join(root, "agents.yaml"); + await writeFile(configPath, projectYaml("Version one")); + await chmod(configPath, 0o640); + const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning"); + const firstVersion = enabled.version!.version_id; + const secondSource = projectYaml("Version two"); + await writeFile(configPath, secondSource); + + const prepared = await prepareProjectVersion(configPath, secondSource); + const version = await commitPreparedProjectVersion(prepared!); + const currentVersionBeforeRestore = (await getProjectVersionStatus(configPath)).head_version; + expect(version?.message).toBe("Apply agents.yaml"); + + const preview = await previewProjectVersion(configPath, firstVersion); + expect(preview.can_restore).toBe(true); + expect(preview.after_yaml).toContain("Version one"); + await restoreProjectVersion(configPath, firstVersion, { + headVersion: preview.base_head_version, + sourceRevision: preview.base_source_revision, + }); + + expect(await readFile(configPath, "utf8")).toContain("Version one"); + expect((await getProjectVersionStatus(configPath)).head_version).toBe( + currentVersionBeforeRestore, + ); + expect((await stat(configPath)).mode & 0o777).toBe(0o640); + }); + + test("rejects abbreviated version IDs and plaintext credentials", async () => { + const root = await temporaryDirectory(); + const configPath = join(root, "agents.yaml"); + await writeFile(configPath, projectYaml("Safe")); + const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning"); + await expect(previewProjectVersion(configPath, enabled.version!.short_version)).rejects.toThrow( + /full 64-character hexadecimal/i, + ); + + await disableProjectVersioning(configPath); + await writeFile( + configPath, + projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"), + ); + await expect( + enableProjectVersioning(configPath, "Enable Bailian CLI versioning"), + ).rejects.toThrow(/environment variable reference/i); + }); +}); + +describe("managed-agent Workbench policy", () => { + test("Session Preview opens the requested Agent or falls back to Workbench", () => { + const summary = { + status: "valid", + agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }], + }; + expect( + playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"), + ).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" }); + expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual( + expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }), + ); + }); +}); + +async function temporaryDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-versions-")); + temporaryDirectories.push(directory); + return directory; +} + +function projectYaml(instructions: string): string { + return `version: "1" +providers: + qoder: {} +defaults: + provider: qoder +agents: + assistant: + model: ultimate + instructions: ${instructions} +`; +} diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index aead755..7317565 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -6,8 +6,8 @@ metadata: bins: ["bl"] description: >- 阿里云百炼托管 Agent 声明式基础设施入口:用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、 - 创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地 Git 版本、 - 生成 CI 仓库、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 + 创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地快照版本、 + 和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 `bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、 apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。 反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、 @@ -21,12 +21,11 @@ description: >- ## Safety guardrail (the most important rule) -`apply` / `destroy` **mutate remote resources**. Interactive execution requires `--yes`; `apply --ci` is only for an already approved CI workflow: +`apply` / `destroy` **mutate remote resources**. Execution requires `--yes`: 1. Always run `bl managed-agent plan` first and show the diff to the user. 2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`. 3. Never add `--yes` on your own initiative before the user has confirmed. -4. Never use `--ci` to bypass user confirmation in an interactive task. CI mode blocks deletes and remote drift, but still mutates remote resources. ## IaC lifecycle @@ -38,22 +37,21 @@ description: >- 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` -## Workbench, local versions, and CI +## Workbench and local versions -| Intent | Command | -| ------------------------------------------- | ---------------------------------------------- | -| Launch project resource editing | `bl managed-agent workbench` | -| Launch one Agent Session Preview | `bl managed-agent playground --agent ` | -| Create or upgrade a local Git/CI repository | `bl managed-agent init --git ` | -| Enable/disable shared automatic versions | `bl managed-agent version enable` / `disable` | -| Inspect local version state and history | `bl managed-agent version status` / `list` | -| Preview or restore a historical YAML | `bl managed-agent version preview` / `restore` | +| Intent | Command | +| ---------------------------------------- | ---------------------------------------------- | +| Launch project resource editing | `bl managed-agent workbench` | +| Launch one Agent Session Preview | `bl managed-agent playground --agent ` | +| Enable/disable shared automatic versions | `bl managed-agent version enable` / `disable` | +| Inspect local version state and history | `bl managed-agent version status` / `list` | +| Preview or restore a historical YAML | `bl managed-agent version preview` / `restore` | -- Bailian CLI and Workbench use the same repository-local switch for the same Git worktree and `agents.yaml` path. The switch lives in private Git metadata and is not cloned or pushed. -- When enabled, a fully successful Apply creates a local commit containing only `agents.yaml`. Failed, partial, cancelled, and `--refresh-only` Apply runs do not commit. -- `version restore` writes the historical YAML to the working tree. It does not move `HEAD`, restore `agents.state.json`, create a commit, or Apply remote changes. +- Bailian CLI and Workbench use the same `.openagentpack/versions` store and enable switch for the same `agents.yaml`. Git is not required. +- `store.json` contains only the switch and head metadata. Immutable linked entries live under `entries/`, while complete YAML is stored as content-addressed blobs under `blobs/`. Neither `agents.state.json` nor referenced files are versioned. +- When enabled, a fully successful Apply creates a local snapshot only when `agents.yaml` changed. Failed, partial, cancelled, and `--refresh-only` Apply runs do not create one. +- `version restore` writes the historical YAML to the working tree. It does not move version history, restore `agents.state.json`, create a new snapshot, or Apply remote changes. - Workbench can edit local drafts while Apply is running, but saving/version mutations are blocked until Apply completes. External file edits are detected through revision checks. -- `init --git` never creates a remote repository or pushes. The generated Aone CI uses `apply --ci`, which blocks deletes and remote drift; review destructive changes in a separately approved workflow. ## Deployment as IaC diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index a535388..a79519a 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -13,7 +13,7 @@ Use this index for the skill-scoped quick index and global flags. | ---------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ | | `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) | | `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project | [managed-agent.md](managed-agent.md) | +| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) | | `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) | | `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) | | `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) | @@ -29,12 +29,12 @@ Use this index for the skill-scoped quick index and global flags. | `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) | | `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) | | `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) | -| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history | [managed-agent.md](managed-agent.md) | -| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml | [managed-agent.md](managed-agent.md) | -| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml | [managed-agent.md](managed-agent.md) | +| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history | [managed-agent.md](managed-agent.md) | +| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml | [managed-agent.md](managed-agent.md) | +| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml | [managed-agent.md](managed-agent.md) | | `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version | [managed-agent.md](managed-agent.md) | | `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree | [managed-agent.md](managed-agent.md) | -| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml | [managed-agent.md](managed-agent.md) | +| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml | [managed-agent.md](managed-agent.md) | | `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench | [managed-agent.md](managed-agent.md) | ## By group diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 28a52ee..5bf4004 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -11,7 +11,7 @@ Index: [index.md](index.md) | ---------------------------------- | -------------- | ------------------------------------------------------------- | | `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | | `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | -| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project | +| `bl managed-agent init` | No Auth | Create an agents.yaml template | | `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | | `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | | `bl managed-agent session create` | API Key | Create a new session for an agent | @@ -27,24 +27,24 @@ Index: [index.md](index.md) | `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | | `bl managed-agent state show` | No Auth | Show details of a resource in agents state | | `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | -| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history | -| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml | -| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml | +| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history | +| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml | +| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml | | `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version | | `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree | -| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml | +| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml | | `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench | ## Command details ### `bl managed-agent apply` -| Field | Value | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent apply` | -| **Description** | Apply planned changes to create/update/delete agent resources | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent apply [--file ] [--provider ] [--yes \| --ci] [--no-refresh] [--refresh-only] [--concurrency ]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent apply` | +| **Description** | Apply planned changes to create/update/delete agent resources | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent apply [--file ] [--provider ] [--yes] [--no-refresh] [--refresh-only] [--concurrency ]` | #### Flags @@ -53,7 +53,6 @@ Index: [index.md](index.md) | `--file ` | string | no | Config file path (default: agents.yaml) | | `--provider ` | string | no | Target provider (default: all configured) | | `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | -| `--ci` | switch | no | Run non-interactively while blocking deletes and remote drift | | `--no-refresh` | switch | no | Skip refreshing state from remote before planning | | `--refresh-only` | switch | no | Refresh state without mutating remote resources | | `--concurrency ` | number | no | Max independent resources to apply in parallel (default 6, max 10) | @@ -76,10 +75,6 @@ bl managed-agent apply --yes bl managed-agent apply --provider bailian --yes ``` -```bash -bl managed-agent apply --ci -``` - ### `bl managed-agent destroy` | Field | Value | @@ -117,12 +112,12 @@ bl managed-agent destroy --yes --cascade ### `bl managed-agent init` -| Field | Value | -| ------------------ | --------------------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent init` | -| **Description** | Create an agents.yaml template or a local CI/Git project | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent init [--provider ] [--agent-name ] [--file ] [--git ] [--force]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent init` | +| **Description** | Create an agents.yaml template | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent init [--provider ] [--agent-name ] [--file ] [--force]` | #### Flags @@ -131,7 +126,6 @@ bl managed-agent destroy --yes --cascade | `--provider ` | string | no | Provider: bailian, claude, qoder, ark, all (default: bailian) | | `--agent-name ` | string | no | Name of the first agent (default: assistant) | | `--file ` | string | no | Output config path (default: agents.yaml) | -| `--git ` | string | no | Create or add CI/Git scaffolding in this project directory | | `--force` | switch | no | Overwrite an existing config file | #### Examples @@ -144,14 +138,6 @@ bl managed-agent init bl managed-agent init --provider bailian --agent-name assistant ``` -```bash -bl managed-agent init --git ./my-agents -``` - -```bash -bl managed-agent init --git . -``` - ### `bl managed-agent plan` | Field | Value | @@ -218,7 +204,7 @@ bl managed-agent plan --no-refresh - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. -- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches. +- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git. #### Examples @@ -681,12 +667,12 @@ bl managed-agent validate --file agents.yaml ### `bl managed-agent version disable` -| Field | Value | -| ------------------ | ---------------------------------------------------------- | -| **Name** | `managed-agent version disable` | -| **Description** | Disable Apply-time Git versioning without removing history | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version disable [--file ]` | +| Field | Value | +| ------------------ | ----------------------------------------------------------- | +| **Name** | `managed-agent version disable` | +| **Description** | Disable Apply-time local snapshots without removing history | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version disable [--file ]` | #### Flags @@ -709,7 +695,7 @@ bl managed-agent version disable --file agents.yaml | Field | Value | | ------------------ | ------------------------------------------------- | | **Name** | `managed-agent version enable` | -| **Description** | Enable Apply-time Git versioning for agents.yaml | +| **Description** | Enable Apply-time local snapshots for agents.yaml | | **Authentication** | No Auth | | **Usage** | `bl managed-agent version enable [--file ]` | @@ -734,7 +720,7 @@ bl managed-agent version enable --file agents.yaml | Field | Value | | ------------------ | --------------------------------------------------------------------------------- | | **Name** | `managed-agent version list` | -| **Description** | List current-branch commits that changed agents.yaml | +| **Description** | List local snapshots of agents.yaml | | **Authentication** | No Auth | | **Usage** | `bl managed-agent version list [--file ] [--limit ] [--cursor ]` | @@ -758,65 +744,65 @@ bl managed-agent version list --limit 20 --output json ### `bl managed-agent version preview` -| Field | Value | -| ------------------ | ---------------------------------------------------------------------- | -| **Name** | `managed-agent version preview` | -| **Description** | Preview a historical agents.yaml version | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version preview --commit [--file ]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------ | +| **Name** | `managed-agent version preview` | +| **Description** | Preview a historical agents.yaml version | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version preview --version-id [--file ]` | #### Flags -| Flag | Type | Required | Description | -| --------------------- | ------ | -------- | --------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | -| `--commit ` | string | yes | Full commit SHA from the current branch | +| Flag | Type | Required | Description | +| ----------------------------- | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--version-id ` | string | yes | Full local version ID | #### Examples ```bash -bl managed-agent version preview --commit +bl managed-agent version preview --version-id ``` ```bash -bl managed-agent version preview --commit --output json +bl managed-agent version preview --version-id --output json ``` ### `bl managed-agent version restore` -| Field | Value | -| ------------------ | ------------------------------------------------------------------------------ | -| **Name** | `managed-agent version restore` | -| **Description** | Restore a historical agents.yaml version to the working tree | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version restore --commit [--file ] [--yes]` | +| Field | Value | +| ------------------ | -------------------------------------------------------------------------------------- | +| **Name** | `managed-agent version restore` | +| **Description** | Restore a historical agents.yaml version to the working tree | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version restore --version-id [--file ] [--yes]` | #### Flags -| Flag | Type | Required | Description | -| --------------------- | ------ | -------- | ------------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | -| `--commit ` | string | yes | Full commit SHA from the current branch | -| `--yes` | switch | no | Restore without an interactive confirmation | +| Flag | Type | Required | Description | +| ----------------------------- | ------ | -------- | ------------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--version-id ` | string | yes | Full local version ID | +| `--yes` | switch | no | Restore without an interactive confirmation | #### Examples ```bash -bl managed-agent version restore --commit +bl managed-agent version restore --version-id ``` ```bash -bl managed-agent version restore --commit --yes --output json +bl managed-agent version restore --version-id --yes --output json ``` ### `bl managed-agent version status` -| Field | Value | -| ------------------ | ------------------------------------------------- | -| **Name** | `managed-agent version status` | -| **Description** | Show local Git versioning status for agents.yaml | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version status [--file ]` | +| Field | Value | +| ------------------ | ----------------------------------------------------- | +| **Name** | `managed-agent version status` | +| **Description** | Show local snapshot versioning status for agents.yaml | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version status [--file ]` | #### Flags @@ -858,7 +844,7 @@ bl managed-agent version status --file agents.yaml --output json - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. -- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches. +- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git. #### Examples