@workos/oagen 0.22.6 → 0.23.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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.mjs +271 -42
  3. package/dist/cli/index.mjs.map +1 -1
  4. package/dist/compat/index.d.mts +2 -2
  5. package/dist/compat/index.mjs +9 -8
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-CQzcGuFJ.mjs → diff-B7dMUCoJ.mjs} +235 -27
  8. package/dist/diff-B7dMUCoJ.mjs.map +1 -0
  9. package/dist/{differ-CC65Efzt.d.mts → differ-DMmw0tbA.d.mts} +5 -5
  10. package/dist/{differ-CC65Efzt.d.mts.map → differ-DMmw0tbA.d.mts.map} +1 -1
  11. package/dist/index.d.mts +64 -9
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +26 -11
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{overlay-cb32jDBS.mjs → overlay-sQHQoN0I.mjs} +4 -4
  16. package/dist/overlay-sQHQoN0I.mjs.map +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +25 -10
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  19. package/dist/scripts/smoke/sdk-test.mjs +3 -3
  20. package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
  21. package/dist/smoke.d.mts +19 -4
  22. package/dist/smoke.d.mts.map +1 -1
  23. package/dist/smoke.mjs +25 -10
  24. package/dist/smoke.mjs.map +1 -1
  25. package/dist/tree-sitter-DcUvm0KA.mjs.map +1 -1
  26. package/dist/{types-Cjh39AGQ.d.mts → types-nM5Kp7UP.d.mts} +16 -9
  27. package/dist/types-nM5Kp7UP.d.mts.map +1 -0
  28. package/dist/verify/index.d.mts +2 -2
  29. package/dist/verify/index.mjs +2 -2
  30. package/docs/architecture/extractor-contract.md +2 -2
  31. package/docs/architecture/pipeline.md +2 -2
  32. package/docs/cli.md +19 -19
  33. package/docs/core/compatibility-policy.md +1 -1
  34. package/docs/core/manifest-schema.md +2 -2
  35. package/package.json +1 -1
  36. package/skills/generate-emitter/SKILL.md +2 -2
  37. package/skills/generate-emitter/references/common-pitfalls.md +1 -1
  38. package/skills/generate-emitter/references/project-scaffold.md +3 -3
  39. package/skills/generate-extractor/SKILL.md +2 -2
  40. package/skills/generate-smoke-test/SKILL.md +1 -1
  41. package/skills/verify-smoke-test/SKILL.md +1 -1
  42. package/dist/diff-CQzcGuFJ.mjs.map +0 -1
  43. package/dist/overlay-cb32jDBS.mjs.map +0 -1
  44. package/dist/types-Cjh39AGQ.d.mts.map +0 -1
package/README.md CHANGED
@@ -165,7 +165,7 @@ export default {
165
165
  retry: { backoff: { initialDelay: 0.5, maxDelay: 8.0 } },
166
166
  timeout: {
167
167
  defaultTimeoutSeconds: 30,
168
- timeoutEnvVar: "WORKOS_REQUEST_TIMEOUT",
168
+ timeoutEnvVar: "SDK_REQUEST_TIMEOUT",
169
169
  },
170
170
  pagination: { autoPageDelayMs: 0 },
171
171
  },
@@ -14,9 +14,8 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rea
14
14
  import { createHash } from "node:crypto";
15
15
  //#region src/ir/sdk-behavior.ts
