@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,310 @@
1
+ import type { InspectorState } from '../../../types.js'
2
+ import type {
3
+ SerializedWorkflowGraph,
4
+ WorkflowWires,
5
+ } from './workflow-graph.types.js'
6
+ import type { CLICommandMeta } from '@pikku/core/cli'
7
+
8
+ function parseWorkflowFuncId(
9
+ pikkuFuncId: string
10
+ ): { workflowName: string; startNode?: string } | null {
11
+ for (const prefix of ['workflowStart:', 'workflow:']) {
12
+ if (pikkuFuncId.startsWith(prefix)) {
13
+ return { workflowName: pikkuFuncId.slice(prefix.length) }
14
+ }
15
+ }
16
+ if (pikkuFuncId.startsWith('graphStart:')) {
17
+ const rest = pikkuFuncId.slice('graphStart:'.length)
18
+ const colonIdx = rest.indexOf(':')
19
+ if (colonIdx !== -1) {
20
+ return {
21
+ workflowName: rest.slice(0, colonIdx),
22
+ startNode: rest.slice(colonIdx + 1),
23
+ }
24
+ }
25
+ }
26
+ return null
27
+ }
28
+
29
+ function resolveStartNode(
30
+ parsed: { workflowName: string; startNode?: string },
31
+ graph: SerializedWorkflowGraph
32
+ ): string {
33
+ return parsed.startNode ?? graph.entryNodeIds[0]
34
+ }
35
+
36
+ function getOrCreateWires(graph: SerializedWorkflowGraph): WorkflowWires {
37
+ if (!graph.wires) {
38
+ graph.wires = {}
39
+ }
40
+ return graph.wires
41
+ }
42
+
43
+ export function finalizeWorkflowHelperTypes(state: InspectorState): void {
44
+ const { functions, workflows } = state
45
+ const graphMeta = workflows.graphMeta
46
+
47
+ for (const meta of Object.values(functions.meta)) {
48
+ if (meta.functionType !== 'helper') continue
49
+ if (meta.pikkuFuncId.startsWith('workflowStatus:')) continue
50
+
51
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
52
+ if (!parsed) continue
53
+
54
+ const graph = graphMeta[parsed.workflowName]
55
+ if (!graph) continue
56
+
57
+ const startNodeId = resolveStartNode(parsed, graph)
58
+ const startNode = graph.nodes[startNodeId]
59
+ if (!startNode || !('rpcName' in startNode)) continue
60
+
61
+ const rpcMeta = functions.meta[startNode.rpcName as string]
62
+ if (!rpcMeta) continue
63
+
64
+ if (rpcMeta.inputSchemaName) {
65
+ meta.inputSchemaName = rpcMeta.inputSchemaName
66
+ }
67
+ if (rpcMeta.inputs && rpcMeta.inputs.length > 0) {
68
+ meta.inputs = rpcMeta.inputs
69
+ }
70
+ }
71
+ }
72
+
73
+ export function finalizeWorkflowWires(state: InspectorState): void {
74
+ const { workflows } = state
75
+ const graphMeta = workflows.graphMeta
76
+
77
+ scanHTTP(state, graphMeta)
78
+ scanScheduledTasks(state, graphMeta)
79
+ scanTriggers(state, graphMeta)
80
+ scanQueueWorkers(state, graphMeta)
81
+ scanChannels(state, graphMeta)
82
+ scanMCPEndpoints(state, graphMeta)
83
+ scanCLI(state, graphMeta)
84
+ }
85
+
86
+ function scanHTTP(
87
+ state: InspectorState,
88
+ graphMeta: Record<string, SerializedWorkflowGraph>
89
+ ): void {
90
+ for (const [method, routes] of Object.entries(state.http.meta)) {
91
+ for (const [route, meta] of Object.entries(routes)) {
92
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
93
+ if (!parsed) continue
94
+ const graph = graphMeta[parsed.workflowName]
95
+ if (!graph) continue
96
+ const wires = getOrCreateWires(graph)
97
+ if (!wires.http) wires.http = []
98
+ wires.http.push({
99
+ route,
100
+ method,
101
+ startNode: resolveStartNode(parsed, graph),
102
+ })
103
+ }
104
+ }
105
+ }
106
+
107
+ function scanScheduledTasks(
108
+ state: InspectorState,
109
+ graphMeta: Record<string, SerializedWorkflowGraph>
110
+ ): void {
111
+ for (const meta of Object.values(state.scheduledTasks.meta)) {
112
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
113
+ if (!parsed) continue
114
+ const graph = graphMeta[parsed.workflowName]
115
+ if (!graph) continue
116
+ const wires = getOrCreateWires(graph)
117
+ if (!wires.schedule) wires.schedule = []
118
+ wires.schedule.push({
119
+ cron: meta.schedule,
120
+ startNode: resolveStartNode(parsed, graph),
121
+ })
122
+ }
123
+ }
124
+
125
+ function scanTriggers(
126
+ state: InspectorState,
127
+ graphMeta: Record<string, SerializedWorkflowGraph>
128
+ ): void {
129
+ for (const meta of Object.values(state.triggers.meta)) {
130
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
131
+ if (!parsed) continue
132
+ const graph = graphMeta[parsed.workflowName]
133
+ if (!graph) continue
134
+ const wires = getOrCreateWires(graph)
135
+ if (!wires.trigger) wires.trigger = []
136
+ wires.trigger.push({
137
+ name: meta.name,
138
+ startNode: resolveStartNode(parsed, graph),
139
+ })
140
+ }
141
+ }
142
+
143
+ function scanQueueWorkers(
144
+ state: InspectorState,
145
+ graphMeta: Record<string, SerializedWorkflowGraph>
146
+ ): void {
147
+ for (const meta of Object.values(state.queueWorkers.meta)) {
148
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
149
+ if (!parsed) continue
150
+ const graph = graphMeta[parsed.workflowName]
151
+ if (!graph) continue
152
+ const wires = getOrCreateWires(graph)
153
+ if (!wires.queue) wires.queue = []
154
+ wires.queue.push({
155
+ name: meta.name,
156
+ startNode: resolveStartNode(parsed, graph),
157
+ })
158
+ }
159
+ }
160
+
161
+ function scanChannels(
162
+ state: InspectorState,
163
+ graphMeta: Record<string, SerializedWorkflowGraph>
164
+ ): void {
165
+ for (const channelMeta of Object.values(state.channels.meta)) {
166
+ const wire: NonNullable<WorkflowWires['channel']>[number] = {
167
+ name: channelMeta.name,
168
+ route: channelMeta.route,
169
+ }
170
+ let targetWorkflow: SerializedWorkflowGraph | undefined
171
+
172
+ if (channelMeta.connect) {
173
+ const parsed = parseWorkflowFuncId(channelMeta.connect.pikkuFuncId)
174
+ if (parsed) {
175
+ const graph = graphMeta[parsed.workflowName]
176
+ if (graph) {
177
+ targetWorkflow = graph
178
+ wire.onConnect = resolveStartNode(parsed, graph)
179
+ }
180
+ }
181
+ }
182
+
183
+ if (channelMeta.disconnect) {
184
+ const parsed = parseWorkflowFuncId(channelMeta.disconnect.pikkuFuncId)
185
+ if (parsed) {
186
+ const graph = graphMeta[parsed.workflowName]
187
+ if (graph) {
188
+ targetWorkflow = targetWorkflow ?? graph
189
+ wire.onDisconnect = resolveStartNode(parsed, graph)
190
+ }
191
+ }
192
+ }
193
+
194
+ if (channelMeta.message) {
195
+ const parsed = parseWorkflowFuncId(channelMeta.message.pikkuFuncId)
196
+ if (parsed) {
197
+ const graph = graphMeta[parsed.workflowName]
198
+ if (graph) {
199
+ targetWorkflow = targetWorkflow ?? graph
200
+ wire.onMessage = resolveStartNode(parsed, graph)
201
+ }
202
+ }
203
+ }
204
+
205
+ for (const [routingProp, routeMap] of Object.entries(
206
+ channelMeta.messageWirings
207
+ )) {
208
+ for (const [routeValue, messageMeta] of Object.entries(routeMap)) {
209
+ const parsed = parseWorkflowFuncId(messageMeta.pikkuFuncId)
210
+ if (!parsed) continue
211
+ const graph = graphMeta[parsed.workflowName]
212
+ if (!graph) continue
213
+ targetWorkflow = targetWorkflow ?? graph
214
+ if (!wire.onMessageRoute) wire.onMessageRoute = {}
215
+ wire.onMessageRoute[`${routingProp}:${routeValue}`] = resolveStartNode(
216
+ parsed,
217
+ graph
218
+ )
219
+ }
220
+ }
221
+
222
+ if (targetWorkflow) {
223
+ const wires = getOrCreateWires(targetWorkflow)
224
+ if (!wires.channel) wires.channel = []
225
+ wires.channel.push(wire)
226
+ }
227
+ }
228
+ }
229
+
230
+ function scanMCPEndpoints(
231
+ state: InspectorState,
232
+ graphMeta: Record<string, SerializedWorkflowGraph>
233
+ ): void {
234
+ for (const meta of Object.values(state.mcpEndpoints.toolsMeta)) {
235
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
236
+ if (!parsed) continue
237
+ const graph = graphMeta[parsed.workflowName]
238
+ if (!graph) continue
239
+ const wires = getOrCreateWires(graph)
240
+ if (!wires.mcp) wires.mcp = {}
241
+ if (!wires.mcp.tool) wires.mcp.tool = []
242
+ wires.mcp.tool.push({
243
+ name: meta.name,
244
+ startNode: resolveStartNode(parsed, graph),
245
+ })
246
+ }
247
+
248
+ for (const meta of Object.values(state.mcpEndpoints.promptsMeta)) {
249
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
250
+ if (!parsed) continue
251
+ const graph = graphMeta[parsed.workflowName]
252
+ if (!graph) continue
253
+ const wires = getOrCreateWires(graph)
254
+ if (!wires.mcp) wires.mcp = {}
255
+ if (!wires.mcp.prompt) wires.mcp.prompt = []
256
+ wires.mcp.prompt.push({
257
+ name: meta.name,
258
+ startNode: resolveStartNode(parsed, graph),
259
+ })
260
+ }
261
+
262
+ for (const meta of Object.values(state.mcpEndpoints.resourcesMeta)) {
263
+ const parsed = parseWorkflowFuncId(meta.pikkuFuncId)
264
+ if (!parsed) continue
265
+ const graph = graphMeta[parsed.workflowName]
266
+ if (!graph) continue
267
+ const wires = getOrCreateWires(graph)
268
+ if (!wires.mcp) wires.mcp = {}
269
+ if (!wires.mcp.resource) wires.mcp.resource = []
270
+ wires.mcp.resource.push({
271
+ uri: meta.uri,
272
+ startNode: resolveStartNode(parsed, graph),
273
+ })
274
+ }
275
+ }
276
+
277
+ function visitCLICommands(
278
+ commands: Record<string, CLICommandMeta>,
279
+ programName: string,
280
+ path: string[],
281
+ graphMeta: Record<string, SerializedWorkflowGraph>
282
+ ): void {
283
+ for (const [name, command] of Object.entries(commands)) {
284
+ const currentPath = [...path, name]
285
+ const parsed = parseWorkflowFuncId(command.pikkuFuncId)
286
+ if (parsed) {
287
+ const graph = graphMeta[parsed.workflowName]
288
+ if (graph) {
289
+ const wires = getOrCreateWires(graph)
290
+ if (!wires.cli) wires.cli = []
291
+ wires.cli.push({
292
+ command: `${programName} ${currentPath.join(' ')}`,
293
+ startNode: resolveStartNode(parsed, graph),
294
+ })
295
+ }
296
+ }
297
+ if (command.subcommands) {
298
+ visitCLICommands(command.subcommands, programName, currentPath, graphMeta)
299
+ }
300
+ }
301
+ }
302
+
303
+ function scanCLI(
304
+ state: InspectorState,
305
+ graphMeta: Record<string, SerializedWorkflowGraph>
306
+ ): void {
307
+ for (const program of Object.values(state.cli.meta.programs)) {
308
+ visitCLICommands(program.commands, program.program, [], graphMeta)
309
+ }
310
+ }
@@ -0,0 +1,100 @@
1
+ import type { FunctionsMeta } from '@pikku/core'
2
+ import { isVersionedId, formatVersionedId, parseVersionedId } from '@pikku/core'
3
+ import type { SerializedWorkflowGraph } from './workflow-graph.types.js'
4
+ import { canonicalJSON, hashString } from '../../hash.js'
5
+ import { convertDslToGraph } from './convert-dsl-to-graph.js'
6
+ import type { InspectorState } from '../../../types.js'
7
+
8
+ export function finalizeWorkflows(state: InspectorState): void {
9
+ const { workflows, functions } = state
10
+ const functionsMeta = functions.meta
11
+
12
+ for (const [name, meta] of Object.entries(workflows.meta)) {
13
+ const graph = convertDslToGraph(name, meta)
14
+ stampVersionsOnGraph(graph, functionsMeta)
15
+ computeStepHashes(graph, functionsMeta)
16
+ graph.graphHash = computeGraphHash(graph)
17
+ workflows.graphMeta[name] = graph
18
+ }
19
+
20
+ for (const graph of Object.values(workflows.graphMeta)) {
21
+ if (graph.graphHash) {
22
+ continue
23
+ }
24
+ stampVersionsOnGraph(graph, functionsMeta)
25
+ computeStepHashes(graph, functionsMeta)
26
+ graph.graphHash = computeGraphHash(graph)
27
+ }
28
+ }
29
+
30
+ function stampVersionsOnGraph(
31
+ graph: SerializedWorkflowGraph,
32
+ functionsMeta: FunctionsMeta
33
+ ): void {
34
+ for (const node of Object.values(graph.nodes)) {
35
+ if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
36
+ continue
37
+ }
38
+
39
+ if (isVersionedId(node.rpcName)) {
40
+ continue
41
+ }
42
+
43
+ const meta = functionsMeta[node.rpcName]
44
+ if (meta?.version !== undefined) {
45
+ node.rpcName = formatVersionedId(node.rpcName, meta.version)
46
+ } else {
47
+ const latestVersion = findLatestVersion(node.rpcName, functionsMeta)
48
+ if (latestVersion > 0) {
49
+ node.rpcName = formatVersionedId(node.rpcName, latestVersion)
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ function findLatestVersion(
56
+ baseName: string,
57
+ functionsMeta: FunctionsMeta
58
+ ): number {
59
+ let max = 0
60
+ for (const id of Object.keys(functionsMeta)) {
61
+ const parsed = parseVersionedId(id)
62
+ if (parsed.baseName === baseName && parsed.version !== null) {
63
+ max = Math.max(max, parsed.version)
64
+ }
65
+ }
66
+ return max
67
+ }
68
+
69
+ function computeStepHashes(
70
+ graph: SerializedWorkflowGraph,
71
+ functionsMeta: FunctionsMeta
72
+ ): void {
73
+ for (const node of Object.values(graph.nodes)) {
74
+ if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
75
+ continue
76
+ }
77
+ const rpcName: string = node.rpcName
78
+ let meta = functionsMeta[rpcName]
79
+ if (!meta) {
80
+ const { baseName } = parseVersionedId(rpcName)
81
+ meta = functionsMeta[baseName]
82
+ }
83
+ ;(node as Record<string, unknown>).stepHash = hashString(
84
+ `${node.nodeId}:${meta?.contractHash ?? ''}`,
85
+ 12
86
+ )
87
+ }
88
+ }
89
+
90
+ function computeGraphHash(graph: SerializedWorkflowGraph): string {
91
+ return hashString(
92
+ canonicalJSON({
93
+ source: graph.source,
94
+ context: graph.context,
95
+ nodes: graph.nodes,
96
+ entryNodeIds: graph.entryNodeIds,
97
+ }),
98
+ 12
99
+ )
100
+ }
@@ -0,0 +1,11 @@
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'
7
+ export { finalizeWorkflows } from './finalize-workflows.js'
8
+ export {
9
+ finalizeWorkflowHelperTypes,
10
+ finalizeWorkflowWires,
11
+ } from './finalize-workflow-wires.js'
@@ -0,0 +1,216 @@
1
+ import type {
2
+ SerializedWorkflowGraph,
3
+ SerializedGraphNode,
4
+ FunctionNode,
5
+ DataRef,
6
+ SerializedNext,
7
+ } from './workflow-graph.types.js'
8
+
9
+ /**
10
+ * Convert a RefValue (from runtime) to DataRef (serialized)
11
+ */
12
+ function convertRef(ref: { nodeId: string; path?: string }): DataRef {
13
+ return {
14
+ $ref: ref.nodeId,
15
+ path: ref.path,
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Check if a value is a runtime RefValue
21
+ */
22
+ function isRefValue(
23
+ value: unknown
24
+ ): value is { __isRef: true; nodeId: string; path?: string } {
25
+ return (
26
+ typeof value === 'object' &&
27
+ value !== null &&
28
+ '__isRef' in value &&
29
+ (value as any).__isRef === true
30
+ )
31
+ }
32
+
33
+ /**
34
+ * Convert input mapping from runtime format to serialized format
35
+ */
36
+ function serializeInputMapping(
37
+ input: Record<string, unknown>
38
+ ): Record<string, unknown | DataRef> {
39
+ const result: Record<string, unknown | DataRef> = {}
40
+
41
+ for (const [key, value] of Object.entries(input)) {
42
+ if (isRefValue(value)) {
43
+ result[key] = convertRef(value)
44
+ } else {
45
+ result[key] = value
46
+ }
47
+ }
48
+
49
+ return result
50
+ }
51
+
52
+ /**
53
+ * Convert next config from runtime format to serialized format
54
+ * Runtime uses Record<string, string | string[]> for branching with graph.branch()
55
+ * Serialized uses { conditions: [...], default: ... } for UI-friendly branching
56
+ */
57
+ function serializeNext(
58
+ next: string | string[] | Record<string, string | string[]> | undefined
59
+ ): SerializedNext | undefined {
60
+ if (!next) return undefined
61
+
62
+ if (typeof next === 'string') return next
63
+ if (Array.isArray(next)) return next
64
+
65
+ // Record format - convert to conditions format
66
+ // For now, treat keys as branch identifiers (from graph.branch())
67
+ // UI can display these as condition labels
68
+ const conditions = Object.entries(next).map(([key, target]) => ({
69
+ expression: key, // The branch key becomes the expression
70
+ target,
71
+ }))
72
+
73
+ return { conditions }
74
+ }
75
+
76
+ /**
77
+ * Serialize a workflow graph definition (from runtime) to JSON format
78
+ *
79
+ * @param definition - The runtime definition (with callbacks evaluated)
80
+ * @param rpcNameLookup - Function to get RPC name from a node's func
81
+ */
82
+ export function serializeWorkflowGraph(
83
+ definition: {
84
+ name: string
85
+ graph: Record<
86
+ string,
87
+ {
88
+ func: { name?: string }
89
+ input?: (ref: any) => Record<string, unknown>
90
+ next?: string | string[] | Record<string, string | string[]>
91
+ onError?: string | string[]
92
+ }
93
+ >
94
+ },
95
+ options?: {
96
+ description?: string
97
+ tags?: string[]
98
+ }
99
+ ): SerializedWorkflowGraph {
100
+ const nodes: Record<string, SerializedGraphNode> = {}
101
+ const entryNodeIds: string[] = []
102
+
103
+ // Create a ref function that captures refs
104
+ const createRef = (nodeId: string, path?: string) => ({
105
+ __isRef: true as const,
106
+ nodeId,
107
+ path,
108
+ })
109
+
110
+ // Track which nodes have incoming edges
111
+ const hasIncomingEdge = new Set<string>()
112
+
113
+ // First pass: identify nodes with incoming edges
114
+ for (const [_nodeId, node] of Object.entries(definition.graph)) {
115
+ const next = node.next
116
+ if (!next) continue
117
+
118
+ if (typeof next === 'string') {
119
+ hasIncomingEdge.add(next)
120
+ } else if (Array.isArray(next)) {
121
+ next.forEach((n) => hasIncomingEdge.add(n))
122
+ } else {
123
+ for (const targets of Object.values(next)) {
124
+ if (typeof targets === 'string') {
125
+ hasIncomingEdge.add(targets)
126
+ } else {
127
+ targets.forEach((n) => hasIncomingEdge.add(n))
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ // Second pass: serialize nodes
134
+ for (const [nodeId, node] of Object.entries(definition.graph)) {
135
+ // Evaluate input callback to get the mapping
136
+ let input: Record<string, unknown | DataRef> = {}
137
+ if (node.input) {
138
+ const rawInput = node.input(createRef)
139
+ input = serializeInputMapping(rawInput)
140
+ }
141
+
142
+ // Get RPC name from func
143
+ const rpcName = node.func?.name || 'unknown'
144
+
145
+ const funcNode: FunctionNode = {
146
+ nodeId,
147
+ rpcName,
148
+ input,
149
+ next: serializeNext(node.next),
150
+ onError: node.onError,
151
+ }
152
+ nodes[nodeId] = funcNode
153
+
154
+ // Entry nodes have no incoming edges
155
+ if (!hasIncomingEdge.has(nodeId)) {
156
+ entryNodeIds.push(nodeId)
157
+ }
158
+ }
159
+
160
+ return {
161
+ name: definition.name,
162
+ pikkuFuncId: definition.name, // For graph workflows, pikkuFuncId is the workflow name
163
+ source: 'graph' as const,
164
+ description: options?.description,
165
+ tags: options?.tags,
166
+ nodes,
167
+ entryNodeIds,
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Deserialize a workflow graph from JSON to runtime format
173
+ * This re-hydrates the JSON so it can be executed
174
+ */
175
+ export function deserializeWorkflowGraph(serialized: SerializedWorkflowGraph): {
176
+ name: string
177
+ graph: Record<
178
+ string,
179
+ {
180
+ rpcName: string
181
+ input: Record<string, unknown | DataRef>
182
+ next?: SerializedNext
183
+ onError?: string | string[]
184
+ }
185
+ >
186
+ entryNodeIds: string[]
187
+ } {
188
+ const graph: Record<
189
+ string,
190
+ {
191
+ rpcName: string
192
+ input: Record<string, unknown | DataRef>
193
+ next?: SerializedNext
194
+ onError?: string | string[]
195
+ }
196
+ > = {}
197
+
198
+ for (const [nodeId, node] of Object.entries(serialized.nodes)) {
199
+ // Only include FunctionNode properties (nodes with rpcName)
200
+ if ('rpcName' in node) {
201
+ const funcNode = node as FunctionNode
202
+ graph[nodeId] = {
203
+ rpcName: funcNode.rpcName,
204
+ input: funcNode.input ?? {},
205
+ next: funcNode.next,
206
+ onError: funcNode.onError,
207
+ }
208
+ }
209
+ }
210
+
211
+ return {
212
+ name: serialized.name,
213
+ graph,
214
+ entryNodeIds: serialized.entryNodeIds,
215
+ }
216
+ }