@workos/oagen 0.22.7 → 0.24.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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.mjs +207 -33
  3. package/dist/cli/index.mjs.map +1 -1
  4. package/dist/compat/index.d.mts +2 -2
  5. package/dist/compat/index.mjs +7 -7
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-D4XaiPeA.mjs → diff-B7dMUCoJ.mjs} +128 -19
  8. package/dist/diff-B7dMUCoJ.mjs.map +1 -0
  9. package/dist/{differ-Q13KY9Rd.d.mts → differ-DMmw0tbA.d.mts} +5 -5
  10. package/dist/{differ-Q13KY9Rd.d.mts.map → differ-DMmw0tbA.d.mts.map} +1 -1
  11. package/dist/index.d.mts +50 -9
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +74 -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 +73 -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 +3 -4
  22. package/dist/smoke.d.mts.map +1 -1
  23. package/dist/smoke.mjs +73 -10
  24. package/dist/smoke.mjs.map +1 -1
  25. package/dist/{types-DZrKFHis.d.mts → types-nM5Kp7UP.d.mts} +7 -9
  26. package/dist/types-nM5Kp7UP.d.mts.map +1 -0
  27. package/dist/verify/index.d.mts +2 -2
  28. package/dist/verify/index.mjs +2 -2
  29. package/docs/architecture/extractor-contract.md +2 -2
  30. package/docs/architecture/pipeline.md +2 -2
  31. package/docs/cli.md +19 -19
  32. package/docs/core/compatibility-policy.md +1 -1
  33. package/docs/core/manifest-schema.md +2 -2
  34. package/package.json +1 -1
  35. package/skills/generate-emitter/SKILL.md +2 -2
  36. package/skills/generate-emitter/references/common-pitfalls.md +1 -1
  37. package/skills/generate-emitter/references/project-scaffold.md +3 -3
  38. package/skills/generate-extractor/SKILL.md +2 -2
  39. package/skills/generate-smoke-test/SKILL.md +1 -1
  40. package/skills/verify-smoke-test/SKILL.md +1 -1
  41. package/dist/diff-D4XaiPeA.mjs.map +0 -1
  42. package/dist/overlay-cb32jDBS.mjs.map +0 -1
  43. package/dist/types-DZrKFHis.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);
@@ -2355,6 +2353,10 @@ function extractRequestBody(body, op, inlineModels) {
2355
2353
  return {};
2356
2354
  }
2357
2355
  const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
2356
+ if (schema.oneOf && schema.discriminator) {
2357
+ const merged = flattenDiscriminatedUnionBody(schema);
2358
+ if (merged) schema = merged;
2359
+ }
2358
2360
  if (schema.properties && (schema.type === "object" || !schema.type)) {
2359
2361
  const requiredSet = new Set(schema.required ?? []);
2360
2362
  const fields = [];
@@ -2420,6 +2422,67 @@ function extractRequestBody(body, op, inlineModels) {
2420
2422
  encoding
2421
2423
  };
2422
2424
  }
