@pikku/inspector 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/add/add-channel.d.ts +17 -0
- package/dist/{add-channel.js → add/add-channel.js} +60 -34
- package/dist/add/add-cli.d.ts +9 -0
- package/dist/add/add-cli.js +566 -0
- package/dist/{add-file-extends-core-type.d.ts → add/add-file-extends-core-type.d.ts} +2 -2
- package/dist/{add-file-extends-core-type.js → add/add-file-extends-core-type.js} +17 -4
- package/dist/{add-file-with-config.d.ts → add/add-file-with-config.d.ts} +1 -1
- package/dist/{add-file-with-config.js → add/add-file-with-config.js} +1 -1
- package/dist/{add-file-with-factory.d.ts → add/add-file-with-factory.d.ts} +2 -2
- package/dist/{add-file-with-factory.js → add/add-file-with-factory.js} +38 -5
- package/dist/add/add-functions.d.ts +6 -0
- package/dist/{add-functions.js → add/add-functions.js} +77 -10
- package/dist/{add-http-route.d.ts → add/add-http-route.d.ts} +2 -3
- package/dist/{add-http-route.js → add/add-http-route.js} +26 -13
- package/dist/add/add-mcp-prompt.d.ts +2 -0
- package/dist/add/add-mcp-prompt.js +74 -0
- package/dist/add/add-mcp-resource.d.ts +2 -0
- package/dist/add/add-mcp-resource.js +84 -0
- package/dist/add/add-mcp-tool.d.ts +2 -0
- package/dist/add/add-mcp-tool.js +80 -0
- package/dist/add/add-middleware.d.ts +5 -0
- package/dist/add/add-middleware.js +290 -0
- package/dist/add/add-permission.d.ts +5 -0
- package/dist/add/add-permission.js +292 -0
- package/dist/add/add-queue-worker.d.ts +2 -0
- package/dist/add/add-queue-worker.js +52 -0
- package/dist/{add-rpc-invocations.d.ts → add/add-rpc-invocations.d.ts} +1 -1
- package/dist/add/add-schedule.d.ts +2 -0
- package/dist/{add-schedule.js → add/add-schedule.js} +16 -11
- package/dist/error-codes.d.ts +35 -0
- package/dist/error-codes.js +40 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/inspector.d.ts +2 -3
- package/dist/inspector.js +38 -8
- package/dist/types.d.ts +108 -1
- package/dist/utils/ensure-function-metadata.d.ts +6 -0
- package/dist/utils/ensure-function-metadata.js +18 -0
- package/dist/utils/extract-function-name.d.ts +31 -0
- package/dist/{utils.js → utils/extract-function-name.js} +35 -149
- package/dist/utils/extract-services.d.ts +6 -0
- package/dist/utils/extract-services.js +29 -0
- package/dist/utils/filter-inspector-state.d.ts +6 -0
- package/dist/utils/filter-inspector-state.js +382 -0
- package/dist/utils/filter-utils.d.ts +19 -0
- package/dist/utils/filter-utils.js +109 -0
- package/dist/utils/find-root-dir.d.ts +23 -0
- package/dist/utils/find-root-dir.js +55 -0
- package/dist/utils/get-files-and-methods.d.ts +22 -0
- package/dist/utils/get-files-and-methods.js +61 -0
- package/dist/utils/get-property-value.d.ts +12 -0
- package/dist/{get-property-value.js → utils/get-property-value.js} +20 -0
- package/dist/utils/middleware.d.ts +39 -0
- package/dist/utils/middleware.js +157 -0
- package/dist/utils/permissions.d.ts +43 -0
- package/dist/utils/permissions.js +178 -0
- package/dist/utils/post-process.d.ts +16 -0
- package/dist/utils/post-process.js +132 -0
- package/dist/utils/serialize-inspector-state.d.ts +179 -0
- package/dist/utils/serialize-inspector-state.js +170 -0
- package/dist/utils/type-utils.d.ts +3 -0
- package/dist/utils/type-utils.js +50 -0
- package/dist/visit.d.ts +3 -3
- package/dist/visit.js +35 -31
- package/package.json +5 -6
- package/src/{add-channel.ts → add/add-channel.ts} +108 -56
- package/src/add/add-cli.ts +822 -0
- package/src/{add-file-extends-core-type.ts → add/add-file-extends-core-type.ts} +23 -5
- package/src/{add-file-with-config.ts → add/add-file-with-config.ts} +2 -2
- package/src/{add-file-with-factory.ts → add/add-file-with-factory.ts} +49 -6
- package/src/{add-functions.ts → add/add-functions.ts} +89 -19
- package/src/{add-http-route.ts → add/add-http-route.ts} +66 -32
- package/src/add/add-mcp-prompt.ts +128 -0
- package/src/add/add-mcp-prompt.ts.tmp +0 -0
- package/src/add/add-mcp-resource.ts +145 -0
- package/src/add/add-mcp-resource.ts.tmp +0 -0
- package/src/add/add-mcp-tool.ts +137 -0
- package/src/add/add-middleware.ts +385 -0
- package/src/add/add-permission.ts +391 -0
- package/src/add/add-queue-worker.ts +92 -0
- package/src/{add-rpc-invocations.ts → add/add-rpc-invocations.ts} +1 -1
- package/src/{add-schedule.ts → add/add-schedule.ts} +30 -28
- package/src/error-codes.ts +43 -0
- package/src/index.ts +12 -0
- package/src/inspector.ts +41 -17
- package/src/types.ts +128 -1
- package/src/utils/ensure-function-metadata.ts +24 -0
- package/src/{utils.ts → utils/extract-function-name.ts} +44 -206
- package/src/utils/extract-services.ts +35 -0
- package/src/utils/filter-inspector-state.test.ts +1433 -0
- package/src/utils/filter-inspector-state.ts +526 -0
- package/src/{utils.test.ts → utils/filter-utils.test.ts} +351 -2
- package/src/utils/filter-utils.ts +152 -0
- package/src/utils/find-root-dir.ts +68 -0
- package/src/utils/get-files-and-methods.ts +151 -0
- package/src/{get-property-value.ts → utils/get-property-value.ts} +27 -0
- package/src/utils/middleware.ts +241 -0
- package/src/utils/permissions.test.ts +327 -0
- package/src/utils/permissions.ts +262 -0
- package/src/utils/post-process.ts +178 -0
- package/src/utils/serialize-inspector-state.ts +375 -0
- package/src/utils/test-data/inspector-state.json +1680 -0
- package/src/utils/type-utils.ts +74 -0
- package/src/visit.ts +50 -34
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add-channel.d.ts +0 -13
- package/dist/add-functions.d.ts +0 -7
- package/dist/add-mcp-prompt.d.ts +0 -3
- package/dist/add-mcp-prompt.js +0 -61
- package/dist/add-mcp-resource.d.ts +0 -3
- package/dist/add-mcp-resource.js +0 -68
- package/dist/add-mcp-tool.d.ts +0 -3
- package/dist/add-mcp-tool.js +0 -64
- package/dist/add-middleware.d.ts +0 -7
- package/dist/add-middleware.js +0 -35
- package/dist/add-permission.d.ts +0 -7
- package/dist/add-permission.js +0 -35
- package/dist/add-queue-worker.d.ts +0 -3
- package/dist/add-queue-worker.js +0 -48
- package/dist/add-schedule.d.ts +0 -3
- package/dist/get-property-value.d.ts +0 -3
- package/dist/utils.d.ts +0 -39
- package/src/add-mcp-prompt.ts +0 -104
- package/src/add-mcp-resource.ts +0 -116
- package/src/add-mcp-tool.ts +0 -107
- package/src/add-middleware.ts +0 -51
- package/src/add-permission.ts +0 -53
- package/src/add-queue-worker.ts +0 -92
- /package/dist/{add-rpc-invocations.js → add/add-rpc-invocations.js} +0 -0
- /package/dist/{does-type-extend-core-type.d.ts → utils/does-type-extend-core-type.d.ts} +0 -0
- /package/dist/{does-type-extend-core-type.js → utils/does-type-extend-core-type.js} +0 -0
- /package/src/{does-type-extend-core-type.ts → utils/does-type-extend-core-type.ts} +0 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import ts, { TypeChecker } from 'typescript'
|
|
2
|
+
import {
|
|
3
|
+
AddWiring,
|
|
4
|
+
InspectorLogger,
|
|
5
|
+
InspectorOptions,
|
|
6
|
+
InspectorState,
|
|
7
|
+
} from '../types.js'
|
|
8
|
+
import { CLIProgramMeta, CLICommandMeta } from '@pikku/core/cli'
|
|
9
|
+
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
10
|
+
import { resolveMiddleware } from '../utils/middleware.js'
|
|
11
|
+
import { extractWireNames } from '../utils/post-process.js'
|
|
12
|
+
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
13
|
+
|
|
14
|
+
// Track if we've warned about missing Config type to avoid duplicate warnings
|
|
15
|
+
const configTypeWarningShown = new Set<string>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adds CLI command metadata to the inspector state
|
|
19
|
+
*/
|
|
20
|
+
export const addCLI: AddWiring = (
|
|
21
|
+
logger,
|
|
22
|
+
node,
|
|
23
|
+
typeChecker,
|
|
24
|
+
inspectorState,
|
|
25
|
+
options
|
|
26
|
+
) => {
|
|
27
|
+
if (!ts.isCallExpression(node)) return
|
|
28
|
+
// Check if this is a wireCLI call
|
|
29
|
+
if (!node || !node.expression) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const expression = node.expression
|
|
33
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'wireCLI') {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get the argument (should be an object literal)
|
|
38
|
+
if (node.arguments.length !== 1) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const arg = node.arguments[0]
|
|
43
|
+
if (!ts.isObjectLiteralExpression(arg)) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sourceFile = node.getSourceFile()
|
|
48
|
+
|
|
49
|
+
// Add to files set
|
|
50
|
+
inspectorState.cli.files.add(sourceFile.fileName)
|
|
51
|
+
|
|
52
|
+
// Process the CLI configuration
|
|
53
|
+
const cliConfig = processCLIConfig(
|
|
54
|
+
logger,
|
|
55
|
+
arg,
|
|
56
|
+
sourceFile,
|
|
57
|
+
typeChecker,
|
|
58
|
+
inspectorState,
|
|
59
|
+
options
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (!cliConfig) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Add this program to the CLI metadata
|
|
67
|
+
inspectorState.cli.meta.programs[cliConfig.programName] =
|
|
68
|
+
cliConfig.programMeta
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Processes a CLI configuration object
|
|
73
|
+
*/
|
|
74
|
+
function processCLIConfig(
|
|
75
|
+
logger: InspectorLogger,
|
|
76
|
+
node: ts.ObjectLiteralExpression,
|
|
77
|
+
sourceFile: ts.SourceFile,
|
|
78
|
+
typeChecker: TypeChecker,
|
|
79
|
+
inspectorState: InspectorState,
|
|
80
|
+
options: InspectorOptions
|
|
81
|
+
): { programName: string; programMeta: CLIProgramMeta } | null {
|
|
82
|
+
let programName = ''
|
|
83
|
+
let programTags: string[] | undefined
|
|
84
|
+
const programMeta: CLIProgramMeta = {
|
|
85
|
+
program: '',
|
|
86
|
+
commands: {},
|
|
87
|
+
options: {},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// First pass: extract program name and tags
|
|
91
|
+
for (const prop of node.properties) {
|
|
92
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
93
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
94
|
+
|
|
95
|
+
const propName = prop.name.text
|
|
96
|
+
|
|
97
|
+
if (propName === 'program' && ts.isStringLiteral(prop.initializer)) {
|
|
98
|
+
programName = prop.initializer.text
|
|
99
|
+
programMeta.program = programName
|
|
100
|
+
} else if (propName === 'tags') {
|
|
101
|
+
programTags = (getPropertyValue(node, 'tags') as string[]) || undefined
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!programName) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Second pass: process other properties with program tags available
|
|
110
|
+
for (const prop of node.properties) {
|
|
111
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
112
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
113
|
+
|
|
114
|
+
const propName = prop.name.text
|
|
115
|
+
|
|
116
|
+
switch (propName) {
|
|
117
|
+
case 'program':
|
|
118
|
+
case 'tags':
|
|
119
|
+
// Already handled in first pass
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
case 'commands':
|
|
123
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
124
|
+
programMeta.commands = processCommands(
|
|
125
|
+
logger,
|
|
126
|
+
prop.initializer,
|
|
127
|
+
sourceFile,
|
|
128
|
+
typeChecker,
|
|
129
|
+
programName,
|
|
130
|
+
inspectorState,
|
|
131
|
+
options,
|
|
132
|
+
programTags
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
case 'options':
|
|
138
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
139
|
+
programMeta.options = processOptions(
|
|
140
|
+
logger,
|
|
141
|
+
prop.initializer,
|
|
142
|
+
typeChecker,
|
|
143
|
+
inspectorState,
|
|
144
|
+
options
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
case 'render':
|
|
150
|
+
// Extract the actual renderer function name
|
|
151
|
+
programMeta.defaultRenderName = extractFunctionName(
|
|
152
|
+
prop.initializer,
|
|
153
|
+
typeChecker,
|
|
154
|
+
inspectorState.rootDir
|
|
155
|
+
).pikkuFuncName
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { programName, programMeta }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Processes the commands object
|
|
165
|
+
*/
|
|
166
|
+
function processCommands(
|
|
167
|
+
logger: InspectorLogger,
|
|
168
|
+
node: ts.ObjectLiteralExpression,
|
|
169
|
+
sourceFile: ts.SourceFile,
|
|
170
|
+
typeChecker: TypeChecker,
|
|
171
|
+
programName: string,
|
|
172
|
+
inspectorState: InspectorState,
|
|
173
|
+
options: InspectorOptions,
|
|
174
|
+
programTags?: string[]
|
|
175
|
+
): Record<string, CLICommandMeta> {
|
|
176
|
+
const commands: Record<string, CLICommandMeta> = {}
|
|
177
|
+
let defaultCommandName: string | null = null
|
|
178
|
+
|
|
179
|
+
for (const prop of node.properties) {
|
|
180
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
181
|
+
|
|
182
|
+
const commandName = getPropertyName(prop)
|
|
183
|
+
if (!commandName) continue
|
|
184
|
+
|
|
185
|
+
const commandMeta = processCommand(
|
|
186
|
+
logger,
|
|
187
|
+
inspectorState,
|
|
188
|
+
options,
|
|
189
|
+
commandName,
|
|
190
|
+
prop.initializer,
|
|
191
|
+
sourceFile,
|
|
192
|
+
typeChecker,
|
|
193
|
+
programName,
|
|
194
|
+
[],
|
|
195
|
+
programTags
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if (commandMeta) {
|
|
199
|
+
commands[commandName] = commandMeta
|
|
200
|
+
|
|
201
|
+
// Validate only one default command
|
|
202
|
+
if (commandMeta.isDefault) {
|
|
203
|
+
if (defaultCommandName !== null) {
|
|
204
|
+
const position = prop.getStart(sourceFile)
|
|
205
|
+
const { line, character } =
|
|
206
|
+
sourceFile.getLineAndCharacterOfPosition(position)
|
|
207
|
+
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Multiple default commands found in CLI program "${programName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
|
|
210
|
+
`Commands "${defaultCommandName}" and "${commandName}" are both marked as default.\n` +
|
|
211
|
+
`Only one command can be marked as default per program.`
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
defaultCommandName = commandName
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return commands
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Processes a single command
|
|
224
|
+
*/
|
|
225
|
+
function processCommand(
|
|
226
|
+
logger: InspectorLogger,
|
|
227
|
+
inspectorState: InspectorState,
|
|
228
|
+
options: InspectorOptions,
|
|
229
|
+
name: string,
|
|
230
|
+
node: ts.Expression,
|
|
231
|
+
sourceFile: ts.SourceFile,
|
|
232
|
+
typeChecker: TypeChecker,
|
|
233
|
+
programName: string,
|
|
234
|
+
parentPath: string[] = [],
|
|
235
|
+
programTags?: string[]
|
|
236
|
+
): CLICommandMeta | null {
|
|
237
|
+
const fullPath = [...parentPath, name]
|
|
238
|
+
|
|
239
|
+
// Handle shorthand (just a function)
|
|
240
|
+
if (
|
|
241
|
+
ts.isIdentifier(node) ||
|
|
242
|
+
ts.isArrowFunction(node) ||
|
|
243
|
+
ts.isFunctionExpression(node)
|
|
244
|
+
) {
|
|
245
|
+
return {
|
|
246
|
+
pikkuFuncName: extractFunctionName(
|
|
247
|
+
node,
|
|
248
|
+
typeChecker,
|
|
249
|
+
inspectorState.rootDir
|
|
250
|
+
).pikkuFuncName,
|
|
251
|
+
positionals: [],
|
|
252
|
+
options: {},
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Handle pikkuCLICommand calls
|
|
257
|
+
if (ts.isCallExpression(node)) {
|
|
258
|
+
// Check if it's a pikkuCLICommand call
|
|
259
|
+
if (
|
|
260
|
+
ts.isIdentifier(node.expression) &&
|
|
261
|
+
node.expression.text === 'pikkuCLICommand' &&
|
|
262
|
+
node.arguments.length > 0 &&
|
|
263
|
+
ts.isObjectLiteralExpression(node.arguments[0])
|
|
264
|
+
) {
|
|
265
|
+
// Process the object literal argument
|
|
266
|
+
return processCommand(
|
|
267
|
+
logger,
|
|
268
|
+
inspectorState,
|
|
269
|
+
options,
|
|
270
|
+
name,
|
|
271
|
+
node.arguments[0],
|
|
272
|
+
sourceFile,
|
|
273
|
+
typeChecker,
|
|
274
|
+
programName,
|
|
275
|
+
parentPath,
|
|
276
|
+
programTags
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle full command object
|
|
283
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const meta: CLICommandMeta = {
|
|
288
|
+
pikkuFuncName: '',
|
|
289
|
+
positionals: [],
|
|
290
|
+
options: {},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// First pass: extract pikkuFuncName and tags so we can use them when processing options/middleware
|
|
294
|
+
let pikkuFuncName: string | undefined
|
|
295
|
+
let optionsNode: ts.ObjectLiteralExpression | undefined
|
|
296
|
+
let tags: string[] | undefined
|
|
297
|
+
|
|
298
|
+
for (const prop of node.properties) {
|
|
299
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
300
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
301
|
+
|
|
302
|
+
const propName = prop.name.text
|
|
303
|
+
|
|
304
|
+
if (propName === 'func') {
|
|
305
|
+
pikkuFuncName = extractFunctionName(
|
|
306
|
+
prop.initializer,
|
|
307
|
+
typeChecker,
|
|
308
|
+
inspectorState.rootDir
|
|
309
|
+
).pikkuFuncName
|
|
310
|
+
meta.pikkuFuncName = pikkuFuncName
|
|
311
|
+
} else if (
|
|
312
|
+
propName === 'options' &&
|
|
313
|
+
ts.isObjectLiteralExpression(prop.initializer)
|
|
314
|
+
) {
|
|
315
|
+
optionsNode = prop.initializer
|
|
316
|
+
} else if (propName === 'tags') {
|
|
317
|
+
tags = (getPropertyValue(node, 'tags') as string[]) || undefined
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Merge program-level tags with command-level tags
|
|
322
|
+
const allTags = [...(programTags || []), ...(tags || [])]
|
|
323
|
+
|
|
324
|
+
// Resolve middleware
|
|
325
|
+
const middleware = resolveMiddleware(
|
|
326
|
+
inspectorState,
|
|
327
|
+
node,
|
|
328
|
+
allTags.length > 0 ? allTags : undefined,
|
|
329
|
+
typeChecker
|
|
330
|
+
)
|
|
331
|
+
if (middleware) {
|
|
332
|
+
meta.middleware = middleware
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Add merged tags to metadata
|
|
336
|
+
if (allTags.length > 0) {
|
|
337
|
+
meta.tags = allTags
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Second pass: process all properties
|
|
341
|
+
for (const prop of node.properties) {
|
|
342
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
343
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
344
|
+
|
|
345
|
+
const propName = prop.name.text
|
|
346
|
+
|
|
347
|
+
switch (propName) {
|
|
348
|
+
case 'parameters':
|
|
349
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
350
|
+
meta.parameters = prop.initializer.text
|
|
351
|
+
meta.positionals = parseCommandPattern(prop.initializer.text)
|
|
352
|
+
}
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
case 'description':
|
|
356
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
357
|
+
meta.description = prop.initializer.text
|
|
358
|
+
}
|
|
359
|
+
break
|
|
360
|
+
|
|
361
|
+
case 'func':
|
|
362
|
+
// Already handled in first pass
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
case 'render':
|
|
366
|
+
meta.renderName = extractFunctionName(
|
|
367
|
+
prop.initializer,
|
|
368
|
+
typeChecker,
|
|
369
|
+
inspectorState.rootDir
|
|
370
|
+
).pikkuFuncName
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
case 'options':
|
|
374
|
+
// Process with pikkuFuncName from first pass
|
|
375
|
+
if (optionsNode) {
|
|
376
|
+
meta.options = processOptions(
|
|
377
|
+
logger,
|
|
378
|
+
optionsNode,
|
|
379
|
+
typeChecker,
|
|
380
|
+
inspectorState,
|
|
381
|
+
options,
|
|
382
|
+
pikkuFuncName
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
case 'subcommands':
|
|
388
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
389
|
+
meta.subcommands = {}
|
|
390
|
+
for (const subProp of prop.initializer.properties) {
|
|
391
|
+
if (!ts.isPropertyAssignment(subProp)) continue
|
|
392
|
+
|
|
393
|
+
const subName = getPropertyName(subProp)
|
|
394
|
+
if (!subName) continue
|
|
395
|
+
|
|
396
|
+
const subCommand = processCommand(
|
|
397
|
+
logger,
|
|
398
|
+
inspectorState,
|
|
399
|
+
options,
|
|
400
|
+
subName,
|
|
401
|
+
subProp.initializer,
|
|
402
|
+
sourceFile,
|
|
403
|
+
typeChecker,
|
|
404
|
+
programName,
|
|
405
|
+
fullPath,
|
|
406
|
+
programTags
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if (subCommand) {
|
|
410
|
+
meta.subcommands[subName] = subCommand
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
break
|
|
415
|
+
|
|
416
|
+
case 'isDefault':
|
|
417
|
+
if (
|
|
418
|
+
prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
419
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
420
|
+
) {
|
|
421
|
+
meta.isDefault = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
422
|
+
}
|
|
423
|
+
break
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- track used functions/middleware for service aggregation ---
|
|
428
|
+
inspectorState.serviceAggregation.usedFunctions.add(meta.pikkuFuncName)
|
|
429
|
+
extractWireNames(meta.middleware).forEach((name) =>
|
|
430
|
+
inspectorState.serviceAggregation.usedMiddleware.add(name)
|
|
431
|
+
)
|
|
432
|
+
// Note: subcommands are tracked recursively when they're processed
|
|
433
|
+
|
|
434
|
+
return meta
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Processes CLI options and extracts enum values from function input types
|
|
439
|
+
*/
|
|
440
|
+
function processOptions(
|
|
441
|
+
logger: InspectorLogger,
|
|
442
|
+
node: ts.ObjectLiteralExpression,
|
|
443
|
+
typeChecker: TypeChecker,
|
|
444
|
+
inspectorState: InspectorState,
|
|
445
|
+
inspectorOptions: InspectorOptions,
|
|
446
|
+
pikkuFuncName?: string
|
|
447
|
+
): Record<string, any> {
|
|
448
|
+
const options: Record<string, any> = {}
|
|
449
|
+
|
|
450
|
+
for (const prop of node.properties) {
|
|
451
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
452
|
+
|
|
453
|
+
const optionName = getPropertyName(prop)
|
|
454
|
+
if (!optionName) continue
|
|
455
|
+
|
|
456
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
457
|
+
const option: any = {}
|
|
458
|
+
let manualChoices: string[] | undefined
|
|
459
|
+
|
|
460
|
+
for (const optProp of prop.initializer.properties) {
|
|
461
|
+
if (!ts.isPropertyAssignment(optProp)) continue
|
|
462
|
+
if (!ts.isIdentifier(optProp.name)) continue
|
|
463
|
+
|
|
464
|
+
const optPropName = optProp.name.text
|
|
465
|
+
|
|
466
|
+
switch (optPropName) {
|
|
467
|
+
case 'description':
|
|
468
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
469
|
+
option.description = optProp.initializer.text
|
|
470
|
+
}
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
case 'short':
|
|
474
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
475
|
+
option.short = optProp.initializer.text
|
|
476
|
+
}
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
case 'default':
|
|
480
|
+
// Extract default value from expression
|
|
481
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
482
|
+
option.default = optProp.initializer.text
|
|
483
|
+
} else if (ts.isNumericLiteral(optProp.initializer)) {
|
|
484
|
+
option.default = parseFloat(optProp.initializer.text)
|
|
485
|
+
} else if (optProp.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
486
|
+
option.default = true
|
|
487
|
+
} else if (
|
|
488
|
+
optProp.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
489
|
+
) {
|
|
490
|
+
option.default = false
|
|
491
|
+
}
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
case 'choices':
|
|
495
|
+
// Extract manually specified choices
|
|
496
|
+
if (ts.isArrayLiteralExpression(optProp.initializer)) {
|
|
497
|
+
manualChoices = []
|
|
498
|
+
for (const element of optProp.initializer.elements) {
|
|
499
|
+
if (ts.isStringLiteral(element)) {
|
|
500
|
+
manualChoices.push(element.text)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
break
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Extract enum values from the function input type if available
|
|
509
|
+
// Get the input type if we have a pikkuFuncName
|
|
510
|
+
let inputTypes: ts.Type[] | undefined
|
|
511
|
+
if (pikkuFuncName) {
|
|
512
|
+
inputTypes = inspectorState.typesLookup.get(pikkuFuncName)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let derivedChoices: string[] | null = null
|
|
516
|
+
|
|
517
|
+
if (inputTypes && inputTypes.length > 0) {
|
|
518
|
+
derivedChoices = extractEnumFromPropertyType(
|
|
519
|
+
inputTypes[0]!,
|
|
520
|
+
optionName,
|
|
521
|
+
typeChecker
|
|
522
|
+
)
|
|
523
|
+
} else {
|
|
524
|
+
// Fallback: try to extract from Config type
|
|
525
|
+
derivedChoices = extractEnumFromConfigType(
|
|
526
|
+
logger,
|
|
527
|
+
optionName,
|
|
528
|
+
typeChecker,
|
|
529
|
+
inspectorState,
|
|
530
|
+
inspectorOptions
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Validate and set choices
|
|
535
|
+
if (manualChoices && derivedChoices) {
|
|
536
|
+
// Both manual and derived choices exist - validate manual is subset of derived
|
|
537
|
+
const invalidChoices = manualChoices.filter(
|
|
538
|
+
(choice) => !derivedChoices!.includes(choice)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if (invalidChoices.length > 0) {
|
|
542
|
+
const sourceFile = node.getSourceFile()
|
|
543
|
+
const position = prop.getStart(sourceFile)
|
|
544
|
+
const { line, character } =
|
|
545
|
+
sourceFile.getLineAndCharacterOfPosition(position)
|
|
546
|
+
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Invalid choices for option "${optionName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
|
|
549
|
+
`The following choices are not valid according to the type: ${invalidChoices.join(', ')}.\n` +
|
|
550
|
+
`Valid choices from type: ${derivedChoices.join(', ')}.`
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Manual choices are valid - use them
|
|
555
|
+
option.choices = manualChoices
|
|
556
|
+
} else if (manualChoices) {
|
|
557
|
+
// Only manual choices - use them
|
|
558
|
+
option.choices = manualChoices
|
|
559
|
+
} else if (derivedChoices) {
|
|
560
|
+
// Only derived choices - use them
|
|
561
|
+
option.choices = derivedChoices
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
options[optionName] = option
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return options
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Extracts enum values from a property of a type
|
|
573
|
+
* Handles both union types ('a' | 'b') and TypeScript enums
|
|
574
|
+
*/
|
|
575
|
+
function extractEnumFromPropertyType(
|
|
576
|
+
type: ts.Type,
|
|
577
|
+
propertyName: string,
|
|
578
|
+
typeChecker: TypeChecker
|
|
579
|
+
): string[] | null {
|
|
580
|
+
// Get the property from the type
|
|
581
|
+
const property = type.getProperty(propertyName)
|
|
582
|
+
if (!property) {
|
|
583
|
+
return null
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Get the type of the property
|
|
587
|
+
const propertyType = typeChecker.getTypeOfSymbolAtLocation(
|
|
588
|
+
property,
|
|
589
|
+
property.valueDeclaration!
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
const enumValues: string[] = []
|
|
593
|
+
|
|
594
|
+
// Check if it's a union type (e.g., 'debug' | 'info' | 'warn')
|
|
595
|
+
if (propertyType.isUnion()) {
|
|
596
|
+
for (const unionType of propertyType.types) {
|
|
597
|
+
// Check if it's a string literal type
|
|
598
|
+
if (unionType.flags & ts.TypeFlags.StringLiteral) {
|
|
599
|
+
const literalType = unionType as ts.StringLiteralType
|
|
600
|
+
enumValues.push(literalType.value)
|
|
601
|
+
}
|
|
602
|
+
// Check if it's an enum member (could be string or number enum)
|
|
603
|
+
else if (unionType.flags & ts.TypeFlags.EnumLiteral) {
|
|
604
|
+
const enumLiteralType = unionType as ts.LiteralType
|
|
605
|
+
// For string enums, use the value directly
|
|
606
|
+
if (typeof enumLiteralType.value === 'string') {
|
|
607
|
+
enumValues.push(enumLiteralType.value)
|
|
608
|
+
}
|
|
609
|
+
// For numeric enums, get the symbol name (e.g., "Debug", "Info")
|
|
610
|
+
else {
|
|
611
|
+
const symbol = (unionType as any).symbol
|
|
612
|
+
if (symbol && symbol.name) {
|
|
613
|
+
enumValues.push(symbol.name)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Check if it's an enum type directly
|
|
620
|
+
else if (propertyType.flags & ts.TypeFlags.Enum) {
|
|
621
|
+
const symbol = propertyType.getSymbol()
|
|
622
|
+
if (symbol && symbol.exports) {
|
|
623
|
+
symbol.exports.forEach((member) => {
|
|
624
|
+
const memberType = typeChecker.getTypeOfSymbolAtLocation(
|
|
625
|
+
member,
|
|
626
|
+
member.valueDeclaration!
|
|
627
|
+
)
|
|
628
|
+
if (memberType.flags & ts.TypeFlags.StringLiteral) {
|
|
629
|
+
const literalType = memberType as ts.StringLiteralType
|
|
630
|
+
enumValues.push(literalType.value)
|
|
631
|
+
} else if (typeof (memberType as any).value === 'string') {
|
|
632
|
+
enumValues.push((memberType as any).value)
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Check if it's an enum literal type
|
|
638
|
+
else if (propertyType.flags & ts.TypeFlags.EnumLiteral) {
|
|
639
|
+
const enumLiteralType = propertyType as ts.LiteralType
|
|
640
|
+
if (typeof enumLiteralType.value === 'string') {
|
|
641
|
+
enumValues.push(enumLiteralType.value)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return enumValues.length > 0 ? enumValues : null
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Extracts enum values from the Config type
|
|
650
|
+
*/
|
|
651
|
+
function extractEnumFromConfigType(
|
|
652
|
+
logger: InspectorLogger,
|
|
653
|
+
propertyName: string,
|
|
654
|
+
typeChecker: TypeChecker,
|
|
655
|
+
inspectorState: InspectorState,
|
|
656
|
+
_inspectorOptions: InspectorOptions
|
|
657
|
+
): string[] | null {
|
|
658
|
+
// Look for Config type in typesLookup
|
|
659
|
+
const configTypes = inspectorState.typesLookup.get('Config')
|
|
660
|
+
if (!configTypes || configTypes.length === 0) {
|
|
661
|
+
// Only warn once per CLI file to avoid spamming logs
|
|
662
|
+
if (!configTypeWarningShown.has('missing-config-type')) {
|
|
663
|
+
configTypeWarningShown.add('missing-config-type')
|
|
664
|
+
logger.warn(
|
|
665
|
+
`Could not find Config type in typesLookup. ` +
|
|
666
|
+
`Make sure you have a Config interface extending CoreConfig in your codebase.`
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
return null
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Use the first Config type (there should only be one)
|
|
673
|
+
const configType = configTypes[0]
|
|
674
|
+
if (!configType) {
|
|
675
|
+
if (!configTypeWarningShown.has('undefined-config-type')) {
|
|
676
|
+
configTypeWarningShown.add('undefined-config-type')
|
|
677
|
+
logger.warn(`Config type is undefined in typesLookup.`)
|
|
678
|
+
}
|
|
679
|
+
return null
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Extract enum from the property
|
|
683
|
+
return extractEnumFromPropertyType(configType, propertyName, typeChecker)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Gets the property name from a property assignment
|
|
688
|
+
*/
|
|
689
|
+
function getPropertyName(prop: ts.PropertyAssignment): string | null {
|
|
690
|
+
if (ts.isIdentifier(prop.name)) {
|
|
691
|
+
return prop.name.text
|
|
692
|
+
}
|
|
693
|
+
if (ts.isStringLiteral(prop.name)) {
|
|
694
|
+
return prop.name.text
|
|
695
|
+
}
|
|
696
|
+
return null
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Parses a parameters string to extract positional arguments
|
|
701
|
+
* Parameters format: "<env> [region] [files...]"
|
|
702
|
+
*/
|
|
703
|
+
function parseCommandPattern(pattern: string): any[] {
|
|
704
|
+
const positionals: any[] = []
|
|
705
|
+
|
|
706
|
+
// Split by spaces to get all parameter definitions
|
|
707
|
+
const parts = pattern.split(' ').filter((p) => p.trim())
|
|
708
|
+
|
|
709
|
+
for (const part of parts) {
|
|
710
|
+
if (part.startsWith('<') && part.endsWith('>')) {
|
|
711
|
+
// Required positional
|
|
712
|
+
const name = part.slice(1, -1)
|
|
713
|
+
if (name.endsWith('...')) {
|
|
714
|
+
positionals.push({
|
|
715
|
+
name: name.slice(0, -3),
|
|
716
|
+
required: true,
|
|
717
|
+
variadic: true,
|
|
718
|
+
})
|
|
719
|
+
} else {
|
|
720
|
+
positionals.push({
|
|
721
|
+
name,
|
|
722
|
+
required: true,
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
} else if (part.startsWith('[') && part.endsWith(']')) {
|
|
726
|
+
// Optional positional
|
|
727
|
+
const name = part.slice(1, -1)
|
|
728
|
+
if (name.endsWith('...')) {
|
|
729
|
+
positionals.push({
|
|
730
|
+
name: name.slice(0, -3),
|
|
731
|
+
required: false,
|
|
732
|
+
variadic: true,
|
|
733
|
+
})
|
|
734
|
+
} else {
|
|
735
|
+
positionals.push({
|
|
736
|
+
name,
|
|
737
|
+
required: false,
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
} else if (part.trim()) {
|
|
741
|
+
// Found a literal word in the parameters pattern
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Invalid parameters pattern '${pattern}': found literal word '${part}'. ` +
|
|
744
|
+
`Parameters should only contain <required> or [optional] arguments. ` +
|
|
745
|
+
`Example: "<env> [region]" or "<files...>"`
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return positionals
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Adds CLI renderer metadata to the inspector state
|
|
755
|
+
*/
|
|
756
|
+
export const addCLIRenderers: AddWiring = (
|
|
757
|
+
logger,
|
|
758
|
+
node,
|
|
759
|
+
typeChecker,
|
|
760
|
+
inspectorState,
|
|
761
|
+
options
|
|
762
|
+
) => {
|
|
763
|
+
if (!ts.isCallExpression(node)) return
|
|
764
|
+
|
|
765
|
+
const { expression, arguments: args, typeArguments } = node
|
|
766
|
+
|
|
767
|
+
// Only handle pikkuCLIRender calls
|
|
768
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (args.length === 0) return
|
|
773
|
+
|
|
774
|
+
// Extract renderer name
|
|
775
|
+
const { pikkuFuncName, exportedName } = extractFunctionName(
|
|
776
|
+
node,
|
|
777
|
+
typeChecker,
|
|
778
|
+
inspectorState.rootDir
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
// Get the source file path
|
|
782
|
+
const sourceFile = node.getSourceFile()
|
|
783
|
+
const filePath = sourceFile.fileName
|
|
784
|
+
|
|
785
|
+
// Extract services from type parameters (second type param is Services)
|
|
786
|
+
const services: { optimized: boolean; services: string[] } = {
|
|
787
|
+
optimized: true,
|
|
788
|
+
services: [],
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (typeArguments && typeArguments.length >= 2) {
|
|
792
|
+
// Second type parameter is the Services type
|
|
793
|
+
const servicesTypeNode = typeArguments[1]
|
|
794
|
+
if (servicesTypeNode) {
|
|
795
|
+
const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode)
|
|
796
|
+
|
|
797
|
+
// Extract property names from the Services type
|
|
798
|
+
const properties = servicesType.getProperties()
|
|
799
|
+
for (const prop of properties) {
|
|
800
|
+
services.services.push(prop.getName())
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// If no specific services found, it might be using the full services object
|
|
804
|
+
if (properties.length === 0) {
|
|
805
|
+
services.optimized = false
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Store renderer metadata
|
|
811
|
+
inspectorState.cli.meta.renderers[pikkuFuncName] = {
|
|
812
|
+
name: pikkuFuncName,
|
|
813
|
+
exportedName: exportedName ?? undefined,
|
|
814
|
+
services,
|
|
815
|
+
filePath,
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Add to files map if exported
|
|
819
|
+
if (exportedName) {
|
|
820
|
+
inspectorState.cli.files.add(filePath)
|
|
821
|
+
}
|
|
822
|
+
}
|