@workos/oagen 0.5.0 → 0.7.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.
@@ -600,7 +600,7 @@ function findSchemaByName(pascalName, schemas) {
600
600
  for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
601
601
  return null;
602
602
  }
603
- /** Walk into a schema to find a field's sub-schema, resolving allOf. */
603
+ /** Walk into a schema to find a field's sub-schema, resolving allOf and oneOf/anyOf. */
604
604
  function findNestedFieldSchema(fieldName, parentSchema, schemas) {
605
605
  if (parentSchema.properties?.[fieldName]) return parentSchema.properties[fieldName];
606
606
  if (parentSchema.allOf) for (const sub of parentSchema.allOf) {
@@ -612,6 +612,15 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
612
612
  }
613
613
  if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
614
614
  }
615
+ for (const variant of [...parentSchema.oneOf ?? [], ...parentSchema.anyOf ?? []]) {
616
+ let resolved = variant;
617
+ if (variant.$ref) {
618
+ const segments = variant.$ref.split("/");
619
+ const refName = segments[segments.length - 1];
620
+ if (refName && schemas[refName]) resolved = schemas[refName];
621
+ }
622
+ if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
623
+ }
615
624
  return null;
616
625
  }
617
626
  /** Extract a model (and nested models) from an inline field schema. */
@@ -679,6 +688,86 @@ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet)
679
688
  example: fieldSchema.example
680
689
  };
681
690
  }
691
+ /**
692
+ * If every non-null oneOf/anyOf variant is a string-valued const, return the
693
+ * list of those const values so the union can collapse into a single enum.
694
+ * Returns null when any variant isn't a pure string-const (model ref,
695
+ * number const, nullable, etc.).
696
+ */
697
+ function collectLiteralStringConsts(variants) {
698
+ if (variants.length === 0) return null;
699
+ const values = [];
700
+ for (const v of variants) {
701
+ if (typeof v.const === "string") {
702
+ values.push(v.const);
703
+ continue;
704
+ }
705
+ if (Array.isArray(v.enum) && v.enum.length === 1 && typeof v.enum[0] === "string") {
706
+ values.push(v.enum[0]);
707
+ continue;
708
+ }
709
+ return null;
710
+ }
711
+ return values;
712
+ }
713
+ /** Build an EnumRef the same way the regular enum path does for naming. */
714
+ function buildSyntheticEnumRef(values, contextName, parentModelName) {
715
+ const baseName = toPascalCase(contextName ?? "UnknownEnum");
716
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
717
+ return {
718
+ kind: "enum",
719
+ name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
720
+ values
721
+ };
722
+ }
723
+ /**
724
+ * Detect an implicit discriminator on an oneOf where every variant is an
725
+ * object schema that pins the same property to a const value. Returns
726
+ * { property, mapping } so the IR can represent the union as discriminated
727
+ * without needing an explicit `discriminator:` key in the spec.
728
+ *
729
+ * Both shapes are supported:
730
+ * { properties: { event: { const: "x" } }, required: ["event"] }
731
+ * { properties: { event: { type: "string", enum: ["x"] } } }
732
+ *
733
+ * Returns null when any variant doesn't fit the pattern or when variants
734
+ * disagree on which property carries the discriminator.
735
+ */
736
+ function detectConstPropertyDiscriminator(variants) {
737
+ if (variants.length < 2) return null;
738
+ let candidateProperty = null;
739
+ const candidates = Object.keys(variants[0]?.properties ?? {});
740
+ for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
741
+ candidateProperty = propName;
742
+ break;
743
+ }
744
+ if (!candidateProperty) return null;
745
+ const mapping = {};
746
+ for (const v of variants) {
747
+ const value = getConstPropertyValue(v, candidateProperty);
748
+ if (value === null) return null;
749
+ const variantName = resolveVariantModelName(v);
750
+ if (!variantName) return null;
751
+ mapping[value] = variantName;
752
+ }
753
+ return {
754
+ property: candidateProperty,
755
+ mapping
756
+ };
757
+ }
758
+ function getConstPropertyValue(schema, property) {
759
+ const prop = schema.properties?.[property];
760
+ if (!prop) return null;
761
+ if (typeof prop.const === "string") return prop.const;
762
+ if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
763
+ return null;
764
+ }
765
+ function resolveVariantModelName(schema) {
766
+ if (schema.$ref) return resolveSchemaName(schema.$ref.replace(/^#\/components\/schemas\//, ""));
767
+ const title = typeof schema["title"] === "string" ? schema["title"] : null;
768
+ if (title) return resolveSchemaName(title);
769
+ return null;
770
+ }
682
771
  function extractEnum(name, schema) {
683
772
  return {
684
773
  name,
@@ -697,6 +786,19 @@ function extractModel(name, schema, schemas) {
697
786
  if (!fieldSchema) continue;
698
787
  fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
699
788
  }
789
+ if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
790
+ const variants = schema.oneOf ?? schema.anyOf ?? [];
791
+ const emptyRequired = /* @__PURE__ */ new Set();
792
+ const seenFieldNames = /* @__PURE__ */ new Set();
793
+ for (const variant of variants) {
794
+ if (!variant.properties) continue;
795
+ for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
796
+ if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
797
+ seenFieldNames.add(fieldName);
798
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
799
+ }
800
+ }
801
+ }
700
802
  if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
701
803
  if (Object.keys(schema.additionalProperties).length > 0) {
702
804
  const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
@@ -736,6 +838,18 @@ function extractAllOfModel(name, schema, schemas) {
736
838
  });
737
839
  if (f.required) requiredSet.add(f.name);
738
840
  }
841
+ } else if (resolved.oneOf || resolved.anyOf) {
842
+ const variants = resolved.oneOf ?? resolved.anyOf ?? [];
843
+ const emptyRequired = /* @__PURE__ */ new Set();
844
+ const seenFieldNames = new Set(fields.map((f) => f.name));
845
+ for (const variant of variants) {
846
+ if (!variant.properties) continue;
847
+ for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
848
+ if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
849
+ seenFieldNames.add(fieldName);
850
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
851
+ }
852
+ }
739
853
  } else {
740
854
  if (resolved.required) for (const r of resolved.required) requiredSet.add(r);
741
855
  if (resolved.properties) {
@@ -902,6 +1016,15 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
902
1016
  kind: "nullable",
903
1017
  inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
904
1018
  };
1019
+ const literalStrings = collectLiteralStringConsts(nonNullVariants);
1020
+ if (literalStrings !== null && literalStrings.length >= 2) {
1021
+ const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
1022
+ return nullVariant ? {
1023
+ kind: "nullable",
1024
+ inner: enumRef
1025
+ } : enumRef;
1026
+ }
1027
+ const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
905
1028
  const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
906
1029
  const hasNull = !!nullVariant;
907
1030
  const union = {
@@ -911,7 +1034,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
911
1034
  ...schema.discriminator ? { discriminator: {
912
1035
  property: schema.discriminator.propertyName,
913
1036
  mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
914
- } } : {}
1037
+ } } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
915
1038
  };
