@codebakers/cli 3.9.38 โ†’ 3.9.40

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.
@@ -8,19 +8,20 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const fs_1 = require("fs");
9
9
  const path_1 = require("path");
10
10
  const PRE_COMMIT_SCRIPT = `#!/bin/sh
11
- # CodeBakers Pre-Commit Hook - Session Enforcement
12
- # Blocks commits unless AI called discover_patterns and validate_complete
11
+ # CodeBakers Pre-Commit Hook - Comprehensive Code Validation
12
+ # Actually scans code for pattern violations
13
13
 
14
14
  # Run the validation script
15
- node "$(dirname "$0")/validate-session.js"
15
+ node "$(dirname "$0")/validate-code.js"
16
16
  exit $?
17
17
  `;
18
- const VALIDATE_SESSION_SCRIPT = `#!/usr/bin/env node
18
+ const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
19
19
  /**
20
- * CodeBakers Pre-Commit Validation
21
- * Blocks commits unless a valid session exists
20
+ * CodeBakers Pre-Commit Code Validator v2.0
21
+ * Comprehensive code validation - 40+ checks
22
22
  */
23
23
 
24
+ const { execSync } = require('child_process');
24
25
  const fs = require('fs');
25
26
  const path = require('path');
26
27
 
@@ -28,124 +29,904 @@ const RED = '\\x1b[31m';
28
29
  const GREEN = '\\x1b[32m';
29
30
  const YELLOW = '\\x1b[33m';
30
31
  const CYAN = '\\x1b[36m';
32
+ const DIM = '\\x1b[2m';
31
33
  const RESET = '\\x1b[0m';
32
34
 
33
35
  function log(color, message) {
34
36
  console.log(color + message + RESET);
35
37
  }
36
38
 
37
- async function validateSession() {
38
- const cwd = process.cwd();
39
- const stateFile = path.join(cwd, '.codebakers.json');
40
-
41
- // Check if this is a CodeBakers project
42
- if (!fs.existsSync(stateFile)) {
43
- return { valid: true, reason: 'not-codebakers-project' };
44
- }
45
-
46
- let state;
39
+ // Get staged files
40
+ function getStagedFiles() {
47
41
  try {
48
- state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
49
- } catch (error) {
50
- return { valid: false, reason: 'invalid-state-file' };
42
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' });
43
+ return output.split('\\n').filter(f => f.trim());
44
+ } catch {
45
+ return [];
51
46
  }
47
+ }
52
48
 
53
- // Check if v6.0 server-enforced mode
54
- if (!state.serverEnforced) {
55
- return { valid: true, reason: 'legacy-project' };
56
- }
49
+ // Get code files only
50
+ function getCodeFiles(files) {
51
+ return files.filter(f =>
52
+ f.endsWith('.ts') || f.endsWith('.tsx') ||
53
+ f.endsWith('.js') || f.endsWith('.jsx')
54
+ );
55
+ }
57
56
 
58
- // Check for session token (means discover_patterns was called)
59
- const sessionToken = state.currentSessionToken;
60
- if (!sessionToken) {
61
- // Check if there's a recent passed validation
62
- const lastValidation = state.lastValidation;
63
- if (!lastValidation || !lastValidation.passed) {
64
- return {
65
- valid: false,
66
- reason: 'no-session',
67
- message: 'No active CodeBakers session.\\nAI must call discover_patterns before writing code.'
68
- };
57
+ // ============================================
58
+ // ALL CHECKS - Organized by Category
59
+ // ============================================
60
+
61
+ const CHECKS = [
62
+ // ==========================================
63
+ // SECURITY CHECKS
64
+ // ==========================================
65
+ {
66
+ name: 'Debugger Statement',
67
+ category: 'security',
68
+ test: (content, file) => {
69
+ if (content.includes('debugger;') || content.includes('debugger ')) {
70
+ return 'debugger statement left in code - remove before commit';
71
+ }
72
+ return null;
69
73
  }
70
- }
74
+ },
75
+ {
76
+ name: 'Hardcoded Secrets',
77
+ category: 'security',
78
+ test: (content, file) => {
79
+ if (file.includes('.env') || file.includes('config')) return null;
80
+ const patterns = [
81
+ /api[_-]?key\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
82
+ /secret\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
83
+ /password\\s*[:=]\\s*['"][^'"]{8,}['"]/i,
84
+ /sk_live_[a-zA-Z0-9]+/,
85
+ /sk_test_[a-zA-Z0-9]+/,
86
+ /ghp_[a-zA-Z0-9]+/, // GitHub token
87
+ /xox[baprs]-[a-zA-Z0-9]+/, // Slack token
88
+ /AKIA[0-9A-Z]{16}/, // AWS access key
89
+ ];
90
+ for (const pattern of patterns) {
91
+ if (pattern.test(content)) {
92
+ return 'Possible hardcoded secret - use environment variables';
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ },
98
+ {
99
+ name: 'XSS Vulnerability',
100
+ category: 'security',
101
+ test: (content, file) => {
102
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
103
+ if (content.includes('dangerouslySetInnerHTML') &&
104
+ !content.includes('DOMPurify') &&
105
+ !content.includes('sanitize')) {
106
+ return 'dangerouslySetInnerHTML without sanitization - XSS risk';
107
+ }
108
+ return null;
109
+ }
110
+ },
111
+ {
112
+ name: 'Merge Conflict Markers',
113
+ category: 'security',
114
+ test: (content, file) => {
115
+ if (content.includes('<<<<<<<') || content.includes('>>>>>>>') || content.includes('=======\\n')) {
116
+ return 'Merge conflict markers found - resolve conflicts first';
117
+ }
118
+ return null;
119
+ }
120
+ },
121
+ {
122
+ name: 'Private Key in Code',
123
+ category: 'security',
124
+ test: (content, file) => {
125
+ if (content.includes('-----BEGIN RSA PRIVATE KEY-----') ||
126
+ content.includes('-----BEGIN PRIVATE KEY-----') ||
127
+ content.includes('-----BEGIN EC PRIVATE KEY-----')) {
128
+ return 'Private key detected in code - NEVER commit private keys';
129
+ }
130
+ return null;
131
+ }
132
+ },
133
+ {
134
+ name: 'Env File Commit',
135
+ category: 'security',
136
+ test: (content, file) => {
137
+ if (file === '.env' || file === '.env.local' || file === '.env.production') {
138
+ return '.env file should not be committed - add to .gitignore';
139
+ }
140
+ return null;
141
+ }
142
+ },
143
+ {
144
+ name: 'SQL Injection Risk',
145
+ category: 'security',
146
+ test: (content, file) => {
147
+ const sqlPatterns = [
148
+ /\\$\\{.*\\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/i,
149
+ /['"]\\s*\\+\\s*.*\\+\\s*['"].*(?:SELECT|INSERT|UPDATE|DELETE)/i,
150
+ /sql\\s*\\(\\s*\`[^\\)]*\\$\\{/,
151
+ ];
152
+ for (const pattern of sqlPatterns) {
153
+ if (pattern.test(content)) {
154
+ return 'Possible SQL injection - use parameterized queries';
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ },
160
+ {
161
+ name: 'Eval Usage',
162
+ category: 'security',
163
+ test: (content, file) => {
164
+ if (/\\beval\\s*\\(/.test(content) || /new\\s+Function\\s*\\(/.test(content)) {
165
+ return 'eval() or new Function() detected - security risk';
166
+ }
167
+ return null;
168
+ }
169
+ },
170
+ {
171
+ name: 'Sensitive Data in Logs',
172
+ category: 'security',
173
+ test: (content, file) => {
174
+ const sensitivePatterns = [
175
+ /console\\.log.*password/i,
176
+ /console\\.log.*token/i,
177
+ /console\\.log.*secret/i,
178
+ /console\\.log.*apiKey/i,
179
+ /console\\.log.*creditCard/i,
180
+ /console\\.log.*ssn/i,
181
+ /console\\.log.*authorization/i,
182
+ ];
183
+ for (const pattern of sensitivePatterns) {
184
+ if (pattern.test(content)) {
185
+ return 'Possible sensitive data being logged';
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ },
71
191
 
72
- // Check session expiry
73
- const sessionExpiry = state.sessionExpiresAt;
74
- if (sessionExpiry && new Date(sessionExpiry) < new Date()) {
75
- return {
76
- valid: false,
77
- reason: 'session-expired',
78
- message: 'CodeBakers session has expired.\\nAI must call discover_patterns again.'
79
- };
80
- }
192
+ // ==========================================
193
+ // ERROR HANDLING CHECKS
194
+ // ==========================================
195
+ {
196
+ name: 'API Error Handling',
197
+ category: 'errors',
198
+ test: (content, file) => {
199
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
200
+ if (!content.includes('try {') && !content.includes('try{')) {
201
+ return 'API route missing try/catch error handling';
202
+ }
203
+ return null;
204
+ }
205
+ },
206
+ {
207
+ name: 'Empty Catch Block',
208
+ category: 'errors',
209
+ test: (content, file) => {
210
+ if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}/.test(content)) {
211
+ return 'Empty catch block - handle or rethrow errors';
212
+ }
213
+ return null;
214
+ }
215
+ },
216
+ {
217
+ name: 'Catch Without Logging',
218
+ category: 'errors',
219
+ test: (content, file) => {
220
+ const catchBlocks = content.match(/catch\\s*\\([^)]*\\)\\s*\\{[^}]{1,50}\\}/g) || [];
221
+ for (const block of catchBlocks) {
222
+ if (!block.includes('console') && !block.includes('log') &&
223
+ !block.includes('throw') && !block.includes('error') &&
224
+ !block.includes('report') && !block.includes('track')) {
225
+ return 'Catch block may be swallowing errors - log or rethrow';
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+ },
231
+ {
232
+ name: 'Unsafe JSON Parse',
233
+ category: 'errors',
234
+ test: (content, file) => {
235
+ if (content.includes('JSON.parse(') &&
236
+ !content.includes('try') && !content.includes('catch')) {
237
+ return 'JSON.parse without try/catch - can throw on invalid JSON';
238
+ }
239
+ return null;
240
+ }
241
+ },
242
+ {
243
+ name: 'Unhandled Promise',
244
+ category: 'errors',
245
+ test: (content, file) => {
246
+ const lines = content.split('\\n');
247
+ for (let i = 0; i < lines.length; i++) {
248
+ const line = lines[i].trim();
249
+ if (line.match(/(?:fetch|axios|db\\.|prisma\\.).*\\(/) &&
250
+ !line.includes('await') &&
251
+ !line.includes('.then') &&
252
+ !line.includes('.catch') &&
253
+ !line.includes('return') &&
254
+ !line.includes('=')) {
255
+ return \`Unhandled promise at line \${i + 1}\`;
256
+ }
257
+ }
258
+ return null;
259
+ }
260
+ },
261
+ {
262
+ name: 'Missing Async Error Handling',
263
+ category: 'errors',
264
+ test: (content, file) => {
265
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
266
+ if (content.includes('async') && content.includes('await') &&
267
+ !content.includes('try') && !content.includes('.catch')) {
268
+ return 'Async function with await but no error handling';
269
+ }
270
+ return null;
271
+ }
272
+ },
81
273
 
82
- // Check if validation was completed
83
- const lastValidation = state.lastValidation;
84
- if (!lastValidation) {
85
- return {
86
- valid: false,
87
- reason: 'no-validation',
88
- message: 'No validation completed.\\nAI must call validate_complete before committing.'
89
- };
274
+ // ==========================================
275
+ // VALIDATION CHECKS
276
+ // ==========================================
277
+ {
278
+ name: 'Zod Validation',
279
+ category: 'validation',
280
+ test: (content, file) => {
281
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
282
+ if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
283
+ content.includes('req.json()') &&
284
+ !content.includes('z.object') &&
285
+ !content.includes('schema.parse') &&
286
+ !content.includes('Schema.parse') &&
287
+ !content.includes('validate')) {
288
+ return 'API route accepts body but missing Zod validation';
289
+ }
290
+ return null;
291
+ }
292
+ },
293
+ {
294
+ name: 'Missing Auth Check',
295
+ category: 'validation',
296
+ test: (content, file) => {
297
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
298
+ if (file.includes('/public/') || file.includes('/auth/') || file.includes('/webhook')) return null;
299
+ if ((content.includes('userId') || content.includes('user.id') || content.includes('session')) &&
300
+ !content.includes('getServerSession') &&
301
+ !content.includes('auth(') &&
302
+ !content.includes('requireAuth') &&
303
+ !content.includes('verifyToken') &&
304
+ !content.includes('validateSession') &&
305
+ !content.includes('getSession')) {
306
+ return 'Route accesses user data but may be missing auth check';
307
+ }
308
+ return null;
309
+ }
310
+ },
311
+
312
+ // ==========================================
313
+ // CODE QUALITY CHECKS
314
+ // ==========================================
315
+ {
316
+ name: 'Console Statements',
317
+ category: 'quality',
318
+ test: (content, file) => {
319
+ if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
320
+ const lines = content.split('\\n');
321
+ for (let i = 0; i < lines.length; i++) {
322
+ const line = lines[i];
323
+ if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
324
+ if (line.includes('console.log(')) {
325
+ return \`console.log at line \${i + 1} - use proper logging\`;
326
+ }
327
+ }
328
+ return null;
329
+ }
330
+ },
331
+ {
332
+ name: 'TODO/FIXME Comments',
333
+ category: 'quality',
334
+ test: (content, file) => {
335
+ const match = content.match(/\\/\\/\\s*(TODO|FIXME|XXX|HACK):/i);
336
+ if (match) {
337
+ return \`Unresolved \${match[1]} comment - address before commit\`;
338
+ }
339
+ return null;
340
+ }
341
+ },
342
+ {
343
+ name: 'Hardcoded URLs',
344
+ category: 'quality',
345
+ test: (content, file) => {
346
+ if (file.includes('.test.') || file.includes('/tests/')) return null;
347
+ if (content.includes('localhost:') &&
348
+ !content.includes('process.env') &&
349
+ !content.includes("|| 'http://localhost") &&
350
+ !content.includes('|| "http://localhost')) {
351
+ return 'Hardcoded localhost URL - use environment variable with fallback';
352
+ }
353
+ return null;
354
+ }
355
+ },
356
+ {
357
+ name: 'Large File',
358
+ category: 'quality',
359
+ test: (content, file) => {
360
+ const lines = content.split('\\n').length;
361
+ if (lines > 500) {
362
+ return \`File has \${lines} lines - consider splitting into smaller modules\`;
363
+ }
364
+ return null;
365
+ }
366
+ },
367
+ {
368
+ name: 'Magic Numbers',
369
+ category: 'quality',
370
+ test: (content, file) => {
371
+ // Look for unexplained numbers in conditions
372
+ const magicPattern = /(?:if|while|for)\\s*\\([^)]*[^0-9.](\\d{3,})[^0-9.]/;
373
+ const match = content.match(magicPattern);
374
+ if (match && !content.includes('const') && !content.includes('let')) {
375
+ return 'Magic number detected - use named constants';
376
+ }
377
+ return null;
378
+ }
379
+ },
380
+ {
381
+ name: 'Commented Out Code',
382
+ category: 'quality',
383
+ test: (content, file) => {
384
+ const lines = content.split('\\n');
385
+ let commentedCodeCount = 0;
386
+ for (const line of lines) {
387
+ if (line.trim().match(/^\\/\\/\\s*(const|let|var|function|if|for|while|return|import|export)\\s/)) {
388
+ commentedCodeCount++;
389
+ }
390
+ }
391
+ if (commentedCodeCount > 5) {
392
+ return \`\${commentedCodeCount} lines of commented code - remove dead code\`;
393
+ }
394
+ return null;
395
+ }
396
+ },
397
+
398
+ // ==========================================
399
+ // TYPESCRIPT CHECKS
400
+ // ==========================================
401
+ {
402
+ name: 'Any Type Usage',
403
+ category: 'typescript',
404
+ test: (content, file) => {
405
+ if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
406
+ if (content.includes(': any)') || content.includes(': any,') ||
407
+ content.includes(': any;') || content.includes(': any =') ||
408
+ content.includes('<any>') || content.includes('as any')) {
409
+ return 'Using "any" type - provide proper TypeScript types';
410
+ }
411
+ return null;
412
+ }
413
+ },
414
+ {
415
+ name: 'Type Assertion Override',
416
+ category: 'typescript',
417
+ test: (content, file) => {
418
+ if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
419
+ if (content.includes('as unknown as') || content.includes('!.') && content.match(/!\\.[a-zA-Z]/)) {
420
+ return 'Unsafe type assertion - validate types properly';
421
+ }
422
+ return null;
423
+ }
424
+ },
425
+ {
426
+ name: 'Missing Return Type',
427
+ category: 'typescript',
428
+ test: (content, file) => {
429
+ if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
430
+ const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
431
+ for (const func of exportedFunctions) {
432
+ if (!func.includes(':')) {
433
+ return 'Exported function missing return type annotation';
434
+ }
435
+ }
436
+ return null;
437
+ }
438
+ },
439
+ {
440
+ name: 'Non-null Assertion',
441
+ category: 'typescript',
442
+ test: (content, file) => {
443
+ if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
444
+ const assertions = (content.match(/\\w+!/g) || []).filter(m => !m.includes('!='));
445
+ if (assertions.length > 3) {
446
+ return \`\${assertions.length} non-null assertions (!) - handle null cases properly\`;
447
+ }
448
+ return null;
449
+ }
450
+ },
451
+
452
+ // ==========================================
453
+ // REACT CHECKS
454
+ // ==========================================
455
+ {
456
+ name: 'Direct DOM Manipulation',
457
+ category: 'react',
458
+ test: (content, file) => {
459
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
460
+ if (content.includes('document.getElementById') ||
461
+ content.includes('document.querySelector') ||
462
+ content.includes('document.createElement') ||
463
+ content.includes('document.body')) {
464
+ return 'Direct DOM manipulation in React - use refs or state';
465
+ }
466
+ return null;
467
+ }
468
+ },
469
+ {
470
+ name: 'Missing useEffect Cleanup',
471
+ category: 'react',
472
+ test: (content, file) => {
473
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
474
+ // Check for useEffect with subscriptions but no cleanup
475
+ if (content.includes('useEffect') &&
476
+ (content.includes('addEventListener') ||
477
+ content.includes('subscribe') ||
478
+ content.includes('setInterval') ||
479
+ content.includes('setTimeout')) &&
480
+ !content.includes('removeEventListener') &&
481
+ !content.includes('unsubscribe') &&
482
+ !content.includes('clearInterval') &&
483
+ !content.includes('clearTimeout') &&
484
+ !content.includes('return () =>')) {
485
+ return 'useEffect with subscription but no cleanup - memory leak risk';
486
+ }
487
+ return null;
488
+ }
489
+ },
490
+ {
491
+ name: 'Conditional Hook',
492
+ category: 'react',
493
+ test: (content, file) => {
494
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
495
+ const lines = content.split('\\n');
496
+ let inCondition = false;
497
+ for (let i = 0; i < lines.length; i++) {
498
+ const line = lines[i].trim();
499
+ if (line.startsWith('if ') || line.startsWith('if(')) inCondition = true;
500
+ if (line.includes('}')) inCondition = false;
501
+ if (inCondition && (line.includes('useState') || line.includes('useEffect') ||
502
+ line.includes('useCallback') || line.includes('useMemo'))) {
503
+ return \`Hook called conditionally at line \${i + 1} - violates Rules of Hooks\`;
504
+ }
505
+ }
506
+ return null;
507
+ }
508
+ },
509
+ {
510
+ name: 'Missing Key Prop',
511
+ category: 'react',
512
+ test: (content, file) => {
513
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
514
+ if ((content.includes('.map(') || content.includes('.map (')) &&
515
+ content.includes('return') &&
516
+ content.includes('<') &&
517
+ !content.includes('key=') &&
518
+ !content.includes('key:')) {
519
+ return 'Array .map() rendering JSX without key prop';
520
+ }
521
+ return null;
522
+ }
523
+ },
524
+ {
525
+ name: 'Index as Key',
526
+ category: 'react',
527
+ test: (content, file) => {
528
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
529
+ if (content.includes('key={i}') || content.includes('key={index}') ||
530
+ content.includes('key={idx}')) {
531
+ return 'Using array index as key - use unique identifier instead';
532
+ }
533
+ return null;
534
+ }
535
+ },
536
+ {
537
+ name: 'Inline Function in JSX',
538
+ category: 'react',
539
+ test: (content, file) => {
540
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
541
+ // Check for arrow functions in onClick, onChange, etc.
542
+ const inlinePatterns = [
543
+ /onClick=\\{\\s*\\(\\)\\s*=>/,
544
+ /onChange=\\{\\s*\\(e?\\)\\s*=>/,
545
+ /onSubmit=\\{\\s*\\(e?\\)\\s*=>/,
546
+ ];
547
+ for (const pattern of inlinePatterns) {
548
+ if (pattern.test(content)) {
549
+ return 'Inline function in JSX - use useCallback for performance';
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+ },
555
+ {
556
+ name: 'Missing Error Boundary',
557
+ category: 'react',
558
+ test: (content, file) => {
559
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
560
+ // Check if it's a page component without error handling
561
+ if ((file.includes('/app/') || file.includes('/pages/')) &&
562
+ file.includes('page.') &&
563
+ !content.includes('ErrorBoundary') &&
564
+ !content.includes('error.') &&
565
+ content.includes('async')) {
566
+ return 'Page component may need error boundary for async operations';
567
+ }
568
+ return null;
569
+ }
570
+ },
571
+
572
+ // ==========================================
573
+ // ACCESSIBILITY CHECKS
574
+ // ==========================================
575
+ {
576
+ name: 'Image Without Alt',
577
+ category: 'a11y',
578
+ test: (content, file) => {
579
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
580
+ if ((content.includes('<img') || content.includes('<Image')) &&
581
+ !content.includes('alt=') && !content.includes('alt:')) {
582
+ return 'Image without alt attribute - add alt text for accessibility';
583
+ }
584
+ return null;
585
+ }
586
+ },
587
+ {
588
+ name: 'Button Without Type',
589
+ category: 'a11y',
590
+ test: (content, file) => {
591
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
592
+ // Check for buttons without type attribute
593
+ if (content.includes('<button') &&
594
+ !content.includes('type="button"') &&
595
+ !content.includes('type="submit"') &&
596
+ !content.includes("type='button'") &&
597
+ !content.includes("type='submit'") &&
598
+ !content.includes('type={"button"}') &&
599
+ !content.includes('type={"submit"}')) {
600
+ return 'Button without type attribute - specify type="button" or type="submit"';
601
+ }
602
+ return null;
603
+ }
604
+ },
605
+ {
606
+ name: 'Missing Form Label',
607
+ category: 'a11y',
608
+ test: (content, file) => {
609
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
610
+ if ((content.includes('<input') || content.includes('<select') || content.includes('<textarea')) &&
611
+ !content.includes('<label') &&
612
+ !content.includes('aria-label') &&
613
+ !content.includes('aria-labelledby') &&
614
+ !content.includes('Label')) {
615
+ return 'Form input without label - add label for accessibility';
616
+ }
617
+ return null;
618
+ }
619
+ },
620
+ {
621
+ name: 'Click Handler Without Keyboard',
622
+ category: 'a11y',
623
+ test: (content, file) => {
624
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
625
+ // Check for divs/spans with onClick but no keyboard handler
626
+ if ((content.includes('<div') || content.includes('<span')) &&
627
+ content.includes('onClick') &&
628
+ !content.includes('onKeyDown') &&
629
+ !content.includes('onKeyPress') &&
630
+ !content.includes('onKeyUp') &&
631
+ !content.includes('role=') &&
632
+ !content.includes('tabIndex')) {
633
+ return 'Clickable element without keyboard support - add onKeyDown and role';
634
+ }
635
+ return null;
636
+ }
637
+ },
638
+ {
639
+ name: 'Missing ARIA Role',
640
+ category: 'a11y',
641
+ test: (content, file) => {
642
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
643
+ // Custom interactive elements should have roles
644
+ if (content.includes('onClick') &&
645
+ (content.includes('<div') || content.includes('<span')) &&
646
+ !content.includes('role=') &&
647
+ !content.includes('<button') &&
648
+ !content.includes('<a ')) {
649
+ return 'Interactive element without ARIA role - add appropriate role';
650
+ }
651
+ return null;
652
+ }
653
+ },
654
+
655
+ // ==========================================
656
+ // PERFORMANCE CHECKS
657
+ // ==========================================
658
+ {
659
+ name: 'Large Import',
660
+ category: 'performance',
661
+ test: (content, file) => {
662
+ // Check for importing entire libraries
663
+ const largeImports = [
664
+ /import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['"]lodash['"]/,
665
+ /import\\s+\\{[^}]{100,}\\}\\s+from/, // Very large destructured import
666
+ /import\\s+moment\\s+from/, // moment.js is large
667
+ ];
668
+ for (const pattern of largeImports) {
669
+ if (pattern.test(content)) {
670
+ return 'Large library import - use specific imports for smaller bundle';
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+ },
676
+ {
677
+ name: 'Sync File Operation',
678
+ category: 'performance',
679
+ test: (content, file) => {
680
+ if (file.includes('.test.') || file.includes('/scripts/')) return null;
681
+ if (content.includes('readFileSync') || content.includes('writeFileSync') ||
682
+ content.includes('existsSync') || content.includes('readdirSync')) {
683
+ if (file.includes('/api/') || file.includes('route.ts')) {
684
+ return 'Synchronous file operation in API route - use async version';
685
+ }
686
+ }
687
+ return null;
688
+ }
689
+ },
690
+ {
691
+ name: 'Missing Memoization',
692
+ category: 'performance',
693
+ test: (content, file) => {
694
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
695
+ // Large computations in render without useMemo
696
+ if ((content.includes('.filter(') || content.includes('.reduce(') || content.includes('.sort(')) &&
697
+ content.includes('return') &&
698
+ content.includes('<') &&
699
+ !content.includes('useMemo') &&
700
+ content.split('.filter(').length > 2) {
701
+ return 'Multiple array operations in render - consider useMemo';
702
+ }
703
+ return null;
704
+ }
705
+ },
706
+
707
+ // ==========================================
708
+ // API/DATABASE CHECKS
709
+ // ==========================================
710
+ {
711
+ name: 'Missing Rate Limit',
712
+ category: 'api',
713
+ test: (content, file) => {
714
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
715
+ if (file.includes('/public/')) return null;
716
+ if (!content.includes('rateLimit') &&
717
+ !content.includes('rateLimiter') &&
718
+ !content.includes('autoRateLimit') &&
719
+ !content.includes('throttle')) {
720
+ return 'API route without rate limiting - add protection against abuse';
721
+ }
722
+ return null;
723
+ }
724
+ },
725
+ {
726
+ name: 'N+1 Query Pattern',
727
+ category: 'api',
728
+ test: (content, file) => {
729
+ // Check for await in loop with database calls
730
+ if ((content.includes('for (') || content.includes('forEach') || content.includes('.map(')) &&
731
+ content.includes('await') &&
732
+ (content.includes('db.') || content.includes('prisma.') || content.includes('findOne') || content.includes('findById'))) {
733
+ return 'Possible N+1 query - fetch data in batch instead of loop';
734
+ }
735
+ return null;
736
+ }
737
+ },
738
+ {
739
+ name: 'Missing CORS Config',
740
+ category: 'api',
741
+ test: (content, file) => {
742
+ if (!file.includes('/api/') && !file.includes('route.ts')) return null;
743
+ if (content.includes("'*'") &&
744
+ (content.includes('Access-Control-Allow-Origin') || content.includes('cors'))) {
745
+ return 'Overly permissive CORS (*) - restrict to specific origins';
746
+ }
747
+ return null;
748
+ }
749
+ },
750
+
751
+ // ==========================================
752
+ // IMPORT CHECKS
753
+ // ==========================================
754
+ {
755
+ name: 'Circular Import Risk',
756
+ category: 'imports',
757
+ test: (content, file) => {
758
+ // Check for importing from parent directory and exporting to child
759
+ const parentImports = (content.match(/from\\s+['"]\\.\\.\\/[^'"]+['"]/g) || []).length;
760
+ const hasExport = content.includes('export ');
761
+ if (parentImports > 3 && hasExport) {
762
+ return 'Multiple parent imports - potential circular dependency risk';
763
+ }
764
+ return null;
765
+ }
766
+ },
767
+ {
768
+ name: 'Unused Import',
769
+ category: 'imports',
770
+ test: (content, file) => {
771
+ // Simple check for imported names not used
772
+ const imports = content.match(/import\\s+\\{([^}]+)\\}/g) || [];
773
+ for (const imp of imports) {
774
+ const names = imp.replace(/import\\s+\\{/, '').replace(/\\}/, '').split(',');
775
+ for (const name of names) {
776
+ const cleanName = name.trim().split(' as ')[0].trim();
777
+ if (cleanName && cleanName.length > 1) {
778
+ // Count occurrences (should be > 1 to include the import itself)
779
+ const regex = new RegExp('\\\\b' + cleanName + '\\\\b', 'g');
780
+ const occurrences = (content.match(regex) || []).length;
781
+ if (occurrences === 1) {
782
+ return \`Possibly unused import: \${cleanName}\`;
783
+ }
784
+ }
785
+ }
786
+ }
787
+ return null;
788
+ }
789
+ },
790
+ ];
791
+
792
+ async function validateCode() {
793
+ const cwd = process.cwd();
794
+ const violations = [];
795
+ const warnings = [];
796
+
797
+ const allStagedFiles = getStagedFiles();
798
+ const codeFiles = getCodeFiles(allStagedFiles);
799
+
800
+ log(CYAN, '\\n๐Ÿช CodeBakers Pre-Commit Checks');
801
+ log(CYAN, '================================\\n');
802
+
803
+ // Check for .env files being committed
804
+ for (const file of allStagedFiles) {
805
+ if (file.startsWith('.env')) {
806
+ violations.push({
807
+ check: 'Env File Commit',
808
+ category: 'security',
809
+ message: '.env file should not be committed',
810
+ file: file
811
+ });
812
+ }
90
813
  }
91
814
 
92
- // Check if validation passed
93
- if (!lastValidation.passed) {
94
- const issues = lastValidation.issues?.map(i => i.message || i).join(', ') || 'Unknown issues';
95
- return {
96
- valid: false,
97
- reason: 'validation-failed',
98
- message: 'Validation failed: ' + issues + '\\nAI must fix issues and call validate_complete again.'
99
- };
815
+ if (codeFiles.length === 0 && violations.length === 0) {
816
+ log(DIM, 'No code files staged.\\n');
817
+ log(GREEN, '================================');
818
+ log(GREEN, 'โœ… All pre-commit checks passed!');
819
+ log(GREEN, '================================\\n');
820
+ return { valid: true };
100
821
  }
101
822
 
102
- // Check if validation is recent (within last 30 minutes)
103
- const validationTime = new Date(lastValidation.timestamp);
104
- const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
105
- if (validationTime < thirtyMinutesAgo) {
106
- return {
107
- valid: false,
108
- reason: 'validation-stale',
109
- message: 'Validation is stale (older than 30 minutes).\\nAI must call validate_complete again.'
110
- };
823
+ log(DIM, '๐Ÿ“‹ Checking \${codeFiles.length} code file(s)...\\n');
824
+
825
+ for (const file of codeFiles) {
826
+ const filePath = path.join(cwd, file);
827
+ if (!fs.existsSync(filePath)) continue;
828
+
829
+ let content;
830
+ try {
831
+ content = fs.readFileSync(filePath, 'utf-8');
832
+ } catch {
833
+ continue;
834
+ }
835
+
836
+ for (const check of CHECKS) {
837
+ try {
838
+ const result = check.test(content, file);
839
+ if (result) {
840
+ violations.push({
841
+ check: check.name,
842
+ category: check.category,
843
+ message: result,
844
+ file: file
845
+ });
846
+ }
847
+ } catch (err) {
848
+ // Skip check on error
849
+ }
850
+ }
111
851
  }
112
852
 
113
- return { valid: true, reason: 'session-valid' };
114
- }
853
+ // Report results
854
+ if (violations.length > 0) {
855
+ // Group by category
856
+ const byCategory = {};
857
+ for (const v of violations) {
858
+ if (!byCategory[v.category]) byCategory[v.category] = [];
859
+ byCategory[v.category].push(v);
860
+ }
115
861
 
116
- async function main() {
117
- console.log('');
118
- log(CYAN, ' ๐Ÿช CodeBakers Pre-Commit Validation');
119
- console.log('');
862
+ const categoryNames = {
863
+ security: '๐Ÿ”’ Security',
864
+ errors: 'โš ๏ธ Error Handling',
865
+ validation: 'โœ… Validation',
866
+ quality: '๐Ÿ“ Code Quality',
867
+ typescript: '๐Ÿ“˜ TypeScript',
868
+ react: 'โš›๏ธ React',
869
+ a11y: 'โ™ฟ Accessibility',
870
+ performance: 'โšก Performance',
871
+ api: '๐ŸŒ API',
872
+ imports: '๐Ÿ“ฆ Imports',
873
+ };
120
874
 
121
- const result = await validateSession();
875
+ log(RED, \`\\nโŒ Found \${violations.length} issue(s):\\n\`);
122
876
 
123
- if (result.valid) {
124
- if (result.reason === 'not-codebakers-project') {
125
- log(GREEN, ' โœ“ Not a CodeBakers project - commit allowed');
126
- } else if (result.reason === 'legacy-project') {
127
- log(GREEN, ' โœ“ Legacy project (pre-6.0) - commit allowed');
128
- } else {
129
- log(GREEN, ' โœ“ Valid CodeBakers session - commit allowed');
877
+ for (const [category, items] of Object.entries(byCategory)) {
878
+ log(YELLOW, \`\\n\${categoryNames[category] || category}:\`);
879
+ const byFile = {};
880
+ for (const v of items) {
881
+ if (!byFile[v.file]) byFile[v.file] = [];
882
+ byFile[v.file].push(v);
883
+ }
884
+ for (const [file, fileViolations] of Object.entries(byFile)) {
885
+ log(DIM, \` \${file}:\`);
886
+ for (const v of fileViolations) {
887
+ log(RED, \` โœ— \${v.message}\`);
888
+ }
889
+ }
130
890
  }
891
+
131
892
  console.log('');
132
- process.exit(0);
133
- } else {
134
- log(RED, ' โœ— Commit blocked: ' + result.reason);
135
- console.log('');
136
- if (result.message) {
137
- log(YELLOW, ' ' + result.message.split('\\n').join('\\n '));
893
+ log(CYAN, 'Fix these issues and try again.');
894
+ log(YELLOW, 'To bypass (not recommended): git commit --no-verify\\n');
895
+
896
+ return { valid: false, violations };
897
+ }
898
+
899
+ log(GREEN, 'โœ… All \${CHECKS.length} checks passed!\\n');
900
+
901
+ // Run tests if available
902
+ log(DIM, '๐Ÿงช Running tests...\\n');
903
+
904
+ try {
905
+ const pkgPath = path.join(cwd, 'package.json');
906
+ if (fs.existsSync(pkgPath)) {
907
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
908
+ if (pkg.scripts && pkg.scripts.test) {
909
+ execSync('npm test', { stdio: 'inherit', cwd });
910
+ log(GREEN, 'โœ… Tests passed!\\n');
911
+ } else {
912
+ log(DIM, 'No test script found, skipping...\\n');
913
+ }
138
914
  }
139
- console.log('');
140
- log(CYAN, ' How to fix:');
141
- log(RESET, ' 1. AI must call discover_patterns before writing code');
142
- log(RESET, ' 2. AI must call validate_complete before saying "done"');
143
- log(RESET, ' 3. Both tools must pass for commits to be allowed');
144
- console.log('');
145
- log(YELLOW, ' To bypass (not recommended): git commit --no-verify');
146
- console.log('');
147
- process.exit(1);
915
+ } catch (error) {
916
+ log(RED, 'โŒ Tests failed!\\n');
917
+ return { valid: false, reason: 'tests-failed' };
148
918
  }
919
+
920
+ log(GREEN, '================================');
921
+ log(GREEN, 'โœ… All pre-commit checks passed!');
922
+ log(GREEN, '================================\\n');
923
+
924
+ return { valid: true };
925
+ }
926
+
927
+ async function main() {
928
+ const result = await validateCode();
929
+ process.exit(result.valid ? 0 : 1);
149
930
  }
150
931
 
151
932
  main().catch(error => {
@@ -163,12 +944,6 @@ async function installPrecommit() {
163
944
  console.log(chalk_1.default.gray(' Initialize git first: git init\n'));
164
945
  process.exit(1);
165
946
  }
166
- // Check if this is a CodeBakers project
167
- const stateFile = (0, path_1.join)(cwd, '.codebakers.json');
168
- if (!(0, fs_1.existsSync)(stateFile)) {
169
- console.log(chalk_1.default.yellow(' โš ๏ธ No .codebakers.json found'));
170
- console.log(chalk_1.default.gray(' Run codebakers upgrade first to enable server enforcement\n'));
171
- }
172
947
  // Create hooks directory if it doesn't exist
173
948
  const hooksDir = (0, path_1.join)(gitDir, 'hooks');
174
949
  if (!(0, fs_1.existsSync)(hooksDir)) {
@@ -177,7 +952,7 @@ async function installPrecommit() {
177
952
  // Write the pre-commit hook
178
953
  const preCommitPath = (0, path_1.join)(hooksDir, 'pre-commit');
179
954
  (0, fs_1.writeFileSync)(preCommitPath, PRE_COMMIT_SCRIPT);
180
- // Make it executable (Unix only, Windows ignores this)
955
+ // Make it executable
181
956
  try {
182
957
  (0, fs_1.chmodSync)(preCommitPath, '755');
183
958
  }
@@ -186,44 +961,50 @@ async function installPrecommit() {
186
961
  }
187
962
  console.log(chalk_1.default.green(' โœ“ Created pre-commit hook'));
188
963
  // Write the validation script
189
- const validatePath = (0, path_1.join)(hooksDir, 'validate-session.js');
190
- (0, fs_1.writeFileSync)(validatePath, VALIDATE_SESSION_SCRIPT);
191
- console.log(chalk_1.default.green(' โœ“ Created validation script'));
964
+ const validatePath = (0, path_1.join)(hooksDir, 'validate-code.js');
965
+ (0, fs_1.writeFileSync)(validatePath, VALIDATE_CODE_SCRIPT);
966
+ console.log(chalk_1.default.green(' โœ“ Created code validation script (40+ checks)'));
192
967
  // Check if husky is being used
193
968
  const huskyDir = (0, path_1.join)(cwd, '.husky');
194
969
  if ((0, fs_1.existsSync)(huskyDir)) {
195
- // Also install in husky
196
970
  const huskyPreCommit = (0, path_1.join)(huskyDir, 'pre-commit');
197
971
  let huskyContent = '';
198
972
  if ((0, fs_1.existsSync)(huskyPreCommit)) {
199
973
  huskyContent = (0, fs_1.readFileSync)(huskyPreCommit, 'utf-8');
200
- if (!huskyContent.includes('validate-session')) {
201
- huskyContent += '\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
974
+ if (!huskyContent.includes('validate-code')) {
975
+ huskyContent += '\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
202
976
  (0, fs_1.writeFileSync)(huskyPreCommit, huskyContent);
203
977
  console.log(chalk_1.default.green(' โœ“ Added to existing husky pre-commit'));
204
978
  }
205
- else {
206
- console.log(chalk_1.default.gray(' โœ“ Husky hook already configured'));
207
- }
208
979
  }
209
980
  else {
210
- huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
981
+ huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
211
982
  (0, fs_1.writeFileSync)(huskyPreCommit, huskyContent);
212
983
  try {
213
984
  (0, fs_1.chmodSync)(huskyPreCommit, '755');
214
985
  }
215
- catch {
216
- // Windows
217
- }
986
+ catch { }
218
987
  console.log(chalk_1.default.green(' โœ“ Created husky pre-commit hook'));
219
988
  }
220
989
  }
221
- console.log(chalk_1.default.green('\n โœ… Pre-commit hook installed!\n'));
222
- console.log(chalk_1.default.cyan(' What this does:'));
223
- console.log(chalk_1.default.gray(' - Blocks commits unless AI called discover_patterns'));
224
- console.log(chalk_1.default.gray(' - Blocks commits unless AI called validate_complete'));
225
- console.log(chalk_1.default.gray(' - Requires validation to pass before committing'));
226
- console.log(chalk_1.default.gray(' - Validation expires after 30 minutes\n'));
227
- console.log(chalk_1.default.yellow(' To bypass (not recommended):'));
228
- console.log(chalk_1.default.gray(' git commit --no-verify\n'));
990
+ console.log(chalk_1.default.green('\n โœ… Pre-commit hook installed with 40+ checks!\n'));
991
+ console.log(chalk_1.default.cyan(' ๐Ÿ”’ Security (9 checks):'));
992
+ console.log(chalk_1.default.gray(' Debugger statements, hardcoded secrets, XSS, SQL injection,'));
993
+ console.log(chalk_1.default.gray(' merge conflicts, private keys, .env files, eval(), sensitive logs\n'));
994
+ console.log(chalk_1.default.cyan(' โš ๏ธ Error Handling (6 checks):'));
995
+ console.log(chalk_1.default.gray(' API try/catch, empty catch, unhandled promises, JSON.parse safety\n'));
996
+ console.log(chalk_1.default.cyan(' ๐Ÿ“˜ TypeScript (4 checks):'));
997
+ console.log(chalk_1.default.gray(' No "any" types, unsafe assertions, return types, non-null assertions\n'));
998
+ console.log(chalk_1.default.cyan(' โš›๏ธ React (7 checks):'));
999
+ console.log(chalk_1.default.gray(' DOM manipulation, useEffect cleanup, conditional hooks, keys,'));
1000
+ console.log(chalk_1.default.gray(' inline functions, error boundaries\n'));
1001
+ console.log(chalk_1.default.cyan(' โ™ฟ Accessibility (5 checks):'));
1002
+ console.log(chalk_1.default.gray(' Image alt text, button types, form labels, keyboard support, ARIA roles\n'));
1003
+ console.log(chalk_1.default.cyan(' โšก Performance (3 checks):'));
1004
+ console.log(chalk_1.default.gray(' Large imports, sync file operations, missing memoization\n'));
1005
+ console.log(chalk_1.default.cyan(' ๐ŸŒ API (3 checks):'));
1006
+ console.log(chalk_1.default.gray(' Rate limiting, N+1 queries, CORS config\n'));
1007
+ console.log(chalk_1.default.cyan(' ๐Ÿ“ Code Quality (6 checks):'));
1008
+ console.log(chalk_1.default.gray(' Console.log, TODO/FIXME, hardcoded URLs, file size, magic numbers\n'));
1009
+ console.log(chalk_1.default.yellow(' To bypass (not recommended): git commit --no-verify\n'));
229
1010
  }