primitive-admin 1.0.43 → 1.0.45

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 (71) hide show
  1. package/README.md +43 -0
  2. package/assets/skill/skills/primitive-platform/SKILL.md +85 -26
  3. package/dist/bin/primitive.js +6 -0
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +16 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +14 -14
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/auth.js +70 -20
  10. package/dist/src/commands/auth.js.map +1 -1
  11. package/dist/src/commands/blob-buckets.js +11 -11
  12. package/dist/src/commands/blob-buckets.js.map +1 -1
  13. package/dist/src/commands/catalog.js +17 -17
  14. package/dist/src/commands/catalog.js.map +1 -1
  15. package/dist/src/commands/collection-type-configs.js +5 -5
  16. package/dist/src/commands/collection-type-configs.js.map +1 -1
  17. package/dist/src/commands/collections.js +6 -6
  18. package/dist/src/commands/collections.js.map +1 -1
  19. package/dist/src/commands/comparisons.js +6 -6
  20. package/dist/src/commands/comparisons.js.map +1 -1
  21. package/dist/src/commands/cron-triggers.js +17 -17
  22. package/dist/src/commands/cron-triggers.js.map +1 -1
  23. package/dist/src/commands/database-types.js +13 -13
  24. package/dist/src/commands/database-types.js.map +1 -1
  25. package/dist/src/commands/databases.js +132 -8
  26. package/dist/src/commands/databases.js.map +1 -1
  27. package/dist/src/commands/email-templates.js +6 -6
  28. package/dist/src/commands/email-templates.js.map +1 -1
  29. package/dist/src/commands/env.js +6 -6
  30. package/dist/src/commands/env.js.map +1 -1
  31. package/dist/src/commands/group-type-configs.js +6 -6
  32. package/dist/src/commands/group-type-configs.js.map +1 -1
  33. package/dist/src/commands/groups.js +7 -7
  34. package/dist/src/commands/groups.js.map +1 -1
  35. package/dist/src/commands/init.js +175 -144
  36. package/dist/src/commands/init.js.map +1 -1
  37. package/dist/src/commands/integrations.js +31 -21
  38. package/dist/src/commands/integrations.js.map +1 -1
  39. package/dist/src/commands/prompts.js +17 -16
  40. package/dist/src/commands/prompts.js.map +1 -1
  41. package/dist/src/commands/rule-sets.js +8 -8
  42. package/dist/src/commands/rule-sets.js.map +1 -1
  43. package/dist/src/commands/sync.js +803 -275
  44. package/dist/src/commands/sync.js.map +1 -1
  45. package/dist/src/commands/tokens.js +9 -9
  46. package/dist/src/commands/tokens.js.map +1 -1
  47. package/dist/src/commands/users.js +44 -3
  48. package/dist/src/commands/users.js.map +1 -1
  49. package/dist/src/commands/webhooks.js +18 -18
  50. package/dist/src/commands/webhooks.js.map +1 -1
  51. package/dist/src/commands/workflows.js +273 -63
  52. package/dist/src/commands/workflows.js.map +1 -1
  53. package/dist/src/lib/api-client.js +240 -72
  54. package/dist/src/lib/api-client.js.map +1 -1
  55. package/dist/src/lib/migration-nag.js +163 -0
  56. package/dist/src/lib/migration-nag.js.map +1 -0
  57. package/dist/src/lib/output.js +58 -6
  58. package/dist/src/lib/output.js.map +1 -1
  59. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  60. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  61. package/dist/src/lib/template.js +80 -1
  62. package/dist/src/lib/template.js.map +1 -1
  63. package/dist/src/lib/toml-database-config.js +384 -0
  64. package/dist/src/lib/toml-database-config.js.map +1 -0
  65. package/dist/src/lib/toml-params-validator.js +183 -0
  66. package/dist/src/lib/toml-params-validator.js.map +1 -0
  67. package/dist/src/lib/workflow-fragments.js +121 -0
  68. package/dist/src/lib/workflow-fragments.js.map +1 -0
  69. package/dist/src/lib/workflow-toml-validator.js +328 -0
  70. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  71. package/package.json +1 -1
