-
Notifications
You must be signed in to change notification settings - Fork 0
Code Generation Internals
Running <cli> generate fetches the OpenAPI spec from the active environment's base URL, then passes it through four generator functions that each produce a TypeScript file. These files are written to the configured generatedDir (default: src/generated/) and are meant to be committed to git so the CLI works without a live API connection.
TypeScript interfaces and type aliases derived from components.schemas in the spec. Every named schema becomes a named export.
// Auto-generated — do not edit
export interface Pet {
id: number;
name: string;
/** @example "dog" */
species: "dog" | "cat" | "bird" | "fish" | "rabbit";
age: number;
status: "available" | "pending" | "adopted";
ownerId: number | null;
}
export type OwnerWithPets = Owner & {
pets?: Pet[];
};An ApiClient class with one typed async method per operationId. Methods accept path parameters as positional arguments, an optional body argument for request bodies, and an optional params object for query parameters. Return types are resolved from the 200/201/202/204 response schema.
export class ApiClient {
/** List all pets — GET /pets */
async listPets(params?: { species?: string; status?: string }): Promise<Pet[]> { ... }
/** Create a new pet — POST /pets */
async createPet(body: CreatePet): Promise<Pet> { ... }
/** Get a pet by ID — GET /pets/{id} */
async getPet(id: number): Promise<Pet> { ... }
}The class also exports CapturedRequest and HeadersProvider types, and a dryRun flag that returns the request object instead of sending it (used for command-level dry-run mode).
A registerGeneratedCommands function that mounts Commander subcommands onto the program. Operations are grouped by their first OpenAPI tag into top-level command groups. Each group becomes a Commander subcommand, and each operation becomes a leaf command under it.
export function registerGeneratedCommands(
program: Command,
client: ApiClient,
onResult: OutputHandler,
): void { ... }Options are derived from query parameters and request body properties. Required body fields use .requiredOption(), optional fields use .option().
A lookup table mapping command paths to their metadata. Used by the routine engine and the MCP server to discover available commands and their parameters at runtime without needing the live Commander tree.
export const commandMap: Record<string, CommandMapping> = {
"pets list": { operationId: "listPets", pathParams: [], queryParams: ["species", "status"], hasBody: false },
"pets create": { operationId: "createPet", pathParams: [], queryParams: [], hasBody: true, bodyFields: [...] },
"pets get": { operationId: "getPet", pathParams: ["id"], queryParams: [], hasBody: false },
};CommandMapping includes operationId, pathParams, queryParams, hasBody, optional bodyFields, and optional description.
The first tag on each operation determines which command group it belongs to. Tag names are normalized for use as CLI command names with normalizeTag():
- Convert to lowercase
- Split on whitespace, forward slashes, and colons (
/[\s/:]+/) - Strip leading and trailing hyphens from each token
- Filter out empty tokens
The resulting array of tokens determines the command tree depth. The first token is always the top-level group name. If there are additional tokens, they become a nested subgroup.
Examples:
| OpenAPI tag | Normalized tokens | CLI command prefix |
|---|---|---|
pets |
["pets"] |
pets |
Pet Operations |
["pet", "operations"] |
pet operations |
api/users |
["api", "users"] |
api users |
billing:invoices |
["billing", "invoices"] |
billing invoices |
Tags with dots or other non-splitting characters are left as a single lowercase token.
Each HTTP method maps to a default verb for the leaf command name:
| HTTP method | Has path params? | Verb |
|---|---|---|
GET |
No | list |
GET |
Yes | get |
POST |
Any | create |
PUT |
Any | update |
DELETE |
Any | delete |
PATCH |
Any | patch |
Path parameters are detected by checking in: "path" on the merged parameter list (path-level parameters from the path item object are merged with operation-level parameters, with operation-level taking precedence).
The operationId field is required — operations without one are skipped by all four generators.
When multiple operations under the same parent command share the same default verb (for example, two POST operations both wanting create), the verb is replaced by a kebab-cased form of the operationId:
operationId: "adoptPet" → command name: "adopt-pet"
The transformation is: insert a hyphen before each uppercase letter, lowercase everything, strip any leading hyphen.
In the Petstore example, the pets tag has five operations. Three of them (GET /pets, GET /pets/{id}, POST /pets) get standard verbs (list, get, create). PUT /pets/{id} gets update and DELETE /pets/{id} gets delete. None collide, so operationId-based names are not needed.
Operations without an operationId are silently skipped by all generators. Every endpoint must have a unique operationId for code generation to include it.
The resolveType() function in src/codegen/util.ts handles the full mapping from OpenAPI schema to TypeScript type string.
| OpenAPI type | TypeScript type |
|---|---|
string |
string |
integer |
number |
number |
number |
boolean |
boolean |
A schema with type: "object" and properties becomes an interface (at the top-level generateTypes pass) or an inline object literal type (when nested, up to depth 3). Deeper nesting falls back to Record<string, unknown>.
Required fields listed in the schema's required array are emitted without ?. All other fields get ?.
// OpenAPI
// Pet:
// type: object
// properties:
// id: { type: integer }
// name: { type: string }
// required: [id, name]
export interface Pet {
id: number;
name: string;
}A schema with type: "object" but no properties becomes Record<string, unknown>.
additionalProperties: true appends [key: string]: unknown;. additionalProperties: <schema> appends a typed index signature.
type: "array" with items becomes ItemType[]. Union or intersection item types are wrapped in parentheses: (Foo | Bar)[].
$ref values are resolved to the referenced schema name using the last path segment. The name is sanitized with sanitizeTypeName(): dots become __ (double underscore) to avoid collisions, and other non-identifier characters become _.
#/components/schemas/Pet → Pet
#/components/schemas/billing.alert → billing__alert
allOf schemas become TypeScript intersection types. $ref members resolve to named types; members with inline properties become inline object literals.
// OpenAPI: OwnerWithPets allOf [Owner, { pets: Pet[] }]
export type OwnerWithPets = Owner & {
pets?: Pet[];
};The required arrays from all parts of the allOf are merged when determining optionality of inline properties.
oneOf and anyOf schemas become TypeScript union types. When a discriminator is present, each variant is intersected with a literal type for the discriminator property:
// With discriminator on "type" property
export type ContentItem =
| TextContent & { type: "text" }
| ImageContent & { type: "image" }
| VideoContent & { type: "video" };Without a discriminator, variants are joined with | directly.
String enums become string literal union types:
// OpenAPI: status enum [available, pending, adopted]
export type PetStatus = "available" | "pending" | "adopted";When an enum appears inline on a property (rather than as a named schema), it is inlined in the interface:
export interface Pet {
status: "available" | "pending" | "adopted";
}Integer and mixed enums are also supported: each value is rendered as its literal (e.g. 0 | 1 | 2). Null values in an enum become the null literal.
nullable: true (OAS 3.0) appends | null to the resolved type:
// OpenAPI: ownerId type:integer nullable:true
ownerId: number | null;OAS 3.1 nullable is expressed as a type array: type: ["integer", "null"]. Both forms produce the same output.
| Feature | TypeScript output |
|---|---|
const: "active" |
"active" (literal type) |
type: ["string", "null"] |
string | null |
prefixItems |
Tuple type [A, B, C]
|
not (standalone) |
unknown with @not JSDoc tag |
$defs |
Flattened into the main schema map |
patternProperties |
Index signature with a JSDoc comment per pattern |
The buildJsDoc() function emits JSDoc comments above interfaces and properties when the schema has a description, deprecated, or any of the following constraints: format, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, pattern, minItems, maxItems, uniqueItems, multipleOf, minProperties, maxProperties, default, example, readOnly, writeOnly.
Deprecated schemas use @deprecated and incorporate the description into the deprecation message. Deprecated operations produce command descriptions prefixed with [DEPRECATED].
When a request body schema resolves to a primitive type (string, number, boolean) or an array of primitives (string[], number[]) and cannot be decomposed into named properties, the generated command registers a single -B <value> flag instead of per-property flags.
For string[] and number[] bodies, -B accepts a comma-separated string that is split at runtime.
The following OpenAPI features are not currently supported:
-
multipart/form-data— file upload endpoints are skipped; onlyapplication/jsonrequest bodies are processed -
Multiple content types per endpoint — only
application/jsonis read fromrequestBody.content; other content types (e.g.text/plain,application/octet-stream) are ignored - Response headers — headers in operation responses are not surfaced in the generated client or commands
-
Cookie parameters —
in: "cookie"parameters are parsed by the OpenAPI type definitions but not wired into the generated client - OAuth2 / OIDC security schemes — only HTTP Basic, Bearer token, and API key schemes are supported via the auth strategy abstraction
-
Callbacks and webhooks — the
callbacksandwebhooksOpenAPI fields are ignored -
XML — only JSON responses and request bodies are handled;
application/xmlcontent types are skipped -
Server variables —
servers[].variablestemplating is not processed; the base URL is taken as-is from the configured environment -
headerparameters —in: "header"parameters are parsed but not wired into CLI flags or the client method signatures
Essentials
Using a CLI
Authoring Routines
- Writing Routines
- Variables
- Output Capture
- Conditions & Assertions
- Loops
- Error Handling
- Sub-Routines & Meta-Commands
- Routine Testing
Building a CLI
- Building a CLI
- Authentication Strategies
- Session Auth
- Project Mode
- MCP Server Integration
- Code Generation Internals
Reference