@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,830 @@
1
+ /**
2
+ * Deserialize workflow JSON back to DSL code
3
+ * Converts the serialized workflow graph format back to TypeScript DSL code
4
+ */
5
+ /**
6
+ * Check if a value is a DataRef
7
+ */
8
+ function isDataRef(value) {
9
+ return (typeof value === 'object' &&
10
+ value !== null &&
11
+ '$ref' in value &&
12
+ typeof value.$ref === 'string');
13
+ }
14
+ /**
15
+ * Check if a value is a StateRef
16
+ */
17
+ function isStateRef(value) {
18
+ return (typeof value === 'object' &&
19
+ value !== null &&
20
+ '$state' in value &&
21
+ typeof value.$state === 'string');
22
+ }
23
+ function isTemplateRef(value) {
24
+ return (typeof value === 'object' &&
25
+ value !== null &&
26
+ '$template' in value &&
27
+ typeof value.$template === 'object');
28
+ }
29
+ /**
30
+ * Convert a DataRef to code expression
31
+ */
32
+ function dataRefToCode(ref, itemVar) {
33
+ if (ref.$ref === 'trigger') {
34
+ // Reference to trigger input (data)
35
+ return ref.path ? `data.${ref.path}` : 'data';
36
+ }
37
+ if (ref.$ref === '$item') {
38
+ // Reference to the current loop item
39
+ // The path contains the variable name in this case
40
+ return ref.path || itemVar || 'item';
41
+ }
42
+ // Reference to a step output variable
43
+ return ref.path ? `${ref.$ref}.${ref.path}` : ref.$ref;
44
+ }
45
+ /**
46
+ * Convert a template ref to template literal code
47
+ */
48
+ function templateRefToCode(template, itemVar) {
49
+ const { parts, expressions } = template.$template;
50
+ let result = '`';
51
+ for (let i = 0; i < parts.length; i++) {
52
+ result += parts[i];
53
+ if (i < expressions.length) {
54
+ const expr = expressions[i];
55
+ let exprCode;
56
+ if (isDataRef(expr)) {
57
+ exprCode = dataRefToCode(expr, itemVar);
58
+ }
59
+ else if (isTemplateRef(expr)) {
60
+ // Nested template (unlikely but handle it)
61
+ exprCode = templateRefToCode(expr, itemVar);
62
+ }
63
+ else {
64
+ // Literal value
65
+ exprCode = String(expr);
66
+ }
67
+ result += '${' + exprCode + '}';
68
+ }
69
+ }
70
+ result += '`';
71
+ return result;
72
+ }
73
+ /**
74
+ * Convert a StateRef to code expression
75
+ */
76
+ function stateRefToCode(ref) {
77
+ return ref.path ? `${ref.$state}.${ref.path}` : ref.$state;
78
+ }
79
+ /**
80
+ * Convert a single value to code (handles refs, templates, state refs, and literals)
81
+ */
82
+ function valueToCode(value, itemVar) {
83
+ if (isDataRef(value)) {
84
+ return dataRefToCode(value, itemVar);
85
+ }
86
+ if (isStateRef(value)) {
87
+ return stateRefToCode(value);
88
+ }
89
+ if (isTemplateRef(value)) {
90
+ return templateRefToCode(value, itemVar);
91
+ }
92
+ if (Array.isArray(value)) {
93
+ const elements = value.map((v) => valueToCode(v, itemVar));
94
+ return `[${elements.join(', ')}]`;
95
+ }
96
+ if (typeof value === 'object' && value !== null) {
97
+ const entries = Object.entries(value);
98
+ const props = entries.map(([k, v]) => `${k}: ${valueToCode(v, itemVar)}`);
99
+ return `{ ${props.join(', ')} }`;
100
+ }
101
+ return JSON.stringify(value);
102
+ }
103
+ /**
104
+ * Check if input represents passthrough (entire data object)
105
+ */
106
+ function isPassthrough(input) {
107
+ if (Object.keys(input).length === 1 && '$passthrough' in input) {
108
+ const passthrough = input.$passthrough;
109
+ return isDataRef(passthrough) && passthrough.$ref === 'trigger';
110
+ }
111
+ return false;
112
+ }
113
+ /**
114
+ * Convert input object to code
115
+ */
116
+ function inputToCode(input, indent, itemVar) {
117
+ // Check if this is a passthrough (entire data object)
118
+ if (isPassthrough(input)) {
119
+ return 'data';
120
+ }
121
+ const entries = Object.entries(input);
122
+ if (entries.length === 0)
123
+ return '{}';
124
+ const lines = entries.map(([key, value]) => {
125
+ return `${indent} ${key}: ${valueToCode(value, itemVar)},`;
126
+ });
127
+ return `{\n${lines.join('\n')}\n${indent}}`;
128
+ }
129
+ /**
130
+ * Convert options to code
131
+ */
132
+ function optionsToCode(options) {
133
+ const parts = [];
134
+ if (options.retries !== undefined) {
135
+ parts.push(`retries: ${options.retries}`);
136
+ }
137
+ if (options.retryDelay !== undefined) {
138
+ parts.push(`retryDelay: '${options.retryDelay}'`);
139
+ }
140
+ return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
141
+ }
142
+ /**
143
+ * Convert a simple condition to code expression
144
+ */
145
+ function conditionToCode(condition) {
146
+ if (!condition)
147
+ return 'true';
148
+ if (condition.type === 'simple') {
149
+ return condition.expression;
150
+ }
151
+ if (condition.type === 'and') {
152
+ const parts = condition.conditions.map(conditionToCode);
153
+ return parts.length > 1 ? `(${parts.join(' && ')})` : parts[0];
154
+ }
155
+ if (condition.type === 'or') {
156
+ const parts = condition.conditions.map(conditionToCode);
157
+ return parts.length > 1 ? `(${parts.join(' || ')})` : parts[0];
158
+ }
159
+ return 'true';
160
+ }
161
+ /**
162
+ * Traverse nodes in execution order starting from entry
163
+ */
164
+ function traverseNodes(nodes, entryNodeIds) {
165
+ const result = [];
166
+ const visited = new Set();
167
+ function visit(nodeId) {
168
+ if (visited.has(nodeId))
169
+ return;
170
+ visited.add(nodeId);
171
+ const node = nodes[nodeId];
172
+ if (!node)
173
+ return;
174
+ result.push(node);
175
+ // Follow next pointer
176
+ if ('next' in node && node.next) {
177
+ if (typeof node.next === 'string') {
178
+ visit(node.next);
179
+ }
180
+ }
181
+ }
182
+ for (const entryId of entryNodeIds) {
183
+ visit(entryId);
184
+ }
185
+ return result;
186
+ }
187
+ /**
188
+ * Collect conditional variables that need to be declared before a branch
189
+ */
190
+ function collectBranchConditionalVars(branchNode, nodes, conditionalVars) {
191
+ const vars = [];
192
+ // Check all branches (if/else-if chain)
193
+ if (branchNode.branches) {
194
+ for (const branch of branchNode.branches) {
195
+ if (branch.entry) {
196
+ collectVarsFromBranch(branch.entry, nodes, conditionalVars, vars);
197
+ }
198
+ }
199
+ }
200
+ // Check else branch
201
+ if (branchNode.elseEntry) {
202
+ collectVarsFromBranch(branchNode.elseEntry, nodes, conditionalVars, vars);
203
+ }
204
+ return vars;
205
+ }
206
+ /**
207
+ * Recursively collect output variables from a branch that are in conditionalVars
208
+ */
209
+ function collectVarsFromBranch(nodeId, nodes, conditionalVars, result) {
210
+ const node = nodes[nodeId];
211
+ if (!node)
212
+ return;
213
+ // Check if this node has an outputVar that's conditional
214
+ if ('outputVar' in node && node.outputVar) {
215
+ const varName = node.outputVar;
216
+ if (conditionalVars.has(varName) && !result.includes(varName)) {
217
+ result.push(varName);
218
+ }
219
+ }
220
+ // Follow the chain of nodes within the branch
221
+ if ('next' in node && node.next) {
222
+ const nextId = node.next;
223
+ // Only follow if it's still within the branch
224
+ if (isWithinBranch(nextId)) {
225
+ collectVarsFromBranch(nextId, nodes, conditionalVars, result);
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Check if a node ID is still within a branch (not the main flow)
231
+ */
232
+ function isWithinBranch(nodeId) {
233
+ return (nodeId.includes('_then_') ||
234
+ nodeId.includes('_else_') ||
235
+ nodeId.includes('_branch'));
236
+ }
237
+ /**
238
+ * Generate code for branch content (then/else blocks)
239
+ */
240
+ function generateBranchContent(entryNodeId, nodes, indent, conditionalVars) {
241
+ const lines = [];
242
+ let currentId = entryNodeId;
243
+ while (currentId) {
244
+ const node = nodes[currentId];
245
+ if (!node)
246
+ break;
247
+ const nodeLines = nodeToCode(node, nodes, indent, conditionalVars, true);
248
+ lines.push(...nodeLines);
249
+ // Follow to next node within the branch
250
+ if ('next' in node && node.next) {
251
+ const nextId = node.next;
252
+ // Only continue if it's still within the branch
253
+ if (isWithinBranch(nextId)) {
254
+ currentId = nextId;
255
+ }
256
+ else {
257
+ break;
258
+ }
259
+ }
260
+ else {
261
+ break;
262
+ }
263
+ }
264
+ return lines;
265
+ }
266
+ /**
267
+ * Generate DSL code for a single node
268
+ */
269
+ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBranch = false) {
270
+ const lines = [];
271
+ // Handle RPC nodes (function calls)
272
+ if ('rpcName' in node && node.rpcName && node.rpcName !== 'unknown') {
273
+ const stepName = node.stepName || `Call ${node.rpcName}`;
274
+ const input = (node.input || {});
275
+ const inputCode = inputToCode(input, indent);
276
+ const outputVar = node.outputVar;
277
+ let doCall = `await workflow.do('${stepName}', '${node.rpcName}', ${inputCode}`;
278
+ // Add options if present
279
+ if (node.options) {
280
+ const optCode = optionsToCode(node.options);
281
+ if (optCode) {
282
+ doCall += `, ${optCode}`;
283
+ }
284
+ }
285
+ doCall += ')';
286
+ if (outputVar) {
287
+ // If this is a conditional var inside a branch, use assignment (let was declared above)
288
+ if (isInsideBranch && conditionalVars.has(outputVar)) {
289
+ lines.push(`${indent}${outputVar} = ${doCall}`);
290
+ }
291
+ else {
292
+ lines.push(`${indent}const ${outputVar} = ${doCall}`);
293
+ }
294
+ }
295
+ else {
296
+ lines.push(`${indent}${doCall}`);
297
+ }
298
+ lines.push('');
299
+ return lines;
300
+ }
301
+ // Handle flow nodes
302
+ if ('flow' in node) {
303
+ const flowNode = node;
304
+ switch (flowNode.flow) {
305
+ case 'sleep':
306
+ lines.push(`${indent}await workflow.sleep('${flowNode.stepName || 'Sleep'}', '${flowNode.duration}')`);
307
+ lines.push('');
308
+ break;
309
+ case 'cancel':
310
+ const cancelReason = flowNode.reason || flowNode.stepName || 'Workflow cancelled';
311
+ lines.push(`${indent}throw new WorkflowCancelledException('${cancelReason}')`);
312
+ lines.push('');
313
+ break;
314
+ case 'branch':
315
+ // Declare conditional variables before the if statement
316
+ const branchConditionalVars = collectBranchConditionalVars(flowNode, nodes, conditionalVars);
317
+ for (const varName of branchConditionalVars) {
318
+ lines.push(`${indent}let ${varName}`);
319
+ }
320
+ // Generate if/else-if/else chain
321
+ const branches = flowNode.branches || [];
322
+ for (let i = 0; i < branches.length; i++) {
323
+ const branch = branches[i];
324
+ const condition = conditionToCode(branch.condition);
325
+ const keyword = i === 0 ? 'if' : 'else if';
326
+ lines.push(`${indent}${keyword} (${condition}) {`);
327
+ if (branch.entry && nodes[branch.entry]) {
328
+ const branchLines = generateBranchContent(branch.entry, nodes, indent + ' ', conditionalVars);
329
+ lines.push(...branchLines);
330
+ }
331
+ lines.push(`${indent}}`);
332
+ }
333
+ // Generate else block if present
334
+ if (flowNode.elseEntry && nodes[flowNode.elseEntry]) {
335
+ lines.push(`${indent}else {`);
336
+ const elseLines = generateBranchContent(flowNode.elseEntry, nodes, indent + ' ', conditionalVars);
337
+ lines.push(...elseLines);
338
+ lines.push(`${indent}}`);
339
+ }
340
+ lines.push('');
341
+ break;
342
+ case 'switch':
343
+ lines.push(`${indent}switch (${flowNode.expression}) {`);
344
+ for (const caseItem of flowNode.cases || []) {
345
+ lines.push(`${indent} case '${caseItem.value}':`);
346
+ if (caseItem.entry && nodes[caseItem.entry]) {
347
+ const caseLines = nodeToCode(nodes[caseItem.entry], nodes, indent + ' ');
348
+ lines.push(...caseLines);
349
+ }
350
+ lines.push(`${indent} break`);
351
+ }
352
+ if (flowNode.defaultEntry && nodes[flowNode.defaultEntry]) {
353
+ lines.push(`${indent} default:`);
354
+ const defaultLines = nodeToCode(nodes[flowNode.defaultEntry], nodes, indent + ' ');
355
+ lines.push(...defaultLines);
356
+ lines.push(`${indent} break`);
357
+ }
358
+ lines.push(`${indent}}`);
359
+ lines.push('');
360
+ break;
361
+ case 'parallel':
362
+ lines.push(`${indent}await Promise.all([`);
363
+ for (const childId of flowNode.children || []) {
364
+ if (nodes[childId]) {
365
+ const childNode = nodes[childId];
366
+ if ('rpcName' in childNode && childNode.rpcName) {
367
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
368
+ const input = (childNode.input || {});
369
+ const inputCode = inputToCode(input, indent + ' ');
370
+ lines.push(`${indent} workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode}),`);
371
+ }
372
+ }
373
+ }
374
+ lines.push(`${indent}])`);
375
+ lines.push('');
376
+ break;
377
+ case 'fanout':
378
+ if (flowNode.mode === 'parallel') {
379
+ lines.push(`${indent}await Promise.all(`);
380
+ lines.push(`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar}) =>`);
381
+ if (flowNode.childEntry && nodes[flowNode.childEntry]) {
382
+ const childNode = nodes[flowNode.childEntry];
383
+ if ('rpcName' in childNode && childNode.rpcName) {
384
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
385
+ const input = (childNode.input || {});
386
+ const inputCode = inputToCode(input, indent + ' ', flowNode.itemVar);
387
+ lines.push(`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`);
388
+ }
389
+ }
390
+ lines.push(`${indent} )`);
391
+ lines.push(`${indent})`);
392
+ }
393
+ else {
394
+ // Sequential fanout
395
+ lines.push(`${indent}for (const ${flowNode.itemVar} of ${flowNode.sourceVar}) {`);
396
+ if (flowNode.childEntry && nodes[flowNode.childEntry]) {
397
+ const childNode = nodes[flowNode.childEntry];
398
+ if ('rpcName' in childNode && childNode.rpcName) {
399
+ const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
400
+ const input = (childNode.input || {});
401
+ const inputCode = inputToCode(input, indent + ' ', flowNode.itemVar);
402
+ lines.push(`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`);
403
+ }
404
+ }
405
+ if (flowNode.timeBetween) {
406
+ lines.push(`${indent} await workflow.sleep('Wait between iterations', '${flowNode.timeBetween}')`);
407
+ }
408
+ lines.push(`${indent}}`);
409
+ }
410
+ lines.push('');
411
+ break;
412
+ case 'filter':
413
+ lines.push(`${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.filter((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`);
414
+ lines.push('');
415
+ break;
416
+ case 'arrayPredicate':
417
+ const method = flowNode.mode === 'some' ? 'some' : 'every';
418
+ lines.push(`${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.${method}((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`);
419
+ lines.push('');
420
+ break;
421
+ case 'return':
422
+ if (flowNode.outputs) {
423
+ const returnObj = [];
424
+ for (const [key, output] of Object.entries(flowNode.outputs)) {
425
+ let value;
426
+ if (output.from === 'outputVar') {
427
+ value = output.path
428
+ ? `${output.name}?.${output.path}`
429
+ : output.name;
430
+ }
431
+ else if (output.from === 'stateVar') {
432
+ value = output.path
433
+ ? `${output.name}.${output.path}`
434
+ : output.name;
435
+ }
436
+ else if (output.from === 'input') {
437
+ value = `data.${output.path}`;
438
+ }
439
+ else if (output.from === 'literal') {
440
+ value = JSON.stringify(output.value);
441
+ }
442
+ else if (output.from === 'expression') {
443
+ value = output.expression;
444
+ }
445
+ else {
446
+ continue;
447
+ }
448
+ returnObj.push(`${indent} ${key}: ${value},`);
449
+ }
450
+ if (returnObj.length > 0) {
451
+ lines.push(`${indent}return {`);
452
+ lines.push(...returnObj);
453
+ lines.push(`${indent}}`);
454
+ }
455
+ }
456
+ break;
457
+ case 'inline':
458
+ lines.push(`${indent}// Inline step: ${flowNode.stepName || 'Dynamic code'}`);
459
+ lines.push(`${indent}// ${flowNode.description || '<dynamic code>'}`);
460
+ lines.push('');
461
+ break;
462
+ case 'set':
463
+ // Generate variable assignment: varName = value
464
+ const setVar = flowNode.variable;
465
+ const setValue = typeof flowNode.value === 'string'
466
+ ? `'${flowNode.value}'`
467
+ : JSON.stringify(flowNode.value);
468
+ lines.push(`${indent}${setVar} = ${setValue}`);
469
+ lines.push('');
470
+ break;
471
+ }
472
+ }
473
+ return lines;
474
+ }
475
+ /**
476
+ * Find variables that are defined inside branches but used in return statements
477
+ * These need to be hoisted with `let` declarations
478
+ */
479
+ function findConditionalVars(nodes) {
480
+ const conditionalVars = new Set();
481
+ const varsInBranches = new Set();
482
+ const varsUsedInReturn = new Set();
483
+ // Collect variables defined in branches (then/else/case/default nodes)
484
+ for (const [nodeId, node] of Object.entries(nodes)) {
485
+ if (nodeId.includes('_then_') ||
486
+ nodeId.includes('_else_') ||
487
+ nodeId.includes('_case') ||
488
+ nodeId.includes('_default_')) {
489
+ if ('outputVar' in node && node.outputVar) {
490
+ varsInBranches.add(node.outputVar);
491
+ }
492
+ }
493
+ }
494
+ // Collect variables used in return statements
495
+ for (const node of Object.values(nodes)) {
496
+ if ('flow' in node && node.flow === 'return' && node.outputs) {
497
+ for (const output of Object.values(node.outputs)) {
498
+ if (output.from === 'outputVar' && output.name) {
499
+ varsUsedInReturn.add(output.name);
500
+ }
501
+ }
502
+ }
503
+ }
504
+ // Variables that are both in branches and used in return need hoisting
505
+ for (const varName of varsInBranches) {
506
+ if (varsUsedInReturn.has(varName)) {
507
+ conditionalVars.add(varName);
508
+ }
509
+ }
510
+ return conditionalVars;
511
+ }
512
+ /**
513
+ * Get default value for a context variable type
514
+ */
515
+ function getDefaultForType(type) {
516
+ switch (type) {
517
+ case 'string':
518
+ return "''";
519
+ case 'number':
520
+ return '0';
521
+ case 'boolean':
522
+ return 'false';
523
+ case 'array':
524
+ return '[]';
525
+ case 'object':
526
+ return '{}';
527
+ default:
528
+ return 'undefined';
529
+ }
530
+ }
531
+ /**
532
+ * Deserialize a workflow graph to DSL code
533
+ */
534
+ export function deserializeDslWorkflow(workflow, options = {}) {
535
+ const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } = options;
536
+ const lines = [];
537
+ // Check if workflow has any cancel nodes
538
+ const hasCancelNode = Object.values(workflow.nodes).some((node) => 'flow' in node && node.flow === 'cancel');
539
+ // Find variables defined in branches that need hoisting
540
+ const conditionalVars = findConditionalVars(workflow.nodes);
541
+ // Import statement
542
+ if (hasCancelNode) {
543
+ lines.push(`import { pikkuWorkflowFunc, WorkflowCancelledException } from '${pikkuImportPath}'`);
544
+ }
545
+ else {
546
+ lines.push(`import { pikkuWorkflowFunc } from '${pikkuImportPath}'`);
547
+ }
548
+ lines.push('');
549
+ // Add description as comment if present
550
+ if (workflow.description) {
551
+ lines.push(`/**`);
552
+ lines.push(` * ${workflow.description}`);
553
+ lines.push(` */`);
554
+ }
555
+ // Function signature
556
+ const tagsComment = workflow.tags?.length
557
+ ? ` // tags: ${workflow.tags.join(', ')}`
558
+ : '';
559
+ lines.push(`export const ${workflow.name} = pikkuWorkflowFunc(async ({}, data, { workflow }) => {${tagsComment}`);
560
+ // Generate context variable declarations at the top
561
+ if (workflow.context && Object.keys(workflow.context).length > 0) {
562
+ for (const [varName, varDef] of Object.entries(workflow.context)) {
563
+ const defaultValue = varDef.default !== undefined
564
+ ? typeof varDef.default === 'string'
565
+ ? `'${varDef.default}'`
566
+ : JSON.stringify(varDef.default)
567
+ : getDefaultForType(varDef.type);
568
+ lines.push(` let ${varName} = ${defaultValue}`);
569
+ }
570
+ lines.push('');
571
+ }
572
+ // Process nodes in order
573
+ const orderedNodes = traverseNodes(workflow.nodes, workflow.entryNodeIds);
574
+ for (const node of orderedNodes) {
575
+ // Skip child nodes that are processed as part of their parent
576
+ if (node.nodeId.includes('_then_') ||
577
+ node.nodeId.includes('_else_') ||
578
+ node.nodeId.includes('_case') ||
579
+ node.nodeId.includes('_default_') ||
580
+ node.nodeId.includes('_child_') ||
581
+ node.nodeId.includes('_item_')) {
582
+ continue;
583
+ }
584
+ const nodeLines = nodeToCode(node, workflow.nodes, ' ', conditionalVars);
585
+ lines.push(...nodeLines);
586
+ }
587
+ lines.push('})');
588
+ lines.push('');
589
+ return lines.join('\n');
590
+ }
591
+ /**
592
+ * Convert a DataRef to graph ref() call
593
+ * @param ref - The data reference
594
+ * @param outputVarToNodeId - Map from outputVar names to node IDs
595
+ */
596
+ function dataRefToGraphRef(ref, outputVarToNodeId) {
597
+ // Convert outputVar reference to nodeId reference
598
+ const nodeId = outputVarToNodeId.get(ref.$ref) || ref.$ref;
599
+ if (ref.path) {
600
+ return `ref('${nodeId}', '${ref.path}')`;
601
+ }
602
+ return `ref('${nodeId}')`;
603
+ }
604
+ /**
605
+ * Convert a template ref to template() function call for graph code
606
+ * e.g. {$template: {parts: ["Hello ", ""], expressions: [{$ref: "trigger", path: "name"}]}}
607
+ * becomes: template('Hello $0', [ref('trigger', 'name')])
608
+ */
609
+ function templateRefToGraphCode(tmpl, outputVarToNodeId) {
610
+ const { parts, expressions } = tmpl.$template;
611
+ // Build the template string with $0, $1, etc. placeholders
612
+ let templateStr = '';
613
+ for (let i = 0; i < parts.length; i++) {
614
+ templateStr += parts[i];
615
+ if (i < expressions.length) {
616
+ templateStr += `$${i}`;
617
+ }
618
+ }
619
+ // Build the refs array
620
+ const refs = [];
621
+ for (const expr of expressions) {
622
+ if (isDataRef(expr)) {
623
+ refs.push(dataRefToGraphRef(expr, outputVarToNodeId));
624
+ }
625
+ else {
626
+ // Literal JS expression - can't be represented as a typed ref
627
+ refs.push(`{ $ref: '${String(expr).replace(/'/g, "\\'")}' } as any`);
628
+ }
629
+ }
630
+ // Escape single quotes and newlines in the template string
631
+ templateStr = templateStr
632
+ .replace(/\\/g, '\\\\')
633
+ .replace(/'/g, "\\'")
634
+ .replace(/\n/g, '\\n')
635
+ .replace(/\r/g, '\\r');
636
+ return `template('${templateStr}', [${refs.join(', ')}])`;
637
+ }
638
+ function valueToGraphCode(value, outputVarToNodeId, refTracker) {
639
+ if (isDataRef(value)) {
640
+ refTracker.hasRefs = true;
641
+ return dataRefToGraphRef(value, outputVarToNodeId);
642
+ }
643
+ if (isTemplateRef(value)) {
644
+ refTracker.hasRefs = true;
645
+ return templateRefToGraphCode(value, outputVarToNodeId);
646
+ }
647
+ if (Array.isArray(value)) {
648
+ const elements = value.map((v) => valueToGraphCode(v, outputVarToNodeId, refTracker));
649
+ return `[${elements.join(', ')}]`;
650
+ }
651
+ if (typeof value === 'object' && value !== null) {
652
+ const entries = Object.entries(value);
653
+ const props = entries.map(([k, v]) => `${k}: ${valueToGraphCode(v, outputVarToNodeId, refTracker)}`);
654
+ return `{ ${props.join(', ')} }`;
655
+ }
656
+ return JSON.stringify(value);
657
+ }
658
+ /**
659
+ * Convert input object to graph input code using ref()
660
+ * @param input - The input mapping
661
+ * @param outputVarToNodeId - Map from outputVar names to node IDs
662
+ */
663
+ function inputToGraphCode(input, outputVarToNodeId) {
664
+ const entries = Object.entries(input);
665
+ if (entries.length === 0)
666
+ return { hasRefs: false, code: '{}' };
667
+ const refTracker = { hasRefs: false };
668
+ const lines = entries.map(([key, value]) => {
669
+ return ` ${key}: ${valueToGraphCode(value, outputVarToNodeId, refTracker)},`;
670
+ });
671
+ return {
672
+ hasRefs: refTracker.hasRefs,
673
+ code: `{\n${lines.join('\n')}\n }`,
674
+ };
675
+ }
676
+ /**
677
+ * Check if a node is a flow node (non-RPC control flow)
678
+ */
679
+ function isFlowNode(node) {
680
+ return 'flow' in node;
681
+ }
682
+ /**
683
+ * Follow through flow nodes to find the next RPC node
684
+ * This traverses the 'next' chain, skipping flow nodes until finding an RPC node
685
+ */
686
+ function findNextRpcNode(startNextId, nodes, flowNodeIds, visited = new Set()) {
687
+ if (visited.has(startNextId)) {
688
+ return null; // Cycle detected, stop
689
+ }
690
+ visited.add(startNextId);
691
+ // If it's not a flow node, we found our target
692
+ if (!flowNodeIds.has(startNextId)) {
693
+ // Make sure the node exists and has an rpcName (is an RPC node)
694
+ const node = nodes[startNextId];
695
+ if (node && 'rpcName' in node) {
696
+ return startNextId;
697
+ }
698
+ return null;
699
+ }
700
+ // It's a flow node - follow its 'next' if it has one
701
+ const flowNode = nodes[startNextId];
702
+ if (flowNode && 'next' in flowNode && flowNode.next) {
703
+ return findNextRpcNode(flowNode.next, nodes, flowNodeIds, visited);
704
+ }
705
+ return null;
706
+ }
707
+ /**
708
+ * Deserialize a graph workflow to pikkuWorkflowGraph code
709
+ */
710
+ export function deserializeGraphWorkflow(workflow, options = {}) {
711
+ const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } = options;
712
+ const lines = [];
713
+ // Import statement
714
+ lines.push(`import { pikkuWorkflowGraph } from '${pikkuImportPath}'`);
715
+ lines.push('');
716
+ // Add description as comment if present
717
+ if (workflow.description) {
718
+ lines.push(`/**`);
719
+ lines.push(` * ${workflow.description}`);
720
+ lines.push(` */`);
721
+ }
722
+ // Identify flow nodes (non-RPC nodes like return, sleep, branch)
723
+ const flowNodeIds = new Set();
724
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
725
+ if (isFlowNode(node)) {
726
+ flowNodeIds.add(nodeId);
727
+ }
728
+ }
729
+ // Build outputVar to nodeId mapping (for resolving variable references to node IDs)
730
+ const outputVarToNodeId = new Map();
731
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
732
+ if ('outputVar' in node && typeof node.outputVar === 'string') {
733
+ outputVarToNodeId.set(node.outputVar, nodeId);
734
+ }
735
+ }
736
+ // Build node to RPC mapping (only RPC nodes, not flow nodes)
737
+ const nodeRpcMap = {};
738
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
739
+ if ('rpcName' in node &&
740
+ typeof node.rpcName === 'string' &&
741
+ node.rpcName !== 'unknown') {
742
+ nodeRpcMap[nodeId] = node.rpcName;
743
+ }
744
+ }
745
+ // Build node configurations (only for RPC nodes)
746
+ const nodeConfigs = [];
747
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
748
+ // Skip flow nodes - they can't be represented in pikkuWorkflowGraph
749
+ if (flowNodeIds.has(nodeId)) {
750
+ continue;
751
+ }
752
+ const configParts = [];
753
+ // Add next if present - follow through flow nodes to find the actual next RPC node
754
+ if ('next' in node && node.next) {
755
+ const nextId = node.next;
756
+ // If next points to a flow node, follow through to find the next RPC node
757
+ const actualNextId = flowNodeIds.has(nextId)
758
+ ? findNextRpcNode(nextId, workflow.nodes, flowNodeIds)
759
+ : nextId;
760
+ // Only add if we found a valid next RPC node
761
+ if (actualNextId && !flowNodeIds.has(actualNextId)) {
762
+ configParts.push(`next: '${actualNextId}'`);
763
+ }
764
+ }
765
+ // Add input if present
766
+ // Always use callback form to avoid excess property checking in TypeScript
767
+ if ('input' in node && node.input) {
768
+ const input = node.input;
769
+ if (Object.keys(input).length > 0) {
770
+ const { hasRefs, code } = inputToGraphCode(input, outputVarToNodeId);
771
+ if (hasRefs) {
772
+ // Always pass both ref and template for consistent type signature
773
+ configParts.push(`input: (ref, template) => (${code})`);
774
+ }
775
+ else {
776
+ // Wrap in callback to avoid TypeScript excess property checking
777
+ configParts.push(`input: () => (${code})`);
778
+ }
779
+ }
780
+ }
781
+ if (configParts.length > 0) {
782
+ nodeConfigs.push(` ${nodeId}: {\n ${configParts.join(',\n ')},\n }`);
783
+ }
784
+ }
785
+ // Generate the pikkuWorkflowGraph call (builds graph and registers with core)
786
+ lines.push(`export const ${workflow.name} = pikkuWorkflowGraph({`);
787
+ lines.push(` name: '${workflow.name}',`);
788
+ if (workflow.description) {
789
+ lines.push(` description: '${workflow.description}',`);
790
+ }
791
+ if (workflow.tags && workflow.tags.length > 0) {
792
+ lines.push(` tags: [${workflow.tags.map((t) => `'${t}'`).join(', ')}],`);
793
+ }
794
+ // Generate nodes (RPC mapping)
795
+ const rpcMapEntries = Object.entries(nodeRpcMap);
796
+ if (rpcMapEntries.length > 0) {
797
+ lines.push(` nodes: {`);
798
+ for (const [nodeId, rpcName] of rpcMapEntries) {
799
+ lines.push(` ${nodeId}: '${rpcName}',`);
800
+ }
801
+ lines.push(` },`);
802
+ }
803
+ else {
804
+ lines.push(` nodes: {},`);
805
+ }
806
+ // Generate config (node configurations)
807
+ if (nodeConfigs.length > 0) {
808
+ lines.push(` config: {`);
809
+ lines.push(nodeConfigs.join(',\n'));
810
+ lines.push(` },`);
811
+ }
812
+ lines.push(`})`);
813
+ lines.push('');
814
+ return lines.join('\n');
815
+ }
816
+ /**
817
+ * Deserialize all workflows from JSON to DSL code
818
+ */
819
+ export function deserializeAllDslWorkflows(workflows, options = {}) {
820
+ const result = {};
821
+ for (const [name, workflow] of Object.entries(workflows)) {
822
+ if (workflow.source === 'dsl') {
823
+ result[name] = deserializeDslWorkflow(workflow, options);
824
+ }
825
+ else if (workflow.source === 'graph') {
826
+ result[name] = deserializeGraphWorkflow(workflow, options);
827
+ }
828
+ }
829
+ return result;
830
+ }