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.
- package/README.md +319 -20
- package/adapters/collections/insomnia-adapter.js +73 -0
- package/adapters/collections/postman-adapter.js +145 -0
- package/adapters/docs/doc-adapter.js +119 -0
- package/adapters/flow-output-adapters.js +15 -0
- package/adapters/shared/helpers.js +87 -0
- package/adapters/ui/redoc/x-openapi-flow-redoc-plugin.js +127 -0
- package/adapters/ui/redoc-adapter.js +75 -0
- package/adapters/ui/swagger-ui/x-openapi-flow-plugin.js +856 -0
- package/bin/x-openapi-flow.js +1502 -64
- package/lib/sdk-generator.js +673 -0
- package/lib/validator.js +36 -3
- package/package.json +9 -3
- package/schema/flow-schema.json +2 -2
- package/templates/go/README.md +3 -0
- package/templates/kotlin/README.md +3 -0
- package/templates/python/README.md +3 -0
- package/templates/typescript/flow-helpers.hbs +26 -0
- package/templates/typescript/http-client.hbs +37 -0
- package/templates/typescript/index.hbs +16 -0
- package/templates/typescript/resource.hbs +24 -0
- package/examples/swagger-ui/index.html +0 -33
- package/lib/swagger-ui/x-openapi-flow-plugin.js +0 -455
package/bin/x-openapi-flow.js
CHANGED
|
@@ -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
|
|
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
|
|
305
|
-
const
|
|
306
|
-
|
|
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
|
|
313
|
-
if (
|
|
314
|
-
return {
|
|
666
|
+
const langOpt = getOptionValue(args, "--lang");
|
|
667
|
+
if (langOpt.error) {
|
|
668
|
+
return { error: `${langOpt.error} Use 'typescript'.` };
|
|
315
669
|
}
|
|
316
670
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
671
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
672
|
+
if (outputOpt.error) {
|
|
673
|
+
return { error: outputOpt.error };
|
|
320
674
|
}
|
|
321
675
|
|
|
322
|
-
if (
|
|
323
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 (
|
|
338
|
-
|
|
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 {
|
|
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
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
717
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
718
|
+
if (formatOpt.error) {
|
|
719
|
+
return { error: `${formatOpt.error} Use 'markdown' or 'json'.` };
|
|
720
|
+
}
|
|
363
721
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
405
|
-
|
|
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:
|
|
498
|
-
current_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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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:
|
|
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}`);
|