@testsmith/api-spector 0.7.1 → 0.7.3

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/bin/cli.js CHANGED
@@ -18,6 +18,7 @@ const [, , cmd = 'ui', ...rest] = process.argv
18
18
  const COMMANDS = {
19
19
  ui: { entrypoint: null, runner: 'electron' },
20
20
  run: { entrypoint: 'runner.js', runner: 'node' },
21
+ flow: { entrypoint: 'flow.js', runner: 'node' },
21
22
  mock: { entrypoint: 'mock.js', runner: 'node' },
22
23
  record: { entrypoint: 'record.js', runner: 'node' },
23
24
  agents: { entrypoint: 'agents.js', runner: 'node' },
@@ -35,6 +36,7 @@ function printHelp() {
35
36
  console.log(' Usage:')
36
37
  console.log(' api-spector ui Launch the app')
37
38
  console.log(' api-spector run --workspace <path> Run tests from CLI')
39
+ console.log(' api-spector flow list|run <name> Run a visual flow from CLI')
38
40
  console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
39
41
  console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
40
42
  console.log(' api-spector contract list|run Manage & run pinned contract snapshots')
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const cliCommon = require("./chunks/cli-common-Cq_cphpZ.js");
5
+ const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
6
6
  require("./chunks/data-files-BYqhsoPe.js");
7
7
  require("node:crypto");
8
8
  const AGENTS = {
@@ -3,7 +3,7 @@ const promises = require("node:fs/promises");
3
3
  const path = require("node:path");
4
4
  const crypto = require("node:crypto");
5
5
  const undici = require("undici");
6
- const requestExec = require("./request-exec-DLBjLZe1.js");
6
+ const requestExec = require("./request-exec-CcLHtkwG.js");
7
7
  const Ajv = require("ajv");
8
8
  const jsYaml = require("js-yaml");
9
9
  require("node:http");
@@ -90,11 +90,24 @@ async function loadMocks(workspace, dir, onError) {
90
90
  }
91
91
  return mocks;
92
92
  }
93
+ async function loadFlows(workspace, dir, onError) {
94
+ const flows = [];
95
+ for (const relPath of workspace.flows ?? []) {
96
+ try {
97
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
98
+ flows.push(JSON.parse(raw));
99
+ } catch {
100
+ onError?.(relPath);
101
+ }
102
+ }
103
+ return flows;
104
+ }
93
105
  exports.C = C;
94
106
  exports.color = color;
95
107
  exports.colorAlways = colorAlways;
96
108
  exports.loadCollections = loadCollections;
97
109
  exports.loadEnvironments = loadEnvironments;
110
+ exports.loadFlows = loadFlows;
98
111
  exports.loadMocks = loadMocks;
99
112
  exports.loadWorkspace = loadWorkspace;
100
113
  exports.parseArgs = parseArgs;
@@ -0,0 +1,533 @@
1
+ "use strict";
2
+ const IN = [{ id: "in" }];
3
+ const OUT = () => [{ id: "out" }];
4
+ const BLOCK_SPECS = {
5
+ start: {
6
+ type: "start",
7
+ category: "terminal",
8
+ label: "Start",
9
+ description: "Where the flow begins.",
10
+ inputs: [],
11
+ outputs: OUT
12
+ },
13
+ end: {
14
+ type: "end",
15
+ category: "terminal",
16
+ label: "End",
17
+ description: "Ends this path of the flow.",
18
+ inputs: IN,
19
+ outputs: () => []
20
+ },
21
+ // ── Action ──────────────────────────────────────────────────────────────
22
+ request: {
23
+ type: "request",
24
+ category: "action",
25
+ label: "Send Request",
26
+ description: "Send a saved request; routes success or fail on its response.",
27
+ inputs: IN,
28
+ outputs: () => [{ id: "success", label: "success" }, { id: "fail", label: "fail" }]
29
+ },
30
+ subflow: {
31
+ type: "subflow",
32
+ category: "action",
33
+ label: "Run Flow",
34
+ description: "Run another flow from this workspace.",
35
+ inputs: IN,
36
+ outputs: OUT
37
+ },
38
+ // ── Logic ───────────────────────────────────────────────────────────────
39
+ if: {
40
+ type: "if",
41
+ category: "logic",
42
+ label: "If",
43
+ description: "Route to true or false based on a JS condition.",
44
+ inputs: IN,
45
+ outputs: () => [{ id: "true", label: "true" }, { id: "false", label: "false" }],
46
+ defaultConfig: { expression: "sp.response.code === 200" }
47
+ },
48
+ condition: {
49
+ type: "condition",
50
+ category: "logic",
51
+ label: "Condition",
52
+ description: "Multi-way switch: the first matching case wins, else `else`.",
53
+ inputs: IN,
54
+ outputs: (config) => [
55
+ ...(config?.cases ?? []).map((c) => ({ id: c.id, label: c.label })),
56
+ { id: "else", label: "else" }
57
+ ],
58
+ defaultConfig: {
59
+ cases: [
60
+ { id: "case1", label: "ok", expression: "sp.response.code < 400" }
61
+ ]
62
+ }
63
+ },
64
+ validate: {
65
+ type: "validate",
66
+ category: "logic",
67
+ label: "Validate",
68
+ description: "Assert with sp.test(...); routes pass or fail.",
69
+ inputs: IN,
70
+ outputs: () => [{ id: "pass", label: "pass" }, { id: "fail", label: "fail" }],
71
+ defaultConfig: {
72
+ script: "sp.test('status is 2xx', () => {\n sp.expect(sp.response.code).to.be.below(300);\n});"
73
+ }
74
+ },
75
+ evaluate: {
76
+ type: "evaluate",
77
+ category: "logic",
78
+ label: "Evaluate",
79
+ description: "Run JS to compute values and set variables.",
80
+ inputs: IN,
81
+ outputs: OUT,
82
+ defaultConfig: { script: "// sp.variables.set('id', sp.response.json().id)" }
83
+ },
84
+ setVar: {
85
+ type: "setVar",
86
+ category: "logic",
87
+ label: "Set Variable",
88
+ description: "Set a variable from an expression, in a chosen scope.",
89
+ inputs: IN,
90
+ outputs: OUT,
91
+ defaultConfig: { varName: "myVar", valueExpression: "sp.response.json()", scope: "local" }
92
+ },
93
+ delay: {
94
+ type: "delay",
95
+ category: "logic",
96
+ label: "Delay",
97
+ description: "Wait before continuing.",
98
+ inputs: IN,
99
+ outputs: OUT,
100
+ defaultConfig: { delayMs: 1e3 }
101
+ },
102
+ merge: {
103
+ type: "merge",
104
+ category: "logic",
105
+ label: "Or / Merge",
106
+ description: "Continue as soon as any one incoming branch arrives.",
107
+ inputs: IN,
108
+ outputs: OUT
109
+ },
110
+ // ── Looping ─────────────────────────────────────────────────────────────
111
+ forEach: {
112
+ type: "forEach",
113
+ category: "looping",
114
+ label: "For Each",
115
+ description: "Run the body once per item of a list; then continue on done.",
116
+ inputs: IN,
117
+ outputs: () => [{ id: "body", label: "body" }, { id: "done", label: "done" }],
118
+ defaultConfig: { expression: "sp.response.json()", itemVar: "item", indexVar: "index" }
119
+ },
120
+ repeat: {
121
+ type: "repeat",
122
+ category: "looping",
123
+ label: "Repeat",
124
+ description: "Run the body a fixed number of times, then continue on done.",
125
+ inputs: IN,
126
+ outputs: () => [{ id: "body", label: "body" }, { id: "done", label: "done" }],
127
+ defaultConfig: { count: 3, indexVar: "index" }
128
+ },
129
+ collect: {
130
+ type: "collect",
131
+ category: "looping",
132
+ label: "Collect",
133
+ description: "Append each iteration's value into a list variable.",
134
+ inputs: IN,
135
+ outputs: OUT,
136
+ defaultConfig: { intoVar: "results" }
137
+ },
138
+ // ── Visualize ───────────────────────────────────────────────────────────
139
+ display: {
140
+ type: "display",
141
+ category: "visualize",
142
+ label: "Display",
143
+ description: "Show a value in the run results (text, JSON or table).",
144
+ inputs: IN,
145
+ outputs: OUT,
146
+ defaultConfig: { message: "sp.response.json()", as: "json" }
147
+ },
148
+ log: {
149
+ type: "log",
150
+ category: "visualize",
151
+ label: "Log",
152
+ description: "Append a message to the run log.",
153
+ inputs: IN,
154
+ outputs: OUT,
155
+ defaultConfig: { message: "Reached this point" }
156
+ }
157
+ };
158
+ [
159
+ { category: "action", blocks: [BLOCK_SPECS.request, BLOCK_SPECS.subflow] },
160
+ { category: "logic", blocks: [BLOCK_SPECS.if, BLOCK_SPECS.condition, BLOCK_SPECS.validate, BLOCK_SPECS.evaluate, BLOCK_SPECS.setVar, BLOCK_SPECS.delay, BLOCK_SPECS.merge] },
161
+ { category: "looping", blocks: [BLOCK_SPECS.forEach, BLOCK_SPECS.repeat, BLOCK_SPECS.collect] },
162
+ { category: "visualize", blocks: [BLOCK_SPECS.display, BLOCK_SPECS.log] }
163
+ ];
164
+ function normalizeFlow(raw) {
165
+ const nodes = (raw.nodes ?? []).map((n) => {
166
+ const legacy = n;
167
+ const type = legacy.type ?? legacy.kind ?? "request";
168
+ const config = { ...n.config ?? {} };
169
+ if (legacy.ref && !config.ref) config.ref = legacy.ref;
170
+ return {
171
+ id: n.id,
172
+ type,
173
+ position: n.position ?? { x: 0, y: 0 },
174
+ label: n.label,
175
+ color: n.color,
176
+ config
177
+ };
178
+ });
179
+ const requestIds = new Set(nodes.filter((n) => n.type === "request").map((n) => n.id));
180
+ const edges = (raw.edges ?? []).map((e) => {
181
+ let sourcePort = e.sourcePort;
182
+ if (requestIds.has(e.source) && (sourcePort == null || sourcePort === "out")) sourcePort = "success";
183
+ return { id: e.id, source: e.source, sourcePort, target: e.target, targetPort: e.targetPort };
184
+ });
185
+ return { version: "1.0", id: raw.id, name: raw.name, description: raw.description, nodes, edges };
186
+ }
187
+ function toScalar(v) {
188
+ if (v == null) return "";
189
+ if (typeof v === "object") return JSON.stringify(v);
190
+ return String(v);
191
+ }
192
+ function parseBody(body) {
193
+ if (body == null) return void 0;
194
+ try {
195
+ return JSON.parse(body);
196
+ } catch {
197
+ return body;
198
+ }
199
+ }
200
+ async function runFlow(flow, deps, options = {}) {
201
+ const startedAt = Date.now();
202
+ const maxSteps = deps.maxSteps ?? 1e4;
203
+ const ctx = {
204
+ envVars: { ...options.envVars ?? {} },
205
+ collectionVars: { ...options.collectionVars ?? {} },
206
+ globals: { ...options.globals ?? {} },
207
+ localVars: { ...options.localVars ?? {} },
208
+ response: null,
209
+ data: {}
210
+ };
211
+ const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0, blocks: [] };
212
+ const state = { steps: 0 };
213
+ const relTime = () => Date.now() - startedAt;
214
+ const takenEdges = /* @__PURE__ */ new Set();
215
+ const emitEdges = (blockId, blockType, ids) => {
216
+ if (ids.length === 0) return;
217
+ for (const id of ids) takenEdges.add(id);
218
+ deps.onEvent?.({ kind: "edge", blockId, blockType, edgeIds: ids, atMs: relTime() });
219
+ };
220
+ const scriptInput = () => ({
221
+ envVars: ctx.envVars,
222
+ collectionVars: ctx.collectionVars,
223
+ globals: ctx.globals,
224
+ localVars: ctx.localVars,
225
+ response: ctx.response,
226
+ data: ctx.data
227
+ });
228
+ const applyScopes = (r) => {
229
+ ctx.envVars = r.envVars;
230
+ ctx.collectionVars = r.collectionVars;
231
+ ctx.globals = r.globals;
232
+ ctx.localVars = r.localVars;
233
+ };
234
+ const mergedVars = () => ({
235
+ ...ctx.globals,
236
+ ...ctx.collectionVars,
237
+ ...ctx.envVars,
238
+ ...ctx.localVars
239
+ });
240
+ const record = (block, status, extra = {}) => {
241
+ const rec = {
242
+ blockId: block.id,
243
+ type: block.type,
244
+ label: block.label || BLOCK_SPECS[block.type].label,
245
+ status,
246
+ atMs: relTime(),
247
+ ...extra
248
+ };
249
+ summary.blocks.push(rec);
250
+ summary.total++;
251
+ if (status === "passed") summary.passed++;
252
+ else if (status === "failed") summary.failed++;
253
+ else if (status === "error") summary.errors++;
254
+ deps.onEvent?.({
255
+ kind: "block-result",
256
+ blockId: block.id,
257
+ blockType: block.type,
258
+ label: rec.label,
259
+ atMs: relTime(),
260
+ status,
261
+ httpStatus: extra.httpStatus,
262
+ durationMs: extra.durationMs,
263
+ error: extra.error,
264
+ testResults: extra.testResults,
265
+ iteration: extra.iteration,
266
+ vars: mergedVars()
267
+ });
268
+ };
269
+ const renderMessage = async (msg) => {
270
+ const r = await deps.evalExpression(msg, scriptInput());
271
+ applyScopes(r);
272
+ if (!r.error) return r.value;
273
+ if (deps.interpolate) return deps.interpolate(msg, mergedVars());
274
+ return msg;
275
+ };
276
+ async function runOne(current, stack) {
277
+ const nodeById = new Map(current.nodes.map((n) => [n.id, n]));
278
+ const outEdges = (block, port) => current.edges.filter((e) => {
279
+ if (e.source !== block.id) return false;
280
+ const sp = e.sourcePort ?? "out";
281
+ if (sp === port) return true;
282
+ return block.type === "request" && port === "success" && sp === "out";
283
+ });
284
+ async function execBlock(block, iteration) {
285
+ const cfg = block.config ?? {};
286
+ const label = block.label || BLOCK_SPECS[block.type].label;
287
+ switch (block.type) {
288
+ case "start":
289
+ return ["out"];
290
+ case "end":
291
+ return [];
292
+ case "merge":
293
+ return ["out"];
294
+ case "log": {
295
+ const value = await renderMessage(cfg.message ?? "");
296
+ const message = toScalar(value);
297
+ deps.onEvent?.({ kind: "log", blockId: block.id, blockType: block.type, label, atMs: relTime(), message });
298
+ record(block, "done", { iteration, message });
299
+ return ["out"];
300
+ }
301
+ case "display": {
302
+ const value = await renderMessage(cfg.message ?? "");
303
+ deps.onEvent?.({
304
+ kind: "display",
305
+ blockId: block.id,
306
+ blockType: block.type,
307
+ label,
308
+ atMs: relTime(),
309
+ display: { as: cfg.as ?? "json", value, label }
310
+ });
311
+ record(block, "done", { iteration, display: value });
312
+ return ["out"];
313
+ }
314
+ case "delay": {
315
+ const ms = Math.max(0, cfg.delayMs ?? 0);
316
+ deps.onEvent?.({ kind: "block-start", blockId: block.id, blockType: block.type, status: "running", iteration });
317
+ const t0 = Date.now();
318
+ await deps.sleep(ms);
319
+ record(block, "done", { iteration, durationMs: Date.now() - t0 });
320
+ return ["out"];
321
+ }
322
+ case "evaluate": {
323
+ deps.onEvent?.({ kind: "block-start", blockId: block.id, blockType: block.type, status: "running", iteration });
324
+ const r = await deps.runScript(cfg.script ?? "", scriptInput());
325
+ applyScopes(r);
326
+ record(block, r.error ? "error" : "done", { iteration, error: r.error, testResults: r.testResults });
327
+ return ["out"];
328
+ }
329
+ case "validate": {
330
+ deps.onEvent?.({ kind: "block-start", blockId: block.id, blockType: block.type, status: "running", iteration });
331
+ const r = await deps.runScript(cfg.script ?? "", scriptInput());
332
+ applyScopes(r);
333
+ const failed = Boolean(r.error) || (r.testResults ?? []).some((t) => !t.passed);
334
+ record(block, r.error ? "error" : failed ? "failed" : "passed", { iteration, error: r.error, testResults: r.testResults });
335
+ return [failed ? "fail" : "pass"];
336
+ }
337
+ case "setVar": {
338
+ const r = await deps.evalExpression(cfg.valueExpression ?? "undefined", scriptInput());
339
+ applyScopes(r);
340
+ const name = cfg.varName;
341
+ if (name) {
342
+ const scalar = toScalar(r.value);
343
+ switch (cfg.scope ?? "local") {
344
+ case "collection":
345
+ ctx.collectionVars[name] = scalar;
346
+ break;
347
+ case "environment":
348
+ ctx.envVars[name] = scalar;
349
+ break;
350
+ case "global":
351
+ ctx.globals[name] = scalar;
352
+ break;
353
+ default:
354
+ ctx.localVars[name] = scalar;
355
+ ctx.data[name] = r.value;
356
+ break;
357
+ }
358
+ }
359
+ record(block, r.error ? "error" : "done", { iteration, error: r.error });
360
+ return ["out"];
361
+ }
362
+ case "if": {
363
+ const r = await deps.evalExpression(cfg.expression ?? "false", scriptInput());
364
+ applyScopes(r);
365
+ if (r.error) {
366
+ record(block, "error", { iteration, error: r.error });
367
+ return ["false"];
368
+ }
369
+ record(block, "done", { iteration });
370
+ return [r.value ? "true" : "false"];
371
+ }
372
+ case "condition": {
373
+ for (const c of cfg.cases ?? []) {
374
+ const r = await deps.evalExpression(c.expression, scriptInput());
375
+ applyScopes(r);
376
+ if (!r.error && r.value) {
377
+ record(block, "done", { iteration });
378
+ return [c.id];
379
+ }
380
+ }
381
+ record(block, "done", { iteration });
382
+ return ["else"];
383
+ }
384
+ case "request": {
385
+ if (!cfg.ref) {
386
+ record(block, "error", { iteration, error: "No request selected" });
387
+ return ["fail"];
388
+ }
389
+ deps.onEvent?.({ kind: "block-start", blockId: block.id, blockType: block.type, status: "running", iteration });
390
+ const r = await deps.runRequest(cfg.ref, scriptInput());
391
+ if (!r) {
392
+ record(block, "error", { iteration, error: "Linked request not found" });
393
+ return ["fail"];
394
+ }
395
+ applyScopes(r);
396
+ ctx.response = r.response;
397
+ const ok = r.status === "passed" || r.status === "skipped";
398
+ const reqStatus = r.status === "skipped" ? "done" : r.status === "pending" ? "running" : r.status;
399
+ record(block, reqStatus, {
400
+ iteration,
401
+ httpStatus: r.httpStatus,
402
+ durationMs: r.durationMs,
403
+ error: r.error,
404
+ testResults: r.testResults,
405
+ method: r.method,
406
+ resolvedUrl: r.resolvedUrl
407
+ });
408
+ return [ok ? "success" : "fail"];
409
+ }
410
+ case "subflow": {
411
+ const sub = cfg.flowId ? deps.resolveFlow(cfg.flowId) : null;
412
+ if (!sub) {
413
+ record(block, "error", { iteration, error: "Linked flow not found" });
414
+ return ["out"];
415
+ }
416
+ if (stack.includes(sub.id)) {
417
+ record(block, "error", { iteration, error: "Recursive sub-flow blocked" });
418
+ return ["out"];
419
+ }
420
+ deps.onEvent?.({ kind: "block-start", blockId: block.id, blockType: block.type, status: "running", iteration });
421
+ await runOne(sub, [...stack, sub.id]);
422
+ record(block, "done", { iteration });
423
+ return ["out"];
424
+ }
425
+ case "forEach": {
426
+ const r = await deps.evalExpression(cfg.expression ?? "[]", scriptInput());
427
+ applyScopes(r);
428
+ if (r.error) {
429
+ record(block, "error", { iteration, error: r.error });
430
+ return ["done"];
431
+ }
432
+ const list = Array.isArray(r.value) ? r.value : r.value == null ? [] : [r.value];
433
+ const itemVar = cfg.itemVar || "item";
434
+ const indexVar = cfg.indexVar || "index";
435
+ const bodyEdges = outEdges(block, "body");
436
+ emitEdges(block.id, block.type, bodyEdges.map((e) => e.id));
437
+ const bodyTargets = bodyEdges.map((e) => e.target);
438
+ for (let i = 0; i < list.length; i++) {
439
+ ctx.data[itemVar] = list[i];
440
+ ctx.data[indexVar] = i;
441
+ ctx.localVars[itemVar] = toScalar(list[i]);
442
+ ctx.localVars[indexVar] = String(i);
443
+ await walk(bodyTargets, i);
444
+ }
445
+ record(block, "done", { iteration });
446
+ return ["done"];
447
+ }
448
+ case "repeat": {
449
+ const n = Math.max(0, Math.floor(cfg.count ?? 0));
450
+ const indexVar = cfg.indexVar || "index";
451
+ const bodyEdges = outEdges(block, "body");
452
+ emitEdges(block.id, block.type, bodyEdges.map((e) => e.id));
453
+ const bodyTargets = bodyEdges.map((e) => e.target);
454
+ for (let i = 0; i < n; i++) {
455
+ ctx.data[indexVar] = i;
456
+ ctx.localVars[indexVar] = String(i);
457
+ await walk(bodyTargets, i);
458
+ }
459
+ record(block, "done", { iteration });
460
+ return ["done"];
461
+ }
462
+ case "collect": {
463
+ const intoVar = cfg.intoVar || "results";
464
+ let value;
465
+ if (cfg.valueExpression) {
466
+ const r = await deps.evalExpression(cfg.valueExpression, scriptInput());
467
+ applyScopes(r);
468
+ value = r.value;
469
+ } else {
470
+ value = ctx.response ? parseBody(ctx.response.body) : void 0;
471
+ }
472
+ const arr = Array.isArray(ctx.data[intoVar]) ? ctx.data[intoVar] : [];
473
+ arr.push(value);
474
+ ctx.data[intoVar] = arr;
475
+ ctx.localVars[intoVar] = JSON.stringify(arr);
476
+ record(block, "done", { iteration });
477
+ return ["out"];
478
+ }
479
+ default:
480
+ return ["out"];
481
+ }
482
+ }
483
+ async function walk(startNodeIds, iteration) {
484
+ const queue = [...startNodeIds];
485
+ const firedMerges = /* @__PURE__ */ new Set();
486
+ while (queue.length) {
487
+ if (++state.steps > maxSteps) {
488
+ throw new Error("Flow exceeded the step limit — check for an unbounded loop or merge cycle.");
489
+ }
490
+ const id = queue.shift();
491
+ const block = nodeById.get(id);
492
+ if (!block) continue;
493
+ if (block.type === "merge") {
494
+ if (firedMerges.has(id)) continue;
495
+ firedMerges.add(id);
496
+ }
497
+ const ports = await execBlock(block, iteration);
498
+ const takenIds = [];
499
+ for (const port of ports) {
500
+ for (const e of outEdges(block, port)) {
501
+ queue.push(e.target);
502
+ takenIds.push(e.id);
503
+ }
504
+ }
505
+ emitEdges(id, block.type, takenIds);
506
+ }
507
+ }
508
+ const startBlock = current.nodes.find((n) => n.type === "start");
509
+ const roots = startBlock ? [startBlock.id] : current.nodes.filter((n) => !current.edges.some((e) => e.target === n.id)).map((n) => n.id);
510
+ await walk(roots);
511
+ }
512
+ const startInputs = flow.nodes.find((n) => n.type === "start")?.config?.inputs ?? [];
513
+ for (const { key, value } of startInputs) {
514
+ if (!key) continue;
515
+ const resolved = deps.interpolate ? deps.interpolate(value, mergedVars()) : value;
516
+ ctx.localVars[key] = resolved;
517
+ ctx.data[key] = resolved;
518
+ }
519
+ await runOne(flow, [flow.id]);
520
+ summary.durationMs = Date.now() - startedAt;
521
+ summary.variables = {
522
+ envVars: { ...ctx.envVars },
523
+ collectionVars: { ...ctx.collectionVars },
524
+ globals: { ...ctx.globals },
525
+ localVars: { ...ctx.localVars }
526
+ };
527
+ summary.data = ctx.data;
528
+ summary.traversedEdges = [...takenEdges];
529
+ return summary;
530
+ }
531
+ exports.BLOCK_SPECS = BLOCK_SPECS;
532
+ exports.normalizeFlow = normalizeFlow;
533
+ exports.runFlow = runFlow;
@@ -20,6 +20,8 @@ const IPC = {
20
20
  deleteWorkspaceFile: "file:deleteWorkspaceFile",
21
21
  saveMock: "file:saveMock",
22
22
  loadMock: "file:loadMock",
23
+ saveFlow: "file:saveFlow",
24
+ loadFlow: "file:loadFlow",
23
25
  loadHistory: "file:loadHistory",
24
26
  saveHistory: "file:saveHistory"
25
27
  },
@@ -58,6 +60,12 @@ const IPC = {
58
60
  /** Event: per-request progress pushed from main during a run. */
59
61
  progress: "runner:progress"
60
62
  },
63
+ // ─── Flow runner ───────────────────────────────────────────────────────────
64
+ flow: {
65
+ run: "flow:run",
66
+ /** Event: per-block progress pushed from main during a flow run. */
67
+ progress: "flow:progress"
68
+ },
61
69
  // ─── Run results export ────────────────────────────────────────────────────
62
70
  results: {
63
71
  save: "results:save"
@@ -226,6 +234,14 @@ const IPC = {
226
234
  pushDesignContract: "cloud:pushDesignContract",
227
235
  /** Publish a provider OpenAPI spec (PUT /api/provider-contracts). */
228
236
  pushSpec: "cloud:pushSpec",
237
+ /** Upload a self-contained flow definition (POST /api/flows). */
238
+ pushFlow: "cloud:pushFlow",
239
+ /** Trigger a server-side run of an uploaded flow (POST /api/flows/{slug}/run). */
240
+ runFlow: "cloud:runFlow",
241
+ /** Poll a flow run's status/result (GET /api/flows/{slug}/runs/{id}). */
242
+ getFlowRun: "cloud:getFlowRun",
243
+ /** Open a flow's page in the cloud UI. */
244
+ openFlow: "cloud:openFlow",
229
245
  /** Open the cloud deployment matrix in the browser. */
230
246
  openMatrix: "cloud:openMatrix"
231
247
  }
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-C_LCcPwE.js");
5
- require("./handle-DDZ37J8G.js");
4
+ const soapHandler = require("./soap-handler-CkoLJHoI.js");
5
+ require("./handle-BY_jaVBT.js");
6
6
  require("node:https");
7
7
  require("node:http");
8
8
  require("@xmldom/xmldom");