@hyphaene/hexa-ts-kit 1.2.4 → 1.3.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.
@@ -0,0 +1,2922 @@
1
+ // src/commands/lint.ts
2
+ import { resolve as resolve2 } from "path";
3
+ import { execSync } from "child_process";
4
+
5
+ // src/lint/checkers/structure/colocation.ts
6
+ import fg from "fast-glob";
7
+ import { existsSync } from "fs";
8
+ import { basename, dirname, join } from "path";
9
+
10
+ // src/lint/checkers/structure/naming.ts
11
+ import fg2 from "fast-glob";
12
+ import { readFile } from "fs/promises";
13
+ import { basename as basename2 } from "path";
14
+
15
+ // src/lint/checkers/structure/domains.ts
16
+ import fg3 from "fast-glob";
17
+ import { basename as basename3 } from "path";
18
+
19
+ // src/lint/checkers/ast/vue-component.ts
20
+ import fg4 from "fast-glob";
21
+ import { readFile as readFile2 } from "fs/promises";
22
+
23
+ // src/lint/checkers/ast/rules-file.ts
24
+ import fg5 from "fast-glob";
25
+ import { readFile as readFile3 } from "fs/promises";
26
+
27
+ // src/lint/checkers/ast/typescript.ts
28
+ import fg6 from "fast-glob";
29
+ import { readFile as readFile4 } from "fs/promises";
30
+
31
+ // src/lint/rules.registry.ts
32
+ var rulesRegistry = {
33
+ // ==========================================================================
34
+ // Colocation Rules (COL-*)
35
+ // ==========================================================================
36
+ "COL-001": {
37
+ title: "Vue component outside domains",
38
+ severity: "error",
39
+ category: "colocation",
40
+ why: "All Vue components must live in src/domains/{domain}/{feature}/ for colocation.",
41
+ autoFixable: false
42
+ },
43
+ "COL-002": {
44
+ title: "Missing colocated composable",
45
+ severity: "error",
46
+ category: "colocation",
47
+ why: "Each Vue component should have a colocated composable for logic separation.",
48
+ autoFixable: false
49
+ },
50
+ "COL-003": {
51
+ title: "Missing colocated types",
52
+ severity: "warning",
53
+ category: "colocation",
54
+ why: "Types should be colocated with the component that uses them.",
55
+ autoFixable: false
56
+ },
57
+ "COL-004": {
58
+ title: "Missing colocated tests",
59
+ severity: "warning",
60
+ category: "colocation",
61
+ why: "Tests should be colocated with the code they test.",
62
+ autoFixable: false
63
+ },
64
+ "COL-005": {
65
+ title: "Forbidden src/components directory",
66
+ severity: "error",
67
+ category: "colocation",
68
+ why: "Root-level components/ directory violates colocation. Move to domains/.",
69
+ autoFixable: false
70
+ },
71
+ "COL-006": {
72
+ title: "Forbidden src/hooks or src/composables directory",
73
+ severity: "error",
74
+ category: "colocation",
75
+ why: "Root-level hooks/composables directories violate colocation. Move to domains/.",
76
+ autoFixable: false
77
+ },
78
+ "COL-007": {
79
+ title: "Forbidden src/services directory",
80
+ severity: "error",
81
+ category: "colocation",
82
+ why: "Root-level services/ directory violates colocation. Move to domains/.",
83
+ autoFixable: false
84
+ },
85
+ "COL-008": {
86
+ title: "Forbidden src/types directory",
87
+ severity: "error",
88
+ category: "colocation",
89
+ why: "Root-level types/ directory violates colocation. Move to domains/ or shared/.",
90
+ autoFixable: false
91
+ },
92
+ "COL-009": {
93
+ title: "Unused colocated file",
94
+ severity: "warning",
95
+ category: "colocation",
96
+ why: "Colocated files should be used by their parent component.",
97
+ autoFixable: false
98
+ },
99
+ "COL-010": {
100
+ title: "Cross-domain import",
101
+ severity: "error",
102
+ category: "colocation",
103
+ why: "Direct imports between domains create coupling. Use shared/ for common code.",
104
+ autoFixable: false
105
+ },
106
+ "COL-011": {
107
+ title: "Missing index barrel export",
108
+ severity: "warning",
109
+ category: "colocation",
110
+ why: "Features should expose a clean public API via index.ts.",
111
+ autoFixable: true
112
+ },
113
+ "COL-012": {
114
+ title: "Deep import into feature",
115
+ severity: "warning",
116
+ category: "colocation",
117
+ why: "Import from feature index, not internal files.",
118
+ autoFixable: false
119
+ },
120
+ // ==========================================================================
121
+ // Naming Rules (NAM-*)
122
+ // ==========================================================================
123
+ "NAM-001": {
124
+ title: "Vue file not PascalCase",
125
+ severity: "error",
126
+ category: "naming",
127
+ why: "Vue components must use PascalCase naming (e.g., MyComponent.vue).",
128
+ autoFixable: false
129
+ },
130
+ "NAM-002": {
131
+ title: "Composable missing use prefix",
132
+ severity: "error",
133
+ category: "naming",
134
+ why: "Composables must start with 'use' (e.g., useMyFeature.ts).",
135
+ autoFixable: false
136
+ },
137
+ "NAM-003": {
138
+ title: "Types file wrong suffix",
139
+ severity: "error",
140
+ category: "naming",
141
+ why: "Type files must use .types.ts suffix.",
142
+ autoFixable: false
143
+ },
144
+ "NAM-004": {
145
+ title: "Test file wrong suffix",
146
+ severity: "error",
147
+ category: "naming",
148
+ why: "Test files must use .test.ts or .spec.ts suffix.",
149
+ autoFixable: false
150
+ },
151
+ "NAM-005": {
152
+ title: "Feature folder not kebab-case",
153
+ severity: "error",
154
+ category: "naming",
155
+ why: "Feature folders must use kebab-case (e.g., my-feature/).",
156
+ autoFixable: false
157
+ },
158
+ "NAM-006": {
159
+ title: "Domain folder not kebab-case",
160
+ severity: "error",
161
+ category: "naming",
162
+ why: "Domain folders must use kebab-case.",
163
+ autoFixable: false
164
+ },
165
+ "NAM-007": {
166
+ title: "Constant not SCREAMING_SNAKE_CASE",
167
+ severity: "warning",
168
+ category: "naming",
169
+ why: "Constants should use SCREAMING_SNAKE_CASE.",
170
+ autoFixable: false
171
+ },
172
+ "NAM-008": {
173
+ title: "Interface missing I prefix",
174
+ severity: "info",
175
+ category: "naming",
176
+ why: "Consider using I prefix for interfaces (IMyInterface).",
177
+ autoFixable: false
178
+ },
179
+ "NAM-009": {
180
+ title: "Type alias wrong suffix",
181
+ severity: "info",
182
+ category: "naming",
183
+ why: "Consider using Type suffix for type aliases (MyType).",
184
+ autoFixable: false
185
+ },
186
+ "NAM-010": {
187
+ title: "Enum not PascalCase",
188
+ severity: "error",
189
+ category: "naming",
190
+ why: "Enums must use PascalCase naming.",
191
+ autoFixable: false
192
+ },
193
+ "NAM-011": {
194
+ title: "Query key not camelCase",
195
+ severity: "error",
196
+ category: "naming",
197
+ why: "Query keys must use camelCase.",
198
+ autoFixable: false
199
+ },
200
+ "NAM-012": {
201
+ title: "Event handler missing on prefix",
202
+ severity: "warning",
203
+ category: "naming",
204
+ why: "Event handlers should start with 'on' (e.g., onSubmit).",
205
+ autoFixable: false
206
+ },
207
+ // ==========================================================================
208
+ // Vue Component Rules (VUE-*)
209
+ // ==========================================================================
210
+ "VUE-001": {
211
+ title: "ref() in component",
212
+ severity: "error",
213
+ category: "vue",
214
+ why: "ref() should be in composable, not component. Components are for template binding.",
215
+ autoFixable: false
216
+ },
217
+ "VUE-002": {
218
+ title: "reactive() in component",
219
+ severity: "error",
220
+ category: "vue",
221
+ why: "reactive() should be in composable, not component.",
222
+ autoFixable: false
223
+ },
224
+ "VUE-003": {
225
+ title: "computed() in component",
226
+ severity: "error",
227
+ category: "vue",
228
+ why: "computed() should be in composable, not component.",
229
+ autoFixable: false
230
+ },
231
+ "VUE-004": {
232
+ title: "watch() in component",
233
+ severity: "error",
234
+ category: "vue",
235
+ why: "watch() should be in composable, not component.",
236
+ autoFixable: false
237
+ },
238
+ "VUE-005": {
239
+ title: "watchEffect() in component",
240
+ severity: "error",
241
+ category: "vue",
242
+ why: "watchEffect() should be in composable, not component.",
243
+ autoFixable: false
244
+ },
245
+ "VUE-006": {
246
+ title: "Complex logic in template",
247
+ severity: "warning",
248
+ category: "vue",
249
+ why: "Move complex expressions to computed properties in composable.",
250
+ autoFixable: false
251
+ },
252
+ "VUE-007": {
253
+ title: "Inline styles in template",
254
+ severity: "warning",
255
+ category: "vue",
256
+ why: "Use CSS classes instead of inline styles.",
257
+ autoFixable: false
258
+ },
259
+ "VUE-008": {
260
+ title: "Missing component name",
261
+ severity: "warning",
262
+ category: "vue",
263
+ why: "Components should have explicit names for debugging.",
264
+ autoFixable: true
265
+ },
266
+ "VUE-009": {
267
+ title: "v-if with v-for",
268
+ severity: "error",
269
+ category: "vue",
270
+ why: "v-if and v-for on same element is anti-pattern. Use computed or wrapper.",
271
+ autoFixable: false
272
+ },
273
+ "VUE-010": {
274
+ title: "Missing key in v-for",
275
+ severity: "error",
276
+ category: "vue",
277
+ why: "v-for requires :key for proper DOM diffing.",
278
+ autoFixable: false
279
+ },
280
+ "VUE-011": {
281
+ title: "Direct DOM manipulation",
282
+ severity: "error",
283
+ category: "vue",
284
+ why: "Avoid direct DOM manipulation. Use refs and Vue reactivity.",
285
+ autoFixable: false
286
+ },
287
+ "VUE-012": {
288
+ title: "Prop mutation",
289
+ severity: "error",
290
+ category: "vue",
291
+ why: "Never mutate props directly. Emit events instead.",
292
+ autoFixable: false
293
+ },
294
+ "VUE-013": {
295
+ title: "Missing prop validation",
296
+ severity: "warning",
297
+ category: "vue",
298
+ why: "Props should have type validation.",
299
+ autoFixable: false
300
+ },
301
+ // ==========================================================================
302
+ // Composable Rules (CMP-*)
303
+ // ==========================================================================
304
+ "CMP-001": {
305
+ title: "Missing return statement",
306
+ severity: "error",
307
+ category: "composable",
308
+ why: "Composables must return their reactive state and methods.",
309
+ autoFixable: false
310
+ },
311
+ "CMP-002": {
312
+ title: "Side effect outside onMounted",
313
+ severity: "warning",
314
+ category: "composable",
315
+ why: "Side effects should be in lifecycle hooks, not at module level.",
316
+ autoFixable: false
317
+ },
318
+ "CMP-003": {
319
+ title: "Missing cleanup in onUnmounted",
320
+ severity: "warning",
321
+ category: "composable",
322
+ why: "Subscriptions and listeners need cleanup to prevent memory leaks.",
323
+ autoFixable: false
324
+ },
325
+ "CMP-004": {
326
+ title: "Non-reactive return value",
327
+ severity: "warning",
328
+ category: "composable",
329
+ why: "Composables should return reactive values (ref, computed, reactive).",
330
+ autoFixable: false
331
+ },
332
+ "CMP-005": {
333
+ title: "Direct API call without error handling",
334
+ severity: "warning",
335
+ category: "composable",
336
+ why: "API calls should have try/catch or error handling.",
337
+ autoFixable: false
338
+ },
339
+ "CMP-006": {
340
+ title: "Hardcoded values",
341
+ severity: "info",
342
+ category: "composable",
343
+ why: "Consider extracting hardcoded values to constants or config.",
344
+ autoFixable: false
345
+ },
346
+ "CMP-007": {
347
+ title: "Missing TypeScript types",
348
+ severity: "warning",
349
+ category: "composable",
350
+ why: "Composable parameters and return type should be typed.",
351
+ autoFixable: false
352
+ },
353
+ "CMP-008": {
354
+ title: "Too many responsibilities",
355
+ severity: "warning",
356
+ category: "composable",
357
+ why: "Composable does too much. Consider splitting into smaller composables.",
358
+ autoFixable: false
359
+ },
360
+ "CMP-009": {
361
+ title: "Global state mutation",
362
+ severity: "error",
363
+ category: "composable",
364
+ why: "Avoid mutating global state directly. Use stores.",
365
+ autoFixable: false
366
+ },
367
+ "CMP-010": {
368
+ title: "Synchronous blocking operation",
369
+ severity: "warning",
370
+ category: "composable",
371
+ why: "Heavy operations should be async to avoid blocking UI.",
372
+ autoFixable: false
373
+ },
374
+ "CMP-011": {
375
+ title: "Missing loading state",
376
+ severity: "info",
377
+ category: "composable",
378
+ why: "Async operations should expose loading state.",
379
+ autoFixable: false
380
+ },
381
+ // ==========================================================================
382
+ // Rules File Rules (RUL-*)
383
+ // ==========================================================================
384
+ "RUL-001": {
385
+ title: "Missing rules file",
386
+ severity: "warning",
387
+ category: "rules",
388
+ why: "Features should have a .rules.ts file documenting business rules.",
389
+ autoFixable: false
390
+ },
391
+ "RUL-002": {
392
+ title: "Rules not exported",
393
+ severity: "error",
394
+ category: "rules",
395
+ why: "Rules must be exported for documentation generation.",
396
+ autoFixable: false
397
+ },
398
+ "RUL-003": {
399
+ title: "Rule missing description",
400
+ severity: "warning",
401
+ category: "rules",
402
+ why: "Each rule should have a human-readable description.",
403
+ autoFixable: false
404
+ },
405
+ "RUL-004": {
406
+ title: "Rule missing validation",
407
+ severity: "warning",
408
+ category: "rules",
409
+ why: "Rules should include validation logic.",
410
+ autoFixable: false
411
+ },
412
+ "RUL-005": {
413
+ title: "Orphan rule",
414
+ severity: "warning",
415
+ category: "rules",
416
+ why: "Rule is defined but not used in any composable.",
417
+ autoFixable: false
418
+ },
419
+ "RUL-006": {
420
+ title: "Inline business rule",
421
+ severity: "warning",
422
+ category: "rules",
423
+ why: "Business logic should be in .rules.ts, not inline in component.",
424
+ autoFixable: false
425
+ },
426
+ "RUL-007": {
427
+ title: "Missing rule test",
428
+ severity: "info",
429
+ category: "rules",
430
+ why: "Business rules should have unit tests.",
431
+ autoFixable: false
432
+ },
433
+ "RUL-008": {
434
+ title: "Rule complexity too high",
435
+ severity: "warning",
436
+ category: "rules",
437
+ why: "Complex rules should be broken down into smaller rules.",
438
+ autoFixable: false
439
+ },
440
+ "RUL-009": {
441
+ title: "Duplicate rule logic",
442
+ severity: "warning",
443
+ category: "rules",
444
+ why: "Same rule logic appears in multiple places. Centralize it.",
445
+ autoFixable: false
446
+ },
447
+ "RUL-010": {
448
+ title: "Rule without error message",
449
+ severity: "warning",
450
+ category: "rules",
451
+ why: "Rules should define user-facing error messages.",
452
+ autoFixable: false
453
+ },
454
+ "RUL-011": {
455
+ title: "Magic number in rule",
456
+ severity: "warning",
457
+ category: "rules",
458
+ why: "Extract magic numbers to named constants.",
459
+ autoFixable: false
460
+ },
461
+ "RUL-012": {
462
+ title: "Rule depends on external state",
463
+ severity: "warning",
464
+ category: "rules",
465
+ why: "Rules should be pure functions. Pass dependencies as parameters.",
466
+ autoFixable: false
467
+ },
468
+ // ==========================================================================
469
+ // Query Rules (QRY-*)
470
+ // ==========================================================================
471
+ "QRY-001": {
472
+ title: "Query key not array",
473
+ severity: "error",
474
+ category: "query",
475
+ why: "Query keys must be arrays for proper cache invalidation.",
476
+ autoFixable: true
477
+ },
478
+ "QRY-002": {
479
+ title: "Missing query key factory",
480
+ severity: "warning",
481
+ category: "query",
482
+ why: "Use query key factories for consistency.",
483
+ autoFixable: false
484
+ },
485
+ "QRY-003": {
486
+ title: "Hardcoded stale time",
487
+ severity: "info",
488
+ category: "query",
489
+ why: "Consider extracting stale time to config.",
490
+ autoFixable: false
491
+ },
492
+ "QRY-004": {
493
+ title: "Missing error handling",
494
+ severity: "warning",
495
+ category: "query",
496
+ why: "Queries should handle errors gracefully.",
497
+ autoFixable: false
498
+ },
499
+ "QRY-005": {
500
+ title: "Query in component",
501
+ severity: "error",
502
+ category: "query",
503
+ why: "useQuery should be in composable, not component.",
504
+ autoFixable: false
505
+ },
506
+ "QRY-006": {
507
+ title: "Missing enabled condition",
508
+ severity: "info",
509
+ category: "query",
510
+ why: "Consider using enabled option for conditional queries.",
511
+ autoFixable: false
512
+ },
513
+ "QRY-007": {
514
+ title: "Mutation without invalidation",
515
+ severity: "warning",
516
+ category: "query",
517
+ why: "Mutations should invalidate related queries.",
518
+ autoFixable: false
519
+ },
520
+ "QRY-008": {
521
+ title: "Direct fetch instead of query",
522
+ severity: "warning",
523
+ category: "query",
524
+ why: "Use useQuery/useMutation for API calls to get caching benefits.",
525
+ autoFixable: false
526
+ },
527
+ // ==========================================================================
528
+ // Translation Rules (TRN-*)
529
+ // ==========================================================================
530
+ "TRN-001": {
531
+ title: "Hardcoded string",
532
+ severity: "warning",
533
+ category: "translations",
534
+ why: "User-facing strings should use i18n.",
535
+ autoFixable: false
536
+ },
537
+ "TRN-002": {
538
+ title: "Missing translation key",
539
+ severity: "error",
540
+ category: "translations",
541
+ why: "Translation key not found in any locale file.",
542
+ autoFixable: false
543
+ },
544
+ "TRN-003": {
545
+ title: "Unused translation key",
546
+ severity: "warning",
547
+ category: "translations",
548
+ why: "Translation key is defined but never used.",
549
+ autoFixable: false
550
+ },
551
+ "TRN-004": {
552
+ title: "Inconsistent translation keys",
553
+ severity: "warning",
554
+ category: "translations",
555
+ why: "Key exists in some locales but not all.",
556
+ autoFixable: false
557
+ },
558
+ "TRN-005": {
559
+ title: "Non-namespaced translation key",
560
+ severity: "info",
561
+ category: "translations",
562
+ why: "Translation keys should be namespaced by feature.",
563
+ autoFixable: false
564
+ },
565
+ // ==========================================================================
566
+ // TypeScript Rules (TSP-*)
567
+ // ==========================================================================
568
+ "TSP-001": {
569
+ title: "any type usage",
570
+ severity: "error",
571
+ category: "typescript",
572
+ why: "Avoid 'any'. Use 'unknown' or proper types.",
573
+ autoFixable: false
574
+ },
575
+ "TSP-002": {
576
+ title: "Type assertion without check",
577
+ severity: "warning",
578
+ category: "typescript",
579
+ why: "Type assertions (as X) should be preceded by runtime checks.",
580
+ autoFixable: false
581
+ },
582
+ "TSP-003": {
583
+ title: "Non-null assertion",
584
+ severity: "warning",
585
+ category: "typescript",
586
+ why: "Avoid ! operator. Use proper null checks.",
587
+ autoFixable: false
588
+ },
589
+ "TSP-004": {
590
+ title: "Implicit any in function",
591
+ severity: "error",
592
+ category: "typescript",
593
+ why: "Function parameters must have explicit types.",
594
+ autoFixable: false
595
+ },
596
+ "TSP-005": {
597
+ title: "Missing return type",
598
+ severity: "warning",
599
+ category: "typescript",
600
+ why: "Functions should have explicit return types.",
601
+ autoFixable: false
602
+ },
603
+ "TSP-006": {
604
+ title: "Type import not type-only",
605
+ severity: "info",
606
+ category: "typescript",
607
+ why: "Use 'import type' for type-only imports.",
608
+ autoFixable: true
609
+ },
610
+ "TSP-007": {
611
+ title: "Enum instead of const object",
612
+ severity: "info",
613
+ category: "typescript",
614
+ why: "Consider using const objects instead of enums for better tree-shaking.",
615
+ autoFixable: false
616
+ },
617
+ "TSP-008": {
618
+ title: "Object type instead of Record",
619
+ severity: "info",
620
+ category: "typescript",
621
+ why: "Use Record<K, V> instead of { [key: K]: V }.",
622
+ autoFixable: true
623
+ },
624
+ "TSP-009": {
625
+ title: "Function type instead of arrow",
626
+ severity: "info",
627
+ category: "typescript",
628
+ why: "Use arrow function type: (x: T) => R instead of Function.",
629
+ autoFixable: false
630
+ },
631
+ "TSP-010": {
632
+ title: "Redundant type annotation",
633
+ severity: "info",
634
+ category: "typescript",
635
+ why: "Type can be inferred. Remove redundant annotation.",
636
+ autoFixable: true
637
+ },
638
+ "TSP-011": {
639
+ title: "Unsafe optional chaining",
640
+ severity: "warning",
641
+ category: "typescript",
642
+ why: "Optional chaining on non-nullable type suggests incorrect types.",
643
+ autoFixable: false
644
+ },
645
+ "TSP-012": {
646
+ title: "Missing generic constraint",
647
+ severity: "info",
648
+ category: "typescript",
649
+ why: "Generics should have constraints when possible.",
650
+ autoFixable: false
651
+ },
652
+ // ==========================================================================
653
+ // Domain Rules (DOM-*)
654
+ // ==========================================================================
655
+ "DOM-001": {
656
+ title: "Missing domain index",
657
+ severity: "warning",
658
+ category: "domain",
659
+ why: "Domains should have index.ts exposing public API.",
660
+ autoFixable: true
661
+ },
662
+ "DOM-002": {
663
+ title: "Circular domain dependency",
664
+ severity: "error",
665
+ category: "domain",
666
+ why: "Domains should not have circular dependencies.",
667
+ autoFixable: false
668
+ },
669
+ "DOM-003": {
670
+ title: "Shared code in domain",
671
+ severity: "warning",
672
+ category: "domain",
673
+ why: "Code used by multiple domains should be in shared/.",
674
+ autoFixable: false
675
+ },
676
+ "DOM-004": {
677
+ title: "Domain too large",
678
+ severity: "info",
679
+ category: "domain",
680
+ why: "Consider splitting large domains into sub-domains.",
681
+ autoFixable: false
682
+ },
683
+ // ==========================================================================
684
+ // Contracts Rules (CTR-*)
685
+ // ==========================================================================
686
+ "CTR-001": {
687
+ title: "Missing metadata",
688
+ severity: "error",
689
+ category: "contracts",
690
+ why: "Every contract endpoint must have metadata for documentation and tooling.",
691
+ autoFixable: false,
692
+ examples: {
693
+ invalid: "contracts/CTR-001-invalid.ts",
694
+ valid: "contracts/CTR-001-valid.ts"
695
+ }
696
+ },
697
+ "CTR-002": {
698
+ title: "Missing openApiTags",
699
+ severity: "error",
700
+ category: "contracts",
701
+ why: "Endpoints must have openApiTags for API documentation grouping.",
702
+ autoFixable: false,
703
+ examples: {
704
+ invalid: "contracts/CTR-002-invalid.ts",
705
+ valid: "contracts/CTR-002-valid.ts"
706
+ }
707
+ },
708
+ "CTR-003": {
709
+ title: "Legacy openapi format",
710
+ severity: "error",
711
+ category: "contracts",
712
+ why: "Use metadata.openApiTags instead of metadata.openapi.tags.",
713
+ autoFixable: true,
714
+ examples: {
715
+ invalid: "contracts/CTR-003-invalid.ts",
716
+ valid: "contracts/CTR-003-valid.ts"
717
+ }
718
+ },
719
+ "CTR-004": {
720
+ title: "Missing permission",
721
+ severity: "error",
722
+ category: "contracts",
723
+ why: "Every endpoint must declare its requiredPermission for access control.",
724
+ autoFixable: false,
725
+ examples: {
726
+ invalid: "contracts/CTR-004-invalid.ts",
727
+ valid: "contracts/CTR-004-valid.ts"
728
+ }
729
+ },
730
+ "CTR-005": {
731
+ title: "Invalid permission value",
732
+ severity: "error",
733
+ category: "contracts",
734
+ why: "Permission must be a valid value from the Permission enum.",
735
+ autoFixable: false
736
+ },
737
+ "CTR-006": {
738
+ title: "Invalid apiService value",
739
+ severity: "error",
740
+ category: "contracts",
741
+ why: "apiService must be a valid value from the ApiService enum.",
742
+ autoFixable: false
743
+ },
744
+ "CTR-007": {
745
+ title: "Missing strictStatusCodes",
746
+ severity: "warning",
747
+ category: "contracts",
748
+ why: "Contracts should use strictStatusCodes: true for type safety.",
749
+ autoFixable: true,
750
+ examples: {
751
+ invalid: "contracts/CTR-007-invalid.ts",
752
+ valid: "contracts/CTR-007-valid.ts"
753
+ }
754
+ }
755
+ };
756
+ var allRuleIds = Object.keys(rulesRegistry);
757
+ function getRulesByCategory(category) {
758
+ return allRuleIds.filter((id) => rulesRegistry[id].category === category);
759
+ }
760
+
761
+ // src/lib/contracts/config-loader.ts
762
+ import { readFileSync, existsSync as existsSync2 } from "fs";
763
+ import { resolve, join as join2 } from "path";
764
+ import { parse as parseYaml } from "yaml";
765
+ import { z } from "zod";
766
+ var SourceExtractConfigSchema = z.object({
767
+ source: z.string(),
768
+ export: z.string(),
769
+ extract: z.enum(["const-values", "zod-keys"]).default("const-values")
770
+ });
771
+ var ContractsLintConfigSchema = z.object({
772
+ metadata: z.object({
773
+ permissions: SourceExtractConfigSchema,
774
+ apiServices: SourceExtractConfigSchema
775
+ }),
776
+ disabledRules: z.array(z.string()).optional()
777
+ });
778
+ var ContractsScaffoldConfigSchema = z.object({
779
+ path: z.string(),
780
+ // Absolute path to contracts lib
781
+ importPath: z.string()
782
+ // Import path for generated files
783
+ });
784
+ var ProjectConfigSchema = z.object({
785
+ type: z.enum(["contracts-lib", "nestjs-bff", "vue-frontend"])
786
+ });
787
+ var HexaTsKitConfigSchema = z.object({
788
+ project: ProjectConfigSchema,
789
+ lint: z.object({
790
+ contracts: ContractsLintConfigSchema.optional()
791
+ }).optional(),
792
+ scaffold: z.object({
793
+ contracts: ContractsScaffoldConfigSchema.optional()
794
+ }).optional()
795
+ });
796
+ var CONFIG_FILE_NAME = ".hexa-ts-kit.yaml";
797
+ function loadConfig(repoPath) {
798
+ const absolutePath = resolve(repoPath);
799
+ const configPath = join2(absolutePath, CONFIG_FILE_NAME);
800
+ if (!existsSync2(configPath)) {
801
+ return {
802
+ success: false,
803
+ error: `Config file not found: ${configPath}`,
804
+ configPath
805
+ };
806
+ }
807
+ let content;
808
+ try {
809
+ content = readFileSync(configPath, "utf-8");
810
+ } catch (err) {
811
+ return {
812
+ success: false,
813
+ error: `Failed to read config file: ${err instanceof Error ? err.message : String(err)}`,
814
+ configPath
815
+ };
816
+ }
817
+ let rawConfig;
818
+ try {
819
+ rawConfig = parseYaml(content);
820
+ } catch (err) {
821
+ return {
822
+ success: false,
823
+ error: `Invalid YAML syntax: ${err instanceof Error ? err.message : String(err)}`,
824
+ configPath
825
+ };
826
+ }
827
+ const result = HexaTsKitConfigSchema.safeParse(rawConfig);
828
+ if (!result.success) {
829
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
830
+ return {
831
+ success: false,
832
+ error: `Invalid config:
833
+ ${issues}`,
834
+ configPath
835
+ };
836
+ }
837
+ return {
838
+ success: true,
839
+ config: result.data,
840
+ configPath
841
+ };
842
+ }
843
+ function getPermissionsConfig(config) {
844
+ return config.lint?.contracts?.metadata?.permissions ?? null;
845
+ }
846
+ function getApiServicesConfig(config) {
847
+ return config.lint?.contracts?.metadata?.apiServices ?? null;
848
+ }
849
+ function resolveConfigPath(repoPath, relativePath) {
850
+ return resolve(repoPath, relativePath);
851
+ }
852
+ function getDisabledRules(config) {
853
+ return config.lint?.contracts?.disabledRules ?? [];
854
+ }
855
+
856
+ // src/lib/contracts/permissions-extractor.ts
857
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
858
+ import { parse } from "@typescript-eslint/typescript-estree";
859
+ function extractValues(sourcePath, config) {
860
+ if (!existsSync3(sourcePath)) {
861
+ return {
862
+ success: false,
863
+ error: `Source file not found: ${sourcePath}`,
864
+ sourcePath
865
+ };
866
+ }
867
+ let content;
868
+ try {
869
+ content = readFileSync2(sourcePath, "utf-8");
870
+ } catch (err) {
871
+ return {
872
+ success: false,
873
+ error: `Failed to read source file: ${err instanceof Error ? err.message : String(err)}`,
874
+ sourcePath
875
+ };
876
+ }
877
+ let ast;
878
+ try {
879
+ ast = parse(content, {
880
+ loc: true,
881
+ range: true
882
+ });
883
+ } catch (err) {
884
+ return {
885
+ success: false,
886
+ error: `Failed to parse TypeScript: ${err instanceof Error ? err.message : String(err)}`,
887
+ sourcePath
888
+ };
889
+ }
890
+ const exportName = config.export;
891
+ const extractMode = config.extract;
892
+ if (extractMode === "const-values") {
893
+ return extractConstValues(ast, exportName, sourcePath);
894
+ } else if (extractMode === "zod-keys") {
895
+ return extractZodKeys(ast, exportName, sourcePath);
896
+ }
897
+ return {
898
+ success: false,
899
+ error: `Unknown extract mode: ${extractMode}`,
900
+ sourcePath
901
+ };
902
+ }
903
+ function extractConstValues(ast, exportName, sourcePath) {
904
+ const values = [];
905
+ const keyToValue = {};
906
+ for (const node of ast.body) {
907
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
908
+ for (const declarator of node.declaration.declarations) {
909
+ if (declarator.id.type === "Identifier" && declarator.id.name === exportName && declarator.init) {
910
+ let objectExpr = null;
911
+ if (declarator.init.type === "TSAsExpression") {
912
+ const expr = declarator.init.expression;
913
+ if (expr.type === "ObjectExpression") {
914
+ objectExpr = expr;
915
+ }
916
+ } else if (declarator.init.type === "ObjectExpression") {
917
+ objectExpr = declarator.init;
918
+ }
919
+ if (objectExpr) {
920
+ for (const prop of objectExpr.properties) {
921
+ if (prop.type === "Property" && prop.value.type === "Literal" && typeof prop.value.value === "string") {
922
+ const value = prop.value.value;
923
+ values.push(value);
924
+ if (prop.key.type === "Identifier") {
925
+ keyToValue[prop.key.name] = value;
926
+ }
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ }
933
+ }
934
+ if (values.length === 0) {
935
+ return {
936
+ success: false,
937
+ error: `Could not find const object '${exportName}' with string values in ${sourcePath}`,
938
+ sourcePath
939
+ };
940
+ }
941
+ return {
942
+ success: true,
943
+ values,
944
+ keyToValue,
945
+ sourcePath
946
+ };
947
+ }
948
+ function extractZodKeys(ast, exportName, sourcePath) {
949
+ const values = [];
950
+ for (const node of ast.body) {
951
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
952
+ for (const declarator of node.declaration.declarations) {
953
+ if (declarator.id.type === "Identifier" && declarator.id.name === exportName && declarator.init) {
954
+ const init = declarator.init;
955
+ if (init.type === "CallExpression" && init.callee.type === "MemberExpression" && init.callee.property.type === "Identifier" && init.callee.property.name === "object") {
956
+ const firstArg = init.arguments[0];
957
+ if (firstArg && firstArg.type === "ObjectExpression") {
958
+ for (const prop of firstArg.properties) {
959
+ if (prop.type === "Property" && prop.key.type === "Identifier") {
960
+ values.push(prop.key.name);
961
+ }
962
+ }
963
+ }
964
+ }
965
+ }
966
+ }
967
+ }
968
+ }
969
+ if (values.length === 0) {
970
+ return {
971
+ success: false,
972
+ error: `Could not find zod schema '${exportName}' with z.object() in ${sourcePath}`,
973
+ sourcePath
974
+ };
975
+ }
976
+ return {
977
+ success: true,
978
+ values,
979
+ sourcePath
980
+ };
981
+ }
982
+
983
+ // src/lib/contracts/contract-parser.ts
984
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
985
+ import fg7 from "fast-glob";
986
+ import { parse as parse2 } from "@typescript-eslint/typescript-estree";
987
+ async function findContractFiles(repoPath) {
988
+ const files = await fg7("**/*.contract.ts", {
989
+ cwd: repoPath,
990
+ ignore: ["**/node_modules/**", "**/dist/**", "**/worktrees/**"],
991
+ absolute: true
992
+ });
993
+ return files;
994
+ }
995
+ function parseContractFile(filePath) {
996
+ if (!existsSync4(filePath)) {
997
+ return {
998
+ success: false,
999
+ error: `Contract file not found: ${filePath}`,
1000
+ filePath
1001
+ };
1002
+ }
1003
+ let content;
1004
+ try {
1005
+ content = readFileSync3(filePath, "utf-8");
1006
+ } catch (err) {
1007
+ return {
1008
+ success: false,
1009
+ error: `Failed to read contract file: ${err instanceof Error ? err.message : String(err)}`,
1010
+ filePath
1011
+ };
1012
+ }
1013
+ let ast;
1014
+ try {
1015
+ ast = parse2(content, {
1016
+ loc: true,
1017
+ range: true
1018
+ });
1019
+ } catch (err) {
1020
+ return {
1021
+ success: false,
1022
+ error: `Failed to parse TypeScript: ${err instanceof Error ? err.message : String(err)}`,
1023
+ filePath
1024
+ };
1025
+ }
1026
+ const endpoints = [];
1027
+ let contractName = "UnknownContract";
1028
+ for (const node of ast.body) {
1029
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
1030
+ for (const declarator of node.declaration.declarations) {
1031
+ if (declarator.id.type === "Identifier") {
1032
+ contractName = declarator.id.name;
1033
+ const routerCall = findRouterCall(declarator.init);
1034
+ if (routerCall) {
1035
+ const parsedEndpoints = extractEndpointsFromRouter(
1036
+ routerCall,
1037
+ content
1038
+ );
1039
+ endpoints.push(...parsedEndpoints);
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ return {
1046
+ filePath,
1047
+ contractName,
1048
+ endpoints
1049
+ };
1050
+ }
1051
+ async function parseContracts(repoPath) {
1052
+ const files = await findContractFiles(repoPath);
1053
+ if (files.length === 0) {
1054
+ return {
1055
+ success: true,
1056
+ contracts: []
1057
+ };
1058
+ }
1059
+ const contracts = [];
1060
+ const errors = [];
1061
+ for (const file of files) {
1062
+ const result = parseContractFile(file);
1063
+ if ("success" in result && !result.success) {
1064
+ errors.push(result.error);
1065
+ } else {
1066
+ contracts.push(result);
1067
+ }
1068
+ }
1069
+ if (errors.length > 0 && contracts.length === 0) {
1070
+ return {
1071
+ success: false,
1072
+ error: errors.join("\n")
1073
+ };
1074
+ }
1075
+ return {
1076
+ success: true,
1077
+ contracts
1078
+ };
1079
+ }
1080
+ function findRouterCall(node) {
1081
+ if (!node) return null;
1082
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && node.callee.property.name === "router") {
1083
+ return node;
1084
+ }
1085
+ return null;
1086
+ }
1087
+ function extractEndpointsFromRouter(routerCall, sourceContent) {
1088
+ const endpoints = [];
1089
+ const firstArg = routerCall.arguments[0];
1090
+ if (!firstArg || firstArg.type !== "ObjectExpression") {
1091
+ return endpoints;
1092
+ }
1093
+ const routerObject = firstArg;
1094
+ for (const prop of routerObject.properties) {
1095
+ if (prop.type !== "Property") continue;
1096
+ if (prop.key.type !== "Identifier") continue;
1097
+ const endpointName = prop.key.name;
1098
+ const endpointLine = prop.loc?.start.line ?? 0;
1099
+ if (prop.value.type !== "ObjectExpression") continue;
1100
+ const endpoint = extractEndpointDetails(
1101
+ endpointName,
1102
+ prop.value,
1103
+ endpointLine,
1104
+ sourceContent
1105
+ );
1106
+ if (endpoint) {
1107
+ endpoints.push(endpoint);
1108
+ }
1109
+ }
1110
+ return endpoints;
1111
+ }
1112
+ function extractEndpointDetails(name, objectExpr, line, sourceContent) {
1113
+ let method = "";
1114
+ let path = "";
1115
+ let metadata;
1116
+ let strictStatusCodes;
1117
+ for (const prop of objectExpr.properties) {
1118
+ if (prop.type !== "Property") continue;
1119
+ if (prop.key.type !== "Identifier") continue;
1120
+ const propName = prop.key.name;
1121
+ if (propName === "method" && prop.value.type === "Literal") {
1122
+ method = String(prop.value.value);
1123
+ }
1124
+ if (propName === "path" && prop.value.type === "Literal") {
1125
+ path = String(prop.value.value);
1126
+ }
1127
+ if (propName === "metadata") {
1128
+ if (prop.value.type === "ObjectExpression" || prop.value.type === "CallExpression") {
1129
+ metadata = extractMetadata(prop.value, sourceContent);
1130
+ }
1131
+ }
1132
+ if (propName === "strictStatusCodes" && prop.value.type === "Literal") {
1133
+ strictStatusCodes = prop.value.value === true;
1134
+ }
1135
+ }
1136
+ return {
1137
+ name,
1138
+ method,
1139
+ path,
1140
+ metadata,
1141
+ strictStatusCodes,
1142
+ line
1143
+ };
1144
+ }
1145
+ function extractMetadata(node, sourceContent) {
1146
+ if (node.type === "ObjectExpression") {
1147
+ return parseMetadataObject(node, sourceContent);
1148
+ }
1149
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "contractMetadata") {
1150
+ const firstArg = node.arguments[0];
1151
+ if (firstArg && firstArg.type === "ObjectExpression") {
1152
+ return parseMetadataObject(firstArg, sourceContent);
1153
+ }
1154
+ }
1155
+ return void 0;
1156
+ }
1157
+ function parseMetadataObject(objectExpr, sourceContent) {
1158
+ const metadata = {};
1159
+ for (const prop of objectExpr.properties) {
1160
+ if (prop.type !== "Property") continue;
1161
+ if (prop.key.type !== "Identifier") continue;
1162
+ const propName = prop.key.name;
1163
+ if (propName === "openApiTags" && prop.value.type === "ArrayExpression") {
1164
+ metadata.openApiTags = extractStringArray(prop.value);
1165
+ }
1166
+ if (propName === "requiredPermission") {
1167
+ if (prop.value.type === "Literal" || prop.value.type === "MemberExpression") {
1168
+ const permValue = extractPermissionValue(prop.value, sourceContent);
1169
+ if (permValue) {
1170
+ metadata.requiredPermission = permValue;
1171
+ }
1172
+ }
1173
+ }
1174
+ if (propName === "requiresAuth" && prop.value.type === "Literal") {
1175
+ metadata.requiresAuth = prop.value.value === true;
1176
+ }
1177
+ if (propName === "bff" && prop.value.type === "ObjectExpression") {
1178
+ metadata.bff = extractBffConfig(prop.value);
1179
+ }
1180
+ if (propName === "openapi" && prop.value.type === "ObjectExpression") {
1181
+ metadata.openapi = extractLegacyOpenApi(prop.value);
1182
+ }
1183
+ }
1184
+ return metadata;
1185
+ }
1186
+ function extractStringArray(arrayExpr) {
1187
+ const values = [];
1188
+ for (const element of arrayExpr.elements) {
1189
+ if (element?.type === "Literal" && typeof element.value === "string") {
1190
+ values.push(element.value);
1191
+ }
1192
+ }
1193
+ return values;
1194
+ }
1195
+ function extractPermissionValue(node, sourceContent) {
1196
+ if (node.type === "Literal" && typeof node.value === "string") {
1197
+ return node.value;
1198
+ }
1199
+ if (node.type === "MemberExpression") {
1200
+ if (node.range) {
1201
+ const [start, end] = node.range;
1202
+ return sourceContent.slice(start, end);
1203
+ }
1204
+ }
1205
+ return void 0;
1206
+ }
1207
+ function extractBffConfig(objectExpr) {
1208
+ const bff = {};
1209
+ for (const prop of objectExpr.properties) {
1210
+ if (prop.type !== "Property") continue;
1211
+ if (prop.key.type !== "Identifier") continue;
1212
+ if (prop.key.name === "apiServices" && prop.value.type === "ArrayExpression") {
1213
+ bff.apiServices = [];
1214
+ for (const element of prop.value.elements) {
1215
+ if (element?.type === "Literal" && typeof element.value === "string") {
1216
+ bff.apiServices.push(element.value);
1217
+ }
1218
+ if (element?.type === "MemberExpression") {
1219
+ if (element.property.type === "Identifier") {
1220
+ bff.apiServices.push(element.property.name);
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1226
+ return bff;
1227
+ }
1228
+ function extractLegacyOpenApi(objectExpr) {
1229
+ const openapi = {};
1230
+ for (const prop of objectExpr.properties) {
1231
+ if (prop.type !== "Property") continue;
1232
+ if (prop.key.type !== "Identifier") continue;
1233
+ if (prop.key.name === "tags" && prop.value.type === "ArrayExpression") {
1234
+ openapi.tags = extractStringArray(prop.value);
1235
+ }
1236
+ }
1237
+ return openapi;
1238
+ }
1239
+
1240
+ // src/lib/contracts/metadata-validator.ts
1241
+ import { z as z2 } from "zod";
1242
+ function validateContracts(contracts, ctx) {
1243
+ const issues = [];
1244
+ for (const contract of contracts) {
1245
+ for (const endpoint of contract.endpoints) {
1246
+ const endpointIssues = validateEndpoint(endpoint, contract.filePath, ctx);
1247
+ issues.push(...endpointIssues);
1248
+ }
1249
+ }
1250
+ return {
1251
+ valid: issues.filter((i) => i.severity === "error").length === 0,
1252
+ issues
1253
+ };
1254
+ }
1255
+ function validateEndpoint(endpoint, filePath, ctx) {
1256
+ const issues = [];
1257
+ const { metadata, name, line } = endpoint;
1258
+ if (endpoint.strictStatusCodes !== true) {
1259
+ issues.push({
1260
+ rule: "CTR-007",
1261
+ severity: "error",
1262
+ message: `Endpoint '${name}' is missing strictStatusCodes: true`,
1263
+ endpointName: name,
1264
+ filePath,
1265
+ line,
1266
+ suggestion: "Add strictStatusCodes: true to the endpoint definition"
1267
+ });
1268
+ }
1269
+ if (!metadata) {
1270
+ issues.push({
1271
+ rule: "CTR-001",
1272
+ severity: "error",
1273
+ message: `Endpoint '${name}' is missing metadata`,
1274
+ endpointName: name,
1275
+ filePath,
1276
+ line,
1277
+ suggestion: "Add metadata: contractMetadata({ openApiTags: [...], requiredPermission: Permission.XXX })"
1278
+ });
1279
+ return issues;
1280
+ }
1281
+ if (metadata.openapi?.tags) {
1282
+ issues.push({
1283
+ rule: "CTR-003",
1284
+ severity: "error",
1285
+ message: `Endpoint '${name}' uses legacy format metadata.openapi.tags`,
1286
+ endpointName: name,
1287
+ filePath,
1288
+ line,
1289
+ suggestion: "Migrate to openApiTags: [...] at the root of metadata"
1290
+ });
1291
+ }
1292
+ if (!metadata.openApiTags || metadata.openApiTags.length === 0) {
1293
+ if (!metadata.openapi?.tags || metadata.openapi.tags.length === 0) {
1294
+ issues.push({
1295
+ rule: "CTR-002",
1296
+ severity: "error",
1297
+ message: `Endpoint '${name}' is missing openApiTags`,
1298
+ endpointName: name,
1299
+ filePath,
1300
+ line,
1301
+ suggestion: "Add openApiTags: ['TagName'] to metadata"
1302
+ });
1303
+ }
1304
+ }
1305
+ if (!metadata.requiredPermission) {
1306
+ if (metadata.requiresAuth === false) {
1307
+ issues.push({
1308
+ rule: "CTR-004",
1309
+ severity: "warning",
1310
+ message: `Endpoint '${name}' uses legacy requiresAuth: false instead of Permission.NO_PERMISSION`,
1311
+ endpointName: name,
1312
+ filePath,
1313
+ line,
1314
+ suggestion: "Replace with requiredPermission: Permission.NO_PERMISSION"
1315
+ });
1316
+ } else {
1317
+ issues.push({
1318
+ rule: "CTR-004",
1319
+ severity: "error",
1320
+ message: `Endpoint '${name}' is missing requiredPermission`,
1321
+ endpointName: name,
1322
+ filePath,
1323
+ line,
1324
+ suggestion: "Add requiredPermission: Permission.XXX (or Permission.NO_PERMISSION for public endpoints)"
1325
+ });
1326
+ }
1327
+ }
1328
+ if (metadata.requiredPermission && ctx.validPermissions.length > 0) {
1329
+ const permission = normalizePermissionValue(
1330
+ metadata.requiredPermission,
1331
+ ctx.permissionKeyToValue
1332
+ );
1333
+ if (!ctx.validPermissions.includes(permission)) {
1334
+ issues.push({
1335
+ rule: "CTR-005",
1336
+ severity: "error",
1337
+ message: `Endpoint '${name}' uses invalid permission '${metadata.requiredPermission}'`,
1338
+ endpointName: name,
1339
+ filePath,
1340
+ line,
1341
+ suggestion: `Valid permissions: ${ctx.validPermissions.slice(0, 5).join(", ")}...`
1342
+ });
1343
+ }
1344
+ }
1345
+ if (metadata.bff?.apiServices && ctx.validApiServices.length > 0) {
1346
+ for (const service of metadata.bff.apiServices) {
1347
+ const normalizedService = normalizeApiServiceValue(service);
1348
+ if (!ctx.validApiServices.includes(normalizedService)) {
1349
+ issues.push({
1350
+ rule: "CTR-006",
1351
+ severity: "error",
1352
+ message: `Endpoint '${name}' uses invalid apiService '${service}'`,
1353
+ endpointName: name,
1354
+ filePath,
1355
+ line,
1356
+ suggestion: `Valid services: ${ctx.validApiServices.join(", ")}`
1357
+ });
1358
+ }
1359
+ }
1360
+ }
1361
+ return issues;
1362
+ }
1363
+ function normalizePermissionValue(value, keyToValue) {
1364
+ if (value.includes(".")) {
1365
+ const parts = value.split(".");
1366
+ const enumKey = parts[parts.length - 1] ?? value;
1367
+ if (keyToValue && enumKey in keyToValue) {
1368
+ const resolved = keyToValue[enumKey];
1369
+ if (resolved) return resolved;
1370
+ }
1371
+ return enumKey;
1372
+ }
1373
+ return value;
1374
+ }
1375
+ function normalizeApiServiceValue(value) {
1376
+ const serviceMap = {
1377
+ SEE_CORE: "seeCore",
1378
+ LOCUS: "locus",
1379
+ SEE_BUDGET: "seeBudget",
1380
+ SMD: "smd",
1381
+ SEE_PROVIDER: "seeProvider"
1382
+ };
1383
+ if (serviceMap[value]) {
1384
+ return serviceMap[value];
1385
+ }
1386
+ return value;
1387
+ }
1388
+
1389
+ // src/lib/contracts/generators/types.ts
1390
+ var API_SERVICE_CONFIG = {
1391
+ seeCore: {
1392
+ moduleName: "SeeCoreModule",
1393
+ moduleImport: "@/common/apis/seeCore/seeCore.module",
1394
+ serviceName: "SeeCoreService",
1395
+ serviceImport: "@/common/apis/seeCore/seeCore.service",
1396
+ configMethod: "getAPIConfig",
1397
+ configReturn: { baseUrl: "seeCoreBaseUrl", apiKey: "seeCoreApiKey" }
1398
+ },
1399
+ locus: {
1400
+ moduleName: "LocusModule",
1401
+ moduleImport: "@/common/apis/locus/locus.module",
1402
+ serviceName: "LocusService",
1403
+ serviceImport: "@/common/apis/locus/locus.service",
1404
+ configMethod: "getAPIConfig",
1405
+ configReturn: { baseUrl: "locusBaseUrl", apiKey: "locusApiKey" }
1406
+ },
1407
+ seeBudget: {
1408
+ moduleName: "SeeBudgetModule",
1409
+ moduleImport: "@/common/apis/seeBudget/seeBudget.module",
1410
+ serviceName: "SeeBudgetService",
1411
+ serviceImport: "@/common/apis/seeBudget/seeBudget.service",
1412
+ configMethod: "getAPIConfig",
1413
+ configReturn: { baseUrl: "seeBudgetBaseUrl", apiKey: "seeBudgetApiKey" }
1414
+ },
1415
+ smd: {
1416
+ moduleName: "SmdModule",
1417
+ moduleImport: "@/common/apis/smd/smd.module",
1418
+ serviceName: "SmdService",
1419
+ serviceImport: "@/common/apis/smd/smd.service",
1420
+ configMethod: "getAPIConfig",
1421
+ configReturn: { baseUrl: "smdBaseUrl", apiKey: "smdApiKey" }
1422
+ },
1423
+ seeProvider: {
1424
+ moduleName: "SeeProviderModule",
1425
+ moduleImport: "@/common/apis/seeProvider/seeProvider.module",
1426
+ serviceName: "SeeProviderService",
1427
+ serviceImport: "@/common/apis/seeProvider/seeProvider.service",
1428
+ configMethod: "getAPIConfig",
1429
+ configReturn: {
1430
+ baseUrl: "seeProviderBaseUrl",
1431
+ apiKey: "seeProviderApiKey"
1432
+ }
1433
+ }
1434
+ };
1435
+ function toCase(name, targetCase) {
1436
+ const words = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/-/g, " ").replace(/_/g, " ").toLowerCase().split(" ").filter(Boolean);
1437
+ switch (targetCase) {
1438
+ case "pascal":
1439
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1440
+ case "camel":
1441
+ return words.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("");
1442
+ case "snake":
1443
+ return words.join("_").toUpperCase();
1444
+ case "kebab":
1445
+ return words.join("-");
1446
+ }
1447
+ }
1448
+ function toPermissionConstant(permissionValue) {
1449
+ if (permissionValue.includes(".")) {
1450
+ const parts = permissionValue.split(".");
1451
+ return parts[parts.length - 1] ?? permissionValue;
1452
+ }
1453
+ return permissionValue.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
1454
+ }
1455
+ function getSuccessStatus(responses) {
1456
+ if (!responses) return 200;
1457
+ const statusCodes = Object.keys(responses).map(Number).filter((code) => code >= 200 && code < 300).sort();
1458
+ return statusCodes[0] ?? 200;
1459
+ }
1460
+
1461
+ // src/lib/contracts/generators/module.generator.ts
1462
+ function generateModule(ctx) {
1463
+ const { featureName, featureNameCamel, apiServices } = ctx;
1464
+ const apiModuleImports = apiServices.map((service) => {
1465
+ const config = API_SERVICE_CONFIG[service];
1466
+ if (!config) return null;
1467
+ return `import { ${config.moduleName} } from "${config.moduleImport}";`;
1468
+ }).filter(Boolean).join("\n");
1469
+ const apiModuleNames = apiServices.map((service) => API_SERVICE_CONFIG[service]?.moduleName).filter(Boolean);
1470
+ const content = `import { Module } from "@nestjs/common";
1471
+ ${apiModuleImports}
1472
+ import { AuthorizationModule } from "@/common/security/authorization/module/authorization.module";
1473
+ import { TsRestClientModule } from "@/common/ts-rest/tsrestclient.module";
1474
+
1475
+ import { ${featureName}Config } from "./config/${featureNameCamel}.config";
1476
+ import { ${featureName}Controller } from "./controller/${featureNameCamel}.controller";
1477
+ import { ${featureName}Service } from "./service/${featureNameCamel}.service";
1478
+ import { ${featureNameCamel}QueryProvider } from "./service/${featureNameCamel}Query.factory";
1479
+
1480
+ @Module({
1481
+ controllers: [${featureName}Controller],
1482
+ providers: [${featureName}Service, ${featureName}Config, ${featureNameCamel}QueryProvider],
1483
+ imports: [AuthorizationModule, TsRestClientModule${apiModuleNames.length > 0 ? ", " + apiModuleNames.join(", ") : ""}],
1484
+ exports: [${featureName}Service],
1485
+ })
1486
+ export class ${featureName}Module {}
1487
+ `;
1488
+ return {
1489
+ path: `${featureNameCamel}.module.ts`,
1490
+ content,
1491
+ isComplete: true
1492
+ };
1493
+ }
1494
+
1495
+ // src/lib/contracts/generators/controller.generator.ts
1496
+ function generateController(ctx) {
1497
+ const {
1498
+ featureName,
1499
+ featureNameCamel,
1500
+ contractName,
1501
+ contractsLib,
1502
+ endpoint,
1503
+ permissionConstant,
1504
+ successStatus
1505
+ } = ctx;
1506
+ const pathParams = extractPathParams(endpoint.path);
1507
+ const hasPathParams = pathParams.length > 0;
1508
+ const paramImports = hasPathParams ? ", Param" : "";
1509
+ const paramDecorators = pathParams.map((param) => `@Param("${param}") ${param}: string`).join(", ");
1510
+ const httpMethod = endpoint.method.charAt(0).toUpperCase() + endpoint.method.slice(1).toLowerCase();
1511
+ const needsBody = ["POST", "PUT", "PATCH"].includes(
1512
+ endpoint.method.toUpperCase()
1513
+ );
1514
+ const bodyImport = needsBody ? ", Body" : "";
1515
+ const serviceParams = pathParams.join(", ");
1516
+ const content = `import { ${contractName} } from "${contractsLib}";
1517
+ import { Controller, HttpCode, Inject${paramImports}${bodyImport}, ${httpMethod} } from "@nestjs/common";
1518
+ import { TsRestHandler, tsRestHandler } from "@ts-rest/nest";
1519
+
1520
+ import { Authorize } from "@/common/security/authorization/decorators/authorize.decorator";
1521
+ import { Permissions } from "@/common/security/session/constants/permissions.constants";
1522
+
1523
+ import { ${featureName}Service } from "../service/${featureNameCamel}.service";
1524
+
1525
+ @Controller()
1526
+ export class ${featureName}Controller {
1527
+ constructor(
1528
+ @Inject(${featureName}Service)
1529
+ private readonly ${featureNameCamel}Service: ${featureName}Service,
1530
+ ) {}
1531
+
1532
+ @${httpMethod}("${endpoint.path}")
1533
+ @Authorize(Permissions.${permissionConstant})
1534
+ @TsRestHandler(${contractName}.${endpoint.name})
1535
+ @HttpCode(${successStatus})
1536
+ async ${endpoint.name}(${paramDecorators}) {
1537
+ return tsRestHandler(${contractName}.${endpoint.name}, async () => {
1538
+ await this.${featureNameCamel}Service.${endpoint.name}(${serviceParams});
1539
+ return { status: ${successStatus} as const, body: undefined };
1540
+ });
1541
+ }
1542
+ }
1543
+ `;
1544
+ return {
1545
+ path: `controller/${featureNameCamel}.controller.ts`,
1546
+ content,
1547
+ isComplete: true
1548
+ };
1549
+ }
1550
+ function extractPathParams(path) {
1551
+ const matches = path.match(/:(\w+)/g);
1552
+ if (!matches) return [];
1553
+ return matches.map((m) => m.slice(1));
1554
+ }
1555
+
1556
+ // src/lib/contracts/generators/config.generator.ts
1557
+ function generateConfig(ctx) {
1558
+ const { featureName, featureNameCamel, apiServices } = ctx;
1559
+ const primaryService = apiServices[0];
1560
+ const config = primaryService ? API_SERVICE_CONFIG[primaryService] : null;
1561
+ if (!config) {
1562
+ const content2 = `import { Injectable } from "@nestjs/common";
1563
+
1564
+ @Injectable()
1565
+ export class ${featureName}Config {
1566
+ public get${featureName}Config() {
1567
+ return {};
1568
+ }
1569
+ }
1570
+ `;
1571
+ return {
1572
+ path: `config/${featureNameCamel}.config.ts`,
1573
+ content: content2,
1574
+ isComplete: true
1575
+ };
1576
+ }
1577
+ const serviceImports = apiServices.map((service) => {
1578
+ const svcConfig = API_SERVICE_CONFIG[service];
1579
+ if (!svcConfig) return null;
1580
+ return `import { ${svcConfig.serviceName} } from "${svcConfig.serviceImport}";`;
1581
+ }).filter(Boolean).join("\n");
1582
+ const constructorParams = apiServices.map((service) => {
1583
+ const svcConfig = API_SERVICE_CONFIG[service];
1584
+ if (!svcConfig) return null;
1585
+ const serviceCamel = svcConfig.serviceName.charAt(0).toLowerCase() + svcConfig.serviceName.slice(1);
1586
+ return ` @Inject(${svcConfig.serviceName}) private readonly ${serviceCamel}: ${svcConfig.serviceName},`;
1587
+ }).filter(Boolean).join("\n");
1588
+ const configReturns = apiServices.map((service) => {
1589
+ const svcConfig = API_SERVICE_CONFIG[service];
1590
+ if (!svcConfig) return null;
1591
+ const serviceCamel = svcConfig.serviceName.charAt(0).toLowerCase() + svcConfig.serviceName.slice(1);
1592
+ return ` const { ${svcConfig.configReturn.baseUrl}, ${svcConfig.configReturn.apiKey} } = this.${serviceCamel}.${svcConfig.configMethod}();`;
1593
+ }).filter(Boolean).join("\n");
1594
+ const returnProps = apiServices.map((service) => {
1595
+ const svcConfig = API_SERVICE_CONFIG[service];
1596
+ if (!svcConfig) return null;
1597
+ return `${svcConfig.configReturn.baseUrl}, ${svcConfig.configReturn.apiKey}`;
1598
+ }).filter(Boolean).join(", ");
1599
+ const content = `import { Inject, Injectable } from "@nestjs/common";
1600
+ ${serviceImports}
1601
+
1602
+ @Injectable()
1603
+ export class ${featureName}Config {
1604
+ constructor(
1605
+ ${constructorParams}
1606
+ ) {}
1607
+
1608
+ public get${featureName}Config() {
1609
+ ${configReturns}
1610
+ return { ${returnProps} };
1611
+ }
1612
+ }
1613
+ `;
1614
+ return {
1615
+ path: `config/${featureNameCamel}.config.ts`,
1616
+ content,
1617
+ isComplete: true
1618
+ };
1619
+ }
1620
+
1621
+ // src/lib/contracts/generators/service.generator.ts
1622
+ function generateService(ctx) {
1623
+ const { featureName, featureNameCamel, featureNameUpperSnake, endpoint } = ctx;
1624
+ const pathParams = extractPathParams2(endpoint.path);
1625
+ const methodParams = pathParams.map((p) => `${p}: string`).join(", ");
1626
+ const content = `import { Inject, Injectable } from "@nestjs/common";
1627
+
1628
+ import {
1629
+ ${featureNameUpperSnake}_QUERY,
1630
+ ${featureName}Query,
1631
+ } from "./${featureNameCamel}Query.factory";
1632
+
1633
+ @Injectable()
1634
+ export class ${featureName}Service {
1635
+ constructor(
1636
+ @Inject(${featureNameUpperSnake}_QUERY)
1637
+ private readonly ${featureNameCamel}Query: ${featureName}Query,
1638
+ ) {}
1639
+
1640
+ async ${endpoint.name}(${methodParams}): Promise<void> {
1641
+ // TODO: Implement business logic
1642
+ // 1. Call external API via query factory
1643
+ // 2. Transform response if needed
1644
+ // 3. Handle errors
1645
+
1646
+ const response = await this.${featureNameCamel}Query(${pathParams.join(", ")});
1647
+
1648
+ if (response.status !== 200 && response.status !== 204) {
1649
+ // TODO: Handle error response
1650
+ throw new Error(\`External API error: \${response.status}\`);
1651
+ }
1652
+ }
1653
+ }
1654
+ `;
1655
+ return {
1656
+ path: `service/${featureNameCamel}.service.ts`,
1657
+ content,
1658
+ isComplete: false
1659
+ // Has TODOs
1660
+ };
1661
+ }
1662
+ function extractPathParams2(path) {
1663
+ const matches = path.match(/:(\w+)/g);
1664
+ if (!matches) return [];
1665
+ return matches.map((m) => m.slice(1));
1666
+ }
1667
+
1668
+ // src/lib/contracts/generators/query-factory.generator.ts
1669
+ function generateQueryFactory(ctx) {
1670
+ const {
1671
+ featureName,
1672
+ featureNameCamel,
1673
+ featureNameUpperSnake,
1674
+ apiServices,
1675
+ endpoint
1676
+ } = ctx;
1677
+ const primaryService = apiServices[0];
1678
+ const config = primaryService ? API_SERVICE_CONFIG[primaryService] : null;
1679
+ const baseUrlVar = config?.configReturn.baseUrl ?? "baseUrl";
1680
+ const apiKeyVar = config?.configReturn.apiKey ?? "apiKey";
1681
+ const pathParams = extractPathParams3(endpoint.path);
1682
+ const methodParams = pathParams.map((p) => `${p}: string`).join(", ");
1683
+ const content = `import {
1684
+ SESSION_REQUEST_HEADERS_WITH_AUTH,
1685
+ SessionRequestHeadersWithAuthFactory,
1686
+ } from "@/common/security/session/services/sessionUserRequest.provider";
1687
+ import { TSREST_INIT_CLIENT_SERVICE } from "@/common/ts-rest/tsrestclient.service";
1688
+ import { TsRestInitClient } from "@/common/ts-rest/types";
1689
+
1690
+ import { ${featureName}Config } from "../config/${featureNameCamel}.config";
1691
+ import { ${featureName}ExternalContract } from "../${featureNameCamel}.external.contract";
1692
+
1693
+ export const ${featureNameUpperSnake}_QUERY = "${featureNameUpperSnake}_QUERY";
1694
+
1695
+ export type ${featureName}Query = ReturnType<typeof create${featureName}QueryFactory>;
1696
+
1697
+ export const ${featureNameCamel}QueryProvider = {
1698
+ provide: ${featureNameUpperSnake}_QUERY,
1699
+ useFactory: create${featureName}QueryFactory,
1700
+ inject: [
1701
+ ${featureName}Config,
1702
+ SESSION_REQUEST_HEADERS_WITH_AUTH,
1703
+ TSREST_INIT_CLIENT_SERVICE,
1704
+ ],
1705
+ };
1706
+
1707
+ export function create${featureName}QueryFactory(
1708
+ configService: ${featureName}Config,
1709
+ getRequestHeaders: SessionRequestHeadersWithAuthFactory,
1710
+ initClient: TsRestInitClient,
1711
+ ) {
1712
+ function ${featureNameCamel}Query(${methodParams}) {
1713
+ const { ${baseUrlVar}, ${apiKeyVar} } = configService.get${featureName}Config();
1714
+
1715
+ const client = initClient(${featureName}ExternalContract, {
1716
+ baseUrl: ${baseUrlVar},
1717
+ });
1718
+
1719
+ return client.${endpoint.name}({
1720
+ params: {
1721
+ // TODO: Map path params from contract to external API
1722
+ ${pathParams.map((p) => `// ${p},`).join("\n ")}
1723
+ },
1724
+ body: {
1725
+ // TODO: Map request body if needed
1726
+ },
1727
+ headers: getRequestHeaders({ apiKey: ${apiKeyVar} }),
1728
+ });
1729
+ }
1730
+
1731
+ return ${featureNameCamel}Query;
1732
+ }
1733
+ `;
1734
+ return {
1735
+ path: `service/${featureNameCamel}Query.factory.ts`,
1736
+ content,
1737
+ isComplete: false
1738
+ // Has TODOs
1739
+ };
1740
+ }
1741
+ function extractPathParams3(path) {
1742
+ const matches = path.match(/:(\w+)/g);
1743
+ if (!matches) return [];
1744
+ return matches.map((m) => m.slice(1));
1745
+ }
1746
+
1747
+ // src/lib/contracts/generators/external-contract.generator.ts
1748
+ function generateExternalContract(ctx) {
1749
+ const { featureName, featureNameCamel, apiServices, endpoint } = ctx;
1750
+ const apiComment = apiServices.length > 0 ? `// API: ${apiServices.join(", ")} (see config/${featureNameCamel}.config.ts for reference)` : "// API: (not specified in metadata)";
1751
+ const content = `// TODO: Define external API contract
1752
+ ${apiComment}
1753
+ // Reference: src/domains/serviceExecution/skipContract/skipContract.external.contract.ts
1754
+
1755
+ import { initContract } from "@ts-rest/core";
1756
+ import { z } from "zod";
1757
+ import { Headers } from "@/common/contants/headers.contants";
1758
+
1759
+ const c = initContract();
1760
+
1761
+ export const ${featureName}ExternalContract = c.router({
1762
+ ${endpoint.name}: {
1763
+ method: "${endpoint.method}",
1764
+ path: "/v1/...", // TODO: External API path
1765
+ pathParams: z.object({
1766
+ // TODO: Define path params for external API
1767
+ }),
1768
+ headers: z.object({
1769
+ [Headers.AUTHORIZATION]: z.string(),
1770
+ [Headers.X_GATEWAY_APIKEY]: z.string(),
1771
+ [Headers.X_BU_CODE]: z.string(),
1772
+ }),
1773
+ body: z.object({
1774
+ // TODO: External API body schema
1775
+ }),
1776
+ responses: {
1777
+ 200: z.object({
1778
+ // TODO: Success response schema
1779
+ }),
1780
+ 400: z.object({
1781
+ status: z.number(),
1782
+ message: z.string().nullable(),
1783
+ }),
1784
+ },
1785
+ },
1786
+ });
1787
+ `;
1788
+ return {
1789
+ path: `${featureNameCamel}.external.contract.ts`,
1790
+ content,
1791
+ isComplete: false
1792
+ // Has TODOs
1793
+ };
1794
+ }
1795
+
1796
+ // src/lib/contracts/generators/e2e-test.generator.ts
1797
+ function generateE2eTest(ctx) {
1798
+ const {
1799
+ featureName,
1800
+ featureNameCamel,
1801
+ apiServices,
1802
+ endpoint,
1803
+ permissionConstant,
1804
+ successStatus
1805
+ } = ctx;
1806
+ const primaryService = apiServices[0];
1807
+ const config = primaryService ? API_SERVICE_CONFIG[primaryService] : null;
1808
+ const mockBaseUrl = config && primaryService ? `https://mock-${primaryService.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "")}-api` : "https://mock-api";
1809
+ const pathParams = extractPathParams4(endpoint.path);
1810
+ const testPath = endpoint.path.replace(
1811
+ /:(\w+)/g,
1812
+ (_, param) => `\${${param.toUpperCase()}}`
1813
+ );
1814
+ const fixtureConstants = pathParams.map(
1815
+ (p) => ` const ${p.toUpperCase()} = "12345"; // TODO: Use appropriate test value`
1816
+ ).join("\n");
1817
+ const permissionCamel = permissionConstant.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1818
+ const content = `import { INestApplication } from "@nestjs/common";
1819
+ import { JwtService } from "@nestjs/jwt";
1820
+ import {
1821
+ TEST_ROLES,
1822
+ initServerApp,
1823
+ mockExpiredToken,
1824
+ mockToken,
1825
+ } from "src/test/mock/mock.utils";
1826
+ import request from "supertest";
1827
+ import {
1828
+ afterAll,
1829
+ beforeAll,
1830
+ beforeEach,
1831
+ describe,
1832
+ expect,
1833
+ it,
1834
+ vi,
1835
+ } from "vitest";
1836
+
1837
+ import { Headers } from "@/common/contants/headers.contants";
1838
+ import { BACK_PARAMETERS } from "@/test/mock/backParameters";
1839
+ import { getCookieHeader } from "@/test/utils/test.utils";
1840
+
1841
+ describe("${featureName}Controller (e2e)", () => {
1842
+ let app: INestApplication;
1843
+ let jwtService: JwtService;
1844
+ let mock${featureName}Fn: ReturnType<typeof vi.fn>;
1845
+ let mockInitClient: ReturnType<typeof vi.fn>;
1846
+ let mockGetExternalParametersFn: ReturnType<typeof vi.fn>;
1847
+
1848
+ ${fixtureConstants}
1849
+
1850
+ beforeAll(async () => {
1851
+ mock${featureName}Fn = vi.fn();
1852
+ mockGetExternalParametersFn = vi.fn();
1853
+ mockGetExternalParametersFn.mockResolvedValue(BACK_PARAMETERS);
1854
+
1855
+ mockInitClient = vi.fn().mockReturnValue({
1856
+ ${endpoint.name}: mock${featureName}Fn,
1857
+ getExternalParameters: mockGetExternalParametersFn,
1858
+ });
1859
+
1860
+ app = await initServerApp(mockInitClient);
1861
+ jwtService = app.get(JwtService);
1862
+ await app.listen(0);
1863
+ });
1864
+
1865
+ afterAll(async () => {
1866
+ await app.close();
1867
+ });
1868
+
1869
+ beforeEach(() => {
1870
+ mock${featureName}Fn.mockReset();
1871
+ mockInitClient.mockClear();
1872
+ });
1873
+
1874
+ const verifyApiHeaders = () => {
1875
+ expect(mockInitClient).toHaveBeenCalled();
1876
+ expect(mockInitClient.mock.calls[0][1].baseUrl).toBe("${mockBaseUrl}");
1877
+ expect(mock${featureName}Fn).toHaveBeenCalled();
1878
+ expect(
1879
+ mock${featureName}Fn.mock.calls[0][0].headers[Headers.X_GATEWAY_APIKEY],
1880
+ ).toBe("see"); // TODO: Verify expected API key
1881
+ expect(
1882
+ mock${featureName}Fn.mock.calls[0][0].headers[Headers.AUTHORIZATION],
1883
+ ).toBe("Bearer token");
1884
+ expect(mock${featureName}Fn.mock.calls[0][0].headers[Headers.X_BU_CODE]).toBe(
1885
+ "LMES",
1886
+ );
1887
+ };
1888
+
1889
+ describe.each(TEST_ROLES)(
1890
+ "User with $role role",
1891
+ ({ roleValue, ${permissionCamel} }) => {
1892
+ it("${endpoint.method} Valid request: ${successStatus}", async () => {
1893
+ mockToken(jwtService, roleValue);
1894
+
1895
+ if (${permissionCamel}) {
1896
+ mock${featureName}Fn.mockResolvedValue({ status: ${successStatus} });
1897
+ }
1898
+
1899
+ const response = await request(app.getHttpServer())
1900
+ .${endpoint.method.toLowerCase()}(\`${testPath}\`)
1901
+ .send({}) // TODO: Add valid request body
1902
+ .set("x-kobi-authentication-cookie-name", "ahs-operator-portal.jwt")
1903
+ .set("cookie", getCookieHeader("token", "ES"));
1904
+
1905
+ expect(response.status).toBe(${permissionCamel} ? ${successStatus} : 403);
1906
+
1907
+ if (${permissionCamel}) {
1908
+ expect(mock${featureName}Fn).toHaveBeenCalled();
1909
+ // TODO: Verify body transformation
1910
+ verifyApiHeaders();
1911
+ } else {
1912
+ expect(mock${featureName}Fn).not.toHaveBeenCalled();
1913
+ }
1914
+ });
1915
+
1916
+ it("${endpoint.method} Invalid request: 400", async () => {
1917
+ mockToken(jwtService, roleValue);
1918
+ if (${permissionCamel}) {
1919
+ mock${featureName}Fn.mockResolvedValue({ status: 400 });
1920
+ }
1921
+
1922
+ const response = await request(app.getHttpServer())
1923
+ .${endpoint.method.toLowerCase()}(\`${testPath}\`)
1924
+ .send({}) // TODO: Add invalid request body
1925
+ .set("x-kobi-authentication-cookie-name", "ahs-operator-portal.jwt")
1926
+ .set("cookie", getCookieHeader("token", "ES"));
1927
+
1928
+ if (${permissionCamel}) {
1929
+ expect(response.status).toBe(400);
1930
+ expect(mock${featureName}Fn).toHaveBeenCalled();
1931
+ } else {
1932
+ expect(response.status).toBe(403);
1933
+ expect(mock${featureName}Fn).not.toHaveBeenCalled();
1934
+ }
1935
+ });
1936
+
1937
+ it("${endpoint.method} Unauthorized: 401", async () => {
1938
+ mockExpiredToken(jwtService);
1939
+
1940
+ const response = await request(app.getHttpServer())
1941
+ .${endpoint.method.toLowerCase()}(\`${testPath}\`)
1942
+ .send({})
1943
+ .set("x-kobi-authentication-cookie-name", "ahs-operator-execution.jwt")
1944
+ .set("cookie", getCookieHeader("token", "ES"));
1945
+
1946
+ expect(response.status).toBe(401);
1947
+ expect(mock${featureName}Fn).not.toHaveBeenCalled();
1948
+ });
1949
+ },
1950
+ );
1951
+ });
1952
+ `;
1953
+ return {
1954
+ path: `controller/__tests__/${featureNameCamel}.controller.e2e.spec.ts`,
1955
+ content,
1956
+ isComplete: false
1957
+ // Has TODOs
1958
+ };
1959
+ }
1960
+ function extractPathParams4(path) {
1961
+ const matches = path.match(/:(\w+)/g);
1962
+ if (!matches) return [];
1963
+ return matches.map((m) => m.slice(1));
1964
+ }
1965
+
1966
+ // src/lib/contracts/generators/types.generator.ts
1967
+ function generateTypes(ctx) {
1968
+ const { featureName, featureNameCamel } = ctx;
1969
+ const content = `// TODO: Define types specific to ${featureName}
1970
+ // These types are for internal use within this feature
1971
+
1972
+ /**
1973
+ * Request payload after transformation (if different from contract)
1974
+ */
1975
+ export interface ${featureName}Request {
1976
+ // TODO: Define internal request type
1977
+ }
1978
+
1979
+ /**
1980
+ * Response type from external API (if different from contract response)
1981
+ */
1982
+ export interface ${featureName}ExternalResponse {
1983
+ // TODO: Define external API response type
1984
+ }
1985
+ `;
1986
+ return {
1987
+ path: `${featureNameCamel}.types.ts`,
1988
+ content,
1989
+ isComplete: false
1990
+ // Has TODOs
1991
+ };
1992
+ }
1993
+
1994
+ // src/lib/contracts/generators/index.ts
1995
+ function generateNestJsFeature(endpoint, options) {
1996
+ const errors = [];
1997
+ const baseName = options.featureName ?? endpoint.name;
1998
+ const featureName = toCase(baseName, "pascal");
1999
+ const featureNameCamel = toCase(baseName, "camel");
2000
+ const featureNameUpperSnake = toCase(baseName, "snake");
2001
+ const featureNameKebab = toCase(baseName, "kebab");
2002
+ const apiServices = endpoint.metadata?.bff?.apiServices ?? [];
2003
+ const permissionValue = endpoint.metadata?.requiredPermission ?? "NO_PERMISSION";
2004
+ const permissionConstant = toPermissionConstant(permissionValue);
2005
+ const ctx = {
2006
+ featureName,
2007
+ featureNameCamel,
2008
+ featureNameUpperSnake,
2009
+ featureNameKebab,
2010
+ contractName: options.contractName,
2011
+ contractsLib: options.contractsLib,
2012
+ endpoint,
2013
+ apiServices,
2014
+ permissionConstant,
2015
+ successStatus: getSuccessStatus(void 0)
2016
+ // TODO: pass responses from endpoint
2017
+ };
2018
+ const files = [
2019
+ generateModule(ctx),
2020
+ generateController(ctx),
2021
+ generateConfig(ctx),
2022
+ generateService(ctx),
2023
+ generateQueryFactory(ctx),
2024
+ generateExternalContract(ctx),
2025
+ generateE2eTest(ctx),
2026
+ generateTypes(ctx)
2027
+ ];
2028
+ return { files, errors };
2029
+ }
2030
+ function summarizeGeneration(result) {
2031
+ const complete = result.files.filter((f) => f.isComplete);
2032
+ const skeleton = result.files.filter((f) => !f.isComplete);
2033
+ const lines = [];
2034
+ if (complete.length > 0) {
2035
+ lines.push(`\u2705 Complete files (${complete.length}):`);
2036
+ for (const file of complete) {
2037
+ lines.push(` - ${file.path}`);
2038
+ }
2039
+ }
2040
+ if (skeleton.length > 0) {
2041
+ lines.push(`\u{1F4DD} Skeleton files with TODOs (${skeleton.length}):`);
2042
+ for (const file of skeleton) {
2043
+ lines.push(` - ${file.path}`);
2044
+ }
2045
+ }
2046
+ if (result.errors.length > 0) {
2047
+ lines.push(`\u274C Errors (${result.errors.length}):`);
2048
+ for (const error of result.errors) {
2049
+ lines.push(` - ${error}`);
2050
+ }
2051
+ }
2052
+ return lines.join("\n");
2053
+ }
2054
+
2055
+ // src/lint/checkers/contracts/index.ts
2056
+ var contractsChecker = {
2057
+ name: "contracts",
2058
+ rules: getRulesByCategory("contracts"),
2059
+ async check(ctx) {
2060
+ const results = [];
2061
+ const configResult = loadConfig(ctx.cwd);
2062
+ if (!configResult.success) {
2063
+ return [];
2064
+ }
2065
+ const config = configResult.config;
2066
+ if (config.project.type !== "contracts-lib") {
2067
+ return [];
2068
+ }
2069
+ const permissionsConfig = getPermissionsConfig(config);
2070
+ let validPermissions = [];
2071
+ let permissionKeyToValue;
2072
+ if (permissionsConfig) {
2073
+ const sourcePath = resolveConfigPath(ctx.cwd, permissionsConfig.source);
2074
+ const permissionsResult = extractValues(sourcePath, permissionsConfig);
2075
+ if (permissionsResult.success) {
2076
+ validPermissions = permissionsResult.values;
2077
+ permissionKeyToValue = permissionsResult.keyToValue;
2078
+ } else {
2079
+ results.push({
2080
+ ruleId: "CTR-005",
2081
+ severity: "warning",
2082
+ message: `Failed to load permissions: ${permissionsResult.error}`,
2083
+ file: permissionsConfig.source,
2084
+ suggestion: "Check lint.contracts.metadata.permissions config in .hexa-ts-kit.yaml"
2085
+ });
2086
+ }
2087
+ }
2088
+ const apiServicesConfig = getApiServicesConfig(config);
2089
+ let validApiServices = [];
2090
+ if (apiServicesConfig) {
2091
+ const sourcePath = resolveConfigPath(ctx.cwd, apiServicesConfig.source);
2092
+ const apiServicesResult = extractValues(sourcePath, apiServicesConfig);
2093
+ if (apiServicesResult.success) {
2094
+ validApiServices = apiServicesResult.values;
2095
+ } else {
2096
+ results.push({
2097
+ ruleId: "CTR-006",
2098
+ severity: "warning",
2099
+ message: `Failed to load apiServices: ${apiServicesResult.error}`,
2100
+ file: apiServicesConfig.source,
2101
+ suggestion: "Check lint.contracts.metadata.apiServices config in .hexa-ts-kit.yaml"
2102
+ });
2103
+ }
2104
+ }
2105
+ const parseResult = await parseContracts(ctx.cwd);
2106
+ if (!parseResult.success) {
2107
+ results.push({
2108
+ ruleId: "CTR-001",
2109
+ severity: "error",
2110
+ message: `Failed to parse contracts: ${parseResult.error}`,
2111
+ file: ctx.cwd
2112
+ });
2113
+ return results;
2114
+ }
2115
+ const disabledRules = getDisabledRules(config);
2116
+ const validationResult = validateContracts(parseResult.contracts, {
2117
+ validPermissions,
2118
+ validApiServices,
2119
+ permissionKeyToValue
2120
+ });
2121
+ for (const issue of validationResult.issues) {
2122
+ if (disabledRules.includes(issue.rule)) {
2123
+ continue;
2124
+ }
2125
+ results.push({
2126
+ ruleId: issue.rule,
2127
+ severity: issue.severity,
2128
+ message: issue.message,
2129
+ file: issue.filePath,
2130
+ line: issue.line,
2131
+ suggestion: issue.suggestion
2132
+ });
2133
+ }
2134
+ return results;
2135
+ }
2136
+ };
2137
+
2138
+ // src/lint/reporters/console.ts
2139
+ import pc from "picocolors";
2140
+ var severityColors = {
2141
+ error: pc.red,
2142
+ warning: pc.yellow,
2143
+ info: pc.blue
2144
+ };
2145
+ var severityIcons = {
2146
+ error: "\u2716",
2147
+ warning: "\u26A0",
2148
+ info: "\u2139"
2149
+ };
2150
+ function formatConsole(results) {
2151
+ if (results.length === 0) {
2152
+ return pc.green("\u2713 No issues found");
2153
+ }
2154
+ const byFile = /* @__PURE__ */ new Map();
2155
+ for (const result of results) {
2156
+ const existing = byFile.get(result.file) ?? [];
2157
+ existing.push(result);
2158
+ byFile.set(result.file, existing);
2159
+ }
2160
+ const lines = [];
2161
+ for (const [file, fileResults] of byFile) {
2162
+ lines.push("");
2163
+ lines.push(pc.underline(file));
2164
+ for (const result of fileResults) {
2165
+ const color = severityColors[result.severity];
2166
+ const icon = severityIcons[result.severity];
2167
+ const location = result.line !== void 0 ? `:${result.line}:${result.column ?? 0}` : "";
2168
+ lines.push(
2169
+ ` ${color(icon)} ${result.message} ${pc.dim(`[${result.ruleId}]`)}`
2170
+ );
2171
+ if (result.suggestion) {
2172
+ lines.push(` ${pc.dim("\u2192")} ${pc.cyan(result.suggestion)}`);
2173
+ }
2174
+ }
2175
+ }
2176
+ const errorCount = results.filter((r) => r.severity === "error").length;
2177
+ const warningCount = results.filter((r) => r.severity === "warning").length;
2178
+ const infoCount = results.filter((r) => r.severity === "info").length;
2179
+ lines.push("");
2180
+ lines.push(
2181
+ pc.bold(
2182
+ `${pc.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)}, ${pc.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}, ${pc.blue(`${infoCount} info`)}`
2183
+ )
2184
+ );
2185
+ return lines.join("\n");
2186
+ }
2187
+ function formatSummary(results) {
2188
+ return {
2189
+ errors: results.filter((r) => r.severity === "error").length,
2190
+ warnings: results.filter((r) => r.severity === "warning").length,
2191
+ info: results.filter((r) => r.severity === "info").length,
2192
+ total: results.length
2193
+ };
2194
+ }
2195
+
2196
+ // src/commands/lint.ts
2197
+ var contractsLibCheckers = [contractsChecker];
2198
+ function detectProjectType(cwd) {
2199
+ const configResult = loadConfig(cwd);
2200
+ if (!configResult.success) {
2201
+ return {
2202
+ type: null,
2203
+ checkers: [],
2204
+ supported: false,
2205
+ message: "No .hexa-ts-kit.yaml found. Skipping lint."
2206
+ };
2207
+ }
2208
+ const projectType = configResult.config.project.type;
2209
+ switch (projectType) {
2210
+ case "contracts-lib":
2211
+ return {
2212
+ type: "contracts-lib",
2213
+ checkers: contractsLibCheckers,
2214
+ supported: true
2215
+ };
2216
+ case "vue-frontend":
2217
+ return {
2218
+ type: "vue-frontend",
2219
+ checkers: [],
2220
+ supported: false,
2221
+ message: "vue-frontend lint not yet implemented. Skipping. (Coming soon)"
2222
+ };
2223
+ case "nestjs-bff":
2224
+ return {
2225
+ type: "nestjs-bff",
2226
+ checkers: [],
2227
+ supported: false,
2228
+ message: "nestjs-bff lint not yet implemented. Skipping. (Coming soon)"
2229
+ };
2230
+ default:
2231
+ return {
2232
+ type: null,
2233
+ checkers: [],
2234
+ supported: false,
2235
+ message: `Unknown project type: ${projectType}. Valid types: contracts-lib, vue-frontend, nestjs-bff`
2236
+ };
2237
+ }
2238
+ }
2239
+ function getChangedFiles(cwd) {
2240
+ try {
2241
+ const output = execSync("git diff --name-only HEAD", {
2242
+ cwd,
2243
+ encoding: "utf-8"
2244
+ });
2245
+ const stagedOutput = execSync("git diff --name-only --cached", {
2246
+ cwd,
2247
+ encoding: "utf-8"
2248
+ });
2249
+ const allFiles = [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
2250
+ return [...new Set(allFiles)];
2251
+ } catch {
2252
+ return [];
2253
+ }
2254
+ }
2255
+ async function lintCore(options) {
2256
+ const cwd = resolve2(options.path || ".");
2257
+ const projectConfig = detectProjectType(cwd);
2258
+ if (!projectConfig.supported) {
2259
+ return {
2260
+ success: true,
2261
+ // Not an error, just not supported yet
2262
+ command: "lint",
2263
+ projectType: projectConfig.type,
2264
+ message: projectConfig.message,
2265
+ results: [],
2266
+ summary: { errors: 0, warnings: 0, info: 0, total: 0 }
2267
+ };
2268
+ }
2269
+ let files = [];
2270
+ if (options.changed) {
2271
+ files = getChangedFiles(cwd);
2272
+ if (files.length === 0) {
2273
+ return {
2274
+ success: true,
2275
+ command: "lint",
2276
+ projectType: projectConfig.type,
2277
+ message: "No changed files to lint",
2278
+ files: [],
2279
+ results: [],
2280
+ summary: { errors: 0, warnings: 0, info: 0, total: 0 }
2281
+ };
2282
+ }
2283
+ }
2284
+ let checkersToRun = projectConfig.checkers;
2285
+ if (options.rules) {
2286
+ const prefixes = options.rules.split(",").map((r) => r.trim().toUpperCase());
2287
+ checkersToRun = checkersToRun.filter(
2288
+ (checker) => checker.rules.some(
2289
+ (rule) => prefixes.some((p) => rule.startsWith(p))
2290
+ )
2291
+ );
2292
+ }
2293
+ const allResults = [];
2294
+ for (const checker of checkersToRun) {
2295
+ const results = await checker.check({ cwd, files });
2296
+ if (options.changed && files.length > 0) {
2297
+ const filteredResults2 = results.filter(
2298
+ (r) => files.some((f) => r.file.endsWith(f))
2299
+ );
2300
+ allResults.push(...filteredResults2);
2301
+ } else {
2302
+ allResults.push(...results);
2303
+ }
2304
+ }
2305
+ const filteredResults = options.quiet ? allResults.filter((r) => r.severity === "error") : allResults;
2306
+ const summary = formatSummary(allResults);
2307
+ return {
2308
+ success: summary.errors === 0,
2309
+ command: "lint",
2310
+ projectType: projectConfig.type,
2311
+ files: options.changed ? files : void 0,
2312
+ results: filteredResults,
2313
+ summary
2314
+ };
2315
+ }
2316
+ async function lintCommand(path = ".", options) {
2317
+ const startTime = performance.now();
2318
+ const targetPath = options.cwd || path;
2319
+ if (options.debug) {
2320
+ console.log(`Linting: ${resolve2(targetPath)}`);
2321
+ const projectConfig = detectProjectType(resolve2(targetPath));
2322
+ console.log(`Project type: ${projectConfig.type ?? "not configured"}`);
2323
+ console.log(
2324
+ `Checkers: ${projectConfig.checkers.map((c) => c.name).join(", ") || "none"}`
2325
+ );
2326
+ }
2327
+ const result = await lintCore({
2328
+ path: targetPath,
2329
+ changed: options.changed,
2330
+ rules: options.rules,
2331
+ quiet: options.quiet
2332
+ });
2333
+ if (options.format === "json") {
2334
+ console.log(JSON.stringify(result, null, 2));
2335
+ } else {
2336
+ if (result.message) {
2337
+ console.log(result.message);
2338
+ } else {
2339
+ console.log(formatConsole(result.results));
2340
+ }
2341
+ }
2342
+ if (options.debug) {
2343
+ const elapsed = (performance.now() - startTime).toFixed(0);
2344
+ console.log(`
2345
+ Completed in ${elapsed}ms`);
2346
+ }
2347
+ process.exit(result.summary.errors > 0 ? 1 : 0);
2348
+ }
2349
+
2350
+ // src/commands/analyze.ts
2351
+ import { basename as basename4 } from "path";
2352
+ import { execSync as execSync2 } from "child_process";
2353
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2354
+ import fg8 from "fast-glob";
2355
+ import matter from "gray-matter";
2356
+ import { minimatch } from "minimatch";
2357
+ function expandPath(p) {
2358
+ if (p.startsWith("~")) {
2359
+ return p.replace("~", process.env.HOME || "");
2360
+ }
2361
+ return p;
2362
+ }
2363
+ function getChangedFiles2(cwd) {
2364
+ try {
2365
+ const output = execSync2("git diff --name-only HEAD", {
2366
+ cwd,
2367
+ encoding: "utf-8"
2368
+ });
2369
+ const stagedOutput = execSync2("git diff --name-only --cached", {
2370
+ cwd,
2371
+ encoding: "utf-8"
2372
+ });
2373
+ return [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
2374
+ } catch {
2375
+ return [];
2376
+ }
2377
+ }
2378
+ function loadKnowledgeMappings(knowledgePath) {
2379
+ const expandedPath = expandPath(knowledgePath);
2380
+ if (!existsSync5(expandedPath)) {
2381
+ return [];
2382
+ }
2383
+ const knowledgeFiles = fg8.sync("**/*.knowledge.md", {
2384
+ cwd: expandedPath,
2385
+ absolute: true
2386
+ });
2387
+ const mappings = [];
2388
+ for (const file of knowledgeFiles) {
2389
+ try {
2390
+ const content = readFileSync4(file, "utf-8");
2391
+ const { data } = matter(content);
2392
+ if (data.match) {
2393
+ mappings.push({
2394
+ name: data.name || basename4(file, ".knowledge.md"),
2395
+ path: file,
2396
+ match: data.match,
2397
+ description: data.description
2398
+ });
2399
+ }
2400
+ } catch {
2401
+ }
2402
+ }
2403
+ return mappings;
2404
+ }
2405
+ function matchFileToKnowledges(file, mappings) {
2406
+ const results = [];
2407
+ const fileName = basename4(file);
2408
+ for (const mapping of mappings) {
2409
+ if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
2410
+ results.push({
2411
+ file,
2412
+ knowledge: mapping.name,
2413
+ path: mapping.path
2414
+ });
2415
+ }
2416
+ }
2417
+ return results;
2418
+ }
2419
+ var DEFAULT_KNOWLEDGE_PATH = "~/.claude/marketplace/shared/knowledge";
2420
+ async function analyzeCore(options) {
2421
+ const cwd = process.cwd();
2422
+ const knowledgePath = options.knowledgePath || DEFAULT_KNOWLEDGE_PATH;
2423
+ let filesToAnalyze = options.files || [];
2424
+ if (options.changed) {
2425
+ filesToAnalyze = getChangedFiles2(cwd);
2426
+ }
2427
+ if (filesToAnalyze.length === 0) {
2428
+ return {
2429
+ success: true,
2430
+ command: "analyze",
2431
+ message: "No files to analyze",
2432
+ files: [],
2433
+ knowledges: []
2434
+ };
2435
+ }
2436
+ const mappings = loadKnowledgeMappings(knowledgePath);
2437
+ if (mappings.length === 0) {
2438
+ return {
2439
+ success: false,
2440
+ command: "analyze",
2441
+ error: `No knowledge files with 'match' pattern found in ${knowledgePath}`,
2442
+ files: filesToAnalyze,
2443
+ knowledges: []
2444
+ };
2445
+ }
2446
+ const allResults = [];
2447
+ for (const file of filesToAnalyze) {
2448
+ const matches = matchFileToKnowledges(file, mappings);
2449
+ allResults.push(...matches);
2450
+ }
2451
+ return {
2452
+ success: true,
2453
+ command: "analyze",
2454
+ files: filesToAnalyze,
2455
+ knowledges: allResults,
2456
+ availableMappings: mappings.length
2457
+ };
2458
+ }
2459
+ async function analyzeCommand(files = [], options) {
2460
+ const result = await analyzeCore({
2461
+ files,
2462
+ changed: options.changed,
2463
+ knowledgePath: options.knowledgePath
2464
+ });
2465
+ console.log(JSON.stringify(result, null, 2));
2466
+ }
2467
+
2468
+ // src/commands/scaffold.ts
2469
+ import { resolve as resolve3, dirname as dirname3, basename as basename5, join as join3 } from "path";
2470
+ import { mkdirSync, writeFileSync, existsSync as existsSync6 } from "fs";
2471
+ function toPascalCase(str) {
2472
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
2473
+ }
2474
+ function toCamelCase(str) {
2475
+ const pascal = toPascalCase(str);
2476
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
2477
+ }
2478
+ function generateVueFeature(featurePath) {
2479
+ const featureName = basename5(featurePath);
2480
+ const pascalName = toPascalCase(featureName);
2481
+ const camelName = toCamelCase(featureName);
2482
+ return [
2483
+ {
2484
+ path: `${featurePath}/${pascalName}.vue`,
2485
+ content: `<script setup lang="ts">
2486
+ import { use${pascalName} } from './${camelName}.composable';
2487
+
2488
+ const { /* state and methods */ } = use${pascalName}();
2489
+ </script>
2490
+
2491
+ <template>
2492
+ <div class="${featureName}">
2493
+ <!-- Template -->
2494
+ </div>
2495
+ </template>
2496
+ `,
2497
+ knowledge: "vue-component",
2498
+ rules: ["COL-001", "VUE-001"]
2499
+ },
2500
+ {
2501
+ path: `${featurePath}/${camelName}.composable.ts`,
2502
+ content: `import { ref, computed } from 'vue';
2503
+ import { ${camelName}Rules } from './${camelName}.rules';
2504
+ import type { ${pascalName}State } from './${camelName}.types';
2505
+
2506
+ export function use${pascalName}() {
2507
+ // State
2508
+
2509
+ // Computed
2510
+
2511
+ // Methods
2512
+
2513
+ return {
2514
+ // Expose state and methods
2515
+ };
2516
+ }
2517
+ `,
2518
+ knowledge: "vue-composable",
2519
+ rules: ["COL-002", "NAM-002", "NAM-003"]
2520
+ },
2521
+ {
2522
+ path: `${featurePath}/${camelName}.rules.ts`,
2523
+ content: `// Pure functions only - no framework imports, no side effects
2524
+
2525
+ export class ${pascalName}Rules {
2526
+ // Validation, calculation, transformation functions
2527
+
2528
+ static validate(input: unknown): boolean {
2529
+ // Implement validation logic
2530
+ return true;
2531
+ }
2532
+ }
2533
+
2534
+ export const ${camelName}Rules = ${pascalName}Rules;
2535
+ `,
2536
+ knowledge: "vue-rules",
2537
+ rules: ["RUL-001", "RUL-002", "NAM-008"]
2538
+ },
2539
+ {
2540
+ path: `${featurePath}/${camelName}.types.ts`,
2541
+ content: `export interface ${pascalName}State {
2542
+ // Define state types
2543
+ }
2544
+
2545
+ export interface ${pascalName}Props {
2546
+ // Define component props
2547
+ }
2548
+ `,
2549
+ knowledge: "typescript-types",
2550
+ rules: ["COL-010", "NAM-005"]
2551
+ },
2552
+ {
2553
+ path: `${featurePath}/${camelName}.query.ts`,
2554
+ content: `import { useQuery, useMutation } from '@tanstack/vue-query';
2555
+
2556
+ export function use${pascalName}Query() {
2557
+ // Implement TanStack Query hooks
2558
+ }
2559
+ `,
2560
+ knowledge: "tanstack-query",
2561
+ rules: ["NAM-007"]
2562
+ },
2563
+ {
2564
+ path: `${featurePath}/${camelName}.translations.ts`,
2565
+ content: `export const ${camelName}Translations = {
2566
+ fr: {
2567
+ // French translations
2568
+ },
2569
+ en: {
2570
+ // English translations
2571
+ },
2572
+ } as const;
2573
+ `,
2574
+ knowledge: "translations",
2575
+ rules: ["COL-011", "NAM-009"]
2576
+ },
2577
+ {
2578
+ path: `${featurePath}/__tests__/${camelName}.rules.test.ts`,
2579
+ content: `import { describe, it, expect } from 'vitest';
2580
+ import { ${pascalName}Rules } from '../${camelName}.rules';
2581
+
2582
+ describe('${pascalName}Rules', () => {
2583
+ describe('validate', () => {
2584
+ it('should validate correct input', () => {
2585
+ expect(${pascalName}Rules.validate({})).toBe(true);
2586
+ });
2587
+ });
2588
+ });
2589
+ `,
2590
+ knowledge: "testing-rules",
2591
+ rules: ["COL-004"]
2592
+ }
2593
+ ];
2594
+ }
2595
+ function generateNestJSFeature(featurePath) {
2596
+ const featureName = basename5(featurePath);
2597
+ const pascalName = toPascalCase(featureName);
2598
+ const camelName = toCamelCase(featureName);
2599
+ return [
2600
+ {
2601
+ path: `${featurePath}/${camelName}.module.ts`,
2602
+ content: `import { Module } from '@nestjs/common';
2603
+ import { ${pascalName}Controller } from './${camelName}.controller';
2604
+ import { ${pascalName}Service } from './${camelName}.service';
2605
+
2606
+ @Module({
2607
+ controllers: [${pascalName}Controller],
2608
+ providers: [${pascalName}Service],
2609
+ exports: [${pascalName}Service],
2610
+ })
2611
+ export class ${pascalName}Module {}
2612
+ `,
2613
+ knowledge: "nestjs-module"
2614
+ },
2615
+ {
2616
+ path: `${featurePath}/${camelName}.controller.ts`,
2617
+ content: `import { Controller, Get, Post, Body, Param } from '@nestjs/common';
2618
+ import { ${pascalName}Service } from './${camelName}.service';
2619
+
2620
+ @Controller('${featureName}')
2621
+ export class ${pascalName}Controller {
2622
+ constructor(private readonly ${camelName}Service: ${pascalName}Service) {}
2623
+
2624
+ @Get()
2625
+ findAll() {
2626
+ return this.${camelName}Service.findAll();
2627
+ }
2628
+ }
2629
+ `,
2630
+ knowledge: "nestjs-controller"
2631
+ },
2632
+ {
2633
+ path: `${featurePath}/${camelName}.service.ts`,
2634
+ content: `import { Injectable } from '@nestjs/common';
2635
+
2636
+ @Injectable()
2637
+ export class ${pascalName}Service {
2638
+ findAll() {
2639
+ // Implement service logic
2640
+ return [];
2641
+ }
2642
+ }
2643
+ `,
2644
+ knowledge: "nestjs-service"
2645
+ },
2646
+ {
2647
+ path: `${featurePath}/${camelName}.types.ts`,
2648
+ content: `export interface ${pascalName}Entity {
2649
+ id: string;
2650
+ // Define entity properties
2651
+ }
2652
+
2653
+ export interface Create${pascalName}Dto {
2654
+ // Define creation DTO
2655
+ }
2656
+ `,
2657
+ knowledge: "typescript-types"
2658
+ },
2659
+ {
2660
+ path: `${featurePath}/__tests__/${camelName}.controller.e2e.spec.ts`,
2661
+ content: `import { Test, TestingModule } from '@nestjs/testing';
2662
+ import { ${pascalName}Controller } from '../${camelName}.controller';
2663
+ import { ${pascalName}Service } from '../${camelName}.service';
2664
+
2665
+ describe('${pascalName}Controller', () => {
2666
+ let controller: ${pascalName}Controller;
2667
+
2668
+ beforeEach(async () => {
2669
+ const module: TestingModule = await Test.createTestingModule({
2670
+ controllers: [${pascalName}Controller],
2671
+ providers: [${pascalName}Service],
2672
+ }).compile();
2673
+
2674
+ controller = module.get<${pascalName}Controller>(${pascalName}Controller);
2675
+ });
2676
+
2677
+ it('should be defined', () => {
2678
+ expect(controller).toBeDefined();
2679
+ });
2680
+ });
2681
+ `,
2682
+ knowledge: "nestjs-testing"
2683
+ }
2684
+ ];
2685
+ }
2686
+ function generatePlaywrightFeature(featurePath) {
2687
+ const featureName = basename5(featurePath);
2688
+ const pascalName = toPascalCase(featureName);
2689
+ const camelName = toCamelCase(featureName);
2690
+ return [
2691
+ {
2692
+ path: `${featurePath}/${camelName}.spec.ts`,
2693
+ content: `import { test, expect } from '@playwright/test';
2694
+ import { ${pascalName}Page } from './${camelName}.page';
2695
+
2696
+ test.describe('${pascalName}', () => {
2697
+ test('should load correctly', async ({ page }) => {
2698
+ const ${camelName}Page = new ${pascalName}Page(page);
2699
+ await ${camelName}Page.goto();
2700
+ // Add assertions
2701
+ });
2702
+ });
2703
+ `,
2704
+ knowledge: "playwright-spec"
2705
+ },
2706
+ {
2707
+ path: `${featurePath}/${camelName}.page.ts`,
2708
+ content: `import type { Page, Locator } from '@playwright/test';
2709
+
2710
+ export class ${pascalName}Page {
2711
+ readonly page: Page;
2712
+
2713
+ constructor(page: Page) {
2714
+ this.page = page;
2715
+ }
2716
+
2717
+ async goto() {
2718
+ await this.page.goto('/${featureName}');
2719
+ }
2720
+
2721
+ // Add page object methods
2722
+ }
2723
+ `,
2724
+ knowledge: "playwright-page-object"
2725
+ },
2726
+ {
2727
+ path: `${featurePath}/${camelName}.fixtures.ts`,
2728
+ content: `import { test as base } from '@playwright/test';
2729
+ import { ${pascalName}Page } from './${camelName}.page';
2730
+
2731
+ export const test = base.extend<{ ${camelName}Page: ${pascalName}Page }>({
2732
+ ${camelName}Page: async ({ page }, use) => {
2733
+ const ${camelName}Page = new ${pascalName}Page(page);
2734
+ await use(${camelName}Page);
2735
+ },
2736
+ });
2737
+
2738
+ export { expect } from '@playwright/test';
2739
+ `,
2740
+ knowledge: "playwright-fixtures"
2741
+ }
2742
+ ];
2743
+ }
2744
+ var generators = {
2745
+ "vue-feature": generateVueFeature,
2746
+ "nestjs-feature": generateNestJSFeature,
2747
+ "playwright-feature": generatePlaywrightFeature
2748
+ };
2749
+ function generateNestJsBffFeature(outputPath, contractPath, endpointName, contractsLib) {
2750
+ const parseResult = parseContractFile(contractPath);
2751
+ if ("success" in parseResult && !parseResult.success) {
2752
+ return { success: false, error: parseResult.error };
2753
+ }
2754
+ const contract = parseResult;
2755
+ const endpoint = contract.endpoints.find((e) => e.name === endpointName);
2756
+ if (!endpoint) {
2757
+ return {
2758
+ success: false,
2759
+ error: `Endpoint '${endpointName}' not found in contract. Available: ${contract.endpoints.map((e) => e.name).join(", ")}`
2760
+ };
2761
+ }
2762
+ const result = generateNestJsFeature(endpoint, {
2763
+ contractName: contract.contractName,
2764
+ contractsLib,
2765
+ featureName: endpointName
2766
+ });
2767
+ const files = result.files.map((f) => ({
2768
+ path: join3(outputPath, f.path),
2769
+ content: f.content,
2770
+ knowledge: f.isComplete ? void 0 : "nestjs-bff-skeleton"
2771
+ }));
2772
+ return {
2773
+ success: true,
2774
+ files,
2775
+ summary: summarizeGeneration(result)
2776
+ };
2777
+ }
2778
+ async function scaffoldCore(options) {
2779
+ const featureType = options.type;
2780
+ if (featureType === "nestjs-bff-feature") {
2781
+ if (!options.contractPath || !options.endpointName) {
2782
+ return {
2783
+ success: false,
2784
+ command: "scaffold",
2785
+ error: "nestjs-bff-feature requires contractPath and endpointName options",
2786
+ availableTypes: [...Object.keys(generators), "nestjs-bff-feature"]
2787
+ };
2788
+ }
2789
+ const contractsLib = options.contractsLib ?? "@adeo/ahs-operator-execution-contracts";
2790
+ const absolutePath2 = resolve3(options.path);
2791
+ const absoluteContractPath = resolve3(options.contractPath);
2792
+ const bffResult = generateNestJsBffFeature(
2793
+ absolutePath2,
2794
+ absoluteContractPath,
2795
+ options.endpointName,
2796
+ contractsLib
2797
+ );
2798
+ if (!bffResult.success || !bffResult.files) {
2799
+ return {
2800
+ success: false,
2801
+ command: "scaffold",
2802
+ error: bffResult.error
2803
+ };
2804
+ }
2805
+ if (options.dryRun) {
2806
+ return {
2807
+ success: true,
2808
+ command: "scaffold",
2809
+ dryRun: true,
2810
+ type: featureType,
2811
+ path: absolutePath2,
2812
+ files: bffResult.files.map((f) => ({
2813
+ path: f.path,
2814
+ knowledge: f.knowledge
2815
+ }))
2816
+ };
2817
+ }
2818
+ const created2 = [];
2819
+ const errors2 = [];
2820
+ for (const file of bffResult.files) {
2821
+ try {
2822
+ const dir = dirname3(file.path);
2823
+ if (!existsSync6(dir)) {
2824
+ mkdirSync(dir, { recursive: true });
2825
+ }
2826
+ writeFileSync(file.path, file.content, "utf-8");
2827
+ created2.push(file.path);
2828
+ } catch (err) {
2829
+ errors2.push({
2830
+ path: file.path,
2831
+ error: err instanceof Error ? err.message : String(err)
2832
+ });
2833
+ }
2834
+ }
2835
+ return {
2836
+ success: errors2.length === 0,
2837
+ command: "scaffold",
2838
+ type: featureType,
2839
+ path: absolutePath2,
2840
+ created: created2,
2841
+ errors: errors2.length > 0 ? errors2 : void 0
2842
+ };
2843
+ }
2844
+ const generator = generators[featureType];
2845
+ if (!generator) {
2846
+ return {
2847
+ success: false,
2848
+ command: "scaffold",
2849
+ error: `Unknown feature type: ${options.type}`,
2850
+ availableTypes: [...Object.keys(generators), "nestjs-bff-feature"]
2851
+ };
2852
+ }
2853
+ const absolutePath = resolve3(options.path);
2854
+ const files = generator(absolutePath);
2855
+ if (options.dryRun) {
2856
+ return {
2857
+ success: true,
2858
+ command: "scaffold",
2859
+ dryRun: true,
2860
+ type: featureType,
2861
+ path: absolutePath,
2862
+ files: files.map((f) => ({
2863
+ path: f.path,
2864
+ knowledge: f.knowledge,
2865
+ rules: f.rules
2866
+ }))
2867
+ };
2868
+ }
2869
+ const created = [];
2870
+ const errors = [];
2871
+ for (const file of files) {
2872
+ try {
2873
+ const dir = dirname3(file.path);
2874
+ if (!existsSync6(dir)) {
2875
+ mkdirSync(dir, { recursive: true });
2876
+ }
2877
+ writeFileSync(file.path, file.content, "utf-8");
2878
+ created.push(file.path);
2879
+ } catch (err) {
2880
+ errors.push({
2881
+ path: file.path,
2882
+ error: err instanceof Error ? err.message : String(err)
2883
+ });
2884
+ }
2885
+ }
2886
+ return {
2887
+ success: errors.length === 0,
2888
+ command: "scaffold",
2889
+ type: featureType,
2890
+ path: absolutePath,
2891
+ created,
2892
+ errors: errors.length > 0 ? errors : void 0,
2893
+ knowledges: files.filter((f) => f.knowledge).map((f) => ({
2894
+ path: f.path,
2895
+ knowledge: f.knowledge,
2896
+ rules: f.rules
2897
+ }))
2898
+ };
2899
+ }
2900
+ async function scaffoldCommand(type, path, options) {
2901
+ const result = await scaffoldCore({
2902
+ type,
2903
+ path,
2904
+ dryRun: options.dryRun,
2905
+ contractPath: options.contractPath,
2906
+ endpointName: options.endpointName,
2907
+ contractsLib: options.contractsLib
2908
+ });
2909
+ console.log(JSON.stringify(result, null, 2));
2910
+ if (!result.success) {
2911
+ process.exit(1);
2912
+ }
2913
+ }
2914
+
2915
+ export {
2916
+ lintCore,
2917
+ lintCommand,
2918
+ analyzeCore,
2919
+ analyzeCommand,
2920
+ scaffoldCore,
2921
+ scaffoldCommand
2922
+ };