16
16
  /**
17
- * Canonical SDK behavior defaults. These match the PHP emitter's current
18
- * implementation (the most complete). Per-language overrides can be applied
19
- * via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
17
+ * Canonical SDK behavior defaults. Per-language or per-project overrides can
18
+ * be applied via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
20
19
  *
21
20
  * - Retry: exponential backoff with jitter, retries on 429/5xx
22
21
  * - Errors: standard HTTP status → exception kind mapping
@@ -59,12 +58,11 @@ function defaultSdkBehavior() {
59
58
  429: "RateLimitExceeded"
60
59
  },
61
60
  serverErrorKind: "Server",
62
- clientErrorKind: "Api",
63
- errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
61
+ clientErrorKind: "Api"
64
62
  },
65
63
  telemetry: {
66
64
  enabledByDefault: true,
67
- headerName: "X-WorkOS-Client-Telemetry",
65
+ headerName: "X-Client-Telemetry",
68
66
  requestIdHeader: "X-Request-ID"
69
67
  },
70
68
  pagination: { autoPageDelayMs: 0 },
@@ -2044,11 +2042,11 @@ function inferVerb(method, path) {
2044
2042
  * Uses the last non-param segment (the resource being operated on).
2045
2043
  *
2046
2044
  * Examples:
2047
- * /user_management/users → "Users"
2048
- * /user_management/users/{id} → "User" (singular for item operations)
2049
- * /user_management/users/{id}/auth_factors → "AuthFactors"
2045
+ * /accounts/users → "Users"
2046
+ * /accounts/users/{id} → "User" (singular for item operations)
2047
+ * /accounts/users/{id}/auth_factors → "AuthFactors"
2050
2048
  * /organizations/{id}/api_keys → "ApiKeys"
2051
- * /sso/authorize → "Authorize" (action endpoint)
2049
+ * /auth/authorize → "Authorize" (action endpoint)
2052
2050
  */
