x-openapi-flow 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -18
- package/bin/x-openapi-flow.js +690 -18
- package/lib/swagger-ui/x-openapi-flow-plugin.js +472 -71
- package/lib/validator.js +36 -3
- package/package.json +3 -2
- package/schema/flow-schema.json +2 -2
package/bin/x-openapi-flow.js
CHANGED
|
@@ -8,6 +8,10 @@ const {
|
|
|
8
8
|
run,
|
|
9
9
|
loadApi,
|
|
10
10
|
extractFlows,
|
|
11
|
+
buildStateGraph,
|
|
12
|
+
detectDuplicateTransitions,
|
|
13
|
+
detectInvalidOperationReferences,
|
|
14
|
+
detectTerminalCoverage,
|
|
11
15
|
} = require("../lib/validator");
|
|
12
16
|
|
|
13
17
|
const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
@@ -43,8 +47,10 @@ function printHelp() {
|
|
|
43
47
|
|
|
44
48
|
Usage:
|
|
45
49
|
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]
|
|
50
|
+
x-openapi-flow init [openapi-file] [--flows path] [--force] [--dry-run]
|
|
47
51
|
x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
|
|
52
|
+
x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
|
|
53
|
+
x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
|
|
48
54
|
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
49
55
|
x-openapi-flow doctor [--config path]
|
|
50
56
|
x-openapi-flow --help
|
|
@@ -53,11 +59,17 @@ Examples:
|
|
|
53
59
|
x-openapi-flow validate examples/order-api.yaml
|
|
54
60
|
x-openapi-flow validate examples/order-api.yaml --profile relaxed
|
|
55
61
|
x-openapi-flow validate examples/order-api.yaml --strict-quality
|
|
56
|
-
x-openapi-flow init openapi.yaml --flows openapi
|
|
62
|
+
x-openapi-flow init openapi.yaml --flows openapi.x.yaml
|
|
63
|
+
x-openapi-flow init openapi.yaml --force
|
|
64
|
+
x-openapi-flow init openapi.yaml --dry-run
|
|
57
65
|
x-openapi-flow init
|
|
58
66
|
x-openapi-flow apply openapi.yaml
|
|
59
67
|
x-openapi-flow apply openapi.yaml --in-place
|
|
60
68
|
x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
|
|
69
|
+
x-openapi-flow diff openapi.yaml
|
|
70
|
+
x-openapi-flow diff openapi.yaml --format json
|
|
71
|
+
x-openapi-flow lint openapi.yaml
|
|
72
|
+
x-openapi-flow lint openapi.yaml --format json
|
|
61
73
|
x-openapi-flow graph examples/order-api.yaml
|
|
62
74
|
x-openapi-flow doctor
|
|
63
75
|
`);
|
|
@@ -69,6 +81,73 @@ function deriveFlowOutputPath(openApiFile) {
|
|
|
69
81
|
return path.join(parsed.dir, `${parsed.name}.flow${extension}`);
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
function readSingleLineFromStdin() {
|
|
85
|
+
const sleepBuffer = new SharedArrayBuffer(4);
|
|
86
|
+
const sleepView = new Int32Array(sleepBuffer);
|
|
87
|
+
const chunks = [];
|
|
88
|
+
const buffer = Buffer.alloc(256);
|
|
89
|
+
const maxEagainRetries = 200;
|
|
90
|
+
let eagainRetries = 0;
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
let bytesRead;
|
|
94
|
+
try {
|
|
95
|
+
bytesRead = fs.readSync(0, buffer, 0, buffer.length, null);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err && (err.code === "EAGAIN" || err.code === "EWOULDBLOCK")) {
|
|
98
|
+
eagainRetries += 1;
|
|
99
|
+
if (eagainRetries >= maxEagainRetries) {
|
|
100
|
+
const retryError = new Error("Could not read interactive input from stdin.");
|
|
101
|
+
retryError.code = "EAGAIN";
|
|
102
|
+
throw retryError;
|
|
103
|
+
}
|
|
104
|
+
Atomics.wait(sleepView, 0, 0, 25);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
eagainRetries = 0;
|
|
111
|
+
if (bytesRead === 0) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const chunk = buffer.toString("utf8", 0, bytesRead);
|
|
116
|
+
chunks.push(chunk);
|
|
117
|
+
if (chunk.includes("\n")) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const input = chunks.join("");
|
|
123
|
+
const firstLine = input.split(/\r?\n/)[0] || "";
|
|
124
|
+
return firstLine.trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function askForConfirmation(question) {
|
|
128
|
+
process.stdout.write(`${question} [y/N]: `);
|
|
129
|
+
const answer = readSingleLineFromStdin().toLowerCase();
|
|
130
|
+
return answer === "y" || answer === "yes";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getNextBackupPath(filePath) {
|
|
134
|
+
let index = 1;
|
|
135
|
+
while (true) {
|
|
136
|
+
const candidate = `${filePath}.backup-${index}`;
|
|
137
|
+
if (!fs.existsSync(candidate)) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
index += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applyFlowsAndWrite(openApiFile, flowsDoc, outputPath) {
|
|
145
|
+
const api = loadApi(openApiFile);
|
|
146
|
+
const appliedCount = applyFlowsToOpenApi(api, flowsDoc);
|
|
147
|
+
saveOpenApi(outputPath, api);
|
|
148
|
+
return appliedCount;
|
|
149
|
+
}
|
|
150
|
+
|
|
72
151
|
function getOptionValue(args, optionName) {
|
|
73
152
|
const index = args.indexOf(optionName);
|
|
74
153
|
if (index === -1) {
|
|
@@ -175,7 +254,7 @@ function parseValidateArgs(args) {
|
|
|
175
254
|
}
|
|
176
255
|
|
|
177
256
|
function parseInitArgs(args) {
|
|
178
|
-
const unknown = findUnknownOptions(args, ["--flows"], []);
|
|
257
|
+
const unknown = findUnknownOptions(args, ["--flows"], ["--force", "--dry-run"]);
|
|
179
258
|
if (unknown) {
|
|
180
259
|
return { error: `Unknown option: ${unknown}` };
|
|
181
260
|
}
|
|
@@ -189,6 +268,12 @@ function parseInitArgs(args) {
|
|
|
189
268
|
if (token === "--flows") {
|
|
190
269
|
return false;
|
|
191
270
|
}
|
|
271
|
+
if (token === "--force") {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if (token === "--dry-run") {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
192
277
|
if (index > 0 && args[index - 1] === "--flows") {
|
|
193
278
|
return false;
|
|
194
279
|
}
|
|
@@ -202,6 +287,203 @@ function parseInitArgs(args) {
|
|
|
202
287
|
return {
|
|
203
288
|
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
204
289
|
flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
|
|
290
|
+
force: args.includes("--force"),
|
|
291
|
+
dryRun: args.includes("--dry-run"),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function summarizeSidecarDiff(existingFlowsDoc, mergedFlowsDoc) {
|
|
296
|
+
const existingOps = new Map();
|
|
297
|
+
for (const entry of (existingFlowsDoc && existingFlowsDoc.operations) || []) {
|
|
298
|
+
if (!entry || !entry.operationId) continue;
|
|
299
|
+
existingOps.set(entry.operationId, entry["x-openapi-flow"] || null);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const mergedOps = new Map();
|
|
303
|
+
for (const entry of (mergedFlowsDoc && mergedFlowsDoc.operations) || []) {
|
|
304
|
+
if (!entry || !entry.operationId) continue;
|
|
305
|
+
mergedOps.set(entry.operationId, entry["x-openapi-flow"] || null);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let added = 0;
|
|
309
|
+
let removed = 0;
|
|
310
|
+
let changed = 0;
|
|
311
|
+
const addedOperationIds = [];
|
|
312
|
+
const removedOperationIds = [];
|
|
313
|
+
const changedOperationIds = [];
|
|
314
|
+
const changedOperationDetails = [];
|
|
315
|
+
|
|
316
|
+
function collectLeafPaths(value, prefix = "") {
|
|
317
|
+
if (value === null || value === undefined) {
|
|
318
|
+
return [prefix || "(root)"];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
return [prefix || "(root)"];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (typeof value !== "object") {
|
|
326
|
+
return [prefix || "(root)"];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const keys = Object.keys(value);
|
|
330
|
+
if (keys.length === 0) {
|
|
331
|
+
return [prefix || "(root)"];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return keys.flatMap((key) => {
|
|
335
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
336
|
+
return collectLeafPaths(value[key], nextPrefix);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function diffPaths(left, right, prefix = "") {
|
|
341
|
+
if (JSON.stringify(left) === JSON.stringify(right)) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const leftIsObject = left && typeof left === "object" && !Array.isArray(left);
|
|
346
|
+
const rightIsObject = right && typeof right === "object" && !Array.isArray(right);
|
|
347
|
+
|
|
348
|
+
if (leftIsObject && rightIsObject) {
|
|
349
|
+
const keys = Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).sort();
|
|
350
|
+
return keys.flatMap((key) => {
|
|
351
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
352
|
+
return diffPaths(left[key], right[key], nextPrefix);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (rightIsObject && !leftIsObject) {
|
|
357
|
+
return collectLeafPaths(right, prefix);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (leftIsObject && !rightIsObject) {
|
|
361
|
+
return collectLeafPaths(left, prefix);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [prefix || "(root)"];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const [operationId, mergedFlow] of mergedOps.entries()) {
|
|
368
|
+
if (!existingOps.has(operationId)) {
|
|
369
|
+
added += 1;
|
|
370
|
+
addedOperationIds.push(operationId);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const existingFlow = existingOps.get(operationId);
|
|
375
|
+
if (JSON.stringify(existingFlow) !== JSON.stringify(mergedFlow)) {
|
|
376
|
+
changed += 1;
|
|
377
|
+
changedOperationIds.push(operationId);
|
|
378
|
+
changedOperationDetails.push({
|
|
379
|
+
operationId,
|
|
380
|
+
changedPaths: Array.from(new Set(diffPaths(existingFlow, mergedFlow))).sort(),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const operationId of existingOps.keys()) {
|
|
386
|
+
if (!mergedOps.has(operationId)) {
|
|
387
|
+
removed += 1;
|
|
388
|
+
removedOperationIds.push(operationId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
added,
|
|
394
|
+
removed,
|
|
395
|
+
changed,
|
|
396
|
+
addedOperationIds: addedOperationIds.sort(),
|
|
397
|
+
removedOperationIds: removedOperationIds.sort(),
|
|
398
|
+
changedOperationIds: changedOperationIds.sort(),
|
|
399
|
+
changedOperationDetails: changedOperationDetails.sort((a, b) => a.operationId.localeCompare(b.operationId)),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function parseDiffArgs(args) {
|
|
404
|
+
const unknown = findUnknownOptions(args, ["--flows", "--format"], []);
|
|
405
|
+
if (unknown) {
|
|
406
|
+
return { error: `Unknown option: ${unknown}` };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const flowsOpt = getOptionValue(args, "--flows");
|
|
410
|
+
if (flowsOpt.error) {
|
|
411
|
+
return { error: flowsOpt.error };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
415
|
+
if (formatOpt.error) {
|
|
416
|
+
return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const format = formatOpt.found ? formatOpt.value : "pretty";
|
|
420
|
+
if (!["pretty", "json"].includes(format)) {
|
|
421
|
+
return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const positional = args.filter((token, index) => {
|
|
425
|
+
if (token === "--flows" || token === "--format") {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if (index > 0 && (args[index - 1] === "--flows" || args[index - 1] === "--format")) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return !token.startsWith("--");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (positional.length > 1) {
|
|
435
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
440
|
+
flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
|
|
441
|
+
format,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseLintArgs(args) {
|
|
446
|
+
const unknown = findUnknownOptions(args, ["--format", "--config"], []);
|
|
447
|
+
if (unknown) {
|
|
448
|
+
return { error: `Unknown option: ${unknown}` };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
452
|
+
if (formatOpt.error) {
|
|
453
|
+
return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const configOpt = getOptionValue(args, "--config");
|
|
457
|
+
if (configOpt.error) {
|
|
458
|
+
return { error: configOpt.error };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const format = formatOpt.found ? formatOpt.value : "pretty";
|
|
462
|
+
if (![
|
|
463
|
+
"pretty",
|
|
464
|
+
"json",
|
|
465
|
+
].includes(format)) {
|
|
466
|
+
return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const positional = args.filter((token, index) => {
|
|
470
|
+
if (token === "--format" || token === "--config") {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (index > 0 && (args[index - 1] === "--format" || args[index - 1] === "--config")) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return !token.startsWith("--");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (positional.length > 1) {
|
|
480
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
485
|
+
format,
|
|
486
|
+
configPath: configOpt.found ? configOpt.value : undefined,
|
|
205
487
|
};
|
|
206
488
|
}
|
|
207
489
|
|
|
@@ -334,6 +616,16 @@ function parseArgs(argv) {
|
|
|
334
616
|
return parsed.error ? parsed : { command, ...parsed };
|
|
335
617
|
}
|
|
336
618
|
|
|
619
|
+
if (command === "diff") {
|
|
620
|
+
const parsed = parseDiffArgs(commandArgs);
|
|
621
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (command === "lint") {
|
|
625
|
+
const parsed = parseLintArgs(commandArgs);
|
|
626
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
627
|
+
}
|
|
628
|
+
|
|
337
629
|
if (command === "doctor") {
|
|
338
630
|
const parsed = parseDoctorArgs(commandArgs);
|
|
339
631
|
return parsed.error ? parsed : { command, ...parsed };
|
|
@@ -401,13 +693,40 @@ function resolveFlowsPath(openApiFile, customFlowsPath) {
|
|
|
401
693
|
if (openApiFile) {
|
|
402
694
|
const parsed = path.parse(openApiFile);
|
|
403
695
|
const extension = parsed.ext.toLowerCase() === ".json" ? ".json" : ".yaml";
|
|
404
|
-
const
|
|
405
|
-
|
|
696
|
+
const baseDir = path.dirname(openApiFile);
|
|
697
|
+
const newFileName = `${parsed.name}.x${extension}`;
|
|
698
|
+
const legacyFileName = `${parsed.name}-openapi-flow${extension}`;
|
|
699
|
+
const newPath = path.join(baseDir, newFileName);
|
|
700
|
+
const legacyPath = path.join(baseDir, legacyFileName);
|
|
701
|
+
|
|
702
|
+
if (fs.existsSync(newPath)) {
|
|
703
|
+
return newPath;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (fs.existsSync(legacyPath)) {
|
|
707
|
+
return legacyPath;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return newPath;
|
|
406
711
|
}
|
|
407
712
|
|
|
408
713
|
return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
|
|
409
714
|
}
|
|
410
715
|
|
|
716
|
+
function looksLikeFlowsSidecar(filePath) {
|
|
717
|
+
if (!filePath) return false;
|
|
718
|
+
const normalized = filePath.toLowerCase();
|
|
719
|
+
return normalized.endsWith(".x.yaml")
|
|
720
|
+
|| normalized.endsWith(".x.yml")
|
|
721
|
+
|| normalized.endsWith(".x.json")
|
|
722
|
+
|| normalized.endsWith("-openapi-flow.yaml")
|
|
723
|
+
|| normalized.endsWith("-openapi-flow.yml")
|
|
724
|
+
|| normalized.endsWith("-openapi-flow.json")
|
|
725
|
+
|| normalized.endsWith("x-openapi-flow.flows.yaml")
|
|
726
|
+
|| normalized.endsWith("x-openapi-flow.flows.yml")
|
|
727
|
+
|| normalized.endsWith("x-openapi-flow.flows.json");
|
|
728
|
+
}
|
|
729
|
+
|
|
411
730
|
function getOpenApiFormat(filePath) {
|
|
412
731
|
return filePath.endsWith(".json") ? "json" : "yaml";
|
|
413
732
|
}
|
|
@@ -494,8 +813,8 @@ function buildFlowTemplate(operationId) {
|
|
|
494
813
|
const safeOperationId = operationId || "operation";
|
|
495
814
|
return {
|
|
496
815
|
version: "1.0",
|
|
497
|
-
id:
|
|
498
|
-
current_state:
|
|
816
|
+
id: safeOperationId,
|
|
817
|
+
current_state: safeOperationId,
|
|
499
818
|
transitions: [],
|
|
500
819
|
};
|
|
501
820
|
}
|
|
@@ -606,6 +925,7 @@ function runInit(parsed) {
|
|
|
606
925
|
}
|
|
607
926
|
|
|
608
927
|
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
928
|
+
const flowOutputPath = deriveFlowOutputPath(targetOpenApiFile);
|
|
609
929
|
|
|
610
930
|
let api;
|
|
611
931
|
try {
|
|
@@ -624,12 +944,91 @@ function runInit(parsed) {
|
|
|
624
944
|
}
|
|
625
945
|
|
|
626
946
|
const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
|
|
627
|
-
writeFlowsFile(flowsPath, mergedFlows);
|
|
628
947
|
const trackedCount = mergedFlows.operations.length;
|
|
948
|
+
const sidecarDiff = summarizeSidecarDiff(flowsDoc, mergedFlows);
|
|
949
|
+
|
|
950
|
+
let applyMessage = "Init completed without regenerating flow output.";
|
|
951
|
+
const flowOutputExists = fs.existsSync(flowOutputPath);
|
|
952
|
+
let shouldRecreateFlowOutput = !flowOutputExists;
|
|
953
|
+
let sidecarBackupPath = null;
|
|
954
|
+
|
|
955
|
+
if (parsed.dryRun) {
|
|
956
|
+
let dryRunFlowPlan;
|
|
957
|
+
if (!flowOutputExists) {
|
|
958
|
+
dryRunFlowPlan = `Would generate flow output: ${flowOutputPath}`;
|
|
959
|
+
} else if (parsed.force) {
|
|
960
|
+
dryRunFlowPlan = `Would recreate flow output: ${flowOutputPath} (with sidecar backup).`;
|
|
961
|
+
} else {
|
|
962
|
+
dryRunFlowPlan = `Flow output exists at ${flowOutputPath}; would require interactive confirmation to recreate (or use --force).`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
console.log(`[dry-run] Using existing OpenAPI file: ${targetOpenApiFile}`);
|
|
966
|
+
console.log(`[dry-run] Flows sidecar target: ${flowsPath}`);
|
|
967
|
+
console.log(`[dry-run] Tracked operations: ${trackedCount}`);
|
|
968
|
+
console.log(`[dry-run] Sidecar changes -> added: ${sidecarDiff.added}, changed: ${sidecarDiff.changed}, removed: ${sidecarDiff.removed}`);
|
|
969
|
+
console.log(`[dry-run] ${dryRunFlowPlan}`);
|
|
970
|
+
console.log("[dry-run] No files were written.");
|
|
971
|
+
return 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (flowOutputExists) {
|
|
975
|
+
if (parsed.force) {
|
|
976
|
+
shouldRecreateFlowOutput = true;
|
|
977
|
+
if (fs.existsSync(flowsPath)) {
|
|
978
|
+
sidecarBackupPath = getNextBackupPath(flowsPath);
|
|
979
|
+
fs.copyFileSync(flowsPath, sidecarBackupPath);
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
983
|
+
if (isInteractive) {
|
|
984
|
+
let shouldRecreate;
|
|
985
|
+
try {
|
|
986
|
+
shouldRecreate = askForConfirmation(
|
|
987
|
+
`Flow output already exists at ${flowOutputPath}. Recreate it from current OpenAPI + sidecar?`
|
|
988
|
+
);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
if (err && (err.code === "EAGAIN" || err.code === "EWOULDBLOCK")) {
|
|
991
|
+
console.error("ERROR: Could not read interactive confirmation from stdin (EAGAIN).");
|
|
992
|
+
console.error("Run `x-openapi-flow apply` to update the existing flow output in this environment.");
|
|
993
|
+
return 1;
|
|
994
|
+
}
|
|
995
|
+
throw err;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (shouldRecreate) {
|
|
999
|
+
shouldRecreateFlowOutput = true;
|
|
1000
|
+
if (fs.existsSync(flowsPath)) {
|
|
1001
|
+
sidecarBackupPath = getNextBackupPath(flowsPath);
|
|
1002
|
+
fs.copyFileSync(flowsPath, sidecarBackupPath);
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
applyMessage = "Flow output kept as-is (recreate cancelled by user).";
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
console.error(`ERROR: Flow output already exists at ${flowOutputPath}.`);
|
|
1009
|
+
console.error("Use `x-openapi-flow init --force` to recreate, or `x-openapi-flow apply` to update in non-interactive mode.");
|
|
1010
|
+
return 1;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
writeFlowsFile(flowsPath, mergedFlows);
|
|
1016
|
+
|
|
1017
|
+
if (shouldRecreateFlowOutput) {
|
|
1018
|
+
const appliedCount = applyFlowsAndWrite(targetOpenApiFile, mergedFlows, flowOutputPath);
|
|
1019
|
+
if (flowOutputExists) {
|
|
1020
|
+
applyMessage = sidecarBackupPath
|
|
1021
|
+
? `Flow output recreated: ${flowOutputPath} (applied entries: ${appliedCount}). Sidecar backup: ${sidecarBackupPath}.`
|
|
1022
|
+
: `Flow output recreated: ${flowOutputPath} (applied entries: ${appliedCount}).`;
|
|
1023
|
+
} else {
|
|
1024
|
+
applyMessage = `Flow output generated: ${flowOutputPath} (applied entries: ${appliedCount}).`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
629
1027
|
|
|
630
1028
|
console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
|
|
631
1029
|
console.log(`Flows sidecar synced: ${flowsPath}`);
|
|
632
1030
|
console.log(`Tracked operations: ${trackedCount}`);
|
|
1031
|
+
console.log(applyMessage);
|
|
633
1032
|
console.log("OpenAPI source unchanged. Edit the sidecar and run apply to generate the full spec.");
|
|
634
1033
|
|
|
635
1034
|
console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
|
|
@@ -637,18 +1036,28 @@ function runInit(parsed) {
|
|
|
637
1036
|
}
|
|
638
1037
|
|
|
639
1038
|
function runApply(parsed) {
|
|
640
|
-
|
|
1039
|
+
let targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
1040
|
+
let flowsPathFromPositional = null;
|
|
1041
|
+
|
|
1042
|
+
if (!parsed.flowsPath && parsed.openApiFile && looksLikeFlowsSidecar(parsed.openApiFile)) {
|
|
1043
|
+
flowsPathFromPositional = parsed.openApiFile;
|
|
1044
|
+
targetOpenApiFile = findOpenApiFile(process.cwd());
|
|
1045
|
+
}
|
|
641
1046
|
|
|
642
1047
|
if (!targetOpenApiFile) {
|
|
643
1048
|
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
644
1049
|
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
1050
|
+
if (flowsPathFromPositional) {
|
|
1051
|
+
console.error(`Detected sidecar argument: ${flowsPathFromPositional}`);
|
|
1052
|
+
console.error("Provide an OpenAPI file explicitly or run the command from the OpenAPI project root.");
|
|
1053
|
+
}
|
|
645
1054
|
return 1;
|
|
646
1055
|
}
|
|
647
1056
|
|
|
648
|
-
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
1057
|
+
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath || flowsPathFromPositional);
|
|
649
1058
|
if (!fs.existsSync(flowsPath)) {
|
|
650
1059
|
console.error(`ERROR: Flows sidecar not found: ${flowsPath}`);
|
|
651
|
-
console.error("Run `x-openapi-flow init` first to create and sync the sidecar
|
|
1060
|
+
console.error("Run `x-openapi-flow init` first to create and sync the sidecar, or use --flows <sidecar-file>.");
|
|
652
1061
|
return 1;
|
|
653
1062
|
}
|
|
654
1063
|
|
|
@@ -681,6 +1090,68 @@ function runApply(parsed) {
|
|
|
681
1090
|
return 0;
|
|
682
1091
|
}
|
|
683
1092
|
|
|
1093
|
+
function runDiff(parsed) {
|
|
1094
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
1095
|
+
|
|
1096
|
+
if (!targetOpenApiFile) {
|
|
1097
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
1098
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
1099
|
+
return 1;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
1103
|
+
|
|
1104
|
+
let api;
|
|
1105
|
+
let existingFlows;
|
|
1106
|
+
try {
|
|
1107
|
+
api = loadApi(targetOpenApiFile);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
|
|
1110
|
+
return 1;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
existingFlows = readFlowsFile(flowsPath);
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
console.error(`ERROR: Could not parse flows file — ${err.message}`);
|
|
1117
|
+
return 1;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const mergedFlows = mergeFlowsWithOpenApi(api, existingFlows);
|
|
1121
|
+
const diff = summarizeSidecarDiff(existingFlows, mergedFlows);
|
|
1122
|
+
|
|
1123
|
+
if (parsed.format === "json") {
|
|
1124
|
+
console.log(JSON.stringify({
|
|
1125
|
+
openApiFile: targetOpenApiFile,
|
|
1126
|
+
flowsPath,
|
|
1127
|
+
trackedOperations: mergedFlows.operations.length,
|
|
1128
|
+
exists: fs.existsSync(flowsPath),
|
|
1129
|
+
diff,
|
|
1130
|
+
}, null, 2));
|
|
1131
|
+
return 0;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const addedText = diff.addedOperationIds.length ? diff.addedOperationIds.join(", ") : "-";
|
|
1135
|
+
const changedText = diff.changedOperationIds.length ? diff.changedOperationIds.join(", ") : "-";
|
|
1136
|
+
const removedText = diff.removedOperationIds.length ? diff.removedOperationIds.join(", ") : "-";
|
|
1137
|
+
|
|
1138
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
1139
|
+
console.log(`Flows sidecar: ${flowsPath}${fs.existsSync(flowsPath) ? "" : " (not found; treated as empty)"}`);
|
|
1140
|
+
console.log(`Tracked operations: ${mergedFlows.operations.length}`);
|
|
1141
|
+
console.log(`Sidecar diff -> added: ${diff.added}, changed: ${diff.changed}, removed: ${diff.removed}`);
|
|
1142
|
+
console.log(`Added operationIds: ${addedText}`);
|
|
1143
|
+
console.log(`Changed operationIds: ${changedText}`);
|
|
1144
|
+
if (diff.changedOperationDetails.length > 0) {
|
|
1145
|
+
console.log("Changed details:");
|
|
1146
|
+
diff.changedOperationDetails.forEach((detail) => {
|
|
1147
|
+
const paths = detail.changedPaths.length ? detail.changedPaths.join(", ") : "(root)";
|
|
1148
|
+
console.log(`- ${detail.operationId}: ${paths}`);
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
console.log(`Removed operationIds: ${removedText}`);
|
|
1152
|
+
return 0;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
684
1155
|
function runDoctor(parsed) {
|
|
685
1156
|
const config = loadConfig(parsed.configPath);
|
|
686
1157
|
let hasErrors = false;
|
|
@@ -723,13 +1194,173 @@ function runDoctor(parsed) {
|
|
|
723
1194
|
return hasErrors ? 1 : 0;
|
|
724
1195
|
}
|
|
725
1196
|
|
|
1197
|
+
function collectOperationIds(api) {
|
|
1198
|
+
const operationsById = new Map();
|
|
1199
|
+
const paths = (api && api.paths) || {};
|
|
1200
|
+
const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
1201
|
+
|
|
1202
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
1203
|
+
for (const method of methods) {
|
|
1204
|
+
const operation = pathItem[method];
|
|
1205
|
+
if (!operation || !operation.operationId) {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
operationsById.set(operation.operationId, {
|
|
1209
|
+
endpoint: `${method.toUpperCase()} ${pathKey}`,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return operationsById;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function runLint(parsed, configData = {}) {
|
|
1218
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
1219
|
+
if (!targetOpenApiFile) {
|
|
1220
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
1221
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
1222
|
+
return 1;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let api;
|
|
1226
|
+
try {
|
|
1227
|
+
api = loadApi(targetOpenApiFile);
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
|
|
1230
|
+
return 1;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const flows = extractFlows(api);
|
|
1234
|
+
const lintConfig = (configData && configData.lint && configData.lint.rules) || {};
|
|
1235
|
+
const ruleConfig = {
|
|
1236
|
+
next_operation_id_exists: lintConfig.next_operation_id_exists !== false,
|
|
1237
|
+
prerequisite_operation_ids_exist: lintConfig.prerequisite_operation_ids_exist !== false,
|
|
1238
|
+
duplicate_transitions: lintConfig.duplicate_transitions !== false,
|
|
1239
|
+
terminal_path: lintConfig.terminal_path !== false,
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const operationsById = collectOperationIds(api);
|
|
1243
|
+
const graph = buildStateGraph(flows);
|
|
1244
|
+
const invalidOperationReferences = detectInvalidOperationReferences(operationsById, flows);
|
|
1245
|
+
const duplicateTransitions = detectDuplicateTransitions(flows);
|
|
1246
|
+
const terminalCoverage = detectTerminalCoverage(graph);
|
|
1247
|
+
|
|
1248
|
+
const nextOperationIssues = invalidOperationReferences
|
|
1249
|
+
.filter((entry) => entry.type === "next_operation_id")
|
|
1250
|
+
.map((entry) => ({
|
|
1251
|
+
operation_id: entry.operation_id,
|
|
1252
|
+
declared_in: entry.declared_in,
|
|
1253
|
+
}));
|
|
1254
|
+
|
|
1255
|
+
const prerequisiteIssues = invalidOperationReferences
|
|
1256
|
+
.filter((entry) => entry.type === "prerequisite_operation_ids")
|
|
1257
|
+
.map((entry) => ({
|
|
1258
|
+
operation_id: entry.operation_id,
|
|
1259
|
+
declared_in: entry.declared_in,
|
|
1260
|
+
}));
|
|
1261
|
+
|
|
1262
|
+
const issues = {
|
|
1263
|
+
next_operation_id_exists: ruleConfig.next_operation_id_exists ? nextOperationIssues : [],
|
|
1264
|
+
prerequisite_operation_ids_exist: ruleConfig.prerequisite_operation_ids_exist ? prerequisiteIssues : [],
|
|
1265
|
+
duplicate_transitions: ruleConfig.duplicate_transitions ? duplicateTransitions : [],
|
|
1266
|
+
terminal_path: {
|
|
1267
|
+
terminal_states: ruleConfig.terminal_path ? terminalCoverage.terminal_states : [],
|
|
1268
|
+
non_terminating_states: ruleConfig.terminal_path ? terminalCoverage.non_terminating_states : [],
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const errorCount =
|
|
1273
|
+
issues.next_operation_id_exists.length +
|
|
1274
|
+
issues.prerequisite_operation_ids_exist.length +
|
|
1275
|
+
issues.duplicate_transitions.length +
|
|
1276
|
+
issues.terminal_path.non_terminating_states.length;
|
|
1277
|
+
|
|
1278
|
+
const result = {
|
|
1279
|
+
ok: errorCount === 0,
|
|
1280
|
+
path: targetOpenApiFile,
|
|
1281
|
+
flowCount: flows.length,
|
|
1282
|
+
ruleConfig,
|
|
1283
|
+
issues,
|
|
1284
|
+
summary: {
|
|
1285
|
+
errors: errorCount,
|
|
1286
|
+
violated_rules: Object.entries({
|
|
1287
|
+
next_operation_id_exists: issues.next_operation_id_exists.length,
|
|
1288
|
+
prerequisite_operation_ids_exist: issues.prerequisite_operation_ids_exist.length,
|
|
1289
|
+
duplicate_transitions: issues.duplicate_transitions.length,
|
|
1290
|
+
terminal_path: issues.terminal_path.non_terminating_states.length,
|
|
1291
|
+
})
|
|
1292
|
+
.filter(([, count]) => count > 0)
|
|
1293
|
+
.map(([rule]) => rule),
|
|
1294
|
+
},
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
if (parsed.format === "json") {
|
|
1298
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1299
|
+
return result.ok ? 0 : 1;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
console.log(`Linting: ${targetOpenApiFile}`);
|
|
1303
|
+
console.log(`Found ${flows.length} x-openapi-flow definition(s).`);
|
|
1304
|
+
console.log("Rules:");
|
|
1305
|
+
Object.entries(ruleConfig).forEach(([ruleName, enabled]) => {
|
|
1306
|
+
console.log(`- ${ruleName}: ${enabled ? "enabled" : "disabled"}`);
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
if (flows.length === 0) {
|
|
1310
|
+
console.log("No x-openapi-flow definitions found.");
|
|
1311
|
+
return 0;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (issues.next_operation_id_exists.length === 0) {
|
|
1315
|
+
console.log("✔ next_operation_id_exists: no invalid references.");
|
|
1316
|
+
} else {
|
|
1317
|
+
console.error(`✘ next_operation_id_exists: ${issues.next_operation_id_exists.length} invalid reference(s).`);
|
|
1318
|
+
issues.next_operation_id_exists.forEach((entry) => {
|
|
1319
|
+
console.error(` - ${entry.operation_id} (declared in ${entry.declared_in})`);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (issues.prerequisite_operation_ids_exist.length === 0) {
|
|
1324
|
+
console.log("✔ prerequisite_operation_ids_exist: no invalid references.");
|
|
1325
|
+
} else {
|
|
1326
|
+
console.error(`✘ prerequisite_operation_ids_exist: ${issues.prerequisite_operation_ids_exist.length} invalid reference(s).`);
|
|
1327
|
+
issues.prerequisite_operation_ids_exist.forEach((entry) => {
|
|
1328
|
+
console.error(` - ${entry.operation_id} (declared in ${entry.declared_in})`);
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (issues.duplicate_transitions.length === 0) {
|
|
1333
|
+
console.log("✔ duplicate_transitions: none found.");
|
|
1334
|
+
} else {
|
|
1335
|
+
console.error(`✘ duplicate_transitions: ${issues.duplicate_transitions.length} duplicate transition group(s).`);
|
|
1336
|
+
issues.duplicate_transitions.forEach((entry) => {
|
|
1337
|
+
console.error(` - ${entry.from} -> ${entry.to} (${entry.trigger_type}), count=${entry.count}`);
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (issues.terminal_path.non_terminating_states.length === 0) {
|
|
1342
|
+
console.log("✔ terminal_path: all states can reach a terminal state.");
|
|
1343
|
+
} else {
|
|
1344
|
+
console.error(
|
|
1345
|
+
`✘ terminal_path: states without path to terminal -> ${issues.terminal_path.non_terminating_states.join(", ")}`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (result.ok) {
|
|
1350
|
+
console.log("Lint checks passed ✔");
|
|
1351
|
+
} else {
|
|
1352
|
+
console.error("Lint checks finished with errors.");
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
return result.ok ? 0 : 1;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
726
1358
|
function buildMermaidGraph(filePath) {
|
|
727
1359
|
const flows = extractFlowsForGraph(filePath);
|
|
728
1360
|
if (flows.length === 0) {
|
|
729
1361
|
throw new Error("No x-openapi-flow definitions found in OpenAPI or sidecar file");
|
|
730
1362
|
}
|
|
731
1363
|
|
|
732
|
-
const lines = ["stateDiagram-v2"];
|
|
733
1364
|
const nodes = new Set();
|
|
734
1365
|
const edges = [];
|
|
735
1366
|
const edgeSeen = new Set();
|
|
@@ -771,19 +1402,47 @@ function buildMermaidGraph(filePath) {
|
|
|
771
1402
|
next_operation_id: transition.next_operation_id,
|
|
772
1403
|
prerequisite_operation_ids: transition.prerequisite_operation_ids || [],
|
|
773
1404
|
});
|
|
774
|
-
|
|
775
|
-
lines.push(` ${from} --> ${to}${label ? `: ${label}` : ""}`);
|
|
776
1405
|
}
|
|
777
1406
|
}
|
|
778
1407
|
|
|
779
|
-
|
|
780
|
-
|
|
1408
|
+
const sortedNodes = [...nodes].sort((a, b) => a.localeCompare(b));
|
|
1409
|
+
const sortedEdges = [...edges].sort((left, right) => {
|
|
1410
|
+
const leftKey = [
|
|
1411
|
+
left.from,
|
|
1412
|
+
left.to,
|
|
1413
|
+
left.next_operation_id || "",
|
|
1414
|
+
(left.prerequisite_operation_ids || []).join(","),
|
|
1415
|
+
].join("::");
|
|
1416
|
+
const rightKey = [
|
|
1417
|
+
right.from,
|
|
1418
|
+
right.to,
|
|
1419
|
+
right.next_operation_id || "",
|
|
1420
|
+
(right.prerequisite_operation_ids || []).join(","),
|
|
1421
|
+
].join("::");
|
|
1422
|
+
return leftKey.localeCompare(rightKey);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
const lines = ["stateDiagram-v2"];
|
|
1426
|
+
for (const state of sortedNodes) {
|
|
1427
|
+
lines.push(` state ${state}`);
|
|
1428
|
+
}
|
|
1429
|
+
for (const edge of sortedEdges) {
|
|
1430
|
+
const labelParts = [];
|
|
1431
|
+
if (edge.next_operation_id) {
|
|
1432
|
+
labelParts.push(`next:${edge.next_operation_id}`);
|
|
1433
|
+
}
|
|
1434
|
+
if (Array.isArray(edge.prerequisite_operation_ids) && edge.prerequisite_operation_ids.length > 0) {
|
|
1435
|
+
labelParts.push(`requires:${edge.prerequisite_operation_ids.join(",")}`);
|
|
1436
|
+
}
|
|
1437
|
+
const label = labelParts.join(" | ");
|
|
1438
|
+
lines.push(` ${edge.from} --> ${edge.to}${label ? `: ${label}` : ""}`);
|
|
781
1439
|
}
|
|
782
1440
|
|
|
783
1441
|
return {
|
|
1442
|
+
format_version: "1.0",
|
|
784
1443
|
flowCount: flows.length,
|
|
785
|
-
nodes:
|
|
786
|
-
edges,
|
|
1444
|
+
nodes: sortedNodes,
|
|
1445
|
+
edges: sortedEdges,
|
|
787
1446
|
mermaid: lines.join("\n"),
|
|
788
1447
|
};
|
|
789
1448
|
}
|
|
@@ -879,6 +1538,19 @@ function main() {
|
|
|
879
1538
|
process.exit(runApply(parsed));
|
|
880
1539
|
}
|
|
881
1540
|
|
|
1541
|
+
if (parsed.command === "diff") {
|
|
1542
|
+
process.exit(runDiff(parsed));
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (parsed.command === "lint") {
|
|
1546
|
+
const config = loadConfig(parsed.configPath);
|
|
1547
|
+
if (config.error) {
|
|
1548
|
+
console.error(`ERROR: ${config.error}`);
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
process.exit(runLint(parsed, config.data));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
882
1554
|
const config = loadConfig(parsed.configPath);
|
|
883
1555
|
if (config.error) {
|
|
884
1556
|
console.error(`ERROR: ${config.error}`);
|