Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)

## [1.22.0] - 2026-09-08

### Changed

- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.

### Fixed

- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.

### Internal

- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.

## [1.21.0] - 2026-09-07

### Added
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@

[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)

## [1.22.0] - 2026-09-08

### 变更

- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。

### 修复

- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。

### 内部

- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。

## [1.21.0] - 2026-09-07

### 新增
Expand Down
1 change: 1 addition & 0 deletions docs/agents/command-flag-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。

### B. 鉴权 / 全局选项

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.21.0",
"version": "1.22.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
Expand Down
4 changes: 2 additions & 2 deletions packages/commands/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.21.0",
"version": "1.22.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
Expand Down Expand Up @@ -40,7 +40,7 @@
"check": "vp check"
},
"dependencies": {
"@openagentpack/sdk": "0.7.0",
"@openagentpack/sdk": "0.7.1",
"bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*",
"boxen": "catalog:",
Expand Down
5 changes: 3 additions & 2 deletions packages/commands/src/commands/managed-agent/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
BailianError,
defineCommand,
Expand Down Expand Up @@ -87,7 +88,7 @@ export default defineCommand({

if (existsSync(file) && !flags.force) {
throw new BailianError(
`${file} already exists.`,
`${resolve(file)} already exists.`,
ExitCode.USAGE,
"Pass --force to overwrite.",
);
Expand Down Expand Up @@ -128,7 +129,7 @@ export default defineCommand({
if (format === "json") {
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
} else {
emitBare(`Created ${file}`);
emitBare(`Created ${resolve(file)}`);
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
Expand Down
62 changes: 50 additions & 12 deletions packages/commands/src/commands/managed-agent/project.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { stat } from "node:fs/promises";
import { join } from "node:path";
import {
BailianError,
defineCommand,
Expand All @@ -14,6 +16,7 @@ import {
planProjectPublish,
previewProjectBuild,
type ProjectBuildResolver,
resolveDirectoryProjectRoot,
validateDirectoryProject,
} from "@openagentpack/sdk/project-workspace";
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
Expand Down Expand Up @@ -44,9 +47,23 @@ export const managedAgentProjectInit = defineCommand({
},
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent"],
flags: {
project: {
...PROJECT_FLAG.project,
description: {
"en-US": "Directory project root (default: ./managed-agent under the current directory)",
"zh-CN": "目录项目根路径(默认:当前目录下的 ./managed-agent)",
},
},
},
exampleArgs: ["", "--project ./my-agent", "--project ."],
notes: [
{
"en-US":
"Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.",
"zh-CN":
"不传 --project 时创建 managed-agent/ 子目录;后续项目操作请先进入该目录。使用 --project . 可在当前目录初始化或转换 agents.yaml;不会覆盖已有项目文件。",
},
{
"en-US":
"New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.",
Expand All @@ -55,16 +72,17 @@ export const managedAgentProjectInit = defineCommand({
},
],
async run(ctx) {
const projectRoot = ctx.flags.project ?? "./managed-agent";
if (ctx.settings.dryRun) {
emitResult(
{
would_initialize_project: ctx.flags.project ?? ".",
would_initialize_project: projectRoot,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
const result = await initializeDirectoryProject({ projectRoot: ctx.flags.project ?? "." });
const result = await initializeDirectoryProject({ projectRoot });
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
Expand Down Expand Up @@ -99,18 +117,34 @@ export const managedAgentProjectBuild = defineCommand({
"zh-CN": "整理目录源文件并生成不可变的发布 Build",
},
auth: "none",
risk: {
level: "high",
message: {
notes: [
{
"en-US":
"This organizes project source, moves literal Vault secrets into the local .env, and writes the previewed immutable Build.",
"Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.",
"zh-CN":
"该操作会整理项目源文件,将 Vault 明文密钥移入本地 .env,并写入已预览的不可变 Build。",
"Build 无需确认即可写入本地项目文件,包括推断的资源关联及将 Vault 明文密钥移入 .env。使用 --dry-run 可只预览不写入。Publish 变更远端资源前仍需显式确认。",
},
},
],
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["--dry-run", "--yes", "--project ./my-agent --yes"],
exampleArgs: ["", "--dry-run", "--project ./my-agent"],
async validate(flags) {
await withAgentErrors(async () => {
const root = await resolveDirectoryProjectRoot(flags.project ?? ".");
const metadata = await stat(join(root, "project.json")).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
});
if (!metadata?.isFile()) {
throw new BailianError(
`Not a project root: ${root} (project.json is missing).`,
ExitCode.USAGE,
"Run from the directory containing project.json, or pass --project <directory>.",
);
}
});
return undefined;
},
async run(ctx) {
const root = ctx.flags.project ?? ".";
const preview = await previewProjectBuild(root);
Expand All @@ -120,7 +154,11 @@ export const managedAgentProjectBuild = defineCommand({
return;
}
if (!preview.can_build)
throw new BailianError("Directory project is invalid and cannot be built.", ExitCode.GENERAL);
throw new BailianError(
preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ??
"Directory project is invalid and cannot be built.",
ExitCode.GENERAL,
);
const built = await commitProjectBuild({
projectRoot: root,
baseRevision: preview.project_revision,
Expand Down
112 changes: 112 additions & 0 deletions packages/commands/tests/e2e/managed-agent-init.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { runNodeMain } from "e2e/runner";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { parseStdoutJson } from "./helpers.ts";

const directories: string[] = [];

async function temporaryDirectory() {
const directory = await realpath(await mkdtemp(join(tmpdir(), "bailian-yaml-init-")));
directories.push(directory);
return directory;
}

afterEach(async () => {
for (const directory of directories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});

async function runInit(directory: string, args: string[] = []) {
return runNodeMain(
fileURLToPath(new URL("./harness/main.ts", import.meta.url)),
["managed-agent", "init", ...args],
{
cwd: directory,
env: {
BAILIAN_CONFIG_DIR: await temporaryDirectory(),
BAILIAN_E2E_ROUTES: JSON.stringify([
{ path: "managed-agent init", export: "managedAgentInit" },
]),
},
},
);
}

describe("e2e: managed-agent init output path", () => {
test.each(["default", "relative", "absolute"] as const)(
"reports the absolute YAML path for a %s output path",
async (pathKind) => {
const directory = await temporaryDirectory();
const relativePath =
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
const outputPath = join(directory, relativePath);
await mkdir(dirname(outputPath), { recursive: true });
const args =
pathKind === "default"
? []
: ["--file", pathKind === "absolute" ? outputPath : relativePath];

const result = await runInit(directory, args);

expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.split(/\r?\n/)[0]).toBe(`Created ${outputPath}`);
expect(await readFile(outputPath, "utf8")).toContain("agents:");
},
);

test("preserves the JSON output contract", async () => {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--file", "custom.yaml", "--output", "json"]);

expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toEqual({
created: "custom.yaml",
provider: "bailian",
agent: "assistant",
});
expect((await stat(join(directory, "custom.yaml"))).isFile()).toBe(true);
});

test("keeps dry-run read-only without reporting a created file", async () => {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--dry-run", "--output", "json"]);

expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toEqual({
would_create: "agents.yaml",
provider: "bailian",
agent: "assistant",
would_update_gitignore: true,
});
expect(await stat(join(directory, "agents.yaml")).catch(() => null)).toBeNull();
expect(await stat(join(directory, ".gitignore")).catch(() => null)).toBeNull();
});

test.each(["default", "relative", "absolute"] as const)(
"reports the absolute existing %s path without overwriting the YAML",
async (pathKind) => {
const directory = await temporaryDirectory();
const relativePath =
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
const outputPath = join(directory, relativePath);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, "Keep user configuration.\n");
const args =
pathKind === "default"
? []
: ["--file", pathKind === "absolute" ? outputPath : relativePath];

const result = await runInit(directory, args);

expect(result.exitCode).toBe(2);
expect(result.stderr).toContain(`${outputPath} already exists.`);
expect(result.stderr).toContain("Pass --force to overwrite.");
expect(result.stdout).not.toContain("Created ");
expect(await readFile(outputPath, "utf8")).toBe("Keep user configuration.\n");
},
);
});
Loading