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.
- package/README.md +168 -21
- package/assets/skill/skills/primitive-platform/SKILL.md +226 -0
- package/dist/bin/primitive.js +198 -15
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +107 -0
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +464 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +27 -13
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +165 -5
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +354 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- 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 +178 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.js +526 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +364 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-types.js +462 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.js +1067 -58
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +510 -2
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +281 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.js +260 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +189 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.js +28 -25
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +719 -188
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +504 -33
- 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 +366 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync.js +2363 -182
- 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 +426 -4
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/webhooks.js +386 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.js +614 -64
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +942 -83
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config.js +51 -53
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/constants.js +3 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/credentials-store.js +307 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/init-config.js +87 -0
- package/dist/src/lib/init-config.js.map +1 -0
- 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/paginate.js +42 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/project-config.js +209 -0
- package/dist/src/lib/project-config.js.map +1 -0
- 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/skill-installer.js +135 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/sync-paths.js +102 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/template.js +279 -18
- 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/version-check.js +172 -0
- package/dist/src/lib/version-check.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 +7 -4
|
@@ -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")
|
|
@@ -63,35 +142,48 @@ Examples:
|
|
|
63
142
|
.option("--name <name>", "Display name")
|
|
64
143
|
.option("--description <desc>", "Description")
|
|
65
144
|
.option("--from-file <path>", "Load workflow from TOML file")
|
|
145
|
+
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
66
146
|
.option("--json", "Output as JSON")
|
|
67
147
|
.action(async (appId, options) => {
|
|
68
148
|
const resolvedAppId = resolveAppId(appId, options);
|
|
69
149
|
const client = new ApiClient();
|
|
70
150
|
let payload;
|
|
71
151
|
if (options.fromFile) {
|
|
152
|
+
let tomlData;
|
|
72
153
|
try {
|
|
73
154
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
74
|
-
|
|
75
|
-
const workflow = tomlData.workflow || tomlData;
|
|
76
|
-
payload = {
|
|
77
|
-
workflowKey: workflow.key || workflow.workflowKey,
|
|
78
|
-
name: workflow.name,
|
|
79
|
-
description: workflow.description,
|
|
80
|
-
steps: tomlData.steps || [],
|
|
81
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
82
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
83
|
-
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
84
|
-
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
85
|
-
perAppMaxRunning: workflow.perAppMaxRunning,
|
|
86
|
-
perAppMaxQueued: workflow.perAppMaxQueued,
|
|
87
|
-
queueTtlSeconds: workflow.queueTtlSeconds,
|
|
88
|
-
dequeueOrder: workflow.dequeueOrder,
|
|
89
|
-
};
|
|
155
|
+
tomlData = TOML.parse(content);
|
|
90
156
|
}
|
|
91
157
|
catch (err) {
|
|
92
158
|
error(`Failed to read TOML file: ${err.message}`);
|
|
93
159
|
process.exit(1);
|
|
94
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
|
+
};
|
|
95
187
|
}
|
|
96
188
|
else {
|
|
97
189
|
if (!options.key || !options.name) {
|
|
@@ -105,6 +197,9 @@ Examples:
|
|
|
105
197
|
steps: [],
|
|
106
198
|
};
|
|
107
199
|
}
|
|
200
|
+
if (options.requiresClientApply !== undefined) {
|
|
201
|
+
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
202
|
+
}
|
|
108
203
|
try {
|
|
109
204
|
const result = await client.createWorkflow(resolvedAppId, payload);
|
|
110
205
|
if (options.json) {
|
|
@@ -137,19 +232,23 @@ Examples:
|
|
|
137
232
|
return;
|
|
138
233
|
}
|
|
139
234
|
const wf = result.workflow;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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");
|
|
243
|
+
if (wf.accessRule) {
|
|
244
|
+
printResult("Access Rule", wf.accessRule);
|
|
245
|
+
}
|
|
147
246
|
divider();
|
|
148
247
|
info("Queue Settings:");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
|
153
252
|
if (wf.inputSchema) {
|
|
154
253
|
divider();
|
|
155
254
|
info("Input Schema:");
|
|
@@ -202,6 +301,7 @@ Examples:
|
|
|
202
301
|
.option("--per-user-max-running <n>", "Max running per user")
|
|
203
302
|
.option("--per-user-max-queued <n>", "Max queued per user")
|
|
204
303
|
.option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
|
|
304
|
+
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
205
305
|
.option("--json", "Output as JSON")
|
|
206
306
|
.action(async (workflowId, options) => {
|
|
207
307
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
@@ -218,6 +318,9 @@ Examples:
|
|
|
218
318
|
payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
219
319
|
if (options.dequeueOrder)
|
|
220
320
|
payload.dequeueOrder = options.dequeueOrder;
|
|
321
|
+
if (options.requiresClientApply !== undefined) {
|
|
322
|
+
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
323
|
+
}
|
|
221
324
|
if (Object.keys(payload).length === 0) {
|
|
222
325
|
error("No update options specified.");
|
|
223
326
|
process.exit(1);
|
|
@@ -278,12 +381,39 @@ Examples:
|
|
|
278
381
|
process.exit(1);
|
|
279
382
|
}
|
|
280
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
|
+
});
|
|
281
411
|
// Draft subcommand
|
|
282
412
|
const draft = workflows.command("draft").description("Manage workflow draft");
|
|
283
413
|
// Update draft
|
|
284
414
|
draft
|
|
285
415
|
.command("update")
|
|
286
|
-
.description("Update workflow draft steps")
|
|
416
|
+
.description("Update workflow draft steps (deprecated; use 'workflows configs' for staged rollouts)")
|
|
287
417
|
.argument("<workflow-id>", "Workflow ID")
|
|
288
418
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
289
419
|
.option("--from-file <path>", "Load steps from TOML file")
|
|
@@ -294,21 +424,27 @@ Examples:
|
|
|
294
424
|
error("--from-file is required");
|
|
295
425
|
process.exit(1);
|
|
296
426
|
}
|
|
297
|
-
let
|
|
427
|
+
let tomlData;
|
|
298
428
|
try {
|
|
299
429
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
300
|
-
|
|
301
|
-
const workflow = tomlData.workflow || tomlData;
|
|
302
|
-
payload = {
|
|
303
|
-
steps: tomlData.steps || [],
|
|
304
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
305
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
306
|
-
};
|
|
430
|
+
tomlData = TOML.parse(content);
|
|
307
431
|
}
|
|
308
432
|
catch (err) {
|
|
309
433
|
error(`Failed to read TOML file: ${err.message}`);
|
|
310
434
|
process.exit(1);
|
|
311
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
|
+
};
|
|
312
448
|
const client = new ApiClient();
|
|
313
449
|
try {
|
|
314
450
|
const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
|
|
@@ -318,6 +454,10 @@ Examples:
|
|
|
318
454
|
}
|
|
319
455
|
success("Draft updated.");
|
|
320
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
|
+
}
|
|
321
461
|
}
|
|
322
462
|
catch (err) {
|
|
323
463
|
error(err.message);
|
|
@@ -327,7 +467,7 @@ Examples:
|
|
|
327
467
|
// Publish workflow
|
|
328
468
|
workflows
|
|
329
469
|
.command("publish")
|
|
330
|
-
.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')")
|
|
331
471
|
.argument("<workflow-id>", "Workflow ID")
|
|
332
472
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
333
473
|
.option("--json", "Output as JSON")
|
|
@@ -343,6 +483,10 @@ Examples:
|
|
|
343
483
|
success("Workflow published.");
|
|
344
484
|
keyValue("Revision ID", result.revision?.revisionId);
|
|
345
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
|
+
}
|
|
346
490
|
}
|
|
347
491
|
catch (err) {
|
|
348
492
|
error(err.message);
|
|
@@ -355,12 +499,18 @@ Examples:
|
|
|
355
499
|
.description("Run a preview execution of the workflow")
|
|
356
500
|
.argument("<workflow-id>", "Workflow ID")
|
|
357
501
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
358
|
-
.option("--config <config-id>", "Use specific configuration
|
|
502
|
+
.option("--config <config-id>", "Use specific configuration")
|
|
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)")
|
|
359
505
|
.option("--input <json>", "Root input as JSON")
|
|
360
506
|
.option("--wait", "Wait for completion and show result")
|
|
361
507
|
.option("--json", "Output as JSON")
|
|
362
508
|
.action(async (workflowId, options) => {
|
|
363
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
|
+
}
|
|
364
514
|
let rootInput;
|
|
365
515
|
if (options.input) {
|
|
366
516
|
try {
|
|
@@ -376,7 +526,15 @@ Examples:
|
|
|
376
526
|
const result = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
377
527
|
rootInput,
|
|
378
528
|
configId: options.config,
|
|
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,
|
|
379
533
|
});
|
|
534
|
+
// Display warning from the server (e.g. "previewing draft because newer than active")
|
|
535
|
+
if (result.warning) {
|
|
536
|
+
warn(result.warning);
|
|
537
|
+
}
|
|
380
538
|
if (!options.wait) {
|
|
381
539
|
if (options.json) {
|
|
382
540
|
json(result);
|
|
@@ -384,6 +542,23 @@ Examples:
|
|
|
384
542
|
}
|
|
385
543
|
success("Preview started.");
|
|
386
544
|
keyValue("Instance ID", result.instanceId);
|
|
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") {
|
|
549
|
+
info("Previewing draft version.");
|
|
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
|
+
}
|
|
387
562
|
info("Use 'workflows runs status' to check progress.");
|
|
388
563
|
return;
|
|
389
564
|
}
|
|
@@ -395,7 +570,8 @@ Examples:
|
|
|
395
570
|
await new Promise((r) => setTimeout(r, 1000));
|
|
396
571
|
const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
|
|
397
572
|
status = statusResult.status;
|
|
398
|
-
|
|
573
|
+
const s = status?.status;
|
|
574
|
+
if (s === "completed" || s === "complete" || s === "failed" || s === "errored") {
|
|
399
575
|
break;
|
|
400
576
|
}
|
|
401
577
|
}
|
|
@@ -403,7 +579,7 @@ Examples:
|
|
|
403
579
|
json({ instanceId: result.instanceId, status });
|
|
404
580
|
return;
|
|
405
581
|
}
|
|
406
|
-
if (status?.status === "completed") {
|
|
582
|
+
if (status?.status === "completed" || status?.status === "complete") {
|
|
407
583
|
success("Preview completed.");
|
|
408
584
|
if (status.output) {
|
|
409
585
|
console.log("\nOutput:");
|
|
@@ -417,7 +593,7 @@ Examples:
|
|
|
417
593
|
});
|
|
418
594
|
}
|
|
419
595
|
}
|
|
420
|
-
else if (status?.status === "failed") {
|
|
596
|
+
else if (status?.status === "failed" || status?.status === "errored") {
|
|
421
597
|
error("Preview failed.");
|
|
422
598
|
if (status.error) {
|
|
423
599
|
console.log("\nError:");
|
|
@@ -506,9 +682,9 @@ Examples:
|
|
|
506
682
|
if (status.stepResults && status.stepResults.length > 0) {
|
|
507
683
|
divider();
|
|
508
684
|
info("Step Results:");
|
|
509
|
-
status.stepResults
|
|
510
|
-
console.log(
|
|
511
|
-
}
|
|
685
|
+
for (const line of renderRunStatusStepResults(status.stepResults)) {
|
|
686
|
+
console.log(line);
|
|
687
|
+
}
|
|
512
688
|
}
|
|
513
689
|
if (status.error) {
|
|
514
690
|
divider();
|
|
@@ -522,6 +698,357 @@ Examples:
|
|
|
522
698
|
process.exit(1);
|
|
523
699
|
}
|
|
524
700
|
});
|
|
701
|
+
// Steps for a run
|
|
702
|
+
runs
|
|
703
|
+
.command("steps")
|
|
704
|
+
.description("Show step-level details for a workflow run")
|
|
705
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
706
|
+
.argument("<run-id>", "Run ID")
|
|
707
|
+
.option("--app <app-id>", "App ID")
|
|
708
|
+
.option("--json", "Output as JSON")
|
|
709
|
+
.action(async (workflowId, runId, options) => {
|
|
710
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
711
|
+
const client = new ApiClient();
|
|
712
|
+
try {
|
|
713
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
714
|
+
if (options.json) {
|
|
715
|
+
json(items);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (!items || items.length === 0) {
|
|
719
|
+
info("No step runs found.");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
console.log(formatTable(items, [
|
|
723
|
+
{ header: "STEP", key: "stepId" },
|
|
724
|
+
{ header: "KIND", key: "stepKind" },
|
|
725
|
+
{
|
|
726
|
+
header: "STATUS",
|
|
727
|
+
key: "status",
|
|
728
|
+
format: (v) => v === "completed" ? chalk.green(v) :
|
|
729
|
+
v === "failed" ? chalk.red(v) :
|
|
730
|
+
v === "skipped" ? chalk.gray(v) :
|
|
731
|
+
v === "error_captured" ? chalk.yellow(v) : v,
|
|
732
|
+
},
|
|
733
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
734
|
+
{
|
|
735
|
+
header: "TOKENS",
|
|
736
|
+
key: "totalTokens",
|
|
737
|
+
format: (v) => v ? String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
header: "ERROR",
|
|
741
|
+
key: "error",
|
|
742
|
+
format: (v) => v ? (v.length > 60 ? v.slice(0, 57) + "..." : v) : "",
|
|
743
|
+
},
|
|
744
|
+
]));
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
error(err.message);
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
// Step detail
|
|
752
|
+
runs
|
|
753
|
+
.command("step-detail")
|
|
754
|
+
.description("Show full details for a single step")
|
|
755
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
756
|
+
.argument("<run-id>", "Run ID")
|
|
757
|
+
.argument("<step-id>", "Step ID")
|
|
758
|
+
.option("--app <app-id>", "App ID")
|
|
759
|
+
.option("--json", "Output as JSON")
|
|
760
|
+
.action(async (workflowId, runId, stepId, options) => {
|
|
761
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
762
|
+
const client = new ApiClient();
|
|
763
|
+
try {
|
|
764
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
765
|
+
const step = items.find((s) => s.stepId === stepId);
|
|
766
|
+
if (!step) {
|
|
767
|
+
error(`Step "${stepId}" not found in run ${runId}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
if (options.json) {
|
|
771
|
+
json(step);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
keyValue("Step", step.stepId);
|
|
775
|
+
keyValue("Kind", step.stepKind);
|
|
776
|
+
keyValue("Status", formatStatus(step.status));
|
|
777
|
+
keyValue("Duration", formatDuration(step.durationMs));
|
|
778
|
+
keyValue("Started", formatDate(step.startedAt));
|
|
779
|
+
keyValue("Ended", formatDate(step.endedAt));
|
|
780
|
+
if (step.totalTokens) {
|
|
781
|
+
keyValue("Tokens", `${step.inputTokens || 0} in / ${step.outputTokens || 0} out / ${step.totalTokens} total`);
|
|
782
|
+
}
|
|
783
|
+
if (step.retryCount) {
|
|
784
|
+
keyValue("Retries", String(step.retryCount));
|
|
785
|
+
}
|
|
786
|
+
if (step.config) {
|
|
787
|
+
divider();
|
|
788
|
+
info("Config (rendered):");
|
|
789
|
+
console.log(JSON.stringify(step.config, null, 2));
|
|
790
|
+
}
|
|
791
|
+
if (step.rawConfig) {
|
|
792
|
+
divider();
|
|
793
|
+
info("Config (raw):");
|
|
794
|
+
console.log(JSON.stringify(step.rawConfig, null, 2));
|
|
795
|
+
}
|
|
796
|
+
if (step.input) {
|
|
797
|
+
divider();
|
|
798
|
+
info("Input:");
|
|
799
|
+
console.log(JSON.stringify(step.input, null, 2));
|
|
800
|
+
}
|
|
801
|
+
if (step.output) {
|
|
802
|
+
divider();
|
|
803
|
+
info("Output:");
|
|
804
|
+
console.log(JSON.stringify(step.output, null, 2));
|
|
805
|
+
}
|
|
806
|
+
if (step.error) {
|
|
807
|
+
divider();
|
|
808
|
+
error("Error: " + step.error);
|
|
809
|
+
if (step.errorDetails) {
|
|
810
|
+
console.log(step.errorDetails);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (step.templateWarnings && step.templateWarnings.length > 0) {
|
|
814
|
+
divider();
|
|
815
|
+
warn("Template Warnings:");
|
|
816
|
+
step.templateWarnings.forEach((w) => {
|
|
817
|
+
console.log(` ${w.expression}: ${w.message}`);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (step.context) {
|
|
821
|
+
divider();
|
|
822
|
+
info("Context Snapshot:");
|
|
823
|
+
console.log(JSON.stringify(step.context, null, 2));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
error(err.message);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
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
|
+
});
|
|
873
|
+
// Error summary
|
|
874
|
+
runs
|
|
875
|
+
.command("error")
|
|
876
|
+
.description("Show error details for a failed workflow run")
|
|
877
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
878
|
+
.argument("<run-id>", "Run ID")
|
|
879
|
+
.option("--app <app-id>", "App ID")
|
|
880
|
+
.option("--json", "Output as JSON")
|
|
881
|
+
.action(async (workflowId, runId, options) => {
|
|
882
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
883
|
+
const client = new ApiClient();
|
|
884
|
+
try {
|
|
885
|
+
const [runResult, stepsResult] = await Promise.all([
|
|
886
|
+
client.getWorkflowRunStatus(resolvedAppId, workflowId, runId),
|
|
887
|
+
client.getWorkflowStepRuns(resolvedAppId, workflowId, runId),
|
|
888
|
+
]);
|
|
889
|
+
const failedStep = stepsResult.items.find((s) => s.status === "failed");
|
|
890
|
+
const errorInfo = {
|
|
891
|
+
runId,
|
|
892
|
+
status: runResult.run?.status,
|
|
893
|
+
errorMessage: runResult.run?.errorMessage || runResult.instanceStatus?.error?.message,
|
|
894
|
+
failedStep: failedStep ? {
|
|
895
|
+
stepId: failedStep.stepId,
|
|
896
|
+
stepKind: failedStep.stepKind,
|
|
897
|
+
error: failedStep.error,
|
|
898
|
+
errorDetails: failedStep.errorDetails,
|
|
899
|
+
input: failedStep.input,
|
|
900
|
+
config: failedStep.config,
|
|
901
|
+
} : null,
|
|
902
|
+
};
|
|
903
|
+
if (options.json) {
|
|
904
|
+
json(errorInfo);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
keyValue("Run ID", runId);
|
|
908
|
+
keyValue("Status", formatStatus(errorInfo.status));
|
|
909
|
+
if (errorInfo.errorMessage) {
|
|
910
|
+
divider();
|
|
911
|
+
error("Error: " + errorInfo.errorMessage);
|
|
912
|
+
}
|
|
913
|
+
if (failedStep) {
|
|
914
|
+
divider();
|
|
915
|
+
info(`Failed Step: ${failedStep.stepId} (${failedStep.stepKind})`);
|
|
916
|
+
if (failedStep.error) {
|
|
917
|
+
error(" " + failedStep.error);
|
|
918
|
+
}
|
|
919
|
+
if (failedStep.errorDetails) {
|
|
920
|
+
console.log(failedStep.errorDetails);
|
|
921
|
+
}
|
|
922
|
+
if (failedStep.input) {
|
|
923
|
+
divider();
|
|
924
|
+
info("Step Input:");
|
|
925
|
+
console.log(JSON.stringify(failedStep.input, null, 2));
|
|
926
|
+
}
|
|
927
|
+
if (failedStep.config) {
|
|
928
|
+
divider();
|
|
929
|
+
info("Step Config:");
|
|
930
|
+
console.log(JSON.stringify(failedStep.config, null, 2));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
info("No failed step found in step runs.");
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
error(err.message);
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
// Failures list
|
|
943
|
+
runs
|
|
944
|
+
.command("failures")
|
|
945
|
+
.description("List recent workflow run failures")
|
|
946
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
947
|
+
.option("--app <app-id>", "App ID")
|
|
948
|
+
.option("--limit <n>", "Number of failures to show", "10")
|
|
949
|
+
.option("--json", "Output as JSON")
|
|
950
|
+
.action(async (workflowId, options) => {
|
|
951
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
952
|
+
const client = new ApiClient();
|
|
953
|
+
try {
|
|
954
|
+
const { items } = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
955
|
+
status: "failed",
|
|
956
|
+
limit: parseInt(options.limit),
|
|
957
|
+
});
|
|
958
|
+
if (options.json) {
|
|
959
|
+
json(items);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (!items || items.length === 0) {
|
|
963
|
+
info("No failures found.");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
console.log(formatTable(items, [
|
|
967
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
968
|
+
{ header: "ERROR", key: "errorMessage", format: (v) => v ? (v.length > 50 ? v.slice(0, 47) + "..." : v) : "-" },
|
|
969
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
970
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
971
|
+
]));
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
error(err.message);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
// Analytics overview
|
|
979
|
+
const analyticsCmd = workflows.command("analytics").description("View workflow performance analytics");
|
|
980
|
+
analyticsCmd
|
|
981
|
+
.command("overview")
|
|
982
|
+
.description("View workflow performance metrics")
|
|
983
|
+
.option("--app <app-id>", "App ID")
|
|
984
|
+
.option("--days <n>", "Time window in days", "7")
|
|
985
|
+
.option("--json", "Output as JSON")
|
|
986
|
+
.action(async (options) => {
|
|
987
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
988
|
+
const client = new ApiClient();
|
|
989
|
+
try {
|
|
990
|
+
const result = await client.getWorkflowAnalytics(resolvedAppId, {
|
|
991
|
+
windowDays: parseInt(options.days),
|
|
992
|
+
});
|
|
993
|
+
if (options.json) {
|
|
994
|
+
json(result);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
info(`Workflow Analytics (${result.windowDays || options.days} days)`);
|
|
998
|
+
console.log();
|
|
999
|
+
keyValue("Total Runs", String(result.totals?.totalRuns ?? 0));
|
|
1000
|
+
keyValue("Success Rate", `${((result.totals?.successRate ?? 0) * 100).toFixed(1)}%`);
|
|
1001
|
+
keyValue("Avg Duration", formatDuration(result.totals?.avgDurationMs));
|
|
1002
|
+
if (result.tokenUsage) {
|
|
1003
|
+
console.log();
|
|
1004
|
+
info("Token Usage:");
|
|
1005
|
+
keyValue(" Input Tokens", String(result.tokenUsage.totalInputTokens ?? 0));
|
|
1006
|
+
keyValue(" Output Tokens", String(result.tokenUsage.totalOutputTokens ?? 0));
|
|
1007
|
+
keyValue(" Total Tokens", String(result.tokenUsage.totalTokens ?? 0));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch (err) {
|
|
1011
|
+
error(err.message);
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
analyticsCmd
|
|
1016
|
+
.command("top")
|
|
1017
|
+
.description("View top workflows by usage")
|
|
1018
|
+
.option("--app <app-id>", "App ID")
|
|
1019
|
+
.option("--days <n>", "Time window in days", "7")
|
|
1020
|
+
.option("--limit <n>", "Number of workflows to show", "10")
|
|
1021
|
+
.option("--json", "Output as JSON")
|
|
1022
|
+
.action(async (options) => {
|
|
1023
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1024
|
+
const client = new ApiClient();
|
|
1025
|
+
try {
|
|
1026
|
+
const result = await client.getTopWorkflows(resolvedAppId, {
|
|
1027
|
+
windowDays: parseInt(options.days),
|
|
1028
|
+
limit: parseInt(options.limit),
|
|
1029
|
+
});
|
|
1030
|
+
if (options.json) {
|
|
1031
|
+
json(result);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (!result.items || result.items.length === 0) {
|
|
1035
|
+
info("No workflow activity in this period.");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
console.log(formatTable(result.items, [
|
|
1039
|
+
{ header: "WORKFLOW", key: "workflow" },
|
|
1040
|
+
{ header: "RUNS", key: "executions" },
|
|
1041
|
+
{ header: "SUCCESS", key: "success_pct", format: (v) => `${v}%` },
|
|
1042
|
+
{ header: "P50", key: "p50", format: (v) => formatDuration(v) },
|
|
1043
|
+
{ header: "P95", key: "p95", format: (v) => formatDuration(v) },
|
|
1044
|
+
{ header: "TOKENS", key: "total_tokens", format: (v) => v ? String(Math.round(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-" },
|
|
1045
|
+
]));
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
error(err.message);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
525
1052
|
// ============================================
|
|
526
1053
|
// CONFIGS SUBCOMMAND
|
|
527
1054
|
// ============================================
|
|
@@ -593,12 +1120,12 @@ Examples:
|
|
|
593
1120
|
json(config);
|
|
594
1121
|
return;
|
|
595
1122
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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));
|
|
602
1129
|
if (config.steps && config.steps.length > 0) {
|
|
603
1130
|
divider();
|
|
604
1131
|
info(`Steps (${config.steps.length}):`);
|
|
@@ -630,15 +1157,22 @@ Examples:
|
|
|
630
1157
|
}
|
|
631
1158
|
let steps = [];
|
|
632
1159
|
if (options.fromFile) {
|
|
1160
|
+
let tomlData;
|
|
633
1161
|
try {
|
|
634
1162
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
635
|
-
|
|
636
|
-
steps = tomlData.steps || [];
|
|
1163
|
+
tomlData = TOML.parse(content);
|
|
637
1164
|
}
|
|
638
1165
|
catch (err) {
|
|
639
1166
|
error(`Failed to read TOML file: ${err.message}`);
|
|
640
1167
|
process.exit(1);
|
|
641
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 || [];
|
|
642
1176
|
}
|
|
643
1177
|
const client = new ApiClient();
|
|
644
1178
|
try {
|
|
@@ -678,15 +1212,22 @@ Examples:
|
|
|
678
1212
|
if (options.description !== undefined)
|
|
679
1213
|
payload.description = options.description;
|
|
680
1214
|
if (options.fromFile) {
|
|
1215
|
+
let tomlData;
|
|
681
1216
|
try {
|
|
682
1217
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
683
|
-
|
|
684
|
-
payload.steps = tomlData.steps || [];
|
|
1218
|
+
tomlData = TOML.parse(content);
|
|
685
1219
|
}
|
|
686
1220
|
catch (err) {
|
|
687
1221
|
error(`Failed to read TOML file: ${err.message}`);
|
|
688
1222
|
process.exit(1);
|
|
689
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 || [];
|
|
690
1231
|
}
|
|
691
1232
|
if (Object.keys(payload).length === 0) {
|
|
692
1233
|
error("No update options specified. Use --name, --description, or --from-file.");
|
|
@@ -925,13 +1466,15 @@ Examples:
|
|
|
925
1466
|
json(result);
|
|
926
1467
|
return;
|
|
927
1468
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1469
|
+
printResult("Test Case ID", result.testCaseId);
|
|
1470
|
+
printResult("Name", result.name);
|
|
1471
|
+
printResult("Created", formatDate(result.createdAt));
|
|
931
1472
|
divider();
|
|
932
1473
|
info("Input Variables:");
|
|
933
1474
|
try {
|
|
934
|
-
const vars =
|
|
1475
|
+
const vars = typeof result.inputVariables === 'string'
|
|
1476
|
+
? JSON.parse(result.inputVariables || "{}")
|
|
1477
|
+
: (result.inputVariables || {});
|
|
935
1478
|
console.log(JSON.stringify(vars, null, 2));
|
|
936
1479
|
}
|
|
937
1480
|
catch {
|
|
@@ -939,13 +1482,16 @@ Examples:
|
|
|
939
1482
|
}
|
|
940
1483
|
if (result.expectedOutputPattern) {
|
|
941
1484
|
divider();
|
|
942
|
-
|
|
1485
|
+
printResult("Expected Pattern", result.expectedOutputPattern);
|
|
943
1486
|
}
|
|
944
1487
|
if (result.expectedOutputContains) {
|
|
945
1488
|
divider();
|
|
946
1489
|
info("Expected Contains:");
|
|
947
1490
|
try {
|
|
948
|
-
|
|
1491
|
+
const contains = typeof result.expectedOutputContains === 'string'
|
|
1492
|
+
? JSON.parse(result.expectedOutputContains)
|
|
1493
|
+
: result.expectedOutputContains;
|
|
1494
|
+
console.log(JSON.stringify(contains, null, 2));
|
|
949
1495
|
}
|
|
950
1496
|
catch {
|
|
951
1497
|
console.log(result.expectedOutputContains);
|
|
@@ -955,7 +1501,10 @@ Examples:
|
|
|
955
1501
|
divider();
|
|
956
1502
|
info("Expected JSON Subset:");
|
|
957
1503
|
try {
|
|
958
|
-
|
|
1504
|
+
const subset = typeof result.expectedJsonSubset === 'string'
|
|
1505
|
+
? JSON.parse(result.expectedJsonSubset)
|
|
1506
|
+
: result.expectedJsonSubset;
|
|
1507
|
+
console.log(JSON.stringify(subset, null, 2));
|
|
959
1508
|
}
|
|
960
1509
|
catch {
|
|
961
1510
|
console.log(result.expectedJsonSubset);
|
|
@@ -1337,9 +1886,10 @@ Examples:
|
|
|
1337
1886
|
while (result.status === "running") {
|
|
1338
1887
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1339
1888
|
result = await fetchStatus();
|
|
1340
|
-
|
|
1889
|
+
// Progress goes to stderr so it can't corrupt JSON on stdout under --json.
|
|
1890
|
+
progress(` Completed: ${result.completed}/${result.results?.length || 0} `);
|
|
1341
1891
|
}
|
|
1342
|
-
|
|
1892
|
+
progressEnd();
|
|
1343
1893
|
}
|
|
1344
1894
|
if (options.json) {
|
|
1345
1895
|
json(result);
|