x-openapi-flow 1.2.3 → 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.
@@ -8,7 +8,18 @@ const {
8
8
  run,
9
9
  loadApi,
10
10
  extractFlows,
11
+ buildStateGraph,
12
+ detectDuplicateTransitions,
13
+ detectInvalidOperationReferences,
14
+ detectTerminalCoverage,
11
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");
12
23
 
13
24
  const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
14
25
  const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
@@ -43,8 +54,16 @@ function printHelp() {
43
54
 
44
55
  Usage:
45
56
  x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
46
- x-openapi-flow init [openapi-file] [--flows path]
57
+ x-openapi-flow init [openapi-file] [--flows path] [--force] [--dry-run]
47
58
  x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
59
+ x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
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]
48
67
  x-openapi-flow graph <openapi-file> [--format mermaid|json]
49
68
  x-openapi-flow doctor [--config path]
50
69
  x-openapi-flow --help
@@ -53,11 +72,26 @@ Examples:
53
72
  x-openapi-flow validate examples/order-api.yaml
54
73
  x-openapi-flow validate examples/order-api.yaml --profile relaxed
55
74
  x-openapi-flow validate examples/order-api.yaml --strict-quality
56
- x-openapi-flow init openapi.yaml --flows openapi-openapi-flow.yaml
75
+ x-openapi-flow init openapi.yaml --flows openapi.x.yaml
76
+ x-openapi-flow init openapi.yaml --force
77
+ x-openapi-flow init openapi.yaml --dry-run
57
78
  x-openapi-flow init
58
79
  x-openapi-flow apply openapi.yaml
59
80
  x-openapi-flow apply openapi.yaml --in-place
60
81
  x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
82
+ x-openapi-flow diff openapi.yaml
83
+ x-openapi-flow diff openapi.yaml --format json
84
+ x-openapi-flow lint openapi.yaml
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
61
95
  x-openapi-flow graph examples/order-api.yaml
62
96
  x-openapi-flow doctor
63
97
  `);
@@ -69,6 +103,73 @@ function deriveFlowOutputPath(openApiFile) {
69
103
  return path.join(parsed.dir, `${parsed.name}.flow${extension}`);
70
104
  }
71
105
 
106
+ function readSingleLineFromStdin() {
107
+ const sleepBuffer = new SharedArrayBuffer(4);
108
+ const sleepView = new Int32Array(sleepBuffer);
109
+ const chunks = [];
110
+ const buffer = Buffer.alloc(256);
111
+ const maxEagainRetries = 200;
112
+ let eagainRetries = 0;
113
+
114
+ while (true) {
115
+ let bytesRead;
116
+ try {
117
+ bytesRead = fs.readSync(0, buffer, 0, buffer.length, null);
118
+ } catch (err) {
119
+ if (err && (err.code === "EAGAIN" || err.code === "EWOULDBLOCK")) {
120
+ eagainRetries += 1;
121
+ if (eagainRetries >= maxEagainRetries) {
122
+ const retryError = new Error("Could not read interactive input from stdin.");
123
+ retryError.code = "EAGAIN";
124
+ throw retryError;
125
+ }
126
+ Atomics.wait(sleepView, 0, 0, 25);
127
+ continue;
128
+ }
129
+ throw err;
130
+ }
131
+
132
+ eagainRetries = 0;
133
+ if (bytesRead === 0) {
134
+ break;
135
+ }
136
+
137
+ const chunk = buffer.toString("utf8", 0, bytesRead);
138
+ chunks.push(chunk);
139
+ if (chunk.includes("\n")) {
140
+ break;
141
+ }
142
+ }
143
+
144
+ const input = chunks.join("");
145
+ const firstLine = input.split(/\r?\n/)[0] || "";
146
+ return firstLine.trim();
147
+ }
148
+
149
+ function askForConfirmation(question) {
150
+ process.stdout.write(`${question} [y/N]: `);
151
+ const answer = readSingleLineFromStdin().toLowerCase();
152
+ return answer === "y" || answer === "yes";
153
+ }
154
+
155
+ function getNextBackupPath(filePath) {
156
+ let index = 1;
157
+ while (true) {
158
+ const candidate = `${filePath}.backup-${index}`;
159
+ if (!fs.existsSync(candidate)) {
160
+ return candidate;
161
+ }
162
+ index += 1;
163
+ }
164
+ }
165
+
166
+ function applyFlowsAndWrite(openApiFile, flowsDoc, outputPath) {
167
+ const api = loadApi(openApiFile);
168
+ const appliedCount = applyFlowsToOpenApi(api, flowsDoc);
169
+ saveOpenApi(outputPath, api);
170
+ return appliedCount;
171
+ }
172
+
72
173
  function getOptionValue(args, optionName) {
73
174
  const index = args.indexOf(optionName);
74
175
  if (index === -1) {
@@ -175,7 +276,7 @@ function parseValidateArgs(args) {
175
276
  }
176
277
 
177
278
  function parseInitArgs(args) {
178
- const unknown = findUnknownOptions(args, ["--flows"], []);
279
+ const unknown = findUnknownOptions(args, ["--flows"], ["--force", "--dry-run"]);
179
280
  if (unknown) {
180
281
  return { error: `Unknown option: ${unknown}` };
181
282
  }
@@ -189,6 +290,12 @@ function parseInitArgs(args) {
189
290
  if (token === "--flows") {
190
291
  return false;
191
292
  }
293
+ if (token === "--force") {
294
+ return false;
295
+ }
296
+ if (token === "--dry-run") {
297
+ return false;
298
+ }
192
299
  if (index > 0 && args[index - 1] === "--flows") {
193
300
  return false;
194
301
  }
@@ -202,6 +309,203 @@ function parseInitArgs(args) {
202
309
  return {
203
310
  openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
204
311
  flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
312
+ force: args.includes("--force"),
313
+ dryRun: args.includes("--dry-run"),
314
+ };
315
+ }
316
+
317
+ function summarizeSidecarDiff(existingFlowsDoc, mergedFlowsDoc) {
318
+ const existingOps = new Map();
319
+ for (const entry of (existingFlowsDoc && existingFlowsDoc.operations) || []) {
320
+ if (!entry || !entry.operationId) continue;
321
+ existingOps.set(entry.operationId, entry["x-openapi-flow"] || null);
322
+ }
323
+
324
+ const mergedOps = new Map();
325
+ for (const entry of (mergedFlowsDoc && mergedFlowsDoc.operations) || []) {
326
+ if (!entry || !entry.operationId) continue;
327
+ mergedOps.set(entry.operationId, entry["x-openapi-flow"] || null);
328
+ }
329
+
330
+ let added = 0;
331
+ let removed = 0;
332
+ let changed = 0;
333
+ const addedOperationIds = [];
334
+ const removedOperationIds = [];
335
+ const changedOperationIds = [];
336
+ const changedOperationDetails = [];
337
+
338
+ function collectLeafPaths(value, prefix = "") {
339
+ if (value === null || value === undefined) {
340
+ return [prefix || "(root)"];
341
+ }
342
+
343
+ if (Array.isArray(value)) {
344
+ return [prefix || "(root)"];
345
+ }
346
+
347
+ if (typeof value !== "object") {
348
+ return [prefix || "(root)"];
349
+ }
350
+
351
+ const keys = Object.keys(value);
352
+ if (keys.length === 0) {
353
+ return [prefix || "(root)"];
354
+ }
355
+
356
+ return keys.flatMap((key) => {
357
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
358
+ return collectLeafPaths(value[key], nextPrefix);
359
+ });
360
+ }
361
+
362
+ function diffPaths(left, right, prefix = "") {
363
+ if (JSON.stringify(left) === JSON.stringify(right)) {
364
+ return [];
365
+ }
366
+
367
+ const leftIsObject = left && typeof left === "object" && !Array.isArray(left);
368
+ const rightIsObject = right && typeof right === "object" && !Array.isArray(right);
369
+
370
+ if (leftIsObject && rightIsObject) {
371
+ const keys = Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).sort();
372
+ return keys.flatMap((key) => {
373
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
374
+ return diffPaths(left[key], right[key], nextPrefix);
375
+ });
376
+ }
377
+
378
+ if (rightIsObject && !leftIsObject) {
379
+ return collectLeafPaths(right, prefix);
380
+ }
381
+
382
+ if (leftIsObject && !rightIsObject) {
383
+ return collectLeafPaths(left, prefix);
384
+ }
385
+
386
+ return [prefix || "(root)"];
387
+ }
388
+
389
+ for (const [operationId, mergedFlow] of mergedOps.entries()) {
390
+ if (!existingOps.has(operationId)) {
391
+ added += 1;
392
+ addedOperationIds.push(operationId);
393
+ continue;
394
+ }
395
+
396
+ const existingFlow = existingOps.get(operationId);
397
+ if (JSON.stringify(existingFlow) !== JSON.stringify(mergedFlow)) {
398
+ changed += 1;
399
+ changedOperationIds.push(operationId);
400
+ changedOperationDetails.push({
401
+ operationId,
402
+ changedPaths: Array.from(new Set(diffPaths(existingFlow, mergedFlow))).sort(),
403
+ });
404
+ }
405
+ }
406
+
407
+ for (const operationId of existingOps.keys()) {
408
+ if (!mergedOps.has(operationId)) {
409
+ removed += 1;
410
+ removedOperationIds.push(operationId);
411
+ }
412
+ }
413
+
414
+ return {
415
+ added,
416
+ removed,
417
+ changed,
418
+ addedOperationIds: addedOperationIds.sort(),
419
+ removedOperationIds: removedOperationIds.sort(),
420
+ changedOperationIds: changedOperationIds.sort(),
421
+ changedOperationDetails: changedOperationDetails.sort((a, b) => a.operationId.localeCompare(b.operationId)),
422
+ };
423
+ }
424
+
425
+ function parseDiffArgs(args) {
426
+ const unknown = findUnknownOptions(args, ["--flows", "--format"], []);
427
+ if (unknown) {
428
+ return { error: `Unknown option: ${unknown}` };
429
+ }
430
+
431
+ const flowsOpt = getOptionValue(args, "--flows");
432
+ if (flowsOpt.error) {
433
+ return { error: flowsOpt.error };
434
+ }
435
+
436
+ const formatOpt = getOptionValue(args, "--format");
437
+ if (formatOpt.error) {
438
+ return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
439
+ }
440
+
441
+ const format = formatOpt.found ? formatOpt.value : "pretty";
442
+ if (!["pretty", "json"].includes(format)) {
443
+ return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
444
+ }
445
+
446
+ const positional = args.filter((token, index) => {
447
+ if (token === "--flows" || token === "--format") {
448
+ return false;
449
+ }
450
+ if (index > 0 && (args[index - 1] === "--flows" || args[index - 1] === "--format")) {
451
+ return false;
452
+ }
453
+ return !token.startsWith("--");
454
+ });
455
+
456
+ if (positional.length > 1) {
457
+ return { error: `Unexpected argument: ${positional[1]}` };
458
+ }
459
+
460
+ return {
461
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
462
+ flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
463
+ format,
464
+ };
465
+ }
466
+
467
+ function parseLintArgs(args) {
468
+ const unknown = findUnknownOptions(args, ["--format", "--config"], []);
469
+ if (unknown) {
470
+ return { error: `Unknown option: ${unknown}` };
471
+ }
472
+
473
+ const formatOpt = getOptionValue(args, "--format");
474
+ if (formatOpt.error) {
475
+ return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
476
+ }
477
+
478
+ const configOpt = getOptionValue(args, "--config");
479
+ if (configOpt.error) {
480
+ return { error: configOpt.error };
481
+ }
482
+
483
+ const format = formatOpt.found ? formatOpt.value : "pretty";
484
+ if (![
485
+ "pretty",
486
+ "json",
487
+ ].includes(format)) {
488
+ return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
489
+ }
490
+
491
+ const positional = args.filter((token, index) => {
492
+ if (token === "--format" || token === "--config") {
493
+ return false;
494
+ }
495
+ if (index > 0 && (args[index - 1] === "--format" || args[index - 1] === "--config")) {
496
+ return false;
497
+ }
498
+ return !token.startsWith("--");
499
+ });
500
+
501
+ if (positional.length > 1) {
502
+ return { error: `Unexpected argument: ${positional[1]}` };
503
+ }
504
+
505
+ return {
506
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
507
+ format,
508
+ configPath: configOpt.found ? configOpt.value : undefined,
205
509
  };
206
510
  }
207
511
 
@@ -285,6 +589,58 @@ function parseGraphArgs(args) {
285
589
  return { filePath: path.resolve(positional[0]), format };
286
590
  }
287
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
+
288
644
  function parseDoctorArgs(args) {
289
645
  const unknown = findUnknownOptions(args, ["--config"], []);
290
646
  if (unknown) {
@@ -301,71 +657,281 @@ function parseDoctorArgs(args) {
301
657
  };
302
658
  }
303
659
 
304
- function parseArgs(argv) {
305
- const args = argv.slice(2);
306
- const command = args[0];
307
-
308
- if (!command || command === "--help" || command === "-h") {
309
- return { help: true };
660
+ function parseGenerateSdkArgs(args) {
661
+ const unknown = findUnknownOptions(args, ["--lang", "--output"], []);
662
+ if (unknown) {
663
+ return { error: `Unknown option: ${unknown}` };
310
664
  }
311
665
 
312
- const commandArgs = args.slice(1);
313
- if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
314
- return { help: true, command };
666
+ const langOpt = getOptionValue(args, "--lang");
667
+ if (langOpt.error) {
668
+ return { error: `${langOpt.error} Use 'typescript'.` };
315
669
  }
316
670
 
317
- if (command === "validate") {
318
- const parsed = parseValidateArgs(commandArgs);
319
- return parsed.error ? parsed : { command, ...parsed };
671
+ const outputOpt = getOptionValue(args, "--output");
672
+ if (outputOpt.error) {
673
+ return { error: outputOpt.error };
320
674
  }
321
675
 
322
- if (command === "init") {
323
- const parsed = parseInitArgs(commandArgs);
324
- return parsed.error ? parsed : { command, ...parsed };
676
+ if (!langOpt.found) {
677
+ return { error: "Missing --lang option. Usage: x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]" };
325
678
  }
326
679
 
327
- if (command === "graph") {
328
- const parsed = parseGraphArgs(commandArgs);
329
- return parsed.error ? parsed : { command, ...parsed };
680
+ const language = langOpt.value;
681
+ if (language !== "typescript") {
682
+ return { error: `Unsupported --lang '${language}'. MVP currently supports only 'typescript'.` };
330
683
  }
331
684
 
332
- if (command === "apply") {
333
- const parsed = parseApplyArgs(commandArgs);
334
- return parsed.error ? parsed : { command, ...parsed };
335
- }
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
+ });
336
694
 
337
- if (command === "doctor") {
338
- const parsed = parseDoctorArgs(commandArgs);
339
- return parsed.error ? parsed : { command, ...parsed };
695
+ if (positional.length > 1) {
696
+ return { error: `Unexpected argument: ${positional[1]}` };
340
697
  }
341
698
 
342
- return { error: `Unknown command: ${command}` };
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
+ };
343
704
  }
344
705
 
345
- function findOpenApiFile(startDirectory) {
346
- const preferredNames = [
347
- "openapi.yaml",
348
- "openapi.yml",
349
- "openapi.json",
350
- "swagger.yaml",
351
- "swagger.yml",
352
- "swagger.json",
353
- ];
706
+ function parseExportDocFlowsArgs(args) {
707
+ const unknown = findUnknownOptions(args, ["--output", "--format"], []);
708
+ if (unknown) {
709
+ return { error: `Unknown option: ${unknown}` };
710
+ }
354
711
 
355
- for (const fileName of preferredNames) {
356
- const candidate = path.join(startDirectory, fileName);
357
- if (fs.existsSync(candidate)) {
358
- return candidate;
359
- }
712
+ const outputOpt = getOptionValue(args, "--output");
713
+ if (outputOpt.error) {
714
+ return { error: outputOpt.error };
360
715
  }
361
716
 
362
- const ignoredDirs = new Set(["node_modules", ".git", "dist", "build"]);
717
+ const formatOpt = getOptionValue(args, "--format");
718
+ if (formatOpt.error) {
719
+ return { error: `${formatOpt.error} Use 'markdown' or 'json'.` };
720
+ }
363
721
 
364
- function walk(directory) {
365
- let entries = [];
366
- try {
367
- entries = fs.readdirSync(directory, { withFileTypes: true });
368
- } catch (_err) {
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
+
830
+ function parseArgs(argv) {
831
+ const args = argv.slice(2);
832
+ const command = args[0];
833
+
834
+ if (!command || command === "--help" || command === "-h") {
835
+ return { help: true };
836
+ }
837
+
838
+ const commandArgs = args.slice(1);
839
+ if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
840
+ return { help: true, command };
841
+ }
842
+
843
+ if (command === "validate") {
844
+ const parsed = parseValidateArgs(commandArgs);
845
+ return parsed.error ? parsed : { command, ...parsed };
846
+ }
847
+
848
+ if (command === "init") {
849
+ const parsed = parseInitArgs(commandArgs);
850
+ return parsed.error ? parsed : { command, ...parsed };
851
+ }
852
+
853
+ if (command === "graph") {
854
+ const parsed = parseGraphArgs(commandArgs);
855
+ return parsed.error ? parsed : { command, ...parsed };
856
+ }
857
+
858
+ if (command === "analyze") {
859
+ const parsed = parseAnalyzeArgs(commandArgs);
860
+ return parsed.error ? parsed : { command, ...parsed };
861
+ }
862
+
863
+ if (command === "apply") {
864
+ const parsed = parseApplyArgs(commandArgs);
865
+ return parsed.error ? parsed : { command, ...parsed };
866
+ }
867
+
868
+ if (command === "diff") {
869
+ const parsed = parseDiffArgs(commandArgs);
870
+ return parsed.error ? parsed : { command, ...parsed };
871
+ }
872
+
873
+ if (command === "lint") {
874
+ const parsed = parseLintArgs(commandArgs);
875
+ return parsed.error ? parsed : { command, ...parsed };
876
+ }
877
+
878
+ if (command === "doctor") {
879
+ const parsed = parseDoctorArgs(commandArgs);
880
+ return parsed.error ? parsed : { command, ...parsed };
881
+ }
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
+
908
+ return { error: `Unknown command: ${command}` };
909
+ }
910
+
911
+ function findOpenApiFile(startDirectory) {
912
+ const preferredNames = [
913
+ "openapi.yaml",
914
+ "openapi.yml",
915
+ "openapi.json",
916
+ "swagger.yaml",
917
+ "swagger.yml",
918
+ "swagger.json",
919
+ ];
920
+
921
+ for (const fileName of preferredNames) {
922
+ const candidate = path.join(startDirectory, fileName);
923
+ if (fs.existsSync(candidate)) {
924
+ return candidate;
925
+ }
926
+ }
927
+
928
+ const ignoredDirs = new Set(["node_modules", ".git", "dist", "build"]);
929
+
930
+ function walk(directory) {
931
+ let entries = [];
932
+ try {
933
+ entries = fs.readdirSync(directory, { withFileTypes: true });
934
+ } catch (_err) {
369
935
  return null;
370
936
  }
371
937
 
@@ -401,13 +967,40 @@ function resolveFlowsPath(openApiFile, customFlowsPath) {
401
967
  if (openApiFile) {
402
968
  const parsed = path.parse(openApiFile);
403
969
  const extension = parsed.ext.toLowerCase() === ".json" ? ".json" : ".yaml";
404
- const fileName = `${parsed.name}-openapi-flow${extension}`;
405
- return path.join(path.dirname(openApiFile), fileName);
970
+ const baseDir = path.dirname(openApiFile);
971
+ const newFileName = `${parsed.name}.x${extension}`;
972
+ const legacyFileName = `${parsed.name}-openapi-flow${extension}`;
973
+ const newPath = path.join(baseDir, newFileName);
974
+ const legacyPath = path.join(baseDir, legacyFileName);
975
+
976
+ if (fs.existsSync(newPath)) {
977
+ return newPath;
978
+ }
979
+
980
+ if (fs.existsSync(legacyPath)) {
981
+ return legacyPath;
982
+ }
983
+
984
+ return newPath;
406
985
  }
407
986
 
408
987
  return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
409
988
  }
410
989
 
990
+ function looksLikeFlowsSidecar(filePath) {
991
+ if (!filePath) return false;
992
+ const normalized = filePath.toLowerCase();
993
+ return normalized.endsWith(".x.yaml")
994
+ || normalized.endsWith(".x.yml")
995
+ || normalized.endsWith(".x.json")
996
+ || normalized.endsWith("-openapi-flow.yaml")
997
+ || normalized.endsWith("-openapi-flow.yml")
998
+ || normalized.endsWith("-openapi-flow.json")
999
+ || normalized.endsWith("x-openapi-flow.flows.yaml")
1000
+ || normalized.endsWith("x-openapi-flow.flows.yml")
1001
+ || normalized.endsWith("x-openapi-flow.flows.json");
1002
+ }
1003
+
411
1004
  function getOpenApiFormat(filePath) {
412
1005
  return filePath.endsWith(".json") ? "json" : "yaml";
413
1006
  }
@@ -494,8 +1087,8 @@ function buildFlowTemplate(operationId) {
494
1087
  const safeOperationId = operationId || "operation";
495
1088
  return {
496
1089
  version: "1.0",
497
- id: `${safeOperationId}_FLOW_ID`,
498
- current_state: `${safeOperationId}_STATE`,
1090
+ id: safeOperationId,
1091
+ current_state: safeOperationId,
499
1092
  transitions: [],
500
1093
  };
501
1094
  }
@@ -606,6 +1199,7 @@ function runInit(parsed) {
606
1199
  }
607
1200
 
608
1201
  const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
1202
+ const flowOutputPath = deriveFlowOutputPath(targetOpenApiFile);
609
1203
 
610
1204
  let api;
611
1205
  try {
@@ -624,12 +1218,91 @@ function runInit(parsed) {
624
1218
  }
625
1219
 
626
1220
  const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
627
- writeFlowsFile(flowsPath, mergedFlows);
628
1221
  const trackedCount = mergedFlows.operations.length;
1222
+ const sidecarDiff = summarizeSidecarDiff(flowsDoc, mergedFlows);
1223
+
1224
+ let applyMessage = "Init completed without regenerating flow output.";
1225
+ const flowOutputExists = fs.existsSync(flowOutputPath);
1226
+ let shouldRecreateFlowOutput = !flowOutputExists;
1227
+ let sidecarBackupPath = null;
1228
+
1229
+ if (parsed.dryRun) {
1230
+ let dryRunFlowPlan;
1231
+ if (!flowOutputExists) {
1232
+ dryRunFlowPlan = `Would generate flow output: ${flowOutputPath}`;
1233
+ } else if (parsed.force) {
1234
+ dryRunFlowPlan = `Would recreate flow output: ${flowOutputPath} (with sidecar backup).`;
1235
+ } else {
1236
+ dryRunFlowPlan = `Flow output exists at ${flowOutputPath}; would require interactive confirmation to recreate (or use --force).`;
1237
+ }
1238
+
1239
+ console.log(`[dry-run] Using existing OpenAPI file: ${targetOpenApiFile}`);
1240
+ console.log(`[dry-run] Flows sidecar target: ${flowsPath}`);
1241
+ console.log(`[dry-run] Tracked operations: ${trackedCount}`);
1242
+ console.log(`[dry-run] Sidecar changes -> added: ${sidecarDiff.added}, changed: ${sidecarDiff.changed}, removed: ${sidecarDiff.removed}`);
1243
+ console.log(`[dry-run] ${dryRunFlowPlan}`);
1244
+ console.log("[dry-run] No files were written.");
1245
+ return 0;
1246
+ }
1247
+
1248
+ if (flowOutputExists) {
1249
+ if (parsed.force) {
1250
+ shouldRecreateFlowOutput = true;
1251
+ if (fs.existsSync(flowsPath)) {
1252
+ sidecarBackupPath = getNextBackupPath(flowsPath);
1253
+ fs.copyFileSync(flowsPath, sidecarBackupPath);
1254
+ }
1255
+ } else {
1256
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
1257
+ if (isInteractive) {
1258
+ let shouldRecreate;
1259
+ try {
1260
+ shouldRecreate = askForConfirmation(
1261
+ `Flow output already exists at ${flowOutputPath}. Recreate it from current OpenAPI + sidecar?`
1262
+ );
1263
+ } catch (err) {
1264
+ if (err && (err.code === "EAGAIN" || err.code === "EWOULDBLOCK")) {
1265
+ console.error("ERROR: Could not read interactive confirmation from stdin (EAGAIN).");
1266
+ console.error("Run `x-openapi-flow apply` to update the existing flow output in this environment.");
1267
+ return 1;
1268
+ }
1269
+ throw err;
1270
+ }
1271
+
1272
+ if (shouldRecreate) {
1273
+ shouldRecreateFlowOutput = true;
1274
+ if (fs.existsSync(flowsPath)) {
1275
+ sidecarBackupPath = getNextBackupPath(flowsPath);
1276
+ fs.copyFileSync(flowsPath, sidecarBackupPath);
1277
+ }
1278
+ } else {
1279
+ applyMessage = "Flow output kept as-is (recreate cancelled by user).";
1280
+ }
1281
+ } else {
1282
+ console.error(`ERROR: Flow output already exists at ${flowOutputPath}.`);
1283
+ console.error("Use `x-openapi-flow init --force` to recreate, or `x-openapi-flow apply` to update in non-interactive mode.");
1284
+ return 1;
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ writeFlowsFile(flowsPath, mergedFlows);
1290
+
1291
+ if (shouldRecreateFlowOutput) {
1292
+ const appliedCount = applyFlowsAndWrite(targetOpenApiFile, mergedFlows, flowOutputPath);
1293
+ if (flowOutputExists) {
1294
+ applyMessage = sidecarBackupPath
1295
+ ? `Flow output recreated: ${flowOutputPath} (applied entries: ${appliedCount}). Sidecar backup: ${sidecarBackupPath}.`
1296
+ : `Flow output recreated: ${flowOutputPath} (applied entries: ${appliedCount}).`;
1297
+ } else {
1298
+ applyMessage = `Flow output generated: ${flowOutputPath} (applied entries: ${appliedCount}).`;
1299
+ }
1300
+ }
629
1301
 
630
1302
  console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
631
1303
  console.log(`Flows sidecar synced: ${flowsPath}`);
632
1304
  console.log(`Tracked operations: ${trackedCount}`);
1305
+ console.log(applyMessage);
633
1306
  console.log("OpenAPI source unchanged. Edit the sidecar and run apply to generate the full spec.");
634
1307
 
635
1308
  console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
@@ -637,18 +1310,28 @@ function runInit(parsed) {
637
1310
  }
638
1311
 
639
1312
  function runApply(parsed) {
640
- const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
1313
+ let targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
1314
+ let flowsPathFromPositional = null;
1315
+
1316
+ if (!parsed.flowsPath && parsed.openApiFile && looksLikeFlowsSidecar(parsed.openApiFile)) {
1317
+ flowsPathFromPositional = parsed.openApiFile;
1318
+ targetOpenApiFile = findOpenApiFile(process.cwd());
1319
+ }
641
1320
 
642
1321
  if (!targetOpenApiFile) {
643
1322
  console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
644
1323
  console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
1324
+ if (flowsPathFromPositional) {
1325
+ console.error(`Detected sidecar argument: ${flowsPathFromPositional}`);
1326
+ console.error("Provide an OpenAPI file explicitly or run the command from the OpenAPI project root.");
1327
+ }
645
1328
  return 1;
646
1329
  }
647
1330
 
648
- const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
1331
+ const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath || flowsPathFromPositional);
649
1332
  if (!fs.existsSync(flowsPath)) {
650
1333
  console.error(`ERROR: Flows sidecar not found: ${flowsPath}`);
651
- console.error("Run `x-openapi-flow init` first to create and sync the sidecar.");
1334
+ console.error("Run `x-openapi-flow init` first to create and sync the sidecar, or use --flows <sidecar-file>.");
652
1335
  return 1;
653
1336
  }
654
1337
 
@@ -681,6 +1364,68 @@ function runApply(parsed) {
681
1364
  return 0;
682
1365
  }
683
1366
 
1367
+ function runDiff(parsed) {
1368
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
1369
+
1370
+ if (!targetOpenApiFile) {
1371
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
1372
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
1373
+ return 1;
1374
+ }
1375
+
1376
+ const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
1377
+
1378
+ let api;
1379
+ let existingFlows;
1380
+ try {
1381
+ api = loadApi(targetOpenApiFile);
1382
+ } catch (err) {
1383
+ console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
1384
+ return 1;
1385
+ }
1386
+
1387
+ try {
1388
+ existingFlows = readFlowsFile(flowsPath);
1389
+ } catch (err) {
1390
+ console.error(`ERROR: Could not parse flows file — ${err.message}`);
1391
+ return 1;
1392
+ }
1393
+
1394
+ const mergedFlows = mergeFlowsWithOpenApi(api, existingFlows);
1395
+ const diff = summarizeSidecarDiff(existingFlows, mergedFlows);
1396
+
1397
+ if (parsed.format === "json") {
1398
+ console.log(JSON.stringify({
1399
+ openApiFile: targetOpenApiFile,
1400
+ flowsPath,
1401
+ trackedOperations: mergedFlows.operations.length,
1402
+ exists: fs.existsSync(flowsPath),
1403
+ diff,
1404
+ }, null, 2));
1405
+ return 0;
1406
+ }
1407
+
1408
+ const addedText = diff.addedOperationIds.length ? diff.addedOperationIds.join(", ") : "-";
1409
+ const changedText = diff.changedOperationIds.length ? diff.changedOperationIds.join(", ") : "-";
1410
+ const removedText = diff.removedOperationIds.length ? diff.removedOperationIds.join(", ") : "-";
1411
+
1412
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
1413
+ console.log(`Flows sidecar: ${flowsPath}${fs.existsSync(flowsPath) ? "" : " (not found; treated as empty)"}`);
1414
+ console.log(`Tracked operations: ${mergedFlows.operations.length}`);
1415
+ console.log(`Sidecar diff -> added: ${diff.added}, changed: ${diff.changed}, removed: ${diff.removed}`);
1416
+ console.log(`Added operationIds: ${addedText}`);
1417
+ console.log(`Changed operationIds: ${changedText}`);
1418
+ if (diff.changedOperationDetails.length > 0) {
1419
+ console.log("Changed details:");
1420
+ diff.changedOperationDetails.forEach((detail) => {
1421
+ const paths = detail.changedPaths.length ? detail.changedPaths.join(", ") : "(root)";
1422
+ console.log(`- ${detail.operationId}: ${paths}`);
1423
+ });
1424
+ }
1425
+ console.log(`Removed operationIds: ${removedText}`);
1426
+ return 0;
1427
+ }
1428
+
684
1429
  function runDoctor(parsed) {
685
1430
  const config = loadConfig(parsed.configPath);
686
1431
  let hasErrors = false;
@@ -723,13 +1468,173 @@ function runDoctor(parsed) {
723
1468
  return hasErrors ? 1 : 0;
724
1469
  }
725
1470
 
1471
+ function collectOperationIds(api) {
1472
+ const operationsById = new Map();
1473
+ const paths = (api && api.paths) || {};
1474
+ const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1475
+
1476
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
1477
+ for (const method of methods) {
1478
+ const operation = pathItem[method];
1479
+ if (!operation || !operation.operationId) {
1480
+ continue;
1481
+ }
1482
+ operationsById.set(operation.operationId, {
1483
+ endpoint: `${method.toUpperCase()} ${pathKey}`,
1484
+ });
1485
+ }
1486
+ }
1487
+
1488
+ return operationsById;
1489
+ }
1490
+
1491
+ function runLint(parsed, configData = {}) {
1492
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
1493
+ if (!targetOpenApiFile) {
1494
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
1495
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
1496
+ return 1;
1497
+ }
1498
+
1499
+ let api;
1500
+ try {
1501
+ api = loadApi(targetOpenApiFile);
1502
+ } catch (err) {
1503
+ console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
1504
+ return 1;
1505
+ }
1506
+
1507
+ const flows = extractFlows(api);
1508
+ const lintConfig = (configData && configData.lint && configData.lint.rules) || {};
1509
+ const ruleConfig = {
1510
+ next_operation_id_exists: lintConfig.next_operation_id_exists !== false,
1511
+ prerequisite_operation_ids_exist: lintConfig.prerequisite_operation_ids_exist !== false,
1512
+ duplicate_transitions: lintConfig.duplicate_transitions !== false,
1513
+ terminal_path: lintConfig.terminal_path !== false,
1514
+ };
1515
+
1516
+ const operationsById = collectOperationIds(api);
1517
+ const graph = buildStateGraph(flows);
1518
+ const invalidOperationReferences = detectInvalidOperationReferences(operationsById, flows);
1519
+ const duplicateTransitions = detectDuplicateTransitions(flows);
1520
+ const terminalCoverage = detectTerminalCoverage(graph);
1521
+
1522
+ const nextOperationIssues = invalidOperationReferences
1523
+ .filter((entry) => entry.type === "next_operation_id")
1524
+ .map((entry) => ({
1525
+ operation_id: entry.operation_id,
1526
+ declared_in: entry.declared_in,
1527
+ }));
1528
+
1529
+ const prerequisiteIssues = invalidOperationReferences
1530
+ .filter((entry) => entry.type === "prerequisite_operation_ids")
1531
+ .map((entry) => ({
1532
+ operation_id: entry.operation_id,
1533
+ declared_in: entry.declared_in,
1534
+ }));
1535
+
1536
+ const issues = {
1537
+ next_operation_id_exists: ruleConfig.next_operation_id_exists ? nextOperationIssues : [],
1538
+ prerequisite_operation_ids_exist: ruleConfig.prerequisite_operation_ids_exist ? prerequisiteIssues : [],
1539
+ duplicate_transitions: ruleConfig.duplicate_transitions ? duplicateTransitions : [],
1540
+ terminal_path: {
1541
+ terminal_states: ruleConfig.terminal_path ? terminalCoverage.terminal_states : [],
1542
+ non_terminating_states: ruleConfig.terminal_path ? terminalCoverage.non_terminating_states : [],
1543
+ },
1544
+ };
1545
+
1546
+ const errorCount =
1547
+ issues.next_operation_id_exists.length +
1548
+ issues.prerequisite_operation_ids_exist.length +
1549
+ issues.duplicate_transitions.length +
1550
+ issues.terminal_path.non_terminating_states.length;
1551
+
1552
+ const result = {
1553
+ ok: errorCount === 0,
1554
+ path: targetOpenApiFile,
1555
+ flowCount: flows.length,
1556
+ ruleConfig,
1557
+ issues,
1558
+ summary: {
1559
+ errors: errorCount,
1560
+ violated_rules: Object.entries({
1561
+ next_operation_id_exists: issues.next_operation_id_exists.length,
1562
+ prerequisite_operation_ids_exist: issues.prerequisite_operation_ids_exist.length,
1563
+ duplicate_transitions: issues.duplicate_transitions.length,
1564
+ terminal_path: issues.terminal_path.non_terminating_states.length,
1565
+ })
1566
+ .filter(([, count]) => count > 0)
1567
+ .map(([rule]) => rule),
1568
+ },
1569
+ };
1570
+
1571
+ if (parsed.format === "json") {
1572
+ console.log(JSON.stringify(result, null, 2));
1573
+ return result.ok ? 0 : 1;
1574
+ }
1575
+
1576
+ console.log(`Linting: ${targetOpenApiFile}`);
1577
+ console.log(`Found ${flows.length} x-openapi-flow definition(s).`);
1578
+ console.log("Rules:");
1579
+ Object.entries(ruleConfig).forEach(([ruleName, enabled]) => {
1580
+ console.log(`- ${ruleName}: ${enabled ? "enabled" : "disabled"}`);
1581
+ });
1582
+
1583
+ if (flows.length === 0) {
1584
+ console.log("No x-openapi-flow definitions found.");
1585
+ return 0;
1586
+ }
1587
+
1588
+ if (issues.next_operation_id_exists.length === 0) {
1589
+ console.log("✔ next_operation_id_exists: no invalid references.");
1590
+ } else {
1591
+ console.error(`✘ next_operation_id_exists: ${issues.next_operation_id_exists.length} invalid reference(s).`);
1592
+ issues.next_operation_id_exists.forEach((entry) => {
1593
+ console.error(` - ${entry.operation_id} (declared in ${entry.declared_in})`);
1594
+ });
1595
+ }
1596
+
1597
+ if (issues.prerequisite_operation_ids_exist.length === 0) {
1598
+ console.log("✔ prerequisite_operation_ids_exist: no invalid references.");
1599
+ } else {
1600
+ console.error(`✘ prerequisite_operation_ids_exist: ${issues.prerequisite_operation_ids_exist.length} invalid reference(s).`);
1601
+ issues.prerequisite_operation_ids_exist.forEach((entry) => {
1602
+ console.error(` - ${entry.operation_id} (declared in ${entry.declared_in})`);
1603
+ });
1604
+ }
1605
+
1606
+ if (issues.duplicate_transitions.length === 0) {
1607
+ console.log("✔ duplicate_transitions: none found.");
1608
+ } else {
1609
+ console.error(`✘ duplicate_transitions: ${issues.duplicate_transitions.length} duplicate transition group(s).`);
1610
+ issues.duplicate_transitions.forEach((entry) => {
1611
+ console.error(` - ${entry.from} -> ${entry.to} (${entry.trigger_type}), count=${entry.count}`);
1612
+ });
1613
+ }
1614
+
1615
+ if (issues.terminal_path.non_terminating_states.length === 0) {
1616
+ console.log("✔ terminal_path: all states can reach a terminal state.");
1617
+ } else {
1618
+ console.error(
1619
+ `✘ terminal_path: states without path to terminal -> ${issues.terminal_path.non_terminating_states.join(", ")}`
1620
+ );
1621
+ }
1622
+
1623
+ if (result.ok) {
1624
+ console.log("Lint checks passed ✔");
1625
+ } else {
1626
+ console.error("Lint checks finished with errors.");
1627
+ }
1628
+
1629
+ return result.ok ? 0 : 1;
1630
+ }
1631
+
726
1632
  function buildMermaidGraph(filePath) {
727
1633
  const flows = extractFlowsForGraph(filePath);
728
1634
  if (flows.length === 0) {
729
1635
  throw new Error("No x-openapi-flow definitions found in OpenAPI or sidecar file");
730
1636
  }
731
1637
 
732
- const lines = ["stateDiagram-v2"];
733
1638
  const nodes = new Set();
734
1639
  const edges = [];
735
1640
  const edgeSeen = new Set();
@@ -771,19 +1676,47 @@ function buildMermaidGraph(filePath) {
771
1676
  next_operation_id: transition.next_operation_id,
772
1677
  prerequisite_operation_ids: transition.prerequisite_operation_ids || [],
773
1678
  });
774
-
775
- lines.push(` ${from} --> ${to}${label ? `: ${label}` : ""}`);
776
1679
  }
777
1680
  }
778
1681
 
779
- for (const state of nodes) {
780
- lines.splice(1, 0, ` state ${state}`);
1682
+ const sortedNodes = [...nodes].sort((a, b) => a.localeCompare(b));
1683
+ const sortedEdges = [...edges].sort((left, right) => {
1684
+ const leftKey = [
1685
+ left.from,
1686
+ left.to,
1687
+ left.next_operation_id || "",
1688
+ (left.prerequisite_operation_ids || []).join(","),
1689
+ ].join("::");
1690
+ const rightKey = [
1691
+ right.from,
1692
+ right.to,
1693
+ right.next_operation_id || "",
1694
+ (right.prerequisite_operation_ids || []).join(","),
1695
+ ].join("::");
1696
+ return leftKey.localeCompare(rightKey);
1697
+ });
1698
+
1699
+ const lines = ["stateDiagram-v2"];
1700
+ for (const state of sortedNodes) {
1701
+ lines.push(` state ${state}`);
1702
+ }
1703
+ for (const edge of sortedEdges) {
1704
+ const labelParts = [];
1705
+ if (edge.next_operation_id) {
1706
+ labelParts.push(`next:${edge.next_operation_id}`);
1707
+ }
1708
+ if (Array.isArray(edge.prerequisite_operation_ids) && edge.prerequisite_operation_ids.length > 0) {
1709
+ labelParts.push(`requires:${edge.prerequisite_operation_ids.join(",")}`);
1710
+ }
1711
+ const label = labelParts.join(" | ");
1712
+ lines.push(` ${edge.from} --> ${edge.to}${label ? `: ${label}` : ""}`);
781
1713
  }
782
1714
 
783
1715
  return {
1716
+ format_version: "1.0",
784
1717
  flowCount: flows.length,
785
- nodes: [...nodes],
786
- edges,
1718
+ nodes: sortedNodes,
1719
+ edges: sortedEdges,
787
1720
  mermaid: lines.join("\n"),
788
1721
  };
789
1722
  }
@@ -848,6 +1781,474 @@ function runGraph(parsed) {
848
1781
  }
849
1782
  }
850
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
+
851
2252
  function main() {
852
2253
  const parsed = parseArgs(process.argv);
853
2254
 
@@ -875,10 +2276,47 @@ function main() {
875
2276
  process.exit(runGraph(parsed));
876
2277
  }
877
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
+
878
2303
  if (parsed.command === "apply") {
879
2304
  process.exit(runApply(parsed));
880
2305
  }
881
2306
 
2307
+ if (parsed.command === "diff") {
2308
+ process.exit(runDiff(parsed));
2309
+ }
2310
+
2311
+ if (parsed.command === "lint") {
2312
+ const config = loadConfig(parsed.configPath);
2313
+ if (config.error) {
2314
+ console.error(`ERROR: ${config.error}`);
2315
+ process.exit(1);
2316
+ }
2317
+ process.exit(runLint(parsed, config.data));
2318
+ }
2319
+
882
2320
  const config = loadConfig(parsed.configPath);
883
2321
  if (config.error) {
884
2322
  console.error(`ERROR: ${config.error}`);