@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
@@ -2,7 +2,7 @@ import { test, describe } from 'node:test'
2
2
  import { strict as assert } from 'node:assert'
3
3
  import { PikkuWiringTypes } from '@pikku/core'
4
4
  import { InspectorFilters } from '../types'
5
- import { matchesFilters } from './filter-utils'
5
+ import { matchesFilters, matchesWildcard } from './filter-utils'
6
6
 
7
7
  describe('matchesFilters', () => {
8
8
  // Mock logger for testing
@@ -481,4 +481,353 @@ describe('matchesFilters', () => {
481
481
  assert.equal(result, true)
482
482
  })
483
483
  })
484
+
485
+ describe('Name filtering', () => {
486
+ test('should return true when name matches exactly', () => {
487
+ const filters: InspectorFilters = {
488
+ names: ['email-worker', 'notification-worker'],
489
+ }
490
+
491
+ const result = matchesFilters(
492
+ filters,
493
+ { name: 'email-worker' },
494
+ { type: PikkuWiringTypes.queue, name: 'email-queue' },
495
+ mockLogger
496
+ )
497
+
498
+ assert.equal(result, true)
499
+ })
500
+
501
+ test('should return true when name matches with wildcard', () => {
502
+ const filters: InspectorFilters = {
503
+ names: ['email-*'],
504
+ }
505
+
506
+ const result = matchesFilters(
507
+ filters,
508
+ { name: 'email-worker' },
509
+ { type: PikkuWiringTypes.queue, name: 'email-queue' },
510
+ mockLogger
511
+ )
512
+
513
+ assert.equal(result, true)
514
+ })
515
+
516
+ test('should return false when name does not match wildcard', () => {
517
+ const filters: InspectorFilters = {
518
+ names: ['email-*'],
519
+ }
520
+
521
+ const result = matchesFilters(
522
+ filters,
523
+ { name: 'notification-worker' },
524
+ { type: PikkuWiringTypes.queue, name: 'notification-queue' },
525
+ mockLogger
526
+ )
527
+
528
+ assert.equal(result, false)
529
+ })
530
+
531
+ test('should use meta.name when params.name is not provided', () => {
532
+ const filters: InspectorFilters = {
533
+ names: ['test-*'],
534
+ }
535
+
536
+ const result = matchesFilters(
537
+ filters,
538
+ {},
539
+ { type: PikkuWiringTypes.http, name: 'test-route' },
540
+ mockLogger
541
+ )
542
+
543
+ assert.equal(result, true)
544
+ })
545
+
546
+ test('should prefer params.name over meta.name', () => {
547
+ const filters: InspectorFilters = {
548
+ names: ['email-*'],
549
+ }
550
+
551
+ const result = matchesFilters(
552
+ filters,
553
+ { name: 'email-worker' },
554
+ { type: PikkuWiringTypes.queue, name: 'other-name' },
555
+ mockLogger
556
+ )
557
+
558
+ assert.equal(result, true)
559
+ })
560
+ })
561
+
562
+ describe('HTTP route filtering', () => {
563
+ test('should return true when httpRoute matches exactly', () => {
564
+ const filters: InspectorFilters = {
565
+ httpRoutes: ['/api/users', '/api/posts'],
566
+ }
567
+
568
+ const result = matchesFilters(
569
+ filters,
570
+ {},
571
+ {
572
+ type: PikkuWiringTypes.http,
573
+ name: 'users-route',
574
+ httpRoute: '/api/users',
575
+ },
576
+ mockLogger
577
+ )
578
+
579
+ assert.equal(result, true)
580
+ })
581
+
582
+ test('should return true when httpRoute matches with wildcard', () => {
583
+ const filters: InspectorFilters = {
584
+ httpRoutes: ['/api/*'],
585
+ }
586
+
587
+ const result = matchesFilters(
588
+ filters,
589
+ {},
590
+ {
591
+ type: PikkuWiringTypes.http,
592
+ name: 'users-route',
593
+ httpRoute: '/api/users',
594
+ },
595
+ mockLogger
596
+ )
597
+
598
+ assert.equal(result, true)
599
+ })
600
+
601
+ test('should return false when httpRoute does not match wildcard', () => {
602
+ const filters: InspectorFilters = {
603
+ httpRoutes: ['/api/*'],
604
+ }
605
+
606
+ const result = matchesFilters(
607
+ filters,
608
+ {},
609
+ {
610
+ type: PikkuWiringTypes.http,
611
+ name: 'webhook-route',
612
+ httpRoute: '/webhooks/stripe',
613
+ },
614
+ mockLogger
615
+ )
616
+
617
+ assert.equal(result, false)
618
+ })
619
+
620
+ test('should not filter when httpRoute is not provided in meta', () => {
621
+ const filters: InspectorFilters = {
622
+ httpRoutes: ['/api/*'],
623
+ }
624
+
625
+ const result = matchesFilters(
626
+ filters,
627
+ {},
628
+ { type: PikkuWiringTypes.queue, name: 'worker' },
629
+ mockLogger
630
+ )
631
+
632
+ assert.equal(result, true) // Does not apply to non-HTTP
633
+ })
634
+
635
+ test('should match multiple route patterns', () => {
636
+ const filters: InspectorFilters = {
637
+ httpRoutes: ['/api/*', '/webhooks/*'],
638
+ }
639
+
640
+ const result = matchesFilters(
641
+ filters,
642
+ {},
643
+ {
644
+ type: PikkuWiringTypes.http,
645
+ name: 'webhook-route',
646
+ httpRoute: '/webhooks/stripe',
647
+ },
648
+ mockLogger
649
+ )
650
+
651
+ assert.equal(result, true)
652
+ })
653
+ })
654
+
655
+ describe('HTTP method filtering', () => {
656
+ test('should return true when httpMethod matches', () => {
657
+ const filters: InspectorFilters = {
658
+ httpMethods: ['GET', 'POST'],
659
+ }
660
+
661
+ const result = matchesFilters(
662
+ filters,
663
+ {},
664
+ {
665
+ type: PikkuWiringTypes.http,
666
+ name: 'users-route',
667
+ httpMethod: 'GET',
668
+ },
669
+ mockLogger
670
+ )
671
+
672
+ assert.equal(result, true)
673
+ })
674
+
675
+ test('should return false when httpMethod does not match', () => {
676
+ const filters: InspectorFilters = {
677
+ httpMethods: ['GET', 'POST'],
678
+ }
679
+
680
+ const result = matchesFilters(
681
+ filters,
682
+ {},
683
+ {
684
+ type: PikkuWiringTypes.http,
685
+ name: 'users-route',
686
+ httpMethod: 'DELETE',
687
+ },
688
+ mockLogger
689
+ )
690
+
691
+ assert.equal(result, false)
692
+ })
693
+
694
+ test('should handle case-insensitive method matching', () => {
695
+ const filters: InspectorFilters = {
696
+ httpMethods: ['GET', 'POST'],
697
+ }
698
+
699
+ const result = matchesFilters(
700
+ filters,
701
+ {},
702
+ {
703
+ type: PikkuWiringTypes.http,
704
+ name: 'users-route',
705
+ httpMethod: 'get',
706
+ },
707
+ mockLogger
708
+ )
709
+
710
+ assert.equal(result, true)
711
+ })
712
+
713
+ test('should not filter when httpMethod is not provided in meta', () => {
714
+ const filters: InspectorFilters = {
715
+ httpMethods: ['GET'],
716
+ }
717
+
718
+ const result = matchesFilters(
719
+ filters,
720
+ {},
721
+ { type: PikkuWiringTypes.queue, name: 'worker' },
722
+ mockLogger
723
+ )
724
+
725
+ assert.equal(result, true) // Does not apply to non-HTTP
726
+ })
727
+ })
728
+
729
+ describe('Combined filtering with new filters', () => {
730
+ test('should return true when all filters including new ones pass', () => {
731
+ const filters: InspectorFilters = {
732
+ tags: ['api'],
733
+ types: ['http'],
734
+ httpRoutes: ['/api/*'],
735
+ httpMethods: ['GET'],
736
+ }
737
+
738
+ const result = matchesFilters(
739
+ filters,
740
+ { tags: ['api'] },
741
+ {
742
+ type: PikkuWiringTypes.http,
743
+ name: 'users-route',
744
+ httpRoute: '/api/users',
745
+ httpMethod: 'GET',
746
+ },
747
+ mockLogger
748
+ )
749
+
750
+ assert.equal(result, true)
751
+ })
752
+
753
+ test('should return false when httpRoute filter fails', () => {
754
+ const filters: InspectorFilters = {
755
+ tags: ['api'],
756
+ httpRoutes: ['/admin/*'],
757
+ }
758
+
759
+ const result = matchesFilters(
760
+ filters,
761
+ { tags: ['api'] },
762
+ {
763
+ type: PikkuWiringTypes.http,
764
+ name: 'users-route',
765
+ httpRoute: '/api/users',
766
+ },
767
+ mockLogger
768
+ )
769
+
770
+ assert.equal(result, false)
771
+ })
772
+
773
+ test('should return false when httpMethod filter fails', () => {
774
+ const filters: InspectorFilters = {
775
+ httpRoutes: ['/api/*'],
776
+ httpMethods: ['POST'],
777
+ }
778
+
779
+ const result = matchesFilters(
780
+ filters,
781
+ {},
782
+ {
783
+ type: PikkuWiringTypes.http,
784
+ name: 'users-route',
785
+ httpRoute: '/api/users',
786
+ httpMethod: 'GET',
787
+ },
788
+ mockLogger
789
+ )
790
+
791
+ assert.equal(result, false)
792
+ })
793
+ })
794
+ })
795
+
796
+ describe('matchesWildcard', () => {
797
+ test('should match exact strings', () => {
798
+ assert.equal(matchesWildcard('email-worker', 'email-worker'), true)
799
+ assert.equal(matchesWildcard('test', 'test'), true)
800
+ })
801
+
802
+ test('should not match different strings', () => {
803
+ assert.equal(matchesWildcard('email-worker', 'notification-worker'), false)
804
+ assert.equal(matchesWildcard('test', 'other'), false)
805
+ })
806
+
807
+ test('should match wildcard prefix', () => {
808
+ assert.equal(matchesWildcard('email-worker', 'email-*'), true)
809
+ assert.equal(matchesWildcard('email-sender', 'email-*'), true)
810
+ assert.equal(matchesWildcard('email', 'email-*'), false) // Needs prefix before *
811
+ })
812
+
813
+ test('should match wildcard for routes', () => {
814
+ assert.equal(matchesWildcard('/api/users', '/api/*'), true)
815
+ assert.equal(matchesWildcard('/api/posts', '/api/*'), true)
816
+ assert.equal(matchesWildcard('/webhooks/stripe', '/api/*'), false)
817
+ })
818
+
819
+ test('should handle empty prefix with wildcard', () => {
820
+ assert.equal(matchesWildcard('anything', '*'), true)
821
+ assert.equal(matchesWildcard('', '*'), true)
822
+ })
823
+
824
+ test('should handle exact match when no wildcard', () => {
825
+ assert.equal(matchesWildcard('test', 'test'), true)
826
+ assert.equal(matchesWildcard('test', 'test*'), false) // Has suffix after exact match
827
+ })
828
+
829
+ test('should handle special characters in prefix', () => {
830
+ assert.equal(matchesWildcard('api-v2-users', 'api-v2-*'), true)
831
+ assert.equal(matchesWildcard('api.v2.users', 'api.v2.*'), true)
832
+ })
484
833
  })