2053
2051
  function inferResourceFromPath(path) {
2054
2052
  const segments = path.split("/").filter(Boolean);
@@ -2694,6 +2692,7 @@ async function parseSpec(specPath, options) {
2694
2692
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
2695
2693
  clearSchemaNameTransform();
2696
2694
  const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
2695
+ applyFieldHints(finalModels, options?.fieldHints);
2697
2696
  collectInlineEnumsFromModels(finalModels, enums);
2698
2697
  collectInlineEnumsFromOperations(services, enums);
2699
2698
  const auth = extractAuthSchemes(spec.components?.securitySchemes);
@@ -2716,6 +2715,22 @@ async function parseSpec(specPath, options) {
2716
2715
  validateModelRefs(result);
2717
2716
  return result;
2718
2717
  }
2718
+ /**
2719
+ * Apply `fieldHints` onto the built models, setting `Field.domainName` for each
2720
+ * matching (model name, wire field name) pair. Serialization still uses
2721
+ * `Field.name`; only the domain-facing identifier changes.
2722
+ */
2723
+ function applyFieldHints(models, fieldHints) {
2724
+ if (!fieldHints) return;
2725
+ for (const model of models) {
2726
+ const hints = fieldHints[model.name];
2727
+ if (!hints) continue;
2728
+ for (const field of model.fields) {
2729
+ const domainName = hints[field.name];
2730
+ if (domainName) field.domainName = domainName;
2731
+ }
2732
+ }
2733
+ }
2719
2734
  /** Extract authentication schemes from OpenAPI securitySchemes. */
2720
2735
  function extractAuthSchemes(securitySchemes) {
2721
2736
  if (!securitySchemes) return void 0;
@@ -3313,12 +3328,12 @@ function extractKotlinClassMembers(classBody, source) {
3313
3328
  }
3314
3329
  /**
3315
3330
  * KDoc signature that identifies a generator-emitted service accessor inside
3316
- * the WorkOS class. The Kotlin client emitter writes every accessor with this
3331
+ * a Kotlin client class. The Kotlin client emitter writes every accessor with this
3317
3332
  * exact doc preamble — when the existing file carries a member with this doc
3318
3333
  * but the regenerated content omits it, the member is stale (its mount target
3319
3334
  * was remapped or removed) and gets pruned.
3320
3335
  */
3321
- const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[WorkOS\] client\.[\s\S]*?\*\//;
3336
+ const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[[^\]]+\] client\.[\s\S]*?\*\//;
3322
3337
  const kotlinMergeAdapter = {
3323
3338
  language: "kotlin",
3324
3339
  grammarModule: "tree-sitter-kotlin",
@@ -3571,7 +3586,6 @@ const nodeMergeAdapter = {
3571
3586
  const newText = newMembers.map((m) => m.text).join("\n");
3572
3587
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
3573
3588
  const propName = match[1];
3574
- if (propName === "workos") continue;
3575
3589
  if (propName && !existingMemberKeys.has(propName)) return true;
3576
3590
  }
3577
3591
  return false;
@@ -5272,6 +5286,60 @@ function collectTsImports(lines) {
5272
5286
  return out;
5273
5287
  }
5274
5288
  /**
5289
+ * Remove the given symbols from generated `import type` bindings, in place.
5290
+ *
5291
+ * Called when an existing *value* import for the same symbol is being spliced
5292
+ * back into the file. A value import satisfies both runtime and type uses, so
5293
+ * leaving the generated `import type { X }` would re-declare `X` (TS2300
5294
+ * duplicate identifier). Callers guarantee every symbol here is absent from
5295
+ * generated *value* imports, so each occurrence is type-only and safe to drop.
5296
+ *
5297
+ * Handles whole-line `import type { … }` and inline `import { …, type X }`,
5298
+ * single- or multi-line. A statement whose bindings all get stripped is
5299
+ * removed entirely.
5300
+ */
5301
+ function dropGeneratedTypeOnlyBindings(resultLines, symbols) {
5302
+ if (symbols.size === 0) return;
5303
+ const out = [];
5304
+ for (let i = 0; i < resultLines.length; i++) {
5305
+ const line = resultLines[i];
5306
+ const trimmed = line.trim();
5307
+ if (!/^import\b/.test(trimmed) || !trimmed.includes("{")) {
5308
+ out.push(line);
5309
+ continue;
5310
+ }
5311
+ const span = [line];
5312
+ while (!span.join(" ").includes("}") && i + 1 < resultLines.length) {
5313
+ i++;
5314
+ span.push(resultLines[i]);
5315
+ }
5316
+ const m = span.join(" ").replace(/\s+/g, " ").trim().replace(/;\s*$/, "").match(/^import\s+(type\s+)?(?:(\w+)\s*,\s*)?\{\s*([^}]*)\s*\}\s+from\s+(['"][^'"]+['"])$/);
5317
+ if (!m) {
5318
+ for (const s of span) out.push(s);
5319
+ continue;
5320
+ }
5321
+ const isTypeOnly = !!m[1];
5322
+ const defaultName = m[2];
5323
+ const items = m[3].split(",").map((s) => s.trim()).filter(Boolean);
5324
+ const modQuoted = m[4];
5325
+ const kept = items.filter((raw) => {
5326
+ const isTypeBinding = isTypeOnly || raw.startsWith("type ");
5327
+ const bare = raw.replace(/^type\s+/, "");
5328
+ return !(isTypeBinding && symbols.has(bare));
5329
+ });
5330
+ if (kept.length === items.length) {
5331
+ for (const s of span) out.push(s);
5332
+ continue;
5333
+ }
5334
+ if (kept.length === 0 && !defaultName) continue;
5335
+ const lead = line.match(/^(\s*)/)?.[1] ?? "";
5336
+ const prefix = isTypeOnly ? "import type " : "import ";
5337
+ const defaultPart = defaultName ? `${defaultName}, ` : "";
5338
+ out.push(`${lead}${prefix}${defaultPart}{ ${kept.join(", ")} } from ${modQuoted};`);
5339
+ }
5340
+ resultLines.splice(0, resultLines.length, ...out);
5341
+ }
5342
+ /**
5275
5343
  * Add imports from the existing file that are absent from the generated
5276
5344
  * result. Handles:
5277
5345
  *
@@ -5393,28 +5461,49 @@ function spliceExtraImports(existingLines, resultLines) {
5393
5461
  const existingTs = collectTsImports(existingOutsideIgnore);
5394
5462
  const generatedTs = collectTsImports(resultLines);
5395
5463
  const extraTsLines = [];
5464
+ const allGeneratedTsIds = /* @__PURE__ */ new Set();
5465
+ const generatedValueIds = /* @__PURE__ */ new Set();
5466
+ for (const ids of generatedTs.named.values()) for (const id of ids) {
5467
+ allGeneratedTsIds.add(id);
5468
+ generatedValueIds.add(id);
5469
+ }
5470
+ for (const ids of generatedTs.typeOnly.values()) for (const id of ids) allGeneratedTsIds.add(id);
5471
+ for (const name of generatedTs.defaults.values()) {
5472
+ allGeneratedTsIds.add(name);
5473
+ generatedValueIds.add(name);
5474
+ }
5475
+ for (const name of generatedTs.namespaces.values()) {
5476
+ allGeneratedTsIds.add(name);
5477
+ generatedValueIds.add(name);
5478
+ }
5479
+ const reclaimedValueIds = /* @__PURE__ */ new Set();
5480
+ const generatedHasValue = (id) => generatedValueIds.has(id);
5481
+ const generatedHasTypeOnly = (id) => allGeneratedTsIds.has(id) && !generatedValueIds.has(id);
5396
5482
  for (const mod of existingTs.sideEffects) if (!generatedTs.sideEffects.has(mod)) extraTsLines.push(`import '${mod}';`);
5397
5483
  for (const [mod, name] of existingTs.defaults) {
5398
5484
  if (generatedTs.defaults.has(mod)) continue;
5399
5485
  if (!identifierUsed(ignoreRegionText, name)) continue;
5486
+ if (generatedHasValue(name)) continue;
5487
+ if (generatedHasTypeOnly(name)) reclaimedValueIds.add(name);
5400
5488
  extraTsLines.push(`import ${name} from '${mod}';`);
5401
5489
  }
5402
5490
  for (const [mod, name] of existingTs.namespaces) {
5403
5491
  if (!identifierUsed(ignoreRegionText, name)) continue;
5404
- if (!generatedTs.namespaces.has(mod)) extraTsLines.push(`import * as ${name} from '${mod}';`);
5492
+ if (generatedHasValue(name)) continue;
5493
+ if (generatedTs.namespaces.has(mod)) continue;
5494
+ if (generatedHasTypeOnly(name)) reclaimedValueIds.add(name);
5495
+ extraTsLines.push(`import * as ${name} from '${mod}';`);
5405
5496
  }
5406
5497
  for (const [mod, existIds] of existingTs.named) {
5407
- const genIds = generatedTs.named.get(mod) ?? /* @__PURE__ */ new Set();
5408
- const genTypeIds = generatedTs.typeOnly.get(mod) ?? /* @__PURE__ */ new Set();
5409
- const extra = [...existIds].filter((id) => !genIds.has(id) && !genTypeIds.has(id) && identifierUsed(ignoreRegionText, id));
5498
+ const extra = [...existIds].filter((id) => !generatedHasValue(id) && identifierUsed(ignoreRegionText, id));
5499
+ for (const id of extra) if (generatedHasTypeOnly(id)) reclaimedValueIds.add(id);
5410
5500
  if (extra.length > 0) extraTsLines.push(`import { ${extra.join(", ")} } from '${mod}';`);
5411
5501
  }
5412
5502
  for (const [mod, existIds] of existingTs.typeOnly) {
5413
- const genIds = generatedTs.typeOnly.get(mod) ?? /* @__PURE__ */ new Set();
5414
- const genNamedIds = generatedTs.named.get(mod) ?? /* @__PURE__ */ new Set();
5415
- const extra = [...existIds].filter((id) => !genIds.has(id) && !genNamedIds.has(id) && identifierUsed(ignoreRegionText, id));
5503
+ const extra = [...existIds].filter((id) => !allGeneratedTsIds.has(id) && identifierUsed(ignoreRegionText, id));
5416
5504
  if (extra.length > 0) extraTsLines.push(`import type { ${extra.join(", ")} } from '${mod}';`);
5417
5505
  }
5506
+ dropGeneratedTypeOnlyBindings(resultLines, reclaimedValueIds);
5418
5507
  const isPhpUseImport = (l) => /^use\s+[\w\\]+\\[\w\\]*(\s+as\s+\w+)?\s*;?$/.test(l);
5419
5508
  const existingPhpUse = new Set(existingOutsideIgnore.filter(isPhpUseImport).map((l) => l.trim()));
5420
5509
  const generatedPhpUse = new Set(resultLines.filter(isPhpUseImport).map((l) => l.trim()));
@@ -5458,7 +5547,31 @@ function spliceExtraImports(existingLines, resultLines) {
5458
5547
  if ((resultLines[i].match(/[)}]/g) ?? []).length > 0) break;
5459
5548
  }
