@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
@@ -1,5 +1,6 @@
1
1
  import { parseVersionedId } from '@pikku/core';
2
2
  import { aggregateRequiredServices } from './post-process.js';
3
+ import { resolveDeployTarget } from './resolve-deploy-target.js';
3
4
  // Module-level Set to track warned groups across multiple filter calls
4
5
  const globalWarnedGroups = new Set();
5
6
  /**
@@ -27,26 +28,85 @@ function matchesWildcard(value, pattern) {
27
28
  }
28
29
  return value === pattern;
29
30
  }
31
+ function collectSourceFiles(value, sourceFiles = new Set(), seen = new Set()) {
32
+ if (typeof value === 'string') {
33
+ return sourceFiles;
34
+ }
35
+ if (Array.isArray(value)) {
36
+ for (const entry of value) {
37
+ collectSourceFiles(entry, sourceFiles, seen);
38
+ }
39
+ return sourceFiles;
40
+ }
41
+ if (!value || typeof value !== 'object') {
42
+ return sourceFiles;
43
+ }
44
+ if (seen.has(value)) {
45
+ return sourceFiles;
46
+ }
47
+ seen.add(value);
48
+ if ('sourceFile' in value && typeof value.sourceFile === 'string') {
49
+ sourceFiles.add(value.sourceFile);
50
+ }
51
+ for (const nestedValue of Object.values(value)) {
52
+ collectSourceFiles(nestedValue, sourceFiles, seen);
53
+ }
54
+ return sourceFiles;
55
+ }
56
+ function repopulateFileSetFromMeta(meta, fallbackFiles, hasEntries) {
57
+ const sourceFiles = collectSourceFiles(meta);
58
+ if (sourceFiles.size > 0) {
59
+ return sourceFiles;
60
+ }
61
+ if (hasEntries) {
62
+ return new Set(fallbackFiles);
63
+ }
64
+ return new Set();
65
+ }
30
66
  /**
31
67
  * Check if metadata matches the given filters
32
68
  */