@@ -4,8 +4,87 @@ import * as TOML from "@iarna/toml";
4
4
  import { lookup as mimeLookup } from "mime-types";
5
5
  import { ApiClient } from "../lib/api-client.js";
6
6
  import { resolveAppId } from "../lib/config.js";
7
+ import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
8
+ import { expandWorkflow } from "../lib/workflow-fragments.js";
7
9
  import chalk from "chalk";
8
- import { success, error, info, warn, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
10
+ import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, progress, progressEnd, } from "../lib/output.js";
11
+ /**
12
+ * Render the `Step Results` section of `workflows runs status`. Pure
13
+ * formatting helper — returns lines as strings so it's unit-testable
14
+ * (the caller console.logs them). For each step, emits:
15
+ *
16
+ * ` <id>: <status-label> <duration>`
17
+ *
18
+ * and, when applicable, indented follow-up lines:
19
+ *
20
+ * - `error_captured` step with `error` → ` error: <truncated message>`
21
+ * - forEach output with `errors[]` non-empty →
22
+ * ` forEach errors: <N/M> - first: <truncated message>`
23
+ *
24
+ * The truncation length matches `runs steps` ERROR column (80 chars,
25
+ * trailing "..." when truncated). See #688.
26
+ */
27
+ export function renderRunStatusStepResults(stepResults) {
28
+ const lines = [];
29
+ for (const step of stepResults || []) {
30
+ const statusLabel = step.skipped
31
+ ? chalk.gray("skipped")
32
+ : step.status === "error_captured"
33
+ ? chalk.yellow("error_captured")
34
+ : step.status === "failed"
35
+ ? chalk.red("failed")
36
+ : step.status === "completed"
37
+ ? chalk.green("completed")
38
+ : step.status || "?";
39
+ const durationLabel = formatDuration(step.durationMs);
40
+ lines.push(` ${step.id}: ${statusLabel} ${durationLabel}`);
41
+ // Single-step error_captured: show the captured error message.
42
+ if (step.status === "error_captured" && step.error) {
43
+ const errMsg = typeof step.error === "string" ? step.error : String(step.error);
44
+ const truncated = errMsg.length > 80 ? errMsg.slice(0, 77) + "..." : errMsg;
45
+ lines.push(` ${chalk.dim("error:")} ${chalk.yellow(truncated)}`);
46
+ }
47
+ else if (step.status === "error_captured" && step.errorDetails) {
48
+ // No top-level error but errorDetails present — hint to drill in.
49
+ lines.push(` ${chalk.dim("error: <see runs step-detail for full errorDetails>")}`);
50
+ }
51
+ // forEach aggregated errors: a one-line summary surfaces the count
52
+ // of `error_captured` iterations and a sample message (#688).
53
+ const fo = step.output;
54
+ if (fo && typeof fo === "object" && Array.isArray(fo.errors) && fo.errors.length > 0) {
55
+ const total = (Array.isArray(fo.items) ? fo.items.length : 0)
56
+ || (fo.totalSucceeded ?? 0) + (fo.totalFailed ?? 0);
57
+ const firstErr = (Array.isArray(fo.errorMessages) && fo.errorMessages.length > 0
58
+ ? fo.errorMessages[0]
59
+ : (fo.errors[0]?.error ?? "?"));
60
+ const firstStr = typeof firstErr === "string" ? firstErr : String(firstErr);
61
+ const truncated = firstStr.length > 80 ? firstStr.slice(0, 77) + "..." : firstStr;
62
+ lines.push(` ${chalk.dim("forEach errors:")} ${chalk.yellow(`${fo.totalFailed ?? fo.errors.length}/${total}`)} ${chalk.dim("- first:")} ${chalk.yellow(truncated)}`);
63
+ }
64
+ }
65
+ return lines;
66
+ }
67
+ /**
68
+ * Issue #687 (review feedback): render an unambiguous label for a workflow
69
+ * config slot ("active config", "draft config", etc.) when the slot may be
70
+ * unnamed. Falls back to a short ID prefix so users staging multiple unnamed
71
+ * configs can still tell them apart.
72
+ *
73
+ * formatConfigSlotLabel("active config", "v2", "01KRT...") → 'active config "v2"'
74
+ * formatConfigSlotLabel("active config", null, "01KRTABCDXYZ...") → 'active config (01KRTABCDXYZ…)'
75
+ * formatConfigSlotLabel("active config", null, null) → 'active config'
76
+ *
77
+ * Exported for testability.
78
+ */
79
+ export function formatConfigSlotLabel(slot, configName, configId, prefixLength = 12) {
80
+ if (configName) {
81
+ return `${slot} "${configName}"`;
82
+ }
83
+ if (configId) {
84
+ return `${slot} (${String(configId).slice(0, prefixLength)}…)`;
85
+ }
86
+ return slot;
87
+ }
9
88
  export function registerWorkflowsCommands(program) {
10
89
  const workflows = program
11
90
  .command("workflows")
@@ -70,30 +149,41 @@ Examples:
70
149
  const client = new ApiClient();
71
150
  let payload;
72
151
  if (options.fromFile) {
152
+ let tomlData;
73
153
  try {
74
154
  const content = readFileSync(options.fromFile, "utf-8");
75
- const tomlData = TOML.parse(content);
76
- const workflow = tomlData.workflow || tomlData;
77
- payload = {
78
- workflowKey: workflow.key || workflow.workflowKey,
79
- name: workflow.name,
80
- description: workflow.description,
81
- steps: tomlData.steps || [],
82
- inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
83
- outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
84
- perUserMaxRunning: workflow.perUserMaxRunning,
85
- perUserMaxQueued: workflow.perUserMaxQueued,
86
- perAppMaxRunning: workflow.perAppMaxRunning,
87
- perAppMaxQueued: workflow.perAppMaxQueued,
88
- queueTtlSeconds: workflow.queueTtlSeconds,
89
- dequeueOrder: workflow.dequeueOrder,
90
- requiresClientApply: workflow.requiresClientApply,
91
- };
155
+ tomlData = TOML.parse(content);
92
156
  }
93
157
  catch (err) {
94
158
  error(`Failed to read TOML file: ${err.message}`);
95
159
  process.exit(1);
96
160
  }
161
+ // Issue #685: reject misnested headers (e.g. [steps.<id>.request])
162
+ // before we send anything to the server. These parse to TOML
163
+ // sub-tables that the runtime silently ignores. We validate
164
+ // outside the parse try-block so the diagnostic isn't masked by
165
+ // the generic "Failed to read TOML" handler.
166
+ const tomlErrors = validateWorkflowToml(tomlData);
167
+ if (tomlErrors.length > 0) {
168
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
169
+ process.exit(1);
170
+ }
171
+ const workflow = tomlData.workflow || tomlData;
172
+ payload = {
173
+ workflowKey: workflow.key || workflow.workflowKey,
174
+ name: workflow.name,
175
+ description: workflow.description,
176
+ steps: tomlData.steps || [],
177
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
178
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
179
+ perUserMaxRunning: workflow.perUserMaxRunning,
180
+ perUserMaxQueued: workflow.perUserMaxQueued,
181
+ perAppMaxRunning: workflow.perAppMaxRunning,
182
+ perAppMaxQueued: workflow.perAppMaxQueued,
183
+ queueTtlSeconds: workflow.queueTtlSeconds,
184
+ dequeueOrder: workflow.dequeueOrder,
185
+ requiresClientApply: workflow.requiresClientApply,
186
+ };
97
187
  }
98
188
  else {
99
189
  if (!options.key || !options.name) {
@@ -142,23 +232,23 @@ Examples:
142
232
  return;
143
233
  }
144
234
  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("Active Config", wf.activeConfigId || "-");
151
- keyValue("Latest Revision", wf.latestRevision || "-");
152
- keyValue("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
235
+ printResult("Workflow ID", wf.workflowId);
236
+ printResult("Key", wf.workflowKey);
237
+ printResult("Name", wf.name);
238
+ printResult("Description", wf.description);
239
+ printResult("Status", formatStatus(wf.status));
240
+ printResult("Active Config", wf.activeConfigId || "-");
241
+ printResult("Latest Revision", wf.latestRevision || "-");
242
+ printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
153
243
  if (wf.accessRule) {
154
- keyValue("Access Rule", wf.accessRule);
244
+ printResult("Access Rule", wf.accessRule);
155
245
  }
156
246
  divider();
157
247
  info("Queue Settings:");
158
- keyValue(" Per User Max Running", wf.perUserMaxRunning);
159
- keyValue(" Per User Max Queued", wf.perUserMaxQueued);
160
- keyValue(" Per App Max Running", wf.perAppMaxRunning);
161
- keyValue(" Dequeue Order", wf.dequeueOrder);
248
+ printResult(" Per User Max Running", wf.perUserMaxRunning);
249
+ printResult(" Per User Max Queued", wf.perUserMaxQueued);
250
+ printResult(" Per App Max Running", wf.perAppMaxRunning);
251
+ printResult(" Dequeue Order", wf.dequeueOrder);
162
252
  if (wf.inputSchema) {
163
253
  divider();
164
254
  info("Input Schema:");
@@ -291,12 +381,39 @@ Examples:
291
381
  process.exit(1);
292
382
  }
293
383
  });
384
+ // Expand a workflow TOML and print the result (no server contact).
385
+ //
386
+ // Authors use this to inspect what `include = [...]` fragments produce
387
+ // before pushing. Surfaces include-collision and unique-id failures with
388
+ // helpful messages without going through `sync push`.
389
+ workflows
390
+ .command("expand")
391
+ .description("Expand a workflow TOML's include fragments and print the result")
392
+ .argument("<file>", "Path to the workflow TOML file")
393
+ .option("--format <format>", "Output format: json (default) or toml", "json")
394
+ .action((file, options) => {
395
+ try {
396
+ const expanded = expandWorkflow(file);
397
+ if (options.format === "toml") {
398
+ // Strip the `include` key (already removed by expander) and emit
399
+ // valid TOML so the result is round-trippable.
400
+ console.log(TOML.stringify(expanded));
401
+ }
402
+ else {
403
+ console.log(JSON.stringify(expanded, null, 2));
404
+ }
405
+ }
406
+ catch (err) {
407
+ error(err?.message ?? String(err));
408
+ process.exit(1);
409
+ }
410
+ });
294
411
  // Draft subcommand
295
412
  const draft = workflows.command("draft").description("Manage workflow draft");
296
413
  // Update draft
297
414
  draft
298
415
  .command("update")
299
- .description("Update workflow draft steps")
416
+ .description("Update workflow draft steps (deprecated; use 'workflows configs' for staged rollouts)")
300
417
  .argument("<workflow-id>", "Workflow ID")
301
418
  .option("--app <app-id>", "App ID (uses current app if not specified)")
302
419
  .option("--from-file <path>", "Load steps from TOML file")
@@ -307,21 +424,27 @@ Examples:
307
424
  error("--from-file is required");
308
425
  process.exit(1);
309
426
  }
