@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,11 @@
1
+ /**
2
+ * DSL (Domain Specific Language) workflow extraction exports
3
+ */
4
+ export { extractDSLWorkflow } from './extract-dsl-workflow.js'
5
+ export {
6
+ deserializeDslWorkflow,
7
+ deserializeGraphWorkflow,
8
+ deserializeAllDslWorkflows,
9
+ } from './deserialize-dsl-workflow.js'
10
+ export * from './patterns.js'
11
+ export * from './validation.js'
@@ -42,6 +42,94 @@ export function isWorkflowSleepCall(
42
42
  )
43
43
  }
44
44
 
45
+ /**
46
+ * Check if a throw statement throws WorkflowCancelledException
47
+ * Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
48
+ */
49
+ export function isThrowCancelException(node: ts.ThrowStatement): boolean {
50
+ const expr = node.expression
51
+ if (!expr) return false
52
+
53
+ // Check for: throw new WorkflowCancelledException(...)
54
+ if (ts.isNewExpression(expr)) {
55
+ if (ts.isIdentifier(expr.expression)) {
56
+ return expr.expression.text === 'WorkflowCancelledException'
57
+ }
58
+ }
59
+
60
+ // Check for: throw WorkflowCancelledException(...) - function call style
61
+ if (ts.isCallExpression(expr)) {
62
+ if (ts.isIdentifier(expr.expression)) {
63
+ return expr.expression.text === 'WorkflowCancelledException'
64
+ }
65
+ }
66
+
67
+ return false
68
+ }
69
+
70
+ /**
71
+ * Extract the reason string from a throw WorkflowCancelledException statement
72
+ */
73
+ export function extractCancelReason(
74
+ node: ts.ThrowStatement,
75
+ checker: ts.TypeChecker
76
+ ): string | undefined {
77
+ const expr = node.expression
78
+ if (!expr) return undefined
79
+
80
+ let args: ts.NodeArray<ts.Expression> | undefined
81
+
82
+ if (ts.isNewExpression(expr) && expr.arguments) {
83
+ args = expr.arguments
84
+ } else if (ts.isCallExpression(expr)) {
85
+ args = expr.arguments
86
+ }
87
+
88
+ if (args && args.length > 0) {
89
+ const firstArg = args[0]
90
+ if (ts.isStringLiteral(firstArg)) {
91
+ return firstArg.text
92
+ }
93
+ // For template literals or other expressions, return the source text
94
+ return firstArg.getText()
95
+ }
96
+
97
+ return undefined
98
+ }
99
+
100
+ /**
101
+ * Check if a call expression is array.filter()
102
+ */
103
+ export function isArrayFilter(node: ts.CallExpression): boolean {
104
+ if (!ts.isPropertyAccessExpression(node.expression)) {
105
+ return false
106
+ }
107
+
108
+ return node.expression.name.text === 'filter'
109
+ }
110
+
111
+ /**
112
+ * Check if a call expression is array.some()
113
+ */
114
+ export function isArraySome(node: ts.CallExpression): boolean {
115
+ if (!ts.isPropertyAccessExpression(node.expression)) {
116
+ return false
117
+ }
118
+
119
+ return node.expression.name.text === 'some'
120
+ }
121
+
122
+ /**
123
+ * Check if a call expression is array.every()
124
+ */
125
+ export function isArrayEvery(node: ts.CallExpression): boolean {
126
+ if (!ts.isPropertyAccessExpression(node.expression)) {
127
+ return false
128
+ }
129
+
130
+ return node.expression.name.text === 'every'
131
+ }
132
+
45
133
  /**
46
134
  * Check if an expression is Promise.all(array.map(...))
47
135
  */
@@ -118,6 +206,24 @@ export function isSequentialFanout(node: ts.ForOfStatement): boolean {
118
206
  return true
119
207
  }
120
208
 
209
+ /**
210
+ * Extract full source path from an expression (e.g., data.memberEmails)
211
+ */
212
+ function extractSourcePath(expr: ts.Expression): string | null {
213
+ if (ts.isIdentifier(expr)) {
214
+ return expr.text
215
+ }
216
+
217
+ if (ts.isPropertyAccessExpression(expr)) {
218
+ const base = extractSourcePath(expr.expression)
219
+ if (base) {
220
+ return `${base}.${expr.name.text}`
221
+ }
222
+ }
223
+
224
+ return null
225
+ }
226
+
121
227
  /**
122
228
  * Extract the variable name from a for..of statement
123
229
  */