2425
+ /**
2426
+ * Fold a discriminated-union request body into the equivalent single object so
2427
+ * its fields can be spread into method arguments instead of forcing callers to
2428
+ * construct an opaque union.
2429
+ *
2430
+ * The result is the union of every variant's properties: the discriminator is
2431
+ * collapsed to an enum of the pinned const values, and any field that isn't
2432
+ * present-and-required in *every* variant becomes optional (it varies by
2433
+ * branch, so it can't be unconditionally required). The discriminator itself is
2434
+ * always required. This mirrors what a spec author would write by hand to model
2435
+ * the same shape as a flat object.
2436
+ *
2437
+ * Returns null — leaving the union intact — unless the body is a clean
2438
+ * inline-object discriminated union: a variant that is a `$ref`, a non-object,
2439
+ * or that doesn't pin the discriminator to a string const can't be safely
2440
+ * merged. Gating on an *explicit* `discriminator` keeps undiscriminated `oneOf`
2441
+ * bodies untouched.
2442
+ */
2443
+ function flattenDiscriminatedUnionBody(schema) {
2444
+ const discProp = schema.discriminator?.propertyName;
2445
+ if (!discProp || !Array.isArray(schema.oneOf) || schema.oneOf.length === 0) return null;
2446
+ const variants = schema.oneOf;
2447
+ const discValues = [];
2448
+ let discPropSchema;
2449
+ for (const variant of variants) {
2450
+ if (variant.$ref || !variant.properties || !(variant.type === "object" || variant.type === void 0)) return null;
2451
+ const dp = variant.properties[discProp];
2452
+ const constVal = typeof dp?.const === "string" ? dp.const : Array.isArray(dp?.enum) && dp.enum.length === 1 && typeof dp.enum[0] === "string" ? dp.enum[0] : void 0;
2453
+ if (constVal === void 0) return null;
2454
+ if (!discValues.includes(constVal)) discValues.push(constVal);
2455
+ if (!discPropSchema) discPropSchema = dp;
2456
+ }
2457
+ const variantCount = variants.length;
2458
+ const mergedProps = {};
2459
+ const requiredCounts = {};
2460
+ const order = [discProp];
2461
+ for (const variant of variants) {
2462
+ const required = new Set(variant.required ?? []);
2463
+ for (const [name, propSchema] of Object.entries(variant.properties)) {
2464
+ if (name === discProp || !propSchema) continue;
2465
+ if (!(name in mergedProps)) {
2466
+ mergedProps[name] = propSchema;
2467
+ order.push(name);
2468
+ }
2469
+ if (required.has(name)) requiredCounts[name] = (requiredCounts[name] ?? 0) + 1;
2470
+ }
2471
+ }
2472
+ mergedProps[discProp] = {
2473
+ type: "string",
2474
+ enum: discValues,
2475
+ ...discPropSchema?.description ? { description: discPropSchema.description } : {}
2476
+ };
2477
+ const properties = {};
2478
+ for (const name of order) properties[name] = mergedProps[name];
2479
+ return {
2480
+ type: "object",
2481
+ properties,
2482
+ required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
2483
+ ...schema.description ? { description: schema.description } : {}
2484
+ };
2485
+ }
2423
2486
  /** Derive a unique variant name for a oneOf inline model. */
