@pikku/inspector 0.12.11 → 0.12.12

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 (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/add/add-cli.js +10 -3
  3. package/dist/add/add-credential.js +2 -1
  4. package/dist/add/add-functions.js +28 -1
  5. package/dist/add/add-http-route.js +24 -5
  6. package/dist/add/add-keyed-wiring.js +3 -1
  7. package/dist/add/add-middleware.js +33 -4
  8. package/dist/add/add-permission.js +7 -7
  9. package/dist/add/add-workflow-graph.js +20 -1
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +2 -5
  15. package/dist/types.d.ts +10 -19
  16. package/dist/utils/extract-function-name.js +6 -0
  17. package/dist/utils/filter-inspector-state.js +187 -59
  18. package/dist/utils/filter-utils.js +13 -5
  19. package/dist/utils/get-property-value.d.ts +10 -0
  20. package/dist/utils/get-property-value.js +30 -0
  21. package/dist/utils/post-process.d.ts +2 -3
  22. package/dist/utils/post-process.js +3 -23
  23. package/dist/utils/resolve-addon-package.d.ts +4 -5
  24. package/dist/utils/resolve-addon-package.js +64 -16
  25. package/dist/utils/resolve-deploy-target.d.ts +28 -0
  26. package/dist/utils/resolve-deploy-target.js +56 -0
  27. package/dist/utils/resolve-versions.js +79 -0
  28. package/dist/utils/schema-generator.js +31 -12
  29. package/package.json +2 -2
  30. package/src/add/add-cli.ts +10 -3
  31. package/src/add/add-credential.ts +3 -0
  32. package/src/add/add-functions.test.ts +149 -0
  33. package/src/add/add-functions.ts +37 -1
  34. package/src/add/add-gateway.ts +5 -1
  35. package/src/add/add-http-route.ts +26 -6
  36. package/src/add/add-keyed-wiring.ts +7 -1
  37. package/src/add/add-mcp-prompt.ts +5 -1
  38. package/src/add/add-mcp-resource.ts +5 -1
  39. package/src/add/add-middleware.ts +42 -4
  40. package/src/add/add-permission.ts +7 -7
  41. package/src/add/add-schedule.ts +5 -1
  42. package/src/add/add-workflow-graph.ts +19 -1
  43. package/src/add/wire-name-literal.test.ts +114 -0
  44. package/src/error-codes.ts +1 -0
  45. package/src/index.ts +1 -0
  46. package/src/inspector.ts +1 -5
  47. package/src/types.ts +19 -15
  48. package/src/utils/extract-function-name.ts +8 -0
  49. package/src/utils/filter-inspector-state.test.ts +168 -64
  50. package/src/utils/filter-inspector-state.ts +290 -64
  51. package/src/utils/filter-utils.test.ts +30 -15
  52. package/src/utils/filter-utils.ts +14 -5
  53. package/src/utils/get-property-value.ts +40 -0
  54. package/src/utils/post-process.ts +3 -38
  55. package/src/utils/resolve-addon-package.ts +65 -14
  56. package/src/utils/resolve-deploy-target.test.ts +105 -0
  57. package/src/utils/resolve-deploy-target.ts +63 -0
  58. package/src/utils/resolve-versions.test.ts +108 -0
  59. package/src/utils/resolve-versions.ts +86 -0
  60. package/src/utils/schema-generator.ts +37 -13
  61. package/tsconfig.tsbuildinfo +1 -1
@@ -6,6 +6,7 @@ import type {
6
6
  import type { PikkuWiringTypes } from '@pikku/core'
7
7
  import { parseVersionedId } from '@pikku/core'
8
8
  import { aggregateRequiredServices } from './post-process.js'
9
+ import { resolveDeployTarget } from './resolve-deploy-target.js'
9
10
 
10
11
  // Module-level Set to track warned groups across multiple filter calls
11
12
  const globalWarnedGroups = new Set<string>()
@@ -35,6 +36,59 @@ function matchesWildcard(value: string, pattern: string): boolean {
35
36
  return value === pattern
36
37
  }
37
38
 
39
+ function collectSourceFiles(
40
+ value: unknown,
41
+ sourceFiles = new Set<string>(),
42
+ seen = new Set<object>()
43
+ ): Set<string> {
44
+ if (typeof value === 'string') {
45
+ return sourceFiles
46
+ }
47
+
48
+ if (Array.isArray(value)) {
49
+ for (const entry of value) {
50
+ collectSourceFiles(entry, sourceFiles, seen)
51
+ }
52
+ return sourceFiles
53
+ }
54
+
55
+ if (!value || typeof value !== 'object') {
56
+ return sourceFiles
57
+ }
58
+
59
+ if (seen.has(value)) {
60
+ return sourceFiles
61
+ }
62
+ seen.add(value)
63
+
64
+ if ('sourceFile' in value && typeof value.sourceFile === 'string') {
65
+ sourceFiles.add(value.sourceFile)
66
+ }
67
+
68
+ for (const nestedValue of Object.values(value)) {
69
+ collectSourceFiles(nestedValue, sourceFiles, seen)
70
+ }
71
+
72
+ return sourceFiles
73
+ }
74
+
75
+ function repopulateFileSetFromMeta(
76
+ meta: unknown,
77
+ fallbackFiles: Set<string>,
78
+ hasEntries: boolean
79
+ ): Set<string> {
80
+ const sourceFiles = collectSourceFiles(meta)
81
+ if (sourceFiles.size > 0) {
82
+ return sourceFiles
83
+ }
84
+
85
+ if (hasEntries) {
86
+ return new Set(fallbackFiles)
87
+ }
88
+
89
+ return new Set<string>()
90
+ }
91
+
38
92
  /**
39
93
  * Check if metadata matches the given filters
40
94
  */
@@ -50,7 +104,10 @@ function matchesFilters(
50
104
  groupBasePath?: string
51
105
  },
52
106
  logger: InspectorLogger,
53
- warnedGroups?: Set<string>
107
+ warnedGroups?: Set<string>,
108
+ // Set of pikkuFuncIds to keep based on the deploy filter; null when
109
+ // the deploy filter is inactive.
110
+ keptByDeploy?: Set<string> | null
54
111
  ): boolean {
55
112
  // If no filters, allow everything
56
113
  if (Object.keys(filters).length === 0) return true
@@ -59,18 +116,41 @@ function matchesFilters(
59
116
  if (
60
117
  (!filters.names || filters.names.length === 0) &&
61
118
  (!filters.tags || filters.tags.length === 0) &&
62
- (!filters.types || filters.types.length === 0) &&
119
+ (!filters.wires || filters.wires.length === 0) &&
120
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
63
121
  (!filters.directories || filters.directories.length === 0) &&
64
122
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
65
- (!filters.httpMethods || filters.httpMethods.length === 0)
123
+ (!filters.httpMethods || filters.httpMethods.length === 0) &&
124
+ (!filters.target || filters.target.length === 0) &&
125
+ (!filters.excludeNames || filters.excludeNames.length === 0) &&
126
+ (!filters.excludeTags || filters.excludeTags.length === 0) &&
127
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
128
+ (!filters.excludeDirectories || filters.excludeDirectories.length === 0) &&
129
+ (!filters.excludeHttpRoutes || filters.excludeHttpRoutes.length === 0) &&
130
+ (!filters.excludeHttpMethods || filters.excludeHttpMethods.length === 0) &&
131
+ (!filters.excludeTarget || filters.excludeTarget.length === 0)
66
132
  ) {
67
133
  return true
68
134
  }
69
135
 
70
- // Check type filter
71
- if (filters.types && filters.types.length > 0) {
72
- if (!filters.types.includes(meta.type)) {
73
- logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`)
136
+ // Deploy-target include filter (computed once per filterInspectorState call).
137
+ if (keptByDeploy && !keptByDeploy.has(meta.name)) {
138
+ logger.debug(`⒡ Filtered by deploy include: ${meta.type}:${meta.name}`)
139
+ return false
140
+ }
141
+
142
+ // Check wire include filter
143
+ if (filters.wires && filters.wires.length > 0) {
144
+ if (!filters.wires.includes(meta.type)) {
145
+ logger.debug(`⒡ Filtered by wire include: ${meta.type}:${meta.name}`)
146
+ return false
147
+ }
148
+ }
149
+
150
+ // Check wire exclude filter
151
+ if (filters.excludeWires && filters.excludeWires.length > 0) {
152
+ if (filters.excludeWires.includes(meta.type)) {
153
+ logger.debug(`⒡ Filtered by wire exclude: ${meta.type}:${meta.name}`)
74
154
  return false
75
155
  }
76
156
  }
@@ -89,35 +169,55 @@ function matchesFilters(
89
169
  }
90
170
  }
91
171
 
92
- // Check tag filter
172
+ // Check tag include filter
93
173
  if (filters.tags && filters.tags.length > 0) {
94
174
  if (!meta.tags || !filters.tags.some((tag) => meta.tags!.includes(tag))) {
95
- logger.debug(`⒡ Filtered by tags: ${meta.type}:${meta.name}`)
175
+ logger.debug(`⒡ Filtered by tags include: ${meta.type}:${meta.name}`)
96
176
  return false
97
177
  }
98
178
  }
99
179
 
100
- // Check name filter (match against both full ID and base name for versioned functions)
180
+ // Check tag exclude filter
181
+ if (filters.excludeTags && filters.excludeTags.length > 0) {
182
+ if (
183
+ meta.tags &&
184
+ filters.excludeTags.some((tag) => meta.tags!.includes(tag))
185
+ ) {
186
+ logger.debug(`⒡ Filtered by tags exclude: ${meta.type}:${meta.name}`)
187
+ return false
188
+ }
189
+ }
190
+
191
+ const { baseName } = parseVersionedId(meta.name)
192
+ const matchesNamePattern = (pattern: string) =>
193
+ matchesWildcard(meta.name, pattern) ||
194
+ (baseName !== meta.name && matchesWildcard(baseName, pattern))
195
+
196
+ // Check name include filter (match against both full ID and base name for versioned functions)
101
197
  if (filters.names && filters.names.length > 0) {
102
- const { baseName } = parseVersionedId(meta.name)
103
- const nameMatches = filters.names.some(
104
- (pattern) =>
105
- matchesWildcard(meta.name, pattern) ||
106
- (baseName !== meta.name && matchesWildcard(baseName, pattern))
107
- )
198
+ const nameMatches = filters.names.some(matchesNamePattern)
108
199
  if (!nameMatches) {
109
- logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`)
200
+ logger.debug(`⒡ Filtered by name include: ${meta.type}:${meta.name}`)
201
+ return false
202
+ }
203
+ }
204
+
205
+ // Check name exclude filter
206
+ if (filters.excludeNames && filters.excludeNames.length > 0) {
207
+ if (filters.excludeNames.some(matchesNamePattern)) {
208
+ logger.debug(`⒡ Filtered by name exclude: ${meta.type}:${meta.name}`)
110
209
  return false
111
210
  }
112
211
  }
113
212
 
114
- // Check HTTP route filter
213
+ const matchesRoutePattern = (pattern: string) =>
214
+ !!meta.httpRoute && matchesWildcard(meta.httpRoute, pattern)
215
+
216
+ // Check HTTP route include filter
115
217
  if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
116
- const routeMatches = filters.httpRoutes.some((pattern) =>
117
- matchesWildcard(meta.httpRoute!, pattern)
118
- )
218
+ const routeMatches = filters.httpRoutes.some(matchesRoutePattern)
119
219
  if (!routeMatches) {
120
- logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`)
220
+ logger.debug(`⒡ Filtered by HTTP route include: ${meta.httpRoute}`)
121
221
  return false
122
222
  }
123
223
 
@@ -139,15 +239,40 @@ function matchesFilters(
139
239
  }
140
240
  }
141
241
 
142
- // Check HTTP method filter
242
+ // Check HTTP route exclude filter
243
+ if (
244
+ filters.excludeHttpRoutes &&
245
+ filters.excludeHttpRoutes.length > 0 &&
246
+ meta.httpRoute
247
+ ) {
248
+ if (filters.excludeHttpRoutes.some(matchesRoutePattern)) {
249
+ logger.debug(`⒡ Filtered by HTTP route exclude: ${meta.httpRoute}`)
250
+ return false
251
+ }
252
+ }
253
+
254
+ const normalizedMethod = meta.httpMethod?.toUpperCase()
255
+
256
+ // Check HTTP method include filter
143
257
  if (
144
258
  filters.httpMethods &&
145
259
  filters.httpMethods.length > 0 &&
146
- meta.httpMethod
260
+ normalizedMethod
147
261
  ) {
148
- const normalizedMethod = meta.httpMethod.toUpperCase()
149
262
  if (!filters.httpMethods.includes(normalizedMethod)) {
150
- logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`)
263
+ logger.debug(`⒡ Filtered by HTTP method include: ${meta.httpMethod}`)
264
+ return false
265
+ }
266
+ }
267
+
268
+ // Check HTTP method exclude filter
269
+ if (
270
+ filters.excludeHttpMethods &&
271
+ filters.excludeHttpMethods.length > 0 &&
272
+ normalizedMethod
273
+ ) {
274
+ if (filters.excludeHttpMethods.includes(normalizedMethod)) {
275
+ logger.debug(`⒡ Filtered by HTTP method exclude: ${meta.httpMethod}`)
151
276
  return false
152
277
  }
153
278
  }
