@redocly/client-generator 0.0.0-snapshot.1786460732 → 0.0.0-snapshot.1786626470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/eject-assets/AGENTS.md +2 -2
  3. package/eject-assets/generators/cli-docs.mjs +5 -4
  4. package/eject-assets/generators/cli.mjs +12 -6
  5. package/eject-assets/generators/go.mjs +2 -2
  6. package/eject-assets/generators/mock.mjs +3 -3
  7. package/eject-assets/generators/php.mjs +2 -2
  8. package/eject-assets/generators/python.mjs +2 -2
  9. package/eject-assets/generators/swr.mjs +3 -3
  10. package/eject-assets/generators/tanstack-query.mjs +4 -4
  11. package/eject-assets/generators/transformers.mjs +3 -3
  12. package/eject-assets/generators/{sdk.mjs → typescript.mjs} +16 -16
  13. package/eject-assets/generators/zod.mjs +2 -2
  14. package/eject-assets/skills/cli-docs-generator/SKILL.md +1 -1
  15. package/eject-assets/skills/cli-generator/SKILL.md +1 -1
  16. package/eject-assets/skills/client-generators/SKILL.md +2 -2
  17. package/eject-assets/skills/swr-generator/SKILL.md +1 -1
  18. package/eject-assets/skills/tanstack-query-generator/SKILL.md +1 -1
  19. package/eject-assets/skills/transformers-generator/SKILL.md +1 -1
  20. package/eject-assets/skills/{sdk-generator → typescript-generator}/SKILL.md +8 -8
  21. package/lib/emitters/cli-docs.d.ts.map +1 -1
  22. package/lib/emitters/cli-docs.js +4 -1
  23. package/lib/emitters/cli-docs.js.map +1 -1
  24. package/lib/emitters/runtime-sources.d.ts +2 -2
  25. package/lib/emitters/runtime-sources.js +2 -2
  26. package/lib/emitters/runtime-sources.js.map +1 -1
  27. package/lib/emitters/tanstack-query.js +1 -1
  28. package/lib/emitters/tanstack-query.js.map +1 -1
  29. package/lib/generators/cli/index.d.ts.map +1 -1
  30. package/lib/generators/cli/index.js +8 -6
  31. package/lib/generators/cli/index.js.map +1 -1
  32. package/lib/generators/index.d.ts.map +1 -1
  33. package/lib/generators/index.js +2 -2
  34. package/lib/generators/index.js.map +1 -1
  35. package/lib/generators/meta.d.ts.map +1 -1
  36. package/lib/generators/meta.js +21 -17
  37. package/lib/generators/meta.js.map +1 -1
  38. package/lib/generators/resolve.js +5 -0
  39. package/lib/generators/resolve.js.map +1 -1
  40. package/lib/generators/swr/index.d.ts +1 -1
  41. package/lib/generators/swr/index.js +1 -1
  42. package/lib/generators/tanstack-query/index.d.ts +1 -1
  43. package/lib/generators/tanstack-query/index.js +1 -1
  44. package/lib/generators/types.d.ts +3 -3
  45. package/lib/generators/types.d.ts.map +1 -1
  46. package/lib/generators/{sdk → typescript}/index.d.ts +2 -2
  47. package/lib/generators/typescript/index.d.ts.map +1 -0
  48. package/lib/generators/{sdk → typescript}/index.js +2 -2
  49. package/lib/generators/typescript/index.js.map +1 -0
  50. package/lib/pipeline.js +3 -3
  51. package/lib/pipeline.js.map +1 -1
  52. package/lib/plugin.js +2 -2
  53. package/lib/plugin.js.map +1 -1
  54. package/lib/runtime/cli.js +4 -4
  55. package/lib/runtime/cli.js.map +1 -1
  56. package/lib/types.d.ts +2 -2
  57. package/package.json +2 -2
  58. package/lib/generators/sdk/index.d.ts.map +0 -1
  59. package/lib/generators/sdk/index.js.map +0 -1
package/README.md CHANGED
@@ -27,7 +27,7 @@ import { generateClient } from '@redocly/client-generator';
27
27
  const result = await generateClient({
28
28
  api: './openapi.yaml', // file path or URL; OpenAPI 3.0/3.1/3.2 or Swagger 2.0
29
29
  output: './src/api/client.ts',
30
- generators: ['sdk', 'zod'],
30
+ generators: ['typescript', 'zod'],
31
31
  });
32
32
 
33
33
  console.log(`Wrote ${result.files.length} file(s), ${result.bytes} bytes.`);
@@ -64,7 +64,7 @@ import { tsType } from '@redocly/client-generator/generate';
64
64
 
