primitive-admin 1.0.0

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.
Files changed (38) hide show
  1. package/README.md +495 -0
  2. package/dist/bin/primitive.js +72 -0
  3. package/dist/bin/primitive.js.map +1 -0
  4. package/dist/src/commands/admins.js +268 -0
  5. package/dist/src/commands/admins.js.map +1 -0
  6. package/dist/src/commands/analytics.js +195 -0
  7. package/dist/src/commands/analytics.js.map +1 -0
  8. package/dist/src/commands/apps.js +238 -0
  9. package/dist/src/commands/apps.js.map +1 -0
  10. package/dist/src/commands/auth.js +178 -0
  11. package/dist/src/commands/auth.js.map +1 -0
  12. package/dist/src/commands/catalog.js +460 -0
  13. package/dist/src/commands/catalog.js.map +1 -0
  14. package/dist/src/commands/integrations.js +438 -0
  15. package/dist/src/commands/integrations.js.map +1 -0
  16. package/dist/src/commands/prompts.js +999 -0
  17. package/dist/src/commands/prompts.js.map +1 -0
  18. package/dist/src/commands/sync.js +598 -0
  19. package/dist/src/commands/sync.js.map +1 -0
  20. package/dist/src/commands/users.js +293 -0
  21. package/dist/src/commands/users.js.map +1 -0
  22. package/dist/src/commands/waitlist.js +176 -0
  23. package/dist/src/commands/waitlist.js.map +1 -0
  24. package/dist/src/commands/workflows.js +876 -0
  25. package/dist/src/commands/workflows.js.map +1 -0
  26. package/dist/src/lib/api-client.js +522 -0
  27. package/dist/src/lib/api-client.js.map +1 -0
  28. package/dist/src/lib/auth-flow.js +306 -0
  29. package/dist/src/lib/auth-flow.js.map +1 -0
  30. package/dist/src/lib/config.js +90 -0
  31. package/dist/src/lib/config.js.map +1 -0
  32. package/dist/src/lib/fetch.js +43 -0
  33. package/dist/src/lib/fetch.js.map +1 -0
  34. package/dist/src/lib/output.js +143 -0
  35. package/dist/src/lib/output.js.map +1 -0
  36. package/dist/src/types/index.js +2 -0
  37. package/dist/src/types/index.js.map +1 -0
  38. package/package.json +47 -0