33
- function matchesFilters(filters, meta, logger, warnedGroups) {
69
+ function matchesFilters(filters, meta, logger, warnedGroups,
70
+ // Set of pikkuFuncIds to keep based on the deploy filter; null when
71
+ // the deploy filter is inactive.
72
+ keptByDeploy) {
34
73
  // If no filters, allow everything
35
74
  if (Object.keys(filters).length === 0)
36
75
  return true;
37
76
  // If all filter arrays are empty, allow everything
38
77
  if ((!filters.names || filters.names.length === 0) &&
39
78
  (!filters.tags || filters.tags.length === 0) &&
40
- (!filters.types || filters.types.length === 0) &&
79
+ (!filters.wires || filters.wires.length === 0) &&
80
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
41
81
  (!filters.directories || filters.directories.length === 0) &&
42
82
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
43
- (!filters.httpMethods || filters.httpMethods.length === 0)) {
83
+ (!filters.httpMethods || filters.httpMethods.length === 0) &&
84
+ (!filters.target || filters.target.length === 0) &&
85
+ (!filters.excludeNames || filters.excludeNames.length === 0) &&
86
+ (!filters.excludeTags || filters.excludeTags.length === 0) &&
87
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
88
+ (!filters.excludeDirectories || filters.excludeDirectories.length === 0) &&
89
+ (!filters.excludeHttpRoutes || filters.excludeHttpRoutes.length === 0) &&
90
+ (!filters.excludeHttpMethods || filters.excludeHttpMethods.length === 0) &&
91
+ (!filters.excludeTarget || filters.excludeTarget.length === 0)) {
44
92
  return true;
45
93
  }
46
- // Check type filter
47
- if (filters.types && filters.types.length > 0) {
48
- if (!filters.types.includes(meta.type)) {
49
- logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`);
94
+ // Deploy-target include filter (computed once per filterInspectorState call).
95
+ if (keptByDeploy && !keptByDeploy.has(meta.name)) {
96
+ logger.debug(`⒡ Filtered by deploy include: ${meta.type}:${meta.name}`);
97
+ return false;
98
+ }
99
+ // Check wire include filter
100
+ if (filters.wires && filters.wires.length > 0) {
101
+ if (!filters.wires.includes(meta.type)) {
102
+ logger.debug(`⒡ Filtered by wire include: ${meta.type}:${meta.name}`);
103
+ return false;
104
+ }
105
+ }
106
+ // Check wire exclude filter
107
+ if (filters.excludeWires && filters.excludeWires.length > 0) {
108
+ if (filters.excludeWires.includes(meta.type)) {
109
+ logger.debug(`⒡ Filtered by wire exclude: ${meta.type}:${meta.name}`);
50
110
  return false;
51
111
  }
52
112
  }
@@ -62,28 +122,45 @@ function matchesFilters(filters, meta, logger, warnedGroups) {
62
122
  return false;
63
123
  }
64
124
  }
65
- // Check tag filter
125
+ // Check tag include filter
66
126
  if (filters.tags && filters.tags.length > 0) {
67
127
  if (!meta.tags || !filters.tags.some((tag) => meta.tags.includes(tag))) {
68
- logger.debug(`⒡ Filtered by tags: ${meta.type}:${meta.name}`);
128
+ logger.debug(`⒡ Filtered by tags include: ${meta.type}:${meta.name}`);
69
129
  return false;
70
130
  }
71
131
  }
72
- // Check name filter (match against both full ID and base name for versioned functions)
132
+ // Check tag exclude filter
133
+ if (filters.excludeTags && filters.excludeTags.length > 0) {
134
+ if (meta.tags &&
135
+ filters.excludeTags.some((tag) => meta.tags.includes(tag))) {
136
+ logger.debug(`⒡ Filtered by tags exclude: ${meta.type}:${meta.name}`);
137
+ return false;
138
+ }
139
+ }
140
+ const { baseName } = parseVersionedId(meta.name);
141
+ const matchesNamePattern = (pattern) => matchesWildcard(meta.name, pattern) ||
142
+ (baseName !== meta.name && matchesWildcard(baseName, pattern));
143
+ // Check name include filter (match against both full ID and base name for versioned functions)
73
144
  if (filters.names && filters.names.length > 0) {
74
- const { baseName } = parseVersionedId(meta.name);
75
- const nameMatches = filters.names.some((pattern) => matchesWildcard(meta.name, pattern) ||
76
- (baseName !== meta.name && matchesWildcard(baseName, pattern)));
145
+ const nameMatches = filters.names.some(matchesNamePattern);
77
146
  if (!nameMatches) {
78
- logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`);
147
+ logger.debug(`⒡ Filtered by name include: ${meta.type}:${meta.name}`);
79
148
  return false;
80
149
  }
81
150
  }
82
- // Check HTTP route filter
151
+ // Check name exclude filter
152
+ if (filters.excludeNames && filters.excludeNames.length > 0) {
153
+ if (filters.excludeNames.some(matchesNamePattern)) {
154
+ logger.debug(`⒡ Filtered by name exclude: ${meta.type}:${meta.name}`);
155
+ return false;
156
+ }
157
+ }
158
+ const matchesRoutePattern = (pattern) => !!meta.httpRoute && matchesWildcard(meta.httpRoute, pattern);
159
+ // Check HTTP route include filter
83
160
  if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
84
- const routeMatches = filters.httpRoutes.some((pattern) => matchesWildcard(meta.httpRoute, pattern));
161
+ const routeMatches = filters.httpRoutes.some(matchesRoutePattern);
85
162
  if (!routeMatches) {
86
- logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`);
163
+ logger.debug(`⒡ Filtered by HTTP route include: ${meta.httpRoute}`);
87
164
  return false;
88
165
  }
89
166
  // If route is part of a wireHTTPRoutes group, check if filter is at group level
@@ -98,13 +175,31 @@ function matchesFilters(filters, meta, logger, warnedGroups) {
98
175
  }
99
176
  }
100
177
  }
101
- // Check HTTP method filter
178
+ // Check HTTP route exclude filter
179
+ if (filters.excludeHttpRoutes &&
180
+ filters.excludeHttpRoutes.length > 0 &&
181
+ meta.httpRoute) {
182
+ if (filters.excludeHttpRoutes.some(matchesRoutePattern)) {
183
+ logger.debug(`⒡ Filtered by HTTP route exclude: ${meta.httpRoute}`);
184
+ return false;
185
+ }
186
+ }
187
+ const normalizedMethod = meta.httpMethod?.toUpperCase();
188
+ // Check HTTP method include filter
102
189
  if (filters.httpMethods &&
103
190
  filters.httpMethods.length > 0 &&
104
- meta.httpMethod) {
105
- const normalizedMethod = meta.httpMethod.toUpperCase();
191
+ normalizedMethod) {
106
192
  if (!filters.httpMethods.includes(normalizedMethod)) {
107
- logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`);
193
+ logger.debug(`⒡ Filtered by HTTP method include: ${meta.httpMethod}`);
194
+ return false;
195
+ }
196
+ }
197
+ // Check HTTP method exclude filter
198
+ if (filters.excludeHttpMethods &&
199
+ filters.excludeHttpMethods.length > 0 &&
200
+ normalizedMethod) {
201
+ if (filters.excludeHttpMethods.includes(normalizedMethod)) {
202
+ logger.debug(`⒡ Filtered by HTTP method exclude: ${meta.httpMethod}`);
108
203
  return false;
109
204
  }
110
205
  }
