The cli generator — its skill
This file is the DESIGN of your ejected cli generator (generators/cli/):
to change the generator, edit this skill first, then make the code match it — a diff
to generators/cli/ that has no covering sentence here is incomplete.
What it emits
A bin-ready <stem>.cli.ts: one command per operation over the sdk's instance client,
with --help, a schema <op> introspection command, and --dry-run.
With client.docs (or --docs), the docs hook also writes <stem>.cli.md: the usage
line, the global flags, the credential variables, the exit-code table, and one section per
command with its positionals and flags.
Design decisions that must hold
Argument shape: path params positional, query params typed --kebab-name flags,
JSON bodies via --json '<json>' | @file | @- (stdin).
Help is the whole interface. A flag that exists but isn't in --help doesn't exist
to the user, so the top-level help carries a Global flags: section (--server-url,
--format, --dry-run, --page-all, --output, --token, --json) plus the
credential environment variables. Descriptions are collapsed to ONE line — an OpenAPI
description with newlines otherwise breaks the alignment of every following flag. The
footer names the form that actually works for a grouped API
(<bin> <group> <command> --help).
Commands are addressable the way a shell allows. A group slug is kebab-cased so a
multi-word OpenAPI tag can be typed without quoting, while help shows the original tag.
A bare operationId resolves to its grouped command when unambiguous.
Exit codes are a contract: 0 ok, 1 API error, 2 auth, 3 validation, 4 usage.
Errors print ONE JSON object to stderr so stdout stays pipeable.
The CLI names itself from process.argv[1]. Only the operator's bin field decides
what the command is called, so help reads the invoked name back instead of printing a
name from generation that may not exist on the machine.
Credentials come from the environment — wiring.envPrefix, the constant-cased output
stem (CLIENT_TOKEN), which a composed entry sets per api alias — or explicit flags;
--dry-run prints the prepared request with credentials REDACTED. The prefix is fixed at
generation on purpose: a renamed binary must keep reading the variables a published CLI
already documents. Help lists only the credentials the description declares, and an
unusable --token is a usage error, never silently dropped.
Validation is on by default. The generator declares requires: ['typescript', 'zod'] and
the pipeline pulls prerequisites in automatically, so --generator cli alone produces a
validating CLI — a user shouldn't have to know which other generator provides it. The
consequence is a zod peer dependency at run time, which the docs state.
Throw-mode only — the exit-code mapping reads thrown ApiErrors.
Runs under node --experimental-strip-types with no build step, including the
modules it imports (the sdk and the zod module). Anything emitted must be erasable
TypeScript; a parameter property anywhere in that import graph breaks the zero-build
runner.
The generated module is a library as well as a binary. It exports COMMANDS,
wiring, and run, and self-executes only when it is the process entry — a REALPATH
comparison of import.meta.url against argv[1], because some runners resolve
symlinks in one but not the other (macOS temp dirs, installed bin symlinks), and a
plain URL comparison silently runs nothing. import.meta.main would be cleaner but is
absent from our Node floors. Importing the module must be side-effect-safe:
module-level wiring (zod validation) touches only the module's OWN client, never a
global.
Behavior that is not in the description is composed, never generated. A custom
command (login, anything) is the operation-command data shape plus a handler, so it
inherits help, parsing, schema, and the exit-code contract; runCli dispatches it
instead of the client. The generator itself never learns what such a command does —
credentials files, login flows, and profiles are user land (or a future satellite),
by design.
One binary can span several descriptions. runCli also accepts sources — each a
command list plus, optionally, its OWN wiring (own base URL, schemes, credentials)
behind a namespace, so colliding operationIds across descriptions are simply different
commands (cafe shop createOrder, cafe kitchen createOrder). A namespace-less source
puts commands at the root (cafe login); a root command whose name matches a namespace
is rejected at startup, never shadowed. A source WITHOUT wiring inherits the first
wired source's — a root login shares the composed binary's identity, which is the
whole point of composing it there.
The composed entry is generated, not hand-rolled. A top-level client.cliOutput
makes redocly generate-client (no api argument) emit one entry over every api that
selected cli: the namespace is the api ALIAS from apis:, and the credential prefix
defaults to <BINNAME>_<ALIAS> (CAFE_SHOP_TOKEN) via wiring.envPrefix — which
exists precisely so the display name and the credential prefix can differ. The composed
entry exports its SOURCES so an adopter layers custom commands around it without
editing a generated file. Without cliOutput, nothing changes.
The CLI documents itself. The page is this generator's docs hook, not a separate
generator: nothing else knows this tool's commands, and a reader who ejects cli gets
the page layout with it. The page renders from commandData — the same table runCli
dispatches on — so it cannot describe a tool other than the one beside it. A capability
reaches the page only by being in that table. The page is Markdown that survives a
linter (ATX headings, a blank line around every block, no hard tabs, one sentence per
line) and it escapes what descriptions contain, because a summary is arbitrary text.
The stage files
render.ts derives commandData from the IR and renders the module and the composed
entry; docs.ts renders the reference page from the same command table;
engine-source.ts supplies the cli engine's source text — the engine itself (runCli,
the parser, help, dispatch) ships inside the package and arrives through
@redocly/client-generator/runtime-sources (in this repo it lives in runtime/cli.ts
beside these files). index.ts is the entry. The sdk calling convention comes from
@redocly/client-generator/contracts/typescript.
Ejecting it
redocly eject-generator cli copies this generator's TypeScript source folder to
generators/cli/, exactly as we wrote it, importing @redocly/client-generator,
@redocly/client-generator/contracts/typescript,
@redocly/client-generator/runtime-sources (the embedded cli engine), and
@redocly/openapi-core. Running a .ts generator uses Node's type stripping (Node
22.18, 23.6, or newer); newer built-in versions merge in per file with --update. Change
the command surface, the help layout, or the exit-code mapping, and regenerate. The exit
codes are a contract for scripts, so change them only deliberately.
The modify loop
- Edit this skill: state the new behavior or decision.
- Make
generators/cli/ match it.
- Run
redocly generate-client and inspect the git diff of the generated output —
generated files are never hand-edited.
Newer built-in versions merge in with redocly eject-generator cli --update.
1---2name: cli-generator3description: Design of the ejected Redocly `cli` client generator. Read it, and update it, before changing generators/cli/.4---56# The `cli` generator — its skill78This file is the DESIGN of your ejected `cli` generator (`generators/cli/`):9**to change the generator, edit this skill first, then make the code match it** — a diff10to `generators/cli/` that has no covering sentence here is incomplete.1112## What it emits1314A bin-ready `<stem>.cli.ts`: one command per operation over the sdk's instance client,15with `--help`, a `schema <op>` introspection command, and `--dry-run`.1617With `client.docs` (or `--docs`), the `docs` hook also writes `<stem>.cli.md`: the usage18line, the global flags, the credential variables, the exit-code table, and one section per19command with its positionals and flags.2021## Design decisions that must hold2223- **Argument shape:** path params positional, query params typed `--kebab-name` flags,24 JSON bodies via `--json '<json>' | @file | @-` (stdin).25- **Help is the whole interface.** A flag that exists but isn't in `--help` doesn't exist26 to the user, so the top-level help carries a `Global flags:` section (`--server-url`,27 `--format`, `--dry-run`, `--page-all`, `--output`, `--token`, `--json`) plus the28 credential environment variables. Descriptions are collapsed to ONE line — an OpenAPI29 description with newlines otherwise breaks the alignment of every following flag. The30 footer names the form that actually works for a grouped API31 (`<bin> <group> <command> --help`).32- **Commands are addressable the way a shell allows.** A group slug is kebab-cased so a33 multi-word OpenAPI tag can be typed without quoting, while help shows the original tag.34 A bare operationId resolves to its grouped command when unambiguous.35- **Exit codes are a contract:** 0 ok, 1 API error, 2 auth, 3 validation, 4 usage.36 Errors print ONE JSON object to stderr so stdout stays pipeable.37- **The CLI names itself from `process.argv[1]`.** Only the operator's `bin` field decides38 what the command is called, so help reads the invoked name back instead of printing a39 name from generation that may not exist on the machine.40- **Credentials come from the environment** — `wiring.envPrefix`, the constant-cased output41 stem (`CLIENT_TOKEN`), which a composed entry sets per api alias — or explicit flags;42 `--dry-run` prints the prepared request with credentials REDACTED. The prefix is fixed at43 generation on purpose: a renamed binary must keep reading the variables a published CLI44 already documents. Help lists only the credentials the description declares, and an45 unusable `--token` is a usage error, never silently dropped.46- **Validation is on by default.** The generator declares `requires: ['typescript', 'zod']` and47 the pipeline pulls prerequisites in automatically, so `--generator cli` alone produces a48 validating CLI — a user shouldn't have to know which other generator provides it. The49 consequence is a zod peer dependency at run time, which the docs state.50- Throw-mode only — the exit-code mapping reads thrown `ApiError`s.51- **Runs under `node --experimental-strip-types` with no build step**, including the52 modules it imports (the sdk and the zod module). Anything emitted must be erasable53 TypeScript; a parameter property anywhere in that import graph breaks the zero-build54 runner.55- **The generated module is a library as well as a binary.** It exports `COMMANDS`,56 `wiring`, and `run`, and self-executes only when it is the process entry — a REALPATH57 comparison of `import.meta.url` against `argv[1]`, because some runners resolve58 symlinks in one but not the other (macOS temp dirs, installed bin symlinks), and a59 plain URL comparison silently runs nothing. `import.meta.main` would be cleaner but is60 absent from our Node floors. Importing the module must be side-effect-safe:61 module-level wiring (zod validation) touches only the module's OWN client, never a62 global.63- **Behavior that is not in the description is composed, never generated.** A custom64 command (`login`, anything) is the operation-command data shape plus a `handler`, so it65 inherits help, parsing, `schema`, and the exit-code contract; `runCli` dispatches it66 instead of the client. The generator itself never learns what such a command does —67 credentials files, login flows, and profiles are user land (or a future satellite),68 by design.69- **One binary can span several descriptions.** `runCli` also accepts sources — each a70 command list plus, optionally, its OWN wiring (own base URL, schemes, credentials)71 behind a namespace, so colliding operationIds across descriptions are simply different72 commands (`cafe shop createOrder`, `cafe kitchen createOrder`). A namespace-less source73 puts commands at the root (`cafe login`); a root command whose name matches a namespace74 is rejected at startup, never shadowed. A source WITHOUT wiring inherits the first75 wired source's — a root `login` shares the composed binary's identity, which is the76 whole point of composing it there.77- **The composed entry is generated, not hand-rolled.** A top-level `client.cliOutput`78 makes `redocly generate-client` (no api argument) emit one entry over every api that79 selected `cli`: the namespace is the api ALIAS from `apis:`, and the credential prefix80 defaults to `<BINNAME>_<ALIAS>` (`CAFE_SHOP_TOKEN`) via `wiring.envPrefix` — which81 exists precisely so the display name and the credential prefix can differ. The composed82 entry exports its `SOURCES` so an adopter layers custom commands around it without83 editing a generated file. Without `cliOutput`, nothing changes.8485- **The CLI documents itself.** The page is this generator's `docs` hook, not a separate86 generator: nothing else knows this tool's commands, and a reader who ejects `cli` gets87 the page layout with it. The page renders from `commandData` — the same table `runCli`88 dispatches on — so it cannot describe a tool other than the one beside it. A capability89 reaches the page only by being in that table. The page is Markdown that survives a90 linter (ATX headings, a blank line around every block, no hard tabs, one sentence per91 line) and it escapes what descriptions contain, because a summary is arbitrary text.9293## The stage files9495`render.ts` derives `commandData` from the IR and renders the module and the composed96entry; `docs.ts` renders the reference page from the same command table;97`engine-source.ts` supplies the cli engine's source text — the engine itself (`runCli`,98the parser, help, dispatch) ships inside the package and arrives through99`@redocly/client-generator/runtime-sources` (in this repo it lives in `runtime/cli.ts`100beside these files). `index.ts` is the entry. The sdk calling convention comes from101`@redocly/client-generator/contracts/typescript`.102103## Ejecting it104105`redocly eject-generator cli` copies this generator's TypeScript source folder to106`generators/cli/`, exactly as we wrote it, importing `@redocly/client-generator`,107`@redocly/client-generator/contracts/typescript`,108`@redocly/client-generator/runtime-sources` (the embedded cli engine), and109`@redocly/openapi-core`. Running a `.ts` generator uses Node's type stripping (Node11022.18, 23.6, or newer); newer built-in versions merge in per file with `--update`. Change111the command surface, the help layout, or the exit-code mapping, and regenerate. The exit112codes are a contract for scripts, so change them only deliberately.113114## The modify loop1151161. Edit this skill: state the new behavior or decision.1172. Make `generators/cli/` match it.1183. Run `redocly generate-client` and inspect the `git diff` of the generated output —119 generated files are never hand-edited.120121Newer built-in versions merge in with `redocly eject-generator cli --update`.