310
- let payload;
427
+ let tomlData;
311
428
  try {
312
429
  const content = readFileSync(options.fromFile, "utf-8");
313
- const tomlData = TOML.parse(content);
314
- const workflow = tomlData.workflow || tomlData;
315
- payload = {
316
- steps: tomlData.steps || [],
317
- inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
318
- outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
319
- };
430
+ tomlData = TOML.parse(content);
320
431
  }
321
432
  catch (err) {
322
433
  error(`Failed to read TOML file: ${err.message}`);
323
434
  process.exit(1);
324
435
  }
436
+ // Issue #685: reject misnested headers before pushing.
437
+ const tomlErrors = validateWorkflowToml(tomlData);
438
+ if (tomlErrors.length > 0) {
439
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
440
+ process.exit(1);
441
+ }
442
+ const workflow = tomlData.workflow || tomlData;
443
+ const payload = {
444
+ steps: tomlData.steps || [],
445
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
446
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
447
+ };
325
448
  const client = new ApiClient();
326
449
  try {
327
450
  const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
@@ -331,6 +454,10 @@ Examples:
331
454
  }
332
455
  success("Draft updated.");
333
456
  keyValue("Steps", result.draft?.steps?.length || 0);
457
+ // Issue #687: surface server-side deprecation hint when applicable.
458
+ if (result.deprecation) {
459
+ warn(`[deprecated] ${result.deprecation}`);
460
+ }
334
461
  }
