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.
- 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 +266 -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 +1054 -284
- 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 +285 -63
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +273 -72
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +255 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.js +104 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +138 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +61 -0
- package/dist/src/lib/db-codegen/dbTsTypes.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/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 +565 -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 +343 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
252
|
+
printResult("Access Rule", wf.accessRule);
|
|
155
253
|
}
|
|
156
254
|
divider();
|
|
157
255
|
info("Queue Settings:");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
439
|
+
let tomlData;
|
|
311
440
|
try {
|
|
312
441
|
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
|
-
};
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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
|
|
533
|
-
console.log(
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
+
progressEnd();
|
|
1683
1905
|
}
|
|
1684
1906
|
if (options.json) {
|
|
1685
1907
|
json(result);
|