@pikku/inspector 0.9.5 → 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.
Files changed (94) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{add-channel.d.ts → add/add-channel.d.ts} +2 -2
  3. package/dist/{add-channel.js → add/add-channel.js} +12 -5
  4. package/dist/add/add-cli.d.ts +5 -0
  5. package/dist/add/add-cli.js +461 -0
  6. package/dist/{add-file-extends-core-type.d.ts → add/add-file-extends-core-type.d.ts} +2 -2
  7. package/dist/{add-file-extends-core-type.js → add/add-file-extends-core-type.js} +17 -5
  8. package/dist/{add-file-with-config.d.ts → add/add-file-with-config.d.ts} +1 -1
  9. package/dist/{add-file-with-config.js → add/add-file-with-config.js} +1 -1
  10. package/dist/{add-file-with-factory.d.ts → add/add-file-with-factory.d.ts} +1 -1
  11. package/dist/{add-file-with-factory.js → add/add-file-with-factory.js} +4 -4
  12. package/dist/add/add-functions.d.ts +6 -0
  13. package/dist/{add-functions.js → add/add-functions.js} +25 -5
  14. package/dist/{add-http-route.d.ts → add/add-http-route.d.ts} +2 -3
  15. package/dist/{add-http-route.js → add/add-http-route.js} +10 -4
  16. package/dist/add/add-mcp-prompt.d.ts +2 -0
  17. package/dist/{add-mcp-prompt.js → add/add-mcp-prompt.js} +10 -4
  18. package/dist/add/add-mcp-resource.d.ts +2 -0
  19. package/dist/{add-mcp-resource.js → add/add-mcp-resource.js} +10 -4
  20. package/dist/add/add-mcp-tool.d.ts +2 -0
  21. package/dist/{add-mcp-tool.js → add/add-mcp-tool.js} +10 -4
  22. package/dist/add/add-middleware.d.ts +5 -0
  23. package/dist/add/add-middleware.js +251 -0
  24. package/dist/add/add-permission.d.ts +6 -0
  25. package/dist/{add-permission.js → add/add-permission.js} +4 -3
  26. package/dist/add/add-queue-worker.d.ts +2 -0
  27. package/dist/{add-queue-worker.js → add/add-queue-worker.js} +10 -4
  28. package/dist/{add-rpc-invocations.d.ts → add/add-rpc-invocations.d.ts} +1 -1
  29. package/dist/add/add-schedule.d.ts +2 -0
  30. package/dist/{add-schedule.js → add/add-schedule.js} +10 -4
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/inspector.d.ts +2 -3
  34. package/dist/inspector.js +19 -8
  35. package/dist/types.d.ts +79 -0
  36. package/dist/{utils.d.ts → utils/extract-function-name.d.ts} +7 -15
  37. package/dist/{utils.js → utils/extract-function-name.js} +23 -142
  38. package/dist/utils/extract-services.d.ts +6 -0
  39. package/dist/utils/extract-services.js +29 -0
  40. package/dist/utils/filter-utils.d.ts +9 -0
  41. package/dist/utils/filter-utils.js +45 -0
  42. package/dist/utils/get-files-and-methods.d.ts +21 -0
  43. package/dist/utils/get-files-and-methods.js +60 -0
  44. package/dist/utils/middleware.d.ts +39 -0
  45. package/dist/utils/middleware.js +157 -0
  46. package/dist/utils/type-utils.d.ts +3 -0
  47. package/dist/utils/type-utils.js +50 -0
  48. package/dist/visit.d.ts +3 -3
  49. package/dist/visit.js +33 -30
  50. package/package.json +3 -4
  51. package/src/{add-channel.ts → add/add-channel.ts} +19 -19
  52. package/src/add/add-cli.ts +663 -0
  53. package/src/{add-file-extends-core-type.ts → add/add-file-extends-core-type.ts} +21 -6
  54. package/src/{add-file-with-config.ts → add/add-file-with-config.ts} +2 -2
  55. package/src/{add-file-with-factory.ts → add/add-file-with-factory.ts} +5 -5
  56. package/src/{add-functions.ts → add/add-functions.ts} +29 -14
  57. package/src/{add-http-route.ts → add/add-http-route.ts} +23 -14
  58. package/src/{add-mcp-prompt.ts → add/add-mcp-prompt.ts} +18 -15
  59. package/src/{add-mcp-resource.ts → add/add-mcp-resource.ts} +18 -15
  60. package/src/{add-mcp-tool.ts → add/add-mcp-tool.ts} +18 -15
  61. package/src/add/add-middleware.ts +326 -0
  62. package/src/{add-permission.ts → add/add-permission.ts} +4 -8
  63. package/src/{add-queue-worker.ts → add/add-queue-worker.ts} +17 -14
  64. package/src/{add-rpc-invocations.ts → add/add-rpc-invocations.ts} +1 -1
  65. package/src/{add-schedule.ts → add/add-schedule.ts} +17 -14
  66. package/src/index.ts +5 -0
  67. package/src/inspector.ts +20 -17
  68. package/src/types.ts +92 -0
  69. package/src/{utils.ts → utils/extract-function-name.ts} +25 -199
  70. package/src/utils/extract-services.ts +35 -0
  71. package/src/{utils.test.ts → utils/filter-utils.test.ts} +2 -2
  72. package/src/utils/filter-utils.ts +72 -0
  73. package/src/utils/get-files-and-methods.ts +143 -0
  74. package/src/utils/middleware.ts +234 -0
  75. package/src/utils/type-utils.ts +74 -0
  76. package/src/visit.ts +47 -33
  77. package/tsconfig.tsbuildinfo +1 -1
  78. package/dist/add-functions.d.ts +0 -7
  79. package/dist/add-mcp-prompt.d.ts +0 -3
  80. package/dist/add-mcp-resource.d.ts +0 -3
  81. package/dist/add-mcp-tool.d.ts +0 -3
  82. package/dist/add-middleware.d.ts +0 -7
  83. package/dist/add-middleware.js +0 -35
  84. package/dist/add-permission.d.ts +0 -7
  85. package/dist/add-queue-worker.d.ts +0 -3
  86. package/dist/add-schedule.d.ts +0 -3
  87. package/src/add-middleware.ts +0 -51
  88. /package/dist/{add-rpc-invocations.js → add/add-rpc-invocations.js} +0 -0
  89. /package/dist/{does-type-extend-core-type.d.ts → utils/does-type-extend-core-type.d.ts} +0 -0
  90. /package/dist/{does-type-extend-core-type.js → utils/does-type-extend-core-type.js} +0 -0
  91. /package/dist/{get-property-value.d.ts → utils/get-property-value.d.ts} +0 -0
  92. /package/dist/{get-property-value.js → utils/get-property-value.js} +0 -0
  93. /package/src/{does-type-extend-core-type.ts → utils/does-type-extend-core-type.ts} +0 -0
  94. /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 './types.js'
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[fileName] || []
36
+ const variables = methods.get(fileName) || []
37
+
38
+ if (!typeName) {
39
+ throw new Error('TODO')
40
+ }
36
41
  variables.push({
37
- variable: undefined,
38
- type: typeName,
42
+ variable: typeName,
43
+ type: typeName || null,
39
44
  typePath: extendedTypeDeclarationPath,
40
45
  })
41
- methods[fileName] = variables
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 './does-type-extend-core-type.js'
3
- import { PathToNameAndType } from './types.js'
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,