@pikku/inspector 0.12.75 → 0.12.82

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 (223) hide show
  1. package/CHANGELOG.md +357 -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-middleware.js +66 -42
  8. package/dist/add/add-wire-addon.js +7 -4
  9. package/dist/add/add-wire-remote-addon.js +1 -0
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/inspector.js +5 -0
  15. package/dist/surface/collect-surface.d.ts +58 -0
  16. package/dist/surface/collect-surface.js +613 -0
  17. package/dist/types.d.ts +36 -12
  18. package/dist/utils/filter-inspector-state.js +14 -0
  19. package/dist/utils/load-addon-functions-meta.js +24 -11
  20. package/dist/utils/post-process.js +15 -0
  21. package/dist/utils/read-module-specifiers.d.ts +8 -0
  22. package/dist/utils/read-module-specifiers.js +23 -0
  23. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  24. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  25. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  26. package/dist/utils/resolve-deploy-target.js +15 -7
  27. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  28. package/dist/utils/serialize-inspector-state.js +10 -0
  29. package/dist/visit.js +4 -0
  30. package/package.json +16 -6
  31. package/run-tests.sh +21 -6
  32. package/src/add/add-addon-bans.ts +0 -85
  33. package/src/add/add-agent-scorer.test.ts +0 -197
  34. package/src/add/add-agent-scorer.ts +0 -83
  35. package/src/add/add-agent.test.ts +0 -87
  36. package/src/add/add-agent.ts +0 -638
  37. package/src/add/add-approval-description.ts +0 -76
  38. package/src/add/add-auth.test.ts +0 -791
  39. package/src/add/add-auth.ts +0 -424
  40. package/src/add/add-channel.ts +0 -778
  41. package/src/add/add-cli-renderers.test.ts +0 -74
  42. package/src/add/add-cli.ts +0 -1006
  43. package/src/add/add-credential.ts +0 -186
  44. package/src/add/add-feature.test.ts +0 -306
  45. package/src/add/add-feature.ts +0 -253
  46. package/src/add/add-file-extends-core-type.ts +0 -66
  47. package/src/add/add-file-with-config.ts +0 -45
  48. package/src/add/add-file-with-factory.test.ts +0 -134
  49. package/src/add/add-file-with-factory.ts +0 -288
  50. package/src/add/add-functions.test.ts +0 -743
  51. package/src/add/add-functions.ts +0 -1548
  52. package/src/add/add-gateway.ts +0 -143
  53. package/src/add/add-http-route-ref.test.ts +0 -149
  54. package/src/add/add-http-route.ts +0 -530
  55. package/src/add/add-http-routes.ts +0 -471
  56. package/src/add/add-keyed-wiring.ts +0 -180
  57. package/src/add/add-mcp-prompt.ts +0 -157
  58. package/src/add/add-mcp-resource.ts +0 -175
  59. package/src/add/add-middleware.test.ts +0 -196
  60. package/src/add/add-middleware.ts +0 -879
  61. package/src/add/add-permission-auth.test.ts +0 -59
  62. package/src/add/add-permission.ts +0 -311
  63. package/src/add/add-personas.test.ts +0 -406
  64. package/src/add/add-personas.ts +0 -287
  65. package/src/add/add-queue-worker.ts +0 -117
  66. package/src/add/add-rpc-invocations.test.ts +0 -287
  67. package/src/add/add-rpc-invocations.ts +0 -221
  68. package/src/add/add-scenario-step.test.ts +0 -509
  69. package/src/add/add-schedule.ts +0 -120
  70. package/src/add/add-scope.test.ts +0 -419
  71. package/src/add/add-scope.ts +0 -221
  72. package/src/add/add-secret.ts +0 -10
  73. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  74. package/src/add/add-system-role.test.ts +0 -297
  75. package/src/add/add-system-role.ts +0 -162
  76. package/src/add/add-trigger.ts +0 -165
  77. package/src/add/add-variable.ts +0 -10
  78. package/src/add/add-wire-addon.test.ts +0 -185
  79. package/src/add/add-wire-addon.ts +0 -146
  80. package/src/add/add-wire-remote-addon.ts +0 -80
  81. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  82. package/src/add/add-workflow-fanout.test.ts +0 -166
  83. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  84. package/src/add/add-workflow-graph-input.test.ts +0 -94
  85. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  86. package/src/add/add-workflow-graph.ts +0 -644
  87. package/src/add/add-workflow-on-error.test.ts +0 -118
  88. package/src/add/add-workflow-return-switch.test.ts +0 -121
  89. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  90. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  91. package/src/add/add-workflow.test.ts +0 -155
  92. package/src/add/add-workflow.ts +0 -482
  93. package/src/add/addon-bans.test.ts +0 -121
  94. package/src/add/addon-contracts.test.ts +0 -221
  95. package/src/add/allow-escape-hatches.test.ts +0 -144
  96. package/src/add/audit-meta.test.ts +0 -75
  97. package/src/add/classified-sink-check.test.ts +0 -347
  98. package/src/add/config-value-metadata.test.ts +0 -263
  99. package/src/add/dynamic-import-check.test.ts +0 -119
  100. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  101. package/src/add/function-body-span.test.ts +0 -96
  102. package/src/add/inline-wiring-function.test.ts +0 -104
  103. package/src/add/pii-check.test.ts +0 -258
  104. package/src/add/rpc-type-cast.test.ts +0 -123
  105. package/src/add/scenario-instrumentation.test.ts +0 -89
  106. package/src/add/scenario-instrumentation.ts +0 -42
  107. package/src/add/scenario-marker.test.ts +0 -89
  108. package/src/add/scenario-not-extractable.test.ts +0 -159
  109. package/src/add/scenario-services.test.ts +0 -103
  110. package/src/add/scenario-skip.test.ts +0 -83
  111. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  112. package/src/add/wire-name-literal.test.ts +0 -117
  113. package/src/error-codes-emitted.test.ts +0 -76
  114. package/src/error-codes.ts +0 -145
  115. package/src/index.ts +0 -66
  116. package/src/inspector.ts +0 -575
  117. package/src/schema-generator.ts +0 -1
  118. package/src/source-file-cache.ts +0 -73
  119. package/src/tsconfig-paths.test.ts +0 -90
  120. package/src/types-map.ts +0 -141
  121. package/src/types.ts +0 -775
  122. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  123. package/src/utils/annotate-http-route-auth.ts +0 -53
  124. package/src/utils/check-classified-sinks.ts +0 -215
  125. package/src/utils/check-pii-output.ts +0 -120
  126. package/src/utils/compute-diagnostics.test.ts +0 -106
  127. package/src/utils/compute-required-schemas.ts +0 -48
  128. package/src/utils/contract-hashes.test.ts +0 -553
  129. package/src/utils/contract-hashes.ts +0 -387
  130. package/src/utils/custom-types-generator.test.ts +0 -124
  131. package/src/utils/custom-types-generator.ts +0 -150
  132. package/src/utils/detect-schema-vendor.test.ts +0 -123
  133. package/src/utils/detect-schema-vendor.ts +0 -120
  134. package/src/utils/does-type-extend-core-type.ts +0 -53
  135. package/src/utils/ensure-function-metadata.ts +0 -385
  136. package/src/utils/extract-function-name.ts +0 -612
  137. package/src/utils/extract-function-node.ts +0 -70
  138. package/src/utils/extract-node-value.test.ts +0 -117
  139. package/src/utils/extract-node-value.ts +0 -172
  140. package/src/utils/extract-secret-usage.test.ts +0 -81
  141. package/src/utils/extract-secret-usage.ts +0 -73
  142. package/src/utils/extract-services.ts +0 -70
  143. package/src/utils/extract-surface-usage.test.ts +0 -147
  144. package/src/utils/extract-surface-usage.ts +0 -121
  145. package/src/utils/filter-inspector-state.test.ts +0 -2232
  146. package/src/utils/filter-inspector-state.ts +0 -1298
  147. package/src/utils/filter-utils.test.ts +0 -843
  148. package/src/utils/filter-utils.ts +0 -161
  149. package/src/utils/find-root-dir.ts +0 -68
  150. package/src/utils/find-zod-transform.test.ts +0 -70
  151. package/src/utils/find-zod-transform.ts +0 -103
  152. package/src/utils/get-exported-variable-name.ts +0 -48
  153. package/src/utils/get-files-and-methods.ts +0 -157
  154. package/src/utils/get-property-value.test.ts +0 -141
  155. package/src/utils/get-property-value.ts +0 -331
  156. package/src/utils/hash.ts +0 -26
  157. package/src/utils/literal-properties.ts +0 -97
  158. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  159. package/src/utils/load-addon-functions-meta.ts +0 -508
  160. package/src/utils/load-addon-scopes.test.ts +0 -138
  161. package/src/utils/middleware.test.ts +0 -204
  162. package/src/utils/middleware.ts +0 -303
  163. package/src/utils/nested-project-filter.test.ts +0 -53
  164. package/src/utils/nested-project-filter.ts +0 -28
  165. package/src/utils/permissions-shorthand.test.ts +0 -79
  166. package/src/utils/permissions.test.ts +0 -125
  167. package/src/utils/permissions.ts +0 -129
  168. package/src/utils/post-process.test.ts +0 -1094
  169. package/src/utils/post-process.ts +0 -1604
  170. package/src/utils/resolve-addon-package.ts +0 -106
  171. package/src/utils/resolve-core-type.test.ts +0 -142
  172. package/src/utils/resolve-core-type.ts +0 -35
  173. package/src/utils/resolve-deploy-target.test.ts +0 -135
  174. package/src/utils/resolve-deploy-target.ts +0 -65
  175. package/src/utils/resolve-function-meta.ts +0 -25
  176. package/src/utils/resolve-function-types.ts +0 -42
  177. package/src/utils/resolve-identifier.ts +0 -46
  178. package/src/utils/resolve-ref-contract.ts +0 -71
  179. package/src/utils/resolve-versions.test.ts +0 -499
  180. package/src/utils/resolve-versions.ts +0 -228
  181. package/src/utils/schema-cache.test.ts +0 -124
  182. package/src/utils/schema-generator.test.ts +0 -272
  183. package/src/utils/schema-generator.ts +0 -780
  184. package/src/utils/secret-alias-services.test.ts +0 -146
  185. package/src/utils/secret-alias-services.ts +0 -58
  186. package/src/utils/serialize-inspector-state.ts +0 -742
  187. package/src/utils/serialize-mcp-json.ts +0 -150
  188. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  189. package/src/utils/serialize-openapi-json.ts +0 -277
  190. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  191. package/src/utils/single-declaration.ts +0 -57
  192. package/src/utils/type-utils.ts +0 -145
  193. package/src/utils/validate-auth-sessionless.ts +0 -30
  194. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  195. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  196. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  197. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  198. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  199. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  200. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  201. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  202. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  203. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  204. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  205. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  206. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  207. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  208. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  209. package/src/utils/workflow/dsl/index.ts +0 -11
  210. package/src/utils/workflow/dsl/patterns.ts +0 -440
  211. package/src/utils/workflow/dsl/validation.ts +0 -203
  212. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  213. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  214. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  215. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  216. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  217. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  218. package/src/utils/workflow/graph/index.ts +0 -28
  219. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  220. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  221. package/src/visit.ts +0 -181
  222. package/tsconfig.json +0 -19
  223. 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
- }