From b9ad5be6599414c9060fc1377d95bb4a505fbfed Mon Sep 17 00:00:00 2001 From: cwg <1227646458@qq.com> Date: Thu, 23 Jul 2026 19:46:29 +0900 Subject: [PATCH 1/3] fix!: rename knowledge-base commands from repo to book MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the CLI surface with Yuque terminology (知识库 = book, matching the API's V2Book/book_id and yuque-mcp-server's yuque_*_book tools): - repo list/get/create/update/delete -> book ...; args -> - search --type takes doc|book (repo kept as a compatibility alias; wire value unchanged) - HTTP paths (/repos/...), YUQUE_E2E_REPO env vars, and the vendored spec stay as-is — they are wire/config contracts, not surface terminology - version 2.0.0 (breaking command rename) Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 12 ++ README.md | 36 +++--- README.zh-CN.md | 28 ++--- package-lock.json | 4 +- package.json | 2 +- src/cli.ts | 4 +- src/client/api/{repo.ts => book.ts} | 44 ++++---- src/client/api/doc.ts | 22 ++-- src/client/api/toc.ts | 10 +- src/client/{repo-ref.ts => book-ref.ts} | 8 +- src/commands/{repo.ts => book.ts} | 106 +++++++++--------- src/commands/doc.ts | 24 ++-- src/commands/search.ts | 14 ++- src/commands/stats.ts | 2 +- src/commands/toc.ts | 20 ++-- tests/client/book-ref.test.ts | 32 ++++++ tests/client/repo-ref.test.ts | 32 ------ tests/commands/auth-user-search.test.ts | 2 +- tests/commands/{repo.test.ts => book.test.ts} | 56 ++++----- tests/e2e/cli.e2e.test.ts | 22 ++-- tests/e2e/errors.e2e.test.ts | 2 +- tests/e2e/read-commands.e2e.test.ts | 10 +- tests/e2e/write-commands.e2e.test.ts | 20 ++-- tests/spec-coverage.test.ts | 30 ++--- 24 files changed, 278 insertions(+), 264 deletions(-) rename src/client/api/{repo.ts => book.ts} (60%) rename src/client/{repo-ref.ts => book-ref.ts} (81%) rename src/commands/{repo.ts => book.ts} (70%) create mode 100644 tests/client/book-ref.test.ts delete mode 100644 tests/client/repo-ref.test.ts rename tests/commands/{repo.test.ts => book.test.ts} (82%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d55103..751073f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.0.0 + +Breaking: knowledge bases are now `book` across the CLI surface, aligning with +Yuque's own terminology (知识库 = book, matching the API's `V2Book`/`book_id`): + +- `repo list/get/create/update/delete` → `book list/get/create/update/delete`; + `` arguments are now ``. +- `search --type` takes `doc|book` (`repo` still accepted as a compatibility + alias; the wire value is unchanged). +- Help text and READMEs updated accordingly. HTTP paths (`/repos/...`) and env + variables (`YUQUE_E2E_REPO`) are unchanged. + ## 1.0.0 Initial release of `yuque-open-cli` — a spec-driven, scriptable command-line diff --git a/README.md b/README.md index f35d064..eefdcef 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Once authenticated, your knowledge base is one command away: yuque search "canary release" --type doc # find that doc you half-remember yuque doc get team/handbook onboarding > onboarding.md yuque doc create team/notes --title "Weekly sync" --body-file weekly.md -yuque repo list my-team --group --all --json | jq '.[].name' +yuque book list my-team --group --all --json | jq '.[].name' ``` ## Quick Start @@ -42,7 +42,7 @@ YUQUE_TOKEN=YOUR_TOKEN npx yuque-open-cli auth status -**3. Start exploring** — `yuque repo list your-login`, then `yuque doc list `. +**3. Start exploring** — `yuque book list your-login`, then `yuque doc list `. ## Configuration @@ -64,30 +64,30 @@ Each command maps to the [Yuque OpenAPI](https://www.yuque.com/yuque/developer/a | | `auth status` | Show who you are signed in as | | **User** | `user info` | Show the authenticated user | | | `user groups ` | List groups a user belongs to | -| **Search** | `search ` | Search docs or repos, with paging | -| **Repos** | `repo list ` | List repos (知识库) of a user or `--group` | -| | `repo get ` | Show a repo by id or `owner/slug` | -| | `repo create ` | Create a repo | -| | `repo update ` | Update name, slug, description, visibility, or TOC | -| | `repo delete ` | Delete a repo — asks for confirmation | -| **Docs** | `doc list ` | List docs in a repo, `--all` drains paging | -| | `doc get ` | Print a doc's markdown body; also takes a global ``, `--meta` for metadata | -| | `doc create ` | Create a doc from `--body` or `--body-file` | -| | `doc update ` | Update a doc's body or metadata | -| | `doc delete ` | Delete a doc — asks for confirmation | +| **Search** | `search ` | Search docs or books, with paging | +| **Books** | `book list ` | List books (知识库) of a user or `--group` | +| | `book get ` | Show a book by id or `owner/slug` | +| | `book create ` | Create a book | +| | `book update ` | Update name, slug, description, visibility, or TOC | +| | `book delete ` | Delete a book — asks for confirmation | +| **Docs** | `doc list ` | List docs in a book, `--all` drains paging | +| | `doc get ` | Print a doc's markdown body; also takes a global ``, `--meta` for metadata | +| | `doc create ` | Create a doc from `--body` or `--body-file` | +| | `doc update ` | Update a doc's body or metadata | +| | `doc delete ` | Delete a doc — asks for confirmation | | | `doc versions ` | List a doc's version history | | | `doc version ` | Show one version's content | -| **TOC** | `toc get ` | Print a repo's table of contents as a tree | -| | `toc update ` | Append, prepend, edit, or remove a TOC node | +| **TOC** | `toc get ` | Print a book's table of contents as a tree | +| | `toc update ` | Append, prepend, edit, or remove a TOC node | | **Groups** | `group members ` | List members of a group | | | `group member set ` | Add a member or change their role | | | `group member remove ` | Remove a member — asks for confirmation | | **Stats** | `stats group ` | Group-level statistics | | | `stats members ` | Per-member statistics | -| | `stats books ` | Per-repo statistics | +| | `stats books ` | Per-book statistics | | | `stats docs ` | Per-doc statistics | -Repos accept either a numeric id or an `owner/slug` namespace everywhere. Run `yuque --help` for all flags. +Books (知识库) accept either a numeric id or an `owner/slug` namespace everywhere. Run `yuque --help` for all flags. ## Output & scripting @@ -121,7 +121,7 @@ Colors are disabled automatically when piping, or force-off with `NO_COLOR=1`. R | `A Yuque API token is required` | Set `YUQUE_TOKEN=YOUR_TOKEN` or pass `--token=YOUR_TOKEN` | | `token invalid or expired` (exit `3`) | [Regenerate the token](https://www.yuque.com/settings/tokens) or fix `YUQUE_TOKEN` / `--token` | | `rate limited by the Yuque API` (exit `5`) | The CLI retries automatically; slow down `--all` loops | -| `the requested resource does not exist` (exit `4`) | Check the repo id / `owner/slug` namespace and the doc slug | +| `the requested resource does not exist` (exit `4`) | Check the book id / `owner/slug` namespace and the doc slug | | `npm` command not found | Install [Node.js](https://nodejs.org/) v20 or later | ## Development diff --git a/README.zh-CN.md b/README.zh-CN.md index a2fa5a1..fbcb402 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -18,7 +18,7 @@ yuque search "灰度发布" --type doc # 找回那篇只记得大概的文档 yuque doc get team/handbook onboarding > onboarding.md yuque doc create team/notes --title "周会纪要" --body-file weekly.md -yuque repo list my-team --group --all --json | jq '.[].name' +yuque book list my-team --group --all --json | jq '.[].name' ``` ## 快速开始 @@ -42,7 +42,7 @@ YUQUE_TOKEN=YOUR_TOKEN npx yuque-open-cli auth status -**第三步:开始探索** —— 先 `yuque repo list your-login`,再 `yuque doc list `。 +**第三步:开始探索** —— 先 `yuque book list your-login`,再 `yuque doc list `。 ## 配置 @@ -65,20 +65,20 @@ YUQUE_TOKEN=YOUR_TOKEN npx yuque-open-cli auth status | **用户** | `user info` | 查看当前 Token 对应的用户 | | | `user groups ` | 列出用户加入的团队 | | **搜索** | `search ` | 搜索文档或知识库,支持分页 | -| **知识库** | `repo list ` | 列出用户或团队(`--group`)的知识库 | -| | `repo get ` | 按 id 或 `owner/slug` 查看知识库 | -| | `repo create ` | 创建知识库 | -| | `repo update ` | 更新名称、路径、简介、可见性或目录 | -| | `repo delete ` | 删除知识库 —— 需要确认 | -| **文档** | `doc list ` | 列出知识库中的文档,`--all` 拉取全量 | -| | `doc get ` | 输出文档 markdown 正文;也接受全局 ``,`--meta` 查看元信息 | -| | `doc create ` | 从 `--body` 或 `--body-file` 创建文档 | -| | `doc update ` | 更新文档正文或元信息 | -| | `doc delete ` | 删除文档 —— 需要确认 | +| **知识库** | `book list ` | 列出用户或团队(`--group`)的知识库 | +| | `book get ` | 按 id 或 `owner/slug` 查看知识库 | +| | `book create ` | 创建知识库 | +| | `book update ` | 更新名称、路径、简介、可见性或目录 | +| | `book delete ` | 删除知识库 —— 需要确认 | +| **文档** | `doc list ` | 列出知识库中的文档,`--all` 拉取全量 | +| | `doc get ` | 输出文档 markdown 正文;也接受全局 ``,`--meta` 查看元信息 | +| | `doc create ` | 从 `--body` 或 `--body-file` 创建文档 | +| | `doc update ` | 更新文档正文或元信息 | +| | `doc delete ` | 删除文档 —— 需要确认 | | | `doc versions ` | 列出文档的版本历史 | | | `doc version ` | 查看某个版本的内容 | -| **目录** | `toc get ` | 以树形输出知识库目录 | -| | `toc update ` | 追加、头插、编辑或删除目录节点 | +| **目录** | `toc get ` | 以树形输出知识库目录 | +| | `toc update ` | 追加、头插、编辑或删除目录节点 | | **团队** | `group members ` | 列出团队成员 | | | `group member set ` | 添加成员或调整角色 | | | `group member remove ` | 移除成员 —— 需要确认 | diff --git a/package-lock.json b/package-lock.json index dcce255..9d5e4db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "yuque-open-cli", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yuque-open-cli", - "version": "1.0.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "axios": "^1.7.9", diff --git a/package.json b/package.json index 6957bb2..3ae1811 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yuque-open-cli", - "version": "1.0.0", + "version": "2.0.0", "description": "Command-line interface for Yuque (语雀) — browse, edit, and manage your knowledge base from the terminal", "type": "module", "main": "dist/cli.js", diff --git a/src/cli.ts b/src/cli.ts index 24feceb..2e47f67 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import { CliError, YuqueError, exitCodeForStatus } from './errors.js'; import { registerAuthCommands } from './commands/auth.js'; import { registerUserCommands } from './commands/user.js'; import { registerSearchCommands } from './commands/search.js'; -import { registerRepoCommands } from './commands/repo.js'; +import { registerBookCommands } from './commands/book.js'; import { registerDocCommands } from './commands/doc.js'; import { registerTocCommands } from './commands/toc.js'; import { registerGroupCommands } from './commands/group.js'; @@ -34,7 +34,7 @@ export function buildProgram(): Command { registerAuthCommands(program); registerUserCommands(program); registerSearchCommands(program); - registerRepoCommands(program); + registerBookCommands(program); registerDocCommands(program); registerTocCommands(program); registerGroupCommands(program); diff --git a/src/client/api/repo.ts b/src/client/api/book.ts similarity index 60% rename from src/client/api/repo.ts rename to src/client/api/book.ts index 1ed1dae..749496b 100644 --- a/src/client/api/repo.ts +++ b/src/client/api/book.ts @@ -1,15 +1,15 @@ import type { YuqueHttp } from '../http.js'; -import { repoBasePath, type RepoRef } from '../repo-ref.js'; +import { bookBasePath, type BookRef } from '../book-ref.js'; import type { ApiEnvelope, V2Book, V2BookDetail } from '../types.js'; /** The spec exposes list/create under both /users/:login and /groups/:login. */ -export type RepoOwner = 'user' | 'group'; +export type BookOwner = 'user' | 'group'; -function ownerReposPath(owner: RepoOwner, login: string): string { +function ownerBooksPath(owner: BookOwner, login: string): string { return `/${owner === 'group' ? 'groups' : 'users'}/${encodeURIComponent(login)}/repos`; } -export interface ListReposOptions { +export interface ListBooksOptions { offset?: number; limit?: number; /** Spec enum: Book | Design. Omit for no server-side filter. */ @@ -18,27 +18,27 @@ export interface ListReposOptions { filterByAbility?: string; } -export async function listRepos( +export async function listBooks( http: YuqueHttp, - owner: RepoOwner, + owner: BookOwner, login: string, - options: ListReposOptions = {} + options: ListBooksOptions = {} ): Promise { const params: Record = {}; if (options.offset !== undefined) params.offset = options.offset; if (options.limit !== undefined) params.limit = options.limit; if (options.type !== undefined) params.type = options.type; if (options.filterByAbility !== undefined) params.filterByAbility = options.filterByAbility; - const res = await http.get>(ownerReposPath(owner, login), params); + const res = await http.get>(ownerBooksPath(owner, login), params); return res.data; } -export async function getRepo(http: YuqueHttp, ref: RepoRef): Promise { - const res = await http.get>(repoBasePath(ref)); +export async function getBook(http: YuqueHttp, ref: BookRef): Promise { + const res = await http.get>(bookBasePath(ref)); return res.data; } -export interface CreateRepoBody { +export interface CreateBookBody { name: string; slug: string; description?: string; @@ -46,17 +46,17 @@ export interface CreateRepoBody { enhancedPrivacy?: boolean; } -export async function createRepo( +export async function createBook( http: YuqueHttp, - owner: RepoOwner, + owner: BookOwner, login: string, - body: CreateRepoBody + body: CreateBookBody ): Promise { - const res = await http.post>(ownerReposPath(owner, login), body); + const res = await http.post>(ownerBooksPath(owner, login), body); return res.data; } -export interface UpdateRepoBody { +export interface UpdateBookBody { name?: string; slug?: string; description?: string; @@ -64,16 +64,16 @@ export interface UpdateRepoBody { toc?: string; } -export async function updateRepo( +export async function updateBook( http: YuqueHttp, - ref: RepoRef, - body: UpdateRepoBody + ref: BookRef, + body: UpdateBookBody ): Promise { - const res = await http.put>(repoBasePath(ref), body); + const res = await http.put>(bookBasePath(ref), body); return res.data; } -export async function deleteRepo(http: YuqueHttp, ref: RepoRef): Promise { - const res = await http.delete>(repoBasePath(ref)); +export async function deleteBook(http: YuqueHttp, ref: BookRef): Promise { + const res = await http.delete>(bookBasePath(ref)); return res.data; } diff --git a/src/client/api/doc.ts b/src/client/api/doc.ts index 5033f63..809195d 100644 --- a/src/client/api/doc.ts +++ b/src/client/api/doc.ts @@ -6,7 +6,7 @@ import type { V2DocVersion, V2DocVersionDetail, } from '../types.js'; -import { repoBasePath, type RepoRef } from '../repo-ref.js'; +import { bookBasePath, type BookRef } from '../book-ref.js'; /** Query parameters of GET {repo}/docs, mirroring the spec. */ export interface DocListParams { @@ -30,16 +30,16 @@ export interface DocWritePayload { export async function listDocs( http: YuqueHttp, - repo: RepoRef, + repo: BookRef, params: DocListParams = {} ): Promise { - const res = await http.get>(`${repoBasePath(repo)}/docs`, params); + const res = await http.get>(`${bookBasePath(repo)}/docs`, params); return res.data; } -export async function getDoc(http: YuqueHttp, repo: RepoRef, doc: string): Promise { +export async function getDoc(http: YuqueHttp, repo: BookRef, doc: string): Promise { const res = await http.get>( - `${repoBasePath(repo)}/docs/${encodeURIComponent(doc)}` + `${bookBasePath(repo)}/docs/${encodeURIComponent(doc)}` ); return res.data; } @@ -52,29 +52,29 @@ export async function getDocById(http: YuqueHttp, id: number): Promise { - const res = await http.post>(`${repoBasePath(repo)}/docs`, payload); + const res = await http.post>(`${bookBasePath(repo)}/docs`, payload); return res.data; } export async function updateDoc( http: YuqueHttp, - repo: RepoRef, + repo: BookRef, doc: string, payload: DocWritePayload ): Promise { const res = await http.put>( - `${repoBasePath(repo)}/docs/${encodeURIComponent(doc)}`, + `${bookBasePath(repo)}/docs/${encodeURIComponent(doc)}`, payload ); return res.data; } -export async function deleteDoc(http: YuqueHttp, repo: RepoRef, doc: string): Promise { +export async function deleteDoc(http: YuqueHttp, repo: BookRef, doc: string): Promise { const res = await http.delete>( - `${repoBasePath(repo)}/docs/${encodeURIComponent(doc)}` + `${bookBasePath(repo)}/docs/${encodeURIComponent(doc)}` ); return res.data; } diff --git a/src/client/api/toc.ts b/src/client/api/toc.ts index a906133..122ee90 100644 --- a/src/client/api/toc.ts +++ b/src/client/api/toc.ts @@ -1,5 +1,5 @@ import type { YuqueHttp } from '../http.js'; -import { repoBasePath, type RepoRef } from '../repo-ref.js'; +import { bookBasePath, type BookRef } from '../book-ref.js'; import type { ApiEnvelope, V2TocItem } from '../types.js'; /** Body for PUT /repos/.../toc — mirrors the spec requestBody field-for-field. */ @@ -18,16 +18,16 @@ export interface TocUpdateBody { visible?: number; } -export async function getToc(http: YuqueHttp, repo: RepoRef): Promise { - const res = await http.get>(`${repoBasePath(repo)}/toc`); +export async function getToc(http: YuqueHttp, repo: BookRef): Promise { + const res = await http.get>(`${bookBasePath(repo)}/toc`); return res.data; } export async function updateToc( http: YuqueHttp, - repo: RepoRef, + repo: BookRef, body: TocUpdateBody ): Promise { - const res = await http.put>(`${repoBasePath(repo)}/toc`, body); + const res = await http.put>(`${bookBasePath(repo)}/toc`, body); return res.data; } diff --git a/src/client/repo-ref.ts b/src/client/book-ref.ts similarity index 81% rename from src/client/repo-ref.ts rename to src/client/book-ref.ts index 879e756..6d9a613 100644 --- a/src/client/repo-ref.ts +++ b/src/client/book-ref.ts @@ -4,10 +4,10 @@ import { UsageError } from '../errors.js'; * A repo (知识库) reference given on the command line: either a numeric id * or a `group/slug` namespace. Both map onto the same /repos/... URL shape. */ -export type RepoRef = +export type BookRef = { kind: 'id'; id: string } | { kind: 'namespace'; group: string; slug: string }; -export function parseRepoRef(input: string): RepoRef { +export function parseBookRef(input: string): BookRef { const trimmed = input.trim(); if (/^\d+$/.test(trimmed)) return { kind: 'id', id: trimmed }; const parts = trimmed.split('/'); @@ -15,12 +15,12 @@ export function parseRepoRef(input: string): RepoRef { return { kind: 'namespace', group: parts[0], slug: parts[1] }; } throw new UsageError( - `Invalid repo reference "${input}" — expected a numeric id (e.g. 123456) or a namespace (e.g. group/slug)` + `Invalid book reference "${input}" — expected a numeric id (e.g. 123456) or a namespace (e.g. group/slug)` ); } /** URL base for a repo: /repos/{id} or /repos/{group}/{slug}. */ -export function repoBasePath(ref: RepoRef): string { +export function bookBasePath(ref: BookRef): string { return ref.kind === 'id' ? `/repos/${encodeURIComponent(ref.id)}` : `/repos/${encodeURIComponent(ref.group)}/${encodeURIComponent(ref.slug)}`; diff --git a/src/commands/repo.ts b/src/commands/book.ts similarity index 70% rename from src/commands/repo.ts rename to src/commands/book.ts index 386a8de..cc5c932 100644 --- a/src/commands/repo.ts +++ b/src/commands/book.ts @@ -3,18 +3,18 @@ import { getContext } from '../context.js'; import { UsageError } from '../errors.js'; import { confirmDestructive } from '../confirm.js'; import { printJson, printOk, printRecord, printTable } from '../output.js'; -import { parseRepoRef } from '../client/repo-ref.js'; +import { parseBookRef } from '../client/book-ref.js'; import { fetchAllPages } from '../client/paginate.js'; import { - createRepo, - deleteRepo, - getRepo, - listRepos, - updateRepo, - type CreateRepoBody, - type RepoOwner, - type UpdateRepoBody, -} from '../client/api/repo.js'; + createBook, + deleteBook, + getBook, + listBooks, + updateBook, + type CreateBookBody, + type BookOwner, + type UpdateBookBody, +} from '../client/api/book.js'; import type { V2Book } from '../client/types.js'; function parseNonNegativeInt(value: string): number { @@ -24,14 +24,14 @@ function parseNonNegativeInt(value: string): number { return Number(value); } -/** The spec caps `limit` at 100 for repo listing. */ +/** The spec caps `limit` at 100 for book listing. */ function parseLimit(value: string): number { const limit = parseNonNegativeInt(value); if (limit > 100) throw new UsageError(`--limit is capped at 100 by the Yuque API, got ${limit}`); return limit; } -/** Spec enum for repo visibility. */ +/** Spec enum for book visibility. */ function parsePublic(value: string): number { if (!['0', '1', '2'].includes(value.trim())) { throw new UsageError( @@ -52,22 +52,22 @@ function typeFilter(value: string | undefined): string | undefined { return value; } -function repoLabel(book: V2Book): string { +function bookLabel(book: V2Book): string { return String(book.namespace ?? book.slug); } -export function registerRepoCommands(program: Command): void { - const repo = program.command('repo').description('Manage knowledge bases (知识库)'); +export function registerBookCommands(program: Command): void { + const book = program.command('book').description('Manage knowledge bases (知识库)'); - const list = repo + const list = book .command('list') - .description('List the repos of a user or group') + .description('List the books (知识库) of a user or group') .argument('', 'user/group login or id') .option('--group', 'treat as a group instead of a user') - .option('--type ', 'filter by repo type: Book, Design, or all') + .option('--type ', 'filter by book type: Book, Design, or all') .option( '--filter-by-ability ', - 'only repos where the token has this ability, e.g. create_doc' + 'only books where the token has this ability, e.g. create_doc' ) .option('--offset ', 'pagination offset', parseNonNegativeInt) .option('--limit ', 'page size, max 100', parseLimit) @@ -86,13 +86,13 @@ export function registerRepoCommands(program: Command): void { ) => { const type = typeFilter(opts.type); const ctx = getContext(list); - const owner: RepoOwner = opts.group ? 'group' : 'user'; + const owner: BookOwner = opts.group ? 'group' : 'user'; const filterByAbility = opts.filterByAbility; const books = opts.all ? await fetchAllPages((offset, limit) => - listRepos(ctx.http, owner, login, { offset, limit, type, filterByAbility }) + listBooks(ctx.http, owner, login, { offset, limit, type, filterByAbility }) ) - : await listRepos(ctx.http, owner, login, { + : await listBooks(ctx.http, owner, login, { offset: opts.offset, limit: opts.limit, type, @@ -103,7 +103,7 @@ export function registerRepoCommands(program: Command): void { return; } printTable(books, [ - { key: 'namespace', header: 'NAMESPACE', format: repoLabel }, + { key: 'namespace', header: 'NAMESPACE', format: bookLabel }, { key: 'name', header: 'NAME' }, { key: 'items_count', header: 'ITEMS' }, { key: 'updated_at', header: 'UPDATED' }, @@ -111,14 +111,14 @@ export function registerRepoCommands(program: Command): void { } ); - const get = repo + const get = book .command('get') - .description('Show the details of a repo') - .argument('', 'repo id or group/slug namespace') - .action(async (repoArg: string) => { - const ref = parseRepoRef(repoArg); + .description('Show the details of a book') + .argument('', 'book id or group/slug namespace') + .action(async (bookArg: string) => { + const ref = parseBookRef(bookArg); const ctx = getContext(get); - const book = await getRepo(ctx.http, ref); + const book = await getBook(ctx.http, ref); if (ctx.json) { printJson(book); return; @@ -137,14 +137,14 @@ export function registerRepoCommands(program: Command): void { ]); }); - const create = repo + const create = book .command('create') - .description('Create a repo under a user or group') + .description('Create a book under a user or group') .argument('', 'owner user/group login or id') - .requiredOption('--name ', 'repo name (required)') - .requiredOption('--slug ', 'repo path (slug) (required)') + .requiredOption('--name ', 'book name (required)') + .requiredOption('--slug ', 'book path (slug) (required)') .option('--group', 'create under a group instead of a user') - .option('--description ', 'repo description') + .option('--description ', 'book description') .option('--public ', 'visibility: 0 private, 1 public, 2 org-only', parsePublic) .option('--enhanced-privacy', 'restrict access to team admins only (增强私密性)') .action( @@ -160,23 +160,23 @@ export function registerRepoCommands(program: Command): void { } ) => { const ctx = getContext(create); - const body: CreateRepoBody = { name: opts.name, slug: opts.slug }; + const body: CreateBookBody = { name: opts.name, slug: opts.slug }; if (opts.description !== undefined) body.description = opts.description; if (opts.public !== undefined) body.public = opts.public; if (opts.enhancedPrivacy) body.enhancedPrivacy = true; - const book = await createRepo(ctx.http, opts.group ? 'group' : 'user', login, body); + const book = await createBook(ctx.http, opts.group ? 'group' : 'user', login, body); if (ctx.json) { printJson(book); return; } - printOk(`Created repo ${repoLabel(book)} (id: ${book.id})`); + printOk(`Created book ${bookLabel(book)} (id: ${book.id})`); } ); - const update = repo + const update = book .command('update') - .description('Update the settings of a repo') - .argument('', 'repo id or group/slug namespace') + .description('Update the settings of a book') + .argument('', 'book id or group/slug namespace') .option('--name ', 'new name') .option('--slug ', 'new path (slug)') .option('--description ', 'new description') @@ -184,11 +184,11 @@ export function registerRepoCommands(program: Command): void { .option('--toc ', 'replace the table of contents (Markdown list)') .action( async ( - repoArg: string, + bookArg: string, opts: { name?: string; slug?: string; description?: string; public?: number; toc?: string } ) => { - const ref = parseRepoRef(repoArg); - const body: UpdateRepoBody = {}; + const ref = parseBookRef(bookArg); + const body: UpdateBookBody = {}; if (opts.name !== undefined) body.name = opts.name; if (opts.slug !== undefined) body.slug = opts.slug; if (opts.description !== undefined) body.description = opts.description; @@ -200,29 +200,29 @@ export function registerRepoCommands(program: Command): void { ); } const ctx = getContext(update); - const book = await updateRepo(ctx.http, ref, body); + const book = await updateBook(ctx.http, ref, body); if (ctx.json) { printJson(book); return; } - printOk(`Updated repo ${repoLabel(book)}`); + printOk(`Updated book ${bookLabel(book)}`); } ); - const del = repo + const del = book .command('delete') - .description('Delete a repo') - .argument('', 'repo id or group/slug namespace') + .description('Delete a book') + .argument('', 'book id or group/slug namespace') .option('--yes', 'skip the confirmation prompt') - .action(async (repoArg: string, opts: { yes?: boolean }) => { - const ref = parseRepoRef(repoArg); - await confirmDestructive(`delete repo ${repoArg}`, Boolean(opts.yes)); + .action(async (bookArg: string, opts: { yes?: boolean }) => { + const ref = parseBookRef(bookArg); + await confirmDestructive(`delete book ${bookArg}`, Boolean(opts.yes)); const ctx = getContext(del); - const book = await deleteRepo(ctx.http, ref); + const book = await deleteBook(ctx.http, ref); if (ctx.json) { printJson(book); return; } - printOk(`Deleted repo ${repoLabel(book)}`); + printOk(`Deleted book ${bookLabel(book)}`); }); } diff --git a/src/commands/doc.ts b/src/commands/doc.ts index 7c3a2b0..82e2c19 100644 --- a/src/commands/doc.ts +++ b/src/commands/doc.ts @@ -4,7 +4,7 @@ import { getContext } from '../context.js'; import { UsageError } from '../errors.js'; import { confirmDestructive } from '../confirm.js'; import { printJson, printOk, printRecord, printTable } from '../output.js'; -import { parseRepoRef } from '../client/repo-ref.js'; +import { parseBookRef } from '../client/book-ref.js'; import { fetchAllPages } from '../client/paginate.js'; import { createDoc, @@ -20,7 +20,7 @@ import { } from '../client/api/doc.js'; import type { V2DocDetail } from '../client/types.js'; -const REPO_ARG_HELP = 'repo id or group/slug namespace'; +const BOOK_ARG_HELP = 'book id or group/slug namespace'; const DOC_META_FIELDS = [ 'id', @@ -132,8 +132,8 @@ export function registerDocCommands(program: Command): void { const list = doc.command('list'); list - .description('List the docs of a repo') - .argument('', REPO_ARG_HELP) + .description('List the docs of a book') + .argument('', BOOK_ARG_HELP) .option('--offset ', 'pagination offset') .option('--limit ', 'page size (max 100)') .option('--all', 'fetch all pages (overrides --offset/--limit)') @@ -156,7 +156,7 @@ export function registerDocCommands(program: Command): void { } ) => { const ctx = getContext(list); - const ref = parseRepoRef(repo); + const ref = parseBookRef(repo); // Validate unconditionally so `--all --limit banana` still exits 2; // with --all the paginator overrides these values. const offset = parseIntFlag(opts.offset, '--offset'); @@ -194,7 +194,7 @@ export function registerDocCommands(program: Command): void { const ctx = getContext(get); let detail: V2DocDetail; if (target.length === 2) { - detail = await getDoc(ctx.http, parseRepoRef(target[0]), target[1]); + detail = await getDoc(ctx.http, parseBookRef(target[0]), target[1]); } else if (target.length === 1) { if (!/^\d+$/.test(target[0])) { throw new UsageError( @@ -219,7 +219,7 @@ export function registerDocCommands(program: Command): void { const create = doc.command('create'); create .description('Create a doc in a repo') - .argument('', REPO_ARG_HELP) + .argument('', BOOK_ARG_HELP) .requiredOption('--title ', 'doc title (required)') .option('--slug <slug>', 'doc slug (URL path)') .option('--body <content>', 'doc body content') @@ -234,7 +234,7 @@ export function registerDocCommands(program: Command): void { 'a doc body is required — pass --body <content> or --body-file <path>' ); } - const created = await createDoc(ctx.http, parseRepoRef(repo), payload); + const created = await createDoc(ctx.http, parseBookRef(repo), payload); if (ctx.json) { printJson(created); return; @@ -245,7 +245,7 @@ export function registerDocCommands(program: Command): void { const update = doc.command('update'); update .description('Update a doc (only the given fields are changed)') - .argument('<repo>', REPO_ARG_HELP) + .argument('<book>', BOOK_ARG_HELP) .argument('<doc>', 'doc slug or id') .option('--title <title>', 'doc title') .option('--slug <slug>', 'doc slug (URL path)') @@ -261,7 +261,7 @@ export function registerDocCommands(program: Command): void { 'nothing to update — pass at least one of --title/--slug/--body/--body-file/--format/--public' ); } - const updated = await updateDoc(ctx.http, parseRepoRef(repo), docRef, payload); + const updated = await updateDoc(ctx.http, parseBookRef(repo), docRef, payload); if (ctx.json) { printJson(updated); return; @@ -272,13 +272,13 @@ export function registerDocCommands(program: Command): void { const del = doc.command('delete'); del .description('Delete a doc') - .argument('<repo>', REPO_ARG_HELP) + .argument('<book>', BOOK_ARG_HELP) .argument('<doc>', 'doc slug or id') .option('--yes', 'skip the confirmation prompt') .action(async (repo: string, docRef: string, opts: { yes?: boolean }) => { await confirmDestructive(`delete doc ${repo}/${docRef}`, Boolean(opts.yes)); const ctx = getContext(del); - const deleted = await deleteDoc(ctx.http, parseRepoRef(repo), docRef); + const deleted = await deleteDoc(ctx.http, parseBookRef(repo), docRef); if (ctx.json) { printJson(deleted); return; diff --git a/src/commands/search.ts b/src/commands/search.ts index 6f192df..7cca108 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -4,7 +4,9 @@ import { UsageError } from '../errors.js'; import { printJson, printTable } from '../output.js'; import { search } from '../client/api/search.js'; -const SEARCH_TYPES = ['doc', 'repo']; +// CLI surface says `book` (知识库); the wire enum is doc|repo, and `repo` stays +// accepted as a compatibility alias mapped to the same wire value. +const SEARCH_TYPES = ['doc', 'book', 'repo']; // Commander-level validation (makeOptionMandatory/choices/argParser errors) exits // via process.exit on subcommands, bypassing runCli's exit-code contract — so @@ -22,22 +24,22 @@ function pageFlag(value: string): number { export function registerSearchCommands(program: Command): void { const cmd = program .command('search') - .description('Search docs or repos') + .description('Search docs or books') .argument('<query>', 'search keywords') - .option('--type <type>', 'what to search for: doc or repo (required)') + .option('--type <type>', 'what to search for: doc or book (required)') .option('--scope <ns>', 'restrict to a group or group/repo namespace') .option('--creator <login>', 'only results created by this user') .option('--page <n>', 'page number (page size is fixed at 20)', pageFlag); cmd.action(async (query: string) => { const opts = cmd.opts<{ type?: string; scope?: string; creator?: string; page?: number }>(); - if (!opts.type) throw new UsageError('--type <doc|repo> is required'); + if (!opts.type) throw new UsageError('--type <doc|book> is required'); if (!SEARCH_TYPES.includes(opts.type)) { - throw new UsageError(`invalid --type "${opts.type}" — expected doc or repo`); + throw new UsageError(`invalid --type "${opts.type}" — expected doc or book`); } const ctx = getContext(cmd); const results = await search(ctx.http, { q: query, - type: opts.type as 'doc' | 'repo', + type: opts.type === 'book' ? 'repo' : (opts.type as 'doc' | 'repo'), scope: opts.scope, creator: opts.creator, page: opts.page, diff --git a/src/commands/stats.ts b/src/commands/stats.ts index 01bc9d9..8b78cf6 100644 --- a/src/commands/stats.ts +++ b/src/commands/stats.ts @@ -236,7 +236,7 @@ export function registerStatsCommands(program: Command): void { stats.command('docs <login>').description('Per-doc statistics for a group'), DOC_SORT_FIELDS ) - .option('--book-id <id>', 'only docs in this book (repo id)', parsePositiveInt('--book-id')) + .option('--book-id <id>', 'only docs in this book (book id)', parsePositiveInt('--book-id')) .action(async (login: string) => { await runList( docsCmd, diff --git a/src/commands/toc.ts b/src/commands/toc.ts index 4382d2f..f032f09 100644 --- a/src/commands/toc.ts +++ b/src/commands/toc.ts @@ -2,7 +2,7 @@ import { Command, InvalidArgumentError, Option } from 'commander'; import { getContext } from '../context.js'; import { UsageError } from '../errors.js'; import { dim, printJson, printOk } from '../output.js'; -import { parseRepoRef } from '../client/repo-ref.js'; +import { parseBookRef } from '../client/book-ref.js'; import { getToc, updateToc, type TocUpdateBody } from '../client/api/toc.js'; import type { V2TocItem } from '../client/types.js'; @@ -90,15 +90,15 @@ function validateTocUpdate(opts: TocUpdateOptions): void { } export function registerTocCommands(program: Command): void { - const toc = program.command('toc').description('Manage the table of contents (目录) of a repo'); + const toc = program.command('toc').description('Manage the table of contents (目录) of a book'); const get = toc .command('get') - .description('Show the toc of a repo as an indented tree') - .argument('<repo>', 'repo id or group/slug namespace') - .action(async (repoArg: string) => { + .description('Show the toc of a book as an indented tree') + .argument('<book>', 'book id or group/slug namespace') + .action(async (bookArg: string) => { const ctx = getContext(get); - const items = await getToc(ctx.http, parseRepoRef(repoArg)); + const items = await getToc(ctx.http, parseBookRef(bookArg)); if (ctx.json) { printJson(items); return; @@ -108,8 +108,8 @@ export function registerTocCommands(program: Command): void { const update = toc .command('update') - .description('Update the toc of a repo (append/prepend/edit/remove nodes)') - .argument('<repo>', 'repo id or group/slug namespace') + .description('Update the toc of a book (append/prepend/edit/remove nodes)') + .argument('<book>', 'book id or group/slug namespace') .addOption( new Option( '--action <action>', @@ -147,7 +147,7 @@ export function registerTocCommands(program: Command): void { .addOption( new Option('--visible <n>', 'node visibility (0: hidden, 1: visible)').choices(['0', '1']) ) - .action(async (repoArg: string) => { + .action(async (bookArg: string) => { const opts = update.opts<TocUpdateOptions>(); validateTocUpdate(opts); const ctx = getContext(update); @@ -164,7 +164,7 @@ export function registerTocCommands(program: Command): void { ...(opts.openWindow !== undefined && { open_window: Number(opts.openWindow) }), ...(opts.visible !== undefined && { visible: Number(opts.visible) }), }; - const items = await updateToc(ctx.http, parseRepoRef(repoArg), body); + const items = await updateToc(ctx.http, parseBookRef(bookArg), body); if (ctx.json) { printJson(items); return; diff --git a/tests/client/book-ref.test.ts b/tests/client/book-ref.test.ts new file mode 100644 index 0000000..069b79c --- /dev/null +++ b/tests/client/book-ref.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { parseBookRef, bookBasePath } from '../../src/client/book-ref.js'; +import { UsageError } from '../../src/errors.js'; + +describe('parseBookRef', () => { + it('parses a numeric id', () => { + expect(parseBookRef('123456')).toEqual({ kind: 'id', id: '123456' }); + }); + + it('parses a namespace', () => { + expect(parseBookRef('yuque/help')).toEqual({ kind: 'namespace', group: 'yuque', slug: 'help' }); + }); + + it('rejects malformed references', () => { + expect(() => parseBookRef('a/b/c')).toThrow(UsageError); + expect(() => parseBookRef('/slug')).toThrow(UsageError); + expect(() => parseBookRef('group/')).toThrow(UsageError); + }); +}); + +describe('bookBasePath', () => { + it('builds id and namespace paths', () => { + expect(bookBasePath(parseBookRef('42'))).toBe('/repos/42'); + expect(bookBasePath(parseBookRef('yuque/help'))).toBe('/repos/yuque/help'); + }); + + it('escapes URL-unsafe characters', () => { + expect(bookBasePath(parseBookRef('团队/知识 库'))).toBe( + '/repos/%E5%9B%A2%E9%98%9F/%E7%9F%A5%E8%AF%86%20%E5%BA%93' + ); + }); +}); diff --git a/tests/client/repo-ref.test.ts b/tests/client/repo-ref.test.ts deleted file mode 100644 index f9bd362..0000000 --- a/tests/client/repo-ref.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { parseRepoRef, repoBasePath } from '../../src/client/repo-ref.js'; -import { UsageError } from '../../src/errors.js'; - -describe('parseRepoRef', () => { - it('parses a numeric id', () => { - expect(parseRepoRef('123456')).toEqual({ kind: 'id', id: '123456' }); - }); - - it('parses a namespace', () => { - expect(parseRepoRef('yuque/help')).toEqual({ kind: 'namespace', group: 'yuque', slug: 'help' }); - }); - - it('rejects malformed references', () => { - expect(() => parseRepoRef('a/b/c')).toThrow(UsageError); - expect(() => parseRepoRef('/slug')).toThrow(UsageError); - expect(() => parseRepoRef('group/')).toThrow(UsageError); - }); -}); - -describe('repoBasePath', () => { - it('builds id and namespace paths', () => { - expect(repoBasePath(parseRepoRef('42'))).toBe('/repos/42'); - expect(repoBasePath(parseRepoRef('yuque/help'))).toBe('/repos/yuque/help'); - }); - - it('escapes URL-unsafe characters', () => { - expect(repoBasePath(parseRepoRef('团队/知识 库'))).toBe( - '/repos/%E5%9B%A2%E9%98%9F/%E7%9F%A5%E8%AF%86%20%E5%BA%93' - ); - }); -}); diff --git a/tests/commands/auth-user-search.test.ts b/tests/commands/auth-user-search.test.ts index b7a9663..e452a79 100644 --- a/tests/commands/auth-user-search.test.ts +++ b/tests/commands/auth-user-search.test.ts @@ -226,7 +226,7 @@ describe('auth / user / search commands', () => { it('prints the full result list with --json', async () => { request.mockResolvedValueOnce({ data: { data: results } }); - const code = await runCli(['node', 'yuque', 'search', 'guide', '--type', 'repo', '--json']); + const code = await runCli(['node', 'yuque', 'search', 'guide', '--type', 'book', '--json']); expect(code).toBe(0); expect(request).toHaveBeenCalledWith( expect.objectContaining({ params: { q: 'guide', type: 'repo' } }) diff --git a/tests/commands/repo.test.ts b/tests/commands/book.test.ts similarity index 82% rename from tests/commands/repo.test.ts rename to tests/commands/book.test.ts index bc92a5c..6f59615 100644 --- a/tests/commands/repo.test.ts +++ b/tests/commands/book.test.ts @@ -38,7 +38,7 @@ const BOOK = { _extra: 'kept in --json output', }; -describe('repo commands', () => { +describe('book commands', () => { const request = vi.fn(); let stdoutChunks: string[] = []; let stderrChunks: string[] = []; @@ -67,10 +67,10 @@ describe('repo commands', () => { vi.restoreAllMocks(); }); - describe('repo list', () => { + describe('book list', () => { it('lists user repos and prints a table', async () => { request.mockResolvedValueOnce(envelope([BOOK])); - await expect(runCli(argv('repo', 'list', 'yuque'))).resolves.toBe(0); + await expect(runCli(argv('book', 'list', 'yuque'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'get', url: '/users/yuque/repos', @@ -84,7 +84,7 @@ describe('repo commands', () => { it('lists group repos with filter and pagination params', async () => { request.mockResolvedValueOnce(envelope([])); - const args = ['repo', 'list', 'mygroup', '--group']; + const args = ['book', 'list', 'mygroup', '--group']; args.push('--type', 'Book', '--offset', '10', '--limit', '20'); await expect(runCli(argv(...args))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ @@ -97,7 +97,7 @@ describe('repo commands', () => { it('treats --type all as no server-side filter', async () => { request.mockResolvedValueOnce(envelope([])); - await expect(runCli(argv('repo', 'list', 'yuque', '--type', 'all'))).resolves.toBe(0); + await expect(runCli(argv('book', 'list', 'yuque', '--type', 'all'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith(expect.objectContaining({ params: {} })); }); @@ -108,7 +108,7 @@ describe('repo commands', () => { namespace: `yuque/r${i}`, })); request.mockResolvedValueOnce(envelope(fullPage)).mockResolvedValueOnce(envelope([BOOK])); - await expect(runCli(argv('repo', 'list', 'yuque', '--all', '--json'))).resolves.toBe(0); + await expect(runCli(argv('book', 'list', 'yuque', '--all', '--json'))).resolves.toBe(0); expect(request).toHaveBeenNthCalledWith( 1, expect.objectContaining({ url: '/users/yuque/repos', params: { offset: 0, limit: 100 } }) @@ -121,21 +121,21 @@ describe('repo commands', () => { }); it('rejects an unknown --type', async () => { - await expect(runCli(argv('repo', 'list', 'yuque', '--type', 'Wiki'))).resolves.toBe(2); + await expect(runCli(argv('book', 'list', 'yuque', '--type', 'Wiki'))).resolves.toBe(2); expect(request).not.toHaveBeenCalled(); expect(stderrText()).toContain('--type'); }); it('rejects a non-numeric --offset', async () => { - await expect(runCli(argv('repo', 'list', 'yuque', '--offset', 'abc'))).resolves.toBe(2); + await expect(runCli(argv('book', 'list', 'yuque', '--offset', 'abc'))).resolves.toBe(2); expect(request).not.toHaveBeenCalled(); }); }); - describe('repo get', () => { + describe('book get', () => { it('gets a repo by id and prints the full payload with --json', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - await expect(runCli(argv('repo', 'get', '42', '--json'))).resolves.toBe(0); + await expect(runCli(argv('book', 'get', '42', '--json'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'get', url: '/repos/42', @@ -147,22 +147,22 @@ describe('repo commands', () => { it('gets a repo by namespace and prints a record', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - await expect(runCli(argv('repo', 'get', 'yuque/help'))).resolves.toBe(0); + await expect(runCli(argv('book', 'get', 'yuque/help'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith(expect.objectContaining({ url: '/repos/yuque/help' })); expect(stdoutText()).toContain('帮助中心'); expect(stdoutText()).not.toContain('_extra'); }); it('rejects a malformed repo reference', async () => { - await expect(runCli(argv('repo', 'get', 'not-a-ref'))).resolves.toBe(2); + await expect(runCli(argv('book', 'get', 'not-a-ref'))).resolves.toBe(2); expect(request).not.toHaveBeenCalled(); }); }); - describe('repo create', () => { - it('creates a user repo with name and slug', async () => { + describe('book create', () => { + it('creates a user book with name and slug', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - const args = ['repo', 'create', 'yuque', '--name', '帮助中心', '--slug', 'help']; + const args = ['book', 'create', 'yuque', '--name', '帮助中心', '--slug', 'help']; await expect(runCli(argv(...args))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'post', @@ -170,12 +170,12 @@ describe('repo commands', () => { params: undefined, data: { name: '帮助中心', slug: 'help' }, }); - expect(stdoutText()).toContain('Created repo yuque/help'); + expect(stdoutText()).toContain('Created book yuque/help'); }); - it('creates a group repo with all optional fields', async () => { + it('creates a group book with all optional fields', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - const args = ['repo', 'create', 'mygroup', '--group', '--name', 'Docs', '--slug', 'docs']; + const args = ['book', 'create', 'mygroup', '--group', '--name', 'Docs', '--slug', 'docs']; args.push('--description', 'd', '--public', '2', '--enhanced-privacy', '--json'); await expect(runCli(argv(...args))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ @@ -188,14 +188,14 @@ describe('repo commands', () => { }); it('rejects a --limit above the spec cap on repo list', async () => { - const args = ['repo', 'list', 'yuque', '--limit', '101']; + const args = ['book', 'list', 'yuque', '--limit', '101']; await expect(runCli(argv(...args))).resolves.toBe(2); expect(request).not.toHaveBeenCalled(); }); it('passes --filter-by-ability through to the query', async () => { request.mockResolvedValueOnce(envelope([BOOK])); - const args = ['repo', 'list', 'yuque', '--filter-by-ability', 'create_doc', '--json']; + const args = ['book', 'list', 'yuque', '--filter-by-ability', 'create_doc', '--json']; await expect(runCli(argv(...args))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'get', @@ -206,10 +206,10 @@ describe('repo commands', () => { }); }); - describe('repo update', () => { + describe('book update', () => { it('sends only the provided fields', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - const args = ['repo', 'update', '42', '--name', 'New name', '--public', '0']; + const args = ['book', 'update', '42', '--name', 'New name', '--public', '0']; await expect(runCli(argv(...args))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'put', @@ -217,34 +217,34 @@ describe('repo commands', () => { params: undefined, data: { name: 'New name', public: 0 }, }); - expect(stdoutText()).toContain('Updated repo yuque/help'); + expect(stdoutText()).toContain('Updated book yuque/help'); }); it('rejects an update with no fields', async () => { - await expect(runCli(argv('repo', 'update', '42'))).resolves.toBe(2); + await expect(runCli(argv('book', 'update', '42'))).resolves.toBe(2); expect(request).not.toHaveBeenCalled(); expect(stderrText()).toContain('Nothing to update'); }); }); - describe('repo delete', () => { + describe('book delete', () => { it('deletes with --yes without prompting', async () => { request.mockResolvedValueOnce(envelope(BOOK)); - await expect(runCli(argv('repo', 'delete', 'yuque/help', '--yes'))).resolves.toBe(0); + await expect(runCli(argv('book', 'delete', 'yuque/help', '--yes'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ method: 'delete', url: '/repos/yuque/help', params: undefined, data: undefined, }); - expect(stdoutText()).toContain('Deleted repo yuque/help'); + expect(stdoutText()).toContain('Deleted book yuque/help'); }); it('refuses without --yes when stdin is not a TTY', async () => { const originalIsTTY = process.stdin.isTTY; process.stdin.isTTY = false; try { - await expect(runCli(argv('repo', 'delete', 'yuque/help'))).resolves.toBe(2); + await expect(runCli(argv('book', 'delete', 'yuque/help'))).resolves.toBe(2); } finally { process.stdin.isTTY = originalIsTTY; } diff --git a/tests/e2e/cli.e2e.test.ts b/tests/e2e/cli.e2e.test.ts index 4ae3418..1f36561 100644 --- a/tests/e2e/cli.e2e.test.ts +++ b/tests/e2e/cli.e2e.test.ts @@ -108,7 +108,7 @@ describeRead('read paths (personal token)', () => { for (const candidate of candidates) { // Only Book repos serve the /docs and /toc endpoints — a Design (board) // repo as the account's first repo would 404 them, so filter server-side. - const res = pc(['repo', 'list', candidate, '--type', 'Book', '--json']); + const res = pc(['book', 'list', candidate, '--type', 'Book', '--json']); if (res.code === 0) { owner = candidate; repos = JSON.parse(res.stdout); @@ -130,7 +130,7 @@ describeRead('read paths (personal token)', () => { // Later runs discover it through the Book listing above and skip this. const created = okJson( pc([ - 'repo', + 'book', 'create', owner, '--name', @@ -182,12 +182,12 @@ describeRead('read paths (personal token)', () => { expect(Array.isArray(okJson(pc(['search', 'test', '--type', 'doc', '--json'])))).toBe(true); }); - it('repo list --json returns an array', () => { - expect(Array.isArray(okJson(pc(['repo', 'list', owner, '--json'])))).toBe(true); + it('book list --json returns an array', () => { + expect(Array.isArray(okJson(pc(['book', 'list', owner, '--json'])))).toBe(true); }); - it('repo get --json returns the repo', () => { - expect(okJson(pc(['repo', 'get', readRepo, '--json'])).id).toBeTruthy(); + it('book get --json returns the book', () => { + expect(okJson(pc(['book', 'get', readRepo, '--json'])).id).toBeTruthy(); }); it('doc list --json returns an array', () => { @@ -214,8 +214,8 @@ describeError('error contract', () => { expect(res.code).toBe(3); }); - it('a nonexistent repo exits 4 (not found)', () => { - expect(pc(['repo', 'get', '999999999']).code).toBe(4); + it('a nonexistent book exits 4 (not found)', () => { + expect(pc(['book', 'get', '999999999']).code).toBe(4); }); }); @@ -308,7 +308,7 @@ describeRepoLifecycle('repo lifecycle (ephemeral repo, local only)', () => { const stamp = `${Date.now()}`; const created = okJson( pc([ - 'repo', + 'book', 'create', login, '--name', @@ -321,9 +321,9 @@ describeRepoLifecycle('repo lifecycle (ephemeral repo, local only)', () => { expect(created.id).toBeTruthy(); const id = String(created.id); try { - expect(okJson(pc(['repo', 'get', id, '--json'])).id).toBeTruthy(); + expect(okJson(pc(['book', 'get', id, '--json'])).id).toBeTruthy(); } finally { - expect(pc(['repo', 'delete', id, '--yes']).code).toBe(0); + expect(pc(['book', 'delete', id, '--yes']).code).toBe(0); } }); }); diff --git a/tests/e2e/errors.e2e.test.ts b/tests/e2e/errors.e2e.test.ts index 6623af2..c610f9c 100644 --- a/tests/e2e/errors.e2e.test.ts +++ b/tests/e2e/errors.e2e.test.ts @@ -67,7 +67,7 @@ describe('retry semantics through the real binary', () => { status: 502, body: { message: 'bad gateway' }, }); - const result = await runYuque(['repo', 'create', 'me', '--name', 'n', '--slug', 's'], { host }); + const result = await runYuque(['book', 'create', 'me', '--name', 'n', '--slug', 's'], { host }); expect(result.code).toBe(1); expect(server.requestsFor('POST', '/api/v2/users/me/repos')).toHaveLength(1); }); diff --git a/tests/e2e/read-commands.e2e.test.ts b/tests/e2e/read-commands.e2e.test.ts index f3b35db..fdd3f20 100644 --- a/tests/e2e/read-commands.e2e.test.ts +++ b/tests/e2e/read-commands.e2e.test.ts @@ -59,7 +59,7 @@ describe('auth & user', () => { }); }); -describe('search & repo', () => { +describe('search & book', () => { it('search sends q/type and renders a table', async () => { server.route('GET', '/api/v2/search', { body: { data: [{ id: 1, type: 'doc', title: '灰度发布', url: '/x/y' }] }, @@ -70,7 +70,7 @@ describe('search & repo', () => { expect(server.requests[0].query).toEqual({ q: '灰度发布', type: 'doc' }); }); - it('repo list --group --all drains pages and prints a JSON array', async () => { + it('book list --group --all drains pages and prints a JSON array', async () => { const fullPage = Array.from({ length: 100 }, (_, i) => ({ id: i, slug: `r${i}`, @@ -81,7 +81,7 @@ describe('search & repo', () => { ? { body: { data: fullPage } } : { body: { data: [{ id: 100, slug: 'last', name: 'Last' }] } } ); - const result = await runYuque(['repo', 'list', 'eng', '--group', '--all', '--json'], { host }); + const result = await runYuque(['book', 'list', 'eng', '--group', '--all', '--json'], { host }); expect(result.code).toBe(0); expect(JSON.parse(result.stdout)).toHaveLength(101); const offsets = server @@ -90,11 +90,11 @@ describe('search & repo', () => { expect(offsets).toEqual(['0', '100']); }); - it('repo get resolves an owner/slug namespace to the namespace path', async () => { + it('book get resolves an owner/slug namespace to the namespace path', async () => { server.route('GET', '/api/v2/repos/yuque/help', { body: { data: { id: 42, name: '帮助中心', namespace: 'yuque/help', slug: 'help' } }, }); - const result = await runYuque(['repo', 'get', 'yuque/help'], { host }); + const result = await runYuque(['book', 'get', 'yuque/help'], { host }); expect(result.code).toBe(0); expect(result.stdout).toContain('帮助中心'); }); diff --git a/tests/e2e/write-commands.e2e.test.ts b/tests/e2e/write-commands.e2e.test.ts index 625368a..e398e68 100644 --- a/tests/e2e/write-commands.e2e.test.ts +++ b/tests/e2e/write-commands.e2e.test.ts @@ -17,14 +17,14 @@ afterEach(async () => { await server.stop(); }); -describe('repo writes', () => { - it('repo create posts the spec body fields', async () => { +describe('book writes', () => { + it('book create posts the spec body fields', async () => { server.route('POST', '/api/v2/users/me/repos', { body: { data: { id: 1, slug: 'notes', namespace: 'me/notes', name: '笔记' } }, }); const result = await runYuque( [ - 'repo', + 'book', 'create', 'me', '--name', @@ -38,7 +38,7 @@ describe('repo writes', () => { { host } ); expect(result.code).toBe(0); - expect(result.stdout).toContain('Created repo me/notes'); + expect(result.stdout).toContain('Created book me/notes'); expect(server.requests[0].body).toEqual({ name: '笔记', slug: 'notes', @@ -47,9 +47,9 @@ describe('repo writes', () => { }); }); - it('repo create rejects an out-of-enum --public locally (exit 2, no request)', async () => { + it('book create rejects an out-of-enum --public locally (exit 2, no request)', async () => { const result = await runYuque( - ['repo', 'create', 'me', '--name', 'n', '--slug', 's', '--public', '3'], + ['book', 'create', 'me', '--name', 'n', '--slug', 's', '--public', '3'], { host, } @@ -59,17 +59,17 @@ describe('repo writes', () => { expect(server.requests).toHaveLength(0); }); - it('repo delete requires --yes when stdin is not a TTY', async () => { - const refused = await runYuque(['repo', 'delete', '42'], { host }); + it('book delete requires --yes when stdin is not a TTY', async () => { + const refused = await runYuque(['book', 'delete', '42'], { host }); expect(refused.code).toBe(2); expect(server.requests).toHaveLength(0); server.route('DELETE', '/api/v2/repos/42', { body: { data: { id: 42, slug: 'gone', namespace: 'me/gone' } }, }); - const deleted = await runYuque(['repo', 'delete', '42', '--yes'], { host }); + const deleted = await runYuque(['book', 'delete', '42', '--yes'], { host }); expect(deleted.code).toBe(0); - expect(deleted.stdout).toContain('Deleted repo me/gone'); + expect(deleted.stdout).toContain('Deleted book me/gone'); }); }); diff --git a/tests/spec-coverage.test.ts b/tests/spec-coverage.test.ts index d1235ab..1c1e196 100644 --- a/tests/spec-coverage.test.ts +++ b/tests/spec-coverage.test.ts @@ -140,52 +140,52 @@ const OPERATION_TO_COMMANDS: Record<string, OperationMapping> = { 'repo_api_v2_repo_list-by_group': { method: 'get', path: '/api/v2/groups/{login}/repos', - commands: ['repo list'], + commands: ['book list'], }, 'repo_api_v2_repo_create-by_group': { method: 'post', path: '/api/v2/groups/{login}/repos', - commands: ['repo create'], + commands: ['book create'], }, repo_api_v2_repo_list: { method: 'get', path: '/api/v2/users/{login}/repos', - commands: ['repo list'], + commands: ['book list'], }, repo_api_v2_repo_create: { method: 'post', path: '/api/v2/users/{login}/repos', - commands: ['repo create'], + commands: ['book create'], }, 'repo_api_v2_repo_show-by_id': { method: 'get', path: '/api/v2/repos/{book_id}', - commands: ['repo get'], + commands: ['book get'], }, 'repo_api_v2_repo_update-by_id': { method: 'put', path: '/api/v2/repos/{book_id}', - commands: ['repo update'], + commands: ['book update'], }, 'repo_api_v2_repo_destroy-by_id': { method: 'delete', path: '/api/v2/repos/{book_id}', - commands: ['repo delete'], + commands: ['book delete'], }, repo_api_v2_repo_show: { method: 'get', path: '/api/v2/repos/{group_login}/{book_slug}', - commands: ['repo get'], + commands: ['book get'], }, repo_api_v2_repo_update: { method: 'put', path: '/api/v2/repos/{group_login}/{book_slug}', - commands: ['repo update'], + commands: ['book update'], }, repo_api_v2_repo_destroy: { method: 'delete', path: '/api/v2/repos/{group_login}/{book_slug}', - commands: ['repo delete'], + commands: ['book delete'], }, statistic_api_v2_statistic_all: { method: 'get', @@ -218,11 +218,11 @@ const EXPECTED_LEAF_COMMANDS = [ 'group members', 'group member set', 'group member remove', - 'repo list', - 'repo get', - 'repo create', - 'repo update', - 'repo delete', + 'book list', + 'book get', + 'book create', + 'book update', + 'book delete', 'doc list', 'doc get', 'doc create', From cc52b2a89c39ad34f2402efa02e025e83daf4051 Mon Sep 17 00:00:00 2001 From: cwg <1227646458@qq.com> Date: Thu, 23 Jul 2026 19:54:32 +0900 Subject: [PATCH 2/3] fix: sweep remaining repo terminology from doc/search help, errors, and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the Codex review note on PR #1: doc get/create help and usage errors, search --scope help, internal params/labels — all now say book. The wire enum comment in search.ts is intentional and stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- src/client/api/doc.ts | 2 +- src/client/book-ref.ts | 2 +- src/commands/doc.ts | 34 +++++++++++++++++----------------- src/commands/search.ts | 2 +- tests/commands/doc.test.ts | 12 ++++++------ tests/e2e/cli.e2e.test.ts | 6 +++--- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/client/api/doc.ts b/src/client/api/doc.ts index 809195d..4b25585 100644 --- a/src/client/api/doc.ts +++ b/src/client/api/doc.ts @@ -44,7 +44,7 @@ export async function getDoc(http: YuqueHttp, repo: BookRef, doc: string): Promi return res.data; } -/** Fetch a doc by its globally unique numeric id (no repo needed). */ +/** Fetch a doc by its globally unique numeric id (no book needed). */ export async function getDocById(http: YuqueHttp, id: number): Promise<V2DocDetail> { const res = await http.get<ApiEnvelope<V2DocDetail>>(`/repos/docs/${id}`); return res.data; diff --git a/src/client/book-ref.ts b/src/client/book-ref.ts index 6d9a613..c0773f6 100644 --- a/src/client/book-ref.ts +++ b/src/client/book-ref.ts @@ -1,7 +1,7 @@ import { UsageError } from '../errors.js'; /** - * A repo (知识库) reference given on the command line: either a numeric id + * A book (知识库) reference given on the command line: either a numeric id * or a `group/slug` namespace. Both map onto the same /repos/... URL shape. */ export type BookRef = diff --git a/src/commands/doc.ts b/src/commands/doc.ts index 82e2c19..69dd848 100644 --- a/src/commands/doc.ts +++ b/src/commands/doc.ts @@ -145,7 +145,7 @@ export function registerDocCommands(program: Command): void { ) .action( async ( - repo: string, + book: string, opts: { offset?: string; limit?: string; @@ -156,7 +156,7 @@ export function registerDocCommands(program: Command): void { } ) => { const ctx = getContext(list); - const ref = parseBookRef(repo); + const ref = parseBookRef(book); // Validate unconditionally so `--all --limit banana` still exits 2; // with --all the paginator overrides these values. const offset = parseIntFlag(opts.offset, '--offset'); @@ -187,8 +187,8 @@ export function registerDocCommands(program: Command): void { const get = doc.command('get'); get - .description('Show a doc: `doc get <repo> <doc>` or `doc get <doc-id>`') - .argument('<target...>', '<repo> <doc slug or id>, or a single global numeric doc id') + .description('Show a doc: `doc get <book> <doc>` or `doc get <doc-id>`') + .argument('<target...>', '<book> <doc slug or id>, or a single global numeric doc id') .option('--meta', 'print metadata instead of the body') .action(async (target: string[], opts: { meta?: boolean }) => { const ctx = getContext(get); @@ -198,12 +198,12 @@ export function registerDocCommands(program: Command): void { } else if (target.length === 1) { if (!/^\d+$/.test(target[0])) { throw new UsageError( - `"${target[0]}" is not a numeric doc id — pass <repo> <doc> or a global numeric doc id` + `"${target[0]}" is not a numeric doc id — pass <book> <doc> or a global numeric doc id` ); } detail = await getDocById(ctx.http, Number(target[0])); } else { - throw new UsageError('doc get takes <repo> <doc> or a single numeric <doc-id>'); + throw new UsageError('doc get takes <book> <doc> or a single numeric <doc-id>'); } if (ctx.json) { printJson(detail); @@ -218,7 +218,7 @@ export function registerDocCommands(program: Command): void { const create = doc.command('create'); create - .description('Create a doc in a repo') + .description('Create a doc in a book') .argument('<book>', BOOK_ARG_HELP) .requiredOption('--title <title>', 'doc title (required)') .option('--slug <slug>', 'doc slug (URL path)') @@ -226,7 +226,7 @@ export function registerDocCommands(program: Command): void { .option('--body-file <path>', 'read the doc body from a file') .addOption(formatOption()) .addOption(publicOption()) - .action(async (repo: string, opts: DocWriteOpts) => { + .action(async (book: string, opts: DocWriteOpts) => { const ctx = getContext(create); const payload = buildWritePayload(opts); if (payload.body === undefined) { @@ -234,12 +234,12 @@ export function registerDocCommands(program: Command): void { 'a doc body is required — pass --body <content> or --body-file <path>' ); } - const created = await createDoc(ctx.http, parseBookRef(repo), payload); + const created = await createDoc(ctx.http, parseBookRef(book), payload); if (ctx.json) { printJson(created); return; } - printOk(`Created doc ${repo}/${created.slug} (id ${created.id}): ${created.title}`); + printOk(`Created doc ${book}/${created.slug} (id ${created.id}): ${created.title}`); }); const update = doc.command('update'); @@ -253,7 +253,7 @@ export function registerDocCommands(program: Command): void { .option('--body-file <path>', 'read the doc body from a file') .addOption(formatOption()) .addOption(publicOption()) - .action(async (repo: string, docRef: string, opts: DocWriteOpts) => { + .action(async (book: string, docRef: string, opts: DocWriteOpts) => { const ctx = getContext(update); const payload = buildWritePayload(opts); if (Object.keys(payload).length === 0) { @@ -261,12 +261,12 @@ export function registerDocCommands(program: Command): void { 'nothing to update — pass at least one of --title/--slug/--body/--body-file/--format/--public' ); } - const updated = await updateDoc(ctx.http, parseBookRef(repo), docRef, payload); + const updated = await updateDoc(ctx.http, parseBookRef(book), docRef, payload); if (ctx.json) { printJson(updated); return; } - printOk(`Updated doc ${repo}/${docRef}`); + printOk(`Updated doc ${book}/${docRef}`); }); const del = doc.command('delete'); @@ -275,15 +275,15 @@ export function registerDocCommands(program: Command): void { .argument('<book>', BOOK_ARG_HELP) .argument('<doc>', 'doc slug or id') .option('--yes', 'skip the confirmation prompt') - .action(async (repo: string, docRef: string, opts: { yes?: boolean }) => { - await confirmDestructive(`delete doc ${repo}/${docRef}`, Boolean(opts.yes)); + .action(async (book: string, docRef: string, opts: { yes?: boolean }) => { + await confirmDestructive(`delete doc ${book}/${docRef}`, Boolean(opts.yes)); const ctx = getContext(del); - const deleted = await deleteDoc(ctx.http, parseBookRef(repo), docRef); + const deleted = await deleteDoc(ctx.http, parseBookRef(book), docRef); if (ctx.json) { printJson(deleted); return; } - printOk(`Deleted doc ${repo}/${docRef}`); + printOk(`Deleted doc ${book}/${docRef}`); }); const versions = doc.command('versions'); diff --git a/src/commands/search.ts b/src/commands/search.ts index 7cca108..975d034 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -27,7 +27,7 @@ export function registerSearchCommands(program: Command): void { .description('Search docs or books') .argument('<query>', 'search keywords') .option('--type <type>', 'what to search for: doc or book (required)') - .option('--scope <ns>', 'restrict to a group or group/repo namespace') + .option('--scope <ns>', 'restrict to a group or group/book namespace') .option('--creator <login>', 'only results created by this user') .option('--page <n>', 'page number (page size is fixed at 20)', pageFlag); cmd.action(async (query: string) => { diff --git a/tests/commands/doc.test.ts b/tests/commands/doc.test.ts index 7a33630..5ab0071 100644 --- a/tests/commands/doc.test.ts +++ b/tests/commands/doc.test.ts @@ -61,7 +61,7 @@ describe('doc commands', () => { { id: 2, slug: 'guide', title: 'Guide', word_count: 300, updated_at: '2026-01-02' }, ]; - it('GETs {repoBase}/docs for a namespace and prints the full JSON with --json', async () => { + it('GETs {bookBase}/docs for a namespace and prints the full JSON with --json', async () => { request.mockResolvedValueOnce(ok(docs)); await expect(runCli(argv('doc', 'list', 'yuque/help', '--json'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ @@ -73,7 +73,7 @@ describe('doc commands', () => { expect(JSON.parse(stdoutText())).toEqual(docs); }); - it('accepts a numeric repo id and spec query flags', async () => { + it('accepts a numeric book id and spec query flags', async () => { request.mockResolvedValueOnce(ok([])); await expect( runCli( @@ -169,7 +169,7 @@ describe('doc commands', () => { body: '# Hello\n\nWorld.\n', }; - it('with <repo> <doc> GETs {repoBase}/docs/{doc} and prints the raw body', async () => { + it('with <book> <doc> GETs {bookBase}/docs/{doc} and prints the raw body', async () => { request.mockResolvedValueOnce(ok(detail)); await expect(runCli(argv('doc', 'get', 'yuque/help', 'intro'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ @@ -217,7 +217,7 @@ describe('doc commands', () => { describe('doc create', () => { const created = { id: 7, slug: 'new-doc', title: 'New Doc' }; - it('POSTs {repoBase}/docs with the spec body fields', async () => { + it('POSTs {bookBase}/docs with the spec body fields', async () => { request.mockResolvedValueOnce(ok(created)); await expect( runCli( @@ -283,7 +283,7 @@ describe('doc commands', () => { }); describe('doc update', () => { - it('PUTs {repoBase}/docs/{doc} with only the given fields', async () => { + it('PUTs {bookBase}/docs/{doc} with only the given fields', async () => { request.mockResolvedValueOnce(ok({ id: 42, slug: 'intro', title: 'Renamed' })); await expect( runCli(argv('doc', 'update', 'yuque/help', 'intro', '--title', 'Renamed')) @@ -305,7 +305,7 @@ describe('doc commands', () => { }); describe('doc delete', () => { - it('--yes skips confirmation and DELETEs {repoBase}/docs/{doc}', async () => { + it('--yes skips confirmation and DELETEs {bookBase}/docs/{doc}', async () => { request.mockResolvedValueOnce(ok({ id: 42, slug: 'intro' })); await expect(runCli(argv('doc', 'delete', 'yuque/help', 'intro', '--yes'))).resolves.toBe(0); expect(request).toHaveBeenCalledWith({ diff --git a/tests/e2e/cli.e2e.test.ts b/tests/e2e/cli.e2e.test.ts index 1f36561..a919d82 100644 --- a/tests/e2e/cli.e2e.test.ts +++ b/tests/e2e/cli.e2e.test.ts @@ -195,7 +195,7 @@ describeRead('read paths (personal token)', () => { }); it('doc get --json returns a doc', () => { - if (!sampleDocSlug) return; // empty repo — nothing to fetch, skip vacuously + if (!sampleDocSlug) return; // empty book — nothing to fetch, skip vacuously expect(okJson(pc(['doc', 'get', readRepo, sampleDocSlug, '--json'])).id).toBeTruthy(); }); @@ -302,8 +302,8 @@ describeWrite('write lifecycle (sandbox repo)', () => { const describeRepoLifecycle = REPO_LIFECYCLE_ENABLED && personalToken ? describe : describe.skip; describeRepoLifecycle('repo lifecycle (ephemeral repo, local only)', () => { - it('creates, reads, and deletes a repo', () => { - expect(configuredLogin, 'YUQUE_E2E_LOGIN is required for the repo lifecycle test').toBeTruthy(); + it('creates, reads, and deletes a book', () => { + expect(configuredLogin, 'YUQUE_E2E_LOGIN is required for the book lifecycle test').toBeTruthy(); const login = configuredLogin as string; const stamp = `${Date.now()}`; const created = okJson( From f603893ca69d7a47daac4ba4aa873c0e490c6b4c Mon Sep 17 00:00:00 2001 From: cwg <1227646458@qq.com> Date: Thu, 23 Jul 2026 19:57:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20version=201.1.0=20=E2=80=94=20same?= =?UTF-8?q?-day=20rename,=20no=20major=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- CHANGELOG.md | 7 ++++--- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 751073f..4a69029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog -## 2.0.0 +## 1.1.0 -Breaking: knowledge bases are now `book` across the CLI surface, aligning with -Yuque's own terminology (知识库 = book, matching the API's `V2Book`/`book_id`): +Knowledge bases are now `book` across the CLI surface, aligning with Yuque's +own terminology (知识库 = book, matching the API's `V2Book`/`book_id`). The +`repo` command group from 1.0.0 (released the same day) is replaced by `book`: - `repo list/get/create/update/delete` → `book list/get/create/update/delete`; `<repo>` arguments are now `<book>`. diff --git a/package-lock.json b/package-lock.json index 9d5e4db..36c2875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "yuque-open-cli", - "version": "2.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yuque-open-cli", - "version": "2.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "axios": "^1.7.9", diff --git a/package.json b/package.json index 3ae1811..e871a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yuque-open-cli", - "version": "2.0.0", + "version": "1.1.0", "description": "Command-line interface for Yuque (语雀) — browse, edit, and manage your knowledge base from the terminal", "type": "module", "main": "dist/cli.js",