@@ -133,12 +228,44 @@ export function filterInspectorState(state, filters, logger) {
133
228
  if (Object.keys(filters).length === 0 ||
134
229
  ((!filters.names || filters.names.length === 0) &&
135
230
  (!filters.tags || filters.tags.length === 0) &&
136
- (!filters.types || filters.types.length === 0) &&
231
+ (!filters.wires || filters.wires.length === 0) &&
232
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
137
233
  (!filters.directories || filters.directories.length === 0) &&
138
234
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
139
- (!filters.httpMethods || filters.httpMethods.length === 0))) {
235
+ (!filters.httpMethods || filters.httpMethods.length === 0) &&
236
+ (!filters.target || filters.target.length === 0) &&
237
+ (!filters.excludeNames || filters.excludeNames.length === 0) &&
238
+ (!filters.excludeTags || filters.excludeTags.length === 0) &&
239
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
240
+ (!filters.excludeDirectories ||
241
+ filters.excludeDirectories.length === 0) &&
242
+ (!filters.excludeHttpRoutes || filters.excludeHttpRoutes.length === 0) &&
243
+ (!filters.excludeHttpMethods ||
244
+ filters.excludeHttpMethods.length === 0) &&
245
+ (!filters.excludeTarget || filters.excludeTarget.length === 0))) {
140
246
  return state;
141
247
  }
248
+ // Precompute kept-function set for the deploy filter (if active).
249
+ // resolveDeployTarget throws IncompatibleDeployTargetError when an
250
+ // explicit deploy: 'serverless' clashes with serverlessIncompatible.
251
+ let keptByDeploy = null;
252
+ if ((filters.target && filters.target.length > 0) ||
253
+ (filters.excludeTarget && filters.excludeTarget.length > 0)) {
254
+ const allowed = filters.target ? new Set(filters.target) : null;
255
+ const excluded = filters.excludeTarget
256
+ ? new Set(filters.excludeTarget)
257
+ : null;
258
+ const incompatible = new Set(filters.serverlessIncompatible ?? []);
259
+ keptByDeploy = new Set();
260
+ for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
261
+ const target = resolveDeployTarget(funcMeta, incompatible, funcId);
262
+ if (allowed && !allowed.has(target))
263
+ continue;
264
+ if (excluded && excluded.has(target))
265
+ continue;
266
+ keptByDeploy.add(funcId);
267
+ }
268
+ }
142
269
  // Snapshot the original workflow graph meta before filtering prunes it
