x-openapi-flow 1.3.0 → 1.3.1

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.
@@ -13,6 +13,13 @@ const {
13
13
  detectInvalidOperationReferences,
14
14
  detectTerminalCoverage,
15
15
  } = require("../lib/validator");
16
+ const { generateSdk } = require("../lib/sdk-generator");
17
+ const {
18
+ exportDocFlows,
19
+ generatePostmanCollection,
20
+ generateInsomniaWorkspace,
21
+ generateRedocPackage,
22
+ } = require("../adapters/flow-output-adapters");
16
23
 
17
24
  const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
18
25
  const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
@@ -51,6 +58,12 @@ Usage:
51
58
  x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
52
59
  x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
53
60
  x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
61
+ x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
62
+ x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
63
+ x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
64
+ x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
65
+ x-openapi-flow generate-insomnia [openapi-file] [--output path]
66
+ x-openapi-flow generate-redoc [openapi-file] [--output path]
54
67
  x-openapi-flow graph <openapi-file> [--format mermaid|json]
55
68
  x-openapi-flow doctor [--config path]
56
69
  x-openapi-flow --help
@@ -70,6 +83,15 @@ Examples:
70
83
  x-openapi-flow diff openapi.yaml --format json
71
84
  x-openapi-flow lint openapi.yaml
72
85
  x-openapi-flow lint openapi.yaml --format json
86
+ x-openapi-flow analyze openapi.yaml
87
+ x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
88
+ x-openapi-flow analyze openapi.yaml --format json
89
+ x-openapi-flow analyze openapi.yaml --merge --flows openapi.x.yaml
90
+ x-openapi-flow generate-sdk openapi.yaml --lang typescript --output ./sdk
91
+ x-openapi-flow export-doc-flows openapi.yaml --output ./docs/api-flows.md
92
+ x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
93
+ x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
94
+ x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
73
95
  x-openapi-flow graph examples/order-api.yaml
74
96
  x-openapi-flow doctor
