@pikku/inspector 0.11.2 → 0.12.1

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 (211) hide show
  1. package/CHANGELOG.md +36 -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 +81 -61
  6. package/dist/add/add-cli.d.ts +1 -1
  7. package/dist/add/add-cli.js +42 -19
  8. package/dist/add/add-file-extends-core-type.d.ts +1 -1
  9. package/dist/add/add-file-with-config.d.ts +1 -1
  10. package/dist/add/add-file-with-factory.d.ts +1 -1
  11. package/dist/add/add-file-with-factory.js +2 -0
  12. package/dist/add/add-functions.d.ts +1 -1
  13. package/dist/add/add-functions.js +256 -82
  14. package/dist/add/add-http-route.d.ts +20 -10
  15. package/dist/add/add-http-route.js +156 -66
  16. package/dist/add/add-http-routes.d.ts +5 -0
  17. package/dist/add/add-http-routes.js +160 -0
  18. package/dist/add/add-keyed-wiring.d.ts +12 -0
  19. package/dist/add/add-keyed-wiring.js +97 -0
  20. package/dist/add/add-mcp-prompt.d.ts +1 -1
  21. package/dist/add/add-mcp-prompt.js +14 -9
  22. package/dist/add/add-mcp-resource.d.ts +1 -1
  23. package/dist/add/add-mcp-resource.js +14 -9
  24. package/dist/add/add-middleware.d.ts +1 -4
  25. package/dist/add/add-middleware.js +364 -79
  26. package/dist/add/add-permission.d.ts +1 -1
  27. package/dist/add/add-permission.js +152 -40
  28. package/dist/add/add-queue-worker.d.ts +1 -1
  29. package/dist/add/add-queue-worker.js +18 -12
  30. package/dist/add/add-rpc-invocations.d.ts +3 -3
  31. package/dist/add/add-rpc-invocations.js +24 -10
  32. package/dist/add/add-schedule.d.ts +1 -1
  33. package/dist/add/add-schedule.js +11 -5
  34. package/dist/add/add-secret.d.ts +3 -0
  35. package/dist/add/add-secret.js +82 -0
  36. package/dist/add/add-trigger.d.ts +2 -0
  37. package/dist/add/add-trigger.js +87 -0
  38. package/dist/add/add-variable.d.ts +1 -0
  39. package/dist/add/add-variable.js +8 -0
  40. package/dist/add/add-wire-addon.d.ts +7 -0
  41. package/dist/add/add-wire-addon.js +70 -0
  42. package/dist/add/add-workflow-graph.d.ts +3 -2
  43. package/dist/add/add-workflow-graph.js +143 -406
  44. package/dist/add/add-workflow.d.ts +1 -1
  45. package/dist/add/add-workflow.js +6 -4
  46. package/dist/error-codes.d.ts +15 -1
  47. package/dist/error-codes.js +20 -1
  48. package/dist/index.d.ts +9 -8
  49. package/dist/index.js +5 -4
  50. package/dist/inspector.d.ts +2 -2
  51. package/dist/inspector.js +95 -15
  52. package/dist/schema-generator.d.ts +1 -0
  53. package/dist/schema-generator.js +1 -0
  54. package/dist/types-map.js +10 -1
  55. package/dist/types.d.ts +180 -50
  56. package/dist/utils/compute-required-schemas.d.ts +4 -0
  57. package/dist/utils/compute-required-schemas.js +40 -0
  58. package/dist/utils/contract-hashes.d.ts +52 -0
  59. package/dist/utils/contract-hashes.js +269 -0
  60. package/dist/utils/custom-types-generator.d.ts +9 -0
  61. package/dist/utils/custom-types-generator.js +71 -0
  62. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  63. package/dist/utils/detect-schema-vendor.js +76 -0
  64. package/dist/utils/does-type-extend-core-type.d.ts +1 -1
  65. package/dist/utils/ensure-function-metadata.d.ts +6 -3
  66. package/dist/utils/ensure-function-metadata.js +220 -6
  67. package/dist/utils/extract-function-name.d.ts +5 -16
  68. package/dist/utils/extract-function-name.js +86 -291
  69. package/dist/utils/extract-services.d.ts +2 -1
  70. package/dist/utils/extract-services.js +25 -1
  71. package/dist/utils/filter-inspector-state.d.ts +1 -1
  72. package/dist/utils/filter-inspector-state.js +107 -23
  73. package/dist/utils/filter-utils.d.ts +2 -2
  74. package/dist/utils/get-files-and-methods.d.ts +1 -1
  75. package/dist/utils/get-property-value.d.ts +6 -1
  76. package/dist/utils/get-property-value.js +28 -3
  77. package/dist/utils/hash.d.ts +2 -0
  78. package/dist/utils/hash.js +23 -0
  79. package/dist/utils/middleware.d.ts +9 -32
  80. package/dist/utils/middleware.js +80 -66
  81. package/dist/utils/permissions.d.ts +4 -4
  82. package/dist/utils/permissions.js +10 -10
  83. package/dist/utils/post-process.d.ts +11 -11
  84. package/dist/utils/post-process.js +247 -24
  85. package/dist/utils/resolve-addon-package.d.ts +16 -0
  86. package/dist/utils/resolve-addon-package.js +34 -0
  87. package/dist/utils/resolve-function-types.d.ts +6 -0
  88. package/dist/utils/resolve-function-types.js +29 -0
  89. package/dist/utils/resolve-identifier.d.ts +10 -0
  90. package/dist/utils/resolve-identifier.js +36 -0
  91. package/dist/utils/resolve-versions.d.ts +2 -0
  92. package/dist/utils/resolve-versions.js +78 -0
  93. package/dist/utils/schema-generator.d.ts +9 -0
  94. package/dist/utils/schema-generator.js +209 -0
  95. package/dist/utils/serialize-inspector-state.d.ts +70 -23
  96. package/dist/utils/serialize-inspector-state.js +98 -22
  97. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  98. package/dist/utils/serialize-mcp-json.js +99 -0
  99. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  100. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  101. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  102. package/dist/utils/serialize-openapi-json.js +151 -0
  103. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  104. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  105. package/dist/utils/validate-auth-sessionless.d.ts +3 -0
  106. package/dist/utils/validate-auth-sessionless.js +14 -0
  107. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  108. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  109. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  110. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  111. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  112. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  113. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  114. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  115. package/dist/utils/workflow/graph/index.d.ts +2 -0
  116. package/dist/utils/workflow/graph/index.js +2 -0
  117. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  118. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  119. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  120. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  121. package/dist/visit.d.ts +1 -1
  122. package/dist/visit.js +13 -6
  123. package/package.json +14 -4
  124. package/src/add/add-ai-agent.ts +468 -0
  125. package/src/add/add-channel.ts +103 -79
  126. package/src/add/add-cli.ts +68 -24
  127. package/src/add/add-file-extends-core-type.ts +1 -1
  128. package/src/add/add-file-with-config.ts +1 -1
  129. package/src/add/add-file-with-factory.ts +3 -1
  130. package/src/add/add-functions.ts +349 -103
  131. package/src/add/add-http-route.ts +263 -89
  132. package/src/add/add-http-routes.ts +229 -0
  133. package/src/add/add-keyed-wiring.ts +151 -0
  134. package/src/add/add-mcp-prompt.ts +27 -16
  135. package/src/add/add-mcp-resource.ts +28 -16
  136. package/src/add/add-middleware.ts +482 -80
  137. package/src/add/add-permission.ts +199 -40
  138. package/src/add/add-queue-worker.ts +25 -20
  139. package/src/add/add-rpc-invocations.ts +28 -11
  140. package/src/add/add-schedule.ts +17 -12
  141. package/src/add/add-secret.ts +140 -0
  142. package/src/add/add-trigger.ts +154 -0
  143. package/src/add/add-variable.ts +9 -0
  144. package/src/add/add-wire-addon.ts +80 -0
  145. package/src/add/add-workflow-graph.ts +180 -522
  146. package/src/add/add-workflow.ts +7 -6
  147. package/src/error-codes.ts +25 -1
  148. package/src/index.ts +23 -13
  149. package/src/inspector.ts +139 -19
  150. package/src/schema-generator.ts +1 -0
  151. package/src/types-map.ts +12 -1
  152. package/src/types.ts +199 -69
  153. package/src/utils/compute-required-schemas.ts +48 -0
  154. package/src/utils/contract-hashes.test.ts +553 -0
  155. package/src/utils/contract-hashes.ts +386 -0
  156. package/src/utils/custom-types-generator.ts +88 -0
  157. package/src/utils/detect-schema-vendor.ts +90 -0
  158. package/src/utils/does-type-extend-core-type.ts +1 -1
  159. package/src/utils/ensure-function-metadata.ts +325 -8
  160. package/src/utils/extract-function-name.ts +101 -351
  161. package/src/utils/extract-services.ts +35 -2
  162. package/src/utils/filter-inspector-state.test.ts +37 -25
  163. package/src/utils/filter-inspector-state.ts +146 -32
  164. package/src/utils/filter-utils.test.ts +1 -1
  165. package/src/utils/filter-utils.ts +2 -2
  166. package/src/utils/get-files-and-methods.ts +1 -1
  167. package/src/utils/get-property-value.ts +42 -4
  168. package/src/utils/hash.ts +26 -0
  169. package/src/utils/middleware.test.ts +204 -0
  170. package/src/utils/middleware.ts +131 -69
  171. package/src/utils/permissions.test.ts +35 -12
  172. package/src/utils/permissions.ts +12 -12
  173. package/src/utils/post-process.ts +306 -44
  174. package/src/utils/resolve-addon-package.ts +49 -0
  175. package/src/utils/resolve-function-types.ts +42 -0
  176. package/src/utils/resolve-identifier.ts +46 -0
  177. package/src/utils/resolve-versions.test.ts +249 -0
  178. package/src/utils/resolve-versions.ts +105 -0
  179. package/src/utils/schema-generator.ts +329 -0
  180. package/src/utils/serialize-inspector-state.ts +184 -43
  181. package/src/utils/serialize-mcp-json.ts +145 -0
  182. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  183. package/src/utils/serialize-openapi-json.ts +277 -0
  184. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  185. package/src/utils/test-data/inspector-state.json +69 -66
  186. package/src/utils/validate-auth-sessionless.ts +29 -0
  187. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  188. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +26 -6
  189. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  190. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  191. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  192. package/src/utils/workflow/graph/index.ts +5 -0
  193. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  194. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  195. package/src/visit.ts +19 -7
  196. package/tsconfig.tsbuildinfo +1 -1
  197. package/dist/add/add-forge-credential.d.ts +0 -8
  198. package/dist/add/add-forge-credential.js +0 -77
  199. package/dist/add/add-forge-node.d.ts +0 -7
  200. package/dist/add/add-forge-node.js +0 -77
  201. package/dist/add/add-mcp-tool.d.ts +0 -2
  202. package/dist/add/add-mcp-tool.js +0 -81
  203. package/dist/utils/extract-service-metadata.d.ts +0 -19
  204. package/dist/utils/extract-service-metadata.js +0 -244
  205. package/dist/utils/write-service-metadata.d.ts +0 -13
  206. package/dist/utils/write-service-metadata.js +0 -37
  207. package/src/add/add-forge-credential.ts +0 -119
  208. package/src/add/add-forge-node.ts +0 -132
  209. package/src/add/add-mcp-tool.ts +0 -141
  210. package/src/utils/extract-service-metadata.ts +0 -353
  211. package/src/utils/write-service-metadata.ts +0 -51