@@ -183,14 +308,47 @@ export function filterInspectorState(
183
308
  Object.keys(filters).length === 0 ||
184
309
  ((!filters.names || filters.names.length === 0) &&
185
310
  (!filters.tags || filters.tags.length === 0) &&
186
- (!filters.types || filters.types.length === 0) &&
311
+ (!filters.wires || filters.wires.length === 0) &&
312
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
187
313
  (!filters.directories || filters.directories.length === 0) &&
188
314
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
189
- (!filters.httpMethods || filters.httpMethods.length === 0))
315
+ (!filters.httpMethods || filters.httpMethods.length === 0) &&
316
+ (!filters.target || filters.target.length === 0) &&
317
+ (!filters.excludeNames || filters.excludeNames.length === 0) &&
318
+ (!filters.excludeTags || filters.excludeTags.length === 0) &&
319
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
320
+ (!filters.excludeDirectories ||
321
+ filters.excludeDirectories.length === 0) &&
322
+ (!filters.excludeHttpRoutes || filters.excludeHttpRoutes.length === 0) &&
323
+ (!filters.excludeHttpMethods ||
324
+ filters.excludeHttpMethods.length === 0) &&
325
+ (!filters.excludeTarget || filters.excludeTarget.length === 0))
190
326
  ) {
191
327
  return state
192
328
  }
