diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 27422935b..764015ab2 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -139,6 +139,18 @@ import { managedAgentPlan, managedAgentApply, managedAgentDestroy, + managedAgentPlayground, + managedAgentProjectInit, + managedAgentProjectValidate, + managedAgentProjectBuild, + managedAgentProjectPublish, + managedAgentProjectWorkbench, + managedAgentProjectVersionEnable, + managedAgentProjectVersionDisable, + managedAgentProjectVersionStatus, + managedAgentProjectVersionList, + managedAgentProjectVersionPreview, + managedAgentProjectVersionRestore, managedAgentStateList, managedAgentStateShow, managedAgentStateRm, @@ -300,6 +312,18 @@ export const commands: Record = { "managed-agent plan": managedAgentPlan, "managed-agent apply": managedAgentApply, "managed-agent destroy": managedAgentDestroy, + "managed-agent playground": managedAgentPlayground, + "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 a1943e1e5..f784ec233 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/project-workspace": "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/playground-launcher.ts b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts new file mode 100644 index 000000000..69c83a29e --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts @@ -0,0 +1,384 @@ +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; + project?: 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 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) { + 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, sourcePath); + 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, + sourcePath: string, +): NodeJS.ProcessEnv { + const credential = options.client.exportApiCredential(); + const environment: NodeJS.ProcessEnv = { + ...process.env, + PORT: String(port), + 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") + ? 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..3f81d026f 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -48,6 +48,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,7 +71,8 @@ export default defineCommand({ "zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源", }, auth: "apiKey", - usageArgs: "[--file ] [--provider ] [--yes] [--concurrency ]", + usageArgs: + "[--file ] [--provider ] [--yes] [--no-refresh] [--refresh-only] [--concurrency ]", flags: APPLY_FLAGS, exampleArgs: ["--yes", "--provider bailian --yes"], notes: CREDENTIALS_NOTE, @@ -80,6 +88,7 @@ export default defineCommand({ provider: flags.provider ?? "all", refresh: !flags.noRefresh, concurrency: flags.concurrency, + refresh_only: flags.refreshOnly, }, config_file: file, hint: "Run `managed-agent plan` to preview the exact resource changes.", @@ -93,12 +102,13 @@ export default defineCommand({ 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; }), ); @@ -127,12 +137,29 @@ 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; - for (const action of actionable) { const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; emitProgress(` ${icon} ${formatResourceLabel(action.address)}`); } + 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) { throw new BailianError( `Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`, @@ -161,6 +188,11 @@ 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, + ); + } }, }); diff --git a/packages/commands/src/commands/managed-agent/init.ts b/packages/commands/src/commands/managed-agent/init.ts index ac3c83caa..980598416 100644 --- a/packages/commands/src/commands/managed-agent/init.ts +++ b/packages/commands/src/commands/managed-agent/init.ts @@ -12,6 +12,7 @@ import { emitBare, emitResult } from "bailian-cli-runtime"; const GITIGNORE_ADDITIONS = ` # agents agents.state.json +.openagentpack/versions/ .env `; @@ -108,13 +109,13 @@ 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", + "zh-CN": "创建 agents.yaml 模板", }, auth: "none", usageArgs: "[--provider ] [--agent-name ] [--file ] [--force]", flags: INIT_FLAGS, - exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"], + exampleArgs: ["", "--provider bailian --agent-name assistant"], async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); 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/workbench.ts b/packages/commands/src/commands/managed-agent/workbench.ts new file mode 100644 index 000000000..7c3ed0007 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/workbench.ts @@ -0,0 +1,90 @@ +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 PLAYGROUND_BASE_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 = { + ...PLAYGROUND_BASE_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 PLAYGROUND_NOTES = [ + ...CREDENTIALS_NOTE, + { + "en-US": + "Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.", + "zh-CN": + "会话预览需要 Node.js 22+,并继续使用 agents.yaml;目录 Workbench 位于 managed-agent project workbench。", + }, +]; + +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: PLAYGROUND_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..074928f38 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -136,6 +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 { + 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 fc6b05881..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 @@ -130,6 +139,102 @@ 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 project 暴露目录项目与共享版本管理子命令", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + 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 project version preview 缺少 --version-id 时退出为用法错误 (2)", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "project", + "version", + "preview", + "--quiet", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--version-id|Missing required/i); }); test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { @@ -218,6 +323,29 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(data.provider).toBe("bailian"); }); + 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", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + would_launch?: string; + project_root?: string; + port?: number; + }>(stdout); + expect(data.would_launch).toBe("workbench"); + expect(data.project_root).toBe("./agent-project"); + expect(data.port).toBe(4848); + }); + 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..af7ba2b8a 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -186,6 +186,18 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "managed-agent plan": "managedAgentPlan", "managed-agent apply": "managedAgentApply", "managed-agent destroy": "managedAgentDestroy", + "managed-agent playground": "managedAgentPlayground", + "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/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 7277707b3..a91a2ef08 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 本地快照版本、 + 和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 `bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、 apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。 反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、 @@ -20,7 +21,7 @@ description: >- ## Safety guardrail (the most important rule) -`apply` / `destroy` **mutate remote resources** and only execute when `--yes` is passed: +`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`. @@ -36,6 +37,25 @@ description: >- 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` +## 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 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..d0604bd54 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,31 +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 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 | [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`, `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`, `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 e3975b7e5..d06f9c42b 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,36 +7,48 @@ 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 | +| `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 ### `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] [--no-refresh] [--refresh-only] [--concurrency ]` | #### Flags @@ -46,6 +58,7 @@ Index: [index.md](index.md) | `--provider ` | string | no | Target provider (default: all configured) | | `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | | `--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 | @@ -106,7 +119,7 @@ bl managed-agent destroy --yes --cascade | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------- | | **Name** | `managed-agent init` | -| **Description** | Create a new agents.yaml template | +| **Description** | Create an agents.yaml template | | **Authentication** | No Auth | | **Usage** | `bl managed-agent init [--provider ] [--agent-name ] [--file ] [--force]` | @@ -129,10 +142,6 @@ bl managed-agent init bl managed-agent init --provider bailian --agent-name assistant ``` -```bash -bl managed-agent init --provider all -``` - ### `bl managed-agent plan` | Field | Value | @@ -174,6 +183,359 @@ 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. +- Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench. + +#### 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 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 |