@pikku/inspector 0.9.6-next.0 → 0.10.1

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 +14 -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 +87 -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 +4 -3
  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 +114 -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 +10 -2
  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,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;
@@ -0,0 +1,55 @@
1
+ import path from 'path';
2
+ /**
3
+ * Finds the common ancestor directory of all the given file paths.
4
+ * This is used to determine the project root directory.
5
+ *
6
+ * @param filePaths - Array of absolute file paths
7
+ * @returns The common ancestor directory path
8
+ *
9
+ * @example
10
+ * findCommonAncestor([
11
+ * '/Users/yasser/git/pikku/pikku/src/functions/a.ts',
12
+ * '/Users/yasser/git/pikku/pikku/src/routes/b.ts'
13
+ * ])
14
+ * // Returns: '/Users/yasser/git/pikku/pikku'
15
+ */
16
+ export function findCommonAncestor(filePaths) {
17
+ if (filePaths.length === 0) {
18
+ return process.cwd();
19
+ }
20
+ if (filePaths.length === 1) {
21
+ return path.dirname(filePaths[0]);
22
+ }
23
+ // Normalize all paths and get their directory parts
24
+ const normalizedPaths = filePaths.map((p) => path.dirname(path.normalize(p)).split(path.sep));
25
+ // Start with the first path's parts
26
+ const firstPath = normalizedPaths[0];
27
+ let commonParts = [];
28
+ // Check each part of the first path
29
+ for (let i = 0; i < firstPath.length; i++) {
30
+ const part = firstPath[i];
31
+ // Check if this part exists in all other paths at the same position
32
+ const existsInAll = normalizedPaths.every((pathParts) => pathParts[i] === part);
33
+ if (existsInAll) {
34
+ commonParts.push(part);
35
+ }
36
+ else {
37
+ break;
38
+ }
39
+ }
40
+ // If no common parts, return root
41
+ if (commonParts.length === 0) {
42
+ return path.sep;
43
+ }
44
+ return commonParts.join(path.sep);
45
+ }
46
+ /**
47
+ * Converts an absolute file path to a relative path from the root directory.
48
+ *
49
+ * @param absolutePath - The absolute file path
50
+ * @param rootDir - The root directory to make the path relative to
51
+ * @returns A relative path from rootDir to the file
52
+ */
53
+ export function toRelativePath(absolutePath, rootDir) {
54
+ return path.relative(rootDir, absolutePath);
55
+ }
@@ -9,12 +9,13 @@ export type FilesAndMethods = {
9
9
  userSessionType: Meta;
10
10
  sessionServicesType: Meta;
11
11
  singletonServicesType: Meta;
12
+ pikkuConfigType: Meta;
12
13
  pikkuConfigFactory: Meta;
13
14
  singletonServicesFactory: Meta;
14
15
  sessionServicesFactory: Meta;
15
16
  };
16
17
  export type FilesAndMethodsErrors = Map<string, PathToNameAndType>;
17
- export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, }?: InspectorOptions["types"]) => {
18
+ export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, }?: InspectorOptions["types"]) => {
18
19
  result: Partial<FilesAndMethods>;
19
20
  errors: FilesAndMethodsErrors;
20
21
  };
