@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,7 @@
1
+ import type { ApiSpec, GeneratedFile, EmitterContext } from '@workos/oagen';
2
+
3
+ export function generateTests(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
4
+ // Production emitters generate per-resource test stubs.
5
+ // This reference emitter returns an empty array to keep things minimal.
6
+ return [];
7
+ }
@@ -0,0 +1 @@
1
+ export { typescriptEmitter } from './emitter.js';
@@ -0,0 +1,21 @@
1
+ import { toCamelCase, toPascalCase } from '@workos/oagen';
2
+
3
+ /** Convert an IR model/enum name to a TypeScript interface name. */
4
+ export function tsTypeName(name: string): string {
5
+ return toPascalCase(name);
6
+ }
7
+
8
+ /** Convert an IR field name to a TypeScript property name (camelCase). */
9
+ export function tsPropertyName(name: string): string {
10
+ return toCamelCase(name);
11
+ }
12
+
13
+ /** Convert an IR operation name to a TypeScript method name (camelCase). */
14
+ export function tsMethodName(name: string): string {
15
+ return toCamelCase(name);
16
+ }
17
+
18
+ /** Convert an IR service name to a TypeScript class name. */
19
+ export function tsClassName(name: string): string {
20
+ return toPascalCase(name);
21
+ }
@@ -0,0 +1,39 @@
1
+ import { mapTypeRef } from '@workos/oagen';
2
+ import type {
3
+ TypeRef,
4
+ PrimitiveType,
5
+ ArrayType,
6
+ ModelRef,
7
+ EnumRef,
8
+ UnionType,
9
+ NullableType,
10
+ LiteralType,
11
+ MapType,
12
+ } from '@workos/oagen';
13
+ import { tsTypeName } from './naming.js';
14
+
15
+ /** Map an IR TypeRef to a TypeScript type string. */
16
+ export function toTsType(ref: TypeRef): string {
17
+ return mapTypeRef<string>(ref, {
18
+ primitive: (r: PrimitiveType) => {
19
+ switch (r.type) {
20
+ case 'string':
21
+ return 'string';
22
+ case 'integer':
23
+ case 'number':
24
+ return 'number';
25
+ case 'boolean':
26
+ return 'boolean';
27
+ case 'unknown':
28
+ return 'unknown';
29
+ }
30
+ },
31
+ array: (_r: ArrayType, items: string) => `${items}[]`,
32
+ model: (r: ModelRef) => tsTypeName(r.name),
33
+ enum: (r: EnumRef) => tsTypeName(r.name),
34
+ union: (_r: UnionType, variants: string[]) => variants.join(' | '),
35
+ nullable: (_r: NullableType, inner: string) => `${inner} | null`,
36
+ literal: (r: LiteralType) => JSON.stringify(r.value),
37
+ map: (_r: MapType, value: string) => `Record<string, ${value}>`,
38
+ });
39
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "sourceMap": true,
9
+ "outDir": "dist",
10
+ "rootDir": ".",
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true
16
+ },
17
+ "include": ["src"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@workos/oagen",
3
+ "version": "0.0.1",
4
+ "description": "Framework for building OpenAPI SDK emitters",
5
+ "author": "WorkOS",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "engines": {
11
+ "node": ">=24.10.0"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/workos/oagen.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/workos/oagen/issues"
19
+ },
20
+ "homepage": "https://github.com/workos/oagen#readme",
21
+ "keywords": [
22
+ "openapi",
23
+ "sdk",
24
+ "generator",
25
+ "codegen",
26
+ "api"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./compat": {
34
+ "types": "./dist/compat/index.d.ts",
35
+ "import": "./dist/compat/index.js"
36
+ },
37
+ "./verify": {
38
+ "types": "./dist/verify/index.d.ts",
39
+ "import": "./dist/verify/index.js"
40
+ },
41
+ "./smoke": {
42
+ "types": "./dist/smoke.d.ts",
43
+ "import": "./dist/smoke.js"
44
+ }
45
+ },
46
+ "bin": {
47
+ "oagen": "dist/cli/index.js"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "skills",
52
+ ".claude-plugin",
53
+ "docs",
54
+ "examples"
55
+ ],
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsx src/cli/index.ts",
59
+ "lint": "oxlint --deny-warnings",
60
+ "lint:fix": "oxlint --fix",
61
+ "format": "oxfmt --check .",
62
+ "format:fix": "oxfmt --write .",
63
+ "format:md": "prettier --write '**/*.md'",
64
+ "lint:all": "oxlint --fix && oxfmt --write . && prettier --write '**/*.md'",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "typecheck": "tsc --noEmit",
68
+ "smoke:raw": "tsx scripts/smoke/raw.ts",
69
+ "smoke:diff": "tsx scripts/smoke/diff.ts",
70
+ "smoke": "tsx scripts/smoke/sdk-test.ts",
71
+ "smoke:baseline": "tsx scripts/smoke/baseline.ts",
72
+ "smoke:validate": "tsx scripts/smoke/validate.ts",
73
+ "smoke:loop": "echo 'Use: oagen generate && oagen verify'",
74
+ "prepare": "husky"
75
+ },
76
+ "dependencies": {
77
+ "@redocly/openapi-core": "^1.34.3",
78
+ "commander": "^13.1.0",
79
+ "tree-sitter": "^0.21.1",
80
+ "tree-sitter-go": "^0.23.4",
81
+ "tree-sitter-php": "^0.23.12",
82
+ "tree-sitter-python": "^0.21.0",
83
+ "tree-sitter-ruby": "^0.21.0",
84
+ "tree-sitter-rust": "^0.21.0",
85
+ "tree-sitter-typescript": "^0.23.2",
86
+ "tsx": "^4.19.0",
87
+ "typescript": "^5.7.0"
88
+ },
89
+ "devDependencies": {
90
+ "@commitlint/cli": "^20.5.0",
91
+ "@commitlint/config-conventional": "^20.5.0",
92
+ "@types/node": "^25.3.3",
93
+ "@vitest/coverage-v8": "^3.2.4",
94
+ "dotenv": "^17.3.1",
95
+ "husky": "^9.1.7",
96
+ "oxfmt": "^0.36.0",
97
+ "oxlint": "^1.51.0",
98
+ "prettier": "^3.8.1",
99
+ "tsup": "^8.4.0",
100
+ "vitest": "^3.0.0"
101
+ }
102
+ }
@@ -0,0 +1,393 @@
1
+ ---
2
+ name: generate-emitter
3
+ description: Scaffold a new language emitter for oagen, implementing the Emitter interface with idiomatic target-language code generation. Use this skill whenever the user wants to add a new target language, generate SDKs for a new language, add Go/Python/Kotlin/Java/etc. support, create an emitter, or asks about code generation for a language — even if they don't use the word "emitter" explicitly. Also triggers when the user mentions "add language support" or "new SDK target".
4
+ ---
5
+
6
+ # /generate-emitter
7
+
8
+ Scaffold a complete language emitter for oagen that translates the intermediate representation (IR) into idiomatic SDK code for a target language.
9
+
10
+ ## Overview
11
+
12
+ oagen has a plugin architecture for code generation. Each target language is an **emitter** — a TypeScript module that implements the `Emitter` interface. An emitter receives parsed IR nodes (models, enums, services, etc.) and returns `GeneratedFile[]` — arrays of `{ path, content }` pairs. The engine orchestrator calls each emitter method, prepends a file header, and writes the results to disk.
13
+
14
+ Emitters live in **external projects** (not inside the oagen core repo). They import all oagen types from `@workos/oagen` and register via `oagen.config.ts` in their project.
15
+
16
+ ## Reference Docs
17
+
18
+ - [Emitter Contract](../../docs/architecture/emitter-contract.md) — `Emitter` interface, `GeneratedFile` shape, overlay integration
19
+ - [IR Types](../../docs/architecture/ir-types.md) — `ApiSpec`, `TypeRef` discriminated union, `Model`, `Enum`, `Service`, `Operation`
20
+ - [Pipeline](../../docs/architecture/pipeline.md) — three-stage parse/emit/write flow
21
+
22
+ ## Resolve Emitter Project
23
+
24
+ Determine the emitter project path before doing anything:
25
+
26
+ 1. If the `project` argument was provided, use that.
27
+ 2. Otherwise, use `AskUserQuestion`: "Where is your emitter project? (absolute or relative path, e.g. `../oagen-emitters/node`)"
28
+
29
+ All generated files go into this project. Store it as `project`.
30
+
31
+ ## Scaffold Project (if needed)
32
+
33
+ If `{project}/package.json` does **NOT** exist, read `references/project-scaffold.md` and create the boilerplate files listed there. If `package.json` already exists, skip this.
34
+
35
+ ## Prerequisites
36
+
37
+ Before starting, read and understand these oagen core types (all imported from `@workos/oagen`):
38
+
39
+ - `Emitter`, `EmitterContext`, `GeneratedFile` — the emitter interface contract
40
+ - `ApiSpec`, `Model`, `Enum`, `Service`, `Operation`, `TypeRef` — the IR type system
41
+ - `planOperation`, `OperationPlan` — operation analysis helpers
42
+ - `toPascalCase`, `toSnakeCase`, `toCamelCase`, `toKebabCase`, `toUpperSnakeCase` — naming utilities
43
+
44
+ Also read the project's `oagen.config.ts` to understand how emitters are registered.
45
+
46
+ If an `sdk_path` argument is provided, you MUST thoroughly study that SDK before proceeding to Step 1. The existing SDK's actual code — not generic conventions — drives every design decision.
47
+
48
+ ## Step 1: Study Target Language Patterns
49
+
50
+ Before writing any code, establish the exact patterns the emitter will replicate.
51
+
52
+ ### Scenario A: Backwards-Compatible (`sdk_path` provided)
53
+
54
+ The existing SDK is the **sole source of truth**. Study it thoroughly before making any design decisions.
55
+
56
+ #### 1a. Explore the Existing SDK (via subagent)
57
+
58
+ **Delegate this to a subagent to keep file-reading noise out of the main context.**
59
+
60
+ Use the `Agent` tool with `subagent_type: Explore` and a prompt like:
61
+
62
+ > Explore the SDK at `{sdk_path}`. Read at least 10 representative files spanning different concerns. For each of the following pattern categories, find the real code and return: a 1-2 sentence description, a real code snippet, and the source file path.
63
+ >
64
+ > Categories: client architecture (constructor, HTTP methods, resource accessors), model/data types (field types, optionality), request/response types (separate input/output/options types?), serialization (wire/domain conversion?), resource classes (method signatures, parameter patterns, return types), pagination (iterator, page object, etc.), error handling (error hierarchy, status code mapping), testing patterns (framework, mocking, fixtures), entry point/exports (barrel exports, public surface), utilities/common (shared helpers, base classes), file/directory layout (tree structure), constructor/factory (instantiation, config patterns).
65
+ >
66
+ > Only report patterns actually found — never invent. If a category doesn't apply, say so.
67
+
68
+ The subagent reads 10+ files and returns only the structured findings — intermediate file reads stay out of context.
69
+
70
+ **Do NOT skip this step.** The entire emitter is derived from these findings.
71
+
72
+ #### File Path Hints
73
+
74
+ When generating for a live SDK, the overlay provides `fileBySymbol` — a map from
75
+ IR symbol names to the relative file paths where those symbols live in the live SDK.
76
+ Emitters can use this to produce `GeneratedFile` entries with paths that match the
77
+ live SDK's layout, enabling the merger to find and merge into the correct files.
78
+
79
+ **Usage in emitter code:**
80
+
81
+ ```typescript
82
+ generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
83
+ return models.map((model) => {
84
+ // Check overlay for live SDK path hint
85
+ const hintPath = ctx.overlayLookup?.fileBySymbol?.get(model.name);
86
+ const filePath = hintPath ?? `models/${toKebabCase(model.name)}.ts`;
87
+
88
+ return { path: filePath, content: renderModel(model) };
89
+ });
90
+ }
91
+ ```
92
+
93
+ This is opt-in. Emitters that don't check `fileBySymbol` generate into their
94
+ default layout. The hint is most valuable during `/integrate` (Phase 3), when
95
+ generated files need to merge into the live SDK at the correct locations.
96
+
97
+ #### 1b. Present Findings
98
+
99
+ After receiving the subagent's structured summary, present it to the user. For each pattern, include the pattern name, a 1–2 sentence description, a real code snippet from the SDK, and the source file path.
100
+
101
+ Ask the user to confirm the findings are complete and accurate.
102
+
103
+ ### Scenario B: Fresh (no `sdk_path`)
104
+
105
+ 1. Check if `docs/sdk-architecture/{language}.md` already exists in the emitter project. If it does, confirm with the user whether changes are needed.
106
+ 2. If no design doc exists, present the Structural Guidelines Table and ask the user to confirm or override each category: Testing Framework, HTTP Mocking, Documentation, Type Signatures, Linting/Formatting, HTTP Client, JSON Parsing, Package Manager, Build Tool.
107
+
108
+ ### 1c. Create SDK Design Document
109
+
110
+ Write the full design document to `docs/sdk-architecture/{language}.md` **in the emitter project**.
111
+
112
+ **For Scenario A:** Derive the doc entirely from the patterns extracted in Step 1a. Every code example must come from the real SDK, not be invented. The design doc is the contract between the study phase and the implementation phase.
113
+
114
+ **For Scenario B:** Use the confirmed structural guidelines plus your knowledge of the language ecosystem.
115
+
116
+ The design doc must include: architecture overview, naming conventions, type mapping table (IR TypeRef to target types), model pattern with example, enum pattern, resource/client pattern, serialization pattern (if applicable), pagination pattern, error handling, retry logic, test pattern, structural guidelines table, directory structure, and utility/common patterns.
117
+
118
+ **CRITICAL for Scenario A:** If the existing SDK has patterns NOT covered by the standard emitter scaffold (e.g., serializers, factory functions, dual type systems), those patterns MUST be documented as additional generator files.
119
+
120
+ ## Step 2: Scaffold Emitter Files
121
+
122
+ Create the following files under `src/{language}/` **in the emitter project**:
123
+
124
+ ```
125
+ {project}/src/{language}/
126
+ ├── index.ts # Emitter entry point, implements Emitter interface
127
+ ├── type-map.ts # IR TypeRef → target language type string mapping
128
+ ├── naming.ts # Target-language naming conventions
129
+ ├── models.ts # IR Model → target language model/data class
130
+ ├── enums.ts # IR Enum → target language enum
131
+ ├── resources.ts # IR Service → target language resource/client class
132
+ ├── client.ts # HTTP client with retry logic
133
+ ├── errors.ts # Error class/type hierarchy
134
+ ├── config.ts # Configuration module/class
135
+ ├── types-*.ts # Type annotation files (language-specific, may be 0-2 files)
136
+ ├── tests.ts # Test file generation
137
+ ├── fixtures.ts # Test fixture generation
138
+ └── manifest.ts # Smoke test manifest (operation → SDK method mapping)
139
+ ```
140
+
141
+ Not every language needs every file, and some languages may need **additional** files beyond this scaffold. **For Scenario A**, the design doc from Step 1c may identify additional generators needed (e.g., `serializers.ts`, `common.ts`, `factory.ts`, `request-types.ts`). The file list in the design doc is authoritative, not this generic scaffold.
142
+
143
+ Omit files that don't apply, and add language-specific utility files as needed. The `index.ts` must still implement all `Emitter` interface methods (return `[]` for inapplicable ones).
144
+
145
+ ### SDK Output Scaffolding (required)
146
+
147
+ The emitter must produce a **self-contained, usable SDK**. In addition to source files, `generateClient` (or another appropriate method) **must** emit these scaffolding files:
148
+
149
+ 1. **Entry point barrel** (e.g., `src/index.ts`) — re-exports all public types, models, enums, exceptions, and the main client class. This is what the extractor and consumers use as the SDK's public surface.
150
+ 2. **Project config** (e.g., `tsconfig.json`, `setup.py`, `Gemfile`) — whatever the target language needs so the generated SDK can be type-checked, built, or imported without manual setup.
151
+ 3. **Package manifest** (e.g., `package.json`, `*.gemspec`, `pyproject.toml`) — with correct entry points (`main`, `types`, `exports`) so tooling can discover the SDK's public surface.
152
+
153
+ Without these, `oagen verify` (compat verification) will fail because the extractor cannot find the SDK's entry point. The generated output must be analyzable by the extractor without any manual intervention.
154
+
155
+ ### Import Convention
156
+
157
+ All emitter files import oagen types from `@workos/oagen`:
158
+
159
+ ```typescript
160
+ import type { Model, TypeRef, Operation } from "@workos/oagen";
161
+ import type { EmitterContext, GeneratedFile } from "@workos/oagen";
162
+ import { planOperation, toCamelCase, toKebabCase } from "@workos/oagen";
163
+ ```
164
+
165
+ Local imports within the emitter use relative paths:
166
+
167
+ ```typescript
168
+ import { mapTypeRef } from "./type-map.js";
169
+ import { className, fileName } from "./naming.js";
170
+ ```
171
+
172
+ ## Steps 3–5: Implement Generators
173
+
174
+ Read `references/generator-guide.md` for detailed instructions on implementing each generator file: type-map.ts, naming.ts, models.ts, enums.ts, resources.ts, serializers.ts (if applicable), client.ts, errors.ts, config.ts, common.ts (if applicable), type signatures, tests.ts, and fixtures.ts.
175
+
176
+ ### Read reference implementations via subagent
177
+
178
+ Before implementing generators, use the `Agent` tool with `subagent_type: Explore` to study the reference emitter files you'll be adapting. This keeps ~20K+ tokens of reference source out of the main context.
179
+
180
+ > Read these files from the reference emitter at `{emitterProject}/src/{reference_language}/`:
181
+ > type-map.ts, naming.ts, models.ts, enums.ts, resources.ts, client.ts, errors.ts, config.ts, tests.ts, fixtures.ts
182
+ >
183
+ > For each file, return:
184
+ >
185
+ > - **Purpose:** one-line description
186
+ > - **Exports:** function/constant signatures
187
+ > - **Pattern:** structural flow (setup → transform → output)
188
+ > - **IR inputs:** which IR types (Model, Enum, Service, Operation, TypeRef) it consumes
189
+ > - **Output shape:** what GeneratedFile paths/content look like
190
+ > - **Language-specific details (do NOT replicate):** parts specific to the reference language that must be adapted
191
+ >
192
+ > Be concise — the consumer needs the pattern, not a line-by-line walkthrough.
193
+
194
+ Then, for each generator, follow this pattern:
195
+
196
+ 1. **Consult the subagent's summary** for the structural pattern of the corresponding reference file
197
+ 2. **Consult the design doc** (`docs/sdk-architecture/{language}.md`) for the exact output patterns to produce
198
+ 3. **Use `GeneratedFile[]` return type** — each function receives IR nodes + `EmitterContext` and returns file path/content pairs
199
+
200
+ **CRITICAL for Scenario A:** Each generator must produce output that matches the patterns documented in the design doc. Do NOT invent patterns that weren't found in the existing SDK.
201
+
202
+ ## Step 6: Create Entry Point (`index.ts`)
203
+
204
+ Wire everything together by implementing the `Emitter` interface:
205
+
206
+ - `generateTests` internally calls `generateFixtures` — fixtures are not a separate Emitter method
207
+ - Interface methods can compose multiple generators (e.g., Node's `generateConfig` returns `[...generateConfig(ctx), ...generateCommon(ctx)]`)
208
+ - Return `[]` for inapplicable methods
209
+
210
+ ```typescript
211
+ import type { Emitter } from '@workos/oagen';
212
+
213
+ export const {language}Emitter: Emitter = {
214
+ language: "{language}",
215
+ generateModels(models, ctx) { return generateModels(models, ctx); },
216
+ generateEnums(enums, ctx) { return generateEnums(enums, ctx); },
217
+ generateResources(services, ctx) { return generateResources(services, ctx); },
218
+ generateClient(spec, ctx) { return generateClient(spec, ctx); },
219
+ generateErrors(ctx) { return generateErrors(ctx); },
220
+ generateConfig(ctx) { return generateConfig(ctx); },
221
+ generateTypeSignatures(spec, ctx) { return generateTypeSignatures(spec, ctx); },
222
+ generateTests(spec, ctx) { return generateTests(spec, ctx); },
223
+ generateManifest(spec, ctx) { return generateManifest(spec, ctx); },
224
+ fileHeader() { return "{language-appropriate auto-generated file header}"; },
225
+ };
226
+ ```
227
+
228
+ ## Step 7: Register Emitter
229
+
230
+ Add the emitter to `oagen.config.ts` and re-export from `src/index.ts`:
231
+
232
+ ```typescript
233
+ // oagen.config.ts
234
+ import { {language}Emitter } from './src/{language}/index.js';
235
+ const config: OagenConfig = { emitters: [/* existing, */ {language}Emitter] };
236
+
237
+ // src/index.ts
238
+ export { {language}Emitter } from './{language}/index.js';
239
+ ```
240
+
241
+ ## Step 8: Create Tests
242
+
243
+ Create test files under `test/{language}/` **in the emitter project**:
244
+
245
+ ```
246
+ {project}/test/{language}/
247
+ ├── models.test.ts # Model generation tests
248
+ ├── enums.test.ts # Enum generation tests
249
+ ├── resources.test.ts # Resource generation tests
250
+ ├── client.test.ts # Client generation tests
251
+ ├── errors.test.ts # Error hierarchy tests
252
+ └── tests.test.ts # Test generation tests (meta!)
253
+ ```
254
+
255
+ For each generator, test: all type mappings, required vs optional fields, file paths and naming, content snapshots (use `toMatchInlineSnapshot()` for at least one case per generator), multiple items, and edge cases (nullable, union, nested model refs, enum refs, arrays of models).
256
+
257
+ **For Scenario A:** Include at least one "golden file" test per generator that verifies the output matches a known-good excerpt from the existing SDK.
258
+
259
+ ### Shared Test Context
260
+
261
+ ```typescript
262
+ import { describe, it, expect } from "vitest";
263
+ import type { EmitterContext, ApiSpec } from "@workos/oagen";
264
+
265
+ const emptySpec: ApiSpec = {
266
+ name: "Test",
267
+ version: "1.0.0",
268
+ baseUrl: "",
269
+ services: [],
270
+ models: [],
271
+ enums: [],
272
+ };
273
+ const ctx: EmitterContext = {
274
+ namespace: "{snake_case_namespace}",
275
+ namespacePascal: "{PascalNamespace}",
276
+ spec: emptySpec,
277
+ };
278
+ ```
279
+
280
+ ## Step 9: Validate
281
+
282
+ ```bash
283
+ # In the emitter project — all tests pass
284
+ npx vitest run
285
+
286
+ # In oagen core — type check and build
287
+ npx tsc --noEmit
288
+ npx tsup
289
+
290
+ # Smoke test — generate from an available spec fixture
291
+ npx tsx src/cli/index.ts generate \
292
+ --spec test/fixtures/{available-spec}.yml \
293
+ --lang {language} --output /tmp/test-{language}-sdk --namespace {namespace}
294
+
295
+ # Determinism — generating twice produces identical output
296
+ npx tsx src/cli/index.ts generate \
297
+ --spec test/fixtures/{available-spec}.yml \
298
+ --lang {language} --output /tmp/test-{language}-sdk-2 --namespace {namespace}
299
+ diff -r /tmp/test-{language}-sdk /tmp/test-{language}-sdk-2
300
+
301
+ # If the target language has a standard linter, run it on the generated output
302
+ ```
303
+
304
+ **For Scenario A:** Also compare generated output against the existing SDK — structure, patterns, and naming should be recognizably similar.
305
+
306
+ ## Verification Report
307
+
308
+ ```
309
+ === Emitter: {language} ===
310
+ Scenario: {A (backwards-compatible) / B (fresh)}
311
+ Files created (in {project}):
312
+ src/{language}/*.ts — {N} files
313
+ test/{language}/*.ts — {N} files
314
+ docs/sdk-architecture/{language}.md — SDK design document
315
+ Validation:
316
+ Tests / Type check / Build / Smoke test / Determinism / Linter / SDK comparison
317
+ Patterns replicated: (Scenario A only)
318
+ Model / Enum / Resource / Client / Error / Serialization / Pagination / Testing / Barrel exports
319
+ ```
320
+
321
+ ## Smoke Manifest Generation (required)
322
+
323
+ Every emitter **must** implement `generateManifest`. The smoke test runner uses the manifest to resolve SDK methods from HTTP operations — without it, the smoke test cannot find methods and most operations will be skipped.
324
+
325
+ The manifest maps every `HTTP_METHOD /path` to `{ sdkMethod, service }`:
326
+
327
+ ```json
328
+ {
329
+ "POST /organizations": {
330
+ "sdkMethod": "createOrganizations",
331
+ "service": "organizations"
332
+ },
333
+ "GET /organizations/{id}": {
334
+ "sdkMethod": "getOrganizations",
335
+ "service": "organizations"
336
+ }
337
+ }
338
+ ```
339
+
340
+ The emitter already knows every operation→method mapping (from `resolveMethodName` or the equivalent). `generateManifest` simply serializes that mapping to `smoke-manifest.json` as a `GeneratedFile`. The orchestrator calls `emitter.generateManifest?.(spec, ctx)` and writes the result alongside the SDK output.
341
+
342
+ **Implementation pattern** (`manifest.ts`):
343
+
344
+ ```typescript
345
+ export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
346
+ const manifest: Record<string, { sdkMethod: string; service: string }> = {};
347
+ for (const service of spec.services) {
348
+ const propName = /* service property name on the client */;
349
+ for (const op of service.operations) {
350
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
351
+ const methodName = /* resolved method name for this operation */;
352
+ manifest[httpKey] = { sdkMethod: methodName, service: propName };
353
+ }
354
+ }
355
+ return [{
356
+ path: "smoke-manifest.json",
357
+ content: JSON.stringify(manifest, null, 2),
358
+ }];
359
+ }
360
+ ```
361
+
362
+ If the emitter disambiguates operation names (e.g., multiple `list` operations in one service), the manifest must use the **disambiguated** names. Run disambiguation before building the manifest, or reuse the same resolved names that `generateResources` uses.
363
+
364
+ ## Overlay Integration
365
+
366
+ When `ctx.overlayLookup` is present (user passed `--api-surface`), check it before generating default names to preserve backwards compatibility:
367
+
368
+ ```typescript
369
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
370
+ const existing = ctx.overlayLookup?.methodByOperation.get(httpKey);
371
+ if (existing) {
372
+ // Use existing.methodName instead of the default generated name
373
+ }
374
+ ```
375
+
376
+ Also check `interfaceByName` and `typeAliasByName` for type names, and `requiredExports` for barrel exports. See `docs/architecture/emitter-contract.md` for the full `OverlayLookup` field reference.
377
+
378
+ ## Common Pitfalls
379
+
380
+ Read `references/common-pitfalls.md` before finalizing the emitter, and when debugging failures. Key ones: don't invent patterns (replicate), keep generators pure, namespace everywhere, don't ignore overlay, match the existing test framework exactly.
381
+
382
+ ## Output
383
+
384
+ This skill produces, in the emitter project:
385
+
386
+ - `src/{language}/*.ts` — emitter generator files implementing the `Emitter` interface
387
+ - `test/{language}/*.ts` — unit tests for each generator
388
+ - `docs/sdk-architecture/{language}.md` — SDK design document
389
+ - Updated `oagen.config.ts` with the new emitter registered
390
+
391
+ ## Backwards Compatibility
392
+
393
+ If the target language has an existing published SDK, scaffold an extractor with `/generate-extractor <language>`, then run `/verify-compat <language>` to verify the generated output preserves the existing API surface.
@@ -0,0 +1,23 @@
1
+ # Common Pitfalls
2
+
3
+ Review this checklist before finalizing the emitter, and when debugging failures.
4
+
5
+ 1. **Don't invent patterns — replicate what exists.** For backwards-compatible scenarios, every architectural decision must come from the existing SDK. If you can't find a pattern in the real code, ask the user rather than guessing. Generic "best practices" are wrong if they don't match the real SDK.
6
+
7
+ 2. **Don't forget path interpolation** — each language handles format strings differently (`%s`, `f"{id}"`, `fmt.Sprintf`, `${id}`, etc.)
8
+
9
+ 3. **Keep generators pure** — they receive IR and return strings. No file I/O, no side effects.
10
+
11
+ 4. **Handle empty inputs** — emitter methods may receive `[]` for models/enums/services. Return `[]` without errors.
12
+
13
+ 5. **Namespace everywhere** — the `ctx.namespacePascal` and `ctx.namespace` must appear in all generated code (module names, class prefixes, import paths).
14
+
15
+ 6. **Ignoring overlay** — When `ctx.overlayLookup` is provided, check it for existing method/type names before generating defaults. Skipping this causes compat verification failures.
16
+
17
+ 7. **Missing serialization layer** — If the existing SDK has serialize/deserialize functions, the emitter MUST generate them. Producing plain models without serializers will fail compat verification.
18
+
19
+ 8. **Wrong pagination type** — Each SDK has its own pagination pattern (AutoPaginatable, CursorPage, PageIterator, etc.). Use the design doc's pagination section, not a generic implementation.
20
+
21
+ 9. **Wrong test framework** — If the existing SDK uses Jest, generate Jest tests. If it uses pytest, generate pytest tests. Never substitute a different framework because it seems "better."
22
+
23
+ 10. **Acronym handling in naming** — `toPascalCase('WorkOS')` may produce `WorkOs` instead of `WorkOS`. If the SDK preserves acronym casing, create an `ensurePascal()` wrapper that only capitalizes the first letter without disturbing the rest.