916
1039
  return hasNull ? {
917
1040
  kind: "nullable",
@@ -1583,6 +1706,94 @@ function inferDeeperContext(path) {
1583
1706
  if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
1584
1707
  return null;
1585
1708
  }
1709
+ /**
1710
+ * Extract mutually-exclusive parameter groups from the `x-mutually-exclusive-parameter-groups`
1711
+ * operation extension. Cross-references grouped parameter names against the already-extracted
1712
+ * IR parameter arrays so emitters get object-identity references.
1713
+ */
1714
+ function extractParameterGroups(op, allIRParams, operationContext) {
1715
+ const raw = op["x-mutually-exclusive-parameter-groups"];
1716
+ if (!raw || typeof raw !== "object") return void 0;
1717
+ const paramByName = /* @__PURE__ */ new Map();
1718
+ for (const p of allIRParams) paramByName.set(p.name, p);
1719
+ const groups = [];
1720
+ for (const [groupName, groupDef] of Object.entries(raw)) {
1721
+ if (!groupDef || typeof groupDef !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: expected an object with "optional" and "variants".`);
1722
+ if (typeof groupDef.optional !== "boolean") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.optional in ${operationContext}: expected a boolean.`);
1723
+ if (!groupDef.variants || typeof groupDef.variants !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants in ${operationContext}: expected an object mapping variant names to parameter name arrays.`);
1724
+ if (Object.keys(groupDef.variants).length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: group has zero variants.`);
1725
+ const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
1726
+ if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
1727
+ return {
1728
+ name: variantName,
1729
+ parameters: paramNames.map((pName) => {
1730
+ const irParam = paramByName.get(pName);
1731
+ if (!irParam) throw new Error(`x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} references parameter "${pName}" which does not exist in the operation's parameters[] (${operationContext}).`);
1732
+ return irParam;
1733
+ })
1734
+ };
1735
+ });
1736
+ groups.push({
1737
+ name: groupName,
1738
+ optional: groupDef.optional,
1739
+ variants
1740
+ });
1741
+ }
1742
+ return groups.length > 0 ? groups : void 0;
1743
+ }
1744
+ /**
1745
+ * Extract mutually-exclusive body parameter groups from the
1746
+ * `x-mutually-exclusive-body-groups` operation extension. Unlike query
1747
+ * parameter groups (whose parameters live in operation.queryParams), body
1748
+ * group parameters are synthetic — built from the oneOf variant schemas in
1749
+ * the request body. Emitters use the group's presence to generate sum-type
1750
+ * interfaces and custom JSON marshalling instead of flat optional fields.
1751
+ */
1752
+ function extractBodyParameterGroups(op, operationContext) {
1753
+ const raw = op["x-mutually-exclusive-body-groups"];
1754
+ if (!raw || typeof raw !== "object") return void 0;
1755
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
1756
+ const variantFieldSchemas = /* @__PURE__ */ new Map();
1757
+ if (bodySchema) {
1758
+ for (const sub of bodySchema.allOf ?? []) for (const variant of sub.oneOf ?? []) if (variant.properties) {
1759
+ for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
1760
+ }
1761
+ for (const variant of bodySchema.oneOf ?? []) if (variant.properties) {
1762
+ for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
1763
+ }
1764
+ }
1765
+ const groups = [];
1766
+ for (const [groupName, groupDef] of Object.entries(raw)) {
1767
+ if (!groupDef || typeof groupDef !== "object") continue;
1768
+ if (typeof groupDef.optional !== "boolean") continue;
1769
+ if (!groupDef.variants || typeof groupDef.variants !== "object") continue;
1770
+ if (Object.keys(groupDef.variants).length === 0) continue;
1771
+ const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
1772
+ if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-body-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
1773
+ return {
1774
+ name: variantName,
1775
+ parameters: paramNames.map((pName) => {
1776
+ const fieldSchema = variantFieldSchemas.get(pName);
1777
+ return {
1778
+ name: pName,
1779
+ type: fieldSchema ? schemaToTypeRef(fieldSchema, toPascalCase(pName)) : {
1780
+ kind: "primitive",
1781
+ type: "string"
1782
+ },
1783
+ required: false,
1784
+ description: fieldSchema?.description
1785
+ };
1786
+ })
1787
+ };
1788
+ });
1789
+ groups.push({
1790
+ name: groupName,
1791
+ optional: groupDef.optional,
1792
+ variants
1793
+ });
1794
+ }
1795
+ return groups.length > 0 ? groups : void 0;
1796
+ }
1586
1797
  function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
