@hyphaene/hexa-ts-kit 1.0.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,1434 @@
1
+ // src/commands/lint.ts
2
+ import { resolve } 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
+ var colocationChecker = {
10
+ name: "colocation",
11
+ rules: [
12
+ "COL-001",
13
+ "COL-002",
14
+ "COL-003",
15
+ "COL-004",
16
+ "COL-005",
17
+ "COL-006",
18
+ "COL-007",
19
+ "COL-008",
20
+ "COL-010",
21
+ "COL-011",
22
+ "COL-012"
23
+ ],
24
+ async check(ctx) {
25
+ const results = [];
26
+ const forbiddenDirs = await checkForbiddenRootDirs(ctx.cwd);
27
+ results.push(...forbiddenDirs);
28
+ const defaultIgnore = [
29
+ "**/node_modules/**",
30
+ "**/__tests__/**",
31
+ "**/worktrees/**",
32
+ "**/dist/**",
33
+ "**/coverage/**"
34
+ ];
35
+ const vueFiles = await fg("**/src/domains/**/*.vue", {
36
+ cwd: ctx.cwd,
37
+ ignore: defaultIgnore
38
+ });
39
+ const vueFilesOutsideDomains = await fg("**/src/**/*.vue", {
40
+ cwd: ctx.cwd,
41
+ ignore: [...defaultIgnore, "**/src/domains/**", "**/src/shared/**"]
42
+ });
43
+ for (const file of vueFilesOutsideDomains) {
44
+ results.push({
45
+ ruleId: "COL-001",
46
+ severity: "error",
47
+ message: `Vue component outside domains/: ${file}`,
48
+ file,
49
+ suggestion: "Move to src/domains/{domain}/{feature}/"
50
+ });
51
+ }
52
+ for (const vueFile of vueFiles) {
53
+ const fileResults = await checkVueFileColocation(ctx.cwd, vueFile);
54
+ results.push(...fileResults);
55
+ }
56
+ return results;
57
+ }
58
+ };
59
+ async function checkForbiddenRootDirs(cwd) {
60
+ const results = [];
61
+ const forbidden = [
62
+ { pattern: "src/components", ruleId: "COL-005" },
63
+ { pattern: "src/hooks", ruleId: "COL-006" },
64
+ { pattern: "src/composables", ruleId: "COL-006" },
65
+ { pattern: "src/services", ruleId: "COL-007" },
66
+ { pattern: "src/types", ruleId: "COL-008" }
67
+ ];
68
+ for (const { pattern, ruleId } of forbidden) {
69
+ const dir = join(cwd, pattern);
70
+ if (existsSync(dir)) {
71
+ results.push({
72
+ ruleId,
73
+ severity: "error",
74
+ message: `Forbidden directory at root: ${pattern}/`,
75
+ file: pattern,
76
+ suggestion: "Move contents to src/domains/{domain}/ or src/shared/"
77
+ });
78
+ }
79
+ }
80
+ return results;
81
+ }
82
+ async function checkVueFileColocation(cwd, vueFile) {
83
+ const results = [];
84
+ const dir = dirname(vueFile);
85
+ const name = basename(vueFile, ".vue");
86
+ const featureName = name.charAt(0).toLowerCase() + name.slice(1);
87
+ const fullDir = join(cwd, dir);
88
+ const composablePath = join(fullDir, `${featureName}.composable.ts`);
89
+ if (!existsSync(composablePath)) {
90
+ results.push({
91
+ ruleId: "COL-002",
92
+ severity: "error",
93
+ message: `Missing colocated composable: ${featureName}.composable.ts`,
94
+ file: vueFile,
95
+ suggestion: `Create ${dir}/${featureName}.composable.ts`
96
+ });
97
+ }
98
+ const testsDir = join(fullDir, "__tests__");
99
+ if (!existsSync(testsDir)) {
100
+ results.push({
101
+ ruleId: "COL-004",
102
+ severity: "error",
103
+ message: `Missing __tests__/ directory`,
104
+ file: vueFile,
105
+ suggestion: `Create ${dir}/__tests__/`
106
+ });
107
+ }
108
+ const typesPath = join(fullDir, `${featureName}.types.ts`);
109
+ if (!existsSync(typesPath)) {
110
+ results.push({
111
+ ruleId: "COL-010",
112
+ severity: "warning",
113
+ message: `Missing colocated types: ${featureName}.types.ts`,
114
+ file: vueFile,
115
+ suggestion: `Create ${dir}/${featureName}.types.ts if types are needed`
116
+ });
117
+ }
118
+ const translationsPath = join(fullDir, `${featureName}.translations.ts`);
119
+ if (!existsSync(translationsPath)) {
120
+ results.push({
121
+ ruleId: "COL-011",
122
+ severity: "warning",
123
+ message: `Missing colocated translations: ${featureName}.translations.ts`,
124
+ file: vueFile,
125
+ suggestion: `Create ${dir}/${featureName}.translations.ts`
126
+ });
127
+ }
128
+ return results;
129
+ }
130
+
131
+ // src/lint/checkers/structure/naming.ts
132
+ import fg2 from "fast-glob";
133
+ import { readFile } from "fs/promises";
134
+ import { basename as basename2 } from "path";
135
+ var namingChecker = {
136
+ name: "naming",
137
+ rules: [
138
+ "NAM-001",
139
+ "NAM-002",
140
+ "NAM-003",
141
+ "NAM-004",
142
+ "NAM-005",
143
+ "NAM-006",
144
+ "NAM-007",
145
+ "NAM-008",
146
+ "NAM-009",
147
+ "NAM-010",
148
+ "NAM-011"
149
+ ],
150
+ async check(ctx) {
151
+ const results = [];
152
+ const vueFiles = await fg2("**/*.vue", {
153
+ cwd: ctx.cwd,
154
+ ignore: ["**/node_modules/**"]
155
+ });
156
+ for (const file of vueFiles) {
157
+ const name = basename2(file, ".vue");
158
+ if (!isPascalCase(name)) {
159
+ results.push({
160
+ ruleId: "NAM-001",
161
+ severity: "error",
162
+ message: `Vue component must be PascalCase: ${name}.vue`,
163
+ file,
164
+ suggestion: `Rename to ${toPascalCase(name)}.vue`
165
+ });
166
+ }
167
+ }
168
+ const tsFiles = await fg2("**/src/domains/**/*.ts", {
169
+ cwd: ctx.cwd,
170
+ ignore: ["**/node_modules/**", "**/__tests__/**", "**/*.d.ts"]
171
+ });
172
+ for (const file of tsFiles) {
173
+ const name = basename2(file);
174
+ if (name.endsWith(".composable.ts")) {
175
+ const featureName = name.replace(".composable.ts", "");
176
+ if (!isKebabOrCamelCase(featureName)) {
177
+ results.push({
178
+ ruleId: "NAM-003",
179
+ severity: "error",
180
+ message: `Composable file must be camelCase: ${name}`,
181
+ file
182
+ });
183
+ }
184
+ const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
185
+ const exportResults = checkComposableExport(file, content);
186
+ results.push(...exportResults);
187
+ }
188
+ if (name.endsWith(".rules.ts")) {
189
+ const featureName = name.replace(".rules.ts", "");
190
+ if (!isKebabOrCamelCase(featureName)) {
191
+ results.push({
192
+ ruleId: "NAM-004",
193
+ severity: "error",
194
+ message: `Rules file must be camelCase: ${name}`,
195
+ file
196
+ });
197
+ }
198
+ const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
199
+ const exportResults = checkRulesExport(
200
+ file,
201
+ content,
202
+ featureName
203
+ );
204
+ results.push(...exportResults);
205
+ }
206
+ if (name.endsWith(".types.ts")) {
207
+ const featureName = name.replace(".types.ts", "");
208
+ if (!isKebabOrCamelCase(featureName)) {
209
+ results.push({
210
+ ruleId: "NAM-005",
211
+ severity: "error",
212
+ message: `Types file must be camelCase: ${name}`,
213
+ file
214
+ });
215
+ }
216
+ }
217
+ if (name.endsWith(".query.ts")) {
218
+ const featureName = name.replace(".query.ts", "");
219
+ if (!isKebabOrCamelCase(featureName)) {
220
+ results.push({
221
+ ruleId: "NAM-006",
222
+ severity: "error",
223
+ message: `Query file must be camelCase: ${name}`,
224
+ file
225
+ });
226
+ }
227
+ const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
228
+ const exportResults = checkQueryKeysExport(
229
+ file,
230
+ content,
231
+ featureName
232
+ );
233
+ results.push(...exportResults);
234
+ }
235
+ if (name.endsWith(".translations.ts")) {
236
+ const featureName = name.replace(".translations.ts", "");
237
+ if (!isKebabOrCamelCase(featureName)) {
238
+ results.push({
239
+ ruleId: "NAM-007",
240
+ severity: "error",
241
+ message: `Translations file must be camelCase: ${name}`,
242
+ file
243
+ });
244
+ }
245
+ const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
246
+ const exportResults = checkTranslationsExport(
247
+ file,
248
+ content,
249
+ featureName
250
+ );
251
+ results.push(...exportResults);
252
+ }
253
+ }
254
+ const domainDirs = await fg2("src/domains/*", {
255
+ cwd: ctx.cwd,
256
+ onlyDirectories: true
257
+ });
258
+ for (const domainPath of domainDirs) {
259
+ const domainName = basename2(domainPath);
260
+ if (!isKebabCase(domainName)) {
261
+ results.push({
262
+ ruleId: "NAM-011",
263
+ severity: "error",
264
+ message: `Domain must be kebab-case: ${domainName}`,
265
+ file: domainPath
266
+ });
267
+ }
268
+ if (!looksPlural(domainName)) {
269
+ results.push({
270
+ ruleId: "NAM-011",
271
+ severity: "warning",
272
+ message: `Domain should be plural: ${domainName}`,
273
+ file: domainPath,
274
+ suggestion: `Consider renaming to ${domainName}s`
275
+ });
276
+ }
277
+ }
278
+ return results;
279
+ }
280
+ };
281
+ function isPascalCase(str) {
282
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str);
283
+ }
284
+ function toPascalCase(str) {
285
+ return str.split(/[-_]/).map(
286
+ (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
287
+ ).join("");
288
+ }
289
+ function isKebabOrCamelCase(str) {
290
+ return /^[a-z][a-zA-Z0-9]*$/.test(str) || /^[a-z][a-z0-9-]*$/.test(str);
291
+ }
292
+ function isKebabCase(str) {
293
+ return /^[a-z][a-z0-9-]*$/.test(str);
294
+ }
295
+ function looksPlural(str) {
296
+ return str.endsWith("s") || str.endsWith("ies") || str === "shared" || str === "data";
297
+ }
298
+ function checkComposableExport(file, content) {
299
+ const results = [];
300
+ const hasUseExport = /export\s+function\s+use[A-Z]/.test(content);
301
+ if (!hasUseExport) {
302
+ results.push({
303
+ ruleId: "NAM-002",
304
+ severity: "error",
305
+ message: `Composable must export function with 'use' prefix`,
306
+ file,
307
+ suggestion: `Export function useFeatureName()`
308
+ });
309
+ }
310
+ return results;
311
+ }
312
+ function checkRulesExport(file, content, featureName) {
313
+ const results = [];
314
+ const expectedClass = `${toPascalCase(featureName)}Rules`;
315
+ const expectedSingleton = `${featureName}Rules`;
316
+ const hasClass = new RegExp(`class\\s+${expectedClass}\\b`).test(content);
317
+ const hasSingleton = new RegExp(
318
+ `export\\s+const\\s+${expectedSingleton}\\s*=`
319
+ ).test(content);
320
+ if (!hasClass) {
321
+ results.push({
322
+ ruleId: "NAM-008",
323
+ severity: "error",
324
+ message: `Rules file must have class ${expectedClass}`,
325
+ file
326
+ });
327
+ }
328
+ if (!hasSingleton) {
329
+ results.push({
330
+ ruleId: "NAM-008",
331
+ severity: "error",
332
+ message: `Rules file must export singleton ${expectedSingleton}`,
333
+ file,
334
+ suggestion: `Add: export const ${expectedSingleton} = new ${expectedClass}()`
335
+ });
336
+ }
337
+ return results;
338
+ }
339
+ function checkQueryKeysExport(file, content, featureName) {
340
+ const results = [];
341
+ const expectedKeys = `${featureName}Keys`;
342
+ const hasKeysExport = new RegExp(
343
+ `export\\s+const\\s+${expectedKeys}\\s*=`
344
+ ).test(content);
345
+ if (!hasKeysExport) {
346
+ results.push({
347
+ ruleId: "NAM-010",
348
+ severity: "error",
349
+ message: `Query file must export ${expectedKeys}`,
350
+ file,
351
+ suggestion: `Add: export const ${expectedKeys} = { all: ['${featureName}'] as const, ... }`
352
+ });
353
+ }
354
+ return results;
355
+ }
356
+ function checkTranslationsExport(file, content, featureName) {
357
+ const results = [];
358
+ const expectedExport = `${featureName}Translations`;
359
+ const hasExport = new RegExp(
360
+ `export\\s+const\\s+${expectedExport}\\s*=`
361
+ ).test(content);
362
+ if (!hasExport) {
363
+ results.push({
364
+ ruleId: "NAM-009",
365
+ severity: "error",
366
+ message: `Translations file must export ${expectedExport}`,
367
+ file,
368
+ suggestion: `Add: export const ${expectedExport} = { ... }`
369
+ });
370
+ }
371
+ return results;
372
+ }
373
+
374
+ // src/lint/checkers/structure/domains.ts
375
+ import fg3 from "fast-glob";
376
+ import { basename as basename3 } from "path";
377
+ var domainsChecker = {
378
+ name: "domains",
379
+ rules: ["DOM-001", "DOM-004"],
380
+ async check(ctx) {
381
+ const results = [];
382
+ const domainDirs = await fg3("src/domains/*", {
383
+ cwd: ctx.cwd,
384
+ onlyDirectories: true
385
+ });
386
+ for (const domainPath of domainDirs) {
387
+ const domainName = basename3(domainPath);
388
+ if (domainName === "shared") continue;
389
+ const features = await fg3(`${domainPath}/*`, {
390
+ cwd: ctx.cwd,
391
+ onlyDirectories: true
392
+ });
393
+ const featureDirs = features.filter(
394
+ (f) => !basename3(f).startsWith("__") && !basename3(f).startsWith(".")
395
+ );
396
+ if (featureDirs.length < 2) {
397
+ results.push({
398
+ ruleId: "DOM-001",
399
+ severity: "warning",
400
+ message: `Domain "${domainName}" has only ${featureDirs.length} feature(s), expected at least 2`,
401
+ file: domainPath,
402
+ suggestion: featureDirs.length === 1 ? "Consider merging into parent domain or wait for more features" : "Add features or merge into another domain"
403
+ });
404
+ }
405
+ const deepDirs = await fg3(`${domainPath}/*/*`, {
406
+ cwd: ctx.cwd,
407
+ onlyDirectories: true
408
+ });
409
+ for (const deepDir of deepDirs) {
410
+ const parts = deepDir.split("/");
411
+ const deepName = basename3(deepDir);
412
+ const hasVueFiles = await fg3(`${deepDir}/*.vue`, {
413
+ cwd: ctx.cwd
414
+ });
415
+ if (hasVueFiles.length > 0 && !deepName.startsWith("__") && deepName !== "components") {
416
+ continue;
417
+ }
418
+ const subFeatures = await fg3(`${deepDir}/*`, {
419
+ cwd: ctx.cwd,
420
+ onlyDirectories: true
421
+ });
422
+ if (subFeatures.length >= 3) {
423
+ results.push({
424
+ ruleId: "DOM-004",
425
+ severity: "warning",
426
+ message: `Nested directory "${deepName}" has ${subFeatures.length} subdirs - consider if it should be a separate domain`,
427
+ file: deepDir,
428
+ suggestion: `If "${deepName}" is a distinct business concept, move to src/domains/${deepName}/`
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return results;
434
+ }
435
+ };
436
+
437
+ // src/lint/checkers/ast/vue-component.ts
438
+ import fg4 from "fast-glob";
439
+ import { readFile as readFile2 } from "fs/promises";
440
+ var FORBIDDEN_CALLS = [
441
+ { name: "ref", ruleId: "VUE-001" },
442
+ { name: "reactive", ruleId: "VUE-002" },
443
+ { name: "computed", ruleId: "VUE-003" },
444
+ { name: "watch", ruleId: "VUE-004" },
445
+ { name: "watchEffect", ruleId: "VUE-005" }
446
+ ];
447
+ var vueComponentChecker = {
448
+ name: "vue-component",
449
+ rules: [
450
+ "VUE-001",
451
+ "VUE-002",
452
+ "VUE-003",
453
+ "VUE-004",
454
+ "VUE-005",
455
+ "VUE-006",
456
+ "VUE-007"
457
+ ],
458
+ async check(ctx) {
459
+ const results = [];
460
+ const vueFiles = await fg4("**/*.vue", {
461
+ cwd: ctx.cwd,
462
+ ignore: ["**/node_modules/**", "**/worktrees/**", "**/dist/**"]
463
+ });
464
+ for (const file of vueFiles) {
465
+ const content = await readFile2(`${ctx.cwd}/${file}`, "utf-8");
466
+ const fileResults = checkVueComponent(file, content);
467
+ results.push(...fileResults);
468
+ }
469
+ return results;
470
+ }
471
+ };
472
+ function checkVueComponent(file, content) {
473
+ const results = [];
474
+ const scriptContent = getScriptContent(content);
475
+ if (!scriptContent) return results;
476
+ const lines = scriptContent.split("\n");
477
+ const scriptStartLine = getScriptStartLine(content);
478
+ for (let i = 0; i < lines.length; i++) {
479
+ const line = lines[i];
480
+ if (!line) continue;
481
+ if (line.trim().startsWith("import")) continue;
482
+ if (line.trim().startsWith("//")) continue;
483
+ for (const { name, ruleId } of FORBIDDEN_CALLS) {
484
+ const pattern = new RegExp(`\\b${name}\\s*\\(`);
485
+ if (pattern.test(line)) {
486
+ results.push({
487
+ ruleId,
488
+ severity: "error",
489
+ message: `${name}() is forbidden in Vue component - move to composable`,
490
+ file,
491
+ line: scriptStartLine + i,
492
+ suggestion: `Move ${name}() call to the composable file`
493
+ });
494
+ }
495
+ }
496
+ }
497
+ const templateResults = checkTemplateInlineLogic(file, content);
498
+ results.push(...templateResults);
499
+ return results;
500
+ }
501
+ function getScriptContent(content) {
502
+ const match = content.match(/<script[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
503
+ return match?.[1] ?? null;
504
+ }
505
+ function getScriptStartLine(content) {
506
+ const lines = content.split("\n");
507
+ for (let i = 0; i < lines.length; i++) {
508
+ if (/<script[^>]*setup[^>]*>/.test(lines[i] ?? "")) {
509
+ return i + 2;
510
+ }
511
+ }
512
+ return 1;
513
+ }
514
+ function checkTemplateInlineLogic(file, content) {
515
+ const results = [];
516
+ const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
517
+ if (!templateMatch) return results;
518
+ const template = templateMatch[1];
519
+ if (!template) return results;
520
+ const templateStartLine = getTemplateStartLine(content);
521
+ const lines = template.split("\n");
522
+ for (let i = 0; i < lines.length; i++) {
523
+ const line = lines[i];
524
+ if (!line) continue;
525
+ const inlineMethods = [
526
+ ".reduce(",
527
+ ".filter(",
528
+ ".map(",
529
+ ".find(",
530
+ ".some(",
531
+ ".every("
532
+ ];
533
+ for (const method of inlineMethods) {
534
+ if (line.includes("{{") && line.includes(method)) {
535
+ results.push({
536
+ ruleId: "VUE-006",
537
+ severity: "error",
538
+ message: `Inline ${method.slice(1, -1)}() in template - move to computed in composable`,
539
+ file,
540
+ line: templateStartLine + i,
541
+ suggestion: "Pre-calculate in composable and expose as computed"
542
+ });
543
+ }
544
+ }
545
+ const bindingMatch = line.match(/:[a-z-]+="([^"]+)"/gi);
546
+ if (bindingMatch) {
547
+ for (const binding of bindingMatch) {
548
+ const value = binding.match(/="([^"]+)"/)?.[1] ?? "";
549
+ const operators = (value.match(/&&|\|\||\?|:/g) ?? []).length;
550
+ if (operators >= 3) {
551
+ results.push({
552
+ ruleId: "VUE-007",
553
+ severity: "warning",
554
+ message: "Complex condition in binding - extract to computed",
555
+ file,
556
+ line: templateStartLine + i,
557
+ suggestion: "Move complex logic to composable computed property"
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ return results;
564
+ }
565
+ function getTemplateStartLine(content) {
566
+ const lines = content.split("\n");
567
+ for (let i = 0; i < lines.length; i++) {
568
+ if (/<template>/.test(lines[i] ?? "")) {
569
+ return i + 2;
570
+ }
571
+ }
572
+ return 1;
573
+ }
574
+
575
+ // src/lint/checkers/ast/rules-file.ts
576
+ import fg5 from "fast-glob";
577
+ import { readFile as readFile3 } from "fs/promises";
578
+ var FORBIDDEN_IMPORTS = [
579
+ { pattern: /from\s+['"]vue['"]/, ruleId: "RUL-001", name: "vue" },
580
+ {
581
+ pattern: /from\s+['"]@vue\//,
582
+ ruleId: "RUL-001",
583
+ name: "@vue/*"
584
+ },
585
+ {
586
+ pattern: /from\s+['"]pinia['"]/,
587
+ ruleId: "RUL-002",
588
+ name: "pinia"
589
+ },
590
+ {
591
+ pattern: /from\s+['"]vue-router['"]/,
592
+ ruleId: "RUL-002",
593
+ name: "vue-router"
594
+ },
595
+ {
596
+ pattern: /from\s+['"]@tanstack/,
597
+ ruleId: "RUL-002",
598
+ name: "@tanstack/*"
599
+ }
600
+ ];
601
+ var FORBIDDEN_PATTERNS = [
602
+ {
603
+ pattern: /\basync\s+\w+\s*\(/,
604
+ ruleId: "RUL-008",
605
+ message: "async function"
606
+ },
607
+ {
608
+ pattern: /\bawait\s+/,
609
+ ruleId: "RUL-008",
610
+ message: "await keyword"
611
+ },
612
+ {
613
+ pattern: /\bconsole\.(log|warn|error|info)\s*\(/,
614
+ ruleId: "RUL-003",
615
+ message: "console.*"
616
+ },
617
+ {
618
+ pattern: /\blocalStorage\b/,
619
+ ruleId: "RUL-003",
620
+ message: "localStorage"
621
+ },
622
+ {
623
+ pattern: /\bsessionStorage\b/,
624
+ ruleId: "RUL-003",
625
+ message: "sessionStorage"
626
+ },
627
+ { pattern: /\bfetch\s*\(/, ruleId: "RUL-003", message: "fetch()" },
628
+ {
629
+ pattern: /\bnew\s+Date\s*\(\s*\)/,
630
+ ruleId: "RUL-007",
631
+ message: "new Date() without injection"
632
+ }
633
+ ];
634
+ var rulesFileChecker = {
635
+ name: "rules-file",
636
+ rules: ["RUL-001", "RUL-002", "RUL-003", "RUL-007", "RUL-008"],
637
+ async check(ctx) {
638
+ const results = [];
639
+ const rulesFiles = await fg5("**/*.rules.ts", {
640
+ cwd: ctx.cwd,
641
+ ignore: [
642
+ "**/node_modules/**",
643
+ "**/worktrees/**",
644
+ "**/dist/**",
645
+ "**/__tests__/**"
646
+ ]
647
+ });
648
+ for (const file of rulesFiles) {
649
+ const content = await readFile3(`${ctx.cwd}/${file}`, "utf-8");
650
+ const fileResults = checkRulesFile(file, content);
651
+ results.push(...fileResults);
652
+ }
653
+ return results;
654
+ }
655
+ };
656
+ function checkRulesFile(file, content) {
657
+ const results = [];
658
+ const lines = content.split("\n");
659
+ for (let i = 0; i < lines.length; i++) {
660
+ const line = lines[i];
661
+ if (!line) continue;
662
+ for (const { pattern, ruleId, name } of FORBIDDEN_IMPORTS) {
663
+ if (pattern.test(line)) {
664
+ results.push({
665
+ ruleId,
666
+ severity: "error",
667
+ message: `Import from "${name}" is forbidden in rules files`,
668
+ file,
669
+ line: i + 1,
670
+ suggestion: "Rules must be pure - no framework dependencies"
671
+ });
672
+ }
673
+ }
674
+ const trimmed = line.trim();
675
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
676
+ for (const { pattern, ruleId, message } of FORBIDDEN_PATTERNS) {
677
+ if (pattern.test(line)) {
678
+ if (ruleId === "RUL-007") {
679
+ if (line.includes("= new Date()") && !line.includes(": Date")) {
680
+ results.push({
681
+ ruleId,
682
+ severity: "warning",
683
+ message: `${message} - inject as parameter with default`,
684
+ file,
685
+ line: i + 1,
686
+ suggestion: "Use: (now: Date = new Date()) for testability"
687
+ });
688
+ }
689
+ } else {
690
+ results.push({
691
+ ruleId,
692
+ severity: "error",
693
+ message: `${message} is forbidden in rules files`,
694
+ file,
695
+ line: i + 1,
696
+ suggestion: ruleId === "RUL-008" ? "Move async operations to query.ts" : "Rules must be pure - no side effects"
697
+ });
698
+ }
699
+ }
700
+ }
701
+ }
702
+ const hasClass = /\bclass\s+\w+Rules\b/.test(content);
703
+ if (!hasClass) {
704
+ results.push({
705
+ ruleId: "RUL-009",
706
+ severity: "error",
707
+ message: "Rules file must export a class with 'Rules' suffix",
708
+ file,
709
+ suggestion: "Create: class FeatureRules { ... }"
710
+ });
711
+ }
712
+ const hasSingleton = /export\s+const\s+\w+Rules\s*=\s*new\s+\w+Rules\s*\(\s*\)/.test(
713
+ content
714
+ );
715
+ if (!hasSingleton) {
716
+ results.push({
717
+ ruleId: "RUL-010",
718
+ severity: "error",
719
+ message: "Rules file must export a singleton instance",
720
+ file,
721
+ suggestion: "Add: export const featureRules = new FeatureRules()"
722
+ });
723
+ }
724
+ return results;
725
+ }
726
+
727
+ // src/lint/checkers/ast/typescript.ts
728
+ import fg6 from "fast-glob";
729
+ import { readFile as readFile4 } from "fs/promises";
730
+ var typescriptChecker = {
731
+ name: "typescript",
732
+ rules: ["TSP-002", "TSP-004", "TSP-009"],
733
+ async check(ctx) {
734
+ const results = [];
735
+ const tsFiles = await fg6("**/*.ts", {
736
+ cwd: ctx.cwd,
737
+ ignore: [
738
+ "**/node_modules/**",
739
+ "**/worktrees/**",
740
+ "**/dist/**",
741
+ "**/*.d.ts",
742
+ "**/*.spec.ts",
743
+ "**/*.test.ts"
744
+ ]
745
+ });
746
+ for (const file of tsFiles) {
747
+ const content = await readFile4(`${ctx.cwd}/${file}`, "utf-8");
748
+ const fileResults = checkTypeScript(file, content);
749
+ results.push(...fileResults);
750
+ }
751
+ return results;
752
+ }
753
+ };
754
+ function checkTypeScript(file, content) {
755
+ const results = [];
756
+ const lines = content.split("\n");
757
+ for (let i = 0; i < lines.length; i++) {
758
+ const line = lines[i];
759
+ if (!line) continue;
760
+ const trimmed = line.trim();
761
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
762
+ if (/\benum\s+\w+/.test(line)) {
763
+ results.push({
764
+ ruleId: "TSP-002",
765
+ severity: "error",
766
+ message: "TypeScript enum is forbidden - use 'as const' or Zod",
767
+ file,
768
+ line: i + 1,
769
+ suggestion: "const Status = { Active: 'ACTIVE' } as const"
770
+ });
771
+ }
772
+ if (/:\s*any\b|as\s+any\b|<any>/.test(line)) {
773
+ results.push({
774
+ ruleId: "TSP-004",
775
+ severity: "error",
776
+ message: "Explicit 'any' type is forbidden",
777
+ file,
778
+ line: i + 1,
779
+ suggestion: "Use 'unknown' and narrow the type, or define proper types"
780
+ });
781
+ }
782
+ if (/\w+!\.\w+!/.test(line)) {
783
+ results.push({
784
+ ruleId: "TSP-009",
785
+ severity: "warning",
786
+ message: "Chained non-null assertions (!.!) - use proper narrowing",
787
+ file,
788
+ line: i + 1,
789
+ suggestion: "Use optional chaining (?.) or add null checks"
790
+ });
791
+ }
792
+ }
793
+ return results;
794
+ }
795
+
796
+ // src/lint/reporters/console.ts
797
+ import pc from "picocolors";
798
+ var severityColors = {
799
+ error: pc.red,
800
+ warning: pc.yellow,
801
+ info: pc.blue
802
+ };
803
+ var severityIcons = {
804
+ error: "\u2716",
805
+ warning: "\u26A0",
806
+ info: "\u2139"
807
+ };
808
+ function formatConsole(results) {
809
+ if (results.length === 0) {
810
+ return pc.green("\u2713 No issues found");
811
+ }
812
+ const byFile = /* @__PURE__ */ new Map();
813
+ for (const result of results) {
814
+ const existing = byFile.get(result.file) ?? [];
815
+ existing.push(result);
816
+ byFile.set(result.file, existing);
817
+ }
818
+ const lines = [];
819
+ for (const [file, fileResults] of byFile) {
820
+ lines.push("");
821
+ lines.push(pc.underline(file));
822
+ for (const result of fileResults) {
823
+ const color = severityColors[result.severity];
824
+ const icon = severityIcons[result.severity];
825
+ const location = result.line !== void 0 ? `:${result.line}:${result.column ?? 0}` : "";
826
+ lines.push(
827
+ ` ${color(icon)} ${result.message} ${pc.dim(`[${result.ruleId}]`)}`
828
+ );
829
+ if (result.suggestion) {
830
+ lines.push(` ${pc.dim("\u2192")} ${pc.cyan(result.suggestion)}`);
831
+ }
832
+ }
833
+ }
834
+ const errorCount = results.filter((r) => r.severity === "error").length;
835
+ const warningCount = results.filter((r) => r.severity === "warning").length;
836
+ const infoCount = results.filter((r) => r.severity === "info").length;
837
+ lines.push("");
838
+ lines.push(
839
+ pc.bold(
840
+ `${pc.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)}, ${pc.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}, ${pc.blue(`${infoCount} info`)}`
841
+ )
842
+ );
843
+ return lines.join("\n");
844
+ }
845
+ function formatSummary(results) {
846
+ return {
847
+ errors: results.filter((r) => r.severity === "error").length,
848
+ warnings: results.filter((r) => r.severity === "warning").length,
849
+ info: results.filter((r) => r.severity === "info").length,
850
+ total: results.length
851
+ };
852
+ }
853
+
854
+ // src/commands/lint.ts
855
+ var structureCheckers = [
856
+ colocationChecker,
857
+ namingChecker,
858
+ domainsChecker
859
+ ];
860
+ var astCheckers = [
861
+ vueComponentChecker,
862
+ rulesFileChecker,
863
+ typescriptChecker
864
+ ];
865
+ var allCheckers = [...structureCheckers, ...astCheckers];
866
+ function getChangedFiles(cwd) {
867
+ try {
868
+ const output = execSync("git diff --name-only HEAD", {
869
+ cwd,
870
+ encoding: "utf-8"
871
+ });
872
+ const stagedOutput = execSync("git diff --name-only --cached", {
873
+ cwd,
874
+ encoding: "utf-8"
875
+ });
876
+ const allFiles = [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
877
+ return [...new Set(allFiles)];
878
+ } catch {
879
+ return [];
880
+ }
881
+ }
882
+ async function lintCore(options) {
883
+ const cwd = resolve(options.path || ".");
884
+ let files = [];
885
+ if (options.changed) {
886
+ files = getChangedFiles(cwd);
887
+ if (files.length === 0) {
888
+ return {
889
+ success: true,
890
+ command: "lint",
891
+ message: "No changed files to lint",
892
+ files: [],
893
+ results: [],
894
+ summary: { errors: 0, warnings: 0, info: 0, total: 0 }
895
+ };
896
+ }
897
+ }
898
+ let checkersToRun = allCheckers;
899
+ if (options.rules) {
900
+ const prefixes = options.rules.split(",").map((r) => r.trim().toUpperCase());
901
+ checkersToRun = allCheckers.filter(
902
+ (checker) => checker.rules.some(
903
+ (rule) => prefixes.some((p) => rule.startsWith(p))
904
+ )
905
+ );
906
+ }
907
+ const allResults = [];
908
+ for (const checker of checkersToRun) {
909
+ const results = await checker.check({ cwd, files });
910
+ if (options.changed && files.length > 0) {
911
+ const filteredResults2 = results.filter(
912
+ (r) => files.some((f) => r.file.endsWith(f))
913
+ );
914
+ allResults.push(...filteredResults2);
915
+ } else {
916
+ allResults.push(...results);
917
+ }
918
+ }
919
+ const filteredResults = options.quiet ? allResults.filter((r) => r.severity === "error") : allResults;
920
+ const summary = formatSummary(allResults);
921
+ return {
922
+ success: summary.errors === 0,
923
+ command: "lint",
924
+ files: options.changed ? files : void 0,
925
+ results: filteredResults,
926
+ summary
927
+ };
928
+ }
929
+ async function lintCommand(path = ".", options) {
930
+ const startTime = performance.now();
931
+ if (options.debug) {
932
+ console.log(`Linting: ${resolve(path)}`);
933
+ console.log(`Checkers: ${allCheckers.map((c) => c.name).join(", ")}`);
934
+ }
935
+ const result = await lintCore({
936
+ path,
937
+ changed: options.changed,
938
+ rules: options.rules,
939
+ quiet: options.quiet
940
+ });
941
+ if (options.format === "json") {
942
+ console.log(JSON.stringify(result, null, 2));
943
+ } else {
944
+ if (result.message) {
945
+ console.log(result.message);
946
+ } else {
947
+ console.log(formatConsole(result.results));
948
+ }
949
+ }
950
+ if (options.debug) {
951
+ const elapsed = (performance.now() - startTime).toFixed(0);
952
+ console.log(`
953
+ Completed in ${elapsed}ms`);
954
+ }
955
+ process.exit(result.summary.errors > 0 ? 1 : 0);
956
+ }
957
+
958
+ // src/commands/analyze.ts
959
+ import { basename as basename4 } from "path";
960
+ import { execSync as execSync2 } from "child_process";
961
+ import { readFileSync, existsSync as existsSync2 } from "fs";
962
+ import fg7 from "fast-glob";
963
+ import matter from "gray-matter";
964
+ import { minimatch } from "minimatch";
965
+ function expandPath(p) {
966
+ if (p.startsWith("~")) {
967
+ return p.replace("~", process.env.HOME || "");
968
+ }
969
+ return p;
970
+ }
971
+ function getChangedFiles2(cwd) {
972
+ try {
973
+ const output = execSync2("git diff --name-only HEAD", {
974
+ cwd,
975
+ encoding: "utf-8"
976
+ });
977
+ const stagedOutput = execSync2("git diff --name-only --cached", {
978
+ cwd,
979
+ encoding: "utf-8"
980
+ });
981
+ return [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
982
+ } catch {
983
+ return [];
984
+ }
985
+ }
986
+ function loadKnowledgeMappings(knowledgePath) {
987
+ const expandedPath = expandPath(knowledgePath);
988
+ if (!existsSync2(expandedPath)) {
989
+ return [];
990
+ }
991
+ const knowledgeFiles = fg7.sync("**/*.knowledge.md", {
992
+ cwd: expandedPath,
993
+ absolute: true
994
+ });
995
+ const mappings = [];
996
+ for (const file of knowledgeFiles) {
997
+ try {
998
+ const content = readFileSync(file, "utf-8");
999
+ const { data } = matter(content);
1000
+ if (data.match) {
1001
+ mappings.push({
1002
+ name: data.name || basename4(file, ".knowledge.md"),
1003
+ path: file,
1004
+ match: data.match,
1005
+ description: data.description
1006
+ });
1007
+ }
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ return mappings;
1012
+ }
1013
+ function matchFileToKnowledges(file, mappings) {
1014
+ const results = [];
1015
+ const fileName = basename4(file);
1016
+ for (const mapping of mappings) {
1017
+ if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
1018
+ results.push({
1019
+ file,
1020
+ knowledge: mapping.name,
1021
+ path: mapping.path
1022
+ });
1023
+ }
1024
+ }
1025
+ return results;
1026
+ }
1027
+ var DEFAULT_KNOWLEDGE_PATH = "~/.claude/marketplace/shared/knowledge";
1028
+ async function analyzeCore(options) {
1029
+ const cwd = process.cwd();
1030
+ const knowledgePath = options.knowledgePath || DEFAULT_KNOWLEDGE_PATH;
1031
+ let filesToAnalyze = options.files || [];
1032
+ if (options.changed) {
1033
+ filesToAnalyze = getChangedFiles2(cwd);
1034
+ }
1035
+ if (filesToAnalyze.length === 0) {
1036
+ return {
1037
+ success: true,
1038
+ command: "analyze",
1039
+ message: "No files to analyze",
1040
+ files: [],
1041
+ knowledges: []
1042
+ };
1043
+ }
1044
+ const mappings = loadKnowledgeMappings(knowledgePath);
1045
+ if (mappings.length === 0) {
1046
+ return {
1047
+ success: false,
1048
+ command: "analyze",
1049
+ error: `No knowledge files with 'match' pattern found in ${knowledgePath}`,
1050
+ files: filesToAnalyze,
1051
+ knowledges: []
1052
+ };
1053
+ }
1054
+ const allResults = [];
1055
+ for (const file of filesToAnalyze) {
1056
+ const matches = matchFileToKnowledges(file, mappings);
1057
+ allResults.push(...matches);
1058
+ }
1059
+ return {
1060
+ success: true,
1061
+ command: "analyze",
1062
+ files: filesToAnalyze,
1063
+ knowledges: allResults,
1064
+ availableMappings: mappings.length
1065
+ };
1066
+ }
1067
+ async function analyzeCommand(files = [], options) {
1068
+ const result = await analyzeCore({
1069
+ files,
1070
+ changed: options.changed,
1071
+ knowledgePath: options.knowledgePath
1072
+ });
1073
+ console.log(JSON.stringify(result, null, 2));
1074
+ }
1075
+
1076
+ // src/commands/scaffold.ts
1077
+ import { resolve as resolve2, dirname as dirname3, basename as basename5 } from "path";
1078
+ import { mkdirSync, writeFileSync, existsSync as existsSync3 } from "fs";
1079
+ function toPascalCase2(str) {
1080
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
1081
+ }
1082
+ function toCamelCase(str) {
1083
+ const pascal = toPascalCase2(str);
1084
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
1085
+ }
1086
+ function generateVueFeature(featurePath) {
1087
+ const featureName = basename5(featurePath);
1088
+ const pascalName = toPascalCase2(featureName);
1089
+ const camelName = toCamelCase(featureName);
1090
+ return [
1091
+ {
1092
+ path: `${featurePath}/${pascalName}.vue`,
1093
+ content: `<script setup lang="ts">
1094
+ import { use${pascalName} } from './${camelName}.composable';
1095
+
1096
+ const { /* state and methods */ } = use${pascalName}();
1097
+ </script>
1098
+
1099
+ <template>
1100
+ <div class="${featureName}">
1101
+ <!-- Template -->
1102
+ </div>
1103
+ </template>
1104
+ `,
1105
+ knowledge: "vue-component",
1106
+ rules: ["COL-001", "VUE-001"]
1107
+ },
1108
+ {
1109
+ path: `${featurePath}/${camelName}.composable.ts`,
1110
+ content: `import { ref, computed } from 'vue';
1111
+ import { ${camelName}Rules } from './${camelName}.rules';
1112
+ import type { ${pascalName}State } from './${camelName}.types';
1113
+
1114
+ export function use${pascalName}() {
1115
+ // State
1116
+
1117
+ // Computed
1118
+
1119
+ // Methods
1120
+
1121
+ return {
1122
+ // Expose state and methods
1123
+ };
1124
+ }
1125
+ `,
1126
+ knowledge: "vue-composable",
1127
+ rules: ["COL-002", "NAM-002", "NAM-003"]
1128
+ },
1129
+ {
1130
+ path: `${featurePath}/${camelName}.rules.ts`,
1131
+ content: `// Pure functions only - no framework imports, no side effects
1132
+
1133
+ export class ${pascalName}Rules {
1134
+ // Validation, calculation, transformation functions
1135
+
1136
+ static validate(input: unknown): boolean {
1137
+ // Implement validation logic
1138
+ return true;
1139
+ }
1140
+ }
1141
+
1142
+ export const ${camelName}Rules = ${pascalName}Rules;
1143
+ `,
1144
+ knowledge: "vue-rules",
1145
+ rules: ["RUL-001", "RUL-002", "NAM-008"]
1146
+ },
1147
+ {
1148
+ path: `${featurePath}/${camelName}.types.ts`,
1149
+ content: `export interface ${pascalName}State {
1150
+ // Define state types
1151
+ }
1152
+
1153
+ export interface ${pascalName}Props {
1154
+ // Define component props
1155
+ }
1156
+ `,
1157
+ knowledge: "typescript-types",
1158
+ rules: ["COL-010", "NAM-005"]
1159
+ },
1160
+ {
1161
+ path: `${featurePath}/${camelName}.query.ts`,
1162
+ content: `import { useQuery, useMutation } from '@tanstack/vue-query';
1163
+
1164
+ export function use${pascalName}Query() {
1165
+ // Implement TanStack Query hooks
1166
+ }
1167
+ `,
1168
+ knowledge: "tanstack-query",
1169
+ rules: ["NAM-007"]
1170
+ },
1171
+ {
1172
+ path: `${featurePath}/${camelName}.translations.ts`,
1173
+ content: `export const ${camelName}Translations = {
1174
+ fr: {
1175
+ // French translations
1176
+ },
1177
+ en: {
1178
+ // English translations
1179
+ },
1180
+ } as const;
1181
+ `,
1182
+ knowledge: "translations",
1183
+ rules: ["COL-011", "NAM-009"]
1184
+ },
1185
+ {
1186
+ path: `${featurePath}/__tests__/${camelName}.rules.test.ts`,
1187
+ content: `import { describe, it, expect } from 'vitest';
1188
+ import { ${pascalName}Rules } from '../${camelName}.rules';
1189
+
1190
+ describe('${pascalName}Rules', () => {
1191
+ describe('validate', () => {
1192
+ it('should validate correct input', () => {
1193
+ expect(${pascalName}Rules.validate({})).toBe(true);
1194
+ });
1195
+ });
1196
+ });
1197
+ `,
1198
+ knowledge: "testing-rules",
1199
+ rules: ["COL-004"]
1200
+ }
1201
+ ];
1202
+ }
1203
+ function generateNestJSFeature(featurePath) {
1204
+ const featureName = basename5(featurePath);
1205
+ const pascalName = toPascalCase2(featureName);
1206
+ const camelName = toCamelCase(featureName);
1207
+ return [
1208
+ {
1209
+ path: `${featurePath}/${camelName}.module.ts`,
1210
+ content: `import { Module } from '@nestjs/common';
1211
+ import { ${pascalName}Controller } from './${camelName}.controller';
1212
+ import { ${pascalName}Service } from './${camelName}.service';
1213
+
1214
+ @Module({
1215
+ controllers: [${pascalName}Controller],
1216
+ providers: [${pascalName}Service],
1217
+ exports: [${pascalName}Service],
1218
+ })
1219
+ export class ${pascalName}Module {}
1220
+ `,
1221
+ knowledge: "nestjs-module"
1222
+ },
1223
+ {
1224
+ path: `${featurePath}/${camelName}.controller.ts`,
1225
+ content: `import { Controller, Get, Post, Body, Param } from '@nestjs/common';
1226
+ import { ${pascalName}Service } from './${camelName}.service';
1227
+
1228
+ @Controller('${featureName}')
1229
+ export class ${pascalName}Controller {
1230
+ constructor(private readonly ${camelName}Service: ${pascalName}Service) {}
1231
+
1232
+ @Get()
1233
+ findAll() {
1234
+ return this.${camelName}Service.findAll();
1235
+ }
1236
+ }
1237
+ `,
1238
+ knowledge: "nestjs-controller"
1239
+ },
1240
+ {
1241
+ path: `${featurePath}/${camelName}.service.ts`,
1242
+ content: `import { Injectable } from '@nestjs/common';
1243
+
1244
+ @Injectable()
1245
+ export class ${pascalName}Service {
1246
+ findAll() {
1247
+ // Implement service logic
1248
+ return [];
1249
+ }
1250
+ }
1251
+ `,
1252
+ knowledge: "nestjs-service"
1253
+ },
1254
+ {
1255
+ path: `${featurePath}/${camelName}.types.ts`,
1256
+ content: `export interface ${pascalName}Entity {
1257
+ id: string;
1258
+ // Define entity properties
1259
+ }
1260
+
1261
+ export interface Create${pascalName}Dto {
1262
+ // Define creation DTO
1263
+ }
1264
+ `,
1265
+ knowledge: "typescript-types"
1266
+ },
1267
+ {
1268
+ path: `${featurePath}/__tests__/${camelName}.controller.e2e.spec.ts`,
1269
+ content: `import { Test, TestingModule } from '@nestjs/testing';
1270
+ import { ${pascalName}Controller } from '../${camelName}.controller';
1271
+ import { ${pascalName}Service } from '../${camelName}.service';
1272
+
1273
+ describe('${pascalName}Controller', () => {
1274
+ let controller: ${pascalName}Controller;
1275
+
1276
+ beforeEach(async () => {
1277
+ const module: TestingModule = await Test.createTestingModule({
1278
+ controllers: [${pascalName}Controller],
1279
+ providers: [${pascalName}Service],
1280
+ }).compile();
1281
+
1282
+ controller = module.get<${pascalName}Controller>(${pascalName}Controller);
1283
+ });
1284
+
1285
+ it('should be defined', () => {
1286
+ expect(controller).toBeDefined();
1287
+ });
1288
+ });
1289
+ `,
1290
+ knowledge: "nestjs-testing"
1291
+ }
1292
+ ];
1293
+ }
1294
+ function generatePlaywrightFeature(featurePath) {
1295
+ const featureName = basename5(featurePath);
1296
+ const pascalName = toPascalCase2(featureName);
1297
+ const camelName = toCamelCase(featureName);
1298
+ return [
1299
+ {
1300
+ path: `${featurePath}/${camelName}.spec.ts`,
1301
+ content: `import { test, expect } from '@playwright/test';
1302
+ import { ${pascalName}Page } from './${camelName}.page';
1303
+
1304
+ test.describe('${pascalName}', () => {
1305
+ test('should load correctly', async ({ page }) => {
1306
+ const ${camelName}Page = new ${pascalName}Page(page);
1307
+ await ${camelName}Page.goto();
1308
+ // Add assertions
1309
+ });
1310
+ });
1311
+ `,
1312
+ knowledge: "playwright-spec"
1313
+ },
1314
+ {
1315
+ path: `${featurePath}/${camelName}.page.ts`,
1316
+ content: `import type { Page, Locator } from '@playwright/test';
1317
+
1318
+ export class ${pascalName}Page {
1319
+ readonly page: Page;
1320
+
1321
+ constructor(page: Page) {
1322
+ this.page = page;
1323
+ }
1324
+
1325
+ async goto() {
1326
+ await this.page.goto('/${featureName}');
1327
+ }
1328
+
1329
+ // Add page object methods
1330
+ }
1331
+ `,
1332
+ knowledge: "playwright-page-object"
1333
+ },
1334
+ {
1335
+ path: `${featurePath}/${camelName}.fixtures.ts`,
1336
+ content: `import { test as base } from '@playwright/test';
1337
+ import { ${pascalName}Page } from './${camelName}.page';
1338
+
1339
+ export const test = base.extend<{ ${camelName}Page: ${pascalName}Page }>({
1340
+ ${camelName}Page: async ({ page }, use) => {
1341
+ const ${camelName}Page = new ${pascalName}Page(page);
1342
+ await use(${camelName}Page);
1343
+ },
1344
+ });
1345
+
1346
+ export { expect } from '@playwright/test';
1347
+ `,
1348
+ knowledge: "playwright-fixtures"
1349
+ }
1350
+ ];
1351
+ }
1352
+ var generators = {
1353
+ "vue-feature": generateVueFeature,
1354
+ "nestjs-feature": generateNestJSFeature,
1355
+ "playwright-feature": generatePlaywrightFeature
1356
+ };
1357
+ async function scaffoldCore(options) {
1358
+ const featureType = options.type;
1359
+ const generator = generators[featureType];
1360
+ if (!generator) {
1361
+ return {
1362
+ success: false,
1363
+ command: "scaffold",
1364
+ error: `Unknown feature type: ${options.type}`,
1365
+ availableTypes: Object.keys(generators)
1366
+ };
1367
+ }
1368
+ const absolutePath = resolve2(options.path);
1369
+ const files = generator(absolutePath);
1370
+ if (options.dryRun) {
1371
+ return {
1372
+ success: true,
1373
+ command: "scaffold",
1374
+ dryRun: true,
1375
+ type: featureType,
1376
+ path: absolutePath,
1377
+ files: files.map((f) => ({
1378
+ path: f.path,
1379
+ knowledge: f.knowledge,
1380
+ rules: f.rules
1381
+ }))
1382
+ };
1383
+ }
1384
+ const created = [];
1385
+ const errors = [];
1386
+ for (const file of files) {
1387
+ try {
1388
+ const dir = dirname3(file.path);
1389
+ if (!existsSync3(dir)) {
1390
+ mkdirSync(dir, { recursive: true });
1391
+ }
1392
+ writeFileSync(file.path, file.content, "utf-8");
1393
+ created.push(file.path);
1394
+ } catch (err) {
1395
+ errors.push({
1396
+ path: file.path,
1397
+ error: err instanceof Error ? err.message : String(err)
1398
+ });
1399
+ }
1400
+ }
1401
+ return {
1402
+ success: errors.length === 0,
1403
+ command: "scaffold",
1404
+ type: featureType,
1405
+ path: absolutePath,
1406
+ created,
1407
+ errors: errors.length > 0 ? errors : void 0,
1408
+ knowledges: files.filter((f) => f.knowledge).map((f) => ({
1409
+ path: f.path,
1410
+ knowledge: f.knowledge,
1411
+ rules: f.rules
1412
+ }))
1413
+ };
1414
+ }
1415
+ async function scaffoldCommand(type, path, options) {
1416
+ const result = await scaffoldCore({
1417
+ type,
1418
+ path,
1419
+ dryRun: options.dryRun
1420
+ });
1421
+ console.log(JSON.stringify(result, null, 2));
1422
+ if (!result.success) {
1423
+ process.exit(1);
1424
+ }
1425
+ }
1426
+
1427
+ export {
1428
+ lintCore,
1429
+ lintCommand,
1430
+ analyzeCore,
1431
+ analyzeCommand,
1432
+ scaffoldCore,
1433
+ scaffoldCommand
1434
+ };