@pikku/inspector 0.11.1 → 0.12.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 (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
@@ -0,0 +1,1104 @@
1
+ /**
2
+ * Deserialize workflow JSON back to DSL code
3
+ * Converts the serialized workflow graph format back to TypeScript DSL code
4
+ */
5
+
6
+ import type {
7
+ SerializedWorkflowGraph,
8
+ SerializedGraphNode,
9
+ DataRef,
10
+ ContextVariable,
11
+ } from '../graph/workflow-graph.types.js'
12
+
13
+ interface DeserializeOptions {
14
+ /** Import path for pikkuWorkflowFunc */
15
+ pikkuImportPath?: string
16
+ /** Whether to include type annotations */
17
+ includeTypes?: boolean
18
+ }
19
+
20
+ /**
21
+ * Check if a value is a DataRef
22
+ */
23
+ function isDataRef(value: unknown): value is DataRef {
24
+ return (
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ '$ref' in value &&
28
+ typeof (value as DataRef).$ref === 'string'
29
+ )
30
+ }
31
+
32
+ /**
33
+ * State reference (context variable)
34
+ */
35
+ interface StateRef {
36
+ $state: string
37
+ path?: string
38
+ }
39
+
40
+ /**
41
+ * Check if a value is a StateRef
42
+ */
43
+ function isStateRef(value: unknown): value is StateRef {
44
+ return (
45
+ typeof value === 'object' &&
46
+ value !== null &&
47
+ '$state' in value &&
48
+ typeof (value as StateRef).$state === 'string'
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Check if value is a template literal reference
54
+ */
55
+ interface TemplateRef {
56
+ $template: {
57
+ parts: string[]
58
+ expressions: unknown[]
59
+ }
60
+ }
61
+
62
+ function isTemplateRef(value: unknown): value is TemplateRef {
63
+ return (
64
+ typeof value === 'object' &&
65
+ value !== null &&
66
+ '$template' in value &&
67
+ typeof (value as TemplateRef).$template === 'object'
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Convert a DataRef to code expression
73
+ */
74
+ function dataRefToCode(ref: DataRef, itemVar?: string): string {
75
+ if (ref.$ref === 'trigger') {
76
+ // Reference to trigger input (data)
77
+ return ref.path ? `data.${ref.path}` : 'data'
78
+ }
79
+ if (ref.$ref === '$item') {
80
+ // Reference to the current loop item
81
+ // The path contains the variable name in this case
82
+ return ref.path || itemVar || 'item'
83
+ }
84
+ // Reference to a step output variable
85
+ return ref.path ? `${ref.$ref}.${ref.path}` : ref.$ref
86
+ }
87
+
88
+ /**
89
+ * Convert a template ref to template literal code
90
+ */
91
+ function templateRefToCode(template: TemplateRef, itemVar?: string): string {
92
+ const { parts, expressions } = template.$template
93
+ let result = '`'
94
+
95
+ for (let i = 0; i < parts.length; i++) {
96
+ result += parts[i]
97
+ if (i < expressions.length) {
98
+ const expr = expressions[i]
99
+ let exprCode: string
100
+ if (isDataRef(expr)) {
101
+ exprCode = dataRefToCode(expr, itemVar)
102
+ } else if (isTemplateRef(expr)) {
103
+ // Nested template (unlikely but handle it)
104
+ exprCode = templateRefToCode(expr, itemVar)
105
+ } else {
106
+ // Literal value
107
+ exprCode = String(expr)
108
+ }
109
+ result += '${' + exprCode + '}'
110
+ }
111
+ }
112
+
113
+ result += '`'
114
+ return result
115
+ }
116
+
117
+ /**
118
+ * Convert a StateRef to code expression
119
+ */
120
+ function stateRefToCode(ref: StateRef): string {
121
+ return ref.path ? `${ref.$state}.${ref.path}` : ref.$state
122
+ }
123
+
124
+ /**
125
+ * Convert a single value to code (handles refs, templates, state refs, and literals)
126
+ */
127
+ function valueToCode(value: unknown, itemVar?: string): string {
128
+ if (isDataRef(value)) {
129
+ return dataRefToCode(value, itemVar)
130
+ }
131
+ if (isStateRef(value)) {
132
+ return stateRefToCode(value)
133
+ }
134
+ if (isTemplateRef(value)) {
135
+ return templateRefToCode(value, itemVar)
136
+ }
137
+ if (Array.isArray(value)) {
138
+ const elements = value.map((v) => valueToCode(v, itemVar))
139
+ return `[${elements.join(', ')}]`
140
+ }
141
+ if (typeof value === 'object' && value !== null) {
142
+ const entries = Object.entries(value)
143
+ const props = entries.map(([k, v]) => `${k}: ${valueToCode(v, itemVar)}`)
144
+ return `{ ${props.join(', ')} }`
145
+ }
146
+ return JSON.stringify(value)
147
+ }
148
+
149
+ /**
150
+ * Check if input represents passthrough (entire data object)
151
+ */
152
+ function isPassthrough(input: Record<string, unknown>): boolean {
153
+ if (Object.keys(input).length === 1 && '$passthrough' in input) {
154
+ const passthrough = input.$passthrough
155
+ return isDataRef(passthrough) && passthrough.$ref === 'trigger'
156
+ }
157
+ return false
158
+ }
159
+
160
+ /**
161
+ * Convert input object to code
162
+ */
163
+ function inputToCode(
164
+ input: Record<string, unknown>,
165
+ indent: string,
166
+ itemVar?: string
167
+ ): string {
168
+ // Check if this is a passthrough (entire data object)
169
+ if (isPassthrough(input)) {
170
+ return 'data'
171
+ }
172
+
173
+ const entries = Object.entries(input)
174
+ if (entries.length === 0) return '{}'
175
+
176
+ const lines = entries.map(([key, value]) => {
177
+ return `${indent} ${key}: ${valueToCode(value, itemVar)},`
178
+ })
179
+
180
+ return `{\n${lines.join('\n')}\n${indent}}`
181
+ }
182
+
183
+ /**
184
+ * Convert options to code
185
+ */
186
+ function optionsToCode(options: Record<string, unknown>): string {
187
+ const parts: string[] = []
188
+ if (options.retries !== undefined) {
189
+ parts.push(`retries: ${options.retries}`)
190
+ }
191
+ if (options.retryDelay !== undefined) {
192
+ parts.push(`retryDelay: '${options.retryDelay}'`)
193
+ }
194
+ return parts.length > 0 ? `{ ${parts.join(', ')} }` : ''
195
+ }
196
+
197
+ /**
198
+ * Convert a simple condition to code expression
199
+ */
200
+ function conditionToCode(condition: any): string {
201
+ if (!condition) return 'true'
202
+
203
+ if (condition.type === 'simple') {
204
+ return condition.expression
205
+ }
206
+
207
+ if (condition.type === 'and') {
208
+ const parts = condition.conditions.map(conditionToCode)
209
+ return parts.length > 1 ? `(${parts.join(' && ')})` : parts[0]
210
+ }
211
+
212
+ if (condition.type === 'or') {
213
+ const parts = condition.conditions.map(conditionToCode)
214
+ return parts.length > 1 ? `(${parts.join(' || ')})` : parts[0]
215
+ }
216
+
217
+ return 'true'
218
+ }
219
+
220
+ /**
221
+ * Traverse nodes in execution order starting from entry
222
+ */
223
+ function traverseNodes(
224
+ nodes: Record<string, SerializedGraphNode>,
225
+ entryNodeIds: string[]
226
+ ): SerializedGraphNode[] {
227
+ const result: SerializedGraphNode[] = []
228
+ const visited = new Set<string>()
229
+
230
+ function visit(nodeId: string) {
231
+ if (visited.has(nodeId)) return
232
+ visited.add(nodeId)
233
+
234
+ const node = nodes[nodeId]
235
+ if (!node) return
236
+
237
+ result.push(node)
238
+
239
+ // Follow next pointer
240
+ if ('next' in node && node.next) {
241
+ if (typeof node.next === 'string') {
242
+ visit(node.next)
243
+ }
244
+ }
245
+ }
246
+
247
+ for (const entryId of entryNodeIds) {
248
+ visit(entryId)
249
+ }
250
+
251
+ return result
252
+ }
253
+
254
+ /**
255
+ * Collect conditional variables that need to be declared before a branch
256
+ */
257
+ function collectBranchConditionalVars(
258
+ branchNode: any,
259
+ nodes: Record<string, SerializedGraphNode>,
260
+ conditionalVars: Set<string>
261
+ ): string[] {
262
+ const vars: string[] = []
263
+
264
+ // Check all branches (if/else-if chain)
265
+ if (branchNode.branches) {
266
+ for (const branch of branchNode.branches) {
267
+ if (branch.entry) {
268
+ collectVarsFromBranch(branch.entry, nodes, conditionalVars, vars)
269
+ }
270
+ }
271
+ }
272
+
273
+ // Check else branch
274
+ if (branchNode.elseEntry) {
275
+ collectVarsFromBranch(branchNode.elseEntry, nodes, conditionalVars, vars)
276
+ }
277
+
278
+ return vars
279
+ }
280
+
281
+ /**
282
+ * Recursively collect output variables from a branch that are in conditionalVars
283
+ */
284
+ function collectVarsFromBranch(
285
+ nodeId: string,
286
+ nodes: Record<string, SerializedGraphNode>,
287
+ conditionalVars: Set<string>,
288
+ result: string[]
289
+ ): void {
290
+ const node = nodes[nodeId]
291
+ if (!node) return
292
+
293
+ // Check if this node has an outputVar that's conditional
294
+ if ('outputVar' in node && node.outputVar) {
295
+ const varName = node.outputVar as string
296
+ if (conditionalVars.has(varName) && !result.includes(varName)) {
297
+ result.push(varName)
298
+ }
299
+ }
300
+
301
+ // Follow the chain of nodes within the branch
302
+ if ('next' in node && node.next) {
303
+ const nextId = node.next as string
304
+ // Only follow if it's still within the branch
305
+ if (isWithinBranch(nextId)) {
306
+ collectVarsFromBranch(nextId, nodes, conditionalVars, result)
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Check if a node ID is still within a branch (not the main flow)
313
+ */
314
+ function isWithinBranch(nodeId: string): boolean {
315
+ return (
316
+ nodeId.includes('_then_') ||
317
+ nodeId.includes('_else_') ||
318
+ nodeId.includes('_branch')
319
+ )
320
+ }
321
+
322
+ /**
323
+ * Generate code for branch content (then/else blocks)
324
+ */
325
+ function generateBranchContent(
326
+ entryNodeId: string,
327
+ nodes: Record<string, SerializedGraphNode>,
328
+ indent: string,
329
+ conditionalVars: Set<string>
330
+ ): string[] {
331
+ const lines: string[] = []
332
+ let currentId: string | undefined = entryNodeId
333
+
334
+ while (currentId) {
335
+ const node = nodes[currentId]
336
+ if (!node) break
337
+
338
+ const nodeLines = nodeToCode(node, nodes, indent, conditionalVars, true)
339
+ lines.push(...nodeLines)
340
+
341
+ // Follow to next node within the branch
342
+ if ('next' in node && node.next) {
343
+ const nextId = node.next as string
344
+ // Only continue if it's still within the branch
345
+ if (isWithinBranch(nextId)) {
346
+ currentId = nextId
347
+ } else {
348
+ break
349
+ }
350
+ } else {
351
+ break
352
+ }
353
+ }
354
+
355
+ return lines
356
+ }
357
+
358
+ /**
359
+ * Generate DSL code for a single node
360
+ */
361
+ function nodeToCode(
362
+ node: SerializedGraphNode,
363
+ nodes: Record<string, SerializedGraphNode>,
364
+ indent: string,
365
+ conditionalVars: Set<string> = new Set(),
366
+ isInsideBranch: boolean = false
367
+ ): string[] {
368
+ const lines: string[] = []
369
+
370
+ // Handle RPC nodes (function calls)
371
+ if ('rpcName' in node && node.rpcName && node.rpcName !== 'unknown') {
372
+ const stepName = node.stepName || `Call ${node.rpcName}`
373
+ const input = (node.input || {}) as Record<string, unknown>
374
+ const inputCode = inputToCode(input, indent)
375
+ const outputVar = (node as any).outputVar
376
+
377
+ let doCall = `await workflow.do('${stepName}', '${node.rpcName}', ${inputCode}`
378
+
379
+ // Add options if present
380
+ if ((node as any).options) {
381
+ const optCode = optionsToCode((node as any).options)
382
+ if (optCode) {
383
+ doCall += `, ${optCode}`
384
+ }
385
+ }
386
+ doCall += ')'
387
+
388
+ if (outputVar) {
389
+ // If this is a conditional var inside a branch, use assignment (let was declared above)
390
+ if (isInsideBranch && conditionalVars.has(outputVar)) {
391
+ lines.push(`${indent}${outputVar} = ${doCall}`)
392
+ } else {
393
+ lines.push(`${indent}const ${outputVar} = ${doCall}`)
394
+ }
395
+ } else {
396
+ lines.push(`${indent}${doCall}`)
397
+ }
398
+ lines.push('')
399
+ return lines
400
+ }
401
+
402
+ // Handle flow nodes
403
+ if ('flow' in node) {
404
+ const flowNode = node as any
405
+
406
+ switch (flowNode.flow) {
407
+ case 'sleep':
408
+ lines.push(
409
+ `${indent}await workflow.sleep('${flowNode.stepName || 'Sleep'}', '${flowNode.duration}')`
410
+ )
411
+ lines.push('')
412
+ break
413
+
414
+ case 'cancel':
415
+ const cancelReason =
416
+ flowNode.reason || flowNode.stepName || 'Workflow cancelled'
417
+ lines.push(
418
+ `${indent}throw new WorkflowCancelledException('${cancelReason}')`
419
+ )
420
+ lines.push('')
421
+ break
422
+
423
+ case 'branch':
424
+ // Declare conditional variables before the if statement
425
+ const branchConditionalVars = collectBranchConditionalVars(
426
+ flowNode,
427
+ nodes,
428
+ conditionalVars
429
+ )
430
+ for (const varName of branchConditionalVars) {
431
+ lines.push(`${indent}let ${varName}`)
432
+ }
433
+
434
+ // Generate if/else-if/else chain
435
+ const branches = flowNode.branches || []
436
+ for (let i = 0; i < branches.length; i++) {
437
+ const branch = branches[i]
438
+ const condition = conditionToCode(branch.condition)
439
+ const keyword = i === 0 ? 'if' : 'else if'
440
+ lines.push(`${indent}${keyword} (${condition}) {`)
441
+
442
+ if (branch.entry && nodes[branch.entry]) {
443
+ const branchLines = generateBranchContent(
444
+ branch.entry,
445
+ nodes,
446
+ indent + ' ',
447
+ conditionalVars
448
+ )
449
+ lines.push(...branchLines)
450
+ }
451
+ lines.push(`${indent}}`)
452
+ }
453
+
454
+ // Generate else block if present
455
+ if (flowNode.elseEntry && nodes[flowNode.elseEntry]) {
456
+ lines.push(`${indent}else {`)
457
+ const elseLines = generateBranchContent(
458
+ flowNode.elseEntry,
459
+ nodes,
460
+ indent + ' ',
461
+ conditionalVars
462
+ )
463
+ lines.push(...elseLines)
464
+ lines.push(`${indent}}`)
465
+ }
466
+ lines.push('')
467
+ break
468
+
469
+ case 'switch':
470
+ lines.push(`${indent}switch (${flowNode.expression}) {`)
471
+ for (const caseItem of flowNode.cases || []) {
472
+ lines.push(`${indent} case '${caseItem.value}':`)
473
+ if (caseItem.entry && nodes[caseItem.entry]) {
474
+ const caseLines = nodeToCode(
475
+ nodes[caseItem.entry],
476
+ nodes,
477
+ indent + ' '
478
+ )
479
+ lines.push(...caseLines)
480
+ }
481
+ lines.push(`${indent} break`)
482
+ }
483
+ if (flowNode.defaultEntry && nodes[flowNode.defaultEntry]) {
484
+ lines.push(`${indent} default:`)
485
+ const defaultLines = nodeToCode(
486
+ nodes[flowNode.defaultEntry],
487
+ nodes,
488
+ indent + ' '
489
+ )
490
+ lines.push(...defaultLines)
491
+ lines.push(`${indent} break`)
492
+ }
493
+ lines.push(`${indent}}`)
494
+ lines.push('')
495
+ break
496
+
497
+ case 'parallel':
498
+ lines.push(`${indent}await Promise.all([`)
499
+ for (const childId of flowNode.children || []) {
500
+ if (nodes[childId]) {
501
+ const childNode = nodes[childId]
502
+ if ('rpcName' in childNode && childNode.rpcName) {
503
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`
504
+ const input = (childNode.input || {}) as Record<string, unknown>
505
+ const inputCode = inputToCode(input, indent + ' ')
506
+ lines.push(
507
+ `${indent} workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode}),`
508
+ )
509
+ }
510
+ }
511
+ }
512
+ lines.push(`${indent}])`)
513
+ lines.push('')
514
+ break
515
+
516
+ case 'fanout':
517
+ if (flowNode.mode === 'parallel') {
518
+ lines.push(`${indent}await Promise.all(`)
519
+ lines.push(
520
+ `${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar}) =>`
521
+ )
522
+ if (flowNode.childEntry && nodes[flowNode.childEntry]) {
523
+ const childNode = nodes[flowNode.childEntry]
524
+ if ('rpcName' in childNode && childNode.rpcName) {
525
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`
526
+ const input = (childNode.input || {}) as Record<string, unknown>
527
+ const inputCode = inputToCode(
528
+ input,
529
+ indent + ' ',
530
+ flowNode.itemVar
531
+ )
532
+ lines.push(
533
+ `${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
534
+ )
535
+ }
536
+ }
537
+ lines.push(`${indent} )`)
538
+ lines.push(`${indent})`)
539
+ } else {
540
+ // Sequential fanout
541
+ lines.push(
542
+ `${indent}for (const ${flowNode.itemVar} of ${flowNode.sourceVar}) {`
543
+ )
544
+ if (flowNode.childEntry && nodes[flowNode.childEntry]) {
545
+ const childNode = nodes[flowNode.childEntry]
546
+ if ('rpcName' in childNode && childNode.rpcName) {
547
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`
548
+ const input = (childNode.input || {}) as Record<string, unknown>
549
+ const inputCode = inputToCode(
550
+ input,
551
+ indent + ' ',
552
+ flowNode.itemVar
553
+ )
554
+ lines.push(
555
+ `${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
556
+ )
557
+ }
558
+ }
559
+ if (flowNode.timeBetween) {
560
+ lines.push(
561
+ `${indent} await workflow.sleep('Wait between iterations', '${flowNode.timeBetween}')`
562
+ )
563
+ }
564
+ lines.push(`${indent}}`)
565
+ }
566
+ lines.push('')
567
+ break
568
+
569
+ case 'filter':
570
+ lines.push(
571
+ `${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.filter((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
572
+ )
573
+ lines.push('')
574
+ break
575
+
576
+ case 'arrayPredicate':
577
+ const method = flowNode.mode === 'some' ? 'some' : 'every'
578
+ lines.push(
579
+ `${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.${method}((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
580
+ )
581
+ lines.push('')
582
+ break
583
+
584
+ case 'return':
585
+ if (flowNode.outputs) {
586
+ const returnObj: string[] = []
587
+ for (const [key, output] of Object.entries(
588
+ flowNode.outputs as Record<string, any>
589
+ )) {
590
+ let value: string
591
+ if (output.from === 'outputVar') {
592
+ value = output.path
593
+ ? `${output.name}?.${output.path}`
594
+ : output.name
595
+ } else if (output.from === 'stateVar') {
596
+ value = output.path
597
+ ? `${output.name}.${output.path}`
598
+ : output.name
599
+ } else if (output.from === 'input') {
600
+ value = `data.${output.path}`
601
+ } else if (output.from === 'literal') {
602
+ value = JSON.stringify(output.value)
603
+ } else if (output.from === 'expression') {
604
+ value = output.expression
605
+ } else {
606
+ continue
607
+ }
608
+ returnObj.push(`${indent} ${key}: ${value},`)
609
+ }
610
+ if (returnObj.length > 0) {
611
+ lines.push(`${indent}return {`)
612
+ lines.push(...returnObj)
613
+ lines.push(`${indent}}`)
614
+ }
615
+ }
616
+ break
617
+
618
+ case 'inline':
619
+ lines.push(
620
+ `${indent}// Inline step: ${flowNode.stepName || 'Dynamic code'}`
621
+ )
622
+ lines.push(`${indent}// ${flowNode.description || '<dynamic code>'}`)
623
+ lines.push('')
624
+ break
625
+
626
+ case 'set':
627
+ // Generate variable assignment: varName = value
628
+ const setVar = flowNode.variable
629
+ const setValue =
630
+ typeof flowNode.value === 'string'
631
+ ? `'${flowNode.value}'`
632
+ : JSON.stringify(flowNode.value)
633
+ lines.push(`${indent}${setVar} = ${setValue}`)
634
+ lines.push('')
635
+ break
636
+ }
637
+ }
638
+
639
+ return lines
640
+ }
641
+
642
+ /**
643
+ * Find variables that are defined inside branches but used in return statements
644
+ * These need to be hoisted with `let` declarations
645
+ */
646
+ function findConditionalVars(
647
+ nodes: Record<string, SerializedGraphNode>
648
+ ): Set<string> {
649
+ const conditionalVars = new Set<string>()
650
+ const varsInBranches = new Set<string>()
651
+ const varsUsedInReturn = new Set<string>()
652
+
653
+ // Collect variables defined in branches (then/else/case/default nodes)
654
+ for (const [nodeId, node] of Object.entries(nodes)) {
655
+ if (
656
+ nodeId.includes('_then_') ||
657
+ nodeId.includes('_else_') ||
658
+ nodeId.includes('_case') ||
659
+ nodeId.includes('_default_')
660
+ ) {
661
+ if ('outputVar' in node && node.outputVar) {
662
+ varsInBranches.add(node.outputVar as string)
663
+ }
664
+ }
665
+ }
666
+
667
+ // Collect variables used in return statements
668
+ for (const node of Object.values(nodes)) {
669
+ if ('flow' in node && node.flow === 'return' && node.outputs) {
670
+ for (const output of Object.values(
671
+ node.outputs as Record<string, { from: string; name?: string }>
672
+ )) {
673
+ if (output.from === 'outputVar' && output.name) {
674
+ varsUsedInReturn.add(output.name)
675
+ }
676
+ }
677
+ }
678
+ }
679
+
680
+ // Variables that are both in branches and used in return need hoisting
681
+ for (const varName of varsInBranches) {
682
+ if (varsUsedInReturn.has(varName)) {
683
+ conditionalVars.add(varName)
684
+ }
685
+ }
686
+
687
+ return conditionalVars
688
+ }
689
+
690
+ /**
691
+ * Get default value for a context variable type
692
+ */
693
+ function getDefaultForType(type: string): string {
694
+ switch (type) {
695
+ case 'string':
696
+ return "''"
697
+ case 'number':
698
+ return '0'
699
+ case 'boolean':
700
+ return 'false'
701
+ case 'array':
702
+ return '[]'
703
+ case 'object':
704
+ return '{}'
705
+ default:
706
+ return 'undefined'
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Deserialize a workflow graph to DSL code
712
+ */
713
+ export function deserializeDslWorkflow(
714
+ workflow: SerializedWorkflowGraph,
715
+ options: DeserializeOptions = {}
716
+ ): string {
717
+ const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } =
718
+ options
719
+
720
+ const lines: string[] = []
721
+
722
+ // Check if workflow has any cancel nodes
723
+ const hasCancelNode = Object.values(workflow.nodes).some(
724
+ (node) => 'flow' in node && (node as any).flow === 'cancel'
725
+ )
726
+
727
+ // Find variables defined in branches that need hoisting
728
+ const conditionalVars = findConditionalVars(workflow.nodes)
729
+
730
+ // Import statement
731
+ if (hasCancelNode) {
732
+ lines.push(
733
+ `import { pikkuWorkflowFunc, WorkflowCancelledException } from '${pikkuImportPath}'`
734
+ )
735
+ } else {
736
+ lines.push(`import { pikkuWorkflowFunc } from '${pikkuImportPath}'`)
737
+ }
738
+ lines.push('')
739
+
740
+ // Add description as comment if present
741
+ if (workflow.description) {
742
+ lines.push(`/**`)
743
+ lines.push(` * ${workflow.description}`)
744
+ lines.push(` */`)
745
+ }
746
+
747
+ // Function signature
748
+ const tagsComment = workflow.tags?.length
749
+ ? ` // tags: ${workflow.tags.join(', ')}`
750
+ : ''
751
+
752
+ lines.push(
753
+ `export const ${workflow.name} = pikkuWorkflowFunc(async ({}, data, { workflow }) => {${tagsComment}`
754
+ )
755
+
756
+ // Generate context variable declarations at the top
757
+ if (workflow.context && Object.keys(workflow.context).length > 0) {
758
+ for (const [varName, varDef] of Object.entries(workflow.context) as [
759
+ string,
760
+ ContextVariable,
761
+ ][]) {
762
+ const defaultValue =
763
+ varDef.default !== undefined
764
+ ? typeof varDef.default === 'string'
765
+ ? `'${varDef.default}'`
766
+ : JSON.stringify(varDef.default)
767
+ : getDefaultForType(varDef.type)
768
+ lines.push(` let ${varName} = ${defaultValue}`)
769
+ }
770
+ lines.push('')
771
+ }
772
+
773
+ // Process nodes in order
774
+ const orderedNodes = traverseNodes(workflow.nodes, workflow.entryNodeIds)
775
+
776
+ for (const node of orderedNodes) {
777
+ // Skip child nodes that are processed as part of their parent
778
+ if (
779
+ node.nodeId.includes('_then_') ||
780
+ node.nodeId.includes('_else_') ||
781
+ node.nodeId.includes('_case') ||
782
+ node.nodeId.includes('_default_') ||
783
+ node.nodeId.includes('_child_') ||
784
+ node.nodeId.includes('_item_')
785
+ ) {
786
+ continue
787
+ }
788
+
789
+ const nodeLines = nodeToCode(node, workflow.nodes, ' ', conditionalVars)
790
+ lines.push(...nodeLines)
791
+ }
792
+
793
+ lines.push('})')
794
+ lines.push('')
795
+
796
+ return lines.join('\n')
797
+ }
798
+
799
+ /**
800
+ * Convert a DataRef to graph ref() call
801
+ * @param ref - The data reference
802
+ * @param outputVarToNodeId - Map from outputVar names to node IDs
803
+ */
804
+ function dataRefToGraphRef(
805
+ ref: DataRef,
806
+ outputVarToNodeId: Map<string, string>
807
+ ): string {
808
+ // Convert outputVar reference to nodeId reference
809
+ const nodeId = outputVarToNodeId.get(ref.$ref) || ref.$ref
810
+ if (ref.path) {
811
+ return `ref('${nodeId}', '${ref.path}')`
812
+ }
813
+ return `ref('${nodeId}')`
814
+ }
815
+
816
+ /**
817
+ * Convert a template ref to template() function call for graph code
818
+ * e.g. {$template: {parts: ["Hello ", ""], expressions: [{$ref: "trigger", path: "name"}]}}
819
+ * becomes: template('Hello $0', [ref('trigger', 'name')])
820
+ */
821
+ function templateRefToGraphCode(
822
+ tmpl: TemplateRef,
823
+ outputVarToNodeId: Map<string, string>
824
+ ): string {
825
+ const { parts, expressions } = tmpl.$template
826
+
827
+ // Build the template string with $0, $1, etc. placeholders
828
+ let templateStr = ''
829
+ for (let i = 0; i < parts.length; i++) {
830
+ templateStr += parts[i]
831
+ if (i < expressions.length) {
832
+ templateStr += `$${i}`
833
+ }
834
+ }
835
+
836
+ // Build the refs array
837
+ const refs: string[] = []
838
+ for (const expr of expressions) {
839
+ if (isDataRef(expr)) {
840
+ refs.push(dataRefToGraphRef(expr, outputVarToNodeId))
841
+ } else {
842
+ // Literal JS expression - can't be represented as a typed ref
843
+ refs.push(`{ $ref: '${String(expr).replace(/'/g, "\\'")}' } as any`)
844
+ }
845
+ }
846
+
847
+ // Escape single quotes and newlines in the template string
848
+ templateStr = templateStr
849
+ .replace(/\\/g, '\\\\')
850
+ .replace(/'/g, "\\'")
851
+ .replace(/\n/g, '\\n')
852
+ .replace(/\r/g, '\\r')
853
+
854
+ return `template('${templateStr}', [${refs.join(', ')}])`
855
+ }
856
+
857
+ function valueToGraphCode(
858
+ value: unknown,
859
+ outputVarToNodeId: Map<string, string>,
860
+ refTracker: { hasRefs: boolean }
861
+ ): string {
862
+ if (isDataRef(value)) {
863
+ refTracker.hasRefs = true
864
+ return dataRefToGraphRef(value, outputVarToNodeId)
865
+ }
866
+ if (isTemplateRef(value)) {
867
+ refTracker.hasRefs = true
868
+ return templateRefToGraphCode(value, outputVarToNodeId)
869
+ }
870
+ if (Array.isArray(value)) {
871
+ const elements = value.map((v) =>
872
+ valueToGraphCode(v, outputVarToNodeId, refTracker)
873
+ )
874
+ return `[${elements.join(', ')}]`
875
+ }
876
+ if (typeof value === 'object' && value !== null) {
877
+ const entries = Object.entries(value)
878
+ const props = entries.map(
879
+ ([k, v]) => `${k}: ${valueToGraphCode(v, outputVarToNodeId, refTracker)}`
880
+ )
881
+ return `{ ${props.join(', ')} }`
882
+ }
883
+ return JSON.stringify(value)
884
+ }
885
+
886
+ /**
887
+ * Convert input object to graph input code using ref()
888
+ * @param input - The input mapping
889
+ * @param outputVarToNodeId - Map from outputVar names to node IDs
890
+ */
891
+ function inputToGraphCode(
892
+ input: Record<string, unknown>,
893
+ outputVarToNodeId: Map<string, string>
894
+ ): {
895
+ hasRefs: boolean
896
+ code: string
897
+ } {
898
+ const entries = Object.entries(input)
899
+ if (entries.length === 0) return { hasRefs: false, code: '{}' }
900
+
901
+ const refTracker = { hasRefs: false }
902
+ const lines = entries.map(([key, value]) => {
903
+ return ` ${key}: ${valueToGraphCode(value, outputVarToNodeId, refTracker)},`
904
+ })
905
+
906
+ return {
907
+ hasRefs: refTracker.hasRefs,
908
+ code: `{\n${lines.join('\n')}\n }`,
909
+ }
910
+ }
911
+
912
+ /**
913
+ * Check if a node is a flow node (non-RPC control flow)
914
+ */
915
+ function isFlowNode(node: any): boolean {
916
+ return 'flow' in node
917
+ }
918
+
919
+ /**
920
+ * Follow through flow nodes to find the next RPC node
921
+ * This traverses the 'next' chain, skipping flow nodes until finding an RPC node
922
+ */
923
+ function findNextRpcNode(
924
+ startNextId: string,
925
+ nodes: Record<string, SerializedGraphNode>,
926
+ flowNodeIds: Set<string>,
927
+ visited: Set<string> = new Set()
928
+ ): string | null {
929
+ if (visited.has(startNextId)) {
930
+ return null // Cycle detected, stop
931
+ }
932
+ visited.add(startNextId)
933
+
934
+ // If it's not a flow node, we found our target
935
+ if (!flowNodeIds.has(startNextId)) {
936
+ // Make sure the node exists and has an rpcName (is an RPC node)
937
+ const node = nodes[startNextId]
938
+ if (node && 'rpcName' in node) {
939
+ return startNextId
940
+ }
941
+ return null
942
+ }
943
+
944
+ // It's a flow node - follow its 'next' if it has one
945
+ const flowNode = nodes[startNextId]
946
+ if (flowNode && 'next' in flowNode && flowNode.next) {
947
+ return findNextRpcNode(flowNode.next as string, nodes, flowNodeIds, visited)
948
+ }
949
+
950
+ return null
951
+ }
952
+
953
+ /**
954
+ * Deserialize a graph workflow to pikkuWorkflowGraph code
955
+ */
956
+ export function deserializeGraphWorkflow(
957
+ workflow: SerializedWorkflowGraph,
958
+ options: DeserializeOptions = {}
959
+ ): string {
960
+ const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } =
961
+ options
962
+
963
+ const lines: string[] = []
964
+
965
+ // Import statement
966
+ lines.push(`import { pikkuWorkflowGraph } from '${pikkuImportPath}'`)
967
+ lines.push('')
968
+
969
+ // Add description as comment if present
970
+ if (workflow.description) {
971
+ lines.push(`/**`)
972
+ lines.push(` * ${workflow.description}`)
973
+ lines.push(` */`)
974
+ }
975
+
976
+ // Identify flow nodes (non-RPC nodes like return, sleep, branch)
977
+ const flowNodeIds = new Set<string>()
978
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
979
+ if (isFlowNode(node)) {
980
+ flowNodeIds.add(nodeId)
981
+ }
982
+ }
983
+
984
+ // Build outputVar to nodeId mapping (for resolving variable references to node IDs)
985
+ const outputVarToNodeId = new Map<string, string>()
986
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
987
+ if ('outputVar' in node && typeof node.outputVar === 'string') {
988
+ outputVarToNodeId.set(node.outputVar, nodeId)
989
+ }
990
+ }
991
+
992
+ // Build node to RPC mapping (only RPC nodes, not flow nodes)
993
+ const nodeRpcMap: Record<string, string> = {}
994
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
995
+ if (
996
+ 'rpcName' in node &&
997
+ typeof node.rpcName === 'string' &&
998
+ node.rpcName !== 'unknown'
999
+ ) {
1000
+ nodeRpcMap[nodeId] = node.rpcName
1001
+ }
1002
+ }
1003
+
1004
+ // Build node configurations (only for RPC nodes)
1005
+ const nodeConfigs: string[] = []
1006
+
1007
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
1008
+ // Skip flow nodes - they can't be represented in pikkuWorkflowGraph
1009
+ if (flowNodeIds.has(nodeId)) {
1010
+ continue
1011
+ }
1012
+
1013
+ const configParts: string[] = []
1014
+
1015
+ // Add next if present - follow through flow nodes to find the actual next RPC node
1016
+ if ('next' in node && node.next) {
1017
+ const nextId = node.next as string
1018
+ // If next points to a flow node, follow through to find the next RPC node
1019
+ const actualNextId = flowNodeIds.has(nextId)
1020
+ ? findNextRpcNode(nextId, workflow.nodes, flowNodeIds)
1021
+ : nextId
1022
+ // Only add if we found a valid next RPC node
1023
+ if (actualNextId && !flowNodeIds.has(actualNextId)) {
1024
+ configParts.push(`next: '${actualNextId}'`)
1025
+ }
1026
+ }
1027
+
1028
+ // Add input if present
1029
+ // Always use callback form to avoid excess property checking in TypeScript
1030
+ if ('input' in node && node.input) {
1031
+ const input = node.input as Record<string, unknown>
1032
+ if (Object.keys(input).length > 0) {
1033
+ const { hasRefs, code } = inputToGraphCode(input, outputVarToNodeId)
1034
+ if (hasRefs) {
1035
+ // Always pass both ref and template for consistent type signature
1036
+ configParts.push(`input: (ref, template) => (${code})`)
1037
+ } else {
1038
+ // Wrap in callback to avoid TypeScript excess property checking
1039
+ configParts.push(`input: () => (${code})`)
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ if (configParts.length > 0) {
1045
+ nodeConfigs.push(
1046
+ ` ${nodeId}: {\n ${configParts.join(',\n ')},\n }`
1047
+ )
1048
+ }
1049
+ }
1050
+
1051
+ // Generate the pikkuWorkflowGraph call (builds graph and registers with core)
1052
+ lines.push(`export const ${workflow.name} = pikkuWorkflowGraph({`)
1053
+ lines.push(` name: '${workflow.name}',`)
1054
+ if (workflow.description) {
1055
+ lines.push(` description: '${workflow.description}',`)
1056
+ }
1057
+ if (workflow.tags && workflow.tags.length > 0) {
1058
+ lines.push(` tags: [${workflow.tags.map((t) => `'${t}'`).join(', ')}],`)
1059
+ }
1060
+
1061
+ // Generate nodes (RPC mapping)
1062
+ const rpcMapEntries = Object.entries(nodeRpcMap)
1063
+ if (rpcMapEntries.length > 0) {
1064
+ lines.push(` nodes: {`)
1065
+ for (const [nodeId, rpcName] of rpcMapEntries) {
1066
+ lines.push(` ${nodeId}: '${rpcName}',`)
1067
+ }
1068
+ lines.push(` },`)
1069
+ } else {
1070
+ lines.push(` nodes: {},`)
1071
+ }
1072
+
1073
+ // Generate config (node configurations)
1074
+ if (nodeConfigs.length > 0) {
1075
+ lines.push(` config: {`)
1076
+ lines.push(nodeConfigs.join(',\n'))
1077
+ lines.push(` },`)
1078
+ }
1079
+
1080
+ lines.push(`})`)
1081
+ lines.push('')
1082
+
1083
+ return lines.join('\n')
1084
+ }
1085
+
1086
+ /**
1087
+ * Deserialize all workflows from JSON to DSL code
1088
+ */
1089
+ export function deserializeAllDslWorkflows(
1090
+ workflows: Record<string, SerializedWorkflowGraph>,
1091
+ options: DeserializeOptions = {}
1092
+ ): Record<string, string> {
1093
+ const result: Record<string, string> = {}
1094
+
1095
+ for (const [name, workflow] of Object.entries(workflows)) {
1096
+ if (workflow.source === 'dsl') {
1097
+ result[name] = deserializeDslWorkflow(workflow, options)
1098
+ } else if (workflow.source === 'graph') {
1099
+ result[name] = deserializeGraphWorkflow(workflow, options)
1100
+ }
1101
+ }
1102
+
1103
+ return result
1104
+ }