@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
|
@@ -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 -
|
|
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
|
-
*
|
|
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()
|
|
43
|
+
return output.split('\\n').filter(f => f.trim());
|
|
44
44
|
} catch {
|
|
45
45
|
return [];
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
//
|
|
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(')
|
|
87
|
-
return \`
|
|
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: '
|
|
332
|
+
name: 'TODO/FIXME Comments',
|
|
333
|
+
category: 'quality',
|
|
95
334
|
test: (content, file) => {
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
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:') &&
|
|
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: '
|
|
357
|
+
name: 'Large File',
|
|
358
|
+
category: 'quality',
|
|
126
359
|
test: (content, file) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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: '
|
|
368
|
+
name: 'Magic Numbers',
|
|
369
|
+
category: 'quality',
|
|
143
370
|
test: (content, file) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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: '
|
|
381
|
+
name: 'Commented Out Code',
|
|
382
|
+
category: 'quality',
|
|
154
383
|
test: (content, file) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
for (const
|
|
158
|
-
if (
|
|
159
|
-
|
|
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: '
|
|
402
|
+
name: 'Any Type Usage',
|
|
403
|
+
category: 'typescript',
|
|
170
404
|
test: (content, file) => {
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return '
|
|
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: '
|
|
415
|
+
name: 'Type Assertion Override',
|
|
416
|
+
category: 'typescript',
|
|
182
417
|
test: (content, file) => {
|
|
183
|
-
if (!file.endsWith('.
|
|
184
|
-
if (content.includes('
|
|
185
|
-
|
|
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(':')
|
|
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: '
|
|
440
|
+
name: 'Non-null Assertion',
|
|
441
|
+
category: 'typescript',
|
|
208
442
|
test: (content, file) => {
|
|
209
|
-
if (
|
|
210
|
-
|
|
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: '
|
|
456
|
+
name: 'Direct DOM Manipulation',
|
|
457
|
+
category: 'react',
|
|
217
458
|
test: (content, file) => {
|
|
218
|
-
if (!file.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
if (line.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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: '
|
|
510
|
+
name: 'Missing Key Prop',
|
|
511
|
+
category: 'react',
|
|
255
512
|
test: (content, file) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
547
|
+
for (const pattern of inlinePatterns) {
|
|
265
548
|
if (pattern.test(content)) {
|
|
266
|
-
return '
|
|
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
|
-
|
|
280
|
-
const
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
823
|
+
log(DIM, '📋 Checking \${codeFiles.length} code file(s)...\\n');
|
|
293
824
|
|
|
294
|
-
for (const file of
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
const byFile = {};
|
|
855
|
+
// Group by category
|
|
856
|
+
const byCategory = {};
|
|
333
857
|
for (const v of violations) {
|
|
334
|
-
if (!
|
|
335
|
-
|
|
858
|
+
if (!byCategory[v.category]) byCategory[v.category] = [];
|
|
859
|
+
byCategory[v.category].push(v);
|
|
336
860
|
}
|
|
337
861
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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(
|
|
347
|
-
log(
|
|
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, '✅
|
|
899
|
+
log(GREEN, '✅ All \${CHECKS.length} checks passed!\\n');
|
|
356
900
|
|
|
357
|
-
//
|
|
358
|
-
log(DIM, '🧪
|
|
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
|
|
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('
|
|
461
|
-
console.log(chalk_1.default.gray('
|
|
462
|
-
console.log(chalk_1.default.gray('
|
|
463
|
-
console.log(chalk_1.default.
|
|
464
|
-
console.log(chalk_1.default.gray('
|
|
465
|
-
console.log(chalk_1.default.
|
|
466
|
-
console.log(chalk_1.default.gray('
|
|
467
|
-
console.log(chalk_1.default.
|
|
468
|
-
console.log(chalk_1.default.gray('
|
|
469
|
-
console.log(chalk_1.default.gray('
|
|
470
|
-
console.log(chalk_1.default.
|
|
471
|
-
console.log(chalk_1.default.
|
|
472
|
-
console.log(chalk_1.default.
|
|
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
|
}
|