143
270
  const originalGraphMeta = {
144
271
  ...(state.workflows?.graphMeta ?? {}),
@@ -230,7 +357,7 @@ export function filterInspectorState(state, filters, logger) {
230
357
  httpRoute: routeMeta.route,
231
358
  httpMethod: routeMeta.method,
232
359
  groupBasePath: routeMeta.groupBasePath,
233
- }, logger, globalWarnedGroups);
360
+ }, logger, globalWarnedGroups, keptByDeploy);
234
361
  if (!matches) {
235
362
  delete routes[route];
236
363
  }
@@ -273,7 +400,7 @@ export function filterInspectorState(state, filters, logger) {
273
400
  type: 'channel',
274
401
  name,
275
402
  tags: channelMeta.tags,
276
- }, logger);
403
+ }, logger, undefined, keptByDeploy);
277
404
  if (!matches) {
278
405
  delete filteredState.channels.meta[name];
279
406
  }
@@ -306,10 +433,7 @@ export function filterInspectorState(state, filters, logger) {
306
433
  extractWireNames(channelMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
307
434
  }
308
435
  }
309
- // Repopulate channels.files if any channels remain
310
- if (Object.keys(filteredState.channels.meta).length > 0) {
311
- filteredState.channels.files = new Set(state.channels.files);
312
- }
436
+ filteredState.channels.files = repopulateFileSetFromMeta(filteredState.channels.meta, state.channels.files, Object.keys(filteredState.channels.meta).length > 0);
313
437
  // Filter triggers
314
438
  for (const name of Object.keys(filteredState.triggers.meta)) {
315
439
  const triggerMeta = filteredState.triggers.meta[name];
@@ -317,7 +441,7 @@ export function filterInspectorState(state, filters, logger) {
317
441
  type: 'trigger',
318
442
  name,
319
443
  tags: triggerMeta.tags,
320
- }, logger);
444
+ }, logger, undefined, keptByDeploy);
321
445
  if (!matches) {
322
446
  delete filteredState.triggers.meta[name];
323
447
  }
@@ -327,10 +451,7 @@ export function filterInspectorState(state, filters, logger) {
327
451
  }
328
452
  }
329
453
  }
330
- // Repopulate triggers.files if any triggers remain
331
- if (Object.keys(filteredState.triggers.meta).length > 0) {
332
- filteredState.triggers.files = new Set(state.triggers.files);
333
- }
454
+ filteredState.triggers.files = repopulateFileSetFromMeta(filteredState.triggers.meta, state.triggers.files, Object.keys(filteredState.triggers.meta).length > 0);
334
455
  // Filter scheduled tasks
335
456
  for (const name of Object.keys(filteredState.scheduledTasks.meta)) {
336
457
  const taskMeta = filteredState.scheduledTasks.meta[name];
@@ -338,7 +459,7 @@ export function filterInspectorState(state, filters, logger) {
338
459
  type: 'scheduler',
339
460
  name,
340
461
  tags: taskMeta.tags,
341
- }, logger);
462
+ }, logger, undefined, keptByDeploy);
342
463
  if (!matches) {
343
464
  delete filteredState.scheduledTasks.meta[name];
344
465
  }
@@ -349,10 +470,7 @@ export function filterInspectorState(state, filters, logger) {
349
470
  extractWireNames(taskMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
350
471
  }
351
472
  }
352
- // Repopulate scheduledTasks.files if any tasks remain
353
- if (Object.keys(filteredState.scheduledTasks.meta).length > 0) {
354
- filteredState.scheduledTasks.files = new Set(state.scheduledTasks.files);
355
- }
473
+ filteredState.scheduledTasks.files = repopulateFileSetFromMeta(filteredState.scheduledTasks.meta, state.scheduledTasks.files, Object.keys(filteredState.scheduledTasks.meta).length > 0);
356
474
  // Filter queue workers
357
475
  for (const name of Object.keys(filteredState.queueWorkers.meta)) {
358
476
  const workerMeta = filteredState.queueWorkers.meta[name];
@@ -360,7 +478,7 @@ export function filterInspectorState(state, filters, logger) {
360
478
  type: 'queue',
361
479
  name,
362
480
  tags: workerMeta.tags,
363
- }, logger);
481
+ }, logger, undefined, keptByDeploy);
364
482
  if (!matches) {
365
483
  delete filteredState.queueWorkers.meta[name];
366
484
  }