@@ -0,0 +1,876 @@
1
+ import { readFileSync } from "fs";
2
+ import * as TOML from "@iarna/toml";
3
+ import { ApiClient } from "../lib/api-client.js";
4
+ import { getCurrentAppId } from "../lib/config.js";
5
+ import { success, error, info, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
6
+ function resolveAppId(appId, options) {
7
+ const resolved = appId || options.app || getCurrentAppId();
8
+ if (!resolved) {
9
+ error("No app specified. Use <app-id>, --app, or 'primitive use <app-id>' to set context.");
10
+ process.exit(1);
11
+ }
12
+ return resolved;
13
+ }
14
+ export function registerWorkflowsCommands(program) {
15
+ const workflows = program
16
+ .command("workflows")
17
+ .description("Build multi-step workflows, publish revisions, and monitor runs")
18
+ .addHelpText("after", `
19
+ Examples:
20
+ $ primitive workflows list
21
+ $ primitive workflows create --from-file process-doc.toml
22
+ $ primitive workflows publish 01HXY...
23
+ $ primitive workflows runs list 01HXY...
24
+ `);
25
+ // List workflows
26
+ workflows
27
+ .command("list")
28
+ .description("List workflows")
29
+ .argument("[app-id]", "App ID (uses current app if not specified)")
30
+ .option("--app <app-id>", "App ID")
31
+ .option("--status <status>", "Filter by status: draft, active, archived")
32
+ .option("--json", "Output as JSON")
33
+ .action(async (appId, options) => {
34
+ const resolvedAppId = resolveAppId(appId, options);
35
+ const client = new ApiClient();
36
+ try {
37
+ const { items } = await client.listWorkflows(resolvedAppId, {
38
+ status: options.status,
39
+ });
40
+ if (options.json) {
41
+ json(items);
42
+ return;
43
+ }
44
+ if (!items || items.length === 0) {
45
+ info("No workflows found.");
46
+ return;
47
+ }
48
+ console.log(formatTable(items, [
49
+ { header: "ID", key: "workflowId", format: formatId },
50
+ { header: "KEY", key: "workflowKey" },
51
+ { header: "NAME", key: "name" },
52
+ { header: "STATUS", key: "status", format: formatStatus },
53
+ { header: "MODIFIED", key: "modifiedAt", format: formatDate },
54
+ ]));
55
+ }
56
+ catch (err) {
57
+ error(err.message);
58
+ process.exit(1);
59
+ }
60
+ });
61
+ // Create workflow
62
+ workflows
63
+ .command("create")
64
+ .description("Create a new workflow")
65
+ .argument("[app-id]", "App ID (uses current app if not specified)")
66
+ .option("--app <app-id>", "App ID")
67
+ .option("--key <key>", "Workflow key (unique identifier)")
68
+ .option("--name <name>", "Display name")
69
+ .option("--description <desc>", "Description")
70
+ .option("--from-file <path>", "Load workflow from TOML file")
71
+ .option("--json", "Output as JSON")
72
+ .action(async (appId, options) => {
73
+ const resolvedAppId = resolveAppId(appId, options);
74
+ const client = new ApiClient();
75
+ let payload;
76
+ if (options.fromFile) {
77
+ try {
78
+ const content = readFileSync(options.fromFile, "utf-8");
79
+ const tomlData = TOML.parse(content);
80
+ const workflow = tomlData.workflow || tomlData;
81
+ payload = {
82
+ workflowKey: workflow.key || workflow.workflowKey,
83
+ name: workflow.name,
84
+ description: workflow.description,
85
+ steps: tomlData.steps || [],
86
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
87
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
88
+ perUserMaxRunning: workflow.perUserMaxRunning,
89
+ perUserMaxQueued: workflow.perUserMaxQueued,
90
+ perAppMaxRunning: workflow.perAppMaxRunning,
91
+ perAppMaxQueued: workflow.perAppMaxQueued,
92
+ queueTtlSeconds: workflow.queueTtlSeconds,
93
+ dequeueOrder: workflow.dequeueOrder,
94
+ };
95
+ }
96
+ catch (err) {
97
+ error(`Failed to read TOML file: ${err.message}`);
98
+ process.exit(1);
99
+ }
100
+ }
101
+ else {
102
+ if (!options.key || !options.name) {
103
+ error("Required: --key, --name (or use --from-file)");
104
+ process.exit(1);
105
+ }
106
+ payload = {
107
+ workflowKey: options.key,
108
+ name: options.name,
109
+ description: options.description,
110
+ steps: [],
111
+ };
112
+ }
113
+ try {
114
+ const result = await client.createWorkflow(resolvedAppId, payload);
115
+ if (options.json) {
116
+ json(result);
117
+ return;
118
+ }
119
+ success(`Workflow created: ${result.workflow?.name || options.name}`);
120
+ keyValue("Workflow ID", result.workflow?.workflowId);
121
+ keyValue("Key", result.workflow?.workflowKey);
122
+ }
123
+ catch (err) {
124
+ error(err.message);
125
+ process.exit(1);
126
+ }
127
+ });
128
+ // Get workflow
129
+ workflows
130
+ .command("get")
131
+ .description("Get workflow details")
132
+ .argument("<workflow-id>", "Workflow ID")
133
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
134
+ .option("--json", "Output as JSON")
135
+ .action(async (workflowId, options) => {
136
+ const resolvedAppId = resolveAppId(undefined, options);
137
+ const client = new ApiClient();
138
+ try {
139
+ const result = await client.getWorkflow(resolvedAppId, workflowId);
140
+ if (options.json) {
141
+ json(result);
142
+ return;
143
+ }
144
+ const wf = result.workflow;
145
+ keyValue("Workflow ID", wf.workflowId);
146
+ keyValue("Key", wf.workflowKey);
147
+ keyValue("Name", wf.name);
148
+ keyValue("Description", wf.description);
149
+ keyValue("Status", formatStatus(wf.status));
150
+ keyValue("Latest Revision", wf.latestRevision || "-");
151
+ divider();
152
+ info("Queue Settings:");
153
+ keyValue(" Per User Max Running", wf.perUserMaxRunning);
154
+ keyValue(" Per User Max Queued", wf.perUserMaxQueued);
155
+ keyValue(" Per App Max Running", wf.perAppMaxRunning);
156
+ keyValue(" Dequeue Order", wf.dequeueOrder);
157
+ if (result.draft?.steps) {
158
+ divider();
159
+ info(`Draft Steps (${result.draft.steps.length}):`);
160
+ result.draft.steps.forEach((step, idx) => {
161
+ console.log(` ${idx + 1}. ${step.id} (${step.kind})`);
162
+ });
163
+ }
164
+ if (result.revisions && result.revisions.length > 0) {
165
+ divider();
166
+ info(`Revisions (${result.revisions.length}):`);
167
+ console.log(formatTable(result.revisions.slice(0, 5), [
168
+ { header: "ID", key: "revisionId", format: formatId },
169
+ { header: "PUBLISHED", key: "publishedAt", format: formatDate },
170
+ ]));
171
+ }
172
+ }
173
+ catch (err) {
174
+ error(err.message);
175
+ process.exit(1);
176
+ }
177
+ });
178
+ // Update workflow
179
+ workflows
180
+ .command("update")
181
+ .description("Update workflow metadata")
182
+ .argument("<workflow-id>", "Workflow ID")
183
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
184
+ .option("--name <name>", "Display name")
185
+ .option("--description <desc>", "Description")
186
+ .option("--status <status>", "Status: draft, active, archived")
187
+ .option("--per-user-max-running <n>", "Max running per user")
188
+ .option("--per-user-max-queued <n>", "Max queued per user")
189
+ .option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
190
+ .option("--json", "Output as JSON")
191
+ .action(async (workflowId, options) => {
192
+ const resolvedAppId = resolveAppId(undefined, options);
193
+ const payload = {};
194
+ if (options.name)
195
+ payload.name = options.name;
196
+ if (options.description)
197
+ payload.description = options.description;
198
+ if (options.status)
199
+ payload.status = options.status;
200
+ if (options.perUserMaxRunning)
201
+ payload.perUserMaxRunning = parseInt(options.perUserMaxRunning);
202
+ if (options.perUserMaxQueued)
203
+ payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
204
+ if (options.dequeueOrder)
205
+ payload.dequeueOrder = options.dequeueOrder;
206
+ if (Object.keys(payload).length === 0) {
207
+ error("No update options specified.");
208
+ process.exit(1);
209
+ }
210
+ const client = new ApiClient();
211
+ try {
212
+ const result = await client.updateWorkflow(resolvedAppId, workflowId, payload);
213
+ if (options.json) {
214
+ json(result);
215
+ return;
216
+ }
217
+ success("Workflow updated.");
218
+ }
219
+ catch (err) {
220
+ error(err.message);
221
+ process.exit(1);
222
+ }
223
+ });
224
+ // Delete workflow
225
+ workflows
226
+ .command("delete")
227
+ .description("Delete or archive a workflow")
228
+ .argument("<workflow-id>", "Workflow ID")
229
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
230
+ .option("--hard", "Permanently delete instead of archive")
231
+ .option("-y, --yes", "Skip confirmation prompt")
232
+ .action(async (workflowId, options) => {
233
+ const resolvedAppId = resolveAppId(undefined, options);
234
+ if (!options.yes) {
235
+ const action = options.hard ? "permanently delete" : "archive";
236
+ const inquirer = await import("inquirer");
237
+ const { confirm } = await inquirer.default.prompt([
238
+ {
239
+ type: "confirm",
240
+ name: "confirm",
241
+ message: `Are you sure you want to ${action} workflow ${workflowId}?`,
242
+ default: false,
243
+ },
244
+ ]);
245
+ if (!confirm) {
246
+ info("Cancelled.");
247
+ return;
248
+ }
249
+ }
250
+ const client = new ApiClient();
251
+ try {
252
+ if (options.hard) {
253
+ await client.deleteWorkflow(resolvedAppId, workflowId);
254
+ success("Workflow deleted.");
255
+ }
256
+ else {
257
+ await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
258
+ success("Workflow archived.");
259
+ }
260
+ }
261
+ catch (err) {
262
+ error(err.message);
263
+ process.exit(1);
264
+ }
265
+ });
266
+ // Draft subcommand
267
+ const draft = workflows.command("draft").description("Manage workflow draft");
268
+ // Update draft
269
+ draft
270
+ .command("update")
271
+ .description("Update workflow draft steps")
272
+ .argument("<workflow-id>", "Workflow ID")
273
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
274
+ .option("--from-file <path>", "Load steps from TOML file")
275
+ .option("--json", "Output as JSON")
276
+ .action(async (workflowId, options) => {
277
+ const resolvedAppId = resolveAppId(undefined, options);
278
+ if (!options.fromFile) {
279
+ error("--from-file is required");
280
+ process.exit(1);
281
+ }
282
+ let payload;
283
+ try {
284
+ const content = readFileSync(options.fromFile, "utf-8");
285
+ const tomlData = TOML.parse(content);
286
+ const workflow = tomlData.workflow || tomlData;
287
+ payload = {
288
+ steps: tomlData.steps || [],
289
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
290
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
291
+ };
292
+ }
293
+ catch (err) {
294
+ error(`Failed to read TOML file: ${err.message}`);
295
+ process.exit(1);
296
+ }
297
+ const client = new ApiClient();
298
+ try {
299
+ const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
300
+ if (options.json) {
301
+ json(result);
302
+ return;
303
+ }
304
+ success("Draft updated.");
305
+ keyValue("Steps", result.draft?.steps?.length || 0);
306
+ }
307
+ catch (err) {
308
+ error(err.message);
309
+ process.exit(1);
310
+ }
311
+ });
312
+ // Publish workflow
313
+ workflows
314
+ .command("publish")
315
+ .description("Publish the current draft as a new revision")
316
+ .argument("<workflow-id>", "Workflow ID")
317
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
318
+ .option("--json", "Output as JSON")
319
+ .action(async (workflowId, options) => {
320
+ const resolvedAppId = resolveAppId(undefined, options);
321
+ const client = new ApiClient();
322
+ try {
323
+ const result = await client.publishWorkflow(resolvedAppId, workflowId);
324
+ if (options.json) {
325
+ json(result);
326
+ return;
327
+ }
328
+ success("Workflow published.");
329
+ keyValue("Revision ID", result.revision?.revisionId);
330
+ keyValue("Published At", formatDate(result.revision?.publishedAt));
331
+ }
332
+ catch (err) {
333
+ error(err.message);
334
+ process.exit(1);
335
+ }
336
+ });
337
+ // Preview workflow
338
+ workflows
339
+ .command("preview")
340
+ .description("Run a preview execution of the workflow draft")
341
+ .argument("<workflow-id>", "Workflow ID")
342
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
343
+ .option("--input <json>", "Root input as JSON")
344
+ .option("--wait", "Wait for completion and show result")
345
+ .option("--json", "Output as JSON")
346
+ .action(async (workflowId, options) => {
347
+ const resolvedAppId = resolveAppId(undefined, options);
348
+ let rootInput;
349
+ if (options.input) {
350
+ try {
351
+ rootInput = JSON.parse(options.input);
352
+ }
353
+ catch {
354
+ error("Invalid JSON in --input");
355
+ process.exit(1);
356
+ }
357
+ }
358
+ const client = new ApiClient();
359
+ try {
360
+ const result = await client.previewWorkflow(resolvedAppId, workflowId, {
361
+ rootInput,
362
+ });
363
+ if (!options.wait) {
364
+ if (options.json) {
365
+ json(result);
366
+ return;
367
+ }
368
+ success("Preview started.");
369
+ keyValue("Instance ID", result.instanceId);
370
+ info("Use 'workflows runs status' to check progress.");
371
+ return;
372
+ }
373
+ // Poll for completion
374
+ info("Waiting for completion...");
375
+ let status;
376
+ const maxAttempts = 60;
377
+ for (let i = 0; i < maxAttempts; i++) {
378
+ await new Promise((r) => setTimeout(r, 1000));
379
+ const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
380
+ status = statusResult.status;
381
+ if (status?.status === "completed" || status?.status === "failed") {
382
+ break;
383
+ }
384
+ }
385
+ if (options.json) {
386
+ json({ instanceId: result.instanceId, status });
387
+ return;
388
+ }
389
+ if (status?.status === "completed") {
390
+ success("Preview completed.");
391
+ if (status.output) {
392
+ console.log("\nOutput:");
393
+ console.log(JSON.stringify(status.output, null, 2));
394
+ }
395
+ if (status.stepResults) {
396
+ divider();
397
+ info("Step Results:");
398
+ status.stepResults.forEach((step) => {
399
+ console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
400
+ });
401
+ }
402
+ }
403
+ else if (status?.status === "failed") {
404
+ error("Preview failed.");
405
+ if (status.error) {
406
+ console.log("\nError:");
407
+ console.log(JSON.stringify(status.error, null, 2));
408
+ }
409
+ }
410
+ else {
411
+ info("Preview still running. Check status with 'workflows runs status'.");
412
+ }
413
+ }
414
+ catch (err) {
415
+ error(err.message);
416
+ process.exit(1);
417
+ }
418
+ });
419
+ // Runs subcommand
420
+ const runs = workflows.command("runs").description("Manage workflow runs");
421
+ // List runs
422
+ runs
423
+ .command("list")
424
+ .description("List workflow runs")
425
+ .argument("<workflow-id>", "Workflow ID")
426
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
427
+ .option("--status <status>", "Filter by status: pending, running, completed, failed")
428
+ .option("--limit <n>", "Number of runs to show", "20")
429
+ .option("--json", "Output as JSON")
430
+ .action(async (workflowId, options) => {
431
+ const resolvedAppId = resolveAppId(undefined, options);
432
+ const client = new ApiClient();
433
+ try {
434
+ const { items } = await client.listWorkflowRuns(resolvedAppId, workflowId, {
435
+ status: options.status,
436
+ limit: parseInt(options.limit),
437
+ });
438
+ if (options.json) {
439
+ json(items);
440
+ return;
441
+ }
442
+ if (!items || items.length === 0) {
443
+ info("No runs found.");
444
+ return;
445
+ }
446
+ console.log(formatTable(items, [
447
+ { header: "RUN ID", key: "runId", format: formatId },
448
+ { header: "STATUS", key: "status", format: formatStatus },
449
+ { header: "STARTED", key: "startedAt", format: formatDate },
450
+ { header: "ENDED", key: "endedAt", format: formatDate },
451
+ { header: "PREVIEW", key: "isPreview", format: (v) => v ? "yes" : "" },
452
+ ]));
453
+ }
454
+ catch (err) {
455
+ error(err.message);
456
+ process.exit(1);
457
+ }
458
+ });
459
+ // Run status
460
+ runs
461
+ .command("status")
462
+ .description("Get status of a workflow run")
463
+ .argument("<workflow-id>", "Workflow ID")
464
+ .argument("<run-id>", "Run ID")
465
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
466
+ .option("--json", "Output as JSON")
467
+ .action(async (workflowId, runId, options) => {
468
+ const resolvedAppId = resolveAppId(undefined, options);
469
+ const client = new ApiClient();
470
+ try {
471
+ const result = await client.getWorkflowRunStatus(resolvedAppId, workflowId, runId);
472
+ if (options.json) {
473
+ json(result);
474
+ return;
475
+ }
476
+ const run = result.run;
477
+ keyValue("Run ID", run.runId);
478
+ keyValue("Status", formatStatus(run.status));
479
+ keyValue("Started", formatDate(run.startedAt));
480
+ keyValue("Ended", formatDate(run.endedAt));
481
+ keyValue("Preview", run.isPreview ? "yes" : "no");
482
+ if (result.instanceStatus) {
483
+ const status = result.instanceStatus;
484
+ if (status.output) {
485
+ divider();
486
+ info("Output:");
487
+ console.log(JSON.stringify(status.output, null, 2));
488
+ }
489
+ if (status.stepResults && status.stepResults.length > 0) {
490
+ divider();
491
+ info("Step Results:");
492
+ status.stepResults.forEach((step) => {
493
+ console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
494
+ });
495
+ }
496
+ if (status.error) {
497
+ divider();
498
+ error("Error:");
499
+ console.log(JSON.stringify(status.error, null, 2));
500
+ }
501
+ }
502
+ }
503
+ catch (err) {
504
+ error(err.message);
505
+ process.exit(1);
506
+ }
507
+ });
508
+ // ============================================
509
+ // TESTS SUBCOMMAND
510
+ // ============================================
511
+ const tests = workflows
512
+ .command("tests")
513
+ .description("Manage and run workflow test cases")
514
+ .addHelpText("after", `
515
+ Examples:
516
+ $ primitive workflows tests list <workflow-id>
517
+ $ primitive workflows tests create <workflow-id> --name "Basic test" --vars '{"input":"hello"}'
518
+ $ primitive workflows tests run <workflow-id> <test-case-id>
519
+ $ primitive workflows tests run-all <workflow-id>
520
+ `);
521
+ // List test cases
522
+ tests
523
+ .command("list")
524
+ .description("List test cases for a workflow")
525
+ .argument("<workflow-id>", "Workflow ID")
526
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
527
+ .option("--json", "Output as JSON")
528
+ .action(async (workflowId, options) => {
529
+ const resolvedAppId = resolveAppId(undefined, options);
530
+ const client = new ApiClient();
531
+ try {
532
+ const { items } = await client.listTestCases(resolvedAppId, "workflow", workflowId);
533
+ if (options.json) {
534
+ json(items);
535
+ return;
536
+ }
537
+ if (!items || items.length === 0) {
538
+ info("No test cases found.");
539
+ return;
540
+ }
541
+ console.log(formatTable(items, [
542
+ { header: "ID", key: "testCaseId", format: formatId },
543
+ { header: "NAME", key: "name" },
544
+ { header: "CREATED", key: "createdAt", format: formatDate },
545
+ ]));
546
+ }
547
+ catch (err) {
548
+ error(err.message);
549
+ process.exit(1);
550
+ }
551
+ });
552
+ // Create test case
553
+ tests
554
+ .command("create")
555
+ .description("Create a test case for a workflow")
556
+ .argument("<workflow-id>", "Workflow ID")
557
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
558
+ .option("--name <name>", "Test case name (required)")
559
+ .option("--vars <json>", "Input variables as JSON (required)")
560
+ .option("--pattern <regex>", "Expected output pattern (regex)")
561
+ .option("--contains <json>", "Expected strings to contain (JSON array)")
562
+ .option("--json-subset <json>", "Expected JSON subset to match")
563
+ .option("--json", "Output as JSON")
564
+ .action(async (workflowId, options) => {
565
+ const resolvedAppId = resolveAppId(undefined, options);
566
+ if (!options.name || !options.vars) {
567
+ error("Required: --name and --vars");
568
+ process.exit(1);
569
+ }
570
+ let inputVariables;
571
+ try {
572
+ inputVariables = JSON.parse(options.vars);
573
+ }
574
+ catch {
575
+ error("Invalid JSON in --vars");
576
+ process.exit(1);
577
+ }
578
+ let expectedOutputContains;
579
+ if (options.contains) {
580
+ try {
581
+ expectedOutputContains = JSON.parse(options.contains);
582
+ }
583
+ catch {
584
+ error("Invalid JSON in --contains");
585
+ process.exit(1);
586
+ }
587
+ }
588
+ let expectedJsonSubset;
589
+ if (options.jsonSubset) {
590
+ try {
591
+ expectedJsonSubset = JSON.parse(options.jsonSubset);
592
+ }
593
+ catch {
594
+ error("Invalid JSON in --json-subset");
595
+ process.exit(1);
596
+ }
597
+ }
598
+ const client = new ApiClient();
599
+ try {
600
+ const result = await client.createTestCase(resolvedAppId, "workflow", workflowId, {
601
+ name: options.name,
602
+ inputVariables,
603
+ expectedOutputPattern: options.pattern,
604
+ expectedOutputContains,
605
+ expectedJsonSubset,
606
+ });
607
+ if (options.json) {
608
+ json(result);
609
+ return;
610
+ }
611
+ success(`Test case created: ${result.name}`);
612
+ keyValue("Test Case ID", result.testCaseId);
613
+ }
614
+ catch (err) {
615
+ error(err.message);
616
+ process.exit(1);
617
+ }
618
+ });
619
+ // Get test case
620
+ tests
621
+ .command("get")
622
+ .description("Get test case details")
623
+ .argument("<workflow-id>", "Workflow ID")
624
+ .argument("<test-case-id>", "Test Case ID")
625
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
626
+ .option("--json", "Output as JSON")
627
+ .action(async (workflowId, testCaseId, options) => {
628
+ const resolvedAppId = resolveAppId(undefined, options);
629
+ const client = new ApiClient();
630
+ try {
631
+ const result = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
632
+ if (options.json) {
633
+ json(result);
634
+ return;
635
+ }
636
+ keyValue("Test Case ID", result.testCaseId);
637
+ keyValue("Name", result.name);
638
+ keyValue("Created", formatDate(result.createdAt));
639
+ divider();
640
+ info("Input Variables:");
641
+ try {
642
+ const vars = JSON.parse(result.inputVariables || "{}");
643
+ console.log(JSON.stringify(vars, null, 2));
644
+ }
645
+ catch {
646
+ console.log(result.inputVariables || "{}");
647
+ }
648
+ if (result.expectedOutputPattern) {
649
+ divider();
650
+ keyValue("Expected Pattern", result.expectedOutputPattern);
651
+ }
652
+ if (result.expectedOutputContains) {
653
+ divider();
654
+ info("Expected Contains:");
655
+ try {
656
+ console.log(JSON.stringify(JSON.parse(result.expectedOutputContains), null, 2));
657
+ }
658
+ catch {
659
+ console.log(result.expectedOutputContains);
660
+ }
661
+ }
662
+ if (result.expectedJsonSubset) {
663
+ divider();
664
+ info("Expected JSON Subset:");
665
+ try {
666
+ console.log(JSON.stringify(JSON.parse(result.expectedJsonSubset), null, 2));
667
+ }
668
+ catch {
669
+ console.log(result.expectedJsonSubset);
670
+ }
671
+ }
672
+ }
673
+ catch (err) {
674
+ error(err.message);
675
+ process.exit(1);
676
+ }
677
+ });
678
+ // Delete test case
679
+ tests
680
+ .command("delete")
681
+ .description("Delete a test case")
682
+ .argument("<workflow-id>", "Workflow ID")
683
+ .argument("<test-case-id>", "Test Case ID")
684
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
685
+ .option("-y, --yes", "Skip confirmation prompt")
686
+ .action(async (workflowId, testCaseId, options) => {
687
+ const resolvedAppId = resolveAppId(undefined, options);
688
+ if (!options.yes) {
689
+ const inquirer = await import("inquirer");
690
+ const { confirm } = await inquirer.default.prompt([
691
+ {
692
+ type: "confirm",
693
+ name: "confirm",
694
+ message: `Are you sure you want to delete test case ${testCaseId}?`,
695
+ default: false,
696
+ },
697
+ ]);
698
+ if (!confirm) {
699
+ info("Cancelled.");
700
+ return;
701
+ }
702
+ }
703
+ const client = new ApiClient();
704
+ try {
705
+ await client.deleteTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
706
+ success("Test case deleted.");
707
+ }
708
+ catch (err) {
709
+ error(err.message);
710
+ process.exit(1);
711
+ }
712
+ });
713
+ // Run a single test case
714
+ tests
715
+ .command("run")
716
+ .description("Run a single test case")
717
+ .argument("<workflow-id>", "Workflow ID")
718
+ .argument("<test-case-id>", "Test Case ID")
719
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
720
+ .option("--json", "Output as JSON")
721
+ .action(async (workflowId, testCaseId, options) => {
722
+ const resolvedAppId = resolveAppId(undefined, options);
723
+ const client = new ApiClient();
724
+ try {
725
+ // Get test case to retrieve its variables
726
+ const testCase = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
727
+ let variables = {};
728
+ try {
729
+ variables = JSON.parse(testCase.inputVariables || "{}");
730
+ }
731
+ catch {
732
+ // Use empty object if parsing fails
733
+ }
734
+ const result = await client.executeBlockTest(resolvedAppId, "workflow", workflowId, {
735
+ variables,
736
+ testCaseId,
737
+ });
738
+ if (options.json) {
739
+ json(result);
740
+ return;
741
+ }
742
+ const passed = result.verification?.passed;
743
+ if (passed) {
744
+ success("Test PASSED");
745
+ }
746
+ else {
747
+ error("Test FAILED");
748
+ }
749
+ keyValue("Duration", formatDuration(result.metrics?.durationMs));
750
+ if (result.verification) {
751
+ divider();
752
+ info("Verification:");
753
+ const v = result.verification;
754
+ keyValue(" Passed", v.passed ? "Yes" : "No");
755
+ keyValue(" Checks", `${v.summary?.passed || 0}/${v.summary?.total || 0} passed`);
756
+ if (v.checks && v.checks.length > 0) {
757
+ console.log();
758
+ for (const check of v.checks) {
759
+ const icon = check.passed ? "✓" : "✗";
760
+ console.log(` ${icon} ${check.name}`);
761
+ if (!check.passed && check.message) {
762
+ console.log(` ${check.message}`);
763
+ }
764
+ }
765
+ }
766
+ }
767
+ if (result.output) {
768
+ divider();
769
+ info("Output:");
770
+ console.log(typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2));
771
+ }
772
+ if (result.error) {
773
+ divider();
774
+ error(`Error: ${result.error}`);
775
+ }
776
+ }
777
+ catch (err) {
778
+ error(err.message);
779
+ process.exit(1);
780
+ }
781
+ });
782
+ // Run all test cases
783
+ tests
784
+ .command("run-all")
785
+ .description("Run all test cases for a workflow")
786
+ .argument("<workflow-id>", "Workflow ID")
787
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
788
+ .option("--json", "Output as JSON")
789
+ .action(async (workflowId, options) => {
790
+ const resolvedAppId = resolveAppId(undefined, options);
791
+ const client = new ApiClient();
792
+ try {
793
+ if (!options.json) {
794
+ info("Running all test cases...");
795
+ }
796
+ const result = await client.runAllTestCases(resolvedAppId, "workflow", workflowId, {});
797
+ if (options.json) {
798
+ json(result);
799
+ return;
800
+ }
801
+ const { summary, results, comparisonGroup } = result;
802
+ divider();
803
+ keyValue("Comparison Group", comparisonGroup);
804
+ keyValue("Total Tests", summary.total);
805
+ keyValue("Passed", summary.passed);
806
+ keyValue("Failed", summary.failed);
807
+ divider();
808
+ if (results && results.length > 0) {
809
+ for (const r of results) {
810
+ const passed = r.verification?.passed;
811
+ const icon = passed ? "✓" : "✗";
812
+ const status = passed ? "PASSED" : "FAILED";
813
+ console.log(`${icon} ${r.testCaseName} - ${status}`);
814
+ if (!passed && r.verification?.details?.reason) {
815
+ console.log(` ${r.verification.details.reason}`);
816
+ }
817
+ }
818
+ }
819
+ // Exit with error code if any tests failed
820
+ if (summary.failed > 0) {
821
+ process.exit(1);
822
+ }
823
+ }
824
+ catch (err) {
825
+ error(err.message);
826
+ process.exit(1);
827
+ }
828
+ });
829
+ // List test runs
830
+ tests
831
+ .command("runs")
832
+ .description("List recent test runs for a workflow")
833
+ .argument("<workflow-id>", "Workflow ID")
834
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
835
+ .option("--limit <n>", "Maximum number of runs to show", "20")
836
+ .option("--group <group>", "Filter by comparison group")
837
+ .option("--json", "Output as JSON")
838
+ .action(async (workflowId, options) => {
839
+ const resolvedAppId = resolveAppId(undefined, options);
840
+ const client = new ApiClient();
841
+ try {
842
+ const { items } = await client.listTestRuns(resolvedAppId, "workflow", workflowId, {
843
+ limit: parseInt(options.limit),
844
+ comparisonGroup: options.group,
845
+ });
846
+ if (options.json) {
847
+ json(items);
848
+ return;
849
+ }
850
+ if (!items || items.length === 0) {
851
+ info("No test runs found.");
852
+ return;
853
+ }
854
+ console.log(formatTable(items, [
855
+ { header: "RUN ID", key: "runId", format: formatId },
856
+ { header: "TEST CASE ID", key: "testCaseId", format: (v) => v ? formatId(v) : "-" },
857
+ {
858
+ header: "PASSED",
859
+ key: "verificationPassed",
860
+ format: (v) => v === true
861
+ ? "Yes"
862
+ : v === false
863
+ ? "No"
864
+ : "-",
865
+ },
866
+ { header: "DURATION", key: "durationMs", format: formatDuration },
867
+ { header: "STARTED", key: "startedAt", format: formatDate },
868
+ ]));
869
+ }
870
+ catch (err) {
871
+ error(err.message);
872
+ process.exit(1);
873
+ }
874
+ });
875
+ }
876
+ //# sourceMappingURL=workflows.js.map