@@ -135,17 +241,8 @@ export function extractForOfVariable(
135
241
 
136
242
  const itemVar = decl.name.text
137
243
 
138
- // Extract source variable
139
- let sourceVar: string | null = null
140
- if (ts.isIdentifier(node.expression)) {
141
- sourceVar = node.expression.text
142
- } else if (
143
- ts.isPropertyAccessExpression(node.expression) &&
144
- ts.isIdentifier(node.expression.expression)
145
- ) {
146
- // Handle data.memberEmails
147
- sourceVar = node.expression.expression.text
148
- }
244
+ // Extract source variable with full path (e.g., data.memberEmails)
245
+ const sourceVar = extractSourcePath(node.expression)
149
246
 
150
247
  if (!sourceVar) {
151
248
  return null
@@ -16,8 +16,10 @@ export interface ValidationError {
16
16
  * - VariableStatement (const/let declarations)
17
17
  * - ExpressionStatement (await workflow.do, await workflow.sleep, await Promise.all)
18
18
  * - IfStatement (branches)
19
+ * - SwitchStatement (switch/case)
19
20
  * - ForOfStatement (sequential fanout)
20
21
  * - ReturnStatement
22
+ * - ThrowStatement (for WorkflowCancelledException)
21
23
  * - Block (containers)
22
24
  */
23
25
  export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
@@ -29,8 +31,10 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
29
31
  ts.isVariableStatement(statement) ||
30
32
  ts.isExpressionStatement(statement) ||
31
33
  ts.isIfStatement(statement) ||
34
+ ts.isSwitchStatement(statement) ||
32
35
  ts.isForOfStatement(statement) ||
33
- ts.isReturnStatement(statement)
36
+ ts.isReturnStatement(statement) ||
37
+ ts.isThrowStatement(statement)
34
38
  ) {
35
39
  // Allowed statement type - recurse into it
36
40
  visitNode(statement)
@@ -38,7 +42,7 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
38
42
  // Unknown/disallowed statement type
39
43
  const nodeType = ts.SyntaxKind[statement.kind]
40
44
  errors.push({
41
- message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, for..of, return, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
45
+ message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
42
46
  node: statement,
43
47
  })
44
48
  }
@@ -109,7 +113,30 @@ export function validateNoDisallowedPatterns(node: ts.Node): ValidationError[] {
109
113
  export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
110
114
  const errors: ValidationError[] = []
111
115
 
112
- function visit(node: ts.Node, parentIsAwait: boolean = false) {
116
+ function visit(
117
+ node: ts.Node,
118
+ parentIsAwait: boolean = false,
119
+ insidePromiseAll: boolean = false
120
+ ) {
121
+ // Check if this is Promise.all(...) first, before checking for workflow calls
122
+ if (
123
+ ts.isCallExpression(node) &&
124
+ ts.isPropertyAccessExpression(node.expression)
125
+ ) {
126
+ const propAccess = node.expression
127
+ if (
128
+ propAccess.name.text === 'all' &&
129
+ ts.isIdentifier(propAccess.expression) &&
130
+ propAccess.expression.text === 'Promise'
131
+ ) {
132
+ // console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
133
+ // Visit children with insidePromiseAll = true
134
+ ts.forEachChild(node, (child) => visit(child, parentIsAwait, true))
135
+ return
136
+ }
137
+ }
138
+
139
+ // Now check for workflow calls
113
140
  if (ts.isCallExpression(node)) {
114
141
  if (ts.isPropertyAccessExpression(node.expression)) {
115
142
  const propAccess = node.expression
@@ -118,7 +145,7 @@ export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
118
145
  ts.isIdentifier(propAccess.expression) &&
119
146
  propAccess.expression.text === 'workflow'
120
147
  ) {
121
- if (!parentIsAwait) {
148
+ if (!parentIsAwait && !insidePromiseAll) {
122
149
  errors.push({
123
150
  message: `workflow.${propAccess.name.text}() must be awaited`,
124
151
  node,
@@ -130,10 +157,10 @@ export function validateAwaitedCalls(node: ts.Node): ValidationError[] {
130
157
  }
131
158
 
132
159
  if (ts.isAwaitExpression(node)) {
133
- // Mark child as awaited
134
- ts.forEachChild(node.expression, (child) => visit(child, true))
160
+ // Visit the expression itself with parentIsAwait=true
161
+ visit(node.expression, true, insidePromiseAll)
135
162
  } else {
136
- ts.forEachChild(node, (child) => visit(child, false))
163
+ ts.forEachChild(node, (child) => visit(child, false, insidePromiseAll))
137
164
  }
138
165
  }
139
166
 
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Converts DSL (Domain Specific Language) step-based format to graph node format
3
+ */
4
+ import type { WorkflowStepMeta, WorkflowsMeta } from '@pikku/core/workflow'
5
+ import type {
6
+ SerializedGraphNode,
7
+ SerializedWorkflowGraph,
8
+ FunctionNode,
9
+ FlowNode,
10
+ DataRef,
11
+ } from './workflow-graph.types.js'
12
+
13
+ /**
14
+ * Check if a node is a terminal flow (no next step should follow)
15
+ */
16
+ function isTerminalFlow(node: SerializedGraphNode): boolean {
17
+ if ('flow' in node) {
18
+ // Cancel and return are terminal flows - they end execution
19
+ return node.flow === 'cancel' || node.flow === 'return'
20
+ }
21
+ return false
22
+ }
23
+
24
+ /**
25
+ * Convert InputSource to DataRef
26
+ */
27
+ function convertInputSource(source: {
28
+ from: string
29
+ path?: string
30
+ name?: string
31
+ value?: unknown
32
+ parts?: string[]
33
+ expressions?: unknown[]
34
+ }): unknown | DataRef {
35
+ if (source.from === 'literal') {
36
+ return source.value
37
+ }
38
+ if (source.from === 'input') {
39
+ return { $ref: 'trigger', path: source.path }
40
+ }
41
+ if (source.from === 'outputVar') {
42
+ return { $ref: source.name!, path: source.path }
43
+ }
44
+ if (source.from === 'item') {
45
+ return { $ref: '$item', path: source.path }
46
+ }
47
+ if (source.from === 'stateVar') {
48
+ return { $state: source.name!, path: source.path }
49
+ }
50
+ if (source.from === 'template') {
51
+ return {
52
+ $template: {
53
+ parts: source.parts,
54
+ expressions: source.expressions?.map((expr) =>
55
+ convertInputSource(expr as any)
56
+ ),
57
+ },
58
+ }
59
+ }
60
+ return source.value
61
+ }
62
+
63
+ /**
64
+ * Convert a single DSL step to graph node(s)
65
+ */
66
+ function convertStepToNode(
67
+ step: WorkflowStepMeta,
68
+ index: number,
69
+ steps: WorkflowStepMeta[],
70
+ nodeIdPrefix: string = 'step'
71
+ ): SerializedGraphNode[] {
72
+ const nodeId = `${nodeIdPrefix}_${index}`
73
+ const nextNodeId =
74
+ index < steps.length - 1 ? `${nodeIdPrefix}_${index + 1}` : undefined
75
+
76
+ switch (step.type) {
77
+ case 'rpc': {
78
+ const node: FunctionNode = {
79
+ nodeId,
80
+ rpcName: step.rpcName,
81
+ stepName: step.stepName,
82
+ next: nextNodeId,
83
+ }
84
+ if (step.inputs) {
85
+ if (step.inputs === 'passthrough') {
86
+ // Entire data is passed through - store as reference to trigger
87
+ node.input = { $passthrough: { $ref: 'trigger' } }
88
+ } else {
89
+ node.input = {}
90
+ for (const [key, source] of Object.entries(step.inputs)) {
91
+ node.input[key] = convertInputSource(source as any)
92
+ }
93
+ }
94
+ }
95
+ if (step.outputVar) {
96
+ node.outputVar = step.outputVar
97
+ }
98
+ if (step.options) {
99
+ node.options = {
100
+ retries: step.options.retries,
101
+ retryDelay: step.options.retryDelay?.toString(),
102
+ }
103
+ }
104
+ return [node]
105
+ }
106
+
107
+ case 'sleep': {
108
+ const node: FlowNode = {
109
+ nodeId,
110
+ flow: 'sleep',
111
+ stepName: step.stepName,
112
+ duration: step.duration,
113
+ next: nextNodeId,
114
+ }
115
+ return [node]
116
+ }
117
+
118
+ case 'inline': {
119
+ const node: FlowNode = {
120
+ nodeId,
121
+ flow: 'inline',
122
+ stepName: step.stepName,
123
+ description: step.description,
124
+ next: nextNodeId,
125
+ }
126
+ return [node]
127
+ }
128
+
129
+ case 'branch': {
130
+ // Convert all branch conditions (if/else-if chain)
131
+ const branchNodes: SerializedGraphNode[] = []
132
+ const branches: Array<{
133
+ condition: unknown
134
+ entry: string
135
+ }> = []
136
+
137
+ for (let i = 0; i < step.branches.length; i++) {
138
+ const branchSteps = convertStepsToNodes(
139
+ step.branches[i].steps,
140
+ `${nodeId}_branch${i}`
141
+ )
142
+ if (branchSteps.length > 0) {
143
+ branches.push({
144
+ condition: step.branches[i].condition,
145
+ entry: branchSteps[0].nodeId,
146
+ })
147
+ // Link last branch node back to next (unless terminal flow)
148
+ if (nextNodeId) {
149
+ const lastBranch = branchSteps[branchSteps.length - 1]
150
+ if (!lastBranch.next && !isTerminalFlow(lastBranch))
151
+ lastBranch.next = nextNodeId
152
+ }
153
+ branchNodes.push(...branchSteps)
154
+ }
155
+ }
156
+
157
+ // Convert else branch
158
+ const elseNodes = step.elseSteps
159
+ ? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
160
+ : []
161
+ if (elseNodes.length > 0 && nextNodeId) {
162
+ const lastElse = elseNodes[elseNodes.length - 1]
163
+ if (!lastElse.next && !isTerminalFlow(lastElse))
164
+ lastElse.next = nextNodeId
165
+ }
166
+
167
+ const node: FlowNode = {
168
+ nodeId,
169
+ flow: 'branch',
170
+ branches,
171
+ elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
172
+ next: nextNodeId,
173
+ }
174
+
175
+ return [node, ...branchNodes, ...elseNodes]
176
+ }
177
+
178
+ case 'switch': {
179
+ const caseNodes: SerializedGraphNode[] = []
180
+ const cases: Array<{
181
+ value?: unknown
182
+ expression?: string
183
+ entry: string
184
+ }> = []
185
+
186
+ for (let i = 0; i < step.cases.length; i++) {
187
+ const caseSteps = convertStepsToNodes(
188
+ step.cases[i].steps,
189
+ `${nodeId}_case${i}`
190
+ )
191
+ if (caseSteps.length > 0) {
192
+ cases.push({
193
+ value: step.cases[i].value,
194
+ expression: step.cases[i].expression,
195
+ entry: caseSteps[0].nodeId,
196
+ })
197
+ // Link last case node to next (unless terminal flow)
198
+ if (nextNodeId) {
199
+ const lastCase = caseSteps[caseSteps.length - 1]
200
+ if (!lastCase.next && !isTerminalFlow(lastCase))
201
+ lastCase.next = nextNodeId
202
+ }
203
+ caseNodes.push(...caseSteps)
204
+ }
205
+ }
206
+
207
+ let defaultEntry: string | undefined
208
+ if (step.defaultSteps) {
209
+ const defaultNodes = convertStepsToNodes(
210
+ step.defaultSteps,
211
+ `${nodeId}_default`
212
+ )
213
+ if (defaultNodes.length > 0) {
214
+ defaultEntry = defaultNodes[0].nodeId
215
+ // Link last default node to next (unless terminal flow)
216
+ if (nextNodeId) {
217
+ const lastDefault = defaultNodes[defaultNodes.length - 1]
218
+ if (!lastDefault.next && !isTerminalFlow(lastDefault))
219
+ lastDefault.next = nextNodeId
220
+ }
221
+ caseNodes.push(...defaultNodes)
222
+ }
223
+ }
224
+
225
+ const node: FlowNode = {
226
+ nodeId,
227
+ flow: 'switch',
228
+ expression: step.expression,
229
+ cases,
230
+ defaultEntry,
231
+ next: nextNodeId,
232
+ }
233
+
234
+ return [node, ...caseNodes]
235
+ }
236
+
237
+ case 'parallel': {
238
+ // Convert children to nodes
239
+ const childNodes: SerializedGraphNode[] = []
240
+ const childEntries: string[] = []
241
+
242
+ for (let i = 0; i < step.children.length; i++) {
243
+ const childSteps = convertStepToNode(
244
+ step.children[i],
245
+ i,
246
+ step.children,
247
+ `${nodeId}_child`
248
+ )
249
+ if (childSteps.length > 0) {
250
+ childEntries.push(childSteps[0].nodeId)
251
+ childNodes.push(...childSteps)
252
+ }
253
+ }
254
+
255
+ const node: FlowNode = {
256
+ nodeId,
257
+ flow: 'parallel',
258
+ children: childEntries,
259
+ next: nextNodeId,
260
+ }
261
+
262
+ return [node, ...childNodes]
263
+ }
264
+
265
+ case 'fanout': {
266
+ // Convert child step
267
+ const childNodes = convertStepToNode(
268
+ step.child,
269
+ 0,
270
+ [step.child],
271
+ `${nodeId}_item`
272
+ )
273
+
274
+ const node: FlowNode = {
275
+ nodeId,
276
+ flow: 'fanout',
277
+ stepName: step.stepName,
278
+ sourceVar: step.sourceVar,
279
+ itemVar: step.itemVar,
280
+ mode: step.mode,
281
+ childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
282
+ timeBetween: step.timeBetween,
283
+ next: nextNodeId,
284
+ }
285
+
286
+ return [node, ...childNodes]
287
+ }
288
+
289
+ case 'filter': {
290
+ const node: FlowNode = {
291
+ nodeId,
292
+ flow: 'filter',
293
+ sourceVar: step.sourceVar,
294
+ itemVar: step.itemVar,
295
+ condition: step.condition,
296
+ outputVar: step.outputVar,
297
+ next: nextNodeId,
298
+ }
299
+ return [node]
300
+ }
301
+
302
+ case 'arrayPredicate': {
303
+ const node: FlowNode = {
304
+ nodeId,
305
+ flow: 'arrayPredicate',
306
+ mode: step.mode,
307
+ sourceVar: step.sourceVar,
308
+ itemVar: step.itemVar,
309
+ condition: step.condition,
310
+ outputVar: step.outputVar,
311
+ next: nextNodeId,
312
+ }
313
+ return [node]
314
+ }
315
+
316
+ case 'return': {
317
+ const node: FlowNode = {
318
+ nodeId,
319
+ flow: 'return',
320
+ outputs: step.outputs,
321
+ }
322
+ return [node]
323
+ }
324
+
325
+ case 'cancel': {
326
+ const node: FlowNode = {
327
+ nodeId,
328
+ flow: 'cancel',
329
+ reason: step.reason,
330
+ }
331
+ return [node]
332
+ }
333
+
334
+ case 'set': {
335
+ const node: FlowNode = {
336
+ nodeId,
337
+ flow: 'set',
338
+ variable: step.variable,
339
+ value: step.value,
340
+ next: nextNodeId,
341
+ }
342
+ return [node]
343
+ }
344
+
345
+ default:
346
+ return []
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Convert array of steps to graph nodes
352
+ */
353
+ function convertStepsToNodes(
354
+ steps: WorkflowStepMeta[],
355
+ nodeIdPrefix: string = 'step'
356
+ ): SerializedGraphNode[] {
357
+ const allNodes: SerializedGraphNode[] = []
358
+
359
+ for (let i = 0; i < steps.length; i++) {
360
+ const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix)
361
+ allNodes.push(...nodes)
362
+ }
363
+
364
+ return allNodes
365
+ }
366
+
367
+ /**
368
+ * Convert a DSL workflow to graph format
369
+ */
370
+ export function convertDslToGraph(
371
+ workflowName: string,
372
+ meta: WorkflowsMeta[string]
373
+ ): SerializedWorkflowGraph {
374
+ const nodes = convertStepsToNodes(meta.steps)
375
+ const nodesRecord: Record<string, SerializedGraphNode> = {}
376
+
377
+ for (const node of nodes) {
378
+ nodesRecord[node.nodeId] = node
379
+ }
380
+
381
+ // Find entry nodes (step_0 is always entry for sequential workflows)
382
+ const entryNodeIds = nodes.length > 0 ? ['step_0'] : []
383
+
384
+ // Determine source type based on dsl flag:
385
+ // - dsl === true: pure DSL workflow, can be serialized
386
+ // - dsl === false: complex workflow with inline steps, not serializable
387
+ const source = meta.dsl === false ? 'complex' : 'dsl'
388
+
389
+ return {
390
+ name: workflowName,
391
+ pikkuFuncName: meta.pikkuFuncName,
392
+ source,
393
+ description: meta.description,
394
+ tags: meta.tags,
395
+ context: meta.context,
396
+ wires: {}, // DSL workflows don't have explicit wires in meta
397
+ nodes: nodesRecord,
398
+ entryNodeIds,
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Convert all DSL workflows to graph format
404
+ */
405
+ export function convertAllDslToGraphs(
406
+ workflowsMeta: WorkflowsMeta
407
+ ): Record<string, SerializedWorkflowGraph> {
408
+ const result: Record<string, SerializedWorkflowGraph> = {}
409
+
410
+ for (const [name, meta] of Object.entries(workflowsMeta)) {
411
+ result[name] = convertDslToGraph(name, meta)
412
+ }
413
+
414
+ return result
415
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workflow graph serialization exports
3
+ */
4
+ export * from './workflow-graph.types.js'
5
+ export { serializeWorkflowGraph } from './serialize-workflow-graph.js'
6
+ export { convertDslToGraph } from './convert-dsl-to-graph.js'