@@ -375,10 +493,7 @@ export function filterInspectorState(state, filters, logger) {
375
493
  extractWireNames(workerMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
376
494
  }
377
495
  }
378
- // Repopulate queueWorkers.files if any workers remain
379
- if (Object.keys(filteredState.queueWorkers.meta).length > 0) {
380
- filteredState.queueWorkers.files = new Set(state.queueWorkers.files);
381
- }
496
+ filteredState.queueWorkers.files = repopulateFileSetFromMeta(filteredState.queueWorkers.meta, state.queueWorkers.files, Object.keys(filteredState.queueWorkers.meta).length > 0);
382
497
  // Filter MCP tools
383
498
  for (const name of Object.keys(filteredState.mcpEndpoints.toolsMeta)) {
384
499
  const toolMeta = filteredState.mcpEndpoints.toolsMeta[name];
@@ -386,7 +501,7 @@ export function filterInspectorState(state, filters, logger) {
386
501
  type: 'mcp',
387
502
  name,
388
503
  tags: toolMeta.tags,
389
- }, logger);
504
+ }, logger, undefined, keptByDeploy);
390
505
  if (!matches) {
391
506
  delete filteredState.mcpEndpoints.toolsMeta[name];
392
507
  }
@@ -405,7 +520,7 @@ export function filterInspectorState(state, filters, logger) {
405
520
  type: 'mcp',
406
521
  name,
407
522
  tags: resourceMeta.tags,
408
- }, logger);
523
+ }, logger, undefined, keptByDeploy);
409
524
  if (!matches) {
410
525
  delete filteredState.mcpEndpoints.resourcesMeta[name];
411
526
  }
@@ -424,7 +539,7 @@ export function filterInspectorState(state, filters, logger) {
424
539
  type: 'mcp',
425
540
  name,
426
541
  tags: promptMeta.tags,
427
- }, logger);
542
+ }, logger, undefined, keptByDeploy);
428
543
  if (!matches) {
429
544
  delete filteredState.mcpEndpoints.promptsMeta[name];
430
545
  }
@@ -436,13 +551,15 @@ export function filterInspectorState(state, filters, logger) {
436
551
  extractWireNames(promptMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
437
552
  }
438
553
  }
439
- // Repopulate mcpEndpoints.files if any MCP endpoints remain
554
+ // Repopulate mcpEndpoints.files from surviving endpoint metadata
440
555
  const hasMcpEndpoints = Object.keys(filteredState.mcpEndpoints.toolsMeta).length > 0 ||
441
556
  Object.keys(filteredState.mcpEndpoints.resourcesMeta).length > 0 ||
442
557
  Object.keys(filteredState.mcpEndpoints.promptsMeta).length > 0;
443
- if (hasMcpEndpoints) {
444
- filteredState.mcpEndpoints.files = new Set(state.mcpEndpoints.files);
445
- }
558
+ filteredState.mcpEndpoints.files = repopulateFileSetFromMeta({
559
+ toolsMeta: filteredState.mcpEndpoints.toolsMeta,
560
+ resourcesMeta: filteredState.mcpEndpoints.resourcesMeta,
561
+ promptsMeta: filteredState.mcpEndpoints.promptsMeta,
562
+ }, state.mcpEndpoints.files, hasMcpEndpoints);
446
563
  // Filter AI agents
447
564
  for (const name of Object.keys(filteredState.agents.agentsMeta)) {
448
565
  const agentMeta = filteredState.agents.agentsMeta[name];
@@ -450,7 +567,7 @@ export function filterInspectorState(state, filters, logger) {
450
567
  type: 'agent',
451
568
  name,
452
569
  tags: agentMeta.tags,
453
- }, logger);
570
+ }, logger, undefined, keptByDeploy);
454
571
  if (!matches) {
455
572
  delete filteredState.agents.agentsMeta[name];
456
573
  }
