@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.
@@ -8,7 +8,7 @@ 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 - Real Code Validation
11
+ # CodeBakers Pre-Commit Hook - Comprehensive Code Validation
12
12
  # Actually scans code for pattern violations
13
13
 
14
14
  # Run the validation script
@@ -17,8 +17,8 @@ exit $?
17
17
  `;
18
18
  const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
19
19
  /**
20
- * CodeBakers Pre-Commit Code Validator
21
- * Actually validates code against patterns - not just honor system
20
+ * CodeBakers Pre-Commit Code Validator v2.0
21
+ * Comprehensive code validation - 40+ checks
22
22
  */
23
23
 
24
24
  const { execSync } = require('child_process');
@@ -40,16 +40,161 @@ function log(color, message) {
40
40
  function getStagedFiles() {
41
41
  try {
42
42
  const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' });
43
- return output.split('\\n').filter(f => f.trim() && (f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js') || f.endsWith('.jsx')));
43
+ return output.split('\\n').filter(f => f.trim());
44
44
  } catch {
45
45
  return [];
46
46
  }
47
47
  }
48
48
 
49
- // Pattern violations to check
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
+ }
56
+
57
+ // ============================================
58
+ // ALL CHECKS - Organized by Category
59
+ // ============================================
60
+
50
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;
73
+ }
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
+ },
191
+
192
+ // ==========================================
193
+ // ERROR HANDLING CHECKS
194
+ // ==========================================
51
195
  {
52
196
  name: 'API Error Handling',
197
+ category: 'errors',
53
198
  test: (content, file) => {
54
199
  if (!file.includes('/api/') && !file.includes('route.ts')) return null;
55
200
  if (!content.includes('try {') && !content.includes('try{')) {
@@ -58,145 +203,233 @@ const CHECKS = [
58
203
  return null;
59
204
  }
60
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
+ },
273
+
274
+ // ==========================================
275
+ // VALIDATION CHECKS
276
+ // ==========================================
61
277
  {
62
278
  name: 'Zod Validation',
279
+ category: 'validation',
63
280
  test: (content, file) => {
64
281
  if (!file.includes('/api/') && !file.includes('route.ts')) return null;
65
- // Check if it's a POST/PUT/PATCH that should have validation
66
282
  if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
67
283
  content.includes('req.json()') &&
68
284
  !content.includes('z.object') &&
69
285
  !content.includes('schema.parse') &&
70
- !content.includes('Schema.parse')) {
286
+ !content.includes('Schema.parse') &&
287
+ !content.includes('validate')) {
71
288
  return 'API route accepts body but missing Zod validation';
72
289
  }
73
290
  return null;
74
291
  }
75
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
+ // ==========================================
76
315
  {
77
316
  name: 'Console Statements',
317
+ category: 'quality',
78
318
  test: (content, file) => {
79
- // Allow in test files and scripts
80
319
  if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
81
320
  const lines = content.split('\\n');
82
321
  for (let i = 0; i < lines.length; i++) {
83
322
  const line = lines[i];
84
- // Skip commented lines
85
323
  if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
86
- if (line.includes('console.log(') || line.includes('console.error(') || line.includes('console.warn(')) {
87
- return \`Console statement found at line \${i + 1} - use proper logging\`;
324
+ if (line.includes('console.log(')) {
325
+ return \`console.log at line \${i + 1} - use proper logging\`;
88
326
  }
89
327
  }
90
328
  return null;
91
329
  }
92
330
  },
93
331
  {
94
- name: 'Hardcoded Secrets',
332
+ name: 'TODO/FIXME Comments',
333
+ category: 'quality',
95
334
  test: (content, file) => {
96
- // Skip env files and configs
97
- if (file.includes('.env') || file.includes('config')) return null;
98
- const patterns = [
99
- /api[_-]?key\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
100
- /secret\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
101
- /password\\s*[:=]\\s*['"][^'"]{8,}['"]/i,
102
- /sk_live_[a-zA-Z0-9]+/,
103
- /sk_test_[a-zA-Z0-9]+/,
104
- ];
105
- for (const pattern of patterns) {
106
- if (pattern.test(content)) {
107
- return 'Possible hardcoded secret detected - use environment variables';
108
- }
335
+ const match = content.match(/\\/\\/\\s*(TODO|FIXME|XXX|HACK):/i);
336
+ if (match) {
337
+ return \`Unresolved \${match[1]} comment - address before commit\`;
109
338
  }
110
339
  return null;
111
340
  }
112
341
  },
113
342
  {
114
343
  name: 'Hardcoded URLs',
344
+ category: 'quality',
115
345
  test: (content, file) => {
116
- // Skip test files
117
346
  if (file.includes('.test.') || file.includes('/tests/')) return null;
118
- if (content.includes('localhost:') && !content.includes('process.env') && !content.includes('|| \\'http://localhost')) {
347
+ if (content.includes('localhost:') &&
348
+ !content.includes('process.env') &&
349
+ !content.includes("|| 'http://localhost") &&
350
+ !content.includes('|| "http://localhost')) {
119
351
  return 'Hardcoded localhost URL - use environment variable with fallback';
120
352
  }
121
353
  return null;
122
354
  }
123
355
  },
124
356
  {
125
- name: 'SQL Injection Risk',
357
+ name: 'Large File',
358
+ category: 'quality',
126
359
  test: (content, file) => {
127
- // Check for string concatenation in SQL
128
- const sqlPatterns = [
129
- /\\$\\{.*\\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/i,
130
- /['"]\\s*\\+\\s*.*\\+\\s*['"].*(?:SELECT|INSERT|UPDATE|DELETE)/i,
131
- /sql\\s*\\(\\s*\`[^\\)]*\\$\\{/,
132
- ];
133
- for (const pattern of sqlPatterns) {
134
- if (pattern.test(content)) {
135
- return 'Possible SQL injection - use parameterized queries';
136
- }
360
+ const lines = content.split('\\n').length;
361
+ if (lines > 500) {
362
+ return \`File has \${lines} lines - consider splitting into smaller modules\`;
137
363
  }
138
364
  return null;
139
365
  }
140
366
  },
141
367
  {
142
- name: 'Untyped Function Parameters',
368
+ name: 'Magic Numbers',
369
+ category: 'quality',
143
370
  test: (content, file) => {
144
- if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
145
- // Check for functions with 'any' type
146
- if (content.includes(': any)') || content.includes(': any,') || content.includes(': any =')) {
147
- return 'Using "any" type - provide proper TypeScript types';
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';
148
376
  }
149
377
  return null;
150
378
  }
151
379
  },
152
380
  {
153
- name: 'Missing Async Error Handling',
381
+ name: 'Commented Out Code',
382
+ category: 'quality',
154
383
  test: (content, file) => {
155
- // Check for await without try/catch in the same function scope
156
- const asyncFunctions = content.match(/async\\s+(?:function\\s+)?\\w*\\s*\\([^)]*\\)\\s*(?::\\s*[^{]+)?\\s*\\{[^}]+\\}/g) || [];
157
- for (const func of asyncFunctions) {
158
- if (func.includes('await') && !func.includes('try') && !func.includes('catch')) {
159
- // Check if it's wrapped in a try/catch at a higher level
160
- if (!content.includes('.catch(') && !content.includes('try {')) {
161
- return 'Async function with await but no error handling';
162
- }
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++;
163
389
  }
164
390
  }
391
+ if (commentedCodeCount > 5) {
392
+ return \`\${commentedCodeCount} lines of commented code - remove dead code\`;
393
+ }
165
394
  return null;
166
395
  }
167
396
  },
397
+
398
+ // ==========================================
399
+ // TYPESCRIPT CHECKS
400
+ // ==========================================
168
401
  {
169
- name: 'Empty Catch Block',
402
+ name: 'Any Type Usage',
403
+ category: 'typescript',
170
404
  test: (content, file) => {
171
- if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}/.test(content)) {
172
- return 'Empty catch block - handle or rethrow errors';
173
- }
174
- if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\/\\//.test(content)) {
175
- return 'Catch block with only comment - properly handle errors';
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';
176
410
  }
177
411
  return null;
178
412
  }
179
413
  },
180
414
  {
181
- name: 'Direct DOM Manipulation in React',
415
+ name: 'Type Assertion Override',
416
+ category: 'typescript',
182
417
  test: (content, file) => {
183
- if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
184
- if (content.includes('document.getElementById') ||
185
- content.includes('document.querySelector') ||
186
- content.includes('document.createElement')) {
187
- return 'Direct DOM manipulation in React - use refs or state instead';
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';
188
421
  }
189
422
  return null;
190
423
  }
191
424
  },
192
425
  {
193
426
  name: 'Missing Return Type',
427
+ category: 'typescript',
194
428
  test: (content, file) => {
195
429
  if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
196
- // Check exported functions without return types
197
430
  const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
198
431
  for (const func of exportedFunctions) {
199
- if (!func.includes(':') || func.match(/\\)\\s*\\{$/)) {
432
+ if (!func.includes(':')) {
200
433
  return 'Exported function missing return type annotation';
201
434
  }
202
435
  }
@@ -204,71 +437,356 @@ const CHECKS = [
204
437
  }
205
438
  },
206
439
  {
207
- name: 'Unsafe JSON Parse',
440
+ name: 'Non-null Assertion',
441
+ category: 'typescript',
208
442
  test: (content, file) => {
209
- if (content.includes('JSON.parse(') && !content.includes('try') && !content.includes('catch')) {
210
- return 'JSON.parse without try/catch - can throw on invalid JSON';
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\`;
211
447
  }
212
448
  return null;
213
449
  }
214
450
  },
451
+
452
+ // ==========================================
453
+ // REACT CHECKS
454
+ // ==========================================
215
455
  {
216
- name: 'Missing Auth Check',
456
+ name: 'Direct DOM Manipulation',
457
+ category: 'react',
217
458
  test: (content, file) => {
218
- if (!file.includes('/api/') && !file.includes('route.ts')) return null;
219
- // Skip public routes
220
- if (file.includes('/public/') || file.includes('/auth/') || file.includes('/webhook')) return null;
221
- // Check if it's accessing user data without auth
222
- if ((content.includes('userId') || content.includes('user.id') || content.includes('session')) &&
223
- !content.includes('getServerSession') &&
224
- !content.includes('auth(') &&
225
- !content.includes('requireAuth') &&
226
- !content.includes('verifyToken') &&
227
- !content.includes('validateSession')) {
228
- return 'Route accesses user data but may be missing auth check';
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';
229
465
  }
230
466
  return null;
231
467
  }
232
468
  },
233
469
  {
234
- name: 'Unhandled Promise',
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',
235
493
  test: (content, file) => {
236
- // Check for promises without await or .then/.catch
494
+ if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
237
495
  const lines = content.split('\\n');
496
+ let inCondition = false;
238
497
  for (let i = 0; i < lines.length; i++) {
239
498
  const line = lines[i].trim();
240
- // Skip if line ends with await, .then, .catch, or is assigned
241
- if (line.match(/(?:fetch|axios|db\\.|prisma\\.).*\\(/) &&
242
- !line.includes('await') &&
243
- !line.includes('.then') &&
244
- !line.includes('.catch') &&
245
- !line.includes('return') &&
246
- !line.includes('=')) {
247
- return \`Unhandled promise at line \${i + 1}\`;
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\`;
248
504
  }
249
505
  }
250
506
  return null;
251
507
  }
252
508
  },
253
509
  {
254
- name: 'Sensitive Data in Logs',
510
+ name: 'Missing Key Prop',
511
+ category: 'react',
255
512
  test: (content, file) => {
256
- const sensitivePatterns = [
257
- /console\\.log.*password/i,
258
- /console\\.log.*token/i,
259
- /console\\.log.*secret/i,
260
- /console\\.log.*apiKey/i,
261
- /console\\.log.*creditCard/i,
262
- /console\\.log.*ssn/i,
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*=>/,
263
546
  ];
264
- for (const pattern of sensitivePatterns) {
547
+ for (const pattern of inlinePatterns) {
265
548
  if (pattern.test(content)) {
266
- return 'Possible sensitive data being logged';
549
+ return 'Inline function in JSX - use useCallback for performance';
267
550
  }
268
551
  }
269
552
  return null;
270
553
  }
271
- }
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
+ },
272
790
  ];
273
791
 
274
792
  async function validateCode() {
@@ -276,22 +794,35 @@ async function validateCode() {
276
794
  const violations = [];
277
795
  const warnings = [];
278
796
 
279
- // Get staged files
280
- const stagedFiles = getStagedFiles();
281
-
282
- if (stagedFiles.length === 0) {
283
- return { valid: true, message: 'No code files staged' };
284
- }
797
+ const allStagedFiles = getStagedFiles();
798
+ const codeFiles = getCodeFiles(allStagedFiles);
285
799
 
286
800
  log(CYAN, '\\n🍪 CodeBakers Pre-Commit Checks');
287
801
  log(CYAN, '================================\\n');
288
802
 
289
- log(DIM, \`📋 Step 1/2: Checking pattern compliance...\\n\`);
290
- log(DIM, \`🔍 Validating CodeBakers pattern compliance...\\n\`);
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
+ }
813
+ }
814
+
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 };
821
+ }
291
822
 
292
- let filesChecked = 0;
823
+ log(DIM, '📋 Checking \${codeFiles.length} code file(s)...\\n');
293
824
 
294
- for (const file of stagedFiles) {
825
+ for (const file of codeFiles) {
295
826
  const filePath = path.join(cwd, file);
296
827
  if (!fs.existsSync(filePath)) continue;
297
828
 
@@ -302,63 +833,75 @@ async function validateCode() {
302
833
  continue;
303
834
  }
304
835
 
305
- filesChecked++;
306
- const fileViolations = [];
307
-
308
836
  for (const check of CHECKS) {
309
- const result = check.test(content, file);
310
- if (result) {
311
- fileViolations.push({
312
- check: check.name,
313
- message: result,
314
- file: file
315
- });
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
316
849
  }
317
850
  }
318
-
319
- if (fileViolations.length > 0) {
320
- violations.push(...fileViolations);
321
- }
322
- }
323
-
324
- if (filesChecked === 0) {
325
- log(DIM, 'No files to validate.\\n');
326
851
  }
327
852
 
328
853
  // Report results
329
854
  if (violations.length > 0) {
330
- log(RED, \`\\n❌ Found \${violations.length} violation(s):\\n\`);
331
-
332
- const byFile = {};
855
+ // Group by category
856
+ const byCategory = {};
333
857
  for (const v of violations) {
334
- if (!byFile[v.file]) byFile[v.file] = [];
335
- byFile[v.file].push(v);
858
+ if (!byCategory[v.category]) byCategory[v.category] = [];
859
+ byCategory[v.category].push(v);
336
860
  }
337
861
 
338
- for (const [file, fileViolations] of Object.entries(byFile)) {
339
- log(YELLOW, \` \${file}:\`);
340
- for (const v of fileViolations) {
341
- log(RED, \` ✗ [\${v.check}] \${v.message}\`);
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
+ };
874
+
875
+ log(RED, \`\\n❌ Found \${violations.length} issue(s):\\n\`);
876
+
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
+ }
342
889
  }
343
- console.log('');
344
890
  }
345
891
 
346
- log(CYAN, '\\nHow to fix:');
347
- log(RESET, ' 1. Address each violation listed above');
348
- log(RESET, ' 2. Re-stage your changes: git add <files>');
349
- log(RESET, ' 3. Try committing again\\n');
892
+ console.log('');
893
+ log(CYAN, 'Fix these issues and try again.');
350
894
  log(YELLOW, 'To bypass (not recommended): git commit --no-verify\\n');
351
895
 
352
896
  return { valid: false, violations };
353
897
  }
354
898
 
355
- log(GREEN, '✅ Pattern compliance passed!\\n');
899
+ log(GREEN, '✅ All \${CHECKS.length} checks passed!\\n');
356
900
 
357
- // Step 2: Run tests if available
358
- log(DIM, '🧪 Step 2/2: Running tests...\\n');
901
+ // Run tests if available
902
+ log(DIM, '🧪 Running tests...\\n');
359
903
 
360
904
  try {
361
- // Check if there's a test script
362
905
  const pkgPath = path.join(cwd, 'package.json');
363
906
  if (fs.existsSync(pkgPath)) {
364
907
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
@@ -371,7 +914,6 @@ async function validateCode() {
371
914
  }
372
915
  } catch (error) {
373
916
  log(RED, '❌ Tests failed!\\n');
374
- log(YELLOW, 'Fix failing tests before committing.\\n');
375
917
  return { valid: false, reason: 'tests-failed' };
376
918
  }
377
919
 
@@ -384,12 +926,7 @@ async function validateCode() {
384
926
 
385
927
  async function main() {
386
928
  const result = await validateCode();
387
-
388
- if (result.valid) {
389
- process.exit(0);
390
- } else {
391
- process.exit(1);
392
- }
929
+ process.exit(result.valid ? 0 : 1);
393
930
  }
394
931
 
395
932
  main().catch(error => {
@@ -415,7 +952,7 @@ async function installPrecommit() {
415
952
  // Write the pre-commit hook
416
953
  const preCommitPath = (0, path_1.join)(hooksDir, 'pre-commit');
417
954
  (0, fs_1.writeFileSync)(preCommitPath, PRE_COMMIT_SCRIPT);
418
- // Make it executable (Unix only, Windows ignores this)
955
+ // Make it executable
419
956
  try {
420
957
  (0, fs_1.chmodSync)(preCommitPath, '755');
421
958
  }
@@ -426,11 +963,10 @@ async function installPrecommit() {
426
963
  // Write the validation script
427
964
  const validatePath = (0, path_1.join)(hooksDir, 'validate-code.js');
428
965
  (0, fs_1.writeFileSync)(validatePath, VALIDATE_CODE_SCRIPT);
429
- console.log(chalk_1.default.green(' ✓ Created code validation script'));
966
+ console.log(chalk_1.default.green(' ✓ Created code validation script (40+ checks)'));
430
967
  // Check if husky is being used
431
968
  const huskyDir = (0, path_1.join)(cwd, '.husky');
432
969
  if ((0, fs_1.existsSync)(huskyDir)) {
433
- // Also install in husky
434
970
  const huskyPreCommit = (0, path_1.join)(huskyDir, 'pre-commit');
435
971
  let huskyContent = '';
436
972
  if ((0, fs_1.existsSync)(huskyPreCommit)) {
@@ -440,9 +976,6 @@ async function installPrecommit() {
440
976
  (0, fs_1.writeFileSync)(huskyPreCommit, huskyContent);
441
977
  console.log(chalk_1.default.green(' ✓ Added to existing husky pre-commit'));
442
978
  }
443
- else {
444
- console.log(chalk_1.default.gray(' ✓ Husky hook already configured'));
445
- }
446
979
  }
447
980
  else {
448
981
  huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
@@ -450,24 +983,28 @@ async function installPrecommit() {
450
983
  try {
451
984
  (0, fs_1.chmodSync)(huskyPreCommit, '755');
452
985
  }
453
- catch {
454
- // Windows
455
- }
986
+ catch { }
456
987
  console.log(chalk_1.default.green(' ✓ Created husky pre-commit hook'));
457
988
  }
458
989
  }
459
- console.log(chalk_1.default.green('\n ✅ Pre-commit hook installed!\n'));
460
- console.log(chalk_1.default.cyan(' What this validates:'));
461
- console.log(chalk_1.default.gray(' - API routes have error handling'));
462
- console.log(chalk_1.default.gray(' - Request bodies are validated with Zod'));
463
- console.log(chalk_1.default.gray(' - No console.log statements in production code'));
464
- console.log(chalk_1.default.gray(' - No hardcoded secrets or URLs'));
465
- console.log(chalk_1.default.gray(' - No SQL injection vulnerabilities'));
466
- console.log(chalk_1.default.gray(' - Proper TypeScript types (no "any")'));
467
- console.log(chalk_1.default.gray(' - Async functions have error handling'));
468
- console.log(chalk_1.default.gray(' - No empty catch blocks'));
469
- console.log(chalk_1.default.gray(' - Auth checks on protected routes'));
470
- console.log(chalk_1.default.gray(' - Runs tests before commit\n'));
471
- console.log(chalk_1.default.yellow(' To bypass (not recommended):'));
472
- 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'));
473
1010
  }