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