193
329
 
330
+ // Precompute kept-function set for the deploy filter (if active).
331
+ // resolveDeployTarget throws IncompatibleDeployTargetError when an
332
+ // explicit deploy: 'serverless' clashes with serverlessIncompatible.
333
+ let keptByDeploy: Set<string> | null = null
334
+ if (
335
+ (filters.target && filters.target.length > 0) ||
336
+ (filters.excludeTarget && filters.excludeTarget.length > 0)
337
+ ) {
338
+ const allowed = filters.target ? new Set(filters.target) : null
339
+ const excluded = filters.excludeTarget
340
+ ? new Set(filters.excludeTarget)
341
+ : null
342
+ const incompatible = new Set(filters.serverlessIncompatible ?? [])
343
+ keptByDeploy = new Set<string>()
344
+ for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
345
+ const target = resolveDeployTarget(funcMeta as any, incompatible, funcId)
346
+ if (allowed && !allowed.has(target)) continue
347
+ if (excluded && excluded.has(target)) continue
348
+ keptByDeploy.add(funcId)
349
+ }
350
+ }
351
+
194
352
  // Snapshot the original workflow graph meta before filtering prunes it
195
353
  const originalGraphMeta = {
196
354
  ...((state as InspectorState).workflows?.graphMeta ?? {}),
@@ -292,7 +450,8 @@ export function filterInspectorState(
292
450
  groupBasePath: routeMeta.groupBasePath,
293
451
  },
294
452
  logger,
295
- globalWarnedGroups
453
+ globalWarnedGroups,
454
+ keptByDeploy
296
455
  )
