@workos/oagen 0.0.1

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 (64) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +197 -0
  4. package/dist/chunk-CIX5NOBF.js +263 -0
  5. package/dist/chunk-IJ5E5AE7.js +1040 -0
  6. package/dist/chunk-P5XUDUGY.js +1748 -0
  7. package/dist/cli/index.js +5603 -0
  8. package/dist/compat/index.d.ts +195 -0
  9. package/dist/compat/index.js +4453 -0
  10. package/dist/index.d.ts +300 -0
  11. package/dist/index.js +1733 -0
  12. package/dist/scripts/smoke/baseline.js +1751 -0
  13. package/dist/scripts/smoke/sdk-test.js +93 -0
  14. package/dist/smoke.js +1963 -0
  15. package/dist/types-B5t9O9Wm.d.ts +367 -0
  16. package/dist/verify/index.d.ts +76 -0
  17. package/dist/verify/index.js +336 -0
  18. package/docs/advanced/index.md +11 -0
  19. package/docs/advanced/oagen-ignore.md +144 -0
  20. package/docs/agents/architecture.md +53 -0
  21. package/docs/agents/emitters.md +69 -0
  22. package/docs/agents/testing.md +60 -0
  23. package/docs/architecture/dependency-layers.md +75 -0
  24. package/docs/architecture/emitter-contract.md +160 -0
  25. package/docs/architecture/extractor-contract.md +227 -0
  26. package/docs/architecture/ir-types.md +275 -0
  27. package/docs/architecture/non-additive-changes.md +76 -0
  28. package/docs/architecture/pipeline.md +110 -0
  29. package/docs/architecture/workflows.md +215 -0
  30. package/docs/cli.md +156 -0
  31. package/docs/contributor/index.md +10 -0
  32. package/docs/contributor/public-api.md +59 -0
  33. package/docs/contributor/versioning.md +48 -0
  34. package/docs/core/index.md +12 -0
  35. package/docs/core/minimal-emitter.md +134 -0
  36. package/docs/core/quickstart.md +142 -0
  37. package/docs/diagrams/generate-sdk-workflow.md +50 -0
  38. package/docs/index.md +40 -0
  39. package/examples/reference-emitter/README.md +41 -0
  40. package/examples/reference-emitter/oagen.config.ts +7 -0
  41. package/examples/reference-emitter/spec/github-subset.yml +400 -0
  42. package/examples/reference-emitter/src/emitter.ts +52 -0
  43. package/examples/reference-emitter/src/generators/client.ts +37 -0
  44. package/examples/reference-emitter/src/generators/config.ts +19 -0
  45. package/examples/reference-emitter/src/generators/enums.ts +16 -0
  46. package/examples/reference-emitter/src/generators/errors.ts +39 -0
  47. package/examples/reference-emitter/src/generators/models.ts +29 -0
  48. package/examples/reference-emitter/src/generators/resources.ts +60 -0
  49. package/examples/reference-emitter/src/generators/tests.ts +7 -0
  50. package/examples/reference-emitter/src/index.ts +1 -0
  51. package/examples/reference-emitter/src/naming.ts +21 -0
  52. package/examples/reference-emitter/src/type-mapper.ts +39 -0
  53. package/examples/reference-emitter/tsconfig.json +19 -0
  54. package/package.json +102 -0
  55. package/skills/generate-emitter/SKILL.md +393 -0
  56. package/skills/generate-emitter/references/common-pitfalls.md +23 -0
  57. package/skills/generate-emitter/references/generator-guide.md +201 -0
  58. package/skills/generate-emitter/references/project-scaffold.md +138 -0
  59. package/skills/generate-extractor/SKILL.md +200 -0
  60. package/skills/generate-sdk/SKILL.md +155 -0
  61. package/skills/generate-smoke-test/SKILL.md +185 -0
  62. package/skills/integrate/SKILL.md +108 -0
  63. package/skills/verify-compat/SKILL.md +151 -0
  64. package/skills/verify-smoke-test/SKILL.md +176 -0