2424
2487
  function deriveOneOfVariantName(variant, baseName, existingModels) {
2425
2488
  if (variant.properties) {
@@ -3330,12 +3393,12 @@ function extractKotlinClassMembers(classBody, source) {
3330
3393
  }
3331
3394
  /**
3332
3395
  * KDoc signature that identifies a generator-emitted service accessor inside
3333
- * the WorkOS class. The Kotlin client emitter writes every accessor with this
3396
+ * a Kotlin client class. The Kotlin client emitter writes every accessor with this
3334
3397
  * exact doc preamble — when the existing file carries a member with this doc
3335
3398
  * but the regenerated content omits it, the member is stale (its mount target
3336
3399
  * was remapped or removed) and gets pruned.
3337
3400
  */
3338
- const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[WorkOS\] client\.[\s\S]*?\*\//;
3401
+ const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[[^\]]+\] client\.[\s\S]*?\*\//;
3339
3402
  const kotlinMergeAdapter = {
3340
3403
  language: "kotlin",
3341
3404
  grammarModule: "tree-sitter-kotlin",
@@ -3588,7 +3651,6 @@ const nodeMergeAdapter = {
3588
3651
  const newText = newMembers.map((m) => m.text).join("\n");
3589
3652
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
3590
3653
  const propName = match[1];
3591
- if (propName === "workos") continue;
3592
3654
  if (propName && !existingMemberKeys.has(propName)) return true;
3593
3655
  }
3594
3656
  return false;
@@ -5719,6 +5781,15 @@ function resolveResponseModelName(op) {
5719
5781
  //#endregion
5720
5782
  //#region src/engine/generate-files.ts
5721
5783
  function buildEmitterContext(spec, options) {
5784
+ const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
5785
+ let scopedModelNames;
5786
+ let scopedEnumNames;
5787
+ if (options.scopedServices && options.scopedServices.size > 0) {
5788
+ const selected = options.scopedServices;
5789
+ const reachable = collectReferencedNames(spec.services.filter((s) => resolvedOperations.some((r) => r.service.name === s.name && selected.has(r.mountOn))), spec.models, { preserveAllDiscriminated: false });
5790
+ scopedModelNames = reachable.models;
5791
+ scopedEnumNames = reachable.enums;
5792
+ }
5722
5793
  return {
5723
5794
  namespace: toSnakeCase(options.namespace),
5724
5795
  namespacePascal: options.namespace,
@@ -5726,11 +5797,14 @@ function buildEmitterContext(spec, options) {
5726
5797
  outputDir: options.outputDir,
5727
5798
  apiSurface: options.apiSurface,
5728
5799
  overlayLookup: options.overlayLookup,
5729
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
5800
+ resolvedOperations,
5730
5801
  modelHints: options.modelHints,
5731
5802
  emitterOptions: options.emitterOptions,
5732
5803
  targetDir: options.target,
5733
- priorTargetManifestPaths: options.priorTargetManifestPaths
5804
+ priorTargetManifestPaths: options.priorTargetManifestPaths,
5805
+ scopedServices: options.scopedServices,
5806
+ scopedModelNames,
5807
+ scopedEnumNames
5734
5808
  };
5735
5809
  }
5736
5810
  /**
@@ -5738,7 +5812,8 @@ function buildEmitterContext(spec, options) {
5738
5812
  * Walks operation parameters, request bodies, responses, and pagination item
5739
5813
  * types, then chases model field references until the set stabilizes.
5740
5814
  */
5741
- function collectReferencedNames(services, models) {
5815
+ function collectReferencedNames(services, models, opts = {}) {
5816
+ const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
5742
5817
  const referencedModels = /* @__PURE__ */ new Set();
5743
5818
  const referencedEnums = /* @__PURE__ */ new Set();
5744
5819
  const collectFromTypeRef = (ref) => {
@@ -5760,7 +5835,7 @@ function collectReferencedNames(services, models) {
5760
5835
  for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
5761
5836
  if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
5762
5837
  }
5763
- for (const model of models) {
5838
+ if (preserveAllDiscriminated) for (const model of models) {
5764
5839
  if (isDiscriminatedModel(model)) referencedModels.add(model.name);
5765
5840
  const disc = model.discriminator;
5766
5841
  if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
@@ -5774,6 +5849,13 @@ function collectReferencedNames(services, models) {
5774
5849
  visited.add(name);
5775
5850
  const model = modelsByName.get(name);
5776
5851
  if (!model) continue;
5852
+ const disc = model.discriminator;
5853
+ if (disc?.mapping) {
5854
+ for (const variantName of Object.values(disc.mapping)) if (!referencedModels.has(variantName)) {
5855
+ referencedModels.add(variantName);
5856
+ queue.push(variantName);
5857
+ }
5858
+ }
5777
5859
  for (const field of model.fields) {
5778
5860
  collectFromTypeRef(field.type);
5779
5861
  for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
@@ -5787,7 +5869,16 @@ function collectReferencedNames(services, models) {
5787
5869
  function isDiscriminatedModel(model) {
5788
5870
  return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
5789
5871
  }
5790
- /** Collect all generated files from an emitter (no headers, no path prefixes). */
5872
+ /**
5873
+ * Collect all generated files from an emitter (no headers, no path prefixes).
5874
+ *
5875
+ * In a scoped (`--services`) run the spec is NOT filtered: every generator runs
5876
+ * over the full spec so model placement, dedup, the root client, and all
5877
+ * aggregate/barrel files are byte-identical to a full run (and a brand-new
5878
+ * selected service is wired into the client automatically). The emitters
5879
+ * themselves consult `ctx.scopedServices` to emit only the selected services'
5880
+ * per-service resource/test files.
5881
+ */
5791
5882
  function generateAllFiles(spec, emitter, ctx) {
5792
5883
  const referenced = collectReferencedNames(spec.services, spec.models);
5793
5884
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
@@ -5828,6 +5919,47 @@ function generateFiles(spec, emitter, options) {
5828
5919
  };
5829
5920
  }
5830
5921
  //#endregion
5922
+ //#region src/engine/scoped-services.ts
5923
+ /**
5924
+ * Validate a `--services` selection and return it as a set of POST-MOUNT service
5925
+ * names for emitters to scope on.
5926
+ *
5927
+ * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
5928
+ * shared-schema decisions are global computations over the full service/model set
5929
+ * (filtering them churns shared `common/` files). Instead the full spec flows to
5930
+ * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
5931
+ * emitter gates only its per-service resource/test emission on it.
5932
+ *
5933
+ * Matching is on the POST-MOUNT name. Mount resolution never mutates
5934
+ * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
5935
+ * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
5936
+ * post-mount name of a source service is derived here as
5937
+ * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
5938
+ * `DirectorySync`) is valid because every source tag that mounts into it
5939
+ * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
5940
+ * emitters group resources by mount, so all siblings emit together.
5941
+ *
5942
+ * @param spec - The parsed API spec (never mutated; never filtered).
5943
+ * @param selected - Post-mount service names from `--services` (normalized/trimmed).
5944
+ * @param mountRules - Source-service-name → post-mount-target mappings.
5945
+ * @returns The validated selection as a `Set` of post-mount names.
5946
+ * @throws A `ConfigError` listing valid post-mount names when a selection is
5947
+ * unknown — scoped generation never silently emits the wrong set.
5948
+ */
5949
+ function resolveScopedServices(spec, selected, mountRules) {
5950
+ const mounts = mountRules ?? {};
5951
+ const postMountName = (name) => mounts[name] ?? name;
5952
+ const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
5953
+ const selectedSet = new Set(selected);
5954
+ const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
5955
+ if (unknown.length > 0) {
5956
+ const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
5957
+ err.name = "ConfigError";
5958
+ throw err;
5959
+ }
5960
+ return selectedSet;
5961
+ }
5962
+ //#endregion
5831
5963
  //#region src/engine/integrate.ts
5832
5964
  /**
5833
5965
  * Resolve a relative import path against a source file path to get the
@@ -5944,7 +6076,7 @@ function mapFilesForTargetIntegration(files, language) {
5944
6076
  * that would shadow an existing module file (e.g., `api_keys.py`), move the
5945
6077
  * module file's content into `_compat.py` inside the package and add a
5946
6078
  * re-export from `__init__.py`. This preserves backward-compatible imports
5947
- * (e.g., `from workos.api_keys import ApiKeysModule`) while the package directory
6079
+ * (e.g., `from acme.api_keys import ApiKeysModule`) while the package directory
5948
6080
  * provides the generated models and resource classes.
5949
6081
  */
5950
6082
  async function resolveFileToDirectoryConflicts(files, targetDir) {
@@ -6110,6 +6242,35 @@ function computeStalePaths(prev, currentPaths) {
6110
6242
  return prev.files.filter((p) => !current.has(p)).sort();
6111
6243
  }
6112
6244
  /**
6245
+ * Merge a scoped run's emitted records into the prior manifest's records.
6246
+ *
6247
+ * A scoped (`--services`) run only emits files for the selected services, but it
6248
+ * must NOT discard the prior manifest's record of unselected services' files
6249
+ * records. The result unions prior + scoped file paths and shallow-merges the
6250
+ * operations maps, with scoped operations overriding prior entries for the same
6251
+ * "METHOD /path" key (a re-emission updates that operation's record).
6252
+ *
6253
+ * When `prev` is null (the first-ever run is scoped) there is nothing to
6254
+ * preserve, so the scoped subset is returned unchanged — matching the
6255
+ * first-adoption behavior of a full run.
6256
+ */
6257
+ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
6258
+ const scopedFileList = [...scopedFiles];
6259
+ if (!prev) return {
6260
+ files: scopedFileList,
6261
+ operations: scopedOperations
6262
+ };
6263
+ const files = [...new Set([...prev.files, ...scopedFileList])].sort();
6264
+ const priorOps = prev.operations;
6265
+ return {
6266
+ files,
6267
+ operations: priorOps || scopedOperations ? {
6268
+ ...priorOps,
6269
+ ...scopedOperations
6270
+ } : void 0
6271
+ };
6272
+ }
6273
+ /**
6113
6274
  * Delete stale files from `dir`.
6114
6275
  *
6115
6276
  * When `header` is provided, a file is only deleted if its contents start with
@@ -6178,12 +6339,17 @@ async function removeEmptyParents(startDir, stopDir) {
6178
6339
  //#endregion
6179
6340
  //#region src/engine/orchestrator.ts
6180
6341
  async function generate(spec, emitter, options) {
6181
- const outputPrevManifestForCtx = options.noPrune ? null : await readManifest(options.outputDir);
6182
- const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
6342
+ const scopedServices = options.services && options.services.length > 0 ? resolveScopedServices(spec, options.services, options.mountRules) : void 0;
6343
+ const scoped = scopedServices !== void 0;
6344
+ const noPrune = options.noPrune === true || scoped;
6345
+ const skipManifestRead = options.noPrune === true && !scoped;
6346
+ const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
6347
+ const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
6183
6348
  const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
6184
6349
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
6185
6350
  ...options,
6186
- priorTargetManifestPaths
6351
+ priorTargetManifestPaths,
6352
+ scopedServices
6187
6353
  });
6188
6354
  if (options.dryRun) {
6189
6355
  if (options.target) {
@@ -6207,8 +6373,9 @@ async function generate(spec, emitter, options) {
6207
6373
  currentPaths: outputEmittedPaths,
6208
6374
  language: emitter.language,
6209
6375
  header,
6210
- noPrune: options.noPrune,
6211
- operations
6376
+ noPrune,
6377
+ operations,
6378
+ scoped
6212
6379
  });
6213
6380
  const allOutputFiles = [...writeResult.written, ...writeResult.merged];
6214
6381
  if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
@@ -6230,8 +6397,9 @@ async function generate(spec, emitter, options) {
6230
6397
  currentPaths: targetResult.emittedPaths,
6231
6398
  language: emitter.language,
6232
6399
  header,
6233
- noPrune: options.noPrune,
6234
- operations
6400
+ noPrune,
6401
+ operations,
6402
+ scoped
6235
6403
  });
6236
6404
  const allTargetFiles = [
6237
6405
  ...targetResult.written,
@@ -6247,11 +6415,15 @@ async function generate(spec, emitter, options) {
6247
6415
  * then write the fresh manifest. Safe on first adoption (no previous manifest).
6248
6416
  */
6249
6417
  async function applyManifestPrune(opts) {
6250
- const manifestOpts = {
6251
- language: opts.language,
6418
+ const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
6252
6419
  files: opts.currentPaths,
6253
6420
  operations: opts.operations
6254
6421
  };
6422
+ const manifestOpts = {
6423
+ language: opts.language,
6424
+ files: records.files,
6425
+ operations: records.operations
6426
+ };
6255
6427
  if (opts.noPrune) {
6256
6428
  await writeManifest(opts.dir, manifestOpts);
6257
6429
  return;
@@ -6891,7 +7063,7 @@ function loadOverlayContext(opts) {
6891
7063
  //#region src/utils/expand-doc-urls.ts
6892
7064
  /**
6893
7065
  * Walk all description fields in the IR and expand relative URL paths
6894
- * (e.g. `/reference/authkit/user`) into full URLs using the given base.
7066
+ * (e.g. `/reference/users/user`) into full URLs using the given base.
6895
7067
  *
6896
7068
  * Matches markdown-style links: `[text](/path)` → `[text](https://example.com/path)`
6897
7069
  * and bare parenthetical refs: `(/path)` → `(https://example.com/path)`
@@ -6954,6 +7126,7 @@ async function generateCommand(opts) {
6954
7126
  if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
6955
7127
  const emitter = getEmitter(opts.lang);
6956
7128
  const namespace = opts.namespace ?? ir.name;
7129
+ const services = (Array.isArray(opts.services) ? opts.services : opts.services?.split(","))?.map((s) => s.trim()).filter(Boolean);
6957
7130
  let apiSurface;
6958
7131
  let overlayLookup;
6959
7132
  if (opts.apiSurface && opts.compatCheck !== false) {
@@ -6977,7 +7150,8 @@ async function generateCommand(opts) {
6977
7150
  mountRules: opts.mountRules,
6978
7151
  modelHints: opts.modelHints,
6979
7152
  emitterOptions: opts.emitterOptions,
6980
- noPrune: opts.prune === false
7153
+ noPrune: opts.prune === false,
7154
+ services: services && services.length > 0 ? services : void 0
6981
7155
  });
6982
7156
  if (opts.dryRun) for (const f of files) console.log(f.path);
6983
7157
  else {
@@ -8240,7 +8414,7 @@ function specDerivedFieldPaths(spec, hints) {
8240
8414
  return paths;
8241
8415
  }
8242
8416
  /**
8243
- * Compute the set of method paths (e.g., "SSO.authorize") that are defined
8417
+ * Compute the set of method paths (e.g., "Auth.authorize") that are defined
8244
8418
  * as operations in the OpenAPI spec's services. Used by filterSurface to
8245
8419
  * exclude hand-written SDK methods that don't correspond to spec operations.
8246
8420
  */
@@ -8276,7 +8450,7 @@ function filterRecord(record, allowed) {
8276
8450
  *
8277
8451
  * For classes: when methodPaths is provided, only keeps methods whose names
8278
8452
  * appear in the spec-derived method paths. This prevents false positives from
8279
- * hand-written SDK methods (e.g., SSO.authorization_url, Webhooks.construct_event)
8453
+ * hand-written SDK methods (e.g., Auth.authorization_url, Webhooks.construct_event)
8280
8454
  * that don't correspond to spec operations. Properties on service classes are
8281
8455
  * also filtered: only UPPER_CASE constants that are spec-derivable are kept.
8282
8456
  */
@@ -9955,7 +10129,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
9955
10129
  transformSpec: configTransformSpec
9956
10130
  });
9957
10131
  });
9958
- 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) => {
10132
+ 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) => {
9959
10133
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
9960
10134
  if (!opts.spec) {
9961
10135
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");