@pikku/inspector 0.11.1 → 0.11.2

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