primitive-admin 1.0.53 → 1.0.55
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 +38 -10
- package/dist/bin/primitive.js +8 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/apps.js +54 -2
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +16 -6
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +84 -7
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +3 -3
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collections.js +12 -9
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/databases.js +220 -82
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +77 -0
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +1 -9
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +12 -6
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +161 -0
- package/dist/src/commands/guides.js +502 -84
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/integrations.js +2 -2
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +344 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.js +2 -2
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -0
- package/dist/src/commands/rule-sets.js +25 -3
- package/dist/src/commands/rule-sets.js.map +1 -1
- 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/settings.d.ts +15 -0
- package/dist/src/commands/settings.js +102 -0
- package/dist/src/commands/settings.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +105 -0
- package/dist/src/commands/sync-app-settings.js +339 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +204 -9
- package/dist/src/commands/sync.js +1706 -371
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +110 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/webhooks.js +17 -3
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +30 -0
- package/dist/src/commands/workflows.js +667 -59
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +122 -18
- package/dist/src/lib/api-client.js +131 -2
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +108 -0
- package/dist/src/lib/app-settings-descriptor.js +250 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -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/codegen-shared/generatedFiles.d.ts +76 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +144 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +68 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +16 -0
- package/dist/src/lib/config.js +41 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/constants.d.ts +9 -0
- package/dist/src/lib/constants.js +9 -0
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/csv.d.ts +1 -2
- package/dist/src/lib/csv.js +3 -8
- package/dist/src/lib/csv.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +21 -65
- package/dist/src/lib/db-codegen/dbGenerator.js +117 -321
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +37 -0
- package/dist/src/lib/db-codegen/dbNaming.js +70 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +165 -5
- package/dist/src/lib/db-codegen/dbTemplates.js +292 -16
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +7 -3
- package/dist/src/lib/db-codegen/dbTsTypes.js +23 -1
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +517 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +101 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +200 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +43 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/init-config.js +2 -2
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/output.d.ts +25 -5
- package/dist/src/lib/output.js +32 -4
- package/dist/src/lib/output.js.map +1 -1
- 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/swift-codegen/dbGenerator.d.ts +68 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +380 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +84 -0
- package/dist/src/lib/swift-codegen/generator.js +178 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +225 -0
- package/dist/src/lib/sync-resource-types.js +394 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +10 -19
- package/dist/src/lib/toml-database-config.js +28 -59
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +147 -0
- package/dist/src/lib/toml-metadata-config.js +463 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +34 -0
- package/dist/src/lib/toml-params-validator.js +118 -3
- package/dist/src/lib/toml-params-validator.js.map +1 -1
- package/dist/src/lib/workflow-apply.d.ts +86 -0
- package/dist/src/lib/workflow-apply.js +127 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +87 -0
- package/dist/src/lib/workflow-codegen/generator.js +290 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +85 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +71 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +64 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +282 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +3 -3
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-payload.d.ts +68 -0
- package/dist/src/lib/workflow-payload.js +150 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +1 -1
- package/dist/src/lib/workflow-toml-validator.js +37 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +11 -0
- package/dist/src/validators.d.ts +4 -3
- package/dist/src/validators.js +6 -5
- package/dist/src/validators.js.map +1 -1
- package/package.json +7 -3
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, statSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, readdirSync, } from "fs";
|
|
2
2
|
import { basename } from "path";
|
|
3
|
-
import * as
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { parseConfigToml, stringifyConfigToml } from "../lib/config-toml.js";
|
|
4
5
|
import { lookup as mimeLookup } from "mime-types";
|
|
6
|
+
import { generateWorkflowTypes, } from "../lib/workflow-codegen/generator.js";
|
|
7
|
+
import { generateWorkflowSwiftTypes } from "../lib/swift-codegen/generator.js";
|
|
5
8
|
import { ApiClient } from "../lib/api-client.js";
|
|
6
9
|
import { resolveAppId } from "../lib/config.js";
|
|
10
|
+
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
7
11
|
import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
|
|
8
12
|
import { expandWorkflow } from "../lib/workflow-fragments.js";
|
|
13
|
+
import { buildWorkflowPayloadFromToml } from "../lib/workflow-payload.js";
|
|
14
|
+
import { applyWorkflowBody, workflowKeyMismatchError, parseCapabilitiesFlag, } from "../lib/workflow-apply.js";
|
|
9
15
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
10
16
|
import chalk from "chalk";
|
|
11
17
|
import { success, error, printApiError, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, progress, progressEnd, } from "../lib/output.js";
|
|
@@ -88,6 +94,75 @@ export function renderRunStatusStepResults(stepResults) {
|
|
|
88
94
|
}
|
|
89
95
|
return lines;
|
|
90
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* #1367 — annotate step-run rows with the inter-step gap: the wall-clock time
|
|
99
|
+
* between the previous step's `endedAt` and this step's `startedAt`. Sorts by
|
|
100
|
+
* `startedAt` when every row has a parseable one (tie-break `stepIndex`),
|
|
101
|
+
* otherwise by `stepIndex` for the whole list — the ordering mode must be
|
|
102
|
+
* chosen once for all rows, not per pair: a per-pair timestamp-vs-index
|
|
103
|
+
* fallback is not transitive when some rows lack a timestamp (A<B by index,
|
|
104
|
+
* B<C by index, C<A by time), and a non-transitive comparator leaves
|
|
105
|
+
* Array.prototype.sort free to return an arbitrary order, computing gaps
|
|
106
|
+
* against the wrong predecessor. The first step has no predecessor, so its
|
|
107
|
+
* `gapMs` is `null`. A gap is only meaningful when both adjacent timestamps
|
|
108
|
+
* parse and the gap is positive. When the immediate predecessor lacks a valid
|
|
109
|
+
* `endedAt`, the gap is left `null` rather than measured from an older,
|
|
110
|
+
* non-adjacent step — otherwise a later step could raise a false stall
|
|
111
|
+
* warning against a step it does not actually follow. Exported for
|
|
112
|
+
* testability.
|
|
113
|
+
*/
|
|
114
|
+
export function annotateStepGaps(steps) {
|
|
115
|
+
const rows = [...(steps || [])];
|
|
116
|
+
const allTimed = rows.every((r) => r?.startedAt && !Number.isNaN(new Date(r.startedAt).getTime()));
|
|
117
|
+
rows.sort((a, b) => {
|
|
118
|
+
if (allTimed) {
|
|
119
|
+
const sa = new Date(a.startedAt).getTime();
|
|
120
|
+
const sb = new Date(b.startedAt).getTime();
|
|
121
|
+
if (sa !== sb)
|
|
122
|
+
return sa - sb;
|
|
123
|
+
}
|
|
124
|
+
return (a?.stepIndex ?? 0) - (b?.stepIndex ?? 0);
|
|
125
|
+
});
|
|
126
|
+
let prevEnded = null;
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const started = row?.startedAt ? new Date(row.startedAt).getTime() : NaN;
|
|
129
|
+
if (prevEnded != null && !Number.isNaN(started)) {
|
|
130
|
+
const gap = started - prevEnded;
|
|
131
|
+
row.gapMs = gap > 0 ? gap : 0;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
row.gapMs = null;
|
|
135
|
+
}
|
|
136
|
+
// Only genuinely adjacent steps yield a gap: if this step lacks a valid
|
|
137
|
+
// `endedAt`, clear the predecessor so the NEXT step's gap is left null
|
|
138
|
+
// instead of being measured from a non-adjacent earlier step.
|
|
139
|
+
const ended = row?.endedAt ? new Date(row.endedAt).getTime() : NaN;
|
|
140
|
+
prevEnded = Number.isNaN(ended) ? null : ended;
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* #1367 — compute min / p50 / p95 / max over a set of latency samples (ms).
|
|
146
|
+
* Ignores non-finite values. Returns `null` when there are no valid samples.
|
|
147
|
+
* Percentiles use nearest-rank on the sorted samples. Exported for testability.
|
|
148
|
+
*/
|
|
149
|
+
export function summarizeLatencyDistribution(values) {
|
|
150
|
+
const nums = (values || []).filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
151
|
+
if (nums.length === 0)
|
|
152
|
+
return null;
|
|
153
|
+
nums.sort((a, b) => a - b);
|
|
154
|
+
const pct = (p) => {
|
|
155
|
+
const rank = Math.ceil((p / 100) * nums.length);
|
|
156
|
+
return nums[Math.min(nums.length - 1, Math.max(0, rank - 1))];
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
count: nums.length,
|
|
160
|
+
min: nums[0],
|
|
161
|
+
p50: pct(50),
|
|
162
|
+
p95: pct(95),
|
|
163
|
+
max: nums[nums.length - 1],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
91
166
|
/**
|
|
92
167
|
* Issue #687 (review feedback): render an unambiguous label for a workflow
|
|
93
168
|
* config slot ("active config", "draft config", etc.) when the slot may be
|
|
@@ -177,7 +252,7 @@ Examples:
|
|
|
177
252
|
let tomlData;
|
|
178
253
|
try {
|
|
179
254
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
180
|
-
tomlData =
|
|
255
|
+
tomlData = parseConfigToml(content);
|
|
181
256
|
}
|
|
182
257
|
catch (err) {
|
|
183
258
|
error(`Failed to read TOML file: ${err.message}`);
|
|
@@ -194,39 +269,15 @@ Examples:
|
|
|
194
269
|
process.exit(1);
|
|
195
270
|
}
|
|
196
271
|
const workflow = tomlData.workflow || tomlData;
|
|
272
|
+
// #1177: build the `[workflow]`-derived fields through the shared
|
|
273
|
+
// builder so this create path can't drift from `sync push` again
|
|
274
|
+
// (`status`, `capabilities`, `perAppMax*`, `queueTtlSeconds`, `runAs`,
|
|
275
|
+
// `accessRule`, `syncCallable` all flow from one field list). Structural
|
|
276
|
+
// fields (`workflowKey`, `steps`) are attached here.
|
|
197
277
|
payload = {
|
|
198
278
|
workflowKey: workflow.key || workflow.workflowKey,
|
|
199
|
-
name: workflow.name,
|
|
200
|
-
description: workflow.description,
|
|
201
279
|
steps: tomlData.steps || [],
|
|
202
|
-
|
|
203
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
204
|
-
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
205
|
-
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
206
|
-
perAppMaxRunning: workflow.perAppMaxRunning,
|
|
207
|
-
perAppMaxQueued: workflow.perAppMaxQueued,
|
|
208
|
-
queueTtlSeconds: workflow.queueTtlSeconds,
|
|
209
|
-
dequeueOrder: workflow.dequeueOrder,
|
|
210
|
-
requiresClientApply: workflow.requiresClientApply,
|
|
211
|
-
// #807: align create --from-file with sync/update — read syncCallable
|
|
212
|
-
// from the TOML so all three entry points behave identically.
|
|
213
|
-
syncCallable: workflow.syncCallable,
|
|
214
|
-
// #571: carry accessRule through so a TOML's access-control rule
|
|
215
|
-
// lands on create instead of being silently dropped. On create the
|
|
216
|
-
// server default is already null, so coalesce a falsy/absent rule to
|
|
217
|
-
// undefined (omit) rather than sending "" — both clear, but undefined
|
|
218
|
-
// is the precise "no rule declared" signal.
|
|
219
|
-
accessRule: workflow.accessRule || undefined,
|
|
220
|
-
// #1173: forward the workflow principal mode (`runAs`) from the TOML,
|
|
221
|
-
// matching `sync push` create (sync.ts ~3345). Without this, a
|
|
222
|
-
// `runAs="system"` TOML reached the server as non-system, so the
|
|
223
|
-
// identity validator correctly rejected system-only steps (e.g.
|
|
224
|
-
// `iterate-users`). Create semantics — `workflow.runAs || undefined`
|
|
225
|
-
// (absent/empty → server default), NOT the presence-aware clearing
|
|
226
|
-
// form that `sync push` update uses. `capabilities` is intentionally
|
|
227
|
-
// NOT forwarded: `sync push` doesn't serialize/persist it today, so
|
|
228
|
-
// forwarding it only here would create a new asymmetry (see #1173).
|
|
229
|
-
runAs: workflow.runAs || undefined,
|
|
280
|
+
...buildWorkflowPayloadFromToml(workflow, { mode: "create" }),
|
|
230
281
|
};
|
|
231
282
|
}
|
|
232
283
|
else {
|
|
@@ -268,7 +319,7 @@ Examples:
|
|
|
268
319
|
workflows
|
|
269
320
|
.command("get")
|
|
270
321
|
.description("Get workflow details")
|
|
271
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
322
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
272
323
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
273
324
|
.option("--json", "Output as JSON")
|
|
274
325
|
.action(async (workflowId, options) => {
|
|
@@ -290,6 +341,12 @@ Examples:
|
|
|
290
341
|
printResult("Latest Revision", wf.latestRevision || "-");
|
|
291
342
|
printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
|
|
292
343
|
printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
|
|
344
|
+
// #1233 — runAs unset means the engine defaults to "caller"; show that
|
|
345
|
+
// default rather than a blank so the deployed principal mode is visible.
|
|
346
|
+
printResult("Run As", wf.runAs || "caller");
|
|
347
|
+
printResult("Capabilities", Array.isArray(wf.capabilities) && wf.capabilities.length > 0
|
|
348
|
+
? wf.capabilities.join(", ")
|
|
349
|
+
: "-");
|
|
293
350
|
if (wf.accessRule) {
|
|
294
351
|
printResult("Access Rule", wf.accessRule);
|
|
295
352
|
}
|
|
@@ -342,8 +399,8 @@ Examples:
|
|
|
342
399
|
// Update workflow
|
|
343
400
|
workflows
|
|
344
401
|
.command("update")
|
|
345
|
-
.description("Update workflow metadata")
|
|
346
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
402
|
+
.description("Update workflow metadata, or push a new body with --from-file")
|
|
403
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
347
404
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
348
405
|
.option("--name <name>", "Display name")
|
|
349
406
|
.option("--description <desc>", "Description")
|
|
@@ -354,9 +411,117 @@ Examples:
|
|
|
354
411
|
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
355
412
|
.option("--sync-callable <bool>", "Allow client.workflows.runSync(): true or false")
|
|
356
413
|
.option("--access-rule <expr>", "CEL expression for workflow access (empty string to clear)")
|
|
414
|
+
.option("--capabilities <list>", "Comma-separated capability grants (e.g. membership,secrets); empty string revokes all")
|
|
415
|
+
.option("--from-file <path>", "Push a revised body (metadata + steps) from a TOML file, keeping the same key/id. " +
|
|
416
|
+
"Overwrites the active config in place — live immediately (no separate publish). " +
|
|
417
|
+
"Explicit metadata flags above override values in the TOML.")
|
|
357
418
|
.option("--json", "Output as JSON")
|
|
358
419
|
.action(async (workflowId, options) => {
|
|
359
420
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
421
|
+
// --from-file: push a full body (metadata + steps) to the existing
|
|
422
|
+
// workflow, keeping its key/id. Symmetric with `create --from-file` and
|
|
423
|
+
// mirrors the `sync push` update-existing path via the shared
|
|
424
|
+
// `applyWorkflowBody` helper (#1249).
|
|
425
|
+
if (options.fromFile) {
|
|
426
|
+
let tomlData;
|
|
427
|
+
try {
|
|
428
|
+
// Expand top-level `include = [...]` workflow fragments before
|
|
429
|
+
// deriving steps, matching what `sync push` does (`parseTomlFile`
|
|
430
|
+
// runs `expandWorkflowTomlData`). `update --from-file` overwrites an
|
|
431
|
+
// existing workflow's live active config, so parsing the raw TOML
|
|
432
|
+
// and reading only `tomlData.steps` would drop fragment-supplied
|
|
433
|
+
// steps — an include-only workflow would overwrite the live body
|
|
434
|
+
// with an empty step list (silent data loss, #1286). `expandWorkflow`
|
|
435
|
+
// reads + parses + splices fragments; a TOML with no `include` key is
|
|
436
|
+
// returned unchanged, so the non-fragment case is unaffected.
|
|
437
|
+
tomlData = expandWorkflow(options.fromFile);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
error(`Failed to read TOML file: ${err.message}`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
// Issue #685: reject misnested headers before sending anything to the
|
|
444
|
+
// server — identical validation to `create --from-file`. Runs on the
|
|
445
|
+
// expanded data (post fragment splice), matching `sync push`.
|
|
446
|
+
const tomlErrors = validateWorkflowToml(tomlData);
|
|
447
|
+
if (tomlErrors.length > 0) {
|
|
448
|
+
error(formatWorkflowTomlErrors(options.fromFile, tomlErrors));
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
const tomlWorkflow = tomlData.workflow || tomlData;
|
|
452
|
+
const steps = tomlData.steps || [];
|
|
453
|
+
const client = new ApiClient();
|
|
454
|
+
// Fetch the existing workflow: we need its real workflowKey (Fork 2
|
|
455
|
+
// key-mismatch guard) and its activeConfigId (live body overwrite).
|
|
456
|
+
let existing;
|
|
457
|
+
try {
|
|
458
|
+
const got = await client.getWorkflow(resolvedAppId, workflowId);
|
|
459
|
+
existing = got?.workflow;
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
printApiError(err);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
if (!existing) {
|
|
466
|
+
error(`Workflow not found: ${workflowId}`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
// Fork 2: a TOML key that contradicts the target's key is almost
|
|
470
|
+
// certainly the wrong file — error rather than silently ignore it.
|
|
471
|
+
const mismatch = workflowKeyMismatchError(tomlWorkflow, existing.workflowKey);
|
|
472
|
+
if (mismatch) {
|
|
473
|
+
error(mismatch);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
// Fork 1 (B): explicit `update` flags override TOML-derived metadata.
|
|
477
|
+
const mergedWorkflow = { ...tomlWorkflow };
|
|
478
|
+
if (options.name)
|
|
479
|
+
mergedWorkflow.name = options.name;
|
|
480
|
+
if (options.description)
|
|
481
|
+
mergedWorkflow.description = options.description;
|
|
482
|
+
if (options.status)
|
|
483
|
+
mergedWorkflow.status = options.status;
|
|
484
|
+
if (options.perUserMaxRunning) {
|
|
485
|
+
mergedWorkflow.perUserMaxRunning = parseInt(options.perUserMaxRunning);
|
|
486
|
+
}
|
|
487
|
+
if (options.perUserMaxQueued) {
|
|
488
|
+
mergedWorkflow.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
489
|
+
}
|
|
490
|
+
if (options.dequeueOrder)
|
|
491
|
+
mergedWorkflow.dequeueOrder = options.dequeueOrder;
|
|
492
|
+
if (options.requiresClientApply !== undefined) {
|
|
493
|
+
mergedWorkflow.requiresClientApply = options.requiresClientApply === "true";
|
|
494
|
+
}
|
|
495
|
+
if (options.syncCallable !== undefined) {
|
|
496
|
+
mergedWorkflow.syncCallable = options.syncCallable === "true";
|
|
497
|
+
}
|
|
498
|
+
if (options.accessRule !== undefined) {
|
|
499
|
+
mergedWorkflow.accessRule = options.accessRule;
|
|
500
|
+
}
|
|
501
|
+
// #1232: an explicit --capabilities flag overrides whatever the TOML
|
|
502
|
+
// declares (grant/revoke on top of a --from-file body push).
|
|
503
|
+
if (options.capabilities !== undefined) {
|
|
504
|
+
mergedWorkflow.capabilities = parseCapabilitiesFlag(options.capabilities);
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const { updateSlotLabel, fullWorkflow } = await applyWorkflowBody(client, resolvedAppId, workflowId, mergedWorkflow, steps, { activeConfigId: existing.activeConfigId });
|
|
508
|
+
if (options.json) {
|
|
509
|
+
json(fullWorkflow);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
success(`Workflow updated from ${options.fromFile} (${updateSlotLabel}).`);
|
|
513
|
+
keyValue("Workflow ID", workflowId);
|
|
514
|
+
keyValue("Key", existing.workflowKey);
|
|
515
|
+
keyValue("Steps", steps.length);
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
// Surface the server's structured `details[]` (e.g. syncCallable /
|
|
519
|
+
// identity validation reasons) like create/sync push do.
|
|
520
|
+
printApiError(err);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
360
525
|
const payload = {};
|
|
361
526
|
if (options.name)
|
|
362
527
|
payload.name = options.name;
|
|
@@ -383,6 +548,14 @@ Examples:
|
|
|
383
548
|
if (options.accessRule !== undefined) {
|
|
384
549
|
payload.accessRule = options.accessRule;
|
|
385
550
|
}
|
|
551
|
+
// #1232: the CLI fallback the issue asked for — grant/revoke a workflow's
|
|
552
|
+
// sensitive `capabilities` (e.g. `membership`) on an existing workflow.
|
|
553
|
+
// Presence-guarded like --access-rule: an omitted flag stays undefined so
|
|
554
|
+
// the server's hasOwnProperty guard leaves the existing grant untouched;
|
|
555
|
+
// `--capabilities ""` sends [] to revoke. The server validates the names.
|
|
556
|
+
if (options.capabilities !== undefined) {
|
|
557
|
+
payload.capabilities = parseCapabilitiesFlag(options.capabilities);
|
|
558
|
+
}
|
|
386
559
|
if (Object.keys(payload).length === 0) {
|
|
387
560
|
error("No update options specified.");
|
|
388
561
|
process.exit(1);
|
|
@@ -406,7 +579,7 @@ Examples:
|
|
|
406
579
|
workflows
|
|
407
580
|
.command("delete")
|
|
408
581
|
.description("Delete or archive a workflow")
|
|
409
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
582
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
410
583
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
411
584
|
.option("--hard", "Permanently delete instead of archive")
|
|
412
585
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
@@ -429,6 +602,10 @@ Examples:
|
|
|
429
602
|
else {
|
|
430
603
|
await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
|
|
431
604
|
success("Workflow archived.");
|
|
605
|
+
// #1238 — archiving keeps the workflowKey reserved, so recreating
|
|
606
|
+
// under the same key fails with "workflowKey already exists". Point
|
|
607
|
+
// at the remedy so the reserved-key behavior is discoverable.
|
|
608
|
+
info(`The workflowKey stays reserved while archived. To free it for reuse, run: primitive workflows delete ${workflowId} --hard`);
|
|
432
609
|
}
|
|
433
610
|
}
|
|
434
611
|
catch (err) {
|
|
@@ -452,7 +629,7 @@ Examples:
|
|
|
452
629
|
if (options.format === "toml") {
|
|
453
630
|
// Strip the `include` key (already removed by expander) and emit
|
|
454
631
|
// valid TOML so the result is round-trippable.
|
|
455
|
-
console.log(
|
|
632
|
+
console.log(stringifyConfigToml(expanded));
|
|
456
633
|
}
|
|
457
634
|
else {
|
|
458
635
|
console.log(JSON.stringify(expanded, null, 2));
|
|
@@ -469,7 +646,7 @@ Examples:
|
|
|
469
646
|
draft
|
|
470
647
|
.command("update")
|
|
471
648
|
.description("Update workflow draft steps (deprecated; use 'workflows configs' for staged rollouts)")
|
|
472
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
649
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
473
650
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
474
651
|
.option("--from-file <path>", "Load steps from TOML file")
|
|
475
652
|
.option("--json", "Output as JSON")
|
|
@@ -482,7 +659,7 @@ Examples:
|
|
|
482
659
|
let tomlData;
|
|
483
660
|
try {
|
|
484
661
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
485
|
-
tomlData =
|
|
662
|
+
tomlData = parseConfigToml(content);
|
|
486
663
|
}
|
|
487
664
|
catch (err) {
|
|
488
665
|
error(`Failed to read TOML file: ${err.message}`);
|
|
@@ -538,7 +715,7 @@ Examples:
|
|
|
538
715
|
workflows
|
|
539
716
|
.command("publish")
|
|
540
717
|
.description("Publish the current draft as a new revision (deprecated for new-model workflows; use 'workflows configs activate')")
|
|
541
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
718
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
542
719
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
543
720
|
.option("--json", "Output as JSON")
|
|
544
721
|
.action(async (workflowId, options) => {
|
|
@@ -582,7 +759,7 @@ Examples:
|
|
|
582
759
|
workflows
|
|
583
760
|
.command("preview")
|
|
584
761
|
.description("Run a preview execution of the workflow")
|
|
585
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
762
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
586
763
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
587
764
|
.option("--config <config-id>", "Use specific configuration")
|
|
588
765
|
.option("--draft", "Force preview of the draft version, even if active is newer")
|
|
@@ -731,13 +908,195 @@ Examples:
|
|
|
731
908
|
process.exit(1);
|
|
732
909
|
}
|
|
733
910
|
});
|
|
911
|
+
// #1367 — hidden queue-latency measurement tool. Fires a batch of runs of a
|
|
912
|
+
// (trivial, no-op) workflow, polls until each has an executionStartedAt, and
|
|
913
|
+
// reports the observed queueDelayMs / createCallDurationMs distribution. This
|
|
914
|
+
// is the post-deploy verification vehicle: local miniflare does not exhibit
|
|
915
|
+
// real Cloudflare scheduling delay, so run it against an agents/alpha scratch
|
|
916
|
+
// app. Creates are paced (default 10/s) to stay clear of CF's ~100/s shared
|
|
917
|
+
// instance-creation limit; a create failure (e.g. 429) is recorded, not fatal.
|
|
918
|
+
workflows
|
|
919
|
+
.command("queue-latency-test", { hidden: true })
|
|
920
|
+
.description("(hidden) Fire N no-op runs and report the queue-delay distribution (post-deploy tool)")
|
|
921
|
+
.argument("<workflow-id>", "Workflow ID or key of a trivial no-op workflow")
|
|
922
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
923
|
+
.option("--count <n>", "Number of runs to fire", "50")
|
|
924
|
+
.option("--rate <r>", "Max creates per second (<=10 recommended)", "10")
|
|
925
|
+
.option("--input <json>", "Root input as JSON")
|
|
926
|
+
.option("--timeout <sec>", "Seconds to wait for all runs to start", "120")
|
|
927
|
+
.option("--verbose", "Print a per-run row")
|
|
928
|
+
.option("--json", "Output as JSON")
|
|
929
|
+
.action(async (workflowId, options) => {
|
|
930
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
931
|
+
const client = new ApiClient();
|
|
932
|
+
const count = Math.max(1, parseInt(options.count, 10) || 50);
|
|
933
|
+
const rate = Math.max(1, parseInt(options.rate, 10) || 10);
|
|
934
|
+
const timeoutSec = Math.max(1, parseInt(options.timeout, 10) || 120);
|
|
935
|
+
const intervalMs = Math.ceil(1000 / rate);
|
|
936
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
937
|
+
let rootInput;
|
|
938
|
+
if (options.input) {
|
|
939
|
+
try {
|
|
940
|
+
rootInput = JSON.parse(options.input);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
error("Invalid JSON in --input");
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const fired = [];
|
|
948
|
+
if (!options.json) {
|
|
949
|
+
info(`Firing ${count} run(s) of ${workflowId} at up to ${rate}/s (interval ${intervalMs}ms)...`);
|
|
950
|
+
}
|
|
951
|
+
for (let i = 0; i < count; i++) {
|
|
952
|
+
try {
|
|
953
|
+
const res = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
954
|
+
rootInput,
|
|
955
|
+
});
|
|
956
|
+
fired.push({
|
|
957
|
+
index: i,
|
|
958
|
+
instanceId: res?.instanceId,
|
|
959
|
+
createFailed: false,
|
|
960
|
+
started: false,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
// A create failure (e.g. HTTP 500 from CF's shared-class rate limit) is
|
|
965
|
+
// a recorded datapoint, not a reason to abort the whole batch.
|
|
966
|
+
fired.push({
|
|
967
|
+
index: i,
|
|
968
|
+
createFailed: true,
|
|
969
|
+
createError: err?.message ? String(err.message) : String(err),
|
|
970
|
+
started: false,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (i < count - 1)
|
|
974
|
+
await sleep(intervalMs);
|
|
975
|
+
}
|
|
976
|
+
// Poll the run list until every non-failed run has executionStartedAt set
|
|
977
|
+
// (queueDelayMs present) or we hit the timeout.
|
|
978
|
+
const byInstance = new Map();
|
|
979
|
+
for (const f of fired) {
|
|
980
|
+
if (f.instanceId)
|
|
981
|
+
byInstance.set(f.instanceId, f);
|
|
982
|
+
}
|
|
983
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
984
|
+
const pending = () => fired.filter((f) => !f.createFailed && !f.started).length;
|
|
985
|
+
// The admin run-list endpoint caps each page at 200, so a single large
|
|
986
|
+
// request cannot cover a batch of more than 200 runs — the earliest ones
|
|
987
|
+
// would never be matched and would be falsely reported as not started.
|
|
988
|
+
// Page through with the cursor (runs come back newest-first, so our batch
|
|
989
|
+
// is at the front) until every pending run is matched or we run out of
|
|
990
|
+
// pages. Bound the scan so we never walk the whole run history.
|
|
991
|
+
const PAGE_LIMIT = 200;
|
|
992
|
+
const scanBudget = count + PAGE_LIMIT;
|
|
993
|
+
while (pending() > 0 && Date.now() < deadline) {
|
|
994
|
+
await sleep(1000);
|
|
995
|
+
try {
|
|
996
|
+
let cursor;
|
|
997
|
+
let scanned = 0;
|
|
998
|
+
do {
|
|
999
|
+
const { items, cursor: nextCursor } = await client.listWorkflowRuns(resolvedAppId, workflowId, { limit: PAGE_LIMIT, cursor: cursor ?? undefined });
|
|
1000
|
+
for (const run of items || []) {
|
|
1001
|
+
const f = run?.instanceId ? byInstance.get(run.instanceId) : undefined;
|
|
1002
|
+
if (f && !f.started && run.executionStartedAt) {
|
|
1003
|
+
f.started = true;
|
|
1004
|
+
f.queueDelayMs =
|
|
1005
|
+
typeof run.queueDelayMs === "number" ? run.queueDelayMs : null;
|
|
1006
|
+
f.createCallDurationMs =
|
|
1007
|
+
typeof run.createCallDurationMs === "number"
|
|
1008
|
+
? run.createCallDurationMs
|
|
1009
|
+
: null;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
scanned += items?.length || 0;
|
|
1013
|
+
cursor = nextCursor;
|
|
1014
|
+
} while (pending() > 0 && cursor && scanned < scanBudget);
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
// Transient list error — keep polling until the deadline.
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const createFailures = fired.filter((f) => f.createFailed).length;
|
|
1021
|
+
const startedRuns = fired.filter((f) => f.started);
|
|
1022
|
+
const queueDelays = startedRuns.map((f) => f.queueDelayMs);
|
|
1023
|
+
const createDurations = startedRuns.map((f) => f.createCallDurationMs);
|
|
1024
|
+
const queueDist = summarizeLatencyDistribution(queueDelays);
|
|
1025
|
+
const createDist = summarizeLatencyDistribution(createDurations);
|
|
1026
|
+
const notStarted = fired.filter((f) => !f.createFailed && !f.started).length;
|
|
1027
|
+
if (options.json) {
|
|
1028
|
+
json({
|
|
1029
|
+
workflowId,
|
|
1030
|
+
requested: count,
|
|
1031
|
+
created: count - createFailures,
|
|
1032
|
+
createFailures,
|
|
1033
|
+
started: startedRuns.length,
|
|
1034
|
+
notStartedWithinTimeout: notStarted,
|
|
1035
|
+
queueDelayMs: queueDist,
|
|
1036
|
+
createCallDurationMs: createDist,
|
|
1037
|
+
runs: options.verbose
|
|
1038
|
+
? fired.map((f) => ({
|
|
1039
|
+
index: f.index,
|
|
1040
|
+
instanceId: f.instanceId ?? null,
|
|
1041
|
+
createFailed: f.createFailed,
|
|
1042
|
+
createError: f.createError ?? null,
|
|
1043
|
+
started: f.started,
|
|
1044
|
+
queueDelayMs: f.queueDelayMs ?? null,
|
|
1045
|
+
createCallDurationMs: f.createCallDurationMs ?? null,
|
|
1046
|
+
}))
|
|
1047
|
+
: undefined,
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
divider();
|
|
1052
|
+
keyValue("Requested", count);
|
|
1053
|
+
keyValue("Created", count - createFailures);
|
|
1054
|
+
keyValue("Create failures", createFailures);
|
|
1055
|
+
keyValue("Started", startedRuns.length);
|
|
1056
|
+
keyValue("Not started (timeout)", notStarted);
|
|
1057
|
+
divider();
|
|
1058
|
+
if (queueDist) {
|
|
1059
|
+
info("Queue delay (executionStartedAt - requested):");
|
|
1060
|
+
keyValue(" min", formatDuration(queueDist.min));
|
|
1061
|
+
keyValue(" p50", formatDuration(queueDist.p50));
|
|
1062
|
+
keyValue(" p95", formatDuration(queueDist.p95));
|
|
1063
|
+
keyValue(" max", formatDuration(queueDist.max));
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
warn("No runs started — no queue-delay samples collected.");
|
|
1067
|
+
}
|
|
1068
|
+
if (createDist) {
|
|
1069
|
+
divider();
|
|
1070
|
+
info("Create-call duration:");
|
|
1071
|
+
keyValue(" min", formatDuration(createDist.min));
|
|
1072
|
+
keyValue(" p50", formatDuration(createDist.p50));
|
|
1073
|
+
keyValue(" p95", formatDuration(createDist.p95));
|
|
1074
|
+
keyValue(" max", formatDuration(createDist.max));
|
|
1075
|
+
}
|
|
1076
|
+
if (options.verbose) {
|
|
1077
|
+
divider();
|
|
1078
|
+
console.log(formatTable(fired.map((f) => ({
|
|
1079
|
+
run: f.index,
|
|
1080
|
+
instanceId: f.instanceId ?? "-",
|
|
1081
|
+
status: f.createFailed ? "create-failed" : f.started ? "started" : "pending",
|
|
1082
|
+
queueDelayMs: f.queueDelayMs,
|
|
1083
|
+
createCallDurationMs: f.createCallDurationMs,
|
|
1084
|
+
})), [
|
|
1085
|
+
{ header: "RUN", key: "run" },
|
|
1086
|
+
{ header: "INSTANCE", key: "instanceId", format: formatId },
|
|
1087
|
+
{ header: "STATE", key: "status" },
|
|
1088
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
1089
|
+
{ header: "CREATE", key: "createCallDurationMs", format: formatDuration },
|
|
1090
|
+
]));
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
734
1093
|
// Runs subcommand
|
|
735
1094
|
const runs = workflows.command("runs").description("Manage workflow runs");
|
|
736
1095
|
// List runs
|
|
737
1096
|
runs
|
|
738
1097
|
.command("list")
|
|
739
1098
|
.description("List workflow runs")
|
|
740
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1099
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
741
1100
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
742
1101
|
.option("--status <status>", "Filter by status: pending, running, completed, failed")
|
|
743
1102
|
.option("--limit <n>", "Number of runs to show", "20")
|
|
@@ -762,6 +1121,8 @@ Examples:
|
|
|
762
1121
|
{ header: "RUN ID", key: "runId", format: formatId },
|
|
763
1122
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
764
1123
|
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
1124
|
+
// #1367 — queue delay (requested → execution start).
|
|
1125
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
765
1126
|
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
766
1127
|
{ header: "PREVIEW", key: "isPreview", format: (v) => v ? "yes" : "" },
|
|
767
1128
|
]));
|
|
@@ -775,7 +1136,7 @@ Examples:
|
|
|
775
1136
|
runs
|
|
776
1137
|
.command("status")
|
|
777
1138
|
.description("Get status of a workflow run")
|
|
778
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1139
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
779
1140
|
.argument("<run-id>", "Run ID")
|
|
780
1141
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
781
1142
|
.option("--json", "Output as JSON")
|
|
@@ -791,7 +1152,13 @@ Examples:
|
|
|
791
1152
|
const run = result.run;
|
|
792
1153
|
keyValue("Run ID", run.runId);
|
|
793
1154
|
keyValue("Status", formatStatus(run.status));
|
|
794
|
-
|
|
1155
|
+
// #1367 — `startedAt` is the request time; `executionStartedAt` is when
|
|
1156
|
+
// Cloudflare actually started running the instance. Queue delay is the
|
|
1157
|
+
// gap. A still-queued run shows "-" for execution start / queue delay.
|
|
1158
|
+
keyValue("Requested", formatDate(run.startedAt));
|
|
1159
|
+
keyValue("Execution started", formatDate(run.executionStartedAt));
|
|
1160
|
+
keyValue("Queue delay", formatDuration(run.queueDelayMs));
|
|
1161
|
+
keyValue("Create call", formatDuration(run.createCallDurationMs));
|
|
795
1162
|
keyValue("Ended", formatDate(run.endedAt));
|
|
796
1163
|
keyValue("Preview", run.isPreview ? "yes" : "no");
|
|
797
1164
|
if (result.instanceStatus) {
|
|
@@ -824,7 +1191,7 @@ Examples:
|
|
|
824
1191
|
runs
|
|
825
1192
|
.command("steps")
|
|
826
1193
|
.description("Show step-level details for a workflow run")
|
|
827
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1194
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
828
1195
|
.argument("<run-id>", "Run ID")
|
|
829
1196
|
.option("--app <app-id>", "App ID")
|
|
830
1197
|
.option("--json", "Output as JSON")
|
|
@@ -841,7 +1208,12 @@ Examples:
|
|
|
841
1208
|
info("No step runs found.");
|
|
842
1209
|
return;
|
|
843
1210
|
}
|
|
844
|
-
|
|
1211
|
+
// #1367 — inter-step gap: idle time between one step ending and the
|
|
1212
|
+
// next starting. Only surfaced above a 1s threshold so normal
|
|
1213
|
+
// back-to-back steps stay quiet; a large gap flags a mid-run stall.
|
|
1214
|
+
const STEP_GAP_THRESHOLD_MS = 1000;
|
|
1215
|
+
const rows = annotateStepGaps(items);
|
|
1216
|
+
console.log(formatTable(rows, [
|
|
845
1217
|
{ header: "STEP", key: "stepId" },
|
|
846
1218
|
{ header: "KIND", key: "stepKind" },
|
|
847
1219
|
{
|
|
@@ -852,6 +1224,13 @@ Examples:
|
|
|
852
1224
|
v === "skipped" ? chalk.gray(v) :
|
|
853
1225
|
v === "error_captured" ? chalk.yellow(v) : v,
|
|
854
1226
|
},
|
|
1227
|
+
{
|
|
1228
|
+
header: "GAP",
|
|
1229
|
+
key: "gapMs",
|
|
1230
|
+
format: (v) => typeof v === "number" && v >= STEP_GAP_THRESHOLD_MS
|
|
1231
|
+
? chalk.yellow(formatDuration(v))
|
|
1232
|
+
: chalk.dim("-"),
|
|
1233
|
+
},
|
|
855
1234
|
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
856
1235
|
{
|
|
857
1236
|
header: "TOKENS",
|
|
@@ -874,7 +1253,7 @@ Examples:
|
|
|
874
1253
|
runs
|
|
875
1254
|
.command("step-detail")
|
|
876
1255
|
.description("Show full details for a single step")
|
|
877
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1256
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
878
1257
|
.argument("<run-id>", "Run ID")
|
|
879
1258
|
.argument("<step-id>", "Step ID")
|
|
880
1259
|
.option("--app <app-id>", "App ID")
|
|
@@ -996,7 +1375,7 @@ Examples:
|
|
|
996
1375
|
runs
|
|
997
1376
|
.command("error")
|
|
998
1377
|
.description("Show error details for a failed workflow run")
|
|
999
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1378
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1000
1379
|
.argument("<run-id>", "Run ID")
|
|
1001
1380
|
.option("--app <app-id>", "App ID")
|
|
1002
1381
|
.option("--json", "Output as JSON")
|
|
@@ -1065,7 +1444,7 @@ Examples:
|
|
|
1065
1444
|
runs
|
|
1066
1445
|
.command("failures")
|
|
1067
1446
|
.description("List recent workflow run failures")
|
|
1068
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1447
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1069
1448
|
.option("--app <app-id>", "App ID")
|
|
1070
1449
|
.option("--limit <n>", "Number of failures to show", "10")
|
|
1071
1450
|
.option("--json", "Output as JSON")
|
|
@@ -1097,6 +1476,111 @@ Examples:
|
|
|
1097
1476
|
process.exit(1);
|
|
1098
1477
|
}
|
|
1099
1478
|
});
|
|
1479
|
+
// ── Iterations: inspect / reset iterate-users singleton iterations (#1209) ──
|
|
1480
|
+
const iterations = workflows
|
|
1481
|
+
.command("iterations")
|
|
1482
|
+
.description("Inspect and reset iterate-users iterations");
|
|
1483
|
+
iterations
|
|
1484
|
+
.command("list")
|
|
1485
|
+
.description("List iterate-users iterations for the app")
|
|
1486
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1487
|
+
.option("--json", "Output as JSON")
|
|
1488
|
+
.action(async (options) => {
|
|
1489
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1490
|
+
const client = new ApiClient();
|
|
1491
|
+
try {
|
|
1492
|
+
const { items } = await client.listIterations(resolvedAppId);
|
|
1493
|
+
if (options.json) {
|
|
1494
|
+
json(items);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (!items || items.length === 0) {
|
|
1498
|
+
info("No iterations found.");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
console.log(formatTable(items, [
|
|
1502
|
+
{ header: "ITERATION", key: "iterationName" },
|
|
1503
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
1504
|
+
{ header: "ACQUIRED", key: "lastAcquireMode", format: (v) => v || "" },
|
|
1505
|
+
{ header: "PROCESSED", key: "totalProcessed" },
|
|
1506
|
+
{ header: "FAILED", key: "failedCount" },
|
|
1507
|
+
{ header: "LAST RUN", key: "lastRunAt", format: formatDate },
|
|
1508
|
+
]));
|
|
1509
|
+
}
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
error(err.message);
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
iterations
|
|
1516
|
+
.command("get")
|
|
1517
|
+
.description("Show details for a single iterate-users iteration")
|
|
1518
|
+
.argument("<name>", "Iteration name")
|
|
1519
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1520
|
+
.option("--json", "Output as JSON")
|
|
1521
|
+
.action(async (name, options) => {
|
|
1522
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1523
|
+
const client = new ApiClient();
|
|
1524
|
+
try {
|
|
1525
|
+
const it = await client.getIteration(resolvedAppId, name);
|
|
1526
|
+
if (options.json) {
|
|
1527
|
+
json(it);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
keyValue("Iteration", it.iterationName);
|
|
1531
|
+
keyValue("Status", formatStatus(it.status));
|
|
1532
|
+
keyValue("Acquired as", it.lastAcquireMode || "(unknown)");
|
|
1533
|
+
keyValue("Run ID", it.runId);
|
|
1534
|
+
keyValue("Instance ID", it.instanceId);
|
|
1535
|
+
keyValue("Started", formatDate(it.startedAt));
|
|
1536
|
+
keyValue("Last run", formatDate(it.lastRunAt));
|
|
1537
|
+
keyValue("Lock expires", formatDate(it.lockExpiresAt));
|
|
1538
|
+
keyValue("Processed", String(it.totalProcessed ?? 0));
|
|
1539
|
+
keyValue("Succeeded", String(it.succeededCount ?? 0));
|
|
1540
|
+
keyValue("Failed (total)", String(it.failedCount ?? 0));
|
|
1541
|
+
keyValue("Skipped", String(it.skippedCount ?? 0));
|
|
1542
|
+
if (it.lastError) {
|
|
1543
|
+
keyValue("Last error", it.lastError);
|
|
1544
|
+
}
|
|
1545
|
+
const sample = Array.isArray(it.failedUserIdsSample)
|
|
1546
|
+
? it.failedUserIdsSample
|
|
1547
|
+
: [];
|
|
1548
|
+
if (sample.length > 0) {
|
|
1549
|
+
const label = it.failedUserIdsTruncated
|
|
1550
|
+
? `Failed user IDs (sample, truncated; ${it.failedCount} total)`
|
|
1551
|
+
: `Failed user IDs (sample)`;
|
|
1552
|
+
keyValue(label, sample.join(", "));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
catch (err) {
|
|
1556
|
+
error(err.message);
|
|
1557
|
+
process.exit(1);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
iterations
|
|
1561
|
+
.command("reset")
|
|
1562
|
+
.description("Reset a terminal (completed/failed) iteration so the next trigger runs fresh")
|
|
1563
|
+
.argument("<name>", "Iteration name")
|
|
1564
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1565
|
+
.option("--json", "Output as JSON")
|
|
1566
|
+
.action(async (name, options) => {
|
|
1567
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1568
|
+
const client = new ApiClient();
|
|
1569
|
+
try {
|
|
1570
|
+
const res = await client.resetIteration(resolvedAppId, name);
|
|
1571
|
+
if (options.json) {
|
|
1572
|
+
json(res);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
success(`Reset iteration "${name}" (was ${res.previousStatus}). The next trigger will run fresh.`);
|
|
1576
|
+
}
|
|
1577
|
+
catch (err) {
|
|
1578
|
+
// A running iteration is refused with 409 + lock metadata — surface a
|
|
1579
|
+
// clear message rather than a raw error.
|
|
1580
|
+
error(err.message);
|
|
1581
|
+
process.exit(1);
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1100
1584
|
// Analytics overview
|
|
1101
1585
|
const analyticsCmd = workflows.command("analytics").description("View workflow performance analytics");
|
|
1102
1586
|
analyticsCmd
|
|
@@ -1189,7 +1673,7 @@ Examples:
|
|
|
1189
1673
|
configs
|
|
1190
1674
|
.command("list")
|
|
1191
1675
|
.description("List configurations for a workflow")
|
|
1192
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1676
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1193
1677
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1194
1678
|
.option("--json", "Output as JSON")
|
|
1195
1679
|
.action(async (workflowId, options) => {
|
|
@@ -1229,7 +1713,7 @@ Examples:
|
|
|
1229
1713
|
configs
|
|
1230
1714
|
.command("get")
|
|
1231
1715
|
.description("Get configuration details")
|
|
1232
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1716
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1233
1717
|
.argument("<config-id>", "Configuration ID")
|
|
1234
1718
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1235
1719
|
.option("--json", "Output as JSON")
|
|
@@ -1265,7 +1749,7 @@ Examples:
|
|
|
1265
1749
|
configs
|
|
1266
1750
|
.command("create")
|
|
1267
1751
|
.description("Create a new configuration")
|
|
1268
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1752
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1269
1753
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1270
1754
|
.option("--name <name>", "Configuration name (required)")
|
|
1271
1755
|
.option("--description <desc>", "Description")
|
|
@@ -1282,7 +1766,7 @@ Examples:
|
|
|
1282
1766
|
let tomlData;
|
|
1283
1767
|
try {
|
|
1284
1768
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
1285
|
-
tomlData =
|
|
1769
|
+
tomlData = parseConfigToml(content);
|
|
1286
1770
|
}
|
|
1287
1771
|
catch (err) {
|
|
1288
1772
|
error(`Failed to read TOML file: ${err.message}`);
|
|
@@ -1319,7 +1803,7 @@ Examples:
|
|
|
1319
1803
|
configs
|
|
1320
1804
|
.command("update")
|
|
1321
1805
|
.description("Update a configuration")
|
|
1322
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1806
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1323
1807
|
.argument("<config-id>", "Configuration ID")
|
|
1324
1808
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1325
1809
|
.option("--name <name>", "Configuration name")
|
|
@@ -1337,7 +1821,7 @@ Examples:
|
|
|
1337
1821
|
let tomlData;
|
|
1338
1822
|
try {
|
|
1339
1823
|
const content = readFileSync(options.fromFile, "utf-8");
|
|
1340
|
-
tomlData =
|
|
1824
|
+
tomlData = parseConfigToml(content);
|
|
1341
1825
|
}
|
|
1342
1826
|
catch (err) {
|
|
1343
1827
|
error(`Failed to read TOML file: ${err.message}`);
|
|
@@ -1375,7 +1859,7 @@ Examples:
|
|
|
1375
1859
|
configs
|
|
1376
1860
|
.command("activate")
|
|
1377
1861
|
.description("Set a configuration as the active (default) configuration")
|
|
1378
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1862
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1379
1863
|
.argument("<config-id>", "Configuration ID")
|
|
1380
1864
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1381
1865
|
.option("--json", "Output as JSON")
|
|
@@ -1400,7 +1884,7 @@ Examples:
|
|
|
1400
1884
|
configs
|
|
1401
1885
|
.command("duplicate")
|
|
1402
1886
|
.description("Duplicate a configuration")
|
|
1403
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1887
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1404
1888
|
.argument("<config-id>", "Configuration ID to duplicate")
|
|
1405
1889
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1406
1890
|
.option("--name <name>", "Name for the new configuration")
|
|
@@ -1428,7 +1912,7 @@ Examples:
|
|
|
1428
1912
|
configs
|
|
1429
1913
|
.command("archive")
|
|
1430
1914
|
.description("Archive a configuration")
|
|
1431
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1915
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1432
1916
|
.argument("<config-id>", "Configuration ID")
|
|
1433
1917
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1434
1918
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
@@ -2247,6 +2731,130 @@ Examples:
|
|
|
2247
2731
|
process.exit(1);
|
|
2248
2732
|
}
|
|
2249
2733
|
});
|
|
2734
|
+
// ============================================
|
|
2735
|
+
// Codegen command (issue #1442)
|
|
2736
|
+
// ============================================
|
|
2737
|
+
workflows
|
|
2738
|
+
.command("codegen")
|
|
2739
|
+
.description("Generate typed client signatures from the local workflows/*.toml schemas. TypeScript (default): one <key>.generated.ts per workflow (<Key>Input/<Key>Output types + a <key>(client) invoker factory). Swift (--lang swift): one <key>.generated.swift per workflow (<Key>Input/<Key>Output Codable types).")
|
|
2740
|
+
.argument("[workflow-key]", "Generate for a single workflow (defaults to every workflows/*.toml found)")
|
|
2741
|
+
.option("--app <app-id>", "App ID")
|
|
2742
|
+
.option("--sync-dir <path>", "Override path to the sync directory (defaults to ./.primitive/sync/<env>/<appId>/)")
|
|
2743
|
+
.option("-o, --output <dir>", "Output directory for the generated files (defaults to <sync-dir>/workflows/generated/)")
|
|
2744
|
+
.option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
|
|
2745
|
+
.option("--check", "Exit non-zero if generated output is out of date (CI guard); does not write.")
|
|
2746
|
+
.option("--json", "Output the result summary as JSON")
|
|
2747
|
+
.action(async (workflowKey, options) => {
|
|
2748
|
+
try {
|
|
2749
|
+
// 0. Validate --lang up front so an unknown value fails clearly rather
|
|
2750
|
+
// than silently defaulting to the TS emitter.
|
|
2751
|
+
const lang = String(options.lang ?? "ts").toLowerCase();
|
|
2752
|
+
if (lang !== "ts" && lang !== "swift") {
|
|
2753
|
+
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
2754
|
+
process.exit(1);
|
|
2755
|
+
}
|
|
2756
|
+
// 1. Resolve the single source workflows/ directory via the shared
|
|
2757
|
+
// active-environment resolver (issue #1510). Honors --sync-dir /
|
|
2758
|
+
// --app overrides, resolves the active env
|
|
2759
|
+
// (--env → PRIMITIVE_ENV → defaultEnvironment → single-env), and
|
|
2760
|
+
// preserves the legacy per-app scan fallback in bare-dir mode.
|
|
2761
|
+
const workflowsSourceDir = resolveCodegenSourceDir({
|
|
2762
|
+
subdir: "workflows",
|
|
2763
|
+
options: { app: options.app, syncDir: options.syncDir },
|
|
2764
|
+
});
|
|
2765
|
+
// 2. Collect the source .toml files (one per workflow), filtered to a
|
|
2766
|
+
// single workflow when an argument is given. Match on the file stem;
|
|
2767
|
+
// the generator resolves the real key from `[workflow].key`.
|
|
2768
|
+
const inputs = [];
|
|
2769
|
+
for (const fileName of readdirSync(workflowsSourceDir)) {
|
|
2770
|
+
if (!fileName.endsWith(".toml"))
|
|
2771
|
+
continue;
|
|
2772
|
+
const stem = fileName.slice(0, -".toml".length);
|
|
2773
|
+
if (workflowKey && stem !== workflowKey)
|
|
2774
|
+
continue;
|
|
2775
|
+
const tomlPath = path.join(workflowsSourceDir, fileName);
|
|
2776
|
+
inputs.push({
|
|
2777
|
+
fileStem: stem,
|
|
2778
|
+
tomlPath,
|
|
2779
|
+
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
if (inputs.length === 0) {
|
|
2783
|
+
error(workflowKey
|
|
2784
|
+
? `No workflows/${workflowKey}.toml found under .primitive/sync/.`
|
|
2785
|
+
: "No workflows/*.toml files found to generate from.");
|
|
2786
|
+
process.exit(1);
|
|
2787
|
+
}
|
|
2788
|
+
// 3. Resolve the output directory. Default: a `generated/` subdir of
|
|
2789
|
+
// the first source dir (keeps generated output beside the schema).
|
|
2790
|
+
const outputDir = options.output
|
|
2791
|
+
? path.resolve(options.output)
|
|
2792
|
+
: path.join(path.dirname(inputs[0].tomlPath), "generated");
|
|
2793
|
+
// 4. Run codegen (or --check) with the selected emitter. Both the TS
|
|
2794
|
+
// and Swift generators return the same WriteOrCheckResult shape, so
|
|
2795
|
+
// the reporting below is emitter-agnostic. Fork 1 = A: only the
|
|
2796
|
+
// emitter differs; both read the same synced TOMLs and the same
|
|
2797
|
+
// drift-guarded schema descriptor.
|
|
2798
|
+
const codegenResult = lang === "swift"
|
|
2799
|
+
? await generateWorkflowSwiftTypes({
|
|
2800
|
+
inputs,
|
|
2801
|
+
outputDir,
|
|
2802
|
+
check: !!options.check,
|
|
2803
|
+
singleWorkflow: !!workflowKey,
|
|
2804
|
+
})
|
|
2805
|
+
: await generateWorkflowTypes({
|
|
2806
|
+
inputs,
|
|
2807
|
+
outputDir,
|
|
2808
|
+
check: !!options.check,
|
|
2809
|
+
singleWorkflow: !!workflowKey,
|
|
2810
|
+
});
|
|
2811
|
+
if (options.check) {
|
|
2812
|
+
if (codegenResult.mismatches.length > 0) {
|
|
2813
|
+
if (options.json) {
|
|
2814
|
+
json({ ok: false, mismatches: codegenResult.mismatches });
|
|
2815
|
+
}
|
|
2816
|
+
else {
|
|
2817
|
+
error(`Check failed: ${codegenResult.mismatches.length} file(s) out of date.`);
|
|
2818
|
+
for (const m of codegenResult.mismatches) {
|
|
2819
|
+
error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
|
|
2820
|
+
}
|
|
2821
|
+
info(lang === "swift"
|
|
2822
|
+
? "Run `primitive workflows codegen --lang swift` to regenerate."
|
|
2823
|
+
: "Run `primitive workflows codegen` to regenerate.");
|
|
2824
|
+
}
|
|
2825
|
+
process.exit(1);
|
|
2826
|
+
}
|
|
2827
|
+
if (options.json) {
|
|
2828
|
+
json({ ok: true, checked: codegenResult.writtenFiles.length });
|
|
2829
|
+
}
|
|
2830
|
+
else {
|
|
2831
|
+
success(`Check passed: ${codegenResult.writtenFiles.length} file(s) up to date.`);
|
|
2832
|
+
}
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (options.json) {
|
|
2836
|
+
json({
|
|
2837
|
+
written: codegenResult.writtenFiles,
|
|
2838
|
+
deleted: codegenResult.deletedFiles,
|
|
2839
|
+
});
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
success(`Generated ${codegenResult.writtenFiles.length} file(s)` +
|
|
2843
|
+
(codegenResult.deletedFiles.length > 0
|
|
2844
|
+
? `, deleted ${codegenResult.deletedFiles.length} stale file(s).`
|
|
2845
|
+
: "."));
|
|
2846
|
+
for (const f of codegenResult.writtenFiles) {
|
|
2847
|
+
keyValue(" wrote", path.relative(process.cwd(), f));
|
|
2848
|
+
}
|
|
2849
|
+
for (const f of codegenResult.deletedFiles) {
|
|
2850
|
+
keyValue(" deleted", path.relative(process.cwd(), f));
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
catch (err) {
|
|
2854
|
+
error(err.message);
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2250
2858
|
}
|
|
2251
2859
|
function formatFileSize(bytes) {
|
|
2252
2860
|
if (bytes < 1024)
|