@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.
- package/dist/commands/install-precommit.js +715 -178
- package/package.json +1 -1
- package/src/commands/install-precommit.ts +724 -177
|
@@ -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 -
|
|
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
|
-
*
|
|
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()
|
|
39
|
+
return output.split('\\n').filter(f => f.trim());
|
|
40
40
|
} catch {
|
|
41
41
|
return [];
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
//
|
|
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(')
|
|
83
|
-
return \`
|
|
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: '
|
|
328
|
+
name: 'TODO/FIXME Comments',
|
|
329
|
+
category: 'quality',
|
|
91
330
|
test: (content, file) => {
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
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:') &&
|
|
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: '
|
|
353
|
+
name: 'Large File',
|
|
354
|
+
category: 'quality',
|
|
122
355
|
test: (content, file) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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: '
|
|
364
|
+
name: 'Magic Numbers',
|
|
365
|
+
category: 'quality',
|
|
139
366
|
test: (content, file) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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: '
|
|
377
|
+
name: 'Commented Out Code',
|
|
378
|
+
category: 'quality',
|
|
150
379
|
test: (content, file) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const
|
|
154
|
-
if (
|
|
155
|
-
|
|
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: '
|
|
398
|
+
name: 'Any Type Usage',
|
|
399
|
+
category: 'typescript',
|
|
166
400
|
test: (content, file) => {
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return '
|
|
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: '
|
|
411
|
+
name: 'Type Assertion Override',
|
|
412
|
+
category: 'typescript',
|
|
178
413
|
test: (content, file) => {
|
|
179
|
-
if (!file.endsWith('.
|
|
180
|
-
if (content.includes('
|
|
181
|
-
|
|
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(':')
|
|
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: '
|
|
436
|
+
name: 'Non-null Assertion',
|
|
437
|
+
category: 'typescript',
|
|
204
438
|
test: (content, file) => {
|
|
205
|
-
if (
|
|
206
|
-
|
|
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: '
|
|
452
|
+
name: 'Direct DOM Manipulation',
|
|
453
|
+
category: 'react',
|
|
213
454
|
test: (content, file) => {
|
|
214
|
-
if (!file.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
if (line.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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: '
|
|
506
|
+
name: 'Missing Key Prop',
|
|
507
|
+
category: 'react',
|
|
251
508
|
test: (content, file) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
543
|
+
for (const pattern of inlinePatterns) {
|
|
261
544
|
if (pattern.test(content)) {
|
|
262
|
-
return '
|
|
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
|
-
|
|
276
|
-
const
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
819
|
+
log(DIM, '📋 Checking \${codeFiles.length} code file(s)...\\n');
|
|
289
820
|
|
|
290
|
-
for (const file of
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
const byFile = {};
|
|
851
|
+
// Group by category
|
|
852
|
+
const byCategory = {};
|
|
329
853
|
for (const v of violations) {
|
|
330
|
-
if (!
|
|
331
|
-
|
|
854
|
+
if (!byCategory[v.category]) byCategory[v.category] = [];
|
|
855
|
+
byCategory[v.category].push(v);
|
|
332
856
|
}
|
|
333
857
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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(
|
|
343
|
-
log(
|
|
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, '✅
|
|
895
|
+
log(GREEN, '✅ All \${CHECKS.length} checks passed!\\n');
|
|
352
896
|
|
|
353
|
-
//
|
|
354
|
-
log(DIM, '🧪
|
|
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
|
|
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
|
-
|
|
465
|
-
console.log(chalk.
|
|
466
|
-
console.log(chalk.gray('
|
|
467
|
-
console.log(chalk.gray('
|
|
468
|
-
|
|
469
|
-
console.log(chalk.
|
|
470
|
-
console.log(chalk.gray('
|
|
471
|
-
|
|
472
|
-
console.log(chalk.
|
|
473
|
-
console.log(chalk.gray('
|
|
474
|
-
|
|
475
|
-
console.log(chalk.
|
|
476
|
-
console.log(chalk.gray('
|
|
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
|
}
|