335
462
  catch (err) {
336
463
  error(err.message);
@@ -340,7 +467,7 @@ Examples:
340
467
  // Publish workflow
341
468
  workflows
342
469
  .command("publish")
343
- .description("Publish the current draft as a new revision")
470
+ .description("Publish the current draft as a new revision (deprecated for new-model workflows; use 'workflows configs activate')")
344
471
  .argument("<workflow-id>", "Workflow ID")
345
472
  .option("--app <app-id>", "App ID (uses current app if not specified)")
346
473
  .option("--json", "Output as JSON")
@@ -356,6 +483,10 @@ Examples:
356
483
  success("Workflow published.");
357
484
  keyValue("Revision ID", result.revision?.revisionId);
358
485
  keyValue("Published At", formatDate(result.revision?.publishedAt));
486
+ // Issue #687: surface server-side deprecation hint when applicable.
487
+ if (result.deprecation) {
488
+ warn(`[deprecated] ${result.deprecation}`);
489
+ }
359
490
  }
360
491
  catch (err) {
361
492
  error(err.message);
@@ -369,12 +500,17 @@ Examples:
369
500
  .argument("<workflow-id>", "Workflow ID")
370
501
  .option("--app <app-id>", "App ID (uses current app if not specified)")
371
502
  .option("--config <config-id>", "Use specific configuration")
372
- .option("--draft", "Preview the draft version instead of the active config")
503
+ .option("--draft", "Force preview of the draft version, even if active is newer")
504
+ .option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
373
505
  .option("--input <json>", "Root input as JSON")
374
506
  .option("--wait", "Wait for completion and show result")
375
507
  .option("--json", "Output as JSON")
376
508
  .action(async (workflowId, options) => {
377
509
  const resolvedAppId = resolveAppId(undefined, options);
510
+ if (options.draft && options.active) {
511
+ error("--draft and --active are mutually exclusive");
512
+ process.exit(1);
513
+ }
378
514
  let rootInput;
379
515
  if (options.input) {
380
516
  try {
@@ -391,8 +527,11 @@ Examples:
391
527
  rootInput,
392
528
  configId: options.config,
393
529
  useDraft: options.draft || false,
530
+ // Issue #687: --active forces the active config even when a newer
531
+ // draft exists (the inverse of --draft).
532
+ preferActive: options.active || false,
394
533
  });
395
- // Display warning if draft differs from active
534
+ // Display warning from the server (e.g. "previewing draft because newer than active")
396
535
  if (result.warning) {
397
536
  warn(result.warning);
398
537
  }
@@ -403,9 +542,23 @@ Examples:
403
542
  }
404
543
  success("Preview started.");
405
544
  keyValue("Instance ID", result.instanceId);
406
- if (result.usingDraft) {
545
+ // Issue #687: name the side we ran so the user always knows which
546
+ // version produced the output (the "print the source" pattern).
547
+ const source = result.source;
548
+ if (source === "draft") {
407
549
  info("Previewing draft version.");
408
550
  }
551
+ else if (source === "active") {
552
+ // Issue #687 (review feedback): when configName is missing,
553
+ // fall back to a short ID prefix so users staging multiple
554
+ // unnamed configs can disambiguate.
555
+ const label = formatConfigSlotLabel("active config", result.configName, result.configId);
556
+ info(`Previewing ${label}.`);
557
+ }
558
+ else if (source === "config") {
559
+ const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
560
+ info(`Previewing ${label}.`);
561
+ }
409
562
  info("Use 'workflows runs status' to check progress.");
410
563
  return;
411
564
  }
@@ -529,9 +682,9 @@ Examples:
529
682
  if (status.stepResults && status.stepResults.length > 0) {
530
683
  divider();
531
684
  info("Step Results:");
532
- status.stepResults.forEach((step) => {
533
- console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
534
- });
685
+ for (const line of renderRunStatusStepResults(status.stepResults)) {
686
+ console.log(line);
687
+ }
535
688
  }
536
689
  if (status.error) {
537
690
  divider();
@@ -675,6 +828,48 @@ Examples:
675
828
  process.exit(1);
676
829
  }
677
830
  });