5460
5549
  }
5461
- if (lastImportIdx >= 0) resultLines.splice(lastImportIdx + 1, 0, ...allExtra);
5550
+ if (lastImportIdx >= 0) {
5551
+ resultLines.splice(lastImportIdx + 1, 0, ...allExtra);
5552
+ return;
5553
+ }
5554
+ let insertAt = 0;
5555
+ let inBlockComment = false;
5556
+ for (let i = 0; i < resultLines.length; i++) {
5557
+ const t = resultLines[i].trim();
5558
+ if (inBlockComment) {
5559
+ insertAt = i + 1;
5560
+ if (t.includes("*/")) inBlockComment = false;
5561
+ continue;
5562
+ }
5563
+ if (t.startsWith("//")) {
5564
+ insertAt = i + 1;
5565
+ continue;
5566
+ }
5567
+ if (t.startsWith("/*")) {
5568
+ insertAt = i + 1;
5569
+ inBlockComment = !t.includes("*/");
5570
+ continue;
5571
+ }
5572
+ break;
5573
+ }
5574
+ resultLines.splice(insertAt, 0, ...allExtra);
5462
5575
  }
5463
5576
  //#endregion
5464
5577
  //#region src/ir/operation-hints.ts
@@ -5603,6 +5716,15 @@ function resolveResponseModelName(op) {
5603
5716
  //#endregion
5604
5717
  //#region src/engine/generate-files.ts
5605
5718
  function buildEmitterContext(spec, options) {
5719
+ const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
5720
+ let scopedModelNames;
5721
+ let scopedEnumNames;
5722
+ if (options.scopedServices && options.scopedServices.size > 0) {
5723
+ const selected = options.scopedServices;
5724
+ const reachable = collectReferencedNames(spec.services.filter((s) => resolvedOperations.some((r) => r.service.name === s.name && selected.has(r.mountOn))), spec.models, { preserveAllDiscriminated: false });
5725
+ scopedModelNames = reachable.models;
5726
+ scopedEnumNames = reachable.enums;
5727
+ }
5606
5728
  return {
5607
5729
  namespace: toSnakeCase(options.namespace),
5608
5730
  namespacePascal: options.namespace,
@@ -5610,11 +5732,14 @@ function buildEmitterContext(spec, options) {
5610
5732
  outputDir: options.outputDir,
5611
5733
  apiSurface: options.apiSurface,
5612
5734
  overlayLookup: options.overlayLookup,
5613
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
5735
+ resolvedOperations,
5614
5736
  modelHints: options.modelHints,
5615
5737
  emitterOptions: options.emitterOptions,
5616
5738
  targetDir: options.target,
5617
- priorTargetManifestPaths: options.priorTargetManifestPaths
5739
+ priorTargetManifestPaths: options.priorTargetManifestPaths,
5740
+ scopedServices: options.scopedServices,
5741
+ scopedModelNames,
5742
+ scopedEnumNames
5618
5743
  };
5619
5744
  }
5620
5745
  /**
@@ -5622,7 +5747,8 @@ function buildEmitterContext(spec, options) {
5622
5747
  * Walks operation parameters, request bodies, responses, and pagination item
5623
5748
  * types, then chases model field references until the set stabilizes.
5624
5749
  */
5625
- function collectReferencedNames(services, models) {
5750
+ function collectReferencedNames(services, models, opts = {}) {
5751
+ const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
5626
5752
  const referencedModels = /* @__PURE__ */ new Set();
5627
5753
  const referencedEnums = /* @__PURE__ */ new Set();
5628
5754
  const collectFromTypeRef = (ref) => {
@@ -5644,7 +5770,7 @@ function collectReferencedNames(services, models) {
5644
5770
  for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
5645
5771
  if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
5646
5772
  }
5647
- for (const model of models) {
5773
+ if (preserveAllDiscriminated) for (const model of models) {
5648
5774
  if (isDiscriminatedModel(model)) referencedModels.add(model.name);
5649
5775
  const disc = model.discriminator;
5650
5776
  if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
@@ -5658,6 +5784,13 @@ function collectReferencedNames(services, models) {
5658
5784
  visited.add(name);
5659
5785
  const model = modelsByName.get(name);
5660
5786
  if (!model) continue;
5787
+ const disc = model.discriminator;
5788
+ if (disc?.mapping) {
5789
+ for (const variantName of Object.values(disc.mapping)) if (!referencedModels.has(variantName)) {
5790
+ referencedModels.add(variantName);
5791
+ queue.push(variantName);
5792
+ }
5793
+ }
5661
5794
  for (const field of model.fields) {
5662
5795
  collectFromTypeRef(field.type);
5663
5796
  for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
@@ -5671,7 +5804,16 @@ function collectReferencedNames(services, models) {
5671
5804
  function isDiscriminatedModel(model) {
5672
5805
  return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
5673
5806
  }
5674
- /** Collect all generated files from an emitter (no headers, no path prefixes). */
5807
+ /**
5808
+ * Collect all generated files from an emitter (no headers, no path prefixes).
5809
+ *
5810
+ * In a scoped (`--services`) run the spec is NOT filtered: every generator runs
5811
+ * over the full spec so model placement, dedup, the root client, and all
5812
+ * aggregate/barrel files are byte-identical to a full run (and a brand-new
5813
+ * selected service is wired into the client automatically). The emitters
5814
+ * themselves consult `ctx.scopedServices` to emit only the selected services'
5815
+ * per-service resource/test files.
5816
+ */
5675
5817
  function generateAllFiles(spec, emitter, ctx) {
5676
5818
  const referenced = collectReferencedNames(spec.services, spec.models);
5677
5819
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
@@ -5712,6 +5854,47 @@ function generateFiles(spec, emitter, options) {
5712
5854
  };
5713
5855
  }
5714
5856
  //#endregion
5857
+ //#region src/engine/scoped-services.ts
5858
+ /**
5859
+ * Validate a `--services` selection and return it as a set of POST-MOUNT service
5860
+ * names for emitters to scope on.
5861
+ *
5862
+ * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
5863
+ * shared-schema decisions are global computations over the full service/model set
5864
+ * (filtering them churns shared `common/` files). Instead the full spec flows to
5865
+ * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
5866
+ * emitter gates only its per-service resource/test emission on it.
5867
+ *
5868
+ * Matching is on the POST-MOUNT name. Mount resolution never mutates
5869
+ * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
5870
+ * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
5871
+ * post-mount name of a source service is derived here as
5872
+ * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
5873
+ * `DirectorySync`) is valid because every source tag that mounts into it
5874
+ * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
5875
+ * emitters group resources by mount, so all siblings emit together.
5876
+ *
5877
+ * @param spec - The parsed API spec (never mutated; never filtered).
5878
+ * @param selected - Post-mount service names from `--services` (normalized/trimmed).
5879
+ * @param mountRules - Source-service-name → post-mount-target mappings.
5880
+ * @returns The validated selection as a `Set` of post-mount names.
5881
+ * @throws A `ConfigError` listing valid post-mount names when a selection is
5882
+ * unknown — scoped generation never silently emits the wrong set.
5883
+ */
5884
+ function resolveScopedServices(spec, selected, mountRules) {
5885
+ const mounts = mountRules ?? {};
5886
+ const postMountName = (name) => mounts[name] ?? name;
5887
+ const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
5888
+ const selectedSet = new Set(selected);
5889
+ const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
5890
+ if (unknown.length > 0) {
5891
+ const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
5892
+ err.name = "ConfigError";
5893
+ throw err;
5894
+ }
5895
+ return selectedSet;
5896
+ }
5897
+ //#endregion
5715
5898
  //#region src/engine/integrate.ts
5716
5899
  /**
5717
5900
  * Resolve a relative import path against a source file path to get the
@@ -5828,7 +6011,7 @@ function mapFilesForTargetIntegration(files, language) {
5828
6011
  * that would shadow an existing module file (e.g., `api_keys.py`), move the
5829
6012
  * module file's content into `_compat.py` inside the package and add a
5830
6013
  * re-export from `__init__.py`. This preserves backward-compatible imports
5831
- * (e.g., `from workos.api_keys import ApiKeysModule`) while the package directory
6014
+ * (e.g., `from acme.api_keys import ApiKeysModule`) while the package directory
5832
6015
  * provides the generated models and resource classes.
5833
6016
  */
5834
6017
  async function resolveFileToDirectoryConflicts(files, targetDir) {
@@ -5994,6 +6177,35 @@ function computeStalePaths(prev, currentPaths) {
5994
6177
  return prev.files.filter((p) => !current.has(p)).sort();
5995
6178
  }
5996
6179
  /**
6180
+ * Merge a scoped run's emitted records into the prior manifest's records.
6181
+ *
6182
+ * A scoped (`--services`) run only emits files for the selected services, but it
6183
+ * must NOT discard the prior manifest's record of unselected services' files
6184
+ * records. The result unions prior + scoped file paths and shallow-merges the
6185
+ * operations maps, with scoped operations overriding prior entries for the same
6186
+ * "METHOD /path" key (a re-emission updates that operation's record).
6187
+ *
6188
+ * When `prev` is null (the first-ever run is scoped) there is nothing to
6189
+ * preserve, so the scoped subset is returned unchanged — matching the
6190
+ * first-adoption behavior of a full run.
6191
+ */
6192
+ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
6193
+ const scopedFileList = [...scopedFiles];
6194
+ if (!prev) return {
6195
+ files: scopedFileList,
6196
+ operations: scopedOperations
6197
+ };
6198
+ const files = [...new Set([...prev.files, ...scopedFileList])].sort();
6199
+ const priorOps = prev.operations;
6200
+ return {
6201
+ files,
6202
+ operations: priorOps || scopedOperations ? {
6203
+ ...priorOps,
6204
+ ...scopedOperations
6205
+ } : void 0
6206
+ };
6207
+ }
6208
+ /**
5997
6209
  * Delete stale files from `dir`.
5998
6210
  *
5999
6211
  * When `header` is provided, a file is only deleted if its contents start with
@@ -6062,12 +6274,17 @@ async function removeEmptyParents(startDir, stopDir) {
6062
6274
  //#endregion
6063
6275
  //#region src/engine/orchestrator.ts
6064
6276
  async function generate(spec, emitter, options) {
6065
- const outputPrevManifestForCtx = options.noPrune ? null : await readManifest(options.outputDir);
6066
- const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
6277
+ const scopedServices = options.services && options.services.length > 0 ? resolveScopedServices(spec, options.services, options.mountRules) : void 0;
6278
+ const scoped = scopedServices !== void 0;
6279
+ const noPrune = options.noPrune === true || scoped;
6280
+ const skipManifestRead = options.noPrune === true && !scoped;
6281
+ const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
6282
+ const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
6067
6283
  const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
6068
6284
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
6069
6285
  ...options,
6070
- priorTargetManifestPaths
6286
+ priorTargetManifestPaths,
6287
+ scopedServices
6071
6288
  });
6072
6289
  if (options.dryRun) {
6073
6290
  if (options.target) {
@@ -6091,8 +6308,9 @@ async function generate(spec, emitter, options) {
6091
6308
  currentPaths: outputEmittedPaths,
6092
6309
  language: emitter.language,
6093
6310
  header,
6094
- noPrune: options.noPrune,
6095
- operations
6311
+ noPrune,
6312
+ operations,
6313
+ scoped
6096
6314
  });
6097
6315
  const allOutputFiles = [...writeResult.written, ...writeResult.merged];
6098
6316
  if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
@@ -6114,8 +6332,9 @@ async function generate(spec, emitter, options) {
6114
6332
  currentPaths: targetResult.emittedPaths,
6115
6333
  language: emitter.language,
6116
6334
  header,
6117
- noPrune: options.noPrune,
6118
- operations
6335
+ noPrune,
6336
+ operations,
6337
+ scoped
6119
6338
  });
6120
6339
  const allTargetFiles = [
6121
6340
  ...targetResult.written,
@@ -6131,11 +6350,15 @@ async function generate(spec, emitter, options) {
6131
6350
  * then write the fresh manifest. Safe on first adoption (no previous manifest).
6132
6351
  */
6133
6352
  async function applyManifestPrune(opts) {
6134
- const manifestOpts = {
6135
- language: opts.language,
6353
+ const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
6136
6354
  files: opts.currentPaths,
6137
6355
  operations: opts.operations
6138
6356
  };
6357
+ const manifestOpts = {
6358
+ language: opts.language,
6359
+ files: records.files,
6360
+ operations: records.operations
6361
+ };
6139
6362
  if (opts.noPrune) {
6140
6363
  await writeManifest(opts.dir, manifestOpts);
6141
6364
  return;
@@ -6775,7 +6998,7 @@ function loadOverlayContext(opts) {
6775
6998
  //#region src/utils/expand-doc-urls.ts
6776
6999
  /**
6777
7000
  * Walk all description fields in the IR and expand relative URL paths
6778
- * (e.g. `/reference/authkit/user`) into full URLs using the given base.
7001
+ * (e.g. `/reference/users/user`) into full URLs using the given base.
6779
7002
  *
6780
7003
  * Matches markdown-style links: `[text](/path)` → `[text](https://example.com/path)`
6781
7004
  * and bare parenthetical refs: `(/path)` → `(https://example.com/path)`
@@ -6832,11 +7055,13 @@ async function generateCommand(opts) {
6832
7055
  let ir = await parseSpec(opts.spec, {
6833
7056
  operationIdTransform: opts.operationIdTransform,
6834
7057
  schemaNameTransform: opts.schemaNameTransform,
6835
- transformSpec: opts.transformSpec
7058
+ transformSpec: opts.transformSpec,
7059
+ fieldHints: opts.fieldHints
6836
7060
  });
6837
7061
  if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
6838
7062
  const emitter = getEmitter(opts.lang);
6839
7063
  const namespace = opts.namespace ?? ir.name;
7064
+ const services = (Array.isArray(opts.services) ? opts.services : opts.services?.split(","))?.map((s) => s.trim()).filter(Boolean);
6840
7065
  let apiSurface;
6841
7066
  let overlayLookup;
6842
7067
  if (opts.apiSurface && opts.compatCheck !== false) {
@@ -6860,7 +7085,8 @@ async function generateCommand(opts) {
6860
7085
  mountRules: opts.mountRules,
6861
7086
  modelHints: opts.modelHints,
6862
7087
  emitterOptions: opts.emitterOptions,
6863
- noPrune: opts.prune === false
7088
+ noPrune: opts.prune === false,
7089
+ services: services && services.length > 0 ? services : void 0
6864
7090
  });
6865
7091
  if (opts.dryRun) for (const f of files) console.log(f.path);
6866
7092
  else {
@@ -8123,7 +8349,7 @@ function specDerivedFieldPaths(spec, hints) {
8123
8349
  return paths;
8124
8350
  }
8125
8351
  /**
8126
- * Compute the set of method paths (e.g., "SSO.authorize") that are defined
8352
+ * Compute the set of method paths (e.g., "Auth.authorize") that are defined
8127
8353
  * as operations in the OpenAPI spec's services. Used by filterSurface to
8128
8354
  * exclude hand-written SDK methods that don't correspond to spec operations.
8129
8355
  */
@@ -8159,7 +8385,7 @@ function filterRecord(record, allowed) {
8159
8385
  *
8160
8386
  * For classes: when methodPaths is provided, only keeps methods whose names
8161
8387
  * appear in the spec-derived method paths. This prevents false positives from
8162
- * hand-written SDK methods (e.g., SSO.authorization_url, Webhooks.construct_event)
8388
+ * hand-written SDK methods (e.g., Auth.authorization_url, Webhooks.construct_event)
8163
8389
  * that don't correspond to spec operations. Properties on service classes are
8164
8390
  * also filtered: only UPPER_CASE constants that are spec-derivable are kept.
8165
8391
  */
@@ -9802,6 +10028,7 @@ let configDocUrl;
9802
10028
  let configOperationHints;
9803
10029
  let configMountRules;
9804
10030
  let configModelHints;
10031
+ let configFieldHints;
9805
10032
  let configEmitterOptions;
9806
10033
  let configCompat;
9807
10034
  try {
@@ -9816,6 +10043,7 @@ try {
9816
10043
  configOperationHints = config.operationHints;
9817
10044
  configMountRules = config.mountRules;
9818
10045
  configModelHints = config.modelHints;
10046
+ configFieldHints = config.fieldHints;
9819
10047
  configEmitterOptions = config.emitterOptions;
9820
10048
  configCompat = config.compat;
9821
10049
  }
@@ -9836,7 +10064,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
9836
10064
  transformSpec: configTransformSpec
9837
10065
  });
9838
10066
  });
9839
- program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").action((opts) => {
10067
+ program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").option("--services <list>", "Comma-separated post-mount service names to generate (default: all)").action((opts) => {
9840
10068
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
9841
10069
  if (!opts.spec) {
9842
10070
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
@@ -9851,6 +10079,7 @@ program.command("generate").description("Run a registered emitter against an Ope
9851
10079
  operationHints: configOperationHints,
9852
10080
  mountRules: configMountRules,
9853
10081
  modelHints: configModelHints,
10082
+ fieldHints: configFieldHints,
9854
10083
  emitterOptions: configEmitterOptions?.[opts.lang]
9855
10084
  }).catch(handleError);
9856
10085
  });