1587
1798
  const allParams = [...pathLevelParams, ...op.parameters ?? []];
1588
1799
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
@@ -1608,6 +1819,16 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
1608
1819
  };
1609
1820
  } else if (paginationFromParams) pagination = paginationFromParams;
1610
1821
  const security = extractOperationSecurity(op.security);
1822
+ const allIRParams = [
1823
+ ...pathParams,
1824
+ ...queryParams,
1825
+ ...headerParams,
1826
+ ...cookieParams
1827
+ ];
1828
+ const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
1829
+ const queryParamGroups = extractParameterGroups(op, allIRParams, opLabel);
1830
+ const bodyParamGroups = extractBodyParameterGroups(op, opLabel);
1831
+ const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
1611
1832
  return {
1612
1833
  operation: {
1613
1834
  name: inferOperationName(method, path, op.operationId, operationIdTransform),
@@ -1627,7 +1848,8 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
1627
1848
  injectIdempotencyKey: hasIdempotencyHeader,
1628
1849
  deprecated: op.deprecated || void 0,
1629
1850
  async: op["x-oagen-async"],
1630
- security
1851
+ security,
1852
+ parameterGroups
1631
1853
  },
1632
1854
  inlineModels
1633
1855
  };
@@ -2598,6 +2820,41 @@ function findPrecedingKdoc(children, index, source) {
2598
2820
  }
2599
2821
  return null;
2600
2822
  }