297
456
 
298
457
  if (!matches) {
@@ -356,7 +515,11 @@ export function filterInspectorState(
356
515
  name,
357
516
  tags: channelMeta.tags,
358
517
  },
359
- logger
518
+ logger,
519
+
520
+ undefined,
521
+
522
+ keptByDeploy
360
523
  )
361
524
 
362
525
  if (!matches) {
@@ -405,10 +568,11 @@ export function filterInspectorState(
405
568
  }
406
569
  }
407
570
 
408
- // Repopulate channels.files if any channels remain
409
- if (Object.keys(filteredState.channels.meta).length > 0) {
410
- filteredState.channels.files = new Set(state.channels.files)
411
- }
571
+ filteredState.channels.files = repopulateFileSetFromMeta(
572
+ filteredState.channels.meta,
573
+ state.channels.files,
574
+ Object.keys(filteredState.channels.meta).length > 0
575
+ )
412
576
 
413
577
  // Filter triggers
414
578
  for (const name of Object.keys(filteredState.triggers.meta)) {
@@ -420,7 +584,11 @@ export function filterInspectorState(
420
584
  name,
421
585
  tags: triggerMeta.tags,
422
586
  },
423
- logger
587
+ logger,
588
+
589
+ undefined,
590
+
591
+ keptByDeploy
424
592
  )
