@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,951 @@
1
+ import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
+ import {
3
+ ApiSequenceFileSchema,
4
+ diffWorkflow,
5
+ emitSpec,
6
+ extractPreservedFrontmatter,
7
+ hasAnyChange,
8
+ lintSpec,
9
+ parseSpec,
10
+ reconcileLinearEdges,
11
+ summarize,
12
+ workflowToSpec,
13
+ writeNodeIds,
14
+ writeWorkflowId
15
+ } from "./chunk-B2VISN2V.js";
16
+ import "./chunk-SJJW77WN.js";
17
+ import {
18
+ fetchOrExit
19
+ } from "./chunk-3CZU7XCS.js";
20
+ import {
21
+ getArgValue,
22
+ hasFlag,
23
+ resolveAppUrl
24
+ } from "./chunk-A4OFTUVV.js";
25
+ import "./chunk-VKVL7WBN.js";
26
+
27
+ // src/spec/index.ts
28
+ import fs from "fs/promises";
29
+ import path from "path";
30
+ import process from "process";
31
+
32
+ // src/spec/sync.ts
33
+ async function applyDiff(ctx, diff, emitted, live) {
34
+ const deletes = diff.ops.filter((o) => o.kind === "delete");
35
+ if (deletes.length > 0) {
36
+ await postJson(ctx, `/workflows/${ctx.workflowId}/nodes/bulk-delete`, {
37
+ nodeIds: deletes.map((o) => o.nodeId),
38
+ reconnect: true
39
+ });
40
+ }
41
+ const creates = diff.ops.filter((o) => o.kind === "create");
42
+ const createdIds = /* @__PURE__ */ new Map();
43
+ const writebackPairs = [];
44
+ for (const op of creates) {
45
+ const res = await postJson(
46
+ ctx,
47
+ `/workflows/${ctx.workflowId}/nodes`,
48
+ {
49
+ nodeType: op.nodeType,
50
+ configJson: op.configJson,
51
+ orderIndex: op.orderIndex
52
+ }
53
+ );
54
+ const created = res.node;
55
+ if (!created) {
56
+ throw new Error(
57
+ `Failed to create node at orderIndex ${op.orderIndex}: ${res.error ?? JSON.stringify(res)}`
58
+ );
59
+ }
60
+ createdIds.set(op.specIndex, created.id);
61
+ if (op.sourceLine !== void 0) {
62
+ writebackPairs.push({ sourceLine: op.sourceLine, nodeId: created.id });
63
+ }
64
+ }
65
+ const updates = diff.ops.filter((o) => o.kind === "update");
66
+ for (const op of updates) {
67
+ if (!op.changes.configChanged && !op.changes.orderChanged) continue;
68
+ await putJson(ctx, `/workflows/${ctx.workflowId}/nodes/${op.nodeId}`, {
69
+ ...op.changes.configChanged ? { configJson: op.configJson } : {},
70
+ ...op.changes.orderChanged ? { orderIndex: op.orderIndex } : {}
71
+ });
72
+ }
73
+ if (diff.ops.length > 0) {
74
+ const finalNodeIds = computeFinalNodeOrder(diff, live, createdIds);
75
+ const fresh = await getJson(
76
+ ctx,
77
+ `/workflows/${ctx.workflowId}`
78
+ );
79
+ const liveEdges = fresh.workflow?.edges ?? live.edges ?? [];
80
+ const { ops: edgeOps } = reconcileLinearEdges(finalNodeIds, liveEdges);
81
+ for (const op of edgeOps) {
82
+ if (op.kind === "delete") {
83
+ await deleteJson(ctx, `/workflows/${ctx.workflowId}/edges/${op.edgeId}`);
84
+ } else {
85
+ await postJson(ctx, `/workflows/${ctx.workflowId}/edges`, {
86
+ sourceNodeId: op.sourceNodeId,
87
+ targetNodeId: op.targetNodeId,
88
+ edgeType: "default"
89
+ });
90
+ }
91
+ }
92
+ }
93
+ rejectUnsupportedFieldEdits(diff.workflowFieldChanges);
94
+ if (Object.keys(diff.workflowFieldChanges).length > 0) {
95
+ const payload = await resolveFieldUpdatePayload(ctx, diff.workflowFieldChanges);
96
+ await putJson(ctx, `/workflows/${ctx.workflowId}`, payload);
97
+ }
98
+ if (emitted.apiSequenceConfig) {
99
+ await postJson(ctx, `/api-sequences/save`, {
100
+ workflowId: ctx.workflowId,
101
+ config: emitted.apiSequenceConfig
102
+ });
103
+ }
104
+ return { created: writebackPairs };
105
+ }
106
+ async function resolveTeardownWorkflowId(ctx, specName) {
107
+ const res = await getJson(
108
+ ctx,
109
+ `/workflows?search=${encodeURIComponent(specName)}&flowType=teardown&pageSize=50`
110
+ );
111
+ const match = (res.data ?? []).find(
112
+ (w) => w.name === specName && w.flowType === "teardown"
113
+ );
114
+ if (!match) {
115
+ throw new Error(
116
+ `Teardown workflow "${specName}" not found (flowType=teardown). Compile the teardown spec first so the seed can link to it by name.`
117
+ );
118
+ }
119
+ return match.id;
120
+ }
121
+ async function resolveFieldUpdatePayload(ctx, changes) {
122
+ const payload = fieldUpdatePayload(changes);
123
+ if ("teardownSpec" in payload) {
124
+ const specName = payload.teardownSpec;
125
+ delete payload.teardownSpec;
126
+ if (specName === null) {
127
+ payload.teardownWorkflowId = null;
128
+ } else if (typeof specName === "string") {
129
+ payload.teardownWorkflowId = await resolveTeardownWorkflowId(ctx, specName);
130
+ }
131
+ }
132
+ if ("teardownSnapshotVersion" in payload) {
133
+ if (payload.teardownSnapshotVersion === "latest") payload.teardownSnapshotVersion = null;
134
+ }
135
+ if ("seedOutputs" in payload) {
136
+ const inputRows = payload.seedOutputs;
137
+ delete payload.seedOutputs;
138
+ if (inputRows) {
139
+ const propIds = await resolvePropertyNameMap(
140
+ ctx,
141
+ Array.from(new Set(inputRows.map((r) => r.propertyName).filter((p) => !!p)))
142
+ );
143
+ payload.seedOutputsJson = inputRows.map((r) => {
144
+ const propertyId = r.propertyName ? propIds.get(r.propertyName) : void 0;
145
+ if (r.propertyName && !propertyId) {
146
+ throw new Error(
147
+ `Declared output "${r.dataKey}": property "${r.propertyName}" not found in this org.`
148
+ );
149
+ }
150
+ return {
151
+ dataKey: r.dataKey,
152
+ name: r.dataKey,
153
+ displayLabel: r.label ?? r.dataKey,
154
+ ...r.description ? { description: r.description } : {},
155
+ ...propertyId ? { propertyId } : {},
156
+ ...r.credentialOutput !== void 0 ? { credentialOutput: r.credentialOutput } : {},
157
+ ...r.attributes !== void 0 ? { attributes: r.attributes } : {}
158
+ };
159
+ });
160
+ }
161
+ }
162
+ return payload;
163
+ }
164
+ async function resolvePropertyNameMap(ctx, names) {
165
+ const out = /* @__PURE__ */ new Map();
166
+ if (names.length === 0) return out;
167
+ const res = await getJson(ctx, `/org/properties`);
168
+ const all = res.data ?? res.properties ?? [];
169
+ for (const want of names) {
170
+ const match = all.find((p) => p.name === want);
171
+ if (match) out.set(want, match.id);
172
+ }
173
+ return out;
174
+ }
175
+ var UnsupportedFieldEditError = class extends Error {
176
+ constructor(fields) {
177
+ super(
178
+ `Editing ${fields.join(", ")} via .canary.md is not supported yet. These fields require name \u2192 id resolution that lands in a follow-up. Edit them in the Canary UI for now; the rest of your changes will still apply.`
179
+ );
180
+ this.name = "UnsupportedFieldEditError";
181
+ }
182
+ };
183
+ function rejectUnsupportedFieldEdits(changes) {
184
+ const unsupported = ["tags", "environmentConstraints", "sandboxTemplate"].filter(
185
+ (k) => changes[k] !== void 0
186
+ );
187
+ if (unsupported.length > 0) throw new UnsupportedFieldEditError(unsupported);
188
+ }
189
+ function computeFinalNodeOrder(diff, live, createdIds) {
190
+ const liveById = new Map(live.nodes.map((n) => [n.id, n]));
191
+ const total = diff.ops.filter((o) => o.kind !== "delete").length;
192
+ const result = Array.from({ length: total });
193
+ for (const op of diff.ops) {
194
+ if (op.kind === "create") {
195
+ const id = createdIds.get(op.specIndex);
196
+ if (!id) throw new Error(`Missing created node id for specIndex ${op.specIndex}`);
197
+ result[op.specIndex] = id;
198
+ } else if (op.kind === "update") {
199
+ if (!liveById.has(op.nodeId)) {
200
+ throw new Error(`UPDATE references nodeId ${op.nodeId} not in live workflow`);
201
+ }
202
+ result[op.specIndex] = op.nodeId;
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+ function fieldUpdatePayload(changes) {
208
+ const payload = {};
209
+ for (const k of Object.keys(changes)) {
210
+ if (k === "scenariosJson" && Array.isArray(changes.scenariosJson)) {
211
+ payload.scenariosJson = changes.scenariosJson.map((s) => {
212
+ const { skipReason: _skip, ...rest } = s;
213
+ return rest;
214
+ });
215
+ } else {
216
+ payload[k] = changes[k];
217
+ }
218
+ }
219
+ return payload;
220
+ }
221
+ async function postJson(ctx, path2, body) {
222
+ const res = await fetchOrExit(`${ctx.apiUrl}${path2}`, {
223
+ method: "POST",
224
+ headers: {
225
+ Authorization: `Bearer ${ctx.token}`,
226
+ "Content-Type": "application/json"
227
+ },
228
+ token: ctx.token,
229
+ body: JSON.stringify(body)
230
+ });
231
+ return await res.json();
232
+ }
233
+ async function putJson(ctx, path2, body) {
234
+ const res = await fetchOrExit(`${ctx.apiUrl}${path2}`, {
235
+ method: "PUT",
236
+ headers: {
237
+ Authorization: `Bearer ${ctx.token}`,
238
+ "Content-Type": "application/json"
239
+ },
240
+ token: ctx.token,
241
+ body: JSON.stringify(body)
242
+ });
243
+ return await res.json();
244
+ }
245
+ async function getJson(ctx, path2) {
246
+ const res = await fetchOrExit(`${ctx.apiUrl}${path2}`, {
247
+ method: "GET",
248
+ headers: { Authorization: `Bearer ${ctx.token}` },
249
+ token: ctx.token
250
+ });
251
+ return await res.json();
252
+ }
253
+ async function deleteJson(ctx, path2) {
254
+ const res = await fetchOrExit(`${ctx.apiUrl}${path2}`, {
255
+ method: "DELETE",
256
+ headers: { Authorization: `Bearer ${ctx.token}` },
257
+ token: ctx.token
258
+ });
259
+ return await res.json();
260
+ }
261
+
262
+ // src/spec/index.ts
263
+ async function handleSpec(argv, apiUrl, token) {
264
+ const sub = argv[0];
265
+ switch (sub) {
266
+ case "validate":
267
+ await handleValidate(argv.slice(1));
268
+ return;
269
+ case "compile":
270
+ await handleCompile(argv.slice(1), apiUrl, token);
271
+ return;
272
+ case "pull":
273
+ await handlePull(argv.slice(1), apiUrl, token);
274
+ return;
275
+ case "diff":
276
+ await handleDiff(argv.slice(1), apiUrl, token);
277
+ return;
278
+ default:
279
+ printSpecHelp();
280
+ process.exit(sub ? 1 : 0);
281
+ }
282
+ }
283
+ function printSpecHelp() {
284
+ console.log(
285
+ [
286
+ "Usage: canary spec <subcommand> <file>",
287
+ "",
288
+ "Subcommands:",
289
+ " validate <file> Parse + lint the spec offline",
290
+ " compile <file> Validate, then create the workflow",
291
+ " diff <file> Preview what `compile` would change (no writes)",
292
+ " pull <workflow-id> Reverse: workflow \u2192 round-trippable .canary.md",
293
+ " pull --all Sync a directory of specs down from all workflows",
294
+ "",
295
+ "Compile flags:",
296
+ " --publish Create as published instead of draft",
297
+ " --run-and-publish After create, trigger a test run; publish if green",
298
+ " --allow-duplicate Fresh compile: create even when a same-named workflow exists",
299
+ " --create-with-duplicate-title",
300
+ " Alias for --allow-duplicate",
301
+ " --json Machine-readable output",
302
+ "",
303
+ "Pull flags:",
304
+ " --output <path|dir> File (single) or directory (--all); default canary/specs/",
305
+ " --all Sync every workflow into the directory (add new, update matching by workflowId)",
306
+ " --status <s> With --all, filter workflows by status (draft|published|\u2026)",
307
+ " --force Overwrite an existing file (single pull)"
308
+ ].join("\n")
309
+ );
310
+ }
311
+ async function handleValidate(argv) {
312
+ const file = argv[0];
313
+ if (!file) {
314
+ console.error("Error: canary spec validate <file>");
315
+ process.exit(1);
316
+ }
317
+ const json = hasFlag(argv, "--json");
318
+ const result = await loadAndValidate(file);
319
+ if (json) {
320
+ console.log(
321
+ JSON.stringify(
322
+ {
323
+ ok: result.ok,
324
+ issues: result.issues,
325
+ ...result.ok ? { workflowName: result.ast.frontmatter.name } : {}
326
+ },
327
+ null,
328
+ 2
329
+ )
330
+ );
331
+ process.exit(result.ok ? 0 : 1);
332
+ }
333
+ printIssues(result.issues, file);
334
+ if (!result.ok) {
335
+ process.exit(1);
336
+ }
337
+ console.log(`\u2713 ${file} validates`);
338
+ }
339
+ async function handleCompile(argv, apiUrl, token) {
340
+ const file = argv[0];
341
+ if (!file) {
342
+ console.error("Error: canary spec compile <file>");
343
+ process.exit(1);
344
+ }
345
+ const result = await loadAndValidate(file);
346
+ printIssues(result.issues, file);
347
+ if (!result.ok) {
348
+ process.exit(1);
349
+ }
350
+ const ast = result.ast;
351
+ const emitted = emitSpec(ast, { appendDefaultEnd: !ast.frontmatter.workflowId });
352
+ try {
353
+ emitted.apiSequenceConfig = await loadApiSequenceSidecar(emitted.nodes, file);
354
+ } catch (err) {
355
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
356
+ process.exit(1);
357
+ }
358
+ if (emitted.workflowId) {
359
+ await runRecompile({ argv, apiUrl, token, file, emitted });
360
+ return;
361
+ }
362
+ await runFreshCompile({ argv, apiUrl, token, file, emitted });
363
+ }
364
+ async function loadApiSequenceSidecar(nodes, specFile) {
365
+ const refs = nodes.filter((n) => n.nodeType === "api_sequence").map((n) => n.configJson["__apiSequenceSidecar"]).filter((p) => typeof p === "string" && p.length > 0);
366
+ if (refs.length === 0) return void 0;
367
+ const unique = Array.from(new Set(refs));
368
+ if (unique.length > 1) {
369
+ throw new Error(
370
+ `Spec has divergent api_sequence sidecar paths: ${unique.map((p) => `"${p}"`).join(", ")}. The DB stores one config per workflow \u2014 pick a single path.`
371
+ );
372
+ }
373
+ const sidecarPath = path.resolve(path.dirname(specFile), unique[0]);
374
+ let raw;
375
+ try {
376
+ raw = await fs.readFile(sidecarPath, "utf-8");
377
+ } catch (err) {
378
+ throw new Error(
379
+ `Could not read api_sequence sidecar at ${sidecarPath}: ${err instanceof Error ? err.message : String(err)}`
380
+ );
381
+ }
382
+ const yaml = await import("yaml");
383
+ let parsed;
384
+ try {
385
+ parsed = yaml.parse(raw);
386
+ } catch (err) {
387
+ throw new Error(
388
+ `Sidecar YAML at ${sidecarPath} is invalid: ${err instanceof Error ? err.message : String(err)}`
389
+ );
390
+ }
391
+ const validated = ApiSequenceFileSchema.safeParse(parsed);
392
+ if (!validated.success) {
393
+ throw new Error(
394
+ `Sidecar at ${sidecarPath} doesn't match the api_sequence shape: ${validated.error.message}`
395
+ );
396
+ }
397
+ return validated.data;
398
+ }
399
+ async function handleDiff(argv, apiUrl, token) {
400
+ const file = argv[0];
401
+ if (!file) {
402
+ console.error("Error: canary spec diff <file>");
403
+ process.exit(1);
404
+ }
405
+ const result = await loadAndValidate(file);
406
+ printIssues(result.issues, file);
407
+ if (!result.ok) process.exit(1);
408
+ const emitted = emitSpec(result.ast, { appendDefaultEnd: false });
409
+ if (!emitted.workflowId) {
410
+ console.log(
411
+ `\u2139 ${file} has no workflowId in frontmatter \u2014 \`canary spec compile\` would create a new workflow.`
412
+ );
413
+ console.log(` Nodes that would be created: ${emitted.nodes.length}`);
414
+ return;
415
+ }
416
+ const liveRes = await fetchOrExit(`${apiUrl}/workflows/${emitted.workflowId}`, {
417
+ method: "GET",
418
+ headers: { Authorization: `Bearer ${token}` },
419
+ token
420
+ });
421
+ if (!liveRes.ok) {
422
+ console.error(`Error: failed to fetch workflow ${emitted.workflowId} (${liveRes.status})`);
423
+ process.exit(1);
424
+ }
425
+ const liveBody = await liveRes.json().catch(() => ({}));
426
+ const live = liveBody.workflow;
427
+ if (!live) {
428
+ console.error(`Error: workflow ${emitted.workflowId} not found.`);
429
+ process.exit(1);
430
+ }
431
+ let diff;
432
+ try {
433
+ diff = diffWorkflow(emitted, live);
434
+ } catch (err) {
435
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
436
+ process.exit(1);
437
+ }
438
+ if (hasFlag(argv, "--json")) {
439
+ console.log(JSON.stringify({ ok: true, summary: summarize(diff), diff }, null, 2));
440
+ return;
441
+ }
442
+ if (!hasAnyChange(diff)) {
443
+ console.log(`\u2713 ${file} \u2014 in sync with workflow ${emitted.workflowId}`);
444
+ return;
445
+ }
446
+ const s = summarize(diff);
447
+ console.log(`
448
+ Preview for "${live.name}" (${emitted.workflowId}):`);
449
+ console.log(
450
+ ` +${s.created} create \xB7 ~${s.updated} update \xB7 -${s.deleted} delete \xB7 \u21BB ${s.moved} move`
451
+ );
452
+ if (diff.nameDrift) console.log(` \u270E rename: "${diff.nameDrift.from}" \u2192 "${diff.nameDrift.to}"`);
453
+ const fieldKeys = Object.keys(diff.workflowFieldChanges).filter((k) => k !== "name");
454
+ if (fieldKeys.length > 0) console.log(` \u270E workflow fields: ${fieldKeys.join(", ")}`);
455
+ console.log(`
456
+ Run \`canary spec compile ${file}\` to apply.`);
457
+ }
458
+ async function handlePull(argv, apiUrl, token) {
459
+ if (hasFlag(argv, "--all")) {
460
+ await handlePullAll(argv, apiUrl, token);
461
+ return;
462
+ }
463
+ const workflowId = argv.find((a) => !a.startsWith("-"));
464
+ if (!workflowId) {
465
+ console.error("Error: canary spec pull <workflow-id> [--output <path>] [--force]");
466
+ console.error(" canary spec pull --all [--output <dir>] [--status <s>] [--force]");
467
+ process.exit(1);
468
+ }
469
+ const json = hasFlag(argv, "--json");
470
+ const force = hasFlag(argv, "--force");
471
+ const live = await fetchWorkflow(apiUrl, token, workflowId);
472
+ if (!live) {
473
+ console.error(`Error: workflow ${workflowId} not found (or no permission).`);
474
+ process.exit(1);
475
+ }
476
+ const outPath = getArgValue(argv, "--output") ?? path.join("canary", "specs", `${slugify(live.name)}.canary.md`);
477
+ const preserved = await readPreservedFrontmatter(outPath);
478
+ const sidecarPath = await fetchAndWriteApiSequenceSidecar(
479
+ apiUrl,
480
+ token,
481
+ workflowId,
482
+ live,
483
+ outPath
484
+ );
485
+ if (sidecarPath) {
486
+ for (const node of live.nodes) {
487
+ if (node.nodeType === "api_sequence") {
488
+ node.configJson = {
489
+ ...node.configJson,
490
+ __apiSequenceSidecar: path.relative(path.dirname(outPath), sidecarPath)
491
+ };
492
+ }
493
+ }
494
+ }
495
+ const source = workflowToSpec(live, preserved);
496
+ const parsed = parseSpec(source);
497
+ const lintIssues = parsed.ok ? lintSpec(parsed.ast) : [];
498
+ const parseIssues = parsed.ok ? [] : parsed.issues;
499
+ const issues = [...parseIssues, ...lintIssues];
500
+ if (!force) {
501
+ try {
502
+ await fs.access(outPath);
503
+ console.error(
504
+ `Error: ${outPath} already exists. Use --output <path> or --force to overwrite.`
505
+ );
506
+ process.exit(1);
507
+ } catch {
508
+ }
509
+ }
510
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
511
+ await fs.writeFile(outPath, source, "utf-8");
512
+ if (json) {
513
+ console.log(JSON.stringify({ ok: true, file: outPath, workflowId, issues }, null, 2));
514
+ return;
515
+ }
516
+ console.log(`
517
+ \u2713 Pulled "${live.name}" \u2192 ${outPath}`);
518
+ console.log(` Nodes: ${live.nodes.length}`);
519
+ console.log(` Edit the file, then \`canary spec compile ${outPath}\` to push changes back.`);
520
+ if (issues.length > 0) {
521
+ console.log("\n Notes:");
522
+ printIssues(issues, outPath);
523
+ }
524
+ }
525
+ async function handlePullAll(argv, apiUrl, token) {
526
+ const json = hasFlag(argv, "--json");
527
+ const dir = getArgValue(argv, "--output") ?? argv.find((a) => !a.startsWith("-")) ?? path.join("canary", "specs");
528
+ const status = getArgValue(argv, "--status");
529
+ const workflows = await fetchWorkflowList(apiUrl, token, status);
530
+ const byWorkflowId = await indexSpecFiles(dir);
531
+ const usedNames = new Set([...byWorkflowId.values()].map((p) => path.basename(p)));
532
+ const results = [];
533
+ for (const wf of workflows) {
534
+ const live = await fetchWorkflow(apiUrl, token, wf.id);
535
+ if (!live) {
536
+ results.push({ id: wf.id, name: wf.name, file: "", action: "failed", note: "fetch failed" });
537
+ continue;
538
+ }
539
+ const existing = byWorkflowId.get(wf.id);
540
+ let target;
541
+ let isNew = false;
542
+ if (existing) {
543
+ target = existing;
544
+ } else {
545
+ target = path.join(dir, uniqueName(slugify(live.name), wf.id, usedNames));
546
+ usedNames.add(path.basename(target));
547
+ isNew = true;
548
+ }
549
+ let prior = null;
550
+ try {
551
+ prior = await fs.readFile(target, "utf-8");
552
+ } catch {
553
+ }
554
+ const preserved = prior ? extractPreservedFrontmatter(prior) : {};
555
+ const source = workflowToSpec(live, preserved);
556
+ if (prior === source) {
557
+ results.push({ id: wf.id, name: live.name, file: target, action: "unchanged" });
558
+ continue;
559
+ }
560
+ await fs.mkdir(path.dirname(target), { recursive: true });
561
+ await fs.writeFile(target, source, "utf-8");
562
+ results.push({ id: wf.id, name: live.name, file: target, action: isNew ? "added" : "synced" });
563
+ }
564
+ if (json) {
565
+ console.log(JSON.stringify({ ok: true, dir, results }, null, 2));
566
+ return;
567
+ }
568
+ const count = (a) => results.filter((r) => r.action === a).length;
569
+ console.log(`
570
+ \u2713 Synced ${results.length} workflow(s) \u2192 ${dir}`);
571
+ console.log(
572
+ ` +${count("added")} added \xB7 ~${count("synced")} synced \xB7 =${count("unchanged")} unchanged${count("failed") ? ` \xB7 \u2717${count("failed")} failed` : ""}`
573
+ );
574
+ for (const r of results) {
575
+ if (r.action === "unchanged") continue;
576
+ const mark = r.action === "added" ? "+" : r.action === "synced" ? "~" : "\u2717";
577
+ console.log(
578
+ ` ${mark} ${path.relative(process.cwd(), r.file) || r.file}${r.note ? ` (${r.note})` : ""}`
579
+ );
580
+ }
581
+ }
582
+ async function fetchAndWriteApiSequenceSidecar(apiUrl, token, workflowId, live, outPath) {
583
+ if (!live.nodes.some((n) => n.nodeType === "api_sequence")) return null;
584
+ const res = await fetchOrExit(`${apiUrl}/api-sequences/by-workflow/${workflowId}`, {
585
+ method: "GET",
586
+ headers: { Authorization: `Bearer ${token}` },
587
+ token
588
+ });
589
+ if (!res.ok) {
590
+ return null;
591
+ }
592
+ const body = await res.json().catch(() => ({}));
593
+ const config = body.data?.config;
594
+ if (!config) return null;
595
+ const sequencesDir = path.join(path.dirname(outPath), "sequences");
596
+ const sidecarSlug = slugify(live.name);
597
+ const sidecarPath = path.join(sequencesDir, `${sidecarSlug}.yaml`);
598
+ await fs.mkdir(sequencesDir, { recursive: true });
599
+ const yaml = await import("yaml");
600
+ await fs.writeFile(sidecarPath, yaml.stringify(config), "utf-8");
601
+ return sidecarPath;
602
+ }
603
+ async function readPreservedFrontmatter(target) {
604
+ try {
605
+ const prior = await fs.readFile(target, "utf-8");
606
+ return extractPreservedFrontmatter(prior);
607
+ } catch {
608
+ return {};
609
+ }
610
+ }
611
+ async function fetchWorkflow(apiUrl, token, workflowId) {
612
+ const res = await fetchOrExit(`${apiUrl}/workflows/${workflowId}`, {
613
+ method: "GET",
614
+ headers: { Authorization: `Bearer ${token}` },
615
+ token
616
+ });
617
+ if (!res.ok) return null;
618
+ const body = await res.json().catch(() => ({}));
619
+ return body.workflow ?? null;
620
+ }
621
+ async function fetchWorkflowList(apiUrl, token, status) {
622
+ const out = [];
623
+ let page = 1;
624
+ for (; ; ) {
625
+ const params = new URLSearchParams({ page: String(page), pageSize: "100" });
626
+ if (status) params.set("status", status);
627
+ const res = await fetchOrExit(`${apiUrl}/workflows?${params.toString()}`, {
628
+ method: "GET",
629
+ headers: { Authorization: `Bearer ${token}` },
630
+ token
631
+ });
632
+ if (!res.ok) {
633
+ console.error(`Error: failed to list workflows (${res.status})`);
634
+ process.exit(1);
635
+ }
636
+ const body = await res.json().catch(() => ({}));
637
+ for (const w of body.data ?? []) out.push({ id: w.id, name: w.name });
638
+ const pg = body.pagination;
639
+ if (!pg || pg.page >= pg.totalPages || (body.data ?? []).length === 0) break;
640
+ page += 1;
641
+ }
642
+ return out;
643
+ }
644
+ async function indexSpecFiles(dir) {
645
+ const map = /* @__PURE__ */ new Map();
646
+ let entries;
647
+ try {
648
+ entries = await fs.readdir(dir, { recursive: true });
649
+ } catch {
650
+ return map;
651
+ }
652
+ const ID_RE = /^workflowId:\s*([0-9a-f-]{36})\s*$/im;
653
+ for (const rel of entries) {
654
+ if (!rel.endsWith(".canary.md")) continue;
655
+ const full = path.join(dir, rel);
656
+ try {
657
+ const src = await fs.readFile(full, "utf-8");
658
+ const m = ID_RE.exec(src);
659
+ if (m && !map.has(m[1])) map.set(m[1], full);
660
+ } catch {
661
+ }
662
+ }
663
+ return map;
664
+ }
665
+ function uniqueName(slug, workflowId, used) {
666
+ let name = `${slug}.canary.md`;
667
+ if (used.has(name)) name = `${slug}-${workflowId.slice(0, 8)}.canary.md`;
668
+ return name;
669
+ }
670
+ async function runFreshCompile(opts) {
671
+ const { argv, apiUrl, token, file, emitted } = opts;
672
+ const duplicateMatches = await fetchFreshCompileDuplicateWorkflows({
673
+ argv,
674
+ apiUrl,
675
+ token,
676
+ workflowName: emitted.name
677
+ });
678
+ if (duplicateMatches.length > 0) {
679
+ const message = formatDuplicateWorkflowTitleWarning({
680
+ file,
681
+ workflowName: emitted.name,
682
+ matches: duplicateMatches
683
+ });
684
+ if (hasFlag(argv, "--json")) {
685
+ console.log(
686
+ JSON.stringify(
687
+ {
688
+ ok: false,
689
+ error: "duplicate_workflow_title",
690
+ message,
691
+ workflows: duplicateMatches
692
+ },
693
+ null,
694
+ 2
695
+ )
696
+ );
697
+ } else {
698
+ console.error(message);
699
+ }
700
+ process.exit(1);
701
+ }
702
+ const status = hasFlag(argv, "--publish") ? "published" : emitted.status ?? "draft";
703
+ const payload = {
704
+ name: emitted.name,
705
+ description: emitted.description,
706
+ preflight: emitted.preflight,
707
+ cleanup: emitted.cleanup,
708
+ status,
709
+ flowType: emitted.flowType,
710
+ createdVia: "cli-spec",
711
+ // The route only knows about (nodeType, configJson) — strip our meta.
712
+ nodes: emitted.nodes.map(({ nodeType, configJson }) => ({ nodeType, configJson })),
713
+ ...emitted.inputsJson.length > 0 ? { inputsJson: emitted.inputsJson } : {},
714
+ ...emitted.scenariosJson.length > 0 ? { scenariosJson: emitted.scenariosJson } : {}
715
+ };
716
+ const res = await fetchOrExit(`${apiUrl}/workflows/create-with-nodes`, {
717
+ method: "POST",
718
+ headers: {
719
+ Authorization: `Bearer ${token}`,
720
+ "Content-Type": "application/json"
721
+ },
722
+ token,
723
+ body: JSON.stringify(payload)
724
+ });
725
+ const body = await res.json().catch(() => ({}));
726
+ if (!res.ok || !body.ok || !body.workflow) {
727
+ console.error(`Error: ${body.error ?? `${res.status} ${res.statusText}`}`);
728
+ process.exit(1);
729
+ }
730
+ const writebackPairs = [];
731
+ if (body.nodes && body.nodes.length === emitted.nodes.length) {
732
+ for (let i = 0; i < emitted.nodes.length; i++) {
733
+ const sourceLine = emitted.nodes[i].sourceLine;
734
+ if (sourceLine === void 0) continue;
735
+ writebackPairs.push({ sourceLine, nodeId: body.nodes[i].id });
736
+ }
737
+ }
738
+ try {
739
+ const source = await fs.readFile(file, "utf-8");
740
+ const withWorkflowId = writeWorkflowId(source, body.workflow.id);
741
+ const lineShift = withWorkflowId.split("\n").length - source.split("\n").length;
742
+ const shiftedPairs = writebackPairs.map((p) => ({
743
+ ...p,
744
+ sourceLine: p.sourceLine + lineShift
745
+ }));
746
+ const next = writeNodeIds(withWorkflowId, shiftedPairs);
747
+ if (next !== source) {
748
+ await fs.writeFile(file, next, "utf-8");
749
+ }
750
+ } catch (err) {
751
+ console.warn(
752
+ `\u26A0 Workflow created, but failed to write IDs back to ${file}: ${err instanceof Error ? err.message : String(err)}`
753
+ );
754
+ }
755
+ if (hasFlag(argv, "--json")) {
756
+ console.log(JSON.stringify({ ok: true, workflow: body.workflow, writebackPairs }, null, 2));
757
+ return;
758
+ }
759
+ const appUrl = resolveAppUrl(apiUrl);
760
+ console.log(`
761
+ \u2713 Spec compiled: ${body.workflow.name}`);
762
+ console.log(` ID: ${body.workflow.id}`);
763
+ console.log(` Status: ${body.workflow.status}`);
764
+ console.log(` Nodes: ${body.nodes?.length ?? 0}`);
765
+ console.log(` URL: ${appUrl}/flows/${body.workflow.id}`);
766
+ console.log(` \u2192 wrote workflowId + @id markers back to ${file}`);
767
+ if (body.warnings && body.warnings.length > 0) {
768
+ console.log("\n Warnings:");
769
+ for (const w of body.warnings) console.log(` \u26A0 ${w}`);
770
+ }
771
+ if (hasFlag(argv, "--run-and-publish")) {
772
+ console.log("\n\u2139 --run-and-publish for spec compile is not wired in this MVP. Run:");
773
+ console.log(` canary workflow run ${body.workflow.id} --publish-on-pass`);
774
+ }
775
+ }
776
+ var DUPLICATE_WORKFLOW_TITLE_ALLOW_FLAGS = [
777
+ "--allow-duplicate",
778
+ "--create-with-duplicate-title"
779
+ ];
780
+ function allowsDuplicateWorkflowTitle(argv) {
781
+ return DUPLICATE_WORKFLOW_TITLE_ALLOW_FLAGS.some((flag) => hasFlag(argv, flag));
782
+ }
783
+ function exactWorkflowTitleMatches(workflows, workflowName) {
784
+ return workflows.filter((workflow) => workflow.name === workflowName);
785
+ }
786
+ function formatDuplicateWorkflowTitleWarning(opts) {
787
+ const first = opts.matches[0];
788
+ const stampedFile = stampedSpecPath(opts.file);
789
+ const workflowLines = opts.matches.map((workflow) => {
790
+ const details = [workflow.status, workflow.flowType].filter(Boolean).join(", ");
791
+ return ` - ${workflow.id}${details ? ` (${details})` : ""}`;
792
+ }).join("\n");
793
+ return [
794
+ `Error: fresh spec compile would create a duplicate workflow named "${opts.workflowName}".`,
795
+ "",
796
+ "Existing workflow(s):",
797
+ workflowLines,
798
+ "",
799
+ "To update an existing workflow instead, pull a stamped spec, re-apply your edits, then compile it:",
800
+ ` canary spec pull ${first.id} --output ${stampedFile}`,
801
+ ` canary spec compile ${stampedFile}`,
802
+ "",
803
+ "To intentionally create another workflow with the same title, re-run with:",
804
+ ` canary spec compile ${opts.file} --allow-duplicate`
805
+ ].join("\n");
806
+ }
807
+ async function fetchFreshCompileDuplicateWorkflows(opts) {
808
+ if (allowsDuplicateWorkflowTitle(opts.argv)) return [];
809
+ const params = new URLSearchParams({
810
+ search: opts.workflowName,
811
+ pageSize: "50"
812
+ });
813
+ const res = await fetchOrExit(`${opts.apiUrl}/workflows?${params.toString()}`, {
814
+ method: "GET",
815
+ headers: { Authorization: `Bearer ${opts.token}` },
816
+ token: opts.token
817
+ });
818
+ if (!res.ok) {
819
+ console.error(`Error: failed to check for existing workflow titles (${res.status})`);
820
+ process.exit(1);
821
+ }
822
+ const body = await res.json().catch(() => ({}));
823
+ return exactWorkflowTitleMatches(body.data ?? [], opts.workflowName);
824
+ }
825
+ function stampedSpecPath(file) {
826
+ if (file.endsWith(".canary.md")) {
827
+ return `${file.slice(0, -".canary.md".length)}.stamped.canary.md`;
828
+ }
829
+ return `${file}.stamped.canary.md`;
830
+ }
831
+ async function runRecompile(opts) {
832
+ const { argv, apiUrl, token, file, emitted } = opts;
833
+ const workflowId = emitted.workflowId;
834
+ const liveRes = await fetchOrExit(`${apiUrl}/workflows/${workflowId}`, {
835
+ method: "GET",
836
+ headers: { Authorization: `Bearer ${token}` },
837
+ token
838
+ });
839
+ if (!liveRes.ok) {
840
+ console.error(`Error: failed to fetch workflow ${workflowId} (${liveRes.status})`);
841
+ process.exit(1);
842
+ }
843
+ const liveBody = await liveRes.json().catch(() => ({}));
844
+ const live = liveBody.workflow;
845
+ if (!live) {
846
+ console.error(`Error: workflow ${workflowId} not found (or no permission).`);
847
+ process.exit(1);
848
+ }
849
+ let diff;
850
+ try {
851
+ diff = diffWorkflow(emitted, live);
852
+ } catch (err) {
853
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
854
+ process.exit(1);
855
+ }
856
+ if (!hasAnyChange(diff)) {
857
+ console.log(`\u2713 ${file} \u2014 no changes (workflow ${workflowId} already in sync)`);
858
+ return;
859
+ }
860
+ const ctx = { apiUrl, token, workflowId };
861
+ const sync = await applyDiff(ctx, diff, emitted, live);
862
+ if (sync.created.length > 0) {
863
+ try {
864
+ const source = await fs.readFile(file, "utf-8");
865
+ const next = writeNodeIds(source, sync.created);
866
+ if (next !== source) await fs.writeFile(file, next, "utf-8");
867
+ } catch (err) {
868
+ console.warn(
869
+ `\u26A0 Recompile applied, but failed to write new @id markers to ${file}: ${err instanceof Error ? err.message : String(err)}`
870
+ );
871
+ }
872
+ }
873
+ const s = summarize(diff);
874
+ const appUrl = resolveAppUrl(apiUrl);
875
+ console.log(`
876
+ \u2713 Recompiled "${live.name}"`);
877
+ console.log(
878
+ ` +${s.created} created \xB7 ~${s.updated} updated \xB7 -${s.deleted} deleted \xB7 \u21BB ${s.moved} moved`
879
+ );
880
+ console.log(` URL: ${appUrl}/flows/${workflowId}`);
881
+ if (diff.nameDrift) {
882
+ console.log(` \u270E renamed: "${diff.nameDrift.from}" \u2192 "${diff.nameDrift.to}"`);
883
+ }
884
+ if (live.status === "published") {
885
+ console.log(
886
+ ` \u26A0 workflow is published \u2014 changes land on draft state; next publish captures them.`
887
+ );
888
+ }
889
+ if (diff.duplicateSpecIds.length > 0) {
890
+ console.log(
891
+ ` \u26A0 duplicate @id in spec (treated as fresh creates): ${diff.duplicateSpecIds.join(", ")}`
892
+ );
893
+ }
894
+ if (diff.orphanedSpecIds.length > 0) {
895
+ console.log(
896
+ ` \u26A0 stale @id in spec (no matching node \u2014 rebound to new): ${diff.orphanedSpecIds.join(", ")}`
897
+ );
898
+ }
899
+ if (hasFlag(argv, "--json")) {
900
+ console.log(JSON.stringify({ ok: true, workflowId, summary: s, diff }, null, 2));
901
+ }
902
+ }
903
+ async function loadAndValidate(file) {
904
+ let source;
905
+ try {
906
+ source = await fs.readFile(file, "utf-8");
907
+ } catch (err) {
908
+ return {
909
+ ok: false,
910
+ issues: [
911
+ {
912
+ severity: "error",
913
+ message: `Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`
914
+ }
915
+ ]
916
+ };
917
+ }
918
+ const parsed = parseSpec(source);
919
+ if (!parsed.ok) return { ok: false, issues: parsed.issues };
920
+ const lintIssues = lintSpec(parsed.ast);
921
+ const allIssues = [...lintIssues];
922
+ const hasError = allIssues.some((i) => i.severity === "error");
923
+ if (hasError) return { ok: false, issues: allIssues };
924
+ return { ok: true, ast: parsed.ast, issues: allIssues };
925
+ }
926
+ function slugify(name) {
927
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
928
+ return slug || "workflow";
929
+ }
930
+ function printIssues(issues, file) {
931
+ if (issues.length === 0) return;
932
+ for (const i of issues) {
933
+ const prefix = i.severity === "error" ? "\u2717 error" : "\u26A0 warn";
934
+ const loc = i.line ? `${file}:${i.line}` : file;
935
+ console.error(`${prefix} ${loc}: ${i.message}`);
936
+ if (i.hint) console.error(` hint: ${i.hint}`);
937
+ }
938
+ }
939
+ export {
940
+ DUPLICATE_WORKFLOW_TITLE_ALLOW_FLAGS,
941
+ allowsDuplicateWorkflowTitle,
942
+ emitSpec,
943
+ exactWorkflowTitleMatches,
944
+ formatDuplicateWorkflowTitleWarning,
945
+ handleCompile,
946
+ handleSpec,
947
+ handleValidate,
948
+ lintSpec,
949
+ parseSpec
950
+ };
951
+ //# sourceMappingURL=spec-RA3CFEKG.js.map