2823
+ function extractKotlinClassMembers(classBody, source) {
2824
+ const members = [];
2825
+ const children = classBody.namedChildren;
2826
+ for (let i = 0; i < children.length; i++) {
2827
+ const child = children[i];
2828
+ let memberName = null;
2829
+ if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
2830
+ else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
2831
+ else if (child.type === "companion_object") memberName = "companion";
2832
+ if (!memberName) continue;
2833
+ let endIdx = child.endIndex;
2834
+ if (child.type === "property_declaration") while (i + 1 < children.length) {
2835
+ const next = children[i + 1];
2836
+ if (next.type === "getter" || next.type === "setter") {
2837
+ endIdx = next.endIndex;
2838
+ i++;
2839
+ } else break;
2840
+ }
2841
+ let startIdx = child.startIndex;
2842
+ for (let k = i - 1; k >= 0; k--) {
2843
+ const prev = children[k];
2844
+ if (prev.type === "multiline_comment") {
2845
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
2846
+ break;
2847
+ }
2848
+ if (prev.type === "line_comment") continue;
2849
+ break;
2850
+ }
2851
+ members.push({
2852
+ key: memberName,
2853
+ text: source.slice(startIdx, endIdx)
2854
+ });
2855
+ }
2856
+ return members;
2857
+ }
2601
2858
  const kotlinMergeAdapter = {
2602
2859
  language: "kotlin",
2603
2860
  grammarModule: "tree-sitter-kotlin",
@@ -2638,6 +2895,33 @@ const kotlinMergeAdapter = {
2638
2895
  renderImports(imports) {
2639
2896
  return imports.map((entry) => entry.text);
2640
2897
  },
2898
+ shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
2899
+ const newText = newMembers.map((m) => m.text).join("\n");
2900
+ for (const match of newText.matchAll(/this\.(\w+)/g)) {
2901
+ const propName = match[1];
2902
+ if (propName && !existingMemberKeys.has(propName)) return true;
2903
+ }
2904
+ return false;
2905
+ },
2906
+ extractMembers(tree, source) {
2907
+ const result = /* @__PURE__ */ new Map();
2908
+ for (const child of tree.rootNode.children) {
2909
+ if (child.type !== "class_declaration") continue;
2910
+ const nameNode = child.children.find((c) => c.type === "type_identifier");
2911
+ if (!nameNode) continue;
2912
+ const body = child.children.find((c) => c.type === "class_body");
2913
+ if (!body) continue;
2914
+ const members = extractKotlinClassMembers(body, source);
2915
+ const firstMember = body.firstNamedChild;
2916
+ const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
2917
+ result.set(nameNode.text, {
2918
+ members,
2919
+ bodyEndLine: body.endPosition.row,
2920
+ memberIndent
2921
+ });
2922
+ }
2923
+ return result;
2924
+ },
2641
2925
  extractDocstrings(tree, source) {
2642
2926
  const result = /* @__PURE__ */ new Map();
2643
2927
  const rootChildren = tree.rootNode.children;
@@ -2820,6 +3104,7 @@ const nodeMergeAdapter = {
2820
3104
  const newText = newMembers.map((m) => m.text).join("\n");
2821
3105
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
2822
3106
  const propName = match[1];
3107
+ if (propName === "workos") continue;
2823
3108
  if (propName && !existingMemberKeys.has(propName)) return true;
2824
3109
  }
2825
3110
  return false;
@@ -3476,6 +3761,40 @@ function buildIgnoredSymbolNames(docstrings, regions) {
3476
3761
  for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
3477
3762
  return ignored;
3478
3763
  }
3764
+ function clampIndex(value, contentLength) {
3765
+ if (!Number.isFinite(value)) return 0;
3766
+ if (value < 0) return 0;
3767
+ if (value > contentLength) return contentLength;
3768
+ return value;
3769
+ }
3770
+ /**
3771
+ * Resolve the declaration line start for insertion using the current content,
3772
+ * instead of relying on parser column arithmetic. This avoids misplaced inserts
3773
+ * when parser columns/offsets drift.
3774
+ */
3775
+ function declarationLineStart(content, declStartIndex) {
3776
+ const idx = clampIndex(declStartIndex, content.length);
3777
+ const prevNewline = content.lastIndexOf("\n", Math.max(0, idx - 1));
3778
+ return prevNewline === -1 ? 0 : prevNewline + 1;
3779
+ }
3780
+ /**
3781
+ * Resolve a docstring range by searching for the nearest matching text before
3782
+ * the declaration start. Falls back to parser-provided offsets when search fails.
3783
+ */
3784
+ function resolveDocRange(content, docText, declStartIndex, fallbackStart, fallbackEnd) {
3785
+ const safeDecl = clampIndex(declStartIndex, content.length);
3786
+ const safeFallbackStart = clampIndex(fallbackStart, content.length);
3787
+ const safeFallbackEnd = clampIndex(fallbackEnd, content.length);
3788
+ const found = content.lastIndexOf(docText, safeDecl);
3789
+ if (found !== -1) return {
3790
+ start: found,
3791
+ end: found + docText.length
3792
+ };
3793
+ return {
3794
+ start: safeFallbackStart,
3795
+ end: safeFallbackEnd
3796
+ };
3797
+ }
3479
3798
  /**
3480
3799
  * Check if a tree-sitter grammar is configured for the given language.
3481
3800
  */
@@ -3524,15 +3843,17 @@ async function extractClassEndLines(source, language) {
3524
3843
  if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
3525
3844
  else map.set(name, { bodyEndLine: endRow + 1 });
3526
3845
  }
3527
- for (const child of tree.rootNode.children) {
3528
- if (child.type === "class_definition" || child.type === "class_declaration") processClassNode(child);
3529
- if (child.type === "decorated_definition") {
3530
- for (const inner of child.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
3846
+ function walk(node) {
3847
+ if (node.type === "class" || node.type === "class_definition" || node.type === "class_declaration") processClassNode(node);
3848
+ if (node.type === "decorated_definition") {
3849
+ for (const inner of node.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
3531
3850
  }
3532
- if (child.type === "export_statement") {
3533
- for (const inner of child.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
3851
+ if (node.type === "export_statement") {
3852
+ for (const inner of node.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
3534
3853
  }
3854
+ for (const child of node.namedChildren) walk(child);
3535
3855
  }
3856
+ for (const child of tree.rootNode.children) walk(child);
3536
3857
  return map;
3537
3858
  }
3538
3859
  /**
@@ -3687,10 +4008,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3687
4008
  if (newMembers.length > 0) {
3688
4009
  if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
3689
4010
  const indent = existSymbol.memberIndent ?? " ";
3690
- const insertText = newMembers.map((m) => indent + m.text).join("\n");
4011
+ const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
3691
4012
  insertions.push({
3692
4013
  line: existSymbol.bodyEndLine,
3693
- text: insertText
4014
+ text: "\n" + insertText
3694
4015
  });
3695
4016
  deepAdded += newMembers.length;
3696
4017
  }
@@ -3752,15 +4073,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3752
4073
  if (genDoc) if (existInfo.docstring) {
3753
4074
  if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
3754
4075
  const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
4076
+ const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
3755
4077
  edits.push({
3756
- start: existInfo.docstring.startIndex,
3757
- end: existInfo.docstring.endIndex,
4078
+ start: range.start,
4079
+ end: range.end,
3758
4080
  newText
3759
4081
  });
3760
4082
  docstringUpdates++;
3761
4083
  }
3762
4084
  } else {
3763
- const lineStart = existInfo.declStartIndex - existInfo.declColumn;
4085
+ const lineStart = declarationLineStart(result, existInfo.declStartIndex);
3764
4086
  const indent = " ".repeat(existInfo.declColumn);
3765
4087
  edits.push({
3766
4088
  start: lineStart,
@@ -3778,15 +4100,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3778
4100
  if (existMember.docstring) {
3779
4101
  if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
3780
4102
  const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
4103
+ const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
3781
4104
  edits.push({
3782
- start: existMember.docstring.startIndex,
3783
- end: existMember.docstring.endIndex,
4105
+ start: range.start,
4106
+ end: range.end,
3784
4107
  newText
3785
4108
  });
3786
4109
  docstringUpdates++;
3787
4110
  }
3788
4111
  } else {
3789
- const lineStart = existMember.declStartIndex - existMember.declColumn;
4112
+ const lineStart = declarationLineStart(result, existMember.declStartIndex);
3790
4113
  const indent = " ".repeat(existMember.declColumn);
3791
4114
  edits.push({
3792
4115
  start: lineStart,
@@ -3831,15 +4154,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3831
4154
  if (existMember.docstring.text.includes("@oagen-ignore")) continue;
3832
4155
  if (existMember.docstring.text !== genMember.docstring.text) {
3833
4156
  const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
4157
+ const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
3834
4158
  edits.push({
3835
- start: existMember.docstring.startIndex,
3836
- end: existMember.docstring.endIndex,
4159
+ start: range.start,
4160
+ end: range.end,
3837
4161
  newText
3838
4162
  });
3839
4163
  docstringUpdates++;
3840
4164
  }
3841
4165
  } else {
3842
- const lineStart = existMember.declStartIndex - existMember.declColumn;
4166
+ const lineStart = declarationLineStart(result, existMember.declStartIndex);
3843
4167
  const indent = " ".repeat(existMember.declColumn);
3844
4168
  edits.push({
3845
4169
  start: lineStart,
@@ -4012,7 +4336,7 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
4012
4336
  });
4013
4337
  }
4014
4338
  if (blocks.length === 0) return generatedContent;
4015
- const result = [...generatedLines];
4339
+ const result = stripIgnoreRegions(generatedLines);
4016
4340
  spliceExtraImports(existingLines, result);
4017
4341
  const classEndLines = await extractClassEndLines(result.join("\n"), language);
4018
4342
  const blocksByClass = /* @__PURE__ */ new Map();
@@ -4079,10 +4403,33 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
4079
4403
  for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
4080
4404
  return result.join("\n");
4081
4405
  }
4406
+ /**
4407
+ * Remove every `@oagen-ignore-start`..`@oagen-ignore-end` region from the
4408
+ * supplied lines, collapsing stretches of blank lines that get left behind.
4409
+ * Used to drop generator-emitted seed blocks before splicing the on-disk
4410
+ * versions back in — otherwise every regen doubles up.
4411
+ */
4412
+ function stripIgnoreRegions(lines) {
4413
+ const out = [];
4414
+ let skipping = false;
4415
+ for (const line of lines) {
4416
+ if (!skipping && line.includes("@oagen-ignore-start")) {
4417
+ skipping = true;
4418
+ while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
4419
+ continue;
4420
+ }
4421
+ if (skipping) {
4422
+ if (line.includes("@oagen-ignore-end")) skipping = false;
4423
+ continue;
4424
+ }
4425
+ out.push(line);
4426
+ }
4427
+ return out;
4428
+ }
4082
4429
  /** Scan backwards from `lineIdx` to find the nearest class declaration name. */
4083
4430
  function findContainingClass(lines, lineIdx) {
4084
4431
  for (let j = lineIdx - 1; j >= 0; j--) {
4085
- const m = lines[j].match(/^(?:export\s+)?class\s+(\w+)/);
4432
+ const m = lines[j].match(/^\s*(?:export\s+)?class\s+(\w+)/);
4086
4433
  if (m) return m[1];
4087
4434
  }
4088
4435
  return "";
@@ -4092,7 +4439,7 @@ function findContainingClass(lines, lineIdx) {
4092
4439
  * by scanning backwards from `beforeLine`.
4093
4440
  */
4094
4441
  function findClassDeclarationStart(lines, className, beforeLine) {
4095
- const classPattern = new RegExp(`^(?:export\\s+)?class\\s+${className}\\b`);
4442
+ const classPattern = new RegExp(`^\\s*(?:export\\s+)?class\\s+${className}\\b`);
4096
4443
  for (let i = beforeLine - 1; i >= 0; i--) {
4097
4444
  if (!classPattern.test(lines[i])) continue;
4098
4445
  let start = i;
@@ -4251,7 +4598,7 @@ function deriveMethodName(op, _service) {
4251
4598
  const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
4252
4599
  if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
4253
4600
  }
4254
- const noun = trailingParam ? singularize(resource) : resource;
4601
+ const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
4255
4602
  return `${verb}_${verb === "list" ? resource : noun}`;
4256
4603
  }
4257
4604
  /**
@@ -4277,7 +4624,7 @@ function resolveOperations(spec, hints, mountRules) {
4277
4624
  defaults: sh.defaults ?? {},
4278
4625
  inferFromClient: sh.inferFromClient ?? [],
4279
4626
  exposedParams: sh.exposedParams ?? [],
4280
- optionalParams: [],
4627
+ optionalParams: sh.optionalParams ?? [],
4281
4628
  responseModelName: resolveResponseModelName(op)
4282
4629
  }));
4283
4630
  resolved.push({
@@ -4287,7 +4634,8 @@ function resolveOperations(spec, hints, mountRules) {
4287
4634
  mountOn,
4288
4635
  wrappers,
4289
4636
  defaults: hint?.defaults ?? {},
4290
- inferFromClient: hint?.inferFromClient ?? []
4637
+ inferFromClient: hint?.inferFromClient ?? [],
4638
+ urlBuilder: hint?.urlBuilder ?? false
4291
4639
  });
4292
4640
  }
4293
4641
  return resolved;
@@ -4310,7 +4658,9 @@ function buildEmitterContext(spec, options) {
4310
4658
  outputDir: options.outputDir,
4311
4659
  apiSurface: options.apiSurface,
4312
4660
  overlayLookup: options.overlayLookup,
4313
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
4661
+ resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
4662
+ targetDir: options.target,
4663
+ priorTargetManifestPaths: options.priorTargetManifestPaths
4314
4664
  };
4315
4665
  }
4316
4666
  /**
@@ -4429,7 +4779,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
4429
4779
  */
4430
4780
  function isRootFile(filePath) {
4431
4781
  if (filePath.includes("/fixtures/")) return false;
4432
- if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return false;
4782
+ if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
4433
4783
  if (filePath.includes("/serializers/")) return false;
4434
4784
  if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
4435
4785
  return true;
@@ -4558,7 +4908,8 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
4558
4908
  };
4559
4909
  }
4560
4910
  async function integrateGeneratedFiles(opts) {
4561
- const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.language), opts.targetDir);
4911
+ const mapped = mapFilesForTargetIntegration(opts.files, opts.language);
4912
+ const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapped, opts.language), opts.targetDir);
4562
4913
  const result = await writeFiles(resolved, opts.targetDir, {
4563
4914
  language: opts.language,
4564
4915
  header: opts.header
@@ -4566,7 +4917,10 @@ async function integrateGeneratedFiles(opts) {
4566
4917
  for (const filePath of removals) try {
4567
4918
  await fs.unlink(filePath);
4568
4919
  } catch {}
4569
- return result;
4920
+ return {
4921
+ ...result,
4922
+ emittedPaths: mapped.map((f) => f.path)
4923
+ };
4570
4924
  }
4571
4925
  //#endregion
4572
4926
  //#region src/engine/formatter.ts
@@ -4589,6 +4943,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
4589
4943
  if (!formatCmd) return;
4590
4944
  const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
4591
4945
  const batchSize = formatCmd.batchSize ?? 100;
4946
+ process.stderr.write(`[oagen] formatting ${absolutePaths.length} file(s) with ${formatCmd.cmd} (batch ${batchSize})\n`);
4592
4947
  for (let i = 0; i < absolutePaths.length; i += batchSize) {
4593
4948
  const batch = absolutePaths.slice(i, i + batchSize);
4594
4949
  try {
@@ -4597,13 +4952,148 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
4597
4952
  maxBuffer: 10 * 1024 * 1024,
4598
4953
  timeout: 12e4
4599
4954
  });
4600
- } catch {}
4955
+ } catch (err) {
4956
+ const msg = err instanceof Error ? err.message : String(err);
4957
+ process.stderr.write(`[oagen] formatter batch failed: ${msg}\n`);
4958
+ }
4959
+ }
4960
+ }
4961
+ //#endregion
4962
+ //#region src/engine/manifest.ts
4963
+ /**
4964
+ * Prune-manifest support for `oagen generate`.
4965
+ *
4966
+ * The manifest records every auto-generated file emitted on the previous run,
4967
+ * relative to the directory it lives in (either the `--output` standalone dir
4968
+ * or the `--target` integration dir). On the next run we diff the previous
4969
+ * list against the current emission set and delete anything that's no longer
4970
+ * produced — preventing stale-file accumulation across regens.
4971
+ *
4972
+ * Design notes:
4973
+ * - The manifest is versioned so future format changes can be handled safely.
4974
+ * - Paths are stored sorted for deterministic diffs in git.
4975
+ * - Deletion is gated on a header guard when one is available: we only remove
4976
+ * files whose contents start with the auto-generated header. This protects
4977
+ * a hand-maintained file that happens to share a name with a previously
4978
+ * generated one (extremely rare, but worth catching).
4979
+ * - On first adoption (no previous manifest), pruning is skipped and a
4980
+ * one-line notice is printed — no surprise deletions.
4981
+ */
4982
+ const MANIFEST_FILENAME = ".oagen-manifest.json";
4983
+ const MANIFEST_VERSION = 1;
4984
+ /** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
4985
+ async function readManifest(dir) {
4986
+ const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
4987
+ let raw;
4988
+ try {
4989
+ raw = await fs.readFile(manifestPath, "utf-8");
4990
+ } catch {
4991
+ return null;
4992
+ }
4993
+ try {
4994
+ const parsed = JSON.parse(raw);
4995
+ 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;
4996
+ if (parsed.version > MANIFEST_VERSION) {
4997
+ console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
4998
+ return null;
4999
+ }
5000
+ return parsed;
5001
+ } catch {
5002
+ return null;
5003
+ }
5004
+ }
5005
+ /** Write `.oagen-manifest.json` to a directory with sorted paths. */
5006
+ async function writeManifest(dir, opts) {
5007
+ const manifest = {
5008
+ version: MANIFEST_VERSION,
5009
+ language: opts.language,
5010
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5011
+ files: [...new Set(opts.files)].sort()
5012
+ };
5013
+ const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
5014
+ await fs.mkdir(dir, { recursive: true });
5015
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
5016
+ }
5017
+ /** Return the set of previous paths no longer present in the current emission. */
5018
+ function computeStalePaths(prev, currentPaths) {
5019
+ const current = new Set(currentPaths);
5020
+ return prev.files.filter((p) => !current.has(p)).sort();
5021
+ }
5022
+ /**
5023
+ * Delete stale files from `dir`.
5024
+ *
5025
+ * When `header` is provided, a file is only deleted if its contents start with
5026
+ * that header — so hand-maintained files that somehow collide with a previously
5027
+ * generated path can't be clobbered. When `header` is omitted, the guard is
5028
+ * skipped (useful for non-source artifacts like fixture JSON).
5029
+ *
5030
+ * Returns lists for reporting. Empty parent directories are removed after
5031
+ * deletion, up to (but not including) `dir` itself.
5032
+ */
5033
+ async function pruneStaleFiles(dir, paths, opts = {}) {
5034
+ const pruned = [];
5035
+ const preserved = [];
5036
+ const missing = [];
5037
+ for (const relPath of paths) {
5038
+ const fullPath = path$1.join(dir, relPath);
5039
+ let content;
5040
+ try {
5041
+ content = await fs.readFile(fullPath, "utf-8");
5042
+ } catch {
5043
+ missing.push(relPath);
5044
+ continue;
5045
+ }
5046
+ if (opts.header && !content.startsWith(opts.header)) {
5047
+ preserved.push(relPath);
5048
+ continue;
5049
+ }
5050
+ try {
5051
+ await fs.unlink(fullPath);
5052
+ pruned.push(relPath);
5053
+ } catch {
5054
+ missing.push(relPath);
5055
+ continue;
5056
+ }
5057
+ await removeEmptyParents(path$1.dirname(fullPath), dir);
5058
+ }
5059
+ return {
5060
+ pruned,
5061
+ preserved,
5062
+ missing
5063
+ };
5064
+ }
5065
+ /**
5066
+ * Walk up from `startDir` removing empty directories until we hit `stopDir`
5067
+ * (exclusive) or a non-empty directory. Silently ignores errors.
5068
+ */
5069
+ async function removeEmptyParents(startDir, stopDir) {
5070
+ let current = path$1.resolve(startDir);
5071
+ const stop = path$1.resolve(stopDir);
5072
+ while (current.startsWith(stop + path$1.sep) && current !== stop) {
5073
+ let entries;
5074
+ try {
5075
+ entries = await fs.readdir(current);
5076
+ } catch {
5077
+ return;
5078
+ }
5079
+ if (entries.length > 0) return;
5080
+ try {
5081
+ await fs.rmdir(current);
5082
+ } catch {
5083
+ return;
5084
+ }
5085
+ current = path$1.dirname(current);
4601
5086
  }
4602
5087
  }
4603
5088
  //#endregion
4604
5089
  //#region src/engine/orchestrator.ts
4605
5090
  async function generate(spec, emitter, options) {
4606
- const { files: withHeaders, header } = generateFiles(spec, emitter, options);
5091
+ const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
5092
+ const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
5093
+ const { files: withHeaders, header } = generateFiles(spec, emitter, {
5094
+ ...options,
5095
+ priorTargetManifestPaths
5096
+ });
4607
5097
  if (options.dryRun) {
4608
5098
  if (options.target) {
4609
5099
  console.log(`\nTarget integration (${options.target}):`);
@@ -4611,13 +5101,25 @@ async function generate(spec, emitter, options) {
4611
5101
  }
4612
5102
  return withHeaders;
4613
5103
  }
5104
+ const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
4614
5105
  const writeResult = await writeFiles(withHeaders, options.outputDir, {
4615
5106
  language: emitter.language,
4616
5107
  header
4617
5108
  });
4618
5109
  if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
4619
5110
  if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
5111
+ const outputEmittedPaths = withHeaders.map((f) => f.path);
5112
+ await applyManifestPrune({
5113
+ dir: options.outputDir,
5114
+ label: "Output",
5115
+ prevManifest: outputPrevManifest,
5116
+ currentPaths: outputEmittedPaths,
5117
+ language: emitter.language,
5118
+ header,
5119
+ noPrune: options.noPrune
5120
+ });
4620
5121
  if (options.target) {
5122
+ const targetPrevManifest = targetManifestForCtx;
4621
5123
  const targetResult = await integrateGeneratedFiles({
4622
5124
  files: withHeaders,
4623
5125
  language: emitter.language,
@@ -4627,6 +5129,15 @@ async function generate(spec, emitter, options) {
4627
5129
  if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
4628
5130
  if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
4629
5131
  if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
5132
+ await applyManifestPrune({
5133
+ dir: options.target,
5134
+ label: "Target",
5135
+ prevManifest: targetPrevManifest,
5136
+ currentPaths: targetResult.emittedPaths,
5137
+ language: emitter.language,
5138
+ header,
5139
+ noPrune: options.noPrune
5140
+ });
4630
5141
  const allTargetFiles = [
4631
5142
  ...targetResult.written,
4632
5143
  ...targetResult.merged,
@@ -4636,6 +5147,40 @@ async function generate(spec, emitter, options) {
4636
5147
  }
4637
5148
  return withHeaders;
4638
5149
  }
5150
+ /**
5151
+ * Prune files recorded in the previous manifest but absent from the current emission,
5152
+ * then write the fresh manifest. Safe on first adoption (no previous manifest).
5153
+ */
5154
+ async function applyManifestPrune(opts) {
5155
+ if (opts.noPrune) {
5156
+ await writeManifest(opts.dir, {
5157
+ language: opts.language,
5158
+ files: opts.currentPaths
5159
+ });
5160
+ return;
5161
+ }
5162
+ if (!opts.prevManifest) {
5163
+ console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
5164
+ await writeManifest(opts.dir, {
5165
+ language: opts.language,
5166
+ files: opts.currentPaths
5167
+ });
5168
+ return;
5169
+ }
5170
+ const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
5171
+ if (stale.length > 0) {
5172
+ const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
5173
+ if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
5174
+ if (preserved.length > 0) {
5175
+ console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
5176
+ for (const p of preserved) console.log(` ${p}`);
5177
+ }
5178
+ }
5179
+ await writeManifest(opts.dir, {
5180
+ language: opts.language,
5181
+ files: opts.currentPaths
5182
+ });
5183
+ }
4639
5184
  //#endregion
4640
5185
  //#region src/engine/registry.ts
4641
5186
  const emitters = /* @__PURE__ */ new Map();
@@ -5320,7 +5865,8 @@ async function generateCommand(opts) {
5320
5865
  apiSurface,
5321
5866
  overlayLookup,
5322
5867
  operationHints: opts.operationHints,
5323
- mountRules: opts.mountRules
5868
+ mountRules: opts.mountRules,
5869
+ noPrune: opts.prune === false
5324
5870
  });
5325
5871
  if (opts.dryRun) for (const f of files) console.log(f.path);
5326
5872
  else {
@@ -7036,7 +7582,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
7036
7582
  }
7037
7583
  return parseCommand(opts);
7038
7584
  });
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) => {
7585
+ 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
7586
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
7041
7587
  if (!opts.spec) {
7042
7588
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");