831
+ // Integration call logs for a run (cross-pivot from workflow-run to
832
+ // integrations.logs — see issue #699).
833
+ runs
834
+ .command("logs")
835
+ .description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
836
+ .argument("<run-id>", "Workflow run ID")
837
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
838
+ .option("--limit <n>", "Number of logs to show", "100")
839
+ .option("--json", "Output as JSON")
840
+ .action(async (runId, options) => {
841
+ const resolvedAppId = resolveAppId(undefined, options);
842
+ const client = new ApiClient();
843
+ try {
844
+ const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
845
+ if (options.json) {
846
+ json(logs);
847
+ return;
848
+ }
849
+ if (!logs || logs.length === 0) {
850
+ info("No integration calls found for this run.");
851
+ return;
852
+ }
853
+ console.log(formatTable(logs, [
854
+ { header: "TIME", key: "timestamp", format: formatDate },
855
+ { header: "STEP", key: "stepId", format: (v) => v || "" },
856
+ {
857
+ header: "INTEGRATION",
858
+ key: "integrationKey",
859
+ format: (v) => v || "",
860
+ },
861
+ { header: "METHOD", key: "method" },
862
+ { header: "PATH", key: "path" },
863
+ { header: "STATUS", key: "status" },
864
+ { header: "DURATION", key: "durationMs", format: formatDuration },
865
+ { header: "TRACE", key: "traceId", format: formatId },
866
+ ]));
867
+ }
868
+ catch (err) {
869
+ error(err.message);
870
+ process.exit(1);
871
+ }
872
+ });
678
873
  // Error summary
