@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,522 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+ import { extractStringLiteral } from '../utils/extract-node-value.js'
5
+ import type {
6
+ SerializedWorkflowGraph,
7
+ DataRef,
8
+ } from '../utils/workflow/graph/workflow-graph.types.js'
9
+
10
+ function extractAstValue(
11
+ expr: ts.Expression,
12
+ refParamName: string,
13
+ templateParamName: string | undefined
14
+ ): unknown {
15
+ if (ts.isStringLiteral(expr)) {
16
+ return expr.text
17
+ }
18
+ if (ts.isNumericLiteral(expr)) {
19
+ return Number(expr.text)
20
+ }
21
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
22
+ return true
23
+ }
24
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
25
+ return false
26
+ }
27
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
28
+ return null
29
+ }
30
+ if (ts.isCallExpression(expr)) {
31
+ const callee = expr.expression
32
+ if (ts.isIdentifier(callee)) {
33
+ if (callee.text === refParamName) {
34
+ const args = expr.arguments
35
+ const nodeId =
36
+ args[0] && ts.isStringLiteral(args[0]) ? args[0].text : 'unknown'
37
+ const path =
38
+ args[1] && ts.isStringLiteral(args[1]) ? args[1].text : undefined
39
+ return { $ref: nodeId, path } as DataRef
40
+ }
41
+ if (templateParamName && callee.text === templateParamName) {
42
+ const templateStr =
43
+ expr.arguments[0] && ts.isStringLiteral(expr.arguments[0])
44
+ ? expr.arguments[0].text
45
+ : ''
46
+ const refsArg = expr.arguments[1]
47
+ const refs: Array<{ $ref: string; path?: string }> = []
48
+ if (refsArg && ts.isArrayLiteralExpression(refsArg)) {
49
+ for (const el of refsArg.elements) {
50
+ const resolved = extractAstValue(
51
+ el,
52
+ refParamName,
53
+ templateParamName
54
+ )
55
+ if (
56
+ typeof resolved === 'object' &&
57
+ resolved !== null &&
58
+ '$ref' in resolved
59
+ ) {
60
+ refs.push(resolved as { $ref: string; path?: string })
61
+ }
62
+ }
63
+ }
64
+ const parts: string[] = []
65
+ const expressions: Array<{ $ref: string; path?: string }> = []
66
+ const regex = /\$(\d+)/g
67
+ let lastIndex = 0
68
+ let match
69
+ while ((match = regex.exec(templateStr)) !== null) {
70
+ parts.push(templateStr.slice(lastIndex, match.index))
71
+ const refIndex = parseInt(match[1]!, 10)
72
+ expressions.push(refs[refIndex] ?? { $ref: 'unknown' })
73
+ lastIndex = regex.lastIndex
74
+ }
75
+ parts.push(templateStr.slice(lastIndex))
76
+ return { $template: { parts, expressions } }
77
+ }
78
+ }
79
+ }
80
+ if (ts.isArrayLiteralExpression(expr)) {
81
+ return expr.elements.map((el) =>
82
+ extractAstValue(el, refParamName, templateParamName)
83
+ )
84
+ }
85
+ if (ts.isObjectLiteralExpression(expr)) {
86
+ const obj: Record<string, unknown> = {}
87
+ for (const prop of expr.properties) {
88
+ if (!ts.isPropertyAssignment(prop)) continue
89
+ const key = ts.isIdentifier(prop.name)
90
+ ? prop.name.text
91
+ : ts.isStringLiteral(prop.name)
92
+ ? prop.name.text
93
+ : null
94
+ if (!key) continue
95
+ obj[key] = extractAstValue(
96
+ prop.initializer,
97
+ refParamName,
98
+ templateParamName
99
+ )
100
+ }
101
+ return obj
102
+ }
103
+ if (ts.isPrefixUnaryExpression(expr)) {
104
+ if (
105
+ expr.operator === ts.SyntaxKind.MinusToken &&
106
+ ts.isNumericLiteral(expr.operand)
107
+ ) {
108
+ return -Number(expr.operand.text)
109
+ }
110
+ }
111
+ return undefined
112
+ }
113
+
114
+ function extractInputMapping(
115
+ node: ts.Node,
116
+ _checker: ts.TypeChecker
117
+ ): Record<string, unknown | DataRef> {
118
+ if (!ts.isArrowFunction(node)) {
119
+ return {}
120
+ }
121
+
122
+ let bodyObj: ts.ObjectLiteralExpression | undefined
123
+
124
+ if (ts.isObjectLiteralExpression(node.body)) {
125
+ bodyObj = node.body
126
+ } else if (ts.isParenthesizedExpression(node.body)) {
127
+ if (ts.isObjectLiteralExpression(node.body.expression)) {
128
+ bodyObj = node.body.expression
129
+ }
130
+ } else if (ts.isBlock(node.body)) {
131
+ for (const stmt of node.body.statements) {
132
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
133
+ if (ts.isObjectLiteralExpression(stmt.expression)) {
134
+ bodyObj = stmt.expression
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ if (!bodyObj) {
141
+ return {}
142
+ }
143
+
144
+ const refParamName =
145
+ node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
146
+ ? node.parameters[0].name.text
147
+ : 'ref'
148
+
149
+ const templateParamName =
150
+ node.parameters.length > 1 && ts.isIdentifier(node.parameters[1].name)
151
+ ? node.parameters[1].name.text
152
+ : undefined
153
+
154
+ const input: Record<string, unknown | DataRef> = {}
155
+
156
+ for (const prop of bodyObj.properties) {
157
+ if (!ts.isPropertyAssignment(prop)) continue
158
+
159
+ const key = ts.isIdentifier(prop.name)
160
+ ? prop.name.text
161
+ : ts.isStringLiteral(prop.name)
162
+ ? prop.name.text
163
+ : null
164
+
165
+ if (!key) continue
166
+
167
+ const value = extractAstValue(
168
+ prop.initializer,
169
+ refParamName,
170
+ templateParamName
171
+ )
172
+ if (value !== undefined) {
173
+ input[key] = value
174
+ }
175
+ }
176
+
177
+ return input
178
+ }
179
+
180
+ /**
181
+ * Extract next config (string, array, or record)
182
+ */
183
+ function extractNextConfig(
184
+ node: ts.Node,
185
+ _checker: ts.TypeChecker
186
+ ): string | string[] | Record<string, string | string[]> | undefined {
187
+ if (ts.isStringLiteral(node)) {
188
+ return node.text
189
+ }
190
+
191
+ if (ts.isArrayLiteralExpression(node)) {
192
+ return node.elements
193
+ .filter(ts.isStringLiteral)
194
+ .map((el) => (el as ts.StringLiteral).text)
195
+ }
196
+
197
+ if (ts.isObjectLiteralExpression(node)) {
198
+ const result: Record<string, string | string[]> = {}
199
+ for (const prop of node.properties) {
200
+ if (!ts.isPropertyAssignment(prop)) continue
201
+
202
+ const key = ts.isIdentifier(prop.name)
203
+ ? prop.name.text
204
+ : ts.isStringLiteral(prop.name)
205
+ ? prop.name.text
206
+ : null
207
+
208
+ if (!key) continue
209
+
210
+ if (ts.isStringLiteral(prop.initializer)) {
211
+ result[key] = prop.initializer.text
212
+ } else if (ts.isArrayLiteralExpression(prop.initializer)) {
213
+ result[key] = prop.initializer.elements
214
+ .filter(ts.isStringLiteral)
215
+ .map((el) => (el as ts.StringLiteral).text)
216
+ }
217
+ }
218
+ return result
219
+ }
220
+
221
+ return undefined
222
+ }
223
+
224
+ /**
225
+ * Compute entry node IDs from graph nodes
226
+ */
227
+ function computeEntryNodeIds(graphNodes: Record<string, any>): string[] {
228
+ const hasIncomingEdge = new Set<string>()
229
+ for (const node of Object.values(graphNodes)) {
230
+ const next = node.next
231
+ if (!next) continue
232
+
233
+ if (typeof next === 'string') {
234
+ hasIncomingEdge.add(next)
235
+ } else if (Array.isArray(next)) {
236
+ next.forEach((n: string) => hasIncomingEdge.add(n))
237
+ } else if (typeof next === 'object') {
238
+ for (const targets of Object.values(next)) {
239
+ if (typeof targets === 'string') {
240
+ hasIncomingEdge.add(targets)
241
+ } else if (Array.isArray(targets)) {
242
+ ;(targets as string[]).forEach((n) => hasIncomingEdge.add(n))
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return Object.keys(graphNodes).filter(
249
+ (nodeId) => !hasIncomingEdge.has(nodeId)
250
+ )
251
+ }
252
+
253
+ interface PikkuWorkflowGraphExtract {
254
+ name?: string
255
+ description?: string
256
+ tags?: string[]
257
+ disabled?: true
258
+ nodesNode?: ts.ObjectLiteralExpression
259
+ configNode?: ts.ObjectLiteralExpression
260
+ exportedName?: string
261
+ }
262
+
263
+ /**
264
+ * Extract pikkuWorkflowGraph config from an object literal argument
265
+ */
266
+ function extractWorkflowGraphConfig(
267
+ configArg: ts.ObjectLiteralExpression,
268
+ checker: ts.TypeChecker
269
+ ): PikkuWorkflowGraphExtract | undefined {
270
+ let name: string | undefined
271
+ let description: string | undefined
272
+ let tags: string[] | undefined
273
+ let disabled: true | undefined
274
+ let nodesNode: ts.ObjectLiteralExpression | undefined
275
+ let configNode: ts.ObjectLiteralExpression | undefined
276
+
277
+ for (const prop of configArg.properties) {
278
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
279
+
280
+ const propName = prop.name.text
281
+ if (propName === 'name') {
282
+ name = extractStringLiteral(prop.initializer, checker)
283
+ } else if (propName === 'description') {
284
+ description = extractStringLiteral(prop.initializer, checker)
285
+ } else if (propName === 'disabled') {
286
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
287
+ disabled = true
288
+ }
289
+ } else if (
290
+ propName === 'tags' &&
291
+ ts.isArrayLiteralExpression(prop.initializer)
292
+ ) {
293
+ tags = prop.initializer.elements
294
+ .filter(ts.isStringLiteral)
295
+ .map((el) => (el as ts.StringLiteral).text)
296
+ } else if (
297
+ propName === 'nodes' &&
298
+ ts.isObjectLiteralExpression(prop.initializer)
299
+ ) {
300
+ nodesNode = prop.initializer
301
+ } else if (
302
+ propName === 'config' &&
303
+ ts.isObjectLiteralExpression(prop.initializer)
304
+ ) {
305
+ configNode = prop.initializer
306
+ }
307
+ }
308
+
309
+ return { name, description, tags, disabled, nodesNode, configNode }
310
+ }
311
+
312
+ /**
313
+ * Extract graph nodes from the new pikkuWorkflowGraph format
314
+ * New format: { nodes: { entry: 'rpcName', ... }, config: { entry: { next: 'sendWelcome', ... }, ... } }
315
+ */
316
+ function extractGraphFromNewFormat(
317
+ nodesNode: ts.ObjectLiteralExpression | undefined,
318
+ configNode: ts.ObjectLiteralExpression | undefined,
319
+ checker: ts.TypeChecker,
320
+ state: any
321
+ ): Record<string, any> {
322
+ const nodes: Record<string, any> = {}
323
+
324
+ if (!nodesNode) {
325
+ return nodes
326
+ }
327
+
328
+ // Extract node ID to RPC name mapping from 'nodes' property
329
+ const nodeRpcMap: Record<string, string> = {}
330
+ for (const prop of nodesNode.properties) {
331
+ if (!ts.isPropertyAssignment(prop)) continue
332
+
333
+ const nodeId = ts.isIdentifier(prop.name)
334
+ ? prop.name.text
335
+ : ts.isStringLiteral(prop.name)
336
+ ? prop.name.text
337
+ : null
338
+
339
+ if (!nodeId) continue
340
+
341
+ const rpcName = extractStringLiteral(prop.initializer, checker)
342
+ if (rpcName) {
343
+ nodeRpcMap[nodeId] = rpcName
344
+ state.rpc.invokedFunctions.add(rpcName)
345
+ const funcFile = state.functions.files.get(rpcName)
346
+ if (funcFile && !state.rpc.internalFiles.has(rpcName)) {
347
+ state.rpc.internalFiles.set(rpcName, funcFile)
348
+ }
349
+ }
350
+ }
351
+
352
+ // Initialize nodes with their RPC names
353
+ for (const [nodeId, rpcName] of Object.entries(nodeRpcMap)) {
354
+ nodes[nodeId] = {
355
+ nodeId,
356
+ rpcName,
357
+ input: {},
358
+ next: undefined,
359
+ onError: undefined,
360
+ }
361
+ }
362
+
363
+ // Extract config for each node from 'config' property
364
+ if (configNode) {
365
+ for (const prop of configNode.properties) {
366
+ if (!ts.isPropertyAssignment(prop)) continue
367
+
368
+ const nodeId = ts.isIdentifier(prop.name)
369
+ ? prop.name.text
370
+ : ts.isStringLiteral(prop.name)
371
+ ? prop.name.text
372
+ : null
373
+
374
+ if (!nodeId || !nodes[nodeId]) continue
375
+
376
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
377
+ const nodeConfig = extractNodeConfigFromObject(
378
+ prop.initializer,
379
+ checker
380
+ )
381
+ if (nodeConfig) {
382
+ nodes[nodeId].next = nodeConfig.next
383
+ nodes[nodeId].onError = nodeConfig.onError
384
+ nodes[nodeId].input = nodeConfig.input
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ return nodes
391
+ }
392
+
393
+ /**
394
+ * Extract node config (next, onError, input) from object literal
395
+ */
396
+ function extractNodeConfigFromObject(
397
+ obj: ts.ObjectLiteralExpression,
398
+ checker: ts.TypeChecker
399
+ ): {
400
+ next: any
401
+ onError: any
402
+ input: Record<string, any>
403
+ } {
404
+ let next: any = undefined
405
+ let onError: any = undefined
406
+ let input: Record<string, any> = {}
407
+
408
+ for (const prop of obj.properties) {
409
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
410
+
411
+ const propName = prop.name.text
412
+
413
+ if (propName === 'next') {
414
+ next = extractNextConfig(prop.initializer, checker)
415
+ } else if (propName === 'onError') {
416
+ onError = extractNextConfig(prop.initializer, checker)
417
+ } else if (propName === 'input') {
418
+ input = extractInputMapping(prop.initializer, checker)
419
+ }
420
+ }
421
+
422
+ return { next, onError, input }
423
+ }
424
+
425
+ /**
426
+ * Inspector for pikkuWorkflowGraph() calls
427
+ * Detects: pikkuWorkflowGraph({ nodes: {...}, config: {...} })
428
+ * or: export const x = pikkuWorkflowGraph({...}) where the call is found via variable declaration
429
+ */
430
+ export const addWorkflowGraph: AddWiring = (logger, node, checker, state) => {
431
+ if (!ts.isCallExpression(node)) {
432
+ return
433
+ }
434
+
435
+ const expression = node.expression
436
+ if (
437
+ !ts.isIdentifier(expression) ||
438
+ expression.text !== 'pikkuWorkflowGraph'
439
+ ) {
440
+ return
441
+ }
442
+
443
+ const args = node.arguments
444
+ const firstArg = args[0]
445
+
446
+ if (!firstArg) {
447
+ logger.critical(
448
+ ErrorCode.MISSING_FUNC,
449
+ 'pikkuWorkflowGraph requires an argument'
450
+ )
451
+ return
452
+ }
453
+
454
+ if (!ts.isObjectLiteralExpression(firstArg)) {
455
+ logger.critical(
456
+ ErrorCode.MISSING_FUNC,
457
+ 'pikkuWorkflowGraph requires an object argument'
458
+ )
459
+ return
460
+ }
461
+
462
+ const graphConfig = extractWorkflowGraphConfig(firstArg, checker)
463
+
464
+ if (!graphConfig) {
465
+ logger.critical(
466
+ ErrorCode.MISSING_NAME,
467
+ 'pikkuWorkflowGraph: failed to extract config'
468
+ )
469
+ return
470
+ }
471
+
472
+ // Resolve exportedName from variable declaration if this is `export const x = pikkuWorkflowGraph({...})`
473
+ const parent = node.parent
474
+ if (
475
+ !graphConfig.exportedName &&
476
+ parent &&
477
+ ts.isVariableDeclaration(parent) &&
478
+ ts.isIdentifier(parent.name)
479
+ ) {
480
+ graphConfig.exportedName = parent.name.text
481
+ }
482
+
483
+ const workflowName = graphConfig.name || graphConfig.exportedName
484
+
485
+ if (!workflowName) {
486
+ logger.critical(
487
+ ErrorCode.MISSING_NAME,
488
+ 'pikkuWorkflowGraph requires a name property or exported variable name'
489
+ )
490
+ return
491
+ }
492
+
493
+ let graphNodes: Record<string, any> = {}
494
+ if (graphConfig.nodesNode) {
495
+ graphNodes = extractGraphFromNewFormat(
496
+ graphConfig.nodesNode,
497
+ graphConfig.configNode,
498
+ checker,
499
+ state
500
+ )
501
+ }
502
+
503
+ const entryNodeIds = computeEntryNodeIds(graphNodes)
504
+
505
+ const serialized: SerializedWorkflowGraph = {
506
+ name: workflowName,
507
+ pikkuFuncId: workflowName,
508
+ source: 'graph',
509
+ description: graphConfig.description,
510
+ tags: graphConfig.tags,
511
+ nodes: graphNodes,
512
+ entryNodeIds,
513
+ }
514
+
515
+ if (graphConfig.disabled) return
516
+
517
+ state.workflows.graphMeta[workflowName] = serialized
518
+ state.workflows.graphFiles.set(workflowName, {
519
+ path: node.getSourceFile().fileName,
520
+ exportedName: graphConfig.exportedName || workflowName,
521
+ })
522
+ }