425
593
 
426
594
  if (!matches) {
@@ -434,10 +602,11 @@ export function filterInspectorState(
434
602
  }
435
603
  }
436
604
 
437
- // Repopulate triggers.files if any triggers remain
438
- if (Object.keys(filteredState.triggers.meta).length > 0) {
439
- filteredState.triggers.files = new Set(state.triggers.files)
440
- }
605
+ filteredState.triggers.files = repopulateFileSetFromMeta(
606
+ filteredState.triggers.meta,
607
+ state.triggers.files,
608
+ Object.keys(filteredState.triggers.meta).length > 0
609
+ )
441
610
 
442
611
  // Filter scheduled tasks
443
612
  for (const name of Object.keys(filteredState.scheduledTasks.meta)) {
@@ -449,7 +618,11 @@ export function filterInspectorState(
449
618
  name,
450
619
  tags: taskMeta.tags,
451
620
  },
452
- logger
621
+ logger,
622
+
623
+ undefined,
624
+
625
+ keptByDeploy
453
626
  )
454
627
 
455
628
  if (!matches) {
@@ -464,10 +637,11 @@ export function filterInspectorState(
464
637
  }
465
638
  }
466
639
 
467
- // Repopulate scheduledTasks.files if any tasks remain
468
- if (Object.keys(filteredState.scheduledTasks.meta).length > 0) {
469
- filteredState.scheduledTasks.files = new Set(state.scheduledTasks.files)
470
- }
640
+ filteredState.scheduledTasks.files = repopulateFileSetFromMeta(
641
+ filteredState.scheduledTasks.meta,
642
+ state.scheduledTasks.files,
643
+ Object.keys(filteredState.scheduledTasks.meta).length > 0
644
+ )
471
645
 
472
646
  // Filter queue workers
473
647
  for (const name of Object.keys(filteredState.queueWorkers.meta)) {
@@ -479,7 +653,11 @@ export function filterInspectorState(
479
653
  name,
480
654
  tags: workerMeta.tags,
481
655
  },
482
- logger
656
+ logger,
657
+
658
+ undefined,
659
+
660
+ keptByDeploy
483
661
  )
484
662
 
