Skip to content
Open
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
54 changes: 35 additions & 19 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,58 @@

## 项目地图

monorepo 双包结构:
monorepo 多包结构:

- `packages/cli` — `bailian-cli` 包,CLI 命令、UI、入口
- `packages/core` — `bailian-cli-core` 包,鉴权 / HTTP / 类型,纯逻辑层
- `packages/core` — `bailian-cli-core`,鉴权 / HTTP / 类型,纯逻辑层
- `packages/runtime` — `bailian-cli-runtime`,`createCli` / registry / args / output
- `packages/commands` — `bailian-cli-commands`,命令实现 + **契约 e2e** + e2e 公共基建(`tests/e2e/core`)
- `packages/cli` — `bailian-cli`,`bl` 产品入口 + smoke e2e
- `packages/rag` — `bailian-cli-rag`,`rag` 产品入口 + smoke e2e

### E2E 测试分布

- 契约 e2e(help / dry-run / 真实集成): `packages/commands/tests/e2e/`
- e2e 基建(`createCliRunner` 等): `packages/commands/tests/e2e/core/`(cli/rag 以相对路径引用)
- 产品 smoke: `packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts`
- 一次跑全量 e2e: 根目录 `pnpm test:e2e`

### 命令登记分层

命令**实现**在 `packages/commands/src/commands/`;**路径映射**由各产品 / 测试 fixture 自行维护,库本身不预设:

| 层 | 文件 | 作用 |
| ------------ | -------------------------------------------------- | -------------------------------------- |
| 单命令导出 | `packages/commands/src/index.ts` | 从实现文件 re-export,供产品与 e2e 引用 |
| bl 产品 map | `packages/cli/src/commands.ts` | `bl` 暴露的全量命令路径 |
| rag 产品 map | `packages/rag/src/main.ts` 内 `commands` | `rag` 裁剪后的命令路径(如 `retrieve`) |
| 契约 e2e map | `packages/commands/tests/fixtures/e2e-commands.ts` | 测试用全量 map,不依赖任何产品包 |

`bl --help` 与 `tools/generate-reference.ts` 读 **`packages/cli/src/commands.ts`**,见 [command-add-remove.md](docs/agents/command-add-remove.md)。

### `packages/cli` 目录要点

```
packages/cli/
├── src/
│ ├── main.ts # 入口、鉴权分支、调用 registry
│ ├── registry.ts # 命令树解析、动态 help(读 catalog)
│ ├── commands/
│ │ ├── catalog.ts # 命令总表(登记处,构建脚本也读它)
│ │ ├── index.ts # re-export commands
│ │ └── <group>/...ts # 各命令 defineCommand 实现
│ ├── output/ # CLI 输出、prompt、progress
│ └── urls.ts # 控制台/文档 URL(仅 cli)
└── tests/e2e/
├── src/main.ts # bl 入口(createCli + commands)
├── src/commands.ts # bl 产品 command map
└── tests/e2e/smoke.e2e.test.ts
```

Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts` 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);与 `tools/sync-skill-metadata.ts` 一起在 **pre-commit**(`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行。

非代码资产:

- `tools/release/` — 发版自动化(CI 驱动,见 `.github/workflows/publish.yml`)
- `tools/generate-reference.ts` — 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`
- `tools/generate-reference.ts` — 从 `packages/cli/src/commands.ts` 生成命令手册到 `skills/bailian-cli/reference/`
- `tools/sync-skill-metadata.ts` — 从 `packages/cli/package.json` 同步 `skills/bailian-cli/SKILL.md` 的 `metadata.version`(与 `generate:reference` 一并由根目录 `pnpm run sync:skill-assets` 及 pre-commit 执行)
- `README.md` / `README.zh.md` — npm 和 GitHub 主页

约定:

- core 是纯库,不依赖 cli(详见下方通用约定)
- 文件路径与命令路径一一对应:`commands/text/chat.ts` ↔ `bl text chat`
- 文件路径与命令路径一一对应:`packages/commands/src/commands/text/chat.ts` ↔ `bl text chat`(路径 key 由产品 map 决定,rag 可 remap)
- 单级命令:`commands/<name>.ts`(如 `update.ts`);两级:`commands/<group>/<action>.ts`
- 命令登记在 **`catalog.ts`**;`bl --help` 与 `tools/generate-reference.ts` 生成的命令手册同源,见 [command-add-remove.md](docs/agents/command-add-remove.md)
- 新增命令:实现 + `index.ts` 导出 + `e2e-commands.ts` 登记;bl 暴露则同步 `cli/src/commands.ts`(详见 [command-add-remove.md](docs/agents/command-add-remove.md))

Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts` 从 `cli/src/commands.ts` 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);与 `tools/sync-skill-metadata.ts` 一起在 **pre-commit**(`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行。

## 业务场景索引

Expand Down Expand Up @@ -75,7 +91,7 @@ core 不应该知道 cli 的存在。具体表现:

- core 不写 stderr,不调 `process.exit`(用 `console.*` 或 `throw`)
- core 抛的 `BailianError`,hint 字符串不出现 `bl xxx` 命令名
- core 不写死域名 / region / 追踪参数(URL 集中在 `packages/cli/src/urls.ts`)
- core 不写死域名 / region / 追踪参数(URL 集中在 `packages/runtime/src/urls.ts`)
- core 接收 cli 通过 `Config` 注入的 metadata(`clientName` / `clientVersion`)

### 3. 错误处理边界:CLI 不翻译服务端错误
Expand Down
56 changes: 44 additions & 12 deletions docs/agents/cli-e2e-tests.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
# CLI E2E 测试规范

## 架构(composable-cli)

E2E 按包分层,与命令实现 / 产品入口解耦:

| 层级 | 路径 | 测什么 |
| ---------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **公共基建** | `packages/commands/tests/e2e/core/` | `createCliRunner`、skip 判定、输出目录、global-setup |
| **契约 e2e map** | `packages/commands/tests/fixtures/e2e-commands.ts` | 测试用全量 `{ "<path>": command }` map,仅依赖 `bailian-cli-commands` 单命令导出,不绑定任何产品 |
| **命令契约 e2e** | `packages/commands/tests/e2e/*.e2e.test.ts` | help、缺参、dry-run、**真实集成**;跑 `tests/fixtures/test-cli.ts`(引用上述 fixture map) |
| **产品 smoke** | `packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts` | 版本、root help、命令面裁剪;跑各产品 `src/main.ts` |

> `commands/tests/e2e/core` 是 e2e 测试基建,与 `packages/core`(`bailian-cli-core`)无关。

## 触发条件

- 新增/修改 `packages/cli/src` 下的 command(`commands/catalog.ts` 登记、`defineCommand` 实现、options/usage)
- 新建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例
- 新增/修改 `packages/commands/src/commands/` 下的 command(`src/index.ts` 单命令导出、`defineCommand` 实现、options/usage)
- 新建或扩展 `packages/commands/tests/e2e/<topic>.e2e.test.ts` 用例
- 新增/变更产品命令面(bl / rag)时同步维护对应 smoke 用例
- 为命令补 help / 缺参 / dry-run / 真实集成测试

以上情况必须同步维护 `packages/cli/tests/e2e/<topic>.e2e.test.ts`。跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。

## 文件与工具

- 路径:`packages/cli/tests/e2e/<kebab-topic>.e2e.test.ts`
- 框架:`vite-plus/test`;子进程跑 CLI:`runCli` from `./helpers.ts`
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
- **契约 e2e 路径**:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`
- **框架**:`vite-plus/test`;契约测试 `runCli` from `./setup.ts`;cli/rag smoke 通过相对路径引用 `packages/commands/tests/e2e/core/`
- **解析 JSON stdout**:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
- **长任务**:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时

## 跑测命令

```sh
# 一次跑全部 e2e(契约 + bl/rag smoke)
pnpm test:e2e

# 仅命令契约(含真实集成,需 BAILIAN_E2E=1 等)
pnpm --filter bailian-cli-commands test

# 单文件(在 bailian-cli-commands 包目录下运行,路径相对于 packages/commands)
pnpm --filter bailian-cli-commands test tests/e2e/text-chat.e2e.test.ts

# 全 monorepo(unit + e2e)
vp run -r test
```

环境变量与 skip 条件见 `.cursor/skills/bailian-cli-e2e/SKILL.md`(若存在)或下文 skip 表。

## 双层 describe(固定结构)

Expand All @@ -32,7 +62,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
});
```

## skip 条件(helpers.ts)
## skip 条件(`tests/e2e/core/skip.ts`

| 场景 | 条件 |
| ------------------- | ----------------------------------------------------- |
Expand All @@ -59,12 +89,14 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {

## 新增 command 检查清单

- [ ] `commands/catalog.ts` 登记 + `tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] `packages/commands/src/index.ts` 导出 + `tests/fixtures/e2e-commands.ts` 登记路径 + `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] 若 bl 也暴露该命令,同步 `packages/cli/src/commands.ts` 产品 map
- [ ] 若改了 `usage` / `options` / `examples`,跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/` 并提交
- [ ] 顶层:分组 help + 子命令 `--help`(多子命令则各一条 help)
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
- [ ] `pnpm test packages/cli/tests/e2e/<file>` 通过
- [ ] 若 bl/rag 命令面变更,更新对应 smoke 用例
- [ ] `pnpm --filter bailian-cli-commands test tests/e2e/<file>` 通过

## 示例片段

Expand Down Expand Up @@ -97,4 +129,4 @@ test("foo bar --dry-run 仅输出计划", async () => {
- **E2E**:单条/少量调用、断言固定、可进 `vp test`(见上文 skip 条件)
- **批量压测**:`packages/cli/tests/stress/run.mjs` + `targets/*.mjs`,并发 + 报告,**仅手动** `pnpm run test:stress -- <target>`

勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md)
勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md).
61 changes: 35 additions & 26 deletions docs/agents/command-add-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,47 @@
仅当子组下有 ≥2 个 action 时合理(否则拍平到两级)
```

文件路径与命令路径必须 1:1 对齐。
实现文件路径与命令语义 1:1 对齐;**产品在 map 里决定暴露路径**(rag 可将 `knowledge retrieve` remap 为 `retrieve`)

## CLI 命令注册架构(必读)

命令元数据以 **`catalog.ts` 为单一登记处**;`registry.ts` 只负责解析与打印 help,不再内嵌命令表或手写 Resources 列表
composable-cli:命令库只 export 单命令,**路径 map 由产品 / 测试各自注入**;`createCli` + `CommandRegistry` 在 `bailian-cli-runtime` 里解析 help

```
commands/<...>.ts defineCommand({ name, description, usage, options, examples, run })
packages/commands/src/commands/<...>.ts
defineCommand({ name, description, usage, options, examples, run })
commands/catalog.ts export const commands: Record<string, Command>
packages/commands/src/index.ts 单命令 re-export(无路径 preset)
┌────┴────┬──────────────────────┬─────────────────────┐
↓ ↓ ↓ ↓
registry.ts main.ts tools/generate-reference.ts export-schema.ts
(解析/help) (入口) → skills/bailian-cli/reference/index.md + <group>.md
┌────┴────────────┬─────────────────────────┬──────────────────────────┐
↓ ↓ ↓ ↓
cli/src/commands.ts rag/src/main.ts tests/fixtures/e2e-commands.ts (其他产品…)
(bl 全量 map) (rag 裁剪 map) (契约 e2e 全量 map)
↓ ↓ ↓
createCli(commands, opts) → runtime/registry.ts(建树、resolve、printHelp)
tools/generate-reference.ts 读 cli/src/commands.ts → skills/bailian-cli/reference/
```

- **`packages/cli/src/commands/catalog.ts`**: `import` 命令模块 + `"<path>": handler` 映射;**不** `import registry.ts`(避免构建时循环依赖)
- **`packages/cli/src/commands/index.ts`**: `export { commands } from "./catalog.ts"`(给包内 re-export 用)
- **`packages/cli/src/registry.ts`**: `import { commands } from "./commands/catalog.ts"`,建树、`resolve`、`printHelp`;Commands / Global Flags 从 `Command` 元数据与 `GLOBAL_OPTIONS` **动态生成**
- **`tools/generate-reference.ts`**: pre-commit / `pnpm run sync:skill-assets` 时读 `catalog.ts`,写 `skills/bailian-cli/reference/index.md`(索引) + `skills/bailian-cli/reference/<一级命令>.md`(详情,勿手改)。该目录**纳入 git**,随 `npx skills add modelstudioai/cli` 分发
- **`packages/commands/src/index.ts`**: `export { default as textChat } from "./commands/text/chat.ts"` 等;**不**含 `"<path>": handler` 映射
- **`packages/cli/src/commands.ts`**: bl 产品 map;`main.ts` 传入 `createCli`
- **`packages/rag/src/main.ts`**: rag 产品 map(内联);路径可不同于 bl
- **`packages/commands/tests/fixtures/e2e-commands.ts`**: 契约 e2e 全量 map;**不** import `packages/cli`
- **`packages/runtime/src/registry.ts`**: 从注入的 `Record<string, Command>` 建树;Commands / Global Flags 从 `Command` 元数据与 `GLOBAL_OPTIONS` **动态生成**
- **`tools/generate-reference.ts`**: pre-commit / `pnpm run sync:skill-assets` 时读 **`packages/cli/src/commands.ts`**,写 `skills/bailian-cli/reference/index.md`(索引) + `skills/bailian-cli/reference/<一级命令>.md`(详情,勿手改)。该目录**纳入 git**,随 `npx skills add modelstudioai/cli` 分发

已删除、勿再引用:`commands/help.ts`、`registry.ts` 内联 `new CommandRegistry({...})`、`printRootHelp` 手写命令行
已删除、勿再引用:`packages/cli/src/commands/catalog.ts`、`groups.ts`、`packages/cli/src/registry.ts`、`config/export-schema.ts`

## 必查清单

### A. 代码层

- [ ] **新建/删除/移动**对应的 `packages/cli/src/commands/<...>.ts` 文件
- [ ] **`packages/cli/src/commands/catalog.ts`**:
- 增删 `import xxx from "./.../xxx.ts"`
- 在 `export const commands` 里增删 `"<group> <action>": xxx`(key 与 `defineCommand({ name })` 一致)
- [ ] **不要**在 `registry.ts` 里重复登记命令(已从 catalog 读取)
- [ ] 如果命令需要跳过入口的默认 DashScope API key 引导(`ensureApiKey`),在对应 `defineCommand` 上设 `skipDefaultApiKeySetup: true`(字段定义见 `packages/core/src/types/command.ts`;`main.ts` 根据已解析的 `command` 读取)
- [ ] **`config/export-schema.ts`**: 若新命令不适合作为 agent tool,评估是否加入 `SKIP_PREFIXES`;该文件在 `run()` 内 `import("../catalog.ts")`,勿顶层 import catalog 以免循环依赖
- [ ] **新建/删除/移动**对应的 `packages/commands/src/commands/<...>.ts` 文件
- [ ] **`packages/commands/src/index.ts`**: 增删 `export { default as xxx } from "./commands/.../xxx.ts"`
- [ ] **`packages/commands/tests/fixtures/e2e-commands.ts`**: 增删 import 与 `"<path>": xxx`(契约 e2e 命令面)
- [ ] **`packages/cli/src/commands.ts`**(bl 暴露该命令时): 同步 import 与 map key(key 与 `defineCommand({ name })` 一致)
- [ ] **`packages/rag/src/main.ts`**(rag 暴露该命令时): 同步 import 与 map key(路径可按 rag 产品约定 remap)
- [ ] 如果命令需要跳过入口的默认 DashScope API key 引导(`ensureApiKey`),在对应 `defineCommand` 上设 `skipDefaultApiKeySetup: true`(字段定义见 `packages/core/src/types/command.ts`;`createCli` 根据已解析的 `command` 读取)

### B. 文档层

Expand All @@ -64,13 +70,14 @@ registry.ts main.ts tools/generate-reference.ts export-schema.ts

### C. 测试层

- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/cli/tests/e2e/<topic>.e2e.test.ts`
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/commands/tests/e2e/<topic>.e2e.test.ts`
- [ ] bl / rag 命令面变更时,同步对应 smoke:`packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts`
- [ ] 删除命令时一并删对应 e2e

### D. 重命名特殊处理

- [ ] 全仓 grep **旧命令名字符串**,确保以下位置全部更新:
- `catalog.ts` 的 key
- 各产品 / fixture map 的 key(`cli/src/commands.ts`、`e2e-commands.ts`、`rag/src/main.ts` 等)
- error hints(cli 层)
- `skills/bailian-cli/reference/`(重建后检查并提交)
- README 示例
Expand All @@ -79,15 +86,17 @@ registry.ts main.ts tools/generate-reference.ts export-schema.ts
## 完成后自查

```sh
pnpm run sync:skill-assets # reference/ + SKILL metadata.version 与 catalog / package.json 一致
pnpm run sync:skill-assets # reference/ + SKILL metadata.version 与 cli/commands.ts / package.json 一致
node packages/cli/src/main.ts <new-command> --help
node packages/cli/src/main.ts # 根 help 列表含新命令
vp test packages/cli/tests/e2e/<topic>.e2e.test.ts # 相关 e2e
pnpm --filter bailian-cli-commands test tests/e2e/<topic>.e2e.test.ts
pnpm --filter bailian-cli test tests/e2e/smoke.e2e.test.ts # bl 命令面变更时
```

## 常见漏点

- ✗ 只改了命令文件,忘了 **`catalog.ts`** → 命令不存在或 help 里没有
- ✗ 只改了命令文件,忘了 **`index.ts` 导出** → 产品 / e2e 无法 import
- ✗ 忘了 **`e2e-commands.ts`** → 契约 e2e 跑不起来或缺命令
- ✗ bl 要暴露却忘了 **`cli/src/commands.ts`** → `bl --help` 里没有、reference 也不会生成
- ✗ 手改 **`skills/bailian-cli/reference/*.md`** → 下次 generate 被覆盖;应改 `defineCommand` 后重新 generate 并提交
- ✗ 在 `export-schema.ts` 顶层 `import catalog` → 可能与 registry 循环依赖
- ✗ 单 action 的子组是反模式,新增时优先拍平为两级
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ready": "vp check && vp run -r test && vp run -r build",
"prepare": "vp config",
"check": "vp check",
"sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
"sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli-runtime run build && pnpm --filter bailian-cli-commands run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
"dev": "pnpm -F bailian-cli-core dev",
"bl": "pnpm -F bailian-cli dev",
"rag": "pnpm -F bailian-cli-rag dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createCli } from "bailian-cli-runtime";
import { commands } from "./commands.ts";
import pkg from "../package.json" with { type: "json" };

createCli(commands, {
void createCli(commands, {
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
Expand Down
Loading