@workos/oagen 0.22.7 → 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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.mjs +142 -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 +9 -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 +8 -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 +8 -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);
@@ -3330,12 +3328,12 @@ function extractKotlinClassMembers(classBody, source) {
3330
3328
  }
3331
3329
  /**
3332
3330
  * KDoc signature that identifies a generator-emitted service accessor inside
3333
- * 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
3334
3332
  * exact doc preamble — when the existing file carries a member with this doc
3335
3333
  * but the regenerated content omits it, the member is stale (its mount target
3336
3334
  * was remapped or removed) and gets pruned.
3337
3335
  */
3338
- 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]*?\*\//;
3339
3337
  const kotlinMergeAdapter = {
3340
3338
  language: "kotlin",
3341
3339
  grammarModule: "tree-sitter-kotlin",
@@ -3588,7 +3586,6 @@ const nodeMergeAdapter = {
3588
3586
  const newText = newMembers.map((m) => m.text).join("\n");
3589
3587
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
3590
3588
  const propName = match[1];
3591
- if (propName === "workos") continue;
3592
3589
  if (propName && !existingMemberKeys.has(propName)) return true;
3593
3590
  }
3594
3591
  return false;
@@ -5719,6 +5716,15 @@ function resolveResponseModelName(op) {
5719
5716
  //#endregion
5720
5717
  //#region src/engine/generate-files.ts
5721
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
+ }
5722
5728
  return {
5723
5729
  namespace: toSnakeCase(options.namespace),
5724
5730
  namespacePascal: options.namespace,
@@ -5726,11 +5732,14 @@ function buildEmitterContext(spec, options) {
5726
5732
  outputDir: options.outputDir,
5727
5733
  apiSurface: options.apiSurface,
5728
5734
  overlayLookup: options.overlayLookup,
5729
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
5735
+ resolvedOperations,
5730
5736
  modelHints: options.modelHints,
5731
5737
  emitterOptions: options.emitterOptions,
5732
5738
  targetDir: options.target,
5733
- priorTargetManifestPaths: options.priorTargetManifestPaths
5739
+ priorTargetManifestPaths: options.priorTargetManifestPaths,
5740
+ scopedServices: options.scopedServices,
5741
+ scopedModelNames,
5742
+ scopedEnumNames
5734
5743
  };
5735
5744
  }
5736
5745
  /**
@@ -5738,7 +5747,8 @@ function buildEmitterContext(spec, options) {
5738
5747
  * Walks operation parameters, request bodies, responses, and pagination item
5739
5748
  * types, then chases model field references until the set stabilizes.
5740
5749
  */
5741
- function collectReferencedNames(services, models) {
5750
+ function collectReferencedNames(services, models, opts = {}) {
5751
+ const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
5742
5752
  const referencedModels = /* @__PURE__ */ new Set();
5743
5753
  const referencedEnums = /* @__PURE__ */ new Set();
5744
5754
  const collectFromTypeRef = (ref) => {
@@ -5760,7 +5770,7 @@ function collectReferencedNames(services, models) {
5760
5770
  for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
5761
5771
  if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
5762
5772
  }
5763
- for (const model of models) {
5773
+ if (preserveAllDiscriminated) for (const model of models) {
5764
5774
  if (isDiscriminatedModel(model)) referencedModels.add(model.name);
5765
5775
  const disc = model.discriminator;
5766
5776
  if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
@@ -5774,6 +5784,13 @@ function collectReferencedNames(services, models) {
5774
5784
  visited.add(name);
5775
5785
  const model = modelsByName.get(name);
5776
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
+ }
5777
5794
  for (const field of model.fields) {
5778
5795
  collectFromTypeRef(field.type);
5779
5796
  for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
@@ -5787,7 +5804,16 @@ function collectReferencedNames(services, models) {
5787
5804
  function isDiscriminatedModel(model) {
5788
5805
  return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
5789
5806
  }
5790
- /** 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
+ */
5791
5817
  function generateAllFiles(spec, emitter, ctx) {
5792
5818
  const referenced = collectReferencedNames(spec.services, spec.models);
5793
5819
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
@@ -5828,6 +5854,47 @@ function generateFiles(spec, emitter, options) {
5828
5854
  };
5829
5855
  }
5830
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
5831
5898
  //#region src/engine/integrate.ts
5832
5899
  /**
5833
5900
  * Resolve a relative import path against a source file path to get the
@@ -5944,7 +6011,7 @@ function mapFilesForTargetIntegration(files, language) {
5944
6011
  * that would shadow an existing module file (e.g., `api_keys.py`), move the
5945
6012
  * module file's content into `_compat.py` inside the package and add a
5946
6013
  * re-export from `__init__.py`. This preserves backward-compatible imports
5947
- * (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
5948
6015
  * provides the generated models and resource classes.
5949
6016
  */
5950
6017
  async function resolveFileToDirectoryConflicts(files, targetDir) {
@@ -6110,6 +6177,35 @@ function computeStalePaths(prev, currentPaths) {
6110
6177
  return prev.files.filter((p) => !current.has(p)).sort();
6111
6178
  }
6112
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
+ /**
6113
6209
  * Delete stale files from `dir`.
6114
6210
  *
6115
6211
  * When `header` is provided, a file is only deleted if its contents start with
@@ -6178,12 +6274,17 @@ async function removeEmptyParents(startDir, stopDir) {
6178
6274
  //#endregion
6179
6275
  //#region src/engine/orchestrator.ts
6180
6276
  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;
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;
6183
6283
  const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
6184
6284
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
6185
6285
  ...options,
6186
- priorTargetManifestPaths
6286
+ priorTargetManifestPaths,
6287
+ scopedServices
6187
6288
  });
6188
6289
  if (options.dryRun) {
6189
6290
  if (options.target) {
@@ -6207,8 +6308,9 @@ async function generate(spec, emitter, options) {
6207
6308
  currentPaths: outputEmittedPaths,
6208
6309
  language: emitter.language,
6209
6310
  header,
6210
- noPrune: options.noPrune,
6211
- operations
6311
+ noPrune,
6312
+ operations,
6313
+ scoped
6212
6314
  });
6213
6315
  const allOutputFiles = [...writeResult.written, ...writeResult.merged];
6214
6316
  if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
@@ -6230,8 +6332,9 @@ async function generate(spec, emitter, options) {
6230
6332
  currentPaths: targetResult.emittedPaths,
6231
6333
  language: emitter.language,
6232
6334
  header,
6233
- noPrune: options.noPrune,
6234
- operations
6335
+ noPrune,
6336
+ operations,
6337
+ scoped
6235
6338
  });
6236
6339
  const allTargetFiles = [
6237
6340
  ...targetResult.written,
@@ -6247,11 +6350,15 @@ async function generate(spec, emitter, options) {
6247
6350
  * then write the fresh manifest. Safe on first adoption (no previous manifest).
6248
6351
  */
6249
6352
  async function applyManifestPrune(opts) {
6250
- const manifestOpts = {
6251
- language: opts.language,
6353
+ const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
6252
6354
  files: opts.currentPaths,
6253
6355
  operations: opts.operations
6254
6356
  };
6357
+ const manifestOpts = {
6358
+ language: opts.language,
6359
+ files: records.files,
6360
+ operations: records.operations
6361
+ };
6255
6362
  if (opts.noPrune) {
6256
6363
  await writeManifest(opts.dir, manifestOpts);
6257
6364
  return;
@@ -6891,7 +6998,7 @@ function loadOverlayContext(opts) {
6891
6998
  //#region src/utils/expand-doc-urls.ts
6892
6999
  /**
6893
7000
  * 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.
7001
+ * (e.g. `/reference/users/user`) into full URLs using the given base.
6895
7002
  *
6896
7003
  * Matches markdown-style links: `[text](/path)` → `[text](https://example.com/path)`
6897
7004
  * and bare parenthetical refs: `(/path)` → `(https://example.com/path)`
@@ -6954,6 +7061,7 @@ async function generateCommand(opts) {
6954
7061
  if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
6955
7062
  const emitter = getEmitter(opts.lang);
6956
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);
6957
7065
  let apiSurface;
6958
7066
  let overlayLookup;
6959
7067
  if (opts.apiSurface && opts.compatCheck !== false) {
@@ -6977,7 +7085,8 @@ async function generateCommand(opts) {
6977
7085
  mountRules: opts.mountRules,
6978
7086
  modelHints: opts.modelHints,
6979
7087
  emitterOptions: opts.emitterOptions,
6980
- noPrune: opts.prune === false
7088
+ noPrune: opts.prune === false,
7089
+ services: services && services.length > 0 ? services : void 0
6981
7090
  });
6982
7091
  if (opts.dryRun) for (const f of files) console.log(f.path);
6983
7092
  else {
@@ -8240,7 +8349,7 @@ function specDerivedFieldPaths(spec, hints) {
8240
8349
  return paths;
8241
8350
  }
8242
8351
  /**
8243
- * 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
8244
8353
  * as operations in the OpenAPI spec's services. Used by filterSurface to
8245
8354
  * exclude hand-written SDK methods that don't correspond to spec operations.
8246
8355
  */
@@ -8276,7 +8385,7 @@ function filterRecord(record, allowed) {
8276
8385
  *
8277
8386
  * For classes: when methodPaths is provided, only keeps methods whose names
8278
8387
  * 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)
8388
+ * hand-written SDK methods (e.g., Auth.authorization_url, Webhooks.construct_event)
8280
8389
  * that don't correspond to spec operations. Properties on service classes are
8281
8390
  * also filtered: only UPPER_CASE constants that are spec-derivable are kept.
8282
8391
  */
@@ -9955,7 +10064,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
9955
10064
  transformSpec: configTransformSpec
9956
10065
  });
9957
10066
  });
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) => {
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) => {
9959
10068
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
9960
10069
  if (!opts.spec) {
9961
10070
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");