mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b93e0d586d |
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、
|
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、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 链路;
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user