@workos/oagen 0.5.0 → 0.6.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.
@@ -679,6 +679,86 @@ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet)
679
679
  example: fieldSchema.example
680
680
  };
681
681
  }
682
+ /**
683
+ * If every non-null oneOf/anyOf variant is a string-valued const, return the
684
+ * list of those const values so the union can collapse into a single enum.
685
+ * Returns null when any variant isn't a pure string-const (model ref,
686
+ * number const, nullable, etc.).
687
+ */
688
+ function collectLiteralStringConsts(variants) {
689
+ if (variants.length === 0) return null;
690
+ const values = [];
691
+ for (const v of variants) {
692
+ if (typeof v.const === "string") {
693
+ values.push(v.const);
694
+ continue;
695
+ }
696
+ if (Array.isArray(v.enum) && v.enum.length === 1 && typeof v.enum[0] === "string") {
697
+ values.push(v.enum[0]);
698
+ continue;
699
+ }
700
+ return null;
701
+ }
702
+ return values;
703
+ }
704
+ /** Build an EnumRef the same way the regular enum path does for naming. */
705
+ function buildSyntheticEnumRef(values, contextName, parentModelName) {
706
+ const baseName = toPascalCase(contextName ?? "UnknownEnum");
707
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
708
+ return {
709
+ kind: "enum",
710
+ name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
711
+ values
712
+ };
713
+ }
714
+ /**
715
+ * Detect an implicit discriminator on an oneOf where every variant is an
716
+ * object schema that pins the same property to a const value. Returns
717
+ * { property, mapping } so the IR can represent the union as discriminated
718
+ * without needing an explicit `discriminator:` key in the spec.
719
+ *
720
+ * Both shapes are supported:
721
+ * { properties: { event: { const: "x" } }, required: ["event"] }
722
+ * { properties: { event: { type: "string", enum: ["x"] } } }
723
+ *
724
+ * Returns null when any variant doesn't fit the pattern or when variants
725
+ * disagree on which property carries the discriminator.
726
+ */
727
+ function detectConstPropertyDiscriminator(variants) {
728
+ if (variants.length < 2) return null;
729
+ let candidateProperty = null;
730
+ const candidates = Object.keys(variants[0]?.properties ?? {});
731
+ for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
732
+ candidateProperty = propName;
733
+ break;
734
+ }
735
+ if (!candidateProperty) return null;
736
+ const mapping = {};
737
+ for (const v of variants) {
738
+ const value = getConstPropertyValue(v, candidateProperty);
739
+ if (value === null) return null;
740
+ const variantName = resolveVariantModelName(v);
741
+ if (!variantName) return null;
742
+ mapping[value] = variantName;
743
+ }
744
+ return {
745
+ property: candidateProperty,
746
+ mapping
747
+ };
748
+ }
749
+ function getConstPropertyValue(schema, property) {
750
+ const prop = schema.properties?.[property];
751
+ if (!prop) return null;
752
+ if (typeof prop.const === "string") return prop.const;
753
+ if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
754
+ return null;
755
+ }
756
+ function resolveVariantModelName(schema) {
757
+ if (schema.$ref) return resolveSchemaName(schema.$ref.replace(/^#\/components\/schemas\//, ""));
758
+ const title = typeof schema["title"] === "string" ? schema["title"] : null;
759
+ if (title) return resolveSchemaName(title);
760
+ return null;
761
+ }
682
762
  function extractEnum(name, schema) {
683
763
  return {
684
764
  name,
@@ -902,6 +982,15 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
902
982
  kind: "nullable",
903
983
  inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
904
984
  };
985
+ const literalStrings = collectLiteralStringConsts(nonNullVariants);
986
+ if (literalStrings !== null && literalStrings.length >= 2) {
987
+ const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
988
+ return nullVariant ? {
989
+ kind: "nullable",
990
+ inner: enumRef
991
+ } : enumRef;
992
+ }
993
+ const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
905
994
  const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
906
995
  const hasNull = !!nullVariant;
907
996
  const union = {
@@ -911,7 +1000,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
911
1000
  ...schema.discriminator ? { discriminator: {
912
1001
  property: schema.discriminator.propertyName,
913
1002
  mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
914
- } } : {}
1003
+ } } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
915
1004
  };
916
1005
  return hasNull ? {
917
1006
  kind: "nullable",
@@ -2598,6 +2687,41 @@ function findPrecedingKdoc(children, index, source) {
2598
2687
  }
2599
2688
  return null;
2600
2689
  }
2690
+ function extractKotlinClassMembers(classBody, source) {
2691
+ const members = [];
2692
+ const children = classBody.namedChildren;
2693
+ for (let i = 0; i < children.length; i++) {
2694
+ const child = children[i];
2695
+ let memberName = null;
2696
+ if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
2697
+ else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
2698
+ else if (child.type === "companion_object") memberName = "companion";
2699
+ if (!memberName) continue;
2700
+ let endIdx = child.endIndex;
2701
+ if (child.type === "property_declaration") while (i + 1 < children.length) {
2702
+ const next = children[i + 1];
2703
+ if (next.type === "getter" || next.type === "setter") {
2704
+ endIdx = next.endIndex;
2705
+ i++;
2706
+ } else break;
2707
+ }
2708
+ let startIdx = child.startIndex;
2709
+ for (let k = i - 1; k >= 0; k--) {
2710
+ const prev = children[k];
2711
+ if (prev.type === "multiline_comment") {
2712
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
2713
+ break;
2714
+ }
2715
+ if (prev.type === "line_comment") continue;
2716
+ break;
2717
+ }
2718
+ members.push({
2719
+ key: memberName,
2720
+ text: source.slice(startIdx, endIdx)
2721
+ });
2722
+ }
2723
+ return members;
2724
+ }
2601
2725
  const kotlinMergeAdapter = {
2602
2726
  language: "kotlin",
2603
2727
  grammarModule: "tree-sitter-kotlin",
@@ -2638,6 +2762,33 @@ const kotlinMergeAdapter = {
2638
2762
  renderImports(imports) {
2639
2763
  return imports.map((entry) => entry.text);
2640
2764
  },
2765
+ shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
2766
+ const newText = newMembers.map((m) => m.text).join("\n");
2767
+ for (const match of newText.matchAll(/this\.(\w+)/g)) {
2768
+ const propName = match[1];
2769
+ if (propName && !existingMemberKeys.has(propName)) return true;
2770
+ }
2771
+ return false;
2772
+ },
2773
+ extractMembers(tree, source) {
2774
+ const result = /* @__PURE__ */ new Map();
2775
+ for (const child of tree.rootNode.children) {
2776
+ if (child.type !== "class_declaration") continue;
2777
+ const nameNode = child.children.find((c) => c.type === "type_identifier");
2778
+ if (!nameNode) continue;
2779
+ const body = child.children.find((c) => c.type === "class_body");
2780
+ if (!body) continue;
2781
+ const members = extractKotlinClassMembers(body, source);
2782
+ const firstMember = body.firstNamedChild;
2783
+ const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
2784
+ result.set(nameNode.text, {
2785
+ members,
2786
+ bodyEndLine: body.endPosition.row,
2787
+ memberIndent
2788
+ });
2789
+ }
2790
+ return result;
2791
+ },
2641
2792
  extractDocstrings(tree, source) {
2642
2793
  const result = /* @__PURE__ */ new Map();
2643
2794
  const rootChildren = tree.rootNode.children;
@@ -3687,10 +3838,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3687
3838
  if (newMembers.length > 0) {
3688
3839
  if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
3689
3840
  const indent = existSymbol.memberIndent ?? " ";
3690
- const insertText = newMembers.map((m) => indent + m.text).join("\n");
3841
+ const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
3691
3842
  insertions.push({
3692
3843
  line: existSymbol.bodyEndLine,
3693
- text: insertText
3844
+ text: "\n" + insertText
3694
3845
  });
3695
3846
  deepAdded += newMembers.length;
3696
3847
  }
@@ -4251,7 +4402,7 @@ function deriveMethodName(op, _service) {
4251
4402
  const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
4252
4403
  if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
4253
4404
  }
4254
- const noun = trailingParam ? singularize(resource) : resource;
4405
+ const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
4255
4406
  return `${verb}_${verb === "list" ? resource : noun}`;
4256
4407
  }
4257
4408
  /**
@@ -4277,7 +4428,7 @@ function resolveOperations(spec, hints, mountRules) {
4277
4428
  defaults: sh.defaults ?? {},
4278
4429
  inferFromClient: sh.inferFromClient ?? [],
4279
4430
  exposedParams: sh.exposedParams ?? [],
4280
- optionalParams: [],
4431
+ optionalParams: sh.optionalParams ?? [],
4281
4432
  responseModelName: resolveResponseModelName(op)
4282
4433
  }));
4283
4434
  resolved.push({
@@ -4287,7 +4438,8 @@ function resolveOperations(spec, hints, mountRules) {
4287
4438
  mountOn,
4288
4439
  wrappers,
4289
4440
  defaults: hint?.defaults ?? {},
4290
- inferFromClient: hint?.inferFromClient ?? []
4441
+ inferFromClient: hint?.inferFromClient ?? [],
4442
+ urlBuilder: hint?.urlBuilder ?? false
4291
4443
  });
4292
4444
  }
4293
4445
  return resolved;
@@ -4310,7 +4462,9 @@ function buildEmitterContext(spec, options) {
4310
4462
  outputDir: options.outputDir,
4311
4463
  apiSurface: options.apiSurface,
4312
4464
  overlayLookup: options.overlayLookup,
4313
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
4465
+ resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
4466
+ targetDir: options.target,
4467
+ priorTargetManifestPaths: options.priorTargetManifestPaths
4314
4468
  };
4315
4469
  }
4316
4470
  /**
@@ -4429,7 +4583,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
4429
4583
  */
4430
4584
  function isRootFile(filePath) {
4431
4585
  if (filePath.includes("/fixtures/")) return false;
4432
- if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return false;
4586
+ if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
4433
4587
  if (filePath.includes("/serializers/")) return false;
4434
4588
  if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
4435
4589
  return true;
@@ -4566,7 +4720,10 @@ async function integrateGeneratedFiles(opts) {
4566
4720
  for (const filePath of removals) try {
4567
4721
  await fs.unlink(filePath);
4568
4722
  } catch {}
4569
- return result;
4723
+ return {
4724
+ ...result,
4725
+ emittedPaths: resolved.map((f) => f.path)
4726
+ };
4570
4727
  }
4571
4728
  //#endregion
4572
4729
  //#region src/engine/formatter.ts
@@ -4601,9 +4758,141 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
4601
4758
  }
4602
4759
  }
4603
4760
  //#endregion
4761
+ //#region src/engine/manifest.ts
4762
+ /**
4763
+ * Prune-manifest support for `oagen generate`.
4764
+ *
4765
+ * The manifest records every auto-generated file emitted on the previous run,
4766
+ * relative to the directory it lives in (either the `--output` standalone dir
4767
+ * or the `--target` integration dir). On the next run we diff the previous
4768
+ * list against the current emission set and delete anything that's no longer
4769
+ * produced — preventing stale-file accumulation across regens.
4770
+ *
4771
+ * Design notes:
4772
+ * - The manifest is versioned so future format changes can be handled safely.
4773
+ * - Paths are stored sorted for deterministic diffs in git.
4774
+ * - Deletion is gated on a header guard when one is available: we only remove
4775
+ * files whose contents start with the auto-generated header. This protects
4776
+ * a hand-maintained file that happens to share a name with a previously
4777
+ * generated one (extremely rare, but worth catching).
4778
+ * - On first adoption (no previous manifest), pruning is skipped and a
4779
+ * one-line notice is printed — no surprise deletions.
4780
+ */
4781
+ const MANIFEST_FILENAME = ".oagen-manifest.json";
4782
+ const MANIFEST_VERSION = 1;
4783
+ /** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
4784
+ async function readManifest(dir) {
4785
+ const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
4786
+ let raw;
4787
+ try {
4788
+ raw = await fs.readFile(manifestPath, "utf-8");
4789
+ } catch {
4790
+ return null;
4791
+ }
4792
+ try {
4793
+ const parsed = JSON.parse(raw);
4794
+ if (typeof parsed.version !== "number" || typeof parsed.language !== "string" || typeof parsed.generatedAt !== "string" || !Array.isArray(parsed.files) || !parsed.files.every((p) => typeof p === "string")) return null;
4795
+ if (parsed.version > MANIFEST_VERSION) {
4796
+ console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
4797
+ return null;
4798
+ }
4799
+ return parsed;
4800
+ } catch {
4801
+ return null;
4802
+ }
4803
+ }
4804
+ /** Write `.oagen-manifest.json` to a directory with sorted paths. */
4805
+ async function writeManifest(dir, opts) {
4806
+ const manifest = {
4807
+ version: MANIFEST_VERSION,
4808
+ language: opts.language,
4809
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4810
+ files: [...new Set(opts.files)].sort()
4811
+ };
4812
+ const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
4813
+ await fs.mkdir(dir, { recursive: true });
4814
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
4815
+ }
4816
+ /** Return the set of previous paths no longer present in the current emission. */
4817
+ function computeStalePaths(prev, currentPaths) {
4818
+ const current = new Set(currentPaths);
4819
+ return prev.files.filter((p) => !current.has(p)).sort();
4820
+ }
4821
+ /**
4822
+ * Delete stale files from `dir`.
4823
+ *
4824
+ * When `header` is provided, a file is only deleted if its contents start with
4825
+ * that header — so hand-maintained files that somehow collide with a previously
4826
+ * generated path can't be clobbered. When `header` is omitted, the guard is
4827
+ * skipped (useful for non-source artifacts like fixture JSON).
4828
+ *
4829
+ * Returns lists for reporting. Empty parent directories are removed after
4830
+ * deletion, up to (but not including) `dir` itself.
4831
+ */
4832
+ async function pruneStaleFiles(dir, paths, opts = {}) {
4833
+ const pruned = [];
4834
+ const preserved = [];
4835
+ const missing = [];
4836
+ for (const relPath of paths) {
4837
+ const fullPath = path$1.join(dir, relPath);
4838
+ let content;
4839
+ try {
4840
+ content = await fs.readFile(fullPath, "utf-8");
4841
+ } catch {
4842
+ missing.push(relPath);
4843
+ continue;
4844
+ }
4845
+ if (opts.header && !content.startsWith(opts.header)) {
4846
+ preserved.push(relPath);
4847
+ continue;
4848
+ }
4849
+ try {
4850
+ await fs.unlink(fullPath);
4851
+ pruned.push(relPath);
4852
+ } catch {
4853
+ missing.push(relPath);
4854
+ continue;
4855
+ }
4856
+ await removeEmptyParents(path$1.dirname(fullPath), dir);
4857
+ }
4858
+ return {
4859
+ pruned,
4860
+ preserved,
4861
+ missing
4862
+ };
4863
+ }
4864
+ /**
4865
+ * Walk up from `startDir` removing empty directories until we hit `stopDir`
4866
+ * (exclusive) or a non-empty directory. Silently ignores errors.
4867
+ */
4868
+ async function removeEmptyParents(startDir, stopDir) {
4869
+ let current = path$1.resolve(startDir);
4870
+ const stop = path$1.resolve(stopDir);
4871
+ while (current.startsWith(stop + path$1.sep) && current !== stop) {
4872
+ let entries;
4873
+ try {
4874
+ entries = await fs.readdir(current);
4875
+ } catch {
4876
+ return;
4877
+ }
4878
+ if (entries.length > 0) return;
4879
+ try {
4880
+ await fs.rmdir(current);
4881
+ } catch {
4882
+ return;
4883
+ }
4884
+ current = path$1.dirname(current);
4885
+ }
4886
+ }
4887
+ //#endregion
4604
4888
  //#region src/engine/orchestrator.ts
4605
4889
  async function generate(spec, emitter, options) {
4606
- const { files: withHeaders, header } = generateFiles(spec, emitter, options);
4890
+ const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
4891
+ const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
4892
+ const { files: withHeaders, header } = generateFiles(spec, emitter, {
4893
+ ...options,
4894
+ priorTargetManifestPaths
4895
+ });
4607
4896
  if (options.dryRun) {
4608
4897
  if (options.target) {
4609
4898
  console.log(`\nTarget integration (${options.target}):`);
@@ -4611,13 +4900,25 @@ async function generate(spec, emitter, options) {
4611
4900
  }
4612
4901
  return withHeaders;
4613
4902
  }
4903
+ const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
4614
4904
  const writeResult = await writeFiles(withHeaders, options.outputDir, {
4615
4905
  language: emitter.language,
4616
4906
  header
4617
4907
  });
4618
4908
  if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
4619
4909
  if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
4910
+ const outputEmittedPaths = withHeaders.map((f) => f.path);
4911
+ await applyManifestPrune({
4912
+ dir: options.outputDir,
4913
+ label: "Output",
4914
+ prevManifest: outputPrevManifest,
4915
+ currentPaths: outputEmittedPaths,
4916
+ language: emitter.language,
4917
+ header,
4918
+ noPrune: options.noPrune
4919
+ });
4620
4920
  if (options.target) {
4921
+ const targetPrevManifest = targetManifestForCtx;
4621
4922
  const targetResult = await integrateGeneratedFiles({
4622
4923
  files: withHeaders,
4623
4924
  language: emitter.language,
@@ -4627,6 +4928,15 @@ async function generate(spec, emitter, options) {
4627
4928
  if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
4628
4929
  if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
4629
4930
  if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
4931
+ await applyManifestPrune({
4932
+ dir: options.target,
4933
+ label: "Target",
4934
+ prevManifest: targetPrevManifest,
4935
+ currentPaths: targetResult.emittedPaths,
4936
+ language: emitter.language,
4937
+ header,
4938
+ noPrune: options.noPrune
4939
+ });
4630
4940
  const allTargetFiles = [
4631
4941
  ...targetResult.written,
4632
4942
  ...targetResult.merged,
@@ -4636,6 +4946,40 @@ async function generate(spec, emitter, options) {
4636
4946
  }
4637
4947
  return withHeaders;
4638
4948
  }
4949
+ /**
4950
+ * Prune files recorded in the previous manifest but absent from the current emission,
4951
+ * then write the fresh manifest. Safe on first adoption (no previous manifest).
4952
+ */
4953
+ async function applyManifestPrune(opts) {
4954
+ if (opts.noPrune) {
4955
+ await writeManifest(opts.dir, {
4956
+ language: opts.language,
4957
+ files: opts.currentPaths
4958
+ });
4959
+ return;
4960
+ }
4961
+ if (!opts.prevManifest) {
4962
+ console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
4963
+ await writeManifest(opts.dir, {
4964
+ language: opts.language,
4965
+ files: opts.currentPaths
4966
+ });
4967
+ return;
4968
+ }
4969
+ const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
4970
+ if (stale.length > 0) {
4971
+ const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
4972
+ if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
4973
+ if (preserved.length > 0) {
4974
+ console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
4975
+ for (const p of preserved) console.log(` ${p}`);
4976
+ }
4977
+ }
4978
+ await writeManifest(opts.dir, {
4979
+ language: opts.language,
4980
+ files: opts.currentPaths
4981
+ });
4982
+ }
4639
4983
  //#endregion
4640
4984
  //#region src/engine/registry.ts
4641
4985
  const emitters = /* @__PURE__ */ new Map();
@@ -5320,7 +5664,8 @@ async function generateCommand(opts) {
5320
5664
  apiSurface,
5321
5665
  overlayLookup,
5322
5666
  operationHints: opts.operationHints,
5323
- mountRules: opts.mountRules
5667
+ mountRules: opts.mountRules,
5668
+ noPrune: opts.prune === false
5324
5669
  });
5325
5670
  if (opts.dryRun) for (const f of files) console.log(f.path);
5326
5671
  else {
@@ -7036,7 +7381,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
7036
7381
  }
7037
7382
  return parseCommand(opts);
7038
7383
  });
7039
- 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("--manifest <path>", "Path to smoke-manifest.json for method overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").action((opts) => {
7384
+ 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("--manifest <path>", "Path to smoke-manifest.json for method 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) => {
7040
7385
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
7041
7386
  if (!opts.spec) {
7042
7387
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");