primitive-admin 1.1.0-alpha.3 → 1.1.0-alpha.31

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 (99) hide show
  1. package/README.md +168 -21
  2. package/assets/skill/skills/primitive-platform/SKILL.md +226 -0
  3. package/dist/bin/primitive.js +198 -15
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +107 -0
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +464 -55
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +27 -13
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/auth.js +165 -5
  12. package/dist/src/commands/auth.js.map +1 -1
  13. package/dist/src/commands/blob-buckets.js +354 -0
  14. package/dist/src/commands/blob-buckets.js.map +1 -0
  15. package/dist/src/commands/catalog.js +17 -17
  16. package/dist/src/commands/catalog.js.map +1 -1
  17. package/dist/src/commands/collection-type-configs.js +178 -0
  18. package/dist/src/commands/collection-type-configs.js.map +1 -0
  19. package/dist/src/commands/collections.js +526 -0
  20. package/dist/src/commands/collections.js.map +1 -0
  21. package/dist/src/commands/comparisons.js +6 -6
  22. package/dist/src/commands/comparisons.js.map +1 -1
  23. package/dist/src/commands/cron-triggers.js +364 -0
  24. package/dist/src/commands/cron-triggers.js.map +1 -0
  25. package/dist/src/commands/database-types.js +462 -0
  26. package/dist/src/commands/database-types.js.map +1 -0
  27. package/dist/src/commands/databases.js +1067 -58
  28. package/dist/src/commands/databases.js.map +1 -1
  29. package/dist/src/commands/documents.js +510 -2
  30. package/dist/src/commands/documents.js.map +1 -1
  31. package/dist/src/commands/email-templates.js +281 -0
  32. package/dist/src/commands/email-templates.js.map +1 -0
  33. package/dist/src/commands/env.js +260 -0
  34. package/dist/src/commands/env.js.map +1 -0
  35. package/dist/src/commands/group-type-configs.js +189 -0
  36. package/dist/src/commands/group-type-configs.js.map +1 -0
  37. package/dist/src/commands/groups.js +28 -25
  38. package/dist/src/commands/groups.js.map +1 -1
  39. package/dist/src/commands/init.js +719 -188
  40. package/dist/src/commands/init.js.map +1 -1
  41. package/dist/src/commands/integrations.js +504 -33
  42. package/dist/src/commands/integrations.js.map +1 -1
  43. package/dist/src/commands/prompts.js +17 -16
  44. package/dist/src/commands/prompts.js.map +1 -1
  45. package/dist/src/commands/rule-sets.js +366 -0
  46. package/dist/src/commands/rule-sets.js.map +1 -0
  47. package/dist/src/commands/secrets.js +108 -0
  48. package/dist/src/commands/secrets.js.map +1 -0
  49. package/dist/src/commands/skill.js +29 -0
  50. package/dist/src/commands/skill.js.map +1 -0
  51. package/dist/src/commands/sync.js +2363 -182
  52. package/dist/src/commands/sync.js.map +1 -1
  53. package/dist/src/commands/tokens.js +9 -9
  54. package/dist/src/commands/tokens.js.map +1 -1
  55. package/dist/src/commands/users.js +426 -4
  56. package/dist/src/commands/users.js.map +1 -1
  57. package/dist/src/commands/webhooks.js +386 -0
  58. package/dist/src/commands/webhooks.js.map +1 -0
  59. package/dist/src/commands/workflows.js +614 -64
  60. package/dist/src/commands/workflows.js.map +1 -1
  61. package/dist/src/lib/api-client.js +942 -83
  62. package/dist/src/lib/api-client.js.map +1 -1
  63. package/dist/src/lib/config.js +51 -53
  64. package/dist/src/lib/config.js.map +1 -1
  65. package/dist/src/lib/constants.js +3 -0
  66. package/dist/src/lib/constants.js.map +1 -0
  67. package/dist/src/lib/credentials-store.js +307 -0
  68. package/dist/src/lib/credentials-store.js.map +1 -0
  69. package/dist/src/lib/env-resolver.js +121 -0
  70. package/dist/src/lib/env-resolver.js.map +1 -0
  71. package/dist/src/lib/init-config.js +87 -0
  72. package/dist/src/lib/init-config.js.map +1 -0
  73. package/dist/src/lib/migration-nag.js +163 -0
  74. package/dist/src/lib/migration-nag.js.map +1 -0
  75. package/dist/src/lib/output.js +58 -6
  76. package/dist/src/lib/output.js.map +1 -1
  77. package/dist/src/lib/paginate.js +42 -0
  78. package/dist/src/lib/paginate.js.map +1 -0
  79. package/dist/src/lib/project-config.js +209 -0
  80. package/dist/src/lib/project-config.js.map +1 -0
  81. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  82. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  83. package/dist/src/lib/skill-installer.js +135 -0
  84. package/dist/src/lib/skill-installer.js.map +1 -0
  85. package/dist/src/lib/sync-paths.js +102 -0
  86. package/dist/src/lib/sync-paths.js.map +1 -0
  87. package/dist/src/lib/template.js +279 -18
  88. package/dist/src/lib/template.js.map +1 -1
  89. package/dist/src/lib/toml-database-config.js +384 -0
  90. package/dist/src/lib/toml-database-config.js.map +1 -0
  91. package/dist/src/lib/toml-params-validator.js +183 -0
  92. package/dist/src/lib/toml-params-validator.js.map +1 -0
  93. package/dist/src/lib/version-check.js +172 -0
  94. package/dist/src/lib/version-check.js.map +1 -0
  95. package/dist/src/lib/workflow-fragments.js +121 -0
  96. package/dist/src/lib/workflow-fragments.js.map +1 -0
  97. package/dist/src/lib/workflow-toml-validator.js +328 -0
  98. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  99. package/package.json +7 -4
