@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 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 --profile local --dry-run
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 --profile local --dry-run
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 flow run login --config-root /path/to/project/cli --profile dev
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 --profile local
326
+ evt flow run login
280
327
  evt cache show
281
328
  evt cache clear
282
- evt api test-all --profile local
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 --profile local --dry-run
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 --profile local --dry-run
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 flow run login --config-root /path/to/project/cli --profile dev
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 --profile local
314
+ evt flow run login
270
315
  evt cache show
271
316
  evt cache clear
272
- evt api test-all --profile local
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.2",
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",
@@ -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
- const envName = envNameFor(key, input);
44
- let value;
45
- if (Object.prototype.hasOwnProperty.call(options.set || {}, key)) {
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 === undefined || value === null || 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 local] [--set k=v] [--body '{...}'] [--dry-run]",
46
- " evt api test-all [--profile local] [--include-dangerous] [--only namespace]",
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 local] [--set k=v] [--dry-run]",
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 || "local");
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
+ });