@icyouo/evt-cli 0.1.0 → 0.1.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.
@@ -97,6 +97,7 @@ function normalizeConfig(config, baseDir = repoRoot) {
97
97
  return {
98
98
  ...config,
99
99
  root,
100
+ apiDir: config.apiDir ? path.resolve(baseDir, config.apiDir) : undefined,
100
101
  targets: (config.targets || []).map((target) => ({ ...target }))
101
102
  };
102
103
  }
@@ -375,37 +376,85 @@ function normalizeSkillEndpoint(rawEndpoint, target, config) {
375
376
  }
376
377
 
377
378
  function scanSkillTarget(config, target) {
378
- if (!target.command) {
379
+ const resolvedTarget = resolveSkillTarget(target);
380
+ if (!resolvedTarget.command) {
379
381
  throw new Error("Skill scanner target must define command");
380
382
  }
381
- const cwd = target.cwd ? path.resolve(config.root, target.cwd) : config.root;
382
- const result = spawnSync(target.command, (target.args || []).map(String), {
383
+ const cwd = resolvedTarget.cwd ? path.resolve(config.root, resolvedTarget.cwd) : config.root;
384
+ const result = spawnSync(resolvedTarget.command, (resolvedTarget.args || []).map(String), {
383
385
  cwd,
384
386
  encoding: "utf8",
385
387
  shell: false,
386
388
  env: {
387
389
  ...process.env,
388
- ...(target.env || {})
390
+ EVT_SCAN_ROOT: config.root,
391
+ ...(resolvedTarget.env || {})
389
392
  },
390
- maxBuffer: target.maxBuffer || 10 * 1024 * 1024
393
+ maxBuffer: resolvedTarget.maxBuffer || 10 * 1024 * 1024
391
394
  });
392
395
 
393
396
  if (result.error) {
394
- throw new Error(`Skill scanner ${target.name || target.command} failed: ${result.error.message}`);
397
+ throw new Error(`Skill scanner ${resolvedTarget.name || resolvedTarget.command} failed: ${result.error.message}`);
395
398
  }
396
399
  if (result.status !== 0) {
397
400
  const stderr = String(result.stderr || "").trim();
398
- throw new Error(`Skill scanner ${target.name || target.command} exited with ${result.status}${stderr ? `: ${stderr}` : ""}`);
401
+ throw new Error(`Skill scanner ${resolvedTarget.name || resolvedTarget.command} exited with ${result.status}${stderr ? `: ${stderr}` : ""}`);
399
402
  }
400
403
 
401
- return parseSkillOutput(result.stdout, target)
402
- .map((endpoint) => normalizeSkillEndpoint(endpoint, target, config));
404
+ return parseSkillOutput(result.stdout, resolvedTarget)
405
+ .map((endpoint) => normalizeSkillEndpoint(endpoint, resolvedTarget, config));
406
+ }
407
+
408
+ function resolveSkillTarget(target) {
409
+ if (!target.skill) return target;
410
+ if (target.skill !== "evt-api-scanner") return target;
411
+ return {
412
+ ...target,
413
+ name: target.name || "evt-api-scanner",
414
+ command: process.execPath,
415
+ args: target.args || [
416
+ path.join(cliRoot, "skills", "evt-api-scanner", "scripts", "scan.js"),
417
+ "--root",
418
+ "."
419
+ ]
420
+ };
421
+ }
422
+
423
+ function splitTargets(targets) {
424
+ const skillTargets = [];
425
+ const fallbackTargets = [];
426
+ for (const target of targets || []) {
427
+ if (isSkillTarget(target)) {
428
+ skillTargets.push(target);
429
+ } else {
430
+ fallbackTargets.push(target);
431
+ }
432
+ }
433
+ return { skillTargets, fallbackTargets };
434
+ }
435
+
436
+ function scanTargets(config, targets) {
437
+ return targets.flatMap((target) => scanTarget(config, target));
403
438
  }
404
439
 
405
440
  function scanServices(options = {}) {
406
441
  const config = loadScanConfig(options);
407
442
  if (!config.targets || config.targets.length === 0) return [];
408
- return config.targets.flatMap((target) => scanTarget(config, target));
443
+ if (options.preferFallback) {
444
+ return scanTargets(config, config.targets.filter((target) => !isSkillTarget(target)));
445
+ }
446
+
447
+ const { skillTargets, fallbackTargets } = splitTargets(config.targets);
448
+ if (skillTargets.length === 0) {
449
+ return scanTargets(config, fallbackTargets);
450
+ }
451
+ try {
452
+ const skillEndpoints = scanTargets(config, skillTargets);
453
+ if (skillEndpoints.length > 0 || fallbackTargets.length === 0) return skillEndpoints;
454
+ } catch (error) {
455
+ if (fallbackTargets.length === 0 || options.strictSkill) throw error;
456
+ }
457
+ return scanTargets(config, fallbackTargets);
409
458
  }
410
459
 
411
460
  function compareScanToRegistry(scanned, registry) {
@@ -422,5 +471,6 @@ module.exports = {
422
471
  loadScanConfig,
423
472
  defaultScanConfig,
424
473
  parseCliScanOptions,
474
+ resolveSkillTarget,
425
475
  repoRoot
426
476
  };
package/src/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ const path = require("node:path");
2
+ const { spawnSync } = require("node:child_process");
1
3
  const { parseArgs } = require("./util/args");
2
4
  const { parseJsonObject, stableJson } = require("./util/json");
3
5
  const { redact } = require("./util/redact");
4
- const { setConfigRoot } = require("./util/paths");
6
+ const { cliRoot, setConfigRoot } = require("./util/paths");
5
7
  const {
6
8
  listApiFiles,
7
9
  listProfiles,
@@ -16,6 +18,8 @@ const { runFlow } = require("./flow/runner");
16
18
  const { toCurl } = require("./http/requestBuilder");
17
19
  const { compareScanToRegistry, scanServices } = require("./config/serviceScanner");
18
20
  const { runLiveApiTest } = require("./api/liveTester");
21
+ const { runSyncApiCoverage } = require("../scripts/sync-api-coverage");
22
+ const { runApiCoverage } = require("../scripts/check-api-coverage");
19
23
 
20
24
  function print(value, json = false) {
21
25
  if (json || typeof value !== "string") {
@@ -32,7 +36,10 @@ function usage() {
32
36
  " evt profile show <name>",
33
37
  " evt api list",
34
38
  " evt api show <id>",
39
+ " evt api discover [--config-root ./cli]",
35
40
  " evt api scan [--missing] [--scan-config path/to/scanner.json]",
41
+ " evt api sync [--config-root ./cli]",
42
+ " evt api coverage [--config-root ./cli]",
36
43
  " evt api call <id> [--profile local] [--set k=v] [--body '{...}'] [--dry-run]",
37
44
  " evt api test-all [--profile local] [--include-dangerous] [--only namespace]",
38
45
  " evt validate",
@@ -42,6 +49,17 @@ function usage() {
42
49
  ].join("\n");
43
50
  }
44
51
 
52
+ function runNodeScript(relativeScript, args) {
53
+ const result = spawnSync(process.execPath, [path.join(cliRoot, relativeScript), ...args], {
54
+ cwd: process.cwd(),
55
+ stdio: "inherit",
56
+ shell: false
57
+ });
58
+ if (result.status !== 0) {
59
+ throw new Error(`${relativeScript} failed with exit code ${result.status || 1}`);
60
+ }
61
+ }
62
+
45
63
  function commonRuntime(options) {
46
64
  const profile = loadProfile(options.profile || "local");
47
65
  const cachePath = resolveCachePath(options.cache);
@@ -73,6 +91,25 @@ async function handleApi(tokens) {
73
91
  const sub = tokens[0];
74
92
  const options = parseArgs(tokens.slice(1));
75
93
  setConfigRoot(options.configRoot);
94
+
95
+ if (sub === "discover") {
96
+ const args = tokens.slice(1);
97
+ if (!options.configRoot && !args.includes("--config-root")) {
98
+ args.push("--config-root", "./cli");
99
+ }
100
+ runNodeScript("skills/evt-api-scanner/scripts/discover.js", args);
101
+ return;
102
+ }
103
+ if (sub === "sync") {
104
+ runSyncApiCoverage(tokens.slice(1));
105
+ return;
106
+ }
107
+ if (sub === "coverage") {
108
+ const result = runApiCoverage(tokens.slice(1));
109
+ if (result.failed) throw new Error("API coverage failed");
110
+ return;
111
+ }
112
+
76
113
  const registry = loadApiRegistry();
77
114
 
78
115
  if (sub === "list") {
@@ -0,0 +1,11 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { parseDuration } = require("../src/util/duration");
4
+
5
+ test("parses duration values", () => {
6
+ assert.equal(parseDuration(1000), 1000);
7
+ assert.equal(parseDuration("1000"), 1000);
8
+ assert.equal(parseDuration("500ms"), 500);
9
+ assert.equal(parseDuration("2s"), 2000);
10
+ assert.equal(parseDuration("1m"), 60000);
11
+ });
@@ -0,0 +1,215 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { runFlow } = require("../src/flow/runner");
4
+ const { loadApiRegistry, loadProfile } = require("../src/config/loaders");
5
+
6
+ test("runs login flow in dry-run mode without prompt", async () => {
7
+ const result = await runFlow({
8
+ name: "login",
9
+ inputs: {
10
+ email: { required: true },
11
+ password: { required: true },
12
+ code: { default: "" },
13
+ googleCode: { default: "" }
14
+ },
15
+ steps: [
16
+ {
17
+ id: "login",
18
+ call: "auth.login",
19
+ body: {
20
+ email: "{{inputs.email}}",
21
+ password: "{{inputs.password}}"
22
+ }
23
+ }
24
+ ]
25
+ }, {
26
+ registry: loadApiRegistry(),
27
+ profile: loadProfile("local"),
28
+ cache: {},
29
+ cachePath: "/tmp/evt-cli-test-cache.json",
30
+ set: {
31
+ email: "user@example.com",
32
+ password: "secret"
33
+ },
34
+ dryRun: true,
35
+ noInteractive: true
36
+ });
37
+
38
+ assert.equal(result.flow, "login");
39
+ assert.equal(result.timeline.filter((item) => item.call).length, 1);
40
+ assert.equal(result.steps.login.request.method, "POST");
41
+ });
42
+
43
+ test("fails when required cache save value is missing", async () => {
44
+ const cachePath = "/tmp/evt-cli-required-cache-test.json";
45
+ const originalFetch = global.fetch;
46
+ global.fetch = async () => ({
47
+ ok: true,
48
+ status: 200,
49
+ text: async () => JSON.stringify({ code: 0, data: { info: { email: "user@example.com" } } })
50
+ });
51
+
52
+ try {
53
+ await assert.rejects(
54
+ runFlow({
55
+ name: "save-required",
56
+ steps: [
57
+ {
58
+ id: "login",
59
+ call: "auth.login",
60
+ body: {
61
+ email: "user@example.com",
62
+ password: "secret"
63
+ }
64
+ }
65
+ ],
66
+ save: {
67
+ cache: {
68
+ token: "{{steps.login.response.data.token}}"
69
+ },
70
+ required: ["cache.token"]
71
+ }
72
+ }, {
73
+ registry: loadApiRegistry(),
74
+ profile: loadProfile("local"),
75
+ cache: {},
76
+ cachePath,
77
+ set: {},
78
+ dryRun: false,
79
+ noInteractive: true
80
+ }),
81
+ /required save value: cache\.token/
82
+ );
83
+ } finally {
84
+ global.fetch = originalFetch;
85
+ }
86
+ });
87
+
88
+ test("supports step expect and extract variables", async () => {
89
+ const originalFetch = global.fetch;
90
+ global.fetch = async () => ({
91
+ ok: true,
92
+ status: 200,
93
+ text: async () => JSON.stringify({ code: 0, data: { token: "token-value" } })
94
+ });
95
+
96
+ try {
97
+ const result = await runFlow({
98
+ name: "expect-extract",
99
+ steps: [
100
+ {
101
+ id: "login",
102
+ call: "auth.login",
103
+ body: {
104
+ email: "user@example.com",
105
+ password: "secret"
106
+ },
107
+ expect: [
108
+ { path: "response.code", equals: 0 },
109
+ { path: "response.data.token", exists: true }
110
+ ],
111
+ extract: {
112
+ token: "response.data.token"
113
+ }
114
+ }
115
+ ]
116
+ }, {
117
+ registry: loadApiRegistry(),
118
+ profile: loadProfile("local"),
119
+ cache: {},
120
+ cachePath: "/tmp/evt-cli-extract-cache-test.json",
121
+ set: {},
122
+ dryRun: false,
123
+ noInteractive: true
124
+ });
125
+
126
+ assert.deepEqual(result.vars, ["token"]);
127
+ } finally {
128
+ global.fetch = originalFetch;
129
+ }
130
+ });
131
+
132
+ test("updates flow cache for later authenticated steps", async () => {
133
+ const originalFetch = global.fetch;
134
+ const seen = [];
135
+ global.fetch = async (url, options) => {
136
+ seen.push({ url, options });
137
+ if (seen.length === 1) {
138
+ return {
139
+ ok: true,
140
+ status: 200,
141
+ text: async () => JSON.stringify({ code: 0, data: { token: "token-value" } })
142
+ };
143
+ }
144
+ return {
145
+ ok: true,
146
+ status: 200,
147
+ text: async () => JSON.stringify({ code: 0, data: { email: "user@example.com" } })
148
+ };
149
+ };
150
+
151
+ try {
152
+ await runFlow({
153
+ name: "flow-cache",
154
+ steps: [
155
+ {
156
+ id: "login",
157
+ call: "auth.login",
158
+ body: {
159
+ email: "user@example.com",
160
+ password: "secret"
161
+ },
162
+ extract: {
163
+ token: "response.data.token"
164
+ },
165
+ cache: {
166
+ token: "{{vars.token}}"
167
+ }
168
+ },
169
+ {
170
+ id: "user",
171
+ call: "todo.list"
172
+ }
173
+ ]
174
+ }, {
175
+ registry: loadApiRegistry(),
176
+ profile: loadProfile("local"),
177
+ cache: {},
178
+ cachePath: "/tmp/evt-cli-flow-cache-test.json",
179
+ set: {},
180
+ dryRun: false,
181
+ noInteractive: true
182
+ });
183
+
184
+ assert.equal(seen[1].options.headers.Authorization, "Bearer token-value");
185
+ } finally {
186
+ global.fetch = originalFetch;
187
+ }
188
+ });
189
+
190
+ test("skips response expectations during dry-run", async () => {
191
+ const result = await runFlow({
192
+ name: "dry-run-expect",
193
+ steps: [
194
+ {
195
+ id: "oauth",
196
+ call: "auth.login",
197
+ expect: { path: "response.code", equals: 0 },
198
+ extract: {
199
+ token: "response.data.token"
200
+ }
201
+ }
202
+ ]
203
+ }, {
204
+ registry: loadApiRegistry(),
205
+ profile: loadProfile("local"),
206
+ cache: {},
207
+ cachePath: "/tmp/evt-cli-dry-run-expect-cache-test.json",
208
+ set: {},
209
+ dryRun: true,
210
+ noInteractive: true
211
+ });
212
+
213
+ assert.equal(result.flow, "dry-run-expect");
214
+ assert.deepEqual(result.vars, []);
215
+ });
@@ -0,0 +1,54 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { sampleArgsForEndpoint } = require("../src/api/liveTester");
4
+
5
+ test("builds live test values from profile fixtures and schema metadata", () => {
6
+ const endpoint = {
7
+ id: "demo.create",
8
+ namespace: "demo",
9
+ schema: {
10
+ path: {
11
+ id: { type: "integer", example: 9 }
12
+ },
13
+ query: {
14
+ page: { type: "integer", default: 1 },
15
+ mode: { type: "string", enum: ["fast", "slow"] }
16
+ },
17
+ body: {
18
+ name: { type: "string", required: true },
19
+ enabled: { type: "boolean" }
20
+ }
21
+ }
22
+ };
23
+
24
+ const args = sampleArgsForEndpoint(endpoint, {
25
+ profile: {
26
+ fixtures: {
27
+ defaults: {
28
+ name: "default-name"
29
+ },
30
+ namespaces: {
31
+ demo: {
32
+ mode: "slow"
33
+ }
34
+ },
35
+ endpoints: {
36
+ "demo.create": {
37
+ path: {
38
+ id: 42
39
+ }
40
+ }
41
+ }
42
+ }
43
+ },
44
+ cache: {}
45
+ });
46
+
47
+ assert.deepEqual(args, {
48
+ id: 42,
49
+ page: 1,
50
+ mode: "slow",
51
+ name: "default-name",
52
+ enabled: false
53
+ });
54
+ });
@@ -0,0 +1,214 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { buildRequest, toCurl } = require("../src/http/requestBuilder");
7
+ const { executeEndpoint } = require("../src/http/client");
8
+ const { redact } = require("../src/util/redact");
9
+
10
+ test("builds authenticated form-url-encoded requests", () => {
11
+ const request = buildRequest({
12
+ id: "trade.createOrder",
13
+ service: "trade",
14
+ method: "POST",
15
+ path: "/api/trade/orders",
16
+ auth: true,
17
+ bodyType: "formUrlEncoded",
18
+ body: {
19
+ symbol: "{{args.symbol}}",
20
+ orderSide: "{{args.orderSide}}",
21
+ origQty: "{{args.origQty}}",
22
+ positionSide: "{{args.positionSide}}",
23
+ price: "{{args.price}}"
24
+ }
25
+ }, {
26
+ profile: {
27
+ baseUrls: { default: "https://api.example.com", trade: "https://f.example.com" },
28
+ headers: { Platform: "IOS" },
29
+ auth: { header: "Authorization", scheme: "raw" }
30
+ },
31
+ cache: { token: "token-value" },
32
+ context: {
33
+ args: {
34
+ symbol: "btc_usdt",
35
+ orderSide: "BUY",
36
+ origQty: "1",
37
+ positionSide: "LONG"
38
+ },
39
+ profile: {},
40
+ cache: {},
41
+ env: {},
42
+ steps: {},
43
+ inputs: {}
44
+ }
45
+ });
46
+
47
+ assert.equal(request.url, "https://f.example.com/api/trade/orders");
48
+ assert.equal(request.headers.Authorization, "token-value");
49
+ assert.equal(request.headers["Content-Type"], "application/x-www-form-urlencoded");
50
+ assert.equal(request.encodedBody, "symbol=btc_usdt&orderSide=BUY&origQty=1&positionSide=LONG");
51
+ });
52
+
53
+ test("renders dynamic path parameters from args", () => {
54
+ const request = buildRequest({
55
+ id: "spot.cancelSpotOrder",
56
+ method: "PATCH",
57
+ path: "/api/spot/order/:code/cancel",
58
+ auth: false
59
+ }, {
60
+ profile: { baseUrl: "https://api.example.com", headers: {} },
61
+ cache: {},
62
+ context: { args: { code: "abc/123" }, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} }
63
+ });
64
+
65
+ assert.equal(request.url, "https://api.example.com/api/spot/order/abc%2F123/cancel");
66
+ });
67
+
68
+ test("materializes schema query and body values from args", () => {
69
+ const request = buildRequest({
70
+ id: "market.getKline",
71
+ service: "market",
72
+ method: "GET",
73
+ path: "/api/market/kline",
74
+ auth: false,
75
+ schema: {
76
+ query: {
77
+ symbol: { type: "string", required: true },
78
+ interval: { type: "string", default: "1m" },
79
+ limit: { type: "integer", default: 100 }
80
+ }
81
+ }
82
+ }, {
83
+ profile: { baseUrls: { default: "https://api.example.com", market: "https://m.example.com" }, headers: {} },
84
+ cache: {},
85
+ context: { args: { symbol: "btc_usdt" }, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} }
86
+ });
87
+
88
+ assert.equal(request.url, "https://m.example.com/api/market/kline?symbol=btc_usdt&interval=1m&limit=100");
89
+ });
90
+
91
+ test("validates required schema values", () => {
92
+ assert.throws(
93
+ () => buildRequest({
94
+ id: "market.getSymbolDetail",
95
+ service: "market",
96
+ method: "GET",
97
+ path: "/api/market/symbol/detail",
98
+ auth: false,
99
+ schema: {
100
+ query: {
101
+ symbol: { type: "string", required: true }
102
+ }
103
+ }
104
+ }, {
105
+ profile: { baseUrls: { default: "https://api.example.com", market: "https://m.example.com" }, headers: {} },
106
+ cache: {},
107
+ context: { args: {}, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} }
108
+ }),
109
+ /Missing required parameter: symbol/
110
+ );
111
+ });
112
+
113
+ test("builds multipart file upload requests", () => {
114
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "evt-cli-"));
115
+ const filePath = path.join(dir, "avatar.png");
116
+ fs.writeFileSync(filePath, "fake image");
117
+
118
+ const request = buildRequest({
119
+ id: "user.uploadImage",
120
+ method: "POST",
121
+ path: "/api/user/upload/image",
122
+ auth: true,
123
+ bodyType: "multipart"
124
+ }, {
125
+ profile: {
126
+ baseUrl: "https://api.example.com",
127
+ headers: { "Content-Type": "application/json" },
128
+ auth: { header: "Authorization", scheme: "raw" }
129
+ },
130
+ cache: { token: "token-value" },
131
+ body: { file: filePath, contentType: "image/png" },
132
+ context: { args: {}, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} }
133
+ });
134
+
135
+ assert.equal(request.headers.Authorization, "token-value");
136
+ assert.equal(request.headers["Content-Type"], undefined);
137
+ assert.equal(request.encodedBody.constructor.name, "FormData");
138
+ assert.equal(request.encodedBody.get("file").name, "avatar.png");
139
+ assert.match(toCurl(request), /-F "file=@.*avatar\.png"/);
140
+ });
141
+
142
+ test("does not require multipart files during dry-run", () => {
143
+ const request = buildRequest({
144
+ id: "user.uploadImage",
145
+ method: "POST",
146
+ path: "/api/user/upload/image",
147
+ auth: false,
148
+ bodyType: "multipart"
149
+ }, {
150
+ profile: { baseUrl: "https://api.example.com", headers: {} },
151
+ cache: {},
152
+ body: { file: "/path/that/does/not/exist.png", contentType: "image/png" },
153
+ context: { args: {}, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} },
154
+ dryRun: true
155
+ });
156
+
157
+ assert.equal(request.encodedBody, undefined);
158
+ assert.match(toCurl(request), /-F "file=@\/path\/that\/does\/not\/exist\.png"/);
159
+ });
160
+
161
+ test("redacts encoded bodies in dry-run output", () => {
162
+ const safe = redact({
163
+ body: {
164
+ password: "secret"
165
+ },
166
+ encodedBody: "{\"password\":\"secret\"}"
167
+ });
168
+
169
+ assert.equal(safe.body.password, "***");
170
+ assert.equal(safe.encodedBody, "***");
171
+ });
172
+
173
+ test("keeps response business code visible while redacting request verification code", () => {
174
+ const safe = redact({
175
+ response: {
176
+ code: 0,
177
+ data: {
178
+ order: {
179
+ code: 123
180
+ }
181
+ }
182
+ },
183
+ body: {
184
+ code: "123456"
185
+ }
186
+ });
187
+
188
+ assert.equal(safe.response.code, 0);
189
+ assert.equal(safe.response.data.order.code, 123);
190
+ assert.equal(safe.body.code, "***");
191
+ });
192
+
193
+ test("blocks dangerous endpoints without unsafe flag", async () => {
194
+ await assert.rejects(
195
+ executeEndpoint({
196
+ id: "trade.createOrder",
197
+ service: "trade",
198
+ method: "POST",
199
+ path: "/api/trade/orders",
200
+ auth: false,
201
+ dangerous: true,
202
+ bodyType: "formUrlEncoded"
203
+ }, {
204
+ profile: {
205
+ baseUrls: { default: "https://api.example.com", trade: "https://t.example.com" },
206
+ headers: {}
207
+ },
208
+ cache: {},
209
+ context: { args: {}, profile: {}, cache: {}, env: {}, steps: {}, inputs: {} },
210
+ dryRun: false
211
+ }),
212
+ /dangerous/
213
+ );
214
+ });