65
65
  export default defineGenerator({
66
66
  name: 'response-map',
67
- requires: ['sdk'],
67
+ requires: ['typescript'],
68
68
  run({ model, outputPath }) {
69
69
  const printer = new Printer();
70
70
  // One `ResponseShapes` entry per operation with a JSON success body.
@@ -6,7 +6,7 @@ in the same pass as the built-ins; select it by path in `redocly.yaml`:
6
6
 
7
7
  ```yaml
8
8
  client:
9
- generators: [sdk, ./generators/my-generator.mjs]
9
+ generators: [typescript, ./generators/my-generator.mjs]
10
10
  ```
11
11
 
12
12
  ## The contract
@@ -51,7 +51,7 @@ Users set them per generator name:
51
51
 
52
52
  ```yaml
53
53
  client:
54
- generators: [sdk, ./generators/permissions-matrix.mjs]
54
+ generators: [typescript, ./generators/permissions-matrix.mjs]
55
55
  options:
56
56
  permissions-matrix:
57
57
  groupBy: path
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "cli-docs" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "cli-docs" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator cli-docs --update`.
@@ -49,7 +49,7 @@ var Printer = class {
49
49
 
50
50
  // src/runtime/cli.ts
51
51
  function groupSlug(group) {
52
- return group.trim().replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
52
+ return group.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
53
53
  }
54
54
  __name(groupSlug, "groupSlug");
55
55
  function envPrefix(binName) {
@@ -153,13 +153,14 @@ function renderCliDocs(commands, options) {
153
153
  printer.blank();
154
154
  printer.line("| Flag | Description |");
155
155
  printer.line("| ---- | ----------- |");
156
+ const hasBearer = options.schemes.some((scheme) => scheme.kind === "bearer");
156
157
  for (const [flag, description] of [
157
158
  ["--server-url <url>", "Override the server URL included in the client."],
158
159
  ["--format <json\\|ndjson>", "Output format."],
159
160
  ["--dry-run", "Print the prepared request, credentials redacted, without sending it."],
160
161
  ["--page-all", "Follow pagination, printing one JSON page per line."],
161
162
  ["--output <path>", "Write the response body to a file. Required for binary responses."],
162
- ["--token <token>", "Bearer token, overriding the environment."],
163
+ ...hasBearer ? [["--token <token>", "Bearer token, overriding the environment."]] : [],
163
164
  ["--json <json\\|@file\\|@->", "Request body, inline or from a file or stdin."]
164
165
  ]) {
165
166
  printer.line(`| \`${flag}\` | ${description} |`);
@@ -586,7 +587,7 @@ var entry_cli_docs_default = {
586
587
  "importExt": "a Markdown page has no imports",
587
588
  "runtime": "a Markdown page embeds no runtime"
588
589
  },
589
- requiresGenerator: "^0.0.0-snapshot.1786460732"
590
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
590
591
  };
591
592
  export {
592
593
  entry_cli_docs_default as default
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "cli" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "cli" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator cli --update`.
@@ -43,7 +43,7 @@ var RUNTIME_SOURCES_STRIPPED = {
43
43
  "sse.ts": "/**\n * A frame delimiter: two consecutive line terminators (each CR, LF, or CRLF, per the SSE\n * spec \u2014 so mixed endings like `\\n\\r\\n` are valid boundaries, not just matching pairs).\n */\nconst FRAME_DELIMITER = /(?:\\r\\n|\\r|\\n){2}/;\n\n/**\n * A terminally malformed event stream \u2014 unparseable JSON `data` or an unbounded frame.\n * A stable bad payload, not a dropped connection, so the stream never reconnects on it.\n */\nclass SseParseError extends Error {}\n\n/**\n * Consume a `text/event-stream` operation as typed events (capability module \u2014 wired\n * into `createClient`). Auto-reconnects on dropped connections, resuming from the last\n * seen event id via `Last-Event-ID` (backoff: the server's `retry:` value, then\n * `reconnectDelay`, then 1s \u2014 exponential with jitter, capped at 30s). A clean stream\n * end flushes a trailing frame and finishes; `break`/abort end the iterator cleanly.\n */\nasync function* sse<T>(\n config: ClientConfig,\n op: OperationContext,\n prepare: () => Promise<{ url: string; init: SseOptions; body?: unknown }>,\n dataKind: 'json' | 'text' = 'text'\n): AsyncGenerator<ServerSentEvent<T>> {\n let lastEventId: string | undefined;\n let serverRetry: number | undefined;\n let failures = 0;\n while (true) {\n // Re-prepare each attempt so a refresh-style TokenProvider yields a fresh credential\n // on reconnect (the auth is baked into `url` query + `init.headers`). `reconnect`,\n // `reconnectDelay`, and `signal` come from the caller's original options unchanged.\n const { url, init, body: requestBody } = await prepare();\n const { reconnect = true, reconnectDelay, ...rest } = init;\n const signal = rest.signal ?? undefined;\n if (signal?.aborted) return;\n const headers: Record<string, string> = {\n Accept: 'text/event-stream',\n ...toHeaderRecord(rest.headers),\n };\n const sendHeaders =\n lastEventId === undefined ? headers : { ...headers, 'Last-Event-ID': lastEventId };\n try {\n const { response } = await send(\n config,\n op,\n url,\n // `timeout: 0` opts the stream out of a config-level timeout \u2014 an event stream\n // is long-lived by design and must not be severed after N milliseconds.\n { ...rest, method: rest.method ?? 'GET', headers: sendHeaders, timeout: 0 },\n requestBody,\n undefined,\n {}\n );\n if (!response.ok) {\n const errorBody = await readError(response);\n throw new ApiError(url, response.status, response.statusText, errorBody);\n }\n failures = 0;\n const body = response.body;\n if (!body) return;\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n let match: RegExpExecArray | null;\n while ((match = FRAME_DELIMITER.exec(buffer)) !== null) {\n const raw = buffer.slice(0, match.index);\n buffer = buffer.slice(match.index + match[0].length);\n const event = parseSseFrame(raw, dataKind);\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n }\n if (done) {\n // Stream closed cleanly. Flush a final event that arrived without a trailing\n // delimiter, then finish \u2014 a clean end is not a dropped connection, so do not reconnect.\n const event = buffer.length > 0 ? parseSseFrame(buffer, dataKind) : undefined;\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n return;\n }\n // Bound memory: a server that never sends a frame delimiter would otherwise\n // grow `buffer` without limit. 1 MiB is far above any real SSE frame.\n if (buffer.length > 1048576) {\n throw new SseParseError('SSE frame exceeded 1048576 characters without a delimiter');\n }\n }\n } finally {\n await reader.cancel().catch(() => undefined);\n }\n } catch (error) {\n if (signal?.aborted) return;\n // A non-OK HTTP response (4xx/5xx) or an unparseable JSON payload is a definitive\n // error, not a transient drop \u2014 surface it instead of reconnecting in a loop (a\n // stable bad payload would otherwise reconnect forever).\n if (error instanceof ApiError || error instanceof SseParseError) throw error;\n // A transport failure (connect/DNS/reset) when opening the request, or a mid-stream\n // read error, is a dropped connection: fall through to backoff/reconnect when enabled.\n if (!reconnect) throw error;\n }\n // Only the swallowed-drop path reaches here: reconnect is on and the signal not aborted.\n failures++;\n const base = serverRetry ?? reconnectDelay ?? 1000;\n const delay = Math.min(base * Math.pow(2, failures - 1), 30_000);\n try {\n await sleep(Math.random() * delay, signal);\n } catch {\n return; // sleep rejects only on abort \u2014 end the iterator cleanly\n }\n }\n}\n\n/** Parse one raw SSE frame (its lines) into an event; returns undefined for comment-only frames. */\nfunction parseSseFrame(\n raw: string,\n dataKind: 'json' | 'text'\n): ServerSentEvent<unknown> | undefined {\n let event: string | undefined;\n const dataLines: string[] = [];\n let id: string | undefined;\n let retry: number | undefined;\n let sawField = false;\n for (const line of raw.split(/\\r\\n|\\n|\\r/)) {\n if (line === '' || line.startsWith(':')) continue;\n const colon = line.indexOf(':');\n const field = colon === -1 ? line : line.slice(0, colon);\n let val = colon === -1 ? '' : line.slice(colon + 1);\n if (val.startsWith(' ')) val = val.slice(1);\n sawField = true;\n if (field === 'event') event = val;\n else if (field === 'data') dataLines.push(val);\n else if (field === 'id') id = val;\n else if (field === 'retry') {\n // ASCII digits only, per the EventSource spec \u2014 anything else is ignored\n // (`Number('')` is 0 and would zero the reconnect backoff).\n if (/^\\d+$/.test(val)) retry = Number(val);\n }\n }\n if (!sawField) return undefined;\n const dataText = dataLines.join('\\n');\n let data: unknown = dataText;\n if (dataKind === 'json' && dataText !== '') {\n try {\n data = JSON.parse(dataText);\n } catch (error) {\n throw new SseParseError(\n `Failed to parse SSE event data as JSON: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n return { event, data, id, retry };\n}",
44
44
  "create-client.ts": "/**\n * The optional behaviors `createClientCore` can dispatch to but never statically\n * imports. The package's public `createClient` wires the full set; the future\n * inline-mode assembler wires only the capabilities a spec needs.\n */\ntype Capabilities = SendCapabilities & {\n resolveAuth?: (\n security: readonly (readonly SecuritySpec[])[],\n config: ClientConfig\n ) => Promise<{ headers: Record<string, string>; query: Record<string, string> }>;\n sse?: (\n config: ClientConfig,\n op: OperationContext,\n // Re-preparing per (re)connect (not a frozen url/init) lets a refresh-style\n // TokenProvider issue a fresh credential after a dropped stream reconnects.\n prepare: () => Promise<{ url: string; init: SseOptions }>,\n dataKind: 'json' | 'text'\n ) => AsyncGenerator<ServerSentEvent<unknown>>;\n paginate?: {\n pages: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n items: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n // The `link`-style iterators need the raw `Link` header + page URL, which the\n // parsed-page call above cannot carry (the shape mirrors paginate's `LinkPageCall`).\n pagesByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n itemsByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n };\n};\n\n/** The grouped args wire shape: path params by name plus the `params`/`body`/`headers`/`cookies` slots. */\ntype OperationArgs = {\n params?: Record<string, QueryValue>;\n body?: unknown;\n headers?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n} & Record<string, unknown>;\n\n/** The response reader implied by the descriptor (before any per-call `parseAs` override). */\n/**\n * The `Accept` header matching how the response will be read \u2014 a blob/text operation\n * must not ask for `application/json` (a content-negotiating server would 406 or\n * answer with a JSON error body instead of the payload). Caller `init.headers` and\n * `config.headers` still override.\n */\nfunction acceptFor(kind: ParseAs | 'void'): string {\n if (kind === 'text') return 'text/*';\n if (kind === 'blob' || kind === 'arrayBuffer' || kind === 'stream' || kind === 'formData') {\n return '*/*';\n }\n return 'application/json'; // json | auto | void\n}\n\nfunction kindFor(op: OperationDescriptor): ParseAs | 'void' {\n if (op.responseKind === 'void' || op.responseKind === 'blob' || op.responseKind === 'text') {\n return op.responseKind;\n }\n return 'auto';\n}\n\n/** Route the grouped args by the descriptor: path values, query object, body, extra headers, cookies. */\nfunction splitArgs(op: OperationDescriptor, args: OperationArgs) {\n const path: Record<string, unknown> = {};\n const pathNames = new Set<string>();\n for (const param of op.params ?? []) {\n if (param.in === 'path') {\n pathNames.add(param.name);\n path[param.name] = args[param.name];\n }\n }\n // An unknown top-level key can only be a bug (usually a flat-style call shape passed\n // to a grouped client: `{ limit: 10 }` instead of `{ params: { limit: 10 } }`).\n // TypeScript catches it, but transpilers that skip type-checking would otherwise\n // ship a request that silently drops the value \u2014 fail the call loudly instead.\n for (const key of Object.keys(args)) {\n if (key === 'params' || key === 'body' || key === 'headers' || key === 'cookies') continue;\n if (pathNames.has(key)) continue;\n throw new TypeError(\n `Unknown argument \"${key}\" for operation \"${op.id}\". Query parameters go under params: { \u2026 } and the request body under body; valid keys are params, body, headers, cookies` +\n (pathNames.size > 0 ? `, and the path parameters (${[...pathNames].join(', ')}).` : '.')\n );\n }\n return {\n path,\n query: args.params,\n body: args.body,\n headers: args.headers,\n cookies: args.cookies,\n };\n}\n\n/**\n * The query-serialization hints for the descriptor's query params. A spec is built only\n * when the param deviates from the OpenAPI defaults (`form` + `explode: true`, encoded),\n * and always fully resolved \u2014 so `explode: false` or `allowReserved` alone (no `style`)\n * are honored, and an omitted `explode` keeps the exploded default.\n */\nfunction queryStyles(op: OperationDescriptor): Record<string, QueryStyle> | undefined {\n let styles: Record<string, QueryStyle> | undefined;\n for (const param of op.params ?? []) {\n if (param.in !== 'query') continue;\n const deviates =\n (param.style !== undefined && param.style !== 'form') ||\n param.explode === false ||\n param.allowReserved === true;\n if (!deviates) continue;\n styles ??= {};\n styles[param.name] = {\n style: param.style ?? 'form',\n explode: param.explode ?? true,\n allowReserved: param.allowReserved,\n };\n }\n return styles;\n}\n\n/** Stringify caller-supplied extra headers, skipping empty entries. */\nfunction stringHeaders(headers: Record<string, unknown> | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers ?? {})) {\n if (value !== undefined && value !== null) out[key] = String(value);\n }\n return out;\n}\n\n/** Build the request pieces an attempt needs: the final URL and the merged per-call init. */\nasync function prepareRequest(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions | SseOptions,\n caps: Capabilities\n): Promise<{ url: string; init: RequestOptions; body: unknown }> {\n const { path, query, body, headers, cookies } = splitArgs(op, args);\n const authed: { headers: Record<string, string>; query: Record<string, string> } =\n op.security?.length && caps.resolveAuth\n ? await caps.resolveAuth(op.security, config)\n : { headers: {}, query: {} };\n // Cookie params join the auth-injected cookies in one `Cookie` header (values\n // percent-encoded, like auth cookies). Server-side only \u2014 browsers own the header.\n const cookiePairs = Object.entries(cookies ?? {})\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([cookieName, value]) => `${cookieName}=${encodeURIComponent(String(value))}`);\n if (cookiePairs.length > 0) {\n authed.headers.Cookie = [authed.headers.Cookie, ...cookiePairs].filter(Boolean).join('; ');\n }\n const fullQuery: Record<string, QueryValue> = { ...query, ...authed.query };\n const url = buildUrl(\n config.serverUrl ?? '',\n substitutePath(op.path, path),\n Object.keys(fullQuery).length > 0 ? fullQuery : undefined,\n queryStyles(op)\n );\n const mergedInit: RequestOptions = {\n ...init,\n method: op.method.toUpperCase(),\n // Precedence, lowest \u2192 highest (later spreads win): injected auth \u2192 explicit\n // header params \u2192 caller `init.headers` \u2014 the caller always overrides both.\n headers: {\n ...authed.headers,\n ...stringHeaders(headers),\n ...toHeaderRecord(init.headers),\n },\n };\n return { url, init: mergedInit, body };\n}\n\n/** Coerce a single declared response header value; omit when absent or unparsable. */\nfunction coerceResponseHeader(\n raw: string | null,\n type: ResponseHeaderSpec['type']\n): string | number | boolean | undefined {\n if (raw === null) return undefined;\n if (type === 'number') {\n if (raw.trim() === '') return undefined;\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n }\n if (type === 'boolean') {\n const value = raw.trim().toLowerCase();\n if (value === 'true') return true;\n if (value === 'false') return false;\n return undefined;\n }\n return raw;\n}\n\n/** Build the camelCase declared-header bag for a throw-mode envelope. */\nfunction readEnvelopeHeaders(\n response: Response,\n specs: readonly ResponseHeaderSpec[] | undefined\n): Record<string, string | number | boolean> {\n const headers: Record<string, string | number | boolean> = {};\n for (const spec of specs ?? []) {\n const value = coerceResponseHeader(response.headers.get(spec.name), spec.type);\n if (value !== undefined) headers[spec.key] = value;\n }\n return headers;\n}\n\n/** One non-SSE call: send, then branch on the configured error mode. */\nasync function execute(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions,\n caps: Capabilities\n): Promise<unknown> {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // `parseAs` / `envelope` are client options, not fetch RequestInit fields.\n const { parseAs, envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const { response, context } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (config.errorMode === 'result') {\n if (!response.ok) {\n return { data: undefined, error: await readError(response), response };\n }\n return { data: await parse(response, readKind), error: undefined, response };\n }\n if (!response.ok) {\n let error: globalThis.Error = new ApiError(\n context.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n // Thread the error through each middleware's onError in turn (each may replace it).\n for (const mw of middlewareChain(config)) {\n if (mw.onError) error = await mw.onError(error as ApiErrorLike, context);\n }\n throw error;\n }\n const data = await parse(response, readKind);\n if (envelope === true) {\n return {\n data,\n headers: readEnvelopeHeaders(response, op.responseHeaders),\n response,\n };\n }\n return data;\n}\n\n/** The paginate capability, or a descriptive throw when a paginated op is iterated unwired. */\nfunction paginateCapability(caps: Capabilities, op: OperationDescriptor) {\n if (!caps.paginate) {\n throw new Error(`Pagination capability not wired: cannot iterate operation \"${op.id}\"`);\n }\n return caps.paginate;\n}\n\n/**\n * The per-page call the iterators drive: the method itself in throw mode; in result\n * mode a wrapper that unwraps the `{ data, error, response }` envelope \u2014 the page\n * pointers are data-rooted \u2014 rethrowing a failed page as `ApiError` (iteration is\n * error-mode-agnostic; the throw-mode-only `onError` middleware hook is not invoked).\n */\nfunction pageCall(\n method: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n config: ClientConfig\n) {\n const callWithoutEnvelope = (args?: OperationArgs, init?: RequestOptions) => {\n if (!init || init.envelope === undefined) return method(args, init);\n const { envelope: _envelope, ...pageInit } = init;\n return method(args, pageInit);\n };\n if (config.errorMode !== 'result') return callWithoutEnvelope;\n return async (args?: OperationArgs, init?: RequestOptions) => {\n const envelope = (await callWithoutEnvelope(args, init)) as {\n data: unknown;\n error: unknown;\n response: Response;\n };\n // Failure is `!response.ok` \u2014 NOT `data === undefined`: a successful bodyless page\n // (204/void) also parses to undefined data, and a failed page's `error` can be\n // undefined too (unreadable body). The pointers then miss on the undefined data\n // and iteration stops cleanly, which is the correct semantics for an empty page.\n if (!envelope.response.ok) {\n const { response } = envelope;\n throw new ApiError(response.url, response.status, response.statusText, envelope.error);\n }\n return envelope.data;\n };\n}\n\n/**\n * The per-page call the `link`-style iterators drive: like `execute`, but returning the\n * parsed page together with the raw `Link` header and the page's own URL (for resolving\n * a relative `rel=\"next\"` target). Error-mode-agnostic like all iteration: a failed\n * page throws `ApiError` even on result-mode clients.\n */\nfunction linkPageCall(config: ClientConfig, op: OperationDescriptor, caps: Capabilities) {\n return async (args: OperationArgs = {}, init: RequestOptions = {}) => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const { parseAs, envelope: _envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n const { response } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (!response.ok) {\n throw new ApiError(\n prepared.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n }\n return {\n page: await parse(response, readKind),\n linkHeader: response.headers.get('link'),\n // Some `Response` implementations leave `url` empty (mocks, constructed responses).\n url: response.url === '' ? prepared.url : response.url,\n };\n };\n}\n\n/**\n * Build a typed instance client over operation descriptors: one real bound method per\n * operation (attached by a construction-time loop \u2014 no Proxy), plus the core members\n * (`configure`/`use`/`auth`), which are assigned AFTER the loop so they win any name\n * collision with an operation. All behavior dispatches through the capability seam.\n */\nfunction createClientCore<\n Ops extends OpsShape,\n Id extends string = string,\n Path extends string = string,\n Tag extends string = string,\n>(\n operations: Record<string, OperationDescriptor>,\n initial: ClientConfig<OperationContext<Id, Path, Tag>> = {},\n caps: Capabilities = {}\n): Client<Ops, OperationContext<Id, Path, Tag>> {\n // The literal-union narrowing is a compile-time DX contract only; internally the\n // runtime works with the base (string-typed) context. One cast at this boundary \u2014\n // `ClientConfig<Narrow>` is not assignable to `ClientConfig` (middleware ctx\n // params are contravariant).\n const given = initial as ClientConfig;\n // Private mutable config; the middleware array is copied so `use()` never mutates the caller's.\n const config: ClientConfig = { ...given, middleware: [...(given.middleware ?? [])] };\n const client = {} as Record<string, unknown>;\n\n for (const [name, op] of Object.entries(operations)) {\n if (op.responseKind === 'sse') {\n const method = (args: OperationArgs = {}, init: SseOptions = {}) => {\n if (!caps.sse) {\n throw new Error(`SSE capability not wired: cannot stream operation \"${op.id}\"`);\n }\n const stream = caps.sse;\n return (async function* () {\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // A thunk the stream re-runs on every (re)connect, so auth (which `prepareRequest`\n // resolves) is refreshed per attempt rather than frozen at the first connect.\n const prepare = async () => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n return { url: prepared.url, init: prepared.init as SseOptions, body: prepared.body };\n };\n yield* stream(config, opCtx, prepare, op.sseDataKind ?? 'text');\n })();\n };\n // Consumers key off the function reference (cache keys, `OPERATIONS[fn.name]`), so\n // each closure carries its operationId instead of an inferred binding name.\n // `operationId` is the explicit, minification-proof form of the same identity\n // (the SPEC operationId \u2014 `name` is the emitted key, which a collision may rename).\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n client[name] = method;\n } else {\n const method = (args: OperationArgs = {}, init: RequestOptions = {}) =>\n execute(config, op, args, init, caps);\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n const spec = op.pagination;\n // Paginated ops keep their one-shot call and gain `.pages`/`.items`, dispatching\n // through the capability seam (like SSE: absent capability throws descriptively).\n // Iteration is error-mode-agnostic: the iterators' pointers are data-rooted, so on\n // a result-mode client (`errorMode` is fixed at construction \u2014 `configure()`\n // ignores it) each page's envelope is unwrapped before it reaches the capability.\n // A failed page aborts iteration by throwing ApiError, even on result-mode\n // clients; the `onError` middleware hook (throw-mode-only) is not invoked.\n client[name] =\n spec === undefined\n ? method\n : spec.style === 'link'\n ? Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pagesByLink(\n linkPageCall(config, op, caps),\n args,\n init\n ),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).itemsByLink(\n linkPageCall(config, op, caps),\n spec,\n args,\n init\n ),\n })\n : Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pages(pageCall(method, config), spec, args, init),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).items(pageCall(method, config), spec, args, init),\n });\n }\n }\n\n // Core members are assigned AFTER the operation loop \u2014 they win over colliding op names.\n client.configure = (next: ClientConfig): void => {\n // `errorMode` is fixed at generate time (it shapes the static types); flipping it at\n // runtime would silently desync return shapes from `Client<Ops>`, so it is ignored.\n const { errorMode: _fixed, auth, ...rest } = next;\n Object.assign(config, rest);\n // `auth` merges into existing credentials (like the `auth.*` setters) rather than\n // replacing wholesale \u2014 so `configure({ auth: { bearer } })` keeps a previously set\n // basic/apiKey. `apiKey` merges per scheme.\n if (auth) {\n config.auth = {\n ...config.auth,\n ...auth,\n ...(auth.apiKey ? { apiKey: { ...config.auth?.apiKey, ...auth.apiKey } } : {}),\n };\n }\n };\n client.use = (...middleware: Middleware[]): void => {\n // Reassign (don't push) so a caller-provided `middleware` array isn't mutated.\n config.middleware = [...(config.middleware ?? []), ...middleware];\n };\n client.auth = {\n bearer(token: TokenProvider): void {\n config.auth = { ...config.auth, bearer: token };\n },\n basic(username: string, password: string): void {\n config.auth = { ...config.auth, basic: { username, password } };\n },\n apiKey(scheme: string, value: TokenProvider): void {\n config.auth = { ...config.auth, apiKey: { ...config.auth?.apiKey, [scheme]: value } };\n },\n };\n\n return client as Client<Ops, OperationContext<Id, Path, Tag>>;\n}",
45
45
  "paginate.ts": "/**\n * Auto-pagination (capability module \u2014 wired into `createClient`, dispatched by the\n * method's `.pages()`/`.items()`): walk an operation's pages by advancing the descriptor's\n * `param` query parameter, per its `style`. The caller's args are never mutated \u2014 each\n * request gets a fresh `params` clone \u2014 and `init` is forwarded to every call.\n *\n * Iteration is error-mode-agnostic: `call` always resolves to the RAW page (on a\n * result-mode client the attachment unwraps the envelope first), so a failed page\n * aborts iteration by throwing `ApiError`, even on result-mode clients; the `onError`\n * middleware hook (throw-mode-only) is not invoked.\n */\n\n/**\n * Resolve an RFC 6901 JSON pointer (`~1` \u2192 `/`, `~0` \u2192 `~`) against a value.\n * The empty pointer is the whole document; anything else must start with `/`.\n * Returns `undefined` on any miss (bad token, absent key, non-object step) \u2014 never throws.\n */\nfunction resolvePointer(value: unknown, pointer: string): unknown {\n if (pointer === '') return value;\n if (!pointer.startsWith('/')) return undefined;\n let current = value;\n for (const token of pointer.slice(1).split('/')) {\n const key = token.replaceAll('~1', '/').replaceAll('~0', '~');\n if (Array.isArray(current)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) return undefined;\n current = current[Number(key)];\n } else if (Object(current) === current && key in (current as object)) {\n current = (current as Record<string, unknown>)[key];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Iterate an operation's full page results. Every page is yielded before the stop\n * condition is evaluated, so the last page always arrives. Cursor style resumes from a\n * caller-provided `params[spec.param]`, stops when the optional `hasMore` pointer\n * resolves to `false` or when `nextCursor` resolves to `undefined`/`null`/`''`, and\n * throws if the next cursor is not a string or number, or\n * if the same cursor comes back twice in a row (infinite-loop guards). Offset/page\n * styles advance by item count / by one and stop when\n * the `items` pointer misses or the array is empty.\n */\nasync function* pages<TPage>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<TPage>,\n spec: PaginationSpec,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n if (spec.style === 'cursor') {\n let cursor: unknown = args.params?.[spec.param];\n while (true) {\n const params = { ...args.params };\n if (cursor !== undefined) params[spec.param] = cursor as QueryValue;\n const page = await call({ ...args, params }, init);\n yield page;\n // Connection-style APIs keep a non-null cursor on the last page and signal the\n // end via a boolean flag \u2014 honor it before the cursor check to skip the\n // follow-up empty request. Strictly `false`: a missing pointer falls through.\n if (spec.hasMore !== undefined && resolvePointer(page, spec.hasMore) === false) return;\n const next = resolvePointer(page, spec.nextCursor);\n if (next === undefined || next === null || next === '') return;\n if (typeof next !== 'string' && typeof next !== 'number') {\n // A fresh non-scalar cursor never compares equal, so without this guard a lying\n // server would slip past the did-not-advance check into an infinite loop.\n throw new Error(`Pagination cursor at ${spec.nextCursor} is not a string or number`);\n }\n if (next === cursor) {\n throw new Error('Pagination did not advance: operation returned the same cursor twice');\n }\n cursor = next;\n }\n } else if (spec.style === 'link') {\n // `link` iteration needs the response's `Link` header, which the parsed-page `call`\n // cannot carry \u2014 the client wires those operations to `pagesByLink` instead.\n throw new Error('link-style pagination iterates via pagesByLink');\n } else {\n // Coerce the starting position to a number: a caller may pass `params[spec.param]` as a\n // string (common from URL/form input), and `+=` on a string would concatenate. `null`\n // and `''` count as absent \u2014 `Number` would turn them into 0, but a one-shot call\n // omits the param for those values, so the iterator must not start at position 0.\n const start = args.params?.[spec.param];\n const fallback = spec.style === 'page' ? 1 : 0;\n const absent = start === undefined || start === null || start === '';\n let position = absent || Number.isNaN(Number(start)) ? fallback : Number(start);\n let previousItems: string | undefined;\n while (true) {\n const page = await call(\n { ...args, params: { ...args.params, [spec.param]: position } },\n init\n );\n const pageItems = resolvePointer(page, spec.items);\n // Some APIs clamp a past-the-end offset/page to the last non-empty page instead\n // of returning an empty one \u2014 the repeated page would otherwise loop forever\n // (the position always \"advances\", so a cursor-style token check can't catch it).\n const serialized = Array.isArray(pageItems) ? JSON.stringify(pageItems) : undefined;\n if (serialized !== undefined && serialized === previousItems) {\n throw new Error('Pagination did not advance: the operation returned the same page twice');\n }\n yield page;\n if (!Array.isArray(pageItems) || pageItems.length === 0) return;\n previousItems = serialized;\n position += spec.style === 'page' ? 1 : pageItems.length;\n }\n }\n}\n\n/**\n * Iterate the operation's individual items: each page's `items` pointer, flattened.\n * A cursor-style page whose pointer misses yields nothing but pagination continues;\n * for offset/page styles a miss has already stopped `pages`.\n */\nasync function* items<TItem>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pages(call, spec, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}\n\n/**\n * The per-page call the `link`-style iterators drive: the parsed page plus the raw\n * `Link` header and the page's own URL (for resolving a relative `next` target).\n */\ntype LinkPageCall = (\n args?: OperationArgs,\n init?: RequestOptions\n) => Promise<{ page: unknown; linkHeader: string | null; url: string }>;\n\n/** The `rel=\"next\"` target of an RFC 8288 `Link` header, or `undefined` when absent. */\nfunction linkNext(header: string | null): string | undefined {\n if (header === null) return undefined;\n // Entries are `<url>; rel=\"next\"`, comma-separated; the URL is inside `<>`, so split\n // on commas that precede a `<` and a comma inside a target cannot break an entry.\n for (const entry of header.split(/,\\s*(?=<)/)) {\n const target = /^\\s*<([^>]*)>(.*)$/.exec(entry);\n if (!target) continue;\n const rel = /;\\s*rel\\s*=\\s*\"?([^\";]+)\"?/i.exec(target[2]);\n // `rel` may carry several space-separated relation types (RFC 8288 \xA73.3).\n if (rel && rel[1].split(/\\s+/).includes('next')) return target[1];\n }\n return undefined;\n}\n\n/**\n * Iterate a `link`-style operation's pages: follow the `Link` header's `rel=\"next\"`\n * target by merging ITS query params into the next call \u2014 every page goes through the\n * same declared endpoint, so auth, middleware, and `serverUrl` handling apply\n * unchanged, and credentials can never be handed to a cross-origin `next` URL.\n * Stops when no `rel=\"next\"` is present; throws when the link does not advance\n * (the same target twice, or a self-link \u2014 an infinite-loop guard).\n */\nasync function* pagesByLink<TPage>(\n call: LinkPageCall,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n let params = args.params;\n let previous: string | undefined;\n while (true) {\n const { page, linkHeader, url } = await call({ ...args, params }, init);\n yield page as TPage;\n const target = linkNext(linkHeader);\n if (target === undefined) return;\n // A relative target resolves against the page's own URL (RFC 8288 \xA73.1) \u2014 which may\n // itself be relative (relative `serverUrl`, mocked fetch), so both resolve against a\n // placeholder origin. It never reaches the wire: only the target's query params\n // carry into the next call.\n const pageUrl = new URL(url, 'http://relative.invalid');\n const next = new URL(target, pageUrl).toString();\n if (next === previous || next === pageUrl.toString()) {\n throw new Error('Pagination did not advance: the Link rel=\"next\" target repeats');\n }\n previous = next;\n // A repeated key (`?tag=a&tag=b`) folds into an array \u2014 the query serializer\n // expands arrays back into repeated pairs.\n const linkParams: Record<string, string | string[]> = {};\n for (const [key, value] of new URL(next).searchParams) {\n const seen = linkParams[key];\n if (seen === undefined) linkParams[key] = value;\n else if (Array.isArray(seen)) seen.push(value);\n else linkParams[key] = [seen, value];\n }\n params = { ...args.params, ...linkParams };\n }\n}\n\n/** Iterate a `link`-style operation's individual items: each page's `items` pointer, flattened. */\nasync function* itemsByLink<TItem>(\n call: LinkPageCall,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pagesByLink(call, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}",
46
- "cli.ts": "// The generated-CLI engine: a pure argv parser plus a dispatcher that drives the\n// instance client and maps outcomes to the documented exit-code contract\n// (0 success, 1 API error, 2 auth, 3 validation, 4 usage). Node-only by nature,\n// but every effect (env, stdin, files, output) is injected through the wiring so\n// the module itself stays dependency-free and fully unit-testable; the emitted\n// entry fills the defaults with real `node:fs`/`process` bindings.\n\n/** One flag derived from a query parameter. */\ntype CliFlag = {\n /** Kebab-cased flag name (`--page-size`). */\n name: string;\n /** Original wire parameter name. */\n param: string;\n type: 'string' | 'number' | 'boolean' | 'array';\n required: boolean;\n enum?: string[];\n description?: string;\n};\n\n/** One executable command, derived from the IR at generate time. Pure data. */\ntype CliCommand = {\n /** Tag; absent = flat/untagged. */\n group?: string;\n name: string;\n summary?: string;\n method: string;\n path: string;\n /** Path params, in path-template order. Always required \u2014 that is what a path is. */\n positionals: Array<{\n name: string;\n type?: CliFlag['type'];\n description?: string;\n }>;\n flags: CliFlag[];\n /** Present when the operation takes a JSON request body. */\n body?: { required: boolean };\n /**\n * The content type of a request body that is NOT JSON (multipart, url-encoded, binary).\n * `--json` cannot build one, so the command is reported as library-only rather than\n * offered as if it were runnable.\n */\n unsupportedBody?: string;\n paginated?: boolean;\n sse?: boolean;\n blob?: boolean;\n /** IR schemas for the `schema` command, serialized verbatim. */\n schemas?: { request?: unknown; response?: unknown };\n};\n\ntype CliAuthScheme = { key: string; kind: 'bearer' | 'basic' | 'apiKey' };\n\ntype CliWiring = {\n binName: string;\n /** Credential variable prefix. Defaults to `binName`, constant-cased \u2014 set it when the\n * displayed name and the credential family must differ (a composed multi-API binary). */\n envPrefix?: string;\n /** The generated instance client (grouped-args methods). */\n client: Record<string, unknown>;\n configure: (config: Record<string, unknown>) => void;\n /** Security schemes of the API \u2014 drives env-var credential resolution. */\n schemes?: CliAuthScheme[];\n env?: Record<string, string | undefined>;\n stdin?: () => string;\n readFile?: (path: string) => string;\n writeFile?: (path: string, data: Uint8Array) => void;\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n};\n\ntype CliGlobals = {\n serverUrl?: string;\n format?: 'json' | 'ndjson';\n dryRun?: boolean;\n pageAll?: boolean;\n output?: string;\n token?: string;\n json?: string;\n};\n\ntype CliInvocation =\n | { kind: 'help'; topic?: CliCommand | string }\n | { kind: 'schema'; command: CliCommand }\n | {\n kind: 'run';\n command: CliCommand;\n positionals: Record<string, string>;\n params: Record<string, unknown>;\n globals: CliGlobals;\n }\n | { kind: 'usage-error'; message: string };\n\n/**\n * A hand-written command composed NEXT TO the generated ones: the same data shape plus a\n * `handler`, so it inherits help, parsing, `schema`, and the exit-code contract. This is\n * how behavior that is not in the description (a `login`, a doctor command) joins the\n * binary without the generator ever learning what it does.\n */\ntype CustomCommand = {\n name: string;\n group?: string;\n summary?: string;\n positionals?: CliCommand['positionals'];\n flags?: CliFlag[];\n /** Returns the process exit code; throwing exits 1 with the standard error JSON. */\n handler: (context: CommandContext) => number | Promise<number>;\n};\n\ntype CommandContext = {\n positionals: Record<string, string>;\n params: Record<string, unknown>;\n globals: CliGlobals;\n wiring: CliWiring;\n};\n\n/** One API's contribution to a composed binary: its commands behind a namespace, with its\n * OWN wiring (base URL, schemes, credentials). A namespace-less source sits at the root. */\ntype CommandSource = {\n namespace?: string;\n commands: Array<CliCommand | CustomCommand>;\n /** Absent = inherit the first wired source's \u2014 a root `login` shares the binary's identity. */\n wiring?: CliWiring;\n};\n\ntype ResolvedCommand = CliCommand & { handler?: CustomCommand['handler'] };\n\n/** Custom commands as the command shape the parser reads; generated ones pass through. */\nfunction normalizeCommands(commands: Array<CliCommand | CustomCommand>): ResolvedCommand[] {\n return commands.map((command) =>\n 'handler' in command\n ? { method: '', path: '', positionals: [], flags: [], ...command }\n : command\n );\n}\n\n/**\n * The name of a custom command that shadows another command. Rejected at startup: an\n * operator typing an operationId must never silently run something else.\n */\nfunction shadowedCommandName(commands: ResolvedCommand[]): string | undefined {\n const seen = new Map<string, ResolvedCommand[]>();\n for (const command of commands) {\n const key = `${command.group ?? ''}\\u0000${command.name}`;\n seen.set(key, [...(seen.get(key) ?? []), command]);\n }\n for (const clashing of seen.values()) {\n if (clashing.length > 1 && clashing.some((command) => command.handler !== undefined)) {\n return clashing[0].name;\n }\n }\n return undefined;\n}\n\nconst GLOBAL_FLAGS: Record<string, { key: keyof CliGlobals; boolean?: boolean }> = {\n 'server-url': { key: 'serverUrl' },\n format: { key: 'format' },\n 'dry-run': { key: 'dryRun', boolean: true },\n 'page-all': { key: 'pageAll', boolean: true },\n output: { key: 'output' },\n token: { key: 'token' },\n json: { key: 'json' },\n};\n\n/**\n * The shell-typable form of a group name: an OpenAPI tag can contain spaces (\"Some\n * multi-word tag\"), which only resolves if the user quotes it. Commands are addressed by\n * this slug; help still shows the original tag.\n */\nfunction groupSlug(group: string): string {\n return group\n .trim()\n .replace(/[^A-Za-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase();\n}\n\n/** A description on ONE line: newlines in an OpenAPI description wreck help alignment. */\nfunction oneLine(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\n/** Resolve argv against the command table. Pure \u2014 no I/O, no env. */\nfunction parseInvocation(commands: CliCommand[], argv: string[]): CliInvocation {\n if (argv.length === 0 || argv[0] === '--help') return { kind: 'help' };\n\n if (argv[0] === 'schema') {\n const command = commands.find((candidate) => candidate.name === argv[1]);\n return command\n ? { kind: 'schema', command }\n : { kind: 'usage-error', message: `Unknown command: schema ${argv[1] ?? ''}`.trim() };\n }\n\n const slugs = new Set(commands.filter((c) => c.group).map((c) => groupSlug(c.group as string)));\n let command: CliCommand | undefined;\n let rest: string[];\n if (slugs.has(argv[0])) {\n if (argv[1] === '--help' || argv[1] === undefined) return { kind: 'help', topic: argv[0] };\n command = commands.find((c) => c.group && groupSlug(c.group) === argv[0] && c.name === argv[1]);\n if (!command) return { kind: 'usage-error', message: `Unknown command: ${argv[0]} ${argv[1]}` };\n rest = argv.slice(2);\n } else {\n // An ungrouped command, or a bare operationId \u2014 knowing the group shouldn't be\n // required when the name alone is unambiguous.\n const named = commands.filter((c) => c.name === argv[0]);\n command =\n named.find((c) => c.group === undefined) ?? (named.length === 1 ? named[0] : undefined);\n if (!command) {\n const ambiguous = named.length > 1;\n return {\n kind: 'usage-error',\n message: ambiguous\n ? `Ambiguous command: ${argv[0]} \u2014 prefix it with its group (${named\n .map((c) => groupSlug(c.group as string))\n .join(', ')})`\n : `Unknown command: ${argv[0]}`,\n };\n }\n rest = argv.slice(1);\n }\n if (rest.includes('--help')) return { kind: 'help', topic: command };\n\n const positionals: Record<string, string> = {};\n const params: Record<string, unknown> = {};\n const globals: CliGlobals = {};\n let positionalIndex = 0;\n for (let index = 0; index < rest.length; index++) {\n const token = rest[index];\n if (!token.startsWith('--')) {\n const slot = command.positionals[positionalIndex++];\n if (!slot) return { kind: 'usage-error', message: `Unexpected argument: ${token}` };\n positionals[slot.name] = token;\n continue;\n }\n const equals = token.indexOf('=');\n const flagName = equals === -1 ? token.slice(2) : token.slice(2, equals);\n const inlineValue = equals === -1 ? undefined : token.slice(equals + 1);\n const takeValue = (): string | undefined =>\n inlineValue !== undefined ? inlineValue : rest[++index];\n\n const global = GLOBAL_FLAGS[flagName];\n if (global) {\n if (global.boolean) {\n (globals[global.key] as boolean) = true;\n continue;\n }\n const value = takeValue();\n if (value === undefined) {\n return { kind: 'usage-error', message: `Flag --${flagName} expects a value` };\n }\n if (global.key === 'format' && value !== 'json' && value !== 'ndjson') {\n return { kind: 'usage-error', message: `--format must be one of: json, ndjson` };\n }\n (globals[global.key] as string) = value;\n continue;\n }\n\n const flag = command.flags.find((candidate) => candidate.name === flagName);\n if (!flag) return { kind: 'usage-error', message: `Unknown flag: --${flagName}` };\n if (flag.type === 'boolean') {\n params[flag.param] = true;\n continue;\n }\n const value = takeValue();\n if (value === undefined) {\n return { kind: 'usage-error', message: `Flag --${flagName} expects a value` };\n }\n if (flag.enum && !flag.enum.includes(value)) {\n return {\n kind: 'usage-error',\n message: `--${flagName} must be one of: ${flag.enum.join(', ')}`,\n };\n }\n if (flag.type === 'number') {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) {\n return { kind: 'usage-error', message: `--${flagName} expects a number, got \"${value}\"` };\n }\n params[flag.param] = numeric;\n } else if (flag.type === 'array') {\n const existing = params[flag.param];\n params[flag.param] = Array.isArray(existing) ? [...existing, value] : [value];\n } else {\n params[flag.param] = value;\n }\n }\n\n for (const slot of command.positionals) {\n if (!(slot.name in positionals)) {\n return { kind: 'usage-error', message: `Missing required argument: <${slot.name}>` };\n }\n }\n for (const flag of command.flags) {\n if (flag.required && !(flag.param in params)) {\n return { kind: 'usage-error', message: `Missing required flag: --${flag.name}` };\n }\n }\n if (globals.json !== undefined && !command.body) {\n return { kind: 'usage-error', message: `${command.name} does not accept a request body` };\n }\n if (command.body?.required && globals.json === undefined) {\n return {\n kind: 'usage-error',\n message: `${command.name} requires a request body: pass --json '<json>', --json @file.json, or --json @-`,\n };\n }\n if (globals.pageAll && !command.paginated) {\n return {\n kind: 'usage-error',\n message: `${command.name} is not paginated; --page-all only applies to paginated operations`,\n };\n }\n return { kind: 'run', command, positionals, params, globals };\n}\n\n/** Credential env-var prefix: bin name constant-cased (`cafe-api` \u2192 `CAFE_API`). */\nfunction envPrefix(binName: string): string {\n return binName\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .toUpperCase();\n}\n\nfunction resolveAuth(wiring: CliWiring, token: string | undefined): Record<string, unknown> {\n const env = wiring.env ?? {};\n const prefix = wiring.envPrefix ?? envPrefix(wiring.binName);\n const auth: Record<string, unknown> = {};\n for (const scheme of wiring.schemes ?? []) {\n if (scheme.kind === 'bearer') {\n const value = token ?? env[`${prefix}_TOKEN`];\n if (value !== undefined) auth.bearer = value;\n } else if (scheme.kind === 'basic') {\n const username = env[`${prefix}_USERNAME`];\n const password = env[`${prefix}_PASSWORD`];\n if (username !== undefined && password !== undefined) auth.basic = { username, password };\n } else {\n const value = env[`${prefix}_API_KEY_${envPrefix(scheme.key)}`];\n if (value !== undefined) {\n auth.apiKey = {\n ...(auth.apiKey as Record<string, string> | undefined),\n [scheme.key]: value,\n };\n }\n }\n }\n return auth;\n}\n\n/**\n * One command's complete contract as plain data \u2014 what `schema <command>` prints. It has\n * to carry the parameters: 'GET' operations have no body, so without them the output says\n * nothing a caller could act on, and the only alternative is scraping `--help`, which is\n * prose written for humans.\n */\nfunction commandContract(command: CliCommand): Record<string, unknown> {\n return {\n operationId: command.name,\n ...(command.group === undefined ? {} : { group: groupSlug(command.group) }),\n ...(command.summary === undefined ? {} : { summary: oneLine(command.summary) }),\n ...(command.method === '' ? {} : { method: command.method }),\n ...(command.path === '' ? {} : { path: command.path }),\n parameters: {\n path: command.positionals.map((positional) => ({\n name: positional.name,\n type: positional.type ?? 'string',\n required: true,\n ...(positional.description === undefined\n ? {}\n : { description: oneLine(positional.description) }),\n })),\n // `name` is what you type (`--max-total`); `param` is the wire name it becomes.\n query: command.flags.map((flag) => ({\n name: flag.name,\n param: flag.param,\n type: flag.type,\n required: flag.required,\n ...(flag.enum === undefined ? {} : { enum: flag.enum }),\n ...(flag.description === undefined ? {} : { description: oneLine(flag.description) }),\n })),\n },\n ...(command.body === undefined ? {} : { body: command.body }),\n ...(command.unsupportedBody === undefined ? {} : { unsupportedBody: command.unsupportedBody }),\n ...(command.paginated === true ? { paginated: true } : {}),\n ...(command.sse === true ? { sse: true } : {}),\n ...(command.blob === true ? { blob: true } : {}),\n ...(command.schemas ?? {}),\n };\n}\n\nfunction renderHelp(\n commands: CliCommand[],\n binName: string,\n schemes: CliAuthScheme[],\n prefix: string,\n topic?: CliCommand | string\n): string[] {\n if (topic !== undefined && typeof topic !== 'string') {\n const command = topic;\n const usage = [\n binName,\n ...(command.group ? [groupSlug(command.group)] : []),\n command.name,\n ...command.positionals.map((slot) => `<${slot.name}>`),\n ...(command.flags.length > 0 ? ['[flags]'] : []),\n ...(command.body ? [\"--json '<json>' | @file | @-\"] : []),\n ].join(' ');\n const lines = [`Usage: ${usage}`];\n if (command.summary) lines.push('', command.summary);\n if (command.unsupportedBody !== undefined) {\n lines.push(\n '',\n `This operation takes a ${command.unsupportedBody} body, which the CLI cannot build \u2014 call it through the generated client instead.`\n );\n }\n if (command.flags.length > 0) {\n lines.push('', 'Flags:');\n for (const flag of command.flags) {\n const choices = flag.enum ? ` (one of: ${flag.enum.join(', ')})` : '';\n const required = flag.required ? ' [required]' : '';\n lines.push(\n ` --${flag.name} <${flag.type}>${choices}${required} ${oneLine(flag.description ?? '')}`.trimEnd()\n );\n }\n }\n return lines;\n }\n const scope =\n typeof topic === 'string'\n ? commands.filter((c) => c.group && groupSlug(c.group) === topic)\n : commands;\n const lines =\n typeof topic === 'string'\n ? [`Usage: ${binName} ${topic} <command> \u2026`, '', 'Commands:']\n : [`Usage: ${binName} [group] <command> \u2026`, '', 'Commands:'];\n const seenGroups = new Set<string>();\n const grouped = commands.some((c) => c.group);\n for (const command of scope) {\n if (typeof topic !== 'string' && command.group) {\n const slug = groupSlug(command.group);\n if (seenGroups.has(slug)) continue;\n seenGroups.add(slug);\n // The slug is what you type; the tag is what you recognize.\n const title = slug === command.group ? '' : ` (${command.group})`;\n lines.push(` ${slug} <command>${title}`);\n continue;\n }\n lines.push(\n ` ${[command.group === undefined ? undefined : groupSlug(command.group), command.name]\n .filter(Boolean)\n .join(' ')} ${oneLine(command.summary ?? '')}`.trimEnd()\n );\n }\n // Flags that apply to every command, and the env vars credentials come from: a flag\n // absent from --help may as well not exist \u2014 and one this API cannot use should not be\n // listed at all, since the operator would spend the debugging session on their token.\n const kinds = new Set(schemes.map((scheme) => scheme.kind));\n const credentials = [\n ...(kinds.has('bearer') ? [`${prefix}_TOKEN`] : []),\n ...(kinds.has('basic') ? [`${prefix}_USERNAME`, `${prefix}_PASSWORD`] : []),\n ...schemes\n .filter((scheme) => scheme.kind === 'apiKey')\n .map((scheme) => `${prefix}_API_KEY_${envPrefix(scheme.key)}`),\n ];\n lines.push(\n '',\n 'Global flags:',\n ' --server-url <url> Override the baked server URL',\n ' --format <json|ndjson> Output format',\n ' --dry-run Print the prepared request without sending it',\n ' --page-all Follow pagination, one JSON page per line',\n ' --output <path> Write the response body to a file (required for binary)',\n ...(kinds.has('bearer') ? [' --token <token> Bearer token'] : []),\n ` --json <json|@file|@-> Request body`,\n ...(credentials.length > 0 ? ['', 'Environment:', ` ${credentials.join(', ')}`] : []),\n '',\n `Run ${binName} ${grouped ? '<group> <command>' : '<command>'} --help for command details; ${binName} schema <command> prints its schemas.`\n );\n return lines;\n}\n\nfunction loadBody(source: string, wiring: CliWiring): unknown {\n const raw =\n source === '@-'\n ? (wiring.stdin ?? (() => ''))()\n : source.startsWith('@')\n ? (wiring.readFile ?? (() => ''))(source.slice(1))\n : source;\n return JSON.parse(raw);\n}\n\n/** Replace header values containing a known credential with `***`. */\nfunction redactHeaders(headers: Record<string, string>, secrets: string[]): Record<string, string> {\n const redacted: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n redacted[name] = secrets.some((secret) => secret !== '' && value.includes(secret))\n ? '***'\n : value;\n }\n return redacted;\n}\n\n/** Parse argv, resolve env auth, dispatch through the client, print, return the exit code. */\nasync function runCli(\n commands: Array<CliCommand | CustomCommand>,\n wiring: CliWiring,\n argv: string[]\n): Promise<number>;\n/** The composed form: one binary over several sources, each namespaced with its own wiring. */\nasync function runCli(sources: CommandSource[], argv: string[]): Promise<number>;\nasync function runCli(\n commandsOrSources: Array<CliCommand | CustomCommand> | CommandSource[],\n wiringOrArgv: CliWiring | string[],\n argv?: string[]\n): Promise<number> {\n if (Array.isArray(wiringOrArgv)) {\n return runSources(commandsOrSources as CommandSource[], wiringOrArgv);\n }\n return runSingle(\n commandsOrSources as Array<CliCommand | CustomCommand>,\n wiringOrArgv,\n argv ?? []\n );\n}\n\n/** Route the first token to its source; the namespace-less source owns the root. */\nasync function runSources(sources: CommandSource[], argv: string[]): Promise<number> {\n // A source without wiring inherits the first wired one, so the documented root-source\n // shape `{ commands: [login] }` works: the login shares the composed binary's identity.\n const inherited = sources.find((source) => source.wiring !== undefined)?.wiring;\n const wiringOf = (source: CommandSource): CliWiring => source.wiring ?? (inherited as CliWiring);\n // Top-level output goes through the first source: with a root source that is the one\n // carrying the shared commands, otherwise the first API listed.\n const top = wiringOf(sources[0]);\n const fail = (code: number, message: string): number => {\n top.stderr(JSON.stringify({ error: { code, message } }));\n return code;\n };\n const namespaced = sources.filter(\n (source): source is CommandSource & { namespace: string } => source.namespace !== undefined\n );\n const root = sources.find((source) => source.namespace === undefined);\n if (root !== undefined) {\n const clash = root.commands.find((command) =>\n namespaced.some((source) => source.namespace === command.name)\n );\n if (clash !== undefined) {\n return fail(\n 4,\n `Root command \"${clash.name}\" collides with the \"${clash.name}\" namespace \u2014 rename one of them.`\n );\n }\n }\n if (argv.length === 0 || argv[0] === '--help') {\n for (const line of renderComposedHelp(sources, top.binName)) top.stdout(line);\n return 0;\n }\n const source = namespaced.find((candidate) => candidate.namespace === argv[0]);\n if (source !== undefined) return runSingle(source.commands, wiringOf(source), argv.slice(1));\n const rootTakes =\n root !== undefined &&\n (argv[0] === 'schema' ||\n root.commands.some(\n (command) =>\n command.name === argv[0] ||\n (command.group !== undefined && groupSlug(command.group) === argv[0])\n ));\n if (rootTakes)\n return runSingle((root as CommandSource).commands, wiringOf(root as CommandSource), argv);\n return fail(\n 4,\n `Unknown command: ${argv[0]} \u2014 expected an API namespace (${namespaced\n .map((candidate) => candidate.namespace)\n .join(', ')})${root !== undefined ? ' or a root command' : ''}`\n );\n}\n\n/** The composed top-level help: namespaces, root commands, and how to descend. */\nfunction renderComposedHelp(sources: CommandSource[], binName: string): string[] {\n const lines = [`Usage: ${binName} <api> <command> \u2026`, '', 'APIs:'];\n for (const source of sources) {\n if (source.namespace !== undefined) lines.push(` ${source.namespace}`);\n }\n const root = sources.find((source) => source.namespace === undefined);\n if (root !== undefined && root.commands.length > 0) {\n lines.push('', 'Commands:');\n for (const command of root.commands) {\n lines.push(` ${command.name} ${oneLine(command.summary ?? '')}`.trimEnd());\n }\n }\n lines.push('', `Run ${binName} <api> --help for that API's commands.`);\n return lines;\n}\n\nasync function runSingle(\n rawCommands: Array<CliCommand | CustomCommand>,\n wiring: CliWiring,\n argv: string[]\n): Promise<number> {\n const { stdout, stderr } = wiring;\n const commands = normalizeCommands(rawCommands);\n const shadowed = shadowedCommandName(commands);\n if (shadowed !== undefined) {\n stderr(\n JSON.stringify({\n error: {\n code: 4,\n message: `Custom command \"${shadowed}\" collides with another command of the same name \u2014 rename it.`,\n },\n })\n );\n return 4;\n }\n const fail = (code: number, error: Record<string, unknown>): number => {\n stderr(JSON.stringify({ error: { code, ...error } }));\n return code;\n };\n\n const invocation = parseInvocation(commands, argv);\n if (invocation.kind === 'usage-error') return fail(4, { message: invocation.message });\n if (invocation.kind === 'help') {\n const prefix = wiring.envPrefix ?? envPrefix(wiring.binName);\n for (const line of renderHelp(\n commands,\n wiring.binName,\n wiring.schemes ?? [],\n prefix,\n invocation.topic\n ))\n stdout(line);\n return 0;\n }\n if (invocation.kind === 'schema') {\n stdout(JSON.stringify(commandContract(invocation.command), null, 2));\n return 0;\n }\n\n const { command, positionals, params, globals } = invocation;\n // A credential the user passed explicitly must never be dropped in silence: without a\n // bearer scheme the request would go out unauthenticated and come back 401, which reads\n // as \"my token is wrong\" rather than \"that flag does nothing here\".\n const schemes = wiring.schemes ?? [];\n if (globals.token !== undefined && !schemes.some((scheme) => scheme.kind === 'bearer')) {\n const declared = schemes.map((scheme) => `${scheme.key} (${scheme.kind})`).join(', ');\n return fail(4, {\n message:\n `--token is a bearer credential, and this API declares no bearer scheme. ` +\n (declared === '' ? 'It declares no security schemes at all.' : `It accepts: ${declared}.`),\n });\n }\n if (command.blob && globals.output === undefined) {\n return fail(4, {\n message: `${command.name} downloads a file: pass --output <path>`,\n operationId: command.name,\n });\n }\n let body: unknown;\n if (globals.json !== undefined) {\n try {\n body = loadBody(globals.json, wiring);\n } catch (error) {\n return fail(4, {\n message: `Invalid --json body: ${(error as Error).message}`,\n operationId: command.name,\n });\n }\n }\n\n const auth = resolveAuth(wiring, globals.token);\n if (Object.keys(auth).length > 0) wiring.configure({ auth });\n if (globals.serverUrl !== undefined) wiring.configure({ serverUrl: globals.serverUrl });\n\n // Redaction matches these against header VALUES \u2014 so basic auth must contribute the\n // form the wire actually carries (`Basic ${base64(user:pass)}`), not just the raw\n // password, which never appears in the encoded header.\n const basic = auth.basic as { username: string; password: string } | undefined;\n const secrets = [\n ...(typeof auth.bearer === 'string' ? [auth.bearer] : []),\n ...(basic ? [basic.password, btoa(`${basic.username}:${basic.password}`)] : []),\n ...Object.values((auth.apiKey as Record<string, string> | undefined) ?? {}),\n ];\n let captured: Record<string, unknown> | undefined;\n if (globals.dryRun) {\n wiring.configure({\n fetch: async (\n url: string,\n init: { method?: string; headers?: Record<string, string>; body?: unknown }\n ) => {\n captured = {\n url,\n method: init.method,\n headers: redactHeaders(init.headers ?? {}, secrets),\n ...(init.body !== undefined && init.body !== null ? { body: String(init.body) } : {}),\n };\n return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });\n },\n });\n }\n\n const variables: Record<string, unknown> = { ...positionals };\n if (Object.keys(params).length > 0) variables.params = params;\n if (body !== undefined) variables.body = body;\n const argument = Object.keys(variables).length > 0 ? variables : undefined;\n\n // The client's methods are typed per-operation; the dispatcher only needs \"callable\n // by name\", so one localized widening here keeps the emitted wiring cast-free.\n const methods = wiring.client as Record<string, unknown>;\n try {\n const resolved = command as ResolvedCommand;\n if (resolved.handler !== undefined) {\n return await resolved.handler({ positionals, params, globals, wiring });\n }\n if (globals.pageAll && !globals.dryRun) {\n const paginated = methods[command.name] as {\n pages: (variables?: unknown) => AsyncIterable<unknown>;\n };\n for await (const page of paginated.pages(argument)) stdout(JSON.stringify(page));\n return 0;\n }\n const method = methods[command.name] as (variables?: unknown) => Promise<unknown>;\n const result = await method(argument);\n if (globals.dryRun) {\n // An SSE method returns a lazy stream \u2014 drain the stubbed response so its fetch\n // actually runs and captures the request.\n if (command.sse) for await (const _event of result as AsyncIterable<unknown>);\n stdout(JSON.stringify(captured, null, 2));\n return 0;\n }\n if (command.sse) {\n for await (const event of result as AsyncIterable<unknown>) stdout(JSON.stringify(event));\n return 0;\n }\n if (command.blob) {\n const bytes = new Uint8Array(await (result as Blob).arrayBuffer());\n (wiring.writeFile ?? (() => {}))(globals.output as string, bytes);\n stdout(JSON.stringify({ saved: globals.output, bytes: bytes.length }));\n return 0;\n }\n if (result !== undefined && result !== null) stdout(JSON.stringify(result, null, 2));\n return 0;\n } catch (error) {\n const thrown = error as Error & { status?: number; issues?: unknown };\n const detail = {\n message: thrown.message,\n operationId: command.name,\n ...(thrown.status !== undefined ? { status: thrown.status } : {}),\n ...(thrown.issues !== undefined ? { issues: thrown.issues } : {}),\n };\n if (thrown.name === 'ZodValidationError') return fail(3, detail);\n if (thrown.name === 'ApiError' && (thrown.status === 401 || thrown.status === 403)) {\n return fail(2, detail);\n }\n return fail(1, detail);\n }\n}"
46
+ "cli.ts": "// The generated-CLI engine: a pure argv parser plus a dispatcher that drives the\n// instance client and maps outcomes to the documented exit-code contract\n// (0 success, 1 API error, 2 auth, 3 validation, 4 usage). Node-only by nature,\n// but every effect (env, stdin, files, output) is injected through the wiring so\n// the module itself stays dependency-free and fully unit-testable; the emitted\n// entry fills the defaults with real `node:fs`/`process` bindings.\n\n/** One flag derived from a query parameter. */\ntype CliFlag = {\n /** Kebab-cased flag name (`--page-size`). */\n name: string;\n /** Original wire parameter name. */\n param: string;\n type: 'string' | 'number' | 'boolean' | 'array';\n required: boolean;\n enum?: string[];\n description?: string;\n};\n\n/** One executable command, derived from the IR at generate time. Pure data. */\ntype CliCommand = {\n /** Tag; absent = flat/untagged. */\n group?: string;\n name: string;\n summary?: string;\n method: string;\n path: string;\n /** Path params, in path-template order. Always required \u2014 that is what a path is. */\n positionals: Array<{\n name: string;\n type?: CliFlag['type'];\n description?: string;\n }>;\n flags: CliFlag[];\n /** Present when the operation takes a JSON request body. */\n body?: { required: boolean };\n /**\n * The content type of a request body that is NOT JSON (multipart, url-encoded, binary).\n * `--json` cannot build one, so the command is reported as library-only rather than\n * offered as if it were runnable.\n */\n unsupportedBody?: string;\n paginated?: boolean;\n sse?: boolean;\n blob?: boolean;\n /** IR schemas for the `schema` command, serialized verbatim. */\n schemas?: { request?: unknown; response?: unknown };\n};\n\ntype CliAuthScheme = { key: string; kind: 'bearer' | 'basic' | 'apiKey' };\n\ntype CliWiring = {\n binName: string;\n /** Credential variable prefix. Defaults to `binName`, constant-cased \u2014 set it when the\n * displayed name and the credential family must differ (a composed multi-API binary). */\n envPrefix?: string;\n /** The generated instance client (grouped-args methods). */\n client: Record<string, unknown>;\n configure: (config: Record<string, unknown>) => void;\n /** Security schemes of the API \u2014 drives env-var credential resolution. */\n schemes?: CliAuthScheme[];\n env?: Record<string, string | undefined>;\n stdin?: () => string;\n readFile?: (path: string) => string;\n writeFile?: (path: string, data: Uint8Array) => void;\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n};\n\ntype CliGlobals = {\n serverUrl?: string;\n format?: 'json' | 'ndjson';\n dryRun?: boolean;\n pageAll?: boolean;\n output?: string;\n token?: string;\n json?: string;\n};\n\ntype CliInvocation =\n | { kind: 'help'; topic?: CliCommand | string }\n | { kind: 'schema'; command: CliCommand }\n | {\n kind: 'run';\n command: CliCommand;\n positionals: Record<string, string>;\n params: Record<string, unknown>;\n globals: CliGlobals;\n }\n | { kind: 'usage-error'; message: string };\n\n/**\n * A hand-written command composed NEXT TO the generated ones: the same data shape plus a\n * `handler`, so it inherits help, parsing, `schema`, and the exit-code contract. This is\n * how behavior that is not in the description (a `login`, a doctor command) joins the\n * binary without the generator ever learning what it does.\n */\ntype CustomCommand = {\n name: string;\n group?: string;\n summary?: string;\n positionals?: CliCommand['positionals'];\n flags?: CliFlag[];\n /** Returns the process exit code; throwing exits 1 with the standard error JSON. */\n handler: (context: CommandContext) => number | Promise<number>;\n};\n\ntype CommandContext = {\n positionals: Record<string, string>;\n params: Record<string, unknown>;\n globals: CliGlobals;\n wiring: CliWiring;\n};\n\n/** One API's contribution to a composed binary: its commands behind a namespace, with its\n * OWN wiring (base URL, schemes, credentials). A namespace-less source sits at the root. */\ntype CommandSource = {\n namespace?: string;\n commands: Array<CliCommand | CustomCommand>;\n /** Absent = inherit the first wired source's \u2014 a root `login` shares the binary's identity. */\n wiring?: CliWiring;\n};\n\ntype ResolvedCommand = CliCommand & { handler?: CustomCommand['handler'] };\n\n/** Custom commands as the command shape the parser reads; generated ones pass through. */\nfunction normalizeCommands(commands: Array<CliCommand | CustomCommand>): ResolvedCommand[] {\n return commands.map((command) =>\n 'handler' in command\n ? { method: '', path: '', positionals: [], flags: [], ...command }\n : command\n );\n}\n\n/**\n * The name of a custom command that shadows another command. Rejected at startup: an\n * operator typing an operationId must never silently run something else.\n */\nfunction shadowedCommandName(commands: ResolvedCommand[]): string | undefined {\n const seen = new Map<string, ResolvedCommand[]>();\n for (const command of commands) {\n const key = `${command.group ?? ''}\\u0000${command.name}`;\n seen.set(key, [...(seen.get(key) ?? []), command]);\n }\n for (const clashing of seen.values()) {\n if (clashing.length > 1 && clashing.some((command) => command.handler !== undefined)) {\n return clashing[0].name;\n }\n }\n return undefined;\n}\n\nconst GLOBAL_FLAGS: Record<string, { key: keyof CliGlobals; boolean?: boolean }> = {\n 'server-url': { key: 'serverUrl' },\n format: { key: 'format' },\n 'dry-run': { key: 'dryRun', boolean: true },\n 'page-all': { key: 'pageAll', boolean: true },\n output: { key: 'output' },\n token: { key: 'token' },\n json: { key: 'json' },\n};\n\n/**\n * The shell-typable form of a group name: an OpenAPI tag can contain spaces (\"Some\n * multi-word tag\"), which only resolves if the user quotes it. Commands are addressed by\n * this slug; help still shows the original tag.\n */\nfunction groupSlug(group: string): string {\n return group\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(Boolean)\n .join('-');\n}\n\n/** A description on ONE line: newlines in an OpenAPI description wreck help alignment. */\nfunction oneLine(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\n/** Resolve argv against the command table. Pure \u2014 no I/O, no env. */\nfunction parseInvocation(commands: CliCommand[], argv: string[]): CliInvocation {\n if (argv.length === 0 || argv[0] === '--help') return { kind: 'help' };\n\n if (argv[0] === 'schema') {\n const command = commands.find((candidate) => candidate.name === argv[1]);\n return command\n ? { kind: 'schema', command }\n : { kind: 'usage-error', message: `Unknown command: schema ${argv[1] ?? ''}`.trim() };\n }\n\n const slugs = new Set(commands.filter((c) => c.group).map((c) => groupSlug(c.group as string)));\n let command: CliCommand | undefined;\n let rest: string[];\n if (slugs.has(argv[0])) {\n if (argv[1] === '--help' || argv[1] === undefined) return { kind: 'help', topic: argv[0] };\n command = commands.find((c) => c.group && groupSlug(c.group) === argv[0] && c.name === argv[1]);\n if (!command) return { kind: 'usage-error', message: `Unknown command: ${argv[0]} ${argv[1]}` };\n rest = argv.slice(2);\n } else {\n // An ungrouped command, or a bare operationId \u2014 knowing the group shouldn't be\n // required when the name alone is unambiguous.\n const named = commands.filter((c) => c.name === argv[0]);\n command =\n named.find((c) => c.group === undefined) ?? (named.length === 1 ? named[0] : undefined);\n if (!command) {\n const ambiguous = named.length > 1;\n return {\n kind: 'usage-error',\n message: ambiguous\n ? `Ambiguous command: ${argv[0]} \u2014 prefix it with its group (${named\n .map((c) => groupSlug(c.group as string))\n .join(', ')})`\n : `Unknown command: ${argv[0]}`,\n };\n }\n rest = argv.slice(1);\n }\n if (rest.includes('--help')) return { kind: 'help', topic: command };\n\n const positionals: Record<string, string> = {};\n const params: Record<string, unknown> = {};\n const globals: CliGlobals = {};\n let positionalIndex = 0;\n for (let index = 0; index < rest.length; index++) {\n const token = rest[index];\n if (!token.startsWith('--')) {\n const slot = command.positionals[positionalIndex++];\n if (!slot) return { kind: 'usage-error', message: `Unexpected argument: ${token}` };\n positionals[slot.name] = token;\n continue;\n }\n const equals = token.indexOf('=');\n const flagName = equals === -1 ? token.slice(2) : token.slice(2, equals);\n const inlineValue = equals === -1 ? undefined : token.slice(equals + 1);\n const takeValue = (): string | undefined =>\n inlineValue !== undefined ? inlineValue : rest[++index];\n\n const global = GLOBAL_FLAGS[flagName];\n if (global) {\n if (global.boolean) {\n (globals[global.key] as boolean) = true;\n continue;\n }\n const value = takeValue();\n if (value === undefined) {\n return { kind: 'usage-error', message: `Flag --${flagName} expects a value` };\n }\n if (global.key === 'format' && value !== 'json' && value !== 'ndjson') {\n return { kind: 'usage-error', message: `--format must be one of: json, ndjson` };\n }\n (globals[global.key] as string) = value;\n continue;\n }\n\n const flag = command.flags.find((candidate) => candidate.name === flagName);\n if (!flag) return { kind: 'usage-error', message: `Unknown flag: --${flagName}` };\n if (flag.type === 'boolean') {\n params[flag.param] = true;\n continue;\n }\n const value = takeValue();\n if (value === undefined) {\n return { kind: 'usage-error', message: `Flag --${flagName} expects a value` };\n }\n if (flag.enum && !flag.enum.includes(value)) {\n return {\n kind: 'usage-error',\n message: `--${flagName} must be one of: ${flag.enum.join(', ')}`,\n };\n }\n if (flag.type === 'number') {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) {\n return { kind: 'usage-error', message: `--${flagName} expects a number, got \"${value}\"` };\n }\n params[flag.param] = numeric;\n } else if (flag.type === 'array') {\n const existing = params[flag.param];\n params[flag.param] = Array.isArray(existing) ? [...existing, value] : [value];\n } else {\n params[flag.param] = value;\n }\n }\n\n for (const slot of command.positionals) {\n if (!(slot.name in positionals)) {\n return { kind: 'usage-error', message: `Missing required argument: <${slot.name}>` };\n }\n }\n for (const flag of command.flags) {\n if (flag.required && !(flag.param in params)) {\n return { kind: 'usage-error', message: `Missing required flag: --${flag.name}` };\n }\n }\n if (globals.json !== undefined && !command.body) {\n return { kind: 'usage-error', message: `${command.name} does not accept a request body` };\n }\n if (command.body?.required && globals.json === undefined) {\n return {\n kind: 'usage-error',\n message: `${command.name} requires a request body: pass --json '<json>', --json @file.json, or --json @-`,\n };\n }\n if (globals.pageAll && !command.paginated) {\n return {\n kind: 'usage-error',\n message: `${command.name} is not paginated; --page-all only applies to paginated operations`,\n };\n }\n return { kind: 'run', command, positionals, params, globals };\n}\n\n/** Credential env-var prefix: bin name constant-cased (`cafe-api` \u2192 `CAFE_API`). */\nfunction envPrefix(binName: string): string {\n return binName\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .toUpperCase();\n}\n\nfunction resolveAuth(wiring: CliWiring, token: string | undefined): Record<string, unknown> {\n const env = wiring.env ?? {};\n const prefix = wiring.envPrefix ?? envPrefix(wiring.binName);\n const auth: Record<string, unknown> = {};\n for (const scheme of wiring.schemes ?? []) {\n if (scheme.kind === 'bearer') {\n const value = token ?? env[`${prefix}_TOKEN`];\n if (value !== undefined) auth.bearer = value;\n } else if (scheme.kind === 'basic') {\n const username = env[`${prefix}_USERNAME`];\n const password = env[`${prefix}_PASSWORD`];\n if (username !== undefined && password !== undefined) auth.basic = { username, password };\n } else {\n const value = env[`${prefix}_API_KEY_${envPrefix(scheme.key)}`];\n if (value !== undefined) {\n auth.apiKey = {\n ...(auth.apiKey as Record<string, string> | undefined),\n [scheme.key]: value,\n };\n }\n }\n }\n return auth;\n}\n\n/**\n * One command's complete contract as plain data \u2014 what `schema <command>` prints. It has\n * to carry the parameters: 'GET' operations have no body, so without them the output says\n * nothing a caller could act on, and the only alternative is scraping `--help`, which is\n * prose written for humans.\n */\nfunction commandContract(command: CliCommand): Record<string, unknown> {\n return {\n operationId: command.name,\n ...(command.group === undefined ? {} : { group: groupSlug(command.group) }),\n ...(command.summary === undefined ? {} : { summary: oneLine(command.summary) }),\n ...(command.method === '' ? {} : { method: command.method }),\n ...(command.path === '' ? {} : { path: command.path }),\n parameters: {\n path: command.positionals.map((positional) => ({\n name: positional.name,\n type: positional.type ?? 'string',\n required: true,\n ...(positional.description === undefined\n ? {}\n : { description: oneLine(positional.description) }),\n })),\n // `name` is what you type (`--max-total`); `param` is the wire name it becomes.\n query: command.flags.map((flag) => ({\n name: flag.name,\n param: flag.param,\n type: flag.type,\n required: flag.required,\n ...(flag.enum === undefined ? {} : { enum: flag.enum }),\n ...(flag.description === undefined ? {} : { description: oneLine(flag.description) }),\n })),\n },\n ...(command.body === undefined ? {} : { body: command.body }),\n ...(command.unsupportedBody === undefined ? {} : { unsupportedBody: command.unsupportedBody }),\n ...(command.paginated === true ? { paginated: true } : {}),\n ...(command.sse === true ? { sse: true } : {}),\n ...(command.blob === true ? { blob: true } : {}),\n ...(command.schemas ?? {}),\n };\n}\n\nfunction renderHelp(\n commands: CliCommand[],\n binName: string,\n schemes: CliAuthScheme[],\n prefix: string,\n topic?: CliCommand | string\n): string[] {\n if (topic !== undefined && typeof topic !== 'string') {\n const command = topic;\n const usage = [\n binName,\n ...(command.group ? [groupSlug(command.group)] : []),\n command.name,\n ...command.positionals.map((slot) => `<${slot.name}>`),\n ...(command.flags.length > 0 ? ['[flags]'] : []),\n ...(command.body ? [\"--json '<json>' | @file | @-\"] : []),\n ].join(' ');\n const lines = [`Usage: ${usage}`];\n if (command.summary) lines.push('', command.summary);\n if (command.unsupportedBody !== undefined) {\n lines.push(\n '',\n `This operation takes a ${command.unsupportedBody} body, which the CLI cannot build \u2014 call it through the generated client instead.`\n );\n }\n if (command.flags.length > 0) {\n lines.push('', 'Flags:');\n for (const flag of command.flags) {\n const choices = flag.enum ? ` (one of: ${flag.enum.join(', ')})` : '';\n const required = flag.required ? ' [required]' : '';\n lines.push(\n ` --${flag.name} <${flag.type}>${choices}${required} ${oneLine(flag.description ?? '')}`.trimEnd()\n );\n }\n }\n return lines;\n }\n const scope =\n typeof topic === 'string'\n ? commands.filter((c) => c.group && groupSlug(c.group) === topic)\n : commands;\n const lines =\n typeof topic === 'string'\n ? [`Usage: ${binName} ${topic} <command> \u2026`, '', 'Commands:']\n : [`Usage: ${binName} [group] <command> \u2026`, '', 'Commands:'];\n const seenGroups = new Set<string>();\n const grouped = commands.some((c) => c.group);\n for (const command of scope) {\n if (typeof topic !== 'string' && command.group) {\n const slug = groupSlug(command.group);\n if (seenGroups.has(slug)) continue;\n seenGroups.add(slug);\n // The slug is what you type; the tag is what you recognize.\n const title = slug === command.group ? '' : ` (${command.group})`;\n lines.push(` ${slug} <command>${title}`);\n continue;\n }\n lines.push(\n ` ${[command.group === undefined ? undefined : groupSlug(command.group), command.name]\n .filter(Boolean)\n .join(' ')} ${oneLine(command.summary ?? '')}`.trimEnd()\n );\n }\n // Flags that apply to every command, and the env vars credentials come from: a flag\n // absent from --help may as well not exist \u2014 and one this API cannot use should not be\n // listed at all, since the operator would spend the debugging session on their token.\n const kinds = new Set(schemes.map((scheme) => scheme.kind));\n const credentials = [\n ...(kinds.has('bearer') ? [`${prefix}_TOKEN`] : []),\n ...(kinds.has('basic') ? [`${prefix}_USERNAME`, `${prefix}_PASSWORD`] : []),\n ...schemes\n .filter((scheme) => scheme.kind === 'apiKey')\n .map((scheme) => `${prefix}_API_KEY_${envPrefix(scheme.key)}`),\n ];\n lines.push(\n '',\n 'Global flags:',\n ' --server-url <url> Override the baked server URL',\n ' --format <json|ndjson> Output format',\n ' --dry-run Print the prepared request without sending it',\n ' --page-all Follow pagination, one JSON page per line',\n ' --output <path> Write the response body to a file (required for binary)',\n ...(kinds.has('bearer') ? [' --token <token> Bearer token'] : []),\n ` --json <json|@file|@-> Request body`,\n ...(credentials.length > 0 ? ['', 'Environment:', ` ${credentials.join(', ')}`] : []),\n '',\n `Run ${binName} ${grouped ? '<group> <command>' : '<command>'} --help for command details; ${binName} schema <command> prints its schemas.`\n );\n return lines;\n}\n\nfunction loadBody(source: string, wiring: CliWiring): unknown {\n const raw =\n source === '@-'\n ? (wiring.stdin ?? (() => ''))()\n : source.startsWith('@')\n ? (wiring.readFile ?? (() => ''))(source.slice(1))\n : source;\n return JSON.parse(raw);\n}\n\n/** Replace header values containing a known credential with `***`. */\nfunction redactHeaders(headers: Record<string, string>, secrets: string[]): Record<string, string> {\n const redacted: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n redacted[name] = secrets.some((secret) => secret !== '' && value.includes(secret))\n ? '***'\n : value;\n }\n return redacted;\n}\n\n/** Parse argv, resolve env auth, dispatch through the client, print, return the exit code. */\nasync function runCli(\n commands: Array<CliCommand | CustomCommand>,\n wiring: CliWiring,\n argv: string[]\n): Promise<number>;\n/** The composed form: one binary over several sources, each namespaced with its own wiring. */\nasync function runCli(sources: CommandSource[], argv: string[]): Promise<number>;\nasync function runCli(\n commandsOrSources: Array<CliCommand | CustomCommand> | CommandSource[],\n wiringOrArgv: CliWiring | string[],\n argv?: string[]\n): Promise<number> {\n if (Array.isArray(wiringOrArgv)) {\n return runSources(commandsOrSources as CommandSource[], wiringOrArgv);\n }\n return runSingle(\n commandsOrSources as Array<CliCommand | CustomCommand>,\n wiringOrArgv,\n argv ?? []\n );\n}\n\n/** Route the first token to its source; the namespace-less source owns the root. */\nasync function runSources(sources: CommandSource[], argv: string[]): Promise<number> {\n // A source without wiring inherits the first wired one, so the documented root-source\n // shape `{ commands: [login] }` works: the login shares the composed binary's identity.\n const inherited = sources.find((source) => source.wiring !== undefined)?.wiring;\n const wiringOf = (source: CommandSource): CliWiring => source.wiring ?? (inherited as CliWiring);\n // Top-level output goes through the first source: with a root source that is the one\n // carrying the shared commands, otherwise the first API listed.\n const top = wiringOf(sources[0]);\n const fail = (code: number, message: string): number => {\n top.stderr(JSON.stringify({ error: { code, message } }));\n return code;\n };\n const namespaced = sources.filter(\n (source): source is CommandSource & { namespace: string } => source.namespace !== undefined\n );\n const root = sources.find((source) => source.namespace === undefined);\n if (root !== undefined) {\n const clash = root.commands.find((command) =>\n namespaced.some((source) => source.namespace === command.name)\n );\n if (clash !== undefined) {\n return fail(\n 4,\n `Root command \"${clash.name}\" collides with the \"${clash.name}\" namespace \u2014 rename one of them.`\n );\n }\n }\n if (argv.length === 0 || argv[0] === '--help') {\n for (const line of renderComposedHelp(sources, top.binName)) top.stdout(line);\n return 0;\n }\n const source = namespaced.find((candidate) => candidate.namespace === argv[0]);\n if (source !== undefined) return runSingle(source.commands, wiringOf(source), argv.slice(1));\n const rootTakes =\n root !== undefined &&\n (argv[0] === 'schema' ||\n root.commands.some(\n (command) =>\n command.name === argv[0] ||\n (command.group !== undefined && groupSlug(command.group) === argv[0])\n ));\n if (rootTakes)\n return runSingle((root as CommandSource).commands, wiringOf(root as CommandSource), argv);\n return fail(\n 4,\n `Unknown command: ${argv[0]} \u2014 expected an API namespace (${namespaced\n .map((candidate) => candidate.namespace)\n .join(', ')})${root !== undefined ? ' or a root command' : ''}`\n );\n}\n\n/** The composed top-level help: namespaces, root commands, and how to descend. */\nfunction renderComposedHelp(sources: CommandSource[], binName: string): string[] {\n const lines = [`Usage: ${binName} <api> <command> \u2026`, '', 'APIs:'];\n for (const source of sources) {\n if (source.namespace !== undefined) lines.push(` ${source.namespace}`);\n }\n const root = sources.find((source) => source.namespace === undefined);\n if (root !== undefined && root.commands.length > 0) {\n lines.push('', 'Commands:');\n for (const command of root.commands) {\n lines.push(` ${command.name} ${oneLine(command.summary ?? '')}`.trimEnd());\n }\n }\n lines.push('', `Run ${binName} <api> --help for that API's commands.`);\n return lines;\n}\n\nasync function runSingle(\n rawCommands: Array<CliCommand | CustomCommand>,\n wiring: CliWiring,\n argv: string[]\n): Promise<number> {\n const { stdout, stderr } = wiring;\n const commands = normalizeCommands(rawCommands);\n const shadowed = shadowedCommandName(commands);\n if (shadowed !== undefined) {\n stderr(\n JSON.stringify({\n error: {\n code: 4,\n message: `Custom command \"${shadowed}\" collides with another command of the same name \u2014 rename it.`,\n },\n })\n );\n return 4;\n }\n const fail = (code: number, error: Record<string, unknown>): number => {\n stderr(JSON.stringify({ error: { code, ...error } }));\n return code;\n };\n\n const invocation = parseInvocation(commands, argv);\n if (invocation.kind === 'usage-error') return fail(4, { message: invocation.message });\n if (invocation.kind === 'help') {\n const prefix = wiring.envPrefix ?? envPrefix(wiring.binName);\n for (const line of renderHelp(\n commands,\n wiring.binName,\n wiring.schemes ?? [],\n prefix,\n invocation.topic\n ))\n stdout(line);\n return 0;\n }\n if (invocation.kind === 'schema') {\n stdout(JSON.stringify(commandContract(invocation.command), null, 2));\n return 0;\n }\n\n const { command, positionals, params, globals } = invocation;\n // A credential the user passed explicitly must never be dropped in silence: without a\n // bearer scheme the request would go out unauthenticated and come back 401, which reads\n // as \"my token is wrong\" rather than \"that flag does nothing here\".\n const schemes = wiring.schemes ?? [];\n if (globals.token !== undefined && !schemes.some((scheme) => scheme.kind === 'bearer')) {\n const declared = schemes.map((scheme) => `${scheme.key} (${scheme.kind})`).join(', ');\n return fail(4, {\n message:\n `--token is a bearer credential, and this API declares no bearer scheme. ` +\n (declared === '' ? 'It declares no security schemes at all.' : `It accepts: ${declared}.`),\n });\n }\n if (command.blob && globals.output === undefined) {\n return fail(4, {\n message: `${command.name} downloads a file: pass --output <path>`,\n operationId: command.name,\n });\n }\n let body: unknown;\n if (globals.json !== undefined) {\n try {\n body = loadBody(globals.json, wiring);\n } catch (error) {\n return fail(4, {\n message: `Invalid --json body: ${(error as Error).message}`,\n operationId: command.name,\n });\n }\n }\n\n const auth = resolveAuth(wiring, globals.token);\n if (Object.keys(auth).length > 0) wiring.configure({ auth });\n if (globals.serverUrl !== undefined) wiring.configure({ serverUrl: globals.serverUrl });\n\n // Redaction matches these against header VALUES \u2014 so basic auth must contribute the\n // form the wire actually carries (`Basic ${base64(user:pass)}`), not just the raw\n // password, which never appears in the encoded header.\n const basic = auth.basic as { username: string; password: string } | undefined;\n const secrets = [\n ...(typeof auth.bearer === 'string' ? [auth.bearer] : []),\n ...(basic ? [basic.password, btoa(`${basic.username}:${basic.password}`)] : []),\n ...Object.values((auth.apiKey as Record<string, string> | undefined) ?? {}),\n ];\n let captured: Record<string, unknown> | undefined;\n if (globals.dryRun) {\n wiring.configure({\n fetch: async (\n url: string,\n init: { method?: string; headers?: Record<string, string>; body?: unknown }\n ) => {\n captured = {\n url,\n method: init.method,\n headers: redactHeaders(init.headers ?? {}, secrets),\n ...(init.body !== undefined && init.body !== null ? { body: String(init.body) } : {}),\n };\n return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });\n },\n });\n }\n\n const variables: Record<string, unknown> = { ...positionals };\n if (Object.keys(params).length > 0) variables.params = params;\n if (body !== undefined) variables.body = body;\n const argument = Object.keys(variables).length > 0 ? variables : undefined;\n\n // The client's methods are typed per-operation; the dispatcher only needs \"callable\n // by name\", so one localized widening here keeps the emitted wiring cast-free.\n const methods = wiring.client as Record<string, unknown>;\n try {\n const resolved = command as ResolvedCommand;\n if (resolved.handler !== undefined) {\n return await resolved.handler({ positionals, params, globals, wiring });\n }\n if (globals.pageAll && !globals.dryRun) {\n const paginated = methods[command.name] as {\n pages: (variables?: unknown) => AsyncIterable<unknown>;\n };\n for await (const page of paginated.pages(argument)) stdout(JSON.stringify(page));\n return 0;\n }\n const method = methods[command.name] as (variables?: unknown) => Promise<unknown>;\n const result = await method(argument);\n if (globals.dryRun) {\n // An SSE method returns a lazy stream \u2014 drain the stubbed response so its fetch\n // actually runs and captures the request.\n if (command.sse) for await (const _event of result as AsyncIterable<unknown>);\n stdout(JSON.stringify(captured, null, 2));\n return 0;\n }\n if (command.sse) {\n for await (const event of result as AsyncIterable<unknown>) stdout(JSON.stringify(event));\n return 0;\n }\n if (command.blob) {\n const bytes = new Uint8Array(await (result as Blob).arrayBuffer());\n (wiring.writeFile ?? (() => {}))(globals.output as string, bytes);\n stdout(JSON.stringify({ saved: globals.output, bytes: bytes.length }));\n return 0;\n }\n if (result !== undefined && result !== null) stdout(JSON.stringify(result, null, 2));\n return 0;\n } catch (error) {\n const thrown = error as Error & { status?: number; issues?: unknown };\n const detail = {\n message: thrown.message,\n operationId: command.name,\n ...(thrown.status !== undefined ? { status: thrown.status } : {}),\n ...(thrown.issues !== undefined ? { issues: thrown.issues } : {}),\n };\n if (thrown.name === 'ZodValidationError') return fail(3, detail);\n if (thrown.name === 'ApiError' && (thrown.status === 401 || thrown.status === 403)) {\n return fail(2, detail);\n }\n return fail(1, detail);\n }\n}"
47
47
  };
48
48
 
49
49
  // src/emitters/inline-runtime.ts
@@ -421,6 +421,12 @@ ${ENTRY_GUARD}`
421
421
  }
422
422
  __name(renderCliModule, "renderCliModule");
423
423
 
424
+ // src/runtime/cli.ts
425
+ function groupSlug(group) {
426
+ return group.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
427
+ }
428
+ __name(groupSlug, "groupSlug");
429
+
424
430
  // src/generators/anchor.ts
425
431
  import { parse } from "node:path";
426
432
  function anchor(outputPath) {
@@ -431,7 +437,7 @@ __name(anchor, "anchor");
431
437
 
432
438
  // src/generators/cli/index.ts
433
439
  function commandName(stem) {
434
- return stem.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "client";
440
+ return stem.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-") || "client";
435
441
  }
436
442
  __name(commandName, "commandName");
437
443
  var cliGenerator = /* @__PURE__ */ __name(({ model, outputPath, emit, selected }) => {
@@ -453,7 +459,7 @@ function cliSample(op, ctx) {
453
459
  if (command === void 0) return void 0;
454
460
  const words = [
455
461
  "client",
456
- ...command.group ? [command.group] : [],
462
+ ...command.group ? [groupSlug(command.group)] : [],
457
463
  command.name,
458
464
  ...command.positionals.map((positional) => `<${positional.name}>`),
459
465
  ...command.flags.filter((flag) => flag.required).map((flag) => `--${flag.name} <${flag.type}>`),
@@ -473,9 +479,9 @@ var entry_cli_default = {
473
479
  name: "cli",
474
480
  run: cliGenerator,
475
481
  sample: cliSample,
476
- requires: ["sdk", "zod"],
482
+ requires: ["typescript", "zod"],
477
483
  errorModes: ["throw"],
478
- requiresGenerator: "^0.0.0-snapshot.1786460732"
484
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
479
485
  };
480
486
  export {
481
487
  entry_cli_default as default
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "go" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "go" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator go --update`.
@@ -827,5 +827,5 @@ export default {
827
827
  "argsStyle": "inputs follow the target language's own idiom",
828
828
  "importExt": "the generated file has no relative imports"
829
829
  },
830
- requiresGenerator: '^0.0.0-snapshot.1786460732',
830
+ requiresGenerator: '^0.0.0-snapshot.1786626470',
831
831
  };
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "mock" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "mock" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator mock --update`.
@@ -681,8 +681,8 @@ ${content}` }];
681
681
  var entry_mock_default = {
682
682
  name: "mock",
683
683
  run: mockGenerator,
684
- requires: ["sdk"],
685
- requiresGenerator: "^0.0.0-snapshot.1786460732"
684
+ requires: ["typescript"],
685
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
686
686
  };
687
687
  export {
688
688
  entry_mock_default as default
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -801,5 +801,5 @@ export default {
801
801
  "argsStyle": "inputs follow the target language's own idiom",
802
802
  "importExt": "the generated file has no relative imports"
803
803
  },
804
- requiresGenerator: '^0.0.0-snapshot.1786460732',
804
+ requiresGenerator: '^0.0.0-snapshot.1786626470',
805
805
  };
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -666,5 +666,5 @@ export default {
666
666
  "argsStyle": "inputs follow the target language's own idiom",
667
667
  "importExt": "the generated file has no relative imports"
668
668
  },
669
- requiresGenerator: '^0.0.0-snapshot.1786460732',
669
+ requiresGenerator: '^0.0.0-snapshot.1786626470',
670
670
  };
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "swr" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "swr" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator swr --update`.
@@ -258,9 +258,9 @@ ${content}` }];
258
258
  var entry_swr_default = {
259
259
  name: "swr",
260
260
  run: swrGenerator,
261
- requires: ["sdk"],
261
+ requires: ["typescript"],
262
262
  errorModes: ["throw"],
263
- requiresGenerator: "^0.0.0-snapshot.1786460732"
263
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
264
264
  };
265
265
  export {
266
266
  entry_swr_default as default
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "tanstack-query" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "tanstack-query" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator tanstack-query --update`.
@@ -474,7 +474,7 @@ function factoriesSource(model, ops, pagination, prefix) {
474
474
  const paginated = pagination.get(op.name);
475
475
  return paginated !== void 0 && paginated.spec.style !== "link" ? [optionsMember(op), infiniteMember(model, op, paginated.spec)] : [optionsMember(op)];
476
476
  });
477
- return "/**\n * Build the factories over a specific client instance \u2014 its config, middleware, and\n * retry apply to every call (`createQueryFactories(createClient(OPERATIONS, config))`).\n * The module-level exports below are these factories bound to the sdk's default `client`.\n */\nexport const createQueryFactories = (instance: typeof client = client) => ({\n" + members.join(",\n") + "\n});";
477
+ return "/**\n * Build the factories over a specific client instance \u2014 its config, middleware, and\n * retry apply to every call (`createQueryFactories(createClient(OPERATIONS, config))`).\n * The module-level exports below are these factories bound to the generated module's default `client`.\n */\nexport const createQueryFactories = (instance: typeof client = client) => ({\n" + members.join(",\n") + "\n});";
478
478
  }
479
479
  __name(factoriesSource, "factoriesSource");
480
480
  function optionsMember(op) {
@@ -623,9 +623,9 @@ __name(tanstackQueryGenerator, "tanstackQueryGenerator");
623
623
  var entry_tanstack_query_default = {
624
624
  name: "tanstack-query",
625
625
  run: tanstackQueryGenerator("react"),
626
- requires: ["sdk"],
626
+ requires: ["typescript"],
627
627
  errorModes: ["throw"],
628
- requiresGenerator: "^0.0.0-snapshot.1786460732"
628
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
629
629
  };
630
630
  export {
631
631
  entry_tanstack_query_default as default
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.0.0-snapshot.1786460732 — the built-in "transformers" generator.
1
+ // Ejected from @redocly/client-generator@0.0.0-snapshot.1786626470 — the built-in "transformers" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator transformers --update`.
@@ -394,9 +394,9 @@ ${content}` }];
394
394
  var entry_transformers_default = {
395
395
  name: "transformers",
396
396
  run: transformersGenerator,
397
- requires: ["sdk"],
397
+ requires: ["typescript"],
398
398
  dateTypes: ["Date"],
399
- requiresGenerator: "^0.0.0-snapshot.1786460732"
399
+ requiresGenerator: "^0.0.0-snapshot.1786626470"
400
400
  };
401
401
  export {
402
402
  entry_transformers_default as default