@@ -19,9 +19,9 @@ const getMetaTypes = (type, map, desiredType, errors) => {
19
19
  if (totalValues.length === 0) {
20
20
  const helpMessage = type === 'CoreConfig'
21
21
  ? `No ${type} found. Make sure you have exported a createConfig function in your codebase:\n\n` +
22
- `export const createConfig: CreateConfig<Config> = async () => {\n` +
22
+ `export const createConfig = pikkuConfig(async () => {\n` +
23
23
  ` return {}\n` +
24
- `}\n\n` +
24
+ `})\n\n` +
25
25
  `Possible issues:\n` +
26
26
  `- srcDirectories in pikku.config.json doesn't include the file with the createConfig method`
27
27
  : `No ${type} found`;
@@ -46,12 +46,13 @@ const getMetaTypes = (type, map, desiredType, errors) => {
46
46
  }
47
47
  return;
48
48
  };
49
- export const getFilesAndMethods = ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, } = {}) => {
49
+ export const getFilesAndMethods = ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, } = {}) => {
50
50
  const errors = new Map();
51
51
  const result = {
52
52
  userSessionType: getMetaTypes('CoreUserSession', userSessionTypeImportMap, userSessionType, errors),
53
53
  singletonServicesType: getMetaTypes('CoreSingletonServices', singletonServicesTypeImportMap, undefined, errors),
54
54
  sessionServicesType: getMetaTypes('CoreServices', sessionServicesTypeImportMap, undefined, errors),
55
+ pikkuConfigType: getMetaTypes('CoreConfig', configTypeImportMap, undefined, errors),
55
56
  pikkuConfigFactory: getMetaTypes('CoreConfig', configFactories, configFileType, errors),
56
57
  singletonServicesFactory: getMetaTypes('CreateSingletonServices', singletonServicesFactories, singletonServicesFactoryType, errors),
57
58
  sessionServicesFactory: getMetaTypes('CreateSessionServices', sessionServicesFactories, sessionServicesFactoryType, errors),
@@ -1,3 +1,12 @@
1
1
  import { PikkuDocs } from '@pikku/core';
2
2
  import * as ts from 'typescript';
3
+ import { ErrorCode } from '../error-codes.js';
3
4
  export declare const getPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string | string[] | null | PikkuDocs | boolean;
5
+ /**
6
+ * Gets the 'tags' property from an object and validates it's an array.
7
+ * Logs a critical error if tags is not an array but still returns the value.
8
+ * @param logger - Optional logger instance; if not provided, uses console.error
9
+ */
10
+ export declare const getPropertyTags: (obj: ts.ObjectLiteralExpression, wiringType: string, wiringName: string | null, logger?: {
11
+ critical: (code: ErrorCode, message: string) => void;
12
+ }) => string[] | undefined;
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
2
3
  export const getPropertyValue = (obj, propertyName) => {
3
4
  const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
4
5
  ts.isIdentifier(p.name) &&
@@ -64,3 +65,22 @@ export const getPropertyValue = (obj, propertyName) => {
64
65
  }
65
66
  return null;
66
67
  };
68
+ /**
69
+ * Gets the 'tags' property from an object and validates it's an array.
70
+ * Logs a critical error if tags is not an array but still returns the value.
71
+ * @param logger - Optional logger instance; if not provided, uses console.error
72
+ */
73
+ export const getPropertyTags = (obj, wiringType, wiringName, logger) => {
74
+ const tagsValue = getPropertyValue(obj, 'tags');
75
+ if (tagsValue !== null && !Array.isArray(tagsValue)) {
76
+ const errorMsg = `${wiringType} '${wiringName}' has invalid 'tags' property - must be an array of strings.`;
77
+ if (logger) {
78
+ logger.critical(ErrorCode.INVALID_TAGS_TYPE, errorMsg);
79
+ }
80
+ else {
81
+ console.error(errorMsg);
82
+ }
83
+ // Return undefined but don't stop processing - error will be caught by the exit handler
84
+ }
85
+ return Array.isArray(tagsValue) ? tagsValue : undefined;
86
+ };
@@ -6,7 +6,7 @@ import { InspectorState } from '../types.js';
6
6
  * Resolves each identifier to its pikkuFuncName using extractFunctionName
7
7
  * Also handles call expressions (like logCommandInfoAndTime({...}))
8
8
  */
9
- export declare function extractMiddlewarePikkuNames(arrayNode: ts.Expression, checker: ts.TypeChecker): string[];
9
+ export declare function extractMiddlewarePikkuNames(arrayNode: ts.Expression, checker: ts.TypeChecker, rootDir: string): string[];
10
10
  /**
11
11
  * Get middleware array from an object literal expression property
12
12
  * Returns the initializer node for the 'middleware' property if it exists