@icyouo/evt-cli 0.1.2 → 0.1.4
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 +52 -5
- package/README.zh-CN.md +50 -5
- package/data/flows/login.example.yaml +20 -0
- package/package.json +2 -2
- package/src/config/validate.js +19 -0
- package/src/flow/inputResolver.js +143 -25
- package/src/index.js +25 -5
- package/src/util/settings.js +45 -0
- package/test/flow.test.js +86 -0
- package/test/profileDefault.test.js +70 -0
package/README.md
CHANGED
|
@@ -25,18 +25,21 @@ Run the bundled example data:
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
evt profile list
|
|
28
|
+
evt profile set local
|
|
29
|
+
evt profile current
|
|
28
30
|
evt api list
|
|
29
31
|
evt validate
|
|
30
|
-
evt api call todo.list --
|
|
32
|
+
evt api call todo.list --dry-run
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
During local development, run the entry file directly:
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
node bin/evt.js profile list
|
|
39
|
+
node bin/evt.js profile set local
|
|
37
40
|
node bin/evt.js api list
|
|
38
41
|
node bin/evt.js validate
|
|
39
|
-
node bin/evt.js api call todo.list --
|
|
42
|
+
node bin/evt.js api call todo.list --dry-run
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
Use your own project data directory:
|
|
@@ -44,7 +47,8 @@ Use your own project data directory:
|
|
|
44
47
|
```bash
|
|
45
48
|
evt validate --config-root /path/to/project/cli
|
|
46
49
|
evt api list --config-root /path/to/project/cli
|
|
47
|
-
evt
|
|
50
|
+
evt profile set dev --config-root /path/to/project/cli
|
|
51
|
+
evt flow run login --config-root /path/to/project/cli
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
Or set an environment variable:
|
|
@@ -54,6 +58,11 @@ export EVT_CLI_ROOT=/path/to/project/cli
|
|
|
54
58
|
evt validate
|
|
55
59
|
```
|
|
56
60
|
|
|
61
|
+
`evt profile set <name>` stores the default profile in
|
|
62
|
+
`data/.evt/config.json` under the active config root. Commands that need a
|
|
63
|
+
profile use that value when `--profile` is omitted. Passing `--profile <name>`
|
|
64
|
+
still overrides the default for one command.
|
|
65
|
+
|
|
57
66
|
## Data Layout
|
|
58
67
|
|
|
59
68
|
Recommended project data layout:
|
|
@@ -266,18 +275,56 @@ intermediate endpoint should include at least:
|
|
|
266
275
|
|
|
267
276
|
This JSON is used only for scanning, coverage, and sync. Runtime execution still reads YAML.
|
|
268
277
|
|
|
278
|
+
## Flow Inputs
|
|
279
|
+
|
|
280
|
+
Flow inputs can prompt users interactively. Inputs with `type: "password"` are
|
|
281
|
+
masked with `*` while typing.
|
|
282
|
+
|
|
283
|
+
Use `inputGroups.anyOf` when one value from a set is required. For example,
|
|
284
|
+
login flows can accept either an email verification code or a Google OTP:
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
inputs:
|
|
288
|
+
email:
|
|
289
|
+
prompt: Email
|
|
290
|
+
type: string
|
|
291
|
+
required: true
|
|
292
|
+
password:
|
|
293
|
+
prompt: Password
|
|
294
|
+
type: password
|
|
295
|
+
required: true
|
|
296
|
+
code:
|
|
297
|
+
prompt: Email code
|
|
298
|
+
type: string
|
|
299
|
+
default: ""
|
|
300
|
+
googleCode:
|
|
301
|
+
prompt: Google OTP
|
|
302
|
+
type: string
|
|
303
|
+
default: ""
|
|
304
|
+
|
|
305
|
+
inputGroups:
|
|
306
|
+
anyOf:
|
|
307
|
+
- fields:
|
|
308
|
+
- code
|
|
309
|
+
- googleCode
|
|
310
|
+
prompt: Enter either an email code or Google OTP.
|
|
311
|
+
message: email code or Google OTP
|
|
312
|
+
```
|
|
313
|
+
|
|
269
314
|
## Common Commands
|
|
270
315
|
|
|
271
316
|
```bash
|
|
272
317
|
evt profile list
|
|
318
|
+
evt profile set local
|
|
319
|
+
evt profile current
|
|
273
320
|
evt api list
|
|
274
321
|
evt api discover --config-root ./cli
|
|
275
322
|
evt api sync --config-root ./cli
|
|
276
323
|
evt api audit --config-root ./cli --strict
|
|
277
324
|
evt api coverage --config-root ./cli
|
|
278
325
|
evt api call todo.list --dry-run
|
|
279
|
-
evt flow run login
|
|
326
|
+
evt flow run login
|
|
280
327
|
evt cache show
|
|
281
328
|
evt cache clear
|
|
282
|
-
evt api test-all
|
|
329
|
+
evt api test-all
|
|
283
330
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -25,18 +25,21 @@ npm install -g @icyouo/evt-cli
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
evt profile list
|
|
28
|
+
evt profile set local
|
|
29
|
+
evt profile current
|
|
28
30
|
evt api list
|
|
29
31
|
evt validate
|
|
30
|
-
evt api call todo.list --
|
|
32
|
+
evt api call todo.list --dry-run
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
仓库内开发时也可以直接运行:
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
node bin/evt.js profile list
|
|
39
|
+
node bin/evt.js profile set local
|
|
37
40
|
node bin/evt.js api list
|
|
38
41
|
node bin/evt.js validate
|
|
39
|
-
node bin/evt.js api call todo.list --
|
|
42
|
+
node bin/evt.js api call todo.list --dry-run
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
使用项目自己的数据目录:
|
|
@@ -44,7 +47,8 @@ node bin/evt.js api call todo.list --profile local --dry-run
|
|
|
44
47
|
```bash
|
|
45
48
|
evt validate --config-root /path/to/project/cli
|
|
46
49
|
evt api list --config-root /path/to/project/cli
|
|
47
|
-
evt
|
|
50
|
+
evt profile set dev --config-root /path/to/project/cli
|
|
51
|
+
evt flow run login --config-root /path/to/project/cli
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
也可以设置环境变量:
|
|
@@ -54,6 +58,10 @@ export EVT_CLI_ROOT=/path/to/project/cli
|
|
|
54
58
|
evt validate
|
|
55
59
|
```
|
|
56
60
|
|
|
61
|
+
`evt profile set <name>` 会把默认 profile 写到当前 config root 下的
|
|
62
|
+
`data/.evt/config.json`。需要 profile 的命令在未传 `--profile` 时会使用这个默认值。
|
|
63
|
+
临时传入 `--profile <name>` 仍然可以覆盖本次命令。
|
|
64
|
+
|
|
57
65
|
## 数据目录
|
|
58
66
|
|
|
59
67
|
项目数据目录结构:
|
|
@@ -256,18 +264,55 @@ npm run sync:api -- --config-root /path/to/project/cli
|
|
|
256
264
|
|
|
257
265
|
这个 JSON 只参与扫描/覆盖率/sync,最终运行仍然读取 YAML。
|
|
258
266
|
|
|
267
|
+
## Flow 输入
|
|
268
|
+
|
|
269
|
+
flow inputs 支持交互式输入。`type: "password"` 的输入会在键入时显示为 `*`。
|
|
270
|
+
|
|
271
|
+
当一组输入里必须至少提供一个值时,可以使用 `inputGroups.anyOf`。例如登录 flow
|
|
272
|
+
可以支持邮箱验证码和 Google OTP 二选一:
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
inputs:
|
|
276
|
+
email:
|
|
277
|
+
prompt: Email
|
|
278
|
+
type: string
|
|
279
|
+
required: true
|
|
280
|
+
password:
|
|
281
|
+
prompt: Password
|
|
282
|
+
type: password
|
|
283
|
+
required: true
|
|
284
|
+
code:
|
|
285
|
+
prompt: Email code
|
|
286
|
+
type: string
|
|
287
|
+
default: ""
|
|
288
|
+
googleCode:
|
|
289
|
+
prompt: Google OTP
|
|
290
|
+
type: string
|
|
291
|
+
default: ""
|
|
292
|
+
|
|
293
|
+
inputGroups:
|
|
294
|
+
anyOf:
|
|
295
|
+
- fields:
|
|
296
|
+
- code
|
|
297
|
+
- googleCode
|
|
298
|
+
prompt: Enter either an email code or Google OTP.
|
|
299
|
+
message: email code or Google OTP
|
|
300
|
+
```
|
|
301
|
+
|
|
259
302
|
## 常用命令
|
|
260
303
|
|
|
261
304
|
```bash
|
|
262
305
|
evt profile list
|
|
306
|
+
evt profile set local
|
|
307
|
+
evt profile current
|
|
263
308
|
evt api list
|
|
264
309
|
evt api discover --config-root ./cli
|
|
265
310
|
evt api sync --config-root ./cli
|
|
266
311
|
evt api audit --config-root ./cli --strict
|
|
267
312
|
evt api coverage --config-root ./cli
|
|
268
313
|
evt api call todo.list --dry-run
|
|
269
|
-
evt flow run login
|
|
314
|
+
evt flow run login
|
|
270
315
|
evt cache show
|
|
271
316
|
evt cache clear
|
|
272
|
-
evt api test-all
|
|
317
|
+
evt api test-all
|
|
273
318
|
```
|
|
@@ -10,6 +10,24 @@ inputs:
|
|
|
10
10
|
prompt: Password
|
|
11
11
|
type: password
|
|
12
12
|
required: true
|
|
13
|
+
code:
|
|
14
|
+
prompt: Email code
|
|
15
|
+
type: string
|
|
16
|
+
required: false
|
|
17
|
+
default: ""
|
|
18
|
+
googleCode:
|
|
19
|
+
prompt: Google OTP
|
|
20
|
+
type: string
|
|
21
|
+
required: false
|
|
22
|
+
default: ""
|
|
23
|
+
|
|
24
|
+
inputGroups:
|
|
25
|
+
anyOf:
|
|
26
|
+
- fields:
|
|
27
|
+
- code
|
|
28
|
+
- googleCode
|
|
29
|
+
prompt: Enter either an email code or Google OTP.
|
|
30
|
+
message: email code or Google OTP
|
|
13
31
|
|
|
14
32
|
steps:
|
|
15
33
|
- id: login
|
|
@@ -17,6 +35,8 @@ steps:
|
|
|
17
35
|
body:
|
|
18
36
|
email: "{{inputs.email}}"
|
|
19
37
|
password: "{{inputs.password}}"
|
|
38
|
+
code: "{{inputs.code}}"
|
|
39
|
+
googleCode: "{{inputs.googleCode}}"
|
|
20
40
|
|
|
21
41
|
save:
|
|
22
42
|
cache:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icyouo/evt-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Run YAML-defined HTTP APIs and interactive workflows from your terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"ci:local": "node scripts/local-ci.js",
|
|
29
29
|
"test": "node --test",
|
|
30
|
-
"check": "node --check bin/evt.js && node --check src/index.js && node --check src/api/liveTester.js && node --check scripts/sync-api-coverage.js && node --check scripts/check-api-coverage.js && node --check scripts/check-api-audit.js && node --check scripts/local-ci.js && node --check scripts/build-package.js && node --check skills/evt-api-scanner/scripts/discover.js && node --check skills/evt-api-scanner/scripts/scan.js",
|
|
30
|
+
"check": "node --check bin/evt.js && node --check src/index.js && node --check src/util/settings.js && node --check src/flow/inputResolver.js && node --check src/api/liveTester.js && node --check scripts/sync-api-coverage.js && node --check scripts/check-api-coverage.js && node --check scripts/check-api-audit.js && node --check scripts/local-ci.js && node --check scripts/build-package.js && node --check skills/evt-api-scanner/scripts/discover.js && node --check skills/evt-api-scanner/scripts/scan.js",
|
|
31
31
|
"package:local": "node scripts/build-package.js",
|
|
32
32
|
"sync:api": "node scripts/sync-api-coverage.js",
|
|
33
33
|
"coverage:api": "node scripts/check-api-coverage.js",
|
package/src/config/validate.js
CHANGED
|
@@ -143,6 +143,25 @@ function validateFlow(flow, file) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
if (flow.inputGroups !== undefined) {
|
|
147
|
+
if (!isObject(flow.inputGroups)) {
|
|
148
|
+
push(errors, file, "flow.inputGroups must be an object");
|
|
149
|
+
} else if (flow.inputGroups.anyOf !== undefined) {
|
|
150
|
+
if (!Array.isArray(flow.inputGroups.anyOf)) {
|
|
151
|
+
push(errors, file, "flow.inputGroups.anyOf must be an array");
|
|
152
|
+
} else {
|
|
153
|
+
for (const [index, group] of flow.inputGroups.anyOf.entries()) {
|
|
154
|
+
if (!isObject(group)) {
|
|
155
|
+
push(errors, file, `inputGroups.anyOf[${index}] must be an object`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(group.fields) || group.fields.length === 0) {
|
|
159
|
+
push(errors, file, `inputGroups.anyOf[${index}].fields must be a non-empty array`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
146
165
|
if (flow.steps !== undefined && !Array.isArray(flow.steps)) push(errors, file, "flow.steps must be an array");
|
|
147
166
|
for (const [index, step] of (flow.steps || []).entries()) {
|
|
148
167
|
if (!isObject(step)) {
|
|
@@ -4,11 +4,79 @@ function envNameFor(key, input) {
|
|
|
4
4
|
return input.env || `NEPTUNE_${key.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}`;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function hasValue(value) {
|
|
8
|
+
return !(value === undefined || value === null || value === "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function promptPassword(question, input) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let value = "";
|
|
14
|
+
const stdin = process.stdin;
|
|
15
|
+
const stdout = process.stdout;
|
|
16
|
+
|
|
17
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: stdin,
|
|
20
|
+
output: stdout,
|
|
21
|
+
terminal: true
|
|
22
|
+
});
|
|
23
|
+
rl.question(question, (answer) => {
|
|
24
|
+
rl.close();
|
|
25
|
+
resolve(answer === "" && input.default !== undefined ? input.default : answer);
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wasRaw = stdin.isRaw;
|
|
31
|
+
stdout.write(question);
|
|
32
|
+
readline.emitKeypressEvents(stdin);
|
|
33
|
+
stdin.setRawMode(true);
|
|
34
|
+
stdin.resume();
|
|
35
|
+
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
stdin.removeListener("keypress", onKeypress);
|
|
38
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
39
|
+
stdout.write("\n");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function onKeypress(sequence, key = {}) {
|
|
43
|
+
if (key.ctrl && key.name === "c") {
|
|
44
|
+
cleanup();
|
|
45
|
+
reject(new Error("Input cancelled"));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (key.name === "return" || key.name === "enter") {
|
|
49
|
+
cleanup();
|
|
50
|
+
resolve(value === "" && input.default !== undefined ? input.default : value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
54
|
+
if (value.length > 0) {
|
|
55
|
+
value = value.slice(0, -1);
|
|
56
|
+
stdout.write("\b \b");
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.ctrl || key.meta || !sequence || /[\r\n]/.test(sequence)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
value += sequence;
|
|
64
|
+
stdout.write("*");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
stdin.on("keypress", onKeypress);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
7
71
|
function promptValue(key, input) {
|
|
8
72
|
const label = input.prompt || key;
|
|
9
73
|
const suffix = input.default !== undefined && input.default !== "" ? ` (${input.default})` : "";
|
|
10
74
|
const question = `${label}${suffix}: `;
|
|
11
75
|
|
|
76
|
+
if (input.type === "password") {
|
|
77
|
+
return promptPassword(question, input);
|
|
78
|
+
}
|
|
79
|
+
|
|
12
80
|
return new Promise((resolve) => {
|
|
13
81
|
const rl = readline.createInterface({
|
|
14
82
|
input: process.stdin,
|
|
@@ -16,47 +84,92 @@ function promptValue(key, input) {
|
|
|
16
84
|
terminal: true
|
|
17
85
|
});
|
|
18
86
|
|
|
19
|
-
if (input.type === "password") {
|
|
20
|
-
rl.stdoutMuted = true;
|
|
21
|
-
rl._writeToOutput = function writeToOutput(stringToWrite) {
|
|
22
|
-
if (!rl.stdoutMuted) {
|
|
23
|
-
rl.output.write(stringToWrite);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
87
|
rl.question(question, (answer) => {
|
|
29
88
|
rl.close();
|
|
30
|
-
if (input.type === "password") {
|
|
31
|
-
process.stdout.write("\n");
|
|
32
|
-
}
|
|
33
89
|
resolve(answer === "" && input.default !== undefined ? input.default : answer);
|
|
34
90
|
});
|
|
35
91
|
});
|
|
36
92
|
}
|
|
37
93
|
|
|
94
|
+
function readConfiguredInput(key, input, options) {
|
|
95
|
+
const envName = envNameFor(key, input);
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(options.set || {}, key)) {
|
|
97
|
+
return options.set[key];
|
|
98
|
+
}
|
|
99
|
+
if (process.env[envName] !== undefined) {
|
|
100
|
+
return process.env[envName];
|
|
101
|
+
}
|
|
102
|
+
if (options.cache && options.cache.inputs && options.cache.inputs[key] !== undefined) {
|
|
103
|
+
return options.cache.inputs[key];
|
|
104
|
+
}
|
|
105
|
+
if (options.profile && options.profile.inputs && options.profile.inputs[key] !== undefined) {
|
|
106
|
+
return options.profile.inputs[key];
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function anyOfGroups(flow) {
|
|
112
|
+
const inputGroups = flow.inputGroups || {};
|
|
113
|
+
const groups = inputGroups.anyOf || [];
|
|
114
|
+
return Array.isArray(groups) ? groups : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function groupedKeys(flow) {
|
|
118
|
+
const keys = new Set();
|
|
119
|
+
for (const group of anyOfGroups(flow)) {
|
|
120
|
+
for (const key of group.fields || []) keys.add(key);
|
|
121
|
+
}
|
|
122
|
+
return keys;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveAnyOfGroups(flow, options, result) {
|
|
126
|
+
const definitions = flow.inputs || {};
|
|
127
|
+
|
|
128
|
+
for (const group of anyOfGroups(flow)) {
|
|
129
|
+
const fields = group.fields || [];
|
|
130
|
+
if (fields.some((key) => hasValue(result[key]))) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let resolved = false;
|
|
135
|
+
if (!options.noInteractive && process.stdin.isTTY) {
|
|
136
|
+
if (group.prompt) {
|
|
137
|
+
process.stdout.write(`${group.prompt}\n`);
|
|
138
|
+
}
|
|
139
|
+
for (const key of fields) {
|
|
140
|
+
const input = definitions[key];
|
|
141
|
+
if (!input) continue;
|
|
142
|
+
const value = await promptValue(key, { ...input, default: undefined });
|
|
143
|
+
if (hasValue(value)) {
|
|
144
|
+
result[key] = value;
|
|
145
|
+
resolved = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!resolved && !fields.some((key) => hasValue(result[key]))) {
|
|
152
|
+
const label = group.message || `One of ${fields.join(", ")} is required`;
|
|
153
|
+
throw new Error(`Missing required flow input group: ${label}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
38
158
|
async function resolveInputs(flow, options) {
|
|
39
159
|
const definitions = flow.inputs || {};
|
|
40
160
|
const result = {};
|
|
161
|
+
const anyOfKeys = groupedKeys(flow);
|
|
41
162
|
|
|
42
163
|
for (const [key, input] of Object.entries(definitions)) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
value = options.set[key];
|
|
47
|
-
} else if (process.env[envName] !== undefined) {
|
|
48
|
-
value = process.env[envName];
|
|
49
|
-
} else if (options.cache && options.cache.inputs && options.cache.inputs[key] !== undefined) {
|
|
50
|
-
value = options.cache.inputs[key];
|
|
51
|
-
} else if (options.profile && options.profile.inputs && options.profile.inputs[key] !== undefined) {
|
|
52
|
-
value = options.profile.inputs[key];
|
|
53
|
-
} else if (!options.noInteractive && process.stdin.isTTY) {
|
|
164
|
+
let value = readConfiguredInput(key, input, options);
|
|
165
|
+
const deferInteractive = anyOfKeys.has(key) && !input.required;
|
|
166
|
+
if (value === undefined && !deferInteractive && !options.noInteractive && process.stdin.isTTY) {
|
|
54
167
|
value = await promptValue(key, input);
|
|
55
|
-
} else if (input.default !== undefined) {
|
|
168
|
+
} else if (value === undefined && input.default !== undefined) {
|
|
56
169
|
value = input.default;
|
|
57
170
|
}
|
|
58
171
|
|
|
59
|
-
if (input.required && (value
|
|
172
|
+
if (input.required && !hasValue(value)) {
|
|
60
173
|
throw new Error(`Missing required flow input: ${key}`);
|
|
61
174
|
}
|
|
62
175
|
if (value !== undefined) {
|
|
@@ -64,9 +177,14 @@ async function resolveInputs(flow, options) {
|
|
|
64
177
|
}
|
|
65
178
|
}
|
|
66
179
|
|
|
180
|
+
await resolveAnyOfGroups(flow, options, result);
|
|
181
|
+
|
|
67
182
|
return result;
|
|
68
183
|
}
|
|
69
184
|
|
|
70
185
|
module.exports = {
|
|
186
|
+
hasValue,
|
|
187
|
+
promptPassword,
|
|
188
|
+
promptValue,
|
|
71
189
|
resolveInputs
|
|
72
190
|
};
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const { parseArgs } = require("./util/args");
|
|
|
4
4
|
const { parseJsonObject, stableJson } = require("./util/json");
|
|
5
5
|
const { redact } = require("./util/redact");
|
|
6
6
|
const { cliRoot, setConfigRoot } = require("./util/paths");
|
|
7
|
+
const { getDefaultProfile, resolveProfileName, setDefaultProfile, settingsPath } = require("./util/settings");
|
|
7
8
|
const {
|
|
8
9
|
listApiFiles,
|
|
9
10
|
listProfiles,
|
|
@@ -34,6 +35,8 @@ function usage() {
|
|
|
34
35
|
return [
|
|
35
36
|
"Usage:",
|
|
36
37
|
" evt profile list [--config-root ./cli]",
|
|
38
|
+
" evt profile current [--config-root ./cli]",
|
|
39
|
+
" evt profile set <name> [--config-root ./cli]",
|
|
37
40
|
" evt profile show <name>",
|
|
38
41
|
" evt api list",
|
|
39
42
|
" evt api show <id>",
|
|
@@ -42,11 +45,11 @@ function usage() {
|
|
|
42
45
|
" evt api sync [--config-root ./cli]",
|
|
43
46
|
" evt api coverage [--config-root ./cli]",
|
|
44
47
|
" evt api audit [--config-root ./cli] [--strict]",
|
|
45
|
-
" evt api call <id> [--profile
|
|
46
|
-
" evt api test-all [--profile
|
|
48
|
+
" evt api call <id> [--profile name] [--set k=v] [--body '{...}'] [--dry-run]",
|
|
49
|
+
" evt api test-all [--profile name] [--include-dangerous] [--only namespace]",
|
|
47
50
|
" evt validate",
|
|
48
51
|
" evt flow list",
|
|
49
|
-
" evt flow run <name> [--profile
|
|
52
|
+
" evt flow run <name> [--profile name] [--set k=v] [--dry-run]",
|
|
50
53
|
" evt cache show|clear|path"
|
|
51
54
|
].join("\n");
|
|
52
55
|
}
|
|
@@ -63,7 +66,7 @@ function runNodeScript(relativeScript, args) {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function commonRuntime(options) {
|
|
66
|
-
const profile = loadProfile(options.profile
|
|
69
|
+
const profile = loadProfile(resolveProfileName(options.profile));
|
|
67
70
|
const cachePath = resolveCachePath(options.cache);
|
|
68
71
|
const cache = readCache(cachePath);
|
|
69
72
|
return {
|
|
@@ -82,8 +85,25 @@ async function handleProfile(tokens) {
|
|
|
82
85
|
print(listProfiles(), options.json);
|
|
83
86
|
return;
|
|
84
87
|
}
|
|
88
|
+
if (sub === "current") {
|
|
89
|
+
const profile = getDefaultProfile();
|
|
90
|
+
if (options.json) {
|
|
91
|
+
print({ profile, settingsPath: settingsPath() }, true);
|
|
92
|
+
} else {
|
|
93
|
+
print(profile);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (sub === "set") {
|
|
98
|
+
const name = options._[0];
|
|
99
|
+
if (!name) throw new Error("evt profile set expects a profile name");
|
|
100
|
+
loadProfile(name);
|
|
101
|
+
const result = setDefaultProfile(name);
|
|
102
|
+
print(options.json ? { ok: true, ...result } : `Default profile set to ${name}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
85
105
|
if (sub === "show") {
|
|
86
|
-
print(loadProfile(options._[0]), true);
|
|
106
|
+
print(loadProfile(resolveProfileName(options._[0])), true);
|
|
87
107
|
return;
|
|
88
108
|
}
|
|
89
109
|
throw new Error(usage());
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { resolveCliPath } = require("./paths");
|
|
4
|
+
|
|
5
|
+
function settingsPath() {
|
|
6
|
+
return resolveCliPath("data", ".evt", "config.json");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readSettings() {
|
|
10
|
+
const file = settingsPath();
|
|
11
|
+
if (!fs.existsSync(file)) return {};
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeSettings(settings) {
|
|
16
|
+
const file = settingsPath();
|
|
17
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
18
|
+
fs.writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getDefaultProfile() {
|
|
22
|
+
return readSettings().defaultProfile || "local";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setDefaultProfile(profile) {
|
|
26
|
+
const settings = readSettings();
|
|
27
|
+
settings.defaultProfile = profile;
|
|
28
|
+
writeSettings(settings);
|
|
29
|
+
return {
|
|
30
|
+
profile,
|
|
31
|
+
path: settingsPath()
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveProfileName(profile) {
|
|
36
|
+
return profile || getDefaultProfile();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
getDefaultProfile,
|
|
41
|
+
readSettings,
|
|
42
|
+
resolveProfileName,
|
|
43
|
+
setDefaultProfile,
|
|
44
|
+
settingsPath
|
|
45
|
+
};
|
package/test/flow.test.js
CHANGED
|
@@ -213,3 +213,89 @@ test("skips response expectations during dry-run", async () => {
|
|
|
213
213
|
assert.equal(result.flow, "dry-run-expect");
|
|
214
214
|
assert.deepEqual(result.vars, []);
|
|
215
215
|
});
|
|
216
|
+
|
|
217
|
+
test("accepts one input from an anyOf input group", async () => {
|
|
218
|
+
const result = await runFlow({
|
|
219
|
+
name: "login-any-of",
|
|
220
|
+
inputs: {
|
|
221
|
+
email: { required: true },
|
|
222
|
+
password: { required: true, type: "password" },
|
|
223
|
+
code: { default: "" },
|
|
224
|
+
googleCode: { default: "" }
|
|
225
|
+
},
|
|
226
|
+
inputGroups: {
|
|
227
|
+
anyOf: [
|
|
228
|
+
{ fields: ["code", "googleCode"], message: "email code or Google OTP" }
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
steps: [
|
|
232
|
+
{
|
|
233
|
+
id: "login",
|
|
234
|
+
call: "auth.login",
|
|
235
|
+
body: {
|
|
236
|
+
email: "{{inputs.email}}",
|
|
237
|
+
password: "{{inputs.password}}",
|
|
238
|
+
code: "{{inputs.code}}",
|
|
239
|
+
googleCode: "{{inputs.googleCode}}"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}, {
|
|
244
|
+
registry: loadApiRegistry(),
|
|
245
|
+
profile: loadProfile("local"),
|
|
246
|
+
cache: {},
|
|
247
|
+
cachePath: "/tmp/evt-cli-any-of-cache-test.json",
|
|
248
|
+
set: {
|
|
249
|
+
email: "user@example.com",
|
|
250
|
+
password: "secret",
|
|
251
|
+
googleCode: "123456"
|
|
252
|
+
},
|
|
253
|
+
dryRun: true,
|
|
254
|
+
noInteractive: true
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert.deepEqual(result.inputs.sort(), ["code", "email", "googleCode", "password"].sort());
|
|
258
|
+
assert.equal(result.steps.login.request.body.code, "");
|
|
259
|
+
assert.equal(result.steps.login.request.body.googleCode, "123456");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("fails when no input from an anyOf input group is available", async () => {
|
|
263
|
+
await assert.rejects(
|
|
264
|
+
runFlow({
|
|
265
|
+
name: "login-any-of-missing",
|
|
266
|
+
inputs: {
|
|
267
|
+
email: { required: true },
|
|
268
|
+
password: { required: true, type: "password" },
|
|
269
|
+
code: { default: "" },
|
|
270
|
+
googleCode: { default: "" }
|
|
271
|
+
},
|
|
272
|
+
inputGroups: {
|
|
273
|
+
anyOf: [
|
|
274
|
+
{ fields: ["code", "googleCode"], message: "email code or Google OTP" }
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
steps: [
|
|
278
|
+
{
|
|
279
|
+
id: "login",
|
|
280
|
+
call: "auth.login",
|
|
281
|
+
body: {
|
|
282
|
+
email: "{{inputs.email}}",
|
|
283
|
+
password: "{{inputs.password}}"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
}, {
|
|
288
|
+
registry: loadApiRegistry(),
|
|
289
|
+
profile: { name: "test", baseUrl: "https://api.example.com", inputs: {} },
|
|
290
|
+
cache: {},
|
|
291
|
+
cachePath: "/tmp/evt-cli-any-of-missing-cache-test.json",
|
|
292
|
+
set: {
|
|
293
|
+
email: "user@example.com",
|
|
294
|
+
password: "secret"
|
|
295
|
+
},
|
|
296
|
+
dryRun: true,
|
|
297
|
+
noInteractive: true
|
|
298
|
+
}),
|
|
299
|
+
/Missing required flow input group: email code or Google OTP/
|
|
300
|
+
);
|
|
301
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
const assert = require("node:assert/strict");
|
|
6
|
+
const { run } = require("../src/index");
|
|
7
|
+
|
|
8
|
+
function write(file, content) {
|
|
9
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
10
|
+
fs.writeFileSync(file, content);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function capture(fn) {
|
|
14
|
+
const originalLog = console.log;
|
|
15
|
+
const lines = [];
|
|
16
|
+
console.log = (value) => lines.push(value);
|
|
17
|
+
try {
|
|
18
|
+
await fn();
|
|
19
|
+
} finally {
|
|
20
|
+
console.log = originalLog;
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createConfigRoot() {
|
|
26
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "evt-profile-default-"));
|
|
27
|
+
write(path.join(root, "data", "profiles", "dev.json"), JSON.stringify({
|
|
28
|
+
name: "dev",
|
|
29
|
+
baseUrl: "https://dev.example.test",
|
|
30
|
+
headers: {
|
|
31
|
+
"X-Profile": "dev"
|
|
32
|
+
}
|
|
33
|
+
}, null, 2));
|
|
34
|
+
write(path.join(root, "data", "apis", "todo.yaml"), [
|
|
35
|
+
"namespace: todo",
|
|
36
|
+
"",
|
|
37
|
+
"endpoints:",
|
|
38
|
+
" list:",
|
|
39
|
+
" method: \"GET\"",
|
|
40
|
+
" path: \"/api/todos\"",
|
|
41
|
+
" auth: false",
|
|
42
|
+
" response:",
|
|
43
|
+
" data:",
|
|
44
|
+
" type: \"array\"",
|
|
45
|
+
""
|
|
46
|
+
].join("\n"));
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("profile set stores a project default profile used by api calls", async () => {
|
|
51
|
+
const root = createConfigRoot();
|
|
52
|
+
|
|
53
|
+
await capture(() => run(["profile", "set", "dev", "--config-root", root]));
|
|
54
|
+
|
|
55
|
+
const current = await capture(() => run(["profile", "current", "--config-root", root]));
|
|
56
|
+
assert.equal(current[0], "dev");
|
|
57
|
+
|
|
58
|
+
const output = await capture(() => run([
|
|
59
|
+
"api",
|
|
60
|
+
"call",
|
|
61
|
+
"todo.list",
|
|
62
|
+
"--config-root",
|
|
63
|
+
root,
|
|
64
|
+
"--dry-run",
|
|
65
|
+
"--json"
|
|
66
|
+
]));
|
|
67
|
+
const payload = JSON.parse(output[0]);
|
|
68
|
+
assert.equal(payload.request.url, "https://dev.example.test/api/todos");
|
|
69
|
+
assert.equal(payload.request.headers["X-Profile"], "dev");
|
|
70
|
+
});
|