@@ -476,7 +593,7 @@ export function filterInspectorState(state, filters, logger) {
476
593
  type: 'cli',
477
594
  name: commandName,
478
595
  tags: commandMeta.tags,
479
- }, logger);
596
+ }, logger, undefined, keptByDeploy);
480
597
  if (!matches) {
481
598
  delete programMeta.commands[commandName];
482
599
  }
@@ -502,12 +619,10 @@ export function filterInspectorState(state, filters, logger) {
502
619
  delete filteredState.cli.meta.renderers[rendererName];
503
620
  }
504
621
  }
505
- // Repopulate cli.files if any CLI programs or referenced renderers remain
622
+ // Repopulate cli.files from surviving program/renderer metadata
506
623
  const hasCliPrograms = Object.keys(filteredState.cli.meta.programs).length > 0;
507
624
  const hasCliRenderers = Object.keys(filteredState.cli.meta.renderers || {}).length > 0;
508
- if (hasCliPrograms || hasCliRenderers) {
509
- filteredState.cli.files = new Set(state.cli.files);
510
- }
625
+ filteredState.cli.files = repopulateFileSetFromMeta(filteredState.cli.meta, state.cli.files, hasCliPrograms || hasCliRenderers);
511
626
  // Direct function filtering: functions that match the names/tags/directories
512
627
  // filters should be included even if no wiring (HTTP, scheduler, etc.) references them.
513
628
  // This ensures standalone RPC-callable functions survive filtering.
@@ -527,7 +642,7 @@ export function filterInspectorState(state, filters, logger) {
527
642
  name: funcId,
528
643
  tags: funcMeta.tags,
529
644
  filePath,
530
- }, logger);
645
+ }, logger, undefined, keptByDeploy);
531
646
  if (matches) {
532
647
  filteredState.serviceAggregation.usedFunctions.add(funcId);
533
648
  }
@@ -584,12 +699,18 @@ export function filterInspectorState(state, filters, logger) {
584
699
  delete filteredState.channels.meta[name];
585
700
  }
586
701
  }
587
- // Prune workflow graphs whose function was filtered out
702
+ // Prune workflow graphs whose function was filtered out — UNLESS the
703
+ // workflow's own name is in the filter (covers pikkuWorkflowGraph,
704
+ // which doesn't register a function meta, so its pikkuFuncId never
705
+ // ends up in usedFunctions even when callers explicitly want it).
706
+ const filterNamesSet = new Set(filters.names ?? []);
588
707
  const workflowKeys = new Set([
589
708
  ...Object.keys(filteredState.workflows.graphMeta),
590
709
  ...Object.keys(filteredState.workflows.meta),
591
710
  ]);
592
711
  for (const name of workflowKeys) {
712
+ if (filterNamesSet.has(name))
713
+ continue;
593
714
  const graphMeta = filteredState.workflows.graphMeta[name];
594
715
  const workflowMeta = filteredState.workflows.meta[name];
595
716
  // Check both graphMeta.pikkuFuncId and meta.pikkuFuncId
@@ -661,6 +782,13 @@ export function filterInspectorState(state, filters, logger) {
661
782
  }
662
783
  }
663
784
  }
785
+ // workflowService internally calls queueService (via queueStepWorker /
786
+ // queueOrchestrator). Any unit that needs workflowService also needs
787
+ // queueService for the transitive enqueue calls — covers
788
+ // workflow-starter, graph-starter, workflow-runner, etc.
789
+ if (filteredState.serviceAggregation.requiredServices.has('workflowService')) {
790
+ filteredState.serviceAggregation.requiredServices.add('queueService');
791
+ }
664
792
  // Recalculate requiredServices based on filtered functions/middleware/permissions
665
793
  // Need to cast to InspectorState temporarily for aggregateRequiredServices
666
794
  const stateForAggregation = filteredState;
