@workos/oagen 0.4.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.
- package/README.md +0 -1
- package/dist/cli/index.mjs +471 -49
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +1 -1
- package/dist/compat/index.mjs +10 -1
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-DcY93sii.mjs → diff-CltJMcwG.mjs} +376 -42
- package/dist/diff-CltJMcwG.mjs.map +1 -0
- package/dist/index.d.mts +45 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +95 -3
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +93 -2
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +2 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +93 -2
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-D1tMqLVB.d.mts → types-C7g5sLuX.d.mts} +8 -1
- package/dist/types-C7g5sLuX.d.mts.map +1 -0
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.mjs +1 -1
- package/docs/agents/architecture.md +1 -1
- package/docs/architecture/emitter-contract.md +1 -2
- package/docs/architecture/pipeline.md +0 -1
- package/docs/core/minimal-emitter.md +0 -4
- package/docs/core/quickstart.md +0 -1
- package/examples/reference-emitter/README.md +0 -1
- package/examples/reference-emitter/src/emitter.ts +0 -5
- package/package.json +1 -1
- package/dist/diff-DcY93sii.mjs.map +0 -1
- package/dist/types-D1tMqLVB.d.mts.map +0 -1
- package/examples/reference-emitter/src/generators/config.ts +0 -19
package/dist/cli/index.mjs
CHANGED
|
@@ -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",
|
|
@@ -1662,7 +1751,9 @@ function extractParams(params, location, operationContext) {
|
|
|
1662
1751
|
description: p.description,
|
|
1663
1752
|
deprecated: p.deprecated || p.schema?.deprecated || void 0,
|
|
1664
1753
|
default: p.schema?.default,
|
|
1665
|
-
example: p.example ?? p.schema?.example
|
|
1754
|
+
example: p.example ?? p.schema?.example,
|
|
1755
|
+
style: p.style,
|
|
1756
|
+
explode: p.explode
|
|
1666
1757
|
}));
|
|
1667
1758
|
}
|
|
1668
1759
|
function extractRequestBody(body, op, inlineModels) {
|
|
@@ -2596,6 +2687,41 @@ function findPrecedingKdoc(children, index, source) {
|
|
|
2596
2687
|
}
|
|
2597
2688
|
return null;
|
|
2598
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
|
+
}
|
|
2599
2725
|
const kotlinMergeAdapter = {
|
|
2600
2726
|
language: "kotlin",
|
|
2601
2727
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -2636,6 +2762,33 @@ const kotlinMergeAdapter = {
|
|
|
2636
2762
|
renderImports(imports) {
|
|
2637
2763
|
return imports.map((entry) => entry.text);
|
|
2638
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
|
+
},
|
|
2639
2792
|
extractDocstrings(tree, source) {
|
|
2640
2793
|
const result = /* @__PURE__ */ new Map();
|
|
2641
2794
|
const rootChildren = tree.rootNode.children;
|
|
@@ -3426,6 +3579,30 @@ function safeParse(parser, source) {
|
|
|
3426
3579
|
* Supports any language with a tree-sitter grammar package.
|
|
3427
3580
|
*/
|
|
3428
3581
|
const parserCache = /* @__PURE__ */ new Map();
|
|
3582
|
+
/**
|
|
3583
|
+
* When replacing an existing docstring with a generated one, carry over any
|
|
3584
|
+
* `@deprecated` lines from the existing doc that aren't present in the
|
|
3585
|
+
* generated doc. This prevents hand-written deprecation notices from being
|
|
3586
|
+
* silently dropped during docstring refresh.
|
|
3587
|
+
*/
|
|
3588
|
+
function preserveDeprecatedTags(existingDoc, generatedDoc) {
|
|
3589
|
+
if (!existingDoc.includes("@deprecated")) return generatedDoc;
|
|
3590
|
+
if (generatedDoc.includes("@deprecated")) return generatedDoc;
|
|
3591
|
+
const deprecatedParts = [];
|
|
3592
|
+
for (const line of existingDoc.split("\n")) if (/@deprecated\b/.test(line)) {
|
|
3593
|
+
const match = line.match(/@deprecated\b.*/);
|
|
3594
|
+
if (match) {
|
|
3595
|
+
const cleaned = match[0].replace(/\s*\*\/\s*$/, "");
|
|
3596
|
+
deprecatedParts.push(` * ${cleaned}`);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
if (deprecatedParts.length === 0) return generatedDoc;
|
|
3600
|
+
const closingIdx = generatedDoc.lastIndexOf("*/");
|
|
3601
|
+
if (closingIdx === -1) return generatedDoc;
|
|
3602
|
+
const before = generatedDoc.slice(0, closingIdx);
|
|
3603
|
+
const after = generatedDoc.slice(closingIdx);
|
|
3604
|
+
return before + deprecatedParts.join("\n") + "\n " + after;
|
|
3605
|
+
}
|
|
3429
3606
|
function findIgnoredRegions(source) {
|
|
3430
3607
|
const regions = [];
|
|
3431
3608
|
const startTag = "@oagen-ignore-start";
|
|
@@ -3539,10 +3716,21 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3539
3716
|
}
|
|
3540
3717
|
for (const anchor of existingStatements.importAnchors) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(anchor)).split("\n").length - 1 + anchor.split("\n").length - 1;
|
|
3541
3718
|
if (lastImportEndIndex === -1 && existingStatements.importInsertionAnchor) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(existingStatements.importInsertionAnchor)).split("\n").length - 1 + existingStatements.importInsertionAnchor.split("\n").length - 1;
|
|
3719
|
+
const existingExportedNames = /* @__PURE__ */ new Set();
|
|
3542
3720
|
for (const stmt of existingStatements.statements) {
|
|
3543
|
-
if (stmt.kind === "reexport")
|
|
3544
|
-
|
|
3545
|
-
|
|
3721
|
+
if (stmt.kind === "reexport") {
|
|
3722
|
+
existingReexports.add(adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim());
|
|
3723
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
3724
|
+
if (braceMatch) for (const part of braceMatch[1].split(",")) {
|
|
3725
|
+
const segments = part.trim().split(/\s+as\s+/);
|
|
3726
|
+
const exportedName = (segments[1] ?? segments[0]).trim();
|
|
3727
|
+
if (exportedName) existingExportedNames.add(exportedName);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
if (stmt.key) {
|
|
3731
|
+
existingKeys.add(stmt.key);
|
|
3732
|
+
existingExportedNames.add(stmt.key);
|
|
3733
|
+
} else existingUnnamedTexts.add(stmt.text.trim());
|
|
3546
3734
|
}
|
|
3547
3735
|
const headerLine = header.trim();
|
|
3548
3736
|
const newImports = [];
|
|
@@ -3602,6 +3790,17 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3602
3790
|
preserved++;
|
|
3603
3791
|
continue;
|
|
3604
3792
|
}
|
|
3793
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
3794
|
+
if (braceMatch) {
|
|
3795
|
+
const exportedNames = braceMatch[1].split(",").map((n) => {
|
|
3796
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
3797
|
+
return (parts[1] ?? parts[0]).trim();
|
|
3798
|
+
}).filter(Boolean);
|
|
3799
|
+
if (exportedNames.length > 0 && exportedNames.every((n) => existingExportedNames.has(n))) {
|
|
3800
|
+
preserved++;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3605
3804
|
}
|
|
3606
3805
|
if (stmt.key && (existingKeys.has(stmt.key) || existingImportedNames.has(stmt.key))) {
|
|
3607
3806
|
preserved++;
|
|
@@ -3639,10 +3838,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3639
3838
|
if (newMembers.length > 0) {
|
|
3640
3839
|
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
3641
3840
|
const indent = existSymbol.memberIndent ?? " ";
|
|
3642
|
-
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
3841
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
|
|
3643
3842
|
insertions.push({
|
|
3644
3843
|
line: existSymbol.bodyEndLine,
|
|
3645
|
-
text: insertText
|
|
3844
|
+
text: "\n" + insertText
|
|
3646
3845
|
});
|
|
3647
3846
|
deepAdded += newMembers.length;
|
|
3648
3847
|
}
|
|
@@ -3703,10 +3902,11 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3703
3902
|
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
|
|
3704
3903
|
if (genDoc) if (existInfo.docstring) {
|
|
3705
3904
|
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
3905
|
+
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
3706
3906
|
edits.push({
|
|
3707
3907
|
start: existInfo.docstring.startIndex,
|
|
3708
3908
|
end: existInfo.docstring.endIndex,
|
|
3709
|
-
newText
|
|
3909
|
+
newText
|
|
3710
3910
|
});
|
|
3711
3911
|
docstringUpdates++;
|
|
3712
3912
|
}
|
|
@@ -3723,14 +3923,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3723
3923
|
const matchedExistMembers = /* @__PURE__ */ new Set();
|
|
3724
3924
|
for (const [memberName, genMember] of genInfo.members) {
|
|
3725
3925
|
const existMember = existInfo.members.get(memberName);
|
|
3726
|
-
if (!existMember
|
|
3926
|
+
if (!existMember) continue;
|
|
3727
3927
|
matchedExistMembers.add(memberName);
|
|
3928
|
+
if (!genMember.docstring) continue;
|
|
3728
3929
|
if (existMember.docstring) {
|
|
3729
3930
|
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
3931
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
3730
3932
|
edits.push({
|
|
3731
3933
|
start: existMember.docstring.startIndex,
|
|
3732
3934
|
end: existMember.docstring.endIndex,
|
|
3733
|
-
newText
|
|
3935
|
+
newText
|
|
3734
3936
|
});
|
|
3735
3937
|
docstringUpdates++;
|
|
3736
3938
|
}
|
|
@@ -3745,34 +3947,59 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3745
3947
|
docstringUpdates++;
|
|
3746
3948
|
}
|
|
3747
3949
|
}
|
|
3748
|
-
|
|
3950
|
+
const unmatchedGenByFp = /* @__PURE__ */ new Map();
|
|
3951
|
+
for (const [genName, genMember] of genInfo.members) {
|
|
3952
|
+
if (matchedExistMembers.has(genName)) continue;
|
|
3749
3953
|
if (!genMember.docstring || !genMember.urlFingerprint) continue;
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3954
|
+
if (existInfo.members.has(genName)) continue;
|
|
3955
|
+
const bucket = unmatchedGenByFp.get(genMember.urlFingerprint) ?? [];
|
|
3956
|
+
bucket.push({
|
|
3957
|
+
name: genName,
|
|
3958
|
+
member: genMember
|
|
3959
|
+
});
|
|
3960
|
+
unmatchedGenByFp.set(genMember.urlFingerprint, bucket);
|
|
3961
|
+
}
|
|
3962
|
+
const unmatchedExistByFp = /* @__PURE__ */ new Map();
|
|
3963
|
+
for (const [existName, existMember] of existInfo.members) {
|
|
3964
|
+
if (matchedExistMembers.has(existName)) continue;
|
|
3965
|
+
if (!existMember.urlFingerprint) continue;
|
|
3966
|
+
const bucket = unmatchedExistByFp.get(existMember.urlFingerprint) ?? [];
|
|
3967
|
+
bucket.push({
|
|
3968
|
+
name: existName,
|
|
3969
|
+
member: existMember
|
|
3970
|
+
});
|
|
3971
|
+
unmatchedExistByFp.set(existMember.urlFingerprint, bucket);
|
|
3972
|
+
}
|
|
3973
|
+
for (const [fp, genBucket] of unmatchedGenByFp) {
|
|
3974
|
+
if (genBucket.length !== 1) continue;
|
|
3975
|
+
const existBucket = unmatchedExistByFp.get(fp);
|
|
3976
|
+
if (!existBucket || existBucket.length !== 1) continue;
|
|
3977
|
+
const genMember = genBucket[0].member;
|
|
3978
|
+
const existEntry = existBucket[0];
|
|
3979
|
+
const existMember = existEntry.member;
|
|
3980
|
+
if (!genMember.docstring) continue;
|
|
3981
|
+
if (existMember.docstring) {
|
|
3982
|
+
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
3983
|
+
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
3984
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
3766
3985
|
edits.push({
|
|
3767
|
-
start:
|
|
3768
|
-
end:
|
|
3769
|
-
newText
|
|
3986
|
+
start: existMember.docstring.startIndex,
|
|
3987
|
+
end: existMember.docstring.endIndex,
|
|
3988
|
+
newText
|
|
3770
3989
|
});
|
|
3771
3990
|
docstringUpdates++;
|
|
3772
3991
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3992
|
+
} else {
|
|
3993
|
+
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
3994
|
+
const indent = " ".repeat(existMember.declColumn);
|
|
3995
|
+
edits.push({
|
|
3996
|
+
start: lineStart,
|
|
3997
|
+
end: lineStart,
|
|
3998
|
+
newText: indent + genMember.docstring.text + "\n"
|
|
3999
|
+
});
|
|
4000
|
+
docstringUpdates++;
|
|
3775
4001
|
}
|
|
4002
|
+
matchedExistMembers.add(existEntry.name);
|
|
3776
4003
|
}
|
|
3777
4004
|
}
|
|
3778
4005
|
if (edits.length > 0) {
|
|
@@ -4059,12 +4286,16 @@ function spliceExtraImports(existingLines, resultLines) {
|
|
|
4059
4286
|
const extraSimple = [...existingSimple].filter((l) => !generatedSimple.has(l));
|
|
4060
4287
|
const existingFrom = collectFromImports(existingLines);
|
|
4061
4288
|
const generatedFrom = collectFromImports(resultLines);
|
|
4289
|
+
const allGeneratedIds = /* @__PURE__ */ new Set();
|
|
4290
|
+
for (const ids of generatedFrom.values()) for (const id of ids) allGeneratedIds.add(id);
|
|
4062
4291
|
const extraFromLines = [];
|
|
4063
4292
|
for (const [mod, existIds] of existingFrom) {
|
|
4064
4293
|
const genIds = generatedFrom.get(mod);
|
|
4065
|
-
if (!genIds)
|
|
4066
|
-
|
|
4067
|
-
|
|
4294
|
+
if (!genIds) {
|
|
4295
|
+
const extra = [...existIds].filter((id) => !allGeneratedIds.has(id));
|
|
4296
|
+
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
4297
|
+
} else {
|
|
4298
|
+
const extra = [...existIds].filter((id) => !genIds.has(id) && !allGeneratedIds.has(id));
|
|
4068
4299
|
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
4069
4300
|
}
|
|
4070
4301
|
}
|
|
@@ -4171,7 +4402,7 @@ function deriveMethodName(op, _service) {
|
|
|
4171
4402
|
const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
|
|
4172
4403
|
if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
|
|
4173
4404
|
}
|
|
4174
|
-
const noun = trailingParam ? singularize(resource) : resource;
|
|
4405
|
+
const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
|
|
4175
4406
|
return `${verb}_${verb === "list" ? resource : noun}`;
|
|
4176
4407
|
}
|
|
4177
4408
|
/**
|
|
@@ -4197,7 +4428,7 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4197
4428
|
defaults: sh.defaults ?? {},
|
|
4198
4429
|
inferFromClient: sh.inferFromClient ?? [],
|
|
4199
4430
|
exposedParams: sh.exposedParams ?? [],
|
|
4200
|
-
optionalParams: [],
|
|
4431
|
+
optionalParams: sh.optionalParams ?? [],
|
|
4201
4432
|
responseModelName: resolveResponseModelName(op)
|
|
4202
4433
|
}));
|
|
4203
4434
|
resolved.push({
|
|
@@ -4205,7 +4436,10 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4205
4436
|
service,
|
|
4206
4437
|
methodName,
|
|
4207
4438
|
mountOn,
|
|
4208
|
-
wrappers
|
|
4439
|
+
wrappers,
|
|
4440
|
+
defaults: hint?.defaults ?? {},
|
|
4441
|
+
inferFromClient: hint?.inferFromClient ?? [],
|
|
4442
|
+
urlBuilder: hint?.urlBuilder ?? false
|
|
4209
4443
|
});
|
|
4210
4444
|
}
|
|
4211
4445
|
return resolved;
|
|
@@ -4228,7 +4462,9 @@ function buildEmitterContext(spec, options) {
|
|
|
4228
4462
|
outputDir: options.outputDir,
|
|
4229
4463
|
apiSurface: options.apiSurface,
|
|
4230
4464
|
overlayLookup: options.overlayLookup,
|
|
4231
|
-
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
4465
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
|
|
4466
|
+
targetDir: options.target,
|
|
4467
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
4232
4468
|
};
|
|
4233
4469
|
}
|
|
4234
4470
|
/**
|
|
@@ -4297,7 +4533,6 @@ function generateAllFiles(spec, emitter, ctx) {
|
|
|
4297
4533
|
...emitter.generateResources(spec.services, ctx),
|
|
4298
4534
|
...emitter.generateClient(spec, ctx),
|
|
4299
4535
|
...emitter.generateErrors(ctx),
|
|
4300
|
-
...emitter.generateConfig(ctx),
|
|
4301
4536
|
...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
|
|
4302
4537
|
...emitter.generateTests(reachableSpec, ctx),
|
|
4303
4538
|
...emitter.generateManifest?.(spec, ctx) ?? []
|
|
@@ -4348,7 +4583,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
|
4348
4583
|
*/
|
|
4349
4584
|
function isRootFile(filePath) {
|
|
4350
4585
|
if (filePath.includes("/fixtures/")) return false;
|
|
4351
|
-
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return
|
|
4586
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
|
|
4352
4587
|
if (filePath.includes("/serializers/")) return false;
|
|
4353
4588
|
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
4354
4589
|
return true;
|
|
@@ -4485,7 +4720,10 @@ async function integrateGeneratedFiles(opts) {
|
|
|
4485
4720
|
for (const filePath of removals) try {
|
|
4486
4721
|
await fs.unlink(filePath);
|
|
4487
4722
|
} catch {}
|
|
4488
|
-
return
|
|
4723
|
+
return {
|
|
4724
|
+
...result,
|
|
4725
|
+
emittedPaths: resolved.map((f) => f.path)
|
|
4726
|
+
};
|
|
4489
4727
|
}
|
|
4490
4728
|
//#endregion
|
|
4491
4729
|
//#region src/engine/formatter.ts
|
|
@@ -4520,9 +4758,141 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
4520
4758
|
}
|
|
4521
4759
|
}
|
|
4522
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
|
|
4523
4888
|
//#region src/engine/orchestrator.ts
|
|
4524
4889
|
async function generate(spec, emitter, options) {
|
|
4525
|
-
const
|
|
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
|
+
});
|
|
4526
4896
|
if (options.dryRun) {
|
|
4527
4897
|
if (options.target) {
|
|
4528
4898
|
console.log(`\nTarget integration (${options.target}):`);
|
|
@@ -4530,13 +4900,25 @@ async function generate(spec, emitter, options) {
|
|
|
4530
4900
|
}
|
|
4531
4901
|
return withHeaders;
|
|
4532
4902
|
}
|
|
4903
|
+
const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
|
|
4533
4904
|
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
4534
4905
|
language: emitter.language,
|
|
4535
4906
|
header
|
|
4536
4907
|
});
|
|
4537
4908
|
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
4538
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
|
+
});
|
|
4539
4920
|
if (options.target) {
|
|
4921
|
+
const targetPrevManifest = targetManifestForCtx;
|
|
4540
4922
|
const targetResult = await integrateGeneratedFiles({
|
|
4541
4923
|
files: withHeaders,
|
|
4542
4924
|
language: emitter.language,
|
|
@@ -4546,6 +4928,15 @@ async function generate(spec, emitter, options) {
|
|
|
4546
4928
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
4547
4929
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
4548
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
|
+
});
|
|
4549
4940
|
const allTargetFiles = [
|
|
4550
4941
|
...targetResult.written,
|
|
4551
4942
|
...targetResult.merged,
|
|
@@ -4555,6 +4946,40 @@ async function generate(spec, emitter, options) {
|
|
|
4555
4946
|
}
|
|
4556
4947
|
return withHeaders;
|
|
4557
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
|
+
}
|
|
4558
4983
|
//#endregion
|
|
4559
4984
|
//#region src/engine/registry.ts
|
|
4560
4985
|
const emitters = /* @__PURE__ */ new Map();
|
|
@@ -5239,7 +5664,8 @@ async function generateCommand(opts) {
|
|
|
5239
5664
|
apiSurface,
|
|
5240
5665
|
overlayLookup,
|
|
5241
5666
|
operationHints: opts.operationHints,
|
|
5242
|
-
mountRules: opts.mountRules
|
|
5667
|
+
mountRules: opts.mountRules,
|
|
5668
|
+
noPrune: opts.prune === false
|
|
5243
5669
|
});
|
|
5244
5670
|
if (opts.dryRun) for (const f of files) console.log(f.path);
|
|
5245
5671
|
else {
|
|
@@ -6755,10 +7181,6 @@ export const ${`${toCamelCase(lang)}Emitter`}: Emitter = {
|
|
|
6755
7181
|
return [];
|
|
6756
7182
|
},
|
|
6757
7183
|
|
|
6758
|
-
generateConfig(_ctx: EmitterContext): GeneratedFile[] {
|
|
6759
|
-
return [];
|
|
6760
|
-
},
|
|
6761
|
-
|
|
6762
7184
|
generateTypeSignatures(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
|
|
6763
7185
|
return [];
|
|
6764
7186
|
},
|
|
@@ -6959,7 +7381,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
|
|
|
6959
7381
|
}
|
|
6960
7382
|
return parseCommand(opts);
|
|
6961
7383
|
});
|
|
6962
|
-
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) => {
|
|
6963
7385
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
6964
7386
|
if (!opts.spec) {
|
|
6965
7387
|
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|