@@ -1,8 +1,9 @@
1
1
  import { readFileSync } from "fs";
2
2
  import * as TOML from "@iarna/toml";
3
+ import chalk from "chalk";
3
4
  import { ApiClient } from "../lib/api-client.js";
4
5
  import { resolveAppId } from "../lib/config.js";
5
- import { success, error, info, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
6
+ import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
6
7
  export function registerIntegrationsCommands(program) {
7
8
  const integrations = program
8
9
  .command("integrations")
@@ -134,24 +135,24 @@ Examples:
134
135
  json(result);
135
136
  return;
136
137
  }
137
- keyValue("Integration ID", result.integrationId);
138
- keyValue("Key", result.integrationKey);
139
- keyValue("Name", result.displayName);
140
- keyValue("Description", result.description);
141
- keyValue("Status", formatStatus(result.status));
142
- keyValue("Timeout", result.timeoutMs ? `${result.timeoutMs}ms` : "-");
138
+ printResult("Integration ID", result.integrationId);
139
+ printResult("Key", result.integrationKey);
140
+ printResult("Name", result.displayName);
141
+ printResult("Description", result.description);
142
+ printResult("Status", formatStatus(result.status));
143
+ printResult("Timeout", result.timeoutMs ? `${result.timeoutMs}ms` : "-");
143
144
  if (result.requestConfig) {
144
145
  divider();
145
146
  info("Request Config:");
146
- keyValue(" Base URL", result.requestConfig.baseUrl);
147
- keyValue(" Allowed Methods", result.requestConfig.allowedMethods?.join(", ") || "all");
148
- keyValue(" Allowed Paths", result.requestConfig.allowedPaths?.join(", ") || "all");
149
- keyValue(" Response Passthrough", result.requestConfig.responsePassthrough);
147
+ printResult(" Base URL", result.requestConfig.baseUrl);
148
+ printResult(" Allowed Methods", result.requestConfig.allowedMethods?.join(", ") || "all");
149
+ printResult(" Allowed Paths", result.requestConfig.allowedPaths?.join(", ") || "all");
150
+ printResult(" Response Passthrough", result.requestConfig.responsePassthrough);
150
151
  if (result.requestConfig.bodyMode) {
151
- keyValue(" Body Mode", result.requestConfig.bodyMode);
152
+ printResult(" Body Mode", result.requestConfig.bodyMode);
152
153
  }
153
154
  if (result.requestConfig.multipartFieldMapping?.length) {
154
- keyValue(" Multipart Fields", result.requestConfig.multipartFieldMapping
155
+ printResult(" Multipart Fields", result.requestConfig.multipartFieldMapping
155
156
  .map((f) => `${f.fieldName} (${f.type}${f.attachmentIndex != null ? `, attachment[${f.attachmentIndex}]` : ""})`)
156
157
  .join(", "));
157
158
  }
@@ -304,17 +305,24 @@ Examples:
304
305
  // List integration logs
305
306
  integrations
306
307
  .command("logs")
307
- .description("View integration request logs")
308
+ .description("View integration request logs (includes workflow-initiated calls; filter with --source workflow|user|admin|test)")
308
309
  .argument("<integration-id>", "Integration ID")
309
310
  .option("--app <app-id>", "App ID (uses current app if not specified)")
310
311
  .option("--limit <n>", "Number of logs to show", "20")
312
+ .option("--source <source>", "Filter by call source: user, admin, test, or workflow")
311
313
  .option("--json", "Output as JSON")
312
314
  .action(async (integrationId, options) => {
313
315
  const resolvedAppId = resolveAppId(undefined, options);
314
316
  const client = new ApiClient();
315
317
  try {
318
+ const validSources = new Set(["user", "admin", "test", "workflow"]);
319
+ if (options.source && !validSources.has(options.source)) {
320
+ error(`Invalid --source value '${options.source}'. Must be one of: user, admin, test, workflow.`);
321
+ process.exit(1);
322
+ }
316
323
  const logs = await client.listIntegrationLogs(resolvedAppId, integrationId, {
317
324
  limit: parseInt(options.limit),
325
+ ...(options.source && { source: options.source }),
318
326
  });
319
327
  if (options.json) {
320
328
  json(logs);
@@ -326,10 +334,13 @@ Examples:
326
334
  }
327
335
  console.log(formatTable(logs, [
328
336
  { header: "TIME", key: "timestamp", format: formatDate },
337
+ { header: "SOURCE", key: "source", format: (v) => v || "user" },
329
338
  { header: "METHOD", key: "method" },
330
339
  { header: "PATH", key: "path" },
331
340
  { header: "STATUS", key: "status" },
332
341
  { header: "DURATION", key: "durationMs", format: formatDuration },
342
+ { header: "WORKFLOW", key: "workflowKey", format: (v) => v || "" },
343
+ { header: "RUN", key: "runId", format: (v) => (v ? formatId(v) : "") },
333
344
  { header: "TRACE", key: "traceId", format: formatId },
334
345
  ]));
335
346
  }
@@ -351,6 +362,7 @@ Examples:
351
362
  const resolvedAppId = resolveAppId(undefined, options);
352
363
  const client = new ApiClient();
353
364
  try {
365
+ warn("Integration secrets are deprecated. Use App Secrets with {{secrets.KEY}} templates instead.");
354
366
  const secretsList = await client.listIntegrationSecrets(resolvedAppId, integrationId);
355
367
  if (options.json) {
356
368
  json(secretsList);
@@ -382,52 +394,511 @@ Examples:
382
394
  .option("--data <json>", "Secret data as JSON (e.g., '{\"apiKey\":\"...\"}' )")
383
395
  .option("--summary <text>", "Description of the secret")
384
396
  .option("--json", "Output as JSON")
397
+ .action(async () => {
398
+ error("Integration secrets are deprecated and can no longer be created.");
399
+ info("Use App Secrets with {{secrets.KEY}} templates in defaultHeaders or staticQuery instead.");
400
+ info("See: primitive secrets add <key> --value <value>");
401
+ process.exit(1);
402
+ });
403
+ // Archive secret
404
+ secrets
405
+ .command("archive")
406
+ .description("Archive a secret")
407
+ .argument("<integration-id>", "Integration ID")
408
+ .argument("<secret-id>", "Secret ID")
409
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
410
+ .action(async (integrationId, secretId, options) => {
411
+ const resolvedAppId = resolveAppId(undefined, options);
412
+ const client = new ApiClient();
413
+ try {
414
+ await client.archiveIntegrationSecret(resolvedAppId, integrationId, secretId);
415
+ success("Secret archived.");
416
+ }
417
+ catch (err) {
418
+ error(err.message);
419
+ process.exit(1);
420
+ }
421
+ });
422
+ // ============================================
423
+ // TESTS SUBCOMMAND
424
+ // ============================================
425
+ const tests = integrations
426
+ .command("tests")
427
+ .description("Manage and run integration test cases")
428
+ .addHelpText("after", `
429
+ Examples:
430
+ $ primitive integrations tests list <integration-id>
431
+ $ primitive integrations tests create <integration-id> --name "Basic test" --input '{"method":"GET","path":"/get"}'
432
+ $ primitive integrations tests run <integration-id> <test-case-id>
433
+ $ primitive integrations tests run-all <integration-id>
434
+ $ primitive integrations tests run-all <integration-id> --test-cases 01ABC,01DEF,01GHI
435
+ `);
436
+ // List test cases
437
+ tests
438
+ .command("list")
439
+ .description("List test cases for an integration")
440
+ .argument("<integration-id>", "Integration ID")
441
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
442
+ .option("--json", "Output as JSON")
443
+ .action(async (integrationId, options) => {
444
+ const resolvedAppId = resolveAppId(undefined, options);
445
+ const client = new ApiClient();
446
+ try {
447
+ const { items } = await client.listTestCases(resolvedAppId, "integration", integrationId);
448
+ if (options.json) {
449
+ json(items);
450
+ return;
451
+ }
452
+ if (!items || items.length === 0) {
453
+ info("No test cases found.");
454
+ return;
455
+ }
456
+ console.log(formatTable(items, [
457
+ { header: "ID", key: "testCaseId", format: formatId },
458
+ { header: "NAME", key: "name" },
459
+ { header: "CONFIG", key: "configId", format: (v) => v ? formatId(v) : "-" },
460
+ { header: "CREATED", key: "createdAt", format: formatDate },
461
+ ]));
462
+ }
463
+ catch (err) {
464
+ error(err.message);
465
+ process.exit(1);
466
+ }
467
+ });
468
+ // Create test case
469
+ tests
470
+ .command("create")
471
+ .description("Create a test case for an integration")
472
+ .argument("<integration-id>", "Integration ID")
473
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
474
+ .option("--name <name>", "Test case name (required)")
475
+ .option("--input <json>", "Input variables as JSON (required)")
476
+ .option("--pattern <regex>", "Expected output pattern (regex)")
477
+ .option("--contains <json>", "Expected strings to contain (JSON array)")
478
+ .option("--json-subset <json>", "Expected JSON subset to match")
479
+ .option("--config <config-id>", "Config ID to use for this test")
480
+ .option("--evaluator-prompt <prompt-id>", "Evaluator prompt ID")
481
+ .option("--evaluator-config <config-id>", "Evaluator config ID")
482
+ .option("--json", "Output as JSON")
385
483
  .action(async (integrationId, options) => {
386
484
  const resolvedAppId = resolveAppId(undefined, options);
387
- if (!options.data) {
388
- error("--data is required (JSON object with secret values)");
485
+ if (!options.name || !options.input) {
486
+ error("Required: --name and --input");
389
487
  process.exit(1);
390
488
  }
391
- let secretData;
489
+ let inputVariables;
392
490
  try {
393
- secretData = JSON.parse(options.data);
491
+ inputVariables = JSON.parse(options.input);
394
492
  }
395
493
  catch {
396
- error("Invalid JSON in --data");
494
+ error("Invalid JSON in --input");
397
495
  process.exit(1);
398
496
  }
497
+ let expectedOutputContains;
498
+ if (options.contains) {
499
+ try {
500
+ expectedOutputContains = JSON.parse(options.contains);
501
+ }
502
+ catch {
503
+ error("Invalid JSON in --contains");
504
+ process.exit(1);
505
+ }
506
+ }
507
+ let expectedJsonSubset;
508
+ if (options.jsonSubset) {
509
+ try {
510
+ expectedJsonSubset = JSON.parse(options.jsonSubset);
511
+ }
512
+ catch {
513
+ error("Invalid JSON in --json-subset");
514
+ process.exit(1);
515
+ }
516
+ }
399
517
  const client = new ApiClient();
400
518
  try {
401
- const result = await client.addIntegrationSecret(resolvedAppId, integrationId, {
402
- secretData,
403
- secretSummary: options.summary,
519
+ const result = await client.createTestCase(resolvedAppId, "integration", integrationId, {
520
+ name: options.name,
521
+ inputVariables,
522
+ expectedOutputPattern: options.pattern,
523
+ expectedOutputContains,
524
+ expectedJsonSubset,
525
+ configId: options.config,
526
+ evaluatorPromptId: options.evaluatorPrompt,
527
+ evaluatorConfigId: options.evaluatorConfig,
404
528
  });
405
529
  if (options.json) {
406
530
  json(result);
407
531
  return;
408
532
  }
409
- success("Secret added.");
410
- keyValue("Secret ID", result?.secretId);
411
- keyValue("Version", result?.version);
533
+ success(`Test case created: ${result.name}`);
534
+ keyValue("Test Case ID", result.testCaseId);
412
535
  }
413
536
  catch (err) {
414
537
  error(err.message);
415
538
  process.exit(1);
416
539
  }
417
540
  });
418
- // Archive secret
419
- secrets
420
- .command("archive")
421
- .description("Archive a secret")
541
+ // Get test case
542
+ tests
543
+ .command("get")
544
+ .description("Get test case details")
422
545
  .argument("<integration-id>", "Integration ID")
423
- .argument("<secret-id>", "Secret ID")
546
+ .argument("<test-case-id>", "Test Case ID")
424
547
  .option("--app <app-id>", "App ID (uses current app if not specified)")
425
- .action(async (integrationId, secretId, options) => {
548
+ .option("--json", "Output as JSON")
549
+ .action(async (integrationId, testCaseId, options) => {
426
550
  const resolvedAppId = resolveAppId(undefined, options);
427
551
  const client = new ApiClient();
428
552
  try {
429
- await client.archiveIntegrationSecret(resolvedAppId, integrationId, secretId);
430
- success("Secret archived.");
553
+ const result = await client.getTestCase(resolvedAppId, "integration", integrationId, testCaseId);
554
+ if (options.json) {
555
+ json(result);
556
+ return;
557
+ }
558
+ printResult("Test Case ID", result.testCaseId);
559
+ printResult("Name", result.name);
560
+ printResult("Config ID", result.configId || "-");
561
+ printResult("Created", formatDate(result.createdAt));
562
+ divider();
563
+ info("Input Variables:");
564
+ try {
565
+ const vars = JSON.parse(result.inputVariables || "{}");
566
+ console.log(JSON.stringify(vars, null, 2));
567
+ }
568
+ catch {
569
+ console.log(result.inputVariables || "{}");
570
+ }
571
+ if (result.expectedOutputPattern) {
572
+ divider();
573
+ printResult("Expected Pattern", result.expectedOutputPattern);
574
+ }
575
+ if (result.expectedOutputContains) {
576
+ divider();
577
+ info("Expected Contains:");
578
+ try {
579
+ console.log(JSON.stringify(JSON.parse(result.expectedOutputContains), null, 2));
580
+ }
581
+ catch {
582
+ console.log(result.expectedOutputContains);
583
+ }
584
+ }
585
+ if (result.expectedJsonSubset) {
586
+ divider();
587
+ info("Expected JSON Subset:");
588
+ try {
589
+ console.log(JSON.stringify(JSON.parse(result.expectedJsonSubset), null, 2));
590
+ }
591
+ catch {
592
+ console.log(result.expectedJsonSubset);
593
+ }
594
+ }
595
+ if (result.evaluatorPromptId) {
596
+ divider();
597
+ printResult("Evaluator Prompt", result.evaluatorPromptId);
598
+ printResult("Evaluator Config", result.evaluatorConfigId || "-");
599
+ }
600
+ }
601
+ catch (err) {
602
+ error(err.message);
603
+ process.exit(1);
604
+ }
605
+ });
606
+ // Update test case
607
+ tests
608
+ .command("update")
609
+ .description("Update a test case")
610
+ .argument("<integration-id>", "Integration ID")
611
+ .argument("<test-case-id>", "Test Case ID")
612
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
613
+ .option("--name <name>", "Test case name")
614
+ .option("--input <json>", "Input variables as JSON")
615
+ .option("--pattern <regex>", "Expected output pattern (regex)")
616
+ .option("--contains <json>", "Expected strings to contain (JSON array)")
617
+ .option("--json-subset <json>", "Expected JSON subset to match")
618
+ .option("--config <config-id>", "Config ID to use for this test")
619
+ .option("--clear-pattern", "Clear expected pattern")
620
+ .option("--clear-contains", "Clear expected contains")
621
+ .option("--clear-json-subset", "Clear expected JSON subset")
622
+ .option("--json", "Output as JSON")
623
+ .action(async (integrationId, testCaseId, options) => {
624
+ const resolvedAppId = resolveAppId(undefined, options);
625
+ const payload = {};
626
+ if (options.name)
627
+ payload.name = options.name;
628
+ if (options.input) {
629
+ try {
630
+ payload.inputVariables = JSON.parse(options.input);
631
+ }
632
+ catch {
633
+ error("Invalid JSON in --input");
634
+ process.exit(1);
635
+ }
636
+ }
637
+ if (options.clearPattern) {
638
+ payload.expectedOutputPattern = null;
639
+ }
640
+ else if (options.pattern) {
641
+ payload.expectedOutputPattern = options.pattern;
642
+ }
643
+ if (options.clearContains) {
644
+ payload.expectedOutputContains = null;
645
+ }
646
+ else if (options.contains) {
647
+ try {
648
+ payload.expectedOutputContains = JSON.parse(options.contains);
649
+ }
650
+ catch {
651
+ error("Invalid JSON in --contains");
652
+ process.exit(1);
653
+ }
654
+ }
655
+ if (options.clearJsonSubset) {
656
+ payload.expectedJsonSubset = null;
657
+ }
658
+ else if (options.jsonSubset) {
659
+ try {
660
+ payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
661
+ }
662
+ catch {
663
+ error("Invalid JSON in --json-subset");
664
+ process.exit(1);
665
+ }
666
+ }
667
+ if (options.config)
668
+ payload.configId = options.config;
669
+ if (Object.keys(payload).length === 0) {
670
+ error("No update options specified.");
671
+ process.exit(1);
672
+ }
673
+ const client = new ApiClient();
674
+ try {
675
+ const result = await client.updateTestCase(resolvedAppId, "integration", integrationId, testCaseId, payload);
676
+ if (options.json) {
677
+ json(result);
678
+ return;
679
+ }
680
+ success("Test case updated.");
681
+ }
682
+ catch (err) {
683
+ error(err.message);
684
+ process.exit(1);
685
+ }
686
+ });
687
+ // Delete test case
688
+ tests
689
+ .command("delete")
690
+ .description("Delete a test case")
691
+ .argument("<integration-id>", "Integration ID")
692
+ .argument("<test-case-id>", "Test Case ID")
693
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
694
+ .option("-y, --yes", "Skip confirmation prompt")
695
+ .action(async (integrationId, testCaseId, options) => {
696
+ const resolvedAppId = resolveAppId(undefined, options);
697
+ if (!options.yes) {
698
+ const inquirer = await import("inquirer");
699
+ const { confirm } = await inquirer.default.prompt([
700
+ {
701
+ type: "confirm",
702
+ name: "confirm",
703
+ message: `Are you sure you want to delete test case ${testCaseId}?`,
704
+ default: false,
705
+ },
706
+ ]);
707
+ if (!confirm) {
708
+ info("Cancelled.");
709
+ return;
710
+ }
711
+ }
712
+ const client = new ApiClient();
713
+ try {
714
+ await client.deleteTestCase(resolvedAppId, "integration", integrationId, testCaseId);
715
+ success("Test case deleted.");
716
+ }
717
+ catch (err) {
718
+ error(err.message);
719
+ process.exit(1);
720
+ }
721
+ });
722
+ // Run a single test case
723
+ tests
724
+ .command("run")
725
+ .description("Run a single test case")
726
+ .argument("<integration-id>", "Integration ID")
727
+ .argument("<test-case-id>", "Test Case ID")
728
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
729
+ .option("--config <config-id>", "Override config ID")
730
+ .option("--json", "Output as JSON")
731
+ .action(async (integrationId, testCaseId, options) => {
732
+ const resolvedAppId = resolveAppId(undefined, options);
733
+ const client = new ApiClient();
734
+ try {
735
+ // Get test case to retrieve its variables
736
+ const testCase = await client.getTestCase(resolvedAppId, "integration", integrationId, testCaseId);
737
+ let variables = {};
738
+ try {
739
+ variables = JSON.parse(testCase.inputVariables || "{}");
740
+ }
741
+ catch {
742
+ // Use empty object if parsing fails
743
+ }
744
+ const result = await client.executeBlockTest(resolvedAppId, "integration", integrationId, {
745
+ variables,
746
+ testCaseId,
747
+ configId: options.config || testCase.configId,
748
+ });
749
+ if (options.json) {
750
+ json(result);
751
+ return;
752
+ }
753
+ const passed = result.verification?.passed;
754
+ if (passed) {
755
+ success("Test PASSED");
756
+ }
757
+ else {
758
+ error("Test FAILED");
759
+ }
760
+ keyValue("Duration", formatDuration(result.metrics?.durationMs));
761
+ if (result.verification) {
762
+ divider();
763
+ info("Verification:");
764
+ const v = result.verification;
765
+ keyValue(" Passed", v.passed ? chalk.green("Yes") : chalk.red("No"));
766
+ keyValue(" Checks", `${v.summary?.passed || 0}/${v.summary?.total || 0} passed`);
767
+ if (v.checks && v.checks.length > 0) {
768
+ console.log();
769
+ for (const check of v.checks) {
770
+ const icon = check.passed ? chalk.green("\u2713") : chalk.red("\u2717");
771
+ console.log(` ${icon} ${check.name}`);
772
+ if (!check.passed && check.message) {
773
+ console.log(` ${chalk.dim(check.message)}`);
774
+ }
775
+ }
776
+ }
777
+ }
778
+ if (result.output) {
779
+ divider();
780
+ info("Output:");
781
+ console.log(typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2));
782
+ }
783
+ if (result.error) {
784
+ divider();
785
+ error(`Error: ${result.error}`);
786
+ }
787
+ if (!passed) {
788
+ process.exit(1);
789
+ }
790
+ }
791
+ catch (err) {
792
+ error(err.message);
793
+ process.exit(1);
794
+ }
795
+ });
796
+ // Run all test cases (or a specific group)
797
+ tests
798
+ .command("run-all")
799
+ .description("Run all test cases for an integration, or a specific subset")
800
+ .argument("<integration-id>", "Integration ID")
801
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
802
+ .option("--config <config-id>", "Override config ID for all tests")
803
+ .option("--test-cases <ids>", "Comma-separated list of test case IDs to run (runs only these)")
804
+ .option("--json", "Output as JSON")
805
+ .action(async (integrationId, options) => {
806
+ const resolvedAppId = resolveAppId(undefined, options);
807
+ const client = new ApiClient();
808
+ try {
809
+ // Parse test case IDs if provided
810
+ let testCaseIds;
811
+ if (options.testCases) {
812
+ testCaseIds = options.testCases.split(",").map((id) => id.trim()).filter(Boolean);
813
+ if (testCaseIds.length === 0) {
814
+ error("No valid test case IDs provided in --test-cases");
815
+ process.exit(1);
816
+ }
817
+ }
818
+ if (!options.json) {
819
+ if (testCaseIds) {
820
+ info(`Running ${testCaseIds.length} selected test case(s)...`);
821
+ }
822
+ else {
823
+ info("Running all test cases...");
824
+ }
825
+ }
826
+ const result = await client.runAllTestCases(resolvedAppId, "integration", integrationId, {
827
+ overrideConfigId: options.config,
828
+ testCaseIds,
829
+ });
830
+ if (options.json) {
831
+ json(result);
832
+ return;
833
+ }
834
+ const { summary, runs, comparisonGroup } = result;
835
+ divider();
836
+ keyValue("Comparison Group", comparisonGroup);
837
+ keyValue("Total Tests", summary.total);
838
+ keyValue("Passed", chalk.green(summary.passed));
839
+ keyValue("Failed", summary.failed > 0 ? chalk.red(summary.failed) : "0");
840
+ divider();
841
+ if (runs && runs.length > 0) {
842
+ for (const r of runs) {
843
+ const passed = r.verification?.passed;
844
+ const icon = passed ? chalk.green("\u2713") : chalk.red("\u2717");
845
+ const status = passed ? "PASSED" : "FAILED";
846
+ console.log(`${icon} ${r.testCaseName} - ${status}`);
847
+ if (!passed && r.verification?.details?.reason) {
848
+ console.log(` ${chalk.dim(r.verification.details.reason)}`);
849
+ }
850
+ }
851
+ }
852
+ // Exit with error code if any tests failed
853
+ if (summary.failed > 0) {
854
+ process.exit(1);
855
+ }
856
+ }
857
+ catch (err) {
858
+ error(err.message);
859
+ process.exit(1);
860
+ }
861
+ });
862
+ // List test runs
863
+ tests
864
+ .command("runs")
865
+ .description("List recent test runs for an integration")
866
+ .argument("<integration-id>", "Integration ID")
867
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
868
+ .option("--limit <n>", "Maximum number of runs to show", "20")
869
+ .option("--group <group>", "Filter by comparison group")
870
+ .option("--json", "Output as JSON")
871
+ .action(async (integrationId, options) => {
872
+ const resolvedAppId = resolveAppId(undefined, options);
873
+ const client = new ApiClient();
874
+ try {
875
+ const { items } = await client.listTestRuns(resolvedAppId, "integration", integrationId, {
876
+ limit: parseInt(options.limit),
877
+ comparisonGroup: options.group,
878
+ });
879
+ if (options.json) {
880
+ json(items);
881
+ return;
882
+ }
883
+ if (!items || items.length === 0) {
884
+ info("No test runs found.");
885
+ return;
886
+ }
887
+ console.log(formatTable(items, [
888
+ { header: "RUN ID", key: "runId", format: formatId },
889
+ { header: "TEST CASE ID", key: "testCaseId", format: (v) => v ? formatId(v) : "-" },
890
+ {
891
+ header: "PASSED",
892
+ key: "verificationPassed",
893
+ format: (v) => v === true
894
+ ? chalk.green("Yes")
895
+ : v === false
896
+ ? chalk.red("No")
897
+ : "-",
898
+ },
899
+ { header: "DURATION", key: "durationMs", format: formatDuration },
900
+ { header: "STARTED", key: "startedAt", format: formatDate },
901
+ ]));
431
902
  }
432
903
  catch (err) {
433
904
  error(err.message);