primitive-admin 1.0.44 → 1.0.46

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 (81) 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 +266 -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 +1054 -284
  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 +285 -63
  52. package/dist/src/commands/workflows.js.map +1 -1
  53. package/dist/src/lib/api-client.js +273 -72
  54. package/dist/src/lib/api-client.js.map +1 -1
  55. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  56. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  57. package/dist/src/lib/db-codegen/dbGenerator.js +255 -0
  58. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  59. package/dist/src/lib/db-codegen/dbNaming.js +104 -0
  60. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  61. package/dist/src/lib/db-codegen/dbTemplates.js +138 -0
  62. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  63. package/dist/src/lib/db-codegen/dbTsTypes.js +61 -0
  64. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  65. package/dist/src/lib/migration-nag.js +163 -0
  66. package/dist/src/lib/migration-nag.js.map +1 -0
  67. package/dist/src/lib/output.js +58 -6
  68. package/dist/src/lib/output.js.map +1 -1
  69. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  70. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  71. package/dist/src/lib/template.js +80 -1
  72. package/dist/src/lib/template.js.map +1 -1
  73. package/dist/src/lib/toml-database-config.js +565 -0
  74. package/dist/src/lib/toml-database-config.js.map +1 -0
  75. package/dist/src/lib/toml-params-validator.js +183 -0
  76. package/dist/src/lib/toml-params-validator.js.map +1 -0
  77. package/dist/src/lib/workflow-fragments.js +121 -0
  78. package/dist/src/lib/workflow-fragments.js.map +1 -0
  79. package/dist/src/lib/workflow-toml-validator.js +343 -0
  80. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  81. package/package.json +2 -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")
@@ -64,36 +143,51 @@ Examples:
64
143
  .option("--description <desc>", "Description")
65
144
  .option("--from-file <path>", "Load workflow from TOML file")
66
145
  .option("--requires-client-apply <bool>", "Require client-side apply: true or false")
146
+ .option("--sync-callable <bool>", "Allow client.workflows.runSync(): true or false")
67
147
  .option("--json", "Output as JSON")
