@runcofactor/cli 0.10.0

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.
Files changed (150) hide show
  1. package/README.md +125 -0
  2. package/dist/api-catalog-GFGCLVS4.js +401 -0
  3. package/dist/api-catalog-GFGCLVS4.js.map +1 -0
  4. package/dist/attachment-SJC7D7FY.js +276 -0
  5. package/dist/attachment-SJC7D7FY.js.map +1 -0
  6. package/dist/auth-EN6NQCJY.js +31 -0
  7. package/dist/auth-EN6NQCJY.js.map +1 -0
  8. package/dist/bin.js +7 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/chunk-3CZU7XCS.js +272 -0
  11. package/dist/chunk-3CZU7XCS.js.map +1 -0
  12. package/dist/chunk-4NRDONSC.js +1048 -0
  13. package/dist/chunk-4NRDONSC.js.map +1 -0
  14. package/dist/chunk-4PNOV7OV.js +51980 -0
  15. package/dist/chunk-4PNOV7OV.js.map +1 -0
  16. package/dist/chunk-52IO7ACJ.js +2941 -0
  17. package/dist/chunk-52IO7ACJ.js.map +1 -0
  18. package/dist/chunk-6LEZCKKV.js +663 -0
  19. package/dist/chunk-6LEZCKKV.js.map +1 -0
  20. package/dist/chunk-A4OFTUVV.js +170 -0
  21. package/dist/chunk-A4OFTUVV.js.map +1 -0
  22. package/dist/chunk-B2VISN2V.js +2459 -0
  23. package/dist/chunk-B2VISN2V.js.map +1 -0
  24. package/dist/chunk-CLAWT7BJ.js +271 -0
  25. package/dist/chunk-CLAWT7BJ.js.map +1 -0
  26. package/dist/chunk-GKY2KXO5.js +281 -0
  27. package/dist/chunk-GKY2KXO5.js.map +1 -0
  28. package/dist/chunk-I4QLI3JO.js +413 -0
  29. package/dist/chunk-I4QLI3JO.js.map +1 -0
  30. package/dist/chunk-IMPZAX6L.js +180 -0
  31. package/dist/chunk-IMPZAX6L.js.map +1 -0
  32. package/dist/chunk-J6SH7GQT.js +241 -0
  33. package/dist/chunk-J6SH7GQT.js.map +1 -0
  34. package/dist/chunk-JDBCZ3Z2.js +298 -0
  35. package/dist/chunk-JDBCZ3Z2.js.map +1 -0
  36. package/dist/chunk-JICSYFRJ.js +46 -0
  37. package/dist/chunk-JICSYFRJ.js.map +1 -0
  38. package/dist/chunk-JWHYXEA2.js +32 -0
  39. package/dist/chunk-JWHYXEA2.js.map +1 -0
  40. package/dist/chunk-N7HFALX3.js +88 -0
  41. package/dist/chunk-N7HFALX3.js.map +1 -0
  42. package/dist/chunk-Q4BO4Z5V.js +178 -0
  43. package/dist/chunk-Q4BO4Z5V.js.map +1 -0
  44. package/dist/chunk-QFDOWABX.js +195 -0
  45. package/dist/chunk-QFDOWABX.js.map +1 -0
  46. package/dist/chunk-SJJW77WN.js +605 -0
  47. package/dist/chunk-SJJW77WN.js.map +1 -0
  48. package/dist/chunk-VKVL7WBN.js +50 -0
  49. package/dist/chunk-VKVL7WBN.js.map +1 -0
  50. package/dist/chunk-WOHL4FTB.js +353 -0
  51. package/dist/chunk-WOHL4FTB.js.map +1 -0
  52. package/dist/chunk-XQXVG7RA.js +426 -0
  53. package/dist/chunk-XQXVG7RA.js.map +1 -0
  54. package/dist/chunk-ZD3KHESI.js +94 -0
  55. package/dist/chunk-ZD3KHESI.js.map +1 -0
  56. package/dist/credential-COLTBV6D.js +450 -0
  57. package/dist/credential-COLTBV6D.js.map +1 -0
  58. package/dist/credential-template-EEPYFG7R.js +291 -0
  59. package/dist/credential-template-EEPYFG7R.js.map +1 -0
  60. package/dist/debug-workflow-KOVH5GOP.js +259 -0
  61. package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
  62. package/dist/dist-44PZR7BQ.js +423 -0
  63. package/dist/dist-44PZR7BQ.js.map +1 -0
  64. package/dist/docs-3SSORHB5.js +366 -0
  65. package/dist/docs-3SSORHB5.js.map +1 -0
  66. package/dist/doctor-OTSIWV6G.js +132 -0
  67. package/dist/doctor-OTSIWV6G.js.map +1 -0
  68. package/dist/feature-flag-T2LRZME3.js +283 -0
  69. package/dist/feature-flag-T2LRZME3.js.map +1 -0
  70. package/dist/file-type-S3UUBVCT.js +4205 -0
  71. package/dist/file-type-S3UUBVCT.js.map +1 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +982 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/init-TGWRECT2.js +208 -0
  76. package/dist/init-TGWRECT2.js.map +1 -0
  77. package/dist/install-DXSNWZKO.js +295 -0
  78. package/dist/install-DXSNWZKO.js.map +1 -0
  79. package/dist/issues-XIZOPX6T.js +564 -0
  80. package/dist/issues-XIZOPX6T.js.map +1 -0
  81. package/dist/knobs-NBINZIUB.js +295 -0
  82. package/dist/knobs-NBINZIUB.js.map +1 -0
  83. package/dist/list-3TDHK637.js +59 -0
  84. package/dist/list-3TDHK637.js.map +1 -0
  85. package/dist/local-HICVD6PU.js +100 -0
  86. package/dist/local-HICVD6PU.js.map +1 -0
  87. package/dist/login-UOIUXVUK.js +315 -0
  88. package/dist/login-UOIUXVUK.js.map +1 -0
  89. package/dist/mcp-54IJEOUA.js +1956 -0
  90. package/dist/mcp-54IJEOUA.js.map +1 -0
  91. package/dist/pdf-extract-ABWV3L5X.js +13 -0
  92. package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
  93. package/dist/pdfjs-DQ2YA5S3.js +41561 -0
  94. package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
  95. package/dist/property-UXLOLPTD.js +701 -0
  96. package/dist/property-UXLOLPTD.js.map +1 -0
  97. package/dist/property-skill-guide.md +189 -0
  98. package/dist/psql-BYUUNLGG.js +115 -0
  99. package/dist/psql-BYUUNLGG.js.map +1 -0
  100. package/dist/record-4TR2UIIS.js +233 -0
  101. package/dist/record-4TR2UIIS.js.map +1 -0
  102. package/dist/redis-35VBUYW7.js +116 -0
  103. package/dist/redis-35VBUYW7.js.map +1 -0
  104. package/dist/release-OOLTUBUT.js +354 -0
  105. package/dist/release-OOLTUBUT.js.map +1 -0
  106. package/dist/sandbox-XGSUU6G7.js +2046 -0
  107. package/dist/sandbox-XGSUU6G7.js.map +1 -0
  108. package/dist/scenarios-GWLJEGHA.js +280 -0
  109. package/dist/scenarios-GWLJEGHA.js.map +1 -0
  110. package/dist/session-MYFNZPRP.js +2414 -0
  111. package/dist/session-MYFNZPRP.js.map +1 -0
  112. package/dist/skill-KM34LDMO.js +1839 -0
  113. package/dist/skill-KM34LDMO.js.map +1 -0
  114. package/dist/skills/debug-run/SKILL.md +262 -0
  115. package/dist/skills/debug-run/history/1.0.0.md +244 -0
  116. package/dist/skills/debug-run/history/1.0.1.md +245 -0
  117. package/dist/skills/debug-run/history/1.0.2.md +254 -0
  118. package/dist/skills/debug-run/history/1.1.0.md +262 -0
  119. package/dist/skills/debug-run/version.json +3 -0
  120. package/dist/spec-RA3CFEKG.js +951 -0
  121. package/dist/spec-RA3CFEKG.js.map +1 -0
  122. package/dist/src-LB4HBTJQ.js +1516 -0
  123. package/dist/src-LB4HBTJQ.js.map +1 -0
  124. package/dist/src-N3TPKGTY.js +244 -0
  125. package/dist/src-N3TPKGTY.js.map +1 -0
  126. package/dist/src-UVDX5UP6.js +17 -0
  127. package/dist/src-UVDX5UP6.js.map +1 -0
  128. package/dist/start-PSEQSQA3.js +151 -0
  129. package/dist/start-PSEQSQA3.js.map +1 -0
  130. package/dist/tag-YY6YAABQ.js +22 -0
  131. package/dist/tag-YY6YAABQ.js.map +1 -0
  132. package/dist/task-IQC3YR5K.js +922 -0
  133. package/dist/task-IQC3YR5K.js.map +1 -0
  134. package/dist/test-suite-DZNJFXDW.js +394 -0
  135. package/dist/test-suite-DZNJFXDW.js.map +1 -0
  136. package/dist/trace-HXIW7U2D.js +14 -0
  137. package/dist/trace-HXIW7U2D.js.map +1 -0
  138. package/dist/triage-ORG45R5W.js +16 -0
  139. package/dist/triage-ORG45R5W.js.map +1 -0
  140. package/dist/upgrade-L2VYMP2V.js +61 -0
  141. package/dist/upgrade-L2VYMP2V.js.map +1 -0
  142. package/dist/workflow-XYFH5LTP.js +3708 -0
  143. package/dist/workflow-XYFH5LTP.js.map +1 -0
  144. package/dist/workflow-cache-NQJQFWMV.js +253 -0
  145. package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
  146. package/dist/workflow-debug-BGD4UIXU.js +927 -0
  147. package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
  148. package/dist/workflow-proposals-MTUDX46O.js +382 -0
  149. package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
  150. package/package.json +61 -0
