@dreamlake/dreamlake-cli 0.2.0 → 0.9.2

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.
@@ -1,627 +0,0 @@
1
- // `dreamlake workflow ...` — workflow management + run-trace push.
2
- //
3
- // Covers the workflow API endpoints:
4
- // workflow list / create / show / update / delete
5
- // workflow push-run / watch-run (agent-side run-trace snapshots)
6
- import { readFileSync } from "node:fs";
7
- import { resolve } from "node:path";
8
- import { HttpError, requestJson } from "../client.js";
9
- import { resolveRemote, resolveToken, resolveNamespace } from "../config.js";
10
- import { emitJson, fail, ok, renderTable, splitCsv, warn } from "../helpers.js";
11
- import { confirm } from "../prompt.js";
12
- // ─── shared context resolution ────────────────────────────────────────────────
13
- async function ctx(nsFlag) {
14
- const token = resolveToken();
15
- if (!token) {
16
- fail("not authenticated — run 'dreamlake login' first");
17
- return null;
18
- }
19
- const remote = resolveRemote();
20
- const ns = await resolveNamespace(nsFlag, { token, remote });
21
- if (!ns) {
22
- fail("could not resolve namespace — run 'dreamlake login' or pass --namespace");
23
- return null;
24
- }
25
- return { token, remote, ns };
26
- }
27
- function readFile(filePath) {
28
- try {
29
- return readFileSync(resolve(filePath), "utf8");
30
- }
31
- catch {
32
- throw new Error(`cannot read file: ${filePath}`);
33
- }
34
- }
35
- // ─── run-file reduction (local wf_*.json → server trace contract) ─────────────
36
- //
37
- // The Claude Code Workflow tool writes wf_<runId>.json snapshots under the
38
- // session's workflows/ dir. `workflowProgress` interleaves `workflow_phase`
39
- // and `workflow_agent` entries; run totals live at the top level. The file
40
- // format drifts across CLI versions, so every field is optional — reduce
41
- // what's there, drop the rest.
42
- const AGENT_FIELDS = [
43
- "index",
44
- "label",
45
- "phaseIndex",
46
- "phaseTitle",
47
- "agentId",
48
- "model",
49
- "state",
50
- "queuedAt",
51
- "startedAt",
52
- "lastProgressAt",
53
- "durationMs",
54
- "attempt",
55
- "tokens",
56
- "toolCalls",
57
- "lastToolName",
58
- "lastToolSummary",
59
- "promptPreview",
60
- "resultPreview",
61
- ];
62
- // ─── body-budget guardrails (bounded push size) ──────────────────────────────
63
- //
64
- // Every push-run / watch-run PUT must stay under the server's 5MB bodyLimit.
65
- // An oversized body 413s; for the *terminal* snapshot that means the server-side
66
- // run is never flipped out of 'running', so the web detail page polls it every
67
- // few seconds forever. We hold the serialized body under a 4MB budget (headroom
68
- // below the 5MB limit) and degrade deterministically — least-important data
69
- // first — rather than ever emit a body we know will be rejected:
70
- //
71
- // 1. always: cap each agent's free-text previews to AGENT_PREVIEW_CAP chars.
72
- // 2. always: keep only the newest log lines that fit LOGS_BUDGET_BYTES.
73
- // 3. if the whole body is still over budget: drop logs, then replace `result`
74
- // with a size marker. status / trace skeleton (phases+agents) / totals
75
- // always survive — they are what the detail page needs to render.
76
- /** Serialized-body ceiling. Below the server's 5MB bodyLimit, with headroom. */
77
- const BODY_BUDGET_BYTES = 4 * 1024 * 1024;
78
- /** Per-field cap for free-text agent previews. */
79
- const AGENT_PREVIEW_CAP = 2000;
80
- /** Combined byte budget for retained log lines (the newest are kept). */
81
- const LOGS_BUDGET_BYTES = 512 * 1024;
82
- /** Agent fields that are free-text and get length-capped. */
83
- const CAPPED_AGENT_FIELDS = new Set([
84
- "promptPreview",
85
- "resultPreview",
86
- "lastToolSummary",
87
- ]);
88
- function byteLength(v) {
89
- return Buffer.byteLength(JSON.stringify(v), "utf8");
90
- }
91
- /** Cap a free-text field to `max` chars, appending an ellipsis when trimmed. */
92
- function capText(v, max) {
93
- return typeof v === "string" && v.length > max ? v.slice(0, max) + "…" : v;
94
- }
95
- /**
96
- * Keep the LAST log lines that fit within `budget` bytes; when any are dropped
97
- * prepend a marker so the reader knows the head was elided. Always keeps at
98
- * least the most recent line (even if it alone exceeds the budget) so the tail
99
- * of the run is never lost. Returns the input untouched when nothing is dropped.
100
- */
101
- function capLogs(logs, budget) {
102
- if (logs.length === 0)
103
- return logs;
104
- const kept = [];
105
- let used = 0;
106
- for (let i = logs.length - 1; i >= 0; i--) {
107
- const line = logs[i];
108
- const cost = Buffer.byteLength(line, "utf8") + 1; // + newline separator
109
- if (kept.length > 0 && used + cost > budget)
110
- break;
111
- kept.push(line);
112
- used += cost;
113
- }
114
- if (kept.length === logs.length)
115
- return logs; // nothing dropped — keep as-is
116
- kept.reverse();
117
- kept.unshift(`[truncated ${logs.length - kept.length} earlier log lines]`);
118
- return kept;
119
- }
120
- /**
121
- * Final guardrail: if the serialized body still exceeds BODY_BUDGET_BYTES after
122
- * the per-field/log caps (e.g. a huge `result` or thousands of agents), degrade
123
- * further — drop logs entirely, then replace `result` with a size marker. The
124
- * status, trace skeleton (phases + agents) and run totals are never touched.
125
- */
126
- function enforceBodyBudget(body, trace) {
127
- if (byteLength(body) <= BODY_BUDGET_BYTES)
128
- return;
129
- // 1. Drop logs — the phase/agent skeleton is worth far more than log tail.
130
- if (trace.logs.length > 0) {
131
- trace.logs = [];
132
- if (byteLength(body) <= BODY_BUDGET_BYTES)
133
- return;
134
- }
135
- // 2. Replace the result payload with a marker of its original size.
136
- if (body.result !== undefined) {
137
- body.result = { truncated: true, originalBytes: byteLength(body.result) };
138
- }
139
- // If still over budget here, the agent/phase skeleton itself is oversized;
140
- // that is the documented floor — those fields must survive intact even if
141
- // the push risks a 413.
142
- }
143
- function asRecord(v) {
144
- return v && typeof v === "object" && !Array.isArray(v)
145
- ? v
146
- : null;
147
- }
148
- /** Epoch-millis (number) or ISO string → ISO string; anything else → undefined. */
149
- function isoTime(v) {
150
- if (typeof v === "number" && Number.isFinite(v))
151
- return new Date(v).toISOString();
152
- if (typeof v === "string" && v)
153
- return v;
154
- return undefined;
155
- }
156
- export function reduceRunFile(raw) {
157
- const file = asRecord(raw) ?? {};
158
- const progress = Array.isArray(file.workflowProgress) ? file.workflowProgress : [];
159
- const phases = [];
160
- const agents = [];
161
- for (const entry of progress) {
162
- const e = asRecord(entry);
163
- if (!e)
164
- continue;
165
- if (e.type === "workflow_phase") {
166
- const phase = {};
167
- if (e.index !== undefined)
168
- phase.index = e.index;
169
- if (e.title !== undefined)
170
- phase.title = e.title;
171
- phases.push(phase);
172
- }
173
- else if (e.type === "workflow_agent") {
174
- const agent = {};
175
- for (const f of AGENT_FIELDS) {
176
- if (e[f] === undefined)
177
- continue;
178
- // Free-text previews are length-capped; everything else copied verbatim.
179
- agent[f] = CAPPED_AGENT_FIELDS.has(f)
180
- ? capText(e[f], AGENT_PREVIEW_CAP)
181
- : e[f];
182
- }
183
- agents.push(agent);
184
- }
185
- }
186
- // Before the first phase starts running, fall back to the declared meta
187
- // phases so the graph still renders the skeleton.
188
- if (phases.length === 0 && Array.isArray(file.phases)) {
189
- file.phases.forEach((p, i) => {
190
- const rec = asRecord(p);
191
- if (rec?.title !== undefined)
192
- phases.push({ index: i + 1, title: rec.title });
193
- });
194
- }
195
- const trace = {
196
- phases,
197
- agents,
198
- logs: capLogs(Array.isArray(file.logs)
199
- ? file.logs.filter((l) => typeof l === "string")
200
- : [], LOGS_BUDGET_BYTES),
201
- };
202
- if (typeof file.totalToolCalls === "number")
203
- trace.totalToolCalls = file.totalToolCalls;
204
- if (typeof file.defaultModel === "string")
205
- trace.defaultModel = file.defaultModel;
206
- const body = {
207
- status: typeof file.status === "string" && file.status ? file.status : "running",
208
- trace,
209
- };
210
- const startTime = isoTime(file.startTime);
211
- if (startTime)
212
- body.startTime = startTime;
213
- if (typeof file.durationMs === "number")
214
- body.durationMs = file.durationMs;
215
- if (typeof file.agentCount === "number")
216
- body.agentCount = file.agentCount;
217
- if (typeof file.totalTokens === "number")
218
- body.totalTokens = file.totalTokens;
219
- if (typeof file.error === "string" && file.error)
220
- body.error = file.error;
221
- if (file.result !== undefined)
222
- body.result = file.result;
223
- // Guarantee the body fits under the server bodyLimit before it is ever sent.
224
- enforceBodyBudget(body, trace);
225
- return {
226
- runId: typeof file.runId === "string" && file.runId ? file.runId : undefined,
227
- body,
228
- };
229
- }
230
- function readRunFile(filePath) {
231
- const text = readFileSync(resolve(filePath), "utf8");
232
- return reduceRunFile(JSON.parse(text));
233
- }
234
- // ─── workflow list ────────────────────────────────────────────────────────────
235
- export async function runWorkflowList(opts) {
236
- const c = await ctx(opts.namespace);
237
- if (!c)
238
- return 1;
239
- try {
240
- const res = await requestJson(c.remote, `/namespaces/${c.ns}/workflows`, { token: c.token });
241
- if (opts.json) {
242
- emitJson(res.workflows);
243
- return 0;
244
- }
245
- if (res.workflows.length === 0) {
246
- process.stdout.write("No workflows found.\n");
247
- return 0;
248
- }
249
- const rows = res.workflows.map((w) => ({
250
- name: w.name,
251
- hash: w.currentVersionHash ?? "—",
252
- runs: String(w.runCount ?? 0),
253
- lastRun: w.latestRun?.status ?? "—",
254
- updatedAt: w.updatedAt.slice(0, 10),
255
- }));
256
- process.stdout.write(renderTable(rows, ["name", "hash", "runs", "lastRun", "updatedAt"]));
257
- process.stdout.write(`\n ${res.workflows.length} workflow(s)\n`);
258
- return 0;
259
- }
260
- catch (err) {
261
- fail(err.message);
262
- return 1;
263
- }
264
- }
265
- // ─── workflow create ──────────────────────────────────────────────────────────
266
- export async function runWorkflowCreate(name, opts) {
267
- const c = await ctx(opts.namespace);
268
- if (!c)
269
- return 1;
270
- try {
271
- const body = { name };
272
- if (opts.description)
273
- body.description = opts.description;
274
- if (opts.tags)
275
- body.tags = splitCsv(opts.tags);
276
- if (opts.file) {
277
- body.script = readFile(opts.file);
278
- if (opts.message)
279
- body.versionMessage = opts.message;
280
- }
281
- const workflow = await requestJson(c.remote, `/namespaces/${c.ns}/workflows`, { method: "POST", token: c.token, json: body });
282
- if (opts.json) {
283
- emitJson(workflow);
284
- return 0;
285
- }
286
- ok(`Created workflow: ${workflow.name}`);
287
- if (workflow.currentVersionHash) {
288
- process.stdout.write(` version: ${workflow.currentVersionHash}\n`);
289
- process.stdout.write(` phases: ${workflow.meta?.phases?.length ?? 0}\n`);
290
- }
291
- return 0;
292
- }
293
- catch (err) {
294
- if (err instanceof HttpError && err.status === 409) {
295
- fail(`workflow '${name}' already exists in namespace '${c.ns}'`);
296
- return 1;
297
- }
298
- fail(err.message);
299
- return 1;
300
- }
301
- }
302
- // ─── workflow show ────────────────────────────────────────────────────────────
303
- export async function runWorkflowShow(name, opts) {
304
- const c = await ctx(opts.namespace);
305
- if (!c)
306
- return 1;
307
- try {
308
- const workflow = await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}`, { token: c.token });
309
- if (opts.json) {
310
- emitJson(workflow);
311
- return 0;
312
- }
313
- process.stdout.write(`Workflow: ${workflow.name}\n`);
314
- process.stdout.write(` version: ${workflow.currentVersionHash ?? "—"}\n`);
315
- process.stdout.write(` phases: ${workflow.meta?.phases?.length ?? "—"}\n`);
316
- if (workflow.description)
317
- process.stdout.write(` description: ${workflow.description}\n`);
318
- if (workflow.tags.length)
319
- process.stdout.write(` tags: ${workflow.tags.join(", ")}\n`);
320
- process.stdout.write(` created: ${workflow.createdAt.slice(0, 10)}\n`);
321
- process.stdout.write(` updated: ${workflow.updatedAt.slice(0, 10)}\n`);
322
- return 0;
323
- }
324
- catch (err) {
325
- if (err instanceof HttpError && err.status === 404) {
326
- fail(`workflow '${name}' not found`);
327
- return 1;
328
- }
329
- fail(err.message);
330
- return 1;
331
- }
332
- }
333
- // ─── workflow update ──────────────────────────────────────────────────────────
334
- export async function runWorkflowUpdate(name, opts) {
335
- const c = await ctx(opts.namespace);
336
- if (!c)
337
- return 1;
338
- const body = {};
339
- if (opts.description !== undefined)
340
- body.description = opts.description;
341
- if (opts.tags !== undefined)
342
- body.tags = splitCsv(opts.tags);
343
- if (opts.file) {
344
- try {
345
- body.script = readFile(opts.file);
346
- }
347
- catch (err) {
348
- fail(err.message);
349
- return 1;
350
- }
351
- if (opts.message)
352
- body.versionMessage = opts.message;
353
- }
354
- if (Object.keys(body).length === 0) {
355
- fail("nothing to update — use --file, --description, or --tags");
356
- return 1;
357
- }
358
- try {
359
- const workflow = await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}`, { method: "PATCH", token: c.token, json: body });
360
- if (opts.json) {
361
- emitJson(workflow);
362
- return 0;
363
- }
364
- ok(`Updated workflow: ${workflow.name}`);
365
- if (workflow.currentVersionHash) {
366
- process.stdout.write(` version: ${workflow.currentVersionHash}\n`);
367
- }
368
- return 0;
369
- }
370
- catch (err) {
371
- if (err instanceof HttpError && err.status === 404) {
372
- fail(`workflow '${name}' not found`);
373
- return 1;
374
- }
375
- fail(err.message);
376
- return 1;
377
- }
378
- }
379
- // ─── workflow delete ──────────────────────────────────────────────────────────
380
- export async function runWorkflowDelete(name, opts) {
381
- const c = await ctx(opts.namespace);
382
- if (!c)
383
- return 1;
384
- if (!opts.yes) {
385
- const proceed = await confirm(`Delete workflow '${name}'?`, false);
386
- if (!proceed) {
387
- process.stdout.write("Cancelled.\n");
388
- return 0;
389
- }
390
- }
391
- try {
392
- await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}`, { method: "DELETE", token: c.token });
393
- ok(`Deleted workflow: ${name}`);
394
- return 0;
395
- }
396
- catch (err) {
397
- if (err instanceof HttpError && err.status === 404) {
398
- fail(`workflow '${name}' not found`);
399
- return 1;
400
- }
401
- fail(err.message);
402
- return 1;
403
- }
404
- }
405
- // ─── workflow push-run ────────────────────────────────────────────────────────
406
- export async function runWorkflowPushRun(name, filePath, opts) {
407
- const c = await ctx(opts.namespace);
408
- if (!c)
409
- return 1;
410
- let reduced;
411
- try {
412
- reduced = readRunFile(filePath);
413
- }
414
- catch (err) {
415
- fail(`cannot read run file ${filePath}: ${err.message}`);
416
- return 1;
417
- }
418
- if (!reduced.runId) {
419
- fail(`run file ${filePath} has no runId`);
420
- return 1;
421
- }
422
- try {
423
- const run = await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}/runs/${reduced.runId}`, { method: "PUT", token: c.token, json: reduced.body });
424
- if (opts.json) {
425
- emitJson(run);
426
- return 0;
427
- }
428
- ok(`Pushed run ${reduced.runId} → ${run.status ?? reduced.body.status}`);
429
- return 0;
430
- }
431
- catch (err) {
432
- if (err instanceof HttpError && err.status === 404) {
433
- fail(`workflow '${name}' not found`);
434
- return 1;
435
- }
436
- fail(err.message);
437
- return 1;
438
- }
439
- }
440
- /**
441
- * Backoff schedule for the *terminal* push. A running snapshot that fails is
442
- * harmless — the next tick retries it — but the terminal snapshot is the one
443
- * that flips the server run to its final state, and there is no next tick. If
444
- * it is dropped on a transient error the server run is stranded as 'running'
445
- * and the detail page polls it forever, so it gets its own bounded retry.
446
- */
447
- const TERMINAL_RETRY_DELAYS_MS = [2000, 4000, 8000, 16000, 32000];
448
- /**
449
- * Cold-start grace window. The watcher is often launched *before* the Workflow
450
- * tool has written the first wf_*.json (slow orchestration). Until the first
451
- * successful read we tolerate a missing/unreadable file for this long, rather
452
- * than letting the mid-run 5-strike cap (~25s at --interval 5) kill the watcher
453
- * and leave the run with zero snapshots.
454
- */
455
- const STARTUP_GRACE_MS = 120_000;
456
- /**
457
- * Push the terminal snapshot, retrying transient failures with exponential
458
- * backoff. Returns 0 once accepted, 1 on a 404 (workflow gone) or after the
459
- * retry budget is exhausted.
460
- */
461
- async function pushTerminalSnapshot(c, name, reduced, status, sleep, delays) {
462
- for (let attempt = 0;; attempt++) {
463
- try {
464
- await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}/runs/${reduced.runId}`, { method: "PUT", token: c.token, json: reduced.body });
465
- ok(`pushed ${reduced.runId} → ${status}`);
466
- ok(`run ${reduced.runId} finished: ${status}`);
467
- return 0;
468
- }
469
- catch (err) {
470
- if (err instanceof HttpError && err.status === 404) {
471
- fail(`workflow '${name}' not found`);
472
- return 1;
473
- }
474
- if (attempt >= delays.length) {
475
- fail(`final push failed after ${delays.length + 1} attempts: ${err.message}`);
476
- return 1;
477
- }
478
- const delay = delays[attempt];
479
- warn(`final push failed (${err.message}) — retry ${attempt + 1}/${delays.length} in ${delay / 1000}s`);
480
- await sleep(delay);
481
- }
482
- }
483
- }
484
- export async function runWorkflowWatchRun(name, filePath, opts, deps = {}) {
485
- const c = await ctx(opts.namespace);
486
- if (!c)
487
- return 1;
488
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
489
- const terminalDelays = deps.terminalRetryDelaysMs ?? TERMINAL_RETRY_DELAYS_MS;
490
- const startupGraceMs = deps.startupGraceMs ?? STARTUP_GRACE_MS;
491
- const parsed = Number(opts.interval);
492
- const intervalMs = (Number.isFinite(parsed) && parsed > 0 ? parsed : 5) * 1000;
493
- // Cold-start budget: how many read attempts fit in the grace window.
494
- const startupAttempts = Math.max(1, Math.ceil(startupGraceMs / intervalMs));
495
- let hasReadOnce = false; // has the run file ever been read successfully?
496
- let readFailures = 0; // consecutive failures AFTER the first success (mid-run)
497
- let startupMisses = 0; // consecutive misses BEFORE the first success (cold start)
498
- for (;;) {
499
- let reduced = null;
500
- try {
501
- reduced = readRunFile(filePath);
502
- hasReadOnce = true;
503
- readFailures = 0;
504
- }
505
- catch (err) {
506
- if (!hasReadOnce) {
507
- // Cold start: the Workflow tool may not have written the run file yet.
508
- // Wait out the grace window instead of treating a not-yet-created file
509
- // as fatal — otherwise a slow orchestrator start strands the run with
510
- // zero snapshots. Log once, quietly, so we don't spam every tick.
511
- startupMisses += 1;
512
- if (startupMisses === 1)
513
- warn(`waiting for run file ${filePath} …`);
514
- if (startupMisses >= startupAttempts) {
515
- fail(`run file ${filePath} did not appear within ${Math.round(startupGraceMs / 1000)}s — giving up: ${err.message}`);
516
- return 1;
517
- }
518
- }
519
- else {
520
- // Mid-run: the file existed and now can't be read (corrupt/deleted mid-
521
- // write). Retry a few times, but don't spin forever.
522
- readFailures += 1;
523
- if (readFailures >= 5) {
524
- fail(`cannot read run file ${filePath}: ${err.message}`);
525
- return 1;
526
- }
527
- warn(`run file not readable (${err.message}) — retrying`);
528
- }
529
- }
530
- if (reduced) {
531
- if (!reduced.runId) {
532
- fail(`run file ${filePath} has no runId`);
533
- return 1;
534
- }
535
- const status = reduced.body.status;
536
- if (status !== "running") {
537
- // Terminal snapshot — retry with backoff so a transient failure can't
538
- // strand the server run as 'running'.
539
- return pushTerminalSnapshot(c, name, reduced, status, sleep, terminalDelays);
540
- }
541
- try {
542
- await requestJson(c.remote, `/namespaces/${c.ns}/workflows/${name}/runs/${reduced.runId}`, { method: "PUT", token: c.token, json: reduced.body });
543
- ok(`pushed ${reduced.runId} → ${status}`);
544
- }
545
- catch (err) {
546
- if (err instanceof HttpError && err.status === 404) {
547
- fail(`workflow '${name}' not found`);
548
- return 1;
549
- }
550
- // A running snapshot is disposable — the next tick will retry it.
551
- warn(`push failed (${err.message}) — retrying`);
552
- }
553
- }
554
- await sleep(intervalMs);
555
- }
556
- }
557
- // ─── registration ─────────────────────────────────────────────────────────────
558
- export function registerWorkflowCommand(program) {
559
- const workflow = program
560
- .command("workflow")
561
- .description("manage workflows and push run traces");
562
- // ── workflow list ──
563
- workflow
564
- .command("list")
565
- .description("list workflows in a namespace")
566
- .option("--namespace <slug>", "namespace slug (default: active login)")
567
- .option("--json", "emit JSON")
568
- .action(async (opts) => process.exit(await runWorkflowList(opts)));
569
- // ── workflow create ──
570
- workflow
571
- .command("create")
572
- .description("create a workflow (optionally with an initial version)")
573
- .argument("<name>", "workflow name (unique within namespace)")
574
- .option("--namespace <slug>", "namespace slug (default: active login)")
575
- .option("--file <path>", "workflow JS script — meta is parsed and an initial version created")
576
- .option("--description <text>", "description")
577
- .option("--tags <csv>", "comma-separated tags")
578
- .option("--message <text>", "version message (used with --file)")
579
- .option("--json", "emit JSON")
580
- .action(async (name, opts) => process.exit(await runWorkflowCreate(name, opts)));
581
- // ── workflow show ──
582
- workflow
583
- .command("show")
584
- .description("show workflow detail with current version meta")
585
- .argument("<name>", "workflow name")
586
- .option("--namespace <slug>", "namespace slug (default: active login)")
587
- .option("--json", "emit JSON")
588
- .action(async (name, opts) => process.exit(await runWorkflowShow(name, opts)));
589
- // ── workflow update ──
590
- workflow
591
- .command("update")
592
- .description("update metadata or upload a new version")
593
- .argument("<name>", "workflow name")
594
- .option("--namespace <slug>", "namespace slug (default: active login)")
595
- .option("--file <path>", "new workflow JS script — creates a new version")
596
- .option("--description <text>", "new description")
597
- .option("--tags <csv>", "new tags (comma-separated, replaces existing)")
598
- .option("--message <text>", "version message (used with --file)")
599
- .option("--json", "emit JSON")
600
- .action(async (name, opts) => process.exit(await runWorkflowUpdate(name, opts)));
601
- // ── workflow delete ──
602
- workflow
603
- .command("delete")
604
- .description("soft-delete a workflow (preserves version history)")
605
- .argument("<name>", "workflow name")
606
- .option("--namespace <slug>", "namespace slug (default: active login)")
607
- .option("--yes", "skip confirmation prompt")
608
- .action(async (name, opts) => process.exit(await runWorkflowDelete(name, opts)));
609
- // ── workflow push-run ──
610
- workflow
611
- .command("push-run")
612
- .description("push one run-trace snapshot from a local wf_*.json run file")
613
- .argument("<name>", "workflow name")
614
- .argument("<file>", "path to the local wf_*.json run file")
615
- .option("--namespace <slug>", "namespace slug (default: active login)")
616
- .option("--json", "emit JSON")
617
- .action(async (name, file, opts) => process.exit(await runWorkflowPushRun(name, file, opts)));
618
- // ── workflow watch-run ──
619
- workflow
620
- .command("watch-run")
621
- .description("push snapshots until the run file leaves 'running', then a final push")
622
- .argument("<name>", "workflow name")
623
- .argument("<file>", "path to the local wf_*.json run file")
624
- .option("--interval <seconds>", "seconds between pushes", "5")
625
- .option("--namespace <slug>", "namespace slug (default: active login)")
626
- .action(async (name, file, opts) => process.exit(await runWorkflowWatchRun(name, file, opts)));
627
- }