@pikku/inspector 0.12.75 → 0.12.81

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 (219) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +14 -0
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2232
  142. package/src/utils/filter-inspector-state.ts +0 -1298
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1006 +0,0 @@
1
- import type { TypeChecker } from 'typescript'
2
- import ts from 'typescript'
3
- import type {
4
- AddWiring,
5
- InspectorLogger,
6
- InspectorOptions,
7
- InspectorState,
8
- } from '../types.js'
9
- import type { CLIProgramMeta, CLICommandMeta } from '@pikku/core/cli'
10
- import {
11
- extractFunctionName,
12
- makeContextBasedId,
13
- } from '../utils/extract-function-name.js'
14
- import { resolveCoreType } from '../utils/resolve-core-type.js'
15
- import { resolveMiddleware } from '../utils/middleware.js'
16
- import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
17
- import { extractWireNames } from '../utils/post-process.js'
18
- import { getPropertyValue } from '../utils/get-property-value.js'
19
- import { resolveIdentifier } from '../utils/resolve-identifier.js'
20
- import { resolveAddonName } from '../utils/resolve-addon-package.js'
21
- import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
22
- import { extractServicesFromFunction } from '../utils/extract-services.js'
23
- import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
24
- import { resolveRefContract } from '../utils/resolve-ref-contract.js'
25
-
26
- // Track if we've warned about missing Config type to avoid duplicate warnings
27
- const configTypeWarningShown = new Set<string>()
28
-
29
- /**
30
- * Adds CLI command metadata to the inspector state
31
- */
32
- export const addCLI: AddWiring = (
33
- logger,
34
- node,
35
- typeChecker,
36
- inspectorState,
37
- options
38
- ) => {
39
- if (!ts.isCallExpression(node)) return
40
- if (
41
- ts.isIdentifier(node.expression) &&
42
- node.expression.text === 'defineCLICommands'
43
- ) {
44
- const exportName = getExportedVariableName(node, options.sourceFile)
45
- const [firstArg] = node.arguments
46
- if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
47
- inspectorState.exportedContracts.cli[exportName] = processCommands(
48
- logger,
49
- firstArg,
50
- node.getSourceFile(),
51
- typeChecker,
52
- exportName,
53
- inspectorState,
54
- options
55
- )
56
- }
57
- return
58
- }
59
- // Check if this is a wireCLI call
60
- if (!node || !node.expression) {
61
- return
62
- }
63
- const expression = node.expression
64
- if (!ts.isIdentifier(expression) || expression.text !== 'wireCLI') {
65
- return
66
- }
67
-
68
- // Get the argument (should be an object literal)
69
- if (node.arguments.length !== 1) {
70
- return
71
- }
72
-
73
- const arg = node.arguments[0]
74
- if (!ts.isObjectLiteralExpression(arg)) {
75
- return
76
- }
77
-
78
- const sourceFile = node.getSourceFile()
79
-
80
- // Add to files set
81
- inspectorState.cli.files.add(sourceFile.fileName)
82
-
83
- // Process the CLI configuration
84
- const cliConfig = processCLIConfig(
85
- logger,
86
- arg,
87
- sourceFile,
88
- typeChecker,
89
- inspectorState,
90
- options
91
- )
92
-
93
- if (!cliConfig) {
94
- return
95
- }
96
-
97
- // Add this program to the CLI metadata
98
- inspectorState.cli.meta.programs[cliConfig.programName] =
99
- cliConfig.programMeta
100
- }
101
-
102
- /**
103
- * Processes a CLI configuration object
104
- */
105
- function processCLIConfig(
106
- logger: InspectorLogger,
107
- node: ts.ObjectLiteralExpression,
108
- sourceFile: ts.SourceFile,
109
- typeChecker: TypeChecker,
110
- inspectorState: InspectorState,
111
- options: InspectorOptions
112
- ): { programName: string; programMeta: CLIProgramMeta } | null {
113
- let programName = ''
114
- let programTags: string[] | undefined
115
- const programMeta: CLIProgramMeta = {
116
- program: '',
117
- commands: {},
118
- options: {},
119
- }
120
-
121
- // First pass: extract program name and tags
122
- for (const prop of node.properties) {
123
- if (!ts.isPropertyAssignment(prop)) continue
124
- if (!ts.isIdentifier(prop.name)) continue
125
-
126
- const propName = prop.name.text
127
-
128
- if (propName === 'program' && ts.isStringLiteral(prop.initializer)) {
129
- programName = prop.initializer.text
130
- programMeta.program = programName
131
- } else if (propName === 'tags') {
132
- programTags = (getPropertyValue(node, 'tags') as string[]) || undefined
133
- }
134
- }
135
-
136
- if (!programName) {
137
- return null
138
- }
139
-
140
- if (getPropertyValue(node, 'disabled') === true) {
141
- return null
142
- }
143
-
144
- // Second pass: process other properties with program tags available
145
- for (const prop of node.properties) {
146
- if (!ts.isPropertyAssignment(prop)) continue
147
- if (!ts.isIdentifier(prop.name)) continue
148
-
149
- const propName = prop.name.text
150
-
151
- switch (propName) {
152
- case 'program':
153
- case 'tags':
154
- // Already handled in first pass
155
- break
156
-
157
- case 'auth': {
158
- const value = getPropertyValue(node, 'auth')
159
- if (typeof value === 'boolean') {
160
- programMeta.auth = value
161
- }
162
- break
163
- }
164
-
165
- case 'commands':
166
- if (ts.isObjectLiteralExpression(prop.initializer)) {
167
- programMeta.commands = processCommands(
168
- logger,
169
- prop.initializer,
170
- sourceFile,
171
- typeChecker,
172
- programName,
173
- inspectorState,
174
- options,
175
- programTags
176
- )
177
- }
178
- break
179
-
180
- case 'options':
181
- if (ts.isObjectLiteralExpression(prop.initializer)) {
182
- programMeta.options = processOptions(
183
- logger,
184
- prop.initializer,
185
- typeChecker,
186
- inspectorState,
187
- options
188
- )
189
- }
190
- break
191
-
192
- case 'render': {
193
- let renderFuncId = extractFunctionName(
194
- prop.initializer,
195
- typeChecker,
196
- inspectorState.rootDir
197
- ).pikkuFuncId
198
- if (renderFuncId.startsWith('__temp_')) {
199
- renderFuncId = makeContextBasedId('cli-render', programName)
200
- }
201
- programMeta.defaultRenderName = renderFuncId
202
- break
203
- }
204
- }
205
- }
206
-
207
- return { programName, programMeta }
208
- }
209
-
210
- /**
211
- * Processes the commands object
212
- */
213
- function processCommands(
214
- logger: InspectorLogger,
215
- node: ts.ObjectLiteralExpression,
216
- sourceFile: ts.SourceFile,
217
- typeChecker: TypeChecker,
218
- programName: string,
219
- inspectorState: InspectorState,
220
- options: InspectorOptions,
221
- programTags?: string[]
222
- ): Record<string, CLICommandMeta> {
223
- const commands: Record<string, CLICommandMeta> = {}
224
- let defaultCommandName: string | null = null
225
-
226
- for (const prop of node.properties) {
227
- // Handle spread assignments: { ...externalCommands }
228
- if (ts.isSpreadAssignment(prop)) {
229
- let spreadTarget: ts.Node | undefined = prop.expression
230
- if (ts.isIdentifier(prop.expression)) {
231
- spreadTarget = resolveIdentifier(prop.expression, typeChecker, [
232
- 'defineCLICommands',
233
- ])
234
- }
235
- if (spreadTarget && ts.isObjectLiteralExpression(spreadTarget)) {
236
- const spreadCommands = processCommands(
237
- logger,
238
- spreadTarget,
239
- sourceFile,
240
- typeChecker,
241
- programName,
242
- inspectorState,
243
- options,
244
- programTags
245
- )
246
- Object.assign(commands, spreadCommands)
247
- } else {
248
- const refCommands = resolveRefContract(
249
- prop.expression,
250
- 'refCLI',
251
- inspectorState.exportedContracts.addonCli
252
- )
253
- if (refCommands) {
254
- Object.assign(commands, refCommands.contract)
255
- }
256
- }
257
- continue
258
- }
259
-
260
- if (!ts.isPropertyAssignment(prop)) continue
261
-
262
- const commandName = getPropertyName(prop)
263
- if (!commandName) continue
264
-
265
- const commandMeta = processCommand(
266
- logger,
267
- inspectorState,
268
- options,
269
- commandName,
270
- prop.initializer,
271
- sourceFile,
272
- typeChecker,
273
- programName,
274
- [],
275
- programTags
276
- )
277
-
278
- if (commandMeta) {
279
- commands[commandName] = commandMeta
280
-
281
- // Validate only one default command
282
- if (commandMeta.isDefault) {
283
- if (defaultCommandName !== null) {
284
- const position = prop.getStart(sourceFile)
285
- const { line, character } =
286
- sourceFile.getLineAndCharacterOfPosition(position)
287
-
288
- throw new Error(
289
- `Multiple default commands found in CLI program "${programName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
290
- `Commands "${defaultCommandName}" and "${commandName}" are both marked as default.\n` +
291
- `Only one command can be marked as default per program.`
292
- )
293
- }
294
- defaultCommandName = commandName
295
- }
296
- }
297
- }
298
-
299
- return commands
300
- }
301
-
302
- /**
303
- * Processes a single command
304
- */
305
- function processCommand(
306
- logger: InspectorLogger,
307
- inspectorState: InspectorState,
308
- options: InspectorOptions,
309
- name: string,
310
- node: ts.Expression,
311
- sourceFile: ts.SourceFile,
312
- typeChecker: TypeChecker,
313
- programName: string,
314
- parentPath: string[] = [],
315
- programTags?: string[]
316
- ): CLICommandMeta | null {
317
- const fullPath = [...parentPath, name]
318
-
319
- // Handle shorthand (just a function)
320
- if (
321
- ts.isIdentifier(node) ||
322
- ts.isArrowFunction(node) ||
323
- ts.isFunctionExpression(node)
324
- ) {
325
- let pikkuFuncId = extractFunctionName(
326
- node,
327
- typeChecker,
328
- inspectorState.rootDir
329
- ).pikkuFuncId
330
- if (pikkuFuncId.startsWith('__temp_')) {
331
- pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath)
332
- }
333
- return {
334
- pikkuFuncId,
335
- positionals: [],
336
- options: {},
337
- }
338
- }
339
-
340
- // Handle pikkuCLICommand calls
341
- if (ts.isCallExpression(node)) {
342
- // Check if it's a pikkuCLICommand call
343
- if (
344
- ts.isIdentifier(node.expression) &&
345
- node.expression.text === 'pikkuCLICommand' &&
346
- node.arguments.length > 0 &&
347
- ts.isObjectLiteralExpression(node.arguments[0])
348
- ) {
349
- // Process the object literal argument
350
- return processCommand(
351
- logger,
352
- inspectorState,
353
- options,
354
- name,
355
- node.arguments[0],
356
- sourceFile,
357
- typeChecker,
358
- programName,
359
- parentPath,
360
- programTags
361
- )
362
- }
363
- return null
364
- }
365
-
366
- // Handle full command object
367
- if (!ts.isObjectLiteralExpression(node)) {
368
- return null
369
- }
370
-
371
- const meta: CLICommandMeta = {
372
- pikkuFuncId: '',
373
- positionals: [],
374
- options: {},
375
- }
376
-
377
- // First pass: extract pikkuFuncId and tags so we can use them when processing options/middleware
378
- let pikkuFuncId: string | undefined
379
- let optionsNode: ts.ObjectLiteralExpression | undefined
380
- let tags: string[] | undefined
381
-
382
- for (const prop of node.properties) {
383
- if (!ts.isPropertyAssignment(prop)) continue
384
- if (!ts.isIdentifier(prop.name)) continue
385
-
386
- const propName = prop.name.text
387
-
388
- if (propName === 'func') {
389
- if (
390
- ts.isCallExpression(prop.initializer) &&
391
- ts.isIdentifier(prop.initializer.expression) &&
392
- prop.initializer.expression.text === 'ref'
393
- ) {
394
- const [firstArg] = prop.initializer.arguments
395
- if (!firstArg || !ts.isStringLiteral(firstArg)) {
396
- throw new Error(`ref() call requires a string literal argument`)
397
- }
398
- pikkuFuncId = firstArg.text
399
- const addonNamespace = pikkuFuncId.includes(':')
400
- ? pikkuFuncId.split(':')[0]
401
- : null
402
- if (addonNamespace) {
403
- if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
404
- throw new Error(
405
- `Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`
406
- )
407
- }
408
- }
409
- meta.pikkuFuncId = pikkuFuncId
410
- if (addonNamespace) {
411
- meta.packageName =
412
- inspectorState.rpc.wireAddonDeclarations.get(
413
- addonNamespace
414
- )!.package
415
- }
416
- } else {
417
- pikkuFuncId = extractFunctionName(
418
- prop.initializer,
419
- typeChecker,
420
- inspectorState.rootDir
421
- ).pikkuFuncId
422
- if (pikkuFuncId.startsWith('__temp_')) {
423
- pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath)
424
- }
425
- meta.pikkuFuncId = pikkuFuncId
426
- const cliPackageName = ts.isIdentifier(prop.initializer)
427
- ? resolveAddonName(
428
- prop.initializer,
429
- typeChecker,
430
- inspectorState.rpc.wireAddonDeclarations
431
- )
432
- : null
433
- if (cliPackageName) {
434
- meta.packageName = cliPackageName
435
- }
436
- }
437
- } else if (
438
- propName === 'options' &&
439
- ts.isObjectLiteralExpression(prop.initializer)
440
- ) {
441
- optionsNode = prop.initializer
442
- } else if (propName === 'tags') {
443
- tags = (getPropertyValue(node, 'tags') as string[]) || undefined
444
- }
445
- }
446
-
447
- // Merge program-level tags with command-level tags
448
- const allTags = [...(programTags || []), ...(tags || [])]
449
-
450
- // Resolve middleware
451
- const middleware = resolveMiddleware(
452
- inspectorState,
453
- node,
454
- allTags.length > 0 ? allTags : undefined,
455
- typeChecker
456
- )
457
- if (middleware) {
458
- meta.middleware = middleware
459
- }
460
-
461
- // Add merged tags to metadata
462
- if (allTags.length > 0) {
463
- meta.tags = allTags
464
- }
465
-
466
- // Second pass: process all properties
467
- for (const prop of node.properties) {
468
- if (!ts.isPropertyAssignment(prop)) continue
469
- if (!ts.isIdentifier(prop.name)) continue
470
-
471
- const propName = prop.name.text
472
-
473
- switch (propName) {
474
- case 'parameters':
475
- if (ts.isStringLiteral(prop.initializer)) {
476
- meta.parameters = prop.initializer.text
477
- meta.positionals = parseCommandPattern(prop.initializer.text)
478
- }
479
- break
480
-
481
- case 'description':
482
- if (ts.isStringLiteral(prop.initializer)) {
483
- meta.description = prop.initializer.text
484
- }
485
- break
486
-
487
- case 'func':
488
- // Already handled in first pass
489
- break
490
-
491
- case 'render': {
492
- let renderFuncId = extractFunctionName(
493
- prop.initializer,
494
- typeChecker,
495
- inspectorState.rootDir
496
- ).pikkuFuncId
497
- if (renderFuncId.startsWith('__temp_')) {
498
- renderFuncId = makeContextBasedId(
499
- 'cli-render',
500
- programName,
501
- ...fullPath
502
- )
503
- }
504
- meta.renderName = renderFuncId
505
- break
506
- }
507
-
508
- case 'options':
509
- // Process with pikkuFuncId from first pass
510
- if (optionsNode) {
511
- meta.options = processOptions(
512
- logger,
513
- optionsNode,
514
- typeChecker,
515
- inspectorState,
516
- options,
517
- pikkuFuncId
518
- )
519
- }
520
- break
521
-
522
- case 'subcommands': {
523
- let subcommandsNode: ts.Node | undefined = prop.initializer
524
- if (ts.isIdentifier(prop.initializer)) {
525
- subcommandsNode = resolveIdentifier(prop.initializer, typeChecker, [
526
- 'defineCLICommands',
527
- ])
528
- }
529
- if (subcommandsNode && ts.isObjectLiteralExpression(subcommandsNode)) {
530
- meta.subcommands = {}
531
- for (const subProp of subcommandsNode.properties) {
532
- if (!ts.isPropertyAssignment(subProp)) continue
533
-
534
- const subName = getPropertyName(subProp)
535
- if (!subName) continue
536
-
537
- const subCommand = processCommand(
538
- logger,
539
- inspectorState,
540
- options,
541
- subName,
542
- subProp.initializer,
543
- sourceFile,
544
- typeChecker,
545
- programName,
546
- fullPath,
547
- programTags
548
- )
549
-
550
- if (subCommand) {
551
- meta.subcommands[subName] = subCommand
552
- }
553
- }
554
- }
555
- break
556
- }
557
-
558
- case 'isDefault':
559
- if (
560
- prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
561
- prop.initializer.kind === ts.SyntaxKind.FalseKeyword
562
- ) {
563
- meta.isDefault = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
564
- }
565
- break
566
- }
567
- }
568
-
569
- // --- validate auth/sessionless ---
570
- if (
571
- meta.pikkuFuncId &&
572
- !validateAuthSessionless(
573
- logger,
574
- node,
575
- inspectorState,
576
- meta.pikkuFuncId,
577
- `CLI command '${name}'`
578
- )
579
- ) {
580
- return null
581
- }
582
-
583
- // --- track used functions/middleware for service aggregation ---
584
- inspectorState.serviceAggregation.usedFunctions.add(meta.pikkuFuncId)
585
- extractWireNames(meta.middleware).forEach((name) =>
586
- inspectorState.serviceAggregation.usedMiddleware.add(name)
587
- )
588
-
589
- return meta
590
- }
591
-
592
- /**
593
- * Processes CLI options and extracts enum values from function input types
594
- */
595
- function processOptions(
596
- logger: InspectorLogger,
597
- node: ts.ObjectLiteralExpression,
598
- typeChecker: TypeChecker,
599
- inspectorState: InspectorState,
600
- inspectorOptions: InspectorOptions,
601
- pikkuFuncId?: string
602
- ): Record<string, any> {
603
- const options: Record<string, any> = {}
604
-
605
- for (const prop of node.properties) {
606
- if (!ts.isPropertyAssignment(prop)) continue
607
-
608
- const optionName = getPropertyName(prop)
609
- if (!optionName) continue
610
-
611
- if (ts.isObjectLiteralExpression(prop.initializer)) {
612
- const option: any = {}
613
- let manualChoices: string[] | undefined
614
-
615
- for (const optProp of prop.initializer.properties) {
616
- if (!ts.isPropertyAssignment(optProp)) continue
617
- if (!ts.isIdentifier(optProp.name)) continue
618
-
619
- const optPropName = optProp.name.text
620
-
621
- switch (optPropName) {
622
- case 'description':
623
- if (ts.isStringLiteral(optProp.initializer)) {
624
- option.description = optProp.initializer.text
625
- }
626
- break
627
-
628
- case 'short':
629
- if (ts.isStringLiteral(optProp.initializer)) {
630
- option.short = optProp.initializer.text
631
- }
632
- break
633
-
634
- case 'type':
635
- if (ts.isStringLiteral(optProp.initializer)) {
636
- option.type = optProp.initializer.text
637
- }
638
- break
639
-
640
- case 'default':
641
- // Extract default value from expression
642
- if (ts.isStringLiteral(optProp.initializer)) {
643
- option.default = optProp.initializer.text
644
- } else if (ts.isNumericLiteral(optProp.initializer)) {
645
- option.default = parseFloat(optProp.initializer.text)
646
- } else if (optProp.initializer.kind === ts.SyntaxKind.TrueKeyword) {
647
- option.default = true
648
- } else if (
649
- optProp.initializer.kind === ts.SyntaxKind.FalseKeyword
650
- ) {
651
- option.default = false
652
- }
653
- break
654
-
655
- case 'choices':
656
- // Extract manually specified choices
657
- if (ts.isArrayLiteralExpression(optProp.initializer)) {
658
- manualChoices = []
659
- for (const element of optProp.initializer.elements) {
660
- if (ts.isStringLiteral(element)) {
661
- manualChoices.push(element.text)
662
- }
663
- }
664
- }
665
- break
666
- }
667
- }
668
-
669
- // Extract enum values from the function input type if available
670
- let derivedChoices: string[] | null = null
671
-
672
- if (pikkuFuncId) {
673
- // 1. Try TypeScript types first (most precise — handles unions, TS enums)
674
- const inputTypes = inspectorState.typesLookup.get(pikkuFuncId)
675
- if (inputTypes && inputTypes.length > 0) {
676
- derivedChoices = extractEnumFromPropertyType(
677
- inputTypes[0]!,
678
- optionName,
679
- typeChecker
680
- )
681
- }
682
-
683
- // 2. Fallback: try JSON schema (works for addon functions)
684
- if (!derivedChoices) {
685
- derivedChoices = extractEnumFromJsonSchema(
686
- inspectorState,
687
- pikkuFuncId,
688
- optionName
689
- )
690
- }
691
- }
692
-
693
- // 3. Last resort: try Config type
694
- if (!derivedChoices) {
695
- derivedChoices = extractEnumFromConfigType(
696
- logger,
697
- optionName,
698
- typeChecker,
699
- inspectorState,
700
- inspectorOptions
701
- )
702
- }
703
-
704
- // Validate and set choices
705
- if (manualChoices && derivedChoices) {
706
- // Both manual and derived choices exist - validate manual is subset of derived
707
- const invalidChoices = manualChoices.filter(
708
- (choice) => !derivedChoices!.includes(choice)
709
- )
710
-
711
- if (invalidChoices.length > 0) {
712
- const sourceFile = node.getSourceFile()
713
- const position = prop.getStart(sourceFile)
714
- const { line, character } =
715
- sourceFile.getLineAndCharacterOfPosition(position)
716
-
717
- throw new Error(
718
- `Invalid choices for option "${optionName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
719
- `The following choices are not valid according to the type: ${invalidChoices.join(', ')}.\n` +
720
- `Valid choices from type: ${derivedChoices.join(', ')}.`
721
- )
722
- }
723
-
724
- // Manual choices are valid - use them
725
- option.choices = manualChoices
726
- } else if (manualChoices) {
727
- // Only manual choices - use them
728
- option.choices = manualChoices
729
- } else if (derivedChoices) {
730
- // Only derived choices - use them
731
- option.choices = derivedChoices
732
- }
733
-
734
- options[optionName] = option
735
- }
736
- }
737
-
738
- return options
739
- }
740
-
741
- /**
742
- * Extracts enum values from a property of a type
743
- * Handles both union types ('a' | 'b') and TypeScript enums
744
- */
745
- function extractEnumFromPropertyType(
746
- type: ts.Type,
747
- propertyName: string,
748
- typeChecker: TypeChecker
749
- ): string[] | null {
750
- // Get the property from the type
751
- const property = type.getProperty(propertyName)
752
- if (!property) {
753
- return null
754
- }
755
-
756
- // Get the type of the property
757
- const propertyType = typeChecker.getTypeOfSymbolAtLocation(
758
- property,
759
- property.valueDeclaration!
760
- )
761
-
762
- const enumValues: string[] = []
763
-
764
- // Check if it's a union type (e.g., 'debug' | 'info' | 'warn')
765
- if (propertyType.isUnion()) {
766
- for (const unionType of propertyType.types) {
767
- // Check if it's a string literal type
768
- if (unionType.flags & ts.TypeFlags.StringLiteral) {
769
- const literalType = unionType as ts.StringLiteralType
770
- enumValues.push(literalType.value)
771
- }
772
- // Check if it's an enum member (could be string or number enum)
773
- else if (unionType.flags & ts.TypeFlags.EnumLiteral) {
774
- const enumLiteralType = unionType as ts.LiteralType
775
- // For string enums, use the value directly
776
- if (typeof enumLiteralType.value === 'string') {
777
- enumValues.push(enumLiteralType.value)
778
- }
779
- // For numeric enums, get the symbol name (e.g., "Debug", "Info")
780
- else {
781
- const symbol = (unionType as any).symbol
782
- if (symbol && symbol.name) {
783
- enumValues.push(symbol.name)
784
- }
785
- }
786
- }
787
- }
788
- }
789
- // Check if it's an enum type directly
790
- else if (propertyType.flags & ts.TypeFlags.Enum) {
791
- const symbol = propertyType.getSymbol()
792
- if (symbol && symbol.exports) {
793
- symbol.exports.forEach((member) => {
794
- const memberType = typeChecker.getTypeOfSymbolAtLocation(
795
- member,
796
- member.valueDeclaration!
797
- )
798
- if (memberType.flags & ts.TypeFlags.StringLiteral) {
799
- const literalType = memberType as ts.StringLiteralType
800
- enumValues.push(literalType.value)
801
- } else if (typeof (memberType as any).value === 'string') {
802
- enumValues.push((memberType as any).value)
803
- }
804
- })
805
- }
806
- }
807
- // Check if it's an enum literal type
808
- else if (propertyType.flags & ts.TypeFlags.EnumLiteral) {
809
- const enumLiteralType = propertyType as ts.LiteralType
810
- if (typeof enumLiteralType.value === 'string') {
811
- enumValues.push(enumLiteralType.value)
812
- }
813
- }
814
-
815
- return enumValues.length > 0 ? enumValues : null
816
- }
817
-
818
- /**
819
- * Extracts enum values from the Config type
820
- */
821
- function extractEnumFromConfigType(
822
- logger: InspectorLogger,
823
- propertyName: string,
824
- typeChecker: TypeChecker,
825
- inspectorState: InspectorState,
826
- _inspectorOptions: InspectorOptions
827
- ): string[] | null {
828
- // Look for Config type in typesLookup
829
- const configType = resolveCoreType(
830
- inspectorState.typesLookup,
831
- inspectorState.configTypeImportMap
832
- )
833
- if (!configType) {
834
- // Only warn once per CLI file to avoid spamming logs
835
- if (!configTypeWarningShown.has('missing-config-type')) {
836
- configTypeWarningShown.add('missing-config-type')
837
- logger.warn(
838
- `Could not find Config type in typesLookup. ` +
839
- `Make sure you have a Config interface extending CoreConfig in your codebase.`
840
- )
841
- }
842
- return null
843
- }
844
-
845
- // Extract enum from the property
846
- return extractEnumFromPropertyType(configType, propertyName, typeChecker)
847
- }
848
-
849
- /**
850
- * Extracts enum values from the function's JSON schema.
851
- * Works for addon functions whose schemas are generated from OpenAPI/Zod.
852
- */
853
- function extractEnumFromJsonSchema(
854
- inspectorState: InspectorState,
855
- pikkuFuncId: string,
856
- propertyName: string
857
- ): string[] | null {
858
- const fnMeta = resolveFunctionMeta(inspectorState, pikkuFuncId)
859
- if (!fnMeta?.inputSchemaName) return null
860
-
861
- const schema = inspectorState.schemas[fnMeta.inputSchemaName] as any
862
- if (!schema?.properties?.[propertyName]) return null
863
-
864
- const prop = schema.properties[propertyName]
865
-
866
- // Direct enum on property
867
- if (prop.enum && Array.isArray(prop.enum)) {
868
- const strings = prop.enum.filter((v: unknown) => typeof v === 'string')
869
- if (strings.length > 0) return strings
870
- }
871
-
872
- // Array with enum items (e.g. z.array(z.enum([...])))
873
- if (
874
- prop.type === 'array' &&
875
- prop.items?.enum &&
876
- Array.isArray(prop.items.enum)
877
- ) {
878
- const strings = prop.items.enum.filter(
879
- (v: unknown) => typeof v === 'string'
880
- )
881
- if (strings.length > 0) return strings
882
- }
883
-
884
- return null
885
- }
886
-
887
- /**
888
- * Gets the property name from a property assignment
889
- */
890
- function getPropertyName(prop: ts.PropertyAssignment): string | null {
891
- if (ts.isIdentifier(prop.name)) {
892
- return prop.name.text
893
- }
894
- if (ts.isStringLiteral(prop.name)) {
895
- return prop.name.text
896
- }
897
- return null
898
- }
899
-
900
- /**
901
- * Parses a parameters string to extract positional arguments
902
- * Parameters format: "<env> [region] [files...]"
903
- */
904
- function parseCommandPattern(pattern: string): any[] {
905
- const positionals: any[] = []
906
-
907
- // Split by spaces to get all parameter definitions
908
- const parts = pattern.split(' ').filter((p) => p.trim())
909
-
910
- for (const part of parts) {
911
- if (part.startsWith('<') && part.endsWith('>')) {
912
- // Required positional
913
- const name = part.slice(1, -1)
914
- if (name.endsWith('...')) {
915
- positionals.push({
916
- name: name.slice(0, -3),
917
- required: true,
918
- variadic: true,
919
- })
920
- } else {
921
- positionals.push({
922
- name,
923
- required: true,
924
- })
925
- }
926
- } else if (part.startsWith('[') && part.endsWith(']')) {
927
- // Optional positional
928
- const name = part.slice(1, -1)
929
- if (name.endsWith('...')) {
930
- positionals.push({
931
- name: name.slice(0, -3),
932
- required: false,
933
- variadic: true,
934
- })
935
- } else {
936
- positionals.push({
937
- name,
938
- required: false,
939
- })
940
- }
941
- } else if (part.trim()) {
942
- // Found a literal word in the parameters pattern
943
- throw new Error(
944
- `Invalid parameters pattern '${pattern}': found literal word '${part}'. ` +
945
- `Parameters should only contain <required> or [optional] arguments. ` +
946
- `Example: "<env> [region]" or "<files...>"`
947
- )
948
- }
949
- }
950
-
951
- return positionals
952
- }
953
-
954
- /**
955
- * Adds CLI renderer metadata to the inspector state
956
- */
957
- export const addCLIRenderers: AddWiring = (
958
- logger,
959
- node,
960
- typeChecker,
961
- inspectorState,
962
- options
963
- ) => {
964
- if (!ts.isCallExpression(node)) return
965
-
966
- const { expression, arguments: args } = node
967
-
968
- // Only handle pikkuCLIRender calls
969
- if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
970
- return
971
- }
972
-
973
- if (args.length === 0) return
974
-
975
- // Extract renderer name
976
- const { pikkuFuncId, exportedName } = extractFunctionName(
977
- node,
978
- typeChecker,
979
- inspectorState.rootDir
980
- )
981
-
982
- // Get the source file path
983
- const sourceFile = node.getSourceFile()
984
- const filePath = sourceFile.fileName
985
-
986
- // Renderer service usage is defined by the callback's first parameter shape,
987
- // the same way function/auth service usage is tracked elsewhere in the inspector.
988
- const renderFunc = args[0]
989
- const services =
990
- ts.isArrowFunction(renderFunc) || ts.isFunctionExpression(renderFunc)
991
- ? extractServicesFromFunction(renderFunc)
992
- : { optimized: true, services: [] }
993
-
994
- // Store renderer metadata
995
- inspectorState.cli.meta.renderers[pikkuFuncId] = {
996
- name: pikkuFuncId,
997
- exportedName: exportedName ?? undefined,
998
- services,
999
- filePath,
1000
- }
1001
-
1002
- // Add to files map if exported
1003
- if (exportedName) {
1004
- inspectorState.cli.files.add(filePath)
1005
- }
1006
- }