485
663
  if (!matches) {
@@ -502,10 +680,11 @@ export function filterInspectorState(
502
680
  }
503
681
  }
504
682
 
505
- // Repopulate queueWorkers.files if any workers remain
506
- if (Object.keys(filteredState.queueWorkers.meta).length > 0) {
507
- filteredState.queueWorkers.files = new Set(state.queueWorkers.files)
508
- }
683
+ filteredState.queueWorkers.files = repopulateFileSetFromMeta(
684
+ filteredState.queueWorkers.meta,
685
+ state.queueWorkers.files,
686
+ Object.keys(filteredState.queueWorkers.meta).length > 0
687
+ )
509
688
 
510
689
  // Filter MCP tools
511
690
  for (const name of Object.keys(filteredState.mcpEndpoints.toolsMeta)) {
@@ -517,7 +696,11 @@ export function filterInspectorState(
517
696
  name,
518
697
  tags: toolMeta.tags,
519
698
  },
520
- logger
699
+ logger,
700
+
701
+ undefined,
702
+
703
+ keptByDeploy
521
704
  )
522
705
 
523
706
  if (!matches) {
@@ -545,7 +728,11 @@ export function filterInspectorState(
545
728
  name,
546
729
  tags: resourceMeta.tags,
547
730
  },
548
- logger
731
+ logger,
732
+
733
+ undefined,
734
+
735
+ keptByDeploy
549
736
  )
550
737
 
551
738
  if (!matches) {
@@ -575,7 +762,11 @@ export function filterInspectorState(
575
762
  name,
576
763
  tags: promptMeta.tags,
577
764
  },
578
- logger
765
+ logger,
766
+
767
+ undefined,
768
+
769
+ keptByDeploy
579
770
  )
580
771
 
581
772
  if (!matches) {
@@ -595,14 +786,20 @@ export function filterInspectorState(
595
786
  }
596
787
  }
597
788
 
598
- // Repopulate mcpEndpoints.files if any MCP endpoints remain
789
+ // Repopulate mcpEndpoints.files from surviving endpoint metadata
599
790
  const hasMcpEndpoints =
600
791
  Object.keys(filteredState.mcpEndpoints.toolsMeta).length > 0 ||
601
792
  Object.keys(filteredState.mcpEndpoints.resourcesMeta).length > 0 ||
602
793
  Object.keys(filteredState.mcpEndpoints.promptsMeta).length > 0
603
- if (hasMcpEndpoints) {
604
- filteredState.mcpEndpoints.files = new Set(state.mcpEndpoints.files)
605
- }
794
+ filteredState.mcpEndpoints.files = repopulateFileSetFromMeta(
795
+ {
796
+ toolsMeta: filteredState.mcpEndpoints.toolsMeta,
797
+ resourcesMeta: filteredState.mcpEndpoints.resourcesMeta,
798
+ promptsMeta: filteredState.mcpEndpoints.promptsMeta,
799
+ },
800
+ state.mcpEndpoints.files,
801
+ hasMcpEndpoints
802
+ )
606
803
 
607
804
  // Filter AI agents
608
805
  for (const name of Object.keys(filteredState.agents.agentsMeta)) {
@@ -614,7 +811,11 @@ export function filterInspectorState(
614
811
  name,
615
812
  tags: agentMeta.tags,
616
813
  },
617
- logger
814
+ logger,
815
+
816
+ undefined,
817
+
818
+ keptByDeploy
618
819
  )
619
820
 
620
821
  if (!matches) {
@@ -654,7 +855,11 @@ export function filterInspectorState(
654
855
  name: commandName,
655
856
  tags: commandMeta.tags,
656
857
  },
657
- logger
858
+ logger,
859
+
860
+ undefined,
861
+
862
+ keptByDeploy
658
863
  )
659
864
 
660
865
  if (!matches) {
@@ -690,13 +895,15 @@ export function filterInspectorState(
690
895
  }
691
896
  }
692
897
 
693
- // Repopulate cli.files if any CLI programs or referenced renderers remain
898
+ // Repopulate cli.files from surviving program/renderer metadata
694
899
  const hasCliPrograms = Object.keys(filteredState.cli.meta.programs).length > 0
