Autogenerate CLI#1205
Conversation
|
Wrap the hand-written CLI source to the repo's printWidth, and exclude the auto-generated generated-commands.ts from Prettier (it's regenerated on every build, so formatting it would just be undone). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a723a53 to
a95be30
Compare
commit: |
Regenerate generated-commands.ts from the latest deployed OpenAPI spec (@gitbook/api 0.186.0), adding 5 new endpoints: change-request agent conversations (list/update/delete), change-request content update, and site Git Sync installations listing. Also use bracket-notation accessors for query-param options so flag names that aren't valid JS identifiers resolve correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the non-autogenerated commands from the `gitbook` CLI into `gitbook2` so it's a functional superset. The integration build/publish lifecycle commands (new/dev/publish/unpublish/tail/check) are grouped under a singular `integration` command to stay distinct from the spec-generated `integrations` group (raw API ops); `openapi publish` is registered top-level. All reuse the existing shared source modules via registerCustomCommands. cli2.ts gains a keep-alive guard so `integration dev` doesn't get torn down by the post-parse process.exit(0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generated commands hardcoded `if (response.status !== 204)` before calling response.json(), assuming 204 was the only bodyless success. Endpoints that return another no-body status (e.g. DELETE openapi spec returns 205 on deletion, 204 only when absent) hit response.json() on an empty body and failed with "Unexpected end of JSON input", exiting 1 despite the server-side success. Key off body presence instead of a specific status, which covers 204, 205, and any future bodyless success with no regression for JSON responses. Regenerated all 339 commands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The command generator turned each top-level body field into a typed flag
but assigned the raw commander string into the JSON body with no coercion,
so --changes '[...]' shipped {"changes":"[...]"} (a string) and the API
rejected it. Same for --users and any number-typed field.
- Add coerceBodyFlag() to output.ts (unit-tested): JSON-parses array flags,
numeric-coerces number flags, passes string/boolean through, with legible
errors surfaced by the CLI's top-level handler.
- Generator emits coerceBodyFlag(...) for array/number flags and renders
array flags as <json> with a [JSON array] hint.
- Always offer --body <json> alongside typed flags as an escape hatch
(typed flags merge on top), so object-typed fields dropped by
flattenObjectFlags stay reachable.
- Regenerate generated-commands.ts; add coerceBodyFlag tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make gitbook2 a backward-compatible superset of the original `gitbook` CLI: the integration lifecycle verbs (new/dev/publish/unpublish/tail/check) are now also reachable via their historical top-level spelling, in addition to the canonical `integration <verb>` group. - Factor each lifecycle command's argument/option wiring and action handler into a shared LIFECYCLE_COMMANDS table so both mounts stay in sync (no duplication). - The top-level aliases are hidden from help and print a deprecation warning to stderr (never stdout) before delegating to the same handler. - The warning derives the binary name from program.name() rather than hardcoding "gitbook2", so it follows the eventual rename to "gitbook" automatically. No namespace collisions: auth/whoami/openapi-publish already exist at identical paths and are left untouched; the six verbs don't clash with any generated group. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Set a distinct, convention-matching User-Agent (`GitBook-CLI/<version>`, matching GitBook-Open / GitBook-MCP-Server) and fold the invoked command path into it, so API requests can be attributed to a specific CLI command in logs / the GCP per-endpoint dashboard — no per-request plumbing. - remote.ts: UA base is now `GitBook-CLI/<version>`; getAPIClient takes an optional command path and emits `GitBook-CLI/<version> (<command>)`. auth and whoami pass their command names. - generate-commands.ts: each generated action passes its dotted command path (e.g. spaces.change-requests.content.update) to getAPIClient. - generated-commands.ts: regenerated. Verified on the wire: e.g. `GitBook-CLI/0.28.0 (spaces.change-requests.comments.list)`.
spastorelli
left a comment
There was a problem hiding this comment.
Looks good 👌, have a few questions & comments.
| @@ -0,0 +1,11291 @@ | |||
| /** | |||
There was a problem hiding this comment.
I don't think this should belong to src/ folder. If this is autogenerated by the generate-command.ts script does it need to be commited to the repo? Should we instead add it to the .gitignore or even better have the build process generate this file and produce a minized version under dist/ which should already be git ignored.
|
|
||
| // ─── Entry point ────────────────────────────────────────────────────────────── | ||
|
|
||
| const SPEC_PATH = path.resolve(__dirname, '../packages/api/spec/openapi.yaml'); |
There was a problem hiding this comment.
Why read the yaml version instead of importing the .json version directly? don't we have the .jsonversion included in packages/api/spec/?
| * dashboard, without per-request plumbing. Omitted → just the surface token. | ||
| */ | ||
| export async function getAPIClient(requireAuth: boolean = true): Promise<GitBookAPI> { | ||
| function buildUserAgent(command?: string): string { |
There was a problem hiding this comment.
I don't think the UA should be based on the command. GitBook-CLI/<version> should be sufficient.
| method: 'GET', | ||
| secure: true, | ||
| }); | ||
| const text = await response.text(); |
There was a problem hiding this comment.
Why do we handle the response as text do then parse the JSON. I believe the request underlying HttpClient from Api/GitBookAPI class should handle it via the format param.
| const query: Record<string, string> = {}; | ||
| if (options["shareKey"] !== undefined) query['shareKey'] = String(options["shareKey"]); | ||
| try { | ||
| const response = await api.request({ |
There was a problem hiding this comment.
I think our API client classes (Api) is generated based on the resource + operation Id
can't we reuse this and call the associated class instance method named using the operationId get from the spec instead of calling the underlying .request()? It also properly pass the format param that I mention in a another comment.
There was a problem hiding this comment.
More generally and not saying we should do this. But have you explored using SDKs/Libs that allows generating stubs based on an API spec? Weren't there suitable for our case?
This adds
gitbook2, a standalone CLI for exercising the GitBook API. It's a separate binary from the maingitbookCLI (which builds and publishes integrations) so I could develop the generated API commands in isolation without cluttering the stable command tree.The command tree is generated from
packages/api/spec/openapi.yamlbyscripts/generate-commands.ts— one command per public operation, named off the URL path (e.g.gitbook2 organizations list,gitbook2 spaces get <id>). It shares auth/config with the main CLI, and ships shell completion for bash/zsh/fish.Things that would be helpful for review:
fields — with a footer showing the count and the pagination cursor as a
ready-to-paste flag (
next page: --page <cursor>).--full.--fullrestores the complete dump, and--json/--yamlare untouched, soscripts and agents still get the full structured response. When piped (not a
TTY) it defaults to YAML.
I pulled the formatting logic into
packages/cli/src/output.ts(instead of inlining it in the generated file) so it has a single source of truth and can be unit-tested directly — seeoutput.test.ts(bun test).A few notes:
generated-commands.tsis auto-generated — don't hand-edit it;prebuildregenerates it from the spec vianpm run generate-commands.gitbook2/cli2are working names for now — I'll rename before this goes in for real.How to test
gitbook2Requires Node ≥18 and bun.
Then run any command via
node dist/cli2.js <command>:Optional: install and run it as
gitbook2instead ofnode dist/cli2.jsRun the formatting unit tests