x-openapi-flow 1.4.4 → 1.5.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 +247 -6
- package/adapters/flow-output-adapters.js +2 -0
- package/adapters/tests/flow-test-adapter.js +254 -0
- package/bin/x-openapi-flow.js +942 -15
- package/lib/error-codes.js +289 -0
- package/lib/openapi-state-machine-adapter.js +151 -0
- package/lib/runtime-guard/core.js +176 -0
- package/lib/runtime-guard/errors.js +85 -0
- package/lib/runtime-guard/express.js +70 -0
- package/lib/runtime-guard/fastify.js +65 -0
- package/lib/runtime-guard/index.js +15 -0
- package/lib/runtime-guard/model.js +85 -0
- package/lib/runtime-guard.js +3 -0
- package/lib/state-machine-engine.js +208 -0
- package/lib/validator.js +378 -1
- package/package.json +4 -2
package/bin/x-openapi-flow.js
CHANGED
|
@@ -12,13 +12,17 @@ const {
|
|
|
12
12
|
detectDuplicateTransitions,
|
|
13
13
|
detectInvalidOperationReferences,
|
|
14
14
|
detectTerminalCoverage,
|
|
15
|
+
detectSemanticModelingWarnings,
|
|
16
|
+
computeQualityReport,
|
|
15
17
|
} = require("../lib/validator");
|
|
18
|
+
const { CODES } = require("../lib/error-codes");
|
|
16
19
|
const { generateSdk } = require("../lib/sdk-generator");
|
|
17
20
|
const {
|
|
18
21
|
exportDocFlows,
|
|
19
22
|
generatePostmanCollection,
|
|
20
23
|
generateInsomniaWorkspace,
|
|
21
24
|
generateRedocPackage,
|
|
25
|
+
generateFlowTests,
|
|
22
26
|
} = require("../adapters/flow-output-adapters");
|
|
23
27
|
const pkg = require("../package.json");
|
|
24
28
|
|
|
@@ -26,16 +30,19 @@ const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
|
26
30
|
const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
|
|
27
31
|
const KNOWN_COMMANDS = [
|
|
28
32
|
"validate",
|
|
33
|
+
"quickstart",
|
|
29
34
|
"init",
|
|
30
35
|
"apply",
|
|
31
36
|
"diff",
|
|
32
37
|
"lint",
|
|
33
38
|
"analyze",
|
|
39
|
+
"quality-report",
|
|
34
40
|
"generate-sdk",
|
|
35
41
|
"export-doc-flows",
|
|
36
42
|
"generate-postman",
|
|
37
43
|
"generate-insomnia",
|
|
38
44
|
"generate-redoc",
|
|
45
|
+
"generate-flow-tests",
|
|
39
46
|
"graph",
|
|
40
47
|
"doctor",
|
|
41
48
|
"completion",
|
|
@@ -43,7 +50,7 @@ const KNOWN_COMMANDS = [
|
|
|
43
50
|
|
|
44
51
|
const COMMAND_SNIPPETS = {
|
|
45
52
|
validate: {
|
|
46
|
-
usage: "x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]",
|
|
53
|
+
usage: "x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--semantic] [--config path]",
|
|
47
54
|
examples: [
|
|
48
55
|
"x-openapi-flow validate examples/order-api.yaml",
|
|
49
56
|
"x-openapi-flow validate examples/order-api.yaml --profile relaxed",
|
|
@@ -59,6 +66,15 @@ const COMMAND_SNIPPETS = {
|
|
|
59
66
|
"x-openapi-flow init openapi.yaml --dry-run",
|
|
60
67
|
],
|
|
61
68
|
},
|
|
69
|
+
quickstart: {
|
|
70
|
+
usage: "x-openapi-flow quickstart [--dir path] [--runtime express|fastify] [--force]",
|
|
71
|
+
examples: [
|
|
72
|
+
"x-openapi-flow quickstart",
|
|
73
|
+
"x-openapi-flow quickstart --dir ./my-flow-demo",
|
|
74
|
+
"x-openapi-flow quickstart --runtime fastify",
|
|
75
|
+
"x-openapi-flow quickstart --dir ./my-flow-demo --force",
|
|
76
|
+
],
|
|
77
|
+
},
|
|
62
78
|
apply: {
|
|
63
79
|
usage: "x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]",
|
|
64
80
|
examples: [
|
|
@@ -75,12 +91,20 @@ const COMMAND_SNIPPETS = {
|
|
|
75
91
|
],
|
|
76
92
|
},
|
|
77
93
|
lint: {
|
|
78
|
-
usage: "x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]",
|
|
94
|
+
usage: "x-openapi-flow lint [openapi-file] [--format pretty|json] [--semantic] [--config path]",
|
|
79
95
|
examples: [
|
|
80
96
|
"x-openapi-flow lint openapi.yaml",
|
|
81
97
|
"x-openapi-flow lint openapi.yaml --format json",
|
|
82
98
|
],
|
|
83
99
|
},
|
|
100
|
+
"quality-report": {
|
|
101
|
+
usage: "x-openapi-flow quality-report <openapi-file> [--profile core|relaxed|strict] [--semantic] [--output path]",
|
|
102
|
+
examples: [
|
|
103
|
+
"x-openapi-flow quality-report openapi.flow.yaml",
|
|
104
|
+
"x-openapi-flow quality-report openapi.flow.yaml --profile strict --semantic",
|
|
105
|
+
"x-openapi-flow quality-report openapi.flow.yaml --output quality-report.json",
|
|
106
|
+
],
|
|
107
|
+
},
|
|
84
108
|
analyze: {
|
|
85
109
|
usage: "x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]",
|
|
86
110
|
examples: [
|
|
@@ -119,6 +143,14 @@ const COMMAND_SNIPPETS = {
|
|
|
119
143
|
"x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow",
|
|
120
144
|
],
|
|
121
145
|
},
|
|
146
|
+
"generate-flow-tests": {
|
|
147
|
+
usage: "x-openapi-flow generate-flow-tests [openapi-file] [--format jest|vitest|postman] [--output path] [--with-scripts]",
|
|
148
|
+
examples: [
|
|
149
|
+
"x-openapi-flow generate-flow-tests openapi.flow.yaml --format jest --output ./flow.generated.test.js",
|
|
150
|
+
"x-openapi-flow generate-flow-tests openapi.flow.yaml --format vitest --output ./flow.generated.vitest.test.js",
|
|
151
|
+
"x-openapi-flow generate-flow-tests openapi.flow.yaml --format postman --output ./x-openapi-flow.flow-tests.postman_collection.json --with-scripts",
|
|
152
|
+
],
|
|
153
|
+
},
|
|
122
154
|
graph: {
|
|
123
155
|
usage: "x-openapi-flow graph <openapi-file> [--format mermaid|json]",
|
|
124
156
|
examples: [
|
|
@@ -183,11 +215,14 @@ _x_openapi_flow() {
|
|
|
183
215
|
|
|
184
216
|
case "$words[2]" in
|
|
185
217
|
validate)
|
|
186
|
-
_values 'options' --format --profile --strict-quality --config --help
|
|
218
|
+
_values 'options' --format --profile --strict-quality --semantic --config --help
|
|
187
219
|
;;
|
|
188
220
|
init)
|
|
189
221
|
_values 'options' --flows --force --dry-run --help
|
|
190
222
|
;;
|
|
223
|
+
quickstart)
|
|
224
|
+
_values 'options' --dir --runtime --force --help
|
|
225
|
+
;;
|
|
191
226
|
apply)
|
|
192
227
|
_values 'options' --flows --out --in-place --help
|
|
193
228
|
;;
|
|
@@ -195,7 +230,10 @@ _x_openapi_flow() {
|
|
|
195
230
|
_values 'options' --flows --format --help
|
|
196
231
|
;;
|
|
197
232
|
lint)
|
|
198
|
-
_values 'options' --format --config --help
|
|
233
|
+
_values 'options' --format --semantic --config --help
|
|
234
|
+
;;
|
|
235
|
+
quality-report)
|
|
236
|
+
_values 'options' --profile --semantic --output --help
|
|
199
237
|
;;
|
|
200
238
|
analyze)
|
|
201
239
|
_values 'options' --format --out --merge --flows --help
|
|
@@ -215,6 +253,9 @@ _x_openapi_flow() {
|
|
|
215
253
|
generate-redoc)
|
|
216
254
|
_values 'options' --output --help
|
|
217
255
|
;;
|
|
256
|
+
generate-flow-tests)
|
|
257
|
+
_values 'options' --format --output --with-scripts --help
|
|
258
|
+
;;
|
|
218
259
|
graph)
|
|
219
260
|
_values 'options' --format --help
|
|
220
261
|
;;
|
|
@@ -248,11 +289,14 @@ compdef _x_openapi_flow x-openapi-flow
|
|
|
248
289
|
|
|
249
290
|
case "\${COMP_WORDS[1]}" in
|
|
250
291
|
validate)
|
|
251
|
-
COMPREPLY=( $(compgen -W "--format --profile --strict-quality --config --help --verbose" -- "\$cur") )
|
|
292
|
+
COMPREPLY=( $(compgen -W "--format --profile --strict-quality --semantic --config --help --verbose" -- "\$cur") )
|
|
252
293
|
;;
|
|
253
294
|
init)
|
|
254
295
|
COMPREPLY=( $(compgen -W "--flows --force --dry-run --help --verbose" -- "\$cur") )
|
|
255
296
|
;;
|
|
297
|
+
quickstart)
|
|
298
|
+
COMPREPLY=( $(compgen -W "--dir --runtime --force --help --verbose" -- "\$cur") )
|
|
299
|
+
;;
|
|
256
300
|
apply)
|
|
257
301
|
COMPREPLY=( $(compgen -W "--flows --out --in-place --help --verbose" -- "\$cur") )
|
|
258
302
|
;;
|
|
@@ -260,7 +304,10 @@ compdef _x_openapi_flow x-openapi-flow
|
|
|
260
304
|
COMPREPLY=( $(compgen -W "--flows --format --help --verbose" -- "\$cur") )
|
|
261
305
|
;;
|
|
262
306
|
lint)
|
|
263
|
-
COMPREPLY=( $(compgen -W "--format --config --help --verbose" -- "\$cur") )
|
|
307
|
+
COMPREPLY=( $(compgen -W "--format --semantic --config --help --verbose" -- "\$cur") )
|
|
308
|
+
;;
|
|
309
|
+
quality-report)
|
|
310
|
+
COMPREPLY=( $(compgen -W "--profile --semantic --output --help --verbose" -- "\$cur") )
|
|
264
311
|
;;
|
|
265
312
|
analyze)
|
|
266
313
|
COMPREPLY=( $(compgen -W "--format --out --merge --flows --help --verbose" -- "\$cur") )
|
|
@@ -280,6 +327,9 @@ compdef _x_openapi_flow x-openapi-flow
|
|
|
280
327
|
generate-redoc)
|
|
281
328
|
COMPREPLY=( $(compgen -W "--output --help --verbose" -- "\$cur") )
|
|
282
329
|
;;
|
|
330
|
+
generate-flow-tests)
|
|
331
|
+
COMPREPLY=( $(compgen -W "--format --output --with-scripts --help --verbose" -- "\$cur") )
|
|
332
|
+
;;
|
|
283
333
|
graph)
|
|
284
334
|
COMPREPLY=( $(compgen -W "--format --help --verbose" -- "\$cur") )
|
|
285
335
|
;;
|
|
@@ -418,17 +468,20 @@ Global options:
|
|
|
418
468
|
|
|
419
469
|
Usage:
|
|
420
470
|
x-openapi-flow <command> [options]
|
|
421
|
-
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
471
|
+
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--semantic] [--config path]
|
|
472
|
+
x-openapi-flow quickstart [--dir path] [--runtime express|fastify] [--force]
|
|
422
473
|
x-openapi-flow init [openapi-file] [--flows path] [--force] [--dry-run]
|
|
423
474
|
x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
|
|
424
475
|
x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
|
|
425
|
-
x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
|
|
476
|
+
x-openapi-flow lint [openapi-file] [--format pretty|json] [--semantic] [--config path]
|
|
426
477
|
x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
|
|
478
|
+
x-openapi-flow quality-report <openapi-file> [--profile core|relaxed|strict] [--semantic] [--output path]
|
|
427
479
|
x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
|
|
428
480
|
x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
|
|
429
481
|
x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
|
|
430
482
|
x-openapi-flow generate-insomnia [openapi-file] [--output path]
|
|
431
483
|
x-openapi-flow generate-redoc [openapi-file] [--output path]
|
|
484
|
+
x-openapi-flow generate-flow-tests [openapi-file] [--format jest|vitest|postman] [--output path] [--with-scripts]
|
|
432
485
|
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
433
486
|
x-openapi-flow doctor [--config path]
|
|
434
487
|
x-openapi-flow completion [bash|zsh]
|
|
@@ -441,6 +494,10 @@ Examples:
|
|
|
441
494
|
x-openapi-flow validate examples/order-api.yaml
|
|
442
495
|
x-openapi-flow validate examples/order-api.yaml --profile relaxed
|
|
443
496
|
x-openapi-flow validate examples/order-api.yaml --strict-quality
|
|
497
|
+
x-openapi-flow validate examples/order-api.yaml --semantic
|
|
498
|
+
x-openapi-flow quickstart
|
|
499
|
+
x-openapi-flow quickstart --dir ./my-flow-demo
|
|
500
|
+
x-openapi-flow quickstart --runtime fastify
|
|
444
501
|
x-openapi-flow init openapi.yaml --flows openapi.x.yaml
|
|
445
502
|
x-openapi-flow init openapi.yaml --force
|
|
446
503
|
x-openapi-flow init openapi.yaml --dry-run
|
|
@@ -452,6 +509,10 @@ Examples:
|
|
|
452
509
|
x-openapi-flow diff openapi.yaml --format json
|
|
453
510
|
x-openapi-flow lint openapi.yaml
|
|
454
511
|
x-openapi-flow lint openapi.yaml --format json
|
|
512
|
+
x-openapi-flow lint openapi.yaml --semantic
|
|
513
|
+
x-openapi-flow quality-report openapi.flow.yaml
|
|
514
|
+
x-openapi-flow quality-report openapi.flow.yaml --profile strict --semantic
|
|
515
|
+
x-openapi-flow quality-report openapi.flow.yaml --output quality-report.json
|
|
455
516
|
x-openapi-flow analyze openapi.yaml
|
|
456
517
|
x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
|
|
457
518
|
x-openapi-flow analyze openapi.yaml --format json
|
|
@@ -461,10 +522,14 @@ Examples:
|
|
|
461
522
|
x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
|
|
462
523
|
x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
|
|
463
524
|
x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
|
|
525
|
+
x-openapi-flow generate-flow-tests openapi.flow.yaml --format jest --output ./flow.generated.test.js
|
|
464
526
|
x-openapi-flow graph examples/order-api.yaml
|
|
465
527
|
x-openapi-flow doctor
|
|
466
528
|
|
|
467
529
|
Quick Start:
|
|
530
|
+
# 0) Create a runnable starter project (recommended for first contact)
|
|
531
|
+
x-openapi-flow quickstart
|
|
532
|
+
|
|
468
533
|
# 1) Initialize sidecar from your OpenAPI file
|
|
469
534
|
x-openapi-flow init
|
|
470
535
|
|
|
@@ -598,7 +663,7 @@ function parseValidateArgs(args) {
|
|
|
598
663
|
const unknown = findUnknownOptions(
|
|
599
664
|
args,
|
|
600
665
|
["--format", "--profile", "--config"],
|
|
601
|
-
["--strict-quality"]
|
|
666
|
+
["--strict-quality", "--semantic"]
|
|
602
667
|
);
|
|
603
668
|
if (unknown) {
|
|
604
669
|
return { error: `Unknown option: ${unknown}` };
|
|
@@ -620,6 +685,7 @@ function parseValidateArgs(args) {
|
|
|
620
685
|
}
|
|
621
686
|
|
|
622
687
|
const strictQuality = args.includes("--strict-quality");
|
|
688
|
+
const semantic = args.includes("--semantic");
|
|
623
689
|
const format = formatOpt.found ? formatOpt.value : undefined;
|
|
624
690
|
const profile = profileOpt.found ? profileOpt.value : undefined;
|
|
625
691
|
|
|
@@ -657,6 +723,7 @@ function parseValidateArgs(args) {
|
|
|
657
723
|
return {
|
|
658
724
|
filePath: path.resolve(positional[0]),
|
|
659
725
|
strictQuality,
|
|
726
|
+
semantic,
|
|
660
727
|
format,
|
|
661
728
|
profile,
|
|
662
729
|
configPath: configOpt.found ? configOpt.value : undefined,
|
|
@@ -702,6 +769,52 @@ function parseInitArgs(args) {
|
|
|
702
769
|
};
|
|
703
770
|
}
|
|
704
771
|
|
|
772
|
+
function parseQuickstartArgs(args) {
|
|
773
|
+
const unknown = findUnknownOptions(args, ["--dir", "--runtime"], ["--force"]);
|
|
774
|
+
if (unknown) {
|
|
775
|
+
return { error: `Unknown option: ${unknown}` };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const dirOpt = getOptionValue(args, "--dir");
|
|
779
|
+
if (dirOpt.error) {
|
|
780
|
+
return { error: dirOpt.error };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const runtimeOpt = getOptionValue(args, "--runtime");
|
|
784
|
+
if (runtimeOpt.error) {
|
|
785
|
+
return { error: `${runtimeOpt.error} Use 'express' or 'fastify'.` };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const positional = args.filter((token, index) => {
|
|
789
|
+
if (token === "--dir" || token === "--runtime" || token === "--force") {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
if (index > 0 && (args[index - 1] === "--dir" || args[index - 1] === "--runtime")) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
return !token.startsWith("--");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
if (positional.length > 1) {
|
|
799
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const targetDirRaw = dirOpt.found
|
|
803
|
+
? dirOpt.value
|
|
804
|
+
: (positional[0] || "x-openapi-flow-quickstart");
|
|
805
|
+
|
|
806
|
+
const runtime = runtimeOpt.found ? String(runtimeOpt.value).toLowerCase() : "express";
|
|
807
|
+
if (!["express", "fastify"].includes(runtime)) {
|
|
808
|
+
return { error: `Invalid --runtime '${runtime}'. Use 'express' or 'fastify'.` };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
targetDir: path.resolve(targetDirRaw),
|
|
813
|
+
runtime,
|
|
814
|
+
force: args.includes("--force"),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
705
818
|
function summarizeSidecarDiff(existingFlowsDoc, mergedFlowsDoc) {
|
|
706
819
|
const existingOps = new Map();
|
|
707
820
|
for (const entry of (existingFlowsDoc && existingFlowsDoc.operations) || []) {
|
|
@@ -853,7 +966,7 @@ function parseDiffArgs(args) {
|
|
|
853
966
|
}
|
|
854
967
|
|
|
855
968
|
function parseLintArgs(args) {
|
|
856
|
-
const unknown = findUnknownOptions(args, ["--format", "--config"], []);
|
|
969
|
+
const unknown = findUnknownOptions(args, ["--format", "--config"], ["--semantic"]);
|
|
857
970
|
if (unknown) {
|
|
858
971
|
return { error: `Unknown option: ${unknown}` };
|
|
859
972
|
}
|
|
@@ -893,10 +1006,54 @@ function parseLintArgs(args) {
|
|
|
893
1006
|
return {
|
|
894
1007
|
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
895
1008
|
format,
|
|
1009
|
+
semantic: args.includes("--semantic"),
|
|
896
1010
|
configPath: configOpt.found ? configOpt.value : undefined,
|
|
897
1011
|
};
|
|
898
1012
|
}
|
|
899
1013
|
|
|
1014
|
+
function parseQualityReportArgs(args) {
|
|
1015
|
+
const unknown = findUnknownOptions(args, ["--profile", "--output"], ["--semantic"]);
|
|
1016
|
+
if (unknown) {
|
|
1017
|
+
return { error: `Unknown option: ${unknown}` };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const profileOpt = getOptionValue(args, "--profile");
|
|
1021
|
+
if (profileOpt.error) {
|
|
1022
|
+
return { error: `${profileOpt.error} Use 'core', 'relaxed', or 'strict'.` };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
1026
|
+
if (outputOpt.error) {
|
|
1027
|
+
return { error: outputOpt.error };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const profile = profileOpt.found ? profileOpt.value : undefined;
|
|
1031
|
+
if (profile && !["core", "relaxed", "strict"].includes(profile)) {
|
|
1032
|
+
return { error: `Invalid --profile '${profile}'. Use 'core', 'relaxed', or 'strict'.` };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const positional = args.filter((token, index) => {
|
|
1036
|
+
if (["--profile", "--output"].includes(token)) return false;
|
|
1037
|
+
if (index > 0 && ["--profile", "--output"].includes(args[index - 1])) return false;
|
|
1038
|
+
return !token.startsWith("--") || token === "-";
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
if (positional.length === 0) {
|
|
1042
|
+
return { error: "Missing OpenAPI file path. Usage: x-openapi-flow quality-report <openapi-file>" };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (positional.length > 1) {
|
|
1046
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
filePath: path.resolve(positional[0]),
|
|
1051
|
+
profile,
|
|
1052
|
+
semantic: args.includes("--semantic"),
|
|
1053
|
+
outputPath: outputOpt.found ? path.resolve(outputOpt.value) : undefined,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
900
1057
|
function parseApplyArgs(args) {
|
|
901
1058
|
const unknown = findUnknownOptions(args, ["--flows", "--out"], ["--in-place"]);
|
|
902
1059
|
if (unknown) {
|
|
@@ -1215,6 +1372,51 @@ function parseGenerateRedocArgs(args) {
|
|
|
1215
1372
|
};
|
|
1216
1373
|
}
|
|
1217
1374
|
|
|
1375
|
+
function parseGenerateFlowTestsArgs(args) {
|
|
1376
|
+
const unknown = findUnknownOptions(args, ["--format", "--output"], ["--with-scripts"]);
|
|
1377
|
+
if (unknown) {
|
|
1378
|
+
return { error: `Unknown option: ${unknown}` };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
1382
|
+
if (formatOpt.error) {
|
|
1383
|
+
return { error: `${formatOpt.error} Use 'jest', 'vitest', or 'postman'.` };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
1387
|
+
if (outputOpt.error) {
|
|
1388
|
+
return { error: outputOpt.error };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const format = formatOpt.found ? String(formatOpt.value || "").toLowerCase() : "jest";
|
|
1392
|
+
if (!["jest", "vitest", "postman"].includes(format)) {
|
|
1393
|
+
return { error: `Invalid --format '${format}'. Use 'jest', 'vitest', or 'postman'.` };
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const positional = args.filter((token, index) => {
|
|
1397
|
+
if (token === "--format" || token === "--output" || token === "--with-scripts") {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (index > 0 && (args[index - 1] === "--format" || args[index - 1] === "--output")) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
return !token.startsWith("--");
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
if (positional.length > 1) {
|
|
1409
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return {
|
|
1413
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
1414
|
+
format,
|
|
1415
|
+
outputPath: outputOpt.found ? path.resolve(outputOpt.value) : undefined,
|
|
1416
|
+
withScripts: args.includes("--with-scripts") ? true : undefined,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1218
1420
|
function parseArgs(argv) {
|
|
1219
1421
|
const stripped = stripGlobalFlags(argv.slice(2));
|
|
1220
1422
|
const args = stripped.args;
|
|
@@ -1257,6 +1459,11 @@ function parseArgs(argv) {
|
|
|
1257
1459
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1258
1460
|
}
|
|
1259
1461
|
|
|
1462
|
+
if (command === "quickstart") {
|
|
1463
|
+
const parsed = parseQuickstartArgs(commandArgs);
|
|
1464
|
+
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1260
1467
|
if (command === "graph") {
|
|
1261
1468
|
const parsed = parseGraphArgs(commandArgs);
|
|
1262
1469
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
@@ -1282,6 +1489,11 @@ function parseArgs(argv) {
|
|
|
1282
1489
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1283
1490
|
}
|
|
1284
1491
|
|
|
1492
|
+
if (command === "quality-report") {
|
|
1493
|
+
const parsed = parseQualityReportArgs(commandArgs);
|
|
1494
|
+
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1285
1497
|
if (command === "doctor") {
|
|
1286
1498
|
const parsed = parseDoctorArgs(commandArgs);
|
|
1287
1499
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
@@ -1312,6 +1524,11 @@ function parseArgs(argv) {
|
|
|
1312
1524
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1313
1525
|
}
|
|
1314
1526
|
|
|
1527
|
+
if (command === "generate-flow-tests") {
|
|
1528
|
+
const parsed = parseGenerateFlowTestsArgs(commandArgs);
|
|
1529
|
+
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1315
1532
|
if (command === "completion") {
|
|
1316
1533
|
const parsed = parseCompletionArgs(commandArgs);
|
|
1317
1534
|
return withVerbose(parsed.error ? parsed : { command, ...parsed });
|
|
@@ -1471,6 +1688,140 @@ function extractOperationEntries(api) {
|
|
|
1471
1688
|
return entries;
|
|
1472
1689
|
}
|
|
1473
1690
|
|
|
1691
|
+
function normalizeDslState(resourceDsl, stateValue) {
|
|
1692
|
+
const stateAliases = resourceDsl && resourceDsl.states && typeof resourceDsl.states === "object"
|
|
1693
|
+
? resourceDsl.states
|
|
1694
|
+
: {};
|
|
1695
|
+
|
|
1696
|
+
if (stateValue == null) {
|
|
1697
|
+
return stateValue;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const raw = String(stateValue);
|
|
1701
|
+
return stateAliases[raw] || raw;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function expandResourceDsl(resourceDsl) {
|
|
1705
|
+
const defaults = resourceDsl && resourceDsl.defaults && typeof resourceDsl.defaults === "object"
|
|
1706
|
+
? resourceDsl.defaults
|
|
1707
|
+
: {};
|
|
1708
|
+
const flowDefaults = defaults.flow && typeof defaults.flow === "object"
|
|
1709
|
+
? defaults.flow
|
|
1710
|
+
: {};
|
|
1711
|
+
const { id_prefix: flowIdPrefix, ...flowDefaultsForPayload } = flowDefaults;
|
|
1712
|
+
const transitionDefaults = defaults.transition && typeof defaults.transition === "object"
|
|
1713
|
+
? defaults.transition
|
|
1714
|
+
: {};
|
|
1715
|
+
|
|
1716
|
+
const resourceTransitions = Array.isArray(resourceDsl && resourceDsl.transitions)
|
|
1717
|
+
? resourceDsl.transitions
|
|
1718
|
+
: [];
|
|
1719
|
+
|
|
1720
|
+
const outgoingByState = new Map();
|
|
1721
|
+
for (const transition of resourceTransitions) {
|
|
1722
|
+
if (!transition || !transition.from || !transition.to) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const fromState = normalizeDslState(resourceDsl, transition.from);
|
|
1727
|
+
const targetState = normalizeDslState(resourceDsl, transition.to);
|
|
1728
|
+
|
|
1729
|
+
if (!outgoingByState.has(fromState)) {
|
|
1730
|
+
outgoingByState.set(fromState, []);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
outgoingByState.get(fromState).push({
|
|
1734
|
+
...transitionDefaults,
|
|
1735
|
+
target_state: targetState,
|
|
1736
|
+
trigger_type: transition.trigger_type || transitionDefaults.trigger_type || "synchronous",
|
|
1737
|
+
condition: transition.condition,
|
|
1738
|
+
next_operation_id: transition.next_operation_id,
|
|
1739
|
+
prerequisite_operation_ids: Array.isArray(transition.prerequisite_operation_ids)
|
|
1740
|
+
? transition.prerequisite_operation_ids
|
|
1741
|
+
: undefined,
|
|
1742
|
+
prerequisite_field_refs: Array.isArray(transition.prerequisite_field_refs)
|
|
1743
|
+
? transition.prerequisite_field_refs
|
|
1744
|
+
: undefined,
|
|
1745
|
+
propagated_field_refs: Array.isArray(transition.propagated_field_refs)
|
|
1746
|
+
? transition.propagated_field_refs
|
|
1747
|
+
: undefined,
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const operations = Array.isArray(resourceDsl && resourceDsl.operations)
|
|
1752
|
+
? resourceDsl.operations
|
|
1753
|
+
: [];
|
|
1754
|
+
|
|
1755
|
+
return operations
|
|
1756
|
+
.filter((operationEntry) => operationEntry && operationEntry.operationId)
|
|
1757
|
+
.map((operationEntry) => {
|
|
1758
|
+
const currentState = normalizeDslState(
|
|
1759
|
+
resourceDsl,
|
|
1760
|
+
operationEntry.current_state != null ? operationEntry.current_state : operationEntry.state
|
|
1761
|
+
);
|
|
1762
|
+
|
|
1763
|
+
const explicitFlow =
|
|
1764
|
+
operationEntry["x-openapi-flow"] && typeof operationEntry["x-openapi-flow"] === "object"
|
|
1765
|
+
? operationEntry["x-openapi-flow"]
|
|
1766
|
+
: null;
|
|
1767
|
+
|
|
1768
|
+
const explicitTransitions = Array.isArray(operationEntry.transitions)
|
|
1769
|
+
? operationEntry.transitions.map((transition) => ({
|
|
1770
|
+
...transitionDefaults,
|
|
1771
|
+
...transition,
|
|
1772
|
+
target_state: normalizeDslState(resourceDsl, transition.target_state || transition.to),
|
|
1773
|
+
trigger_type: transition.trigger_type || transitionDefaults.trigger_type || "synchronous",
|
|
1774
|
+
}))
|
|
1775
|
+
: null;
|
|
1776
|
+
|
|
1777
|
+
const inheritedTransitions = outgoingByState.has(currentState)
|
|
1778
|
+
? outgoingByState.get(currentState).map((transition) => ({ ...transition }))
|
|
1779
|
+
: [];
|
|
1780
|
+
|
|
1781
|
+
const transitions = explicitTransitions || inheritedTransitions;
|
|
1782
|
+
const defaultIdPrefix = flowIdPrefix ? String(flowIdPrefix) : "";
|
|
1783
|
+
const generatedId = defaultIdPrefix
|
|
1784
|
+
? `${defaultIdPrefix}-${toKebabCase(operationEntry.operationId)}`
|
|
1785
|
+
: toKebabCase(operationEntry.operationId);
|
|
1786
|
+
|
|
1787
|
+
const flow = {
|
|
1788
|
+
version: "1.0",
|
|
1789
|
+
...flowDefaultsForPayload,
|
|
1790
|
+
...explicitFlow,
|
|
1791
|
+
id: operationEntry.id || (explicitFlow && explicitFlow.id) || generatedId,
|
|
1792
|
+
current_state: currentState,
|
|
1793
|
+
description: operationEntry.description || (explicitFlow && explicitFlow.description),
|
|
1794
|
+
idempotency: operationEntry.idempotency || (explicitFlow && explicitFlow.idempotency),
|
|
1795
|
+
transitions,
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
if (!flow.description) {
|
|
1799
|
+
delete flow.description;
|
|
1800
|
+
}
|
|
1801
|
+
if (!flow.idempotency) {
|
|
1802
|
+
delete flow.idempotency;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
return {
|
|
1806
|
+
operationId: operationEntry.operationId,
|
|
1807
|
+
"x-openapi-flow": flow,
|
|
1808
|
+
};
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function expandResourceDslOperations(parsed) {
|
|
1813
|
+
const resourceDslEntries = Array.isArray(parsed && parsed.resources)
|
|
1814
|
+
? parsed.resources
|
|
1815
|
+
: [];
|
|
1816
|
+
|
|
1817
|
+
const expanded = [];
|
|
1818
|
+
for (const resourceDsl of resourceDslEntries) {
|
|
1819
|
+
expanded.push(...expandResourceDsl(resourceDsl));
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
return expanded;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1474
1825
|
function readFlowsFile(flowsPath) {
|
|
1475
1826
|
if (!fs.existsSync(flowsPath)) {
|
|
1476
1827
|
return {
|
|
@@ -1486,9 +1837,12 @@ function readFlowsFile(flowsPath) {
|
|
|
1486
1837
|
return { version: "1.0", operations: [] };
|
|
1487
1838
|
}
|
|
1488
1839
|
|
|
1840
|
+
const directOperations = Array.isArray(parsed.operations) ? parsed.operations : [];
|
|
1841
|
+
const expandedOperations = expandResourceDslOperations(parsed);
|
|
1842
|
+
|
|
1489
1843
|
return {
|
|
1490
1844
|
version: parsed.version || "1.0",
|
|
1491
|
-
operations:
|
|
1845
|
+
operations: [...directOperations, ...expandedOperations],
|
|
1492
1846
|
};
|
|
1493
1847
|
}
|
|
1494
1848
|
|
|
@@ -1726,6 +2080,445 @@ function runInit(parsed) {
|
|
|
1726
2080
|
return 0;
|
|
1727
2081
|
}
|
|
1728
2082
|
|
|
2083
|
+
function isDirectoryEmpty(directoryPath) {
|
|
2084
|
+
if (!fs.existsSync(directoryPath)) {
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
try {
|
|
2089
|
+
const entries = fs.readdirSync(directoryPath);
|
|
2090
|
+
return entries.length === 0;
|
|
2091
|
+
} catch (_err) {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function buildQuickstartOpenApi() {
|
|
2097
|
+
return {
|
|
2098
|
+
openapi: "3.0.3",
|
|
2099
|
+
info: {
|
|
2100
|
+
title: "Quickstart Orders API",
|
|
2101
|
+
version: "1.0.0",
|
|
2102
|
+
},
|
|
2103
|
+
paths: {
|
|
2104
|
+
"/orders": {
|
|
2105
|
+
post: {
|
|
2106
|
+
operationId: "createOrder",
|
|
2107
|
+
responses: {
|
|
2108
|
+
201: { description: "Order created" },
|
|
2109
|
+
},
|
|
2110
|
+
},
|
|
2111
|
+
},
|
|
2112
|
+
"/orders/{id}/pay": {
|
|
2113
|
+
post: {
|
|
2114
|
+
operationId: "payOrder",
|
|
2115
|
+
parameters: [
|
|
2116
|
+
{
|
|
2117
|
+
name: "id",
|
|
2118
|
+
in: "path",
|
|
2119
|
+
required: true,
|
|
2120
|
+
schema: { type: "string" },
|
|
2121
|
+
},
|
|
2122
|
+
],
|
|
2123
|
+
responses: {
|
|
2124
|
+
200: { description: "Order paid" },
|
|
2125
|
+
},
|
|
2126
|
+
},
|
|
2127
|
+
},
|
|
2128
|
+
"/orders/{id}/ship": {
|
|
2129
|
+
post: {
|
|
2130
|
+
operationId: "shipOrder",
|
|
2131
|
+
parameters: [
|
|
2132
|
+
{
|
|
2133
|
+
name: "id",
|
|
2134
|
+
in: "path",
|
|
2135
|
+
required: true,
|
|
2136
|
+
schema: { type: "string" },
|
|
2137
|
+
},
|
|
2138
|
+
],
|
|
2139
|
+
responses: {
|
|
2140
|
+
200: { description: "Order shipped" },
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
"/orders/{id}": {
|
|
2145
|
+
get: {
|
|
2146
|
+
operationId: "getOrder",
|
|
2147
|
+
parameters: [
|
|
2148
|
+
{
|
|
2149
|
+
name: "id",
|
|
2150
|
+
in: "path",
|
|
2151
|
+
required: true,
|
|
2152
|
+
schema: { type: "string" },
|
|
2153
|
+
},
|
|
2154
|
+
],
|
|
2155
|
+
responses: {
|
|
2156
|
+
200: { description: "Order state" },
|
|
2157
|
+
404: { description: "Order not found" },
|
|
2158
|
+
},
|
|
2159
|
+
},
|
|
2160
|
+
},
|
|
2161
|
+
},
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function buildQuickstartSidecar() {
|
|
2166
|
+
return {
|
|
2167
|
+
version: "1.0",
|
|
2168
|
+
operations: [
|
|
2169
|
+
{
|
|
2170
|
+
operationId: "createOrder",
|
|
2171
|
+
"x-openapi-flow": {
|
|
2172
|
+
version: "1.0",
|
|
2173
|
+
id: "create-order-flow",
|
|
2174
|
+
current_state: "CREATED",
|
|
2175
|
+
transitions: [
|
|
2176
|
+
{
|
|
2177
|
+
target_state: "PAID",
|
|
2178
|
+
trigger_type: "synchronous",
|
|
2179
|
+
next_operation_id: "payOrder",
|
|
2180
|
+
},
|
|
2181
|
+
],
|
|
2182
|
+
},
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
operationId: "payOrder",
|
|
2186
|
+
"x-openapi-flow": {
|
|
2187
|
+
version: "1.0",
|
|
2188
|
+
id: "pay-order-flow",
|
|
2189
|
+
current_state: "PAID",
|
|
2190
|
+
transitions: [
|
|
2191
|
+
{
|
|
2192
|
+
target_state: "SHIPPED",
|
|
2193
|
+
trigger_type: "synchronous",
|
|
2194
|
+
next_operation_id: "shipOrder",
|
|
2195
|
+
},
|
|
2196
|
+
],
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
operationId: "shipOrder",
|
|
2201
|
+
"x-openapi-flow": {
|
|
2202
|
+
version: "1.0",
|
|
2203
|
+
id: "ship-order-flow",
|
|
2204
|
+
current_state: "SHIPPED",
|
|
2205
|
+
transitions: [],
|
|
2206
|
+
},
|
|
2207
|
+
},
|
|
2208
|
+
],
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function buildQuickstartServerJs(runtime) {
|
|
2213
|
+
if (runtime === "fastify") {
|
|
2214
|
+
return `"use strict";
|
|
2215
|
+
|
|
2216
|
+
const fastify = require("fastify")({ logger: false });
|
|
2217
|
+
const openapi = require("./openapi.flow.json");
|
|
2218
|
+
const { createFastifyFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
|
|
2219
|
+
|
|
2220
|
+
const PORT = Number(process.env.PORT || 3110);
|
|
2221
|
+
const orderStore = new Map();
|
|
2222
|
+
|
|
2223
|
+
fastify.addHook(
|
|
2224
|
+
"preHandler",
|
|
2225
|
+
createFastifyFlowGuard({
|
|
2226
|
+
openapi,
|
|
2227
|
+
async getCurrentState({ resourceId }) {
|
|
2228
|
+
if (!resourceId) {
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const order = orderStore.get(resourceId);
|
|
2233
|
+
return order ? order.state : null;
|
|
2234
|
+
},
|
|
2235
|
+
resolveResourceId: ({ params }) => (params && params.id ? String(params.id) : null),
|
|
2236
|
+
allowUnknownOperations: true,
|
|
2237
|
+
})
|
|
2238
|
+
);
|
|
2239
|
+
|
|
2240
|
+
fastify.post(
|
|
2241
|
+
"/orders",
|
|
2242
|
+
{
|
|
2243
|
+
config: {
|
|
2244
|
+
operationId: "createOrder",
|
|
2245
|
+
},
|
|
2246
|
+
},
|
|
2247
|
+
async (_request, reply) => {
|
|
2248
|
+
const id = \`ord_\${Date.now()}\`;
|
|
2249
|
+
const order = { id, state: "CREATED" };
|
|
2250
|
+
orderStore.set(id, order);
|
|
2251
|
+
return reply.code(201).send(order);
|
|
2252
|
+
}
|
|
2253
|
+
);
|
|
2254
|
+
|
|
2255
|
+
fastify.post(
|
|
2256
|
+
"/orders/:id/pay",
|
|
2257
|
+
{
|
|
2258
|
+
config: {
|
|
2259
|
+
operationId: "payOrder",
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
async (request, reply) => {
|
|
2263
|
+
const order = orderStore.get(request.params.id);
|
|
2264
|
+
if (!order) {
|
|
2265
|
+
return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
order.state = "PAID";
|
|
2269
|
+
orderStore.set(order.id, order);
|
|
2270
|
+
return reply.code(200).send(order);
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
|
|
2274
|
+
fastify.post(
|
|
2275
|
+
"/orders/:id/ship",
|
|
2276
|
+
{
|
|
2277
|
+
config: {
|
|
2278
|
+
operationId: "shipOrder",
|
|
2279
|
+
},
|
|
2280
|
+
},
|
|
2281
|
+
async (request, reply) => {
|
|
2282
|
+
const order = orderStore.get(request.params.id);
|
|
2283
|
+
if (!order) {
|
|
2284
|
+
return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
order.state = "SHIPPED";
|
|
2288
|
+
orderStore.set(order.id, order);
|
|
2289
|
+
return reply.code(200).send(order);
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
fastify.get(
|
|
2294
|
+
"/orders/:id",
|
|
2295
|
+
{
|
|
2296
|
+
config: {
|
|
2297
|
+
operationId: "getOrder",
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2300
|
+
async (request, reply) => {
|
|
2301
|
+
const order = orderStore.get(request.params.id);
|
|
2302
|
+
if (!order) {
|
|
2303
|
+
return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
return reply.code(200).send(order);
|
|
2307
|
+
}
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
fastify.listen({ port: PORT, host: "0.0.0.0" }).then(() => {
|
|
2311
|
+
console.log(\`Quickstart API running on http://localhost:\${PORT}\`);
|
|
2312
|
+
});
|
|
2313
|
+
`;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
return `"use strict";
|
|
2317
|
+
|
|
2318
|
+
const express = require("express");
|
|
2319
|
+
const openapi = require("./openapi.flow.json");
|
|
2320
|
+
const { createExpressFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
|
|
2321
|
+
|
|
2322
|
+
const app = express();
|
|
2323
|
+
app.use(express.json());
|
|
2324
|
+
|
|
2325
|
+
const PORT = Number(process.env.PORT || 3110);
|
|
2326
|
+
const orderStore = new Map();
|
|
2327
|
+
|
|
2328
|
+
function resolveOrderIdFromPath(req) {
|
|
2329
|
+
const fromParams = req && req.params && req.params.id ? String(req.params.id) : null;
|
|
2330
|
+
if (fromParams) {
|
|
2331
|
+
return fromParams;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const rawPath = req && (req.path || (req.originalUrl ? req.originalUrl.split("?")[0] : null));
|
|
2335
|
+
if (!rawPath) {
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const match = String(rawPath).match(/^\\/orders\\/([^/]+)\\/(pay|ship)$/);
|
|
2340
|
+
return match ? match[1] : null;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
app.use(
|
|
2344
|
+
createExpressFlowGuard({
|
|
2345
|
+
openapi,
|
|
2346
|
+
async getCurrentState({ resourceId }) {
|
|
2347
|
+
if (!resourceId) {
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
const order = orderStore.get(resourceId);
|
|
2352
|
+
return order ? order.state : null;
|
|
2353
|
+
},
|
|
2354
|
+
resolveResourceId: ({ req }) => resolveOrderIdFromPath(req),
|
|
2355
|
+
allowUnknownOperations: true,
|
|
2356
|
+
})
|
|
2357
|
+
);
|
|
2358
|
+
|
|
2359
|
+
app.post("/orders", (_req, res) => {
|
|
2360
|
+
const id = \`ord_\${Date.now()}\`;
|
|
2361
|
+
const order = { id, state: "CREATED" };
|
|
2362
|
+
orderStore.set(id, order);
|
|
2363
|
+
return res.status(201).json(order);
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
app.post("/orders/:id/pay", (req, res) => {
|
|
2367
|
+
const order = orderStore.get(req.params.id);
|
|
2368
|
+
if (!order) {
|
|
2369
|
+
return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
order.state = "PAID";
|
|
2373
|
+
orderStore.set(order.id, order);
|
|
2374
|
+
return res.status(200).json(order);
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
app.post("/orders/:id/ship", (req, res) => {
|
|
2378
|
+
const order = orderStore.get(req.params.id);
|
|
2379
|
+
if (!order) {
|
|
2380
|
+
return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
order.state = "SHIPPED";
|
|
2384
|
+
orderStore.set(order.id, order);
|
|
2385
|
+
return res.status(200).json(order);
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
app.get("/orders/:id", (req, res) => {
|
|
2389
|
+
const order = orderStore.get(req.params.id);
|
|
2390
|
+
if (!order) {
|
|
2391
|
+
return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return res.status(200).json(order);
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
app.listen(PORT, () => {
|
|
2398
|
+
console.log(\`Quickstart API running on http://localhost:\${PORT}\`);
|
|
2399
|
+
});
|
|
2400
|
+
`;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function buildQuickstartReadme(runtime) {
|
|
2404
|
+
return `# x-openapi-flow Quickstart Project
|
|
2405
|
+
|
|
2406
|
+
This starter was generated by \`x-openapi-flow quickstart\`.
|
|
2407
|
+
|
|
2408
|
+
Runtime: \`${runtime}\`
|
|
2409
|
+
|
|
2410
|
+
## Fast path (under 5 minutes)
|
|
2411
|
+
|
|
2412
|
+
1. Install and start:
|
|
2413
|
+
|
|
2414
|
+
\`\`\`bash
|
|
2415
|
+
npm install
|
|
2416
|
+
npm start
|
|
2417
|
+
\`\`\`
|
|
2418
|
+
|
|
2419
|
+
2. Create an order and try invalid shipping (blocked):
|
|
2420
|
+
|
|
2421
|
+
\`\`\`bash
|
|
2422
|
+
curl -s -X POST http://localhost:3110/orders
|
|
2423
|
+
curl -i -X POST http://localhost:3110/orders/<id>/ship
|
|
2424
|
+
\`\`\`
|
|
2425
|
+
|
|
2426
|
+
Expected: \`409 INVALID_STATE_TRANSITION\`.
|
|
2427
|
+
|
|
2428
|
+
3. Follow the valid path:
|
|
2429
|
+
|
|
2430
|
+
\`\`\`bash
|
|
2431
|
+
curl -s -X POST http://localhost:3110/orders/<id>/pay
|
|
2432
|
+
curl -s -X POST http://localhost:3110/orders/<id>/ship
|
|
2433
|
+
\`\`\`
|
|
2434
|
+
|
|
2435
|
+
## About files (keep it simple)
|
|
2436
|
+
|
|
2437
|
+
- \`openapi.flow.json\`: the file used at runtime right now.
|
|
2438
|
+
- \`openapi.json\`: base OpenAPI source.
|
|
2439
|
+
- \`openapi.x.yaml\`: sidecar flow metadata (you can ignore this file at first).
|
|
2440
|
+
|
|
2441
|
+
When you are ready to edit flows manually:
|
|
2442
|
+
|
|
2443
|
+
\`\`\`bash
|
|
2444
|
+
npx x-openapi-flow apply openapi.json --flows openapi.x.yaml --out openapi.flow.json
|
|
2445
|
+
npx x-openapi-flow validate openapi.flow.json --profile strict
|
|
2446
|
+
\`\`\`
|
|
2447
|
+
`;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function runQuickstart(parsed) {
|
|
2451
|
+
const targetDir = parsed.targetDir;
|
|
2452
|
+
const runtime = parsed.runtime || "express";
|
|
2453
|
+
const exists = fs.existsSync(targetDir);
|
|
2454
|
+
|
|
2455
|
+
if (exists && !isDirectoryEmpty(targetDir) && !parsed.force) {
|
|
2456
|
+
console.error(`ERROR: Target directory is not empty: ${targetDir}`);
|
|
2457
|
+
console.error("Use --force to overwrite scaffold files in this directory.");
|
|
2458
|
+
return 1;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
2462
|
+
|
|
2463
|
+
const openapiPath = path.join(targetDir, "openapi.json");
|
|
2464
|
+
const sidecarPath = path.join(targetDir, "openapi.x.yaml");
|
|
2465
|
+
const flowPath = path.join(targetDir, "openapi.flow.json");
|
|
2466
|
+
const packagePath = path.join(targetDir, "package.json");
|
|
2467
|
+
const serverPath = path.join(targetDir, "server.js");
|
|
2468
|
+
const readmePath = path.join(targetDir, "README.md");
|
|
2469
|
+
const gitignorePath = path.join(targetDir, ".gitignore");
|
|
2470
|
+
|
|
2471
|
+
const openapi = buildQuickstartOpenApi();
|
|
2472
|
+
const sidecar = buildQuickstartSidecar();
|
|
2473
|
+
const flowApi = JSON.parse(JSON.stringify(openapi));
|
|
2474
|
+
applyFlowsToOpenApi(flowApi, sidecar);
|
|
2475
|
+
|
|
2476
|
+
fs.writeFileSync(openapiPath, `${JSON.stringify(openapi, null, 2)}\n`, "utf8");
|
|
2477
|
+
fs.writeFileSync(sidecarPath, yaml.dump(sidecar, { noRefs: true, lineWidth: -1 }), "utf8");
|
|
2478
|
+
fs.writeFileSync(flowPath, `${JSON.stringify(flowApi, null, 2)}\n`, "utf8");
|
|
2479
|
+
fs.writeFileSync(
|
|
2480
|
+
packagePath,
|
|
2481
|
+
`${JSON.stringify({
|
|
2482
|
+
name: `x-openapi-flow-quickstart-${runtime}`,
|
|
2483
|
+
private: true,
|
|
2484
|
+
version: "1.0.0",
|
|
2485
|
+
description: `Quickstart scaffold generated by x-openapi-flow (${runtime})`,
|
|
2486
|
+
main: "server.js",
|
|
2487
|
+
scripts: {
|
|
2488
|
+
start: "node server.js",
|
|
2489
|
+
apply: "x-openapi-flow apply openapi.json --flows openapi.x.yaml --out openapi.flow.json",
|
|
2490
|
+
validate: "x-openapi-flow validate openapi.flow.json --profile strict",
|
|
2491
|
+
},
|
|
2492
|
+
dependencies: {
|
|
2493
|
+
...(runtime === "fastify" ? { fastify: "^5.2.1" } : { express: "^4.21.2" }),
|
|
2494
|
+
"x-openapi-flow": "latest",
|
|
2495
|
+
},
|
|
2496
|
+
}, null, 2)}\n`,
|
|
2497
|
+
"utf8"
|
|
2498
|
+
);
|
|
2499
|
+
fs.writeFileSync(serverPath, buildQuickstartServerJs(runtime), "utf8");
|
|
2500
|
+
fs.writeFileSync(readmePath, buildQuickstartReadme(runtime), "utf8");
|
|
2501
|
+
fs.writeFileSync(gitignorePath, "node_modules\n", "utf8");
|
|
2502
|
+
|
|
2503
|
+
console.log(`Quickstart project created: ${targetDir}`);
|
|
2504
|
+
console.log(`Runtime: ${runtime}`);
|
|
2505
|
+
console.log("Generated files:");
|
|
2506
|
+
console.log("- openapi.json (base spec)");
|
|
2507
|
+
console.log("- openapi.x.yaml (sidecar metadata)");
|
|
2508
|
+
console.log("- openapi.flow.json (runtime-ready spec)");
|
|
2509
|
+
console.log(`- server.js (${runtime} runtime guard demo)`);
|
|
2510
|
+
console.log("- package.json (scripts: start/apply/validate)");
|
|
2511
|
+
console.log("---");
|
|
2512
|
+
console.log("Next steps:");
|
|
2513
|
+
console.log(`cd ${targetDir}`);
|
|
2514
|
+
console.log("npm install");
|
|
2515
|
+
console.log("npm start");
|
|
2516
|
+
console.log("curl -s -X POST http://localhost:3110/orders");
|
|
2517
|
+
console.log("curl -i -X POST http://localhost:3110/orders/<id>/ship");
|
|
2518
|
+
console.log("(Expected: 409 INVALID_STATE_TRANSITION)");
|
|
2519
|
+
return 0;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
1729
2522
|
function runApply(parsed) {
|
|
1730
2523
|
let targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
1731
2524
|
let flowsPathFromPositional = null;
|
|
@@ -1923,11 +2716,13 @@ function runLint(parsed, configData = {}) {
|
|
|
1923
2716
|
|
|
1924
2717
|
const flows = extractFlows(api);
|
|
1925
2718
|
const lintConfig = (configData && configData.lint && configData.lint.rules) || {};
|
|
2719
|
+
const semanticEnabled = parsed.semantic === true || lintConfig.semantic === true;
|
|
1926
2720
|
const ruleConfig = {
|
|
1927
2721
|
next_operation_id_exists: lintConfig.next_operation_id_exists !== false,
|
|
1928
2722
|
prerequisite_operation_ids_exist: lintConfig.prerequisite_operation_ids_exist !== false,
|
|
1929
2723
|
duplicate_transitions: lintConfig.duplicate_transitions !== false,
|
|
1930
2724
|
terminal_path: lintConfig.terminal_path !== false,
|
|
2725
|
+
semantic_consistency: semanticEnabled,
|
|
1931
2726
|
};
|
|
1932
2727
|
|
|
1933
2728
|
const operationsById = collectOperationIds(api);
|
|
@@ -1935,10 +2730,12 @@ function runLint(parsed, configData = {}) {
|
|
|
1935
2730
|
const invalidOperationReferences = detectInvalidOperationReferences(operationsById, flows);
|
|
1936
2731
|
const duplicateTransitions = detectDuplicateTransitions(flows);
|
|
1937
2732
|
const terminalCoverage = detectTerminalCoverage(graph);
|
|
2733
|
+
const semanticWarnings = semanticEnabled ? detectSemanticModelingWarnings(flows) : [];
|
|
1938
2734
|
|
|
1939
2735
|
const nextOperationIssues = invalidOperationReferences
|
|
1940
2736
|
.filter((entry) => entry.type === "next_operation_id")
|
|
1941
2737
|
.map((entry) => ({
|
|
2738
|
+
code: CODES.LINT_NEXT_OPERATION_ID_EXISTS.code,
|
|
1942
2739
|
operation_id: entry.operation_id,
|
|
1943
2740
|
declared_in: entry.declared_in,
|
|
1944
2741
|
}));
|
|
@@ -1946,6 +2743,7 @@ function runLint(parsed, configData = {}) {
|
|
|
1946
2743
|
const prerequisiteIssues = invalidOperationReferences
|
|
1947
2744
|
.filter((entry) => entry.type === "prerequisite_operation_ids")
|
|
1948
2745
|
.map((entry) => ({
|
|
2746
|
+
code: CODES.LINT_PREREQUISITE_OPERATION_IDS_EXIST.code,
|
|
1949
2747
|
operation_id: entry.operation_id,
|
|
1950
2748
|
declared_in: entry.declared_in,
|
|
1951
2749
|
}));
|
|
@@ -1953,18 +2751,22 @@ function runLint(parsed, configData = {}) {
|
|
|
1953
2751
|
const issues = {
|
|
1954
2752
|
next_operation_id_exists: ruleConfig.next_operation_id_exists ? nextOperationIssues : [],
|
|
1955
2753
|
prerequisite_operation_ids_exist: ruleConfig.prerequisite_operation_ids_exist ? prerequisiteIssues : [],
|
|
1956
|
-
duplicate_transitions: ruleConfig.duplicate_transitions
|
|
2754
|
+
duplicate_transitions: ruleConfig.duplicate_transitions
|
|
2755
|
+
? duplicateTransitions.map((entry) => ({ code: CODES.LINT_DUPLICATE_TRANSITIONS.code, ...entry }))
|
|
2756
|
+
: [],
|
|
1957
2757
|
terminal_path: {
|
|
1958
2758
|
terminal_states: ruleConfig.terminal_path ? terminalCoverage.terminal_states : [],
|
|
1959
2759
|
non_terminating_states: ruleConfig.terminal_path ? terminalCoverage.non_terminating_states : [],
|
|
1960
2760
|
},
|
|
2761
|
+
semantic_consistency: ruleConfig.semantic_consistency ? semanticWarnings : [],
|
|
1961
2762
|
};
|
|
1962
2763
|
|
|
1963
2764
|
const errorCount =
|
|
1964
2765
|
issues.next_operation_id_exists.length +
|
|
1965
2766
|
issues.prerequisite_operation_ids_exist.length +
|
|
1966
2767
|
issues.duplicate_transitions.length +
|
|
1967
|
-
issues.terminal_path.non_terminating_states.length
|
|
2768
|
+
issues.terminal_path.non_terminating_states.length +
|
|
2769
|
+
issues.semantic_consistency.length;
|
|
1968
2770
|
|
|
1969
2771
|
const result = {
|
|
1970
2772
|
ok: errorCount === 0,
|
|
@@ -1979,6 +2781,7 @@ function runLint(parsed, configData = {}) {
|
|
|
1979
2781
|
prerequisite_operation_ids_exist: issues.prerequisite_operation_ids_exist.length,
|
|
1980
2782
|
duplicate_transitions: issues.duplicate_transitions.length,
|
|
1981
2783
|
terminal_path: issues.terminal_path.non_terminating_states.length,
|
|
2784
|
+
semantic_consistency: issues.semantic_consistency.length,
|
|
1982
2785
|
})
|
|
1983
2786
|
.filter(([, count]) => count > 0)
|
|
1984
2787
|
.map(([rule]) => rule),
|
|
@@ -1986,7 +2789,25 @@ function runLint(parsed, configData = {}) {
|
|
|
1986
2789
|
};
|
|
1987
2790
|
|
|
1988
2791
|
if (parsed.format === "json") {
|
|
1989
|
-
|
|
2792
|
+
const jsonResult = {
|
|
2793
|
+
...result,
|
|
2794
|
+
issues: {
|
|
2795
|
+
...result.issues,
|
|
2796
|
+
terminal_path: {
|
|
2797
|
+
...result.issues.terminal_path,
|
|
2798
|
+
non_terminating_states: result.issues.terminal_path.non_terminating_states.map((state) => ({
|
|
2799
|
+
code: CODES.LINT_TERMINAL_PATH.code,
|
|
2800
|
+
state,
|
|
2801
|
+
})),
|
|
2802
|
+
},
|
|
2803
|
+
semantic_consistency: result.issues.semantic_consistency.map((message) => ({
|
|
2804
|
+
code: CODES.LINT_SEMANTIC_CONSISTENCY.code,
|
|
2805
|
+
message,
|
|
2806
|
+
})),
|
|
2807
|
+
},
|
|
2808
|
+
};
|
|
2809
|
+
|
|
2810
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
1990
2811
|
return result.ok ? 0 : 1;
|
|
1991
2812
|
}
|
|
1992
2813
|
|
|
@@ -2037,6 +2858,17 @@ function runLint(parsed, configData = {}) {
|
|
|
2037
2858
|
);
|
|
2038
2859
|
}
|
|
2039
2860
|
|
|
2861
|
+
if (ruleConfig.semantic_consistency) {
|
|
2862
|
+
if (issues.semantic_consistency.length === 0) {
|
|
2863
|
+
console.log("✔ semantic_consistency: no semantic ambiguities detected.");
|
|
2864
|
+
} else {
|
|
2865
|
+
console.error(`✘ semantic_consistency: ${issues.semantic_consistency.length} issue(s).`);
|
|
2866
|
+
issues.semantic_consistency.forEach((entry) => {
|
|
2867
|
+
console.error(` - ${entry}`);
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2040
2872
|
if (result.ok) {
|
|
2041
2873
|
console.log("Lint checks passed ✔");
|
|
2042
2874
|
} else {
|
|
@@ -2183,6 +3015,51 @@ function extractFlowsForGraph(filePath) {
|
|
|
2183
3015
|
return sidecarFlows;
|
|
2184
3016
|
}
|
|
2185
3017
|
|
|
3018
|
+
function runQualityReport(parsed) {
|
|
3019
|
+
if (!parsed.filePath || !fs.existsSync(parsed.filePath)) {
|
|
3020
|
+
const missing = parsed.filePath || "(unknown)";
|
|
3021
|
+
console.error(`ERROR: File not found — ${missing}`);
|
|
3022
|
+
return 1;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
const options = {
|
|
3026
|
+
output: "json",
|
|
3027
|
+
strictQuality: false,
|
|
3028
|
+
semantic: parsed.semantic === true,
|
|
3029
|
+
profile: parsed.profile || "strict",
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const originalLog = console.log;
|
|
3033
|
+
let result;
|
|
3034
|
+
try {
|
|
3035
|
+
// run(..., { output: "json" }) prints its own JSON payload; suppress it so
|
|
3036
|
+
// quality-report emits only the consolidated report.
|
|
3037
|
+
console.log = () => {};
|
|
3038
|
+
result = run(parsed.filePath, options);
|
|
3039
|
+
} finally {
|
|
3040
|
+
console.log = originalLog;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
const report = computeQualityReport(result, { semantic: options.semantic });
|
|
3044
|
+
|
|
3045
|
+
const output = JSON.stringify(report, null, 2);
|
|
3046
|
+
|
|
3047
|
+
if (parsed.outputPath) {
|
|
3048
|
+
try {
|
|
3049
|
+
fs.mkdirSync(path.dirname(parsed.outputPath), { recursive: true });
|
|
3050
|
+
fs.writeFileSync(parsed.outputPath, output + "\n", "utf8");
|
|
3051
|
+
console.error(`Quality report written to ${parsed.outputPath}`);
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
console.error(`ERROR: Could not write output file — ${err.message}`);
|
|
3054
|
+
return 1;
|
|
3055
|
+
}
|
|
3056
|
+
} else {
|
|
3057
|
+
console.log(output);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
return report.ok ? 0 : 1;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2186
3063
|
function runGraph(parsed) {
|
|
2187
3064
|
try {
|
|
2188
3065
|
const graphResult = buildMermaidGraph(parsed.filePath);
|
|
@@ -2226,7 +3103,6 @@ function inferCurrentState(entry) {
|
|
|
2226
3103
|
.filter(Boolean)
|
|
2227
3104
|
.join(" ")
|
|
2228
3105
|
.toLowerCase();
|
|
2229
|
-
|
|
2230
3106
|
const keywordToState = [
|
|
2231
3107
|
{ match: /cancel|void/, state: "CANCELED" },
|
|
2232
3108
|
{ match: /refund/, state: "REFUNDED" },
|
|
@@ -2666,6 +3542,42 @@ function runGenerateRedoc(parsed) {
|
|
|
2666
3542
|
}
|
|
2667
3543
|
}
|
|
2668
3544
|
|
|
3545
|
+
function runGenerateFlowTests(parsed) {
|
|
3546
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
3547
|
+
if (!targetOpenApiFile) {
|
|
3548
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
3549
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
3550
|
+
return 1;
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
try {
|
|
3554
|
+
const result = generateFlowTests({
|
|
3555
|
+
apiPath: targetOpenApiFile,
|
|
3556
|
+
format: parsed.format,
|
|
3557
|
+
outputPath: parsed.outputPath,
|
|
3558
|
+
withScripts: parsed.withScripts,
|
|
3559
|
+
});
|
|
3560
|
+
|
|
3561
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
3562
|
+
console.log(`Test format: ${result.format}`);
|
|
3563
|
+
console.log(`Output: ${result.outputPath}`);
|
|
3564
|
+
console.log(`Flow transitions processed: ${result.flowCount}`);
|
|
3565
|
+
if (result.happyPathTests != null) {
|
|
3566
|
+
console.log(`Happy path tests: ${result.happyPathTests}`);
|
|
3567
|
+
}
|
|
3568
|
+
if (result.invalidCaseTests != null) {
|
|
3569
|
+
console.log(`Invalid transition tests: ${result.invalidCaseTests}`);
|
|
3570
|
+
}
|
|
3571
|
+
if (result.withScripts != null) {
|
|
3572
|
+
console.log(`Scripts enabled: ${result.withScripts}`);
|
|
3573
|
+
}
|
|
3574
|
+
return 0;
|
|
3575
|
+
} catch (err) {
|
|
3576
|
+
console.error(`ERROR: Could not generate flow tests — ${err.message}`);
|
|
3577
|
+
return 1;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
|
|
2669
3581
|
function main() {
|
|
2670
3582
|
const parsed = parseArgs(process.argv);
|
|
2671
3583
|
printVerbose(parsed);
|
|
@@ -2695,6 +3607,10 @@ function main() {
|
|
|
2695
3607
|
process.exit(runInit(parsed));
|
|
2696
3608
|
}
|
|
2697
3609
|
|
|
3610
|
+
if (parsed.command === "quickstart") {
|
|
3611
|
+
process.exit(runQuickstart(parsed));
|
|
3612
|
+
}
|
|
3613
|
+
|
|
2698
3614
|
if (parsed.command === "doctor") {
|
|
2699
3615
|
process.exit(runDoctor(parsed));
|
|
2700
3616
|
}
|
|
@@ -2727,6 +3643,10 @@ function main() {
|
|
|
2727
3643
|
process.exit(runGenerateRedoc(parsed));
|
|
2728
3644
|
}
|
|
2729
3645
|
|
|
3646
|
+
if (parsed.command === "generate-flow-tests") {
|
|
3647
|
+
process.exit(runGenerateFlowTests(parsed));
|
|
3648
|
+
}
|
|
3649
|
+
|
|
2730
3650
|
if (parsed.command === "completion") {
|
|
2731
3651
|
process.exit(runCompletion(parsed));
|
|
2732
3652
|
}
|
|
@@ -2748,6 +3668,10 @@ function main() {
|
|
|
2748
3668
|
process.exit(runLint(parsed, config.data));
|
|
2749
3669
|
}
|
|
2750
3670
|
|
|
3671
|
+
if (parsed.command === "quality-report") {
|
|
3672
|
+
process.exit(runQualityReport(parsed));
|
|
3673
|
+
}
|
|
3674
|
+
|
|
2751
3675
|
const config = loadConfig(parsed.configPath);
|
|
2752
3676
|
if (config.error) {
|
|
2753
3677
|
console.error(`ERROR: ${config.error}`);
|
|
@@ -2759,6 +3683,9 @@ function main() {
|
|
|
2759
3683
|
strictQuality:
|
|
2760
3684
|
parsed.strictQuality ||
|
|
2761
3685
|
config.data.strictQuality === true,
|
|
3686
|
+
semantic:
|
|
3687
|
+
parsed.semantic ||
|
|
3688
|
+
config.data.semantic === true,
|
|
2762
3689
|
profile: parsed.profile || config.data.profile || "strict",
|
|
2763
3690
|
};
|
|
2764
3691
|
|