@@ -1,13 +1,54 @@
1
1
  import { InspectorFilters, InspectorLogger } from '../types.js'
2
2
  import { PikkuWiringTypes } from '@pikku/core'
3
3
 
4
+ /**
5
+ * Match a value against a pattern with wildcard support
6
+ * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
7
+ * @param value - The value to check
8
+ * @param pattern - The pattern with optional "*" wildcard(s)
9
+ */
10
+ export function matchesWildcard(value: string, pattern: string): boolean {
11
+ // If pattern is just '*', match everything
12
+ if (pattern === '*') {
13
+ return true
14
+ }
15
+
16
+ const startsWithWildcard = pattern.startsWith('*')
17
+ const endsWithWildcard = pattern.endsWith('*')
18
+
19
+ if (startsWithWildcard && endsWithWildcard) {
20
+ // Pattern like "*middle*" - check if value contains the middle part
21
+ const middle = pattern.slice(1, -1)
22
+ if (middle === '') {
23
+ return true // Pattern is "**", match everything
24
+ }
25
+ return value.includes(middle)
26
+ } else if (startsWithWildcard) {
27
+ // Pattern like "*suffix" - check if value ends with suffix and has content before
28
+ const suffix = pattern.slice(1)
29
+ return value.endsWith(suffix) && value.length > suffix.length
30
+ } else if (endsWithWildcard) {
31
+ // Pattern like "prefix*" - check if value starts with prefix and has content after
32
+ const prefix = pattern.slice(0, -1)
33
+ return value.startsWith(prefix) && value.length > prefix.length
34
+ }
35
+
36
+ // No wildcard, exact match
37
+ return value === pattern
38
+ }
39
+
4
40
  export const matchesFilters = (
5
41
  filters: InspectorFilters,
6
- params: { tags?: string[] },
42
+ params: {
43
+ tags?: string[]
44
+ name?: string // Wire/function name for name filter
45
+ },
7
46
  meta: {
8
47
  type: PikkuWiringTypes
9
48
  name: string
10
49
  filePath?: string
50
+ httpRoute?: string // For HTTP route filtering
51
+ httpMethod?: string // For HTTP method filtering
11
52
  },
12
53
  logger: InspectorLogger
13
54
  ) => {
@@ -18,9 +59,12 @@ export const matchesFilters = (
18
59
 
19
60
  // If all filter arrays are empty, allow everything
20
61
  if (
62
+ (!filters.names || filters.names.length === 0) &&
21
63
  (!filters.tags || filters.tags.length === 0) &&
22
64
  (!filters.types || filters.types.length === 0) &&
23
- (!filters.directories || filters.directories.length === 0)
65
+ (!filters.directories || filters.directories.length === 0) &&
66
+ (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
67
+ (!filters.httpMethods || filters.httpMethods.length === 0)
24
68
  ) {
25
69
  return true
26
70
  }
@@ -68,5 +112,41 @@ export const matchesFilters = (
68
112
  }
69
113
  }
70
114
 
115
+ // Check name filter (with wildcard support)
116
+ if (filters.names && filters.names.length > 0) {
117
+ const nameToMatch = params.name || meta.name
118
+ const nameMatches = filters.names.some((pattern) =>
119
+ matchesWildcard(nameToMatch, pattern)
120
+ )
121
+ if (!nameMatches) {
122
+ logger.debug(`⒡ Filtered by name: ${meta.type}:${meta.name}`)
123
+ return false
124
+ }
125
+ }
126
+
127
+ // Check HTTP route filter (with wildcard support)
128
+ if (filters.httpRoutes && filters.httpRoutes.length > 0 && meta.httpRoute) {
129
+ const routeMatches = filters.httpRoutes.some((pattern) =>
130
+ matchesWildcard(meta.httpRoute!, pattern)
131
+ )
132
+ if (!routeMatches) {
133
+ logger.debug(`⒡ Filtered by HTTP route: ${meta.httpRoute}`)
134
+ return false
135
+ }
136
+ }
137
+
138
+ // Check HTTP method filter
139
+ if (
140
+ filters.httpMethods &&
141
+ filters.httpMethods.length > 0 &&
142
+ meta.httpMethod
143
+ ) {
144
+ const normalizedMethod = meta.httpMethod.toUpperCase()
145
+ if (!filters.httpMethods.includes(normalizedMethod)) {
146
+ logger.debug(`⒡ Filtered by HTTP method: ${meta.httpMethod}`)
147
+ return false
148
+ }
149
+ }
150
+
71
151
  return true
72
152
  }
@@ -0,0 +1,68 @@
1
+ import path from 'path'
2
+
3
+ /**
4
+ * Finds the common ancestor directory of all the given file paths.
5
+ * This is used to determine the project root directory.
6
+ *
7
+ * @param filePaths - Array of absolute file paths
8
+ * @returns The common ancestor directory path
9
+ *
10
+ * @example
11
+ * findCommonAncestor([
12
+ * '/Users/yasser/git/pikku/pikku/src/functions/a.ts',
13
+ * '/Users/yasser/git/pikku/pikku/src/routes/b.ts'
14
+ * ])
15
+ * // Returns: '/Users/yasser/git/pikku/pikku'
16
+ */
17
+ export function findCommonAncestor(filePaths: string[]): string {
18
+ if (filePaths.length === 0) {
19
+ return process.cwd()
20
+ }
21
+
22
+ if (filePaths.length === 1) {
23
+ return path.dirname(filePaths[0]!)
24
+ }
25
+
26
+ // Normalize all paths and get their directory parts
27
+ const normalizedPaths = filePaths.map((p) =>
28
+ path.dirname(path.normalize(p)).split(path.sep)
29
+ )
30
+
31
+ // Start with the first path's parts
32
+ const firstPath = normalizedPaths[0]!
33
+ let commonParts: string[] = []
34
+
35
+ // Check each part of the first path
36
+ for (let i = 0; i < firstPath.length; i++) {
37
+ const part = firstPath[i]!
38
+
39
+ // Check if this part exists in all other paths at the same position
40
+ const existsInAll = normalizedPaths.every(
41
+ (pathParts) => pathParts[i] === part
42
+ )
43
+
44
+ if (existsInAll) {
45
+ commonParts.push(part)
46
+ } else {
47
+ break
48
+ }
49
+ }
50
+
51
+ // If no common parts, return root
52
+ if (commonParts.length === 0) {
53
+ return path.sep
54
+ }
55
+
56
+ return commonParts.join(path.sep)
57
+ }
58
+
59
+ /**
60
+ * Converts an absolute file path to a relative path from the root directory.
61
+ *
62
+ * @param absolutePath - The absolute file path
63
+ * @param rootDir - The root directory to make the path relative to
64
+ * @returns A relative path from rootDir to the file
65
+ */
66
+ export function toRelativePath(absolutePath: string, rootDir: string): string {
67
+ return path.relative(rootDir, absolutePath)
68
+ }
@@ -15,6 +15,7 @@ export type FilesAndMethods = {
15
15
  userSessionType: Meta
16
16
  sessionServicesType: Meta
17
17
  singletonServicesType: Meta
18
+ pikkuConfigType: Meta
18
19
  pikkuConfigFactory: Meta
19
20
  singletonServicesFactory: Meta
20
21
  sessionServicesFactory: Meta
@@ -53,9 +54,9 @@ const getMetaTypes = (
53
54
  const helpMessage =
54
55
  type === 'CoreConfig'
55
56
  ? `No ${type} found. Make sure you have exported a createConfig function in your codebase:\n\n` +
56
- `export const createConfig: CreateConfig<Config> = async () => {\n` +
57
+ `export const createConfig = pikkuConfig(async () => {\n` +
57
58
  ` return {}\n` +
58
- `}\n\n` +
59
+ `})\n\n` +
59
60
  `Possible issues:\n` +
60
61
  `- srcDirectories in pikku.config.json doesn't include the file with the createConfig method`
61
62
  : `No ${type} found`
@@ -87,6 +88,7 @@ export const getFilesAndMethods = (
87
88
  singletonServicesTypeImportMap,
88
89
  sessionServicesTypeImportMap,
89
90
  userSessionTypeImportMap,
91
+ configTypeImportMap,
90
92
  sessionServicesFactories,
91
93
  singletonServicesFactories,
92
94
  configFactories,
@@ -119,6 +121,12 @@ export const getFilesAndMethods = (
119
121
  undefined,
120
122
  errors
121
123
  ),
124
+ pikkuConfigType: getMetaTypes(
125
+ 'CoreConfig',
126
+ configTypeImportMap,
127
+ undefined,
128
+ errors
129
+ ),
122
130
  pikkuConfigFactory: getMetaTypes(
123
131
  'CoreConfig',
124
132
  configFactories,
@@ -1,5 +1,6 @@
1
1
  import { PikkuDocs } from '@pikku/core'
2
2
  import * as ts from 'typescript'
3
+ import { ErrorCode } from '../error-codes.js'
3
4
 
4
5
  export const getPropertyValue = (
5
6
  obj: ts.ObjectLiteralExpression,
@@ -91,3 +92,29 @@ export const getPropertyValue = (
91
92
 
92
93
  return null
93
94
  }
95
+
96
+ /**
97
+ * Gets the 'tags' property from an object and validates it's an array.
98
+ * Logs a critical error if tags is not an array but still returns the value.
99
+ * @param logger - Optional logger instance; if not provided, uses console.error
100
+ */
101
+ export const getPropertyTags = (
102
+ obj: ts.ObjectLiteralExpression,
103
+ wiringType: string,
104
+ wiringName: string | null,
105
+ logger?: { critical: (code: ErrorCode, message: string) => void }
106
+ ): string[] | undefined => {
107
+ const tagsValue = getPropertyValue(obj, 'tags')
108
+
109
+ if (tagsValue !== null && !Array.isArray(tagsValue)) {
110
+ const errorMsg = `${wiringType} '${wiringName}' has invalid 'tags' property - must be an array of strings.`
111
+ if (logger) {
112
+ logger.critical(ErrorCode.INVALID_TAGS_TYPE, errorMsg)
113
+ } else {
114
+ console.error(errorMsg)
115
+ }
116
+ // Return undefined but don't stop processing - error will be caught by the exit handler
117
+ }
118
+
119
+ return Array.isArray(tagsValue) ? tagsValue : undefined
120
+ }
@@ -10,7 +10,8 @@ import { InspectorState } from '../types.js'
10
10
  */
11
11
  export function extractMiddlewarePikkuNames(
12
12
  arrayNode: ts.Expression,
13
- checker: ts.TypeChecker
13
+ checker: ts.TypeChecker,
14
+ rootDir: string
14
15
  ): string[] {
15
16
  if (!ts.isArrayLiteralExpression(arrayNode)) {
16
17
  return []
@@ -20,12 +21,16 @@ export function extractMiddlewarePikkuNames(
20
21
  for (const element of arrayNode.elements) {
21
22
  if (ts.isIdentifier(element)) {
22
23
  // Resolve the identifier to its pikkuFuncName
23
- const { pikkuFuncName } = extractFunctionName(element, checker)
24
+ const { pikkuFuncName } = extractFunctionName(element, checker, rootDir)
24
25
  names.push(pikkuFuncName)
25
26
  } else if (ts.isCallExpression(element)) {
26
- // Handle call expressions like logCommandInfoAndTime({...})
27
- // These create inline middleware, so we use the call expression itself as the name
28
- const { pikkuFuncName } = extractFunctionName(element, checker)
27
+ // Handle call expressions like rateLimiter(10) or logCommandInfoAndTime({...})
28
+ // Extract the function being called (e.g., 'rateLimiter' from 'rateLimiter(10)')
29
+ const { pikkuFuncName } = extractFunctionName(
30
+ element.expression,
31
+ checker,
32
+ rootDir
33
+ )
29
34
  names.push(pikkuFuncName)
30
35
  }
31
36
  }
@@ -111,7 +116,8 @@ export function resolveHTTPMiddleware(
111
116
  if (explicitMiddlewareNode) {
112
117
  const middlewareNames = extractMiddlewarePikkuNames(
113
118
  explicitMiddlewareNode,
114
- checker
119
+ checker,
120
+ state.rootDir
115
121
  )
116
122
  for (const name of middlewareNames) {
117
123
  const meta = state.middleware.meta[name]
@@ -156,7 +162,8 @@ function resolveTagAndExplicitMiddleware(
156
162
  if (explicitMiddlewareNode) {
157
163
  const middlewareNames = extractMiddlewarePikkuNames(
158
164
  explicitMiddlewareNode,
159
- checker
165
+ checker,
166
+ state.rootDir
160
167
  )
161
168
  for (const name of middlewareNames) {
162
169
  const meta = state.middleware.meta[name]