Skip to content

Autogenerate CLI#1205

Open
talagluck wants to merge 12 commits into
mainfrom
autogenerate_cli
Open

Autogenerate CLI#1205
talagluck wants to merge 12 commits into
mainfrom
autogenerate_cli

Conversation

@talagluck

Copy link
Copy Markdown
Contributor

This adds gitbook2, a standalone CLI for exercising the GitBook API. It's a separate binary from the main gitbook CLI (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.yaml by scripts/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:

  • The approach - does this seem reasonable for autogenerating the CLI
  • The output - dumping the full response for list commands was unreadable, so pretty output is now compact by default:
    • Lists print one line per item — id + name/title + a couple of identifying
      fields — with a footer showing the count and the pagination cursor as a
      ready-to-paste flag (next page: --page <cursor>).
    • Single objects show just the identifying fields, with a hint to use --full.
    • --full restores the complete dump, and --json / --yaml are untouched, so
      scripts and agents still get the full structured response. When piped (not a
      TTY) it defaults to YAML.
  • Usage - I'm going to do a bunch of testing, but it would be great if others try this out and see how well it works for them!

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 — see output.test.ts (bun test).

A few notes:

  • generated-commands.ts is auto-generated — don't hand-edit it; prebuild regenerates it from the spec via npm run generate-commands.
  • gitbook2 / cli2 are working names for now — I'll rename before this goes in for real.
  • Opening as a draft. I haven't added a changeset yet, and there are open questions on naming, so I'd rather get feedback on the shape first.
  • Some additional things needed:
    • Incorporating the authentication changes we're working on
    • Incorporating commands from the current CLI which aren't autogenerated (like the ones for publishing integrations)
    • Usage stats

How to test gitbook2

Requires Node ≥18 and bun.

# 1. Get the branch
git fetch origin tal/cli_updates
git checkout tal/cli_updates

# 2. Install deps (from the repo root)
bun install

# 3. Build the CLI (regenerates commands from the spec, then bundles)
cd packages/cli
bun run build          # runs `generate-commands` via prebuild, then build.sh

# 4. Authenticate (create a token at https://app.gitbook.com/account/developer)
node dist/cli2.js auth
#   …or non-interactively:
#   node dist/cli2.js auth --token <YOUR_TOKEN>

Then run any command via node dist/cli2.js <command>:

# Compact, readable output (the default when attached to a terminal)
node dist/cli2.js organizations list
node dist/cli2.js spaces list --organization <orgId>
node dist/cli2.js organizations get <orgId>

# Escape hatches
node dist/cli2.js organizations list --full     # full pretty dump
node dist/cli2.js organizations list --json      # machine-readable (unchanged)
node dist/cli2.js organizations list --yaml      # machine-readable (default when piped)

# Pagination — the footer prints the exact flag to paste
node dist/cli2.js organizations list --page <cursor>

# Discover commands
node dist/cli2.js --help
node dist/cli2.js organizations --help

Optional: install and run it as gitbook2 instead of node dist/cli2.js

# from packages/cli
bun link                 # links the `gitbook2` bin onto your PATH
gitbook2 organizations list

Run the formatting unit tests

# from packages/cli
bun test

@changeset-bot

changeset-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ed9cb2d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

talagluck and others added 2 commits June 15, 2026 11:47
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>
@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

bun add https://pkg.pr.new/GitbookIO/integrations/@gitbook/api@1205

commit: ed9cb2d

talagluck and others added 8 commits June 26, 2026 15:06
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>
@talagluck talagluck marked this pull request as ready for review June 30, 2026 08:05
talagluck and others added 2 commits June 30, 2026 12:10
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 spastorelli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👌, have a few questions & comments.

@@ -0,0 +1,11291 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our API client classes (Api) is generated based on the resource + operation Id

Image

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants