chowbea-axios 1.0.0

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 (58) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +162 -0
  3. package/bin/dev.js +13 -0
  4. package/bin/run.js +10 -0
  5. package/dist/commands/diff.d.ts +31 -0
  6. package/dist/commands/diff.d.ts.map +1 -0
  7. package/dist/commands/diff.js +215 -0
  8. package/dist/commands/diff.js.map +1 -0
  9. package/dist/commands/fetch.d.ts +28 -0
  10. package/dist/commands/fetch.d.ts.map +1 -0
  11. package/dist/commands/fetch.js +223 -0
  12. package/dist/commands/fetch.js.map +1 -0
  13. package/dist/commands/generate.d.ts +26 -0
  14. package/dist/commands/generate.d.ts.map +1 -0
  15. package/dist/commands/generate.js +187 -0
  16. package/dist/commands/generate.js.map +1 -0
  17. package/dist/commands/init.d.ts +92 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +738 -0
  20. package/dist/commands/init.js.map +1 -0
  21. package/dist/commands/status.d.ts +38 -0
  22. package/dist/commands/status.d.ts.map +1 -0
  23. package/dist/commands/status.js +233 -0
  24. package/dist/commands/status.js.map +1 -0
  25. package/dist/commands/validate.d.ts +27 -0
  26. package/dist/commands/validate.d.ts.map +1 -0
  27. package/dist/commands/validate.js +209 -0
  28. package/dist/commands/validate.js.map +1 -0
  29. package/dist/commands/watch.d.ts +34 -0
  30. package/dist/commands/watch.d.ts.map +1 -0
  31. package/dist/commands/watch.js +202 -0
  32. package/dist/commands/watch.js.map +1 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +6 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib/config.d.ts +151 -0
  38. package/dist/lib/config.d.ts.map +1 -0
  39. package/dist/lib/config.js +336 -0
  40. package/dist/lib/config.js.map +1 -0
  41. package/dist/lib/errors.d.ts +77 -0
  42. package/dist/lib/errors.d.ts.map +1 -0
  43. package/dist/lib/errors.js +144 -0
  44. package/dist/lib/errors.js.map +1 -0
  45. package/dist/lib/fetcher.d.ts +115 -0
  46. package/dist/lib/fetcher.d.ts.map +1 -0
  47. package/dist/lib/fetcher.js +237 -0
  48. package/dist/lib/fetcher.js.map +1 -0
  49. package/dist/lib/generator.d.ts +96 -0
  50. package/dist/lib/generator.d.ts.map +1 -0
  51. package/dist/lib/generator.js +1575 -0
  52. package/dist/lib/generator.js.map +1 -0
  53. package/dist/lib/logger.d.ts +63 -0
  54. package/dist/lib/logger.d.ts.map +1 -0
  55. package/dist/lib/logger.js +183 -0
  56. package/dist/lib/logger.js.map +1 -0
  57. package/oclif.manifest.json +556 -0
  58. package/package.json +68 -0
@@ -0,0 +1,1575 @@
1
+ /**
2
+ * Core generation logic for TypeScript types and operations.
3
+ * Migrated from scripts/generate-operations.js with atomic writes and rollback.
4
+ */
5
+ import { spawnSync } from "node:child_process";
6
+ import { access, copyFile, readFile, rename, unlink, writeFile, } from "node:fs/promises";
7
+ import { GenerationError } from "./errors.js";
8
+ /**
9
+ * Extracts path parameter names from an OpenAPI path template.
10
+ * Example: "/api/users/{id}/posts/{postId}" -> ["id", "postId"]
11
+ */
12
+ function extractPathParams(pathTemplate) {
13
+ const matches = pathTemplate.matchAll(/\{([^}]+)\}/g);
14
+ return [...matches].map((match) => match[1]);
15
+ }
16
+ /**
17
+ * Converts a path parameter name to camelCase.
18
+ * Example: "user_id" -> "userId", "user-id" -> "userId"
19
+ */
20
+ function toCamelCase(str) {
21
+ return str.replace(/[-_]([a-z])/g, (_, letter) => letter.toUpperCase());
22
+ }
23
+ /**
24
+ * Generates TypeScript type for path parameters.
25
+ */
26
+ function generatePathParamsType(pathParams) {
27
+ if (pathParams.length === 0)
28
+ return null;
29
+ const params = pathParams
30
+ .map((param) => `${toCamelCase(param)}: string | number`)
31
+ .join(", ");
32
+ return `{ ${params} }`;
33
+ }
34
+ /**
35
+ * Generates a single operation function.
36
+ */
37
+ function generateOperationFunction(operation) {
38
+ const { operationId, method, path: pathTemplate, pathParams, hasRequestBody, summary, description, } = operation;
39
+ const httpMethod = method.toLowerCase();
40
+ // Build parameter list
41
+ const params = [];
42
+ // Path parameters
43
+ if (pathParams.length > 0) {
44
+ const pathParamsType = generatePathParamsType(pathParams);
45
+ params.push(`pathParams: ${pathParamsType}`);
46
+ }
47
+ // Request body for POST/PUT/PATCH
48
+ if (hasRequestBody) {
49
+ params.push(`data: RequestBody<"${pathTemplate}", "${httpMethod}">`);
50
+ }
51
+ // Config parameter (always last and optional)
52
+ params.push(`config?: RequestConfig<"${pathTemplate}", "${httpMethod}">`);
53
+ // Generate JSDoc comment
54
+ const jsdoc = [];
55
+ jsdoc.push(" /**");
56
+ if (summary) {
57
+ jsdoc.push(` * ${summary}`);
58
+ }
59
+ if (description && description !== summary) {
60
+ jsdoc.push(` * ${description}`);
61
+ }
62
+ jsdoc.push(" * ");
63
+ jsdoc.push(` * @operationId ${operationId}`);
64
+ jsdoc.push(` * @method ${method.toUpperCase()}`);
65
+ jsdoc.push(` * @path ${pathTemplate}`);
66
+ jsdoc.push(" */");
67
+ // Generate function with explicit return type - uses Result<T> for consistent error handling
68
+ const functionParams = params.join(", ");
69
+ const returnType = `Promise<Result<ResponseData<"${pathTemplate}", "${httpMethod}">>>`;
70
+ // Build the apiClient call (apiClient methods already return Result<T>)
71
+ let apiCall;
72
+ if (hasRequestBody) {
73
+ // POST/PUT/PATCH with body
74
+ if (pathParams.length > 0) {
75
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", data, pathParams, config)`;
76
+ }
77
+ else {
78
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", data, config)`;
79
+ }
80
+ }
81
+ else {
82
+ // GET/DELETE without body
83
+ // PATCH without body still needs undefined as data parameter
84
+ if (httpMethod === "patch") {
85
+ if (pathParams.length > 0) {
86
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", undefined, pathParams, config)`;
87
+ }
88
+ else {
89
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", undefined, config)`;
90
+ }
91
+ }
92
+ else if (pathParams.length > 0) {
93
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", pathParams, config)`;
94
+ }
95
+ else {
96
+ apiCall = `apiClient.${httpMethod}("${pathTemplate}", config)`;
97
+ }
98
+ }
99
+ return `${jsdoc.join("\n")}
100
+ ${operationId}: (${functionParams}): ${returnType} => ${apiCall},\n`;
101
+ }
102
+ /**
103
+ * Parses the OpenAPI spec and extracts all operations with operationIds.
104
+ */
105
+ function parseOperations(spec, logger) {
106
+ const operations = [];
107
+ if (typeof spec !== "object" || spec === null) {
108
+ return operations;
109
+ }
110
+ const specObj = spec;
111
+ const paths = specObj.paths;
112
+ if (!paths) {
113
+ return operations;
114
+ }
115
+ // Iterate through all paths
116
+ for (const [pathTemplate, pathItem] of Object.entries(paths)) {
117
+ // Iterate through all HTTP methods
118
+ for (const method of ["get", "post", "put", "delete", "patch"]) {
119
+ const operation = pathItem[method];
120
+ if (!operation)
121
+ continue;
122
+ // Skip operations without operationId
123
+ if (!operation.operationId || typeof operation.operationId !== "string") {
124
+ logger.warn({ method: method.toUpperCase(), path: pathTemplate }, "Skipping operation without operationId");
125
+ continue;
126
+ }
127
+ // Extract path parameters
128
+ const pathParams = extractPathParams(pathTemplate);
129
+ // Check for request body
130
+ const hasRequestBody = Boolean(operation.requestBody);
131
+ // Check for query parameters
132
+ const parameters = operation.parameters;
133
+ const hasQueryParams = parameters?.some((param) => param.in === "query") ?? false;
134
+ operations.push({
135
+ operationId: operation.operationId,
136
+ method,
137
+ path: pathTemplate,
138
+ pathParams,
139
+ hasRequestBody,
140
+ hasQueryParams,
141
+ summary: operation.summary ?? "",
142
+ description: operation.description ?? "",
143
+ });
144
+ logger.debug({ operationId: operation.operationId }, "Found operation");
145
+ }
146
+ }
147
+ return operations;
148
+ }
149
+ /**
150
+ * Generates the TypeScript file content with all operations.
151
+ */
152
+ function generateOperationsFileContent(operations) {
153
+ const header = `/**
154
+ * Auto-generated API operations from OpenAPI spec.
155
+ *
156
+ * This file is automatically generated by chowbea-axios CLI.
157
+ * DO NOT EDIT MANUALLY - your changes will be overwritten.
158
+ *
159
+ * Last generated: ${new Date().toISOString()}
160
+ * Total operations: ${operations.length}
161
+ */
162
+
163
+ /* ~ =================================== ~ */
164
+ /* -- This file provides semantic operation-based API functions -- */
165
+ /* -- Use apiClient.op.operationName() instead of raw paths -- */
166
+ /* ~ =================================== ~ */
167
+
168
+ import type { paths } from "./api.types"
169
+ import type { AxiosRequestConfig } from "axios"
170
+ import type { Result } from "../api.error"
171
+
172
+ /* ~ =================================== ~ */
173
+ /* -- Type Helpers -- */
174
+ /* ~ =================================== ~ */
175
+
176
+ /**
177
+ * Extracts the operation definition for a given path and method.
178
+ */
179
+ type Operation<
180
+ P extends keyof paths,
181
+ M extends "get" | "post" | "put" | "delete" | "patch"
182
+ > = paths[P][M]
183
+
184
+ /**
185
+ * Maps OpenAPI form-data field types to their runtime equivalents.
186
+ * Uses field names to intelligently detect file upload fields vs regular string fields.
187
+ *
188
+ * File field patterns: images, files, attachments, uploads, documents, photos, videos, media
189
+ * Regular string fields: All other string/string[] fields remain unchanged
190
+ */
191
+ type MapFormDataTypes<T> = T extends Record<string, unknown>
192
+ ? {
193
+ [K in keyof T]:
194
+ // Check if field name suggests it's a file upload field
195
+ K extends \`\${string}image\${string}\` | \`\${string}file\${string}\` | \`\${string}attachment\${string}\` |
196
+ \`\${string}upload\${string}\` | \`\${string}document\${string}\` | \`\${string}photo\${string}\` |
197
+ \`\${string}video\${string}\` | \`\${string}media\${string}\`
198
+ ? T[K] extends string[]
199
+ ? File[] // File upload fields become File[]
200
+ : T[K] extends string
201
+ ? File | Blob // Single file becomes File or Blob
202
+ : T[K]
203
+ : T[K]; // Non-file fields keep their original type
204
+ }
205
+ : T;
206
+
207
+ /**
208
+ * Extracts the request body type for a given path and method directly from OpenAPI spec.
209
+ * Handles both JSON (application/json) and FormData (multipart/form-data) content types.
210
+ * For form-data, maps string/string[] types to File/File[] for file upload fields.
211
+ * Converts Record<string, never> (from generic object schemas) to Record<string, unknown>.
212
+ */
213
+ type RequestBody<
214
+ P extends keyof paths,
215
+ M extends "get" | "post" | "put" | "delete" | "patch"
216
+ > =
217
+ Operation<P, M> extends { requestBody: { content: { "application/json": infer T } } }
218
+ ? T extends Record<string, never>
219
+ ? Record<string, unknown>
220
+ : T
221
+ : Operation<P, M> extends { requestBody?: { content: { "application/json": infer T } } }
222
+ ? T extends Record<string, never>
223
+ ? Record<string, unknown>
224
+ : T
225
+ : Operation<P, M> extends { requestBody: { content: { "multipart/form-data": infer T } } }
226
+ ? MapFormDataTypes<T>
227
+ : Operation<P, M> extends { requestBody?: { content: { "multipart/form-data": infer T } } }
228
+ ? MapFormDataTypes<T>
229
+ : never
230
+
231
+ /**
232
+ * Extracts the response data type for a given path and method from OpenAPI spec.
233
+ * Defaults to 200 status code response.
234
+ */
235
+ type ResponseData<
236
+ P extends keyof paths,
237
+ M extends "get" | "post" | "put" | "delete" | "patch"
238
+ > = Operation<P, M> extends {
239
+ responses: { 200: { content: { "application/json": infer T } } }
240
+ }
241
+ ? T
242
+ : Operation<P, M> extends {
243
+ responses: { 201: { content: { "application/json": infer T } } }
244
+ }
245
+ ? T
246
+ : unknown
247
+
248
+ /**
249
+ * Extracts query parameters for a given path and method from OpenAPI spec.
250
+ */
251
+ type QueryParams<
252
+ P extends keyof paths,
253
+ M extends "get" | "post" | "put" | "delete" | "patch"
254
+ > = Operation<P, M> extends { parameters: { query?: infer Q } }
255
+ ? Q extends Record<string, unknown>
256
+ ? Q
257
+ : never
258
+ : never
259
+
260
+ /**
261
+ * Request config with typed query parameters extracted from OpenAPI spec.
262
+ */
263
+ type RequestConfig<
264
+ P extends keyof paths,
265
+ M extends "get" | "post" | "put" | "delete" | "patch"
266
+ > = Omit<AxiosRequestConfig, "params"> & {
267
+ params?: QueryParams<P, M>
268
+ }
269
+
270
+ /* ~ =================================== ~ */
271
+ /* -- Generated Operations -- */
272
+ /* ~ =================================== ~ */
273
+
274
+ /**
275
+ * Collection of all API operations extracted from the OpenAPI spec.
276
+ * Each operation is a typed function that wraps the underlying apiClient methods.
277
+ *
278
+ * @example
279
+ * \`\`\`typescript
280
+ * // Using operation-based API
281
+ * await apiClient.op.getUserById({ id: "123" })
282
+ *
283
+ * // With query parameters
284
+ * await apiClient.op.listUsers({ params: { limit: 10, offset: 0 } })
285
+ *
286
+ * // With request body
287
+ * await apiClient.op.createUser({ name: "John", email: "john@example.com" })
288
+ * \`\`\`
289
+ */
290
+ export const createOperations = (apiClient: any) => ({
291
+ `;
292
+ const operationFunctions = operations
293
+ .map((op) => generateOperationFunction(op))
294
+ .join("\n");
295
+ const footer = `}) as const
296
+
297
+ /**
298
+ * Type representing all available API operations.
299
+ * This type is inferred from the createOperations return value for proper TypeScript support.
300
+ */
301
+ export type ApiOperations = ReturnType<typeof createOperations>
302
+ `;
303
+ return header + operationFunctions + footer;
304
+ }
305
+ /**
306
+ * Runs openapi-typescript to generate base types.
307
+ * Uses pnpm dlx to avoid requiring it as a direct dependency.
308
+ */
309
+ async function generateTypes(specPath, typesPath, logger) {
310
+ logger.info({ specPath, typesPath }, "Generating TypeScript types...");
311
+ const result = spawnSync("pnpm", ["dlx", "openapi-typescript", specPath, "--output", typesPath], {
312
+ stdio: "pipe",
313
+ cwd: process.cwd(),
314
+ });
315
+ if (result.error) {
316
+ throw new GenerationError("openapi-typescript", `Failed to spawn: ${result.error.message}`);
317
+ }
318
+ if (result.status !== 0) {
319
+ const stderr = result.stderr?.toString() ?? "Unknown error";
320
+ throw new GenerationError("openapi-typescript", `Exited with code ${result.status}: ${stderr}`);
321
+ }
322
+ logger.info("TypeScript types generated successfully");
323
+ }
324
+ /**
325
+ * Atomic write - writes to temp file then renames.
326
+ * This ensures the file is never in a partial state.
327
+ */
328
+ async function atomicWrite(filePath, content) {
329
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
330
+ try {
331
+ await writeFile(tempPath, content, "utf8");
332
+ await rename(tempPath, filePath);
333
+ }
334
+ catch (error) {
335
+ // Clean up temp file if rename fails
336
+ try {
337
+ await unlink(tempPath);
338
+ }
339
+ catch {
340
+ // Ignore cleanup errors
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+ /**
346
+ * Creates a backup of existing generated files for rollback.
347
+ */
348
+ async function createBackup(paths) {
349
+ const timestamp = Date.now();
350
+ let typesBackup = null;
351
+ let operationsBackup = null;
352
+ try {
353
+ const typesBackupPath = `${paths.types}.backup.${timestamp}`;
354
+ await copyFile(paths.types, typesBackupPath);
355
+ typesBackup = typesBackupPath;
356
+ }
357
+ catch {
358
+ // No existing types file to backup
359
+ }
360
+ try {
361
+ const operationsBackupPath = `${paths.operations}.backup.${timestamp}`;
362
+ await copyFile(paths.operations, operationsBackupPath);
363
+ operationsBackup = operationsBackupPath;
364
+ }
365
+ catch {
366
+ // No existing operations file to backup
367
+ }
368
+ return { typesBackup, operationsBackup };
369
+ }
370
+ /**
371
+ * Restores files from backup on generation failure.
372
+ */
373
+ async function restoreFromBackup(backups, paths) {
374
+ if (backups.typesBackup) {
375
+ try {
376
+ await rename(backups.typesBackup, paths.types);
377
+ }
378
+ catch {
379
+ // Ignore restore errors
380
+ }
381
+ }
382
+ if (backups.operationsBackup) {
383
+ try {
384
+ await rename(backups.operationsBackup, paths.operations);
385
+ }
386
+ catch {
387
+ // Ignore restore errors
388
+ }
389
+ }
390
+ }
391
+ /**
392
+ * Cleans up backup files after successful generation.
393
+ */
394
+ async function cleanupBackups(backups) {
395
+ if (backups.typesBackup) {
396
+ try {
397
+ await unlink(backups.typesBackup);
398
+ }
399
+ catch {
400
+ // Ignore cleanup errors
401
+ }
402
+ }
403
+ if (backups.operationsBackup) {
404
+ try {
405
+ await unlink(backups.operationsBackup);
406
+ }
407
+ catch {
408
+ // Ignore cleanup errors
409
+ }
410
+ }
411
+ }
412
+ /**
413
+ * Checks if a file exists using access check (more efficient than reading).
414
+ */
415
+ async function fileExists(filePath) {
416
+ try {
417
+ await access(filePath);
418
+ return true;
419
+ }
420
+ catch {
421
+ return false;
422
+ }
423
+ }
424
+ /* ~ =================================== ~ */
425
+ /* -- Client File Templates -- */
426
+ /* ~ =================================== ~ */
427
+ /**
428
+ * Generates the api.helpers.ts file content.
429
+ * Contains all utility types for extracting request/response types from OpenAPI schema.
430
+ */
431
+ export function generateHelpersFileContent() {
432
+ return `/**
433
+ * Type utilities for extracting request/response types from OpenAPI schema.
434
+ *
435
+ * This file is generated once by chowbea-axios CLI.
436
+ * You can safely modify this file - it will NOT be overwritten.
437
+ *
438
+ * Generated: ${new Date().toISOString()}
439
+ */
440
+
441
+ import type { paths, components, operations } from "./_generated/api.types";
442
+
443
+ /* ~ =================================== ~ */
444
+ /* -- Base Types -- */
445
+ /* ~ =================================== ~ */
446
+
447
+ /** All path templates defined by the OpenAPI paths map. */
448
+ type Paths = keyof paths;
449
+
450
+ /** HTTP methods supported by the client. */
451
+ type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
452
+
453
+ /** Resolves the OpenAPI operation schema for a given path and method. */
454
+ type Operation<P extends Paths, M extends HttpMethod> = paths[P][M];
455
+
456
+ /* ~ =================================== ~ */
457
+ /* -- Path Parameter Extraction -- */
458
+ /* ~ =================================== ~ */
459
+
460
+ /** Extracts placeholder parameter names from an OpenAPI-style path template. */
461
+ type ExtractPathParamNames<T extends string> =
462
+ T extends \`\${string}{\${infer P}}\${infer R}\`
463
+ ? P | ExtractPathParamNames<R>
464
+ : never;
465
+
466
+ /** Maps extracted path parameter names to a simple serializable value type. */
467
+ type PathParams<P extends Paths> = ExtractPathParamNames<P & string> extends never
468
+ ? never
469
+ : Record<ExtractPathParamNames<P & string>, string | number | boolean>;
470
+
471
+ /* ~ =================================== ~ */
472
+ /* -- Query Parameter Extraction -- */
473
+ /* ~ =================================== ~ */
474
+
475
+ /** Extracts query parameter types from the OpenAPI operation schema. */
476
+ type QueryParams<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
477
+ parameters: { query?: infer Q };
478
+ }
479
+ ? Q extends Record<string, unknown>
480
+ ? Q
481
+ : never
482
+ : never;
483
+
484
+ /* ~ =================================== ~ */
485
+ /* -- Request Body Extraction -- */
486
+ /* ~ =================================== ~ */
487
+
488
+ /** Maps OpenAPI form-data field types to their runtime equivalents. */
489
+ type MapFormDataTypes<T> = T extends Record<string, unknown>
490
+ ? {
491
+ [K in keyof T]: K extends
492
+ | \`\${string}image\${string}\`
493
+ | \`\${string}file\${string}\`
494
+ | \`\${string}attachment\${string}\`
495
+ | \`\${string}upload\${string}\`
496
+ | \`\${string}document\${string}\`
497
+ | \`\${string}photo\${string}\`
498
+ | \`\${string}video\${string}\`
499
+ | \`\${string}media\${string}\`
500
+ ? T[K] extends string[]
501
+ ? File[]
502
+ : T[K] extends string
503
+ ? File | Blob
504
+ : T[K]
505
+ : T[K];
506
+ }
507
+ : T;
508
+
509
+ /** Infers the request body type for a given path/method pair from OpenAPI. */
510
+ type RequestBody<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
511
+ requestBody: { content: { "application/json": infer T } };
512
+ }
513
+ ? T extends Record<string, never>
514
+ ? Record<string, unknown>
515
+ : T
516
+ : Operation<P, M> extends { requestBody?: { content: { "application/json": infer T } } }
517
+ ? T extends Record<string, never>
518
+ ? Record<string, unknown>
519
+ : T
520
+ : Operation<P, M> extends { requestBody: { content: { "multipart/form-data": infer T } } }
521
+ ? MapFormDataTypes<T>
522
+ : Operation<P, M> extends { requestBody?: { content: { "multipart/form-data": infer T } } }
523
+ ? MapFormDataTypes<T>
524
+ : never;
525
+
526
+ /* ~ =================================== ~ */
527
+ /* -- Response Data Extraction -- */
528
+ /* ~ =================================== ~ */
529
+
530
+ /** Extracts all available status codes from an operation's responses. */
531
+ type AvailableStatusCodes<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
532
+ responses: infer R;
533
+ }
534
+ ? R extends Record<string, unknown>
535
+ ? keyof R & number
536
+ : never
537
+ : never;
538
+
539
+ /** Infers the JSON response body type for a given status code from OpenAPI. */
540
+ type ResponseData<
541
+ P extends Paths,
542
+ M extends HttpMethod,
543
+ Status extends AvailableStatusCodes<P, M> = 200 extends AvailableStatusCodes<P, M>
544
+ ? 200
545
+ : AvailableStatusCodes<P, M>,
546
+ > = Operation<P, M> extends {
547
+ responses: { [K in Status]: { content: { "application/json": infer T } } };
548
+ }
549
+ ? T
550
+ : never;
551
+
552
+ /* ~ =================================== ~ */
553
+ /* -- Intellisense Helpers -- */
554
+ /* ~ =================================== ~ */
555
+
556
+ /**
557
+ * Forces TypeScript to expand and display the full type structure.
558
+ * Improves intellisense by showing actual type properties instead of type references.
559
+ */
560
+ type Expand<T> = T extends (...args: infer A) => infer R
561
+ ? (...args: Expand<A>) => Expand<R>
562
+ : T extends object
563
+ ? T extends infer O
564
+ ? { [K in keyof O]: O[K] }
565
+ : never
566
+ : T;
567
+
568
+ /**
569
+ * Recursively expands nested types for better intellisense.
570
+ * Expands all levels of nested objects to show full type structure.
571
+ */
572
+ type ExpandRecursively<T> = T extends (...args: infer A) => infer R
573
+ ? (...args: ExpandRecursively<A>) => ExpandRecursively<R>
574
+ : T extends object
575
+ ? T extends infer O
576
+ ? { [K in keyof O]: ExpandRecursively<O[K]> }
577
+ : never
578
+ : T;
579
+
580
+ /* ~ =================================== ~ */
581
+ /* -- Path-Based API Type Helpers -- */
582
+ /* ~ =================================== ~ */
583
+
584
+ /**
585
+ * Extract request body type for a given path and method.
586
+ * @example type CreateUserInput = ApiRequestBody<"/api/users", "post">
587
+ */
588
+ export type ApiRequestBody<P extends Paths, M extends HttpMethod> = ExpandRecursively<
589
+ RequestBody<P, M>
590
+ >;
591
+
592
+ /**
593
+ * Extract response data type for a given path, method, and status code.
594
+ * @example type UserResponse = ApiResponseData<"/api/users/{id}", "get">
595
+ * @example type CreatedResponse = ApiResponseData<"/api/users", "post", 201>
596
+ */
597
+ export type ApiResponseData<
598
+ P extends Paths,
599
+ M extends HttpMethod,
600
+ Status extends AvailableStatusCodes<P, M> = 200 extends AvailableStatusCodes<P, M>
601
+ ? 200
602
+ : AvailableStatusCodes<P, M>,
603
+ > = ExpandRecursively<ResponseData<P, M, Status>>;
604
+
605
+ /**
606
+ * Extract path parameters for a given path.
607
+ * @example type UserPathParams = ApiPathParams<"/api/users/{id}">
608
+ */
609
+ export type ApiPathParams<P extends Paths> = ExpandRecursively<PathParams<P>>;
610
+
611
+ /**
612
+ * Extract query parameters for a given path and method.
613
+ * @example type ListUsersQuery = ApiQueryParams<"/api/users", "get">
614
+ */
615
+ export type ApiQueryParams<P extends Paths, M extends HttpMethod> = ExpandRecursively<
616
+ QueryParams<P, M>
617
+ >;
618
+
619
+ /**
620
+ * Get all available status codes for a given path and method.
621
+ * @example type UserStatusCodes = ApiStatusCodes<"/api/users/{id}", "get">
622
+ */
623
+ export type ApiStatusCodes<P extends Paths, M extends HttpMethod> = AvailableStatusCodes<P, M>;
624
+
625
+ /* ~ =================================== ~ */
626
+ /* -- Operation-Based API Type Helpers -- */
627
+ /* ~ =================================== ~ */
628
+
629
+ /** Extracts all available status codes from an operation's responses by operation ID. */
630
+ type OperationStatusCodes<OpId extends keyof operations> = operations[OpId] extends {
631
+ responses: infer R;
632
+ }
633
+ ? R extends Record<string, unknown>
634
+ ? keyof R & number
635
+ : never
636
+ : never;
637
+
638
+ /** Determines the default positive status code for an operation. */
639
+ type OperationPositiveStatus<OpId extends keyof operations> =
640
+ 200 extends OperationStatusCodes<OpId>
641
+ ? 200
642
+ : 201 extends OperationStatusCodes<OpId>
643
+ ? 201
644
+ : 202 extends OperationStatusCodes<OpId>
645
+ ? 202
646
+ : 204 extends OperationStatusCodes<OpId>
647
+ ? 204
648
+ : OperationStatusCodes<OpId>;
649
+
650
+ /**
651
+ * Extract request body type by operation ID.
652
+ * @example type CreateUserInput = ServerRequestBody<"createUser">
653
+ */
654
+ export type ServerRequestBody<OpId extends keyof operations> = ExpandRecursively<
655
+ operations[OpId] extends { requestBody: { content: { "application/json": infer T } } }
656
+ ? T extends Record<string, never>
657
+ ? Record<string, unknown>
658
+ : T
659
+ : operations[OpId] extends { requestBody?: { content: { "application/json": infer T } } }
660
+ ? T extends Record<string, never>
661
+ ? Record<string, unknown>
662
+ : T
663
+ : never
664
+ >;
665
+
666
+ /**
667
+ * Extract request parameters (path and query) by operation ID.
668
+ * @example type GetUserParams = ServerRequestParams<"getUserById">
669
+ */
670
+ export type ServerRequestParams<OpId extends keyof operations> = ExpandRecursively<
671
+ operations[OpId] extends { parameters: infer P }
672
+ ? P extends { path?: infer Path; query?: infer Query }
673
+ ? (Path extends Record<string, unknown> ? { path: Path } : Record<string, never>) &
674
+ (Query extends Record<string, unknown> ? { query?: Query } : Record<string, never>)
675
+ : P extends { path?: infer Path }
676
+ ? Path extends Record<string, unknown>
677
+ ? { path: Path }
678
+ : never
679
+ : P extends { query?: infer Query }
680
+ ? Query extends Record<string, unknown>
681
+ ? { query?: Query }
682
+ : never
683
+ : never
684
+ : never
685
+ >;
686
+
687
+ /**
688
+ * Extract response type by operation ID with optional status code.
689
+ * Defaults to the positive status code (200, 201, 202, or 204).
690
+ * @example type UserResponse = ServerResponseType<"getUserById">
691
+ * @example type NotFoundResponse = ServerResponseType<"getUserById", 404>
692
+ */
693
+ export type ServerResponseType<
694
+ OpId extends keyof operations,
695
+ Status extends OperationStatusCodes<OpId> = OperationPositiveStatus<OpId>,
696
+ > = ExpandRecursively<
697
+ operations[OpId] extends {
698
+ responses: { [K in Status]: { content: { "application/json": infer T } } };
699
+ }
700
+ ? T
701
+ : never
702
+ >;
703
+
704
+ /**
705
+ * Extract model/schema type from OpenAPI components.
706
+ * @example type User = ServerModel<"UserContract">
707
+ * @example type Meeting = ServerModel<"MeetingContract">
708
+ */
709
+ export type ServerModel<ModelName extends keyof components["schemas"]> = ExpandRecursively<
710
+ components["schemas"][ModelName]
711
+ >;
712
+
713
+ /* ~ =================================== ~ */
714
+ /* -- Re-exports for Convenience -- */
715
+ /* ~ =================================== ~ */
716
+
717
+ export type { Paths, HttpMethod, Expand, ExpandRecursively };
718
+ `;
719
+ }
720
+ /**
721
+ * Generates the api.instance.ts file content.
722
+ */
723
+ export function generateInstanceFileContent(config) {
724
+ return `/**
725
+ * Axios instance with authentication interceptor.
726
+ *
727
+ * This file is generated once by chowbea-axios CLI.
728
+ * You can safely modify this file - it will NOT be overwritten.
729
+ *
730
+ * Generated: ${new Date().toISOString()}
731
+ */
732
+
733
+ import axios from "axios";
734
+
735
+ /** localStorage key for auth token */
736
+ export const tokenKey = "${config.token_key}";
737
+
738
+ /**
739
+ * Shared Axios instance configured with the API base URL.
740
+ */
741
+ export const axiosInstance = axios.create({
742
+ baseURL: import.meta.env.${config.base_url_env},
743
+ withCredentials: ${config.with_credentials},
744
+ timeout: ${config.timeout},
745
+ });
746
+
747
+ /**
748
+ * Request interceptor that automatically attaches the auth token.
749
+ * Reads the token from localStorage and adds it to the Authorization header.
750
+ */
751
+ axiosInstance.interceptors.request.use(
752
+ (config) => {
753
+ // Check if code is running in browser environment
754
+ if (typeof window !== "undefined") {
755
+ const tokenObject = localStorage.getItem(tokenKey);
756
+
757
+ if (tokenObject) {
758
+ try {
759
+ // Handle both { state: { token } } and plain token string
760
+ const parsed = JSON.parse(tokenObject);
761
+ const token = parsed.state?.token || parsed.token || parsed;
762
+ if (typeof token === "string") {
763
+ config.headers.Authorization = \`Bearer \${token}\`;
764
+ }
765
+ } catch {
766
+ // If not JSON, use as-is
767
+ config.headers.Authorization = \`Bearer \${tokenObject}\`;
768
+ }
769
+ }
770
+ }
771
+
772
+ return config;
773
+ },
774
+ (error) => Promise.reject(error)
775
+ );
776
+ `;
777
+ }
778
+ /**
779
+ * Generates the api.error.ts file content.
780
+ */
781
+ export function generateErrorFileContent() {
782
+ return `/**
783
+ * Result-based error handling for API calls.
784
+ *
785
+ * This file is generated once by chowbea-axios CLI.
786
+ * You can safely modify this file - it will NOT be overwritten.
787
+ *
788
+ * Generated: ${new Date().toISOString()}
789
+ */
790
+
791
+ import { AxiosError, type AxiosResponse } from "axios";
792
+
793
+ /* ~ =================================== ~ */
794
+ /* -- Types -- */
795
+ /* ~ =================================== ~ */
796
+
797
+ /**
798
+ * Request context for debugging - what request caused the error.
799
+ */
800
+ export interface RequestContext {
801
+ /** HTTP method (GET, POST, etc.) */
802
+ method: string;
803
+ /** URL that was called */
804
+ url: string;
805
+ /** Base URL from axios config */
806
+ baseURL?: string;
807
+ /** Query parameters */
808
+ params?: unknown;
809
+ /** Request body (sensitive fields redacted) */
810
+ data?: unknown;
811
+ }
812
+
813
+ /**
814
+ * Normalized API error with extracted message and metadata.
815
+ */
816
+ export interface ApiError {
817
+ /** Human-readable error message */
818
+ message: string;
819
+ /** Error code (NETWORK_ERROR, VALIDATION_ERROR, etc.) */
820
+ code: string;
821
+ /** HTTP status code (null for network errors) */
822
+ status: number | null;
823
+ /** What request caused this error */
824
+ request: RequestContext;
825
+ /** Original error response body for debugging */
826
+ details?: unknown;
827
+ }
828
+
829
+ /**
830
+ * Result type - API calls return this instead of throwing.
831
+ * Success: { data: T, error: null }
832
+ * Failure: { data: null, error: ApiError }
833
+ */
834
+ export type Result<T> =
835
+ | { data: T; error: null }
836
+ | { data: null; error: ApiError };
837
+
838
+ /* ~ =================================== ~ */
839
+ /* -- Error Normalization -- */
840
+ /* ~ =================================== ~ */
841
+
842
+ /**
843
+ * Normalizes error messages from various API response formats.
844
+ * Handles common patterns from different backend frameworks.
845
+ */
846
+ export function normalizeErrorMessage(error: unknown): string {
847
+ if (!error || typeof error !== "object") {
848
+ return "An unexpected error occurred";
849
+ }
850
+
851
+ const e = error as Record<string, unknown>;
852
+
853
+ // Common: { message: "..." }
854
+ if (typeof e.message === "string") return e.message;
855
+
856
+ // .NET: { error: "..." } or { error: { message: "..." } }
857
+ if (e.error) {
858
+ if (typeof e.error === "string") return e.error;
859
+ if (typeof (e.error as Record<string, unknown>)?.message === "string") {
860
+ return (e.error as Record<string, unknown>).message as string;
861
+ }
862
+ }
863
+
864
+ // Validation: { errors: [...] } or { errors: { field: [...] } }
865
+ if (e.errors) {
866
+ if (Array.isArray(e.errors)) {
867
+ const first = e.errors[0];
868
+ if (typeof first === "string") return first;
869
+ if (typeof first?.message === "string") return first.message;
870
+ } else if (typeof e.errors === "object") {
871
+ const firstField = Object.values(e.errors)[0];
872
+ if (Array.isArray(firstField) && firstField.length > 0) {
873
+ return String(firstField[0]);
874
+ }
875
+ }
876
+ }
877
+
878
+ // FastAPI: { detail: "..." } or { detail: [...] }
879
+ if (typeof e.detail === "string") return e.detail;
880
+ if (Array.isArray(e.detail) && e.detail[0]?.msg) {
881
+ return e.detail[0].msg;
882
+ }
883
+
884
+ // ASP.NET Problem Details: { title: "..." }
885
+ if (typeof e.title === "string") return e.title;
886
+
887
+ return "An unexpected error occurred";
888
+ }
889
+
890
+ /* ~ =================================== ~ */
891
+ /* -- Request Context Extraction -- */
892
+ /* ~ =================================== ~ */
893
+
894
+ /** Fields that should be redacted from request data */
895
+ const SENSITIVE_FIELDS = [
896
+ "password",
897
+ "token",
898
+ "secret",
899
+ "authorization",
900
+ "apikey",
901
+ "api_key",
902
+ "access_token",
903
+ "refresh_token",
904
+ ];
905
+
906
+ /**
907
+ * Redacts sensitive fields from request data for safe logging.
908
+ */
909
+ function redactSensitive(data: unknown): unknown {
910
+ if (!data || typeof data !== "object") return data;
911
+
912
+ const redacted = { ...(data as Record<string, unknown>) };
913
+
914
+ for (const key of Object.keys(redacted)) {
915
+ if (SENSITIVE_FIELDS.some((s) => key.toLowerCase().includes(s))) {
916
+ redacted[key] = "[REDACTED]";
917
+ }
918
+ }
919
+
920
+ return redacted;
921
+ }
922
+
923
+ /**
924
+ * Extracts request context from AxiosError for debugging.
925
+ */
926
+ function extractRequestContext(err: AxiosError): RequestContext {
927
+ const config = err.config;
928
+
929
+ return {
930
+ method: config?.method?.toUpperCase() || "UNKNOWN",
931
+ url: config?.url || "unknown",
932
+ baseURL: config?.baseURL,
933
+ params: config?.params,
934
+ data: redactSensitive(config?.data),
935
+ };
936
+ }
937
+
938
+ /* ~ =================================== ~ */
939
+ /* -- Error Creation -- */
940
+ /* ~ =================================== ~ */
941
+
942
+ /**
943
+ * Maps HTTP status codes to error codes.
944
+ */
945
+ function getErrorCode(status: number): string {
946
+ if (status >= 500) return "SERVER_ERROR";
947
+ switch (status) {
948
+ case 400:
949
+ return "BAD_REQUEST";
950
+ case 401:
951
+ return "UNAUTHORIZED";
952
+ case 403:
953
+ return "FORBIDDEN";
954
+ case 404:
955
+ return "NOT_FOUND";
956
+ case 409:
957
+ return "CONFLICT";
958
+ case 422:
959
+ return "VALIDATION_ERROR";
960
+ case 429:
961
+ return "RATE_LIMITED";
962
+ default:
963
+ return "REQUEST_ERROR";
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Creates an ApiError from any error.
969
+ */
970
+ export function createApiError(err: unknown): ApiError {
971
+ if (err instanceof AxiosError) {
972
+ const request = extractRequestContext(err);
973
+
974
+ // Network error (no response)
975
+ if (!err.response) {
976
+ return {
977
+ message: err.code === "ECONNABORTED"
978
+ ? "Request timed out"
979
+ : "Network error - please check your connection",
980
+ code: err.code === "ECONNABORTED" ? "TIMEOUT" : "NETWORK_ERROR",
981
+ status: null,
982
+ request,
983
+ details: { code: err.code, message: err.message },
984
+ };
985
+ }
986
+
987
+ // Server responded with error
988
+ const status = err.response.status;
989
+
990
+ return {
991
+ message: normalizeErrorMessage(err.response.data),
992
+ code: getErrorCode(status),
993
+ status,
994
+ request,
995
+ details: err.response.data,
996
+ };
997
+ }
998
+
999
+ // Unknown error
1000
+ return {
1001
+ message: err instanceof Error ? err.message : "An unexpected error occurred",
1002
+ code: "UNKNOWN_ERROR",
1003
+ status: null,
1004
+ request: { method: "UNKNOWN", url: "unknown" },
1005
+ details: err,
1006
+ };
1007
+ }
1008
+
1009
+ /* ~ =================================== ~ */
1010
+ /* -- Safe Request Wrapper -- */
1011
+ /* ~ =================================== ~ */
1012
+
1013
+ /**
1014
+ * Wraps an axios promise and returns a Result instead of throwing.
1015
+ *
1016
+ * @example
1017
+ * \`\`\`typescript
1018
+ * const { data, error } = await safeRequest(axios.get("/users"));
1019
+ * if (error) {
1020
+ * console.error(error.message);
1021
+ * return;
1022
+ * }
1023
+ * console.log(data);
1024
+ * \`\`\`
1025
+ */
1026
+ export async function safeRequest<T>(
1027
+ promise: Promise<AxiosResponse<T>>
1028
+ ): Promise<Result<T>> {
1029
+ try {
1030
+ const response = await promise;
1031
+ return { data: response.data, error: null };
1032
+ } catch (err) {
1033
+ return { data: null, error: createApiError(err) };
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Type guard to check if a result is successful.
1039
+ */
1040
+ export function isSuccess<T>(result: Result<T>): result is { data: T; error: null } {
1041
+ return result.error === null;
1042
+ }
1043
+
1044
+ /**
1045
+ * Type guard to check if a result is an error.
1046
+ */
1047
+ export function isError<T>(result: Result<T>): result is { data: null; error: ApiError } {
1048
+ return result.error !== null;
1049
+ }
1050
+ `;
1051
+ }
1052
+ /**
1053
+ * Generates the api.client.ts file content.
1054
+ */
1055
+ export function generateClientFileContent() {
1056
+ return `/**
1057
+ * Typed HTTP client for API.
1058
+ *
1059
+ * This file is generated once by chowbea-axios CLI.
1060
+ * You can safely modify this file - it will NOT be overwritten.
1061
+ *
1062
+ * Generated: ${new Date().toISOString()}
1063
+ */
1064
+
1065
+ import type { AxiosRequestConfig, AxiosResponse } from "axios";
1066
+
1067
+ import { axiosInstance } from "./api.instance";
1068
+ import { safeRequest, type Result } from "./api.error";
1069
+ import type { paths, components, operations } from "./_generated/api.types";
1070
+ import { createOperations } from "./_generated/api.operations";
1071
+
1072
+ /* ~ =================================== ~ */
1073
+ /* -- Type Helpers -- */
1074
+ /* ~ =================================== ~ */
1075
+
1076
+ /** All path templates defined by the OpenAPI paths map. */
1077
+ type Paths = keyof paths;
1078
+
1079
+ /** HTTP methods supported by the client. */
1080
+ type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
1081
+
1082
+ /** Resolves the OpenAPI operation schema for a given path and method. */
1083
+ type Operation<P extends Paths, M extends HttpMethod> = paths[P][M];
1084
+
1085
+ /** Extracts placeholder parameter names from an OpenAPI-style path template. */
1086
+ type ExtractPathParamNames<T extends string> =
1087
+ T extends \`\${string}{\${infer P}}\${infer R}\`
1088
+ ? P | ExtractPathParamNames<R>
1089
+ : never;
1090
+
1091
+ /** Maps extracted path parameter names to a simple serializable value type. */
1092
+ type PathParams<P extends Paths> = ExtractPathParamNames<
1093
+ P & string
1094
+ > extends never
1095
+ ? never
1096
+ : Record<ExtractPathParamNames<P & string>, string | number | boolean>;
1097
+
1098
+ /** Extracts query parameter types from the OpenAPI operation schema. */
1099
+ type QueryParams<P extends Paths, M extends HttpMethod> = Operation<
1100
+ P,
1101
+ M
1102
+ > extends { parameters: { query?: infer Q } }
1103
+ ? Q extends Record<string, unknown>
1104
+ ? Q
1105
+ : never
1106
+ : never;
1107
+
1108
+ /** Extended Axios config that includes typed query parameters. */
1109
+ type TypedAxiosConfig<P extends Paths, M extends HttpMethod> = Omit<
1110
+ AxiosRequestConfig,
1111
+ "params"
1112
+ > & {
1113
+ params?: QueryParams<P, M>;
1114
+ };
1115
+
1116
+ /** Maps OpenAPI form-data field types to their runtime equivalents. */
1117
+ type MapFormDataTypes<T> = T extends Record<string, unknown>
1118
+ ? {
1119
+ [K in keyof T]: K extends
1120
+ | \`\${string}image\${string}\`
1121
+ | \`\${string}file\${string}\`
1122
+ | \`\${string}attachment\${string}\`
1123
+ | \`\${string}upload\${string}\`
1124
+ | \`\${string}document\${string}\`
1125
+ | \`\${string}photo\${string}\`
1126
+ | \`\${string}video\${string}\`
1127
+ | \`\${string}media\${string}\`
1128
+ ? T[K] extends string[]
1129
+ ? File[]
1130
+ : T[K] extends string
1131
+ ? File | Blob
1132
+ : T[K]
1133
+ : T[K];
1134
+ }
1135
+ : T;
1136
+
1137
+ /** Infers the request body type for a given path/method pair. */
1138
+ type RequestBody<P extends Paths, M extends HttpMethod> = Operation<
1139
+ P,
1140
+ M
1141
+ > extends {
1142
+ requestBody: { content: { "application/json": infer T } };
1143
+ }
1144
+ ? T extends Record<string, never>
1145
+ ? Record<string, unknown>
1146
+ : T
1147
+ : Operation<P, M> extends {
1148
+ requestBody?: { content: { "application/json": infer T } };
1149
+ }
1150
+ ? T extends Record<string, never>
1151
+ ? Record<string, unknown>
1152
+ : T
1153
+ : Operation<P, M> extends {
1154
+ requestBody: { content: { "multipart/form-data": infer T } };
1155
+ }
1156
+ ? MapFormDataTypes<T>
1157
+ : Operation<P, M> extends {
1158
+ requestBody?: { content: { "multipart/form-data": infer T } };
1159
+ }
1160
+ ? MapFormDataTypes<T>
1161
+ : never;
1162
+
1163
+ /** Infers the JSON response body type for a given status code. */
1164
+ type ResponseData<
1165
+ P extends Paths,
1166
+ M extends HttpMethod,
1167
+ > = Operation<P, M> extends {
1168
+ responses: { 200: { content: { "application/json": infer T } } };
1169
+ }
1170
+ ? T
1171
+ : Operation<P, M> extends {
1172
+ responses: { 201: { content: { "application/json": infer T } } };
1173
+ }
1174
+ ? T
1175
+ : unknown;
1176
+
1177
+ /* ~ =================================== ~ */
1178
+ /* -- Utility Functions -- */
1179
+ /* ~ =================================== ~ */
1180
+
1181
+ /**
1182
+ * Replaces {param} placeholders in a path template using provided values.
1183
+ */
1184
+ function interpolatePath<P extends Paths>(
1185
+ template: P,
1186
+ params?: PathParams<P> | never
1187
+ ): string {
1188
+ const pathStr = String(template);
1189
+ if (!params) return pathStr;
1190
+
1191
+ const missing: string[] = [];
1192
+ const result = pathStr.replace(/\\{([^}]+)\\}/g, (match, key: string) => {
1193
+ const value = (params as Record<string, unknown>)[key];
1194
+ if (value === undefined || value === null) {
1195
+ missing.push(key);
1196
+ return match;
1197
+ }
1198
+ return encodeURIComponent(String(value));
1199
+ });
1200
+
1201
+ if (missing.length > 0) {
1202
+ throw new Error(
1203
+ \`Missing required path param(s): \${missing.join(", ")} for template: \${pathStr}\`
1204
+ );
1205
+ }
1206
+
1207
+ return result;
1208
+ }
1209
+
1210
+ /**
1211
+ * Checks if the request should use multipart/form-data.
1212
+ */
1213
+ function shouldUseFormData(path: string, data: unknown): boolean {
1214
+ if (data instanceof FormData) return true;
1215
+ const formDataPatterns = [/\\/upload-images$/, /\\/upload$/, /\\/files\\/upload$/];
1216
+ return formDataPatterns.some((pattern) => pattern.test(path));
1217
+ }
1218
+
1219
+ /**
1220
+ * Converts a plain object to FormData for multipart/form-data requests.
1221
+ */
1222
+ function convertToFormData(data: Record<string, unknown>): FormData {
1223
+ const formData = new FormData();
1224
+ for (const [key, value] of Object.entries(data)) {
1225
+ if (value === undefined || value === null) continue;
1226
+ if (value instanceof File || value instanceof Blob) {
1227
+ formData.append(key, value);
1228
+ } else if (Array.isArray(value)) {
1229
+ for (const item of value) {
1230
+ if (item instanceof File || item instanceof Blob) {
1231
+ formData.append(key, item);
1232
+ } else {
1233
+ formData.append(key, String(item));
1234
+ }
1235
+ }
1236
+ } else {
1237
+ formData.append(key, String(value));
1238
+ }
1239
+ }
1240
+ return formData;
1241
+ }
1242
+
1243
+ /* ~ =================================== ~ */
1244
+ /* -- API Client -- */
1245
+ /* ~ =================================== ~ */
1246
+
1247
+ /**
1248
+ * Typed API client with Result-based error handling.
1249
+ * All methods return { data, error } instead of throwing.
1250
+ */
1251
+ const api = {
1252
+ /**
1253
+ * Sends a GET request to the given OpenAPI path.
1254
+ * Returns Result<T> - never throws.
1255
+ */
1256
+ get<P extends Paths>(
1257
+ url: P,
1258
+ ...args: PathParams<P> extends never
1259
+ ? [config?: TypedAxiosConfig<P, "get">]
1260
+ : [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "get">]
1261
+ ): Promise<Result<ResponseData<P, "get">>> {
1262
+ const hasPathParams = String(url).includes("{");
1263
+ const [pathParamsOrConfig, config] = args;
1264
+
1265
+ const pathParams = hasPathParams
1266
+ ? (pathParamsOrConfig as PathParams<P>)
1267
+ : undefined;
1268
+ const finalConfig = hasPathParams
1269
+ ? (config as TypedAxiosConfig<P, "get"> | undefined)
1270
+ : (pathParamsOrConfig as TypedAxiosConfig<P, "get"> | undefined);
1271
+
1272
+ return safeRequest(
1273
+ axiosInstance.get<ResponseData<P, "get">>(
1274
+ interpolatePath(url, pathParams),
1275
+ finalConfig
1276
+ )
1277
+ );
1278
+ },
1279
+
1280
+ /**
1281
+ * Sends a POST request with a body inferred from the OpenAPI spec.
1282
+ * Returns Result<T> - never throws.
1283
+ */
1284
+ post<P extends Paths>(
1285
+ url: P,
1286
+ data: RequestBody<P, "post">,
1287
+ ...args: PathParams<P> extends never
1288
+ ? [config?: TypedAxiosConfig<P, "post">]
1289
+ : [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "post">]
1290
+ ): Promise<Result<ResponseData<P, "post">>> {
1291
+ const hasPathParams = String(url).includes("{");
1292
+ const [pathParamsOrConfig, config] = args;
1293
+
1294
+ const pathParams = hasPathParams
1295
+ ? (pathParamsOrConfig as PathParams<P>)
1296
+ : undefined;
1297
+ const finalConfig = hasPathParams
1298
+ ? (config as TypedAxiosConfig<P, "post"> | undefined)
1299
+ : (pathParamsOrConfig as TypedAxiosConfig<P, "post"> | undefined);
1300
+
1301
+ const resolvedPath = interpolatePath(url, pathParams);
1302
+ const requestData = shouldUseFormData(resolvedPath, data)
1303
+ ? data instanceof FormData
1304
+ ? data
1305
+ : convertToFormData(data as Record<string, unknown>)
1306
+ : data;
1307
+
1308
+ return safeRequest(
1309
+ axiosInstance.post<ResponseData<P, "post">>(
1310
+ resolvedPath,
1311
+ requestData,
1312
+ finalConfig
1313
+ )
1314
+ );
1315
+ },
1316
+
1317
+ /**
1318
+ * Sends a PUT request with a JSON body.
1319
+ * Returns Result<T> - never throws.
1320
+ */
1321
+ put<P extends Paths>(
1322
+ url: P,
1323
+ data: RequestBody<P, "put">,
1324
+ ...args: PathParams<P> extends never
1325
+ ? [config?: TypedAxiosConfig<P, "put">]
1326
+ : [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "put">]
1327
+ ): Promise<Result<ResponseData<P, "put">>> {
1328
+ const hasPathParams = String(url).includes("{");
1329
+ const [pathParamsOrConfig, config] = args;
1330
+
1331
+ const pathParams = hasPathParams
1332
+ ? (pathParamsOrConfig as PathParams<P>)
1333
+ : undefined;
1334
+ const finalConfig = hasPathParams
1335
+ ? (config as TypedAxiosConfig<P, "put"> | undefined)
1336
+ : (pathParamsOrConfig as TypedAxiosConfig<P, "put"> | undefined);
1337
+
1338
+ return safeRequest(
1339
+ axiosInstance.put<ResponseData<P, "put">>(
1340
+ interpolatePath(url, pathParams),
1341
+ data,
1342
+ finalConfig
1343
+ )
1344
+ );
1345
+ },
1346
+
1347
+ /**
1348
+ * Sends a DELETE request.
1349
+ * Returns Result<T> - never throws.
1350
+ */
1351
+ delete<P extends Paths>(
1352
+ url: P,
1353
+ ...args: PathParams<P> extends never
1354
+ ? [config?: TypedAxiosConfig<P, "delete">]
1355
+ : [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "delete">]
1356
+ ): Promise<Result<ResponseData<P, "delete">>> {
1357
+ const hasPathParams = String(url).includes("{");
1358
+ const [pathParamsOrConfig, config] = args;
1359
+
1360
+ const pathParams = hasPathParams
1361
+ ? (pathParamsOrConfig as PathParams<P>)
1362
+ : undefined;
1363
+ const finalConfig = hasPathParams
1364
+ ? (config as TypedAxiosConfig<P, "delete"> | undefined)
1365
+ : (pathParamsOrConfig as TypedAxiosConfig<P, "delete"> | undefined);
1366
+
1367
+ return safeRequest(
1368
+ axiosInstance.delete<ResponseData<P, "delete">>(
1369
+ interpolatePath(url, pathParams),
1370
+ finalConfig
1371
+ )
1372
+ );
1373
+ },
1374
+
1375
+ /**
1376
+ * Sends a PATCH request with a JSON body.
1377
+ * Returns Result<T> - never throws.
1378
+ */
1379
+ patch<P extends Paths>(
1380
+ url: P,
1381
+ data: RequestBody<P, "patch">,
1382
+ ...args: PathParams<P> extends never
1383
+ ? [config?: TypedAxiosConfig<P, "patch">]
1384
+ : [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "patch">]
1385
+ ): Promise<Result<ResponseData<P, "patch">>> {
1386
+ const hasPathParams = String(url).includes("{");
1387
+ const [pathParamsOrConfig, config] = args;
1388
+
1389
+ const pathParams = hasPathParams
1390
+ ? (pathParamsOrConfig as PathParams<P>)
1391
+ : undefined;
1392
+ const finalConfig = hasPathParams
1393
+ ? (config as TypedAxiosConfig<P, "patch"> | undefined)
1394
+ : (pathParamsOrConfig as TypedAxiosConfig<P, "patch"> | undefined);
1395
+
1396
+ return safeRequest(
1397
+ axiosInstance.patch<ResponseData<P, "patch">>(
1398
+ interpolatePath(url, pathParams),
1399
+ data,
1400
+ finalConfig
1401
+ )
1402
+ );
1403
+ },
1404
+
1405
+ /**
1406
+ * Operation-based API methods generated from OpenAPI operationIds.
1407
+ * Provides semantic function names instead of raw path endpoints.
1408
+ */
1409
+ get op() {
1410
+ return createOperations(this);
1411
+ },
1412
+ };
1413
+
1414
+ export { api };
1415
+ export type { Paths, HttpMethod, PathParams, QueryParams, RequestBody, ResponseData };
1416
+
1417
+ // Re-export error types for convenience
1418
+ export type { ApiError, Result, RequestContext } from "./api.error";
1419
+ export { createApiError, safeRequest, isSuccess, isError } from "./api.error";
1420
+
1421
+ // Re-export types for convenience
1422
+ export type { paths, components, operations };
1423
+ `;
1424
+ }
1425
+ /**
1426
+ * Generates client files if they don't exist.
1427
+ * Returns which files were generated.
1428
+ */
1429
+ export async function generateClientFiles(options) {
1430
+ const { paths: outputPaths, instanceConfig, logger, force = false } = options;
1431
+ const result = {
1432
+ helpers: false,
1433
+ instance: false,
1434
+ error: false,
1435
+ client: false,
1436
+ };
1437
+ // Generate api.helpers.ts if it doesn't exist
1438
+ const helpersExists = await fileExists(outputPaths.helpers);
1439
+ if (!helpersExists || force) {
1440
+ logger.info({ path: outputPaths.helpers }, helpersExists ? "Regenerating api.helpers.ts" : "Creating api.helpers.ts");
1441
+ const content = generateHelpersFileContent();
1442
+ await atomicWrite(outputPaths.helpers, content);
1443
+ result.helpers = true;
1444
+ }
1445
+ else {
1446
+ logger.debug("api.helpers.ts already exists, skipping");
1447
+ }
1448
+ // Generate api.instance.ts if it doesn't exist
1449
+ const instanceExists = await fileExists(outputPaths.instance);
1450
+ if (!instanceExists || force) {
1451
+ logger.info({ path: outputPaths.instance }, instanceExists
1452
+ ? "Regenerating api.instance.ts"
1453
+ : "Creating api.instance.ts");
1454
+ const content = generateInstanceFileContent(instanceConfig);
1455
+ await atomicWrite(outputPaths.instance, content);
1456
+ result.instance = true;
1457
+ }
1458
+ else {
1459
+ logger.debug("api.instance.ts already exists, skipping");
1460
+ }
1461
+ // Generate api.error.ts if it doesn't exist
1462
+ const errorExists = await fileExists(outputPaths.error);
1463
+ if (!errorExists || force) {
1464
+ logger.info({ path: outputPaths.error }, errorExists ? "Regenerating api.error.ts" : "Creating api.error.ts");
1465
+ const content = generateErrorFileContent();
1466
+ await atomicWrite(outputPaths.error, content);
1467
+ result.error = true;
1468
+ }
1469
+ else {
1470
+ logger.debug("api.error.ts already exists, skipping");
1471
+ }
1472
+ // Generate api.client.ts if it doesn't exist
1473
+ const clientExists = await fileExists(outputPaths.client);
1474
+ if (!clientExists || force) {
1475
+ logger.info({ path: outputPaths.client }, clientExists ? "Regenerating api.client.ts" : "Creating api.client.ts");
1476
+ const content = generateClientFileContent();
1477
+ await atomicWrite(outputPaths.client, content);
1478
+ result.client = true;
1479
+ }
1480
+ else {
1481
+ logger.debug("api.client.ts already exists, skipping");
1482
+ }
1483
+ return result;
1484
+ }
1485
+ export async function generate(options) {
1486
+ const { paths: outputPaths, logger, dryRun = false, skipTypes = false, skipOperations = false, } = options;
1487
+ const startTime = Date.now();
1488
+ // Parse spec early for both dry-run and actual generation
1489
+ const specContent = await readFile(outputPaths.spec, "utf8");
1490
+ const spec = JSON.parse(specContent);
1491
+ const operations = parseOperations(spec, logger);
1492
+ if (operations.length === 0) {
1493
+ logger.warn("No operations with operationId found in OpenAPI spec");
1494
+ }
1495
+ else {
1496
+ logger.debug({ count: operations.length }, "Found operations");
1497
+ }
1498
+ // Handle dry-run mode
1499
+ if (dryRun) {
1500
+ logger.info("Dry run mode - no files will be written");
1501
+ const dryRunResult = {
1502
+ files: [],
1503
+ operationCount: operations.length,
1504
+ };
1505
+ // Check types file
1506
+ if (!skipTypes) {
1507
+ const typesExists = await fileExists(outputPaths.types);
1508
+ // We can't easily get line count without running openapi-typescript
1509
+ dryRunResult.files.push({
1510
+ path: outputPaths.types,
1511
+ lines: 0, // Unknown until generated
1512
+ action: typesExists ? "update" : "create",
1513
+ });
1514
+ }
1515
+ // Generate operations content to get line count
1516
+ if (!skipOperations) {
1517
+ const opsContent = generateOperationsFileContent(operations);
1518
+ const opsExists = await fileExists(outputPaths.operations);
1519
+ dryRunResult.files.push({
1520
+ path: outputPaths.operations,
1521
+ lines: opsContent.split("\n").length,
1522
+ action: opsExists ? "update" : "create",
1523
+ });
1524
+ }
1525
+ const durationMs = Date.now() - startTime;
1526
+ return {
1527
+ operationCount: operations.length,
1528
+ durationMs,
1529
+ typesGenerated: false,
1530
+ operationsGenerated: false,
1531
+ dryRunResult,
1532
+ };
1533
+ }
1534
+ // Create backups of existing files
1535
+ const backups = await createBackup(outputPaths);
1536
+ try {
1537
+ let typesGenerated = false;
1538
+ let operationsGenerated = false;
1539
+ // Step 1: Generate TypeScript types from OpenAPI spec
1540
+ if (skipTypes) {
1541
+ logger.info("Skipping types generation (--operations-only)");
1542
+ }
1543
+ else {
1544
+ await generateTypes(outputPaths.spec, outputPaths.types, logger);
1545
+ typesGenerated = true;
1546
+ }
1547
+ // Step 2: Generate operations file
1548
+ if (skipOperations) {
1549
+ logger.info("Skipping operations generation (--types-only)");
1550
+ }
1551
+ else {
1552
+ logger.info("Generating operations file...");
1553
+ const operationsContent = generateOperationsFileContent(operations);
1554
+ await atomicWrite(outputPaths.operations, operationsContent);
1555
+ operationsGenerated = true;
1556
+ }
1557
+ // Clean up backups on success
1558
+ await cleanupBackups(backups);
1559
+ const durationMs = Date.now() - startTime;
1560
+ logger.info({ operationCount: operations.length, durationMs }, "Generation completed successfully");
1561
+ return {
1562
+ operationCount: operations.length,
1563
+ durationMs,
1564
+ typesGenerated,
1565
+ operationsGenerated,
1566
+ };
1567
+ }
1568
+ catch (error) {
1569
+ // Restore from backups on failure
1570
+ logger.error({ error }, "Generation failed, restoring backups...");
1571
+ await restoreFromBackup(backups, outputPaths);
1572
+ throw error;
1573
+ }
1574
+ }
1575
+ //# sourceMappingURL=generator.js.map