679
874
  runs
680
875
  .command("error")
@@ -925,12 +1120,12 @@ Examples:
925
1120
  json(config);
926
1121
  return;
927
1122
  }
928
- keyValue("Config ID", config.configId);
929
- keyValue("Name", config.configName);
930
- keyValue("Description", config.description || "-");
931
- keyValue("Status", formatStatus(config.status));
932
- keyValue("Created", formatDate(config.createdAt));
933
- keyValue("Modified", formatDate(config.modifiedAt));
1123
+ printResult("Config ID", config.configId);
1124
+ printResult("Name", config.configName);
1125
+ printResult("Description", config.description || "-");
1126
+ printResult("Status", formatStatus(config.status));
1127
+ printResult("Created", formatDate(config.createdAt));
1128
+ printResult("Modified", formatDate(config.modifiedAt));
934
1129
  if (config.steps && config.steps.length > 0) {
935
1130
  divider();
936
1131
  info(`Steps (${config.steps.length}):`);
@@ -962,15 +1157,22 @@ Examples:
962
1157
  }
963
1158
  let steps = [];
964
1159
  if (options.fromFile) {
1160
+ let tomlData;
965
1161
  try {
966
1162
  const content = readFileSync(options.fromFile, "utf-8");
967
- const tomlData = TOML.parse(content);
968
- steps = tomlData.steps || [];
1163
+ tomlData = TOML.parse(content);
969
1164
  }
970
1165
  catch (err) {
971
1166
  error(`Failed to read TOML file: ${err.message}`);
972
1167
  process.exit(1);
973
1168
  }
1169
+ // Issue #685: reject misnested headers before pushing.
1170
+ const tomlErrors = validateWorkflowToml(tomlData);
1171
+ if (tomlErrors.length > 0) {
1172
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
1173
+ process.exit(1);
1174
+ }
1175
+ steps = tomlData.steps || [];
974
1176
  }