@@ -0,0 +1,41 @@
1
+ # Reference Emitter
2
+
3
+ A minimal but working TypeScript emitter for oagen, demonstrating the Emitter interface against a GitHub-flavored fixture spec.
4
+
5
+ ## What it covers
6
+
7
+ - `generateModels` — TypeScript interfaces from IR models
8
+ - `generateEnums` — string literal unions from IR enums
9
+ - `generateResources` — resource classes using `planOperation`
10
+ - `generateClient` — top-level client with resource accessors
11
+ - `generateErrors` — error class hierarchy
12
+ - `generateConfig` — config types and base resource class
13
+ - Type mapping via `mapTypeRef`
14
+
15
+ ## What it does NOT cover
16
+
17
+ Production emitters typically also handle:
18
+
19
+ - Overlay resolution for compat-aware naming
20
+ - Merge modes for incremental generation
21
+ - HTTP client implementation (fetch, axios, etc.)
22
+ - Pagination iterators
23
+ - Request/response serialization
24
+
25
+ ## Usage
26
+
27
+ From the oagen root:
28
+
29
+ ```bash
30
+ npx vitest run test/examples/reference-emitter/
31
+ ```
32
+
33
+ Or to generate output manually:
34
+
35
+ ```bash
36
+ npx tsx src/cli/index.ts generate \
37
+ --lang typescript \
38
+ --spec examples/reference-emitter/spec/github-subset.yml \
39
+ --namespace GitHub \
40
+ --output /tmp/github-sdk
41
+ ```
@@ -0,0 +1,7 @@
1
+ import type { OagenConfig } from '@workos/oagen';
2
+ import { typescriptEmitter } from './src/index.js';
3
+
4
+ const config: OagenConfig = {
5
+ emitters: [typescriptEmitter],
6
+ };
7
+ export default config;
@@ -0,0 +1,400 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: GitHub-Flavored Subset API
4
+ version: 1.0.0
5
+ description: >
6
+ A small GitHub-flavored OpenAPI spec for testing oagen's reference emitter.
7
+ Exercises: cursor pagination, bearer auth, models, enums, CRUD, nullable fields,
8
+ error responses, and generic envelope patterns.
9
+
10
+ servers:
11
+ - url: https://api.github-example.com/v1
12
+
13
+ security:
14
+ - bearerAuth: []
15
+
16
+ components:
17
+ securitySchemes:
18
+ bearerAuth:
19
+ type: http
20
+ scheme: bearer
21
+
22
+ schemas:
23
+ PaginationMeta:
24
+ type: object
25
+ properties:
26
+ next_cursor:
27
+ type: string
28
+ nullable: true
29
+ has_more:
30
+ type: boolean
31
+
32
+ Visibility:
33
+ type: string
34
+ enum: [public, private, internal]
35
+
36
+ Repository:
37
+ type: object
38
+ required: [id, name, full_name, visibility]
39
+ properties:
40
+ id:
41
+ type: integer
42
+ name:
43
+ type: string
44
+ full_name:
45
+ type: string
46
+ description:
47
+ type: string
48
+ nullable: true
49
+ visibility:
50
+ $ref: '#/components/schemas/Visibility'
51
+ owner:
52
+ $ref: '#/components/schemas/User'
53
+
54
+ Issue:
55
+ type: object
56
+ required: [id, title, state]
57
+ properties:
58
+ id:
59
+ type: integer
60
+ title:
61
+ type: string
62
+ body:
63
+ type: string
64
+ nullable: true
65
+ state:
66
+ type: string
67
+ enum: [open, closed]
68
+ labels:
69
+ type: array
70
+ items:
71
+ $ref: '#/components/schemas/Label'
72
+ assignee:
73
+ $ref: '#/components/schemas/User'
74
+ nullable: true
75
+
76
+ Label:
77
+ type: object
78
+ required: [id, name, color]
79
+ properties:
80
+ id:
81
+ type: integer
82
+ name:
83
+ type: string
84
+ color:
85
+ type: string
86
+ description:
87
+ type: string
88
+ nullable: true
89
+
90
+ User:
91
+ type: object
92
+ required: [id, login]
93
+ properties:
94
+ id:
95
+ type: integer
96
+ login:
97
+ type: string
98
+ name:
99
+ type: string
100
+ nullable: true
101
+ email:
102
+ type: string
103
+ nullable: true
104
+
105
+ ApiError:
106
+ type: object
107
+ required: [message]
108
+ properties:
109
+ message:
110
+ type: string
111
+ documentation_url:
112
+ type: string
113
+
114
+ paths:
115
+ /repos:
116
+ get:
117
+ operationId: listRepos
118
+ tags: [Repos]
119
+ summary: List repositories
120
+ parameters:
121
+ - name: after
122
+ in: query
123
+ schema:
124
+ type: string
125
+ - name: limit
126
+ in: query
127
+ schema:
128
+ type: integer
129
+ default: 30
130
+ - name: visibility
131
+ in: query
132
+ schema:
133
+ $ref: '#/components/schemas/Visibility'
134
+ responses:
135
+ '200':
136
+ description: A paginated list of repositories
137
+ content:
138
+ application/json:
139
+ schema:
140
+ type: object
141
+ properties:
142
+ results:
143
+ type: array
144
+ items:
145
+ $ref: '#/components/schemas/Repository'
146
+ meta:
147
+ $ref: '#/components/schemas/PaginationMeta'
148
+ '401':
149
+ description: Unauthorized
150
+ content:
151
+ application/json:
152
+ schema:
153
+ $ref: '#/components/schemas/ApiError'
154
+
155
+ /repos/{owner}/{repo}:
156
+ get:
157
+ operationId: getRepo
158
+ tags: [Repos]
159
+ summary: Get a repository
160
+ parameters:
161
+ - name: owner
162
+ in: path
163
+ required: true
164
+ schema:
165
+ type: string
166
+ - name: repo
167
+ in: path
168
+ required: true
169
+ schema:
170
+ type: string
171
+ responses:
172
+ '200':
173
+ description: Repository details
174
+ content:
175
+ application/json:
176
+ schema:
177
+ $ref: '#/components/schemas/Repository'
178
+ '404':
179
+ description: Not found
180
+ content:
181
+ application/json:
182
+ schema:
183
+ $ref: '#/components/schemas/ApiError'
184
+
185
+ /repos/{owner}/{repo}/issues:
186
+ get:
187
+ operationId: listIssues
188
+ tags: [Issues]
189
+ summary: List issues
190
+ parameters:
191
+ - name: owner
192
+ in: path
193
+ required: true
194
+ schema:
195
+ type: string
196
+ - name: repo
197
+ in: path
198
+ required: true
199
+ schema:
200
+ type: string
201
+ - name: after
202
+ in: query
203
+ schema:
204
+ type: string
205
+ - name: limit
206
+ in: query
207
+ schema:
208
+ type: integer
209
+ default: 30
210
+ - name: state
211
+ in: query
212
+ schema:
213
+ type: string
214
+ enum: [open, closed, all]
215
+ responses:
216
+ '200':
217
+ description: A paginated list of issues
218
+ content:
219
+ application/json:
220
+ schema:
221
+ type: object
222
+ properties:
223
+ results:
224
+ type: array
225
+ items:
226
+ $ref: '#/components/schemas/Issue'
227
+ meta:
228
+ $ref: '#/components/schemas/PaginationMeta'
229
+
230
+ post:
231
+ operationId: createIssue
232
+ tags: [Issues]
233
+ summary: Create an issue
234
+ parameters:
235
+ - name: owner
236
+ in: path
237
+ required: true
238
+ schema:
239
+ type: string
240
+ - name: repo
241
+ in: path
242
+ required: true
243
+ schema:
244
+ type: string
245
+ requestBody:
246
+ required: true
247
+ content:
248
+ application/json:
249
+ schema:
250
+ type: object
251
+ required: [title]
252
+ properties:
253
+ title:
254
+ type: string
255
+ body:
256
+ type: string
257
+ labels:
258
+ type: array
259
+ items:
260
+ type: string
261
+ responses:
262
+ '201':
263
+ description: Created issue
264
+ content:
265
+ application/json:
266
+ schema:
267
+ $ref: '#/components/schemas/Issue'
268
+ '422':
269
+ description: Validation error
270
+ content:
271
+ application/json:
272
+ schema:
273
+ $ref: '#/components/schemas/ApiError'
274
+
275
+ /repos/{owner}/{repo}/labels:
276
+ get:
277
+ operationId: listLabels
278
+ tags: [Labels]
279
+ summary: List labels
280
+ parameters:
281
+ - name: owner
282
+ in: path
283
+ required: true
284
+ schema:
285
+ type: string
286
+ - name: repo
287
+ in: path
288
+ required: true
289
+ schema:
290
+ type: string
291
+ - name: page
292
+ in: query
293
+ schema:
294
+ type: integer
295
+ - name: per_page
296
+ in: query
297
+ schema:
298
+ type: integer
299
+ default: 30
300
+ responses:
301
+ '200':
302
+ description: A paginated list of labels
303
+ content:
304
+ application/json:
305
+ schema:
306
+ type: object
307
+ properties:
308
+ items:
309
+ type: array
310
+ items:
311
+ $ref: '#/components/schemas/Label'
312
+ total_count:
313
+ type: integer
314
+
315
+ post:
316
+ operationId: createLabel
317
+ tags: [Labels]
318
+ summary: Create a label
319
+ parameters:
320
+ - name: owner
321
+ in: path
322
+ required: true
323
+ schema:
324
+ type: string
325
+ - name: repo
326
+ in: path
327
+ required: true
328
+ schema:
329
+ type: string
330
+ requestBody:
331
+ required: true
332
+ content:
333
+ application/json:
334
+ schema:
335
+ type: object
336
+ required: [name, color]
337
+ properties:
338
+ name:
339
+ type: string
340
+ color:
341
+ type: string
342
+ description:
343
+ type: string
344
+ responses:
345
+ '201':
346
+ description: Created label
347
+ content:
348
+ application/json:
349
+ schema:
350
+ $ref: '#/components/schemas/Label'
351
+
352
+ /repos/{owner}/{repo}/labels/{label_id}:
353
+ delete:
354
+ operationId: deleteLabel
355
+ tags: [Labels]
356
+ summary: Delete a label
357
+ parameters:
358
+ - name: owner
359
+ in: path
360
+ required: true
361
+ schema:
362
+ type: string
363
+ - name: repo
364
+ in: path
365
+ required: true
366
+ schema:
367
+ type: string
368
+ - name: label_id
369
+ in: path
370
+ required: true
371
+ schema:
372
+ type: integer
373
+ responses:
374
+ '204':
375
+ description: Deleted
376
+
377
+ /users/{username}:
378
+ get:
379
+ operationId: getUser
380
+ tags: [Users]
381
+ summary: Get a user
382
+ parameters:
383
+ - name: username
384
+ in: path
385
+ required: true
386
+ schema:
387
+ type: string
388
+ responses:
389
+ '200':
390
+ description: User details
391
+ content:
392
+ application/json:
393
+ schema:
394
+ $ref: '#/components/schemas/User'
395
+ '404':
396
+ description: Not found
397
+ content:
398
+ application/json:
399
+ schema:
400
+ $ref: '#/components/schemas/ApiError'
@@ -0,0 +1,52 @@
1
+ import type { Emitter, EmitterContext, GeneratedFile, ApiSpec, Model, Enum, Service } from '@workos/oagen';
2
+ import { generateModels } from './generators/models.js';
3
+ import { generateEnums } from './generators/enums.js';
4
+ import { generateResources } from './generators/resources.js';
5
+ import { generateClient } from './generators/client.js';
6
+ import { generateErrors } from './generators/errors.js';
7
+ import { generateConfig } from './generators/config.js';
8
+ import { generateTests } from './generators/tests.js';
9
+
10
+ /**
11
+ * Reference TypeScript emitter for oagen.
12
+ *
13
+ * This emitter demonstrates how to implement the Emitter interface by
14
+ * generating TypeScript interfaces, literal union types, resource classes,
15
+ * and a top-level client. It is intentionally simple — production emitters
16
+ * typically handle overlay resolution, compat-aware naming, and merge modes.
17
+ */
18
+ export const typescriptEmitter: Emitter = {
19
+ language: 'typescript',
20
+
21
+ generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
22
+ return generateModels(models, ctx);
23
+ },
24
+
25
+ generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
26
+ return generateEnums(enums, ctx);
27
+ },
28
+
29
+ generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
30
+ return generateResources(services, ctx);
31
+ },
32
+
33
+ generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
34
+ return generateClient(spec, ctx);
35
+ },
36
+
37
+ generateErrors(ctx: EmitterContext): GeneratedFile[] {
38
+ return generateErrors(ctx);
39
+ },
40
+
41
+ generateConfig(ctx: EmitterContext): GeneratedFile[] {
42
+ return generateConfig(ctx);
43
+ },
44
+
45
+ generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
46
+ return generateTests(spec, ctx);
47
+ },
48
+
49
+ fileHeader(): string {
50
+ return '// Auto-generated by oagen reference emitter. Do not edit.';
51
+ },
52
+ };
@@ -0,0 +1,37 @@
1
+ import type { ApiSpec, GeneratedFile, EmitterContext } from '@workos/oagen';
2
+ import { tsClassName } from '../naming.js';
3
+
4
+ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
5
+ const lines: string[] = [];
6
+ const className = `${ctx.namespacePascal}Client`;
7
+
8
+ // Import resources
9
+ for (const service of spec.services) {
10
+ const lower = service.name.toLowerCase();
11
+ lines.push(`import { ${tsClassName(service.name)} } from './resources/${lower}.js';`);
12
+ }
13
+ lines.push(`import type { ClientConfig } from './config.js';`);
14
+ lines.push('');
15
+
16
+ lines.push(`export class ${className} {`);
17
+ lines.push(` private readonly config: ClientConfig;`);
18
+ lines.push('');
19
+
20
+ // Resource accessors
21
+ for (const service of spec.services) {
22
+ const propName = service.name.charAt(0).toLowerCase() + service.name.slice(1);
23
+ lines.push(` readonly ${propName}: ${tsClassName(service.name)};`);
24
+ }
25
+ lines.push('');
26
+
27
+ lines.push(` constructor(config: ClientConfig) {`);
28
+ lines.push(` this.config = config;`);
29
+ for (const service of spec.services) {
30
+ const propName = service.name.charAt(0).toLowerCase() + service.name.slice(1);
31
+ lines.push(` this.${propName} = new ${tsClassName(service.name)}(this.config);`);
32
+ }
33
+ lines.push(` }`);
34
+ lines.push('}');
35
+
36
+ return [{ path: 'client.ts', content: lines.join('\n') }];
37
+ }
@@ -0,0 +1,19 @@
1
+ import type { GeneratedFile, EmitterContext } from '@workos/oagen';
2
+
3
+ export function generateConfig(_ctx: EmitterContext): GeneratedFile[] {
4
+ const content = `export interface ClientConfig {
5
+ apiKey: string;
6
+ baseUrl?: string;
7
+ }
8
+
9
+ export abstract class BaseResource {
10
+ protected readonly config: ClientConfig;
11
+
12
+ constructor(config: ClientConfig) {
13
+ this.config = config;
14
+ }
15
+ }
16
+ `;
17
+
18
+ return [{ path: 'config.ts', content }];
19
+ }
@@ -0,0 +1,16 @@
1
+ import type { Enum, GeneratedFile, EmitterContext } from '@workos/oagen';
2
+ import { tsTypeName } from '../naming.js';
3
+
4
+ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFile[] {
5
+ if (enums.length === 0) return [];
6
+
7
+ const lines: string[] = [];
8
+
9
+ for (const e of enums) {
10
+ const values = e.values.map((v) => JSON.stringify(v.value)).join(' | ');
11
+ lines.push(`export type ${tsTypeName(e.name)} = ${values};`);
12
+ lines.push('');
13
+ }
14
+
15
+ return [{ path: 'enums.ts', content: lines.join('\n') }];
16
+ }
@@ -0,0 +1,39 @@
1
+ import type { GeneratedFile, EmitterContext } from '@workos/oagen';
2
+
3
+ export function generateErrors(_ctx: EmitterContext): GeneratedFile[] {
4
+ const content = `export class ApiError extends Error {
5
+ readonly statusCode: number;
6
+ readonly body: unknown;
7
+
8
+ constructor(message: string, statusCode: number, body?: unknown) {
9
+ super(message);
10
+ this.name = 'ApiError';
11
+ this.statusCode = statusCode;
12
+ this.body = body;
13
+ }
14
+ }
15
+
16
+ export class NotFoundError extends ApiError {
17
+ constructor(message: string, body?: unknown) {
18
+ super(message, 404, body);
19
+ this.name = 'NotFoundError';
20
+ }
21
+ }
22
+
23
+ export class UnauthorizedError extends ApiError {
24
+ constructor(message: string, body?: unknown) {
25
+ super(message, 401, body);
26
+ this.name = 'UnauthorizedError';
27
+ }
28
+ }
29
+
30
+ export class UnprocessableEntityError extends ApiError {
31
+ constructor(message: string, body?: unknown) {
32
+ super(message, 422, body);
33
+ this.name = 'UnprocessableEntityError';
34
+ }
35
+ }
36
+ `;
37
+
38
+ return [{ path: 'errors.ts', content }];
39
+ }
@@ -0,0 +1,29 @@
1
+ import type { Model, GeneratedFile, EmitterContext } from '@workos/oagen';
2
+ import { tsTypeName, tsPropertyName } from '../naming.js';
3
+ import { toTsType } from '../type-mapper.js';
4
+
5
+ export function generateModels(models: Model[], _ctx: EmitterContext): GeneratedFile[] {
6
+ if (models.length === 0) return [];
7
+
8
+ const lines: string[] = [];
9
+
10
+ for (const model of models) {
11
+ if (model.description) {
12
+ lines.push(`/** ${model.description} */`);
13
+ }
14
+ lines.push(`export interface ${tsTypeName(model.name)} {`);
15
+ for (const field of model.fields) {
16
+ const tsName = tsPropertyName(field.name);
17
+ const tsType = toTsType(field.type);
18
+ const optional = field.required ? '' : '?';
19
+ if (field.description) {
20
+ lines.push(` /** ${field.description} */`);
21
+ }
22
+ lines.push(` ${tsName}${optional}: ${tsType};`);
23
+ }
24
+ lines.push('}');
25
+ lines.push('');
26
+ }
27
+
28
+ return [{ path: 'models.ts', content: lines.join('\n') }];
29
+ }
@@ -0,0 +1,60 @@
1
+ import type { Service, GeneratedFile, EmitterContext } from '@workos/oagen';
2
+ import { planOperation } from '@workos/oagen';
3
+ import { tsClassName, tsMethodName } from '../naming.js';
4
+ import { toTsType } from '../type-mapper.js';
5
+
6
+ export function generateResources(services: Service[], _ctx: EmitterContext): GeneratedFile[] {
7
+ const files: GeneratedFile[] = [];
8
+
9
+ for (const service of services) {
10
+ const lines: string[] = [];
11
+ const className = tsClassName(service.name);
12
+
13
+ lines.push(`import { BaseResource } from './config.js';`);
14
+ lines.push('');
15
+ lines.push(`export class ${className} extends BaseResource {`);
16
+
17
+ for (const op of service.operations) {
18
+ const plan = planOperation(op);
19
+ const method = tsMethodName(op.name);
20
+ const returnType = plan.isDelete ? 'void' : toTsType(op.response);
21
+ const params: string[] = [];
22
+
23
+ for (const p of op.pathParams) {
24
+ params.push(`${tsMethodName(p.name)}: ${toTsType(p.type)}`);
25
+ }
26
+
27
+ if (op.requestBody) {
28
+ params.push(`body: ${toTsType(op.requestBody)}`);
29
+ }
30
+
31
+ if (op.queryParams.length > 0) {
32
+ const queryFields = op.queryParams
33
+ .map((p) => `${tsMethodName(p.name)}${p.required ? '' : '?'}: ${toTsType(p.type)}`)
34
+ .join('; ');
35
+ params.push(`options?: { ${queryFields} }`);
36
+ }
37
+
38
+ const paramStr = params.join(', ');
39
+ const asyncPrefix = plan.isAsync ? 'async ' : '';
40
+
41
+ if (op.description) {
42
+ lines.push(` /** ${op.description} */`);
43
+ }
44
+ lines.push(` ${asyncPrefix}${method}(${paramStr}): Promise<${returnType}> {`);
45
+ lines.push(` // Production emitters generate real HTTP calls here`);
46
+ lines.push(` throw new Error('Not implemented');`);
47
+ lines.push(` }`);
48
+ lines.push('');
49
+ }
50
+
51
+ lines.push('}');
52
+
53
+ files.push({
54
+ path: `resources/${service.name.toLowerCase()}.ts`,
55
+ content: lines.join('\n'),
56
+ });
57
+ }
58
+
59
+ return files;
60
+ }