75
97
  `);
@@ -567,6 +589,58 @@ function parseGraphArgs(args) {
567
589
  return { filePath: path.resolve(positional[0]), format };
568
590
  }
569
591
 
592
+ function parseAnalyzeArgs(args) {
593
+ const unknown = findUnknownOptions(args, ["--format", "--out", "--flows"], ["--merge"]);
594
+ if (unknown) {
595
+ return { error: `Unknown option: ${unknown}` };
596
+ }
597
+
598
+ const formatOpt = getOptionValue(args, "--format");
599
+ if (formatOpt.error) {
600
+ return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
601
+ }
602
+
603
+ const outOpt = getOptionValue(args, "--out");
604
+ if (outOpt.error) {
605
+ return { error: outOpt.error };
606
+ }
607
+
608
+ const flowsOpt = getOptionValue(args, "--flows");
609
+ if (flowsOpt.error) {
610
+ return { error: flowsOpt.error };
611
+ }
612
+
613
+ const format = formatOpt.found ? formatOpt.value : "pretty";
614
+ if (!["pretty", "json"].includes(format)) {
615
+ return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
616
+ }
617
+
618
+ const positional = args.filter((token, index) => {
619
+ if (token === "--format" || token === "--out" || token === "--flows" || token === "--merge") {
620
+ return false;
621
+ }
622
+ if (
623
+ index > 0
624
+ && (args[index - 1] === "--format" || args[index - 1] === "--out" || args[index - 1] === "--flows")
625
+ ) {
626
+ return false;
627
+ }
628
+ return !token.startsWith("--");
629
+ });
630
+
631
+ if (positional.length > 1) {
632
+ return { error: `Unexpected argument: ${positional[1]}` };
633
+ }
634
+
635
+ return {
636
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
637
+ format,
638
+ outPath: outOpt.found ? path.resolve(outOpt.value) : undefined,
639
+ merge: args.includes("--merge"),
640
+ flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
641
+ };
642
+ }
643
+
570
644
  function parseDoctorArgs(args) {
571
645
  const unknown = findUnknownOptions(args, ["--config"], []);
572
646
  if (unknown) {
@@ -583,6 +657,176 @@ function parseDoctorArgs(args) {
583
657
  };
584
658
  }
585
659
 
660
+ function parseGenerateSdkArgs(args) {
661
+ const unknown = findUnknownOptions(args, ["--lang", "--output"], []);
662
+ if (unknown) {
663
+ return { error: `Unknown option: ${unknown}` };
664
+ }
665
+
666
+ const langOpt = getOptionValue(args, "--lang");
667
+ if (langOpt.error) {
668
+ return { error: `${langOpt.error} Use 'typescript'.` };
669
+ }
670
+
671
+ const outputOpt = getOptionValue(args, "--output");
672
+ if (outputOpt.error) {
673
+ return { error: outputOpt.error };
674
+ }
675
+
676
+ if (!langOpt.found) {
677
+ return { error: "Missing --lang option. Usage: x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]" };
678
+ }
679
+
680
+ const language = langOpt.value;
681
+ if (language !== "typescript") {
682
+ return { error: `Unsupported --lang '${language}'. MVP currently supports only 'typescript'.` };
683
+ }
684
+
685
+ const positional = args.filter((token, index) => {
686
+ if (token === "--lang" || token === "--output") {
687
+ return false;
688
+ }
689
+ if (index > 0 && (args[index - 1] === "--lang" || args[index - 1] === "--output")) {
690
+ return false;
691
+ }
692
+ return !token.startsWith("--");
693
+ });
694
+
695
+ if (positional.length > 1) {
696
+ return { error: `Unexpected argument: ${positional[1]}` };
697
+ }
698
+
699
+ return {
700
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
701
+ language,
702
+ outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "sdk"),
703
+ };
704
+ }
705
+
706
+ function parseExportDocFlowsArgs(args) {
707
+ const unknown = findUnknownOptions(args, ["--output", "--format"], []);
708
+ if (unknown) {
709
+ return { error: `Unknown option: ${unknown}` };
710
+ }
711
+
712
+ const outputOpt = getOptionValue(args, "--output");
713
+ if (outputOpt.error) {
714
+ return { error: outputOpt.error };
715
+ }
716
+
717
+ const formatOpt = getOptionValue(args, "--format");
718
+ if (formatOpt.error) {
719
+ return { error: `${formatOpt.error} Use 'markdown' or 'json'.` };
720
+ }
721
+
722
+ const format = formatOpt.found ? formatOpt.value : "markdown";
723
+ if (!["markdown", "json"].includes(format)) {
724
+ return { error: `Invalid --format '${format}'. Use 'markdown' or 'json'.` };
725
+ }
726
+
727
+ const positional = args.filter((token, index) => {
728
+ if (token === "--output" || token === "--format") return false;
729
+ if (index > 0 && (args[index - 1] === "--output" || args[index - 1] === "--format")) return false;
730
+ return !token.startsWith("--");
731
+ });
732
+
733
+ if (positional.length > 1) {
734
+ return { error: `Unexpected argument: ${positional[1]}` };
735
+ }
736
+
737
+ return {
738
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
739
+ outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "api-flows.md"),
740
+ format,
741
+ };
742
+ }
743
+
744
+ function parseGeneratePostmanArgs(args) {
745
+ const unknown = findUnknownOptions(args, ["--output"], ["--with-scripts"]);
746
+ if (unknown) {
747
+ return { error: `Unknown option: ${unknown}` };
748
+ }
749
+
750
+ const outputOpt = getOptionValue(args, "--output");
751
+ if (outputOpt.error) {
752
+ return { error: outputOpt.error };
753
+ }
754
+
755
+ const positional = args.filter((token, index) => {
756
+ if (token === "--output" || token === "--with-scripts") return false;
757
+ if (index > 0 && args[index - 1] === "--output") return false;
758
+ return !token.startsWith("--");
759
+ });
760
+
761
+ if (positional.length > 1) {
762
+ return { error: `Unexpected argument: ${positional[1]}` };
763
+ }
764
+
765
+ return {
766
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
767
+ outputPath: outputOpt.found
768
+ ? path.resolve(outputOpt.value)
769
+ : path.resolve(process.cwd(), "x-openapi-flow.postman_collection.json"),
770
+ withScripts: args.includes("--with-scripts"),
771
+ };
772
+ }
773
+
774
+ function parseGenerateInsomniaArgs(args) {
775
+ const unknown = findUnknownOptions(args, ["--output"], []);
776
+ if (unknown) {
777
+ return { error: `Unknown option: ${unknown}` };
778
+ }
779
+
780
+ const outputOpt = getOptionValue(args, "--output");
781
+ if (outputOpt.error) {
782
+ return { error: outputOpt.error };
783
+ }
784
+
785
+ const positional = args.filter((token, index) => {
786
+ if (token === "--output") return false;
787
+ if (index > 0 && args[index - 1] === "--output") return false;
788
+ return !token.startsWith("--");
789
+ });
790
+
791
+ if (positional.length > 1) {
792
+ return { error: `Unexpected argument: ${positional[1]}` };
793
+ }
794
+
795
+ return {
796
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
797
+ outputPath: outputOpt.found
798
+ ? path.resolve(outputOpt.value)
799
+ : path.resolve(process.cwd(), "x-openapi-flow.insomnia.json"),
800
+ };
801
+ }
802
+
803
+ function parseGenerateRedocArgs(args) {
804
+ const unknown = findUnknownOptions(args, ["--output"], []);
805
+ if (unknown) {
806
+ return { error: `Unknown option: ${unknown}` };
807
+ }
808
+
809
+ const outputOpt = getOptionValue(args, "--output");
810
+ if (outputOpt.error) {
811
+ return { error: outputOpt.error };
812
+ }
813
+
814
+ const positional = args.filter((token, index) => {
815
+ if (token === "--output") return false;
816
+ if (index > 0 && args[index - 1] === "--output") return false;
817
+ return !token.startsWith("--");
818
+ });
819
+
820
+ if (positional.length > 1) {
821
+ return { error: `Unexpected argument: ${positional[1]}` };
822
+ }
823
+
824
+ return {
825
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
826
+ outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "redoc-flow"),
827
+ };
828
+ }
829
+
586
830
  function parseArgs(argv) {
587
831
  const args = argv.slice(2);
588
832
  const command = args[0];
@@ -611,6 +855,11 @@ function parseArgs(argv) {
611
855
  return parsed.error ? parsed : { command, ...parsed };
612
856
  }
613
857
 
858
+ if (command === "analyze") {
859
+ const parsed = parseAnalyzeArgs(commandArgs);
860
+ return parsed.error ? parsed : { command, ...parsed };
861
+ }
862
+
614
863
  if (command === "apply") {
615
864
  const parsed = parseApplyArgs(commandArgs);
616
865
  return parsed.error ? parsed : { command, ...parsed };
@@ -631,6 +880,31 @@ function parseArgs(argv) {
631
880
  return parsed.error ? parsed : { command, ...parsed };
632
881
  }
633
882
 
883
+ if (command === "generate-sdk") {
884
+ const parsed = parseGenerateSdkArgs(commandArgs);
885
+ return parsed.error ? parsed : { command, ...parsed };
886
+ }
887
+
888
+ if (command === "export-doc-flows") {
889
+ const parsed = parseExportDocFlowsArgs(commandArgs);
890
+ return parsed.error ? parsed : { command, ...parsed };
891
+ }
892
+
893
+ if (command === "generate-postman") {
894
+ const parsed = parseGeneratePostmanArgs(commandArgs);
895
+ return parsed.error ? parsed : { command, ...parsed };
896
+ }
897
+
898
+ if (command === "generate-insomnia") {
899
+ const parsed = parseGenerateInsomniaArgs(commandArgs);
900
+ return parsed.error ? parsed : { command, ...parsed };
901
+ }
902
+
903
+ if (command === "generate-redoc") {
904
+ const parsed = parseGenerateRedocArgs(commandArgs);
905
+ return parsed.error ? parsed : { command, ...parsed };
906
+ }
907
+
634
908
  return { error: `Unknown command: ${command}` };
635
909
  }
636
910
 
@@ -1507,6 +1781,474 @@ function runGraph(parsed) {
1507
1781
  }
1508
1782
  }
1509
1783
 
1784
+ function toKebabCase(value) {
1785
+ if (!value) {
1786
+ return "operation";
1787
+ }
1788
+
1789
+ const normalized = String(value)
1790
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
1791
+ .replace(/[^a-zA-Z0-9]+/g, "-")
1792
+ .replace(/-+/g, "-")
1793
+ .replace(/^-|-$/g, "")
1794
+ .toLowerCase();
1795
+
1796
+ return normalized || "operation";
1797
+ }
1798
+
1799
+ function deriveResourceKey(pathKey) {
1800
+ const tokens = String(pathKey || "")
1801
+ .split("/")
1802
+ .filter((token) => token && !token.startsWith("{") && !token.endsWith("}"));
1803
+
1804
+ return tokens[0] || "root";
1805
+ }
1806
+
1807
+ function inferCurrentState(entry) {
1808
+ const fingerprint = [entry.method, entry.path, entry.resolvedOperationId]
1809
+ .filter(Boolean)
1810
+ .join(" ")
1811
+ .toLowerCase();
1812
+
1813
+ const keywordToState = [
1814
+ { match: /cancel|void/, state: "CANCELED" },
1815
+ { match: /refund/, state: "REFUNDED" },
1816
+ { match: /fail|error|decline|reject/, state: "FAILED" },
1817
+ { match: /deliver|received/, state: "DELIVERED" },
1818
+ { match: /ship|dispatch/, state: "SHIPPED" },
1819
+ { match: /complete|done|close|finish/, state: "COMPLETED" },
1820
+ { match: /pay|charge|collect/, state: "PAID" },
1821
+ { match: /confirm|approve|accept/, state: "CONFIRMED" },
1822
+ { match: /process|fulfill/, state: "PROCESSING" },
1823
+ { match: /create|submit|register/, state: "CREATED" },
1824
+ { match: /get|list|find|fetch|read/, state: "OBSERVED" },
1825
+ ];
1826
+
1827
+ for (const rule of keywordToState) {
1828
+ if (rule.match.test(fingerprint)) {
1829
+ return rule.state;
1830
+ }
1831
+ }
1832
+
1833
+ if (entry.method === "post") {
1834
+ return "CREATED";
1835
+ }
1836
+ if (entry.method === "delete") {
1837
+ return "CANCELED";
1838
+ }
1839
+ if (entry.method === "patch" || entry.method === "put") {
1840
+ return "PROCESSING";
1841
+ }
1842
+
1843
+ return "OBSERVED";
1844
+ }
1845
+
1846
+ function inferTriggerType(fromEntry, toEntry) {
1847
+ const toFingerprint = `${toEntry.method} ${toEntry.path} ${toEntry.resolvedOperationId}`.toLowerCase();
1848
+ if (/(webhook|callback|event)/.test(toFingerprint)) {
1849
+ return "webhook";
1850
+ }
1851
+
1852
+ if (fromEntry.method === "get" || fromEntry.method === "head") {
1853
+ return "polling";
1854
+ }
1855
+
1856
+ return "synchronous";
1857
+ }
1858
+
1859
+ function buildAnalyzedFlowsDoc(api) {
1860
+ const stateOrder = {
1861
+ CREATED: 10,
1862
+ CONFIRMED: 20,
1863
+ PAID: 30,
1864
+ PROCESSING: 40,
1865
+ SHIPPED: 50,
1866
+ DELIVERED: 60,
1867
+ COMPLETED: 70,
1868
+ REFUNDED: 80,
1869
+ CANCELED: 90,
1870
+ FAILED: 100,
1871
+ OBSERVED: 1000,
1872
+ };
1873
+
1874
+ const terminalStates = new Set(["COMPLETED", "DELIVERED", "REFUNDED", "CANCELED", "FAILED"]);
1875
+ const entries = extractOperationEntries(api);
1876
+
1877
+ const inferred = entries.map((entry) => ({
1878
+ ...entry,
1879
+ resourceKey: deriveResourceKey(entry.path),
1880
+ inferredState: inferCurrentState(entry),
1881
+ inferredRank: stateOrder[inferCurrentState(entry)] || 1000,
1882
+ }));
1883
+
1884
+ const transitionInsights = [];
1885
+
1886
+ const operations = inferred.map((entry) => {
1887
+ const sameResource = inferred.filter((candidate) => candidate.resourceKey === entry.resourceKey);
1888
+ const prioritized = sameResource.length > 1 ? sameResource : inferred;
1889
+
1890
+ let transition = null;
1891
+ if (!terminalStates.has(entry.inferredState)) {
1892
+ const nextCandidates = prioritized
1893
+ .filter((candidate) => candidate.resolvedOperationId !== entry.resolvedOperationId)
1894
+ .filter((candidate) => candidate.inferredRank > entry.inferredRank)
1895
+ .sort((left, right) => {
1896
+ const rankDelta = (left.inferredRank - entry.inferredRank) - (right.inferredRank - entry.inferredRank);
1897
+ if (rankDelta !== 0) {
1898
+ return rankDelta;
1899
+ }
1900
+ return left.resolvedOperationId.localeCompare(right.resolvedOperationId);
1901
+ });
1902
+
1903
+ const next = nextCandidates[0];
1904
+ if (next) {
1905
+ const confidence = Math.min(
1906
+ 0.95,
1907
+ 0.55
1908
+ + (sameResource.length > 1 ? 0.25 : 0)
1909
+ + (entry.inferredRank < 1000 && next.inferredRank < 1000 ? 0.1 : 0)
1910
+ + (nextCandidates.length === 1 ? 0.1 : 0)
1911
+ );
1912
+
1913
+ const confidenceReasons = [];
1914
+ if (sameResource.length > 1) {
1915
+ confidenceReasons.push("same_resource");
1916
+ }
1917
+ if (entry.inferredRank < 1000 && next.inferredRank < 1000) {
1918
+ confidenceReasons.push("known_state_progression");
1919
+ }
1920
+ if (nextCandidates.length === 1) {
1921
+ confidenceReasons.push("single_candidate");
1922
+ }
1923
+ if (confidenceReasons.length === 0) {
1924
+ confidenceReasons.push("fallback_ordering");
1925
+ }
1926
+
1927
+ transition = {
1928
+ target_state: next.inferredState,
1929
+ trigger_type: inferTriggerType(entry, next),
1930
+ next_operation_id: next.resolvedOperationId,
1931
+ };
1932
+
1933
+ transitionInsights.push({
1934
+ from_operation_id: entry.resolvedOperationId,
1935
+ to_operation_id: next.resolvedOperationId,
1936
+ from_state: entry.inferredState,
1937
+ target_state: next.inferredState,
1938
+ trigger_type: transition.trigger_type,
1939
+ confidence: Number(confidence.toFixed(2)),
1940
+ confidence_reasons: confidenceReasons,
1941
+ });
1942
+ }
1943
+ }
1944
+
1945
+ return {
1946
+ operationId: entry.resolvedOperationId,
1947
+ "x-openapi-flow": {
1948
+ version: "1.0",
1949
+ id: toKebabCase(entry.resolvedOperationId),
1950
+ current_state: entry.inferredState,
1951
+ description: "Auto-generated by x-openapi-flow analyze",
1952
+ transitions: transition ? [transition] : [],
1953
+ },
1954
+ };
1955
+ });
1956
+
1957
+ return {
1958
+ version: "1.0",
1959
+ operations,
1960
+ analysis: {
1961
+ operationCount: inferred.length,
1962
+ uniqueStates: Array.from(new Set(inferred.map((entry) => entry.inferredState))).sort(),
1963
+ inferredTransitions: operations.reduce((total, operation) => {
1964
+ const transitions = operation["x-openapi-flow"].transitions || [];
1965
+ return total + transitions.length;
1966
+ }, 0),
1967
+ transitionConfidence: transitionInsights,
1968
+ },
1969
+ };
1970
+ }
1971
+
1972
+ function mergeSidecarOperations(existingDoc, inferredDoc) {
1973
+ const existingOps = Array.isArray(existingDoc && existingDoc.operations)
1974
+ ? existingDoc.operations
1975
+ : [];
1976
+ const inferredOps = Array.isArray(inferredDoc && inferredDoc.operations)
1977
+ ? inferredDoc.operations
1978
+ : [];
1979
+
1980
+ const existingByOperationId = new Map();
1981
+ for (const operationEntry of existingOps) {
1982
+ if (operationEntry && operationEntry.operationId) {
1983
+ existingByOperationId.set(operationEntry.operationId, operationEntry);
1984
+ }
1985
+ }
1986
+
1987
+ const mergedOps = [];
1988
+ const consumedExisting = new Set();
1989
+
1990
+ for (const inferredEntry of inferredOps) {
1991
+ const existingEntry = existingByOperationId.get(inferredEntry.operationId);
1992
+ if (!existingEntry) {
1993
+ mergedOps.push(inferredEntry);
1994
+ continue;
1995
+ }
1996
+
1997
+ consumedExisting.add(inferredEntry.operationId);
1998
+
1999
+ const existingFlow = existingEntry["x-openapi-flow"] || {};
2000
+ const inferredFlow = inferredEntry["x-openapi-flow"] || {};
2001
+ const existingTransitions = Array.isArray(existingFlow.transitions) ? existingFlow.transitions : [];
2002
+
2003
+ mergedOps.push({
2004
+ operationId: inferredEntry.operationId,
2005
+ "x-openapi-flow": {
2006
+ ...inferredFlow,
2007
+ ...existingFlow,
2008
+ transitions: existingTransitions.length > 0
2009
+ ? existingTransitions
2010
+ : (Array.isArray(inferredFlow.transitions) ? inferredFlow.transitions : []),
2011
+ },
2012
+ });
2013
+ }
2014
+
2015
+ for (const existingEntry of existingOps) {
2016
+ if (!existingEntry || !existingEntry.operationId) {
2017
+ continue;
2018
+ }
2019
+ if (!consumedExisting.has(existingEntry.operationId)) {
2020
+ mergedOps.push(existingEntry);
2021
+ }
2022
+ }
2023
+
2024
+ return {
2025
+ version: "1.0",
2026
+ operations: mergedOps,
2027
+ mergeStats: {
2028
+ existingOperations: existingOps.length,
2029
+ inferredOperations: inferredOps.length,
2030
+ mergedOperations: mergedOps.length,
2031
+ preservedExistingOnly: Math.max(0, mergedOps.length - inferredOps.length),
2032
+ },
2033
+ };
2034
+ }
2035
+
2036
+ function runAnalyze(parsed) {
2037
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2038
+ if (!targetOpenApiFile) {
2039
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2040
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2041
+ return 1;
2042
+ }
2043
+
2044
+ let api;
2045
+ try {
2046
+ api = loadApi(targetOpenApiFile);
2047
+ } catch (err) {
2048
+ console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
2049
+ return 1;
2050
+ }
2051
+
2052
+ const analysisResult = buildAnalyzedFlowsDoc(api);
2053
+ let sidecarDoc = {
2054
+ version: analysisResult.version,
2055
+ operations: analysisResult.operations,
2056
+ };
2057
+ let mergeStats = null;
2058
+
2059
+ if (parsed.merge) {
2060
+ const mergeFlowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
2061
+ let existingFlows = { version: "1.0", operations: [] };
2062
+ if (fs.existsSync(mergeFlowsPath)) {
2063
+ try {
2064
+ existingFlows = readFlowsFile(mergeFlowsPath);
2065
+ } catch (err) {
2066
+ console.error(`ERROR: Could not parse flows file for merge — ${err.message}`);
2067
+ return 1;
2068
+ }
2069
+ }
2070
+
2071
+ const merged = mergeSidecarOperations(existingFlows, sidecarDoc);
2072
+ sidecarDoc = {
2073
+ version: merged.version,
2074
+ operations: merged.operations,
2075
+ };
2076
+ mergeStats = {
2077
+ enabled: true,
2078
+ flowsPath: mergeFlowsPath,
2079
+ ...merged.mergeStats,
2080
+ };
2081
+ }
2082
+
2083
+ if (parsed.outPath) {
2084
+ writeFlowsFile(parsed.outPath, sidecarDoc);
2085
+ }
2086
+
2087
+ if (parsed.format === "json") {
2088
+ console.log(JSON.stringify({
2089
+ openApiFile: targetOpenApiFile,
2090
+ outputPath: parsed.outPath || null,
2091
+ merge: mergeStats || { enabled: false },
2092
+ analysis: analysisResult.analysis,
2093
+ sidecar: sidecarDoc,
2094
+ }, null, 2));
2095
+ return 0;
2096
+ }
2097
+
2098
+ console.log(`Analyzed OpenAPI source: ${targetOpenApiFile}`);
2099
+ console.log(`Inferred operations: ${analysisResult.analysis.operationCount}`);
2100
+ console.log(`Inferred transitions: ${analysisResult.analysis.inferredTransitions}`);
2101
+ console.log(`States: ${analysisResult.analysis.uniqueStates.join(", ") || "-"}`);
2102
+ if (mergeStats && mergeStats.enabled) {
2103
+ console.log(`Merged with sidecar: ${mergeStats.flowsPath}`);
2104
+ console.log(`Merged operations total: ${mergeStats.mergedOperations}`);
2105
+ }
2106
+
2107
+ if (parsed.outPath) {
2108
+ console.log(`Suggested sidecar written to: ${parsed.outPath}`);
2109
+ return 0;
2110
+ }
2111
+
2112
+ console.log("---");
2113
+ console.log(yaml.dump(sidecarDoc, { noRefs: true, lineWidth: -1 }).trimEnd());
2114
+ return 0;
2115
+ }
2116
+
2117
+ function runGenerateSdk(parsed) {
2118
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2119
+ if (!targetOpenApiFile) {
2120
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2121
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2122
+ return 1;
2123
+ }
2124
+
2125
+ try {
2126
+ const result = generateSdk({
2127
+ apiPath: targetOpenApiFile,
2128
+ language: parsed.language,
2129
+ outputDir: parsed.outputPath,
2130
+ });
2131
+
2132
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
2133
+ console.log(`SDK language: ${result.language}`);
2134
+ console.log(`Output directory: ${result.outputDir}`);
2135
+ console.log(`Flow definitions processed: ${result.flowCount}`);
2136
+ console.log(`Resources generated: ${result.resourceCount}`);
2137
+ for (const resource of result.resources) {
2138
+ console.log(`- ${resource.name}: operations=${resource.operations}, states=${resource.states}, initial=[${resource.initialStates.join(", ")}]`);
2139
+ }
2140
+ return 0;
2141
+ } catch (err) {
2142
+ console.error(`ERROR: Could not generate SDK — ${err.message}`);
2143
+ return 1;
2144
+ }
2145
+ }
2146
+
2147
+ function runExportDocFlows(parsed) {
2148
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2149
+ if (!targetOpenApiFile) {
2150
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2151
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2152
+ return 1;
2153
+ }
2154
+
2155
+ try {
2156
+ const result = exportDocFlows({
2157
+ apiPath: targetOpenApiFile,
2158
+ outputPath: parsed.outputPath,
2159
+ format: parsed.format,
2160
+ });
2161
+
2162
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
2163
+ console.log(`Output: ${result.outputPath}`);
2164
+ console.log(`Format: ${result.format}`);
2165
+ console.log(`Resources: ${result.resources}`);
2166
+ console.log(`Flow definitions: ${result.flowCount}`);
2167
+ return 0;
2168
+ } catch (err) {
2169
+ console.error(`ERROR: Could not export doc flows — ${err.message}`);
2170
+ return 1;
2171
+ }
2172
+ }
2173
+
2174
+ function runGeneratePostman(parsed) {
2175
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2176
+ if (!targetOpenApiFile) {
2177
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2178
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2179
+ return 1;
2180
+ }
2181
+
2182
+ try {
2183
+ const result = generatePostmanCollection({
2184
+ apiPath: targetOpenApiFile,
2185
+ outputPath: parsed.outputPath,
2186
+ withScripts: parsed.withScripts,
2187
+ });
2188
+
2189
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
2190
+ console.log(`Output: ${result.outputPath}`);
2191
+ console.log(`Resources: ${result.resources}`);
2192
+ console.log(`Flow definitions: ${result.flowCount}`);
2193
+ console.log(`Scripts enabled: ${result.withScripts}`);
2194
+ return 0;
2195
+ } catch (err) {
2196
+ console.error(`ERROR: Could not generate Postman collection — ${err.message}`);
2197
+ return 1;
2198
+ }
2199
+ }
2200
+
2201
+ function runGenerateInsomnia(parsed) {
2202
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2203
+ if (!targetOpenApiFile) {
2204
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2205
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2206
+ return 1;
2207
+ }
2208
+
2209
+ try {
2210
+ const result = generateInsomniaWorkspace({
2211
+ apiPath: targetOpenApiFile,
2212
+ outputPath: parsed.outputPath,
2213
+ });
2214
+
2215
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
2216
+ console.log(`Output: ${result.outputPath}`);
2217
+ console.log(`Resources: ${result.resources}`);
2218
+ console.log(`Flow definitions: ${result.flowCount}`);
2219
+ return 0;
2220
+ } catch (err) {
2221
+ console.error(`ERROR: Could not generate Insomnia workspace — ${err.message}`);
2222
+ return 1;
2223
+ }
2224
+ }
2225
+
2226
+ function runGenerateRedoc(parsed) {
2227
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
2228
+ if (!targetOpenApiFile) {
2229
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
2230
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
2231
+ return 1;
2232
+ }
2233
+
2234
+ try {
2235
+ const result = generateRedocPackage({
2236
+ apiPath: targetOpenApiFile,
2237
+ outputDir: parsed.outputPath,
2238
+ });
2239
+
2240
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
2241
+ console.log(`Output directory: ${result.outputDir}`);
2242
+ console.log(`Redoc index: ${result.indexPath}`);
2243
+ console.log(`Resources: ${result.resources}`);
2244
+ console.log(`Flow definitions: ${result.flowCount}`);
2245
+ return 0;
2246
+ } catch (err) {
2247
+ console.error(`ERROR: Could not generate Redoc package — ${err.message}`);
2248
+ return 1;
2249
+ }
2250
+ }
2251
+
1510
2252
  function main() {
1511
2253
  const parsed = parseArgs(process.argv);
1512
2254
 
@@ -1534,6 +2276,30 @@ function main() {
1534
2276
  process.exit(runGraph(parsed));
1535
2277
  }
1536
2278
 
2279
+ if (parsed.command === "analyze") {
2280
+ process.exit(runAnalyze(parsed));
2281
+ }
2282
+
2283
+ if (parsed.command === "generate-sdk") {
2284
+ process.exit(runGenerateSdk(parsed));
2285
+ }
2286
+
2287
+ if (parsed.command === "export-doc-flows") {
2288
+ process.exit(runExportDocFlows(parsed));
2289
+ }
2290
+
2291
+ if (parsed.command === "generate-postman") {
2292
+ process.exit(runGeneratePostman(parsed));
2293
+ }
2294
+
2295
+ if (parsed.command === "generate-insomnia") {
2296
+ process.exit(runGenerateInsomnia(parsed));
2297
+ }
2298
+
2299
+ if (parsed.command === "generate-redoc") {
2300
+ process.exit(runGenerateRedoc(parsed));
2301
+ }
2302
+
1537
2303
  if (parsed.command === "apply") {
1538
2304
  process.exit(runApply(parsed));
1539
2305
  }