695
900
  const hasCliRenderers =
696
901
  Object.keys(filteredState.cli.meta.renderers || {}).length > 0
697
- if (hasCliPrograms || hasCliRenderers) {
698
- filteredState.cli.files = new Set(state.cli.files)
699
- }
902
+ filteredState.cli.files = repopulateFileSetFromMeta(
903
+ filteredState.cli.meta,
904
+ state.cli.files,
905
+ hasCliPrograms || hasCliRenderers
906
+ )
700
907
 
701
908
  // Direct function filtering: functions that match the names/tags/directories
702
909
  // filters should be included even if no wiring (HTTP, scheduler, etc.) references them.
@@ -722,7 +929,11 @@ export function filterInspectorState(
722
929
  tags: funcMeta.tags,
723
930
  filePath,
724
931
  },
725
- logger
932
+ logger,
933
+
934
+ undefined,
935
+
936
+ keptByDeploy
726
937
  )
727
938
 
728
939
  if (matches) {
@@ -786,12 +997,17 @@ export function filterInspectorState(
786
997
  }
787
998
  }
788
999
 
789
- // Prune workflow graphs whose function was filtered out
1000
+ // Prune workflow graphs whose function was filtered out — UNLESS the
1001
+ // workflow's own name is in the filter (covers pikkuWorkflowGraph,
1002
+ // which doesn't register a function meta, so its pikkuFuncId never
1003
+ // ends up in usedFunctions even when callers explicitly want it).
1004
+ const filterNamesSet = new Set(filters.names ?? [])
790
1005
  const workflowKeys = new Set([
791
1006
  ...Object.keys(filteredState.workflows.graphMeta),
792
1007
  ...Object.keys(filteredState.workflows.meta),
793
1008
  ])
794
1009
  for (const name of workflowKeys) {
1010
+ if (filterNamesSet.has(name)) continue
795
1011
  const graphMeta = filteredState.workflows.graphMeta[name]
796
1012
  const workflowMeta = filteredState.workflows.meta[name]
797
1013
  // Check both graphMeta.pikkuFuncId and meta.pikkuFuncId
@@ -866,6 +1082,16 @@ export function filterInspectorState(
866
1082
  }
867
1083
  }
868
1084
 
1085
+ // workflowService internally calls queueService (via queueStepWorker /
1086
+ // queueOrchestrator). Any unit that needs workflowService also needs
1087
+ // queueService for the transitive enqueue calls — covers
1088
+ // workflow-starter, graph-starter, workflow-runner, etc.
1089
+ if (
1090
+ filteredState.serviceAggregation.requiredServices.has('workflowService')
1091
+ ) {
1092
+ filteredState.serviceAggregation.requiredServices.add('queueService')
1093
+ }
1094
+
869
1095
  // Recalculate requiredServices based on filtered functions/middleware/permissions
870
1096
  // Need to cast to InspectorState temporarily for aggregateRequiredServices
871
1097
  const stateForAggregation = filteredState as InspectorState
