From b93e0d586d898f5ceebaae1645b8bf22302e9604 Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Tue, 25 Aug 2026 20:41:47 +0800 Subject: [PATCH 1/8] 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/8] 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/8] 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 -``` From a92c58420b1c35dc5893225b0aeab46beadc5e7d Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Fri, 4 Sep 2026 11:22:11 +0800 Subject: [PATCH 4/8] fix(managed-agent): use CLI credentials for directory projects Make project init fully offline, stop generating project-local credential files, bind Publish to the reviewed plan fingerprint, remove the unsafe Apply refresh-only mode, and document Agent-owned File mounts. --- .../managed-agent/_engine/config-loader.ts | 5 ++- .../managed-agent/_engine/credentials.ts | 10 +++-- .../src/commands/managed-agent/apply.ts | 8 ---- .../src/commands/managed-agent/project.ts | 16 +++---- .../commands/tests/credentials-bridge.test.ts | 14 ++++++ .../tests/e2e/managed-agent.e2e.test.ts | 45 ++++++++++++++++++- skills/bailian-managed-agent/SKILL.md | 3 ++ .../reference/managed-agent.md | 1 - 8 files changed, 78 insertions(+), 24 deletions(-) diff --git a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts index 7c83101da..29815d9cb 100644 --- a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts +++ b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts @@ -33,6 +33,7 @@ interface AgentConfigOptions { projectName?: string; statePath?: string; credentials?: CredentialScope; + overrideBailianBaseUrl?: boolean; } /** @@ -56,7 +57,9 @@ export async function resolveAgentProjectConfig( prepareProviderEnv(); const resolved = await resolveProjectConfig(filePath, options); normalizeInterpolatedProviderBlocks(resolved.config.providers); - injectProviderCredentials(resolved.config.providers, host); + injectProviderCredentials(resolved.config.providers, host, { + overrideBaseUrl: options.overrideBailianBaseUrl, + }); scrubCredentialEnv(); assertBailianOnlyProviders(resolved.config.providers); if ((options.credentials ?? "all") !== "none") { diff --git a/packages/commands/src/commands/managed-agent/_engine/credentials.ts b/packages/commands/src/commands/managed-agent/_engine/credentials.ts index 934637b42..1ad42ebee 100644 --- a/packages/commands/src/commands/managed-agent/_engine/credentials.ts +++ b/packages/commands/src/commands/managed-agent/_engine/credentials.ts @@ -99,9 +99,10 @@ export function prepareProviderEnv(): void { * Override the bailian provider block with bl's authStage-resolved credential, so * the bailian API key is authoritatively the CLI auth chain's — never a config * file bare-read or a stale env value. `api_key` is replaced unconditionally - * when a credential resolved; `base_url` / `workspace_id` are filled only when - * the block references them and the interpolated value is empty (a literal in - * agents.yaml is respected). + * when a credential resolved. `base_url` is normally filled only when empty so + * a literal in agents.yaml remains supported; directory projects pass + * `overrideBaseUrl` so their connection always follows the Bailian CLI auth + * chain. `workspace_id` is filled only when empty. * * `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource * paths onto it verbatim; a value already ending in the suffix is left as-is. @@ -114,6 +115,7 @@ export function prepareProviderEnv(): void { export function injectProviderCredentials( providers: Record, host: CredentialHost, + options: { overrideBaseUrl?: boolean } = {}, ): void { const bailian = providers.bailian; if (!bailian || typeof bailian !== "object") return; @@ -121,7 +123,7 @@ export function injectProviderCredentials( const cred = host.client.exportApiCredential(); if (cred) block.api_key = cred.token; - if ("base_url" in block && !block.base_url) { + if ("base_url" in block && (options.overrideBaseUrl || !block.base_url)) { // Defensive normalization: the auth chain already normalizes base_url to // an origin, but never let a trailing slash produce "//api/v1/agentstudio". const origin = host.client.baseUrl.replace(/\/+$/, ""); diff --git a/packages/commands/src/commands/managed-agent/apply.ts b/packages/commands/src/commands/managed-agent/apply.ts index d988b12a8..4d4a940ae 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -29,13 +29,6 @@ const APPLY_FLAGS = { "zh-CN": "规划前跳过从远端刷新状态", }, }, - refreshOnly: { - type: "switch", - description: { - "en-US": "Refresh state without mutating remote resources", - "zh-CN": "仅刷新 State,不修改远端资源", - }, - }, concurrency: { type: "number", valueHint: "", @@ -76,7 +69,6 @@ export default defineCommand({ provider: "bailian", refresh: !flags.noRefresh, concurrency: flags.concurrency, - refresh_only: flags.refreshOnly, }, config_file: file, hint: "Run `managed-agent plan` to preview the exact resource changes.", diff --git a/packages/commands/src/commands/managed-agent/project.ts b/packages/commands/src/commands/managed-agent/project.ts index 6c30f0252..12fc98f73 100644 --- a/packages/commands/src/commands/managed-agent/project.ts +++ b/packages/commands/src/commands/managed-agent/project.ts @@ -49,15 +49,14 @@ export const managedAgentProjectInit = defineCommand({ async run(ctx) { if (ctx.settings.dryRun) { emitResult( - { would_initialize_project: ctx.flags.project ?? "." }, + { + would_initialize_project: ctx.flags.project ?? ".", + }, detectOutputFormat(ctx.settings.output), ); return; } - const result = await initializeDirectoryProject({ - projectRoot: ctx.flags.project ?? ".", - provider: "bailian", - }); + const result = await initializeDirectoryProject({ projectRoot: ctx.flags.project ?? "." }); emitResult(result, detectOutputFormat(ctx.settings.output)); }, }); @@ -163,9 +162,9 @@ export const managedAgentProjectPublish = defineCommand({ installSdkTransport(ctx); const root = ctx.flags.project ?? "."; const resolveBuild: ProjectBuildResolver = async (buildPath) => - (await resolveAgentProjectConfig(ctx, buildPath)) as unknown as Awaited< - ReturnType - >; + (await resolveAgentProjectConfig(ctx, buildPath, { + overrideBailianBaseUrl: true, + })) as unknown as Awaited>; const planned = await withAgentErrors(() => withStdoutProtected(() => planProjectPublish(root, { @@ -199,6 +198,7 @@ export const managedAgentProjectPublish = defineCommand({ projectRoot: planned.project_root, expectedProjectRevision: planned.project_revision, expectedYamlHash: planned.build_manifest.yaml_hash, + expectedPlanFingerprint: planned.plan_fingerprint, provider: "bailian", refresh: !ctx.flags.noRefresh, concurrency: ctx.flags.concurrency, diff --git a/packages/commands/tests/credentials-bridge.test.ts b/packages/commands/tests/credentials-bridge.test.ts index dde12cfaf..92cad27aa 100644 --- a/packages/commands/tests/credentials-bridge.test.ts +++ b/packages/commands/tests/credentials-bridge.test.ts @@ -104,6 +104,20 @@ test("inject:base_url 已带后缀不重复拼;非空字面量 base_url 保留", expect(literal.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio"); }); +test("inject:目录项目可强制使用 Bailian CLI 解析出的 base_url", () => { + const providers = { + bailian: { + api_key: "from-project-env", + base_url: "https://project-env.example.com/api/v1/agentstudio", + }, + }; + injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }), { + overrideBaseUrl: true, + }); + expect(providers.bailian.api_key).toBe("sk-auth-chain"); + expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio"); +}); + test("inject:base_url 尾斜杠被规范化,不产生双斜杠", () => { const providers = { bailian: { api_key: "", base_url: "" } }; injectProviderCredentials( diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index 4a015d323..30ac4958b 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -105,8 +105,8 @@ async function seedTrackedResources( /** * managed-agent:help / 缺参不依赖密钥;所有 mutation 命令的 --dry-run * 必须在构建 SDK runtime(凭证注入 / 联网 / 写盘)之前短路,因此同样不需要密钥。 - * 鉴权分层:离线命令(init/validate/state list|show|rm)auth: "none";联网命令 - * 统一 auth: "apiKey" 硬门禁(见 managed-agent-auth-chain e2e)。 + * 鉴权分层:离线命令(init/project init/validate/state list|show|rm)auth: "none"; + * 联网命令统一 auth: "apiKey" 硬门禁(见 managed-agent-auth-chain e2e)。 * 真实集成(apply/destroy/session 流程)依赖工作区内的 agents.yaml 与远端资源, * 属批量场景,暂仅覆盖 dry-run 契约。 */ @@ -234,6 +234,18 @@ describe("e2e: managed-agent", () => { expect(stderr).toMatch(/--file|--yes/i); expect(stderr).not.toContain("--provider"); expect(stderr).not.toContain("--ci"); + expect(stderr).not.toContain("--refresh-only"); + }); + + test("managed-agent apply 不再接受 --refresh-only", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "apply", + "--dry-run", + "--refresh-only", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/Unknown flag.*--refresh-only/i); }); test("managed-agent init 不再暴露 Git 仓库脚手架", async () => { @@ -272,6 +284,11 @@ describe("e2e: managed-agent", () => { expect( parseStdoutJson<{ baseline_version?: string }>(initialized.stdout).baseline_version, ).toMatch(/^[a-f0-9]{64}$/); + expect(JSON.parse(await readFile(join(projectRoot, "project.json"), "utf8"))).toEqual({ + version: "1", + }); + expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull(); + expect(await stat(join(projectRoot, ".gitignore")).catch(() => null)).toBeNull(); const validated = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", @@ -301,6 +318,9 @@ describe("e2e: managed-agent", () => { expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain( "assistant", ); + expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain( + "provider: bailian", + ); const status = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", @@ -1425,6 +1445,27 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(stderr).toMatch(/Unknown flag.*--provider/i); }); + test("project init --dry-run 不需要密钥且不创建目录", async () => { + const parent = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-dry-run-")); + projectDirectories.push(parent); + const projectRoot = join(parent, "project"); + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "init", + "--dry-run", + "--project", + projectRoot, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + expect( + parseStdoutJson<{ would_initialize_project?: string }>(stdout).would_initialize_project, + ).toBe(projectRoot); + expect(await stat(projectRoot).catch(() => null)).toBeNull(); + }); + test("apply --dry-run 仅输出计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index fd1802e31..1f26fc54f 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -62,7 +62,10 @@ for explicit confirmation before re-running with `--yes`. | 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. +- Directory projects always use Bailian. `project.json` does not declare a Provider; Build supplies the Bailian Provider configuration automatically. +- `project init`, `validate`, `build`, and version commands are local-only. Publish and Workbench resolve credentials from Bailian CLI flags, shell environment, or the active Profile; project initialization does not write credentials into the project directory. - Build is local-only. Publish never runs Build implicitly and consumes only a current `.openagentpack/build/agents.yaml` plus manifest. +- Agent-owned Files live under `agents//files//`. Add `{ "file": "", "mount_path": "/mnt/" }` to `agent.json.files`; Publish uploads the File and every later Session mounts it automatically. A File referenced by multiple Agents is promoted to `resources/files//` during Build. - 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`. diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 85d44df38..1f0baacc1 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -306,7 +306,6 @@ bl managed-agent agent versions --agent-id agent_abc --all --output json | ------------------- | ------ | -------- | ------------------------------------------------------------------ | | `--file ` | string | no | Config file path (default: agents.yaml) | | `--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) | | `--yes` | switch | no | Confirm this high-risk operation | | `--api-key ` | string | no | API key | From c6b9931d6fb3fa6fb0f6a11834ef57cb5d68d082 Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Fri, 4 Sep 2026 17:20:50 +0800 Subject: [PATCH 5/8] feat(managed-agent): integrate directory resource scaffolds and safe builds --- .../managed-agent/_engine/config-loader.ts | 1 + .../src/commands/managed-agent/project.ts | 16 +++- .../tests/e2e/managed-agent.e2e.test.ts | 96 +++++++++++++++++++ skills/bailian-managed-agent/SKILL.md | 5 +- .../reference/managed-agent.md | 20 ++-- 5 files changed, 126 insertions(+), 12 deletions(-) diff --git a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts index 29815d9cb..89185df7d 100644 --- a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts +++ b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts @@ -30,6 +30,7 @@ export const BAILIAN_PROVIDER = "bailian"; interface AgentConfigOptions { resolveEnv?: boolean; + environment?: Record; projectName?: string; statePath?: string; credentials?: CredentialScope; diff --git a/packages/commands/src/commands/managed-agent/project.ts b/packages/commands/src/commands/managed-agent/project.ts index 12fc98f73..c239af8ca 100644 --- a/packages/commands/src/commands/managed-agent/project.ts +++ b/packages/commands/src/commands/managed-agent/project.ts @@ -46,6 +46,14 @@ export const managedAgentProjectInit = defineCommand({ usageArgs: "[--project ]", flags: PROJECT_FLAG, exampleArgs: ["", "--project ./my-agent"], + notes: [ + { + "en-US": + "New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.", + "zh-CN": + "新项目在四类资源目录的 _examples/ 下生成配置示例,不写入 agent.json 引用,也不参与 Build/Publish。需要使用时,将示例复制到 _examples/ 外,再配置 Agent 引用。", + }, + ], async run(ctx) { if (ctx.settings.dryRun) { emitResult( @@ -95,8 +103,9 @@ export const managedAgentProjectBuild = defineCommand({ level: "high", message: { "en-US": - "This organizes the directory project source and writes the previewed immutable Build.", - "zh-CN": "该操作会整理目录项目源文件,并写入已预览的不可变 Build。", + "This organizes project source, moves literal Vault secrets into the local .env, and writes the previewed immutable Build.", + "zh-CN": + "该操作会整理项目源文件,将 Vault 明文密钥移入本地 .env,并写入已预览的不可变 Build。", }, }, usageArgs: "[--project ]", @@ -161,8 +170,9 @@ export const managedAgentProjectPublish = defineCommand({ async run(ctx) { installSdkTransport(ctx); const root = ctx.flags.project ?? "."; - const resolveBuild: ProjectBuildResolver = async (buildPath) => + const resolveBuild: ProjectBuildResolver = async (buildPath, options) => (await resolveAgentProjectConfig(ctx, buildPath, { + environment: options?.environment, overrideBailianBaseUrl: true, })) as unknown as Awaited>; const planned = await withAgentErrors(() => diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index 30ac4958b..30f577701 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -290,6 +290,32 @@ describe("e2e: managed-agent", () => { expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull(); expect(await stat(join(projectRoot, ".gitignore")).catch(() => null)).toBeNull(); + expect( + JSON.parse(await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8")), + ).toEqual({ name: "Assistant", model: "qwen3.7-max" }); + for (const relativePath of [ + "skills/_examples/example-skill/skill.json", + "skills/_examples/example-skill/SKILL.md", + "files/_examples/example-file/file.json", + "files/_examples/example-file/example.md", + "vaults/_examples/example-vault/vault.json", + "environments/_examples/example-env/environment.json", + ]) { + expect((await stat(join(projectRoot, "agents/assistant", relativePath))).isFile()).toBe(true); + expect( + await readFile(join(projectRoot, "agents/assistant", relativePath, "../README.md"), "utf8"), + ).toContain("_examples/"); + } + + const skillDirectory = join(projectRoot, "agents/assistant/skills/writer"); + const filesDirectory = join(projectRoot, "agents/assistant/files"); + const nestedFileDirectory = join(filesDirectory, "mount"); + await mkdir(skillDirectory, { recursive: true }); + await mkdir(nestedFileDirectory, { recursive: true }); + await writeFile(join(skillDirectory, "SKILL.md"), "# Writer\n", "utf8"); + await writeFile(join(filesDirectory, "brief.txt"), "Build this brief.\n", "utf8"); + await writeFile(join(nestedFileDirectory, "mount.md"), "Mount this file.\n", "utf8"); + const validated = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", "project", @@ -304,6 +330,38 @@ describe("e2e: managed-agent", () => { parseStdoutJson<{ diagnostics?: Array<{ severity?: string }> }>(validated.stdout).diagnostics, ).not.toContainEqual(expect.objectContaining({ severity: "error" })); + const vaultDirectory = join(projectRoot, "agents/assistant/vaults/secrets"); + await mkdir(vaultDirectory, { recursive: true }); + await writeFile( + join(vaultDirectory, "vault.json"), + JSON.stringify({ + id: "secrets", + display_name: "Secrets", + credentials: [ + { + name: "service", + type: "environment_variable", + secret_name: "SERVICE_TOKEN", + secret_value: "e2e-vault-private-value", + }, + ], + }), + ); + const dryBuild = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "build", + "--project", + projectRoot, + "--dry-run", + "--output", + "json", + ]); + expect(dryBuild.exitCode, dryBuild.stderr).toBe(0); + expect(dryBuild.stdout + dryBuild.stderr).not.toContain("e2e-vault-private-value"); + expect(dryBuild.stdout).toContain("AGENTS_VAULT_"); + expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull(); + const built = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", "project", @@ -315,12 +373,50 @@ describe("e2e: managed-agent", () => { "json", ]); expect(built.exitCode, built.stderr).toBe(0); + const generatedYaml = await readFile( + join(projectRoot, ".openagentpack/build/agents.yaml"), + "utf8", + ); + expect(generatedYaml).not.toContain("example-"); + expect(generatedYaml).not.toContain("_examples"); + expect(generatedYaml).not.toContain("$" + "{SERVICE_TOKEN}"); + expect(built.stdout + built.stderr).not.toContain("e2e-vault-private-value"); + expect(await readFile(join(projectRoot, ".env"), "utf8")).toContain("e2e-vault-private-value"); + expect(await readFile(join(vaultDirectory, "vault.json"), "utf8")).not.toContain( + "e2e-vault-private-value", + ); + expect( + await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8"), + ).not.toContain("e2e-vault-private-value"); expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain( "assistant", ); expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain( "provider: bailian", ); + expect(JSON.parse(await readFile(join(skillDirectory, "skill.json"), "utf8"))).toEqual({ + id: "writer", + }); + expect(JSON.parse(await readFile(join(filesDirectory, "brief/file.json"), "utf8"))).toEqual({ + id: "brief", + name: "brief.txt", + source: "./brief.txt", + }); + expect(JSON.parse(await readFile(join(nestedFileDirectory, "file.json"), "utf8"))).toEqual({ + id: "mount", + name: "mount.md", + source: "./mount.md", + }); + const builtAgent = JSON.parse( + await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8"), + ); + expect(builtAgent.skills).toEqual(["writer"]); + expect(builtAgent.files).toEqual( + expect.arrayContaining([ + { file: "brief", mount_path: "/mnt/brief.txt" }, + { file: "mount", mount_path: "/mnt/mount.md" }, + ]), + ); const status = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 1f26fc54f..3df081f3e 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -63,9 +63,12 @@ for explicit confirmation before re-running with `--yes`. - Bailian CLI and Workbench use the same `.openagentpack/versions/project` store and enable switch. Git is not required. - Directory projects always use Bailian. `project.json` does not declare a Provider; Build supplies the Bailian Provider configuration automatically. +- Fresh `project init` includes complete Skill/File/Vault/Environment examples with bilingual README files under `agents/assistant//_examples/`. They are not linked in `agent.json` and are excluded from Build discovery, Workbench declarations, and remote Publish. Copy a resource outside `_examples/` and configure its Agent reference to use it. Examples remain local versioned source; never put real secrets into them. / 新项目的四类资源示例默认不启用、不发布;请按 README 复制到 `_examples/` 外再配置引用,不要向示例写入真实密钥。 - `project init`, `validate`, `build`, and version commands are local-only. Publish and Workbench resolve credentials from Bailian CLI flags, shell environment, or the active Profile; project initialization does not write credentials into the project directory. - Build is local-only. Publish never runs Build implicitly and consumes only a current `.openagentpack/build/agents.yaml` plus manifest. -- Agent-owned Files live under `agents//files//`. Add `{ "file": "", "mount_path": "/mnt/" }` to `agent.json.files`; Publish uploads the File and every later Session mounts it automatically. A File referenced by multiple Agents is promoted to `resources/files//` during Build. +- Build moves literal Vault `secret_value` / `access_token` values from Agent-local or shared `vault.json` into project-root `.env`, replacing them with generated environment references. Existing references and `.env` entries are preserved; conflicts receive suffixed variable names. Preview/dry-run never write or print secrets. Publish and Workbench read the selected project's root `.env` as a fallback to inherited environment variables, even when invoked elsewhere. `.env` is private plaintext storage, excluded from local versions but not automatically ignored by Git; keep it backed up securely. +- Build 会将 Agent 本地或共享 `vault.json` 中的明文密钥移入项目根目录 `.env`,再写回环境变量引用;保留已有引用和变量,重名时生成后缀。预览不写文件或输出密钥。`.env` 不进入版本快照,也不加密;请自行备份并加入 Git 忽略规则。 +- Agent-local File and Skill content supports Build-time association. A File may be copied directly into `agents//files/`, or placed in `agents//files//` when that directory contains exactly one content file; Build generates `file.json` and a `/mnt/` entry in `agent.json.files`. A directory under `agents//skills//` containing `SKILL.md` generates `skill.json` and its `agent.json.skills` entry. Explicit JSON always wins; shared root resources remain explicit. Resources referenced by multiple Agents are promoted to the corresponding root shared directory during Build. - 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`. diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 1f0baacc1..a933746c9 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -1181,14 +1181,14 @@ 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 ]` | -| **Risk** | `high` | -| **Risk message** | This organizes the directory project source and writes the previewed immutable 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 ]` | +| **Risk** | `high` | +| **Risk message** | This organizes project source, moves literal Vault secrets into the local .env, and writes the previewed immutable Build. | > **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. @@ -1230,6 +1230,10 @@ bl managed-agent project build --project ./my-agent --yes | ----------------------- | ------ | -------- | --------------------------------------------------- | | `--project ` | string | no | Directory project root (default: current directory) | +#### Notes + +- New projects include Skill, File, Vault, and Environment examples under each resource directory's \_examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside \_examples/ to enable it, then configure its Agent reference. + #### Examples ```bash From f44fb2e95fdaaf296cf884615ba2dd775e926c4a Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Mon, 7 Sep 2026 19:58:47 +0800 Subject: [PATCH 6/8] refactor(managed-agent): use SDK project service subpaths Replace standalone project packages with @openagentpack/sdk 0.7.0 and update project/version imports. The pnpm lockfile remains pending until SDK 0.7.0 is published to npm; this integration has been validated against the local SDK build. --- packages/commands/package.json | 3 +-- packages/commands/src/commands/managed-agent/project.ts | 2 +- packages/commands/tests/managed-agent-local-versions.test.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/commands/package.json b/packages/commands/package.json index 510ba32a5..440be5f7f 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -40,8 +40,7 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/project-workspace": "0.4.0", - "@openagentpack/sdk": "0.5.0", + "@openagentpack/sdk": "0.7.0", "bailian-cli-core": "workspace:*", "bailian-cli-runtime": "workspace:*", "boxen": "catalog:", diff --git a/packages/commands/src/commands/managed-agent/project.ts b/packages/commands/src/commands/managed-agent/project.ts index c239af8ca..50330c4dc 100644 --- a/packages/commands/src/commands/managed-agent/project.ts +++ b/packages/commands/src/commands/managed-agent/project.ts @@ -15,7 +15,7 @@ import { previewProjectBuild, type ProjectBuildResolver, validateDirectoryProject, -} from "@openagentpack/project-workspace"; +} from "@openagentpack/sdk/project-workspace"; import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; diff --git a/packages/commands/tests/managed-agent-local-versions.test.ts b/packages/commands/tests/managed-agent-local-versions.test.ts index dae7d8f26..daca80468 100644 --- a/packages/commands/tests/managed-agent-local-versions.test.ts +++ b/packages/commands/tests/managed-agent-local-versions.test.ts @@ -10,7 +10,7 @@ import { prepareProjectVersion, previewProjectVersion, restoreProjectVersion, -} from "@openagentpack/project-versions"; +} from "@openagentpack/sdk/project-versions"; import { afterEach, describe, expect, test } from "vite-plus/test"; import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts"; From 902516bbad75ad53449043d18e338cb5678954a5 Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Mon, 7 Sep 2026 20:46:07 +0800 Subject: [PATCH 7/8] fix(managed-agent): check latest Playground version before launch Resolve the requested npm version before reusing local builds or running processes, fetch mismatched versions on demand, and preserve explicit binary overrides. Add launcher regression tests and sync the SDK 0.7.0 lockfile. --- .../_engine/playground-launcher.ts | 66 +++++++-- .../managed-agent-playground-launcher.test.ts | 137 ++++++++++++++++++ pnpm-lock.yaml | 10 +- 3 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 packages/commands/tests/managed-agent-playground-launcher.test.ts 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 69c83a29e..f9e3b060a 100644 --- a/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts +++ b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts @@ -1,12 +1,14 @@ -import { spawn, type ChildProcess } from "node:child_process"; +import { execFile, 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 { promisify } from "node:util"; import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core"; import { emitBare } from "bailian-cli-runtime"; const PLAYGROUND_PACKAGE = "@openagentpack/playground"; +const execFileAsync = promisify(execFile); const DEFAULT_PORT = 4848; const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i; @@ -56,7 +58,7 @@ export async function launchManagedAgentPlayground( options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"), ); const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16); - const launcher = resolveLauncher(); + const launcher = await resolveLauncher(); const existing = await probeExistingPlayground(port); if (existing) { const reusable = @@ -147,7 +149,7 @@ function assertSupportedNodeVersion(): void { ); } -function resolveLauncher(): Launcher { +export async function resolveLauncher(): Promise { const explicit = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() || process.env.AGENTS_PLAYGROUND_BIN?.trim(); @@ -161,23 +163,69 @@ function resolveLauncher(): Launcher { return { command: process.execPath, args: [explicit], fetched: false }; } + const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest"; + const { stdout } = await execFileAsync( + "npm", + [ + "view", + `${PLAYGROUND_PACKAGE}@${requestedVersion}`, + "version", + "--json", + "--prefer-online", + "--fetch-retries=0", + "--fetch-timeout=10000", + ], + { timeout: 15_000, maxBuffer: 1024 * 1024 }, + ); + let resolvedVersion: unknown; + try { + resolvedVersion = JSON.parse(stdout); + } catch { + resolvedVersion = undefined; + } + if ( + typeof resolvedVersion !== "string" || + !/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(resolvedVersion) + ) { + throw new BailianError( + "npm did not return a single valid Playground version. Specify an exact version or dist-tag. / npm 未返回唯一有效的 Playground 版本号。请指定精确版本或 dist-tag。", + ExitCode.GENERAL, + ); + } + const installed = resolveInstalledPlayground(); - if (installed) return installed; + if (installed?.version === resolvedVersion) return installed; const monorepoBinary = findLocalPlaygroundBin(process.cwd()); - if (monorepoBinary) { - return { command: process.execPath, args: [monorepoBinary], fetched: false }; + if ( + monorepoBinary && + readPlaygroundVersion(resolve(dirname(monorepoBinary), "../../package.json")) === + resolvedVersion + ) { + return { + command: process.execPath, + args: [monorepoBinary], + version: resolvedVersion, + 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, + args: ["-y", `${PLAYGROUND_PACKAGE}@${resolvedVersion}`], + version: resolvedVersion, fetched: true, }; } +function readPlaygroundVersion(manifestPath: string): string | undefined { + try { + return (JSON.parse(readFileSync(manifestPath, "utf8")) as { version?: string }).version; + } catch { + return undefined; + } +} + function resolveInstalledPlayground(): Launcher | undefined { try { const require = createRequire(import.meta.url); diff --git a/packages/commands/tests/managed-agent-playground-launcher.test.ts b/packages/commands/tests/managed-agent-playground-launcher.test.ts new file mode 100644 index 000000000..e3d62cca5 --- /dev/null +++ b/packages/commands/tests/managed-agent-playground-launcher.test.ts @@ -0,0 +1,137 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; + +const registry = vi.hoisted(() => ({ + version: '"0.7.0"', + error: null as Error | null, + manifest: "", + query: vi.fn(), +})); + +vi.mock("node:child_process", async (importOriginal) => ({ + ...(await importOriginal()), + execFile: ( + command: string, + args: string[], + options: unknown, + callback: (error: Error | null, output: { stdout: string }) => void, + ) => { + registry.query(command, args, options); + callback(registry.error, { stdout: registry.version }); + }, +})); + +vi.mock("node:module", () => ({ + createRequire: () => ({ resolve: () => registry.manifest }), +})); + +import { resolveLauncher } from "../src/commands/managed-agent/_engine/playground-launcher.ts"; + +let directory: string; +let binary: string; + +beforeEach(async () => { + directory = await mkdtemp(join(tmpdir(), "playground-launcher-")); + binary = join(directory, "playground.js"); + registry.manifest = join(directory, "package.json"); + registry.version = '"0.7.0"'; + registry.error = null; + registry.query.mockClear(); + await writeFile(binary, ""); + vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", ""); + vi.stubEnv("AGENTS_PLAYGROUND_BIN", ""); + vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", ""); + vi.spyOn(process, "cwd").mockReturnValue(directory); +}); + +afterEach(async () => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + await rm(directory, { recursive: true, force: true }); +}); + +async function install(version: string) { + await writeFile(registry.manifest, JSON.stringify({ version, bin: "playground.js" })); +} + +test("checks latest on every resolution and reuses only the matching installed version", async () => { + await install("0.7.0"); + expect(await resolveLauncher()).toMatchObject({ + args: [binary], + version: "0.7.0", + fetched: false, + }); + registry.version = '"0.8.0"'; + expect(await resolveLauncher()).toMatchObject({ + command: "npx", + args: ["-y", "@openagentpack/playground@0.8.0"], + version: "0.8.0", + fetched: true, + }); + expect(registry.query).toHaveBeenCalledTimes(2); + expect(registry.query).toHaveBeenCalledWith( + "npm", + expect.arrayContaining([ + "view", + "@openagentpack/playground@latest", + "--prefer-online", + "--fetch-timeout=10000", + ]), + expect.objectContaining({ timeout: 15_000 }), + ); +}); + +test("downloads the resolved exact version when local installation is old or missing", async () => { + for (const version of ["0.6.0", undefined]) { + if (version) await install(version); + else await rm(registry.manifest); + expect(await resolveLauncher()).toMatchObject({ + command: "npx", + args: ["-y", "@openagentpack/playground@0.7.0"], + }); + } +}); + +test("explicit version overrides an incompatible installed version", async () => { + await install("0.6.0"); + vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", "0.7.0"); + expect(await resolveLauncher()).toMatchObject({ fetched: true, version: "0.7.0" }); + expect(registry.query.mock.calls[0]?.[1]).toContain("@openagentpack/playground@0.7.0"); +}); + +test("explicit binary remains an offline development override", async () => { + vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", binary); + expect(await resolveLauncher()).toMatchObject({ args: [binary], fetched: false }); + expect(registry.query).not.toHaveBeenCalled(); +}); + +test("registry failure does not silently reuse an old installation", async () => { + await install("0.6.0"); + registry.error = new Error("registry unavailable"); + await expect(resolveLauncher()).rejects.toThrow("registry unavailable"); +}); + +test("rejects malformed or ambiguous registry metadata with localized diagnostics", async () => { + for (const output of ["invalid", '["0.6.0","0.7.0"]', '"--unsafe"']) { + registry.version = output; + await expect(resolveLauncher()).rejects.toThrow("single valid Playground version"); + await expect(resolveLauncher()).rejects.toThrow("唯一有效的 Playground 版本号"); + } +}); + +test("local source builds must also match the resolved version", async () => { + const packageRoot = join(directory, "packages/playground"); + const sourceBinary = join(packageRoot, "dist/bin/playground.js"); + await mkdir(join(packageRoot, "dist/bin"), { recursive: true }); + await writeFile(sourceBinary, ""); + await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.6.0" })); + expect(await resolveLauncher()).toMatchObject({ fetched: true }); + await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.7.0" })); + expect(await resolveLauncher()).toMatchObject({ + args: [sourceBinary], + version: "0.7.0", + fetched: false, + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27333d153..5c4626f39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,8 +189,8 @@ importers: packages/commands: dependencies: '@openagentpack/sdk': - specifier: 0.5.0 - version: 0.5.0 + specifier: 0.7.0 + version: 0.7.0 bailian-cli-core: specifier: workspace:* version: link:../core @@ -1124,8 +1124,8 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@openagentpack/sdk@0.5.0': - resolution: {integrity: sha512-9mNMvPWuoiK5NQVFN5i2qAhUAFSmw9jO8xrMDXXfa6eSu0XlEAPZzR6DDg4HdhW+ex60KgA+fcNA0h0LRF552A==} + '@openagentpack/sdk@0.7.0': + resolution: {integrity: sha512-MjyKIFvPi4aT1TIcvGUB0bnD4Tly9aMCncjI9+EYG6CPWpuGO4DGj+4zlfBCx1fvtD1G1w4Zi0m8A8ZodqkK9Q==} engines: {node: '>=18.17.0'} '@oxc-project/runtime@0.129.0': @@ -4287,7 +4287,7 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@openagentpack/sdk@0.5.0': + '@openagentpack/sdk@0.7.0': dependencies: jszip: 3.10.1 yaml: 2.9.0 From efb832815a4e60bb8cdf91046b983dca109b9970 Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Mon, 7 Sep 2026 21:11:51 +0800 Subject: [PATCH 8/8] chore(release): prepare 1.21.0 --- CHANGELOG.md | 13 +++++++++++++ CHANGELOG.zh.md | 13 +++++++++++++ packages/cli/package.json | 2 +- packages/commands/package.json | 2 +- packages/core/package.json | 2 +- packages/kscli/package.json | 2 +- packages/runtime/package.json | 2 +- skills/bailian-cli/SKILL.md | 2 +- skills/bailian-finetune/SKILL.md | 2 +- skills/bailian-gen/SKILL.md | 2 +- skills/bailian-managed-agent/SKILL.md | 2 +- skills/bailian-protocol/SKILL.md | 2 +- skills/bailian-web-search/SKILL.md | 2 +- 13 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 881232187..e4ac357a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [1.21.0] - 2026-09-07 + +### Added + +- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples. +- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git. +- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser. + +### Changed + +- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides. +- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports. + ## [1.20.0] - 2026-09-03 > Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations. diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index dcfbba248..4e08c3eb0 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,19 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [1.21.0] - 2026-09-07 + +### 新增 + +- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。 +- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。 +- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。 + +### 变更 + +- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。 +- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。 + ## [1.20.0] - 2026-09-03 > Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。 diff --git a/packages/cli/package.json b/packages/cli/package.json index 5c8ee4d5a..aa42cacfd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "1.20.0", + "version": "1.21.0", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/commands/package.json b/packages/commands/package.json index 440be5f7f..040d3b666 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "1.20.0", + "version": "1.21.0", "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/core/package.json b/packages/core/package.json index 6c3db390c..3b17cafd8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "1.20.0", + "version": "1.21.0", "description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/kscli/package.json b/packages/kscli/package.json index fc8dad481..84fbdb05e 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "1.20.0", + "version": "1.21.0", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index e86a528c8..759628878 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "1.20.0", + "version": "1.21.0", "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 7efd2e9ef..57fa84e71 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index d9336e856..d64581206 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 29dea9d59..7addc20ec 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 3df081f3e..9b437fe9f 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 8e1bb5a70..0da22e46f 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 0030e3d3f..89d09a23c 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-web-search metadata: - version: "1.20.0" + version: "1.21.0" requires: bins: ["bl"] description: >-