mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
refactor(managed-agent)!: replace Git versioning with local snapshots
Replace @openagentpack/local-git with @openagentpack/project-versions so the CLI and Workbench share a Git-independent, lock-protected YAML snapshot store. - create versions only after a successful Apply - migrate version commands to local version IDs - remove Git project scaffolding and CI-specific Apply policy - update tests and managed-agent skill references BREAKING CHANGE: remove `managed-agent init --git` and `managed-agent apply --ci`; version preview and restore now use `--version-id` instead of `--commit`.
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openagentpack/local-git": "0.4.0",
|
||||
"@openagentpack/project-versions": "0.4.0",
|
||||
"@openagentpack/sdk": "0.4.0",
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"bailian-cli-runtime": "workspace:*",
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const REPOSITORY_GITIGNORE = `# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Bailian CLI local runs
|
||||
.openagentpack/state/
|
||||
.openagentpack/runs/
|
||||
|
||||
# Local credentials
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
`;
|
||||
|
||||
const REPOSITORY_GITIGNORE_PATTERNS = [
|
||||
"node_modules/",
|
||||
".openagentpack/state/",
|
||||
".openagentpack/runs/",
|
||||
".env",
|
||||
".env.*",
|
||||
"!.env.example",
|
||||
] as const;
|
||||
|
||||
const PROJECT_SCRIPTS = {
|
||||
"agents:validate": "bl managed-agent validate --file agents.yaml",
|
||||
"agents:plan": "bl managed-agent plan --file agents.yaml",
|
||||
"agents:plan:ci": "bl managed-agent plan --file agents.yaml --output json",
|
||||
"agents:apply:ci": "bl managed-agent apply --file agents.yaml --ci",
|
||||
"agents:workbench": "bl managed-agent workbench --file agents.yaml",
|
||||
} as const;
|
||||
|
||||
const INITIAL_STATE = `${JSON.stringify({ resources: [] }, null, 2)}\n`;
|
||||
|
||||
export interface GitProjectResult {
|
||||
targetDirectory: string;
|
||||
mode: "created" | "upgraded";
|
||||
initializedGit: boolean;
|
||||
createdFiles: string[];
|
||||
updatedFiles: string[];
|
||||
preservedFiles: string[];
|
||||
}
|
||||
|
||||
interface CreateGitProjectOptions {
|
||||
config: string;
|
||||
cliVersion: string;
|
||||
}
|
||||
|
||||
type ProjectTargetMode = "new" | "existing";
|
||||
|
||||
export async function inspectGitProjectTarget(targetDirectory: string): Promise<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);
|
||||
}
|
||||
@@ -6,12 +6,7 @@ import {
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
executePlannedProject,
|
||||
planProjectContext,
|
||||
type PlannedAction,
|
||||
UserError,
|
||||
} from "@openagentpack/sdk";
|
||||
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import {
|
||||
assertProviderConfigured,
|
||||
@@ -22,11 +17,12 @@ import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
import {
|
||||
commitAutomaticVersion,
|
||||
type PreparedAutomaticVersion,
|
||||
prepareAutomaticVersion,
|
||||
readVersionSource,
|
||||
} from "@openagentpack/local-git";
|
||||
commitPreparedProjectVersion,
|
||||
type PreparedProjectVersion,
|
||||
prepareProjectVersion,
|
||||
readProjectVersionSource,
|
||||
releasePreparedProjectVersion,
|
||||
} from "@openagentpack/project-versions";
|
||||
|
||||
const APPLY_FLAGS = {
|
||||
file: {
|
||||
@@ -52,13 +48,6 @@ const APPLY_FLAGS = {
|
||||
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
|
||||
},
|
||||
},
|
||||
ci: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Run non-interactively while blocking deletes and remote drift",
|
||||
"zh-CN": "以非交互模式运行,并阻止删除和远端漂移覆盖",
|
||||
},
|
||||
},
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -90,17 +79,10 @@ export default defineCommand({
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"[--file <path>] [--provider <name>] [--yes | --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]",
|
||||
"[--file <path>] [--provider <name>] [--yes] [--no-refresh] [--refresh-only] [--concurrency <n>]",
|
||||
flags: APPLY_FLAGS,
|
||||
exampleArgs: ["--yes", "--provider bailian --yes", "--ci"],
|
||||
exampleArgs: ["--yes", "--provider bailian --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate(flags) {
|
||||
if (flags.ci && flags.yes) return "--ci cannot be combined with --yes.";
|
||||
if (flags.ci && flags.noRefresh) {
|
||||
return "--ci requires remote state refresh and cannot be combined with --no-refresh.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -113,7 +95,6 @@ export default defineCommand({
|
||||
provider: flags.provider ?? "all",
|
||||
refresh: !flags.noRefresh,
|
||||
concurrency: flags.concurrency,
|
||||
ci: flags.ci,
|
||||
refresh_only: flags.refreshOnly,
|
||||
},
|
||||
config_file: file,
|
||||
@@ -124,7 +105,7 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const versionSource = await readVersionSource(file);
|
||||
const versionSource = await readProjectVersionSource(file);
|
||||
|
||||
const { planned, runtime } = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
@@ -157,7 +138,7 @@ export default defineCommand({
|
||||
const actionable = plan.actions.filter((action) => action.action !== "no-op");
|
||||
if (actionable.length === 0) {
|
||||
if (!flags.refreshOnly) {
|
||||
const preparedVersion = await prepareAutomaticVersion(
|
||||
const preparedVersion = await prepareProjectVersion(
|
||||
runtime.configPath,
|
||||
versionSource.source,
|
||||
);
|
||||
@@ -172,8 +153,6 @@ export default defineCommand({
|
||||
const creates = actionable.filter((action) => action.action === "create").length;
|
||||
const updates = actionable.filter((action) => action.action === "update").length;
|
||||
const deletes = planned.destructiveActions;
|
||||
if (flags.ci) assertCiApplyPolicy(actionable);
|
||||
|
||||
for (const action of actionable) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
@@ -197,7 +176,7 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flags.yes && !flags.ci) {
|
||||
if (!flags.yes) {
|
||||
throw new BailianError(
|
||||
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
|
||||
ExitCode.USAGE,
|
||||
@@ -205,62 +184,50 @@ export default defineCommand({
|
||||
);
|
||||
}
|
||||
|
||||
const preparedVersion = await prepareAutomaticVersion(runtime.configPath, versionSource.source);
|
||||
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executePlannedProject(planned, {
|
||||
onFeedback: renderAgentFeedback,
|
||||
policy: "force",
|
||||
concurrency: flags.concurrency,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const succeeded = result.results.filter((entry) => entry.status === "success").length;
|
||||
const failed = result.results.filter((entry) => entry.status === "failed").length;
|
||||
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ succeeded, failed, skipped, results: result.results }, format);
|
||||
} else {
|
||||
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
if (failed > 0 || skipped > 0) {
|
||||
throw new BailianError(
|
||||
failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.",
|
||||
ExitCode.GENERAL,
|
||||
const preparedVersion = await prepareProjectVersion(runtime.configPath, versionSource.source);
|
||||
let versionCommitted = false;
|
||||
try {
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executePlannedProject(planned, {
|
||||
onFeedback: renderAgentFeedback,
|
||||
policy: "force",
|
||||
concurrency: flags.concurrency,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const succeeded = result.results.filter((entry) => entry.status === "success").length;
|
||||
const failed = result.results.filter((entry) => entry.status === "failed").length;
|
||||
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ succeeded, failed, skipped, results: result.results }, format);
|
||||
} else {
|
||||
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
if (failed > 0 || skipped > 0) {
|
||||
throw new BailianError(
|
||||
failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
await commitSuccessfulApplyVersion(preparedVersion, format);
|
||||
versionCommitted = true;
|
||||
} finally {
|
||||
if (!versionCommitted) await releasePreparedProjectVersion(preparedVersion);
|
||||
}
|
||||
await commitSuccessfulApplyVersion(preparedVersion, format);
|
||||
},
|
||||
});
|
||||
|
||||
export function assertCiApplyPolicy(actions: PlannedAction[]): void {
|
||||
const deletes = actions.filter((action) => action.action === "delete");
|
||||
if (deletes.length > 0) {
|
||||
throw new UserError(
|
||||
`CI policy blocked ${deletes.length} delete action(s). Review the plan and apply this destructive change through an explicitly approved workflow.`,
|
||||
);
|
||||
}
|
||||
const drifted = actions.filter(
|
||||
(action) => action.driftKind === "remote" || action.driftKind === "both",
|
||||
);
|
||||
if (drifted.length > 0) {
|
||||
throw new UserError(
|
||||
`CI policy blocked ${drifted.length} action(s) with remote drift. Review the remote changes before deciding whether YAML should overwrite them.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function commitSuccessfulApplyVersion(
|
||||
prepared: PreparedAutomaticVersion | null,
|
||||
prepared: PreparedProjectVersion | null,
|
||||
format: "text" | "json",
|
||||
): Promise<void> {
|
||||
if (!prepared) return;
|
||||
const version = await commitAutomaticVersion(prepared);
|
||||
const version = await commitPreparedProjectVersion(prepared);
|
||||
if (version && format !== "json") {
|
||||
emitBare(`Created local version ${version.short_commit} (${version.message}).`);
|
||||
emitBare(`Created local version ${version.short_version} (${version.message}).`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { createGitProject, inspectGitProjectTarget } from "./_engine/git-project.ts";
|
||||
|
||||
const GITIGNORE_ADDITIONS = `
|
||||
# agents
|
||||
agents.state.json
|
||||
.openagentpack/versions/
|
||||
.env
|
||||
`;
|
||||
|
||||
@@ -101,14 +101,6 @@ const INIT_FLAGS = {
|
||||
"zh-CN": "输出配置路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
git: {
|
||||
type: "string",
|
||||
valueHint: "<directory>",
|
||||
description: {
|
||||
"en-US": "Create or add CI/Git scaffolding in this project directory",
|
||||
"zh-CN": "在此项目目录中创建或补充 CI/Git 脚手架",
|
||||
},
|
||||
},
|
||||
force: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Overwrite an existing config file", "zh-CN": "覆盖已有配置文件" },
|
||||
@@ -117,19 +109,13 @@ const INIT_FLAGS = {
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Create an agents.yaml template or a local CI/Git project",
|
||||
"zh-CN": "创建 agents.yaml 模板或本地 CI/Git 项目",
|
||||
"en-US": "Create an agents.yaml template",
|
||||
"zh-CN": "创建 agents.yaml 模板",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs:
|
||||
"[--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]",
|
||||
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
|
||||
flags: INIT_FLAGS,
|
||||
exampleArgs: ["", "--provider bailian --agent-name assistant", "--git ./my-agents", "--git ."],
|
||||
validate(flags) {
|
||||
if (flags.git && flags.file) return "--git cannot be combined with --file.";
|
||||
if (flags.git && flags.force) return "--git cannot be combined with --force.";
|
||||
return undefined;
|
||||
},
|
||||
exampleArgs: ["", "--provider bailian --agent-name assistant"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
@@ -137,45 +123,6 @@ export default defineCommand({
|
||||
const agentName = flags.agentName ?? "assistant";
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
|
||||
if (flags.git) {
|
||||
const targetMode = await inspectGitProjectTarget(flags.git);
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_initialize_git_project: flags.git,
|
||||
mode: targetMode === "new" ? "create" : "upgrade",
|
||||
provider,
|
||||
agent: agentName,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const template = buildTemplate({ provider, agentName });
|
||||
const result = await createGitProject(flags.git, {
|
||||
config: template,
|
||||
cliVersion: ctx.identity.version,
|
||||
});
|
||||
if (format === "json") {
|
||||
emitResult(result, format);
|
||||
} else {
|
||||
const action =
|
||||
result.mode === "created" ? "Created CI/Git project" : "Added CI/Git scaffolding";
|
||||
emitBare(`${action} at ${result.targetDirectory}`);
|
||||
if (result.createdFiles.length > 0) {
|
||||
emitBare(`Created: ${result.createdFiles.join(", ")}`);
|
||||
}
|
||||
if (result.updatedFiles.length > 0) {
|
||||
emitBare(`Updated: ${result.updatedFiles.join(", ")}`);
|
||||
}
|
||||
if (result.preservedFiles.length > 0) {
|
||||
emitBare(`Preserved: ${result.preservedFiles.join(", ")}`);
|
||||
}
|
||||
emitBare("Next: add credentials to .env, install dependencies, and open the Workbench.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (existsSync(file) && !flags.force) {
|
||||
throw new BailianError(
|
||||
`${file} already exists.`,
|
||||
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
import { confirmDangerousAction, emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
disableLocalVersioning,
|
||||
enableLocalVersioning,
|
||||
getLocalVersionStatus,
|
||||
type LocalProjectVersion,
|
||||
type LocalVersionPreview,
|
||||
type LocalVersionStatus,
|
||||
listLocalVersions,
|
||||
previewLocalVersion,
|
||||
restoreLocalVersion,
|
||||
} from "@openagentpack/local-git";
|
||||
disableProjectVersioning,
|
||||
enableProjectVersioning,
|
||||
getProjectVersionStatus,
|
||||
type ProjectVersion,
|
||||
type ProjectVersionPreview,
|
||||
type ProjectVersionStatus,
|
||||
listProjectVersions,
|
||||
previewProjectVersion,
|
||||
restoreProjectVersion,
|
||||
} from "@openagentpack/project-versions";
|
||||
|
||||
const FILE_FLAG = {
|
||||
file: {
|
||||
@@ -30,22 +30,22 @@ const FILE_FLAG = {
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const COMMIT_FLAG = {
|
||||
commit: {
|
||||
const VERSION_FLAG = {
|
||||
versionId: {
|
||||
type: "string",
|
||||
valueHint: "<full-sha>",
|
||||
valueHint: "<full-version>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Full commit SHA from the current branch",
|
||||
"zh-CN": "当前分支中的完整 Commit SHA",
|
||||
"en-US": "Full local version ID",
|
||||
"zh-CN": "完整的本地版本 ID",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const managedAgentVersionEnable = defineCommand({
|
||||
description: {
|
||||
"en-US": "Enable Apply-time Git versioning for agents.yaml",
|
||||
"zh-CN": "为 agents.yaml 启用 Apply 后自动 Git 版本管理",
|
||||
"en-US": "Enable Apply-time local snapshots for agents.yaml",
|
||||
"zh-CN": "为 agents.yaml 启用 Apply 后自动本地快照",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--file <path>]",
|
||||
@@ -55,28 +55,30 @@ export const managedAgentVersionEnable = defineCommand({
|
||||
const file = ctx.flags.file ?? "agents.yaml";
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_enable: file, git: await getLocalVersionStatus(file) }, format);
|
||||
emitResult({ would_enable: file, versioning: await getProjectVersionStatus(file) }, format);
|
||||
return;
|
||||
}
|
||||
const result = await enableLocalVersioning(file, "Enable Bailian CLI versioning");
|
||||
const result = await enableProjectVersioning(file, "Enable Bailian CLI versioning");
|
||||
if (format === "json") {
|
||||
emitResult(result, format);
|
||||
return;
|
||||
}
|
||||
if (result.version) {
|
||||
emitBare(`Created baseline version ${result.version.short_commit} ${result.version.message}`);
|
||||
emitBare(
|
||||
`Created baseline version ${result.version.short_version} ${result.version.message}`,
|
||||
);
|
||||
} else {
|
||||
emitBare("Current agents.yaml is already versioned; no commit was created.");
|
||||
emitBare("Current agents.yaml is already versioned; no snapshot was created.");
|
||||
}
|
||||
emitBare("Automatic versioning is enabled for this agents.yaml.");
|
||||
renderStatus(result.git);
|
||||
renderStatus(result.versioning);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentVersionDisable = defineCommand({
|
||||
description: {
|
||||
"en-US": "Disable Apply-time Git versioning without removing history",
|
||||
"zh-CN": "关闭 Apply 后自动 Git 版本管理,但保留历史",
|
||||
"en-US": "Disable Apply-time local snapshots without removing history",
|
||||
"zh-CN": "关闭 Apply 后自动本地快照,但保留历史",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--file <path>]",
|
||||
@@ -86,10 +88,10 @@ export const managedAgentVersionDisable = defineCommand({
|
||||
const file = ctx.flags.file ?? "agents.yaml";
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_disable: file, git: await getLocalVersionStatus(file) }, format);
|
||||
emitResult({ would_disable: file, versioning: await getProjectVersionStatus(file) }, format);
|
||||
return;
|
||||
}
|
||||
const status = await disableLocalVersioning(file);
|
||||
const status = await disableProjectVersioning(file);
|
||||
if (format === "json") {
|
||||
emitResult(status, format);
|
||||
return;
|
||||
@@ -101,15 +103,15 @@ export const managedAgentVersionDisable = defineCommand({
|
||||
|
||||
export const managedAgentVersionStatus = defineCommand({
|
||||
description: {
|
||||
"en-US": "Show local Git versioning status for agents.yaml",
|
||||
"zh-CN": "显示 agents.yaml 的本地 Git 版本管理状态",
|
||||
"en-US": "Show local snapshot versioning status for agents.yaml",
|
||||
"zh-CN": "显示 agents.yaml 的本地快照版本管理状态",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--file <path>]",
|
||||
flags: FILE_FLAG,
|
||||
exampleArgs: ["", "--file agents.yaml --output json"],
|
||||
async run(ctx) {
|
||||
const status = await getLocalVersionStatus(ctx.flags.file ?? "agents.yaml");
|
||||
const status = await getProjectVersionStatus(ctx.flags.file ?? "agents.yaml");
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === "json") emitResult(status, format);
|
||||
else renderStatus(status);
|
||||
@@ -138,15 +140,15 @@ const LIST_FLAGS = {
|
||||
|
||||
export const managedAgentVersionList = defineCommand({
|
||||
description: {
|
||||
"en-US": "List current-branch commits that changed agents.yaml",
|
||||
"zh-CN": "列出当前分支中修改过 agents.yaml 的 Commit",
|
||||
"en-US": "List local snapshots of agents.yaml",
|
||||
"zh-CN": "列出 agents.yaml 的本地快照",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--file <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", {
|
||||
const page = await listProjectVersions(ctx.flags.file ?? "agents.yaml", {
|
||||
limit: ctx.flags.limit,
|
||||
cursor: ctx.flags.cursor,
|
||||
});
|
||||
@@ -156,7 +158,7 @@ export const managedAgentVersionList = defineCommand({
|
||||
return;
|
||||
}
|
||||
if (page.versions.length === 0) {
|
||||
emitBare("No versions of agents.yaml exist on the current branch.");
|
||||
emitBare("No local versions of agents.yaml exist.");
|
||||
return;
|
||||
}
|
||||
for (const version of page.versions) emitBare(formatVersion(version));
|
||||
@@ -166,7 +168,7 @@ export const managedAgentVersionList = defineCommand({
|
||||
|
||||
const PREVIEW_FLAGS = {
|
||||
...FILE_FLAG,
|
||||
...COMMIT_FLAG,
|
||||
...VERSION_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const managedAgentVersionPreview = defineCommand({
|
||||
@@ -175,11 +177,14 @@ export const managedAgentVersionPreview = defineCommand({
|
||||
"zh-CN": "预览 agents.yaml 的历史版本",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "--commit <full-sha> [--file <path>]",
|
||||
usageArgs: "--version-id <full-version> [--file <path>]",
|
||||
flags: PREVIEW_FLAGS,
|
||||
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --output json"],
|
||||
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --output json"],
|
||||
async run(ctx) {
|
||||
const preview = await previewLocalVersion(ctx.flags.file ?? "agents.yaml", ctx.flags.commit);
|
||||
const preview = await previewProjectVersion(
|
||||
ctx.flags.file ?? "agents.yaml",
|
||||
ctx.flags.versionId,
|
||||
);
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === "json") emitResult(preview, format);
|
||||
else renderPreview(preview);
|
||||
@@ -203,12 +208,12 @@ export const managedAgentVersionRestore = defineCommand({
|
||||
"zh-CN": "将 agents.yaml 历史版本恢复到工作区",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "--commit <full-sha> [--file <path>] [--yes]",
|
||||
usageArgs: "--version-id <full-version> [--file <path>] [--yes]",
|
||||
flags: RESTORE_FLAGS,
|
||||
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --yes --output json"],
|
||||
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes --output json"],
|
||||
async run(ctx) {
|
||||
const file = ctx.flags.file ?? "agents.yaml";
|
||||
const preview = await previewLocalVersion(file, ctx.flags.commit);
|
||||
const preview = await previewProjectVersion(file, ctx.flags.versionId);
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format !== "json") renderPreview(preview);
|
||||
if (!preview.can_restore) {
|
||||
@@ -220,49 +225,47 @@ export const managedAgentVersionRestore = defineCommand({
|
||||
);
|
||||
}
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_restore: ctx.flags.commit, preview }, format);
|
||||
emitResult({ would_restore: ctx.flags.versionId, preview }, format);
|
||||
return;
|
||||
}
|
||||
await confirmDangerousAction(
|
||||
"Restore this version to the agents.yaml working tree? HEAD and agents.state.json will not change.",
|
||||
"Restore this version to the agents.yaml working tree? Version history and agents.state.json will not change.",
|
||||
ctx.flags.yes,
|
||||
);
|
||||
const restored = await restoreLocalVersion(file, ctx.flags.commit, {
|
||||
head: preview.base_head,
|
||||
const restored = await restoreProjectVersion(file, ctx.flags.versionId, {
|
||||
headVersion: preview.base_head_version,
|
||||
sourceRevision: preview.base_source_revision,
|
||||
});
|
||||
if (format === "json") {
|
||||
emitResult({ restored: ctx.flags.commit, preview: restored }, format);
|
||||
emitResult({ restored: ctx.flags.versionId, preview: restored }, format);
|
||||
} else {
|
||||
emitBare(
|
||||
`Restored ${ctx.flags.commit.slice(0, 12)} to the working tree. HEAD was not changed.`,
|
||||
`Restored ${ctx.flags.versionId.slice(0, 12)} to the working tree. Version history was not changed.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function renderStatus(status: LocalVersionStatus): void {
|
||||
emitBare(`Git available: ${status.git_available ? "yes" : "no"}`);
|
||||
function renderStatus(status: ProjectVersionStatus): void {
|
||||
emitBare(`Automatic versioning: ${status.enabled ? "enabled" : "disabled"}`);
|
||||
emitBare(`Repository: ${status.repository_root ?? "none"}`);
|
||||
emitBare(`Config path: ${status.config_path ?? "none"}`);
|
||||
emitBare(`Branch: ${status.branch ?? "none"}`);
|
||||
emitBare(`HEAD: ${status.head ?? "none"}`);
|
||||
emitBare(`Version store: ${status.initialized ? status.store_root : "not initialized"}`);
|
||||
emitBare(`Config path: ${status.config_path}`);
|
||||
emitBare(`Current version: ${status.head_version ?? "none"}`);
|
||||
emitBare(
|
||||
`agents.yaml: ${status.config_status}${status.config_versioned ? ", versioned" : ", unversioned"}`,
|
||||
`agents.yaml: ${status.source_status}${status.source_versioned ? ", versioned" : ", unversioned"}`,
|
||||
);
|
||||
const blockers = [...new Set([...status.commit_blockers, ...status.restore_blockers])];
|
||||
const blockers = [...new Set([...status.write_blockers, ...status.restore_blockers])];
|
||||
for (const blocker of blockers) emitBare(chalk.yellow(`Blocker: ${blocker}`));
|
||||
}
|
||||
|
||||
function formatVersion(version: LocalProjectVersion): string {
|
||||
return `${chalk.yellow(version.short_commit)} ${version.authored_at} ${version.message} ${chalk.dim(`(${version.author_name})`)}`;
|
||||
function formatVersion(version: ProjectVersion): string {
|
||||
return `${chalk.yellow(version.short_version)} ${version.created_at} ${version.message} ${chalk.dim(`(${version.created_by})`)}`;
|
||||
}
|
||||
|
||||
function renderPreview(preview: LocalVersionPreview): void {
|
||||
emitBare(chalk.bold(`Version ${preview.commit}`));
|
||||
function renderPreview(preview: ProjectVersionPreview): void {
|
||||
emitBare(chalk.bold(`Version ${preview.version_id}`));
|
||||
emitBare(chalk.red("--- working tree"));
|
||||
emitBare(chalk.green(`+++ ${preview.commit}`));
|
||||
emitBare(chalk.green(`+++ ${preview.version_id}`));
|
||||
for (const line of buildLineDiff(preview.before_yaml, preview.after_yaml)) {
|
||||
if (line.kind === "deletion") emitBare(chalk.red(`-${line.text}`));
|
||||
else if (line.kind === "addition") emitBare(chalk.green(`+${line.text}`));
|
||||
|
||||
@@ -45,9 +45,9 @@ const WORKBENCH_NOTES = [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.",
|
||||
"Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.",
|
||||
"zh-CN":
|
||||
"Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;不会推送 Git Commit 或切换分支。",
|
||||
"Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;本地版本使用共享的 .openagentpack/versions 项目版本存储,不依赖 Git。",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -130,6 +130,17 @@ describe("e2e: managed-agent", () => {
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--file|--provider|--yes/i);
|
||||
expect(stderr).not.toContain("--ci");
|
||||
});
|
||||
|
||||
test("managed-agent init 不再暴露 Git 仓库脚手架", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"init",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).not.toContain("--git");
|
||||
});
|
||||
|
||||
test("managed-agent version 暴露共享版本管理子命令", async () => {
|
||||
@@ -142,7 +153,7 @@ describe("e2e: managed-agent", () => {
|
||||
expect(stderr).toMatch(/enable|disable|status|list|preview|restore/i);
|
||||
});
|
||||
|
||||
test("managed-agent version preview 缺少 --commit 时退出为用法错误 (2)", async () => {
|
||||
test("managed-agent version preview 缺少 --version-id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"version",
|
||||
@@ -150,7 +161,7 @@ describe("e2e: managed-agent", () => {
|
||||
"--quiet",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--commit|Missing required/i);
|
||||
expect(stderr).toMatch(/--version-id|Missing required/i);
|
||||
});
|
||||
|
||||
test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
|
||||
@@ -239,29 +250,6 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () =>
|
||||
expect(data.provider).toBe("bailian");
|
||||
});
|
||||
|
||||
test("init --git --dry-run 仅输出仓库脚手架计划", async () => {
|
||||
const targetDirectory = join(
|
||||
process.cwd(),
|
||||
`.managed-agent-git-dry-run-${process.pid}-${Date.now()}`,
|
||||
);
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"init",
|
||||
"--git",
|
||||
targetDirectory,
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
would_initialize_git_project?: string;
|
||||
mode?: string;
|
||||
}>(stdout);
|
||||
expect(data.would_initialize_git_project).toBe(targetDirectory);
|
||||
expect(data.mode).toBe("create");
|
||||
});
|
||||
|
||||
test("workbench --dry-run 仅输出启动计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { createGitProject } from "../src/commands/managed-agent/_engine/git-project.ts";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("managed-agent init --git project scaffolding", () => {
|
||||
test("creates a main-branch Git project without committing or configuring a remote", async () => {
|
||||
const parentDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-project-"));
|
||||
temporaryDirectories.push(parentDirectory);
|
||||
const targetDirectory = join(parentDirectory, "agent-project");
|
||||
|
||||
const result = await createGitProject(targetDirectory, {
|
||||
config: projectYaml(),
|
||||
cliVersion: "1.17.1",
|
||||
});
|
||||
|
||||
expect(result.mode).toBe("created");
|
||||
expect(result.initializedGit).toBe(true);
|
||||
expect(result.createdFiles).toContain("agents.yaml");
|
||||
expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toContain("assistant:");
|
||||
expect(await readFile(join(targetDirectory, ".aoneci/bailian-cli.yml"), "utf8")).toContain(
|
||||
"agents:apply:ci",
|
||||
);
|
||||
expect(await readFile(join(targetDirectory, "README.md"), "utf8")).toContain(
|
||||
"Create the remote repository yourself",
|
||||
);
|
||||
});
|
||||
|
||||
test("upgrades an initialized config directory without overwriting agents.yaml", async () => {
|
||||
const targetDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-upgrade-"));
|
||||
temporaryDirectories.push(targetDirectory);
|
||||
const originalSource = projectYaml().replace("assistant", "reviewer");
|
||||
await writeFile(join(targetDirectory, "agents.yaml"), originalSource);
|
||||
|
||||
const result = await createGitProject(targetDirectory, {
|
||||
config: projectYaml(),
|
||||
cliVersion: "1.17.1",
|
||||
});
|
||||
|
||||
expect(result.mode).toBe("upgraded");
|
||||
expect(result.preservedFiles).toContain("agents.yaml");
|
||||
expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toBe(originalSource);
|
||||
});
|
||||
});
|
||||
|
||||
function projectYaml(): string {
|
||||
return `version: "1"
|
||||
providers:
|
||||
bailian:
|
||||
api_key: \${DASHSCOPE_API_KEY}
|
||||
defaults:
|
||||
provider: bailian
|
||||
agents:
|
||||
assistant:
|
||||
model: qwen3.8-max
|
||||
instructions: You are helpful.
|
||||
`;
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { PlannedAction } from "@openagentpack/sdk";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import {
|
||||
commitAutomaticVersion,
|
||||
disableLocalVersioning,
|
||||
enableLocalVersioning,
|
||||
getLocalVersionStatus,
|
||||
prepareAutomaticVersion,
|
||||
previewLocalVersion,
|
||||
restoreLocalVersion,
|
||||
} from "@openagentpack/local-git";
|
||||
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
|
||||
import { assertCiApplyPolicy } from "../src/commands/managed-agent/apply.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const temporaryDirectories: string[] = [];
|
||||
const gitIdentity = {
|
||||
GIT_AUTHOR_NAME: "Bailian CLI Test",
|
||||
GIT_AUTHOR_EMAIL: "bailian-cli@example.com",
|
||||
GIT_COMMITTER_NAME: "Bailian CLI Test",
|
||||
GIT_COMMITTER_EMAIL: "bailian-cli@example.com",
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("managed-agent local Git versions", () => {
|
||||
test("uses the shared path-scoped switch and commits only agents.yaml", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
const nestedDirectory = join(root, "nested");
|
||||
const nestedConfigPath = join(nestedDirectory, "agents.yaml");
|
||||
await mkdir(nestedDirectory);
|
||||
await writeFile(configPath, projectYaml("First"));
|
||||
await writeFile(nestedConfigPath, projectYaml("Second"));
|
||||
await git(root, ["init", "--initial-branch", "main"]);
|
||||
await writeFile(join(root, "staged.txt"), "staged\n");
|
||||
await git(root, ["add", "staged.txt"]);
|
||||
const stagedBefore = await git(root, ["status", "--porcelain=v1", "--", "staged.txt"]);
|
||||
|
||||
const enabled = await withGitIdentity(() =>
|
||||
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
);
|
||||
|
||||
expect(enabled.git.enabled).toBe(true);
|
||||
expect((await getLocalVersionStatus(nestedConfigPath)).enabled).toBe(false);
|
||||
expect((await git(root, ["show", "--pretty=", "--name-only", "HEAD"])).trim()).toBe(
|
||||
"agents.yaml",
|
||||
);
|
||||
expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore);
|
||||
expect(
|
||||
await git(root, ["rev-parse", "--git-path", "openagentpack/local-git/versions"]),
|
||||
).toContain("openagentpack/local-git/versions");
|
||||
|
||||
await writeFile(configPath, projectYaml("First updated"));
|
||||
const repeated = await withGitIdentity(() =>
|
||||
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
);
|
||||
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
|
||||
expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore);
|
||||
|
||||
const disabled = await disableLocalVersioning(configPath);
|
||||
expect(disabled.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test("auto-commits after success and restores without moving HEAD or changing permissions", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Version one"));
|
||||
await chmod(configPath, 0o640);
|
||||
const enabled = await withGitIdentity(() =>
|
||||
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
);
|
||||
const firstCommit = enabled.version!.commit;
|
||||
const secondSource = projectYaml("Version two");
|
||||
await writeFile(configPath, secondSource);
|
||||
|
||||
const prepared = await withGitIdentity(() => prepareAutomaticVersion(configPath, secondSource));
|
||||
const version = await withGitIdentity(() => commitAutomaticVersion(prepared!));
|
||||
const headBeforeRestore = (await git(root, ["rev-parse", "HEAD"])).trim();
|
||||
expect(version?.message).toBe("Apply agents.yaml");
|
||||
|
||||
const preview = await previewLocalVersion(configPath, firstCommit);
|
||||
expect(preview.can_restore).toBe(true);
|
||||
expect(preview.after_yaml).toContain("Version one");
|
||||
await restoreLocalVersion(configPath, firstCommit, {
|
||||
head: preview.base_head,
|
||||
sourceRevision: preview.base_source_revision,
|
||||
});
|
||||
|
||||
expect(await readFile(configPath, "utf8")).toContain("Version one");
|
||||
expect((await git(root, ["rev-parse", "HEAD"])).trim()).toBe(headBeforeRestore);
|
||||
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
|
||||
});
|
||||
|
||||
test("rejects short SHAs and plaintext credentials", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Safe"));
|
||||
const enabled = await withGitIdentity(() =>
|
||||
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
);
|
||||
await expect(previewLocalVersion(configPath, enabled.version!.short_commit)).rejects.toThrow(
|
||||
/full hexadecimal commit SHA/i,
|
||||
);
|
||||
|
||||
await disableLocalVersioning(configPath);
|
||||
await writeFile(
|
||||
configPath,
|
||||
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
|
||||
);
|
||||
await expect(
|
||||
withGitIdentity(() => enableLocalVersioning(configPath, "Enable Bailian CLI versioning")),
|
||||
).rejects.toThrow(/environment variable reference/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed-agent CI and Workbench policies", () => {
|
||||
test("CI blocks delete actions and remote drift", () => {
|
||||
expect(() => assertCiApplyPolicy([plannedAction("delete")])).toThrow(/blocked.*delete/i);
|
||||
expect(() => assertCiApplyPolicy([plannedAction("update", "remote")])).toThrow(
|
||||
/blocked.*remote drift/i,
|
||||
);
|
||||
expect(() => assertCiApplyPolicy([plannedAction("update", "local")])).not.toThrow();
|
||||
});
|
||||
|
||||
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
|
||||
const summary = {
|
||||
status: "valid",
|
||||
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
|
||||
};
|
||||
expect(
|
||||
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
|
||||
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
|
||||
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
|
||||
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function temporaryDirectory(): Promise<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
commitPreparedProjectVersion,
|
||||
disableProjectVersioning,
|
||||
enableProjectVersioning,
|
||||
getProjectVersionStatus,
|
||||
listProjectVersions,
|
||||
prepareProjectVersion,
|
||||
previewProjectVersion,
|
||||
restoreProjectVersion,
|
||||
} from "@openagentpack/project-versions";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("managed-agent local snapshot versions", () => {
|
||||
test("uses the shared path-scoped switch and stores full YAML outside store.json", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
const siblingDirectory = join(root, "nested");
|
||||
const siblingConfigPath = join(siblingDirectory, "agents.yaml");
|
||||
await mkdir(siblingDirectory);
|
||||
await writeFile(configPath, projectYaml("First"));
|
||||
await writeFile(siblingConfigPath, projectYaml("Second"));
|
||||
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
|
||||
expect(enabled.versioning.enabled).toBe(true);
|
||||
expect(enabled.version?.message).toBe("Enable Bailian CLI versioning");
|
||||
expect((await getProjectVersionStatus(siblingConfigPath)).enabled).toBe(false);
|
||||
const storeSource = await readFile(join(root, ".openagentpack/versions/store.json"), "utf8");
|
||||
expect(storeSource).not.toContain("instructions: First");
|
||||
const snapshotSource = await readFile(
|
||||
join(root, ".openagentpack/versions/blobs", `${enabled.version!.source_hash}.yaml`),
|
||||
"utf8",
|
||||
);
|
||||
expect(snapshotSource).toContain("instructions: First");
|
||||
|
||||
await writeFile(configPath, projectYaml("First updated"));
|
||||
const repeated = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
|
||||
expect((await listProjectVersions(configPath)).versions).toHaveLength(2);
|
||||
|
||||
const disabled = await disableProjectVersioning(configPath);
|
||||
expect(disabled.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test("auto-snapshots after success and restores without changing history or permissions", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Version one"));
|
||||
await chmod(configPath, 0o640);
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
const firstVersion = enabled.version!.version_id;
|
||||
const secondSource = projectYaml("Version two");
|
||||
await writeFile(configPath, secondSource);
|
||||
|
||||
const prepared = await prepareProjectVersion(configPath, secondSource);
|
||||
const version = await commitPreparedProjectVersion(prepared!);
|
||||
const currentVersionBeforeRestore = (await getProjectVersionStatus(configPath)).head_version;
|
||||
expect(version?.message).toBe("Apply agents.yaml");
|
||||
|
||||
const preview = await previewProjectVersion(configPath, firstVersion);
|
||||
expect(preview.can_restore).toBe(true);
|
||||
expect(preview.after_yaml).toContain("Version one");
|
||||
await restoreProjectVersion(configPath, firstVersion, {
|
||||
headVersion: preview.base_head_version,
|
||||
sourceRevision: preview.base_source_revision,
|
||||
});
|
||||
|
||||
expect(await readFile(configPath, "utf8")).toContain("Version one");
|
||||
expect((await getProjectVersionStatus(configPath)).head_version).toBe(
|
||||
currentVersionBeforeRestore,
|
||||
);
|
||||
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
|
||||
});
|
||||
|
||||
test("rejects abbreviated version IDs and plaintext credentials", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Safe"));
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
await expect(previewProjectVersion(configPath, enabled.version!.short_version)).rejects.toThrow(
|
||||
/full 64-character hexadecimal/i,
|
||||
);
|
||||
|
||||
await disableProjectVersioning(configPath);
|
||||
await writeFile(
|
||||
configPath,
|
||||
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
|
||||
);
|
||||
await expect(
|
||||
enableProjectVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
).rejects.toThrow(/environment variable reference/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed-agent Workbench policy", () => {
|
||||
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
|
||||
const summary = {
|
||||
status: "valid",
|
||||
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
|
||||
};
|
||||
expect(
|
||||
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
|
||||
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
|
||||
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
|
||||
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function temporaryDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-versions-"));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
function projectYaml(instructions: string): string {
|
||||
return `version: "1"
|
||||
providers:
|
||||
qoder: {}
|
||||
defaults:
|
||||
provider: qoder
|
||||
agents:
|
||||
assistant:
|
||||
model: ultimate
|
||||
instructions: ${instructions}
|
||||
`;
|
||||
}
|
||||
@@ -6,8 +6,8 @@ metadata:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
阿里云百炼托管 Agent 声明式基础设施入口:用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、
|
||||
创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地 Git 版本、
|
||||
生成 CI 仓库、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
|
||||
创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地快照版本、
|
||||
和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
|
||||
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、
|
||||
apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。
|
||||
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
|
||||
@@ -21,12 +21,11 @@ description: >-
|
||||
|
||||
## Safety guardrail (the most important rule)
|
||||
|
||||
`apply` / `destroy` **mutate remote resources**. Interactive execution requires `--yes`; `apply --ci` is only for an already approved CI workflow:
|
||||
`apply` / `destroy` **mutate remote resources**. Execution requires `--yes`:
|
||||
|
||||
1. Always run `bl managed-agent plan` first and show the diff to the user.
|
||||
2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`.
|
||||
3. Never add `--yes` on your own initiative before the user has confirmed.
|
||||
4. Never use `--ci` to bypass user confirmation in an interactive task. CI mode blocks deletes and remote drift, but still mutates remote resources.
|
||||
|
||||
## IaC lifecycle
|
||||
|
||||
@@ -38,22 +37,21 @@ description: >-
|
||||
5. Destroy bl managed-agent destroy --yes # only after user confirmation
|
||||
```
|
||||
|
||||
## Workbench, local versions, and CI
|
||||
## Workbench and local versions
|
||||
|
||||
| Intent | Command |
|
||||
| ------------------------------------------- | ---------------------------------------------- |
|
||||
| Launch project resource editing | `bl managed-agent workbench` |
|
||||
| Launch one Agent Session Preview | `bl managed-agent playground --agent <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` |
|
||||
| Intent | Command |
|
||||
| ---------------------------------------- | ---------------------------------------------- |
|
||||
| Launch project resource editing | `bl managed-agent workbench` |
|
||||
| Launch one Agent Session Preview | `bl managed-agent playground --agent <id>` |
|
||||
| Enable/disable shared automatic versions | `bl managed-agent version enable` / `disable` |
|
||||
| Inspect local version state and history | `bl managed-agent version status` / `list` |
|
||||
| Preview or restore a historical YAML | `bl managed-agent version preview` / `restore` |
|
||||
|
||||
- Bailian CLI and Workbench use the same repository-local switch for the same Git worktree and `agents.yaml` path. The switch lives in private Git metadata and is not cloned or pushed.
|
||||
- When enabled, a fully successful Apply creates a local commit containing only `agents.yaml`. Failed, partial, cancelled, and `--refresh-only` Apply runs do not commit.
|
||||
- `version restore` writes the historical YAML to the working tree. It does not move `HEAD`, restore `agents.state.json`, create a commit, or Apply remote changes.
|
||||
- Bailian CLI and Workbench use the same `.openagentpack/versions` store and enable switch for the same `agents.yaml`. Git is not required.
|
||||
- `store.json` contains only the switch and head metadata. Immutable linked entries live under `entries/`, while complete YAML is stored as content-addressed blobs under `blobs/`. Neither `agents.state.json` nor referenced files are versioned.
|
||||
- When enabled, a fully successful Apply creates a local snapshot only when `agents.yaml` changed. Failed, partial, cancelled, and `--refresh-only` Apply runs do not create one.
|
||||
- `version restore` writes the historical YAML to the working tree. It does not move version history, restore `agents.state.json`, create a new snapshot, or Apply remote changes.
|
||||
- Workbench can edit local drafts while Apply is running, but saving/version mutations are blocked until Apply completes. External file edits are detected through revision checks.
|
||||
- `init --git` never creates a remote repository or pushes. The generated Aone CI uses `apply --ci`, which blocks deletes and remote drift; review destructive changes in a separately approved workflow.
|
||||
|
||||
## Deployment as IaC
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| ---------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ |
|
||||
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
|
||||
@@ -29,12 +29,12 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench | [managed-agent.md](managed-agent.md) |
|
||||
|
||||
## By group
|
||||
|
||||
@@ -11,7 +11,7 @@ Index: [index.md](index.md)
|
||||
| ---------------------------------- | -------------- | ------------------------------------------------------------- |
|
||||
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources |
|
||||
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template |
|
||||
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure |
|
||||
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent |
|
||||
| `bl managed-agent session create` | API Key | Create a new session for an agent |
|
||||
@@ -27,24 +27,24 @@ Index: [index.md](index.md)
|
||||
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely |
|
||||
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state |
|
||||
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) |
|
||||
| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history |
|
||||
| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml |
|
||||
| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml |
|
||||
| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history |
|
||||
| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml |
|
||||
| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml |
|
||||
| `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version |
|
||||
| `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree |
|
||||
| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml |
|
||||
| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml |
|
||||
| `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl managed-agent apply`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent apply` |
|
||||
| **Description** | Apply planned changes to create/update/delete agent resources |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes \| --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `managed-agent apply` |
|
||||
| **Description** | Apply planned changes to create/update/delete agent resources |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--no-refresh] [--refresh-only] [--concurrency <n>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -53,7 +53,6 @@ Index: [index.md](index.md)
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider (default: all configured) |
|
||||
| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) |
|
||||
| `--ci` | switch | no | Run non-interactively while blocking deletes and remote drift |
|
||||
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
|
||||
| `--refresh-only` | switch | no | Refresh state without mutating remote resources |
|
||||
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
|
||||
@@ -76,10 +75,6 @@ bl managed-agent apply --yes
|
||||
bl managed-agent apply --provider bailian --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent apply --ci
|
||||
```
|
||||
|
||||
### `bl managed-agent destroy`
|
||||
|
||||
| Field | Value |
|
||||
@@ -117,12 +112,12 @@ bl managed-agent destroy --yes --cascade
|
||||
|
||||
### `bl managed-agent init`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent init` |
|
||||
| **Description** | Create an agents.yaml template or a local CI/Git project |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent init` |
|
||||
| **Description** | Create an agents.yaml template |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--force]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -131,7 +126,6 @@ bl managed-agent destroy --yes --cascade
|
||||
| `--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) |
|
||||
| `--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 |
|
||||
|
||||
#### Examples
|
||||
@@ -144,14 +138,6 @@ bl managed-agent init
|
||||
bl managed-agent init --provider bailian --agent-name assistant
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent init --git ./my-agents
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent init --git .
|
||||
```
|
||||
|
||||
### `bl managed-agent plan`
|
||||
|
||||
| Field | Value |
|
||||
@@ -218,7 +204,7 @@ bl managed-agent plan --no-refresh
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.
|
||||
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -681,12 +667,12 @@ bl managed-agent validate --file agents.yaml
|
||||
|
||||
### `bl managed-agent version disable`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `managed-agent version disable` |
|
||||
| **Description** | Disable Apply-time Git versioning without removing history |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version disable [--file <path>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| **Name** | `managed-agent version disable` |
|
||||
| **Description** | Disable Apply-time local snapshots without removing history |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version disable [--file <path>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -709,7 +695,7 @@ bl managed-agent version disable --file agents.yaml
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| **Name** | `managed-agent version enable` |
|
||||
| **Description** | Enable Apply-time Git versioning for agents.yaml |
|
||||
| **Description** | Enable Apply-time local snapshots for agents.yaml |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version enable [--file <path>]` |
|
||||
|
||||
@@ -734,7 +720,7 @@ bl managed-agent version enable --file agents.yaml
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent version list` |
|
||||
| **Description** | List current-branch commits that changed agents.yaml |
|
||||
| **Description** | List local snapshots of agents.yaml |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version list [--file <path>] [--limit <n>] [--cursor <cursor>]` |
|
||||
|
||||
@@ -758,65 +744,65 @@ bl managed-agent version list --limit 20 --output json
|
||||
|
||||
### `bl managed-agent version preview`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent version preview` |
|
||||
| **Description** | Preview a historical agents.yaml version |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version preview --commit <full-sha> [--file <path>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------ |
|
||||
| **Name** | `managed-agent version preview` |
|
||||
| **Description** | Preview a historical agents.yaml version |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version preview --version-id <full-version> [--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 |
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------------- | ------ | -------- | --------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--version-id <full-version>` | string | yes | Full local version ID |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent version preview --commit <full-sha>
|
||||
bl managed-agent version preview --version-id <full-version>
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent version preview --commit <full-sha> --output json
|
||||
bl managed-agent version preview --version-id <full-version> --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]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent version restore` |
|
||||
| **Description** | Restore a historical agents.yaml version to the working tree |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version restore --version-id <full-version> [--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 |
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------------- | ------ | -------- | ------------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--version-id <full-version>` | string | yes | Full local version ID |
|
||||
| `--yes` | switch | no | Restore without an interactive confirmation |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent version restore --commit <full-sha>
|
||||
bl managed-agent version restore --version-id <full-version>
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent version restore --commit <full-sha> --yes --output json
|
||||
bl managed-agent version restore --version-id <full-version> --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>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| **Name** | `managed-agent version status` |
|
||||
| **Description** | Show local snapshot versioning status for agents.yaml |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent version status [--file <path>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -858,7 +844,7 @@ bl managed-agent version status --file agents.yaml --output json
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.
|
||||
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
Reference in New Issue
Block a user