@@ -31,7 +31,7 @@ describe('matchesFilters', () => {
31
31
  test('should return true when all filter arrays are empty', () => {
32
32
  const filters: InspectorFilters = {
33
33
  tags: [],
34
- types: [],
34
+ wires: [],
35
35
  directories: [],
36
36
  }
37
37
 
@@ -108,10 +108,10 @@ describe('matchesFilters', () => {
108
108
  })
109
109
  })
110
110
 
111
- describe('Type filtering', () => {
112
- test('should return true when type matches', () => {
111
+ describe('Wire filtering', () => {
112
+ test('should return true when wire matches', () => {
113
113
  const filters: InspectorFilters = {
114
- types: ['http', 'channel'],
114
+ wires: ['http', 'channel'],
115
115
  }
116
116
 
117
117
  const result = matchesFilters(
@@ -124,9 +124,9 @@ describe('matchesFilters', () => {
124
124
  assert.equal(result, true)
125
125
  })
126
126
 
127
- test('should return false when type does not match', () => {
127
+ test('should return false when wire does not match', () => {
128
128
  const filters: InspectorFilters = {
129
- types: ['channel', 'queue'],
129
+ wires: ['channel', 'queue'],
130
130
  }
131
131
 
132
132
  const result = matchesFilters(
@@ -144,7 +144,7 @@ describe('matchesFilters', () => {
144
144
 
145
145
  eventTypes.forEach((eventType) => {
146
146
  const filters: InspectorFilters = {
147
- types: [eventType],
147
+ wires: [eventType],
148
148
  }
149
149
 
150
150
  const result = matchesFilters(
@@ -154,9 +154,24 @@ describe('matchesFilters', () => {
154
154
  mockLogger
155
155
  )
156
156
 
157
- assert.equal(result, true, `Should match for type: ${eventType}`)
157
+ assert.equal(result, true, `Should match for wire: ${eventType}`)
158
158
  })
159
159
  })
160
+
161
+ test('should exclude matching wires', () => {
162
+ const filters: InspectorFilters = {
163
+ excludeWires: ['queue', 'scheduler'],
164
+ }
165
+
166
+ const result = matchesFilters(
167
+ filters,
168
+ { tags: ['test'] },
169
+ { type: 'queue', name: 'email-worker' },
170
+ mockLogger
171
+ )
172
+
173
+ assert.equal(result, false)
174
+ })
160
175
  })
161
176
 
162
177
  describe('Directory filtering', () => {
@@ -275,7 +290,7 @@ describe('matchesFilters', () => {
275
290
  test('should return true when all filters pass', () => {
276
291
  const filters: InspectorFilters = {
277
292
  tags: ['api'],
278
- types: ['http'],
293
+ wires: ['http'],
279
294
  directories: ['src/api'],
280
295
  }
281
296
 
@@ -296,7 +311,7 @@ describe('matchesFilters', () => {
296
311
  test('should return false when type filter fails (even if others pass)', () => {
297
312
  const filters: InspectorFilters = {
298
313
  tags: ['api'],
299
- types: ['channel'],
314
+ wires: ['channel'],
300
315
  directories: ['src/api'],
301
316
  }
302
317
 
@@ -317,7 +332,7 @@ describe('matchesFilters', () => {
317
332
  test('should return false when directory filter fails (even if others pass)', () => {
318
333
  const filters: InspectorFilters = {
319
334
  tags: ['api'],
320
- types: ['http'],
335
+ wires: ['http'],
321
336
  directories: ['src/internal'],
322
337
  }
323
338
 
@@ -338,7 +353,7 @@ describe('matchesFilters', () => {
338
353
  test('should return false when tag filter fails (even if others pass)', () => {
339
354
  const filters: InspectorFilters = {
340
355
  tags: ['internal'],
341
- types: ['http'],
356
+ wires: ['http'],
342
357
  directories: ['src/api'],
343
358
  }
344
359
 
@@ -375,7 +390,7 @@ describe('matchesFilters', () => {
375
390
 
376
391
  test('should handle empty meta name', () => {
377
392
  const filters: InspectorFilters = {
378
- types: ['channel'],
393
+ wires: ['channel'],
379
394
  }
380
395
 
381
396
  const result = matchesFilters(
@@ -443,7 +458,7 @@ describe('matchesFilters', () => {
443
458
 
444
459
  test('should handle multiple matching types', () => {
445
460
  const filters: InspectorFilters = {
446
- types: ['http', 'channel'],
461
+ wires: ['http', 'channel'],
447
462
  }
448
463
 
449
464
  const result = matchesFilters(
@@ -724,7 +739,7 @@ describe('matchesFilters', () => {
724
739
  test('should return true when all filters including new ones pass', () => {
725
740
  const filters: InspectorFilters = {
726
741
  tags: ['api'],
727
- types: ['http'],
742
+ wires: ['http'],
728
743
  httpRoutes: ['/api/*'],
729
744
  httpMethods: ['GET'],
730
745
  }