@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,6 @@
1
+ import { InspectorState } from '../types.js';
2
+ /**
3
+ * Ensures that function metadata exists for a given pikkuFuncName.
4
+ * Creates stub metadata if it doesn't exist (useful for inline functions).
5
+ */
6
+ export declare function ensureFunctionMetadata(state: InspectorState, pikkuFuncName: string, fallbackName?: string): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ensures that function metadata exists for a given pikkuFuncName.
3
+ * Creates stub metadata if it doesn't exist (useful for inline functions).
4
+ */
5
+ export function ensureFunctionMetadata(state, pikkuFuncName, fallbackName) {
6
+ if (!state.functions.meta[pikkuFuncName]) {
7
+ state.functions.meta[pikkuFuncName] = {
8
+ pikkuFuncName,
9
+ name: fallbackName || pikkuFuncName,
10
+ services: { optimized: false, services: [] },
11
+ inputSchemaName: null,
12
+ outputSchemaName: null,
13
+ inputs: [],
14
+ outputs: [],
15
+ middleware: undefined,
16
+ };
17
+ }
18
+ }
@@ -12,7 +12,7 @@ export type ExtractedFunctionName = {
12
12
  * but if it's an Identifier pointing to a function, resolve it back
13
13
  * to the function's declaration (so you get the true source location).
14
14
  */
15
- export declare function makeDeterministicAnonName(start: ts.Node, checker: ts.TypeChecker): string;
15
+ export declare function makeDeterministicAnonName(start: ts.Node, checker: ts.TypeChecker, rootDir: string): string;
16
16
  /**
17
17
  * Updated function to extract and prioritize function names correctly
18
18
  * This function follows the priority:
@@ -20,7 +20,7 @@ export declare function makeDeterministicAnonName(start: ts.Node, checker: ts.Ty
20
20
  * 2. Exported name
21
21
  * 3. Fallback to deterministic name
22
22
  */
23
- export declare function extractFunctionName(callExpr: ts.Node, checker: ts.TypeChecker): ExtractedFunctionName;
23
+ export declare function extractFunctionName(callExpr: ts.Node, checker: ts.TypeChecker, rootDir: string): ExtractedFunctionName;
24
24
  /**
25
25
  * Helper function to populate the 'name' field based on priority
26
26
  */
@@ -1,10 +1,11 @@
1
1
  import * as ts from 'typescript';
2
+ import { toRelativePath } from './find-root-dir.js';
2
3
  /**
3
4
  * Generate a deterministic "anonymous" name for any expression node,
4
5
  * but if it's an Identifier pointing to a function, resolve it back
5
6
  * to the function's declaration (so you get the true source location).
6
7
  */
7
- export function makeDeterministicAnonName(start, checker) {
8
+ export function makeDeterministicAnonName(start, checker, rootDir) {
8
9
  let node = start;
9
10
  let target = start;
10
11
  // Handle the case where we're starting with an identifier directly
@@ -27,7 +28,8 @@ export function makeDeterministicAnonName(start, checker) {
27
28
  target = decl.initializer;
28
29
  // Return early - we found the function directly
29
30
  const sf = target.getSourceFile();
30
- const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_');
31
+ const relativePath = toRelativePath(sf.fileName, rootDir);
32
+ const file = relativePath.replace(/[^A-Za-z0-9_]/g, '_');
31
33
  const { line, character } = ts.getLineAndCharacterOfPosition(sf, target.getStart());
32
34
  return `pikkuFn_${file}_L${line + 1}C${character + 1}`;
33
35
  }
@@ -62,7 +64,8 @@ export function makeDeterministicAnonName(start, checker) {
62
64
  target = decl.initializer;
63
65
  // Return early - we found the function directly
64
66
  const sf = target.getSourceFile();
65
- const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_');
67
+ const relativePath = toRelativePath(sf.fileName, rootDir);
68
+ const file = relativePath.replace(/[^A-Za-z0-9_]/g, '_');
66
69
  const { line, character } = ts.getLineAndCharacterOfPosition(sf, target.getStart());
67
70
  return `pikkuFn_${file}_L${line + 1}C${character + 1}`;
68
71
  }
@@ -72,7 +75,8 @@ export function makeDeterministicAnonName(start, checker) {
72
75
  target = decl;
73
76
  // Return early
74
77
  const sf = target.getSourceFile();
75
- const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_');
78
+ const relativePath = toRelativePath(sf.fileName, rootDir);
79
+ const file = relativePath.replace(/[^A-Za-z0-9_]/g, '_');
76
80
  const { line, character } = ts.getLineAndCharacterOfPosition(sf, target.getStart());
77
81
  return `pikkuFn_${file}_L${line + 1}C${character + 1}`;
78
82
  }
@@ -182,7 +186,8 @@ export function makeDeterministicAnonName(start, checker) {
182
186
  break;
183
187
  }
184
188
  const sf = target.getSourceFile();
185
- const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_');
189
+ const relativePath = toRelativePath(sf.fileName, rootDir);
190
+ const file = relativePath.replace(/[^A-Za-z0-9_]/g, '_');
186
191
  const { line, character } = ts.getLineAndCharacterOfPosition(sf, target.getStart());
187
192
  return `pikkuFn_${file}_L${line + 1}C${character + 1}`;
188
193
  }
@@ -193,7 +198,7 @@ export function makeDeterministicAnonName(start, checker) {
193
198
  * 2. Exported name
194
199
  * 3. Fallback to deterministic name
195
200
  */
196
- export function extractFunctionName(callExpr, checker) {
201
+ export function extractFunctionName(callExpr, checker, rootDir) {
197
202
  const parent = callExpr.parent;
198
203
  // Initialize the result
199
204
  const result = {
@@ -230,7 +235,7 @@ export function extractFunctionName(callExpr, checker) {
230
235
  (ts.isArrowFunction(firstArg) ||
231
236
  ts.isFunctionExpression(firstArg))) {
232
237
  // Use the function directly for position calculation
233
- result.pikkuFuncName = makeDeterministicAnonName(firstArg, checker);
238
+ result.pikkuFuncName = makeDeterministicAnonName(firstArg, checker, rootDir);
234
239
  // Continue with name extraction
235
240
  if (ts.isIdentifier(parent.name)) {
236
241
  result.propertyName = parent.name.text;
@@ -532,7 +537,7 @@ export function extractFunctionName(callExpr, checker) {
532
537
  // Generate the deterministic function name based on the original call expression
533
538
  // (the config), not the resolved inner function. This ensures the metadata key
534
539
  // matches what will be looked up at runtime when referencing the config object.
535
- result.pikkuFuncName = makeDeterministicAnonName(originalCallExpr, checker);
540
+ result.pikkuFuncName = makeDeterministicAnonName(originalCallExpr, checker, rootDir);
536
541
  // Continue with regular name extraction for remaining cases
537
542
  // 1) const foo = pikkuFunc(...)
538
543
  if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
@@ -0,0 +1,6 @@
1
+ import { InspectorState, InspectorFilters, InspectorLogger } from '../types.js';
2
+ /**
3
+ * Filters inspector state based on provided filters
4
+ * This is applied post-inspection to support the inspect-once, filter-many pattern
5
+ */
6
+ export declare function filterInspectorState(state: InspectorState | Omit<InspectorState, 'typesLookup'>, filters: InspectorFilters, logger: InspectorLogger): typeof state;
@@ -0,0 +1,382 @@
1
+ import { aggregateRequiredServices } from './post-process.js';
2
+ /**
3
+ * Match a value against a pattern with wildcard support
4
+ * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
5
+ */
6
+ function matchesWildcard(value, pattern) {
7
+ if (pattern === '*')
8
+ return true;
9
+ const startsWithWildcard = pattern.startsWith('*');
10
+ const endsWithWildcard = pattern.endsWith('*');
11
+ if (startsWithWildcard && endsWithWildcard) {
12
+ const middle = pattern.slice(1, -1);
13
+ if (middle === '')
14
+ return true;
15
+ return value.includes(middle);
16
+ }
17
+ else if (startsWithWildcard) {
18
+ const suffix = pattern.slice(1);
19
+ return value.endsWith(suffix) && value.length > suffix.length;
20
+ }
21
+ else if (endsWithWildcard) {
22
+ const prefix = pattern.slice(0, -1);
23
+ return value.startsWith(prefix) && value.length > prefix.length;
24
+ }
25
+ return value === pattern;
26
+ }
27
+ /**
28
+ * Check if metadata matches the given filters
29
+ */
30
+ function matchesFilters(filters, meta, logger) {
31
+ // If no filters, allow everything
32
+ if (Object.keys(filters).length === 0)
33
+ return true;
34
+ // If all filter arrays are empty, allow everything
35
+ if ((!filters.names || filters.names.length === 0) &&
36
+ (!filters.tags || filters.tags.length === 0) &&
37
+ (!filters.types || filters.types.length === 0) &&
38
+ (!filters.directories || filters.directories.length === 0) &&
39
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
40
+ (!filters.httpMethods || filters.httpMethods.length === 0)) {
41
+ return true;
42
+ }
43
+ // Check type filter
44
+ if (filters.types && filters.types.length > 0) {
45
+ if (!filters.types.includes(meta.type)) {
46
+ logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`);
47
+ return false;
48
+ }
49
+ }
50
+ // Check directory filter
51
+ if (filters.directories && filters.directories.length > 0 && meta.filePath) {
52
+ const matchesDirectory = filters.directories.some((dir) => {
53
+ const normalizedFilePath = meta.filePath.replace(/\\/g, '/');
54
+ const normalizedDir = dir.replace(/\\/g, '/');
55
+ return normalizedFilePath.includes(normalizedDir);
56
+ });
57
+ if (!matchesDirectory) {
58
+ logger.debug(`⒡ Filtered by directory: ${meta.type}:${meta.name}`);
59
+ return false;
60
+ }
61
+ }
62
+ // Check tag filter
63
+ if (filters.tags && filters.tags.length > 0) {
64
+ if (!meta.tags || !filters.tags.some((tag) => meta.tags.includes(tag))) {
65
+ logger.debug(`⒡ Filtered by tags: ${meta.type}:${meta.name}`);
66
+ return false;
67
+ }
68
+ }
69
+ // Check name filter
70
+ if (filters.names && filters.names.length > 0) {
71
+ const nameMatches = filters.names.some((pattern) => matchesWildcard(meta.name, pattern));
72
+ if (!nameMatches) {
73
+ logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`);
74
+ return false;
75
+ }
76
+ }
77
+ // Check HTTP route filter
78
+ if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
79
+ const routeMatches = filters.httpRoutes.some((pattern) => matchesWildcard(meta.httpRoute, pattern));
80
+ if (!routeMatches) {
81
+ logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`);
82
+ return false;
83
+ }
84
+ }
85
+ // Check HTTP method filter
86
+ if (filters.httpMethods &&
87
+ filters.httpMethods.length > 0 &&
88
+ meta.httpMethod) {
89
+ const normalizedMethod = meta.httpMethod.toUpperCase();
90
+ if (!filters.httpMethods.includes(normalizedMethod)) {
91
+ logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`);
92
+ return false;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Extract wire names from middleware/permissions metadata
99
+ */
100
+ function extractWireNames(obj) {
101
+ if (!obj)
102
+ return [];
103
+ const names = [];
104
+ for (const key of Object.keys(obj)) {
105
+ if (obj[key] && typeof obj[key] === 'object' && 'name' in obj[key]) {
106
+ names.push(obj[key].name);
107
+ }
108
+ }
109
+ return names;
110
+ }
111
+ /**
112
+ * Filters inspector state based on provided filters
113
+ * This is applied post-inspection to support the inspect-once, filter-many pattern
114
+ */
115
+ export function filterInspectorState(state, filters, logger) {
116
+ // If no filters, return original state
117
+ if (Object.keys(filters).length === 0 ||
118
+ ((!filters.names || filters.names.length === 0) &&
119
+ (!filters.tags || filters.tags.length === 0) &&
120
+ (!filters.types || filters.types.length === 0) &&
121
+ (!filters.directories || filters.directories.length === 0) &&
122
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
123
+ (!filters.httpMethods || filters.httpMethods.length === 0))) {
124
+ return state;
125
+ }
126
+ // Create a shallow copy with new Maps/Sets to avoid mutating the original
127
+ const filteredState = {
128
+ ...state,
129
+ serviceAggregation: {
130
+ ...state.serviceAggregation,
131
+ requiredServices: new Set(), // Reset requiredServices - will be recalculated
132
+ usedFunctions: new Set(),
133
+ usedMiddleware: new Set(),
134
+ usedPermissions: new Set(),
135
+ },
136
+ http: {
137
+ ...state.http,
138
+ meta: JSON.parse(JSON.stringify(state.http.meta)), // Deep clone metadata
139
+ files: new Set(), // Will be repopulated with filtered files
140
+ },
141
+ channels: {
142
+ ...state.channels,
143
+ meta: JSON.parse(JSON.stringify(state.channels.meta)),
144
+ files: new Set(), // Will be repopulated with filtered files
145
+ },
146
+ scheduledTasks: {
147
+ ...state.scheduledTasks,
148
+ meta: JSON.parse(JSON.stringify(state.scheduledTasks.meta)),
149
+ files: new Set(), // Will be repopulated with filtered files
150
+ },
151
+ queueWorkers: {
152
+ ...state.queueWorkers,
153
+ meta: JSON.parse(JSON.stringify(state.queueWorkers.meta)),
154
+ files: new Set(), // Will be repopulated with filtered files
155
+ },
156
+ mcpEndpoints: {
157
+ ...state.mcpEndpoints,
158
+ toolsMeta: JSON.parse(JSON.stringify(state.mcpEndpoints.toolsMeta)),
159
+ resourcesMeta: JSON.parse(JSON.stringify(state.mcpEndpoints.resourcesMeta)),
160
+ promptsMeta: JSON.parse(JSON.stringify(state.mcpEndpoints.promptsMeta)),
161
+ files: new Set(), // Will be repopulated with filtered files
162
+ },
163
+ cli: {
164
+ ...state.cli,
165
+ meta: JSON.parse(JSON.stringify(state.cli.meta)),
166
+ files: new Set(), // Will be repopulated with filtered files
167
+ },
168
+ };
169
+ // Filter HTTP routes
170
+ for (const method of Object.keys(filteredState.http.meta)) {
171
+ const routes = filteredState.http.meta[method];
172
+ for (const route of Object.keys(routes)) {
173
+ const routeMeta = routes[route];
174
+ // Get function file path for directory filtering
175
+ const funcFile = filteredState.functions.files.get(routeMeta.pikkuFuncName);
176
+ const filePath = funcFile?.path;
177
+ const matches = matchesFilters(filters, {
178
+ type: 'http',
179
+ name: routeMeta.pikkuFuncName, // Use function name, not route
180
+ tags: routeMeta.tags,
181
+ filePath,
182
+ httpRoute: routeMeta.route,
183
+ httpMethod: routeMeta.method,
184
+ }, logger);
185
+ if (!matches) {
186
+ delete routes[route];
187
+ }
188
+ else {
189
+ // Track used functions/middleware/permissions
190
+ if (routeMeta.pikkuFuncName) {
191
+ filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncName);
192
+ }
193
+ extractWireNames(routeMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
194
+ extractWireNames(routeMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
195
+ }
196
+ }
197
+ }
198
+ // Repopulate http.files if any routes remain
199
+ const hasHttpRoutes = Object.values(filteredState.http.meta).some((routes) => Object.keys(routes).length > 0);
200
+ if (hasHttpRoutes) {
201
+ filteredState.http.files = new Set(state.http.files);
202
+ }
203
+ // Filter channels
204
+ for (const name of Object.keys(filteredState.channels.meta)) {
205
+ const channelMeta = filteredState.channels.meta[name];
206
+ const matches = matchesFilters(filters, {
207
+ type: 'channel',
208
+ name,
209
+ tags: channelMeta.tags,
210
+ }, logger);
211
+ if (!matches) {
212
+ delete filteredState.channels.meta[name];
213
+ }
214
+ else {
215
+ if (channelMeta.pikkuFuncName) {
216
+ filteredState.serviceAggregation.usedFunctions.add(channelMeta.pikkuFuncName);
217
+ }
218
+ extractWireNames(channelMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
219
+ extractWireNames(channelMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
220
+ }
221
+ }
222
+ // Repopulate channels.files if any channels remain
223
+ if (Object.keys(filteredState.channels.meta).length > 0) {
224
+ filteredState.channels.files = new Set(state.channels.files);
225
+ }
226
+ // Filter scheduled tasks
227
+ for (const name of Object.keys(filteredState.scheduledTasks.meta)) {
228
+ const taskMeta = filteredState.scheduledTasks.meta[name];
229
+ const matches = matchesFilters(filters, {
230
+ type: 'scheduler',
231
+ name,
232
+ tags: taskMeta.tags,
233
+ }, logger);
234
+ if (!matches) {
235
+ delete filteredState.scheduledTasks.meta[name];
236
+ }
237
+ else {
238
+ if (taskMeta.pikkuFuncName) {
239
+ filteredState.serviceAggregation.usedFunctions.add(taskMeta.pikkuFuncName);
240
+ }
241
+ extractWireNames(taskMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
242
+ }
243
+ }
244
+ // Repopulate scheduledTasks.files if any tasks remain
245
+ if (Object.keys(filteredState.scheduledTasks.meta).length > 0) {
246
+ filteredState.scheduledTasks.files = new Set(state.scheduledTasks.files);
247
+ }
248
+ // Filter queue workers
249
+ for (const name of Object.keys(filteredState.queueWorkers.meta)) {
250
+ const workerMeta = filteredState.queueWorkers.meta[name];
251
+ const matches = matchesFilters(filters, {
252
+ type: 'queue',
253
+ name,
254
+ tags: workerMeta.tags,
255
+ }, logger);
256
+ if (!matches) {
257
+ delete filteredState.queueWorkers.meta[name];
258
+ }
259
+ else {
260
+ if (workerMeta.pikkuFuncName) {
261
+ filteredState.serviceAggregation.usedFunctions.add(workerMeta.pikkuFuncName);
262
+ }
263
+ extractWireNames(workerMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
264
+ }
265
+ }
266
+ // Repopulate queueWorkers.files if any workers remain
267
+ if (Object.keys(filteredState.queueWorkers.meta).length > 0) {
268
+ filteredState.queueWorkers.files = new Set(state.queueWorkers.files);
269
+ }
270
+ // Filter MCP tools
271
+ for (const name of Object.keys(filteredState.mcpEndpoints.toolsMeta)) {
272
+ const toolMeta = filteredState.mcpEndpoints.toolsMeta[name];
273
+ const matches = matchesFilters(filters, {
274
+ type: 'mcp',
275
+ name,
276
+ tags: toolMeta.tags,
277
+ }, logger);
278
+ if (!matches) {
279
+ delete filteredState.mcpEndpoints.toolsMeta[name];
280
+ }
281
+ else {
282
+ if (toolMeta.pikkuFuncName) {
283
+ filteredState.serviceAggregation.usedFunctions.add(toolMeta.pikkuFuncName);
284
+ }
285
+ extractWireNames(toolMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
286
+ extractWireNames(toolMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
287
+ }
288
+ }
289
+ // Filter MCP resources
290
+ for (const name of Object.keys(filteredState.mcpEndpoints.resourcesMeta)) {
291
+ const resourceMeta = filteredState.mcpEndpoints.resourcesMeta[name];
292
+ const matches = matchesFilters(filters, {
293
+ type: 'mcp',
294
+ name,
295
+ tags: resourceMeta.tags,
296
+ }, logger);
297
+ if (!matches) {
298
+ delete filteredState.mcpEndpoints.resourcesMeta[name];
299
+ }
300
+ else {
301
+ if (resourceMeta.pikkuFuncName) {
302
+ filteredState.serviceAggregation.usedFunctions.add(resourceMeta.pikkuFuncName);
303
+ }
304
+ extractWireNames(resourceMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
305
+ extractWireNames(resourceMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
306
+ }
307
+ }
308
+ // Filter MCP prompts
309
+ for (const name of Object.keys(filteredState.mcpEndpoints.promptsMeta)) {
310
+ const promptMeta = filteredState.mcpEndpoints.promptsMeta[name];
311
+ const matches = matchesFilters(filters, {
312
+ type: 'mcp',
313
+ name,
314
+ tags: promptMeta.tags,
315
+ }, logger);
316
+ if (!matches) {
317
+ delete filteredState.mcpEndpoints.promptsMeta[name];
318
+ }
319
+ else {
320
+ if (promptMeta.pikkuFuncName) {
321
+ filteredState.serviceAggregation.usedFunctions.add(promptMeta.pikkuFuncName);
322
+ }
323
+ extractWireNames(promptMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
324
+ extractWireNames(promptMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
325
+ }
326
+ }
327
+ // Repopulate mcpEndpoints.files if any MCP endpoints remain
328
+ const hasMcpEndpoints = Object.keys(filteredState.mcpEndpoints.toolsMeta).length > 0 ||
329
+ Object.keys(filteredState.mcpEndpoints.resourcesMeta).length > 0 ||
330
+ Object.keys(filteredState.mcpEndpoints.promptsMeta).length > 0;
331
+ if (hasMcpEndpoints) {
332
+ filteredState.mcpEndpoints.files = new Set(state.mcpEndpoints.files);
333
+ }
334
+ // Filter CLI programs (note: CLI filtering might be more complex with nested commands)
335
+ const referencedRenderers = new Set();
336
+ for (const programName of Object.keys(filteredState.cli.meta.programs)) {
337
+ const programMeta = filteredState.cli.meta.programs[programName];
338
+ // Filter commands in the program
339
+ for (const commandName of Object.keys(programMeta.commands)) {
340
+ const commandMeta = programMeta.commands[commandName];
341
+ const matches = matchesFilters(filters, {
342
+ type: 'cli',
343
+ name: commandName,
344
+ tags: commandMeta.tags,
345
+ }, logger);
346
+ if (!matches) {
347
+ delete programMeta.commands[commandName];
348
+ }
349
+ else {
350
+ if (commandMeta.pikkuFuncName) {
351
+ filteredState.serviceAggregation.usedFunctions.add(commandMeta.pikkuFuncName);
352
+ }
353
+ extractWireNames(commandMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
354
+ // Track referenced renderers
355
+ if (commandMeta.defaultRenderName) {
356
+ referencedRenderers.add(commandMeta.defaultRenderName);
357
+ }
358
+ }
359
+ }
360
+ // Remove program if it has no commands left
361
+ if (Object.keys(programMeta.commands).length === 0) {
362
+ delete filteredState.cli.meta.programs[programName];
363
+ }
364
+ }
365
+ // Filter out renderers that aren't referenced by any remaining commands
366
+ for (const rendererName of Object.keys(filteredState.cli.meta.renderers || {})) {
367
+ if (!referencedRenderers.has(rendererName)) {
368
+ delete filteredState.cli.meta.renderers[rendererName];
369
+ }
370
+ }
371
+ // Repopulate cli.files if any CLI programs or referenced renderers remain
372
+ const hasCliPrograms = Object.keys(filteredState.cli.meta.programs).length > 0;
373
+ const hasCliRenderers = Object.keys(filteredState.cli.meta.renderers || {}).length > 0;
374
+ if (hasCliPrograms || hasCliRenderers) {
375
+ filteredState.cli.files = new Set(state.cli.files);
376
+ }
377
+ // Recalculate requiredServices based on filtered functions/middleware/permissions
378
+ // Need to cast to InspectorState temporarily for aggregateRequiredServices
379
+ const stateForAggregation = filteredState;
380
+ aggregateRequiredServices(stateForAggregation);
381
+ return filteredState;
382
+ }
@@ -1,9 +1,19 @@
1
1
  import { InspectorFilters, InspectorLogger } from '../types.js';
2
2
  import { PikkuWiringTypes } from '@pikku/core';
3
+ /**
4
+ * Match a value against a pattern with wildcard support
5
+ * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
6
+ * @param value - The value to check
7
+ * @param pattern - The pattern with optional "*" wildcard(s)
8
+ */
9
+ export declare function matchesWildcard(value: string, pattern: string): boolean;
3
10
  export declare const matchesFilters: (filters: InspectorFilters, params: {
4
11
  tags?: string[];
12
+ name?: string;
5
13
  }, meta: {
6
14
  type: PikkuWiringTypes;
7
15
  name: string;
8
16
  filePath?: string;
17
+ httpRoute?: string;
18
+ httpMethod?: string;
9
19
  }, logger: InspectorLogger) => boolean;
@@ -1,12 +1,49 @@
1
+ /**
2
+ * Match a value against a pattern with wildcard support
3
+ * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
4
+ * @param value - The value to check
5
+ * @param pattern - The pattern with optional "*" wildcard(s)
6
+ */
7
+ export function matchesWildcard(value, pattern) {
8
+ // If pattern is just '*', match everything
9
+ if (pattern === '*') {
10
+ return true;
11
+ }
12
+ const startsWithWildcard = pattern.startsWith('*');
13
+ const endsWithWildcard = pattern.endsWith('*');
14
+ if (startsWithWildcard && endsWithWildcard) {
15
+ // Pattern like "*middle*" - check if value contains the middle part
16
+ const middle = pattern.slice(1, -1);
17
+ if (middle === '') {
18
+ return true; // Pattern is "**", match everything
19
+ }
20
+ return value.includes(middle);
21
+ }
22
+ else if (startsWithWildcard) {
23
+ // Pattern like "*suffix" - check if value ends with suffix and has content before
24
+ const suffix = pattern.slice(1);
25
+ return value.endsWith(suffix) && value.length > suffix.length;
26
+ }
27
+ else if (endsWithWildcard) {
28
+ // Pattern like "prefix*" - check if value starts with prefix and has content after
29
+ const prefix = pattern.slice(0, -1);
30
+ return value.startsWith(prefix) && value.length > prefix.length;
31
+ }
32
+ // No wildcard, exact match
33
+ return value === pattern;
34
+ }
1
35
  export const matchesFilters = (filters, params, meta, logger) => {
2
36
  // If no filters are provided, allow everything
3
37
  if (Object.keys(filters).length === 0) {
4
38
  return true;
5
39
  }
6
40
  // If all filter arrays are empty, allow everything
7
- if ((!filters.tags || filters.tags.length === 0) &&
41
+ if ((!filters.names || filters.names.length === 0) &&
42
+ (!filters.tags || filters.tags.length === 0) &&
8
43
  (!filters.types || filters.types.length === 0) &&
9
- (!filters.directories || filters.directories.length === 0)) {
44
+ (!filters.directories || filters.directories.length === 0) &&
45
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
46
+ (!filters.httpMethods || filters.httpMethods.length === 0)) {
10
47
  return true;
11
48
  }
12
49
  // Check type filter
@@ -41,5 +78,32 @@ export const matchesFilters = (filters, params, meta, logger) => {
41
78
  return false;
42
79
  }
43
80
  }
81
+ // Check name filter (with wildcard support)
82
+ if (filters.names && filters.names.length > 0) {
83
+ const nameToMatch = params.name || meta.name;
84
+ const nameMatches = filters.names.some((pattern) => matchesWildcard(nameToMatch, pattern));
85
+ if (!nameMatches) {
86
+ logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`);
87
+ return false;
88
+ }
89
+ }
90
+ // Check HTTP route filter (with wildcard support)
91
+ if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
92
+ const routeMatches = filters.httpRoutes.some((pattern) => matchesWildcard(meta.httpRoute, pattern));
93
+ if (!routeMatches) {
94
+ logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`);
95
+ return false;
96
+ }
97
+ }
98
+ // Check HTTP method filter
99
+ if (filters.httpMethods &&
100
+ filters.httpMethods.length > 0 &&
101
+ meta.httpMethod) {
102
+ const normalizedMethod = meta.httpMethod.toUpperCase();
103
+ if (!filters.httpMethods.includes(normalizedMethod)) {
104
+ logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`);
105
+ return false;
106
+ }
107
+ }
44
108
  return true;
45
109
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Finds the common ancestor directory of all the given file paths.
3
+ * This is used to determine the project root directory.
4
+ *
5
+ * @param filePaths - Array of absolute file paths
6
+ * @returns The common ancestor directory path
7
+ *
8
+ * @example
9
+ * findCommonAncestor([
10
+ * '/Users/yasser/git/pikku/pikku/src/functions/a.ts',
11
+ * '/Users/yasser/git/pikku/pikku/src/routes/b.ts'
12
+ * ])
13
+ * // Returns: '/Users/yasser/git/pikku/pikku'
14
+ */
15
+ export declare function findCommonAncestor(filePaths: string[]): string;
16
+ /**
17
+ * Converts an absolute file path to a relative path from the root directory.
18
+ *
19
+ * @param absolutePath - The absolute file path
20
+ * @param rootDir - The root directory to make the path relative to
21
+ * @returns A relative path from rootDir to the file
22
+ */
23
+ export declare function toRelativePath(absolutePath: string, rootDir: string): string;