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.
- package/README.md +43 -0
- package/assets/skill/skills/primitive-platform/SKILL.md +85 -26
- package/dist/bin/primitive.js +6 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +16 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +14 -14
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +70 -20
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +11 -11
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +17 -17
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +5 -5
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +6 -6
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +17 -17
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-types.js +13 -13
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +132 -8
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/email-templates.js +6 -6
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.js +6 -6
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +6 -6
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +7 -7
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +175 -144
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +31 -21
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +17 -16
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +8 -8
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/sync.js +803 -275
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +9 -9
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +44 -3
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/webhooks.js +18 -18
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +273 -63
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +240 -72
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/output.js +58 -6
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/refresh-admin-credentials.js +103 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/template.js +80 -1
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/toml-database-config.js +384 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-params-validator.js +183 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +121 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.js +328 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
244
|
+
printResult("Access Rule", wf.accessRule);
|
|
155
245
|
}
|
|
156
246
|
divider();
|
|
157
247
|
info("Queue Settings:");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
427
|
+
let tomlData;
|
|
311
428
|
try {
|
|
312
429
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
313
|
-
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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
|
|
533
|
-
console.log(
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1892
|
+
progressEnd();
|
|
1683
1893
|
}
|
|
1684
1894
|
if (options.json) {
|
|
1685
1895
|
json(result);
|