@@ -40,16 +40,24 @@ export const matchesFilters = (filters, params, meta, logger) => {
40
40
  // If all filter arrays are empty, allow everything
41
41
  if ((!filters.names || filters.names.length === 0) &&
42
42
  (!filters.tags || filters.tags.length === 0) &&
43
- (!filters.types || filters.types.length === 0) &&
43
+ (!filters.wires || filters.wires.length === 0) &&
44
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
44
45
  (!filters.directories || filters.directories.length === 0) &&
45
46
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
46
47
  (!filters.httpMethods || filters.httpMethods.length === 0)) {
47
48
  return true;
48
49
  }
49
- // Check type filter
50
- if (filters.types && filters.types.length > 0) {
51
- if (!filters.types.includes(meta.type)) {
52
- logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`);
50
+ // Check wire include filter
51
+ if (filters.wires && filters.wires.length > 0) {
52
+ if (!filters.wires.includes(meta.type)) {
53
+ logger.debug(`⒡ Filtered by wire include: ${meta.type}:${meta.name}`);
54
+ return false;
55
+ }
56
+ }
57
+ // Check wire exclude filter
58
+ if (filters.excludeWires && filters.excludeWires.length > 0) {
59
+ if (filters.excludeWires.includes(meta.type)) {
60
+ logger.debug(`⒡ Filtered by wire exclude: ${meta.type}:${meta.name}`);
53
61
  return false;
54
62
  }
55
63
  }
@@ -4,6 +4,16 @@ import { ErrorCode } from '../error-codes.js';
4
4
  * Extracts an array of strings from an object property.
5
5
  */
6
6
  export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
7
+ /**
8
+ * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
9
+ * STATICALLY from source — a const or variable reference is keyed by its
10
+ * identifier text, not its runtime value, so the wiring is silently skipped at
11
+ * runtime (`metadata not found`). If the named property exists but is not an
12
+ * inline literal, raise a fatal diagnostic so the build fails instead.
13
+ */
14
+ export declare const assertStringLiteralProperty: (obj: ts.ObjectLiteralExpression, propertyName: string, wiringType: string, logger?: {
15
+ critical: (code: ErrorCode, message: string) => void;
16
+ }) => void;
7
17
  export declare const getPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string | string[] | number | null | boolean;
8
18
  /**
9
19
  * Extracts common wire metadata (title, tags, summary, description, errors) directly from an object
@@ -17,6 +17,35 @@ export const getArrayPropertyValue = (obj, propertyName) => {
17
17
  }
18
18
  return null;
19
19
  };
20
+ /**
21
+ * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
22
+ * STATICALLY from source — a const or variable reference is keyed by its
23
+ * identifier text, not its runtime value, so the wiring is silently skipped at
24
+ * runtime (`metadata not found`). If the named property exists but is not an
25
+ * inline literal, raise a fatal diagnostic so the build fails instead.
26
+ */
27
+ export const assertStringLiteralProperty = (obj, propertyName, wiringType, logger) => {
28
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
29
+ ts.isIdentifier(p.name) &&
30
+ p.name.text === propertyName);
31
+ if (!property || !ts.isPropertyAssignment(property)) {
32
+ return;
33
+ }
34
+ const init = property.initializer;
35
+ const isStaticLiteral = ts.isStringLiteral(init) ||
36
+ ts.isNoSubstitutionTemplateLiteral(init) ||
37
+ ts.isNumericLiteral(init);
38
+ if (isStaticLiteral) {
39
+ return;
40
+ }
41
+ const errorMsg = `${wiringType} has a non-literal '${propertyName}': \`${init.getText()}\`. Wiring identity fields must be inline string literals — the inspector reads them statically from source, so a const or variable reference is keyed by its identifier text and the wiring is silently skipped at runtime. Inline the literal instead, e.g. ${propertyName}: 'my-wiring-name'.`;
42
+ if (logger) {
43
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, errorMsg);
44
+ }
45
+ else {
46
+ console.error(errorMsg);
47
+ }
48
+ };
20
49
  export const getPropertyValue = (obj, propertyName) => {
21
50
  const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
22
51
  ts.isIdentifier(p.name) &&
@@ -67,6 +96,7 @@ export const getPropertyValue = (obj, propertyName) => {
67
96
  */
68
97
  export const getCommonWireMetaData = (obj, wiringType, wiringName, logger) => {
69
98
  const metadata = {};
99
+ assertStringLiteralProperty(obj, 'name', wiringType, logger);
70
100
  obj.properties.forEach((prop) => {
71
101
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
72
102
  const propName = prop.name.text;
@@ -1,4 +1,4 @@
1
- import type { InspectorState, InspectorLogger, InspectorOptions, InspectorModelConfig } from '../types.js';
1
+ import type { InspectorState, InspectorLogger, InspectorOptions } from '../types.js';
2
2
  import type { MiddlewareMetadata, PermissionMetadata } from '@pikku/core';
3
3
  /**
4
4
  * Helper to extract wire-level middleware/permission names from metadata.
@@ -21,8 +21,7 @@ export declare function computeResolvedIOTypes(state: InspectorState): void;
21
21
  export declare function computeMiddlewareGroupsMeta(state: InspectorState): void;
22
22
  export declare function computePermissionsGroupsMeta(state: InspectorState): void;
23
23
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
24
- export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
25
- export declare function validateAgentOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
24
+ export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
26
25
  /**
27
26
  * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
28
27
  * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
@@ -425,35 +425,15 @@ export function computeRequiredSchemas(state, options) {
425
425
  ...Array.from(schemaLookup.keys()),
426
426
  ]);
427
427
  }
428
- export function validateAgentModels(logger, state, modelConfig) {
429
- const aliases = modelConfig?.models ?? {};
428
+ export function validateAgentModels(logger, state) {
430
429
  for (const [, meta] of Object.entries(state.agents.agentsMeta)) {
431
430
  const model = meta.model;
432
431
  if (!model) {
433
432
  logger.critical(ErrorCode.MISSING_MODEL, `AI agent '${meta.name}' is missing the 'model' property.`);
434
433
  continue;
435
434
  }
436
- if (model.includes('/'))
437
- continue;
438
- if (!aliases[model]) {
439
- const available = Object.keys(aliases);
440
- logger.critical(ErrorCode.INVALID_MODEL, `AI agent '${meta.name}' uses model alias '${model}' which is not defined in pikku.config.json models. ` +
441
- `Available aliases: ${available.join(', ') || 'none'}`);
442
- }
443
- }
444
- }
445
- export function validateAgentOverrides(logger, state, modelConfig) {
446
- const overrides = modelConfig?.agentOverrides ?? {};
447
- const aliases = modelConfig?.models ?? {};
448
- const agentNames = new Set(Object.values(state.agents.agentsMeta).map((m) => m.name));
449
- for (const [agentName, override] of Object.entries(overrides)) {
450
- if (!agentNames.has(agentName)) {
451
- logger.warn(`agentOverrides references unknown agent '${agentName}'`);
452
- }
453
- if (override.model &&
454
- !override.model.includes('/') &&
455
- !aliases[override.model]) {
456
- logger.critical(ErrorCode.INVALID_MODEL, `agentOverrides['${agentName}'].model uses alias '${override.model}' which is not defined in models.`);
435
+ if (!model.includes('/')) {
436
+ logger.critical(ErrorCode.INVALID_MODEL, `AI agent '${meta.name}' uses model '${model}', which must be provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4').`);
457
437
  }
458
438
  }
459
439
  }
@@ -2,11 +2,10 @@ import * as ts from 'typescript';
2
2
  /**
3
3
  * Resolve the addon package name from an imported identifier.
4
4
  * Checks if the identifier's import module specifier matches any
5
- * configured addon package.
6
- *
7
- * This is a general utility any wire handler that processes a `func`
8
- * property can use it to detect when the function comes from an
9
- * addon package.
5
+ * configured addon package — and if the import is relative (because
6
+ * the identifier resolves to a source file inside the addon package
7
+ * itself), walks up to the nearest package.json to obtain the real
8
+ * package name.
10
9
  */
11
10
  export declare const resolveAddonName: (identifier: ts.Identifier, checker: ts.TypeChecker, wireAddonDeclarations?: Map<string, {
12
11
  package: string;