@@ -0,0 +1,3708 @@
1
+ import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
+ import {
3
+ printGuide
4
+ } from "./chunk-4NRDONSC.js";
5
+ import {
6
+ workflowToSpec
7
+ } from "./chunk-B2VISN2V.js";
8
+ import {
9
+ applyWorkflowTagFlags
10
+ } from "./chunk-XQXVG7RA.js";
11
+ import {
12
+ alignSnapshotNodeIds
13
+ } from "./chunk-JDBCZ3Z2.js";
14
+ import {
15
+ apiRequest,
16
+ renderWorkflowUserError
17
+ } from "./chunk-WOHL4FTB.js";
18
+ import {
19
+ NODE_TYPE_LABEL,
20
+ USER_SETTABLE_WORKFLOW_STATUSES,
21
+ normalizeNodeType
22
+ } from "./chunk-SJJW77WN.js";
23
+ import {
24
+ apiFetch,
25
+ exitOnFailure,
26
+ fetchOrExit
27
+ } from "./chunk-3CZU7XCS.js";
28
+ import {
29
+ getArgValue,
30
+ hasFlag,
31
+ resolveAppUrl,
32
+ resolveConfig
33
+ } from "./chunk-A4OFTUVV.js";
34
+ import "./chunk-JWHYXEA2.js";
35
+ import "./chunk-VKVL7WBN.js";
36
+
37
+ // src/workflow.ts
38
+ import fs5 from "fs/promises";
39
+ import process9 from "process";
40
+
41
+ // src/workflow-create.ts
42
+ import fs from "fs/promises";
43
+ import process2 from "process";
44
+ import { createParser } from "eventsource-parser";
45
+ async function readFromStdin() {
46
+ const chunks = [];
47
+ const stdin = process2.stdin;
48
+ if (stdin.isTTY) {
49
+ throw new Error("No input on stdin. Pipe workflow JSON or use --from-file.");
50
+ }
51
+ return new Promise((resolve, reject) => {
52
+ stdin.on("data", (chunk) => {
53
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
54
+ });
55
+ stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
56
+ stdin.on("error", reject);
57
+ });
58
+ }
59
+ async function readWorkflowInput(argv) {
60
+ let raw;
61
+ const fromFile = getArgValue(argv, "--from-file");
62
+ if (fromFile) {
63
+ try {
64
+ raw = await fs.readFile(fromFile, "utf-8");
65
+ } catch (err) {
66
+ throw new Error(`Failed to read file ${fromFile}: ${err instanceof Error ? err.message : String(err)}`);
67
+ }
68
+ } else {
69
+ raw = await readFromStdin();
70
+ }
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(raw);
74
+ } catch {
75
+ throw new Error("Invalid JSON input. Please provide valid workflow JSON.");
76
+ }
77
+ const input = parsed;
78
+ if (!input.name || typeof input.name !== "string") {
79
+ throw new Error('Workflow JSON must include a "name" string field.');
80
+ }
81
+ if (!Array.isArray(input.nodes) || input.nodes.length === 0) {
82
+ throw new Error('Workflow JSON must include a non-empty "nodes" array.');
83
+ }
84
+ const validNodeTypes = ["login", "navigate", "action", "assertion", "end", "setup", "seed", "wait", "condition", "api_sequence", "notification"];
85
+ for (const node of input.nodes) {
86
+ if (typeof node.nodeType === "string") {
87
+ node.nodeType = normalizeNodeType(node.nodeType);
88
+ }
89
+ if (!node.nodeType || !validNodeTypes.includes(node.nodeType)) {
90
+ throw new Error(
91
+ `Invalid nodeType "${node.nodeType}". Valid types: ${validNodeTypes.join(", ")}`
92
+ );
93
+ }
94
+ }
95
+ return input;
96
+ }
97
+ async function fetchOrgCredentials(apiUrl, token) {
98
+ const res = await fetch(`${apiUrl}/org/credentials`, {
99
+ headers: { Authorization: `Bearer ${token}` }
100
+ });
101
+ if (!res.ok) return [];
102
+ const data = await res.json();
103
+ return (data.items ?? []).filter((c) => !c.isEphemeral);
104
+ }
105
+ async function resolveCredential(apiUrl, token, credentialName) {
106
+ const credentials = await fetchOrgCredentials(apiUrl, token);
107
+ if (credentials.length === 0) {
108
+ throw new Error(
109
+ "No credentials found for this organization. Create one in the web app first."
110
+ );
111
+ }
112
+ if (credentialName) {
113
+ const match = credentials.find(
114
+ (c) => c.name.toLowerCase() === credentialName.toLowerCase()
115
+ );
116
+ if (match) {
117
+ const isLocal = /\blocal\b/i.test(match.name);
118
+ if (isLocal) {
119
+ console.warn(
120
+ `Warning: Credential "${match.name}" appears to be local-only. The workflow may fail in deployed environments. Consider using a non-local credential.`
121
+ );
122
+ }
123
+ return { credentialId: match.id, credentialName: match.name, propertyId: match.propertyId };
124
+ }
125
+ console.warn(`Warning: No credential matching name "${credentialName}". Trying other resolution strategies.`);
126
+ }
127
+ const defaults = credentials.filter((c) => c.isDefault);
128
+ if (defaults.length === 1) {
129
+ console.log(`Auto-selected default credential: ${defaults[0].name} (${defaults[0].id})`);
130
+ return { credentialId: defaults[0].id, credentialName: defaults[0].name, propertyId: defaults[0].propertyId };
131
+ }
132
+ const nonLocal = credentials.filter((c) => !/\blocal\b/i.test(c.name));
133
+ if (nonLocal.length === 1) {
134
+ console.log(`Auto-selected credential: ${nonLocal[0].name} (${nonLocal[0].id})`);
135
+ return { credentialId: nonLocal[0].id, credentialName: nonLocal[0].name, propertyId: nonLocal[0].propertyId };
136
+ }
137
+ const lines = credentials.map(
138
+ (c) => ` ${c.id} ${c.name}${c.isDefault ? " (default)" : ""}${c.propertyName ? ` \u2014 ${c.propertyName}` : ""}`
139
+ );
140
+ throw new Error(
141
+ `Cannot auto-resolve credential. Specify credentialId in the login node configJson.
142
+
143
+ Available credentials:
144
+ ${lines.join("\n")}`
145
+ );
146
+ }
147
+ async function fetchSuiteLiveState(apiUrl, token, suiteId) {
148
+ try {
149
+ const res = await fetch(`${apiUrl}/workflows/test-executions/${suiteId}/live-state`, {
150
+ headers: { Authorization: `Bearer ${token}` }
151
+ });
152
+ if (!res.ok) return null;
153
+ return await res.json();
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+ async function runAndPublish(opts) {
159
+ const { apiUrl, token, workflowId, workflowName, timeoutMs } = opts;
160
+ const body = {};
161
+ if (opts.propertyId) body.propertyId = opts.propertyId;
162
+ if (opts.environmentId) body.environmentId = opts.environmentId;
163
+ const triggerUrl = `${apiUrl}/workflows/test-runs?namePattern=${encodeURIComponent(workflowName)}`;
164
+ const triggerRes = await fetchOrExit(triggerUrl, {
165
+ method: "POST",
166
+ headers: {
167
+ Authorization: `Bearer ${token}`,
168
+ "Content-Type": "application/json"
169
+ },
170
+ token,
171
+ body: JSON.stringify(body)
172
+ });
173
+ if (!triggerRes.ok) {
174
+ const errorText = await triggerRes.text();
175
+ console.error(`Failed to trigger test run: ${triggerRes.status} ${errorText}`);
176
+ return false;
177
+ }
178
+ const triggerData = await triggerRes.json();
179
+ if (!triggerData.ok || !triggerData.suiteId) {
180
+ console.error(`Failed to trigger test run: ${triggerData.error ?? "Unknown error"}`);
181
+ return false;
182
+ }
183
+ const { suiteId } = triggerData;
184
+ console.log(`Test run started (suite: ${suiteId})`);
185
+ console.log("Streaming results...\n");
186
+ const streamUrl = `${apiUrl}/workflows/test-runs/stream?suiteId=${suiteId}`;
187
+ const streamRes = await fetchOrExit(streamUrl, {
188
+ headers: {
189
+ Authorization: `Bearer ${token}`,
190
+ Accept: "text/event-stream"
191
+ },
192
+ token,
193
+ // SSE connections stay open; don't let the default 30s timeout cut them off.
194
+ timeoutMs: 0
195
+ });
196
+ if (!streamRes.ok || !streamRes.body) {
197
+ console.error(`Failed to connect to event stream: ${streamRes.status}`);
198
+ return false;
199
+ }
200
+ let success = false;
201
+ let hasCompleted = false;
202
+ const parser = createParser({
203
+ onEvent: (event) => {
204
+ if (!event.data) return;
205
+ try {
206
+ const data = JSON.parse(event.data);
207
+ if (event.event === "workflow-test") {
208
+ const testEvent = data;
209
+ if (testEvent.status === "success") {
210
+ console.log(` \u2713 ${workflowName}`);
211
+ success = true;
212
+ } else if (testEvent.status === "failed") {
213
+ console.log(` \u2717 ${workflowName}`);
214
+ if (testEvent.errorMessage) {
215
+ console.log(` Error: ${testEvent.errorMessage.slice(0, 200)}`);
216
+ }
217
+ success = false;
218
+ } else if (testEvent.status === "running") {
219
+ console.log(` \u25B6 Running ${workflowName}...`);
220
+ }
221
+ }
222
+ if (event.event === "workflow-test-suite") {
223
+ const suiteEvent = data;
224
+ if (suiteEvent.status === "completed") {
225
+ hasCompleted = true;
226
+ }
227
+ }
228
+ } catch {
229
+ }
230
+ }
231
+ });
232
+ const reader = streamRes.body.getReader();
233
+ const decoder = new TextDecoder();
234
+ let timedOut = false;
235
+ const timeoutHandle = setTimeout(() => {
236
+ timedOut = true;
237
+ reader.cancel().catch(() => {
238
+ });
239
+ }, timeoutMs);
240
+ try {
241
+ while (!hasCompleted) {
242
+ const { done, value } = await reader.read();
243
+ if (done) break;
244
+ parser.feed(decoder.decode(value, { stream: true }));
245
+ }
246
+ } finally {
247
+ clearTimeout(timeoutHandle);
248
+ try {
249
+ reader.releaseLock();
250
+ } catch {
251
+ }
252
+ }
253
+ if (!hasCompleted) {
254
+ const reason = timedOut ? `Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for test completion` : "Event stream closed before completion";
255
+ console.warn(`
256
+ \u26A0 ${reason}. Polling final state...`);
257
+ const live = await fetchSuiteLiveState(apiUrl, token, suiteId);
258
+ const exec = live?.execution;
259
+ if (exec && (exec.status === "completed" || exec.status === "completed_with_errors")) {
260
+ success = exec.failedWorkflows === 0;
261
+ const match = live?.runs?.find((r) => r.workflowId === workflowId);
262
+ if (match) {
263
+ console.log(
264
+ success ? ` \u2713 ${workflowName}` : ` \u2717 ${workflowName}`
265
+ );
266
+ if (!success && match.errorMessage) {
267
+ console.log(` Error: ${match.errorMessage.slice(0, 200)}`);
268
+ }
269
+ }
270
+ } else {
271
+ console.error(
272
+ `Suite ${suiteId} is still ${exec?.status ?? "unknown"} on the server. Check it in the web app.`
273
+ );
274
+ return false;
275
+ }
276
+ }
277
+ if (success) {
278
+ try {
279
+ const publishRes = await fetchOrExit(`${apiUrl}/workflows/${workflowId}/publish`, {
280
+ method: "POST",
281
+ headers: {
282
+ Authorization: `Bearer ${token}`,
283
+ "Content-Type": "application/json"
284
+ },
285
+ token
286
+ });
287
+ if (publishRes.ok) {
288
+ console.log(`
289
+ \u2713 Workflow published successfully`);
290
+ return true;
291
+ } else {
292
+ console.error(`
293
+ Failed to publish workflow: ${publishRes.status}`);
294
+ return false;
295
+ }
296
+ } catch (err) {
297
+ console.error(`
298
+ Failed to publish workflow: ${err}`);
299
+ return false;
300
+ }
301
+ }
302
+ console.log(`
303
+ \u2717 Test failed \u2014 workflow left in draft for manual review`);
304
+ return false;
305
+ }
306
+ async function readApiSequenceConfig(argv) {
307
+ const fromFile = getArgValue(argv, "--sequence-from-file");
308
+ const fromStdin = hasFlag(argv, "--sequence-from-stdin");
309
+ if (!fromFile && !fromStdin) {
310
+ return { steps: [] };
311
+ }
312
+ let raw;
313
+ if (fromFile) {
314
+ raw = await fs.readFile(fromFile, "utf-8");
315
+ } else {
316
+ raw = await readFromStdin();
317
+ }
318
+ let parsed;
319
+ try {
320
+ parsed = JSON.parse(raw);
321
+ } catch {
322
+ throw new Error("Invalid JSON for sequence config.");
323
+ }
324
+ const cfg = parsed;
325
+ if (!cfg || !Array.isArray(cfg.steps)) {
326
+ throw new Error('Sequence config must be { "steps": [...] }.');
327
+ }
328
+ return cfg;
329
+ }
330
+ async function handleApiOnlyCreate(argv, apiUrl, token) {
331
+ const name = getArgValue(argv, "--name");
332
+ if (!name) {
333
+ console.error("Error: --api-only requires --name <workflow name>.");
334
+ process2.exit(1);
335
+ }
336
+ const credentialId = getArgValue(argv, "--credential");
337
+ const guidance = getArgValue(argv, "--guidance");
338
+ let config;
339
+ try {
340
+ config = await readApiSequenceConfig(argv);
341
+ } catch (err) {
342
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
343
+ process2.exit(1);
344
+ }
345
+ const body = { name, config };
346
+ if (credentialId) body.credentialId = credentialId;
347
+ if (guidance) body.guidancePrompt = guidance;
348
+ const res = await fetchOrExit(`${apiUrl}/api-sequences/create-with-workflow`, {
349
+ method: "POST",
350
+ headers: {
351
+ Authorization: `Bearer ${token}`,
352
+ "Content-Type": "application/json"
353
+ },
354
+ token,
355
+ body: JSON.stringify(body)
356
+ });
357
+ const raw = await res.json().catch(() => ({}));
358
+ if (!res.ok || !raw.ok || !raw.data) {
359
+ const jsonOutput = hasFlag(argv, "--json") || !process2.stdout.isTTY;
360
+ if (raw.code === "API_SEQUENCE_INVALID" && Array.isArray(raw.issues)) {
361
+ if (jsonOutput) {
362
+ console.log(
363
+ JSON.stringify(
364
+ {
365
+ ok: false,
366
+ error: raw.error ?? "API sequence configuration is invalid",
367
+ code: "API_SEQUENCE_INVALID",
368
+ issueCount: raw.issues.length,
369
+ issues: raw.issues
370
+ },
371
+ null,
372
+ 2
373
+ )
374
+ );
375
+ } else {
376
+ console.error(`Error: ${raw.error ?? "API sequence configuration is invalid"}`);
377
+ console.error(`Found ${raw.issues.length} issue${raw.issues.length === 1 ? "" : "s"}:`);
378
+ console.error("");
379
+ for (const issue of raw.issues) {
380
+ console.error(` ${issue.path || "(root)"} [${issue.code}] :: ${issue.message}`);
381
+ if (issue.hint) console.error(` hint: ${issue.hint}`);
382
+ }
383
+ }
384
+ process2.exit(1);
385
+ }
386
+ console.error(`Error: ${raw.error ?? `${res.status} ${res.statusText}`}`);
387
+ process2.exit(1);
388
+ }
389
+ if (hasFlag(argv, "--json") || !process2.stdout.isTTY) {
390
+ console.log(JSON.stringify({ ok: true, data: raw.data }, null, 2));
391
+ return;
392
+ }
393
+ const appUrl = resolveAppUrl(apiUrl);
394
+ console.log(`
395
+ \u2713 API-only workflow created: ${name}`);
396
+ console.log(` Workflow ID: ${raw.data.workflowId}`);
397
+ console.log(` API Sequence ID: ${raw.data.apiSequenceId}`);
398
+ if (raw.data.nodeId) console.log(` Node ID: ${raw.data.nodeId}`);
399
+ console.log(` Steps: ${config.steps.length}`);
400
+ console.log(` URL: ${appUrl}/flows/${raw.data.workflowId}`);
401
+ console.log(`
402
+ Next:`);
403
+ console.log(` canary workflow api-sequence get ${raw.data.workflowId}`);
404
+ console.log(` canary workflow api-sequence update ${raw.data.workflowId} --from-file seq.json`);
405
+ console.log(` canary workflow api-sequence execute ${raw.data.workflowId}`);
406
+ }
407
+ async function handleWorkflowCreate(argv, apiUrl, token) {
408
+ if (hasFlag(argv, "--api-only")) {
409
+ await handleApiOnlyCreate(argv, apiUrl, token);
410
+ return;
411
+ }
412
+ const runAndPublishFlag = hasFlag(argv, "--run-and-publish");
413
+ const propertyArg = getArgValue(argv, "--property");
414
+ const environmentArg = getArgValue(argv, "--environment");
415
+ const timeoutArg = getArgValue(argv, "--publish-timeout");
416
+ const timeoutSec = timeoutArg ? Number.parseInt(timeoutArg, 10) : 900;
417
+ if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
418
+ console.error(`Error: --publish-timeout must be a positive integer (seconds), got "${timeoutArg}"`);
419
+ process2.exit(1);
420
+ }
421
+ let input;
422
+ try {
423
+ input = await readWorkflowInput(argv);
424
+ } catch (err) {
425
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
426
+ process2.exit(1);
427
+ }
428
+ const loginNodes = input.nodes.filter((n) => n.nodeType === "login");
429
+ for (const loginNode of loginNodes) {
430
+ const config = loginNode.configJson ?? {};
431
+ if (!config.credentialId && !input.credentialId) {
432
+ try {
433
+ const resolved = await resolveCredential(
434
+ apiUrl,
435
+ token,
436
+ config.credentialName
437
+ );
438
+ config.credentialId = resolved.credentialId;
439
+ config.credentialName = resolved.credentialName;
440
+ loginNode.configJson = config;
441
+ if (resolved.propertyId && !input.propertyId) {
442
+ input.propertyId = resolved.propertyId;
443
+ }
444
+ } catch (err) {
445
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
446
+ process2.exit(1);
447
+ }
448
+ }
449
+ }
450
+ input.createdVia = "cli";
451
+ const res = await fetchOrExit(`${apiUrl}/workflows/create-with-nodes`, {
452
+ method: "POST",
453
+ headers: {
454
+ Authorization: `Bearer ${token}`,
455
+ "Content-Type": "application/json"
456
+ },
457
+ token,
458
+ body: JSON.stringify(input)
459
+ });
460
+ const result = await res.json();
461
+ if (!result.ok || !result.workflow) {
462
+ console.error(`Error: ${result.error ?? "Failed to create workflow"}`);
463
+ process2.exit(1);
464
+ }
465
+ const { workflow, nodes, edges } = result;
466
+ const { warnings } = result;
467
+ console.log(`
468
+ \u2713 Workflow created: ${workflow.name}`);
469
+ console.log(` ID: ${workflow.id}`);
470
+ console.log(` Status: ${workflow.status}`);
471
+ console.log(` Nodes: ${nodes?.length ?? 0}`);
472
+ console.log(` Edges: ${edges?.length ?? 0}`);
473
+ if (warnings && warnings.length > 0) {
474
+ console.log(`
475
+ Warnings:`);
476
+ for (const w of warnings) {
477
+ console.log(` \u26A0 ${w}`);
478
+ }
479
+ }
480
+ const appUrl = resolveAppUrl(apiUrl);
481
+ console.log(` URL: ${appUrl}/flows/${workflow.id}`);
482
+ try {
483
+ const tagResult = await applyWorkflowTagFlags({
484
+ apiUrl,
485
+ token,
486
+ argv,
487
+ workflowId: workflow.id
488
+ });
489
+ if (tagResult) {
490
+ console.log(` Tags: ${tagResult.tagIds.length} assigned`);
491
+ }
492
+ } catch (err) {
493
+ console.error(`
494
+ Error applying tags:
495
+ ${err instanceof Error ? err.message : String(err)}`);
496
+ process2.exit(1);
497
+ }
498
+ if (runAndPublishFlag) {
499
+ console.log("\nTriggering test run...");
500
+ const passed = await runAndPublish({
501
+ apiUrl,
502
+ token,
503
+ workflowId: workflow.id,
504
+ workflowName: workflow.name,
505
+ propertyId: propertyArg ?? input.propertyId,
506
+ environmentId: environmentArg,
507
+ timeoutMs: timeoutSec * 1e3
508
+ });
509
+ if (!passed) {
510
+ process2.exit(1);
511
+ }
512
+ }
513
+ }
514
+
515
+ // src/workflow-run.ts
516
+ import process3 from "process";
517
+
518
+ // src/cli-format.ts
519
+ var STATUS_ICONS = {
520
+ completed: "\u2705",
521
+ completed_with_errors: "\u26A0\uFE0F",
522
+ success: "\u2705",
523
+ passed: "\u2705",
524
+ failed: "\u274C",
525
+ running: "\u{1F535}",
526
+ queued: "\u23F3",
527
+ canceled: "\u{1F6AB}",
528
+ timeout: "\u23F0",
529
+ skipped: "\u23ED\uFE0F",
530
+ flaky_success: "\u{1F7E1}",
531
+ pending: "\u23F3"
532
+ };
533
+ function statusIcon(status) {
534
+ return STATUS_ICONS[status] ?? "\u2753";
535
+ }
536
+ function formatRelative(iso) {
537
+ if (!iso) return "\u2014";
538
+ const diffMs = Date.now() - new Date(iso).getTime();
539
+ if (diffMs < 0) return "just now";
540
+ const seconds = Math.floor(diffMs / 1e3);
541
+ if (seconds < 60) return "just now";
542
+ const minutes = Math.floor(seconds / 60);
543
+ if (minutes < 60) return `${minutes}m ago`;
544
+ const hours = Math.floor(minutes / 60);
545
+ if (hours < 24) return `${hours}h ago`;
546
+ const days = Math.floor(hours / 24);
547
+ return `${days}d ago`;
548
+ }
549
+ function computeDuration(startedAt, finishedAt) {
550
+ if (!startedAt || !finishedAt) return "\u2014";
551
+ const ms = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
552
+ if (ms < 0) return "\u2014";
553
+ const totalSeconds = Math.floor(ms / 1e3);
554
+ if (totalSeconds < 60) return `${totalSeconds}s`;
555
+ const minutes = Math.floor(totalSeconds / 60);
556
+ const seconds = totalSeconds % 60;
557
+ if (minutes < 60) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
558
+ const hours = Math.floor(minutes / 60);
559
+ const remainMins = minutes % 60;
560
+ return remainMins > 0 ? `${hours}h ${remainMins}m` : `${hours}h`;
561
+ }
562
+ function pad(str, len) {
563
+ return str.padEnd(len);
564
+ }
565
+ function readFromStdin2() {
566
+ const stdin = process.stdin;
567
+ if (stdin.isTTY) {
568
+ return Promise.reject(
569
+ new Error("No input on stdin. Pipe JSON or use --from-file.")
570
+ );
571
+ }
572
+ const chunks = [];
573
+ return new Promise((resolve, reject) => {
574
+ stdin.on("data", (chunk) => {
575
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
576
+ });
577
+ stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
578
+ stdin.on("error", reject);
579
+ });
580
+ }
581
+
582
+ // src/workflow-run.ts
583
+ function sleep(ms) {
584
+ return new Promise((resolve) => setTimeout(resolve, ms));
585
+ }
586
+ async function handleWorkflowRun(argv, apiUrl, token) {
587
+ const workflowId = argv.find((a) => !a.startsWith("-"));
588
+ if (!workflowId) {
589
+ console.error("Error: Missing workflow ID.");
590
+ console.error(
591
+ "Usage: canary workflow run <workflowId> [--stream] [--json] [--validation | --no-validation]"
592
+ );
593
+ process3.exit(1);
594
+ }
595
+ const jsonOutput = hasFlag(argv, "--json");
596
+ const stream = hasFlag(argv, "--stream");
597
+ const optInValidation = hasFlag(argv, "--validation");
598
+ const optOutValidation = hasFlag(argv, "--no-validation");
599
+ const validationMode = optInValidation ? true : optOutValidation ? false : void 0;
600
+ const requestBody = {
601
+ triggerSource: "cli_direct"
602
+ };
603
+ if (validationMode !== void 0) {
604
+ requestBody.validationMode = validationMode;
605
+ }
606
+ const result = await apiRequest(
607
+ apiUrl,
608
+ token,
609
+ "POST",
610
+ `/workflows/${workflowId}/execute`,
611
+ requestBody
612
+ );
613
+ if (!result.ok) {
614
+ if (jsonOutput) {
615
+ console.log(JSON.stringify(result, null, 2));
616
+ } else {
617
+ const rendered = renderWorkflowUserError(result);
618
+ console.error(rendered ?? `Error: ${result.error ?? "Unknown error"}`);
619
+ }
620
+ process3.exit(1);
621
+ }
622
+ const { runId } = result;
623
+ if (jsonOutput && !stream) {
624
+ console.log(JSON.stringify({ ok: true, runId, streamId: result.streamId }, null, 2));
625
+ return;
626
+ }
627
+ if (!stream) {
628
+ console.log(`Run triggered successfully.`);
629
+ console.log(` Run ID: ${runId}`);
630
+ console.log(`
631
+ Check status: canary workflow runs get ${workflowId} ${runId}`);
632
+ return;
633
+ }
634
+ console.log(`Run triggered (${runId}). Waiting for completion...
635
+ `);
636
+ const terminalStatuses = /* @__PURE__ */ new Set([
637
+ "completed",
638
+ "success",
639
+ "passed",
640
+ "failed",
641
+ "canceled",
642
+ "timeout"
643
+ ]);
644
+ let lastStatus = "";
645
+ for (; ; ) {
646
+ await sleep(3e3);
647
+ const status = await apiRequest(
648
+ apiUrl,
649
+ token,
650
+ "GET",
651
+ `/workflows/${workflowId}/runs/${runId}`
652
+ );
653
+ if (!status.ok) {
654
+ console.error(`Error polling status: ${status.error ?? "Unknown error"}`);
655
+ process3.exit(1);
656
+ }
657
+ const run = status.data.run;
658
+ if (run.status !== lastStatus) {
659
+ console.log(` ${statusIcon(run.status)} ${run.status}`);
660
+ lastStatus = run.status;
661
+ }
662
+ if (terminalStatuses.has(run.status)) {
663
+ if (run.errorMessage) {
664
+ console.log(`
665
+ Error: ${run.errorMessage}`);
666
+ }
667
+ console.log(`
668
+ Run finished: ${run.status}`);
669
+ console.log(`Details: canary workflow runs get ${workflowId} ${runId}`);
670
+ if (run.status === "failed" || run.status === "timeout" || run.status === "canceled") {
671
+ process3.exit(1);
672
+ }
673
+ return;
674
+ }
675
+ }
676
+ }
677
+
678
+ // src/workflow-runs.ts
679
+ import process4 from "process";
680
+ async function fetchBrowserRuns(apiUrl, token, workflowId, page, pageSize) {
681
+ const params = new URLSearchParams({ page, pageSize });
682
+ const result = await apiRequest(
683
+ apiUrl,
684
+ token,
685
+ "GET",
686
+ `/workflows/${workflowId}/runs?${params}`
687
+ );
688
+ if (!result.ok) return { error: result.error ?? "Unknown error" };
689
+ return { runs: result.data.runs, pagination: result.pagination };
690
+ }
691
+ async function fetchApiRuns(apiUrl, token, workflowId, page, pageSize) {
692
+ const seqRes = await apiRequest(
693
+ apiUrl,
694
+ token,
695
+ "GET",
696
+ `/api-sequences/by-workflow/${workflowId}`
697
+ );
698
+ if (!seqRes.ok) {
699
+ return {
700
+ error: seqRes.error ?? "Unknown error",
701
+ ...seqRes.code ? { code: seqRes.code } : {}
702
+ };
703
+ }
704
+ if (!seqRes.data) return { runs: [], pagination: null };
705
+ const params = new URLSearchParams({ page, pageSize });
706
+ const runsRes = await apiRequest(
707
+ apiUrl,
708
+ token,
709
+ "GET",
710
+ `/api-sequences/${seqRes.data.id}/runs?${params}`
711
+ );
712
+ if (!runsRes.ok) return { error: runsRes.error ?? "Unknown error" };
713
+ const runs = (runsRes.data ?? []).map((r) => ({
714
+ id: r.id,
715
+ status: r.status,
716
+ createdAt: r.createdAt ?? r.startedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
717
+ startedAt: r.startedAt,
718
+ finishedAt: r.finishedAt,
719
+ errorMessage: r.errorMessage,
720
+ browserMode: null,
721
+ stepTotals: null,
722
+ snapshotVersion: null
723
+ }));
724
+ return { runs, pagination: runsRes.pagination ?? null };
725
+ }
726
+ async function handleRunsList(argv, apiUrl, token) {
727
+ const workflowId = argv.find((a) => !a.startsWith("-"));
728
+ if (!workflowId) {
729
+ console.error("Error: Missing workflow ID. Usage: canary workflow runs list <workflowId>");
730
+ process4.exit(1);
731
+ }
732
+ const jsonOutput = hasFlag(argv, "--json");
733
+ const page = getArgValue(argv, "--page") ?? "1";
734
+ const pageSize = getArgValue(argv, "--page-size") ?? "10";
735
+ const explicitSide = getArgValue(argv, "--side") ?? getArgValue(argv, "--mode");
736
+ const sideArg = (explicitSide ?? "all").toLowerCase();
737
+ if (!["all", "api", "browser"].includes(sideArg)) {
738
+ console.error(`Error: --side must be one of: all, api, browser (got "${sideArg}")`);
739
+ process4.exit(1);
740
+ }
741
+ const annotated = [];
742
+ let browserPagination = null;
743
+ let apiPagination = null;
744
+ if (sideArg === "browser" || sideArg === "all") {
745
+ const browser = await fetchBrowserRuns(apiUrl, token, workflowId, page, pageSize);
746
+ if ("error" in browser) {
747
+ console.error(`Error: ${browser.error}`);
748
+ process4.exit(1);
749
+ }
750
+ browserPagination = browser.pagination;
751
+ for (const r of browser.runs) annotated.push({ ...r, side: "browser" });
752
+ }
753
+ if (sideArg === "api" || sideArg === "all") {
754
+ const api = await fetchApiRuns(apiUrl, token, workflowId, page, pageSize);
755
+ if ("error" in api) {
756
+ const isMissingSequence = api.code === "API_SEQUENCE_NOT_FOUND";
757
+ if (!explicitSide && isMissingSequence) {
758
+ if (!jsonOutput) {
759
+ console.error("Note: workflow has no api-sequence \u2014 showing browser runs only.");
760
+ }
761
+ } else {
762
+ console.error(`Error: ${api.error}`);
763
+ process4.exit(1);
764
+ }
765
+ } else {
766
+ apiPagination = api.pagination;
767
+ for (const r of api.runs) annotated.push({ ...r, side: "api" });
768
+ }
769
+ }
770
+ annotated.sort((a, b) => {
771
+ const ta = new Date(a.startedAt ?? a.createdAt).getTime();
772
+ const tb = new Date(b.startedAt ?? b.createdAt).getTime();
773
+ return tb - ta;
774
+ });
775
+ if (jsonOutput) {
776
+ console.log(
777
+ JSON.stringify(
778
+ {
779
+ ok: true,
780
+ data: { runs: annotated },
781
+ pagination: { browser: browserPagination, api: apiPagination }
782
+ },
783
+ null,
784
+ 2
785
+ )
786
+ );
787
+ return;
788
+ }
789
+ if (annotated.length === 0) {
790
+ console.log("No runs found.");
791
+ return;
792
+ }
793
+ console.log(`Runs (${annotated.length} shown, side=${sideArg})
794
+ `);
795
+ const idW = 36;
796
+ const sideW = 8;
797
+ const statusW = 12;
798
+ const durationW = 10;
799
+ console.log(
800
+ [
801
+ "ID".padEnd(idW),
802
+ "Side".padEnd(sideW),
803
+ "Status".padEnd(statusW),
804
+ "Duration".padEnd(durationW),
805
+ "Started"
806
+ ].join(" ")
807
+ );
808
+ for (const run of annotated) {
809
+ const icon = statusIcon(run.status);
810
+ const dur = computeDuration(run.startedAt, run.finishedAt);
811
+ const time = formatRelative(run.startedAt ?? run.createdAt);
812
+ console.log(
813
+ [
814
+ run.id.padEnd(idW),
815
+ pad(run.side, sideW),
816
+ `${icon} ${pad(run.status, statusW - 3)}`,
817
+ pad(dur, durationW),
818
+ time
819
+ ].join(" ")
820
+ );
821
+ }
822
+ console.log("");
823
+ }
824
+ async function handleRunsGet(argv, apiUrl, token) {
825
+ const positional = argv.filter((a) => !a.startsWith("-"));
826
+ if (positional.length < 2) {
827
+ console.error(
828
+ "Error: Missing arguments. Usage: canary workflow runs get <workflowId> <runId>"
829
+ );
830
+ process4.exit(1);
831
+ }
832
+ const [workflowId, runId] = positional;
833
+ const jsonOutput = hasFlag(argv, "--json");
834
+ const result = await apiRequest(
835
+ apiUrl,
836
+ token,
837
+ "GET",
838
+ `/workflows/${workflowId}/runs/${runId}`
839
+ );
840
+ if (!result.ok) {
841
+ console.error(`Error: ${result.error ?? "Unknown error"}`);
842
+ process4.exit(1);
843
+ }
844
+ if (jsonOutput) {
845
+ console.log(JSON.stringify(result, null, 2));
846
+ return;
847
+ }
848
+ const { run } = result.data;
849
+ const duration = computeDuration(run.startedAt, run.finishedAt);
850
+ console.log(`Run: ${run.id}`);
851
+ console.log(`Status: ${statusIcon(run.status)} ${run.status}`);
852
+ console.log(`Duration: ${duration}`);
853
+ if (run.errorMessage) {
854
+ console.log(`Error: ${run.errorMessage}`);
855
+ }
856
+ if (run.draftSnapshot) {
857
+ console.log(`Snapshot: v${run.draftSnapshot.version} (base: v${run.draftSnapshot.basePublishedVersion})`);
858
+ }
859
+ const steps = (run.steps ?? []).sort((a, b) => a.orderIndex - b.orderIndex);
860
+ if (steps.length === 0) {
861
+ console.log("\nNo steps recorded.");
862
+ return;
863
+ }
864
+ console.log(`
865
+ Steps (${steps.length}):`);
866
+ for (const step of steps) {
867
+ const idx = String(step.orderIndex + 1).padStart(2);
868
+ const icon = statusIcon(step.status);
869
+ const nodeId = step.nodeId.slice(0, 8);
870
+ const dur = computeDuration(step.startedAt, step.finishedAt);
871
+ console.log(` ${idx}. ${icon} ${pad(nodeId, 10)} ${dur}`);
872
+ if (step.errorMessage) {
873
+ console.log(` Error: ${step.errorMessage}`);
874
+ }
875
+ }
876
+ console.log("");
877
+ }
878
+ async function handleRunsDiagnostics(argv, apiUrl, token) {
879
+ const positional = argv.filter((a) => !a.startsWith("-"));
880
+ if (positional.length < 2) {
881
+ console.error(
882
+ "Error: Missing arguments. Usage: canary workflow runs diagnostics <workflowId> <runId>"
883
+ );
884
+ process4.exit(1);
885
+ }
886
+ const [workflowId, runId] = positional;
887
+ const jsonOutput = hasFlag(argv, "--json");
888
+ const result = await apiRequest(
889
+ apiUrl,
890
+ token,
891
+ "GET",
892
+ `/workflows/${workflowId}/runs/${runId}/failure-diagnostic`
893
+ );
894
+ if (!result.ok) {
895
+ console.error(`Error: ${result.error ?? "Unknown error"}`);
896
+ process4.exit(1);
897
+ }
898
+ if (jsonOutput && result.data) {
899
+ console.log(JSON.stringify(result, null, 2));
900
+ return;
901
+ }
902
+ if (result.data) {
903
+ const { status, diagnostic } = result.data;
904
+ if (status === "pending" || status === "running") {
905
+ console.log(`Diagnostic is still in progress (status: ${status}).`);
906
+ return;
907
+ }
908
+ if (status === "failed") {
909
+ console.log("Diagnostic analysis failed.");
910
+ return;
911
+ }
912
+ if (diagnostic) {
913
+ console.log("Failure Diagnostic");
914
+ console.log(`Status: ${status}`);
915
+ if (diagnostic.category) {
916
+ const conf = diagnostic.confidence !== null && diagnostic.confidence !== void 0 ? ` (${diagnostic.confidence > 1 ? Math.round(diagnostic.confidence) : Math.round(diagnostic.confidence * 100)}% confidence)` : "";
917
+ console.log(`Category: ${diagnostic.category}${conf}`);
918
+ }
919
+ if (diagnostic.title) {
920
+ console.log(`Title: ${diagnostic.title}`);
921
+ }
922
+ if (diagnostic.analysis) {
923
+ console.log("");
924
+ console.log("Analysis:");
925
+ console.log(` ${diagnostic.analysis}`);
926
+ }
927
+ if (diagnostic.recommendation) {
928
+ console.log("");
929
+ console.log("Recommendation:");
930
+ console.log(` ${diagnostic.recommendation}`);
931
+ }
932
+ if (diagnostic.smokingGun) {
933
+ console.log("");
934
+ console.log("Smoking Gun:");
935
+ console.log(` > ${diagnostic.smokingGun.description}`);
936
+ if (diagnostic.smokingGun.detail) {
937
+ console.log(` ${diagnostic.smokingGun.detail}`);
938
+ }
939
+ }
940
+ console.log("");
941
+ return;
942
+ }
943
+ }
944
+ const rawResult = await apiRequest(
945
+ apiUrl,
946
+ token,
947
+ "GET",
948
+ `/workflows/${workflowId}/runs/${runId}/diagnostics`
949
+ );
950
+ if (!rawResult.ok) {
951
+ console.log("No diagnostic available for this run.");
952
+ return;
953
+ }
954
+ if (jsonOutput) {
955
+ console.log(JSON.stringify(rawResult, null, 2));
956
+ return;
957
+ }
958
+ const hasConsole = rawResult.consoleLogs && rawResult.consoleLogs.length > 0;
959
+ const hasNetwork = rawResult.networkRequests && rawResult.networkRequests.length > 0;
960
+ if (!hasConsole && !hasNetwork) {
961
+ console.log("No diagnostic data available for this run.");
962
+ return;
963
+ }
964
+ if (hasConsole) {
965
+ console.log(`Console Logs (${rawResult.consoleLogs.length}):`);
966
+ for (const log of rawResult.consoleLogs.slice(0, 50)) {
967
+ const level = log.level.toUpperCase().padEnd(7);
968
+ console.log(` [${level}] ${log.text}`);
969
+ }
970
+ if (rawResult.consoleLogs.length > 50) {
971
+ console.log(` ... and ${rawResult.consoleLogs.length - 50} more`);
972
+ }
973
+ console.log("");
974
+ }
975
+ if (hasNetwork) {
976
+ console.log(`Network Requests (${rawResult.networkRequests.length}):`);
977
+ for (const req of rawResult.networkRequests.slice(0, 30)) {
978
+ const statusStr = req.status >= 400 ? `${req.status} !!!` : String(req.status);
979
+ console.log(` ${req.method.padEnd(6)} ${statusStr.padEnd(8)} ${req.url}`);
980
+ }
981
+ if (rawResult.networkRequests.length > 30) {
982
+ console.log(` ... and ${rawResult.networkRequests.length - 30} more`);
983
+ }
984
+ console.log("");
985
+ }
986
+ }
987
+ function printRunsHelp() {
988
+ console.log(
989
+ [
990
+ "Usage: canary workflow runs <sub-command> [options]",
991
+ "",
992
+ "Sub-commands:",
993
+ " list <workflowId> [options] List runs for a workflow",
994
+ " get <workflowId> <runId> [options] Get run detail with steps",
995
+ " diagnostics <workflowId> <runId> [options] Get failure diagnostic",
996
+ "",
997
+ "List options:",
998
+ " --page <n> Page number (default: 1)",
999
+ " --page-size <n> Page size (default: 10)",
1000
+ " --side <side> Filter: browser, api, all (default: all). Alias: --mode",
1001
+ "",
1002
+ "Output options:",
1003
+ " --json Output raw JSON",
1004
+ "",
1005
+ "Common options:",
1006
+ " --env <env> Target environment (prod, dev, local)",
1007
+ " --api-url <url> API URL override",
1008
+ " --token <key> API token override"
1009
+ ].join("\n")
1010
+ );
1011
+ }
1012
+ async function runWorkflowRuns(argv, apiUrl, token) {
1013
+ const [subCommand, ...rest] = argv;
1014
+ if (!subCommand || subCommand === "help" || hasFlag(argv, "--help", "-h")) {
1015
+ printRunsHelp();
1016
+ return;
1017
+ }
1018
+ switch (subCommand) {
1019
+ case "list":
1020
+ await handleRunsList(rest, apiUrl, token);
1021
+ break;
1022
+ case "get":
1023
+ await handleRunsGet(rest, apiUrl, token);
1024
+ break;
1025
+ case "diagnostics":
1026
+ await handleRunsDiagnostics(rest, apiUrl, token);
1027
+ break;
1028
+ default:
1029
+ console.error(`Unknown sub-command: ${subCommand}`);
1030
+ printRunsHelp();
1031
+ process4.exit(1);
1032
+ }
1033
+ }
1034
+
1035
+ // src/workflow-sync.ts
1036
+ import crypto from "crypto";
1037
+ import fs2 from "fs/promises";
1038
+ import process5 from "process";
1039
+ function normalizeNodesAndEdges(rawNodes, rawEdges) {
1040
+ const nodes = rawNodes.map((node, i) => ({
1041
+ ...node,
1042
+ id: node.id ?? crypto.randomUUID(),
1043
+ orderIndex: node.orderIndex ?? i,
1044
+ configJson: node.configJson ?? {}
1045
+ }));
1046
+ if (rawEdges && rawEdges.length > 0) {
1047
+ const edges2 = rawEdges.map((edge) => ({
1048
+ ...edge,
1049
+ id: edge.id ?? crypto.randomUUID()
1050
+ }));
1051
+ return { nodes, edges: edges2 };
1052
+ }
1053
+ const edges = [];
1054
+ for (let i = 0; i < nodes.length - 1; i++) {
1055
+ edges.push({
1056
+ id: crypto.randomUUID(),
1057
+ sourceNodeId: nodes[i].id,
1058
+ targetNodeId: nodes[i + 1].id,
1059
+ edgeType: "default"
1060
+ });
1061
+ }
1062
+ return { nodes, edges };
1063
+ }
1064
+ async function readSyncInput(argv) {
1065
+ const filePath = getArgValue(argv, "--from-file");
1066
+ const fromStdin = hasFlag(argv, "--from-stdin");
1067
+ let raw;
1068
+ if (filePath) {
1069
+ try {
1070
+ raw = await fs2.readFile(filePath, "utf-8");
1071
+ } catch (err) {
1072
+ console.error(`Error reading file: ${err instanceof Error ? err.message : String(err)}`);
1073
+ process5.exit(1);
1074
+ }
1075
+ } else if (fromStdin) {
1076
+ try {
1077
+ raw = await readFromStdin2();
1078
+ } catch (err) {
1079
+ console.error(err instanceof Error ? err.message : String(err));
1080
+ process5.exit(1);
1081
+ }
1082
+ } else {
1083
+ console.error("Error: Must provide --from-file <path> or --from-stdin");
1084
+ process5.exit(1);
1085
+ }
1086
+ let parsed;
1087
+ try {
1088
+ parsed = JSON.parse(raw);
1089
+ } catch {
1090
+ console.error("Error: Invalid JSON input");
1091
+ process5.exit(1);
1092
+ }
1093
+ const obj = parsed;
1094
+ const normalizeNodes = (raw2) => {
1095
+ return raw2.map((entry) => {
1096
+ if (entry && typeof entry === "object" && "nodeType" in entry) {
1097
+ const node = entry;
1098
+ if (typeof node.nodeType === "string") {
1099
+ node.nodeType = normalizeNodeType(node.nodeType);
1100
+ }
1101
+ }
1102
+ return entry;
1103
+ });
1104
+ };
1105
+ if (Array.isArray(obj.nodes)) {
1106
+ return {
1107
+ nodes: normalizeNodes(obj.nodes),
1108
+ edges: Array.isArray(obj.edges) ? obj.edges : void 0
1109
+ };
1110
+ }
1111
+ const workflow = obj.workflow;
1112
+ if (workflow && Array.isArray(workflow.nodes)) {
1113
+ return {
1114
+ nodes: normalizeNodes(workflow.nodes),
1115
+ edges: Array.isArray(workflow.edges) ? workflow.edges : void 0
1116
+ };
1117
+ }
1118
+ console.error('Error: JSON must contain a "nodes" array.');
1119
+ console.error("Accepted formats:");
1120
+ console.error(' { "nodes": [{ "nodeType": "login", "configJson": {} }, ...] }');
1121
+ console.error(' { "nodes": [...], "edges": [...] }');
1122
+ console.error(' { "workflow": { "nodes": [...], "edges": [...] } }');
1123
+ console.error("");
1124
+ console.error("Edges, IDs, positions, and ordering are auto-generated when omitted.");
1125
+ process5.exit(1);
1126
+ }
1127
+ async function alignWithCurrentWorkflow(apiUrl, token, workflowId, nodes, edges) {
1128
+ let currentNodes = [];
1129
+ try {
1130
+ const current = await apiRequest(
1131
+ apiUrl,
1132
+ token,
1133
+ "GET",
1134
+ `/workflows/${workflowId}`
1135
+ );
1136
+ if (current.ok && current.workflow?.nodes) {
1137
+ currentNodes = current.workflow.nodes.map((n) => ({
1138
+ id: n.id,
1139
+ nodeType: n.nodeType,
1140
+ orderIndex: n.orderIndex,
1141
+ configJson: n.configJson ?? {}
1142
+ }));
1143
+ }
1144
+ } catch {
1145
+ }
1146
+ if (currentNodes.length === 0) {
1147
+ return { nodes, edges };
1148
+ }
1149
+ const proposedSnapshot = {
1150
+ nodes: nodes.map((n) => ({
1151
+ id: String(n.id),
1152
+ nodeType: String(n.nodeType ?? ""),
1153
+ orderIndex: typeof n.orderIndex === "number" ? n.orderIndex : void 0,
1154
+ configJson: n.configJson ?? {}
1155
+ })),
1156
+ edges: edges.map((e) => ({
1157
+ id: String(e.id),
1158
+ sourceNodeId: String(e.sourceNodeId),
1159
+ targetNodeId: String(e.targetNodeId),
1160
+ edgeType: e.edgeType ?? null
1161
+ }))
1162
+ };
1163
+ const { snapshot } = alignSnapshotNodeIds(proposedSnapshot, currentNodes);
1164
+ const alignedNodes = nodes.map((n, i) => ({
1165
+ ...n,
1166
+ id: snapshot.nodes[i].id
1167
+ }));
1168
+ const alignedEdges = edges.map((e, i) => ({
1169
+ ...e,
1170
+ sourceNodeId: snapshot.edges[i].sourceNodeId,
1171
+ targetNodeId: snapshot.edges[i].targetNodeId
1172
+ }));
1173
+ return { nodes: alignedNodes, edges: alignedEdges };
1174
+ }
1175
+ async function handleWorkflowSync(argv, apiUrl, token) {
1176
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1177
+ if (!workflowId) {
1178
+ console.error("Error: Missing workflow ID.");
1179
+ console.error("Usage: canary workflow sync <workflowId> --from-file <path>");
1180
+ process5.exit(1);
1181
+ }
1182
+ const jsonOutput = hasFlag(argv, "--json");
1183
+ const noSnapshot = hasFlag(argv, "--no-snapshot");
1184
+ const propose = hasFlag(argv, "--propose");
1185
+ const proposalTitle = getArgValue(argv, "--title");
1186
+ const proposalDescription = getArgValue(argv, "--description");
1187
+ const raw = await readSyncInput(argv);
1188
+ const { nodes, edges } = normalizeNodesAndEdges(raw.nodes, raw.edges);
1189
+ if (propose) {
1190
+ const title = proposalTitle ?? `CLI sync proposal (${nodes.length} nodes)`;
1191
+ const aligned = await alignWithCurrentWorkflow(apiUrl, token, workflowId, nodes, edges);
1192
+ const result2 = await apiRequest(
1193
+ apiUrl,
1194
+ token,
1195
+ "POST",
1196
+ `/workflows/${workflowId}/proposals`,
1197
+ {
1198
+ title,
1199
+ description: proposalDescription ?? null,
1200
+ snapshotJson: { nodes: aligned.nodes, edges: aligned.edges },
1201
+ source: "api"
1202
+ }
1203
+ );
1204
+ if (!result2.ok) {
1205
+ if (jsonOutput) {
1206
+ console.log(JSON.stringify({ ok: false, error: result2.error ?? "Unknown error" }, null, 2));
1207
+ } else {
1208
+ console.error(`Error: ${result2.error ?? "Unknown error"}`);
1209
+ }
1210
+ process5.exit(1);
1211
+ }
1212
+ if (jsonOutput) {
1213
+ console.log(JSON.stringify(result2, null, 2));
1214
+ return;
1215
+ }
1216
+ console.log(`Proposal created for workflow ${workflowId} (${nodes.length} nodes, ${edges.length} edges).`);
1217
+ if (result2.data?.id) {
1218
+ console.log(`Proposal ID: ${result2.data.id}`);
1219
+ }
1220
+ return;
1221
+ }
1222
+ const result = await apiRequest(
1223
+ apiUrl,
1224
+ token,
1225
+ "PUT",
1226
+ `/workflows/${workflowId}/draft-state`,
1227
+ { nodes, edges, createSnapshot: !noSnapshot }
1228
+ );
1229
+ if (!result.ok) {
1230
+ if (jsonOutput) {
1231
+ console.log(JSON.stringify({ ok: false, error: result.error ?? "Unknown error" }, null, 2));
1232
+ } else {
1233
+ console.error(`Error: ${result.error ?? "Unknown error"}`);
1234
+ }
1235
+ process5.exit(1);
1236
+ }
1237
+ if (jsonOutput) {
1238
+ console.log(JSON.stringify(result, null, 2));
1239
+ return;
1240
+ }
1241
+ console.log(`Workflow ${workflowId} synced (${nodes.length} nodes, ${edges.length} edges).`);
1242
+ if (result.snapshotVersion) {
1243
+ console.log(`Draft snapshot created: ${result.snapshotVersion}`);
1244
+ }
1245
+ }
1246
+
1247
+ // src/workflow-api-sequence.ts
1248
+ import process6 from "process";
1249
+ import { createParser as createParser2 } from "eventsource-parser";
1250
+ async function handleWorkflowGenerateApiSequence(argv, apiUrl, token) {
1251
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1252
+ const prompt = getArgValue(argv, "--prompt");
1253
+ const guidance = getArgValue(argv, "--guidance");
1254
+ const propertyId = getArgValue(argv, "--property");
1255
+ const jsonOutput = hasFlag(argv, "--json");
1256
+ const verbose = hasFlag(argv, "--verbose", "-v");
1257
+ if (!workflowId && !prompt) {
1258
+ console.error("Error: Provide a workflow ID or --prompt.");
1259
+ console.error("");
1260
+ console.error("Usage:");
1261
+ console.error(' canary workflow generate-api-sequence <workflowId> [--guidance "text"]');
1262
+ console.error(' canary workflow generate-api-sequence --prompt "Create user" [--property <id>]');
1263
+ process6.exit(1);
1264
+ }
1265
+ let endpoint;
1266
+ let body;
1267
+ if (workflowId && !prompt) {
1268
+ endpoint = "/api-sequence-agent/generate-from-run";
1269
+ body = { workflowId };
1270
+ if (guidance) body.guidancePrompt = guidance;
1271
+ if (!jsonOutput) {
1272
+ console.error(`Generating API sequence from latest run of workflow ${workflowId}...`);
1273
+ }
1274
+ } else {
1275
+ endpoint = "/api-sequence-agent/generate";
1276
+ body = { prompt };
1277
+ if (propertyId) body.propertyId = propertyId;
1278
+ if (workflowId) body.workflowId = workflowId;
1279
+ if (!jsonOutput) {
1280
+ console.error(`Generating API sequence from prompt...`);
1281
+ }
1282
+ }
1283
+ let triggerRes;
1284
+ try {
1285
+ triggerRes = await fetch(`${apiUrl}${endpoint}`, {
1286
+ method: "POST",
1287
+ headers: {
1288
+ Authorization: `Bearer ${token}`,
1289
+ "Content-Type": "application/json"
1290
+ },
1291
+ body: JSON.stringify(body)
1292
+ });
1293
+ } catch (err) {
1294
+ console.error(`Failed to connect to API: ${err}`);
1295
+ process6.exit(1);
1296
+ }
1297
+ if (triggerRes.status === 403) {
1298
+ const data = await triggerRes.json().catch(() => ({}));
1299
+ console.error(`Error: ${data.error ?? "This feature is not enabled for your organization."}`);
1300
+ process6.exit(1);
1301
+ }
1302
+ if (!triggerRes.ok) {
1303
+ const data = await triggerRes.json().catch(() => ({}));
1304
+ console.error(`Error: ${data.error ?? `${triggerRes.status} ${triggerRes.statusText}`}`);
1305
+ process6.exit(1);
1306
+ }
1307
+ const triggerData = await triggerRes.json();
1308
+ if (!triggerData.ok || !triggerData.generationId) {
1309
+ console.error(`Error: ${triggerData.error ?? "Failed to start generation"}`);
1310
+ process6.exit(1);
1311
+ }
1312
+ const { generationId } = triggerData;
1313
+ const streamUrl = `${apiUrl}/api-sequence-agent/stream?generationId=${generationId}`;
1314
+ let streamRes;
1315
+ try {
1316
+ streamRes = await fetch(streamUrl, {
1317
+ headers: {
1318
+ Authorization: `Bearer ${token}`,
1319
+ Accept: "text/event-stream"
1320
+ }
1321
+ });
1322
+ } catch (err) {
1323
+ console.error(`Failed to connect to event stream: ${err}`);
1324
+ process6.exit(1);
1325
+ }
1326
+ if (!streamRes.ok || !streamRes.body) {
1327
+ console.error(`Failed to connect to event stream: ${streamRes.status}`);
1328
+ process6.exit(1);
1329
+ }
1330
+ let resultConfig = null;
1331
+ let resultSummary = "";
1332
+ let hasCompleted = false;
1333
+ let exitCode = 0;
1334
+ const parser = createParser2({
1335
+ onEvent: (event) => {
1336
+ if (!event.data || event.event !== "api-sequence-generation") return;
1337
+ try {
1338
+ const data = JSON.parse(event.data);
1339
+ switch (data.eventType) {
1340
+ case "progress":
1341
+ if (!jsonOutput) console.error(` ${data.message}`);
1342
+ break;
1343
+ case "tool-call":
1344
+ if (verbose) console.error(` [tool] ${data.toolName}`);
1345
+ break;
1346
+ case "tool-result":
1347
+ if (verbose) {
1348
+ const icon = data.success ? "\u2713" : "\u2717";
1349
+ console.error(` [tool] ${icon} ${data.toolName}`);
1350
+ }
1351
+ break;
1352
+ case "text":
1353
+ if (verbose) process6.stderr.write(data.delta);
1354
+ break;
1355
+ case "complete":
1356
+ resultConfig = data.config;
1357
+ resultSummary = data.summary;
1358
+ hasCompleted = true;
1359
+ break;
1360
+ case "error":
1361
+ console.error(`
1362
+ Error: ${data.message}`);
1363
+ exitCode = 1;
1364
+ hasCompleted = true;
1365
+ break;
1366
+ }
1367
+ } catch {
1368
+ }
1369
+ }
1370
+ });
1371
+ const reader = streamRes.body.getReader();
1372
+ const decoder = new TextDecoder();
1373
+ try {
1374
+ while (!hasCompleted) {
1375
+ const { done, value } = await reader.read();
1376
+ if (done) break;
1377
+ parser.feed(decoder.decode(value, { stream: true }));
1378
+ }
1379
+ } finally {
1380
+ reader.releaseLock();
1381
+ }
1382
+ if (exitCode !== 0) {
1383
+ process6.exit(exitCode);
1384
+ }
1385
+ if (!resultConfig) {
1386
+ console.error("Generation completed but no config was returned.");
1387
+ process6.exit(1);
1388
+ }
1389
+ if (jsonOutput) {
1390
+ console.log(JSON.stringify({ ok: true, config: resultConfig, summary: resultSummary }, null, 2));
1391
+ } else {
1392
+ if (resultSummary) {
1393
+ console.error("");
1394
+ console.error(resultSummary);
1395
+ }
1396
+ console.error("");
1397
+ console.log(JSON.stringify(resultConfig, null, 2));
1398
+ }
1399
+ }
1400
+
1401
+ // src/workflow-api-sequence-cmds.ts
1402
+ import fs3 from "fs/promises";
1403
+ import process7 from "process";
1404
+ function getArg(argv, key) {
1405
+ const prefix = `${key}=`;
1406
+ const eq = argv.find((a) => a.startsWith(prefix));
1407
+ if (eq) return eq.slice(prefix.length);
1408
+ return getArgValue(argv, key);
1409
+ }
1410
+ function wantsJson(argv) {
1411
+ if (hasFlag(argv, "--json")) return true;
1412
+ if (hasFlag(argv, "--text")) return false;
1413
+ return !process7.stdout.isTTY;
1414
+ }
1415
+ function emit(argv, textFallback, json) {
1416
+ if (wantsJson(argv)) {
1417
+ console.log(JSON.stringify(json, null, 2));
1418
+ } else {
1419
+ console.log(textFallback);
1420
+ }
1421
+ }
1422
+ async function readJsonInput(argv) {
1423
+ const fromFile = getArg(argv, "--from-file");
1424
+ let raw;
1425
+ if (fromFile) {
1426
+ raw = await fs3.readFile(fromFile, "utf-8");
1427
+ } else if (hasFlag(argv, "--from-stdin") || !process7.stdin.isTTY) {
1428
+ raw = await readStdin();
1429
+ } else {
1430
+ throw new Error("Provide --from-file <path> or pipe JSON on stdin.");
1431
+ }
1432
+ try {
1433
+ return JSON.parse(raw);
1434
+ } catch {
1435
+ throw new Error("Invalid JSON input.");
1436
+ }
1437
+ }
1438
+ function readStdin() {
1439
+ return new Promise((resolve, reject) => {
1440
+ const chunks = [];
1441
+ process7.stdin.on("data", (chunk) => {
1442
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1443
+ });
1444
+ process7.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1445
+ process7.stdin.on("error", reject);
1446
+ });
1447
+ }
1448
+ function reportAndMaybeExit(argv, result) {
1449
+ if (result.ok) return;
1450
+ if (result.code === "API_SEQUENCE_INVALID" && Array.isArray(result.issues)) {
1451
+ if (wantsJson(argv)) {
1452
+ console.log(
1453
+ JSON.stringify(
1454
+ {
1455
+ ok: false,
1456
+ error: result.error ?? "API sequence configuration is invalid",
1457
+ code: "API_SEQUENCE_INVALID",
1458
+ issueCount: result.issues.length,
1459
+ issues: result.issues
1460
+ },
1461
+ null,
1462
+ 2
1463
+ )
1464
+ );
1465
+ } else {
1466
+ console.error(`Error: ${result.error ?? "API sequence configuration is invalid"}`);
1467
+ console.error(
1468
+ `Found ${result.issues.length} issue${result.issues.length === 1 ? "" : "s"}:`
1469
+ );
1470
+ console.error("");
1471
+ for (const issue of result.issues) {
1472
+ console.error(` ${issue.path || "(root)"} [${issue.code}] :: ${issue.message}`);
1473
+ if (issue.hint) console.error(` hint: ${issue.hint}`);
1474
+ }
1475
+ console.error("");
1476
+ console.error(
1477
+ "Re-run with --json for machine-readable output, or fix the listed fields and retry."
1478
+ );
1479
+ }
1480
+ process7.exit(1);
1481
+ }
1482
+ return;
1483
+ }
1484
+ async function handleWorkflowApiSequence(argv, apiUrl, token) {
1485
+ const [verb, ...rest] = argv;
1486
+ switch (verb) {
1487
+ case "get":
1488
+ await handleGet(rest, apiUrl, token);
1489
+ break;
1490
+ case "update":
1491
+ await handleUpdate(rest, apiUrl, token);
1492
+ break;
1493
+ case "execute":
1494
+ case "run":
1495
+ await handleExecute(rest, apiUrl, token);
1496
+ break;
1497
+ case "delete":
1498
+ await handleDelete(rest, apiUrl, token);
1499
+ break;
1500
+ case "set-api-only":
1501
+ await handleSetApiOnly(rest, apiUrl, token);
1502
+ break;
1503
+ case "generate":
1504
+ await handleWorkflowGenerateApiSequence(rest, apiUrl, token);
1505
+ break;
1506
+ default:
1507
+ printHelp();
1508
+ process7.exit(verb ? 1 : 0);
1509
+ }
1510
+ }
1511
+ function printHelp() {
1512
+ console.error(
1513
+ [
1514
+ "Usage: canary workflow api-sequence <verb> <workflowId> [options]",
1515
+ "",
1516
+ "Verbs:",
1517
+ " get <wf> fetch the sequence attached to the workflow",
1518
+ " update <wf> --from-file <f> replace config (steps, description, exports...)",
1519
+ " update <wf> --from-stdin replace config from piped JSON",
1520
+ " execute <wf> run the sequence; returns { runId, status }",
1521
+ " delete <wf> delete the api-sequence record (keeps workflow)",
1522
+ " set-api-only <wf> [--off] toggle the workflow's apiOnly flag",
1523
+ " generate <wf> [--prompt ...] AI-generate a config (streams progress)",
1524
+ "",
1525
+ "Global options:",
1526
+ " --json force JSON output",
1527
+ " --text force pretty-text output (default when stdout is a TTY)",
1528
+ "",
1529
+ "Config shape (for update): { steps: [...], description?, stopOnFailure?, exports? }",
1530
+ 'Run "canary workflow guide api-sequences" for step shape and template syntax.'
1531
+ ].join("\n")
1532
+ );
1533
+ }
1534
+ async function fetchSequenceByWorkflow(apiUrl, token, workflowId) {
1535
+ const res = await apiRequest(
1536
+ apiUrl,
1537
+ token,
1538
+ "GET",
1539
+ `/api-sequences/by-workflow/${workflowId}`
1540
+ );
1541
+ if (!res.ok) {
1542
+ console.error(`Error: ${res.error ?? "Failed to fetch sequence"}`);
1543
+ process7.exit(1);
1544
+ }
1545
+ if (!res.data) {
1546
+ console.error(`Error: Workflow ${workflowId} has no attached API sequence.`);
1547
+ console.error("Create one with: canary workflow create --api-only --name <n> --sequence-from-file seq.json");
1548
+ process7.exit(1);
1549
+ }
1550
+ return res.data;
1551
+ }
1552
+ async function handleGet(argv, apiUrl, token) {
1553
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1554
+ if (!workflowId) {
1555
+ console.error("Error: Missing workflow ID.");
1556
+ printHelp();
1557
+ process7.exit(1);
1558
+ }
1559
+ const seq = await fetchSequenceByWorkflow(apiUrl, token, workflowId);
1560
+ emit(argv, renderSequence(seq), seq);
1561
+ }
1562
+ async function handleUpdate(argv, apiUrl, token) {
1563
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1564
+ if (!workflowId) {
1565
+ console.error("Error: Missing workflow ID.");
1566
+ printHelp();
1567
+ process7.exit(1);
1568
+ }
1569
+ let parsed;
1570
+ try {
1571
+ parsed = await readJsonInput(argv);
1572
+ } catch (err) {
1573
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1574
+ process7.exit(1);
1575
+ }
1576
+ const config = parsed;
1577
+ if (!config || !Array.isArray(config.steps)) {
1578
+ console.error('Error: Input must have a "steps" array. Got: ' + JSON.stringify(config));
1579
+ process7.exit(1);
1580
+ }
1581
+ const seq = await fetchSequenceByWorkflow(apiUrl, token, workflowId);
1582
+ const result = await apiRequest(
1583
+ apiUrl,
1584
+ token,
1585
+ "PUT",
1586
+ `/api-sequences/${seq.id}/config`,
1587
+ { config }
1588
+ );
1589
+ reportAndMaybeExit(argv, result);
1590
+ if (!result.ok) {
1591
+ console.error(`Error: ${result.error ?? "Update failed"}`);
1592
+ process7.exit(1);
1593
+ }
1594
+ emit(
1595
+ argv,
1596
+ `Updated api-sequence ${seq.id} (workflow ${workflowId}). Steps: ${config.steps.length}.`,
1597
+ result
1598
+ );
1599
+ }
1600
+ async function handleExecute(argv, apiUrl, token) {
1601
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1602
+ if (!workflowId) {
1603
+ console.error("Error: Missing workflow ID.");
1604
+ printHelp();
1605
+ process7.exit(1);
1606
+ }
1607
+ const seq = await fetchSequenceByWorkflow(apiUrl, token, workflowId);
1608
+ const result = await apiRequest(
1609
+ apiUrl,
1610
+ token,
1611
+ "POST",
1612
+ `/api-sequences/${seq.id}/execute`
1613
+ );
1614
+ if (!result.ok || !result.data) {
1615
+ console.error(`Error: ${result.error ?? "Execute failed"}`);
1616
+ process7.exit(1);
1617
+ }
1618
+ emit(
1619
+ argv,
1620
+ `Run started: ${result.data.runId} (status: ${result.data.status})
1621
+ Inspect: canary workflow runs get ${workflowId} ${result.data.runId}`,
1622
+ result
1623
+ );
1624
+ }
1625
+ async function handleDelete(argv, apiUrl, token) {
1626
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1627
+ if (!workflowId) {
1628
+ console.error("Error: Missing workflow ID.");
1629
+ printHelp();
1630
+ process7.exit(1);
1631
+ }
1632
+ const seq = await fetchSequenceByWorkflow(apiUrl, token, workflowId);
1633
+ const result = await apiRequest(
1634
+ apiUrl,
1635
+ token,
1636
+ "DELETE",
1637
+ `/api-sequences/${seq.id}`
1638
+ );
1639
+ if (!result.ok) {
1640
+ console.error(`Error: ${result.error ?? "Delete failed"}`);
1641
+ process7.exit(1);
1642
+ }
1643
+ emit(argv, `Deleted api-sequence ${seq.id} (workflow ${workflowId}).`, result);
1644
+ }
1645
+ async function handleSetApiOnly(argv, apiUrl, token) {
1646
+ const workflowId = argv.find((a) => !a.startsWith("-"));
1647
+ if (!workflowId) {
1648
+ console.error("Error: Missing workflow ID.");
1649
+ printHelp();
1650
+ process7.exit(1);
1651
+ }
1652
+ const apiOnly = !hasFlag(argv, "--off");
1653
+ const result = await apiRequest(
1654
+ apiUrl,
1655
+ token,
1656
+ "PUT",
1657
+ `/workflows/${workflowId}/api-only`,
1658
+ { apiOnly }
1659
+ );
1660
+ if (!result.ok) {
1661
+ console.error(`Error: ${result.error ?? "Update failed"}`);
1662
+ process7.exit(1);
1663
+ }
1664
+ emit(argv, `Workflow ${workflowId} apiOnly=${apiOnly}.`, result);
1665
+ }
1666
+ function renderSequence(seq) {
1667
+ const lines = [];
1668
+ lines.push(`API Sequence ${seq.id}`);
1669
+ lines.push(` Workflow: ${seq.workflowId}`);
1670
+ lines.push(` Status: ${seq.status}`);
1671
+ if (seq.credentialId) lines.push(` Credential: ${seq.credentialId}`);
1672
+ if (seq.config.description) lines.push(` Description: ${seq.config.description}`);
1673
+ lines.push(` stopOnFailure: ${seq.config.stopOnFailure ?? false}`);
1674
+ lines.push("");
1675
+ const steps = seq.config.steps ?? [];
1676
+ if (steps.length === 0) {
1677
+ lines.push(" (no steps \u2014 use `api-sequence update` to add)");
1678
+ } else {
1679
+ lines.push(` Steps (${steps.length}):`);
1680
+ steps.forEach((step, i) => {
1681
+ const method = String(step.method ?? "??").toUpperCase().padEnd(6);
1682
+ const path = step.path ?? "(no path)";
1683
+ const id = step.id ? ` [${step.id}]` : "";
1684
+ lines.push(` ${String(i + 1).padStart(2)}. ${method} ${path}${id}`);
1685
+ if (step.endpointId) lines.push(` endpointId: ${step.endpointId}`);
1686
+ });
1687
+ }
1688
+ const exports = seq.config.exports ?? [];
1689
+ if (exports.length > 0) {
1690
+ lines.push("");
1691
+ lines.push(` Exports: ${JSON.stringify(exports)}`);
1692
+ }
1693
+ return lines.join("\n");
1694
+ }
1695
+
1696
+ // src/workflow-seed-create.ts
1697
+ import fs4 from "fs/promises";
1698
+ import process8 from "process";
1699
+ function getAllArgValues(argv, flag) {
1700
+ const values = [];
1701
+ for (let i = 0; i < argv.length; i++) {
1702
+ if (argv[i] === flag && i + 1 < argv.length) {
1703
+ values.push(argv[i + 1]);
1704
+ i++;
1705
+ }
1706
+ }
1707
+ return values;
1708
+ }
1709
+ function parseOutputFlag(value) {
1710
+ const parts = value.split(":");
1711
+ if (parts.length < 2) {
1712
+ throw new Error(
1713
+ `Invalid --output format: "${value}". Expected "dataKey:Name" or "dataKey:Name:Description".`
1714
+ );
1715
+ }
1716
+ const [dataKey, name, ...descParts] = parts;
1717
+ const description = descParts.length > 0 ? descParts.join(":") : void 0;
1718
+ return {
1719
+ dataKey: dataKey.trim(),
1720
+ name: name.trim(),
1721
+ displayLabel: name.trim(),
1722
+ description: description?.trim() || void 0
1723
+ };
1724
+ }
1725
+ async function readFromStdin3() {
1726
+ const chunks = [];
1727
+ const stdin = process8.stdin;
1728
+ if (stdin.isTTY) {
1729
+ throw new Error("No input on stdin. Pipe JSON or use --from-file.");
1730
+ }
1731
+ return new Promise((resolve, reject) => {
1732
+ stdin.on("data", (chunk) => {
1733
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1734
+ });
1735
+ stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1736
+ stdin.on("error", reject);
1737
+ });
1738
+ }
1739
+ async function readJsonInput2(argv) {
1740
+ let raw;
1741
+ const fromFile = getArgValue(argv, "--from-file");
1742
+ if (fromFile) {
1743
+ try {
1744
+ raw = await fs4.readFile(fromFile, "utf-8");
1745
+ } catch (err) {
1746
+ throw new Error(`Failed to read file ${fromFile}: ${err instanceof Error ? err.message : String(err)}`);
1747
+ }
1748
+ } else {
1749
+ raw = await readFromStdin3();
1750
+ }
1751
+ let parsed;
1752
+ try {
1753
+ parsed = JSON.parse(raw);
1754
+ } catch {
1755
+ throw new Error("Invalid JSON input.");
1756
+ }
1757
+ const input = parsed;
1758
+ if (!input.name || typeof input.name !== "string") {
1759
+ throw new Error('JSON input must include a "name" string field.');
1760
+ }
1761
+ if (Array.isArray(input.outputs)) {
1762
+ for (const out of input.outputs) {
1763
+ if (!out.dataKey || !out.name) {
1764
+ throw new Error('Each output must have "dataKey" and "name" fields.');
1765
+ }
1766
+ if (!out.displayLabel) {
1767
+ out.displayLabel = out.name;
1768
+ }
1769
+ }
1770
+ }
1771
+ return input;
1772
+ }
1773
+ function resolveInput(argv) {
1774
+ const fromFile = getArgValue(argv, "--from-file");
1775
+ const fromStdin = hasFlag(argv, "--from-stdin");
1776
+ if (fromFile || fromStdin) {
1777
+ return readJsonInput2(argv);
1778
+ }
1779
+ const name = getArgValue(argv, "--name");
1780
+ if (!name) {
1781
+ console.error("Error: --name is required (or use --from-stdin / --from-file for JSON input).");
1782
+ console.error("Usage: canary workflow seed create --name <name> [--output key:name[:desc]] ...");
1783
+ process8.exit(1);
1784
+ }
1785
+ const description = getArgValue(argv, "--description");
1786
+ const status = getArgValue(argv, "--status");
1787
+ const outputFlags = getAllArgValues(argv, "--output");
1788
+ const outputs = outputFlags.map(parseOutputFlag);
1789
+ const outputsJsonRaw = getArgValue(argv, "--outputs-json");
1790
+ if (outputsJsonRaw) {
1791
+ try {
1792
+ const parsed = JSON.parse(outputsJsonRaw);
1793
+ if (!Array.isArray(parsed)) throw new Error("not an array");
1794
+ for (const out of parsed) {
1795
+ if (!out.dataKey || !out.name) {
1796
+ throw new Error('Each output must have "dataKey" and "name" fields.');
1797
+ }
1798
+ if (!out.displayLabel) out.displayLabel = out.name;
1799
+ }
1800
+ outputs.push(...parsed);
1801
+ } catch (err) {
1802
+ console.error(`Error: Invalid --outputs-json: ${err instanceof Error ? err.message : String(err)}`);
1803
+ process8.exit(1);
1804
+ }
1805
+ }
1806
+ return { name, description: description || void 0, status, outputs };
1807
+ }
1808
+ async function handleWorkflowSeedCreate(argv, apiUrl, token) {
1809
+ if (hasFlag(argv, "--help", "-h")) {
1810
+ printSeedCreateHelp();
1811
+ return;
1812
+ }
1813
+ const jsonOutput = hasFlag(argv, "--json");
1814
+ const noTeardown = hasFlag(argv, "--no-teardown");
1815
+ let input;
1816
+ try {
1817
+ input = await resolveInput(argv);
1818
+ } catch (err) {
1819
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1820
+ process8.exit(1);
1821
+ }
1822
+ const seedResult = await apiRequest(
1823
+ apiUrl,
1824
+ token,
1825
+ "POST",
1826
+ "/workflows",
1827
+ {
1828
+ name: input.name,
1829
+ description: input.description,
1830
+ status: input.status ?? "draft",
1831
+ flowType: "seed",
1832
+ createdVia: "cli"
1833
+ }
1834
+ );
1835
+ if (!seedResult.ok || !seedResult.workflow) {
1836
+ if (jsonOutput) {
1837
+ console.log(JSON.stringify({ ok: false, error: seedResult.error ?? "Failed to create seed workflow" }, null, 2));
1838
+ } else {
1839
+ console.error(`Error: ${seedResult.error ?? "Failed to create seed workflow"}`);
1840
+ }
1841
+ process8.exit(1);
1842
+ }
1843
+ const seedId = seedResult.workflow.id;
1844
+ const appUrl = resolveAppUrl(apiUrl);
1845
+ let outputsSaved = false;
1846
+ if (input.outputs && input.outputs.length > 0) {
1847
+ const outputsResult = await apiRequest(
1848
+ apiUrl,
1849
+ token,
1850
+ "PUT",
1851
+ `/workflows/${seedId}`,
1852
+ { seedOutputsJson: input.outputs }
1853
+ );
1854
+ if (outputsResult.ok) {
1855
+ outputsSaved = true;
1856
+ } else {
1857
+ console.warn(`Warning: Seed created but failed to save outputs: ${outputsResult.error ?? "Unknown error"}`);
1858
+ }
1859
+ }
1860
+ let teardownId;
1861
+ let teardownName;
1862
+ if (!noTeardown) {
1863
+ const tdName = `Teardown: ${input.name}`;
1864
+ const tdResult = await apiRequest(
1865
+ apiUrl,
1866
+ token,
1867
+ "POST",
1868
+ "/workflows",
1869
+ {
1870
+ name: tdName,
1871
+ description: `Cleans up seed data from "${input.name}"`,
1872
+ status: "draft",
1873
+ flowType: "teardown",
1874
+ createdVia: "cli"
1875
+ }
1876
+ );
1877
+ if (tdResult.ok && tdResult.workflow) {
1878
+ teardownId = tdResult.workflow.id;
1879
+ teardownName = tdName;
1880
+ const linkResult = await apiRequest(
1881
+ apiUrl,
1882
+ token,
1883
+ "PUT",
1884
+ `/workflows/${seedId}`,
1885
+ { teardownWorkflowId: teardownId }
1886
+ );
1887
+ if (!linkResult.ok) {
1888
+ console.warn(`Warning: Teardown created but failed to link to seed: ${linkResult.error ?? "Unknown error"}`);
1889
+ }
1890
+ } else {
1891
+ console.warn(`Warning: Failed to create paired teardown: ${tdResult.error ?? "Unknown error"}`);
1892
+ }
1893
+ }
1894
+ if (jsonOutput) {
1895
+ const result = {
1896
+ ok: true,
1897
+ seed: {
1898
+ id: seedId,
1899
+ name: input.name,
1900
+ status: input.status ?? "draft",
1901
+ flowType: "seed",
1902
+ outputs: outputsSaved ? input.outputs : [],
1903
+ url: `${appUrl}/flows/${seedId}`
1904
+ }
1905
+ };
1906
+ if (teardownId) {
1907
+ result.teardown = {
1908
+ id: teardownId,
1909
+ name: teardownName,
1910
+ status: "draft",
1911
+ flowType: "teardown",
1912
+ url: `${appUrl}/flows/${teardownId}`
1913
+ };
1914
+ }
1915
+ console.log(JSON.stringify(result, null, 2));
1916
+ return;
1917
+ }
1918
+ console.log(`
1919
+ \u2713 Seed workflow created: ${input.name}`);
1920
+ console.log(` ID: ${seedId}`);
1921
+ console.log(` Status: ${input.status ?? "draft"}`);
1922
+ if (input.outputs && input.outputs.length > 0) {
1923
+ const keys = input.outputs.map((o) => o.dataKey).join(", ");
1924
+ console.log(` Outputs: ${keys}${!outputsSaved ? " (save failed)" : ""}`);
1925
+ }
1926
+ console.log(` URL: ${appUrl}/flows/${seedId}`);
1927
+ if (teardownId) {
1928
+ console.log(`
1929
+ \u2713 Teardown workflow created: ${teardownName}`);
1930
+ console.log(` ID: ${teardownId}`);
1931
+ console.log(` URL: ${appUrl}/flows/${teardownId}`);
1932
+ }
1933
+ }
1934
+ function printSeedCreateHelp() {
1935
+ console.log(
1936
+ [
1937
+ "Usage: canary workflow seed create [options]",
1938
+ "",
1939
+ "Create a seed workflow with declared outputs and an optional paired teardown.",
1940
+ "",
1941
+ "Flag-based input:",
1942
+ " --name <name> Seed workflow name (required)",
1943
+ " --description <desc> Description",
1944
+ " --output <key:name[:desc]> Declare an output (repeatable)",
1945
+ " --outputs-json <json> Full outputs array as inline JSON (for credential configs)",
1946
+ " --no-teardown Skip creating paired teardown workflow",
1947
+ " --status <status> Initial status (default: draft)",
1948
+ " --json Output raw JSON",
1949
+ "",
1950
+ "JSON input:",
1951
+ " --from-stdin Read full config from stdin",
1952
+ " --from-file <path> Read full config from file",
1953
+ "",
1954
+ "Examples:",
1955
+ ' canary workflow seed create --name "Create Project" --output "project_id:Project ID"',
1956
+ "",
1957
+ " canary workflow seed create --from-stdin <<'EOF'",
1958
+ " {",
1959
+ ' "name": "Create Test User",',
1960
+ ' "outputs": [{',
1961
+ ' "dataKey": "user_cred",',
1962
+ ' "name": "Test User Credential",',
1963
+ ' "credentialOutput": {',
1964
+ ' "fieldMapping": { "username": "$generate.email", "password": "Test123!" },',
1965
+ ' "emailPrefix": "testuser",',
1966
+ ' "createInboxRecipient": true',
1967
+ " }",
1968
+ " }]",
1969
+ " }",
1970
+ " EOF"
1971
+ ].join("\n")
1972
+ );
1973
+ }
1974
+
1975
+ // ../content/src/workflow-guide/overview.txt
1976
+ var overview_default = "Workflows are visual test sequences that model user journeys through your application. Each workflow is a directed graph of nodes \u2014 login, navigate, action, assertion, wait, condition, setup, and more \u2014 connected to form a testable path.\n\nWhen to use workflows:\n\n- Critical paths: signup, checkout, billing, onboarding\n- High-risk areas: features that regress often or have complex logic\n- Multi-step journeys: flows spanning multiple pages or user roles\n\nFLOW TYPES\n\n standard The default. Models a single user journey end-to-end.\n Login \u2192 Navigate \u2192 Actions \u2192 Assertions \u2192 Cleanup.\n\n setup A prerequisite workflow that runs before other workflows.\n Use for shared data creation or environment preparation.\n Any published workflow can be referenced as a setup step.\n\n seed A data-seeding workflow that generates test data and exports\n variables for downstream workflows. Seed outputs are declared\n in the workflow definition and available as $var.{dataKey}\n in any workflow that references the seed.\n\n teardown A cleanup workflow that runs after a seed workflow completes.\n Automatically invoked to remove data the seed created.\n\nEXECUTION MODEL\n\n 1. Login node provisions an authenticated browser session\n 2. Nodes execute in sequence (or branch at condition nodes)\n 3. Each action node interacts with the live application\n 4. Assertions verify expected outcomes\n 5. Additional login nodes switch to a different user \u2014 each starts a\n new segment with a fresh session (great for multi-role testing)\n 6. Wait nodes pause execution (minutes, hours, days) \u2014 also creating\n segments. After a wait, a login re-establishes the session.\n 7. On completion, the run is marked success or failure\n\nVERSION HISTORY\n\n Workflows maintain version history. Publishing creates a versioned\n snapshot. Previous versions can be previewed or restored to draft.\n Setup nodes reference a specific published version of another workflow.";
1977
+
1978
+ // ../content/src/workflow-guide/node-types.txt
1979
+ var node_types_default = `Every workflow is built from nodes. Each node has a type, a title, a description,
1980
+ and type-specific configuration.
1981
+
1982
+ STARTING NODE \u2014 pick the first node in this order
1983
+ The first node of a flow follows a strict hierarchy. Getting this wrong is the
1984
+ single most common authoring mistake (most often: opening with a navigate node).
1985
+
1986
+ 1. SEED first \u2014 if the flow depends on pre-existing data, a \`seed\` node comes
1987
+ first. It provisions data in its own isolated context and exports
1988
+ $var.{dataKey} outputs the rest of the flow consumes.
1989
+ 2. Otherwise LOGIN first \u2014 for any authenticated flow, the first node is a
1990
+ \`login\` node that declares the ACTOR (who the journey runs as). This is the
1991
+ "acting as" step. Do NOT open an authenticated flow with a navigate node.
1992
+ 3. Navigate comes AFTER login \u2014 a navigate jumps to the test's entry point, but
1993
+ it follows the login; it is never the opening node of an authenticated flow.
1994
+
1995
+ The only flows that may start with a navigate are UNauthenticated ones \u2014 signup,
1996
+ password reset, a public/marketing page, or a test of the login experience
1997
+ itself. For those, set the login node's \`performLogin: false\` (or omit the login
1998
+ node) and author the steps as the journey.
1999
+
2000
+ LOGIN SUBSUMES LOGGING IN. A login node already provisions an authenticated
2001
+ session \u2014 it IS the act of logging in. Never emit a login node and then a
2002
+ "navigate to /login" \u2192 "enter email/password" \u2192 "click Sign in" sequence. Start
2003
+ with the login node and go straight to the first real action. The one exception
2004
+ is a flow whose explicit purpose is to test login (start logged out and make the
2005
+ login steps the journey).
2006
+
2007
+ LOGIN
2008
+ Sets the user context. Select a credential to act as that user.
2009
+ The login node provisions an authenticated browser session \u2014 subsequent
2010
+ nodes start with the browser already logged in.
2011
+
2012
+ Config:
2013
+ credentialId UUID of the credential to log in as. REQUIRED for the
2014
+ node to bind to a real credential. Without it the
2015
+ Flow Designer shows a red "Select a credential"
2016
+ warning even if credentialName is set.
2017
+ credentialName Human-readable name, used as the node title. Optional
2018
+ but recommended alongside credentialId.
2019
+ performLogin When true, the browser is authenticated automatically.
2020
+
2021
+ Finding the credentialId:
2022
+ canary credential list [--env prod|dev]
2023
+ \u2192 prints "ID: <uuid>" under each credential name.
2024
+ Pick the ID whose property matches the environment the flow runs in.
2025
+
2026
+ Ordering: login nodes should typically follow any seed nodes, not
2027
+ precede them. Seeds export variables (e.g. $var.workflow_id) that
2028
+ later nodes consume, so a seed-first / login-second order avoids
2029
+ wasted browser segments and lets login/navigate URLs reference
2030
+ seed outputs. See the SEED node type and the Node Ordering section
2031
+ of best-practices.
2032
+
2033
+ - Workflows support multiple login nodes for multi-role testing.
2034
+ Each login node starts a new segment with a fresh browser session.
2035
+ Example: Login as Customer \u2192 place order \u2192 Login as Sales Rep \u2192 verify order.
2036
+ Great for two-sided apps, permission checks (member vs admin), and
2037
+ cross-role journeys.
2038
+ - Login nodes also appear after Wait nodes to re-establish the session.
2039
+ - A navigate node after login is optional but useful for jumping
2040
+ directly to the test's entry point.
2041
+
2042
+ NAVIGATE
2043
+ Directs the browser to a specific path.
2044
+
2045
+ Prefer relative paths (e.g. "/flows", "/flows/$var.workflow_id")
2046
+ over hard-coded absolute URLs. Relative paths let the same workflow
2047
+ run unchanged across environments (prod, dev, local, preview) \u2014 the
2048
+ base URL comes from the property/environment the run targets. A
2049
+ workflow that hard-codes "https://app.dev.trycanary.ai/..." will
2050
+ silently drive traffic against dev even when run against prod.
2051
+
2052
+ Use an absolute URL only when the destination is genuinely off the
2053
+ application being tested (external identity provider, third-party
2054
+ checkout, docs site, etc.).
2055
+
2056
+ - A navigate node after login is a great way to jump straight to the
2057
+ page where the test begins.
2058
+ - Select from discovered pages or enter a custom URL.
2059
+
2060
+ ACTION
2061
+ Performs a user interaction on the current page. Describe what the user
2062
+ should do ("Create a new ticket") and optionally provide input data via
2063
+ formFields.
2064
+
2065
+ Every action node must have a title (customActionTitle). The title shows
2066
+ in the canvas, run timeline, failure reports, and issue tickets \u2014 an
2067
+ untitled action is nearly impossible to reason about after the fact.
2068
+ Keep titles short and intent-oriented, e.g. "Create a ticket" or
2069
+ "Publish the flow" \u2014 not "Click the button".
2070
+
2071
+ Keep actions focused on a single user goal:
2072
+ Good: "Create a ticket with required fields"
2073
+ Bad: "Click the title field" (too granular)
2074
+ Bad: "Create a ticket, assign it, comment, upload, and verify" (too stuffed)
2075
+
2076
+ Rule of thumb: if an action crosses multiple screens or changes multiple
2077
+ entities, split it. If it's a single user goal on a single screen, keep it.
2078
+
2079
+ Actions can include:
2080
+ - customActionTitle: short intent-oriented title (required)
2081
+ - formFields: input data the action needs (name, email, etc.)
2082
+ - inlineAssertions: checks verified after the action completes (preferred
2083
+ over standalone assertion nodes \u2014 see Assertions section)
2084
+ - declaredOutputs: values to extract from the page for downstream use
2085
+ - customInstructions: additional guidance for the agent executing the step
2086
+
2087
+ ASSERTION
2088
+ Verifies expected outcomes. Define a condition to check.
2089
+ The run fails if the condition is not met.
2090
+
2091
+ Prefer inline assertions on action nodes over standalone assertion nodes.
2092
+ Inline assertions keep the test on track and produce cleaner failure
2093
+ reports. If verifying something requires significant interaction first,
2094
+ that's better modeled as an action node with inline assertions.
2095
+
2096
+ Use standalone assertion nodes only when the check doesn't belong to
2097
+ any action \u2014 e.g., verifying a page state after navigation, or
2098
+ confirming data absence after a sequence of steps.
2099
+
2100
+ Severity levels:
2101
+ fail Critical \u2014 the test stops if this fails
2102
+ warn Non-critical \u2014 logged but doesn't fail the test
2103
+
2104
+ Agentic assertions: for visual/layout checks that can't be mechanically
2105
+ validated, enable agent evaluation. These require the AI agent during
2106
+ replay and cannot be cached. Use for:
2107
+ - Timestamps or dates
2108
+ - Real-time data feeds
2109
+ - Content that changes between runs
2110
+
2111
+ CONDITION
2112
+ Branches the workflow based on a runtime condition. Define a condition
2113
+ to evaluate, then connect separate paths for true and false outcomes.
2114
+
2115
+ WAIT
2116
+ Pauses execution for a specified duration (minutes, hours, or days).
2117
+ Creates a segmented flow \u2014 execution pauses, then resumes automatically.
2118
+ Add a Login node after the Wait to re-establish the browser session.
2119
+
2120
+ SETUP
2121
+ References another published workflow to run as a prerequisite.
2122
+
2123
+ Execution modes:
2124
+ Inline (default) Runs before each workflow
2125
+ Suite-level Runs once per suite, shared across workflows
2126
+
2127
+ When to use: creating users, projects, or records that multiple tests
2128
+ depend on. Login is handled separately \u2014 setup nodes are for data creation.
2129
+
2130
+ SEED
2131
+ Injects outputs from a seed workflow as variables. Seed workflows
2132
+ generate test data and export named values. Downstream workflows
2133
+ access these as $var.{dataKey}.
2134
+
2135
+ Ordering: seed nodes should almost always come FIRST \u2014 before the
2136
+ login node. Seeds run in their own isolated context (usually via
2137
+ API sequence) and export variables into the workflow. Placing a
2138
+ login before the seed wastes a browser session, and any
2139
+ login/navigate node that references $var.{seed_output} must come
2140
+ after the seed that produces it.
2141
+
2142
+ Typical pattern:
2143
+ Seed \u2192 Login \u2192 Navigate \u2192 ... action/assertion nodes
2144
+
2145
+ Config:
2146
+ seedWorkflowId UUID of the seed workflow (required)
2147
+ seedWorkflowName Human-readable name (used as the node title)
2148
+
2149
+ Seed workflows are the preferred way to provision test data. See the
2150
+ Seed Workflows section for details on building and using them.
2151
+
2152
+ API SEQUENCE
2153
+ Executes multiple API calls in sequence without a browser.
2154
+ Use for backend-only operations, webhooks, or API testing.
2155
+
2156
+ API sequences are also used inside seed workflows \u2014 start with a
2157
+ browser-based seed to discover the right API calls, then convert
2158
+ to an API sequence for speed and reliability.
2159
+
2160
+ NOTIFICATION
2161
+ Sends notifications (Slack, email, etc.) at a point in the workflow.
2162
+
2163
+ END
2164
+ Explicitly terminates the workflow. Use when a condition branch
2165
+ should stop execution rather than continue.`;
2166
+
2167
+ // ../content/src/workflow-guide/playbook.txt
2168
+ var playbook_default = `A playbook is a sequence of human-readable steps that describe the actions
2169
+ taken during a workflow node's execution. Playbooks bridge the gap between
2170
+ high-level node descriptions ("Create a ticket") and the low-level browser
2171
+ interactions that fulfill them.
2172
+
2173
+ WHAT A PLAYBOOK CONTAINS
2174
+
2175
+ Each playbook is an ordered list of PlaybookSteps:
2176
+
2177
+ order Step sequence number
2178
+ action What to do: click, fill, fill_form, select, navigate,
2179
+ wait, scroll, hover, upload, verify, handle_native_dialog,
2180
+ toggle, press_key, focus, drag, resize, capture,
2181
+ extract_table, python_execute, verify_download,
2182
+ verify_download_content, screenshot, agent_loop
2183
+ target The element or area being acted on
2184
+ params Key-value parameters for the action
2185
+ note Human-readable description of the step
2186
+
2187
+ Example playbook for "Create a ticket":
2188
+ 1. click "New Ticket" button
2189
+ 2. fill_form Title: "QA Test \u2014 Billing Bug", Priority: "High"
2190
+ 3. click "Submit" button
2191
+ 4. verify Success toast appears
2192
+
2193
+ HOW PLAYBOOKS ENABLE CACHE REPLAY
2194
+
2195
+ When a workflow runs for the first time, the AI agent explores the
2196
+ application, performs actions, and generates a playbook for each node.
2197
+ The raw browser actions (clicks, keystrokes, waits) are cached alongside
2198
+ the playbook steps.
2199
+
2200
+ On subsequent runs, the system uses playbook-step-driven replay:
2201
+
2202
+ 1. Load the playbook steps from the node config
2203
+ 2. For each step, look up the cached browser actions
2204
+ 3. Replay the cached actions directly (no AI agent needed)
2205
+ 4. If a step's cache misses or fails, the AI agent heals that step
2206
+
2207
+ This means most runs execute instantly via cache, with the agent only
2208
+ invoked for steps that have changed or broken.
2209
+
2210
+ COMPILED ENTRIES
2211
+
2212
+ The cache maps playbook steps to cached action indices:
2213
+
2214
+ cached Playbook step has matching cached actions \u2014 replayed directly
2215
+ gap No cache for this step \u2014 agent performs it fresh
2216
+
2217
+ A fully cached node replays in seconds. A node with gaps falls back to
2218
+ agent execution only for the missing steps.
2219
+
2220
+ PLAYBOOK HEALTH
2221
+
2222
+ Each cached node tracks playbook health:
2223
+
2224
+ verified Cache is working correctly across recent runs
2225
+ degraded Some steps have failures \u2014 cache still used but monitored
2226
+ stale Cache is no longer reliable \u2014 full agent re-execution needed
2227
+
2228
+ Step-level failure counts track which specific steps break most often,
2229
+ guiding targeted cache healing.
2230
+
2231
+ WHY PLAYBOOKS MATTER FOR AUTHORING
2232
+
2233
+ When you build a workflow, the quality of your node descriptions directly
2234
+ affects playbook quality. Clear, intent-focused descriptions produce
2235
+ better playbooks, which produce more reliable cache replay.
2236
+
2237
+ Good: "Create a ticket with title 'QA Test' and priority 'High'"
2238
+ \u2192 Clean playbook: fill_form with clear fields, click submit
2239
+
2240
+ Bad: "Do the thing on the page"
2241
+ \u2192 Vague playbook: agent guesses at actions, cache breaks easily
2242
+
2243
+ The playbook is the semantic contract between your intent and the
2244
+ execution engine. Write nodes that describe what a human would do,
2245
+ and the playbook will faithfully capture and replay it.
2246
+ `;
2247
+
2248
+ // ../content/src/workflow-guide/best-practices.txt
2249
+ var best_practices_default = `SCOPE: ONE COHERENT USER JOURNEY
2250
+
2251
+ A workflow should test one job-to-be-done, not multiple unrelated tasks.
2252
+ Many steps are fine if they're all part of the same flow (creating,
2253
+ editing, verifying one entity). Stop when the goal is accomplished.
2254
+
2255
+ Good \u2014 coherent journey:
2256
+ Login \u2192 Navigate to projects \u2192 Create project \u2192 Edit name \u2192
2257
+ Assert name updated \u2192 Cleanup: delete project
2258
+
2259
+ Good \u2014 multi-role journey:
2260
+ Login as Customer \u2192 Place an order \u2192
2261
+ Login as Sales Rep \u2192 Verify the order appears in the queue
2262
+
2263
+ Good \u2014 read-only flow:
2264
+ Login \u2192 Navigate to dashboard \u2192 Assert widgets visible
2265
+
2266
+ Bad \u2014 multiple unrelated journeys:
2267
+ Dashboard \u2192 Create item \u2192 Manage users \u2192 Generate report
2268
+ (These are separate jobs \u2014 create separate workflows)
2269
+
2270
+ Rule: "Is this step dependent on or naturally connected to the previous
2271
+ steps?" If no, you're starting a new journey \u2014 stop.
2272
+
2273
+ WRITE STEPS AS HUMAN INTENT
2274
+
2275
+ A workflow should read like instructions you'd explain to a teammate:
2276
+ - "Create a ticket assigned to Support."
2277
+ - "Submit the ticket and confirm it appears in the list."
2278
+ - "Change the ticket status to Closed."
2279
+
2280
+ Capture the "why" and the "what" \u2014 not UI mechanics.
2281
+
2282
+ CHOOSE THE RIGHT GRANULARITY
2283
+
2284
+ Too granular:
2285
+ - "Click New"
2286
+ - "Type title"
2287
+ - "Type description"
2288
+ - "Click Save"
2289
+
2290
+ Too stuffed:
2291
+ - "Create a ticket, assign it, comment, upload, and verify in three views"
2292
+
2293
+ Good:
2294
+ - "Create a ticket with required fields"
2295
+ - "Assign the ticket to Support"
2296
+ - "Verify the ticket appears in the queue"
2297
+
2298
+ If an action crosses multiple screens or changes multiple entities, split it.
2299
+ If it's a single user goal on a single screen, keep it as one action.
2300
+
2301
+ MAKE EACH TEST SELF-SUFFICIENT
2302
+
2303
+ Tests should not depend on what another test created or on long-lived
2304
+ shared state. If a workflow needs data, provision it:
2305
+
2306
+ - Use a seed workflow (preferred) to generate data and export variables
2307
+ - Use a setup node to reference a published workflow for shared data
2308
+ - As a last resort, add data creation steps at the start of the workflow
2309
+ - Always use macros ($EMAIL, $UUID, etc.) so reruns don't collide
2310
+
2311
+ This keeps runs deterministic and prevents cascading failures.
2312
+
2313
+ VERIFY EVERY SIGNIFICANT ACTION
2314
+
2315
+ Before recording a step, verify it worked:
2316
+
2317
+ - Form submissions: confirm acceptance (success message, redirect, new item in list)
2318
+ - Data creation: navigate to where the record should appear and confirm
2319
+ - Data modification: reload and confirm the change persisted
2320
+ - Deletions: confirm the item is gone (a toast alone is not enough)
2321
+ - Navigation: confirm the expected page loaded
2322
+
2323
+ If verification fails, debug the issue \u2014 don't record and move on.
2324
+
2325
+ KEEP DESCRIPTIONS CLEAR
2326
+
2327
+ Node titles should be short and action-oriented:
2328
+ Good: "Create a support ticket"
2329
+ Bad: "Step 3 - ticket creation process"
2330
+
2331
+ Node descriptions should add detail the title can't carry:
2332
+ Good: "Fill the form with a QA Test subject and High priority, then submit"
2333
+ Bad: "Do the thing"
2334
+
2335
+ NODE ORDERING
2336
+
2337
+ The start-of-workflow order is a strict hierarchy (see "STARTING NODE"
2338
+ in the Node Types guide):
2339
+
2340
+ Seed (if needed) \u2192 Login \u2192 Navigate \u2192 ...
2341
+
2342
+ The first node is the ACTOR, not a navigate: an authenticated flow opens
2343
+ with a \`login\` node (or a \`seed\`, if it needs data first) \u2014 never with a
2344
+ navigate node. A navigate jumps to the test's entry point AFTER login.
2345
+ Only unauthenticated flows (signup, password reset, public pages, or a
2346
+ login test) may start with a navigate.
2347
+
2348
+ Seed nodes should come first. Seeds run in their own isolated context
2349
+ (usually via API sequence) and export variables the rest of the flow
2350
+ consumes. Putting a login before the seed wastes a browser segment,
2351
+ and any login/navigate that references $var.{seed_output} must come
2352
+ after the seed that produced it.
2353
+
2354
+ Only skip the seed-first rule if the seed itself genuinely depends on
2355
+ a browser session created earlier in the same workflow (rare \u2014 in
2356
+ that case, prefer a separate browser-based seed workflow and reference
2357
+ it here).
2358
+
2359
+ LOGIN BEST PRACTICES
2360
+
2361
+ - Most workflows start with a login node. Workflows can also start
2362
+ with a seed node or API sequence when no browser session is needed yet.
2363
+ - Login SUBSUMES logging in. A login node already provisions an
2364
+ authenticated session \u2014 it IS the act of logging in. Never follow a
2365
+ login node with "navigate to /login" / "enter email + password" /
2366
+ "click Sign in" steps. Start with the login node and go straight to
2367
+ the first real action. (Only exception: a flow whose purpose is to
2368
+ test login \u2014 start logged out and make the login steps the journey.)
2369
+ - Login nodes require a credentialId (UUID), not just a credentialName.
2370
+ Without a credentialId the Flow Designer flags the node as invalid.
2371
+ Find credential IDs with: canary credential list [--env prod|dev]
2372
+ - Use multiple login nodes for multi-role testing \u2014 each login starts
2373
+ a new segment with a fresh session as that user
2374
+ - Don't test login itself (unless that's the explicit goal)
2375
+ - A navigate node after login is a great way to jump directly to the
2376
+ test's entry point
2377
+
2378
+ PREFER INLINE ASSERTIONS
2379
+
2380
+ Attach assertions to action nodes using inlineAssertions rather than
2381
+ adding standalone assertion nodes. Inline assertions keep the test
2382
+ focused and produce clearer failure reports. See the Assertions section
2383
+ for details.`;
2384
+
2385
+ // ../content/src/workflow-guide/macros.txt
2386
+ var macros_default = `Macros generate fresh, unique data every time a workflow runs. Variables let
2387
+ you reference shared values across nodes. Both use the $ prefix.
2388
+
2389
+ AVAILABLE MACROS
2390
+
2391
+ $EMAIL() Unique test email test-a1b2@example-test.com
2392
+ $FIRST_NAME() Random first name Jordan
2393
+ $LAST_NAME() Random last name Chen
2394
+ $PHONE() Random US phone +1-555-382-7491
2395
+ $UUID() Random UUID v4 a1b2c3d4-e5f6-...
2396
+ $NOW() ISO 8601 timestamp 2026-02-19T14:30:00.000Z
2397
+ $TODAY() Current date 2026-02-19
2398
+ $DATE(N) Date offset by N days $DATE(7) = next week
2399
+ $TIMESTAMP() Unix timestamp (ms) 1771520400000
2400
+ $RANDOM_INT(min,max) Random integer $RANDOM_INT(1,100) = 42
2401
+ $RANDOM_STRING(len) Random alphanumeric $RANDOM_STRING(6) = xK9mQ4
2402
+ $SEQUENCE(name) Auto-incrementing $SEQUENCE(order) = 1, 2, 3...
2403
+
2404
+ SYNTAX
2405
+
2406
+ Macros follow the format $MACRO_NAME() or $MACRO_NAME(args).
2407
+ They can appear standalone or inline within a larger string:
2408
+
2409
+ Standalone: $EMAIL()
2410
+ Inline: user-$UUID()@corp.com
2411
+ Escaped: \\$EMAIL() produces the literal text $EMAIL()
2412
+
2413
+ Each run resolves macros independently \u2014 every execution gets fresh values.
2414
+
2415
+ VARIABLES
2416
+
2417
+ Variables reference values set during a workflow run. When an action has
2418
+ input fields or extracts data, those become $var.{name} references.
2419
+
2420
+ Sources:
2421
+ Input A value entered in an action's field $var.company
2422
+ Macro A macro expression in a field $var.email
2423
+ Extracted Data extracted from the page $var.order_id
2424
+ Credential Values from the login credential $var.username
2425
+
2426
+ Use $var.{name} in subsequent step formFields and descriptions.
2427
+ Don't re-hardcode values \u2014 reference the variable from the earlier step.
2428
+
2429
+ HOW VARIABLES GET BOUND
2430
+
2431
+ Two distinct paths create $var.{name} bindings. Picking the wrong one is a
2432
+ common foot-gun.
2433
+
2434
+ 1. Typed input (auto-promote)
2435
+
2436
+ Add an entry to inputValues on an action node:
2437
+
2438
+ inputValues: { company: "$RANDOM_STRING(8)" }
2439
+
2440
+ The key "company" auto-promotes to $var.company for every downstream
2441
+ node. Do NOT also list it in declaredOutputs \u2014 the input itself IS the
2442
+ binding.
2443
+
2444
+ 2. DOM capture (declaredOutputs + playbook capture)
2445
+
2446
+ For values that only exist after the page renders (URL fragment, badge
2447
+ text, generated ID in the breadcrumb), declare them upfront and capture
2448
+ them inside the playbook:
2449
+
2450
+ declaredOutputs: ["order_id"]
2451
+ playbookSteps: [
2452
+ { order: N, action: "capture", target: "<selector or rule>",
2453
+ outputKey: "order_id" }
2454
+ ]
2455
+
2456
+ The capture step writes into $var.order_id at runtime. Without
2457
+ declaredOutputs the downstream resolver won't recognize the binding.
2458
+
2459
+ Rule of thumb: if you can type the value into a field before the run, it's
2460
+ an input. If the value only exists after something happens in the browser,
2461
+ it's a captured output.
2462
+
2463
+ SEED VARIABLES
2464
+
2465
+ When seed workflows are configured, their outputs are available as
2466
+ $var.{dataKey} references. If a seed provides data you need (a project ID,
2467
+ a user record), use the seed variable \u2014 don't create that data again.
2468
+
2469
+ COMMON PATTERNS
2470
+
2471
+ Unique user registration:
2472
+ Email: $EMAIL()
2473
+ Name: $FIRST_NAME() $LAST_NAME()
2474
+
2475
+ Numbered items:
2476
+ $SEQUENCE("orders") \u2192 1, 2, 3... (resets each run, independent per name)
2477
+
2478
+ Date-relative testing:
2479
+ $DATE(30) for 30 days out, $DATE(-7) for last week, $TODAY() for now
2480
+
2481
+ Unique identifiers:
2482
+ $UUID() for globally unique, $RANDOM_STRING(8) for shorter strings
2483
+ Combine with text: order-$RANDOM_STRING(6)
2484
+
2485
+ BEST PRACTICES
2486
+
2487
+ - Use $EMAIL() instead of hardcoded addresses for uniqueness
2488
+ - Use $SEQUENCE("name") when you need predictable ordering within a run
2489
+ - Prefer $DATE(days) over fixed dates so tests stay valid over time
2490
+ - Always include a value for every formField \u2014 don't leave fields empty`;
2491
+
2492
+ // ../content/src/workflow-guide/seed-workflows.txt
2493
+ var seed_workflows_default = `Seed workflows are the preferred way to provision test data. A seed
2494
+ workflow creates data your test needs and exports named variables that
2495
+ downstream workflows can reference.
2496
+
2497
+ WHY USE SEEDS
2498
+
2499
+ - Reusable: one seed can serve many test workflows
2500
+ - Fast: seeds can use API sequences instead of slow browser interactions
2501
+ - Clean: paired teardown workflows handle cleanup automatically
2502
+ - Isolated: each run gets its own fresh data via macros
2503
+
2504
+ HOW SEEDS WORK
2505
+
2506
+ 1. Create a workflow with flowType "seed"
2507
+ 2. Define seedOutputs \u2014 the named values the seed exports
2508
+ (e.g., project_id, user_email, order_number)
2509
+ 3. The seed creates data and captures those values during execution
2510
+ 4. Downstream workflows add a Seed node that references this seed
2511
+ 5. The seed's outputs are available as $var.{dataKey} in the test
2512
+
2513
+ Example:
2514
+ Seed "Create Test Project" exports: project_id, project_name
2515
+ Test workflow references the seed, then uses:
2516
+ Navigate to $var.project_id
2517
+ Assert page title contains $var.project_name
2518
+
2519
+ CREATING SEEDS
2520
+
2521
+ A seed workflow shell needs a name, declared outputs, and usually a paired
2522
+ teardown. Outputs are the contract downstream workflows depend on; define
2523
+ them before building the seed body so every node captures the right values.
2524
+
2525
+ Simple data outputs need:
2526
+ - dataKey: stable variable name, used as $var.{dataKey}
2527
+ - name: human label
2528
+ - description: optional context for builders and reviewers
2529
+
2530
+ Credential outputs additionally define credentialOutput, including provider
2531
+ type, generated field mapping, inbox behavior, and loginUrl.
2532
+
2533
+ After creating the shell, add nodes through the Flow Designer, workflow sync,
2534
+ or API, then publish once the seed and paired teardown both pass.
2535
+
2536
+ CLI users: see \`canary workflow guide cli-seeds\` for command usage.
2537
+
2538
+ BUILDING A SEED: START WITH THE BROWSER
2539
+
2540
+ The easiest way to build a seed is to start with browser-based steps:
2541
+
2542
+ 1. Create a seed workflow (flowType: seed)
2543
+ 2. Add a login node with credentials that have permission to create data
2544
+ 3. Add action nodes that create the data through the UI
2545
+ - Use macros for all names and identifiers ($EMAIL, $RANDOM_STRING, etc.)
2546
+ 4. Add declaredOutputs on action nodes to capture generated values
2547
+ (IDs, URLs, confirmation numbers)
2548
+ 5. Define seedOutputs on the workflow to export those captured values
2549
+ 6. Test and publish the seed
2550
+
2551
+ This approach lets you discover exactly what steps and data are needed
2552
+ by interacting with the real application.
2553
+
2554
+ CONVERTING TO API SEQUENCES
2555
+
2556
+ Once a browser-based seed works reliably, convert it to an API sequence
2557
+ for speed and reliability. Canary can do this automatically.
2558
+
2559
+ Auto-conversion from a workflow run:
2560
+ 1. Open the seed workflow in the Flow Designer
2561
+ 2. Add an api_sequence node
2562
+ 3. In the node config, choose "Generate from workflow run"
2563
+ 4. Select a completed run of the browser-based seed
2564
+ 5. The AI agent analyzes the network requests captured during
2565
+ that run and generates an equivalent API sequence automatically
2566
+ 6. The agent tests each step against the live API to verify it works
2567
+ 7. Review the generated sequence and publish
2568
+
2569
+ The AI agent has access to your application's API catalog and can:
2570
+ - Search for endpoints by keyword
2571
+ - Inspect request/response schemas
2572
+ - Test individual API calls to verify they work
2573
+ - Compose a multi-step sequence with variable passing between steps
2574
+ - Validate the full sequence end-to-end before finalizing
2575
+
2576
+ You can also generate an API sequence from a prompt \u2014 describe what
2577
+ data to create and the agent will find the right endpoints and build
2578
+ the sequence.
2579
+
2580
+ API sequences support two kinds of dynamic values:
2581
+ - Step references: {{stepId.response.path.to.value}} to pass data
2582
+ between steps (e.g., use the project ID from step 1 in step 2)
2583
+ - Macros: $EMAIL(), $UUID(), $RANDOM_STRING(), etc. for fresh
2584
+ unique data each run
2585
+
2586
+ Exports map response fields to seed output variables:
2587
+ - stepId: which step to extract from
2588
+ - responsePath: dot notation path (e.g., "data.id")
2589
+ - variableName: the $var.{name} downstream workflows will use
2590
+
2591
+ EXECUTION MODES
2592
+
2593
+ Seeds automatically choose the best execution mode:
2594
+
2595
+ browser Runs through the browser like a normal workflow
2596
+ api Runs the API sequence directly \u2014 no browser needed
2597
+ recommended Uses the API sequence if one exists, otherwise browser
2598
+
2599
+ Once you've added an API sequence to a seed, it will automatically
2600
+ run via API \u2014 faster, cheaper, and more reliable than browser execution.
2601
+
2602
+ Not all seeds can be converted \u2014 some data creation flows require
2603
+ browser interaction (file uploads, complex wizards, third-party
2604
+ integrations). Keep those as browser-based seeds.
2605
+
2606
+ API CALL TIMEOUT
2607
+
2608
+ When a seed runs in API mode (executionMode "api", or "recommended"
2609
+ resolving to API), each HTTP call uses a 2-minute (120s) timeout by
2610
+ default. Seeds/teardowns that hit slow provisioning endpoints can raise
2611
+ this per seed node via apiTimeoutSeconds \u2014 it applies to both the seed
2612
+ and its paired teardown. Ignored for browser-mode seeds.
2613
+
2614
+ Set it three equivalent ways:
2615
+ - Flow Designer: the "API call timeout (seconds)" field on the Seed node
2616
+ - .canary.md: a "- apiTimeoutSeconds: 300" bullet under the Seed node
2617
+ - workflow JSON (create / sync): on the seed node's configJson, e.g.
2618
+ { "nodeType": "seed",
2619
+ "configJson": { "seedExecutionMode": "api", "apiTimeoutSeconds": 300 } }
2620
+
2621
+ Leave it unset to keep the 120s default; an invalid value falls back to
2622
+ the default rather than erroring.
2623
+
2624
+ TEARDOWN WORKFLOWS
2625
+
2626
+ Every seed can have a paired teardown workflow that cleans up the
2627
+ data it created. Teardowns run automatically after the seed's
2628
+ downstream workflows complete.
2629
+
2630
+ - Set flowType to "teardown" and link it to the seed
2631
+ - The teardown receives the same $var.{dataKey} variables the seed exported
2632
+ - Use those variables to find and delete the created data
2633
+
2634
+ Teardowns are the cleanest way to handle cleanup \u2014 they keep cleanup
2635
+ logic separate from test logic and run automatically.
2636
+
2637
+ SEED BEST PRACTICES
2638
+
2639
+ - Use macros in all created data ($EMAIL, $RANDOM_STRING, etc.)
2640
+ so each run gets unique, non-conflicting data
2641
+ - Keep seeds focused \u2014 one seed per logical dataset
2642
+ (e.g., "Create Project with Members" not "Create Everything")
2643
+ - Export only what downstream workflows need \u2014 don't over-export
2644
+ - Start with browser-based seeds, then use auto-conversion to generate
2645
+ an API sequence from a completed run
2646
+ - Pair seeds with teardown workflows for automatic cleanup
2647
+ - Use suite-level setup nodes to share seed data across multiple
2648
+ workflows in the same test suite
2649
+ `;
2650
+
2651
+ // ../content/src/workflow-guide/data-lifecycle.txt
2652
+ var data_lifecycle_default = `Test data discipline keeps workflows reliable and concurrent-safe.
2653
+
2654
+ PREFER SEED WORKFLOWS FOR DATA
2655
+
2656
+ Seed workflows are the best way to provision test data. They run before
2657
+ your test workflow, create the data it needs, and export variables that
2658
+ downstream workflows can reference as $var.{dataKey}.
2659
+
2660
+ Benefits of seeds over inline data creation:
2661
+ - Reusable across multiple workflows
2662
+ - Faster \u2014 can use API sequences instead of browser interactions
2663
+ - Cleaner separation of setup and test logic
2664
+ - Paired teardown workflows handle cleanup automatically
2665
+
2666
+ See the Seed Workflows section for how to build and use them.
2667
+
2668
+ THE DATA OWNERSHIP PRINCIPLE
2669
+
2670
+ Own your data:
2671
+ - Data you create \u2192 clean it up when the workflow finishes
2672
+ - Settings you modify \u2192 revert them when the workflow finishes
2673
+ - Data that was already there \u2192 never delete it
2674
+
2675
+ It's fine to depend on pre-existing data (existing integrations, org
2676
+ settings, reference data). Only create what doesn't already exist or
2677
+ what the test specifically needs to own.
2678
+
2679
+ CREATING TEST DATA
2680
+
2681
+ If the test operates on a record (edit, delete, archive), create that
2682
+ record as an early step \u2014 unless it already exists and the test is meant
2683
+ to operate on it.
2684
+
2685
+ Never reference "any" resource:
2686
+ Bad: "Pick a project from the list" (which one? may not exist)
2687
+ Good: "Create a project named 'QA Test \u2014 $RANDOM_STRING(6)'"
2688
+ Good: "Use the seed output $var.project_id"
2689
+
2690
+ Always use macros in names to ensure uniqueness across parallel runs:
2691
+ Bad: "Test Item" (generic, will collide with other runs)
2692
+ Bad: "QA Test Project" (still collides if two runs overlap)
2693
+ Good: "QA Test \u2014 $RANDOM_STRING(6)" (unique every run)
2694
+ Good: "QA Test $FIRST_NAME() $LAST_NAME()" (unique and readable)
2695
+
2696
+ After creating data, verify it exists before continuing.
2697
+
2698
+ NAMING CONVENTION
2699
+
2700
+ All test data should follow this pattern:
2701
+
2702
+ Name/Title: "QA Test $FIRST_NAME() $LAST_NAME()"
2703
+ Email: "$EMAIL()"
2704
+ Phone: "$PHONE()"
2705
+ Project/Item: "QA Test \u2014 $RANDOM_STRING(6)"
2706
+ Unique ID: "QA$TIMESTAMP()"
2707
+
2708
+ The "QA Test" prefix makes test data instantly recognizable. The macro
2709
+ suffix ensures every run creates unique data that won't collide with
2710
+ other runs or leftover data from previous executions.
2711
+
2712
+ CLEANING UP (STRONGLY RECOMMENDED)
2713
+
2714
+ Workflows that create data should include cleanup steps at the end.
2715
+ Workflows that modify settings should revert them.
2716
+
2717
+ This isn't strictly required, but it's strongly recommended:
2718
+ - Cleanup prevents data accumulation over time
2719
+ - It keeps the test environment clean for other workflows
2720
+ - The "QA Test" naming convention makes orphaned data identifiable
2721
+ even when cleanup is skipped or fails
2722
+
2723
+ Don't over-engineer cleanup \u2014 a simple "navigate to the item and
2724
+ delete it" is sufficient. For seed workflows, use a paired teardown
2725
+ workflow instead.
2726
+
2727
+ WHAT NOT TO TOUCH
2728
+
2729
+ - Don't create users, roles, or org-level config unless that's the goal
2730
+ - Don't modify shared resources (billing, integrations, API keys) as a side effect
2731
+ - Don't delete or modify data not owned by this workflow
2732
+
2733
+ AVOIDING COLLISIONS WITH MACROS
2734
+
2735
+ The best way to avoid data collisions is to use macros for every
2736
+ name, email, and identifier. When every run generates unique values,
2737
+ there's no risk of conflicting with parallel runs or leftover data:
2738
+
2739
+ $EMAIL() Unique email every run
2740
+ $RANDOM_STRING(6) Short unique suffix for names
2741
+ $UUID() Globally unique identifier
2742
+ $TIMESTAMP() Unique-per-millisecond numeric ID
2743
+
2744
+ Combined with the "QA Test" prefix, this means you never need to
2745
+ worry about whether data from a previous run still exists. Each run
2746
+ creates its own isolated dataset.
2747
+
2748
+ READ-ONLY FLOWS
2749
+
2750
+ Workflows that only read data (verify dashboard loads, check reports)
2751
+ don't need creation or cleanup steps. Not every workflow needs to
2752
+ create data \u2014 only create what the test specifically requires.`;
2753
+
2754
+ // ../content/src/workflow-guide/assertions.txt
2755
+ var assertions_default = `Assertions verify that the application behaves as expected. There are two
2756
+ ways to add assertions: inline on action nodes, or as standalone assertion
2757
+ nodes. Inline assertions are strongly preferred.
2758
+
2759
+ INLINE ASSERTIONS (PREFERRED)
2760
+
2761
+ Attach assertions directly to action nodes using the inlineAssertions
2762
+ field. These are verified immediately after the action completes and
2763
+ cached for fast replay.
2764
+
2765
+ Inline assertions are the best way to verify outcomes because they:
2766
+ - Keep the test on track \u2014 verification happens right where the action is
2767
+ - Produce clean failure reports \u2014 each assertion is tied to the step that caused it
2768
+ - Are cacheable \u2014 fast replay on subsequent runs
2769
+
2770
+ Example \u2014 after submitting a form:
2771
+ Action: "Submit the support ticket"
2772
+ Inline assertions:
2773
+ - "Success toast appears" (severity: warn)
2774
+ - "Form is no longer visible" (severity: fail)
2775
+
2776
+ Severity levels:
2777
+ fail Critical \u2014 the test stops if this fails.
2778
+ Use for: data saved, redirect happened, error absent.
2779
+ warn Non-critical \u2014 logged but doesn't fail the test.
2780
+ Use for: toast appeared, animation completed.
2781
+
2782
+ If verifying something requires significant interaction (scrolling,
2783
+ navigating, expanding sections), model that as an action node with
2784
+ inline assertions \u2014 not a detailed standalone assertion node.
2785
+
2786
+ STANDALONE ASSERTION NODES (USE SPARINGLY)
2787
+
2788
+ Use dedicated assertion nodes only when the check doesn't belong to
2789
+ any action:
2790
+
2791
+ - Verifying a final page state after navigation (no action to attach to)
2792
+ - Checking that data was NOT modified (absence checks after a sequence)
2793
+ - Confirming a state visible only after a wait or page transition
2794
+
2795
+ A typical workflow has 0-1 standalone assertion nodes. If you find
2796
+ yourself writing a complex assertion node that requires multiple steps
2797
+ to verify, consider whether it should be an action node with inline
2798
+ assertions instead.
2799
+
2800
+ AGENTIC ASSERTIONS
2801
+
2802
+ For visual or layout checks that can't be mechanically validated,
2803
+ enable agent evaluation. These require the AI agent during replay
2804
+ and cannot be cached.
2805
+
2806
+ Use for:
2807
+ - Screenshots that should "look correct"
2808
+ - Charts or graphs rendering properly
2809
+ - Dynamic content that changes between runs
2810
+ - Timestamps or real-time data
2811
+
2812
+ Standard assertions are preferred \u2014 only use agentic when the
2813
+ check is inherently subjective or dynamic.
2814
+
2815
+ WRITING GOOD CONDITIONS
2816
+
2817
+ Assertions must be specific and observable:
2818
+
2819
+ Good:
2820
+ - "The status badge shows 'Active'"
2821
+ - "The table contains a row with name 'Test Record'"
2822
+ - "Error message is not visible"
2823
+ - "The page title is 'Dashboard'"
2824
+
2825
+ Bad:
2826
+ - "The page looks correct"
2827
+ - "The action was successful"
2828
+ - "Everything works"
2829
+
2830
+ Start with what's observable: "The page displays...", "The table
2831
+ contains...", "The field shows...". Include the specific value
2832
+ you expect.
2833
+
2834
+ ASSERTION PLACEMENT
2835
+
2836
+ Place assertions at critical verification points:
2837
+
2838
+ 1. After data creation: verify the record exists (inline on the create action)
2839
+ 2. After data modification: verify the change persisted (inline on the edit action)
2840
+ 3. After deletion: verify the item is gone (inline on the delete action)
2841
+ 4. At the goal completion point: verify the journey succeeded
2842
+
2843
+ Don't assert everything \u2014 focus on the claims that matter.
2844
+ A focused assertion that proves the test's core claim is better
2845
+ than a dozen checks on every UI element.`;
2846
+
2847
+ // ../content/src/workflow-guide/api-sequences.txt
2848
+ var api_sequences_default = 'API Sequences are the API-only counterpart to browser workflows. Instead of\ndriving a browser node-by-node, an API sequence is a single node\n(nodeType: "api_sequence") inside an api-only workflow that executes an\nordered list of HTTP calls, chaining outputs from one step into the inputs\nof the next.\n\nWHEN TO USE\n\n - Exercise an API without a UI (integration tests, fixture setup).\n - Seed or tear down state via API before/after a browser run.\n - Fast, deterministic smoke test of a critical flow.\n\nCONFIG SHAPE (ApiSequenceConfig)\n\n {\n "steps": [ ... ], // ordered; required\n "description": "...", // optional\n "stopOnFailure": true, // default: true\n "exports": [ ... ] // optional; see below\n }\n\nSTEP SHAPE (ApiSequenceStep)\n\n The authoritative type is in packages/types/src/api-sequence.ts. Key fields:\n\n {\n "stepId": "createProject", // optional; defaults to "step{index}"\n "apiRequestId": "<catalog id>", // required \u2014 points to api_requests row\n "credentialId": "<uuid>" | null, // null = unauthenticated\n "method": "POST", // optional; falls back to catalog default\n "urlTemplate": "/projects/{{createProject.response.id}}",\n "queryParams": [ { "key": "limit", "value": "10" } ],\n "bodyTemplate": "{\\"name\\": \\"$EMAIL()\\"}", // STRING (JSON), not object\n "headersTemplate": { "X-Trace": "$UUID()" }, // optional\n "responseVariableName": "createProject", // optional alias for stepId\n "expectedStatusCodes": [200, 201], // step fails if status is outside\n "description": "...", // optional UI label\n "validations": [ ... ] // assertions against the response\n }\n\n CREDENTIAL ROLES \u2014 `credentialId` is overloaded; two distinct uses exist:\n\n - Step-level `credentialId` (and a top-level/default credential on the\n sequence) is AUTH: it\'s what the api-sequence runner uses to\n authenticate each HTTP call. Editing this changes who is calling.\n\n - `credentialId` appearing INSIDE a step\'s `bodyTemplate` JSON string is\n DATA: it gets written into the request body and becomes part of the\n provisioned resource (e.g. the credential a created workflow logs in\n with). Editing this changes what the call creates.\n\n The two are independent. Swapping the auth credential won\'t change what\n the call provisions; swapping the body credential won\'t change auth.\n Mixing them up silently produces confusing test failures.\n\n `apiRequestId` references a row in the `api_requests` table. Discover IDs\n from the API catalog for the target property. If `urlTemplate`, `method`,\n or `bodyTemplate` are omitted, they are backfilled from the catalog entry\'s\n defaults at execution time.\n\n `bodyTemplate` is a JSON *string*, not a JSON object \u2014 templates and macros\n are interpolated into it as raw text before parsing.\n\nTEMPLATE SYNTAX\n\n Three ways to inject dynamic values. All three can appear in the same\n string (urlTemplate, bodyTemplate, headersTemplate values, or queryParams\n values).\n\n 1. STEP REFERENCES \u2014 outputs of prior steps.\n\n {{stepId.response.path.to.value}}\n\n The `.response.` segment is REQUIRED. This is the most common foot-gun:\n\n \u2713 {{createProject.response.data.id}}\n \u2717 {{createProject.data.id}} \u2014 missing .response., will warn at\n runtime and resolve to undefined\n \u2717 {{projectId}} \u2014 no step reference at all\n\n The executor logs a warning for malformed references but does NOT\n block execution \u2014 a malformed template resolves to an empty string and\n the request goes out anyway. Always run once and check the run log.\n\n 2. MACROS \u2014 dynamic data generated fresh per run.\n\n $UUID() Random UUID v4\n $NOW() ISO 8601 timestamp (UTC)\n $TODAY() YYYY-MM-DD\n $DATE(N) Date offset by N days\n $TIMESTAMP() Unix ms\n $RANDOM_INT(min,max) Random integer in range\n $RANDOM_STRING(N) Alphanumeric string of length N\n $EMAIL() test-<rand>@example-test.com\n $FIRST_NAME() Random first name\n $LAST_NAME() Random last name\n $PHONE() +1-555-...\n $SEQUENCE("name") Per-run auto-increment counter\n\n Each run resolves macros independently. $SEQUENCE counters persist\n across steps within a single run.\n\n 3. WORKFLOW VARIABLES \u2014 values injected by the workflow runtime.\n\n $var.<name>\n\n These come from two places:\n\n a) Upstream credential variables \u2014 pre-provisioned before the\n sequence runs. Typical names: `$var.credential.username`,\n `$var.credential.password`, `$var.credential.email`. If the\n workflow has a credential, USE these for the matching fields\n instead of `$EMAIL()` / `$RANDOM_STRING()` \u2014 the values must\n match the already-provisioned account.\n\n b) Upstream exports \u2014 values exported by earlier nodes in the same\n workflow (e.g. a seed step produced `$var.project_id`).\n\n Naming convention for exported variables: `resource_field` with\n underscores (e.g. `project_id`, `user_email`).\n\nEXPORTS\n\n After all steps finish, the sequence can register values from the final\n state into the workflow variable registry for downstream nodes:\n\n {\n "exports": [\n {\n "variableName": "project_id", // appears as $var.project_id\n "stepId": "createProject",\n "responsePath": "data.id",\n "source": "response", // "response" (default) | "request"\n "description": "..." // optional UI label\n }\n ]\n }\n\n Only export fields downstream nodes actually need. Use the\n `resource_field` naming convention.\n\nVALIDATIONS\n\n Per-step assertions against the response. Each validation:\n\n {\n "left": "createProject.response.data.status", // "stepId.path" form\n "operator": "equals", // see list\n "right": "active", // omit for is_set/is_not_set\n "description": "..."\n }\n\n Operators: equals, not_equals, gt, lt, gte, lte, contains, not_contains,\n is_set, is_not_set.\n\nWHAT IS VALIDATED AT COMMIT TIME\n\n Very little. The save endpoint accepts anything matching\n { steps: any[], description?, stopOnFailure?, exports? }. It does NOT\n verify that apiRequestId exists in the catalog, that stepIds are unique,\n that {{stepId.response.path}} references a real step, that exports\'\n stepId resolves, or that macro names are known.\n\n The only template sanity check is `validateStepTemplates()` in\n apps/api/src/workflows/execution/api-sequence-executor.ts, which runs\n at EXECUTE time, only flags `{{stepId.X}}` missing `.response.`, and only\n LOGS A WARNING \u2014 the sequence still runs. Always smoke-test the sequence\n before committing it to a workflow.\n\n CLI users: see `canary workflow guide cli-api-sequences` for command usage.\n\nEXAMPLE (3-step sequence, correct shape)\n\n {\n "description": "Create an item, fetch it, delete it",\n "stopOnFailure": true,\n "steps": [\n {\n "stepId": "create",\n "apiRequestId": "<POST /items id>",\n "credentialId": null,\n "method": "POST",\n "bodyTemplate": "{\\"name\\": \\"item-$RANDOM_STRING(6)\\"}",\n "expectedStatusCodes": [201]\n },\n {\n "stepId": "fetch",\n "apiRequestId": "<GET /items/:id id>",\n "credentialId": null,\n "method": "GET",\n "urlTemplate": "/items/{{create.response.id}}",\n "expectedStatusCodes": [200],\n "validations": [\n { "left": "fetch.response.name", "operator": "is_set" }\n ]\n },\n {\n "stepId": "remove",\n "apiRequestId": "<DELETE /items/:id id>",\n "credentialId": null,\n "method": "DELETE",\n "urlTemplate": "/items/{{create.response.id}}",\n "expectedStatusCodes": [200, 204]\n }\n ],\n "exports": [\n { "variableName": "item_id", "stepId": "create", "responsePath": "id" }\n ]\n }\n\nTIPS\n\n - Give every step a short, stable `stepId`. Later steps and exports\n reference it; the default `stepN` label is brittle to reordering.\n - Always include `.response.` in step references. The runtime won\'t\n error out, but values will silently resolve to empty strings.\n - Prefer `$EMAIL()`, `$UUID()`, `$RANDOM_STRING()` for fields that must\n be unique per run \u2014 unless the workflow has an upstream credential,\n in which case use `$var.credential.username` / `.password` / `.email`.\n - Set `expectedStatusCodes` on every step so unexpected 4xx/5xx fails\n the step instead of silently flowing into the next template.\n - `bodyTemplate` is a JSON STRING. Escape inner quotes.\n - `workflow api-sequence generate` can AI-draft a config from a prompt\n or a prior browser run; refine with `update`. The generated output is\n just JSON \u2014 review it before committing.\n';
2849
+
2850
+ // ../content/src/workflow-guide/cli-seeds.txt
2851
+ var cli_seeds_default = 'Use this section when you need to create or inspect seed workflows from the\nCanary CLI. The conceptual model lives in `seed-workflows`; this section is\nonly command orientation. Prefer each command\'s `--help` output for the\ncurrent flag list.\n\nCREATE A SIMPLE SEED SHELL\n\n For simple data outputs, create the seed shell with repeatable output\n declarations:\n\n canary workflow seed create \\\n --name "Create Test Project" \\\n --description "Provisions a test project with default settings" \\\n --output "project_id:Project ID:The created project\'s UUID" \\\n --output "project_name:Project Name"\n\n This creates a draft seed workflow and, by default, a paired teardown\n workflow linked to the seed.\n\nCREATE A COMPLEX SEED SHELL\n\n For credential outputs or larger configs, put the full seed definition in a\n JSON file and create from that file:\n\n canary workflow seed create --from-file seed.json\n\n Keep generated credential fields in credentialOutput, and keep exported data\n fields in outputs. Downstream workflows consume both through $var.{dataKey}.\n\nAFTER CREATION\n\n Add the seed body through the Flow Designer or workflow sync, test both the\n seed and teardown, then publish. If the seed can be API-backed, generate or\n attach an API sequence after the browser version is proven.\n\nREFERENCE\n\n - Seed command help: `canary workflow seed --help`\n - Workflow sync help: `canary workflow sync --help`\n';
2852
+
2853
+ // ../content/src/workflow-guide/cli-api-sequences.txt
2854
+ var cli_api_sequences_default = 'Use this section when you need to drive API sequence authoring from the\nCanary CLI. The conceptual model lives in `api-sequences`; this section is\nonly command orientation. Prefer each command\'s `--help` output for the\ncurrent flag list.\n\nCOMMON FLOW\n\n 1. Inspect the API catalog for candidate endpoints.\n\n canary api-catalog sources list\n canary api-catalog endpoints list --search <keyword>\n canary api-catalog endpoints get <apiRequestId>\n\n 2. Compose a sequence JSON file locally. Reference endpoints by\n apiRequestId and chain steps with {{stepId.response.path}}.\n\n 3. Create an API-only workflow from the sequence file.\n\n canary workflow create --api-only --name "smoke: create project" \\\n --sequence-from-file seq.json\n\n The command returns the workflow, sequence, and node IDs.\n\n 4. Execute immediately to surface template warnings and API failures.\n\n canary workflow api-sequence execute <workflowId>\n\n 5. Inspect the run and iterate on the sequence file.\n\n canary workflow runs list <workflowId> --side api\n canary workflow runs get <workflowId> <runId>\n canary workflow api-sequence update <workflowId> --from-file seq.json\n\nREFERENCE\n\n - Command help: `canary workflow api-sequence --help`\n - Workflow creation help: `canary workflow create --help`\n - API catalog help: `canary api-catalog --help`\n';
2855
+
2856
+ // ../content/src/workflow-guide/index.ts
2857
+ var WORKFLOW_GUIDE_CONCEPT_SECTIONS = [
2858
+ { title: "Workflow Overview", slug: "overview", content: overview_default },
2859
+ { title: "Node Types", slug: "node-types", content: node_types_default },
2860
+ { title: "Playbooks & Cache", slug: "playbook", content: playbook_default },
2861
+ { title: "Best Practices", slug: "best-practices", content: best_practices_default },
2862
+ { title: "Macros & Variables", slug: "macros", content: macros_default },
2863
+ { title: "Seed Workflows", slug: "seed-workflows", content: seed_workflows_default },
2864
+ { title: "Test Data Lifecycle", slug: "data-lifecycle", content: data_lifecycle_default },
2865
+ { title: "Assertions", slug: "assertions", content: assertions_default },
2866
+ { title: "API Sequences", slug: "api-sequences", content: api_sequences_default }
2867
+ ];
2868
+ var WORKFLOW_GUIDE_CLI_SECTIONS = [
2869
+ { title: "CLI: Seed Workflows", slug: "cli-seeds", content: cli_seeds_default },
2870
+ { title: "CLI: API Sequences", slug: "cli-api-sequences", content: cli_api_sequences_default }
2871
+ ];
2872
+ var WORKFLOW_GUIDE_SECTIONS = [
2873
+ ...WORKFLOW_GUIDE_CONCEPT_SECTIONS,
2874
+ ...WORKFLOW_GUIDE_CLI_SECTIONS
2875
+ ];
2876
+
2877
+ // src/workflow.ts
2878
+ var WORKFLOW_STATUS_DESCRIPTIONS = {
2879
+ draft: "editable, not run by suites",
2880
+ published: "active, included in regression runs"
2881
+ };
2882
+ var formatWorkflowStatusList = () => USER_SETTABLE_WORKFLOW_STATUSES.map(
2883
+ (s) => `${s} (${WORKFLOW_STATUS_DESCRIPTIONS[s]})`
2884
+ ).join(", ");
2885
+ function truncate(text, max) {
2886
+ if (text.length <= max) return text;
2887
+ return text.slice(0, max - 1) + "\u2026";
2888
+ }
2889
+ function getNodeTitle(node) {
2890
+ const c = node.configJson ?? {};
2891
+ switch (node.nodeType) {
2892
+ case "login":
2893
+ return c.credentialName || NODE_TYPE_LABEL.login;
2894
+ case "navigate":
2895
+ return c.pageTitle || truncate(c.pageUrl || "Navigate", 40);
2896
+ case "action":
2897
+ return c.customActionTitle || truncate(c.actionDescription || "Action", 50);
2898
+ case "assertion":
2899
+ return truncate(c.condition || "Assertion", 50);
2900
+ case "setup":
2901
+ return c.setupFlowName || "Setup";
2902
+ case "seed":
2903
+ return c.seedWorkflowName || "Seed";
2904
+ case "wait":
2905
+ return "Wait";
2906
+ case "condition":
2907
+ return truncate(c.conditionDescription || "Condition", 50);
2908
+ case "end":
2909
+ return "End";
2910
+ case "api_sequence":
2911
+ return "API Sequence";
2912
+ default:
2913
+ return node.nodeType;
2914
+ }
2915
+ }
2916
+ function getCleanConfig(node) {
2917
+ const c = node.configJson ?? {};
2918
+ switch (node.nodeType) {
2919
+ case "login":
2920
+ return pick(c, ["credentialName", "performLogin"]);
2921
+ case "navigate":
2922
+ return {
2923
+ ...pick(c, ["pageUrl", "pageTitle"]),
2924
+ ...c.openInNewTab ? { openInNewTab: true } : {}
2925
+ };
2926
+ case "action":
2927
+ return pick(c, [
2928
+ "actionDescription",
2929
+ "customActionInstructions",
2930
+ "playbookSteps",
2931
+ "inputValues",
2932
+ "declaredOutputs",
2933
+ "capturedVariableKeys",
2934
+ "dataRequirements",
2935
+ "alwaysUseAgent"
2936
+ ]);
2937
+ case "assertion":
2938
+ return pick(c, ["condition", "expectedOutcome", "strict", "alwaysUseAgent"]);
2939
+ case "setup":
2940
+ return pick(c, ["setupFlowId", "setupFlowName"]);
2941
+ case "seed":
2942
+ return pick(c, ["seedWorkflowId", "seedWorkflowName", "snapshotVersion"]);
2943
+ case "end":
2944
+ return pick(c, ["outcome"]);
2945
+ case "wait":
2946
+ case "condition":
2947
+ case "api_sequence":
2948
+ return c;
2949
+ default:
2950
+ return c;
2951
+ }
2952
+ }
2953
+ function pick(obj, keys) {
2954
+ const result = {};
2955
+ for (const key of keys) {
2956
+ if (obj[key] !== void 0 && obj[key] !== null) {
2957
+ result[key] = obj[key];
2958
+ }
2959
+ }
2960
+ return result;
2961
+ }
2962
+ function formatWorkflowMarkdown(workflow) {
2963
+ const seedOutputs = workflow.seedOutputsJson ?? workflow.seedOutputs;
2964
+ return workflowToSpec({ ...workflow, seedOutputs });
2965
+ }
2966
+ function formatWorkflowJson(workflow) {
2967
+ const sorted = [...workflow.nodes].sort((a, b) => a.orderIndex - b.orderIndex);
2968
+ const seedOutputs = workflow.seedOutputsJson;
2969
+ return {
2970
+ workflow: {
2971
+ id: workflow.id,
2972
+ name: workflow.name,
2973
+ description: workflow.description,
2974
+ status: workflow.status,
2975
+ flowType: workflow.flowType,
2976
+ ...seedOutputs && seedOutputs.length > 0 ? { seedOutputs } : {}
2977
+ },
2978
+ steps: sorted.map((node, i) => ({
2979
+ index: i + 1,
2980
+ nodeId: node.id,
2981
+ nodeType: node.nodeType,
2982
+ title: getNodeTitle(node),
2983
+ config: getCleanConfig(node)
2984
+ }))
2985
+ };
2986
+ }
2987
+ async function handleList(argv, apiUrl, token) {
2988
+ const jsonOutput = hasFlag(argv, "--json");
2989
+ const params = new URLSearchParams();
2990
+ const search = getArgValue(argv, "--search");
2991
+ const flowType = getArgValue(argv, "--flow-type");
2992
+ const status = getArgValue(argv, "--status");
2993
+ const page = getArgValue(argv, "--page");
2994
+ const pageSize = getArgValue(argv, "--page-size");
2995
+ if (search) params.set("search", search);
2996
+ if (flowType) params.set("flowType", flowType);
2997
+ if (status) params.set("status", status);
2998
+ if (page) params.set("page", page);
2999
+ if (pageSize) params.set("pageSize", pageSize);
3000
+ const qs = params.toString();
3001
+ const path = `/workflows${qs ? `?${qs}` : ""}`;
3002
+ const result = await apiRequest(apiUrl, token, "GET", path);
3003
+ if (!result.ok) {
3004
+ console.error(`Error: ${result.error}`);
3005
+ process9.exit(1);
3006
+ }
3007
+ if (jsonOutput) {
3008
+ console.log(JSON.stringify({ data: result.data, pagination: result.pagination }, null, 2));
3009
+ return;
3010
+ }
3011
+ const { data, pagination } = result;
3012
+ console.log(
3013
+ `Workflows: ${pagination.totalItems} total (page ${pagination.page}/${pagination.totalPages})
3014
+ `
3015
+ );
3016
+ if (data.length === 0) {
3017
+ console.log("No workflows found.");
3018
+ return;
3019
+ }
3020
+ const idW = 36;
3021
+ const nameW = 30;
3022
+ const statusW = 12;
3023
+ const typeW = 10;
3024
+ const header = [
3025
+ "ID".padEnd(idW),
3026
+ "Name".padEnd(nameW),
3027
+ "Status".padEnd(statusW),
3028
+ "Type".padEnd(typeW)
3029
+ ].join(" ");
3030
+ console.log(header);
3031
+ for (const w of data) {
3032
+ const row = [
3033
+ w.id.padEnd(idW),
3034
+ truncate(w.name, nameW).padEnd(nameW),
3035
+ w.status.padEnd(statusW),
3036
+ w.flowType.padEnd(typeW)
3037
+ ].join(" ");
3038
+ console.log(row);
3039
+ }
3040
+ }
3041
+ async function handleGet2(argv, apiUrl, token) {
3042
+ const workflowId = argv[0];
3043
+ if (!workflowId || workflowId.startsWith("--")) {
3044
+ console.error("Error: Missing workflow ID.");
3045
+ console.error("Usage: canary workflow get <workflowId>");
3046
+ process9.exit(1);
3047
+ }
3048
+ const _jsonOutput = hasFlag(argv, "--json") || getArgValue(argv, "--format") === "json";
3049
+ const markdownOutput = hasFlag(argv, "--markdown") || getArgValue(argv, "--format") === "markdown" || getArgValue(argv, "--format") === "spec";
3050
+ const outputFile = getArgValue(argv, "--output");
3051
+ const apiRes = await apiFetch(`${apiUrl}/workflows/${workflowId}`, {
3052
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
3053
+ token
3054
+ });
3055
+ if (!apiRes.ok) exitOnFailure(apiRes.failure);
3056
+ const result = await apiRes.response.json();
3057
+ if (result.error === "WORKFLOW_NOT_FOUND") {
3058
+ console.error("Error: Workflow not found.");
3059
+ process9.exit(1);
3060
+ }
3061
+ if (result.error === "WORKFLOW_WRONG_ORG") {
3062
+ console.error(
3063
+ `Error: Workflow belongs to another org: ${result.targetOrgName ?? result.targetOrgId}`
3064
+ );
3065
+ process9.exit(1);
3066
+ }
3067
+ if (!result.workflow) {
3068
+ console.error(`Error: ${result.error ?? "Unexpected response"}`);
3069
+ process9.exit(1);
3070
+ }
3071
+ const workflow = result.workflow;
3072
+ let output;
3073
+ if (markdownOutput) {
3074
+ output = formatWorkflowMarkdown(workflow);
3075
+ } else {
3076
+ output = JSON.stringify(formatWorkflowJson(workflow), null, 2);
3077
+ }
3078
+ if (outputFile) {
3079
+ try {
3080
+ await fs5.writeFile(outputFile, output, "utf-8");
3081
+ console.log(`Written to ${outputFile}`);
3082
+ } catch (err) {
3083
+ console.error(
3084
+ `Error writing to ${outputFile}: ${err instanceof Error ? err.message : String(err)}`
3085
+ );
3086
+ process9.exit(1);
3087
+ }
3088
+ return;
3089
+ }
3090
+ console.log(output);
3091
+ if (!markdownOutput) {
3092
+ const appUrl = resolveAppUrl(apiUrl);
3093
+ console.log(`
3094
+ URL: ${appUrl}/flows/${workflowId}`);
3095
+ }
3096
+ }
3097
+ async function handlePatch(argv, apiUrl, token) {
3098
+ const workflowId = argv.find((a) => !a.startsWith("-"));
3099
+ if (!workflowId) {
3100
+ console.error("Error: Missing workflow ID.");
3101
+ console.error("Usage: canary workflow patch <workflowId> [--name ...] [--description ...] [--status ...] [--flow-type ...]");
3102
+ process9.exit(1);
3103
+ }
3104
+ const name = getArgValue(argv, "--name");
3105
+ const description = getArgValue(argv, "--description");
3106
+ const status = getArgValue(argv, "--status");
3107
+ const flowType = getArgValue(argv, "--flow-type");
3108
+ const seedOutputsJsonRaw = getArgValue(argv, "--seed-outputs-json");
3109
+ const seedOutputsFile = getArgValue(argv, "--seed-outputs-file");
3110
+ const jsonOutput = hasFlag(argv, "--json");
3111
+ const hasTagFlag = getArgValue(argv, "--tags") !== void 0 || getArgValue(argv, "--tags-add") !== void 0 || getArgValue(argv, "--tags-remove") !== void 0 || getArgValue(argv, "--tag-id") !== void 0;
3112
+ if (!name && !description && !status && !flowType && !seedOutputsJsonRaw && !seedOutputsFile && !hasTagFlag) {
3113
+ console.error(
3114
+ "Error: No fields to update. Provide at least one of: --name, --description, --status, --flow-type, --seed-outputs-json, --seed-outputs-file, --tags, --tags-add, --tags-remove, --tag-id"
3115
+ );
3116
+ process9.exit(1);
3117
+ }
3118
+ if (status && !USER_SETTABLE_WORKFLOW_STATUSES.includes(status)) {
3119
+ console.error(`Error: Invalid status "${status}". Valid: ${formatWorkflowStatusList()}`);
3120
+ process9.exit(1);
3121
+ }
3122
+ const validFlowTypes = ["standard", "setup", "seed", "teardown"];
3123
+ if (flowType && !validFlowTypes.includes(flowType)) {
3124
+ console.error(`Error: Invalid flow type "${flowType}". Must be one of: ${validFlowTypes.join(", ")}`);
3125
+ process9.exit(1);
3126
+ }
3127
+ const body = {};
3128
+ if (name) body.name = name;
3129
+ if (description) body.description = description;
3130
+ if (status) body.status = status;
3131
+ if (flowType) body.flowType = flowType;
3132
+ if (seedOutputsJsonRaw || seedOutputsFile) {
3133
+ let raw;
3134
+ if (seedOutputsFile) {
3135
+ try {
3136
+ raw = await fs5.readFile(seedOutputsFile, "utf-8");
3137
+ } catch (err) {
3138
+ console.error(
3139
+ `Error reading ${seedOutputsFile}: ${err instanceof Error ? err.message : String(err)}`
3140
+ );
3141
+ process9.exit(1);
3142
+ }
3143
+ } else {
3144
+ raw = seedOutputsJsonRaw;
3145
+ }
3146
+ try {
3147
+ const parsed = JSON.parse(raw);
3148
+ if (!Array.isArray(parsed)) {
3149
+ throw new Error("seedOutputsJson must be a JSON array");
3150
+ }
3151
+ body.seedOutputsJson = parsed;
3152
+ } catch (err) {
3153
+ console.error(
3154
+ `Error: Invalid --seed-outputs-json: ${err instanceof Error ? err.message : String(err)}`
3155
+ );
3156
+ process9.exit(1);
3157
+ }
3158
+ }
3159
+ let tagResult = null;
3160
+ if (Object.keys(body).length > 0) {
3161
+ const result = await apiRequest(
3162
+ apiUrl,
3163
+ token,
3164
+ "PUT",
3165
+ `/workflows/${workflowId}`,
3166
+ body
3167
+ );
3168
+ if (!result.ok) {
3169
+ if (jsonOutput) {
3170
+ console.log(JSON.stringify({ ok: false, error: result.error ?? "Unknown error" }, null, 2));
3171
+ } else {
3172
+ console.error(`Error: ${result.error ?? "Unknown error"}`);
3173
+ }
3174
+ process9.exit(1);
3175
+ }
3176
+ }
3177
+ if (hasTagFlag) {
3178
+ try {
3179
+ tagResult = await applyWorkflowTagFlags({ apiUrl, token, argv, workflowId });
3180
+ } catch (err) {
3181
+ if (jsonOutput) {
3182
+ console.log(
3183
+ JSON.stringify(
3184
+ { ok: false, error: err instanceof Error ? err.message : String(err) },
3185
+ null,
3186
+ 2
3187
+ )
3188
+ );
3189
+ } else {
3190
+ console.error(`Error applying tags:
3191
+ ${err instanceof Error ? err.message : String(err)}`);
3192
+ }
3193
+ process9.exit(1);
3194
+ }
3195
+ }
3196
+ if (jsonOutput) {
3197
+ console.log(
3198
+ JSON.stringify(
3199
+ { ok: true, workflowId, updated: body, tags: tagResult?.tagIds ?? null },
3200
+ null,
3201
+ 2
3202
+ )
3203
+ );
3204
+ return;
3205
+ }
3206
+ const updatedKeys = Object.keys(body);
3207
+ if (updatedKeys.length > 0) {
3208
+ console.log(`Workflow ${workflowId} updated (${updatedKeys.join(", ")}).`);
3209
+ }
3210
+ if (tagResult) {
3211
+ console.log(`Tags: ${tagResult.tagIds.length} assigned.`);
3212
+ }
3213
+ }
3214
+ async function handleArchive(argv, apiUrl, token) {
3215
+ const workflowId = argv.find((a) => !a.startsWith("-"));
3216
+ if (!workflowId) {
3217
+ console.error("Error: Missing workflow ID.");
3218
+ console.error("Usage: canary workflow archive <workflowId> [--yes] [--json]");
3219
+ process9.exit(1);
3220
+ }
3221
+ const jsonOutput = hasFlag(argv, "--json");
3222
+ const skipConfirm = hasFlag(argv, "--yes", "-y");
3223
+ if (!skipConfirm) {
3224
+ if (!process9.stdin.isTTY) {
3225
+ console.error(
3226
+ "Error: Archiving requires confirmation. Pass --yes (or -y) for non-interactive use."
3227
+ );
3228
+ process9.exit(1);
3229
+ }
3230
+ process9.stdout.write(
3231
+ `Archive workflow ${workflowId}? It will be hidden from lists and skipped by test suites. [y/N] `
3232
+ );
3233
+ const answer = await new Promise((resolve) => {
3234
+ process9.stdin.setEncoding("utf-8");
3235
+ process9.stdin.once("data", (chunk) => resolve(String(chunk).trim().toLowerCase()));
3236
+ });
3237
+ if (answer !== "y" && answer !== "yes") {
3238
+ console.error("Aborted.");
3239
+ process9.exit(1);
3240
+ }
3241
+ }
3242
+ const result = await apiRequest(
3243
+ apiUrl,
3244
+ token,
3245
+ "DELETE",
3246
+ `/workflows/${workflowId}`
3247
+ );
3248
+ if (result.error) {
3249
+ if (jsonOutput) {
3250
+ console.log(JSON.stringify(result, null, 2));
3251
+ } else {
3252
+ renderWorkflowUserError(result);
3253
+ }
3254
+ process9.exit(1);
3255
+ }
3256
+ if (jsonOutput) {
3257
+ console.log(JSON.stringify(result, null, 2));
3258
+ } else {
3259
+ console.log(`Archived workflow ${workflowId}. Use 'canary workflow unarchive ${workflowId}' to restore.`);
3260
+ }
3261
+ }
3262
+ async function handleUnarchive(argv, apiUrl, token) {
3263
+ const workflowId = argv.find((a) => !a.startsWith("-"));
3264
+ if (!workflowId) {
3265
+ console.error("Error: Missing workflow ID.");
3266
+ console.error("Usage: canary workflow unarchive <workflowId> [--json]");
3267
+ process9.exit(1);
3268
+ }
3269
+ const jsonOutput = hasFlag(argv, "--json");
3270
+ const result = await apiRequest(
3271
+ apiUrl,
3272
+ token,
3273
+ "POST",
3274
+ `/workflows/${workflowId}/unarchive`,
3275
+ {}
3276
+ );
3277
+ if (result.error) {
3278
+ if (jsonOutput) {
3279
+ console.log(JSON.stringify(result, null, 2));
3280
+ } else {
3281
+ renderWorkflowUserError(result);
3282
+ }
3283
+ process9.exit(1);
3284
+ }
3285
+ if (jsonOutput) {
3286
+ console.log(JSON.stringify(result, null, 2));
3287
+ } else {
3288
+ console.log(`Restored workflow ${workflowId}.`);
3289
+ }
3290
+ }
3291
+ async function handlePublish(argv, apiUrl, token) {
3292
+ const workflowId = argv.find((a) => !a.startsWith("-"));
3293
+ if (!workflowId) {
3294
+ console.error("Error: Missing workflow ID.");
3295
+ console.error("Usage: canary workflow publish <workflowId> [--force] [--json]");
3296
+ process9.exit(1);
3297
+ }
3298
+ const force = hasFlag(argv, "--force");
3299
+ const jsonOutput = hasFlag(argv, "--json");
3300
+ const result = await apiRequest(
3301
+ apiUrl,
3302
+ token,
3303
+ "POST",
3304
+ `/workflows/${workflowId}/publish`,
3305
+ force ? { force: true } : {}
3306
+ );
3307
+ if (result.error) {
3308
+ if (jsonOutput) {
3309
+ console.log(JSON.stringify(result, null, 2));
3310
+ } else {
3311
+ const hint = result.code === "FLOW_NOT_READY_TO_PUBLISH" ? "\n Trigger a successful run first (canary workflow run <id>), or pass --force." : "";
3312
+ console.error(`Error: ${result.error}${hint}`);
3313
+ }
3314
+ process9.exit(1);
3315
+ }
3316
+ const version = result.workflow?.snapshotVersion;
3317
+ if (jsonOutput) {
3318
+ console.log(
3319
+ JSON.stringify({ ok: true, workflowId, workflow: result.workflow ?? null }, null, 2)
3320
+ );
3321
+ return;
3322
+ }
3323
+ console.log(
3324
+ `Workflow ${workflowId} published${version !== void 0 ? ` as snapshot v${version}` : ""}.`
3325
+ );
3326
+ }
3327
+ function getAllArgValues2(argv, flag) {
3328
+ const values = [];
3329
+ for (let i = 0; i < argv.length; i++) {
3330
+ if (argv[i] === flag && i + 1 < argv.length) {
3331
+ values.push(argv[i + 1]);
3332
+ i++;
3333
+ }
3334
+ }
3335
+ return values;
3336
+ }
3337
+ async function handleNodePatch(argv, apiUrl, token) {
3338
+ const positional = argv.filter((a) => !a.startsWith("-"));
3339
+ const workflowId = positional[0];
3340
+ const nodeId = positional[1];
3341
+ if (!workflowId || !nodeId) {
3342
+ console.error("Error: Missing workflow ID and/or node ID.");
3343
+ console.error(
3344
+ `Usage: canary workflow node patch <workflowId> <nodeId> --set-input key=value [--config '{"..."}'] [--json]`
3345
+ );
3346
+ process9.exit(1);
3347
+ }
3348
+ const configArg = getArgValue(argv, "--config");
3349
+ const setInputArgs = getAllArgValues2(argv, "--set-input");
3350
+ const jsonOutput = hasFlag(argv, "--json");
3351
+ if (!configArg && setInputArgs.length === 0) {
3352
+ console.error(`Error: No changes specified. Provide --set-input key=value and/or --config '{"..."}'`);
3353
+ process9.exit(1);
3354
+ }
3355
+ let configJson = {};
3356
+ if (configArg) {
3357
+ try {
3358
+ configJson = JSON.parse(configArg);
3359
+ } catch (e) {
3360
+ console.error(`Error: Invalid JSON in --config: ${e instanceof Error ? e.message : String(e)}`);
3361
+ process9.exit(1);
3362
+ }
3363
+ }
3364
+ if (setInputArgs.length > 0) {
3365
+ const inputValues = configJson.inputValues ?? {};
3366
+ for (const arg of setInputArgs) {
3367
+ const eqIdx = arg.indexOf("=");
3368
+ if (eqIdx === -1) {
3369
+ console.error(`Error: --set-input must be key=value, got: "${arg}"`);
3370
+ process9.exit(1);
3371
+ }
3372
+ inputValues[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
3373
+ }
3374
+ configJson.inputValues = inputValues;
3375
+ }
3376
+ const getRes = await fetchOrExit(`${apiUrl}/workflows/${workflowId}`, {
3377
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
3378
+ token
3379
+ });
3380
+ const getResult = await getRes.json();
3381
+ if (!getResult.workflow) {
3382
+ console.error(`Error: Workflow not found.`);
3383
+ process9.exit(1);
3384
+ }
3385
+ const existingNode = getResult.workflow.nodes.find((n) => n.id === nodeId);
3386
+ if (!existingNode) {
3387
+ console.error(`Error: Node ${nodeId} not found in workflow ${workflowId}.`);
3388
+ console.error("\nAvailable nodes:");
3389
+ for (const n of getResult.workflow.nodes.sort((a, b) => a.orderIndex - b.orderIndex)) {
3390
+ console.error(` ${n.id} [${n.nodeType}] ${getNodeTitle(n)}`);
3391
+ }
3392
+ process9.exit(1);
3393
+ }
3394
+ const patchRes = await fetchOrExit(`${apiUrl}/workflows/${workflowId}/nodes/${nodeId}`, {
3395
+ method: "PUT",
3396
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
3397
+ token,
3398
+ body: JSON.stringify({ configJson })
3399
+ });
3400
+ if (!patchRes.ok) {
3401
+ const errBody = await patchRes.text();
3402
+ if (jsonOutput) {
3403
+ console.log(errBody || JSON.stringify({ ok: false, status: patchRes.status }, null, 2));
3404
+ } else {
3405
+ let rendered = null;
3406
+ try {
3407
+ const parsed = JSON.parse(errBody);
3408
+ rendered = renderWorkflowUserError(parsed);
3409
+ } catch {
3410
+ rendered = null;
3411
+ }
3412
+ console.error(rendered ?? `Error (${patchRes.status}): ${errBody.slice(0, 500)}`);
3413
+ }
3414
+ process9.exit(1);
3415
+ }
3416
+ const patchResult = await patchRes.json();
3417
+ if (jsonOutput) {
3418
+ console.log(JSON.stringify({ ok: true, node: patchResult.node }, null, 2));
3419
+ return;
3420
+ }
3421
+ const title = getNodeTitle(existingNode);
3422
+ console.log(`Updated node [${existingNode.nodeType}] "${title}" (${nodeId}):
3423
+ `);
3424
+ const oldConfig = existingNode.configJson ?? {};
3425
+ for (const [key, newVal] of Object.entries(configJson)) {
3426
+ if (typeof newVal === "object" && newVal !== null && typeof oldConfig[key] === "object" && oldConfig[key] !== null) {
3427
+ const oldObj = oldConfig[key];
3428
+ const newObj = newVal;
3429
+ for (const [subKey, subVal] of Object.entries(newObj)) {
3430
+ const oldVal = oldObj[subKey];
3431
+ if (oldVal !== subVal) {
3432
+ console.log(` ${key}.${subKey}: ${JSON.stringify(oldVal ?? null)} -> ${JSON.stringify(subVal)}`);
3433
+ }
3434
+ }
3435
+ } else {
3436
+ const oldVal = oldConfig[key];
3437
+ if (oldVal !== newVal) {
3438
+ console.log(` ${key}: ${JSON.stringify(oldVal ?? null)} -> ${JSON.stringify(newVal)}`);
3439
+ }
3440
+ }
3441
+ }
3442
+ }
3443
+ function printWorkflowHelp() {
3444
+ console.log(
3445
+ [
3446
+ "Usage: canary workflow <sub-command> [options]",
3447
+ "",
3448
+ ' Tip: Run "canary workflow guide" for the full authoring reference',
3449
+ " (node types, playbooks, macros, seeds, assertions, best practices)",
3450
+ "",
3451
+ "Sub-commands:",
3452
+ " guide [section] [--json] Authoring guide (node-types, macros, etc.)",
3453
+ " list [options] List workflows",
3454
+ " get <workflowId> [options] Get workflow definition with steps",
3455
+ " create [options] Create workflow from JSON",
3456
+ " sync <workflowId> [options] Replace all nodes (same format as create)",
3457
+ " patch <workflowId> [options] Update workflow properties",
3458
+ " publish <workflowId> [--force] [--json] Promote the current draft to a new published snapshot",
3459
+ " archive <workflowId> [--yes] [--json] Soft-delete: hide from lists, skip in test suites (reversible)",
3460
+ " unarchive <workflowId> [--json] Restore a previously archived workflow",
3461
+ " run <workflowId> [options] Trigger a workflow execution",
3462
+ " api-sequence <verb> <workflowId> Manage the API sequence attached to a workflow",
3463
+ " generate-api-sequence <workflowId> [opts] (deprecated alias for `api-sequence generate`)",
3464
+ " seed <sub-command> Create seed workflows",
3465
+ " node <sub-command> Manage individual nodes",
3466
+ " runs <sub-command> Query workflow run results",
3467
+ " debug <runId|sub-command> Debug a run (overview/steps/cache/thoughts/logs/trace)",
3468
+ "",
3469
+ "List options:",
3470
+ " --search <query> Search by name",
3471
+ " --flow-type <type> Filter: standard, setup, seed, teardown",
3472
+ " --status <status> Filter: draft, published",
3473
+ " --page <n> Page number (default: 1)",
3474
+ " --page-size <n> Page size (default: 25)",
3475
+ " --json Output raw JSON",
3476
+ "",
3477
+ "Get options:",
3478
+ " --format json|markdown|spec Output format (default: json)",
3479
+ " --json Shorthand for --format json",
3480
+ " --markdown Shorthand for editable .canary.md spec output",
3481
+ " --output <file> Write output to file",
3482
+ "",
3483
+ "Create options:",
3484
+ " --from-stdin Read workflow JSON from stdin (default)",
3485
+ " --from-file <path> Read workflow JSON from file",
3486
+ " --api-only Create an API-only workflow with a backing ApiSequence",
3487
+ " Requires: --name <name>",
3488
+ " Optional: --credential <id>, --guidance <text>,",
3489
+ " --sequence-from-file <path> or --sequence-from-stdin",
3490
+ " Input shape: { steps, description?, stopOnFailure?, exports? }",
3491
+ " Returns: { workflowId, apiSequenceId, nodeId }",
3492
+ " --run-and-publish Run test first, publish on success",
3493
+ " --publish-timeout <sec> Timeout waiting for the test to finish (default: 900)",
3494
+ " --property <name|id> Property for run-and-publish",
3495
+ " --environment <name|id> Environment for run-and-publish",
3496
+ " --tags <names> Comma-separated tag names to assign (names, not IDs)",
3497
+ " --tag-id <ids> Comma-separated raw tag IDs (escape hatch)",
3498
+ "",
3499
+ " Input: { name, nodes: [{ nodeType, configJson }] }",
3500
+ ' Run "canary workflow guide node-types" for available node types.',
3501
+ ' Run "canary workflow guide macros" for dynamic test data ($EMAIL, $RANDOM_STRING, etc.)',
3502
+ "",
3503
+ "Sync options:",
3504
+ " --from-file <path> Read nodes JSON from file",
3505
+ " --from-stdin Read nodes JSON from stdin",
3506
+ " --no-snapshot Skip creating a draft snapshot before sync",
3507
+ " --propose Create a proposal instead of applying directly",
3508
+ " --title <title> Proposal title (with --propose)",
3509
+ " --description <desc> Proposal description (with --propose)",
3510
+ " --json Output raw JSON",
3511
+ "",
3512
+ " Sync accepts the same minimal node format as create.",
3513
+ " Edges, IDs, positions, and ordering are auto-generated when omitted.",
3514
+ " Input: { nodes: [{ nodeType, configJson }] }",
3515
+ "",
3516
+ " When using --propose on an existing workflow, run `canary workflow get",
3517
+ " <id>` first and reuse each node's existing `id` for steps you intend",
3518
+ " to keep. Only omit `id` for genuinely new steps. Without ID reuse the",
3519
+ " proposal will diff as a full remove-everything/add-everything rewrite.",
3520
+ "",
3521
+ "Patch options:",
3522
+ " --name <name> Update workflow name",
3523
+ " --description <desc> Update description",
3524
+ " --status <status> Set status. Valid:",
3525
+ " draft \u2014 editable, not run by suites",
3526
+ " published \u2014 active, included in regression runs",
3527
+ " (reliability is tracked automatically by health)",
3528
+ " --flow-type <type> Set type: standard, setup, seed, teardown",
3529
+ " --seed-outputs-json <json> Replace seedOutputsJson (array) \u2014 seed flows only",
3530
+ " --seed-outputs-file <path> Replace seedOutputsJson from a JSON file",
3531
+ " --tags <names> Replace tags with these names (comma-separated)",
3532
+ " --tags-add <names> Add these tag names to the existing set",
3533
+ " --tags-remove <names> Remove these tag names from the existing set",
3534
+ " --tag-id <ids> Raw tag IDs (comma-separated)",
3535
+ " --json Output raw JSON",
3536
+ "",
3537
+ "Publish options:",
3538
+ " --force Publish without requiring a successful run (skips the",
3539
+ " ready-to-publish gate; the new snapshot has no linked run)",
3540
+ " --json Output raw JSON",
3541
+ "",
3542
+ " Publish snapshots the current workflow definition and promotes it",
3543
+ " into the suite. Without --force, the workflow must have a passing",
3544
+ " run (ready_to_publish=true). Different from --status=published which",
3545
+ " only sets a column and does NOT create a new snapshot.",
3546
+ "",
3547
+ "Run options:",
3548
+ " --stream Follow execution until completion",
3549
+ " --json Output raw JSON",
3550
+ "",
3551
+ "API Sequence sub-commands:",
3552
+ " api-sequence get <wf> Show config + steps",
3553
+ " api-sequence update <wf> --from-file <f> Replace config",
3554
+ " api-sequence execute <wf> Run the sequence",
3555
+ " api-sequence delete <wf> Delete the sequence",
3556
+ " api-sequence set-api-only <wf> [--off] Toggle workflow apiOnly flag",
3557
+ " api-sequence generate <wf> [--prompt ...] AI-generate a config (streams)",
3558
+ "",
3559
+ ' Run "canary workflow guide api-sequences" for step shape and template syntax.',
3560
+ "",
3561
+ "Seed sub-commands:",
3562
+ " seed create [options] Create a seed workflow with outputs",
3563
+ "",
3564
+ "Seed create options:",
3565
+ " --name <name> Seed workflow name (required unless JSON input)",
3566
+ " --description <desc> Description",
3567
+ " --output <key:name[:desc]> Declare an output (repeatable)",
3568
+ " --outputs-json <json> Full outputs array as inline JSON",
3569
+ " --from-stdin Read full config from stdin",
3570
+ " --from-file <path> Read full config from file",
3571
+ " --no-teardown Skip creating paired teardown workflow",
3572
+ " --status <status> Initial status (default: draft)",
3573
+ " --json Output raw JSON",
3574
+ "",
3575
+ "Node sub-commands:",
3576
+ " node patch <workflowId> <nodeId> [options] Update a node's config",
3577
+ "",
3578
+ "Node patch options:",
3579
+ " --set-input key=value Set an inputValue (repeatable)",
3580
+ ` --config '{"..."}' Merge arbitrary configJson (partial update)`,
3581
+ " --json Output raw JSON",
3582
+ "",
3583
+ "Runs sub-commands:",
3584
+ " runs list <workflowId> List runs for a workflow",
3585
+ " runs get <workflowId> <runId> Get run detail with steps",
3586
+ " runs diagnostics <workflowId> <runId> Get failure diagnostic",
3587
+ "",
3588
+ "Cache sub-commands:",
3589
+ " cache status <workflowId> Show per-node cache version, health, recent failures",
3590
+ " cache clear <workflowId> [--node <id>] [--below-version <n>] [--stale-only] [--mode all]",
3591
+ " Targeted cache invalidation (rows kept, isValid=false)",
3592
+ " cache clear <workflowId> --mode hard --yes DESTRUCTIVE: delete cache rows, strip playbookSteps,",
3593
+ ' null lastRunId. Same as the UI "Reset flow" button',
3594
+ " cache optimize <workflowId> Enqueue a shadow-cache-writer heal run",
3595
+ "",
3596
+ "Common options:",
3597
+ " --env <env> Target environment (prod, dev, local)",
3598
+ " --api-url <url> API URL override",
3599
+ " --token <key> API token override"
3600
+ ].join("\n")
3601
+ );
3602
+ }
3603
+ async function runWorkflow(argv) {
3604
+ const [subCommand, ...rest] = argv;
3605
+ if (!subCommand || subCommand === "help") {
3606
+ printWorkflowHelp();
3607
+ return;
3608
+ }
3609
+ if (subCommand === "debug" && hasFlag(rest, "--help", "-h")) {
3610
+ const { handleWorkflowDebug } = await import("./workflow-debug-BGD4UIXU.js");
3611
+ await handleWorkflowDebug(rest, "", "");
3612
+ return;
3613
+ }
3614
+ if (hasFlag(argv, "--help", "-h")) {
3615
+ printWorkflowHelp();
3616
+ return;
3617
+ }
3618
+ if (subCommand === "guide") {
3619
+ const sectionSlug = rest.find((a) => !a.startsWith("-"));
3620
+ const jsonOutput = hasFlag(rest, "--json");
3621
+ printGuide(WORKFLOW_GUIDE_SECTIONS, sectionSlug, jsonOutput);
3622
+ return;
3623
+ }
3624
+ const { apiUrl, token } = await resolveConfig(argv);
3625
+ switch (subCommand) {
3626
+ case "list":
3627
+ await handleList(rest, apiUrl, token);
3628
+ break;
3629
+ case "get":
3630
+ await handleGet2(rest, apiUrl, token);
3631
+ break;
3632
+ case "create":
3633
+ await handleWorkflowCreate(rest, apiUrl, token);
3634
+ break;
3635
+ case "patch":
3636
+ await handlePatch(rest, apiUrl, token);
3637
+ break;
3638
+ case "publish":
3639
+ await handlePublish(rest, apiUrl, token);
3640
+ break;
3641
+ case "archive":
3642
+ await handleArchive(rest, apiUrl, token);
3643
+ break;
3644
+ case "unarchive":
3645
+ await handleUnarchive(rest, apiUrl, token);
3646
+ break;
3647
+ case "run":
3648
+ await handleWorkflowRun(rest, apiUrl, token);
3649
+ break;
3650
+ case "runs":
3651
+ await runWorkflowRuns(rest, apiUrl, token);
3652
+ break;
3653
+ case "node": {
3654
+ const [nodeSub, ...nodeRest] = rest;
3655
+ if (nodeSub === "patch") {
3656
+ await handleNodePatch(nodeRest, apiUrl, token);
3657
+ } else {
3658
+ console.error(`Unknown node sub-command: ${nodeSub ?? "(none)"}`);
3659
+ printWorkflowHelp();
3660
+ process9.exit(1);
3661
+ }
3662
+ break;
3663
+ }
3664
+ case "seed": {
3665
+ const [seedSub, ...seedRest] = rest;
3666
+ if (seedSub === "create") {
3667
+ await handleWorkflowSeedCreate(seedRest, apiUrl, token);
3668
+ } else {
3669
+ console.error(`Unknown seed sub-command: ${seedSub ?? "(none)"}`);
3670
+ printWorkflowHelp();
3671
+ process9.exit(1);
3672
+ }
3673
+ break;
3674
+ }
3675
+ case "sync":
3676
+ await handleWorkflowSync(rest, apiUrl, token);
3677
+ break;
3678
+ case "proposals": {
3679
+ const { handleWorkflowProposals } = await import("./workflow-proposals-MTUDX46O.js");
3680
+ await handleWorkflowProposals(rest, apiUrl, token);
3681
+ break;
3682
+ }
3683
+ case "generate-api-sequence":
3684
+ await handleWorkflowGenerateApiSequence(rest, apiUrl, token);
3685
+ break;
3686
+ case "api-sequence":
3687
+ await handleWorkflowApiSequence(rest, apiUrl, token);
3688
+ break;
3689
+ case "cache": {
3690
+ const { handleWorkflowCache } = await import("./workflow-cache-NQJQFWMV.js");
3691
+ await handleWorkflowCache(rest, apiUrl, token);
3692
+ break;
3693
+ }
3694
+ case "debug": {
3695
+ const { handleWorkflowDebug } = await import("./workflow-debug-BGD4UIXU.js");
3696
+ await handleWorkflowDebug(rest, apiUrl, token);
3697
+ break;
3698
+ }
3699
+ default:
3700
+ console.error(`Unknown sub-command: ${subCommand}`);
3701
+ printWorkflowHelp();
3702
+ process9.exit(1);
3703
+ }
3704
+ }
3705
+ export {
3706
+ runWorkflow
3707
+ };
3708
+ //# sourceMappingURL=workflow-XYFH5LTP.js.map