primitive-admin 1.1.0-alpha.4 → 1.1.0-alpha.41
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 +325 -31
- package/assets/skill/skills/primitive-platform/SKILL.md +281 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +268 -16
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +123 -18
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +464 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +55 -13
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +177 -7
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +422 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +34 -35
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- 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.d.ts +2 -0
- package/dist/src/commands/collections.js +515 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +357 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +471 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +2 -0
- package/dist/src/commands/databases.js +1462 -83
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +2 -0
- package/dist/src/commands/documents.js +518 -11
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +281 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +12 -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.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +181 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +48 -45
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +159 -0
- package/dist/src/commands/guides.js +449 -35
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +17 -0
- package/dist/src/commands/init.js +811 -201
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +533 -46
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +194 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +50 -52
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +388 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/scripts.d.ts +2 -0
- package/dist/src/commands/scripts.js +667 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync.d.ts +175 -0
- package/dist/src/commands/sync.js +4412 -233
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +122 -19
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +440 -22
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +10 -10
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +2 -0
- package/dist/src/commands/webhooks.js +391 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +49 -0
- package/dist/src/commands/workflows.js +1009 -123
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +1283 -0
- package/dist/src/lib/api-client.js +1129 -83
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +60 -0
- package/dist/src/lib/cli-manifest.js +70 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/config.d.ts +53 -0
- package/dist/src/lib/config.js +92 -53
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +2 -0
- package/dist/src/lib/constants.js +3 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +79 -0
- package/dist/src/lib/credentials-store.js +307 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +48 -0
- package/dist/src/lib/csv.js +177 -0
- package/dist/src/lib/csv.js.map +1 -0
- package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
- 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.d.ts +111 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +371 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +45 -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.d.ts +97 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +159 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +78 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +112 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +62 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +220 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +59 -0
- package/dist/src/lib/init-config.js +113 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -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.d.ts +86 -0
- package/dist/src/lib/output.js +150 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/paginate.d.ts +33 -0
- package/dist/src/lib/paginate.js +42 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +97 -0
- package/dist/src/lib/project-config.js +209 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +65 -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/resolve-platform.d.ts +45 -0
- package/dist/src/lib/resolve-platform.js +43 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +23 -0
- package/dist/src/lib/skill-installer.js +135 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +99 -0
- package/dist/src/lib/snapshots.js +357 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +72 -0
- package/dist/src/lib/sync-paths.js +130 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/template.d.ts +97 -0
- package/dist/src/lib/template.js +336 -62
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +152 -0
- package/dist/src/lib/toml-database-config.js +600 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +139 -0
- package/dist/src/lib/toml-metadata-config.js +427 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +10 -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-apply.d.ts +66 -0
- package/dist/src/lib/workflow-apply.js +117 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +41 -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.d.ts +95 -0
- package/dist/src/lib/workflow-toml-validator.js +323 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/types/index.d.ts +520 -0
- package/dist/src/validators.d.ts +64 -0
- package/dist/src/validators.js +63 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +18 -4
|
@@ -4,8 +4,112 @@ 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";
|
|
9
|
+
import { applyWorkflowBody, workflowKeyMismatchError, } from "../lib/workflow-apply.js";
|
|
10
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
7
11
|
import chalk from "chalk";
|
|
8
|
-
import { success, error, info, warn, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
|
|
12
|
+
import { success, error, printApiError, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, progress, progressEnd, } from "../lib/output.js";
|
|
13
|
+
/**
|
|
14
|
+
* Render the `Step Results` section of `workflows runs status`. Pure
|
|
15
|
+
* formatting helper — returns lines as strings so it's unit-testable
|
|
16
|
+
* (the caller console.logs them). For each step, emits:
|
|
17
|
+
*
|
|
18
|
+
* ` <id>: <status-label> <duration>`
|
|
19
|
+
*
|
|
20
|
+
* and, when applicable, indented follow-up lines:
|
|
21
|
+
*
|
|
22
|
+
* - `error_captured` step with `error` → ` error: <truncated message>`
|
|
23
|
+
* - forEach output with `errors[]` non-empty →
|
|
24
|
+
* ` forEach errors: <N/M> - first: <truncated message>`
|
|
25
|
+
*
|
|
26
|
+
* The truncation length matches `runs steps` ERROR column (80 chars,
|
|
27
|
+
* trailing "..." when truncated). See #688.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Terminal-status predicate for the `workflows preview --wait` poll loop
|
|
31
|
+
* (issue #941). A status is terminal when the run has reached an end state and
|
|
32
|
+
* the wait should stop polling — as opposed to a still-in-flight status
|
|
33
|
+
* ("running"/"queued"/"waiting") that should keep the loop going until the cap.
|
|
34
|
+
*
|
|
35
|
+
* The admin preview-status endpoint emits these terminal values (see
|
|
36
|
+
* `mapWorkflowStatus` / the server-side `isEnded` set in admin-api.ts):
|
|
37
|
+
* - "completed" (Cloudflare "complete" normalized)
|
|
38
|
+
* - "failed" (Cloudflare "errored" normalized)
|
|
39
|
+
* - "terminated" (an aborted preview — Codex P2: previously NOT treated as
|
|
40
|
+
* terminal, so an aborted preview was misreported as a wait timeout)
|
|
41
|
+
*
|
|
42
|
+
* We accept the un-normalized Cloudflare spellings too ("complete"/"errored")
|
|
43
|
+
* defensively, in case a status slips through un-normalized.
|
|
44
|
+
*/
|
|
45
|
+
export function isPreviewWaitTerminalStatus(s) {
|
|
46
|
+
return (s === "completed" ||
|
|
47
|
+
s === "complete" ||
|
|
48
|
+
s === "failed" ||
|
|
49
|
+
s === "errored" ||
|
|
50
|
+
s === "terminated");
|
|
51
|
+
}
|
|
52
|
+
export function renderRunStatusStepResults(stepResults) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (const step of stepResults || []) {
|
|
55
|
+
const statusLabel = step.skipped
|
|
56
|
+
? chalk.gray("skipped")
|
|
57
|
+
: step.status === "error_captured"
|
|
58
|
+
? chalk.yellow("error_captured")
|
|
59
|
+
: step.status === "failed"
|
|
60
|
+
? chalk.red("failed")
|
|
61
|
+
: step.status === "completed"
|
|
62
|
+
? chalk.green("completed")
|
|
63
|
+
: step.status || "?";
|
|
64
|
+
const durationLabel = formatDuration(step.durationMs);
|
|
65
|
+
lines.push(` ${step.id}: ${statusLabel} ${durationLabel}`);
|
|
66
|
+
// Single-step error_captured: show the captured error message.
|
|
67
|
+
if (step.status === "error_captured" && step.error) {
|
|
68
|
+
const errMsg = typeof step.error === "string" ? step.error : String(step.error);
|
|
69
|
+
const truncated = errMsg.length > 80 ? errMsg.slice(0, 77) + "..." : errMsg;
|
|
70
|
+
lines.push(` ${chalk.dim("error:")} ${chalk.yellow(truncated)}`);
|
|
71
|
+
}
|
|
72
|
+
else if (step.status === "error_captured" && step.errorDetails) {
|
|
73
|
+
// No top-level error but errorDetails present — hint to drill in.
|
|
74
|
+
lines.push(` ${chalk.dim("error: <see runs step-detail for full errorDetails>")}`);
|
|
75
|
+
}
|
|
76
|
+
// forEach aggregated errors: a one-line summary surfaces the count
|
|
77
|
+
// of `error_captured` iterations and a sample message (#688).
|
|
78
|
+
const fo = step.output;
|
|
79
|
+
if (fo && typeof fo === "object" && Array.isArray(fo.errors) && fo.errors.length > 0) {
|
|
80
|
+
const total = (Array.isArray(fo.items) ? fo.items.length : 0)
|
|
81
|
+
|| (fo.totalSucceeded ?? 0) + (fo.totalFailed ?? 0);
|
|
82
|
+
const firstErr = (Array.isArray(fo.errorMessages) && fo.errorMessages.length > 0
|
|
83
|
+
? fo.errorMessages[0]
|
|
84
|
+
: (fo.errors[0]?.error ?? "?"));
|
|
85
|
+
const firstStr = typeof firstErr === "string" ? firstErr : String(firstErr);
|
|
86
|
+
const truncated = firstStr.length > 80 ? firstStr.slice(0, 77) + "..." : firstStr;
|
|
87
|
+
lines.push(` ${chalk.dim("forEach errors:")} ${chalk.yellow(`${fo.totalFailed ?? fo.errors.length}/${total}`)} ${chalk.dim("- first:")} ${chalk.yellow(truncated)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Issue #687 (review feedback): render an unambiguous label for a workflow
|
|
94
|
+
* config slot ("active config", "draft config", etc.) when the slot may be
|
|
95
|
+
* unnamed. Falls back to a short ID prefix so users staging multiple unnamed
|
|
96
|
+
* configs can still tell them apart.
|
|
97
|
+
*
|
|
98
|
+
* formatConfigSlotLabel("active config", "v2", "01KRT...") → 'active config "v2"'
|
|
99
|
+
* formatConfigSlotLabel("active config", null, "01KRTABCDXYZ...") → 'active config (01KRTABCDXYZ…)'
|
|
100
|
+
* formatConfigSlotLabel("active config", null, null) → 'active config'
|
|
101
|
+
*
|
|
102
|
+
* Exported for testability.
|
|
103
|
+
*/
|
|
104
|
+
export function formatConfigSlotLabel(slot, configName, configId, prefixLength = 12) {
|
|
105
|
+
if (configName) {
|
|
106
|
+
return `${slot} "${configName}"`;
|
|
107
|
+
}
|
|
108
|
+
if (configId) {
|
|
109
|
+
return `${slot} (${String(configId).slice(0, prefixLength)}…)`;
|
|
110
|
+
}
|
|
111
|
+
return slot;
|
|
112
|
+
}
|
|
9
113
|
export function registerWorkflowsCommands(program) {
|
|
10
114
|
const workflows = program
|
|
11
115
|
.command("workflows")
|
|
@@ -63,35 +167,68 @@ Examples:
|
|
|
63
167
|
.option("--name <name>", "Display name")
|
|
64
168
|
.option("--description <desc>", "Description")
|
|
65
169
|
.option("--from-file <path>", "Load workflow from TOML file")
|
|
170
|
+
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
171
|
+
.option("--sync-callable <bool>", "Allow client.workflows.runSync(): true or false")
|
|
66
172
|
.option("--json", "Output as JSON")
|
|
67
173
|
.action(async (appId, options) => {
|
|
68
174
|
const resolvedAppId = resolveAppId(appId, options);
|
|
69
175
|
const client = new ApiClient();
|
|
70
176
|
let payload;
|
|
71
177
|
if (options.fromFile) {
|
|
178
|
+
let tomlData;
|
|
72
179
|
try {
|
|
73
180
|
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
|
-
};
|
|
181
|
+
tomlData = TOML.parse(content);
|
|
90
182
|
}
|
|
91
183
|
catch (err) {
|
|
92
184
|
error(`Failed to read TOML file: ${err.message}`);
|
|
93
185
|
process.exit(1);
|
|
94
186
|
}
|
|
187
|
+
// Issue #685: reject misnested headers (e.g. [steps.<id>.request])
|
|
188
|
+
// before we send anything to the server. These parse to TOML
|
|
189
|
+
// sub-tables that the runtime silently ignores. We validate
|
|
190
|
+
// outside the parse try-block so the diagnostic isn't masked by
|
|
191
|
+
// the generic "Failed to read TOML" handler.
|
|
192
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
193
|
+
if (tomlErrors.length > 0) {
|
|
194
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const workflow = tomlData.workflow || tomlData;
|
|
198
|
+
payload = {
|
|
199
|
+
workflowKey: workflow.key || workflow.workflowKey,
|
|
200
|
+
name: workflow.name,
|
|
201
|
+
description: workflow.description,
|
|
202
|
+
steps: tomlData.steps || [],
|
|
203
|
+
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
204
|
+
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
205
|
+
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
206
|
+
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
207
|
+
perAppMaxRunning: workflow.perAppMaxRunning,
|
|
208
|
+
perAppMaxQueued: workflow.perAppMaxQueued,
|
|
209
|
+
queueTtlSeconds: workflow.queueTtlSeconds,
|
|
210
|
+
dequeueOrder: workflow.dequeueOrder,
|
|
211
|
+
requiresClientApply: workflow.requiresClientApply,
|
|
212
|
+
// #807: align create --from-file with sync/update — read syncCallable
|
|
213
|
+
// from the TOML so all three entry points behave identically.
|
|
214
|
+
syncCallable: workflow.syncCallable,
|
|
215
|
+
// #571: carry accessRule through so a TOML's access-control rule
|
|
216
|
+
// lands on create instead of being silently dropped. On create the
|
|
217
|
+
// server default is already null, so coalesce a falsy/absent rule to
|
|
218
|
+
// undefined (omit) rather than sending "" — both clear, but undefined
|
|
219
|
+
// is the precise "no rule declared" signal.
|
|
220
|
+
accessRule: workflow.accessRule || undefined,
|
|
221
|
+
// #1173: forward the workflow principal mode (`runAs`) from the TOML,
|
|
222
|
+
// matching `sync push` create (sync.ts ~3345). Without this, a
|
|
223
|
+
// `runAs="system"` TOML reached the server as non-system, so the
|
|
224
|
+
// identity validator correctly rejected system-only steps (e.g.
|
|
225
|
+
// `iterate-users`). Create semantics — `workflow.runAs || undefined`
|
|
226
|
+
// (absent/empty → server default), NOT the presence-aware clearing
|
|
227
|
+
// form that `sync push` update uses. `capabilities` is intentionally
|
|
228
|
+
// NOT forwarded: `sync push` doesn't serialize/persist it today, so
|
|
229
|
+
// forwarding it only here would create a new asymmetry (see #1173).
|
|
230
|
+
runAs: workflow.runAs || undefined,
|
|
231
|
+
};
|
|
95
232
|
}
|
|
96
233
|
else {
|
|
97
234
|
if (!options.key || !options.name) {
|
|
@@ -105,6 +242,12 @@ Examples:
|
|
|
105
242
|
steps: [],
|
|
106
243
|
};
|
|
107
244
|
}
|
|
245
|
+
if (options.requiresClientApply !== undefined) {
|
|
246
|
+
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
247
|
+
}
|
|
248
|
+
if (options.syncCallable !== undefined) {
|
|
249
|
+
payload.syncCallable = options.syncCallable === "true";
|
|
250
|
+
}
|
|
108
251
|
try {
|
|
109
252
|
const result = await client.createWorkflow(resolvedAppId, payload);
|
|
110
253
|
if (options.json) {
|
|
@@ -116,7 +259,9 @@ Examples:
|
|
|
116
259
|
keyValue("Key", result.workflow?.workflowKey);
|
|
117
260
|
}
|
|
118
261
|
catch (err) {
|
|
119
|
-
|
|
262
|
+
// #1173: render the server's structured `details[]` (e.g. the specific
|
|
263
|
+
// identity-validation reason) instead of only the generic message.
|
|
264
|
+
printApiError(err);
|
|
120
265
|
process.exit(1);
|
|
121
266
|
}
|
|
122
267
|
});
|
|
@@ -137,19 +282,30 @@ Examples:
|
|
|
137
282
|
return;
|
|
138
283
|
}
|
|
139
284
|
const wf = result.workflow;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
285
|
+
printResult("Workflow ID", wf.workflowId);
|
|
286
|
+
printResult("Key", wf.workflowKey);
|
|
287
|
+
printResult("Name", wf.name);
|
|
288
|
+
printResult("Description", wf.description);
|
|
289
|
+
printResult("Status", formatStatus(wf.status));
|
|
290
|
+
printResult("Active Config", wf.activeConfigId || "-");
|
|
291
|
+
printResult("Latest Revision", wf.latestRevision || "-");
|
|
292
|
+
printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
|
|
293
|
+
printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
|
|
294
|
+
// #1233 — runAs unset means the engine defaults to "caller"; show that
|
|
295
|
+
// default rather than a blank so the deployed principal mode is visible.
|
|
296
|
+
printResult("Run As", wf.runAs || "caller");
|
|
297
|
+
printResult("Capabilities", Array.isArray(wf.capabilities) && wf.capabilities.length > 0
|
|
298
|
+
? wf.capabilities.join(", ")
|
|
299
|
+
: "-");
|
|
300
|
+
if (wf.accessRule) {
|
|
301
|
+
printResult("Access Rule", wf.accessRule);
|
|
302
|
+
}
|
|
147
303
|
divider();
|
|
148
304
|
info("Queue Settings:");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
305
|
+
printResult(" Per User Max Running", wf.perUserMaxRunning);
|
|
306
|
+
printResult(" Per User Max Queued", wf.perUserMaxQueued);
|
|
307
|
+
printResult(" Per App Max Running", wf.perAppMaxRunning);
|
|
308
|
+
printResult(" Dequeue Order", wf.dequeueOrder);
|
|
153
309
|
if (wf.inputSchema) {
|
|
154
310
|
divider();
|
|
155
311
|
info("Input Schema:");
|
|
@@ -193,7 +349,7 @@ Examples:
|
|
|
193
349
|
// Update workflow
|
|
194
350
|
workflows
|
|
195
351
|
.command("update")
|
|
196
|
-
.description("Update workflow metadata")
|
|
352
|
+
.description("Update workflow metadata, or push a new body with --from-file")
|
|
197
353
|
.argument("<workflow-id>", "Workflow ID")
|
|
198
354
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
199
355
|
.option("--name <name>", "Display name")
|
|
@@ -202,9 +358,114 @@ Examples:
|
|
|
202
358
|
.option("--per-user-max-running <n>", "Max running per user")
|
|
203
359
|
.option("--per-user-max-queued <n>", "Max queued per user")
|
|
204
360
|
.option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
|
|
361
|
+
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
362
|
+
.option("--sync-callable <bool>", "Allow client.workflows.runSync(): true or false")
|
|
363
|
+
.option("--access-rule <expr>", "CEL expression for workflow access (empty string to clear)")
|
|
364
|
+
.option("--from-file <path>", "Push a revised body (metadata + steps) from a TOML file, keeping the same key/id. " +
|
|
365
|
+
"Overwrites the active config in place — live immediately (no separate publish). " +
|
|
366
|
+
"Explicit metadata flags above override values in the TOML.")
|
|
205
367
|
.option("--json", "Output as JSON")
|
|
206
368
|
.action(async (workflowId, options) => {
|
|
207
369
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
370
|
+
// --from-file: push a full body (metadata + steps) to the existing
|
|
371
|
+
// workflow, keeping its key/id. Symmetric with `create --from-file` and
|
|
372
|
+
// mirrors the `sync push` update-existing path via the shared
|
|
373
|
+
// `applyWorkflowBody` helper (#1249).
|
|
374
|
+
if (options.fromFile) {
|
|
375
|
+
let tomlData;
|
|
376
|
+
try {
|
|
377
|
+
// Expand top-level `include = [...]` workflow fragments before
|
|
378
|
+
// deriving steps, matching what `sync push` does (`parseTomlFile`
|
|
379
|
+
// runs `expandWorkflowTomlData`). `update --from-file` overwrites an
|
|
380
|
+
// existing workflow's live active config, so parsing the raw TOML
|
|
381
|
+
// and reading only `tomlData.steps` would drop fragment-supplied
|
|
382
|
+
// steps — an include-only workflow would overwrite the live body
|
|
383
|
+
// with an empty step list (silent data loss, #1286). `expandWorkflow`
|
|
384
|
+
// reads + parses + splices fragments; a TOML with no `include` key is
|
|
385
|
+
// returned unchanged, so the non-fragment case is unaffected.
|
|
386
|
+
tomlData = expandWorkflow(options.fromFile);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
error(`Failed to read TOML file: ${err.message}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
// Issue #685: reject misnested headers before sending anything to the
|
|
393
|
+
// server — identical validation to `create --from-file`. Runs on the
|
|
394
|
+
// expanded data (post fragment splice), matching `sync push`.
|
|
395
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
396
|
+
if (tomlErrors.length > 0) {
|
|
397
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const tomlWorkflow = tomlData.workflow || tomlData;
|
|
401
|
+
const steps = tomlData.steps || [];
|
|
402
|
+
const client = new ApiClient();
|
|
403
|
+
// Fetch the existing workflow: we need its real workflowKey (Fork 2
|
|
404
|
+
// key-mismatch guard) and its activeConfigId (live body overwrite).
|
|
405
|
+
let existing;
|
|
406
|
+
try {
|
|
407
|
+
const got = await client.getWorkflow(resolvedAppId, workflowId);
|
|
408
|
+
existing = got?.workflow;
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
printApiError(err);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
if (!existing) {
|
|
415
|
+
error(`Workflow not found: ${workflowId}`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
// Fork 2: a TOML key that contradicts the target's key is almost
|
|
419
|
+
// certainly the wrong file — error rather than silently ignore it.
|
|
420
|
+
const mismatch = workflowKeyMismatchError(tomlWorkflow, existing.workflowKey);
|
|
421
|
+
if (mismatch) {
|
|
422
|
+
error(mismatch);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
// Fork 1 (B): explicit `update` flags override TOML-derived metadata.
|
|
426
|
+
const mergedWorkflow = { ...tomlWorkflow };
|
|
427
|
+
if (options.name)
|
|
428
|
+
mergedWorkflow.name = options.name;
|
|
429
|
+
if (options.description)
|
|
430
|
+
mergedWorkflow.description = options.description;
|
|
431
|
+
if (options.status)
|
|
432
|
+
mergedWorkflow.status = options.status;
|
|
433
|
+
if (options.perUserMaxRunning) {
|
|
434
|
+
mergedWorkflow.perUserMaxRunning = parseInt(options.perUserMaxRunning);
|
|
435
|
+
}
|
|
436
|
+
if (options.perUserMaxQueued) {
|
|
437
|
+
mergedWorkflow.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
438
|
+
}
|
|
439
|
+
if (options.dequeueOrder)
|
|
440
|
+
mergedWorkflow.dequeueOrder = options.dequeueOrder;
|
|
441
|
+
if (options.requiresClientApply !== undefined) {
|
|
442
|
+
mergedWorkflow.requiresClientApply = options.requiresClientApply === "true";
|
|
443
|
+
}
|
|
444
|
+
if (options.syncCallable !== undefined) {
|
|
445
|
+
mergedWorkflow.syncCallable = options.syncCallable === "true";
|
|
446
|
+
}
|
|
447
|
+
if (options.accessRule !== undefined) {
|
|
448
|
+
mergedWorkflow.accessRule = options.accessRule;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const { updateSlotLabel, fullWorkflow } = await applyWorkflowBody(client, resolvedAppId, workflowId, mergedWorkflow, steps, { activeConfigId: existing.activeConfigId });
|
|
452
|
+
if (options.json) {
|
|
453
|
+
json(fullWorkflow);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
success(`Workflow updated from ${options.fromFile} (${updateSlotLabel}).`);
|
|
457
|
+
keyValue("Workflow ID", workflowId);
|
|
458
|
+
keyValue("Key", existing.workflowKey);
|
|
459
|
+
keyValue("Steps", steps.length);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
// Surface the server's structured `details[]` (e.g. syncCallable /
|
|
463
|
+
// identity validation reasons) like create/sync push do.
|
|
464
|
+
printApiError(err);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
208
469
|
const payload = {};
|
|
209
470
|
if (options.name)
|
|
210
471
|
payload.name = options.name;
|
|
@@ -218,6 +479,19 @@ Examples:
|
|
|
218
479
|
payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
219
480
|
if (options.dequeueOrder)
|
|
220
481
|
payload.dequeueOrder = options.dequeueOrder;
|
|
482
|
+
if (options.requiresClientApply !== undefined) {
|
|
483
|
+
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
484
|
+
}
|
|
485
|
+
if (options.syncCallable !== undefined) {
|
|
486
|
+
payload.syncCallable = options.syncCallable === "true";
|
|
487
|
+
}
|
|
488
|
+
// #571: presence guard, NOT truthiness — `--access-rule ""` must reach
|
|
489
|
+
// the server (it clears the rule) while an omitted flag leaves
|
|
490
|
+
// options.accessRule undefined so the server's hasOwnProperty guard
|
|
491
|
+
// leaves the existing rule untouched.
|
|
492
|
+
if (options.accessRule !== undefined) {
|
|
493
|
+
payload.accessRule = options.accessRule;
|
|
494
|
+
}
|
|
221
495
|
if (Object.keys(payload).length === 0) {
|
|
222
496
|
error("No update options specified.");
|
|
223
497
|
process.exit(1);
|
|
@@ -232,7 +506,8 @@ Examples:
|
|
|
232
506
|
success("Workflow updated.");
|
|
233
507
|
}
|
|
234
508
|
catch (err) {
|
|
235
|
-
|
|
509
|
+
// #1173: surface structured `details[]` on update too.
|
|
510
|
+
printApiError(err);
|
|
236
511
|
process.exit(1);
|
|
237
512
|
}
|
|
238
513
|
});
|
|
@@ -246,24 +521,16 @@ Examples:
|
|
|
246
521
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
247
522
|
.action(async (workflowId, options) => {
|
|
248
523
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
249
|
-
if (!options.yes) {
|
|
250
|
-
const action = options.hard ? "permanently delete" : "archive";
|
|
251
|
-
const inquirer = await import("inquirer");
|
|
252
|
-
const { confirm } = await inquirer.default.prompt([
|
|
253
|
-
{
|
|
254
|
-
type: "confirm",
|
|
255
|
-
name: "confirm",
|
|
256
|
-
message: `Are you sure you want to ${action} workflow ${workflowId}?`,
|
|
257
|
-
default: false,
|
|
258
|
-
},
|
|
259
|
-
]);
|
|
260
|
-
if (!confirm) {
|
|
261
|
-
info("Cancelled.");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
524
|
const client = new ApiClient();
|
|
266
525
|
try {
|
|
526
|
+
if (!options.yes) {
|
|
527
|
+
const action = options.hard ? "permanently delete" : "archive";
|
|
528
|
+
const confirmed = await confirmPrompt(`Are you sure you want to ${action} workflow ${workflowId}?`);
|
|
529
|
+
if (!confirmed) {
|
|
530
|
+
info("Cancelled.");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
267
534
|
if (options.hard) {
|
|
268
535
|
await client.deleteWorkflow(resolvedAppId, workflowId);
|
|
269
536
|
success("Workflow deleted.");
|
|
@@ -271,6 +538,10 @@ Examples:
|
|
|
271
538
|
else {
|
|
272
539
|
await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
|
|
273
540
|
success("Workflow archived.");
|
|
541
|
+
// #1238 — archiving keeps the workflowKey reserved, so recreating
|
|
542
|
+
// under the same key fails with "workflowKey already exists". Point
|
|
543
|
+
// at the remedy so the reserved-key behavior is discoverable.
|
|
544
|
+
info(`The workflowKey stays reserved while archived. To free it for reuse, run: primitive workflows delete ${workflowId} --hard`);
|
|
274
545
|
}
|
|
275
546
|
}
|
|
276
547
|
catch (err) {
|
|
@@ -278,12 +549,39 @@ Examples:
|
|
|
278
549
|
process.exit(1);
|
|
279
550
|
}
|
|
280
551
|
});
|
|
552
|
+
// Expand a workflow TOML and print the result (no server contact).
|
|
553
|
+
//
|
|
554
|
+
// Authors use this to inspect what `include = [...]` fragments produce
|
|
555
|
+
// before pushing. Surfaces include-collision and unique-id failures with
|
|
556
|
+
// helpful messages without going through `sync push`.
|
|
557
|
+
workflows
|
|
558
|
+
.command("expand")
|
|
559
|
+
.description("Expand a workflow TOML's include fragments and print the result")
|
|
560
|
+
.argument("<file>", "Path to the workflow TOML file")
|
|
561
|
+
.option("--format <format>", "Output format: json (default) or toml", "json")
|
|
562
|
+
.action((file, options) => {
|
|
563
|
+
try {
|
|
564
|
+
const expanded = expandWorkflow(file);
|
|
565
|
+
if (options.format === "toml") {
|
|
566
|
+
// Strip the `include` key (already removed by expander) and emit
|
|
567
|
+
// valid TOML so the result is round-trippable.
|
|
568
|
+
console.log(TOML.stringify(expanded));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
console.log(JSON.stringify(expanded, null, 2));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
error(err?.message ?? String(err));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
281
579
|
// Draft subcommand
|
|
282
580
|
const draft = workflows.command("draft").description("Manage workflow draft");
|
|
283
581
|
// Update draft
|
|
284
582
|
draft
|
|
285
583
|
.command("update")
|
|
286
|
-
.description("Update workflow draft steps")
|
|
584
|
+
.description("Update workflow draft steps (deprecated; use 'workflows configs' for staged rollouts)")
|
|
287
585
|
.argument("<workflow-id>", "Workflow ID")
|
|
288
586
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
289
587
|
.option("--from-file <path>", "Load steps from TOML file")
|
|
@@ -294,21 +592,27 @@ Examples:
|
|
|
294
592
|
error("--from-file is required");
|
|
295
593
|
process.exit(1);
|
|
296
594
|
}
|
|
297
|
-
let
|
|
595
|
+
let tomlData;
|
|
298
596
|
try {
|
|
299
597
|
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
|
-
};
|
|
598
|
+
tomlData = TOML.parse(content);
|
|
307
599
|
}
|
|
308
600
|
catch (err) {
|
|
309
601
|
error(`Failed to read TOML file: ${err.message}`);
|
|
310
602
|
process.exit(1);
|
|
311
603
|
}
|
|
604
|
+
// Issue #685: reject misnested headers before pushing.
|
|
605
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
606
|
+
if (tomlErrors.length > 0) {
|
|
607
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
const workflow = tomlData.workflow || tomlData;
|
|
611
|
+
const payload = {
|
|
612
|
+
steps: tomlData.steps || [],
|
|
613
|
+
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
614
|
+
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
615
|
+
};
|
|
312
616
|
const client = new ApiClient();
|
|
313
617
|
try {
|
|
314
618
|
const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
|
|
@@ -318,8 +622,27 @@ Examples:
|
|
|
318
622
|
}
|
|
319
623
|
success("Draft updated.");
|
|
320
624
|
keyValue("Steps", result.draft?.steps?.length || 0);
|
|
625
|
+
// Issue #687: surface server-side deprecation hint when applicable.
|
|
626
|
+
if (result.deprecation) {
|
|
627
|
+
warn(`[deprecated] ${result.deprecation}`);
|
|
628
|
+
}
|
|
321
629
|
}
|
|
322
630
|
catch (err) {
|
|
631
|
+
// Issue #987: config-model workflows reject the legacy draft slot with
|
|
632
|
+
// a 409 (the draft never executes). Surface the actionable config path
|
|
633
|
+
// cleanly instead of dumping a raw API error.
|
|
634
|
+
if (err?.statusCode === 409 && err?.code === "WORKFLOW_CONFIG_MODEL") {
|
|
635
|
+
error("This workflow has an active configuration, so the legacy draft slot is inert " +
|
|
636
|
+
"(it won't change what runs). For dev-loop edits, use 'sync push' — it writes the\n" +
|
|
637
|
+
"active config directly and activates the workflow end-to-end:\n" +
|
|
638
|
+
" primitive sync push\n" +
|
|
639
|
+
"For staged rollouts, create + activate a config, then flip the status to active\n" +
|
|
640
|
+
"(configs activate only sets the active config; it does not change the workflow status):\n" +
|
|
641
|
+
" primitive workflows configs create <workflow-id> --name <name> --from-file <path>\n" +
|
|
642
|
+
" primitive workflows configs activate <workflow-id> <config-id>\n" +
|
|
643
|
+
" primitive workflows update <workflow-id> --status active");
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
323
646
|
error(err.message);
|
|
324
647
|
process.exit(1);
|
|
325
648
|
}
|
|
@@ -327,7 +650,7 @@ Examples:
|
|
|
327
650
|
// Publish workflow
|
|
328
651
|
workflows
|
|
329
652
|
.command("publish")
|
|
330
|
-
.description("Publish the current draft as a new revision")
|
|
653
|
+
.description("Publish the current draft as a new revision (deprecated for new-model workflows; use 'workflows configs activate')")
|
|
331
654
|
.argument("<workflow-id>", "Workflow ID")
|
|
332
655
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
333
656
|
.option("--json", "Output as JSON")
|
|
@@ -343,8 +666,27 @@ Examples:
|
|
|
343
666
|
success("Workflow published.");
|
|
344
667
|
keyValue("Revision ID", result.revision?.revisionId);
|
|
345
668
|
keyValue("Published At", formatDate(result.revision?.publishedAt));
|
|
669
|
+
// Issue #687: surface server-side deprecation hint when applicable.
|
|
670
|
+
if (result.deprecation) {
|
|
671
|
+
warn(`[deprecated] ${result.deprecation}`);
|
|
672
|
+
}
|
|
346
673
|
}
|
|
347
674
|
catch (err) {
|
|
675
|
+
// Issue #987: 'publish' is inert on config-model workflows (production
|
|
676
|
+
// runs read the active config), so the server rejects with a 409.
|
|
677
|
+
// Point the user at the config primitives instead of a raw API error.
|
|
678
|
+
if (err?.statusCode === 409 && err?.code === "WORKFLOW_CONFIG_MODEL") {
|
|
679
|
+
error("This workflow has an active configuration, so 'publish' is inert " +
|
|
680
|
+
"(it bumps a legacy pointer that doesn't change what runs). For dev-loop edits, use\n" +
|
|
681
|
+
"'sync push' — it writes the active config directly and activates the workflow end-to-end:\n" +
|
|
682
|
+
" primitive sync push\n" +
|
|
683
|
+
"For staged rollouts, create + activate a config, then flip the status to active\n" +
|
|
684
|
+
"(configs activate only sets the active config; it does not change the workflow status):\n" +
|
|
685
|
+
" primitive workflows configs create <workflow-id> --name <name> --from-file <path>\n" +
|
|
686
|
+
" primitive workflows configs activate <workflow-id> <config-id>\n" +
|
|
687
|
+
" primitive workflows update <workflow-id> --status active");
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
348
690
|
error(err.message);
|
|
349
691
|
process.exit(1);
|
|
350
692
|
}
|
|
@@ -355,12 +697,18 @@ Examples:
|
|
|
355
697
|
.description("Run a preview execution of the workflow")
|
|
356
698
|
.argument("<workflow-id>", "Workflow ID")
|
|
357
699
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
358
|
-
.option("--config <config-id>", "Use specific configuration
|
|
700
|
+
.option("--config <config-id>", "Use specific configuration")
|
|
701
|
+
.option("--draft", "Force preview of the draft version, even if active is newer")
|
|
702
|
+
.option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
|
|
359
703
|
.option("--input <json>", "Root input as JSON")
|
|
360
704
|
.option("--wait", "Wait for completion and show result")
|
|
361
705
|
.option("--json", "Output as JSON")
|
|
362
706
|
.action(async (workflowId, options) => {
|
|
363
707
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
708
|
+
if (options.draft && options.active) {
|
|
709
|
+
error("--draft and --active are mutually exclusive");
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
364
712
|
let rootInput;
|
|
365
713
|
if (options.input) {
|
|
366
714
|
try {
|
|
@@ -376,7 +724,15 @@ Examples:
|
|
|
376
724
|
const result = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
377
725
|
rootInput,
|
|
378
726
|
configId: options.config,
|
|
727
|
+
useDraft: options.draft || false,
|
|
728
|
+
// Issue #687: --active forces the active config even when a newer
|
|
729
|
+
// draft exists (the inverse of --draft).
|
|
730
|
+
preferActive: options.active || false,
|
|
379
731
|
});
|
|
732
|
+
// Display warning from the server (e.g. "previewing draft because newer than active")
|
|
733
|
+
if (result.warning) {
|
|
734
|
+
warn(result.warning);
|
|
735
|
+
}
|
|
380
736
|
if (!options.wait) {
|
|
381
737
|
if (options.json) {
|
|
382
738
|
json(result);
|
|
@@ -384,26 +740,72 @@ Examples:
|
|
|
384
740
|
}
|
|
385
741
|
success("Preview started.");
|
|
386
742
|
keyValue("Instance ID", result.instanceId);
|
|
743
|
+
// Issue #687: name the side we ran so the user always knows which
|
|
744
|
+
// version produced the output (the "print the source" pattern).
|
|
745
|
+
const source = result.source;
|
|
746
|
+
if (source === "draft") {
|
|
747
|
+
info("Previewing draft version.");
|
|
748
|
+
}
|
|
749
|
+
else if (source === "active") {
|
|
750
|
+
// Issue #687 (review feedback): when configName is missing,
|
|
751
|
+
// fall back to a short ID prefix so users staging multiple
|
|
752
|
+
// unnamed configs can disambiguate.
|
|
753
|
+
const label = formatConfigSlotLabel("active config", result.configName, result.configId);
|
|
754
|
+
info(`Previewing ${label}.`);
|
|
755
|
+
}
|
|
756
|
+
else if (source === "config") {
|
|
757
|
+
const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
|
|
758
|
+
info(`Previewing ${label}.`);
|
|
759
|
+
}
|
|
387
760
|
info("Use 'workflows runs status' to check progress.");
|
|
388
761
|
return;
|
|
389
762
|
}
|
|
390
|
-
// Poll for completion
|
|
763
|
+
// Poll for completion. Issue #941: track whether the loop broke on a
|
|
764
|
+
// terminal state vs. ran to exhaustion, so a timed-out wait is
|
|
765
|
+
// distinguishable from a workflow that legitimately reported "running".
|
|
391
766
|
info("Waiting for completion...");
|
|
392
767
|
let status;
|
|
393
|
-
|
|
768
|
+
// Fixed ~60s cap (60 × 1s sleep + RTT). PRIMITIVE_PREVIEW_WAIT_ATTEMPTS
|
|
769
|
+
// overrides the iteration count for tests that exercise the
|
|
770
|
+
// poll-loop-exhaustion path without a full 60s wait; unset in normal use.
|
|
771
|
+
const maxAttempts = (() => {
|
|
772
|
+
const raw = process.env.PRIMITIVE_PREVIEW_WAIT_ATTEMPTS;
|
|
773
|
+
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
774
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 60;
|
|
775
|
+
})();
|
|
776
|
+
let reachedTerminal = false;
|
|
394
777
|
for (let i = 0; i < maxAttempts; i++) {
|
|
395
778
|
await new Promise((r) => setTimeout(r, 1000));
|
|
396
779
|
const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
|
|
397
780
|
status = statusResult.status;
|
|
398
|
-
|
|
781
|
+
const s = status?.status;
|
|
782
|
+
// Issue #941 (Codex P2): an aborted preview reports
|
|
783
|
+
// status:"terminated" from the admin preview-status endpoint, which
|
|
784
|
+
// is a real terminal state — the wait did NOT time out. The shared
|
|
785
|
+
// predicate includes it so the loop breaks and `waitTimedOut` stays
|
|
786
|
+
// false (vs. the prior check that omitted "terminated" and let the
|
|
787
|
+
// loop run to the cap, misreporting an abort as a timeout).
|
|
788
|
+
if (isPreviewWaitTerminalStatus(s)) {
|
|
789
|
+
reachedTerminal = true;
|
|
399
790
|
break;
|
|
400
791
|
}
|
|
401
792
|
}
|
|
793
|
+
// Issue #941: on poll-loop exhaustion with a still-non-terminal run,
|
|
794
|
+
// emit an explicit timeout signal and exit non-zero (124, the GNU
|
|
795
|
+
// `timeout` convention) instead of silently returning the bare
|
|
796
|
+
// non-terminal status as if the wait succeeded.
|
|
797
|
+
const waitTimedOut = !reachedTerminal;
|
|
402
798
|
if (options.json) {
|
|
403
|
-
|
|
799
|
+
// Additive field only — `status` is preserved so existing parsers
|
|
800
|
+
// don't break; `waitTimedOut` lets automation tell "still running,
|
|
801
|
+
// wait gave up" from "finished".
|
|
802
|
+
json({ instanceId: result.instanceId, status, waitTimedOut });
|
|
803
|
+
if (waitTimedOut) {
|
|
804
|
+
process.exit(124);
|
|
805
|
+
}
|
|
404
806
|
return;
|
|
405
807
|
}
|
|
406
|
-
if (status?.status === "completed") {
|
|
808
|
+
if (status?.status === "completed" || status?.status === "complete") {
|
|
407
809
|
success("Preview completed.");
|
|
408
810
|
if (status.output) {
|
|
409
811
|
console.log("\nOutput:");
|
|
@@ -417,15 +819,24 @@ Examples:
|
|
|
417
819
|
});
|
|
418
820
|
}
|
|
419
821
|
}
|
|
420
|
-
else if (status?.status === "failed") {
|
|
822
|
+
else if (status?.status === "failed" || status?.status === "errored") {
|
|
421
823
|
error("Preview failed.");
|
|
422
824
|
if (status.error) {
|
|
423
825
|
console.log("\nError:");
|
|
424
826
|
console.log(JSON.stringify(status.error, null, 2));
|
|
425
827
|
}
|
|
426
828
|
}
|
|
829
|
+
else if (status?.status === "terminated") {
|
|
830
|
+
// Issue #941 (Codex P2): the preview was aborted (e.g. via
|
|
831
|
+
// `workflows preview ... abort`, or an external terminate). This is a
|
|
832
|
+
// real terminal state — NOT a timeout — so render it distinctly and
|
|
833
|
+
// exit 0 (the wait succeeded in observing a terminal result).
|
|
834
|
+
warn("Preview was terminated (aborted before completion).");
|
|
835
|
+
}
|
|
427
836
|
else {
|
|
428
|
-
|
|
837
|
+
// Timed out without ever reaching a terminal state (issue #941).
|
|
838
|
+
error(`Wait timed out after ${maxAttempts}s; preview still running. Check status with 'workflows runs status'.`);
|
|
839
|
+
process.exit(124);
|
|
429
840
|
}
|
|
430
841
|
}
|
|
431
842
|
catch (err) {
|
|
@@ -506,9 +917,9 @@ Examples:
|
|
|
506
917
|
if (status.stepResults && status.stepResults.length > 0) {
|
|
507
918
|
divider();
|
|
508
919
|
info("Step Results:");
|
|
509
|
-
status.stepResults
|
|
510
|
-
console.log(
|
|
511
|
-
}
|
|
920
|
+
for (const line of renderRunStatusStepResults(status.stepResults)) {
|
|
921
|
+
console.log(line);
|
|
922
|
+
}
|
|
512
923
|
}
|
|
513
924
|
if (status.error) {
|
|
514
925
|
divider();
|
|
@@ -522,6 +933,462 @@ Examples:
|
|
|
522
933
|
process.exit(1);
|
|
523
934
|
}
|
|
524
935
|
});
|
|
936
|
+
// Steps for a run
|
|
937
|
+
runs
|
|
938
|
+
.command("steps")
|
|
939
|
+
.description("Show step-level details for a workflow run")
|
|
940
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
941
|
+
.argument("<run-id>", "Run ID")
|
|
942
|
+
.option("--app <app-id>", "App ID")
|
|
943
|
+
.option("--json", "Output as JSON")
|
|
944
|
+
.action(async (workflowId, runId, options) => {
|
|
945
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
946
|
+
const client = new ApiClient();
|
|
947
|
+
try {
|
|
948
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
949
|
+
if (options.json) {
|
|
950
|
+
json(items);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (!items || items.length === 0) {
|
|
954
|
+
info("No step runs found.");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
console.log(formatTable(items, [
|
|
958
|
+
{ header: "STEP", key: "stepId" },
|
|
959
|
+
{ header: "KIND", key: "stepKind" },
|
|
960
|
+
{
|
|
961
|
+
header: "STATUS",
|
|
962
|
+
key: "status",
|
|
963
|
+
format: (v) => v === "completed" ? chalk.green(v) :
|
|
964
|
+
v === "failed" ? chalk.red(v) :
|
|
965
|
+
v === "skipped" ? chalk.gray(v) :
|
|
966
|
+
v === "error_captured" ? chalk.yellow(v) : v,
|
|
967
|
+
},
|
|
968
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
969
|
+
{
|
|
970
|
+
header: "TOKENS",
|
|
971
|
+
key: "totalTokens",
|
|
972
|
+
format: (v) => v ? String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-",
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
header: "ERROR",
|
|
976
|
+
key: "error",
|
|
977
|
+
format: (v) => v ? (v.length > 60 ? v.slice(0, 57) + "..." : v) : "",
|
|
978
|
+
},
|
|
979
|
+
]));
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
error(err.message);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
// Step detail
|
|
987
|
+
runs
|
|
988
|
+
.command("step-detail")
|
|
989
|
+
.description("Show full details for a single step")
|
|
990
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
991
|
+
.argument("<run-id>", "Run ID")
|
|
992
|
+
.argument("<step-id>", "Step ID")
|
|
993
|
+
.option("--app <app-id>", "App ID")
|
|
994
|
+
.option("--json", "Output as JSON")
|
|
995
|
+
.action(async (workflowId, runId, stepId, options) => {
|
|
996
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
997
|
+
const client = new ApiClient();
|
|
998
|
+
try {
|
|
999
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
1000
|
+
const step = items.find((s) => s.stepId === stepId);
|
|
1001
|
+
if (!step) {
|
|
1002
|
+
error(`Step "${stepId}" not found in run ${runId}`);
|
|
1003
|
+
process.exit(1);
|
|
1004
|
+
}
|
|
1005
|
+
if (options.json) {
|
|
1006
|
+
json(step);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
keyValue("Step", step.stepId);
|
|
1010
|
+
keyValue("Kind", step.stepKind);
|
|
1011
|
+
keyValue("Status", formatStatus(step.status));
|
|
1012
|
+
keyValue("Duration", formatDuration(step.durationMs));
|
|
1013
|
+
keyValue("Started", formatDate(step.startedAt));
|
|
1014
|
+
keyValue("Ended", formatDate(step.endedAt));
|
|
1015
|
+
if (step.totalTokens) {
|
|
1016
|
+
keyValue("Tokens", `${step.inputTokens || 0} in / ${step.outputTokens || 0} out / ${step.totalTokens} total`);
|
|
1017
|
+
}
|
|
1018
|
+
if (step.retryCount) {
|
|
1019
|
+
keyValue("Retries", String(step.retryCount));
|
|
1020
|
+
}
|
|
1021
|
+
if (step.config) {
|
|
1022
|
+
divider();
|
|
1023
|
+
info("Config (rendered):");
|
|
1024
|
+
console.log(JSON.stringify(step.config, null, 2));
|
|
1025
|
+
}
|
|
1026
|
+
if (step.rawConfig) {
|
|
1027
|
+
divider();
|
|
1028
|
+
info("Config (raw):");
|
|
1029
|
+
console.log(JSON.stringify(step.rawConfig, null, 2));
|
|
1030
|
+
}
|
|
1031
|
+
if (step.input) {
|
|
1032
|
+
divider();
|
|
1033
|
+
info("Input:");
|
|
1034
|
+
console.log(JSON.stringify(step.input, null, 2));
|
|
1035
|
+
}
|
|
1036
|
+
if (step.output) {
|
|
1037
|
+
divider();
|
|
1038
|
+
info("Output:");
|
|
1039
|
+
console.log(JSON.stringify(step.output, null, 2));
|
|
1040
|
+
}
|
|
1041
|
+
if (step.error) {
|
|
1042
|
+
divider();
|
|
1043
|
+
error("Error: " + step.error);
|
|
1044
|
+
if (step.errorDetails) {
|
|
1045
|
+
console.log(step.errorDetails);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (step.templateWarnings && step.templateWarnings.length > 0) {
|
|
1049
|
+
divider();
|
|
1050
|
+
warn("Template Warnings:");
|
|
1051
|
+
step.templateWarnings.forEach((w) => {
|
|
1052
|
+
console.log(` ${w.expression}: ${w.message}`);
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (step.context) {
|
|
1056
|
+
divider();
|
|
1057
|
+
info("Context Snapshot:");
|
|
1058
|
+
console.log(JSON.stringify(step.context, null, 2));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
error(err.message);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
// Integration call logs for a run (cross-pivot from workflow-run to
|
|
1067
|
+
// integrations.logs — see issue #699).
|
|
1068
|
+
runs
|
|
1069
|
+
.command("logs")
|
|
1070
|
+
.description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
|
|
1071
|
+
.argument("<run-id>", "Workflow run ID")
|
|
1072
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1073
|
+
.option("--limit <n>", "Number of logs to show", "100")
|
|
1074
|
+
.option("--json", "Output as JSON")
|
|
1075
|
+
.action(async (runId, options) => {
|
|
1076
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1077
|
+
const client = new ApiClient();
|
|
1078
|
+
try {
|
|
1079
|
+
const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
|
|
1080
|
+
if (options.json) {
|
|
1081
|
+
json(logs);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (!logs || logs.length === 0) {
|
|
1085
|
+
info("No integration calls found for this run.");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
console.log(formatTable(logs, [
|
|
1089
|
+
{ header: "TIME", key: "timestamp", format: formatDate },
|
|
1090
|
+
{ header: "STEP", key: "stepId", format: (v) => v || "" },
|
|
1091
|
+
{
|
|
1092
|
+
header: "INTEGRATION",
|
|
1093
|
+
key: "integrationKey",
|
|
1094
|
+
format: (v) => v || "",
|
|
1095
|
+
},
|
|
1096
|
+
{ header: "METHOD", key: "method" },
|
|
1097
|
+
{ header: "PATH", key: "path" },
|
|
1098
|
+
{ header: "STATUS", key: "status" },
|
|
1099
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
1100
|
+
{ header: "TRACE", key: "traceId", format: formatId },
|
|
1101
|
+
]));
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
error(err.message);
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
// Error summary
|
|
1109
|
+
runs
|
|
1110
|
+
.command("error")
|
|
1111
|
+
.description("Show error details for a failed workflow run")
|
|
1112
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
1113
|
+
.argument("<run-id>", "Run ID")
|
|
1114
|
+
.option("--app <app-id>", "App ID")
|
|
1115
|
+
.option("--json", "Output as JSON")
|
|
1116
|
+
.action(async (workflowId, runId, options) => {
|
|
1117
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1118
|
+
const client = new ApiClient();
|
|
1119
|
+
try {
|
|
1120
|
+
const [runResult, stepsResult] = await Promise.all([
|
|
1121
|
+
client.getWorkflowRunStatus(resolvedAppId, workflowId, runId),
|
|
1122
|
+
client.getWorkflowStepRuns(resolvedAppId, workflowId, runId),
|
|
1123
|
+
]);
|
|
1124
|
+
const failedStep = stepsResult.items.find((s) => s.status === "failed");
|
|
1125
|
+
const errorInfo = {
|
|
1126
|
+
runId,
|
|
1127
|
+
status: runResult.run?.status,
|
|
1128
|
+
errorMessage: runResult.run?.errorMessage || runResult.instanceStatus?.error?.message,
|
|
1129
|
+
failedStep: failedStep ? {
|
|
1130
|
+
stepId: failedStep.stepId,
|
|
1131
|
+
stepKind: failedStep.stepKind,
|
|
1132
|
+
error: failedStep.error,
|
|
1133
|
+
errorDetails: failedStep.errorDetails,
|
|
1134
|
+
input: failedStep.input,
|
|
1135
|
+
config: failedStep.config,
|
|
1136
|
+
} : null,
|
|
1137
|
+
};
|
|
1138
|
+
if (options.json) {
|
|
1139
|
+
json(errorInfo);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
keyValue("Run ID", runId);
|
|
1143
|
+
keyValue("Status", formatStatus(errorInfo.status));
|
|
1144
|
+
if (errorInfo.errorMessage) {
|
|
1145
|
+
divider();
|
|
1146
|
+
error("Error: " + errorInfo.errorMessage);
|
|
1147
|
+
}
|
|
1148
|
+
if (failedStep) {
|
|
1149
|
+
divider();
|
|
1150
|
+
info(`Failed Step: ${failedStep.stepId} (${failedStep.stepKind})`);
|
|
1151
|
+
if (failedStep.error) {
|
|
1152
|
+
error(" " + failedStep.error);
|
|
1153
|
+
}
|
|
1154
|
+
if (failedStep.errorDetails) {
|
|
1155
|
+
console.log(failedStep.errorDetails);
|
|
1156
|
+
}
|
|
1157
|
+
if (failedStep.input) {
|
|
1158
|
+
divider();
|
|
1159
|
+
info("Step Input:");
|
|
1160
|
+
console.log(JSON.stringify(failedStep.input, null, 2));
|
|
1161
|
+
}
|
|
1162
|
+
if (failedStep.config) {
|
|
1163
|
+
divider();
|
|
1164
|
+
info("Step Config:");
|
|
1165
|
+
console.log(JSON.stringify(failedStep.config, null, 2));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
else {
|
|
1169
|
+
info("No failed step found in step runs.");
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
error(err.message);
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
// Failures list
|
|
1178
|
+
runs
|
|
1179
|
+
.command("failures")
|
|
1180
|
+
.description("List recent workflow run failures")
|
|
1181
|
+
.argument("<workflow-id>", "Workflow ID")
|
|
1182
|
+
.option("--app <app-id>", "App ID")
|
|
1183
|
+
.option("--limit <n>", "Number of failures to show", "10")
|
|
1184
|
+
.option("--json", "Output as JSON")
|
|
1185
|
+
.action(async (workflowId, options) => {
|
|
1186
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1187
|
+
const client = new ApiClient();
|
|
1188
|
+
try {
|
|
1189
|
+
const { items } = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1190
|
+
status: "failed",
|
|
1191
|
+
limit: parseInt(options.limit),
|
|
1192
|
+
});
|
|
1193
|
+
if (options.json) {
|
|
1194
|
+
json(items);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (!items || items.length === 0) {
|
|
1198
|
+
info("No failures found.");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
console.log(formatTable(items, [
|
|
1202
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
1203
|
+
{ header: "ERROR", key: "errorMessage", format: (v) => v ? (v.length > 50 ? v.slice(0, 47) + "..." : v) : "-" },
|
|
1204
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
1205
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
1206
|
+
]));
|
|
1207
|
+
}
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
error(err.message);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
// ── Iterations: inspect / reset iterate-users singleton iterations (#1209) ──
|
|
1214
|
+
const iterations = workflows
|
|
1215
|
+
.command("iterations")
|
|
1216
|
+
.description("Inspect and reset iterate-users iterations");
|
|
1217
|
+
iterations
|
|
1218
|
+
.command("list")
|
|
1219
|
+
.description("List iterate-users iterations for the app")
|
|
1220
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1221
|
+
.option("--json", "Output as JSON")
|
|
1222
|
+
.action(async (options) => {
|
|
1223
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1224
|
+
const client = new ApiClient();
|
|
1225
|
+
try {
|
|
1226
|
+
const { items } = await client.listIterations(resolvedAppId);
|
|
1227
|
+
if (options.json) {
|
|
1228
|
+
json(items);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (!items || items.length === 0) {
|
|
1232
|
+
info("No iterations found.");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
console.log(formatTable(items, [
|
|
1236
|
+
{ header: "ITERATION", key: "iterationName" },
|
|
1237
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
1238
|
+
{ header: "ACQUIRED", key: "lastAcquireMode", format: (v) => v || "" },
|
|
1239
|
+
{ header: "PROCESSED", key: "totalProcessed" },
|
|
1240
|
+
{ header: "FAILED", key: "failedCount" },
|
|
1241
|
+
{ header: "LAST RUN", key: "lastRunAt", format: formatDate },
|
|
1242
|
+
]));
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
error(err.message);
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
iterations
|
|
1250
|
+
.command("get")
|
|
1251
|
+
.description("Show details for a single iterate-users iteration")
|
|
1252
|
+
.argument("<name>", "Iteration name")
|
|
1253
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1254
|
+
.option("--json", "Output as JSON")
|
|
1255
|
+
.action(async (name, options) => {
|
|
1256
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1257
|
+
const client = new ApiClient();
|
|
1258
|
+
try {
|
|
1259
|
+
const it = await client.getIteration(resolvedAppId, name);
|
|
1260
|
+
if (options.json) {
|
|
1261
|
+
json(it);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
keyValue("Iteration", it.iterationName);
|
|
1265
|
+
keyValue("Status", formatStatus(it.status));
|
|
1266
|
+
keyValue("Acquired as", it.lastAcquireMode || "(unknown)");
|
|
1267
|
+
keyValue("Run ID", it.runId);
|
|
1268
|
+
keyValue("Instance ID", it.instanceId);
|
|
1269
|
+
keyValue("Started", formatDate(it.startedAt));
|
|
1270
|
+
keyValue("Last run", formatDate(it.lastRunAt));
|
|
1271
|
+
keyValue("Lock expires", formatDate(it.lockExpiresAt));
|
|
1272
|
+
keyValue("Processed", String(it.totalProcessed ?? 0));
|
|
1273
|
+
keyValue("Succeeded", String(it.succeededCount ?? 0));
|
|
1274
|
+
keyValue("Failed (total)", String(it.failedCount ?? 0));
|
|
1275
|
+
keyValue("Skipped", String(it.skippedCount ?? 0));
|
|
1276
|
+
if (it.lastError) {
|
|
1277
|
+
keyValue("Last error", it.lastError);
|
|
1278
|
+
}
|
|
1279
|
+
const sample = Array.isArray(it.failedUserIdsSample)
|
|
1280
|
+
? it.failedUserIdsSample
|
|
1281
|
+
: [];
|
|
1282
|
+
if (sample.length > 0) {
|
|
1283
|
+
const label = it.failedUserIdsTruncated
|
|
1284
|
+
? `Failed user IDs (sample, truncated; ${it.failedCount} total)`
|
|
1285
|
+
: `Failed user IDs (sample)`;
|
|
1286
|
+
keyValue(label, sample.join(", "));
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
error(err.message);
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
iterations
|
|
1295
|
+
.command("reset")
|
|
1296
|
+
.description("Reset a terminal (completed/failed) iteration so the next trigger runs fresh")
|
|
1297
|
+
.argument("<name>", "Iteration name")
|
|
1298
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1299
|
+
.option("--json", "Output as JSON")
|
|
1300
|
+
.action(async (name, options) => {
|
|
1301
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1302
|
+
const client = new ApiClient();
|
|
1303
|
+
try {
|
|
1304
|
+
const res = await client.resetIteration(resolvedAppId, name);
|
|
1305
|
+
if (options.json) {
|
|
1306
|
+
json(res);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
success(`Reset iteration "${name}" (was ${res.previousStatus}). The next trigger will run fresh.`);
|
|
1310
|
+
}
|
|
1311
|
+
catch (err) {
|
|
1312
|
+
// A running iteration is refused with 409 + lock metadata — surface a
|
|
1313
|
+
// clear message rather than a raw error.
|
|
1314
|
+
error(err.message);
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
// Analytics overview
|
|
1319
|
+
const analyticsCmd = workflows.command("analytics").description("View workflow performance analytics");
|
|
1320
|
+
analyticsCmd
|
|
1321
|
+
.command("overview")
|
|
1322
|
+
.description("View workflow performance metrics")
|
|
1323
|
+
.option("--app <app-id>", "App ID")
|
|
1324
|
+
.option("--days <n>", "Time window in days", "7")
|
|
1325
|
+
.option("--json", "Output as JSON")
|
|
1326
|
+
.action(async (options) => {
|
|
1327
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1328
|
+
const client = new ApiClient();
|
|
1329
|
+
try {
|
|
1330
|
+
const result = await client.getWorkflowAnalytics(resolvedAppId, {
|
|
1331
|
+
windowDays: parseInt(options.days),
|
|
1332
|
+
});
|
|
1333
|
+
if (options.json) {
|
|
1334
|
+
json(result);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
info(`Workflow Analytics (${result.windowDays || options.days} days)`);
|
|
1338
|
+
console.log();
|
|
1339
|
+
keyValue("Total Runs", String(result.totals?.totalRuns ?? 0));
|
|
1340
|
+
keyValue("Success Rate", `${((result.totals?.successRate ?? 0) * 100).toFixed(1)}%`);
|
|
1341
|
+
keyValue("Avg Duration", formatDuration(result.totals?.avgDurationMs));
|
|
1342
|
+
if (result.tokenUsage) {
|
|
1343
|
+
console.log();
|
|
1344
|
+
info("Token Usage:");
|
|
1345
|
+
keyValue(" Input Tokens", String(result.tokenUsage.totalInputTokens ?? 0));
|
|
1346
|
+
keyValue(" Output Tokens", String(result.tokenUsage.totalOutputTokens ?? 0));
|
|
1347
|
+
keyValue(" Total Tokens", String(result.tokenUsage.totalTokens ?? 0));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
catch (err) {
|
|
1351
|
+
error(err.message);
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
analyticsCmd
|
|
1356
|
+
.command("top")
|
|
1357
|
+
.description("View top workflows by usage")
|
|
1358
|
+
.option("--app <app-id>", "App ID")
|
|
1359
|
+
.option("--days <n>", "Time window in days", "7")
|
|
1360
|
+
.option("--limit <n>", "Number of workflows to show", "10")
|
|
1361
|
+
.option("--json", "Output as JSON")
|
|
1362
|
+
.action(async (options) => {
|
|
1363
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1364
|
+
const client = new ApiClient();
|
|
1365
|
+
try {
|
|
1366
|
+
const result = await client.getTopWorkflows(resolvedAppId, {
|
|
1367
|
+
windowDays: parseInt(options.days),
|
|
1368
|
+
limit: parseInt(options.limit),
|
|
1369
|
+
});
|
|
1370
|
+
if (options.json) {
|
|
1371
|
+
json(result);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (!result.items || result.items.length === 0) {
|
|
1375
|
+
info("No workflow activity in this period.");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
console.log(formatTable(result.items, [
|
|
1379
|
+
{ header: "WORKFLOW", key: "workflow" },
|
|
1380
|
+
{ header: "RUNS", key: "executions" },
|
|
1381
|
+
{ header: "SUCCESS", key: "success_pct", format: (v) => `${v}%` },
|
|
1382
|
+
{ header: "P50", key: "p50", format: (v) => formatDuration(v) },
|
|
1383
|
+
{ header: "P95", key: "p95", format: (v) => formatDuration(v) },
|
|
1384
|
+
{ header: "TOKENS", key: "total_tokens", format: (v) => v ? String(Math.round(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-" },
|
|
1385
|
+
]));
|
|
1386
|
+
}
|
|
1387
|
+
catch (err) {
|
|
1388
|
+
error(err.message);
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
525
1392
|
// ============================================
|
|
526
1393
|
// CONFIGS SUBCOMMAND
|
|
527
1394
|
// ============================================
|
|
@@ -593,12 +1460,12 @@ Examples:
|
|
|
593
1460
|
json(config);
|
|
594
1461
|
return;
|
|
595
1462
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1463
|
+
printResult("Config ID", config.configId);
|
|
1464
|
+
printResult("Name", config.configName);
|
|
1465
|
+
printResult("Description", config.description || "-");
|
|
1466
|
+
printResult("Status", formatStatus(config.status));
|
|
1467
|
+
printResult("Created", formatDate(config.createdAt));
|
|
1468
|
+
printResult("Modified", formatDate(config.modifiedAt));
|
|
602
1469
|
if (config.steps && config.steps.length > 0) {
|
|
603
1470
|
divider();
|
|
604
1471
|
info(`Steps (${config.steps.length}):`);
|
|
@@ -630,15 +1497,22 @@ Examples:
|
|
|
630
1497
|
}
|
|
631
1498
|
let steps = [];
|
|
632
1499
|
if (options.fromFile) {
|
|
1500
|
+
let tomlData;
|
|
633
1501
|
try {
|
|
634
1502
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
635
|
-
|
|
636
|
-
steps = tomlData.steps || [];
|
|
1503
|
+
tomlData = TOML.parse(content);
|
|
637
1504
|
}
|
|
638
1505
|
catch (err) {
|
|
639
1506
|
error(`Failed to read TOML file: ${err.message}`);
|
|
640
1507
|
process.exit(1);
|
|
641
1508
|
}
|
|
1509
|
+
// Issue #685: reject misnested headers before pushing.
|
|
1510
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
1511
|
+
if (tomlErrors.length > 0) {
|
|
1512
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
steps = tomlData.steps || [];
|
|
642
1516
|
}
|
|
643
1517
|
const client = new ApiClient();
|
|
644
1518
|
try {
|
|
@@ -678,15 +1552,22 @@ Examples:
|
|
|
678
1552
|
if (options.description !== undefined)
|
|
679
1553
|
payload.description = options.description;
|
|
680
1554
|
if (options.fromFile) {
|
|
1555
|
+
let tomlData;
|
|
681
1556
|
try {
|
|
682
1557
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
683
|
-
|
|
684
|
-
payload.steps = tomlData.steps || [];
|
|
1558
|
+
tomlData = TOML.parse(content);
|
|
685
1559
|
}
|
|
686
1560
|
catch (err) {
|
|
687
1561
|
error(`Failed to read TOML file: ${err.message}`);
|
|
688
1562
|
process.exit(1);
|
|
689
1563
|
}
|
|
1564
|
+
// Issue #685: reject misnested headers before pushing.
|
|
1565
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
1566
|
+
if (tomlErrors.length > 0) {
|
|
1567
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
payload.steps = tomlData.steps || [];
|
|
690
1571
|
}
|
|
691
1572
|
if (Object.keys(payload).length === 0) {
|
|
692
1573
|
error("No update options specified. Use --name, --description, or --from-file.");
|
|
@@ -772,15 +1653,14 @@ Examples:
|
|
|
772
1653
|
.action(async (workflowId, configId, options) => {
|
|
773
1654
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
774
1655
|
if (!options.yes) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
]);
|
|
1656
|
+
let confirm;
|
|
1657
|
+
try {
|
|
1658
|
+
confirm = await confirmPrompt(`Are you sure you want to archive configuration ${configId}?`);
|
|
1659
|
+
}
|
|
1660
|
+
catch (err) {
|
|
1661
|
+
error(err.message);
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
}
|
|
784
1664
|
if (!confirm) {
|
|
785
1665
|
info("Cancelled.");
|
|
786
1666
|
return;
|
|
@@ -925,13 +1805,15 @@ Examples:
|
|
|
925
1805
|
json(result);
|
|
926
1806
|
return;
|
|
927
1807
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1808
|
+
printResult("Test Case ID", result.testCaseId);
|
|
1809
|
+
printResult("Name", result.name);
|
|
1810
|
+
printResult("Created", formatDate(result.createdAt));
|
|
931
1811
|
divider();
|
|
932
1812
|
info("Input Variables:");
|
|
933
1813
|
try {
|
|
934
|
-
const vars =
|
|
1814
|
+
const vars = typeof result.inputVariables === 'string'
|
|
1815
|
+
? JSON.parse(result.inputVariables || "{}")
|
|
1816
|
+
: (result.inputVariables || {});
|
|
935
1817
|
console.log(JSON.stringify(vars, null, 2));
|
|
936
1818
|
}
|
|
937
1819
|
catch {
|
|
@@ -939,13 +1821,16 @@ Examples:
|
|
|
939
1821
|
}
|
|
940
1822
|
if (result.expectedOutputPattern) {
|
|
941
1823
|
divider();
|
|
942
|
-
|
|
1824
|
+
printResult("Expected Pattern", result.expectedOutputPattern);
|
|
943
1825
|
}
|
|
944
1826
|
if (result.expectedOutputContains) {
|
|
945
1827
|
divider();
|
|
946
1828
|
info("Expected Contains:");
|
|
947
1829
|
try {
|
|
948
|
-
|
|
1830
|
+
const contains = typeof result.expectedOutputContains === 'string'
|
|
1831
|
+
? JSON.parse(result.expectedOutputContains)
|
|
1832
|
+
: result.expectedOutputContains;
|
|
1833
|
+
console.log(JSON.stringify(contains, null, 2));
|
|
949
1834
|
}
|
|
950
1835
|
catch {
|
|
951
1836
|
console.log(result.expectedOutputContains);
|
|
@@ -955,7 +1840,10 @@ Examples:
|
|
|
955
1840
|
divider();
|
|
956
1841
|
info("Expected JSON Subset:");
|
|
957
1842
|
try {
|
|
958
|
-
|
|
1843
|
+
const subset = typeof result.expectedJsonSubset === 'string'
|
|
1844
|
+
? JSON.parse(result.expectedJsonSubset)
|
|
1845
|
+
: result.expectedJsonSubset;
|
|
1846
|
+
console.log(JSON.stringify(subset, null, 2));
|
|
959
1847
|
}
|
|
960
1848
|
catch {
|
|
961
1849
|
console.log(result.expectedJsonSubset);
|
|
@@ -1056,15 +1944,14 @@ Examples:
|
|
|
1056
1944
|
.action(async (workflowId, testCaseId, options) => {
|
|
1057
1945
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1058
1946
|
if (!options.yes) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
{
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
]);
|
|
1947
|
+
let confirm;
|
|
1948
|
+
try {
|
|
1949
|
+
confirm = await confirmPrompt(`Are you sure you want to delete test case ${testCaseId}?`);
|
|
1950
|
+
}
|
|
1951
|
+
catch (err) {
|
|
1952
|
+
error(err.message);
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1068
1955
|
if (!confirm) {
|
|
1069
1956
|
info("Cancelled.");
|
|
1070
1957
|
return;
|
|
@@ -1337,9 +2224,10 @@ Examples:
|
|
|
1337
2224
|
while (result.status === "running") {
|
|
1338
2225
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1339
2226
|
result = await fetchStatus();
|
|
1340
|
-
|
|
2227
|
+
// Progress goes to stderr so it can't corrupt JSON on stdout under --json.
|
|
2228
|
+
progress(` Completed: ${result.completed}/${result.results?.length || 0} `);
|
|
1341
2229
|
}
|
|
1342
|
-
|
|
2230
|
+
progressEnd();
|
|
1343
2231
|
}
|
|
1344
2232
|
if (options.json) {
|
|
1345
2233
|
json(result);
|
|
@@ -1383,15 +2271,14 @@ Examples:
|
|
|
1383
2271
|
.action(async (workflowId, batchId, options) => {
|
|
1384
2272
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1385
2273
|
if (!options.yes) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
{
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
]);
|
|
2274
|
+
let confirm;
|
|
2275
|
+
try {
|
|
2276
|
+
confirm = await confirmPrompt(`Are you sure you want to cancel batch ${batchId}?`);
|
|
2277
|
+
}
|
|
2278
|
+
catch (err) {
|
|
2279
|
+
error(err.message);
|
|
2280
|
+
process.exit(1);
|
|
2281
|
+
}
|
|
1395
2282
|
if (!confirm) {
|
|
1396
2283
|
info("Cancelled.");
|
|
1397
2284
|
return;
|
|
@@ -1555,15 +2442,14 @@ Examples:
|
|
|
1555
2442
|
.action(async (workflowId, testCaseId, filename, options) => {
|
|
1556
2443
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1557
2444
|
if (!options.yes) {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
{
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
]);
|
|
2445
|
+
let confirm;
|
|
2446
|
+
try {
|
|
2447
|
+
confirm = await confirmPrompt(`Are you sure you want to delete attachment "${filename}"?`);
|
|
2448
|
+
}
|
|
2449
|
+
catch (err) {
|
|
2450
|
+
error(err.message);
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
1567
2453
|
if (!confirm) {
|
|
1568
2454
|
info("Cancelled.");
|
|
1569
2455
|
return;
|