@pikku/inspector 0.9.4 → 0.9.6-next.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-channel.d.ts → add/add-channel.d.ts} +2 -2
- package/dist/{add-channel.js → add/add-channel.js} +12 -5
- package/dist/add/add-cli.d.ts +5 -0
- package/dist/add/add-cli.js +461 -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 -5
- 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} +1 -1
- package/dist/{add-file-with-factory.js → add/add-file-with-factory.js} +4 -4
- package/dist/add/add-functions.d.ts +6 -0
- package/dist/{add-functions.js → add/add-functions.js} +26 -6
- 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} +10 -4
- package/dist/add/add-mcp-prompt.d.ts +2 -0
- package/dist/{add-mcp-prompt.js → add/add-mcp-prompt.js} +10 -4
- package/dist/add/add-mcp-resource.d.ts +2 -0
- package/dist/{add-mcp-resource.js → add/add-mcp-resource.js} +10 -4
- package/dist/add/add-mcp-tool.d.ts +2 -0
- package/dist/{add-mcp-tool.js → add/add-mcp-tool.js} +10 -4
- package/dist/add/add-middleware.d.ts +5 -0
- package/dist/add/add-middleware.js +251 -0
- package/dist/add/add-permission.d.ts +6 -0
- package/dist/{add-permission.js → add/add-permission.js} +4 -3
- package/dist/add/add-queue-worker.d.ts +2 -0
- package/dist/{add-queue-worker.js → add/add-queue-worker.js} +10 -4
- 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} +10 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/inspector.d.ts +2 -3
- package/dist/inspector.js +19 -8
- package/dist/types.d.ts +79 -0
- package/dist/{utils.d.ts → utils/extract-function-name.d.ts} +7 -15
- package/dist/{utils.js → utils/extract-function-name.js} +23 -142
- package/dist/utils/extract-services.d.ts +6 -0
- package/dist/utils/extract-services.js +29 -0
- package/dist/utils/filter-utils.d.ts +9 -0
- package/dist/utils/filter-utils.js +45 -0
- package/dist/utils/get-files-and-methods.d.ts +21 -0
- package/dist/utils/get-files-and-methods.js +60 -0
- package/dist/utils/middleware.d.ts +39 -0
- package/dist/utils/middleware.js +157 -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 +33 -30
- package/package.json +3 -4
- package/run-tests.sh +1 -1
- package/src/{add-channel.ts → add/add-channel.ts} +19 -19
- package/src/add/add-cli.ts +663 -0
- package/src/{add-file-extends-core-type.ts → add/add-file-extends-core-type.ts} +21 -6
- 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} +5 -5
- package/src/{add-functions.ts → add/add-functions.ts} +30 -15
- package/src/{add-http-route.ts → add/add-http-route.ts} +23 -14
- package/src/{add-mcp-prompt.ts → add/add-mcp-prompt.ts} +18 -15
- package/src/{add-mcp-resource.ts → add/add-mcp-resource.ts} +18 -15
- package/src/{add-mcp-tool.ts → add/add-mcp-tool.ts} +18 -15
- package/src/add/add-middleware.ts +326 -0
- package/src/{add-permission.ts → add/add-permission.ts} +4 -8
- package/src/{add-queue-worker.ts → add/add-queue-worker.ts} +17 -14
- package/src/{add-rpc-invocations.ts → add/add-rpc-invocations.ts} +1 -1
- package/src/{add-schedule.ts → add/add-schedule.ts} +17 -14
- package/src/index.ts +5 -0
- package/src/inspector.ts +20 -17
- package/src/types.ts +92 -0
- package/src/{utils.ts → utils/extract-function-name.ts} +25 -199
- package/src/utils/extract-services.ts +35 -0
- package/src/{utils.test.ts → utils/filter-utils.test.ts} +2 -2
- package/src/utils/filter-utils.ts +72 -0
- package/src/utils/get-files-and-methods.ts +143 -0
- package/src/utils/middleware.ts +234 -0
- package/src/utils/type-utils.ts +74 -0
- package/src/visit.ts +47 -33
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add-functions.d.ts +0 -7
- package/dist/add-mcp-prompt.d.ts +0 -3
- package/dist/add-mcp-resource.d.ts +0 -3
- package/dist/add-mcp-tool.d.ts +0 -3
- 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-queue-worker.d.ts +0 -3
- package/dist/add-schedule.d.ts +0 -3
- package/src/add-middleware.ts +0 -51
- /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/dist/{get-property-value.d.ts → utils/get-property-value.d.ts} +0 -0
- /package/dist/{get-property-value.js → utils/get-property-value.js} +0 -0
- /package/src/{does-type-extend-core-type.ts → utils/does-type-extend-core-type.ts} +0 -0
- /package/src/{get-property-value.ts → utils/get-property-value.ts} +0 -0
|
@@ -0,0 +1,663 @@
|
|
|
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'
|
|
9
|
+
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
10
|
+
import { resolveMiddleware } from '../utils/middleware.js'
|
|
11
|
+
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adds CLI command metadata to the inspector state
|
|
15
|
+
*/
|
|
16
|
+
export const addCLI: AddWiring = (
|
|
17
|
+
logger,
|
|
18
|
+
node,
|
|
19
|
+
typeChecker,
|
|
20
|
+
inspectorState,
|
|
21
|
+
options
|
|
22
|
+
) => {
|
|
23
|
+
if (!ts.isCallExpression(node)) return
|
|
24
|
+
// Check if this is a wireCLI call
|
|
25
|
+
if (!node || !node.expression) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
const expression = node.expression
|
|
29
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'wireCLI') {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get the argument (should be an object literal)
|
|
34
|
+
if (node.arguments.length !== 1) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const arg = node.arguments[0]
|
|
39
|
+
if (!ts.isObjectLiteralExpression(arg)) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sourceFile = node.getSourceFile()
|
|
44
|
+
|
|
45
|
+
// Add to files set
|
|
46
|
+
inspectorState.cli.files.add(sourceFile.fileName)
|
|
47
|
+
|
|
48
|
+
// Process the CLI configuration
|
|
49
|
+
const cliConfig = processCLIConfig(
|
|
50
|
+
logger,
|
|
51
|
+
arg,
|
|
52
|
+
sourceFile,
|
|
53
|
+
typeChecker,
|
|
54
|
+
inspectorState,
|
|
55
|
+
options
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (!cliConfig) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add this program to the CLI metadata
|
|
63
|
+
inspectorState.cli.meta[cliConfig.programName] = cliConfig.programMeta
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Processes a CLI configuration object
|
|
68
|
+
*/
|
|
69
|
+
function processCLIConfig(
|
|
70
|
+
logger: InspectorLogger,
|
|
71
|
+
node: ts.ObjectLiteralExpression,
|
|
72
|
+
sourceFile: ts.SourceFile,
|
|
73
|
+
typeChecker: TypeChecker,
|
|
74
|
+
inspectorState: InspectorState,
|
|
75
|
+
options: InspectorOptions
|
|
76
|
+
): { programName: string; programMeta: CLIProgramMeta } | null {
|
|
77
|
+
let programName = ''
|
|
78
|
+
const programMeta: CLIProgramMeta = {
|
|
79
|
+
program: '',
|
|
80
|
+
commands: {},
|
|
81
|
+
options: {},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const prop of node.properties) {
|
|
85
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
86
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
87
|
+
|
|
88
|
+
const propName = prop.name.text
|
|
89
|
+
|
|
90
|
+
switch (propName) {
|
|
91
|
+
case 'program':
|
|
92
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
93
|
+
programName = prop.initializer.text
|
|
94
|
+
programMeta.program = programName
|
|
95
|
+
}
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
case 'commands':
|
|
99
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
100
|
+
programMeta.commands = processCommands(
|
|
101
|
+
logger,
|
|
102
|
+
prop.initializer,
|
|
103
|
+
sourceFile,
|
|
104
|
+
typeChecker,
|
|
105
|
+
programName,
|
|
106
|
+
inspectorState,
|
|
107
|
+
options
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
case 'options':
|
|
113
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
114
|
+
programMeta.options = processOptions(
|
|
115
|
+
logger,
|
|
116
|
+
prop.initializer,
|
|
117
|
+
typeChecker,
|
|
118
|
+
inspectorState,
|
|
119
|
+
options
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
case 'render':
|
|
125
|
+
// Track that a default renderer exists
|
|
126
|
+
programMeta.defaultRenderName = 'defaultRenderer'
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!programName) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { programName, programMeta }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Processes the commands object
|
|
140
|
+
*/
|
|
141
|
+
function processCommands(
|
|
142
|
+
logger: InspectorLogger,
|
|
143
|
+
node: ts.ObjectLiteralExpression,
|
|
144
|
+
sourceFile: ts.SourceFile,
|
|
145
|
+
typeChecker: TypeChecker,
|
|
146
|
+
programName: string,
|
|
147
|
+
inspectorState: InspectorState,
|
|
148
|
+
options: InspectorOptions
|
|
149
|
+
): Record<string, CLICommandMeta> {
|
|
150
|
+
const commands: Record<string, CLICommandMeta> = {}
|
|
151
|
+
|
|
152
|
+
for (const prop of node.properties) {
|
|
153
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
154
|
+
|
|
155
|
+
const commandName = getPropertyName(prop)
|
|
156
|
+
if (!commandName) continue
|
|
157
|
+
|
|
158
|
+
const commandMeta = processCommand(
|
|
159
|
+
logger,
|
|
160
|
+
inspectorState,
|
|
161
|
+
options,
|
|
162
|
+
commandName,
|
|
163
|
+
prop.initializer,
|
|
164
|
+
sourceFile,
|
|
165
|
+
typeChecker,
|
|
166
|
+
programName
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if (commandMeta) {
|
|
170
|
+
commands[commandName] = commandMeta
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return commands
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Processes a single command
|
|
179
|
+
*/
|
|
180
|
+
function processCommand(
|
|
181
|
+
logger: InspectorLogger,
|
|
182
|
+
inspectorState: InspectorState,
|
|
183
|
+
options: InspectorOptions,
|
|
184
|
+
name: string,
|
|
185
|
+
node: ts.Expression,
|
|
186
|
+
sourceFile: ts.SourceFile,
|
|
187
|
+
typeChecker: TypeChecker,
|
|
188
|
+
programName: string,
|
|
189
|
+
parentPath: string[] = []
|
|
190
|
+
): CLICommandMeta | null {
|
|
191
|
+
const fullPath = [...parentPath, name]
|
|
192
|
+
|
|
193
|
+
// Handle shorthand (just a function)
|
|
194
|
+
if (
|
|
195
|
+
ts.isIdentifier(node) ||
|
|
196
|
+
ts.isArrowFunction(node) ||
|
|
197
|
+
ts.isFunctionExpression(node)
|
|
198
|
+
) {
|
|
199
|
+
return {
|
|
200
|
+
pikkuFuncName: extractFunctionName(node, typeChecker).pikkuFuncName,
|
|
201
|
+
positionals: [],
|
|
202
|
+
options: {},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle pikkuCLICommand calls
|
|
207
|
+
if (ts.isCallExpression(node)) {
|
|
208
|
+
// Check if it's a pikkuCLICommand call
|
|
209
|
+
if (
|
|
210
|
+
ts.isIdentifier(node.expression) &&
|
|
211
|
+
node.expression.text === 'pikkuCLICommand' &&
|
|
212
|
+
node.arguments.length > 0 &&
|
|
213
|
+
ts.isObjectLiteralExpression(node.arguments[0])
|
|
214
|
+
) {
|
|
215
|
+
// Process the object literal argument
|
|
216
|
+
return processCommand(
|
|
217
|
+
logger,
|
|
218
|
+
inspectorState,
|
|
219
|
+
options,
|
|
220
|
+
name,
|
|
221
|
+
node.arguments[0],
|
|
222
|
+
sourceFile,
|
|
223
|
+
typeChecker,
|
|
224
|
+
programName,
|
|
225
|
+
parentPath
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle full command object
|
|
232
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const meta: CLICommandMeta = {
|
|
237
|
+
pikkuFuncName: '',
|
|
238
|
+
positionals: [],
|
|
239
|
+
options: {},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// First pass: extract pikkuFuncName and tags so we can use them when processing options/middleware
|
|
243
|
+
let pikkuFuncName: string | undefined
|
|
244
|
+
let optionsNode: ts.ObjectLiteralExpression | undefined
|
|
245
|
+
let tags: string[] | undefined
|
|
246
|
+
|
|
247
|
+
for (const prop of node.properties) {
|
|
248
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
249
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
250
|
+
|
|
251
|
+
const propName = prop.name.text
|
|
252
|
+
|
|
253
|
+
if (propName === 'func') {
|
|
254
|
+
pikkuFuncName = extractFunctionName(
|
|
255
|
+
prop.initializer,
|
|
256
|
+
typeChecker
|
|
257
|
+
).pikkuFuncName
|
|
258
|
+
meta.pikkuFuncName = pikkuFuncName
|
|
259
|
+
} else if (
|
|
260
|
+
propName === 'options' &&
|
|
261
|
+
ts.isObjectLiteralExpression(prop.initializer)
|
|
262
|
+
) {
|
|
263
|
+
optionsNode = prop.initializer
|
|
264
|
+
} else if (propName === 'tags') {
|
|
265
|
+
tags = (getPropertyValue(node, 'tags') as string[]) || undefined
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Resolve middleware
|
|
270
|
+
const middleware = resolveMiddleware(inspectorState, node, tags, typeChecker)
|
|
271
|
+
if (middleware) {
|
|
272
|
+
meta.middleware = middleware
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Second pass: process all properties
|
|
276
|
+
for (const prop of node.properties) {
|
|
277
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
278
|
+
if (!ts.isIdentifier(prop.name)) continue
|
|
279
|
+
|
|
280
|
+
const propName = prop.name.text
|
|
281
|
+
|
|
282
|
+
switch (propName) {
|
|
283
|
+
case 'parameters':
|
|
284
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
285
|
+
meta.parameters = prop.initializer.text
|
|
286
|
+
meta.positionals = parseCommandPattern(prop.initializer.text)
|
|
287
|
+
}
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
case 'description':
|
|
291
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
292
|
+
meta.description = prop.initializer.text
|
|
293
|
+
}
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
case 'func':
|
|
297
|
+
// Already handled in first pass
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
case 'render':
|
|
301
|
+
meta.renderName = extractFunctionName(
|
|
302
|
+
prop.initializer,
|
|
303
|
+
typeChecker
|
|
304
|
+
).pikkuFuncName
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
case 'options':
|
|
308
|
+
// Process with pikkuFuncName from first pass
|
|
309
|
+
if (optionsNode) {
|
|
310
|
+
meta.options = processOptions(
|
|
311
|
+
logger,
|
|
312
|
+
optionsNode,
|
|
313
|
+
typeChecker,
|
|
314
|
+
inspectorState,
|
|
315
|
+
options,
|
|
316
|
+
pikkuFuncName
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
case 'subcommands':
|
|
322
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
323
|
+
meta.subcommands = {}
|
|
324
|
+
for (const subProp of prop.initializer.properties) {
|
|
325
|
+
if (!ts.isPropertyAssignment(subProp)) continue
|
|
326
|
+
|
|
327
|
+
const subName = getPropertyName(subProp)
|
|
328
|
+
if (!subName) continue
|
|
329
|
+
|
|
330
|
+
const subCommand = processCommand(
|
|
331
|
+
logger,
|
|
332
|
+
inspectorState,
|
|
333
|
+
options,
|
|
334
|
+
subName,
|
|
335
|
+
subProp.initializer,
|
|
336
|
+
sourceFile,
|
|
337
|
+
typeChecker,
|
|
338
|
+
programName,
|
|
339
|
+
fullPath
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if (subCommand) {
|
|
343
|
+
meta.subcommands[subName] = subCommand
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
break
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return meta
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Processes CLI options and extracts enum values from function input types
|
|
356
|
+
*/
|
|
357
|
+
function processOptions(
|
|
358
|
+
logger: InspectorLogger,
|
|
359
|
+
node: ts.ObjectLiteralExpression,
|
|
360
|
+
typeChecker: TypeChecker,
|
|
361
|
+
inspectorState: InspectorState,
|
|
362
|
+
inspectorOptions: InspectorOptions,
|
|
363
|
+
pikkuFuncName?: string
|
|
364
|
+
): Record<string, any> {
|
|
365
|
+
const options: Record<string, any> = {}
|
|
366
|
+
|
|
367
|
+
for (const prop of node.properties) {
|
|
368
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
369
|
+
|
|
370
|
+
const optionName = getPropertyName(prop)
|
|
371
|
+
if (!optionName) continue
|
|
372
|
+
|
|
373
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
374
|
+
const option: any = {}
|
|
375
|
+
let manualChoices: string[] | undefined
|
|
376
|
+
|
|
377
|
+
for (const optProp of prop.initializer.properties) {
|
|
378
|
+
if (!ts.isPropertyAssignment(optProp)) continue
|
|
379
|
+
if (!ts.isIdentifier(optProp.name)) continue
|
|
380
|
+
|
|
381
|
+
const optPropName = optProp.name.text
|
|
382
|
+
|
|
383
|
+
switch (optPropName) {
|
|
384
|
+
case 'description':
|
|
385
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
386
|
+
option.description = optProp.initializer.text
|
|
387
|
+
}
|
|
388
|
+
break
|
|
389
|
+
|
|
390
|
+
case 'short':
|
|
391
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
392
|
+
option.short = optProp.initializer.text
|
|
393
|
+
}
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
case 'default':
|
|
397
|
+
// Extract default value from expression
|
|
398
|
+
if (ts.isStringLiteral(optProp.initializer)) {
|
|
399
|
+
option.default = optProp.initializer.text
|
|
400
|
+
} else if (ts.isNumericLiteral(optProp.initializer)) {
|
|
401
|
+
option.default = parseFloat(optProp.initializer.text)
|
|
402
|
+
} else if (optProp.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
403
|
+
option.default = true
|
|
404
|
+
} else if (
|
|
405
|
+
optProp.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
406
|
+
) {
|
|
407
|
+
option.default = false
|
|
408
|
+
}
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
case 'choices':
|
|
412
|
+
// Extract manually specified choices
|
|
413
|
+
if (ts.isArrayLiteralExpression(optProp.initializer)) {
|
|
414
|
+
manualChoices = []
|
|
415
|
+
for (const element of optProp.initializer.elements) {
|
|
416
|
+
if (ts.isStringLiteral(element)) {
|
|
417
|
+
manualChoices.push(element.text)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
break
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Extract enum values from the function input type if available
|
|
426
|
+
// Get the input type if we have a pikkuFuncName
|
|
427
|
+
let inputTypes: ts.Type[] | undefined
|
|
428
|
+
if (pikkuFuncName) {
|
|
429
|
+
inputTypes = inspectorState.typesLookup.get(pikkuFuncName)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let derivedChoices: string[] | null = null
|
|
433
|
+
|
|
434
|
+
if (inputTypes && inputTypes.length > 0) {
|
|
435
|
+
derivedChoices = extractEnumFromPropertyType(
|
|
436
|
+
inputTypes[0]!,
|
|
437
|
+
optionName,
|
|
438
|
+
typeChecker
|
|
439
|
+
)
|
|
440
|
+
} else {
|
|
441
|
+
// Fallback: try to extract from Config type
|
|
442
|
+
derivedChoices = extractEnumFromConfigType(
|
|
443
|
+
logger,
|
|
444
|
+
optionName,
|
|
445
|
+
typeChecker,
|
|
446
|
+
inspectorState,
|
|
447
|
+
inspectorOptions
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Validate and set choices
|
|
452
|
+
if (manualChoices && derivedChoices) {
|
|
453
|
+
// Both manual and derived choices exist - validate manual is subset of derived
|
|
454
|
+
const invalidChoices = manualChoices.filter(
|
|
455
|
+
(choice) => !derivedChoices!.includes(choice)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if (invalidChoices.length > 0) {
|
|
459
|
+
const sourceFile = node.getSourceFile()
|
|
460
|
+
const position = prop.getStart(sourceFile)
|
|
461
|
+
const { line, character } =
|
|
462
|
+
sourceFile.getLineAndCharacterOfPosition(position)
|
|
463
|
+
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Invalid choices for option "${optionName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
|
|
466
|
+
`The following choices are not valid according to the type: ${invalidChoices.join(', ')}.\n` +
|
|
467
|
+
`Valid choices from type: ${derivedChoices.join(', ')}.`
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Manual choices are valid - use them
|
|
472
|
+
option.choices = manualChoices
|
|
473
|
+
} else if (manualChoices) {
|
|
474
|
+
// Only manual choices - use them
|
|
475
|
+
option.choices = manualChoices
|
|
476
|
+
} else if (derivedChoices) {
|
|
477
|
+
// Only derived choices - use them
|
|
478
|
+
option.choices = derivedChoices
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
options[optionName] = option
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return options
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Extracts enum values from a property of a type
|
|
490
|
+
* Handles both union types ('a' | 'b') and TypeScript enums
|
|
491
|
+
*/
|
|
492
|
+
function extractEnumFromPropertyType(
|
|
493
|
+
type: ts.Type,
|
|
494
|
+
propertyName: string,
|
|
495
|
+
typeChecker: TypeChecker
|
|
496
|
+
): string[] | null {
|
|
497
|
+
// Get the property from the type
|
|
498
|
+
const property = type.getProperty(propertyName)
|
|
499
|
+
if (!property) {
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Get the type of the property
|
|
504
|
+
const propertyType = typeChecker.getTypeOfSymbolAtLocation(
|
|
505
|
+
property,
|
|
506
|
+
property.valueDeclaration!
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
const enumValues: string[] = []
|
|
510
|
+
|
|
511
|
+
// Check if it's a union type (e.g., 'debug' | 'info' | 'warn')
|
|
512
|
+
if (propertyType.isUnion()) {
|
|
513
|
+
for (const unionType of propertyType.types) {
|
|
514
|
+
// Check if it's a string literal type
|
|
515
|
+
if (unionType.flags & ts.TypeFlags.StringLiteral) {
|
|
516
|
+
const literalType = unionType as ts.StringLiteralType
|
|
517
|
+
enumValues.push(literalType.value)
|
|
518
|
+
}
|
|
519
|
+
// Check if it's an enum member (could be string or number enum)
|
|
520
|
+
else if (unionType.flags & ts.TypeFlags.EnumLiteral) {
|
|
521
|
+
const enumLiteralType = unionType as ts.LiteralType
|
|
522
|
+
// For string enums, use the value directly
|
|
523
|
+
if (typeof enumLiteralType.value === 'string') {
|
|
524
|
+
enumValues.push(enumLiteralType.value)
|
|
525
|
+
}
|
|
526
|
+
// For numeric enums, get the symbol name (e.g., "Debug", "Info")
|
|
527
|
+
else {
|
|
528
|
+
const symbol = (unionType as any).symbol
|
|
529
|
+
if (symbol && symbol.name) {
|
|
530
|
+
enumValues.push(symbol.name)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Check if it's an enum type directly
|
|
537
|
+
else if (propertyType.flags & ts.TypeFlags.Enum) {
|
|
538
|
+
const symbol = propertyType.getSymbol()
|
|
539
|
+
if (symbol && symbol.exports) {
|
|
540
|
+
symbol.exports.forEach((member) => {
|
|
541
|
+
const memberType = typeChecker.getTypeOfSymbolAtLocation(
|
|
542
|
+
member,
|
|
543
|
+
member.valueDeclaration!
|
|
544
|
+
)
|
|
545
|
+
if (memberType.flags & ts.TypeFlags.StringLiteral) {
|
|
546
|
+
const literalType = memberType as ts.StringLiteralType
|
|
547
|
+
enumValues.push(literalType.value)
|
|
548
|
+
} else if (typeof (memberType as any).value === 'string') {
|
|
549
|
+
enumValues.push((memberType as any).value)
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Check if it's an enum literal type
|
|
555
|
+
else if (propertyType.flags & ts.TypeFlags.EnumLiteral) {
|
|
556
|
+
const enumLiteralType = propertyType as ts.LiteralType
|
|
557
|
+
if (typeof enumLiteralType.value === 'string') {
|
|
558
|
+
enumValues.push(enumLiteralType.value)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return enumValues.length > 0 ? enumValues : null
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Extracts enum values from the Config type
|
|
567
|
+
*/
|
|
568
|
+
function extractEnumFromConfigType(
|
|
569
|
+
logger: InspectorLogger,
|
|
570
|
+
propertyName: string,
|
|
571
|
+
typeChecker: TypeChecker,
|
|
572
|
+
inspectorState: InspectorState,
|
|
573
|
+
_inspectorOptions: InspectorOptions
|
|
574
|
+
): string[] | null {
|
|
575
|
+
// Look for Config type in typesLookup
|
|
576
|
+
const configTypes = inspectorState.typesLookup.get('Config')
|
|
577
|
+
if (!configTypes || configTypes.length === 0) {
|
|
578
|
+
logger.warn(
|
|
579
|
+
`Warning: Could not find Config type in typesLookup for option "${propertyName}". ` +
|
|
580
|
+
`Make sure you have a Config interface extending CoreConfig in your codebase.`
|
|
581
|
+
)
|
|
582
|
+
return null
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Use the first Config type (there should only be one)
|
|
586
|
+
const configType = configTypes[0]
|
|
587
|
+
if (!configType) {
|
|
588
|
+
logger.warn(
|
|
589
|
+
`Warning: Config type is undefined in typesLookup for option "${propertyName}".`
|
|
590
|
+
)
|
|
591
|
+
return null
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Extract enum from the property
|
|
595
|
+
return extractEnumFromPropertyType(configType, propertyName, typeChecker)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Gets the property name from a property assignment
|
|
600
|
+
*/
|
|
601
|
+
function getPropertyName(prop: ts.PropertyAssignment): string | null {
|
|
602
|
+
if (ts.isIdentifier(prop.name)) {
|
|
603
|
+
return prop.name.text
|
|
604
|
+
}
|
|
605
|
+
if (ts.isStringLiteral(prop.name)) {
|
|
606
|
+
return prop.name.text
|
|
607
|
+
}
|
|
608
|
+
return null
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Parses a parameters string to extract positional arguments
|
|
613
|
+
* Parameters format: "<env> [region] [files...]"
|
|
614
|
+
*/
|
|
615
|
+
function parseCommandPattern(pattern: string): any[] {
|
|
616
|
+
const positionals: any[] = []
|
|
617
|
+
|
|
618
|
+
// Split by spaces to get all parameter definitions
|
|
619
|
+
const parts = pattern.split(' ').filter((p) => p.trim())
|
|
620
|
+
|
|
621
|
+
for (const part of parts) {
|
|
622
|
+
if (part.startsWith('<') && part.endsWith('>')) {
|
|
623
|
+
// Required positional
|
|
624
|
+
const name = part.slice(1, -1)
|
|
625
|
+
if (name.endsWith('...')) {
|
|
626
|
+
positionals.push({
|
|
627
|
+
name: name.slice(0, -3),
|
|
628
|
+
required: true,
|
|
629
|
+
variadic: true,
|
|
630
|
+
})
|
|
631
|
+
} else {
|
|
632
|
+
positionals.push({
|
|
633
|
+
name,
|
|
634
|
+
required: true,
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
} else if (part.startsWith('[') && part.endsWith(']')) {
|
|
638
|
+
// Optional positional
|
|
639
|
+
const name = part.slice(1, -1)
|
|
640
|
+
if (name.endsWith('...')) {
|
|
641
|
+
positionals.push({
|
|
642
|
+
name: name.slice(0, -3),
|
|
643
|
+
required: false,
|
|
644
|
+
variadic: true,
|
|
645
|
+
})
|
|
646
|
+
} else {
|
|
647
|
+
positionals.push({
|
|
648
|
+
name,
|
|
649
|
+
required: false,
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
} else if (part.trim()) {
|
|
653
|
+
// Found a literal word in the parameters pattern
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Invalid parameters pattern '${pattern}': found literal word '${part}'. ` +
|
|
656
|
+
`Parameters should only contain <required> or [optional] arguments. ` +
|
|
657
|
+
`Example: "<env> [region]" or "<files...>"`
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return positionals
|
|
663
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import { PathToNameAndType } from '
|
|
2
|
+
import { PathToNameAndType, InspectorState } from '../types.js'
|
|
3
3
|
|
|
4
4
|
export const addFileExtendsCoreType = (
|
|
5
5
|
node: ts.Node,
|
|
6
6
|
checker: ts.TypeChecker,
|
|
7
7
|
methods: PathToNameAndType,
|
|
8
|
-
expectedTypeName: string
|
|
8
|
+
expectedTypeName: string,
|
|
9
|
+
state?: InspectorState
|
|
9
10
|
) => {
|
|
10
11
|
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
11
12
|
const fileName = node.getSourceFile().fileName
|
|
@@ -32,13 +33,27 @@ export const addFileExtendsCoreType = (
|
|
|
32
33
|
extendedTypeDeclarationPath = sourceFile.fileName // Get the path of the file where the extended type was declared
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const variables = methods
|
|
36
|
+
const variables = methods.get(fileName) || []
|
|
37
|
+
|
|
38
|
+
if (!typeName) {
|
|
39
|
+
throw new Error('TODO')
|
|
40
|
+
}
|
|
36
41
|
variables.push({
|
|
37
|
-
variable:
|
|
38
|
-
type: typeName,
|
|
42
|
+
variable: typeName,
|
|
43
|
+
type: typeName || null,
|
|
39
44
|
typePath: extendedTypeDeclarationPath,
|
|
40
45
|
})
|
|
41
|
-
methods
|
|
46
|
+
methods.set(fileName, variables)
|
|
47
|
+
|
|
48
|
+
// Store the type in typesLookup if state is provided
|
|
49
|
+
if (state && node.name) {
|
|
50
|
+
const symbol = checker.getSymbolAtLocation(node.name)
|
|
51
|
+
if (symbol) {
|
|
52
|
+
const declaredType = checker.getDeclaredTypeOfSymbol(symbol)
|
|
53
|
+
// Use the type name as the key in typesLookup
|
|
54
|
+
state.typesLookup.set(typeName, [declaredType])
|
|
55
|
+
}
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import { doesTypeExtendsCore } from '
|
|
3
|
-
import { PathToNameAndType } from '
|
|
2
|
+
import { doesTypeExtendsCore } from '../utils/does-type-extend-core-type.js'
|
|
3
|
+
import { PathToNameAndType } from '../types.js'
|
|
4
4
|
|
|
5
5
|
export const addFileWithConfig = (
|
|
6
6
|
node: ts.Node,
|