Skip to content

Commit 602045b

Browse files
authored
Merge pull request #2 from yuque/harness/agent-harness
Agent harness: doc map, contract locks, toolchain hardening, spec-gap flags
2 parents 07e57f6 + 2543198 commit 602045b

43 files changed

Lines changed: 7791 additions & 673 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.prettierignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/client/types.gen.ts

‎AGENTS.md‎

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,73 @@ bin.ts → cli.ts (commander program, error → exit code)
1717
└── client/http.ts (auth header, retry/backoff, YuqueError)
1818
```
1919

20-
- Command surface is **locked 1:1 against `spec/yuque-openapi.yaml`** by
21-
`tests/spec-coverage.test.ts`. Adding/renaming a command or refreshing the spec means
22-
updating that table deliberately.
23-
- Both READMEs are locked by `tests/docs/command-surface-docs.test.ts` (exact command
24-
count in the heading, every command mentioned). Keep README.md and README.zh-CN.md
25-
strictly isomorphic.
26-
- Exit codes are a stable contract: 0 ok · 1 API/unknown · 2 usage · 3 auth · 4 not
27-
found · 5 rate limited (`src/errors.ts`).
28-
29-
## Rules
30-
31-
- All HTTP goes through `client/http.ts`; command handlers never call axios directly.
32-
- Destructive commands (`delete`, `member remove`) go through `confirmDestructive` and
33-
support `--yes`.
34-
- Human output via `src/output.ts`; `--json` always prints the full raw payload.
35-
- `npm run check` (lint + format + typecheck + unit tests + build + dist smoke +
36-
functional e2e) must exit 0 — it is the merge gate and matches CI.
37-
- Functional tests live in `tests/e2e/` (see its README): a mock-server suite that
38-
spawns the built binary (always on), plus an env-gated real-API suite that CI
39-
(`.github/workflows/ci.yml`) enables when the `YUQUE_E2E_TOKEN` secret exists.
40-
New/changed commands need e2e coverage in the mock suite, not just unit tests.
20+
## Map
21+
22+
- `src/bin.ts` — process entry point; installs quiet `EPIPE` handling and assigns `process.exitCode` from `runCli`.
23+
- `src/cli.ts` — owns `buildProgram`, global options, domain registration, and the single `runCli` error-to-exit-code mapping.
24+
- `src/config.ts` — resolves token (`--token` > `YUQUE_TOKEN` > `YUQUE_PERSONAL_TOKEN`), host (`--host` > `YUQUE_HOST` > default), and timeout (`--timeout` > `YUQUE_TIMEOUT_MS` > 30000); `normalizeHost` strips trailing slashes and `/api/v2`.
25+
- `src/context.ts` — `getContext` builds the per-action HTTP/output context; call it only inside actions so `--help` never requires a token.
26+
- `src/errors.ts` — sole definition of CLI error classes, API status hints, and the stable exit-code contract.
27+
- `src/output.ts` — `printJson`/`printOk`/`printTable`/`printRecord`, TTY color honoring `NO_COLOR`, and CJK/emoji display-width alignment.
28+
- `src/confirm.ts` — `confirmDestructive` enforces the interactive confirmation or `--yes` gate for destructive actions.
29+
- `src/commands/` — Commander-facing domain modules; each exports one `register<Domain>Commands` function and owns flags, action orchestration, and rendering.
30+
- `src/commands/auth.ts` — `registerAuthCommands` registers connectivity and authenticated-account status commands.
31+
- `src/commands/book.ts` — `registerBookCommands` registers book list/get/create/update/delete, pagination, validation, and delete confirmation.
32+
- `src/commands/doc.ts` — `registerDocCommands` registers document CRUD, body/file handling, pagination, and version reads.
33+
- `src/commands/group.ts` — `registerGroupCommands` registers group-member list/set/remove, pagination, roles, and removal confirmation.
34+
- `src/commands/search.ts` — `registerSearchCommands` registers typed doc/book search and maps the book surface name to the API's `repo` value.
35+
- `src/commands/stats.ts` — `registerStatsCommands` registers aggregate/member/book/doc statistics, filters, sorting, and page draining.
36+
- `src/commands/toc.ts` — `registerTocCommands` registers TOC tree reads and cross-field-validated node updates.
37+
- `src/commands/user.ts` — `registerUserCommands` registers current-user and user-group reads with role filtering and pagination.
38+
- `src/client/http.ts` — the only HTTP exit; adds auth/base URL/timeout, normalizes errors, retries 429 for every method, and retries 502/503/504 or network failures only for GET.
39+
- `src/client/api/` — thin domain wrappers: `YuqueHttp` is the first argument, typed `ApiEnvelope` responses are awaited, and `res.data` is returned.
40+
- `src/client/api/book.ts` — book owner collection and id-or-namespace item API wrappers.
41+
- `src/client/api/doc.ts` — document CRUD, global-id lookup, and published-version API wrappers.
42+
- `src/client/api/group.ts` — group-member list/update/remove API wrappers.
43+
- `src/client/api/search.ts` — doc/repo search API wrapper.
44+
- `src/client/api/stats.ts` — group aggregate and paged member/book/doc statistics API wrappers, including the live-array correction to the spec types.
45+
- `src/client/api/toc.ts` — book TOC read/update API wrappers and update-body shape.
46+
- `src/client/api/user.ts` — heartbeat, current-user, and user-groups API wrappers.
47+
- `src/client/book-ref.ts` — parses a book reference as a numeric id or `group/slug` and produces the encoded `/repos/...` base path.
48+
- `src/client/paginate.ts` — drains offset-paged endpoints for `--all` until the first short page.
49+
- `src/client/types.gen.ts` — generated from `spec/yuque-openapi.yaml`; edit the spec and run `npm run gen:types`, never edit this file directly.
50+
- `src/client/types.ts` — thin compatibility adapter over the generated schemas; preserves public type names, live-API extensions, and index signatures for `--json` pass-through.
51+
- `spec/yuque-openapi.yaml` — vendored upstream OpenAPI contract and source of truth for the supported operation surface.
52+
- `scripts/smoke-dist-cli.js` — spawns `dist/bin.js` as an installed CLI would and checks version/help plus representative usage/auth exit codes.
53+
- `tests/commands/` — mocked command-action unit tests for exact requests, rendering, validation, confirmation, and exit behavior.
54+
- `tests/client/` — transport retry/error and book-reference unit contracts.
55+
- `tests/docs/` — programmatic drift locks for README command coverage and this repository guide.
56+
- `tests/e2e/` — built-binary functional tests: always-on mock-server wire assertions plus env-gated real-API coverage.
57+
- `tests/spec-coverage.test.ts` — pins every operationId + method + path to registered leaf commands and locks the expected command list.
58+
59+
## Contracts & locks
60+
61+
- Command surface is **locked 1:1 against `spec/yuque-openapi.yaml`** by `tests/spec-coverage.test.ts`; adding/renaming a command or refreshing the spec requires a deliberate mapping update.
62+
- Both READMEs are locked by `tests/docs/command-surface-docs.test.ts` for the exact heading count and exact registered leaf-command set; keep README.md and README.zh-CN.md strictly isomorphic.
63+
- Exit codes are stable and scripts may rely on them: `0 success · 1 API/unknown error · 2 usage error · 3 auth error · 4 not found · 5 rate limited` (`src/errors.ts`).
64+
- All HTTP goes through `src/client/http.ts`; command handlers never call axios directly.
65+
- Human output goes through `src/output.ts`; `--json` prints the complete unprojected payload returned by the API wrapper.
66+
- `npm run check` is the merge gate and matches CI: lint, format check, generated-type drift check, typecheck, coverage-enforced unit tests, one build, dist smoke, then functional e2e.
67+
- New or changed commands need always-on mock-server coverage in `tests/e2e/`, not only unit coverage; see `tests/e2e/README.md` for the optional real-API gates.
68+
69+
## Adding a command
70+
71+
1. Add the typed wrapper in `src/client/api/<domain>.ts`; take `http` first and `return res.data` to unwrap `ApiEnvelope`.
72+
2. Register the subcommand in `src/commands/<domain>.ts`; action order is local flag validation (`UsageError`) → `confirmDestructive` for destructive work, before `getContext` → `getContext(cmd)` → API call → `if (ctx.json) { printJson(raw); return; }` with the complete wrapper result → `printOk`/`printTable`/`printRecord`.
73+
3. For a new domain, add the import at the top of `src/cli.ts` and call its register function inside `buildProgram`.
74+
4. Update the `OPERATION_TO_COMMANDS` operationId + method + path tuples and `EXPECTED_LEAF_COMMANDS` in `tests/spec-coverage.test.ts`.
75+
5. Update `README.md` and `README.zh-CN.md`: increment each commands-section heading count and add an isomorphic command-table row; `tests/docs/command-surface-docs.test.ts` locks both.
76+
6. Regenerate the pinned help surface — `UPDATE_HELP_GOLDEN=1 npx vitest run tests/docs/help-surface.test.ts` — and review the `tests/docs/help-surface.golden.json` diff; any new or changed flag must appear there deliberately.
77+
7. Add `tests/commands/` unit coverage with exact request assertions and the expected process exit code.
78+
8. Add a `tests/e2e/` mock-suite case with wire-level request assertions against the built binary.
79+
9. Run `npm run check` and require the complete gate to pass.
80+
81+
## Conventions & gotchas
82+
83+
- ESM is mandatory (`package.json` has `"type": "module"`); relative TypeScript imports use `.js` suffixes because those are the emitted runtime paths.
84+
- Runtime support is Node >= 20, as pinned by `package.json#engines`.
85+
- For new flag validation, use `Option.choices` for enums or an argument parser that throws `UsageError`; both map to exit 2 because `buildProgram` calls `exitOverride` before registration and Commander copies that callback into subcommands (`tests/commands/flag-validation.test.ts` locks this). Put cross-field and semantic-required checks inside the action and throw `UsageError`; do not introduce new `InvalidArgumentError` parsers.
86+
- Destructive actions call `confirmDestructive` before `getContext`, so confirmation precedes auth/client setup; every such command supports `--yes`.
87+
- Convenience scripts: `npm run lint:fix` fixes lint, `npm run format` writes formatting, `npm run test:watch` watches unit tests, `npm run test:coverage` runs coverage, and `npm run dev` runs `src/bin.ts` directly through tsx.
88+
- Smoke and e2e exercise built `dist` output: `npm run smoke:dist` and `npm run test:e2e` build first, while `npm run check` builds once then calls their `:built` variants.
89+
- Release work includes updating `CHANGELOG.md`; npm's `prepublishOnly` lifecycle runs the build before publication.

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Each command maps to the [Yuque OpenAPI](https://www.yuque.com/yuque/developer/a
8787
| | `stats books <login>` | Per-book statistics |
8888
| | `stats docs <login>` | Per-doc statistics |
8989

90-
Books (知识库) accept either a numeric id or an `owner/slug` namespace everywhere. Run `yuque <command> --help` for all flags.
90+
Books (知识库) accept either a numeric id or an `owner/slug` namespace everywhere. For data-table docs, `doc get` accepts `--page` and `--page-size` to page the content. Run `yuque <command> --help` for all flags.
9191

9292
## Output & scripting
9393

@@ -132,11 +132,11 @@ cd yuque-open-cli
132132
npm install
133133
npm test # unit tests
134134
npm run build # compile TypeScript
135-
npm run test:e2e # functional tests: the built binary vs a mock Yuque API
135+
npm run test:e2e # build, then run the binary against a mock Yuque API
136136
npm run dev -- --help # run from source
137137
```
138138

139-
The command surface is pinned to [spec/yuque-openapi.yaml](./spec/yuque-openapi.yaml) by [tests/spec-coverage.test.ts](./tests/spec-coverage.test.ts); `npm run check` is the merge gate.
139+
The command surface is pinned to [spec/yuque-openapi.yaml](./spec/yuque-openapi.yaml) by [tests/spec-coverage.test.ts](./tests/spec-coverage.test.ts); `npm run check` is the merge gate (lint, formatting, type checking, coverage-enforced unit tests, one build, dist smoke, and functional e2e).
140140

141141
## Links
142142

@@ -148,7 +148,7 @@ The command surface is pinned to [spec/yuque-openapi.yaml](./spec/yuque-openapi.
148148

149149
[MIT](./LICENSE)
150150

151-
[ci-image]: https://img.shields.io/github/actions/workflow/status/yuque/yuque-cli/ci.yml?style=flat-square&label=CI
151+
[ci-image]: https://img.shields.io/github/actions/workflow/status/yuque/yuque-open-cli/ci.yml?style=flat-square&label=CI
152152
[ci-url]: https://github.com/yuque/yuque-open-cli/actions/workflows/ci.yml
153153
[npm-image]: https://img.shields.io/npm/v/yuque-open-cli?style=flat-square
154154
[npm-url]: https://www.npmjs.com/package/yuque-open-cli

‎README.zh-CN.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ YUQUE_TOKEN=YOUR_TOKEN npx yuque-open-cli auth status
8787
| | `stats books <login>` | 知识库维度统计 |
8888
| | `stats docs <login>` | 文档维度统计 |
8989

90-
知识库参数在所有命令中都同时接受数字 id 和 `owner/slug` 路径。各命令的完整参数见 `yuque <命令> --help`。
90+
知识库参数在所有命令中都同时接受数字 id 和 `owner/slug` 路径。对于数据表文档,`doc get` 接受 `--page` 和 `--page-size` 对正文进行分页。各命令的完整参数见 `yuque <命令> --help`。
9191

9292
## 输出与脚本化
9393

@@ -132,11 +132,11 @@ cd yuque-open-cli
132132
npm install
133133
npm test # 单元测试
134134
npm run build # 编译 TypeScript
135-
npm run test:e2e # 功能测试:构建产物对着 mock 语雀 API 跑
135+
npm run test:e2e # 先构建,再让产物对着 mock 语雀 API 跑功能测试
136136
npm run dev -- --help # 从源码运行
137137
```
138138

139-
命令面由 [tests/spec-coverage.test.ts](./tests/spec-coverage.test.ts) 锁定在 [spec/yuque-openapi.yaml](./spec/yuque-openapi.yaml) 上;`npm run check` 是合并门槛。
139+
命令面由 [tests/spec-coverage.test.ts](./tests/spec-coverage.test.ts) 锁定在 [spec/yuque-openapi.yaml](./spec/yuque-openapi.yaml) 上;`npm run check` 是合并门槛(依次运行 lint、格式检查、类型检查、带覆盖率门槛的单元测试、一次构建、dist 冒烟测试和功能 e2e)。
140140

141141
## 相关链接
142142

@@ -148,7 +148,7 @@ npm run dev -- --help # 从源码运行
148148

149149
[MIT](./LICENSE)
150150

151-
[ci-image]: https://img.shields.io/github/actions/workflow/status/yuque/yuque-cli/ci.yml?style=flat-square&label=CI
151+
[ci-image]: https://img.shields.io/github/actions/workflow/status/yuque/yuque-open-cli/ci.yml?style=flat-square&label=CI
152152
[ci-url]: https://github.com/yuque/yuque-open-cli/actions/workflows/ci.yml
153153
[npm-image]: https://img.shields.io/npm/v/yuque-open-cli?style=flat-square
154154
[npm-url]: https://www.npmjs.com/package/yuque-open-cli

‎eslint.config.js‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
import tseslint from '@typescript-eslint/eslint-plugin';
2-
import tsparser from '@typescript-eslint/parser';
2+
3+
const typedFiles = ['src/**/*.ts', 'tests/**/*.ts', '*.config.ts'];
4+
const javascriptFiles = ['scripts/**/*.js', '*.config.js'];
35

46
export default [
57
{
6-
files: ['src/**/*.ts', 'tests/**/*.ts'],
8+
ignores: ['coverage/', 'dist/', 'node_modules/', 'src/client/types.gen.ts'],
9+
},
10+
...tseslint.configs['flat/recommended-type-checked'].map((config) => ({
11+
...config,
12+
files: typedFiles,
13+
})),
14+
{
15+
files: typedFiles,
716
languageOptions: {
8-
parser: tsparser,
917
parserOptions: {
1018
ecmaVersion: 2022,
1119
sourceType: 'module',
20+
project: './tsconfig.eslint.json',
21+
tsconfigRootDir: import.meta.dirname,
1222
},
1323
},
14-
plugins: {
15-
'@typescript-eslint': tseslint,
16-
},
1724
rules: {
18-
...tseslint.configs.recommended.rules,
1925
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
2026
'@typescript-eslint/explicit-function-return-type': 'off',
21-
'@typescript-eslint/no-explicit-any': 'warn',
22-
'@typescript-eslint/no-non-null-assertion': 'warn',
27+
'@typescript-eslint/no-explicit-any': 'error',
28+
'@typescript-eslint/no-floating-promises': 'error',
29+
'@typescript-eslint/no-misused-promises': 'error',
30+
'@typescript-eslint/no-non-null-assertion': 'error',
2331
},
2432
},
33+
...tseslint.configs['flat/recommended'].map((config) => ({
34+
...config,
35+
files: javascriptFiles,
36+
})),
2537
{
26-
ignores: ['dist/', 'node_modules/', '*.js'],
38+
...tseslint.configs['flat/disable-type-checked'],
39+
files: javascriptFiles,
2740
},
2841
];

0 commit comments

Comments
 (0)