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