Compare commits

...

1 Commits

Author SHA1 Message Date
chenanran555 b93e0d586d 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.
2026-08-25 20:41:47 +08:00
16 changed files with 2237 additions and 68 deletions
+16
View File
@@ -139,6 +139,14 @@ import {
managedAgentPlan, managedAgentPlan,
managedAgentApply, managedAgentApply,
managedAgentDestroy, managedAgentDestroy,
managedAgentWorkbench,
managedAgentPlayground,
managedAgentVersionEnable,
managedAgentVersionDisable,
managedAgentVersionStatus,
managedAgentVersionList,
managedAgentVersionPreview,
managedAgentVersionRestore,
managedAgentStateList, managedAgentStateList,
managedAgentStateShow, managedAgentStateShow,
managedAgentStateRm, managedAgentStateRm,
@@ -300,6 +308,14 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent plan": managedAgentPlan, "managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply, "managed-agent apply": managedAgentApply,
"managed-agent destroy": managedAgentDestroy, "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 list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow, "managed-agent state show": managedAgentStateShow,
"managed-agent state rm": managedAgentStateRm, "managed-agent state rm": managedAgentStateRm,
+2 -1
View File
@@ -40,7 +40,8 @@
"check": "vp check" "check": "vp check"
}, },
"dependencies": { "dependencies": {
"@openagentpack/sdk": "0.3.2", "@openagentpack/local-git": "0.4.0",
"@openagentpack/sdk": "0.4.0",
"bailian-cli-core": "workspace:*", "bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*", "bailian-cli-runtime": "workspace:*",
"boxen": "catalog:", "boxen": "catalog:",
@@ -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<ProjectTargetMode> {
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<GitProjectResult> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string>();
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<string>();
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 <your-codeup-repository-url>
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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -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<void> {
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 <id> 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<string, string>;
};
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<number | null> {
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<void>((resolveWait) => setTimeout(resolveWait, 300));
}
return null;
}
async function probeExistingPlayground(port: number): Promise<ExistingPlayground | null> {
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<boolean> {
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<void>((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<void> {
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<PlaygroundBrowserTarget> {
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<number> {
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);
});
}
@@ -6,7 +6,12 @@ import {
type FlagsDef, type FlagsDef,
} from "bailian-cli-core"; } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime"; 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 { formatResourceLabel } from "./_engine/address-utils.ts";
import { import {
assertProviderConfigured, assertProviderConfigured,
@@ -16,6 +21,12 @@ import {
import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts"; import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts"; import { renderAgentFeedback } from "./_engine/feedback.ts";
import {
commitAutomaticVersion,
type PreparedAutomaticVersion,
prepareAutomaticVersion,
readVersionSource,
} from "@openagentpack/local-git";
const APPLY_FLAGS = { const APPLY_FLAGS = {
file: { file: {
@@ -41,6 +52,13 @@ const APPLY_FLAGS = {
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)", "zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
}, },
}, },
ci: {
type: "switch",
description: {
"en-US": "Run non-interactively while blocking deletes and remote drift",
"zh-CN": "以非交互模式运行,并阻止删除和远端漂移覆盖",
},
},
noRefresh: { noRefresh: {
type: "switch", type: "switch",
description: { description: {
@@ -48,6 +66,13 @@ const APPLY_FLAGS = {
"zh-CN": "规划前跳过从远端刷新状态", "zh-CN": "规划前跳过从远端刷新状态",
}, },
}, },
refreshOnly: {
type: "switch",
description: {
"en-US": "Refresh state without mutating remote resources",
"zh-CN": "仅刷新 State不修改远端资源",
},
},
concurrency: { concurrency: {
type: "number", type: "number",
valueHint: "<n>", valueHint: "<n>",
@@ -64,10 +89,18 @@ export default defineCommand({
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源", "zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
}, },
auth: "apiKey", auth: "apiKey",
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]", usageArgs:
"[--file <path>] [--provider <name>] [--yes | --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]",
flags: APPLY_FLAGS, flags: APPLY_FLAGS,
exampleArgs: ["--yes", "--provider bailian --yes"], exampleArgs: ["--yes", "--provider bailian --yes", "--ci"],
notes: CREDENTIALS_NOTE, 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) { async run(ctx) {
const { settings, flags } = ctx; const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output); const format = detectOutputFormat(settings.output);
@@ -80,6 +113,8 @@ export default defineCommand({
provider: flags.provider ?? "all", provider: flags.provider ?? "all",
refresh: !flags.noRefresh, refresh: !flags.noRefresh,
concurrency: flags.concurrency, concurrency: flags.concurrency,
ci: flags.ci,
refresh_only: flags.refreshOnly,
}, },
config_file: file, config_file: file,
hint: "Run `managed-agent plan` to preview the exact resource changes.", hint: "Run `managed-agent plan` to preview the exact resource changes.",
@@ -89,16 +124,19 @@ export default defineCommand({
return; return;
} }
const planned = await withAgentErrors(() => const versionSource = await readVersionSource(file);
const { planned, runtime } = await withAgentErrors(() =>
withStdoutProtected(async () => { withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file); const runtime = await buildAgentRuntime(ctx, file);
assertProviderConfigured(runtime, flags.provider); assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, { const planned = await planProjectContext(runtime, {
provider: flags.provider, provider: flags.provider,
refresh: !flags.noRefresh, refresh: !flags.noRefresh,
quiet: true, quiet: true,
onFeedback: renderAgentFeedback, onFeedback: renderAgentFeedback,
}); });
return { planned, runtime };
}), }),
); );
@@ -118,6 +156,13 @@ export default defineCommand({
const actionable = plan.actions.filter((action) => action.action !== "no-op"); const actionable = plan.actions.filter((action) => action.action !== "no-op");
if (actionable.length === 0) { if (actionable.length === 0) {
if (!flags.refreshOnly) {
const preparedVersion = await prepareAutomaticVersion(
runtime.configPath,
versionSource.source,
);
await commitSuccessfulApplyVersion(preparedVersion, format);
}
if (format === "json") if (format === "json")
emitResult({ succeeded: 0, failed: 0, skipped: 0, results: [] }, format); emitResult({ succeeded: 0, failed: 0, skipped: 0, results: [] }, format);
else emitBare("No changes. Infrastructure is up-to-date."); 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 creates = actionable.filter((action) => action.action === "create").length;
const updates = actionable.filter((action) => action.action === "update").length; const updates = actionable.filter((action) => action.action === "update").length;
const deletes = planned.destructiveActions; const deletes = planned.destructiveActions;
if (flags.ci) assertCiApplyPolicy(actionable);
for (const action of actionable) { for (const action of actionable) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`); 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( throw new BailianError(
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`, `Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
ExitCode.USAGE, ExitCode.USAGE,
@@ -141,6 +205,8 @@ export default defineCommand({
); );
} }
const preparedVersion = await prepareAutomaticVersion(runtime.configPath, versionSource.source);
const result = await withAgentErrors(() => const result = await withAgentErrors(() =>
withStdoutProtected(() => withStdoutProtected(() =>
executePlannedProject(planned, { executePlannedProject(planned, {
@@ -161,6 +227,40 @@ export default defineCommand({
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`); 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<void> {
if (!prepared) return;
const version = await commitAutomaticVersion(prepared);
if (version && format !== "json") {
emitBare(`Created local version ${version.short_commit} (${version.message}).`);
}
}
@@ -8,6 +8,7 @@ import {
type FlagsDef, type FlagsDef,
} from "bailian-cli-core"; } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime"; import { emitBare, emitResult } from "bailian-cli-runtime";
import { createGitProject, inspectGitProjectTarget } from "./_engine/git-project.ts";
const GITIGNORE_ADDITIONS = ` const GITIGNORE_ADDITIONS = `
# agents # agents
@@ -100,6 +101,14 @@ const INIT_FLAGS = {
"zh-CN": "输出配置路径默认agents.yaml", "zh-CN": "输出配置路径默认agents.yaml",
}, },
}, },
git: {
type: "string",
valueHint: "<directory>",
description: {
"en-US": "Create or add CI/Git scaffolding in this project directory",
"zh-CN": "在此项目目录中创建或补充 CI/Git 脚手架",
},
},
force: { force: {
type: "switch", type: "switch",
description: { "en-US": "Overwrite an existing config file", "zh-CN": "覆盖已有配置文件" }, description: { "en-US": "Overwrite an existing config file", "zh-CN": "覆盖已有配置文件" },
@@ -108,13 +117,19 @@ const INIT_FLAGS = {
export default defineCommand({ export default defineCommand({
description: { description: {
"en-US": "Create a new agents.yaml template", "en-US": "Create an agents.yaml template or a local CI/Git project",
"zh-CN": "创建新的 agents.yaml 模板", "zh-CN": "创建 agents.yaml 模板或本地 CI/Git 项目",
}, },
auth: "none", auth: "none",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]", usageArgs:
"[--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]",
flags: INIT_FLAGS, 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) { async run(ctx) {
const { settings, flags } = ctx; const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output); const format = detectOutputFormat(settings.output);
@@ -122,6 +137,45 @@ export default defineCommand({
const agentName = flags.agentName ?? "assistant"; const agentName = flags.agentName ?? "assistant";
const file = flags.file ?? "agents.yaml"; 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) { if (existsSync(file) && !flags.force) {
throw new BailianError( throw new BailianError(
`${file} already exists.`, `${file} already exists.`,
@@ -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: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
} satisfies FlagsDef;
const COMMIT_FLAG = {
commit: {
type: "string",
valueHint: "<full-sha>",
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 <path>]",
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 <path>]",
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 <path>]",
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: "<n>",
description: {
"en-US": "Maximum versions to return (default: 50, max: 100)",
"zh-CN": "最多返回的版本数默认50最大100",
},
},
cursor: {
type: "string",
valueHint: "<cursor>",
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 <path>] [--limit <n>] [--cursor <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 <full-sha> [--file <path>]",
flags: PREVIEW_FLAGS,
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --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 <full-sha> [--file <path>] [--yes]",
flags: RESTORE_FLAGS,
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --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<number, number>([[1, 0]]);
const traces: Array<Map<number, number>> = [];
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<Map<number, number>>,
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;
}
@@ -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: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
port: {
type: "number",
valueHint: "<n>",
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: "<id>",
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 <path>] [--port <n>] [--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 <path>] [--agent <id>] [--port <n>] [--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,
});
},
});
+12
View File
@@ -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 managedAgentPlan } from "./commands/managed-agent/plan.ts";
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts"; export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.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 managedAgentStateList } from "./commands/managed-agent/state-list.ts";
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts"; export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts"; export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts";
@@ -132,6 +132,27 @@ describe("e2e: managed-agent", () => {
expect(stderr).toMatch(/--file|--provider|--yes/i); 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 () => { test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent", "managed-agent",
@@ -218,6 +239,47 @@ describe("e2e: managed-agent--dry-run 短路,不联网不写盘)", () =>
expect(data.provider).toBe("bailian"); 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 () => { test("apply --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent", "managed-agent",
@@ -186,6 +186,14 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent plan": "managedAgentPlan", "managed-agent plan": "managedAgentPlan",
"managed-agent apply": "managedAgentApply", "managed-agent apply": "managedAgentApply",
"managed-agent destroy": "managedAgentDestroy", "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 list": "managedAgentStateList",
"managed-agent state rm": "managedAgentStateRm", "managed-agent state rm": "managedAgentStateRm",
"managed-agent state import": "managedAgentStateImport", "managed-agent state import": "managedAgentStateImport",
@@ -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.
`;
}
@@ -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<string> {
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<string> {
const result = await execFileAsync("git", arguments_, {
cwd: workingDirectory,
encoding: "utf8",
env: { ...process.env, ...gitIdentity },
});
return result.stdout;
}
async function withGitIdentity<Result>(operation: () => Promise<Result>): Promise<Result> {
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;
}
}
}
+21 -2
View File
@@ -6,7 +6,8 @@ metadata:
bins: ["bl"] bins: ["bl"]
description: >- description: >-
阿里云百炼托管 Agent 声明式基础设施入口用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、 阿里云百炼托管 Agent 声明式基础设施入口用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、
创建/更新/销毁百炼托管 Agent 或 Deployment、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地 Git 版本、
生成 CI 仓库、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaCinit 建脚手架、validate 离线校验、plan 预览 diff、 `bl managed-agent`。以 agents.yaml 为唯一事实源做 IaCinit 建脚手架、validate 离线校验、plan 预览 diff、
apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。 apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、 反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
@@ -20,11 +21,12 @@ description: >-
## Safety guardrail (the most important rule) ## 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. 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`. 2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`.
3. Never add `--yes` on your own initiative before the user has confirmed. 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 ## IaC lifecycle
@@ -36,6 +38,23 @@ description: >-
5. Destroy bl managed-agent destroy --yes # only after user confirmation 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 <id>` |
| Create or upgrade a local Git/CI repository | `bl managed-agent init --git <directory>` |
| 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 as IaC
Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路; Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路;
+30 -22
View File
@@ -9,31 +9,39 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index ## Quick index
| Command | Authentication | Description | Detail | | 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 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 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 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 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 playground` | API Key | Launch a Session Preview for an agents.yaml 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 create` | API Key | Create a new session for an agent | [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 delete` | API Key | Delete 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 events` | API Key | List event history for 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 get` | API Key | Get details of a session | [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 list` | API Key | List sessions from the provider | [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 session run` | API Key | Create a session, send a message, 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 session send` | API Key | Send a message to an existing session and stream the response | [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 skill-list` | API Key | List skills from the provider's skill catalog | [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 import` | API Key | Import an existing remote resource into 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 list` | No Auth | List resources tracked in agents state | [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 state rm` | No Auth | Remove a resource from state without destroying it remotely | [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 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 ## By group
| Group | Commands | Reference | | 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) | | `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 ## Global flags
@@ -7,36 +7,44 @@ Index: [index.md](index.md)
## Commands in this group ## Commands in this group
| Command | Authentication | Description | | Command | Authentication | Description |
| --------------------------------- | -------------- | ------------------------------------------------------------- | | ---------------------------------- | -------------- | ------------------------------------------------------------- |
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | | `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 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 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 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 playground` | API Key | Launch a Session Preview for an agents.yaml Agent |
| `bl managed-agent session delete` | API Key | Delete a session | | `bl managed-agent session create` | API Key | Create a new session for an agent |
| `bl managed-agent session events` | API Key | List event history for a session | | `bl managed-agent session delete` | API Key | Delete a session |
| `bl managed-agent session get` | API Key | Get details of a session | | `bl managed-agent session events` | API Key | List event history for a session |
| `bl managed-agent session list` | API Key | List sessions from the provider | | `bl managed-agent session get` | API Key | Get details of a session |
| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | | `bl managed-agent session list` | API Key | List sessions from the provider |
| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | | `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response |
| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | | `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response |
| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | | `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog |
| `bl managed-agent state list` | No Auth | List resources tracked in agents state | | `bl managed-agent state import` | API Key | Import an existing remote resource into agents state |
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | | `bl managed-agent state list` | No Auth | List resources tracked in agents state |
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | | `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely |
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | | `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 ## Command details
### `bl managed-agent apply` ### `bl managed-agent apply`
| Field | Value | | Field | Value |
| ------------------ | ---------------------------------------------------------------------------------------- | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent apply` | | **Name** | `managed-agent apply` |
| **Description** | Apply planned changes to create/update/delete agent resources | | **Description** | Apply planned changes to create/update/delete agent resources |
| **Authentication** | API Key | | **Authentication** | API Key |
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--concurrency <n>]` | | **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes \| --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]` |
#### Flags #### Flags
@@ -45,7 +53,9 @@ Index: [index.md](index.md)
| `--file <path>` | string | no | Config file path (default: agents.yaml) | | `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--provider <name>` | string | no | Target provider (default: all configured) | | `--provider <name>` | string | no | Target provider (default: all configured) |
| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | | `--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 | | `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
| `--refresh-only` | switch | no | Refresh state without mutating remote resources |
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) | | `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
| `--api-key <key>` | string | no | API key | | `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL | | `--base-url <url>` | string | no | API base URL |
@@ -66,6 +76,10 @@ bl managed-agent apply --yes
bl managed-agent apply --provider bailian --yes bl managed-agent apply --provider bailian --yes
``` ```
```bash
bl managed-agent apply --ci
```
### `bl managed-agent destroy` ### `bl managed-agent destroy`
| Field | Value | | Field | Value |
@@ -103,12 +117,12 @@ bl managed-agent destroy --yes --cascade
### `bl managed-agent init` ### `bl managed-agent init`
| Field | Value | | Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------- | | ------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent init` | | **Name** | `managed-agent init` |
| **Description** | Create a new agents.yaml template | | **Description** | Create an agents.yaml template or a local CI/Git project |
| **Authentication** | No Auth | | **Authentication** | No Auth |
| **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--force]` | | **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]` |
#### Flags #### Flags
@@ -117,6 +131,7 @@ bl managed-agent destroy --yes --cascade
| `--provider <bailian\|claude\|qoder\|ark\|all>` | string | no | Provider: bailian, claude, qoder, ark, all (default: bailian) | | `--provider <bailian\|claude\|qoder\|ark\|all>` | string | no | Provider: bailian, claude, qoder, ark, all (default: bailian) |
| `--agent-name <name>` | string | no | Name of the first agent (default: assistant) | | `--agent-name <name>` | string | no | Name of the first agent (default: assistant) |
| `--file <path>` | string | no | Output config path (default: agents.yaml) | | `--file <path>` | string | no | Output config path (default: agents.yaml) |
| `--git <directory>` | string | no | Create or add CI/Git scaffolding in this project directory |
| `--force` | switch | no | Overwrite an existing config file | | `--force` | switch | no | Overwrite an existing config file |
#### Examples #### Examples
@@ -130,7 +145,11 @@ bl managed-agent init --provider bailian --agent-name assistant
``` ```
```bash ```bash
bl managed-agent init --provider all bl managed-agent init --git ./my-agents
```
```bash
bl managed-agent init --git .
``` ```
### `bl managed-agent plan` ### `bl managed-agent plan`
@@ -174,6 +193,47 @@ bl managed-agent plan --provider bailian
bl managed-agent plan --no-refresh 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 <path>] [--agent <id>] [--port <n>] [--no-open]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | --------------------------------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--port <n>` | number | no | Local port (default: 4848) |
| `--no-open` | switch | no | Do not open a browser automatically |
| `--agent <id>` | string | no | Agent to preview (required when the project declares multiple Agents) |
| `--api-key <key>` | string | no | API key |
| `--base-url <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` ### `bl managed-agent session create`
| Field | Value | | Field | Value |
@@ -618,3 +678,198 @@ bl managed-agent validate
```bash ```bash
bl managed-agent validate --file agents.yaml 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 <path>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | 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 <path>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | 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 <path>] [--limit <n>] [--cursor <cursor>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--limit <n>` | number | no | Maximum versions to return (default: 50, max: 100) |
| `--cursor <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 <full-sha> [--file <path>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--commit <full-sha>` | string | yes | Full commit SHA from the current branch |
#### Examples
```bash
bl managed-agent version preview --commit <full-sha>
```
```bash
bl managed-agent version preview --commit <full-sha> --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 <full-sha> [--file <path>] [--yes]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--commit <full-sha>` | 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 <full-sha>
```
```bash
bl managed-agent version restore --commit <full-sha> --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 <path>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | 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 <path>] [--port <n>] [--no-open]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--port <n>` | number | no | Local port (default: 4848) |
| `--no-open` | switch | no | Do not open a browser automatically |
| `--api-key <key>` | string | no | API key |
| `--base-url <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
```