@codebakers/cli 1.6.0 → 2.1.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,827 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { existsSync, readFileSync, readdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { execSync } from 'child_process';
6
+
7
+ interface AuditCheck {
8
+ name: string;
9
+ category: string;
10
+ passed: boolean;
11
+ message: string;
12
+ details?: string[];
13
+ severity: 'error' | 'warning' | 'info';
14
+ }
15
+
16
+ interface AuditResult {
17
+ checks: AuditCheck[];
18
+ score: number;
19
+ maxScore: number;
20
+ passed: boolean;
21
+ }
22
+
23
+ /**
24
+ * Run automated code quality audit
25
+ */
26
+ export async function audit(): Promise<AuditResult> {
27
+ console.log(chalk.blue('\n CodeBakers Audit\n'));
28
+ console.log(chalk.gray(' Running automated checks...\n'));
29
+
30
+ const cwd = process.cwd();
31
+ const checks: AuditCheck[] = [];
32
+
33
+ // ============================================================
34
+ // BUILD & TYPES
35
+ // ============================================================
36
+ console.log(chalk.white(' Build & Types:'));
37
+
38
+ // Check TypeScript
39
+ const tsCheck = await checkTypeScript(cwd);
40
+ checks.push(tsCheck);
41
+ printCheck(tsCheck);
42
+
43
+ // Check ESLint
44
+ const eslintCheck = await checkESLint(cwd);
45
+ checks.push(eslintCheck);
46
+ printCheck(eslintCheck);
47
+
48
+ // Check Build
49
+ const buildCheck = await checkBuild(cwd);
50
+ checks.push(buildCheck);
51
+ printCheck(buildCheck);
52
+
53
+ // ============================================================
54
+ // SECURITY
55
+ // ============================================================
56
+ console.log(chalk.white('\n Security:'));
57
+
58
+ // Check for secrets in code
59
+ const secretsCheck = checkSecretsInCode(cwd);
60
+ checks.push(secretsCheck);
61
+ printCheck(secretsCheck);
62
+
63
+ // Check npm audit
64
+ const npmAuditCheck = await checkNpmAudit(cwd);
65
+ checks.push(npmAuditCheck);
66
+ printCheck(npmAuditCheck);
67
+
68
+ // Check .env.local not committed
69
+ const envGitCheck = checkEnvNotCommitted(cwd);
70
+ checks.push(envGitCheck);
71
+ printCheck(envGitCheck);
72
+
73
+ // ============================================================
74
+ // CODE QUALITY
75
+ // ============================================================
76
+ console.log(chalk.white('\n Code Quality:'));
77
+
78
+ // Check for console.log
79
+ const consoleLogCheck = checkConsoleLog(cwd);
80
+ checks.push(consoleLogCheck);
81
+ printCheck(consoleLogCheck);
82
+
83
+ // Check for API route validation
84
+ const validationCheck = checkApiValidation(cwd);
85
+ checks.push(validationCheck);
86
+ printCheck(validationCheck);
87
+
88
+ // Check for error boundaries
89
+ const errorBoundaryCheck = checkErrorBoundary(cwd);
90
+ checks.push(errorBoundaryCheck);
91
+ printCheck(errorBoundaryCheck);
92
+
93
+ // ============================================================
94
+ // ENVIRONMENT
95
+ // ============================================================
96
+ console.log(chalk.white('\n Environment:'));
97
+
98
+ // Check .env.example exists
99
+ const envExampleCheck = checkEnvExample(cwd);
100
+ checks.push(envExampleCheck);
101
+ printCheck(envExampleCheck);
102
+
103
+ // Check env vars match
104
+ const envMatchCheck = checkEnvMatch(cwd);
105
+ checks.push(envMatchCheck);
106
+ printCheck(envMatchCheck);
107
+
108
+ // ============================================================
109
+ // PROJECT STRUCTURE
110
+ // ============================================================
111
+ console.log(chalk.white('\n Project Structure:'));
112
+
113
+ // Check for CodeBakers patterns
114
+ const patternsCheck = checkCodeBakersPatterns(cwd);
115
+ checks.push(patternsCheck);
116
+ printCheck(patternsCheck);
117
+
118
+ // Check for tests
119
+ const testsCheck = checkTests(cwd);
120
+ checks.push(testsCheck);
121
+ printCheck(testsCheck);
122
+
123
+ // ============================================================
124
+ // SUMMARY
125
+ // ============================================================
126
+ const passed = checks.filter(c => c.passed).length;
127
+ const total = checks.length;
128
+ const score = Math.round((passed / total) * 100);
129
+
130
+ console.log(chalk.white('\n ─────────────────────────────────────────────────\n'));
131
+
132
+ if (score >= 90) {
133
+ console.log(chalk.green(` ✅ Score: ${passed}/${total} checks passed (${score}%)\n`));
134
+ console.log(chalk.green(' Excellent! Your project is production-ready.\n'));
135
+ } else if (score >= 70) {
136
+ console.log(chalk.yellow(` ⚠️ Score: ${passed}/${total} checks passed (${score}%)\n`));
137
+ console.log(chalk.yellow(' Good progress. Fix the issues above before deploying.\n'));
138
+ } else {
139
+ console.log(chalk.red(` ❌ Score: ${passed}/${total} checks passed (${score}%)\n`));
140
+ console.log(chalk.red(' Needs attention. Address critical issues before deploying.\n'));
141
+ }
142
+
143
+ // Show critical issues summary
144
+ const criticalIssues = checks.filter(c => !c.passed && c.severity === 'error');
145
+ if (criticalIssues.length > 0) {
146
+ console.log(chalk.red(' Critical Issues:'));
147
+ for (const issue of criticalIssues) {
148
+ console.log(chalk.red(` • ${issue.message}`));
149
+ }
150
+ console.log('');
151
+ }
152
+
153
+ // Show warnings
154
+ const warnings = checks.filter(c => !c.passed && c.severity === 'warning');
155
+ if (warnings.length > 0) {
156
+ console.log(chalk.yellow(' Warnings:'));
157
+ for (const warning of warnings) {
158
+ console.log(chalk.yellow(` • ${warning.message}`));
159
+ }
160
+ console.log('');
161
+ }
162
+
163
+ console.log(chalk.gray(' Tip: Run /audit in Claude for full 100-point inspection.\n'));
164
+
165
+ return {
166
+ checks,
167
+ score,
168
+ maxScore: 100,
169
+ passed: score >= 70,
170
+ };
171
+ }
172
+
173
+ // ============================================================
174
+ // CHECK FUNCTIONS
175
+ // ============================================================
176
+
177
+ function printCheck(check: AuditCheck): void {
178
+ const icon = check.passed ? chalk.green('✓') : (check.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠'));
179
+ console.log(` ${icon} ${check.message}`);
180
+ if (!check.passed && check.details && check.details.length > 0) {
181
+ for (const detail of check.details.slice(0, 3)) {
182
+ console.log(chalk.gray(` └─ ${detail}`));
183
+ }
184
+ if (check.details.length > 3) {
185
+ console.log(chalk.gray(` └─ ...and ${check.details.length - 3} more`));
186
+ }
187
+ }
188
+ }
189
+
190
+ async function checkTypeScript(cwd: string): Promise<AuditCheck> {
191
+ const tsconfigPath = join(cwd, 'tsconfig.json');
192
+
193
+ if (!existsSync(tsconfigPath)) {
194
+ return {
195
+ name: 'typescript',
196
+ category: 'build',
197
+ passed: false,
198
+ message: 'No tsconfig.json found',
199
+ severity: 'warning',
200
+ };
201
+ }
202
+
203
+ try {
204
+ execSync('npx tsc --noEmit', { cwd, stdio: 'pipe' });
205
+ return {
206
+ name: 'typescript',
207
+ category: 'build',
208
+ passed: true,
209
+ message: 'TypeScript compiles (0 errors)',
210
+ severity: 'info',
211
+ };
212
+ } catch (error) {
213
+ const output = error instanceof Error && 'stdout' in error
214
+ ? (error as { stdout?: Buffer }).stdout?.toString() || ''
215
+ : '';
216
+ const errorCount = (output.match(/error TS/g) || []).length;
217
+
218
+ return {
219
+ name: 'typescript',
220
+ category: 'build',
221
+ passed: false,
222
+ message: `TypeScript errors (${errorCount} errors)`,
223
+ details: ['Run: npx tsc --noEmit to see details'],
224
+ severity: 'error',
225
+ };
226
+ }
227
+ }
228
+
229
+ async function checkESLint(cwd: string): Promise<AuditCheck> {
230
+ const packageJsonPath = join(cwd, 'package.json');
231
+
232
+ if (!existsSync(packageJsonPath)) {
233
+ return {
234
+ name: 'eslint',
235
+ category: 'build',
236
+ passed: false,
237
+ message: 'No package.json found',
238
+ severity: 'warning',
239
+ };
240
+ }
241
+
242
+ try {
243
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
244
+ const hasEslint = packageJson.devDependencies?.eslint || packageJson.dependencies?.eslint;
245
+
246
+ if (!hasEslint) {
247
+ return {
248
+ name: 'eslint',
249
+ category: 'build',
250
+ passed: false,
251
+ message: 'ESLint not installed',
252
+ details: ['Run: npm install -D eslint'],
253
+ severity: 'warning',
254
+ };
255
+ }
256
+
257
+ execSync('npx eslint . --max-warnings=0', { cwd, stdio: 'pipe' });
258
+ return {
259
+ name: 'eslint',
260
+ category: 'build',
261
+ passed: true,
262
+ message: 'ESLint passes (0 warnings)',
263
+ severity: 'info',
264
+ };
265
+ } catch {
266
+ return {
267
+ name: 'eslint',
268
+ category: 'build',
269
+ passed: false,
270
+ message: 'ESLint has warnings or errors',
271
+ details: ['Run: npx eslint . to see details'],
272
+ severity: 'warning',
273
+ };
274
+ }
275
+ }
276
+
277
+ async function checkBuild(cwd: string): Promise<AuditCheck> {
278
+ const packageJsonPath = join(cwd, 'package.json');
279
+
280
+ if (!existsSync(packageJsonPath)) {
281
+ return {
282
+ name: 'build',
283
+ category: 'build',
284
+ passed: false,
285
+ message: 'No package.json found',
286
+ severity: 'error',
287
+ };
288
+ }
289
+
290
+ try {
291
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
292
+ const hasBuildScript = packageJson.scripts?.build;
293
+
294
+ if (!hasBuildScript) {
295
+ return {
296
+ name: 'build',
297
+ category: 'build',
298
+ passed: true,
299
+ message: 'No build script (skipped)',
300
+ severity: 'info',
301
+ };
302
+ }
303
+
304
+ // Check if .next exists (Next.js) or dist exists
305
+ const hasNextBuild = existsSync(join(cwd, '.next'));
306
+ const hasDistBuild = existsSync(join(cwd, 'dist'));
307
+
308
+ if (hasNextBuild || hasDistBuild) {
309
+ return {
310
+ name: 'build',
311
+ category: 'build',
312
+ passed: true,
313
+ message: 'Build output exists',
314
+ severity: 'info',
315
+ };
316
+ }
317
+
318
+ return {
319
+ name: 'build',
320
+ category: 'build',
321
+ passed: false,
322
+ message: 'No build output found',
323
+ details: ['Run: npm run build'],
324
+ severity: 'warning',
325
+ };
326
+ } catch {
327
+ return {
328
+ name: 'build',
329
+ category: 'build',
330
+ passed: false,
331
+ message: 'Could not check build status',
332
+ severity: 'warning',
333
+ };
334
+ }
335
+ }
336
+
337
+ function checkSecretsInCode(cwd: string): AuditCheck {
338
+ const srcDir = join(cwd, 'src');
339
+ if (!existsSync(srcDir)) {
340
+ return {
341
+ name: 'secrets',
342
+ category: 'security',
343
+ passed: true,
344
+ message: 'No src/ directory (skipped)',
345
+ severity: 'info',
346
+ };
347
+ }
348
+
349
+ const secretPatterns = [
350
+ /sk_live_[a-zA-Z0-9]+/, // Stripe live key
351
+ /sk_test_[a-zA-Z0-9]+/, // Stripe test key
352
+ /ghp_[a-zA-Z0-9]+/, // GitHub token
353
+ /AKIA[A-Z0-9]{16}/, // AWS access key
354
+ /-----BEGIN RSA PRIVATE KEY-----/, // RSA key
355
+ /-----BEGIN PRIVATE KEY-----/, // Generic private key
356
+ ];
357
+
358
+ const issues: string[] = [];
359
+
360
+ function scanDir(dir: string): void {
361
+ try {
362
+ const files = readdirSync(dir, { withFileTypes: true });
363
+ for (const file of files) {
364
+ if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
365
+ scanDir(join(dir, file.name));
366
+ } else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.tsx') || file.name.endsWith('.js'))) {
367
+ const content = readFileSync(join(dir, file.name), 'utf-8');
368
+ for (const pattern of secretPatterns) {
369
+ if (pattern.test(content)) {
370
+ issues.push(join(dir, file.name).replace(cwd, ''));
371
+ break;
372
+ }
373
+ }
374
+ }
375
+ }
376
+ } catch {
377
+ // Ignore read errors
378
+ }
379
+ }
380
+
381
+ scanDir(srcDir);
382
+
383
+ if (issues.length > 0) {
384
+ return {
385
+ name: 'secrets',
386
+ category: 'security',
387
+ passed: false,
388
+ message: `Possible secrets in code (${issues.length} files)`,
389
+ details: issues,
390
+ severity: 'error',
391
+ };
392
+ }
393
+
394
+ return {
395
+ name: 'secrets',
396
+ category: 'security',
397
+ passed: true,
398
+ message: 'No secrets detected in code',
399
+ severity: 'info',
400
+ };
401
+ }
402
+
403
+ async function checkNpmAudit(cwd: string): Promise<AuditCheck> {
404
+ try {
405
+ execSync('npm audit --audit-level=high --json', { cwd, stdio: 'pipe' });
406
+ return {
407
+ name: 'npm-audit',
408
+ category: 'security',
409
+ passed: true,
410
+ message: 'No high/critical vulnerabilities',
411
+ severity: 'info',
412
+ };
413
+ } catch (error) {
414
+ try {
415
+ const output = error instanceof Error && 'stdout' in error
416
+ ? (error as { stdout?: Buffer }).stdout?.toString() || '{}'
417
+ : '{}';
418
+ const auditResult = JSON.parse(output);
419
+ const high = auditResult.metadata?.vulnerabilities?.high || 0;
420
+ const critical = auditResult.metadata?.vulnerabilities?.critical || 0;
421
+
422
+ if (high > 0 || critical > 0) {
423
+ return {
424
+ name: 'npm-audit',
425
+ category: 'security',
426
+ passed: false,
427
+ message: `Vulnerabilities: ${critical} critical, ${high} high`,
428
+ details: ['Run: npm audit to see details', 'Run: npm audit fix to auto-fix'],
429
+ severity: 'error',
430
+ };
431
+ }
432
+ } catch {
433
+ // Parse failed, assume passed
434
+ }
435
+
436
+ return {
437
+ name: 'npm-audit',
438
+ category: 'security',
439
+ passed: true,
440
+ message: 'No high/critical vulnerabilities',
441
+ severity: 'info',
442
+ };
443
+ }
444
+ }
445
+
446
+ function checkEnvNotCommitted(cwd: string): AuditCheck {
447
+ const gitignorePath = join(cwd, '.gitignore');
448
+
449
+ if (!existsSync(gitignorePath)) {
450
+ return {
451
+ name: 'env-git',
452
+ category: 'security',
453
+ passed: false,
454
+ message: 'No .gitignore file',
455
+ details: ['Create .gitignore and add .env.local'],
456
+ severity: 'warning',
457
+ };
458
+ }
459
+
460
+ const gitignore = readFileSync(gitignorePath, 'utf-8');
461
+ const hasEnvLocal = gitignore.includes('.env.local') || gitignore.includes('.env*.local');
462
+
463
+ if (!hasEnvLocal) {
464
+ return {
465
+ name: 'env-git',
466
+ category: 'security',
467
+ passed: false,
468
+ message: '.env.local not in .gitignore',
469
+ details: ['Add .env.local to .gitignore'],
470
+ severity: 'error',
471
+ };
472
+ }
473
+
474
+ return {
475
+ name: 'env-git',
476
+ category: 'security',
477
+ passed: true,
478
+ message: '.env.local properly gitignored',
479
+ severity: 'info',
480
+ };
481
+ }
482
+
483
+ function checkConsoleLog(cwd: string): AuditCheck {
484
+ const srcDir = join(cwd, 'src');
485
+ if (!existsSync(srcDir)) {
486
+ return {
487
+ name: 'console-log',
488
+ category: 'quality',
489
+ passed: true,
490
+ message: 'No src/ directory (skipped)',
491
+ severity: 'info',
492
+ };
493
+ }
494
+
495
+ const issues: string[] = [];
496
+
497
+ function scanDir(dir: string): void {
498
+ try {
499
+ const files = readdirSync(dir, { withFileTypes: true });
500
+ for (const file of files) {
501
+ if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
502
+ scanDir(join(dir, file.name));
503
+ } else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.tsx'))) {
504
+ const content = readFileSync(join(dir, file.name), 'utf-8');
505
+ // Match console.log but not console.error or console.warn
506
+ if (/console\.log\s*\(/.test(content)) {
507
+ issues.push(join(dir, file.name).replace(cwd, ''));
508
+ }
509
+ }
510
+ }
511
+ } catch {
512
+ // Ignore read errors
513
+ }
514
+ }
515
+
516
+ scanDir(srcDir);
517
+
518
+ if (issues.length > 0) {
519
+ return {
520
+ name: 'console-log',
521
+ category: 'quality',
522
+ passed: false,
523
+ message: `console.log found (${issues.length} files)`,
524
+ details: issues,
525
+ severity: 'warning',
526
+ };
527
+ }
528
+
529
+ return {
530
+ name: 'console-log',
531
+ category: 'quality',
532
+ passed: true,
533
+ message: 'No console.log in production code',
534
+ severity: 'info',
535
+ };
536
+ }
537
+
538
+ function checkApiValidation(cwd: string): AuditCheck {
539
+ const apiDir = join(cwd, 'src', 'app', 'api');
540
+ if (!existsSync(apiDir)) {
541
+ return {
542
+ name: 'api-validation',
543
+ category: 'quality',
544
+ passed: true,
545
+ message: 'No API routes (skipped)',
546
+ severity: 'info',
547
+ };
548
+ }
549
+
550
+ const issues: string[] = [];
551
+ let totalRoutes = 0;
552
+
553
+ function scanDir(dir: string): void {
554
+ try {
555
+ const files = readdirSync(dir, { withFileTypes: true });
556
+ for (const file of files) {
557
+ if (file.isDirectory()) {
558
+ scanDir(join(dir, file.name));
559
+ } else if (file.name === 'route.ts' || file.name === 'route.tsx') {
560
+ totalRoutes++;
561
+ const content = readFileSync(join(dir, file.name), 'utf-8');
562
+ // Check for Zod validation
563
+ if (!content.includes('zod') && !content.includes('.parse(') && !content.includes('.safeParse(')) {
564
+ issues.push(join(dir, file.name).replace(cwd, ''));
565
+ }
566
+ }
567
+ }
568
+ } catch {
569
+ // Ignore read errors
570
+ }
571
+ }
572
+
573
+ scanDir(apiDir);
574
+
575
+ if (issues.length > 0) {
576
+ return {
577
+ name: 'api-validation',
578
+ category: 'quality',
579
+ passed: false,
580
+ message: `API routes without validation (${issues.length}/${totalRoutes})`,
581
+ details: issues,
582
+ severity: 'warning',
583
+ };
584
+ }
585
+
586
+ return {
587
+ name: 'api-validation',
588
+ category: 'quality',
589
+ passed: true,
590
+ message: `All ${totalRoutes} API routes have validation`,
591
+ severity: 'info',
592
+ };
593
+ }
594
+
595
+ function checkErrorBoundary(cwd: string): AuditCheck {
596
+ const appDir = join(cwd, 'src', 'app');
597
+ if (!existsSync(appDir)) {
598
+ return {
599
+ name: 'error-boundary',
600
+ category: 'quality',
601
+ passed: true,
602
+ message: 'No app/ directory (skipped)',
603
+ severity: 'info',
604
+ };
605
+ }
606
+
607
+ const errorPath = join(appDir, 'error.tsx');
608
+ const globalErrorPath = join(appDir, 'global-error.tsx');
609
+
610
+ if (existsSync(errorPath) || existsSync(globalErrorPath)) {
611
+ return {
612
+ name: 'error-boundary',
613
+ category: 'quality',
614
+ passed: true,
615
+ message: 'Error boundary exists',
616
+ severity: 'info',
617
+ };
618
+ }
619
+
620
+ return {
621
+ name: 'error-boundary',
622
+ category: 'quality',
623
+ passed: false,
624
+ message: 'No error boundary (error.tsx)',
625
+ details: ['Create src/app/error.tsx for error handling'],
626
+ severity: 'warning',
627
+ };
628
+ }
629
+
630
+ function checkEnvExample(cwd: string): AuditCheck {
631
+ const envExamplePath = join(cwd, '.env.example');
632
+
633
+ if (existsSync(envExamplePath)) {
634
+ return {
635
+ name: 'env-example',
636
+ category: 'environment',
637
+ passed: true,
638
+ message: '.env.example exists',
639
+ severity: 'info',
640
+ };
641
+ }
642
+
643
+ return {
644
+ name: 'env-example',
645
+ category: 'environment',
646
+ passed: false,
647
+ message: 'No .env.example file',
648
+ details: ['Create .env.example with all required variables'],
649
+ severity: 'warning',
650
+ };
651
+ }
652
+
653
+ function checkEnvMatch(cwd: string): AuditCheck {
654
+ const envExamplePath = join(cwd, '.env.example');
655
+ const envLocalPath = join(cwd, '.env.local');
656
+
657
+ if (!existsSync(envExamplePath)) {
658
+ return {
659
+ name: 'env-match',
660
+ category: 'environment',
661
+ passed: true,
662
+ message: 'No .env.example to compare (skipped)',
663
+ severity: 'info',
664
+ };
665
+ }
666
+
667
+ if (!existsSync(envLocalPath)) {
668
+ return {
669
+ name: 'env-match',
670
+ category: 'environment',
671
+ passed: false,
672
+ message: 'No .env.local file',
673
+ details: ['Copy .env.example to .env.local and fill in values'],
674
+ severity: 'warning',
675
+ };
676
+ }
677
+
678
+ const exampleVars = parseEnvFile(readFileSync(envExamplePath, 'utf-8'));
679
+ const localVars = parseEnvFile(readFileSync(envLocalPath, 'utf-8'));
680
+
681
+ const missingInLocal = exampleVars.filter(v => !localVars.includes(v));
682
+ const extraInLocal = localVars.filter(v => !exampleVars.includes(v) && !v.startsWith('#'));
683
+
684
+ if (missingInLocal.length > 0) {
685
+ return {
686
+ name: 'env-match',
687
+ category: 'environment',
688
+ passed: false,
689
+ message: `Missing ${missingInLocal.length} vars from .env.example`,
690
+ details: missingInLocal,
691
+ severity: 'warning',
692
+ };
693
+ }
694
+
695
+ if (extraInLocal.length > 0) {
696
+ return {
697
+ name: 'env-match',
698
+ category: 'environment',
699
+ passed: false,
700
+ message: `${extraInLocal.length} vars in .env.local not in .env.example`,
701
+ details: extraInLocal,
702
+ severity: 'info',
703
+ };
704
+ }
705
+
706
+ return {
707
+ name: 'env-match',
708
+ category: 'environment',
709
+ passed: true,
710
+ message: 'Environment variables match',
711
+ severity: 'info',
712
+ };
713
+ }
714
+
715
+ function parseEnvFile(content: string): string[] {
716
+ return content
717
+ .split('\n')
718
+ .filter(line => line.trim() && !line.startsWith('#'))
719
+ .map(line => line.split('=')[0].trim())
720
+ .filter(Boolean);
721
+ }
722
+
723
+ function checkCodeBakersPatterns(cwd: string): AuditCheck {
724
+ const claudeDir = join(cwd, '.claude');
725
+ const claudeMd = join(cwd, 'CLAUDE.md');
726
+
727
+ if (!existsSync(claudeDir) && !existsSync(claudeMd)) {
728
+ return {
729
+ name: 'patterns',
730
+ category: 'structure',
731
+ passed: false,
732
+ message: 'CodeBakers patterns not installed',
733
+ details: ['Run: codebakers init'],
734
+ severity: 'info',
735
+ };
736
+ }
737
+
738
+ if (existsSync(claudeDir)) {
739
+ const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
740
+ return {
741
+ name: 'patterns',
742
+ category: 'structure',
743
+ passed: true,
744
+ message: `${files.length} CodeBakers modules installed`,
745
+ severity: 'info',
746
+ };
747
+ }
748
+
749
+ return {
750
+ name: 'patterns',
751
+ category: 'structure',
752
+ passed: true,
753
+ message: 'CLAUDE.md exists',
754
+ severity: 'info',
755
+ };
756
+ }
757
+
758
+ function checkTests(cwd: string): AuditCheck {
759
+ const testDirs = [
760
+ join(cwd, '__tests__'),
761
+ join(cwd, 'tests'),
762
+ join(cwd, 'test'),
763
+ join(cwd, 'src', '__tests__'),
764
+ ];
765
+
766
+ const testFiles: string[] = [];
767
+
768
+ for (const dir of testDirs) {
769
+ if (existsSync(dir)) {
770
+ try {
771
+ const files = readdirSync(dir, { recursive: true }) as string[];
772
+ testFiles.push(...files.filter(f =>
773
+ f.endsWith('.test.ts') ||
774
+ f.endsWith('.test.tsx') ||
775
+ f.endsWith('.spec.ts') ||
776
+ f.endsWith('.spec.tsx')
777
+ ));
778
+ } catch {
779
+ // Ignore
780
+ }
781
+ }
782
+ }
783
+
784
+ // Also check for test files in src
785
+ const srcDir = join(cwd, 'src');
786
+ if (existsSync(srcDir)) {
787
+ function findTestFiles(dir: string): void {
788
+ try {
789
+ const files = readdirSync(dir, { withFileTypes: true });
790
+ for (const file of files) {
791
+ if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
792
+ findTestFiles(join(dir, file.name));
793
+ } else if (
794
+ file.name.endsWith('.test.ts') ||
795
+ file.name.endsWith('.test.tsx') ||
796
+ file.name.endsWith('.spec.ts') ||
797
+ file.name.endsWith('.spec.tsx')
798
+ ) {
799
+ testFiles.push(file.name);
800
+ }
801
+ }
802
+ } catch {
803
+ // Ignore
804
+ }
805
+ }
806
+ findTestFiles(srcDir);
807
+ }
808
+
809
+ if (testFiles.length === 0) {
810
+ return {
811
+ name: 'tests',
812
+ category: 'structure',
813
+ passed: false,
814
+ message: 'No test files found',
815
+ details: ['Add tests for critical paths'],
816
+ severity: 'warning',
817
+ };
818
+ }
819
+
820
+ return {
821
+ name: 'tests',
822
+ category: 'structure',
823
+ passed: true,
824
+ message: `${testFiles.length} test files found`,
825
+ severity: 'info',
826
+ };
827
+ }