From b93e0d586d898f5ceebaae1645b8bf22302e9604 Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Tue, 25 Aug 2026 20:41:47 +0800 Subject: [PATCH 1/3] feat(managed-agent): add Workbench and Git-backed CI versioning Add Workbench launch and local Git version commands, enable automatic agents.yaml versioning after successful applies, introduce CI-safe apply policies, and reuse the shared @openagentpack/local-git package. --- packages/cli/src/commands.ts | 16 + packages/commands/package.json | 3 +- .../managed-agent/_engine/git-project.ts | 505 ++++++++++++++++++ .../_engine/playground-launcher.ts | 375 +++++++++++++ .../src/commands/managed-agent/apply.ts | 114 +++- .../src/commands/managed-agent/init.ts | 62 ++- .../src/commands/managed-agent/version.ts | 359 +++++++++++++ .../src/commands/managed-agent/workbench.ts | 126 +++++ packages/commands/src/index.ts | 12 + .../tests/e2e/managed-agent.e2e.test.ts | 62 +++ packages/commands/tests/e2e/topic-routes.ts | 8 + .../tests/managed-agent-git-project.test.ts | 67 +++ .../tests/managed-agent-local-git.test.ts | 202 +++++++ skills/bailian-managed-agent/SKILL.md | 23 +- .../bailian-managed-agent/reference/index.md | 52 +- .../reference/managed-agent.md | 319 +++++++++-- 16 files changed, 2237 insertions(+), 68 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/_engine/git-project.ts create mode 100644 packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts create mode 100644 packages/commands/src/commands/managed-agent/version.ts create mode 100644 packages/commands/src/commands/managed-agent/workbench.ts create mode 100644 packages/commands/tests/managed-agent-git-project.test.ts create mode 100644 packages/commands/tests/managed-agent-local-git.test.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 27422935b..e52ce30c4 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -139,6 +139,14 @@ import { managedAgentPlan, managedAgentApply, managedAgentDestroy, + managedAgentWorkbench, + managedAgentPlayground, + managedAgentVersionEnable, + managedAgentVersionDisable, + managedAgentVersionStatus, + managedAgentVersionList, + managedAgentVersionPreview, + managedAgentVersionRestore, managedAgentStateList, managedAgentStateShow, managedAgentStateRm, @@ -300,6 +308,14 @@ export const commands: Record = { "managed-agent plan": managedAgentPlan, "managed-agent apply": managedAgentApply, "managed-agent destroy": managedAgentDestroy, + "managed-agent workbench": managedAgentWorkbench, + "managed-agent playground": managedAgentPlayground, + "managed-agent version enable": managedAgentVersionEnable, + "managed-agent version disable": managedAgentVersionDisable, + "managed-agent version status": managedAgentVersionStatus, + "managed-agent version list": managedAgentVersionList, + "managed-agent version preview": managedAgentVersionPreview, + "managed-agent version restore": managedAgentVersionRestore, "managed-agent state list": managedAgentStateList, "managed-agent state show": managedAgentStateShow, "managed-agent state rm": managedAgentStateRm, diff --git a/packages/commands/package.json b/packages/commands/package.json index a1943e1e5..d6ab50a93 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -40,7 +40,8 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/sdk": "0.3.2", + "@openagentpack/local-git": "0.4.0", + "@openagentpack/sdk": "0.4.0", "bailian-cli-core": "workspace:*", "bailian-cli-runtime": "workspace:*", "boxen": "catalog:", diff --git a/packages/commands/src/commands/managed-agent/_engine/git-project.ts b/packages/commands/src/commands/managed-agent/_engine/git-project.ts new file mode 100644 index 000000000..eb96b5f71 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/git-project.ts @@ -0,0 +1,505 @@ +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/_engine/playground-launcher.ts b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts new file mode 100644 index 000000000..37198a341 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts @@ -0,0 +1,375 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { createHash, randomBytes } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core"; +import { emitBare } from "bailian-cli-runtime"; + +const PLAYGROUND_PACKAGE = "@openagentpack/playground"; +const DEFAULT_PORT = 4848; +const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i; + +export interface PlaygroundLaunchOptions { + port?: number; + open: boolean; + file: string; + agent?: string; + surface: "preview" | "workbench"; + client: Client; + settings: Settings; +} + +interface Launcher { + command: string; + args: string[]; + version?: string; + fetched: boolean; +} + +interface ExistingPlayground { + version: string; + pid: number; + projectId?: string; +} + +interface PlaygroundProjectSummary { + status?: string; + agents?: Array<{ agent?: { id?: string } }>; +} + +export interface PlaygroundBrowserTarget { + url: string; + warning?: string; +} + +export async function launchManagedAgentPlayground( + options: PlaygroundLaunchOptions, +): Promise { + assertSupportedNodeVersion(); + const port = options.port ?? DEFAULT_PORT; + if (!Number.isInteger(port) || port <= 0 || port > 65_535) { + throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE); + } + const configPath = resolve(options.file); + const projectId = createHash("sha256").update(configPath).digest("hex").slice(0, 16); + const launcher = resolveLauncher(); + const existing = await probeExistingPlayground(port); + if (existing) { + const reusable = + existing.projectId === projectId && + (launcher.version === undefined || existing.version === launcher.version); + if (reusable) { + emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`); + await openPlaygroundSurface(port, options.surface, options.agent, options.open); + return; + } + const released = await replaceExistingPlayground(existing, port); + if (!released) { + throw new BailianError( + `Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`, + ExitCode.GENERAL, + "Stop it manually or choose another --port.", + ); + } + } + + const environment = buildPlaygroundEnvironment(options, port, configPath); + if (launcher.fetched) { + emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`); + } + const child = spawn(launcher.command, launcher.args, { + env: environment, + stdio: ["inherit", "pipe", "inherit"], + }); + const removeSignalForwarding = forwardSignals(child); + try { + const readyPort = await waitForPlaygroundReady(child, port, 30_000, projectId); + if (readyPort === null) { + throw new BailianError( + `Workbench did not become ready in time. Check the logs above, then open http://localhost:${port}.`, + ExitCode.GENERAL, + ); + } + emitBare(`Workbench ready at http://localhost:${readyPort}`); + await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open); + const exitCode = await waitForChildExit(child); + if (exitCode !== 0) { + throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL); + } + } finally { + removeSignalForwarding(); + } +} + +export function playgroundBrowserTargetFromSummary( + baseUrl: string, + summary: PlaygroundProjectSummary, + requestedAgent?: string, +): PlaygroundBrowserTarget { + if (summary.status !== "valid") return { url: baseUrl }; + const agentIds = (summary.agents ?? []) + .map((entry) => entry.agent?.id?.trim()) + .filter((agentId): agentId is string => Boolean(agentId)); + const requested = requestedAgent?.trim(); + if (requested) { + if (agentIds.includes(requested)) { + return { url: `${baseUrl}/agents/${encodeURIComponent(requested)}/preview` }; + } + return { + url: baseUrl, + warning: `Agent '${requested}' was not found. Opening the project Workbench instead.`, + }; + } + if (agentIds.length === 1) { + return { url: `${baseUrl}/agents/${encodeURIComponent(agentIds[0]!)}/preview` }; + } + if (agentIds.length > 1) { + return { + url: baseUrl, + warning: + "This project declares multiple Agents. Opening the Workbench; rerun with --agent for Preview.", + }; + } + return { url: baseUrl }; +} + +function assertSupportedNodeVersion(): void { + const majorVersion = Number(process.versions.node.split(".")[0]); + if (Number.isFinite(majorVersion) && majorVersion >= 22) return; + throw new BailianError( + "Managed Agent Workbench requires Node.js 22 or later.", + ExitCode.USAGE, + "Upgrade Node.js for Workbench; other Bailian CLI commands continue to support Node.js 18.17+.", + ); +} + +function resolveLauncher(): Launcher { + const explicit = + process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() || + process.env.AGENTS_PLAYGROUND_BIN?.trim(); + if (explicit) { + if (!existsSync(explicit)) { + throw new BailianError( + `Configured Workbench binary does not exist: ${explicit}`, + ExitCode.USAGE, + ); + } + return { command: process.execPath, args: [explicit], fetched: false }; + } + + const installed = resolveInstalledPlayground(); + if (installed) return installed; + + const monorepoBinary = findLocalPlaygroundBin(process.cwd()); + if (monorepoBinary) { + return { command: process.execPath, args: [monorepoBinary], fetched: false }; + } + + const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest"; + return { + command: "npx", + args: ["-y", `${PLAYGROUND_PACKAGE}@${requestedVersion}`], + version: requestedVersion === "latest" ? undefined : requestedVersion, + fetched: true, + }; +} + +function resolveInstalledPlayground(): Launcher | undefined { + try { + const require = createRequire(import.meta.url); + const packageJsonPath = require.resolve(`${PLAYGROUND_PACKAGE}/package.json`); + const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { + version?: string; + bin?: string | Record; + }; + const relativeBinary = + typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["agents-playground"]; + if (!relativeBinary) return undefined; + const binaryPath = resolve(dirname(packageJsonPath), relativeBinary); + if (!existsSync(binaryPath)) return undefined; + return { + command: process.execPath, + args: [binaryPath], + version: manifest.version, + fetched: false, + }; + } catch { + return undefined; + } +} + +function findLocalPlaygroundBin(startDirectory: string): string | undefined { + let directory = startDirectory; + for (let depth = 0; depth < 10; depth += 1) { + const candidate = resolve(directory, "packages/playground/dist/bin/playground.js"); + if (existsSync(candidate)) return candidate; + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + return undefined; +} + +function buildPlaygroundEnvironment( + options: PlaygroundLaunchOptions, + port: number, + configPath: string, +): NodeJS.ProcessEnv { + const credential = options.client.exportApiCredential(); + const environment: NodeJS.ProcessEnv = { + ...process.env, + PORT: String(port), + AGENTS_CONFIG_PATH: configPath, + AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"), + }; + if (credential) environment.DASHSCOPE_API_KEY = credential.token; + const baseUrl = options.client.baseUrl.replace(/\/+$/, ""); + environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio") + ? baseUrl + : `${baseUrl}/api/v1/agentstudio`; + if (options.settings.workspaceId) { + environment.BAILIAN_WORKSPACE_ID = options.settings.workspaceId; + } + return environment; +} + +async function waitForPlaygroundReady( + child: ChildProcess, + fallbackPort: number, + timeoutMs: number, + expectedProjectId: string, +): Promise { + let port = fallbackPort; + let outputBuffer = ""; + child.stdout?.on("data", (chunk: Buffer | string) => { + process.stdout.write(chunk); + outputBuffer += chunk.toString(); + const match = outputBuffer.match(PLAYGROUND_URL_PATTERN); + if (match?.[1]) port = Number(match[1]); + }); + + const deadline = Date.now() + timeoutMs; + while (Date.now() <= deadline) { + if (child.exitCode !== null) return null; + try { + const response = await fetch(`http://localhost:${port}/health`, { + signal: AbortSignal.timeout(1_000), + }); + const body = response.ok + ? ((await response.json()) as { playground?: { project_id?: string } }) + : undefined; + if (body?.playground?.project_id === expectedProjectId) return port; + } catch { + // Not ready yet. + } + await new Promise((resolveWait) => setTimeout(resolveWait, 300)); + } + return null; +} + +async function probeExistingPlayground(port: number): Promise { + try { + const response = await fetch(`http://localhost:${port}/health`, { + signal: AbortSignal.timeout(2_000), + }); + if (!response.ok) return null; + const body = (await response.json()) as { + playground?: { version?: string; pid?: number; project_id?: string }; + }; + if (!body.playground?.pid) return null; + return { + version: body.playground.version ?? "unknown", + pid: body.playground.pid, + projectId: body.playground.project_id, + }; + } catch { + return null; + } +} + +async function replaceExistingPlayground( + existing: ExistingPlayground, + port: number, +): Promise { + emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`); + try { + process.kill(existing.pid, "SIGTERM"); + } catch { + return true; + } + for (let attempt = 0; attempt < 30; attempt += 1) { + await new Promise((resolveWait) => setTimeout(resolveWait, 100)); + if (!(await probeExistingPlayground(port))) return true; + } + return false; +} + +async function openPlaygroundSurface( + port: number, + surface: "preview" | "workbench", + requestedAgent: string | undefined, + shouldOpen: boolean, +): Promise { + if (!shouldOpen) return; + const target = + surface === "workbench" + ? { url: `http://localhost:${port}` } + : await resolvePlaygroundBrowserTarget(port, requestedAgent); + if (target.warning) emitBare(`Warning: ${target.warning}`); + openBrowser(target.url); +} + +async function resolvePlaygroundBrowserTarget( + port: number, + requestedAgent?: string, +): Promise { + const baseUrl = `http://localhost:${port}`; + try { + const response = await fetch(`${baseUrl}/api/project`, { + signal: AbortSignal.timeout(3_000), + }); + if (!response.ok) return { url: baseUrl }; + return playgroundBrowserTargetFromSummary( + baseUrl, + (await response.json()) as PlaygroundProjectSummary, + requestedAgent, + ); + } catch { + return { url: baseUrl }; + } +} + +function openBrowser(url: string): void { + const command = + process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; + const args = process.platform === "win32" ? ["", url] : [url]; + try { + spawn(command, args, { + stdio: "ignore", + detached: true, + shell: process.platform === "win32", + }).unref(); + } catch { + emitBare(`Could not open a browser automatically. Visit ${url}`); + } +} + +function forwardSignals(child: ChildProcess): () => void { + const forwardInterrupt = () => child.kill("SIGINT"); + const forwardTerminate = () => child.kill("SIGTERM"); + process.on("SIGINT", forwardInterrupt); + process.on("SIGTERM", forwardTerminate); + return () => { + process.off("SIGINT", forwardInterrupt); + process.off("SIGTERM", forwardTerminate); + }; +} + +function waitForChildExit(child: ChildProcess): Promise { + if (child.exitCode !== null) return Promise.resolve(child.exitCode); + return new Promise((resolveExit, rejectExit) => { + child.once("exit", (exitCode) => resolveExit(exitCode ?? 0)); + child.once("error", rejectExit); + }); +} diff --git a/packages/commands/src/commands/managed-agent/apply.ts b/packages/commands/src/commands/managed-agent/apply.ts index 6040a0add..320ef8a3c 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -6,7 +6,12 @@ import { type FlagsDef, } from "bailian-cli-core"; import { emitBare, emitResult } from "bailian-cli-runtime"; -import { executePlannedProject, planProjectContext } from "@openagentpack/sdk"; +import { + executePlannedProject, + planProjectContext, + type PlannedAction, + UserError, +} from "@openagentpack/sdk"; import { formatResourceLabel } from "./_engine/address-utils.ts"; import { assertProviderConfigured, @@ -16,6 +21,12 @@ import { 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"; const APPLY_FLAGS = { file: { @@ -41,6 +52,13 @@ 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: { @@ -48,6 +66,13 @@ const APPLY_FLAGS = { "zh-CN": "规划前跳过从远端刷新状态", }, }, + refreshOnly: { + type: "switch", + description: { + "en-US": "Refresh state without mutating remote resources", + "zh-CN": "仅刷新 State,不修改远端资源", + }, + }, concurrency: { type: "number", valueHint: "", @@ -64,10 +89,18 @@ export default defineCommand({ "zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源", }, auth: "apiKey", - usageArgs: "[--file ] [--provider ] [--yes] [--concurrency ]", + usageArgs: + "[--file ] [--provider ] [--yes | --ci] [--no-refresh] [--refresh-only] [--concurrency ]", flags: APPLY_FLAGS, - exampleArgs: ["--yes", "--provider bailian --yes"], + exampleArgs: ["--yes", "--provider bailian --yes", "--ci"], 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); @@ -80,6 +113,8 @@ export default defineCommand({ provider: flags.provider ?? "all", refresh: !flags.noRefresh, concurrency: flags.concurrency, + ci: flags.ci, + refresh_only: flags.refreshOnly, }, config_file: file, hint: "Run `managed-agent plan` to preview the exact resource changes.", @@ -89,16 +124,19 @@ export default defineCommand({ return; } - const planned = await withAgentErrors(() => + const versionSource = await readVersionSource(file); + + const { planned, runtime } = await withAgentErrors(() => withStdoutProtected(async () => { const runtime = await buildAgentRuntime(ctx, file); assertProviderConfigured(runtime, flags.provider); - return planProjectContext(runtime, { + const planned = await planProjectContext(runtime, { provider: flags.provider, refresh: !flags.noRefresh, quiet: true, onFeedback: renderAgentFeedback, }); + return { planned, runtime }; }), ); @@ -118,6 +156,13 @@ export default defineCommand({ const actionable = plan.actions.filter((action) => action.action !== "no-op"); if (actionable.length === 0) { + if (!flags.refreshOnly) { + const preparedVersion = await prepareAutomaticVersion( + runtime.configPath, + versionSource.source, + ); + await commitSuccessfulApplyVersion(preparedVersion, format); + } if (format === "json") emitResult({ succeeded: 0, failed: 0, skipped: 0, results: [] }, format); else emitBare("No changes. Infrastructure is up-to-date."); @@ -127,13 +172,32 @@ 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)}`); } - if (!flags.yes) { + if (flags.refreshOnly) { + if (format === "json") { + emitResult( + { + refresh_only: true, + actions: actionable, + succeeded: 0, + failed: 0, + skipped: actionable.length, + }, + format, + ); + } else { + emitBare("Refresh-only mode: no remote mutations were performed."); + } + return; + } + + if (!flags.yes && !flags.ci) { throw new BailianError( `Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`, ExitCode.USAGE, @@ -141,6 +205,8 @@ export default defineCommand({ ); } + const preparedVersion = await prepareAutomaticVersion(runtime.configPath, versionSource.source); + const result = await withAgentErrors(() => withStdoutProtected(() => executePlannedProject(planned, { @@ -161,6 +227,40 @@ export default defineCommand({ emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`); } - if (failed > 0) throw new BailianError("Apply failed.", ExitCode.GENERAL); + 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); }, }); + +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, + format: "text" | "json", +): Promise { + if (!prepared) return; + const version = await commitAutomaticVersion(prepared); + if (version && format !== "json") { + emitBare(`Created local version ${version.short_commit} (${version.message}).`); + } +} diff --git a/packages/commands/src/commands/managed-agent/init.ts b/packages/commands/src/commands/managed-agent/init.ts index ac3c83caa..f2fc8eeb6 100644 --- a/packages/commands/src/commands/managed-agent/init.ts +++ b/packages/commands/src/commands/managed-agent/init.ts @@ -8,6 +8,7 @@ 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 @@ -100,6 +101,14 @@ 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": "覆盖已有配置文件" }, @@ -108,13 +117,19 @@ const INIT_FLAGS = { export default defineCommand({ description: { - "en-US": "Create a new agents.yaml template", - "zh-CN": "创建新的 agents.yaml 模板", + "en-US": "Create an agents.yaml template or a local CI/Git project", + "zh-CN": "创建 agents.yaml 模板或本地 CI/Git 项目", }, auth: "none", - usageArgs: "[--provider ] [--agent-name ] [--file ] [--force]", + usageArgs: + "[--provider ] [--agent-name ] [--file ] [--git ] [--force]", flags: INIT_FLAGS, - exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"], + 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; + }, async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); @@ -122,6 +137,45 @@ 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 new file mode 100644 index 000000000..e9caefc1a --- /dev/null +++ b/packages/commands/src/commands/managed-agent/version.ts @@ -0,0 +1,359 @@ +import { + BailianError, + defineCommand, + detectOutputFormat, + ExitCode, + type FlagsDef, +} from "bailian-cli-core"; +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"; + +const FILE_FLAG = { + file: { + type: "string", + valueHint: "", + description: { + "en-US": "Config file path (default: agents.yaml)", + "zh-CN": "配置文件路径(默认:agents.yaml)", + }, + }, +} satisfies FlagsDef; + +const COMMIT_FLAG = { + commit: { + type: "string", + valueHint: "", + required: true, + description: { + "en-US": "Full commit SHA from the current branch", + "zh-CN": "当前分支中的完整 Commit SHA", + }, + }, +} satisfies FlagsDef; + +export const managedAgentVersionEnable = defineCommand({ + description: { + "en-US": "Enable Apply-time Git versioning for agents.yaml", + "zh-CN": "为 agents.yaml 启用 Apply 后自动 Git 版本管理", + }, + auth: "none", + usageArgs: "[--file ]", + flags: FILE_FLAG, + exampleArgs: ["", "--file agents.yaml"], + async run(ctx) { + 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); + return; + } + const result = await enableLocalVersioning(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}`); + } else { + emitBare("Current agents.yaml is already versioned; no commit was created."); + } + emitBare("Automatic versioning is enabled for this agents.yaml."); + renderStatus(result.git); + }, +}); + +export const managedAgentVersionDisable = defineCommand({ + description: { + "en-US": "Disable Apply-time Git versioning without removing history", + "zh-CN": "关闭 Apply 后自动 Git 版本管理,但保留历史", + }, + auth: "none", + usageArgs: "[--file ]", + flags: FILE_FLAG, + exampleArgs: ["", "--file agents.yaml"], + async run(ctx) { + 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); + return; + } + const status = await disableLocalVersioning(file); + if (format === "json") { + emitResult(status, format); + return; + } + emitBare("Automatic versioning is disabled for this agents.yaml."); + renderStatus(status); + }, +}); + +export const managedAgentVersionStatus = defineCommand({ + description: { + "en-US": "Show local Git versioning status for agents.yaml", + "zh-CN": "显示 agents.yaml 的本地 Git 版本管理状态", + }, + 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 format = detectOutputFormat(ctx.settings.output); + if (format === "json") emitResult(status, format); + else renderStatus(status); + }, +}); + +const LIST_FLAGS = { + ...FILE_FLAG, + limit: { + type: "number", + valueHint: "", + description: { + "en-US": "Maximum versions to return (default: 50, max: 100)", + "zh-CN": "最多返回的版本数(默认:50,最大:100)", + }, + }, + cursor: { + type: "string", + valueHint: "", + description: { + "en-US": "Pagination cursor returned by the previous page", + "zh-CN": "上一页返回的分页游标", + }, + }, +} satisfies FlagsDef; + +export const managedAgentVersionList = defineCommand({ + description: { + "en-US": "List current-branch commits that changed agents.yaml", + "zh-CN": "列出当前分支中修改过 agents.yaml 的 Commit", + }, + 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", { + limit: ctx.flags.limit, + cursor: ctx.flags.cursor, + }); + const format = detectOutputFormat(ctx.settings.output); + if (format === "json") { + emitResult(page, format); + return; + } + if (page.versions.length === 0) { + emitBare("No versions of agents.yaml exist on the current branch."); + return; + } + for (const version of page.versions) emitBare(formatVersion(version)); + if (page.next_cursor) emitBare(chalk.dim(`Next cursor: ${page.next_cursor}`)); + }, +}); + +const PREVIEW_FLAGS = { + ...FILE_FLAG, + ...COMMIT_FLAG, +} satisfies FlagsDef; + +export const managedAgentVersionPreview = defineCommand({ + description: { + "en-US": "Preview a historical agents.yaml version", + "zh-CN": "预览 agents.yaml 的历史版本", + }, + auth: "none", + usageArgs: "--commit [--file ]", + flags: PREVIEW_FLAGS, + exampleArgs: ["--commit ", "--commit --output json"], + async run(ctx) { + const preview = await previewLocalVersion(ctx.flags.file ?? "agents.yaml", ctx.flags.commit); + const format = detectOutputFormat(ctx.settings.output); + if (format === "json") emitResult(preview, format); + else renderPreview(preview); + }, +}); + +const RESTORE_FLAGS = { + ...PREVIEW_FLAGS, + yes: { + type: "switch", + description: { + "en-US": "Restore without an interactive confirmation", + "zh-CN": "无需交互确认直接恢复", + }, + }, +} satisfies FlagsDef; + +export const managedAgentVersionRestore = defineCommand({ + description: { + "en-US": "Restore a historical agents.yaml version to the working tree", + "zh-CN": "将 agents.yaml 历史版本恢复到工作区", + }, + auth: "none", + usageArgs: "--commit [--file ] [--yes]", + flags: RESTORE_FLAGS, + exampleArgs: ["--commit ", "--commit --yes --output json"], + async run(ctx) { + const file = ctx.flags.file ?? "agents.yaml"; + const preview = await previewLocalVersion(file, ctx.flags.commit); + const format = detectOutputFormat(ctx.settings.output); + if (format !== "json") renderPreview(preview); + if (!preview.can_restore) { + throw new BailianError( + preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ?? + preview.blockers[0] ?? + "This version cannot be restored.", + ExitCode.GENERAL, + ); + } + if (ctx.settings.dryRun) { + emitResult({ would_restore: ctx.flags.commit, preview }, format); + return; + } + await confirmDangerousAction( + "Restore this version to the agents.yaml working tree? HEAD and agents.state.json will not change.", + ctx.flags.yes, + ); + const restored = await restoreLocalVersion(file, ctx.flags.commit, { + head: preview.base_head, + sourceRevision: preview.base_source_revision, + }); + if (format === "json") { + emitResult({ restored: ctx.flags.commit, preview: restored }, format); + } else { + emitBare( + `Restored ${ctx.flags.commit.slice(0, 12)} to the working tree. HEAD was not changed.`, + ); + } + }, +}); + +function renderStatus(status: LocalVersionStatus): void { + emitBare(`Git available: ${status.git_available ? "yes" : "no"}`); + 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( + `agents.yaml: ${status.config_status}${status.config_versioned ? ", versioned" : ", unversioned"}`, + ); + const blockers = [...new Set([...status.commit_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 renderPreview(preview: LocalVersionPreview): void { + emitBare(chalk.bold(`Version ${preview.commit}`)); + emitBare(chalk.red("--- working tree")); + emitBare(chalk.green(`+++ ${preview.commit}`)); + 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}`)); + else emitBare(chalk.dim(` ${line.text}`)); + } + for (const diagnostic of preview.diagnostics) { + const color = + diagnostic.severity === "error" + ? chalk.red + : diagnostic.severity === "warning" + ? chalk.yellow + : chalk.dim; + emitBare(color(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`)); + } + for (const blocker of preview.blockers) emitBare(chalk.yellow(`blocker: ${blocker}`)); + emitBare(`Can restore: ${preview.can_restore ? "yes" : "no"}`); +} + +type DiffLine = { kind: "context" | "addition" | "deletion"; text: string }; + +function buildLineDiff(beforeSource: string, afterSource: string): DiffLine[] { + const beforeLines = yamlLines(beforeSource); + const afterLines = yamlLines(afterSource); + const maximumDistance = beforeLines.length + afterLines.length; + const frontier = new Map([[1, 0]]); + const traces: Array> = []; + + for (let editDistance = 0; editDistance <= maximumDistance; editDistance += 1) { + traces.push(new Map(frontier)); + for (let diagonal = -editDistance; diagonal <= editDistance; diagonal += 2) { + const deletionStart = frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY; + const additionStart = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY; + const startsWithAddition = + diagonal === -editDistance || (diagonal !== editDistance && deletionStart < additionStart); + let beforeIndex = startsWithAddition ? (frontier.get(diagonal + 1) ?? 0) : deletionStart + 1; + let afterIndex = beforeIndex - diagonal; + while ( + beforeIndex < beforeLines.length && + afterIndex < afterLines.length && + beforeLines[beforeIndex] === afterLines[afterIndex] + ) { + beforeIndex += 1; + afterIndex += 1; + } + frontier.set(diagonal, beforeIndex); + if (beforeIndex >= beforeLines.length && afterIndex >= afterLines.length) { + return backtrackDiff(beforeLines, afterLines, traces, editDistance); + } + } + } + return []; +} + +function backtrackDiff( + beforeLines: string[], + afterLines: string[], + traces: Array>, + finalDistance: number, +): DiffLine[] { + let beforeIndex = beforeLines.length; + let afterIndex = afterLines.length; + const reversedLines: DiffLine[] = []; + for (let editDistance = finalDistance; editDistance >= 0; editDistance -= 1) { + const frontier = traces[editDistance]!; + const diagonal = beforeIndex - afterIndex; + const deletionStart = frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY; + const additionStart = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY; + const cameFromAddition = + diagonal === -editDistance || (diagonal !== editDistance && deletionStart < additionStart); + const previousDiagonal = cameFromAddition ? diagonal + 1 : diagonal - 1; + const previousBeforeIndex = frontier.get(previousDiagonal) ?? 0; + const previousAfterIndex = previousBeforeIndex - previousDiagonal; + while (beforeIndex > previousBeforeIndex && afterIndex > previousAfterIndex) { + reversedLines.push({ kind: "context", text: beforeLines[beforeIndex - 1]! }); + beforeIndex -= 1; + afterIndex -= 1; + } + if (editDistance === 0) break; + if (beforeIndex === previousBeforeIndex) { + reversedLines.push({ kind: "addition", text: afterLines[afterIndex - 1]! }); + afterIndex -= 1; + } else { + reversedLines.push({ kind: "deletion", text: beforeLines[beforeIndex - 1]! }); + beforeIndex -= 1; + } + } + return reversedLines.reverse(); +} + +function yamlLines(source: string): string[] { + const lines = source.split("\n"); + if (lines[lines.length - 1] === "") lines.pop(); + return lines; +} diff --git a/packages/commands/src/commands/managed-agent/workbench.ts b/packages/commands/src/commands/managed-agent/workbench.ts new file mode 100644 index 000000000..059925cd6 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/workbench.ts @@ -0,0 +1,126 @@ +import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; +import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; +import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts"; + +const WORKBENCH_FLAGS = { + file: { + type: "string", + valueHint: "", + description: { + "en-US": "Config file path (default: agents.yaml)", + "zh-CN": "配置文件路径(默认:agents.yaml)", + }, + }, + port: { + type: "number", + valueHint: "", + description: { + "en-US": "Local port (default: 4848)", + "zh-CN": "本地端口(默认:4848)", + }, + }, + noOpen: { + type: "switch", + description: { + "en-US": "Do not open a browser automatically", + "zh-CN": "不自动打开浏览器", + }, + }, +} satisfies FlagsDef; + +const PLAYGROUND_FLAGS = { + ...WORKBENCH_FLAGS, + agent: { + type: "string", + valueHint: "", + description: { + "en-US": "Agent to preview (required when the project declares multiple Agents)", + "zh-CN": "要预览的 Agent(项目包含多个 Agent 时需要指定)", + }, + }, +} satisfies FlagsDef; + +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.", + "zh-CN": + "Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;不会推送 Git Commit 或切换分支。", + }, +]; + +export const managedAgentWorkbench = defineCommand({ + description: { + "en-US": "Launch the agents.yaml project Workbench", + "zh-CN": "启动 agents.yaml 项目 Workbench", + }, + auth: "apiKey", + usageArgs: "[--file ] [--port ] [--no-open]", + flags: WORKBENCH_FLAGS, + exampleArgs: ["", "--file agents.yaml --no-open", "--port 4949"], + notes: WORKBENCH_NOTES, + async run(ctx) { + const file = ctx.flags.file ?? "agents.yaml"; + const port = ctx.flags.port ?? 4848; + if (ctx.settings.dryRun) { + emitResult( + { + would_launch: "workbench", + config_file: file, + port, + open_browser: !ctx.flags.noOpen, + }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + await launchManagedAgentPlayground({ + file, + port, + open: !ctx.flags.noOpen, + surface: "workbench", + client: ctx.client, + settings: ctx.settings, + }); + }, +}); + +export const managedAgentPlayground = defineCommand({ + description: { + "en-US": "Launch a Session Preview for an agents.yaml Agent", + "zh-CN": "为 agents.yaml 中的 Agent 启动会话预览", + }, + auth: "apiKey", + usageArgs: "[--file ] [--agent ] [--port ] [--no-open]", + flags: PLAYGROUND_FLAGS, + exampleArgs: ["", "--agent assistant", "--file agents.yaml --no-open"], + notes: WORKBENCH_NOTES, + async run(ctx) { + const file = ctx.flags.file ?? "agents.yaml"; + const port = ctx.flags.port ?? 4848; + if (ctx.settings.dryRun) { + emitResult( + { + would_launch: "playground", + config_file: file, + agent: ctx.flags.agent, + port, + open_browser: !ctx.flags.noOpen, + }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + await launchManagedAgentPlayground({ + file, + agent: ctx.flags.agent, + port, + open: !ctx.flags.noOpen, + surface: "preview", + client: ctx.client, + settings: ctx.settings, + }); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 79e311ebf..abf358782 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -136,6 +136,18 @@ export { default as managedAgentValidate } from "./commands/managed-agent/valida export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts"; export { default as managedAgentApply } from "./commands/managed-agent/apply.ts"; export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts"; +export { + managedAgentPlayground, + managedAgentWorkbench, +} from "./commands/managed-agent/workbench.ts"; +export { + managedAgentVersionDisable, + managedAgentVersionEnable, + managedAgentVersionList, + managedAgentVersionPreview, + managedAgentVersionRestore, + managedAgentVersionStatus, +} from "./commands/managed-agent/version.ts"; export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts"; export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts"; export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts"; diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index fc6b05881..403450922 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -132,6 +132,27 @@ describe("e2e: managed-agent", () => { expect(stderr).toMatch(/--file|--provider|--yes/i); }); + test("managed-agent version 暴露共享版本管理子命令", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "version", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/enable|disable|status|list|preview|restore/i); + }); + + test("managed-agent version preview 缺少 --commit 时退出为用法错误 (2)", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "version", + "preview", + "--quiet", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--commit|Missing required/i); + }); + test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", @@ -218,6 +239,47 @@ 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", + "workbench", + "--dry-run", + "--no-open", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + would_launch?: string; + open_browser?: boolean; + }>(stdout); + expect(data.would_launch).toBe("workbench"); + expect(data.open_browser).toBe(false); + }); + test("apply --dry-run 仅输出计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index fe249e8d7..df8bf3c1c 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -186,6 +186,14 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "managed-agent plan": "managedAgentPlan", "managed-agent apply": "managedAgentApply", "managed-agent destroy": "managedAgentDestroy", + "managed-agent workbench": "managedAgentWorkbench", + "managed-agent playground": "managedAgentPlayground", + "managed-agent version enable": "managedAgentVersionEnable", + "managed-agent version disable": "managedAgentVersionDisable", + "managed-agent version status": "managedAgentVersionStatus", + "managed-agent version list": "managedAgentVersionList", + "managed-agent version preview": "managedAgentVersionPreview", + "managed-agent version restore": "managedAgentVersionRestore", "managed-agent state list": "managedAgentStateList", "managed-agent state rm": "managedAgentStateRm", "managed-agent state import": "managedAgentStateImport", diff --git a/packages/commands/tests/managed-agent-git-project.test.ts b/packages/commands/tests/managed-agent-git-project.test.ts new file mode 100644 index 000000000..4fe6305ec --- /dev/null +++ b/packages/commands/tests/managed-agent-git-project.test.ts @@ -0,0 +1,67 @@ +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 new file mode 100644 index 000000000..dfaa25aaa --- /dev/null +++ b/packages/commands/tests/managed-agent-local-git.test.ts @@ -0,0 +1,202 @@ +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/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 7277707b3..aead755e4 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -6,7 +6,8 @@ metadata: bins: ["bl"] description: >- 阿里云百炼托管 Agent 声明式基础设施入口:用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、 - 创建/更新/销毁百炼托管 Agent 或 Deployment、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 + 创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地 Git 版本、 + 生成 CI 仓库、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 `bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、 apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。 反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、 @@ -20,11 +21,12 @@ description: >- ## Safety guardrail (the most important rule) -`apply` / `destroy` **mutate remote resources** and only execute when `--yes` is passed: +`apply` / `destroy` **mutate remote resources**. Interactive execution requires `--yes`; `apply --ci` is only for an already approved CI workflow: 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 @@ -36,6 +38,23 @@ description: >- 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` +## Workbench, local versions, and CI + +| 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` | + +- 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. +- 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 Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路; diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index a401eac0b..a53538824 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,31 +9,39 @@ Use this index for the skill-scoped quick index and global flags. ## Quick index -| Command | Authentication | Description | Detail | -| --------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ | -| `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 a new 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 session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session events` | API Key | List event history for a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | -| `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) | +| Command | Authentication | Description | Detail | +| ---------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ | +| `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 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) | +| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session events` | API Key | List event history for a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | +| `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 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 workbench` | API Key | Launch the agents.yaml project Workbench | [managed-agent.md](managed-agent.md) | ## By group -| Group | Commands | Reference | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) | +| Group | Commands | Reference | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `playground`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `version disable`, `version enable`, `version list`, `version preview`, `version restore`, `version status`, `workbench` | [managed-agent.md](managed-agent.md) | ## Global flags diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index e3975b7e5..28a52ee01 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,36 +7,44 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| --------------------------------- | -------------- | ------------------------------------------------------------- | -| `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 a new agents.yaml template | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | -| `bl managed-agent session create` | API Key | Create a new session for an agent | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session events` | API Key | List event history for a session | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `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) | +| Command | Authentication | Description | +| ---------------------------------- | -------------- | ------------------------------------------------------------- | +| `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 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 | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session events` | API Key | List event history for a session | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `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 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 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] [--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 \| --ci] [--no-refresh] [--refresh-only] [--concurrency ]` | #### Flags @@ -45,7 +53,9 @@ 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) | | `--api-key ` | string | no | API key | | `--base-url ` | string | no | API base URL | @@ -66,6 +76,10 @@ bl managed-agent apply --yes bl managed-agent apply --provider bailian --yes ``` +```bash +bl managed-agent apply --ci +``` + ### `bl managed-agent destroy` | Field | Value | @@ -103,12 +117,12 @@ bl managed-agent destroy --yes --cascade ### `bl managed-agent init` -| Field | Value | -| ------------------ | ------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent init` | -| **Description** | Create a new agents.yaml template | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent init [--provider ] [--agent-name ] [--file ] [--force]` | +| 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]` | #### Flags @@ -117,6 +131,7 @@ 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 @@ -130,7 +145,11 @@ bl managed-agent init --provider bailian --agent-name assistant ``` ```bash -bl managed-agent init --provider all +bl managed-agent init --git ./my-agents +``` + +```bash +bl managed-agent init --git . ``` ### `bl managed-agent plan` @@ -174,6 +193,47 @@ bl managed-agent plan --provider bailian bl managed-agent plan --no-refresh ``` +### `bl managed-agent playground` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------- | +| **Name** | `managed-agent playground` | +| **Description** | Launch a Session Preview for an agents.yaml Agent | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent playground [--file ] [--agent ] [--port ] [--no-open]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | --------------------------------------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--port ` | number | no | Local port (default: 4848) | +| `--no-open` | switch | no | Do not open a browser automatically | +| `--agent ` | string | no | Agent to preview (required when the project declares multiple Agents) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- 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. + +#### Examples + +```bash +bl managed-agent playground +``` + +```bash +bl managed-agent playground --agent assistant +``` + +```bash +bl managed-agent playground --file agents.yaml --no-open +``` + ### `bl managed-agent session create` | Field | Value | @@ -618,3 +678,198 @@ bl managed-agent validate ```bash 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 ]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------- | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | + +#### Examples + +```bash +bl managed-agent version disable +``` + +```bash +bl managed-agent version disable --file agents.yaml +``` + +### `bl managed-agent version enable` + +| Field | Value | +| ------------------ | ------------------------------------------------- | +| **Name** | `managed-agent version enable` | +| **Description** | Enable Apply-time Git versioning for agents.yaml | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version enable [--file ]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------- | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | + +#### Examples + +```bash +bl managed-agent version enable +``` + +```bash +bl managed-agent version enable --file agents.yaml +``` + +### `bl managed-agent version list` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------- | +| **Name** | `managed-agent version list` | +| **Description** | List current-branch commits that changed agents.yaml | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent version list [--file ] [--limit ] [--cursor ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | -------------------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--limit ` | number | no | Maximum versions to return (default: 50, max: 100) | +| `--cursor ` | string | no | Pagination cursor returned by the previous page | + +#### Examples + +```bash +bl managed-agent version list +``` + +```bash +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 ]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--commit ` | string | yes | Full commit SHA from the current branch | + +#### Examples + +```bash +bl managed-agent version preview --commit +``` + +```bash +bl managed-agent version preview --commit --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]` | + +#### 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 | + +#### Examples + +```bash +bl managed-agent version restore --commit +``` + +```bash +bl managed-agent version restore --commit --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 ]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------- | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | + +#### Examples + +```bash +bl managed-agent version status +``` + +```bash +bl managed-agent version status --file agents.yaml --output json +``` + +### `bl managed-agent workbench` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------- | +| **Name** | `managed-agent workbench` | +| **Description** | Launch the agents.yaml project Workbench | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent workbench [--file ] [--port ] [--no-open]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | --------------------------------------- | +| `--file ` | string | no | Config file path (default: agents.yaml) | +| `--port ` | number | no | Local port (default: 4848) | +| `--no-open` | switch | no | Do not open a browser automatically | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- 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. + +#### Examples + +```bash +bl managed-agent workbench +``` + +```bash +bl managed-agent workbench --file agents.yaml --no-open +``` + +```bash +bl managed-agent workbench --port 4949 +``` From 94695566711c2ddeba1afd0c9d0070b29e4115cb Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Wed, 26 Aug 2026 18:00:05 +0800 Subject: [PATCH 2/3] refactor(managed-agent)!: replace Git versioning with local snapshots Replace @openagentpack/local-git with @openagentpack/project-versions so the CLI and Workbench share a Git-independent, lock-protected YAML snapshot store. - create versions only after a successful Apply - migrate version commands to local version IDs - remove Git project scaffolding and CI-specific Apply policy - update tests and managed-agent skill references BREAKING CHANGE: remove `managed-agent init --git` and `managed-agent apply --ci`; version preview and restore now use `--version-id` instead of `--commit`. --- packages/commands/package.json | 2 +- .../managed-agent/_engine/git-project.ts | 505 ------------------ .../src/commands/managed-agent/apply.ts | 123 ++--- .../src/commands/managed-agent/init.ts | 63 +-- .../src/commands/managed-agent/version.ts | 121 +++-- .../src/commands/managed-agent/workbench.ts | 4 +- .../tests/e2e/managed-agent.e2e.test.ts | 38 +- .../tests/managed-agent-git-project.test.ts | 67 --- .../tests/managed-agent-local-git.test.ts | 202 ------- .../managed-agent-local-versions.test.ts | 139 +++++ skills/bailian-managed-agent/SKILL.md | 32 +- .../bailian-managed-agent/reference/index.md | 10 +- .../reference/managed-agent.md | 130 ++--- 13 files changed, 345 insertions(+), 1091 deletions(-) delete mode 100644 packages/commands/src/commands/managed-agent/_engine/git-project.ts delete mode 100644 packages/commands/tests/managed-agent-git-project.test.ts delete mode 100644 packages/commands/tests/managed-agent-local-git.test.ts create mode 100644 packages/commands/tests/managed-agent-local-versions.test.ts diff --git a/packages/commands/package.json b/packages/commands/package.json index d6ab50a93..6e2f6ae9f 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 eb96b5f71..000000000 --- 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 320ef8a3c..884a2b8b7 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 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; + 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 (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, - ); + 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 f2fc8eeb6..980598416 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 e9caefc1a..5ca51e200 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 059925cd6..08f806c2e 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 403450922..fffd535d7 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 4fe6305ec..000000000 --- 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 dfaa25aaa..000000000 --- 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 000000000..dae7d8f26 --- /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 aead755e4..73175659a 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 a53538824..a79519ad7 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 28a52ee01..5bf40048f 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 From e53daf05eb3d80d4a0de03a6a4a111a1155a19dc Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Mon, 31 Aug 2026 15:55:10 +0800 Subject: [PATCH 3/3] feat(managed-agent): add directory project workflow --- packages/cli/src/commands.ts | 36 +- packages/commands/package.json | 2 +- .../_engine/playground-launcher.ts | 21 +- .../src/commands/managed-agent/apply.ts | 83 +-- .../src/commands/managed-agent/project.ts | 443 ++++++++++++++ .../src/commands/managed-agent/version.ts | 362 ----------- .../src/commands/managed-agent/workbench.ts | 48 +- packages/commands/src/index.ts | 24 +- .../tests/e2e/managed-agent.e2e.test.ts | 94 ++- packages/commands/tests/e2e/topic-routes.ts | 18 +- skills/bailian-managed-agent/SKILL.md | 33 +- .../bailian-managed-agent/reference/index.md | 64 +- .../reference/managed-agent.md | 567 +++++++++++------- 13 files changed, 1017 insertions(+), 778 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/project.ts delete mode 100644 packages/commands/src/commands/managed-agent/version.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index e52ce30c4..764015ab2 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -139,14 +139,18 @@ import { managedAgentPlan, managedAgentApply, managedAgentDestroy, - managedAgentWorkbench, managedAgentPlayground, - managedAgentVersionEnable, - managedAgentVersionDisable, - managedAgentVersionStatus, - managedAgentVersionList, - managedAgentVersionPreview, - managedAgentVersionRestore, + managedAgentProjectInit, + managedAgentProjectValidate, + managedAgentProjectBuild, + managedAgentProjectPublish, + managedAgentProjectWorkbench, + managedAgentProjectVersionEnable, + managedAgentProjectVersionDisable, + managedAgentProjectVersionStatus, + managedAgentProjectVersionList, + managedAgentProjectVersionPreview, + managedAgentProjectVersionRestore, managedAgentStateList, managedAgentStateShow, managedAgentStateRm, @@ -308,14 +312,18 @@ export const commands: Record = { "managed-agent plan": managedAgentPlan, "managed-agent apply": managedAgentApply, "managed-agent destroy": managedAgentDestroy, - "managed-agent workbench": managedAgentWorkbench, "managed-agent playground": managedAgentPlayground, - "managed-agent version enable": managedAgentVersionEnable, - "managed-agent version disable": managedAgentVersionDisable, - "managed-agent version status": managedAgentVersionStatus, - "managed-agent version list": managedAgentVersionList, - "managed-agent version preview": managedAgentVersionPreview, - "managed-agent version restore": managedAgentVersionRestore, + "managed-agent project init": managedAgentProjectInit, + "managed-agent project validate": managedAgentProjectValidate, + "managed-agent project build": managedAgentProjectBuild, + "managed-agent project publish": managedAgentProjectPublish, + "managed-agent project workbench": managedAgentProjectWorkbench, + "managed-agent project version enable": managedAgentProjectVersionEnable, + "managed-agent project version disable": managedAgentProjectVersionDisable, + "managed-agent project version status": managedAgentProjectVersionStatus, + "managed-agent project version list": managedAgentProjectVersionList, + "managed-agent project version preview": managedAgentProjectVersionPreview, + "managed-agent project version restore": managedAgentProjectVersionRestore, "managed-agent state list": managedAgentStateList, "managed-agent state show": managedAgentStateShow, "managed-agent state rm": managedAgentStateRm, diff --git a/packages/commands/package.json b/packages/commands/package.json index 6e2f6ae9f..f784ec233 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -40,7 +40,7 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/project-versions": "0.4.0", + "@openagentpack/project-workspace": "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/playground-launcher.ts b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts index 37198a341..69c83a29e 100644 --- a/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts +++ b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts @@ -13,7 +13,8 @@ const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i; export interface PlaygroundLaunchOptions { port?: number; open: boolean; - file: string; + file?: string; + project?: string; agent?: string; surface: "preview" | "workbench"; client: Client; @@ -51,8 +52,10 @@ export async function launchManagedAgentPlayground( if (!Number.isInteger(port) || port <= 0 || port > 65_535) { throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE); } - const configPath = resolve(options.file); - const projectId = createHash("sha256").update(configPath).digest("hex").slice(0, 16); + const sourcePath = resolve( + options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"), + ); + const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16); const launcher = resolveLauncher(); const existing = await probeExistingPlayground(port); if (existing) { @@ -74,7 +77,7 @@ export async function launchManagedAgentPlayground( } } - const environment = buildPlaygroundEnvironment(options, port, configPath); + const environment = buildPlaygroundEnvironment(options, port, sourcePath); if (launcher.fetched) { emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`); } @@ -214,15 +217,21 @@ function findLocalPlaygroundBin(startDirectory: string): string | undefined { function buildPlaygroundEnvironment( options: PlaygroundLaunchOptions, port: number, - configPath: string, + sourcePath: string, ): NodeJS.ProcessEnv { const credential = options.client.exportApiCredential(); const environment: NodeJS.ProcessEnv = { ...process.env, PORT: String(port), - AGENTS_CONFIG_PATH: configPath, AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"), }; + if (options.surface === "workbench") { + delete environment.AGENTS_CONFIG_PATH; + environment.AGENTS_PROJECT_ROOT = sourcePath; + } else { + delete environment.AGENTS_PROJECT_ROOT; + environment.AGENTS_CONFIG_PATH = sourcePath; + } if (credential) environment.DASHSCOPE_API_KEY = credential.token; const baseUrl = options.client.baseUrl.replace(/\/+$/, ""); environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio") diff --git a/packages/commands/src/commands/managed-agent/apply.ts b/packages/commands/src/commands/managed-agent/apply.ts index 884a2b8b7..3f81d026f 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -16,13 +16,6 @@ import { import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; import { renderAgentFeedback } from "./_engine/feedback.ts"; -import { - commitPreparedProjectVersion, - type PreparedProjectVersion, - prepareProjectVersion, - readProjectVersionSource, - releasePreparedProjectVersion, -} from "@openagentpack/project-versions"; const APPLY_FLAGS = { file: { @@ -105,9 +98,7 @@ export default defineCommand({ return; } - const versionSource = await readProjectVersionSource(file); - - const { planned, runtime } = await withAgentErrors(() => + const planned = await withAgentErrors(() => withStdoutProtected(async () => { const runtime = await buildAgentRuntime(ctx, file); assertProviderConfigured(runtime, flags.provider); @@ -117,7 +108,7 @@ export default defineCommand({ quiet: true, onFeedback: renderAgentFeedback, }); - return { planned, runtime }; + return planned; }), ); @@ -137,13 +128,6 @@ export default defineCommand({ const actionable = plan.actions.filter((action) => action.action !== "no-op"); if (actionable.length === 0) { - if (!flags.refreshOnly) { - const preparedVersion = await prepareProjectVersion( - runtime.configPath, - versionSource.source, - ); - await commitSuccessfulApplyVersion(preparedVersion, format); - } if (format === "json") emitResult({ succeeded: 0, failed: 0, skipped: 0, results: [] }, format); else emitBare("No changes. Infrastructure is up-to-date."); @@ -184,50 +168,31 @@ export default defineCommand({ ); } - 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 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; + 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 (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); + if (failed > 0 || skipped > 0) { + throw new BailianError( + failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.", + ExitCode.GENERAL, + ); } }, }); - -async function commitSuccessfulApplyVersion( - prepared: PreparedProjectVersion | null, - format: "text" | "json", -): Promise { - if (!prepared) return; - const version = await commitPreparedProjectVersion(prepared); - if (version && format !== "json") { - emitBare(`Created local version ${version.short_version} (${version.message}).`); - } -} diff --git a/packages/commands/src/commands/managed-agent/project.ts b/packages/commands/src/commands/managed-agent/project.ts new file mode 100644 index 000000000..50e1070cb --- /dev/null +++ b/packages/commands/src/commands/managed-agent/project.ts @@ -0,0 +1,443 @@ +import { + BailianError, + defineCommand, + detectOutputFormat, + ExitCode, + type FlagsDef, +} from "bailian-cli-core"; +import { confirmDangerousAction, emitBare, emitResult } from "bailian-cli-runtime"; +import { + commitProjectBuild, + createDirectoryWorkspaceVersionService, + executeProjectPublish, + initializeDirectoryProject, + planProjectPublish, + previewProjectBuild, + validateDirectoryProject, +} from "@openagentpack/project-workspace"; +import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts"; +import { withStdoutProtected } from "./_engine/console-capture.ts"; +import { withAgentErrors } from "./_engine/errors.ts"; +import { renderAgentFeedback } from "./_engine/feedback.ts"; +import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts"; +import { installSdkTransport } from "./_engine/transport.ts"; +import { formatResourceLabel } from "./_engine/address-utils.ts"; + +const PROJECT_FLAG = { + project: { + type: "string", + valueHint: "", + description: { + "en-US": "Directory project root (default: current directory)", + "zh-CN": "目录项目根路径(默认:当前目录)", + }, + }, +} satisfies FlagsDef; + +const JSON_FORMAT = "json" as const; + +export const managedAgentProjectInit = defineCommand({ + description: { + "en-US": "Create a directory project or convert the local agents.yaml", + "zh-CN": "创建目录项目,或转换当前 agents.yaml", + }, + auth: "none", + usageArgs: "[--project ] [--provider bailian]", + flags: { + ...PROJECT_FLAG, + provider: { + type: "string", + valueHint: "", + description: { "en-US": "Provider for a new project", "zh-CN": "新项目使用的 Provider" }, + }, + }, + exampleArgs: ["", "--project ./my-agent", "--provider bailian"], + async run(ctx) { + if (ctx.settings.dryRun) { + emitResult( + { would_initialize_project: ctx.flags.project ?? "." }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + const result = await initializeDirectoryProject({ + projectRoot: ctx.flags.project ?? ".", + provider: ctx.flags.provider ?? "bailian", + }); + emitResult(result, detectOutputFormat(ctx.settings.output)); + }, +}); + +export const managedAgentProjectValidate = defineCommand({ + description: { "en-US": "Validate a directory Agent project", "zh-CN": "校验目录式 Agent 项目" }, + auth: "none", + usageArgs: "[--project ]", + flags: PROJECT_FLAG, + exampleArgs: ["", "--project ./my-agent"], + async run(ctx) { + const result = await validateDirectoryProject(ctx.flags.project ?? "."); + const format = detectOutputFormat(ctx.settings.output); + if (format === JSON_FORMAT) emitResult(safeInspection(result), format); + else { + for (const diagnostic of [...result.diagnostics, ...result.warnings]) { + emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`); + } + if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) { + emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`); + } + } + if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) { + throw new BailianError("Directory project validation failed.", ExitCode.GENERAL); + } + }, +}); + +export const managedAgentProjectBuild = defineCommand({ + description: { + "en-US": "Organize directory source and generate the immutable Publish Build", + "zh-CN": "整理目录源文件并生成不可变的发布 Build", + }, + auth: "none", + usageArgs: "[--project ] [--yes]", + flags: { + ...PROJECT_FLAG, + yes: { + type: "switch", + description: { "en-US": "Write the previewed Build", "zh-CN": "写入已预览的 Build" }, + }, + }, + exampleArgs: ["--dry-run", "--yes", "--project ./my-agent --yes"], + async run(ctx) { + const root = ctx.flags.project ?? "."; + const preview = await previewProjectBuild(root); + const format = detectOutputFormat(ctx.settings.output); + if (ctx.settings.dryRun) { + emitResult(safeInspection(preview), format); + return; + } + if (!preview.can_build) + throw new BailianError("Directory project is invalid and cannot be built.", ExitCode.GENERAL); + if (!ctx.flags.yes) { + throw new BailianError( + "Build requires confirmation before it organizes shared skills and writes generated YAML.", + ExitCode.USAGE, + "Preview with --dry-run, then rerun with --yes.", + ); + } + const built = await commitProjectBuild({ + projectRoot: root, + baseRevision: preview.project_revision, + }); + emitResult( + { manifest: built.manifest, organization_moves: preview.organization_moves }, + format, + ); + }, +}); + +export const managedAgentProjectPublish = defineCommand({ + description: { + "en-US": "Publish the current directory-project Build and record a version", + "zh-CN": "发布当前目录项目 Build 并记录版本", + }, + auth: "apiKey", + usageArgs: + "[--project ] [--provider ] [--yes] [--no-refresh] [--concurrency ]", + flags: { + ...PROJECT_FLAG, + provider: { + type: "string", + valueHint: "", + description: { "en-US": "Target provider", "zh-CN": "目标 Provider" }, + }, + yes: { + type: "switch", + description: { "en-US": "Confirm remote Publish", "zh-CN": "确认执行远端发布" }, + }, + noRefresh: { + type: "switch", + description: { + "en-US": "Skip remote refresh before planning", + "zh-CN": "规划前跳过远端刷新", + }, + }, + concurrency: { + type: "number", + valueHint: "", + description: { + "en-US": "Maximum parallel resource operations", + "zh-CN": "最大并行资源操作数", + }, + }, + }, + exampleArgs: ["--yes", "--project ./my-agent --yes", "--provider bailian --yes"], + notes: CREDENTIALS_NOTE, + async run(ctx) { + installSdkTransport(ctx); + const root = ctx.flags.project ?? "."; + const resolveBuild = (buildPath: string) => resolveAgentProjectConfig(ctx, buildPath); + const planned = await withAgentErrors(() => + withStdoutProtected(() => + planProjectPublish(root, { + provider: ctx.flags.provider, + refresh: !ctx.flags.noRefresh, + quiet: true, + onFeedback: renderAgentFeedback, + resolveBuild, + }), + ), + ); + const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op"); + if (ctx.settings.dryRun) { + emitResult( + { + project_revision: planned.project_revision, + build_manifest: planned.build_manifest, + plan: planned.planned.plan, + }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + for (const action of actions) { + const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; + emitBare(`${marker} ${formatResourceLabel(action.address)}`); + } + if (!ctx.flags.yes) { + throw new BailianError( + `Refusing to Publish ${actions.length} remote change(s) without confirmation.`, + ExitCode.USAGE, + "Review with project publish --dry-run, then rerun with --yes.", + ); + } + const result = await withAgentErrors(() => + withStdoutProtected(() => + executeProjectPublish({ + projectRoot: planned.project_root, + expectedProjectRevision: planned.project_revision, + expectedYamlHash: planned.build_manifest.yaml_hash, + provider: ctx.flags.provider, + refresh: !ctx.flags.noRefresh, + concurrency: ctx.flags.concurrency, + policy: "force", + onFeedback: renderAgentFeedback, + resolveBuild, + }), + ), + ); + emitResult(result, detectOutputFormat(ctx.settings.output)); + }, +}); + +export const managedAgentProjectWorkbench = defineCommand({ + description: { + "en-US": "Launch the directory project Workbench", + "zh-CN": "启动目录项目 Workbench", + }, + auth: "apiKey", + usageArgs: "[--project ] [--port ] [--no-open]", + flags: { + ...PROJECT_FLAG, + port: { + type: "number", + valueHint: "", + description: { "en-US": "Local port (default: 4848)", "zh-CN": "本地端口(默认:4848)" }, + }, + noOpen: { + type: "switch", + description: { "en-US": "Do not open a browser", "zh-CN": "不自动打开浏览器" }, + }, + }, + exampleArgs: ["", "--project ./my-agent --no-open"], + notes: CREDENTIALS_NOTE, + async run(ctx) { + const root = ctx.flags.project ?? "."; + if (ctx.settings.dryRun) { + emitResult( + { would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + await launchManagedAgentPlayground({ + project: root, + port: ctx.flags.port ?? 4848, + open: !ctx.flags.noOpen, + surface: "workbench", + client: ctx.client, + settings: ctx.settings, + }); + }, +}); + +export const managedAgentProjectVersionEnable = versionToggleCommand(true); +export const managedAgentProjectVersionDisable = versionToggleCommand(false); + +function versionToggleCommand(enabled: boolean) { + return defineCommand({ + description: enabled + ? { "en-US": "Enable directory project versions", "zh-CN": "启用目录项目版本管理" } + : { "en-US": "Disable directory project versions", "zh-CN": "关闭目录项目版本管理" }, + auth: "none", + usageArgs: "[--project ]", + flags: PROJECT_FLAG, + exampleArgs: ["", "--project ./my-agent"], + async run(ctx) { + const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? "."); + if (ctx.settings.dryRun) { + emitResult( + { would_set_enabled: enabled, status: await service.status() }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + const result = enabled + ? await service.enable("Enable project versions") + : await service.disable(); + emitResult(result, detectOutputFormat(ctx.settings.output)); + }, + }); +} + +export const managedAgentProjectVersionStatus = defineCommand({ + description: { + "en-US": "Show directory project version status", + "zh-CN": "显示目录项目版本状态", + }, + auth: "none", + usageArgs: "[--project ]", + flags: PROJECT_FLAG, + exampleArgs: ["", "--project ./my-agent --output json"], + async run(ctx) { + emitResult( + await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(), + detectOutputFormat(ctx.settings.output), + ); + }, +}); + +export const managedAgentProjectVersionList = defineCommand({ + description: { "en-US": "List directory project versions", "zh-CN": "列出目录项目版本" }, + auth: "none", + usageArgs: "[--project ] [--limit ] [--cursor ]", + flags: { + ...PROJECT_FLAG, + limit: { + type: "number", + valueHint: "", + description: { "en-US": "Maximum versions to return", "zh-CN": "最多返回的版本数" }, + }, + cursor: { + type: "string", + valueHint: "", + description: { "en-US": "Pagination cursor", "zh-CN": "分页游标" }, + }, + }, + exampleArgs: ["", "--limit 20 --output json"], + async run(ctx) { + emitResult( + await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({ + limit: ctx.flags.limit, + cursor: ctx.flags.cursor, + }), + detectOutputFormat(ctx.settings.output), + ); + }, +}); + +const VERSION_ID_FLAG = { + versionId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Full project version ID", "zh-CN": "完整的项目版本 ID" }, + }, +} satisfies FlagsDef; + +export const managedAgentProjectVersionPreview = defineCommand({ + description: { "en-US": "Preview a directory project version", "zh-CN": "预览目录项目历史版本" }, + auth: "none", + usageArgs: "--version-id [--project ]", + flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG }, + exampleArgs: ["--version-id "], + async run(ctx) { + emitResult( + await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion( + ctx.flags.versionId, + ), + detectOutputFormat(ctx.settings.output), + ); + }, +}); + +export const managedAgentProjectVersionRestore = defineCommand({ + description: { + "en-US": "Restore a version to the project working directory", + "zh-CN": "将历史版本恢复到项目工作目录", + }, + auth: "none", + usageArgs: "--version-id [--project ] [--yes]", + flags: { + ...PROJECT_FLAG, + ...VERSION_ID_FLAG, + yes: { + type: "switch", + description: { + "en-US": "Restore without interactive confirmation", + "zh-CN": "无需交互确认直接恢复", + }, + }, + }, + exampleArgs: ["--version-id ", "--version-id --yes"], + async run(ctx) { + const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? "."); + const preview = await service.previewVersion(ctx.flags.versionId); + if (!preview.can_restore) + throw new BailianError( + preview.blockers[0] ?? preview.diagnostics[0]?.message ?? "Version cannot be restored.", + ExitCode.GENERAL, + ); + if (ctx.settings.dryRun) { + emitResult( + { would_restore: ctx.flags.versionId, preview }, + detectOutputFormat(ctx.settings.output), + ); + return; + } + await confirmDangerousAction( + "Restore the full directory source? Version history and remote State will not move.", + ctx.flags.yes, + ); + const restored = await service.restoreVersion(ctx.flags.versionId, { + headVersion: preview.base_head_version, + projectRevision: preview.base_project_revision, + }); + emitResult(restored, detectOutputFormat(ctx.settings.output)); + }, +}); + +function safeInspection(result: { + project_root: string; + project_revision: string; + source_manifest_hash: string; + yaml_hash: string; + diagnostics: unknown[]; + warnings: unknown[]; + organization_moves: unknown[]; + canonical_yaml: string; + before_yaml?: string; + can_build?: boolean; +}) { + return { + project_root: result.project_root, + project_revision: result.project_revision, + source_manifest_hash: result.source_manifest_hash, + yaml_hash: result.yaml_hash, + diagnostics: result.diagnostics, + warnings: result.warnings, + organization_moves: result.organization_moves, + before_yaml: "before_yaml" in result ? result.before_yaml : undefined, + after_yaml: result.canonical_yaml, + can_build: "can_build" in result ? result.can_build : undefined, + }; +} diff --git a/packages/commands/src/commands/managed-agent/version.ts b/packages/commands/src/commands/managed-agent/version.ts deleted file mode 100644 index 5ca51e200..000000000 --- a/packages/commands/src/commands/managed-agent/version.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { confirmDangerousAction, emitBare, emitResult } from "bailian-cli-runtime"; -import chalk from "chalk"; -import { - disableProjectVersioning, - enableProjectVersioning, - getProjectVersionStatus, - type ProjectVersion, - type ProjectVersionPreview, - type ProjectVersionStatus, - listProjectVersions, - previewProjectVersion, - restoreProjectVersion, -} from "@openagentpack/project-versions"; - -const FILE_FLAG = { - file: { - type: "string", - valueHint: "", - description: { - "en-US": "Config file path (default: agents.yaml)", - "zh-CN": "配置文件路径(默认:agents.yaml)", - }, - }, -} satisfies FlagsDef; - -const VERSION_FLAG = { - versionId: { - type: "string", - valueHint: "", - required: true, - description: { - "en-US": "Full local version ID", - "zh-CN": "完整的本地版本 ID", - }, - }, -} satisfies FlagsDef; - -export const managedAgentVersionEnable = defineCommand({ - description: { - "en-US": "Enable Apply-time local snapshots for agents.yaml", - "zh-CN": "为 agents.yaml 启用 Apply 后自动本地快照", - }, - auth: "none", - usageArgs: "[--file ]", - flags: FILE_FLAG, - exampleArgs: ["", "--file agents.yaml"], - async run(ctx) { - const file = ctx.flags.file ?? "agents.yaml"; - const format = detectOutputFormat(ctx.settings.output); - if (ctx.settings.dryRun) { - emitResult({ would_enable: file, versioning: await getProjectVersionStatus(file) }, format); - return; - } - 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_version} ${result.version.message}`, - ); - } else { - emitBare("Current agents.yaml is already versioned; no snapshot was created."); - } - emitBare("Automatic versioning is enabled for this agents.yaml."); - renderStatus(result.versioning); - }, -}); - -export const managedAgentVersionDisable = defineCommand({ - description: { - "en-US": "Disable Apply-time local snapshots without removing history", - "zh-CN": "关闭 Apply 后自动本地快照,但保留历史", - }, - auth: "none", - usageArgs: "[--file ]", - flags: FILE_FLAG, - exampleArgs: ["", "--file agents.yaml"], - async run(ctx) { - const file = ctx.flags.file ?? "agents.yaml"; - const format = detectOutputFormat(ctx.settings.output); - if (ctx.settings.dryRun) { - emitResult({ would_disable: file, versioning: await getProjectVersionStatus(file) }, format); - return; - } - const status = await disableProjectVersioning(file); - if (format === "json") { - emitResult(status, format); - return; - } - emitBare("Automatic versioning is disabled for this agents.yaml."); - renderStatus(status); - }, -}); - -export const managedAgentVersionStatus = defineCommand({ - description: { - "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 getProjectVersionStatus(ctx.flags.file ?? "agents.yaml"); - const format = detectOutputFormat(ctx.settings.output); - if (format === "json") emitResult(status, format); - else renderStatus(status); - }, -}); - -const LIST_FLAGS = { - ...FILE_FLAG, - limit: { - type: "number", - valueHint: "", - description: { - "en-US": "Maximum versions to return (default: 50, max: 100)", - "zh-CN": "最多返回的版本数(默认:50,最大:100)", - }, - }, - cursor: { - type: "string", - valueHint: "", - description: { - "en-US": "Pagination cursor returned by the previous page", - "zh-CN": "上一页返回的分页游标", - }, - }, -} satisfies FlagsDef; - -export const managedAgentVersionList = defineCommand({ - description: { - "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 listProjectVersions(ctx.flags.file ?? "agents.yaml", { - limit: ctx.flags.limit, - cursor: ctx.flags.cursor, - }); - const format = detectOutputFormat(ctx.settings.output); - if (format === "json") { - emitResult(page, format); - return; - } - if (page.versions.length === 0) { - emitBare("No local versions of agents.yaml exist."); - return; - } - for (const version of page.versions) emitBare(formatVersion(version)); - if (page.next_cursor) emitBare(chalk.dim(`Next cursor: ${page.next_cursor}`)); - }, -}); - -const PREVIEW_FLAGS = { - ...FILE_FLAG, - ...VERSION_FLAG, -} satisfies FlagsDef; - -export const managedAgentVersionPreview = defineCommand({ - description: { - "en-US": "Preview a historical agents.yaml version", - "zh-CN": "预览 agents.yaml 的历史版本", - }, - auth: "none", - usageArgs: "--version-id [--file ]", - flags: PREVIEW_FLAGS, - exampleArgs: ["--version-id ", "--version-id --output json"], - async run(ctx) { - 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); - }, -}); - -const RESTORE_FLAGS = { - ...PREVIEW_FLAGS, - yes: { - type: "switch", - description: { - "en-US": "Restore without an interactive confirmation", - "zh-CN": "无需交互确认直接恢复", - }, - }, -} satisfies FlagsDef; - -export const managedAgentVersionRestore = defineCommand({ - description: { - "en-US": "Restore a historical agents.yaml version to the working tree", - "zh-CN": "将 agents.yaml 历史版本恢复到工作区", - }, - auth: "none", - usageArgs: "--version-id [--file ] [--yes]", - flags: RESTORE_FLAGS, - exampleArgs: ["--version-id ", "--version-id --yes --output json"], - async run(ctx) { - const file = ctx.flags.file ?? "agents.yaml"; - const preview = await previewProjectVersion(file, ctx.flags.versionId); - const format = detectOutputFormat(ctx.settings.output); - if (format !== "json") renderPreview(preview); - if (!preview.can_restore) { - throw new BailianError( - preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ?? - preview.blockers[0] ?? - "This version cannot be restored.", - ExitCode.GENERAL, - ); - } - if (ctx.settings.dryRun) { - emitResult({ would_restore: ctx.flags.versionId, preview }, format); - return; - } - await confirmDangerousAction( - "Restore this version to the agents.yaml working tree? Version history and agents.state.json will not change.", - ctx.flags.yes, - ); - 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.versionId, preview: restored }, format); - } else { - emitBare( - `Restored ${ctx.flags.versionId.slice(0, 12)} to the working tree. Version history was not changed.`, - ); - } - }, -}); - -function renderStatus(status: ProjectVersionStatus): void { - emitBare(`Automatic versioning: ${status.enabled ? "enabled" : "disabled"}`); - 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.source_status}${status.source_versioned ? ", versioned" : ", unversioned"}`, - ); - const blockers = [...new Set([...status.write_blockers, ...status.restore_blockers])]; - for (const blocker of blockers) emitBare(chalk.yellow(`Blocker: ${blocker}`)); -} - -function formatVersion(version: ProjectVersion): string { - return `${chalk.yellow(version.short_version)} ${version.created_at} ${version.message} ${chalk.dim(`(${version.created_by})`)}`; -} - -function renderPreview(preview: ProjectVersionPreview): void { - emitBare(chalk.bold(`Version ${preview.version_id}`)); - emitBare(chalk.red("--- working tree")); - 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}`)); - else emitBare(chalk.dim(` ${line.text}`)); - } - for (const diagnostic of preview.diagnostics) { - const color = - diagnostic.severity === "error" - ? chalk.red - : diagnostic.severity === "warning" - ? chalk.yellow - : chalk.dim; - emitBare(color(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`)); - } - for (const blocker of preview.blockers) emitBare(chalk.yellow(`blocker: ${blocker}`)); - emitBare(`Can restore: ${preview.can_restore ? "yes" : "no"}`); -} - -type DiffLine = { kind: "context" | "addition" | "deletion"; text: string }; - -function buildLineDiff(beforeSource: string, afterSource: string): DiffLine[] { - const beforeLines = yamlLines(beforeSource); - const afterLines = yamlLines(afterSource); - const maximumDistance = beforeLines.length + afterLines.length; - const frontier = new Map([[1, 0]]); - const traces: Array> = []; - - for (let editDistance = 0; editDistance <= maximumDistance; editDistance += 1) { - traces.push(new Map(frontier)); - for (let diagonal = -editDistance; diagonal <= editDistance; diagonal += 2) { - const deletionStart = frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY; - const additionStart = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY; - const startsWithAddition = - diagonal === -editDistance || (diagonal !== editDistance && deletionStart < additionStart); - let beforeIndex = startsWithAddition ? (frontier.get(diagonal + 1) ?? 0) : deletionStart + 1; - let afterIndex = beforeIndex - diagonal; - while ( - beforeIndex < beforeLines.length && - afterIndex < afterLines.length && - beforeLines[beforeIndex] === afterLines[afterIndex] - ) { - beforeIndex += 1; - afterIndex += 1; - } - frontier.set(diagonal, beforeIndex); - if (beforeIndex >= beforeLines.length && afterIndex >= afterLines.length) { - return backtrackDiff(beforeLines, afterLines, traces, editDistance); - } - } - } - return []; -} - -function backtrackDiff( - beforeLines: string[], - afterLines: string[], - traces: Array>, - finalDistance: number, -): DiffLine[] { - let beforeIndex = beforeLines.length; - let afterIndex = afterLines.length; - const reversedLines: DiffLine[] = []; - for (let editDistance = finalDistance; editDistance >= 0; editDistance -= 1) { - const frontier = traces[editDistance]!; - const diagonal = beforeIndex - afterIndex; - const deletionStart = frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY; - const additionStart = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY; - const cameFromAddition = - diagonal === -editDistance || (diagonal !== editDistance && deletionStart < additionStart); - const previousDiagonal = cameFromAddition ? diagonal + 1 : diagonal - 1; - const previousBeforeIndex = frontier.get(previousDiagonal) ?? 0; - const previousAfterIndex = previousBeforeIndex - previousDiagonal; - while (beforeIndex > previousBeforeIndex && afterIndex > previousAfterIndex) { - reversedLines.push({ kind: "context", text: beforeLines[beforeIndex - 1]! }); - beforeIndex -= 1; - afterIndex -= 1; - } - if (editDistance === 0) break; - if (beforeIndex === previousBeforeIndex) { - reversedLines.push({ kind: "addition", text: afterLines[afterIndex - 1]! }); - afterIndex -= 1; - } else { - reversedLines.push({ kind: "deletion", text: beforeLines[beforeIndex - 1]! }); - beforeIndex -= 1; - } - } - return reversedLines.reverse(); -} - -function yamlLines(source: string): string[] { - const lines = source.split("\n"); - if (lines[lines.length - 1] === "") lines.pop(); - return lines; -} diff --git a/packages/commands/src/commands/managed-agent/workbench.ts b/packages/commands/src/commands/managed-agent/workbench.ts index 08f806c2e..7c3ed0007 100644 --- a/packages/commands/src/commands/managed-agent/workbench.ts +++ b/packages/commands/src/commands/managed-agent/workbench.ts @@ -3,7 +3,7 @@ import { emitResult } from "bailian-cli-runtime"; import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts"; -const WORKBENCH_FLAGS = { +const PLAYGROUND_BASE_FLAGS = { file: { type: "string", valueHint: "", @@ -30,7 +30,7 @@ const WORKBENCH_FLAGS = { } satisfies FlagsDef; const PLAYGROUND_FLAGS = { - ...WORKBENCH_FLAGS, + ...PLAYGROUND_BASE_FLAGS, agent: { type: "string", valueHint: "", @@ -41,52 +41,16 @@ const PLAYGROUND_FLAGS = { }, } satisfies FlagsDef; -const WORKBENCH_NOTES = [ +const PLAYGROUND_NOTES = [ ...CREDENTIALS_NOTE, { "en-US": - "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.", + "Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.", "zh-CN": - "Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;本地版本使用共享的 .openagentpack/versions 项目版本存储,不依赖 Git。", + "会话预览需要 Node.js 22+,并继续使用 agents.yaml;目录 Workbench 位于 managed-agent project workbench。", }, ]; -export const managedAgentWorkbench = defineCommand({ - description: { - "en-US": "Launch the agents.yaml project Workbench", - "zh-CN": "启动 agents.yaml 项目 Workbench", - }, - auth: "apiKey", - usageArgs: "[--file ] [--port ] [--no-open]", - flags: WORKBENCH_FLAGS, - exampleArgs: ["", "--file agents.yaml --no-open", "--port 4949"], - notes: WORKBENCH_NOTES, - async run(ctx) { - const file = ctx.flags.file ?? "agents.yaml"; - const port = ctx.flags.port ?? 4848; - if (ctx.settings.dryRun) { - emitResult( - { - would_launch: "workbench", - config_file: file, - port, - open_browser: !ctx.flags.noOpen, - }, - detectOutputFormat(ctx.settings.output), - ); - return; - } - await launchManagedAgentPlayground({ - file, - port, - open: !ctx.flags.noOpen, - surface: "workbench", - client: ctx.client, - settings: ctx.settings, - }); - }, -}); - export const managedAgentPlayground = defineCommand({ description: { "en-US": "Launch a Session Preview for an agents.yaml Agent", @@ -96,7 +60,7 @@ export const managedAgentPlayground = defineCommand({ usageArgs: "[--file ] [--agent ] [--port ] [--no-open]", flags: PLAYGROUND_FLAGS, exampleArgs: ["", "--agent assistant", "--file agents.yaml --no-open"], - notes: WORKBENCH_NOTES, + notes: PLAYGROUND_NOTES, async run(ctx) { const file = ctx.flags.file ?? "agents.yaml"; const port = ctx.flags.port ?? 4848; diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index abf358782..074928f38 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -136,18 +136,20 @@ export { default as managedAgentValidate } from "./commands/managed-agent/valida export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts"; export { default as managedAgentApply } from "./commands/managed-agent/apply.ts"; export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts"; +export { managedAgentPlayground } from "./commands/managed-agent/workbench.ts"; export { - managedAgentPlayground, - managedAgentWorkbench, -} from "./commands/managed-agent/workbench.ts"; -export { - managedAgentVersionDisable, - managedAgentVersionEnable, - managedAgentVersionList, - managedAgentVersionPreview, - managedAgentVersionRestore, - managedAgentVersionStatus, -} from "./commands/managed-agent/version.ts"; + managedAgentProjectBuild, + managedAgentProjectInit, + managedAgentProjectPublish, + managedAgentProjectValidate, + managedAgentProjectVersionDisable, + managedAgentProjectVersionEnable, + managedAgentProjectVersionList, + managedAgentProjectVersionPreview, + managedAgentProjectVersionRestore, + managedAgentProjectVersionStatus, + managedAgentProjectWorkbench, +} from "./commands/managed-agent/project.ts"; export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts"; export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts"; export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts"; diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index fffd535d7..d1ebc6c72 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -1,5 +1,7 @@ +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, expect, test } from "vite-plus/test"; +import { afterEach, describe, expect, test } from "vite-plus/test"; import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts"; import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts"; @@ -15,6 +17,13 @@ const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [ "bailian.deployment.file.mount_path.required", "bailian.deployment.file.mount_path.duplicate", ]; +const projectDirectories: string[] = []; + +afterEach(async () => { + for (const directory of projectDirectories.splice(0)) { + await rm(directory, { recursive: true, force: true }); + } +}); /** * managed-agent:help / 缺参不依赖密钥;所有 mutation 命令的 --dry-run @@ -143,19 +152,83 @@ describe("e2e: managed-agent", () => { expect(stderr).not.toContain("--git"); }); - test("managed-agent version 暴露共享版本管理子命令", async () => { + test("managed-agent project 暴露目录项目与共享版本管理子命令", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", - "version", + "project", "--help", ]); expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/enable|disable|status|list|preview|restore/i); + expect(stderr).toMatch(/init|validate|build|publish|workbench|version/i); + }); + + test("managed-agent project 完成 init、validate、build 与 version status 本地闭环", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-")); + projectDirectories.push(projectRoot); + const initialized = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "init", + "--project", + projectRoot, + "--provider", + "bailian", + "--output", + "json", + ]); + expect(initialized.exitCode, initialized.stderr).toBe(0); + expect( + parseStdoutJson<{ baseline_version?: string }>(initialized.stdout).baseline_version, + ).toMatch(/^[a-f0-9]{64}$/); + + const validated = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "validate", + "--project", + projectRoot, + "--output", + "json", + ]); + expect(validated.exitCode, validated.stderr).toBe(0); + expect( + parseStdoutJson<{ diagnostics?: Array<{ severity?: string }> }>(validated.stdout).diagnostics, + ).not.toContainEqual(expect.objectContaining({ severity: "error" })); + + const built = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "build", + "--project", + projectRoot, + "--yes", + "--output", + "json", + ]); + expect(built.exitCode, built.stderr).toBe(0); + expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain( + "assistant", + ); + + const status = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "version", + "status", + "--project", + projectRoot, + "--output", + "json", + ]); + expect(status.exitCode, status.stderr).toBe(0); + expect(parseStdoutJson<{ enabled?: boolean }>(status.stdout).enabled).toBe(true); + expect(await stat(join(projectRoot, ".openagentpack/state.json")).catch(() => null)).toBeNull(); }); - test("managed-agent version preview 缺少 --version-id 时退出为用法错误 (2)", async () => { + test("managed-agent project version preview 缺少 --version-id 时退出为用法错误 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", + "project", "version", "preview", "--quiet", @@ -250,11 +323,14 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(data.provider).toBe("bailian"); }); - test("workbench --dry-run 仅输出启动计划", async () => { + test("project workbench --dry-run 仅输出目录项目启动计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", + "project", "workbench", "--dry-run", + "--project", + "./agent-project", "--no-open", "--output", "json", @@ -262,10 +338,12 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(exitCode, stderr).toBe(0); const data = parseStdoutJson<{ would_launch?: string; - open_browser?: boolean; + project_root?: string; + port?: number; }>(stdout); expect(data.would_launch).toBe("workbench"); - expect(data.open_browser).toBe(false); + expect(data.project_root).toBe("./agent-project"); + expect(data.port).toBe(4848); }); test("apply --dry-run 仅输出计划", async () => { diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index df8bf3c1c..af7ba2b8a 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -186,14 +186,18 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "managed-agent plan": "managedAgentPlan", "managed-agent apply": "managedAgentApply", "managed-agent destroy": "managedAgentDestroy", - "managed-agent workbench": "managedAgentWorkbench", "managed-agent playground": "managedAgentPlayground", - "managed-agent version enable": "managedAgentVersionEnable", - "managed-agent version disable": "managedAgentVersionDisable", - "managed-agent version status": "managedAgentVersionStatus", - "managed-agent version list": "managedAgentVersionList", - "managed-agent version preview": "managedAgentVersionPreview", - "managed-agent version restore": "managedAgentVersionRestore", + "managed-agent project init": "managedAgentProjectInit", + "managed-agent project validate": "managedAgentProjectValidate", + "managed-agent project build": "managedAgentProjectBuild", + "managed-agent project publish": "managedAgentProjectPublish", + "managed-agent project workbench": "managedAgentProjectWorkbench", + "managed-agent project version enable": "managedAgentProjectVersionEnable", + "managed-agent project version disable": "managedAgentProjectVersionDisable", + "managed-agent project version status": "managedAgentProjectVersionStatus", + "managed-agent project version list": "managedAgentProjectVersionList", + "managed-agent project version preview": "managedAgentProjectVersionPreview", + "managed-agent project version restore": "managedAgentProjectVersionRestore", "managed-agent state list": "managedAgentStateList", "managed-agent state rm": "managedAgentStateRm", "managed-agent state import": "managedAgentStateImport", diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 73175659a..a91a2ef08 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -37,21 +37,24 @@ description: >- 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` -## Workbench and local versions - -| 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 `.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. +## Directory projects, Workbench, and local versions + +| Intent | Command | +| --------------------------------------- | ------------------------------------------------------ | +| Create or convert a directory project | `bl managed-agent project init` | +| Validate and Build directory source | `bl managed-agent project validate` / `build` | +| Publish the current immutable Build | `bl managed-agent project publish --yes` | +| Launch project resource editing | `bl managed-agent project workbench` | +| Launch one Agent Session Preview | `bl managed-agent playground --agent ` | +| Enable/disable project versions | `bl managed-agent project version enable` / `disable` | +| Inspect local version state and history | `bl managed-agent project version status` / `list` | +| Preview or restore project source | `bl managed-agent project version preview` / `restore` | + +- Bailian CLI and Workbench use the same `.openagentpack/versions/project` store and enable switch. Git is not required. +- Build is local-only. Publish never runs Build implicitly and consumes only a current `.openagentpack/build/agents.yaml` plus manifest. +- A successful Publish versions the canonical YAML and the complete project source tree, including Skill scripts/assets and binary files. Remote State is never versioned or restored. +- `project version restore` restores source files to the working directory, invalidates Build, and does not move version history or remote State. +- `managed-agent playground` remains the standalone `agents.yaml` Session Preview path; directory Workbench is only under `managed-agent project workbench`. ## Deployment as IaC diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index a79519ad7..d0604bd54 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,39 +9,43 @@ Use this index for the skill-scoped quick index and global flags. ## Quick index -| Command | Authentication | Description | Detail | -| ---------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ | -| `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 | [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) | -| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session events` | API Key | List event history for a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | -| `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 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 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) | +| Command | Authentication | Description | Detail | +| ------------------------------------------ | -------------- | ------------------------------------------------------------------ | ------------------------------------ | +| `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 | [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 project build` | No Auth | Organize directory source and generate the immutable Publish Build | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version disable` | No Auth | Disable directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version enable` | No Auth | Enable directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version list` | No Auth | List directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version preview` | No Auth | Preview a directory project version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version status` | No Auth | Show directory project version status | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | [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) | +| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session events` | API Key | List event history for a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | +| `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) | ## By group -| Group | Commands | Reference | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `playground`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `version disable`, `version enable`, `version list`, `version preview`, `version restore`, `version status`, `workbench` | [managed-agent.md](managed-agent.md) | +| Group | Commands | Reference | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `playground`, `project build`, `project init`, `project publish`, `project validate`, `project version disable`, `project version enable`, `project version list`, `project version preview`, `project version restore`, `project version status`, `project workbench`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) | ## Global flags diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 5bf40048f..d06f9c42b 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,33 +7,37 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| ---------------------------------- | -------------- | ------------------------------------------------------------- | -| `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 | -| `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 | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session events` | API Key | List event history for a session | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `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 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 snapshot versioning status for agents.yaml | -| `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench | +| Command | Authentication | Description | +| ------------------------------------------ | -------------- | ------------------------------------------------------------------ | +| `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 | +| `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 project build` | No Auth | Organize directory source and generate the immutable Publish Build | +| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | +| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | +| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | +| `bl managed-agent project version disable` | No Auth | Disable directory project versions | +| `bl managed-agent project version enable` | No Auth | Enable directory project versions | +| `bl managed-agent project version list` | No Auth | List directory project versions | +| `bl managed-agent project version preview` | No Auth | Preview a directory project version | +| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | +| `bl managed-agent project version status` | No Auth | Show directory project version status | +| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | +| `bl managed-agent session create` | API Key | Create a new session for an agent | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session events` | API Key | List event history for a session | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `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) | ## Command details @@ -204,7 +208,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. Local versions use the shared .openagentpack/versions project store and do not require Git. +- Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench. #### Examples @@ -220,6 +224,318 @@ bl managed-agent playground --agent assistant bl managed-agent playground --file agents.yaml --no-open ``` +### `bl managed-agent project build` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------ | +| **Name** | `managed-agent project build` | +| **Description** | Organize directory source and generate the immutable Publish Build | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project build [--project ] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--yes` | switch | no | Write the previewed Build | + +#### Examples + +```bash +bl managed-agent project build --dry-run +``` + +```bash +bl managed-agent project build --yes +``` + +```bash +bl managed-agent project build --project ./my-agent --yes +``` + +### `bl managed-agent project init` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------- | +| **Name** | `managed-agent project init` | +| **Description** | Create a directory project or convert the local agents.yaml | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project init [--project ] [--provider bailian]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--provider ` | string | no | Provider for a new project | + +#### Examples + +```bash +bl managed-agent project init +``` + +```bash +bl managed-agent project init --project ./my-agent +``` + +```bash +bl managed-agent project init --provider bailian +``` + +### `bl managed-agent project publish` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent project publish` | +| **Description** | Publish the current directory-project Build and record a version | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent project publish [--project ] [--provider ] [--yes] [--no-refresh] [--concurrency ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--provider ` | string | no | Target provider | +| `--yes` | switch | no | Confirm remote Publish | +| `--no-refresh` | switch | no | Skip remote refresh before planning | +| `--concurrency ` | number | no | Maximum parallel resource operations | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- 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. + +#### Examples + +```bash +bl managed-agent project publish --yes +``` + +```bash +bl managed-agent project publish --project ./my-agent --yes +``` + +```bash +bl managed-agent project publish --provider bailian --yes +``` + +### `bl managed-agent project validate` + +| Field | Value | +| ------------------ | ----------------------------------------------------------- | +| **Name** | `managed-agent project validate` | +| **Description** | Validate a directory Agent project | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project validate [--project ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | + +#### Examples + +```bash +bl managed-agent project validate +``` + +```bash +bl managed-agent project validate --project ./my-agent +``` + +### `bl managed-agent project version disable` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------ | +| **Name** | `managed-agent project version disable` | +| **Description** | Disable directory project versions | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version disable [--project ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | + +#### Examples + +```bash +bl managed-agent project version disable +``` + +```bash +bl managed-agent project version disable --project ./my-agent +``` + +### `bl managed-agent project version enable` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------- | +| **Name** | `managed-agent project version enable` | +| **Description** | Enable directory project versions | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version enable [--project ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | + +#### Examples + +```bash +bl managed-agent project version enable +``` + +```bash +bl managed-agent project version enable --project ./my-agent +``` + +### `bl managed-agent project version list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent project version list` | +| **Description** | List directory project versions | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version list [--project ] [--limit ] [--cursor ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--limit ` | number | no | Maximum versions to return | +| `--cursor ` | string | no | Pagination cursor | + +#### Examples + +```bash +bl managed-agent project version list +``` + +```bash +bl managed-agent project version list --limit 20 --output json +``` + +### `bl managed-agent project version preview` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent project version preview` | +| **Description** | Preview a directory project version | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version preview --version-id [--project ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--version-id ` | string | yes | Full project version ID | + +#### Examples + +```bash +bl managed-agent project version preview --version-id +``` + +### `bl managed-agent project version restore` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent project version restore` | +| **Description** | Restore a version to the project working directory | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version restore --version-id [--project ] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--version-id ` | string | yes | Full project version ID | +| `--yes` | switch | no | Restore without interactive confirmation | + +#### Examples + +```bash +bl managed-agent project version restore --version-id +``` + +```bash +bl managed-agent project version restore --version-id --yes +``` + +### `bl managed-agent project version status` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------- | +| **Name** | `managed-agent project version status` | +| **Description** | Show directory project version status | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent project version status [--project ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | + +#### Examples + +```bash +bl managed-agent project version status +``` + +```bash +bl managed-agent project version status --project ./my-agent --output json +``` + +### `bl managed-agent project workbench` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------- | +| **Name** | `managed-agent project workbench` | +| **Description** | Launch the directory project Workbench | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent project workbench [--project ] [--port ] [--no-open]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------- | +| `--project ` | string | no | Directory project root (default: current directory) | +| `--port ` | number | no | Local port (default: 4848) | +| `--no-open` | switch | no | Do not open a browser | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- 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. + +#### Examples + +```bash +bl managed-agent project workbench +``` + +```bash +bl managed-agent project workbench --project ./my-agent --no-open +``` + ### `bl managed-agent session create` | Field | Value | @@ -664,198 +980,3 @@ bl managed-agent validate ```bash bl managed-agent validate --file agents.yaml ``` - -### `bl managed-agent version disable` - -| 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 - -| Flag | Type | Required | Description | -| --------------- | ------ | -------- | --------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | - -#### Examples - -```bash -bl managed-agent version disable -``` - -```bash -bl managed-agent version disable --file agents.yaml -``` - -### `bl managed-agent version enable` - -| Field | Value | -| ------------------ | ------------------------------------------------- | -| **Name** | `managed-agent version enable` | -| **Description** | Enable Apply-time local snapshots for agents.yaml | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version enable [--file ]` | - -#### Flags - -| Flag | Type | Required | Description | -| --------------- | ------ | -------- | --------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | - -#### Examples - -```bash -bl managed-agent version enable -``` - -```bash -bl managed-agent version enable --file agents.yaml -``` - -### `bl managed-agent version list` - -| Field | Value | -| ------------------ | --------------------------------------------------------------------------------- | -| **Name** | `managed-agent version list` | -| **Description** | List local snapshots of agents.yaml | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version list [--file ] [--limit ] [--cursor ]` | - -#### Flags - -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | -------------------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | -| `--limit ` | number | no | Maximum versions to return (default: 50, max: 100) | -| `--cursor ` | string | no | Pagination cursor returned by the previous page | - -#### Examples - -```bash -bl managed-agent version list -``` - -```bash -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 --version-id [--file ]` | - -#### Flags - -| 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 --version-id -``` - -```bash -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 --version-id [--file ] [--yes]` | - -#### Flags - -| 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 --version-id -``` - -```bash -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 snapshot versioning status for agents.yaml | -| **Authentication** | No Auth | -| **Usage** | `bl managed-agent version status [--file ]` | - -#### Flags - -| Flag | Type | Required | Description | -| --------------- | ------ | -------- | --------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | - -#### Examples - -```bash -bl managed-agent version status -``` - -```bash -bl managed-agent version status --file agents.yaml --output json -``` - -### `bl managed-agent workbench` - -| Field | Value | -| ------------------ | --------------------------------------------------------------------- | -| **Name** | `managed-agent workbench` | -| **Description** | Launch the agents.yaml project Workbench | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent workbench [--file ] [--port ] [--no-open]` | - -#### Flags - -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | --------------------------------------- | -| `--file ` | string | no | Config file path (default: agents.yaml) | -| `--port ` | number | no | Local port (default: 4848) | -| `--no-open` | switch | no | Do not open a browser automatically | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | - -#### Notes - -- 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. Local versions use the shared .openagentpack/versions project store and do not require Git. - -#### Examples - -```bash -bl managed-agent workbench -``` - -```bash -bl managed-agent workbench --file agents.yaml --no-open -``` - -```bash -bl managed-agent workbench --port 4949 -```