68
148
  .action(async (appId, options) => {
69
149
  const resolvedAppId = resolveAppId(appId, options);
70
150
  const client = new ApiClient();
71
151
  let payload;
72
152
  if (options.fromFile) {
153
+ let tomlData;
73
154
  try {
74
155
  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
- };
156
+ tomlData = TOML.parse(content);
92
157
  }
93
158
  catch (err) {
94
159
  error(`Failed to read TOML file: ${err.message}`);
95
160
  process.exit(1);
96
161
  }
162
+ // Issue #685: reject misnested headers (e.g. [steps.<id>.request])
163
+ // before we send anything to the server. These parse to TOML
164
+ // sub-tables that the runtime silently ignores. We validate
165
+ // outside the parse try-block so the diagnostic isn't masked by
166
+ // the generic "Failed to read TOML" handler.
167
+ const tomlErrors = validateWorkflowToml(tomlData);
168
+ if (tomlErrors.length > 0) {
169
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
170
+ process.exit(1);
171
+ }
172
+ const workflow = tomlData.workflow || tomlData;
173
+ payload = {
174
+ workflowKey: workflow.key || workflow.workflowKey,
175
+ name: workflow.name,
176
+ description: workflow.description,
177
+ steps: tomlData.steps || [],
178
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
179
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
180
+ perUserMaxRunning: workflow.perUserMaxRunning,
181
+ perUserMaxQueued: workflow.perUserMaxQueued,
182
+ perAppMaxRunning: workflow.perAppMaxRunning,
183
+ perAppMaxQueued: workflow.perAppMaxQueued,
184
+ queueTtlSeconds: workflow.queueTtlSeconds,
185
+ dequeueOrder: workflow.dequeueOrder,
186
+ requiresClientApply: workflow.requiresClientApply,
187
+ // #807: align create --from-file with sync/update — read syncCallable
188
+ // from the TOML so all three entry points behave identically.
189
+ syncCallable: workflow.syncCallable,
190
+ };
97
191
  }
98
192
  else {
99
193
  if (!options.key || !options.name) {
@@ -110,6 +204,9 @@ Examples:
110
204
  if (options.requiresClientApply !== undefined) {
111
205
  payload.requiresClientApply = options.requiresClientApply === "true";
112
206
  }
207
+ if (options.syncCallable !== undefined) {
208
+ payload.syncCallable = options.syncCallable === "true";
209
+ }
113
210
  try {
114
211
  const result = await client.createWorkflow(resolvedAppId, payload);
115
212
  if (options.json) {
@@ -142,23 +239,24 @@ Examples:
142
239
  return;
143
240
  }
144
241
  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");
242
+ printResult("Workflow ID", wf.workflowId);
243
+ printResult("Key", wf.workflowKey);
244
+ printResult("Name", wf.name);
245
+ printResult("Description", wf.description);
246
+ printResult("Status", formatStatus(wf.status));
247
+ printResult("Active Config", wf.activeConfigId || "-");
248
+ printResult("Latest Revision", wf.latestRevision || "-");
249
+ printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
250
+ printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
153
251
  if (wf.accessRule) {
154
- keyValue("Access Rule", wf.accessRule);
252
+ printResult("Access Rule", wf.accessRule);
155
253
  }
156
254
  divider();
157
255
  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);
256
+ printResult(" Per User Max Running", wf.perUserMaxRunning);
257
+ printResult(" Per User Max Queued", wf.perUserMaxQueued);
258
+ printResult(" Per App Max Running", wf.perAppMaxRunning);
259
+ printResult(" Dequeue Order", wf.dequeueOrder);
162
260
  if (wf.inputSchema) {
163
261
  divider();
164
262
  info("Input Schema:");
@@ -212,6 +310,7 @@ Examples:
212
310
  .option("--per-user-max-queued <n>", "Max queued per user")
213
311
  .option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
214
312
  .option("--requires-client-apply <bool>", "Require client-side apply: true or false")
313
+ .option("--sync-callable <bool>", "Allow client.workflows.runSync(): true or false")
215
314
  .option("--json", "Output as JSON")
216
315
  .action(async (workflowId, options) => {
217
316
  const resolvedAppId = resolveAppId(undefined, options);
@@ -231,6 +330,9 @@ Examples:
231
330
  if (options.requiresClientApply !== undefined) {
232
331
  payload.requiresClientApply = options.requiresClientApply === "true";
233
332
  }
333
+ if (options.syncCallable !== undefined) {
334
+ payload.syncCallable = options.syncCallable === "true";
335
+ }
234
336
  if (Object.keys(payload).length === 0) {
235
337
  error("No update options specified.");
236
338
  process.exit(1);
@@ -291,12 +393,39 @@ Examples:
291
393
  process.exit(1);
292
394
  }
293
395
  });
396
+ // Expand a workflow TOML and print the result (no server contact).
397
+ //
398
+ // Authors use this to inspect what `include = [...]` fragments produce
399
+ // before pushing. Surfaces include-collision and unique-id failures with
400
+ // helpful messages without going through `sync push`.
401
+ workflows
402
+ .command("expand")
403
+ .description("Expand a workflow TOML's include fragments and print the result")
404
+ .argument("<file>", "Path to the workflow TOML file")
405
+ .option("--format <format>", "Output format: json (default) or toml", "json")
406
+ .action((file, options) => {
407
+ try {
408
+ const expanded = expandWorkflow(file);
409
+ if (options.format === "toml") {
410
+ // Strip the `include` key (already removed by expander) and emit
411
+ // valid TOML so the result is round-trippable.
412
+ console.log(TOML.stringify(expanded));
413
+ }
414
+ else {
415
+ console.log(JSON.stringify(expanded, null, 2));
416
+ }
417
+ }
418
+ catch (err) {
419
+ error(err?.message ?? String(err));
420
+ process.exit(1);
421
+ }
422
+ });
294
423
  // Draft subcommand
295
424
  const draft = workflows.command("draft").description("Manage workflow draft");
296
425
  // Update draft
297
426
  draft
298
427
  .command("update")
299
- .description("Update workflow draft steps")
428
+ .description("Update workflow draft steps (deprecated; use 'workflows configs' for staged rollouts)")
300
429
  .argument("<workflow-id>", "Workflow ID")
301
430
  .option("--app <app-id>", "App ID (uses current app if not specified)")
302
431
  .option("--from-file <path>", "Load steps from TOML file")
@@ -307,21 +436,27 @@ Examples:
307
436
  error("--from-file is required");
308
437
  process.exit(1);
309
438
  }
310
- let payload;
439
+ let tomlData;
311
440
  try {
312
441
  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
- };
442
+ tomlData = TOML.parse(content);
320
443
  }
321
444
  catch (err) {
322
445
  error(`Failed to read TOML file: ${err.message}`);
323
446
  process.exit(1);
324
447
  }
448
+ // Issue #685: reject misnested headers before pushing.
449
+ const tomlErrors = validateWorkflowToml(tomlData);
450
+ if (tomlErrors.length > 0) {
451
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
452
+ process.exit(1);
453
+ }
454
+ const workflow = tomlData.workflow || tomlData;
455
+ const payload = {
456
+ steps: tomlData.steps || [],
457
+ inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
458
+ outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
459
+ };
325
460
  const client = new ApiClient();
326
461
  try {
327
462
  const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
@@ -331,6 +466,10 @@ Examples:
331
466
  }
332
467
  success("Draft updated.");
333
468
  keyValue("Steps", result.draft?.steps?.length || 0);
469
+ // Issue #687: surface server-side deprecation hint when applicable.
470
+ if (result.deprecation) {
471
+ warn(`[deprecated] ${result.deprecation}`);
472
+ }
334
473
  }
335
474
  catch (err) {
336
475
  error(err.message);
@@ -340,7 +479,7 @@ Examples:
340
479
  // Publish workflow
341
480
  workflows
342
481
  .command("publish")
343
- .description("Publish the current draft as a new revision")
482
+ .description("Publish the current draft as a new revision (deprecated for new-model workflows; use 'workflows configs activate')")
344
483
  .argument("<workflow-id>", "Workflow ID")
345
484
  .option("--app <app-id>", "App ID (uses current app if not specified)")
346
485
  .option("--json", "Output as JSON")
@@ -356,6 +495,10 @@ Examples:
356
495
  success("Workflow published.");
357
496
  keyValue("Revision ID", result.revision?.revisionId);
358
497
  keyValue("Published At", formatDate(result.revision?.publishedAt));
498
+ // Issue #687: surface server-side deprecation hint when applicable.
499
+ if (result.deprecation) {
500
+ warn(`[deprecated] ${result.deprecation}`);
501
+ }
359
502
  }
360
503
  catch (err) {
361
504
  error(err.message);
@@ -369,12 +512,17 @@ Examples:
369
512
  .argument("<workflow-id>", "Workflow ID")
370
513
  .option("--app <app-id>", "App ID (uses current app if not specified)")
371
514
  .option("--config <config-id>", "Use specific configuration")
372
- .option("--draft", "Preview the draft version instead of the active config")
515
+ .option("--draft", "Force preview of the draft version, even if active is newer")
516
+ .option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
373
517
  .option("--input <json>", "Root input as JSON")
374
518
  .option("--wait", "Wait for completion and show result")
375
519
  .option("--json", "Output as JSON")
376
520
  .action(async (workflowId, options) => {
377
521
  const resolvedAppId = resolveAppId(undefined, options);
522
+ if (options.draft && options.active) {
523
+ error("--draft and --active are mutually exclusive");
524
+ process.exit(1);
525
+ }
378
526
  let rootInput;
379
527
  if (options.input) {
380
528
  try {
@@ -391,8 +539,11 @@ Examples:
391
539
  rootInput,
392
540
  configId: options.config,
393
541
  useDraft: options.draft || false,
542
+ // Issue #687: --active forces the active config even when a newer
543
+ // draft exists (the inverse of --draft).
544
+ preferActive: options.active || false,
394
545
  });
395
- // Display warning if draft differs from active
546
+ // Display warning from the server (e.g. "previewing draft because newer than active")
396
547
  if (result.warning) {
397
548
  warn(result.warning);
398
549
  }
@@ -403,9 +554,23 @@ Examples:
403
554
  }
404
555
  success("Preview started.");
405
556
  keyValue("Instance ID", result.instanceId);
406
- if (result.usingDraft) {
557
+ // Issue #687: name the side we ran so the user always knows which
558
+ // version produced the output (the "print the source" pattern).
559
+ const source = result.source;
560
+ if (source === "draft") {
407
561
  info("Previewing draft version.");
408
562
  }
563
+ else if (source === "active") {
564
+ // Issue #687 (review feedback): when configName is missing,
565
+ // fall back to a short ID prefix so users staging multiple
566
+ // unnamed configs can disambiguate.
567
+ const label = formatConfigSlotLabel("active config", result.configName, result.configId);
568
+ info(`Previewing ${label}.`);
569
+ }
570
+ else if (source === "config") {
571
+ const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
572
+ info(`Previewing ${label}.`);
573
+ }
409
574
  info("Use 'workflows runs status' to check progress.");
410
575
  return;
411
576
  }
@@ -529,9 +694,9 @@ Examples:
529
694
  if (status.stepResults && status.stepResults.length > 0) {
530
695
  divider();
531
696
  info("Step Results:");
532
- status.stepResults.forEach((step) => {
533
- console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
534
- });
697
+ for (const line of renderRunStatusStepResults(status.stepResults)) {
698
+ console.log(line);
699
+ }
535
700
  }
536
701
  if (status.error) {
537
702
  divider();
@@ -675,6 +840,48 @@ Examples:
675
840
  process.exit(1);
676
841
  }
677
842
  });
843
+ // Integration call logs for a run (cross-pivot from workflow-run to
844
+ // integrations.logs — see issue #699).
845
+ runs
846
+ .command("logs")
847
+ .description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
848
+ .argument("<run-id>", "Workflow run ID")
849
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
850
+ .option("--limit <n>", "Number of logs to show", "100")
851
+ .option("--json", "Output as JSON")
852
+ .action(async (runId, options) => {
853
+ const resolvedAppId = resolveAppId(undefined, options);
854
+ const client = new ApiClient();
855
+ try {
856
+ const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
857
+ if (options.json) {
858
+ json(logs);
859
+ return;
860
+ }
861
+ if (!logs || logs.length === 0) {
862
+ info("No integration calls found for this run.");
863
+ return;
864
+ }
865
+ console.log(formatTable(logs, [
866
+ { header: "TIME", key: "timestamp", format: formatDate },
867
+ { header: "STEP", key: "stepId", format: (v) => v || "" },
868
+ {
869
+ header: "INTEGRATION",
870
+ key: "integrationKey",
871
+ format: (v) => v || "",
872
+ },
873
+ { header: "METHOD", key: "method" },
874
+ { header: "PATH", key: "path" },
875
+ { header: "STATUS", key: "status" },
876
+ { header: "DURATION", key: "durationMs", format: formatDuration },
877
+ { header: "TRACE", key: "traceId", format: formatId },
878
+ ]));
879
+ }
880
+ catch (err) {
881
+ error(err.message);
882
+ process.exit(1);
883
+ }
884
+ });
678
885
  // Error summary
679
886
  runs
680
887
  .command("error")
@@ -925,12 +1132,12 @@ Examples:
925
1132
  json(config);
926
1133
  return;
927
1134
  }
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));
1135
+ printResult("Config ID", config.configId);
1136
+ printResult("Name", config.configName);
1137
+ printResult("Description", config.description || "-");
1138
+ printResult("Status", formatStatus(config.status));
1139
+ printResult("Created", formatDate(config.createdAt));
1140
+ printResult("Modified", formatDate(config.modifiedAt));
934
1141
  if (config.steps && config.steps.length > 0) {
935
1142
  divider();
936
1143
  info(`Steps (${config.steps.length}):`);
@@ -962,15 +1169,22 @@ Examples:
962
1169
  }