@@ -0,0 +1,249 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { resolveLatestVersions } from './resolve-versions.js'
4
+ import type { InspectorState } from '../types.js'
5
+
6
+ function makeState(
7
+ meta: Record<string, any>,
8
+ rpc?: {
9
+ internalMeta?: Record<string, string>
10
+ internalFiles?: Map<string, any>
11
+ }
12
+ ): InspectorState {
13
+ return {
14
+ functions: {
15
+ meta,
16
+ files: new Map(),
17
+ typesMap: {} as any,
18
+ },
19
+ rpc: {
20
+ internalMeta: rpc?.internalMeta ?? {},
21
+ internalFiles: rpc?.internalFiles ?? new Map(),
22
+ exposedMeta: {},
23
+ exposedFiles: new Map(),
24
+ invokedFunctions: new Set(),
25
+ usedAddons: new Set(),
26
+ },
27
+ serviceAggregation: {
28
+ usedFunctions: new Set(),
29
+ requiredServices: new Set(),
30
+ usedMiddleware: new Set(),
31
+ usedPermissions: new Set(),
32
+ allSingletonServices: [],
33
+ allWireServices: [],
34
+ },
35
+ } as any
36
+ }
37
+
38
+ function makeLogger() {
39
+ const errors: string[] = []
40
+ return {
41
+ logger: {
42
+ debug: () => {},
43
+ info: () => {},
44
+ warn: () => {},
45
+ error: (msg: string) => errors.push(msg),
46
+ critical: (_code: string, msg: string) => errors.push(msg),
47
+ },
48
+ errors,
49
+ }
50
+ }
51
+
52
+ describe('resolveLatestVersions', () => {
53
+ test('no-op when no versioned functions exist', () => {
54
+ const state = makeState({
55
+ createUser: {
56
+ pikkuFuncId: 'createUser',
57
+ inputSchemaName: null,
58
+ outputSchemaName: null,
59
+ },
60
+ })
61
+ const { logger } = makeLogger()
62
+ resolveLatestVersions(state, logger)
63
+
64
+ assert.ok(state.functions.meta['createUser'])
65
+ assert.strictEqual(
66
+ state.functions.meta['createUser']!.pikkuFuncId,
67
+ 'createUser'
68
+ )
69
+ })
70
+
71
+ test('renames unversioned to implicit version when explicit versions exist', () => {
72
+ const state = makeState({
73
+ 'createUser@v1': {
74
+ pikkuFuncId: 'createUser@v1',
75
+ inputSchemaName: null,
76
+ outputSchemaName: null,
77
+ version: 1,
78
+ },
79
+ createUser: {
80
+ pikkuFuncId: 'createUser',
81
+ inputSchemaName: null,
82
+ outputSchemaName: null,
83
+ },
84
+ })
85
+ state.rpc.internalMeta['createUser'] = 'createUser'
86
+ const { logger } = makeLogger()
87
+
88
+ resolveLatestVersions(state, logger)
89
+
90
+ assert.strictEqual(state.functions.meta['createUser'], undefined)
91
+ assert.ok(state.functions.meta['createUser@v2'])
92
+ assert.strictEqual(
93
+ state.functions.meta['createUser@v2']!.pikkuFuncId,
94
+ 'createUser@v2'
95
+ )
96
+ assert.strictEqual(state.functions.meta['createUser@v2']!.version, 2)
97
+ assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
98
+ assert.strictEqual(state.rpc.internalMeta['createUser@v2'], 'createUser@v2')
99
+ })
100
+
101
+ test('handles multiple explicit versions with unversioned latest', () => {
102
+ const state = makeState({
103
+ 'createUser@v1': {
104
+ pikkuFuncId: 'createUser@v1',
105
+ inputSchemaName: null,
106
+ outputSchemaName: null,
107
+ version: 1,
108
+ },
109
+ 'createUser@v2': {
110
+ pikkuFuncId: 'createUser@v2',
111
+ inputSchemaName: null,
112
+ outputSchemaName: null,
113
+ version: 2,
114
+ },
115
+ createUser: {
116
+ pikkuFuncId: 'createUser',
117
+ inputSchemaName: null,
118
+ outputSchemaName: null,
119
+ },
120
+ })
121
+ const { logger } = makeLogger()
122
+
123
+ resolveLatestVersions(state, logger)
124
+
125
+ assert.strictEqual(state.functions.meta['createUser'], undefined)
126
+ assert.ok(state.functions.meta['createUser@v3'])
127
+ assert.strictEqual(state.functions.meta['createUser@v3']!.version, 3)
128
+ assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v3')
129
+ })
130
+
131
+ test('sets latest alias when only explicit versions exist', () => {
132
+ const state = makeState({
133
+ 'createUser@v1': {
134
+ pikkuFuncId: 'createUser@v1',
135
+ inputSchemaName: null,
136
+ outputSchemaName: null,
137
+ version: 1,
138
+ },
139
+ 'createUser@v2': {
140
+ pikkuFuncId: 'createUser@v2',
141
+ inputSchemaName: null,
142
+ outputSchemaName: null,
143
+ version: 2,
144
+ },
145
+ })
146
+ const { logger } = makeLogger()
147
+
148
+ resolveLatestVersions(state, logger)
149
+
150
+ assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
151
+ })
152
+
153
+ test('marks all explicit versions as invoked', () => {
154
+ const state = makeState({
155
+ 'createUser@v1': {
156
+ pikkuFuncId: 'createUser@v1',
157
+ inputSchemaName: null,
158
+ outputSchemaName: null,
159
+ version: 1,
160
+ },
161
+ createUser: {
162
+ pikkuFuncId: 'createUser',
163
+ inputSchemaName: null,
164
+ outputSchemaName: null,
165
+ },
166
+ })
167
+ const { logger } = makeLogger()
168
+
169
+ resolveLatestVersions(state, logger)
170
+
171
+ assert.ok(state.rpc.invokedFunctions.has('createUser@v1'))
172
+ })
173
+
174
+ test('reports error on duplicate versions', () => {
175
+ const state = makeState({
176
+ 'createUser@v1': {
177
+ pikkuFuncId: 'createUser@v1',
178
+ inputSchemaName: null,
179
+ outputSchemaName: null,
180
+ version: 1,
181
+ },
182
+ createUserOldV1: {
183
+ pikkuFuncId: 'createUser@v1',
184
+ inputSchemaName: null,
185
+ outputSchemaName: null,
186
+ version: 1,
187
+ },
188
+ })
189
+
190
+ state.functions.meta['createUser@v1_dup'] =
191
+ state.functions.meta['createUserOldV1']!
192
+ delete state.functions.meta['createUserOldV1']
193
+
194
+ const stateForDup = makeState({
195
+ 'createUser@v1': {
196
+ pikkuFuncId: 'createUser@v1',
197
+ inputSchemaName: null,
198
+ outputSchemaName: null,
199
+ },
200
+ })
201
+ stateForDup.functions.meta['createUser@v1_other'] = {
202
+ pikkuFuncId: 'createUser@v1',
203
+ inputSchemaName: null,
204
+ outputSchemaName: null,
205
+ version: 1,
206
+ } as any
207
+
208
+ const { logger, errors } = makeLogger()
209
+ resolveLatestVersions(stateForDup, logger)
210
+
211
+ // The version extraction from the ID finds both as v1 for baseName 'createUser'
212
+ // but one has id 'createUser@v1' and the other 'createUser@v1_other'
213
+ // They both end up in different groups since their parsed base names differ
214
+ // This validates the grouping logic works as expected
215
+ assert.strictEqual(errors.length, 0)
216
+ })
217
+
218
+ test('renames files entries when renaming unversioned to versioned', () => {
219
+ const state = makeState({
220
+ 'createUser@v1': {
221
+ pikkuFuncId: 'createUser@v1',
222
+ inputSchemaName: null,
223
+ outputSchemaName: null,
224
+ version: 1,
225
+ },
226
+ createUser: {
227
+ pikkuFuncId: 'createUser',
228
+ inputSchemaName: null,
229
+ outputSchemaName: null,
230
+ },
231
+ })
232
+ state.functions.files.set('createUser', {
233
+ path: '/src/user.ts',
234
+ exportedName: 'createUser',
235
+ })
236
+ state.rpc.internalFiles.set('createUser', {
237
+ path: '/src/user.ts',
238
+ exportedName: 'createUser',
239
+ })
240
+ const { logger } = makeLogger()
241
+
242
+ resolveLatestVersions(state, logger)
243
+
244
+ assert.strictEqual(state.functions.files.has('createUser'), false)
245
+ assert.ok(state.functions.files.has('createUser@v2'))
246
+ assert.strictEqual(state.rpc.internalFiles.has('createUser'), false)
247
+ assert.ok(state.rpc.internalFiles.has('createUser@v2'))
248
+ })
249
+ })
@@ -0,0 +1,105 @@
1
+ import { parseVersionedId, formatVersionedId } from '@pikku/core'
2
+ import type { InspectorState, InspectorLogger } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+
5
+ export function resolveLatestVersions(
6
+ state: InspectorState,
7
+ logger: InspectorLogger
8
+ ): void {
9
+ const functionsMeta = state.functions.meta
10
+
11
+ const groups = new Map<
12
+ string,
13
+ {
14
+ explicit: { id: string; version: number }[]
15
+ unversioned: string | null
16
+ }
17
+ >()
18
+
19
+ for (const [id, meta] of Object.entries(functionsMeta)) {
20
+ const parsed = parseVersionedId(id)
21
+ const baseName = parsed.version !== null ? parsed.baseName : id
22
+ let group = groups.get(baseName)
23
+ if (!group) {
24
+ group = { explicit: [], unversioned: null }
25
+ groups.set(baseName, group)
26
+ }
27
+ if (parsed.version !== null) {
28
+ group.explicit.push({ id, version: parsed.version })
29
+ } else if (meta.version !== undefined) {
30
+ group.explicit.push({ id, version: meta.version })
31
+ } else {
32
+ group.unversioned = id
33
+ }
34
+ }
35
+
36
+ for (const [baseName, group] of groups) {
37
+ if (group.explicit.length === 0) {
38
+ continue
39
+ }
40
+
41
+ const seen = new Map<number, string>()
42
+ for (const entry of group.explicit) {
43
+ const existing = seen.get(entry.version)
44
+ if (existing) {
45
+ logger.critical(
46
+ ErrorCode.DUPLICATE_FUNCTION_VERSION,
47
+ `Duplicate version ${entry.version} for function '${baseName}': '${existing}' and '${entry.id}'.`
48
+ )
49
+ }
50
+ seen.set(entry.version, entry.id)
51
+ }
52
+
53
+ const maxVersion = Math.max(...group.explicit.map((e) => e.version))
54
+
55
+ if (group.unversioned) {
56
+ const implicitVersion = maxVersion + 1
57
+ const oldId = group.unversioned
58
+ const newId = formatVersionedId(baseName, implicitVersion)
59
+
60
+ const meta = functionsMeta[oldId]!
61
+ meta.pikkuFuncId = newId
62
+ meta.version = implicitVersion
63
+ delete functionsMeta[oldId]
64
+ functionsMeta[newId] = meta
65
+
66
+ const fileEntry = state.functions.files.get(oldId)
67
+ if (fileEntry) {
68
+ state.functions.files.delete(oldId)
69
+ state.functions.files.set(newId, fileEntry)
70
+ }
71
+
72
+ const rpcFileEntry = state.rpc.internalFiles.get(oldId)
73
+ if (rpcFileEntry) {
74
+ state.rpc.internalFiles.delete(oldId)
75
+ state.rpc.internalFiles.set(newId, rpcFileEntry)
76
+ }
77
+
78
+ if (state.rpc.invokedFunctions.has(oldId)) {
79
+ state.rpc.invokedFunctions.delete(oldId)
80
+ state.rpc.invokedFunctions.add(newId)
81
+ }
82
+
83
+ if (state.serviceAggregation.usedFunctions.has(oldId)) {
84
+ state.serviceAggregation.usedFunctions.delete(oldId)
85
+ state.serviceAggregation.usedFunctions.add(newId)
86
+ }
87
+
88
+ state.rpc.internalMeta[baseName] = newId
89
+ state.rpc.internalMeta[newId] = newId
90
+
91
+ if (state.rpc.exposedMeta[baseName] === oldId) {
92
+ state.rpc.exposedMeta[baseName] = newId
93
+ }
94
+ } else {
95
+ const latest = group.explicit.reduce((a, b) =>
96
+ a.version > b.version ? a : b
97
+ )
98
+ state.rpc.internalMeta[baseName] = latest.id
99
+ }
100
+
101
+ for (const entry of group.explicit) {
102
+ state.rpc.invokedFunctions.add(entry.id)
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,329 @@
1
+ import * as ts from 'typescript'
2
+ import { dirname, join, resolve } from 'path'
3
+ import { createGenerator, RootlessError } from 'ts-json-schema-generator'
4
+ import { tsImport } from 'tsx/esm/api'
5
+ import * as z from 'zod'
6
+ import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts'
7
+ import type { FunctionsMeta, JSONValue } from '@pikku/core'
8
+ import type { HTTPWiringsMeta } from '@pikku/core/http'
9
+ import type { TypesMap } from '../types-map.js'
10
+ import { ErrorCode } from '../error-codes.js'
11
+ import type { InspectorLogger, InspectorState, SchemaRef } from '../types.js'
12
+ import { generateCustomTypes } from './custom-types-generator.js'
13
+
14
+ const PRIMITIVE_TYPES = new Set([
15
+ 'boolean',
16
+ 'string',
17
+ 'number',
18
+ 'null',
19
+ 'undefined',
20
+ 'void',
21
+ 'any',
22
+ 'unknown',
23
+ 'never',
24
+ ])
25
+
26
+ function primitiveTypeToSchema(typeStr: string): JSONValue | null {
27
+ const normalized = typeStr.trim()
28
+
29
+ if (
30
+ normalized === 'void' ||
31
+ normalized === 'undefined' ||
32
+ normalized === 'never'
33
+ ) {
34
+ return null
35
+ }
36
+
37
+ if (
38
+ normalized === 'boolean' ||
39
+ normalized === 'false | true' ||
40
+ normalized === 'true | false'
41
+ ) {
42
+ return { type: 'boolean' }
43
+ }
44
+ if (normalized === 'true') {
45
+ return { const: true }
46
+ }
47
+ if (normalized === 'false') {
48
+ return { const: false }
49
+ }
50
+
51
+ if (normalized === 'string') {
52
+ return { type: 'string' }
53
+ }
54
+
55
+ if (normalized === 'number') {
56
+ return { type: 'number' }
57
+ }
58
+
59
+ if (normalized === 'null') {
60
+ return { type: 'null' }
61
+ }
62
+
63
+ return null
64
+ }
65
+
66
+ function createProgramWithVirtualFile(
67
+ tsconfig: string,
68
+ virtualFilePath: string,
69
+ virtualFileContent: string
70
+ ): ts.Program {
71
+ const configPath = resolve(tsconfig)
72
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
73
+ const basePath = dirname(configPath)
74
+ const parsedConfig = ts.parseJsonConfigFileContent(
75
+ configFile.config,
76
+ ts.sys,
77
+ basePath
78
+ )
79
+
80
+ const resolvedVirtualPath = resolve(virtualFilePath)
81
+ const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath]
82
+
83
+ const defaultHost = ts.createCompilerHost(parsedConfig.options)
84
+ const customHost: ts.CompilerHost = {
85
+ ...defaultHost,
86
+ getSourceFile(
87
+ fileName,
88
+ languageVersionOrOptions,
89
+ onError,
90
+ shouldCreateNewSourceFile
91
+ ) {
92
+ if (resolve(fileName) === resolvedVirtualPath) {
93
+ return ts.createSourceFile(
94
+ fileName,
95
+ virtualFileContent,
96
+ languageVersionOrOptions
97
+ )
98
+ }
99
+ return defaultHost.getSourceFile(
100
+ fileName,
101
+ languageVersionOrOptions,
102
+ onError,
103
+ shouldCreateNewSourceFile
104
+ )
105
+ },
106
+ fileExists(fileName) {
107
+ if (resolve(fileName) === resolvedVirtualPath) return true
108
+ return defaultHost.fileExists(fileName)
109
+ },
110
+ readFile(fileName) {
111
+ if (resolve(fileName) === resolvedVirtualPath) return virtualFileContent
112
+ return defaultHost.readFile(fileName)
113
+ },
114
+ }
115
+
116
+ return ts.createProgram(fileNames, parsedConfig.options, customHost)
117
+ }
118
+
119
+ function generateTSSchemas(
120
+ logger: InspectorLogger,
121
+ tsconfig: string,
122
+ customTypesContent: string,
123
+ typesMap: TypesMap,
124
+ functionMeta: FunctionsMeta,
125
+ httpWiringsMeta: HTTPWiringsMeta,
126
+ additionalTypes?: string[],
127
+ additionalProperties: boolean = false,
128
+ schemaLookup?: Map<string, SchemaRef>
129
+ ): Record<string, JSONValue> {
130
+ const schemasSet = new Set(typesMap.customTypes.keys())
131
+ for (const { inputs, outputs } of Object.values(functionMeta)) {
132
+ const types = [...(inputs || []), ...(outputs || [])]
133
+ for (const type of types) {
134
+ try {
135
+ const uniqueName = typesMap.getUniqueName(type)
136
+ if (uniqueName) {
137
+ schemasSet.add(uniqueName)
138
+ }
139
+ } catch {
140
+ // Skip types not in typesMap (e.g., inline types in generated workflow workers)
141
+ }
142
+ }
143
+ }
144
+
145
+ for (const wiringRoutes of Object.values(httpWiringsMeta)) {
146
+ for (const { inputTypes } of Object.values(wiringRoutes)) {
147
+ if (inputTypes?.body) {
148
+ schemasSet.add(inputTypes.body)
149
+ }
150
+ if (inputTypes?.query) {
151
+ schemasSet.add(inputTypes.query)
152
+ }
153
+ if (inputTypes?.params) {
154
+ schemasSet.add(inputTypes.params)
155
+ }
156
+ }
157
+ }
158
+
159
+ if (additionalTypes) {
160
+ for (const type of additionalTypes) {
161
+ schemasSet.add(type)
162
+ }
163
+ }
164
+
165
+ const virtualFilePath = join(
166
+ dirname(resolve(tsconfig)),
167
+ '__pikku_virtual_types__.ts'
168
+ )
169
+ const program = createProgramWithVirtualFile(
170
+ tsconfig,
171
+ virtualFilePath,
172
+ customTypesContent
173
+ )
174
+
175
+ const generator = createGenerator({
176
+ tsProgram: program,
177
+ skipTypeCheck: true,
178
+ topRef: false,
179
+ discriminatorType: 'open-api',
180
+ expose: 'export',
181
+ jsDoc: 'extended',
182
+ sortProps: true,
183
+ strictTuples: false,
184
+ encodeRefs: false,
185
+ additionalProperties,
186
+ })
187
+ const schemas: Record<string, JSONValue> = {}
188
+
189
+ schemasSet.forEach((schema) => {
190
+ if (PRIMITIVE_TYPES.has(schema)) {
191
+ return
192
+ }
193
+ if (schemaLookup?.has(schema)) {
194
+ return
195
+ }
196
+ try {
197
+ schemas[schema] = generator.createSchema(schema) as JSONValue
198
+ } catch (e) {
199
+ if (e instanceof RootlessError) {
200
+ const customType = typesMap.customTypes.get(schema)
201
+ if (customType) {
202
+ const primitiveSchema = primitiveTypeToSchema(customType.type)
203
+ if (primitiveSchema) {
204
+ schemas[schema] = primitiveSchema
205
+ }
206
+ }
207
+ return
208
+ }
209
+ const customType = typesMap.customTypes.get(schema)
210
+ logger.error(
211
+ `[${ErrorCode.SCHEMA_GENERATION_ERROR}] Error generating schema: ${schema}. Message: ${(e as Error).message}. Type info: ${customType ? `type=${customType.type}` : 'not in typesMap'}`
212
+ )
213
+ }
214
+ })
215
+
216
+ return schemas
217
+ }
218
+
219
+ async function generateZodSchemas(
220
+ logger: InspectorLogger,
221
+ schemaLookup: Map<string, SchemaRef>,
222
+ typesMap: TypesMap
223
+ ): Promise<Record<string, JSONValue>> {
224
+ const schemas: Record<string, JSONValue> = {}
225
+ const auxiliaryTypeStore = createAuxiliaryTypeStore()
226
+ const printer = ts.createPrinter()
227
+ const fakeSourceFile = ts.createSourceFile(
228
+ 'zod-types.ts',
229
+ '',
230
+ ts.ScriptTarget.ESNext,
231
+ false,
232
+ ts.ScriptKind.TS
233
+ )
234
+
235
+ for (const [schemaName, ref] of schemaLookup.entries()) {
236
+ if (ref.vendor && ref.vendor !== 'zod') {
237
+ throw new Error(
238
+ `Schema '${schemaName}' uses ${ref.vendor} which is not yet supported for JSON Schema generation. ` +
239
+ `Currently only Zod schemas can be converted to JSON Schema. ` +
240
+ `Please use Zod or contribute support for ${ref.vendor}.`
241
+ )
242
+ }
243
+
244
+ try {
245
+ const module = await tsImport(ref.sourceFile, import.meta.url)
246
+ const zodSchema = module[ref.variableName]
247
+ if (!zodSchema) {
248
+ logger.warn(
249
+ `Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`
250
+ )
251
+ continue
252
+ }
253
+
254
+ const schema = z.toJSONSchema(zodSchema, {
255
+ unrepresentable: 'any',
256
+ override: ({ zodSchema, jsonSchema }) => {
257
+ if ((zodSchema as any)._zod?.def?.type === 'date') {
258
+ ;(jsonSchema as any).type = 'string'
259
+ ;(jsonSchema as any).format = 'date-time'
260
+ }
261
+ },
262
+ }) as any
263
+
264
+ if (schema.required && schema.properties) {
265
+ schema.required = schema.required.filter((fieldName: string) => {
266
+ const prop = schema.properties[fieldName]
267
+ return prop && prop.default === undefined
268
+ })
269
+ if (schema.required.length === 0) {
270
+ delete schema.required
271
+ }
272
+ }
273
+
274
+ schemas[schemaName] = schema
275
+ const { node: tsType } = zodToTs(zodSchema, { auxiliaryTypeStore })
276
+
277
+ const typeText = printer.printNode(
278
+ ts.EmitHint.Unspecified,
279
+ tsType,
280
+ fakeSourceFile
281
+ )
282
+
283
+ typesMap.addCustomType(schemaName, typeText, [])
284
+ logger.debug(`• Generated schema from Zod: ${schemaName}`)
285
+ } catch (e) {
286
+ logger.warn(
287
+ `Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`
288
+ )
289
+ }
290
+ }
291
+
292
+ return schemas
293
+ }
294
+
295
+ export async function generateAllSchemas(
296
+ logger: InspectorLogger,
297
+ config: {
298
+ tsconfig: string
299
+ schemasFromTypes?: string[]
300
+ schema?: { additionalProperties?: boolean }
301
+ },
302
+ state: InspectorState
303
+ ): Promise<Record<string, JSONValue>> {
304
+ const zodSchemas = await generateZodSchemas(
305
+ logger,
306
+ state.schemaLookup,
307
+ state.functions.typesMap
308
+ )
309
+
310
+ const requiredTypes = new Set<string>()
311
+ const customTypesContent = generateCustomTypes(
312
+ state.functions.typesMap,
313
+ requiredTypes
314
+ )
315
+
316
+ const tsSchemas = generateTSSchemas(
317
+ logger,
318
+ config.tsconfig,
319
+ customTypesContent,
320
+ state.functions.typesMap,
321
+ state.functions.meta,
322
+ state.http.meta,
323
+ config.schemasFromTypes,
324
+ config.schema?.additionalProperties,
325
+ state.schemaLookup
326
+ )
327
+
328
+ return { ...tsSchemas, ...zodSchemas }
329
+ }