975
1177
  const client = new ApiClient();
976
1178
  try {
@@ -1010,15 +1212,22 @@ Examples:
1010
1212
  if (options.description !== undefined)
1011
1213
  payload.description = options.description;
1012
1214
  if (options.fromFile) {
1215
+ let tomlData;
1013
1216
  try {
1014
1217
  const content = readFileSync(options.fromFile, "utf-8");
1015
- const tomlData = TOML.parse(content);
1016
- payload.steps = tomlData.steps || [];
1218
+ tomlData = TOML.parse(content);
1017
1219
  }
1018
1220
  catch (err) {
1019
1221
  error(`Failed to read TOML file: ${err.message}`);
1020
1222
  process.exit(1);
1021
1223
  }
1224
+ // Issue #685: reject misnested headers before pushing.
1225
+ const tomlErrors = validateWorkflowToml(tomlData);
1226
+ if (tomlErrors.length > 0) {
1227
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
1228
+ process.exit(1);
1229
+ }
1230
+ payload.steps = tomlData.steps || [];
1022
1231
  }
1023
1232
  if (Object.keys(payload).length === 0) {
1024
1233
  error("No update options specified. Use --name, --description, or --from-file.");
@@ -1257,9 +1466,9 @@ Examples:
1257
1466
  json(result);
1258
1467
  return;
1259
1468
  }
1260
- keyValue("Test Case ID", result.testCaseId);
1261
- keyValue("Name", result.name);
1262
- keyValue("Created", formatDate(result.createdAt));
1469
+ printResult("Test Case ID", result.testCaseId);
1470
+ printResult("Name", result.name);
1471
+ printResult("Created", formatDate(result.createdAt));
1263
1472
  divider();
1264
1473
  info("Input Variables:");
1265
1474
  try {
@@ -1273,7 +1482,7 @@ Examples:
1273
1482
  }
1274
1483
  if (result.expectedOutputPattern) {
1275
1484
  divider();
1276
- keyValue("Expected Pattern", result.expectedOutputPattern);
1485
+ printResult("Expected Pattern", result.expectedOutputPattern);
1277
1486
  }
1278
1487
  if (result.expectedOutputContains) {
1279
1488
  divider();
@@ -1677,9 +1886,10 @@ Examples:
1677
1886
  while (result.status === "running") {
1678
1887
  await new Promise((r) => setTimeout(r, 2000));
1679
1888
  result = await fetchStatus();
1680
- process.stdout.write(`\r Completed: ${result.completed}/${result.results?.length || 0} `);
1889
+ // Progress goes to stderr so it can't corrupt JSON on stdout under --json.
1890
+ progress(` Completed: ${result.completed}/${result.results?.length || 0} `);
1681
1891
  }
1682
- console.log();
1892
+ progressEnd();
1683
1893
  }
1684
1894
  if (options.json) {
1685
1895
  json(result);