963
1170
  let steps = [];
964
1171
  if (options.fromFile) {
1172
+ let tomlData;
965
1173
  try {
966
1174
  const content = readFileSync(options.fromFile, "utf-8");
967
- const tomlData = TOML.parse(content);
968
- steps = tomlData.steps || [];
1175
+ tomlData = TOML.parse(content);
969
1176
  }
970
1177
  catch (err) {
971
1178
  error(`Failed to read TOML file: ${err.message}`);
972
1179
  process.exit(1);
973
1180
  }
1181
+ // Issue #685: reject misnested headers before pushing.
1182
+ const tomlErrors = validateWorkflowToml(tomlData);
1183
+ if (tomlErrors.length > 0) {
1184
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
1185
+ process.exit(1);
1186
+ }
1187
+ steps = tomlData.steps || [];
974
1188
  }
975
1189
  const client = new ApiClient();
976
1190
  try {
@@ -1010,15 +1224,22 @@ Examples:
1010
1224
  if (options.description !== undefined)
1011
1225
  payload.description = options.description;
1012
1226
  if (options.fromFile) {
1227
+ let tomlData;
1013
1228
  try {
1014
1229
  const content = readFileSync(options.fromFile, "utf-8");
1015
- const tomlData = TOML.parse(content);
1016
- payload.steps = tomlData.steps || [];
1230
+ tomlData = TOML.parse(content);
1017
1231
  }
1018
1232
  catch (err) {
1019
1233
  error(`Failed to read TOML file: ${err.message}`);
1020
1234
  process.exit(1);
1021
1235
  }
1236
+ // Issue #685: reject misnested headers before pushing.
1237
+ const tomlErrors = validateWorkflowToml(tomlData);
1238
+ if (tomlErrors.length > 0) {
1239
+ error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
1240
+ process.exit(1);
1241
+ }
1242
+ payload.steps = tomlData.steps || [];
1022
1243
  }
1023
1244
  if (Object.keys(payload).length === 0) {
1024
1245
  error("No update options specified. Use --name, --description, or --from-file.");
@@ -1257,9 +1478,9 @@ Examples:
1257
1478
  json(result);
1258
1479
  return;
1259
1480
  }
1260
- keyValue("Test Case ID", result.testCaseId);
1261
- keyValue("Name", result.name);
1262
- keyValue("Created", formatDate(result.createdAt));
1481
+ printResult("Test Case ID", result.testCaseId);
1482
+ printResult("Name", result.name);
1483
+ printResult("Created", formatDate(result.createdAt));
1263
1484
  divider();
1264
1485
  info("Input Variables:");
1265
1486
  try {
@@ -1273,7 +1494,7 @@ Examples:
1273
1494
  }
1274
1495
  if (result.expectedOutputPattern) {
1275
1496
  divider();
1276
- keyValue("Expected Pattern", result.expectedOutputPattern);
1497
+ printResult("Expected Pattern", result.expectedOutputPattern);
1277
1498
  }
1278
1499
  if (result.expectedOutputContains) {
1279
1500
  divider();
@@ -1677,9 +1898,10 @@ Examples:
1677
1898
  while (result.status === "running") {
1678
1899
  await new Promise((r) => setTimeout(r, 2000));
1679
1900
  result = await fetchStatus();
1680
- process.stdout.write(`\r Completed: ${result.completed}/${result.results?.length || 0} `);
1901
+ // Progress goes to stderr so it can't corrupt JSON on stdout under --json.
1902
+ progress(` Completed: ${result.completed}/${result.results?.length || 0} `);
1681
1903
  }
1682
- console.log();
1904
+ progressEnd();
1683
1905
  }
1684
1906
  if (options.json) {
1685
1907
  json(result);