@pikku/inspector 0.9.6-next.0 → 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 (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/add/add-channel.d.ts +5 -1
  3. package/dist/add/add-channel.js +51 -32
  4. package/dist/add/add-cli.d.ts +4 -0
  5. package/dist/add/add-cli.js +128 -23
  6. package/dist/add/add-file-extends-core-type.js +3 -2
  7. package/dist/add/add-file-with-factory.d.ts +2 -2
  8. package/dist/add/add-file-with-factory.js +34 -1
  9. package/dist/add/add-functions.js +52 -5
  10. package/dist/add/add-http-route.js +19 -12
  11. package/dist/add/add-mcp-prompt.js +20 -13
  12. package/dist/add/add-mcp-resource.js +24 -14
  13. package/dist/add/add-mcp-tool.js +23 -13
  14. package/dist/add/add-middleware.js +51 -12
  15. package/dist/add/add-permission.d.ts +1 -2
  16. package/dist/add/add-permission.js +275 -19
  17. package/dist/add/add-queue-worker.js +10 -12
  18. package/dist/add/add-schedule.js +9 -10
  19. package/dist/error-codes.d.ts +35 -0
  20. package/dist/error-codes.js +40 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/inspector.js +20 -1
  24. package/dist/types.d.ts +31 -3
  25. package/dist/utils/ensure-function-metadata.d.ts +6 -0
  26. package/dist/utils/ensure-function-metadata.js +18 -0
  27. package/dist/utils/extract-function-name.d.ts +2 -2
  28. package/dist/utils/extract-function-name.js +13 -8
  29. package/dist/utils/filter-inspector-state.d.ts +6 -0
  30. package/dist/utils/filter-inspector-state.js +382 -0
  31. package/dist/utils/filter-utils.d.ts +10 -0
  32. package/dist/utils/filter-utils.js +66 -2
  33. package/dist/utils/find-root-dir.d.ts +23 -0
  34. package/dist/utils/find-root-dir.js +55 -0
  35. package/dist/utils/get-files-and-methods.d.ts +2 -1
  36. package/dist/utils/get-files-and-methods.js +2 -1
  37. package/dist/utils/get-property-value.d.ts +9 -0
  38. package/dist/utils/get-property-value.js +20 -0
  39. package/dist/utils/middleware.d.ts +1 -1
  40. package/dist/utils/middleware.js +7 -7
  41. package/dist/utils/permissions.d.ts +43 -0
  42. package/dist/utils/permissions.js +178 -0
  43. package/dist/utils/post-process.d.ts +16 -0
  44. package/dist/utils/post-process.js +132 -0
  45. package/dist/utils/serialize-inspector-state.d.ts +179 -0
  46. package/dist/utils/serialize-inspector-state.js +170 -0
  47. package/dist/visit.js +3 -2
  48. package/package.json +4 -4
  49. package/src/add/add-channel.ts +92 -40
  50. package/src/add/add-cli.ts +188 -29
  51. package/src/add/add-file-extends-core-type.ts +5 -2
  52. package/src/add/add-file-with-factory.ts +45 -2
  53. package/src/add/add-functions.ts +60 -5
  54. package/src/add/add-http-route.ts +46 -21
  55. package/src/add/add-mcp-prompt.ts +42 -21
  56. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  57. package/src/add/add-mcp-resource.ts +50 -24
  58. package/src/add/add-mcp-resource.ts.tmp +0 -0
  59. package/src/add/add-mcp-tool.ts +48 -21
  60. package/src/add/add-middleware.ts +74 -15
  61. package/src/add/add-permission.ts +364 -22
  62. package/src/add/add-queue-worker.ts +22 -25
  63. package/src/add/add-schedule.ts +19 -20
  64. package/src/error-codes.ts +43 -0
  65. package/src/index.ts +7 -0
  66. package/src/inspector.ts +22 -1
  67. package/src/types.ts +38 -3
  68. package/src/utils/ensure-function-metadata.ts +24 -0
  69. package/src/utils/extract-function-name.ts +20 -8
  70. package/src/utils/filter-inspector-state.test.ts +1433 -0
  71. package/src/utils/filter-inspector-state.ts +526 -0
  72. package/src/utils/filter-utils.test.ts +350 -1
  73. package/src/utils/filter-utils.ts +82 -2
  74. package/src/utils/find-root-dir.ts +68 -0
  75. package/src/utils/get-files-and-methods.ts +8 -0
  76. package/src/utils/get-property-value.ts +27 -0
  77. package/src/utils/middleware.ts +14 -7
  78. package/src/utils/permissions.test.ts +327 -0
  79. package/src/utils/permissions.ts +262 -0
  80. package/src/utils/post-process.ts +178 -0
  81. package/src/utils/serialize-inspector-state.ts +375 -0
  82. package/src/utils/test-data/inspector-state.json +1680 -0
  83. package/src/visit.ts +4 -2
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,526 @@
1
+ import { InspectorState, InspectorFilters, InspectorLogger } from '../types.js'
2
+ import { PikkuWiringTypes } from '@pikku/core'
3
+ import { aggregateRequiredServices } from './post-process.js'
4
+
5
+ /**
6
+ * Match a value against a pattern with wildcard support
7
+ * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
8
+ */
9
+ function matchesWildcard(value: string, pattern: string): boolean {
10
+ if (pattern === '*') return true
11
+
12
+ const startsWithWildcard = pattern.startsWith('*')
13
+ const endsWithWildcard = pattern.endsWith('*')
14
+
15
+ if (startsWithWildcard && endsWithWildcard) {
16
+ const middle = pattern.slice(1, -1)
17
+ if (middle === '') return true
18
+ return value.includes(middle)
19
+ } else if (startsWithWildcard) {
20
+ const suffix = pattern.slice(1)
21
+ return value.endsWith(suffix) && value.length > suffix.length
22
+ } else if (endsWithWildcard) {
23
+ const prefix = pattern.slice(0, -1)
24
+ return value.startsWith(prefix) && value.length > prefix.length
25
+ }
26
+
27
+ return value === pattern
28
+ }
29
+
30
+ /**
31
+ * Check if metadata matches the given filters
32
+ */
33
+ function matchesFilters(
34
+ filters: InspectorFilters,
35
+ meta: {
36
+ type: PikkuWiringTypes
37
+ name: string
38
+ tags?: string[]
39
+ filePath?: string
40
+ httpRoute?: string
41
+ httpMethod?: string
42
+ },
43
+ logger: InspectorLogger
44
+ ): boolean {
45
+ // If no filters, allow everything
46
+ if (Object.keys(filters).length === 0) return true
47
+
48
+ // If all filter arrays are empty, allow everything
49
+ if (
50
+ (!filters.names || filters.names.length === 0) &&
51
+ (!filters.tags || filters.tags.length === 0) &&
52
+ (!filters.types || filters.types.length === 0) &&
53
+ (!filters.directories || filters.directories.length === 0) &&
54
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
55
+ (!filters.httpMethods || filters.httpMethods.length === 0)
56
+ ) {
57
+ return true
58
+ }
59
+
60
+ // Check type filter
61
+ if (filters.types && filters.types.length > 0) {
62
+ if (!filters.types.includes(meta.type)) {
63
+ logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`)
64
+ return false
65
+ }
66
+ }
67
+
68
+ // Check directory filter
69
+ if (filters.directories && filters.directories.length > 0 && meta.filePath) {
70
+ const matchesDirectory = filters.directories.some((dir) => {
71
+ const normalizedFilePath = meta.filePath!.replace(/\\/g, '/')
72
+ const normalizedDir = dir.replace(/\\/g, '/')
73
+ return normalizedFilePath.includes(normalizedDir)
74
+ })
75
+
76
+ if (!matchesDirectory) {
77
+ logger.debug(`⒡ Filtered by directory: ${meta.type}:${meta.name}`)
78
+ return false
79
+ }
80
+ }
81
+
82
+ // Check tag filter
83
+ if (filters.tags && filters.tags.length > 0) {
84
+ if (!meta.tags || !filters.tags.some((tag) => meta.tags!.includes(tag))) {
85
+ logger.debug(`⒡ Filtered by tags: ${meta.type}:${meta.name}`)
86
+ return false
87
+ }
88
+ }
89
+
90
+ // Check name filter
91
+ if (filters.names && filters.names.length > 0) {
92
+ const nameMatches = filters.names.some((pattern) =>
93
+ matchesWildcard(meta.name, pattern)
94
+ )
95
+ if (!nameMatches) {
96
+ logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`)
97
+ return false
98
+ }
99
+ }
100
+
101
+ // Check HTTP route filter
102
+ if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
103
+ const routeMatches = filters.httpRoutes.some((pattern) =>
104
+ matchesWildcard(meta.httpRoute!, pattern)
105
+ )
106
+ if (!routeMatches) {
107
+ logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`)
108
+ return false
109
+ }
110
+ }
111
+
112
+ // Check HTTP method filter
113
+ if (
114
+ filters.httpMethods &&
115
+ filters.httpMethods.length > 0 &&
116
+ meta.httpMethod
117
+ ) {
118
+ const normalizedMethod = meta.httpMethod.toUpperCase()
119
+ if (!filters.httpMethods.includes(normalizedMethod)) {
120
+ logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`)
121
+ return false
122
+ }
123
+ }
124
+
125
+ return true
126
+ }
127
+
128
+ /**
129
+ * Extract wire names from middleware/permissions metadata
130
+ */
131
+ function extractWireNames(obj: Record<string, any> | undefined): string[] {
132
+ if (!obj) return []
133
+ const names: string[] = []
134
+ for (const key of Object.keys(obj)) {
135
+ if (obj[key] && typeof obj[key] === 'object' && 'name' in obj[key]) {
136
+ names.push(obj[key].name)
137
+ }
138
+ }
139
+ return names
140
+ }
141
+
142
+ /**
143
+ * Filters inspector state based on provided filters
144
+ * This is applied post-inspection to support the inspect-once, filter-many pattern
145
+ */
146
+ export function filterInspectorState(
147
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>,
148
+ filters: InspectorFilters,
149
+ logger: InspectorLogger
150
+ ): typeof state {
151
+ // If no filters, return original state
152
+ if (
153
+ Object.keys(filters).length === 0 ||
154
+ ((!filters.names || filters.names.length === 0) &&
155
+ (!filters.tags || filters.tags.length === 0) &&
156
+ (!filters.types || filters.types.length === 0) &&
157
+ (!filters.directories || filters.directories.length === 0) &&
158
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
159
+ (!filters.httpMethods || filters.httpMethods.length === 0))
160
+ ) {
161
+ return state
162
+ }
163
+
164
+ // Create a shallow copy with new Maps/Sets to avoid mutating the original
165
+ const filteredState = {
166
+ ...state,
167
+ serviceAggregation: {
168
+ ...state.serviceAggregation,
169
+ requiredServices: new Set<string>(), // Reset requiredServices - will be recalculated
170
+ usedFunctions: new Set<string>(),
171
+ usedMiddleware: new Set<string>(),
172
+ usedPermissions: new Set<string>(),
173
+ },
174
+ http: {
175
+ ...state.http,
176
+ meta: JSON.parse(JSON.stringify(state.http.meta)), // Deep clone metadata
177
+ files: new Set<string>(), // Will be repopulated with filtered files
178
+ },
179
+ channels: {
180
+ ...state.channels,
181
+ meta: JSON.parse(JSON.stringify(state.channels.meta)),
182
+ files: new Set<string>(), // Will be repopulated with filtered files
183
+ },
184
+ scheduledTasks: {
185
+ ...state.scheduledTasks,
186
+ meta: JSON.parse(JSON.stringify(state.scheduledTasks.meta)),
187
+ files: new Set<string>(), // Will be repopulated with filtered files
188
+ },
189
+ queueWorkers: {
190
+ ...state.queueWorkers,
191
+ meta: JSON.parse(JSON.stringify(state.queueWorkers.meta)),
192
+ files: new Set<string>(), // Will be repopulated with filtered files
193
+ },
194
+ mcpEndpoints: {
195
+ ...state.mcpEndpoints,
196
+ toolsMeta: JSON.parse(JSON.stringify(state.mcpEndpoints.toolsMeta)),
197
+ resourcesMeta: JSON.parse(
198
+ JSON.stringify(state.mcpEndpoints.resourcesMeta)
199
+ ),
200
+ promptsMeta: JSON.parse(JSON.stringify(state.mcpEndpoints.promptsMeta)),
201
+ files: new Set<string>(), // Will be repopulated with filtered files
202
+ },
203
+ cli: {
204
+ ...state.cli,
205
+ meta: JSON.parse(JSON.stringify(state.cli.meta)),
206
+ files: new Set<string>(), // Will be repopulated with filtered files
207
+ },
208
+ }
209
+
210
+ // Filter HTTP routes
211
+ for (const method of Object.keys(filteredState.http.meta)) {
212
+ const routes = filteredState.http.meta[method]
213
+ for (const route of Object.keys(routes)) {
214
+ const routeMeta = routes[route]
215
+
216
+ // Get function file path for directory filtering
217
+ const funcFile = filteredState.functions.files.get(
218
+ routeMeta.pikkuFuncName
219
+ )
220
+ const filePath = funcFile?.path
221
+
222
+ const matches = matchesFilters(
223
+ filters,
224
+ {
225
+ type: 'http' as PikkuWiringTypes,
226
+ name: routeMeta.pikkuFuncName, // Use function name, not route
227
+ tags: routeMeta.tags,
228
+ filePath,
229
+ httpRoute: routeMeta.route,
230
+ httpMethod: routeMeta.method,
231
+ },
232
+ logger
233
+ )
234
+
235
+ if (!matches) {
236
+ delete routes[route]
237
+ } else {
238
+ // Track used functions/middleware/permissions
239
+ if (routeMeta.pikkuFuncName) {
240
+ filteredState.serviceAggregation.usedFunctions.add(
241
+ routeMeta.pikkuFuncName
242
+ )
243
+ }
244
+ extractWireNames(routeMeta.middleware).forEach((name: string) =>
245
+ filteredState.serviceAggregation.usedMiddleware.add(name)
246
+ )
247
+ extractWireNames(routeMeta.permissions).forEach((name: string) =>
248
+ filteredState.serviceAggregation.usedPermissions.add(name)
249
+ )
250
+ }
251
+ }
252
+ }
253
+
254
+ // Repopulate http.files if any routes remain
255
+ const hasHttpRoutes = Object.values(
256
+ filteredState.http.meta as Record<string, any>
257
+ ).some((routes) => Object.keys(routes).length > 0)
258
+ if (hasHttpRoutes) {
259
+ filteredState.http.files = new Set(state.http.files)
260
+ }
261
+
262
+ // Filter channels
263
+ for (const name of Object.keys(filteredState.channels.meta)) {
264
+ const channelMeta = filteredState.channels.meta[name]
265
+ const matches = matchesFilters(
266
+ filters,
267
+ {
268
+ type: 'channel' as PikkuWiringTypes,
269
+ name,
270
+ tags: channelMeta.tags,
271
+ },
272
+ logger
273
+ )
274
+
275
+ if (!matches) {
276
+ delete filteredState.channels.meta[name]
277
+ } else {
278
+ if (channelMeta.pikkuFuncName) {
279
+ filteredState.serviceAggregation.usedFunctions.add(
280
+ channelMeta.pikkuFuncName
281
+ )
282
+ }
283
+ extractWireNames(channelMeta.middleware).forEach((name: string) =>
284
+ filteredState.serviceAggregation.usedMiddleware.add(name)
285
+ )
286
+ extractWireNames(channelMeta.permissions).forEach((name: string) =>
287
+ filteredState.serviceAggregation.usedPermissions.add(name)
288
+ )
289
+ }
290
+ }
291
+
292
+ // Repopulate channels.files if any channels remain
293
+ if (Object.keys(filteredState.channels.meta).length > 0) {
294
+ filteredState.channels.files = new Set(state.channels.files)
295
+ }
296
+
297
+ // Filter scheduled tasks
298
+ for (const name of Object.keys(filteredState.scheduledTasks.meta)) {
299
+ const taskMeta = filteredState.scheduledTasks.meta[name]
300
+ const matches = matchesFilters(
301
+ filters,
302
+ {
303
+ type: 'scheduler' as PikkuWiringTypes,
304
+ name,
305
+ tags: taskMeta.tags,
306
+ },
307
+ logger
308
+ )
309
+
310
+ if (!matches) {
311
+ delete filteredState.scheduledTasks.meta[name]
312
+ } else {
313
+ if (taskMeta.pikkuFuncName) {
314
+ filteredState.serviceAggregation.usedFunctions.add(
315
+ taskMeta.pikkuFuncName
316
+ )
317
+ }
318
+ extractWireNames(taskMeta.middleware).forEach((name: string) =>
319
+ filteredState.serviceAggregation.usedMiddleware.add(name)
320
+ )
321
+ }
322
+ }
323
+
324
+ // Repopulate scheduledTasks.files if any tasks remain
325
+ if (Object.keys(filteredState.scheduledTasks.meta).length > 0) {
326
+ filteredState.scheduledTasks.files = new Set(state.scheduledTasks.files)
327
+ }
328
+
329
+ // Filter queue workers
330
+ for (const name of Object.keys(filteredState.queueWorkers.meta)) {
331
+ const workerMeta = filteredState.queueWorkers.meta[name]
332
+ const matches = matchesFilters(
333
+ filters,
334
+ {
335
+ type: 'queue' as PikkuWiringTypes,
336
+ name,
337
+ tags: workerMeta.tags,
338
+ },
339
+ logger
340
+ )
341
+
342
+ if (!matches) {
343
+ delete filteredState.queueWorkers.meta[name]
344
+ } else {
345
+ if (workerMeta.pikkuFuncName) {
346
+ filteredState.serviceAggregation.usedFunctions.add(
347
+ workerMeta.pikkuFuncName
348
+ )
349
+ }
350
+ extractWireNames(workerMeta.middleware).forEach((name: string) =>
351
+ filteredState.serviceAggregation.usedMiddleware.add(name)
352
+ )
353
+ }
354
+ }
355
+
356
+ // Repopulate queueWorkers.files if any workers remain
357
+ if (Object.keys(filteredState.queueWorkers.meta).length > 0) {
358
+ filteredState.queueWorkers.files = new Set(state.queueWorkers.files)
359
+ }
360
+
361
+ // Filter MCP tools
362
+ for (const name of Object.keys(filteredState.mcpEndpoints.toolsMeta)) {
363
+ const toolMeta = filteredState.mcpEndpoints.toolsMeta[name]
364
+ const matches = matchesFilters(
365
+ filters,
366
+ {
367
+ type: 'mcp' as PikkuWiringTypes,
368
+ name,
369
+ tags: toolMeta.tags,
370
+ },
371
+ logger
372
+ )
373
+
374
+ if (!matches) {
375
+ delete filteredState.mcpEndpoints.toolsMeta[name]
376
+ } else {
377
+ if (toolMeta.pikkuFuncName) {
378
+ filteredState.serviceAggregation.usedFunctions.add(
379
+ toolMeta.pikkuFuncName
380
+ )
381
+ }
382
+ extractWireNames(toolMeta.middleware).forEach((name: string) =>
383
+ filteredState.serviceAggregation.usedMiddleware.add(name)
384
+ )
385
+ extractWireNames(toolMeta.permissions).forEach((name: string) =>
386
+ filteredState.serviceAggregation.usedPermissions.add(name)
387
+ )
388
+ }
389
+ }
390
+
391
+ // Filter MCP resources
392
+ for (const name of Object.keys(filteredState.mcpEndpoints.resourcesMeta)) {
393
+ const resourceMeta = filteredState.mcpEndpoints.resourcesMeta[name]
394
+ const matches = matchesFilters(
395
+ filters,
396
+ {
397
+ type: 'mcp' as PikkuWiringTypes,
398
+ name,
399
+ tags: resourceMeta.tags,
400
+ },
401
+ logger
402
+ )
403
+
404
+ if (!matches) {
405
+ delete filteredState.mcpEndpoints.resourcesMeta[name]
406
+ } else {
407
+ if (resourceMeta.pikkuFuncName) {
408
+ filteredState.serviceAggregation.usedFunctions.add(
409
+ resourceMeta.pikkuFuncName
410
+ )
411
+ }
412
+ extractWireNames(resourceMeta.middleware).forEach((name: string) =>
413
+ filteredState.serviceAggregation.usedMiddleware.add(name)
414
+ )
415
+ extractWireNames(resourceMeta.permissions).forEach((name: string) =>
416
+ filteredState.serviceAggregation.usedPermissions.add(name)
417
+ )
418
+ }
419
+ }
420
+
421
+ // Filter MCP prompts
422
+ for (const name of Object.keys(filteredState.mcpEndpoints.promptsMeta)) {
423
+ const promptMeta = filteredState.mcpEndpoints.promptsMeta[name]
424
+ const matches = matchesFilters(
425
+ filters,
426
+ {
427
+ type: 'mcp' as PikkuWiringTypes,
428
+ name,
429
+ tags: promptMeta.tags,
430
+ },
431
+ logger
432
+ )
433
+
434
+ if (!matches) {
435
+ delete filteredState.mcpEndpoints.promptsMeta[name]
436
+ } else {
437
+ if (promptMeta.pikkuFuncName) {
438
+ filteredState.serviceAggregation.usedFunctions.add(
439
+ promptMeta.pikkuFuncName
440
+ )
441
+ }
442
+ extractWireNames(promptMeta.middleware).forEach((name: string) =>
443
+ filteredState.serviceAggregation.usedMiddleware.add(name)
444
+ )
445
+ extractWireNames(promptMeta.permissions).forEach((name: string) =>
446
+ filteredState.serviceAggregation.usedPermissions.add(name)
447
+ )
448
+ }
449
+ }
450
+
451
+ // Repopulate mcpEndpoints.files if any MCP endpoints remain
452
+ const hasMcpEndpoints =
453
+ Object.keys(filteredState.mcpEndpoints.toolsMeta).length > 0 ||
454
+ Object.keys(filteredState.mcpEndpoints.resourcesMeta).length > 0 ||
455
+ Object.keys(filteredState.mcpEndpoints.promptsMeta).length > 0
456
+ if (hasMcpEndpoints) {
457
+ filteredState.mcpEndpoints.files = new Set(state.mcpEndpoints.files)
458
+ }
459
+
460
+ // Filter CLI programs (note: CLI filtering might be more complex with nested commands)
461
+ const referencedRenderers = new Set<string>()
462
+
463
+ for (const programName of Object.keys(filteredState.cli.meta.programs)) {
464
+ const programMeta = filteredState.cli.meta.programs[programName]
465
+
466
+ // Filter commands in the program
467
+ for (const commandName of Object.keys(programMeta.commands)) {
468
+ const commandMeta = programMeta.commands[commandName]
469
+ const matches = matchesFilters(
470
+ filters,
471
+ {
472
+ type: 'cli' as PikkuWiringTypes,
473
+ name: commandName,
474
+ tags: commandMeta.tags,
475
+ },
476
+ logger
477
+ )
478
+
479
+ if (!matches) {
480
+ delete programMeta.commands[commandName]
481
+ } else {
482
+ if (commandMeta.pikkuFuncName) {
483
+ filteredState.serviceAggregation.usedFunctions.add(
484
+ commandMeta.pikkuFuncName
485
+ )
486
+ }
487
+ extractWireNames(commandMeta.middleware).forEach((name: string) =>
488
+ filteredState.serviceAggregation.usedMiddleware.add(name)
489
+ )
490
+ // Track referenced renderers
491
+ if (commandMeta.defaultRenderName) {
492
+ referencedRenderers.add(commandMeta.defaultRenderName)
493
+ }
494
+ }
495
+ }
496
+
497
+ // Remove program if it has no commands left
498
+ if (Object.keys(programMeta.commands).length === 0) {
499
+ delete filteredState.cli.meta.programs[programName]
500
+ }
501
+ }
502
+
503
+ // Filter out renderers that aren't referenced by any remaining commands
504
+ for (const rendererName of Object.keys(
505
+ filteredState.cli.meta.renderers || {}
506
+ )) {
507
+ if (!referencedRenderers.has(rendererName)) {
508
+ delete filteredState.cli.meta.renderers![rendererName]
509
+ }
510
+ }
511
+
512
+ // Repopulate cli.files if any CLI programs or referenced renderers remain
513
+ const hasCliPrograms = Object.keys(filteredState.cli.meta.programs).length > 0
514
+ const hasCliRenderers =
515
+ Object.keys(filteredState.cli.meta.renderers || {}).length > 0
516
+ if (hasCliPrograms || hasCliRenderers) {
517
+ filteredState.cli.files = new Set(state.cli.files)
518
+ }
519
+
520
+ // Recalculate requiredServices based on filtered functions/middleware/permissions
521
+ // Need to cast to InspectorState temporarily for aggregateRequiredServices
522
+ const stateForAggregation = filteredState as InspectorState
523
+ aggregateRequiredServices(stateForAggregation)
524
+
525
+ return filteredState
526
+ }