@codebakers/cli 3.9.38 โ 3.9.39
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 +354 -110
- package/package.json +1 -1
- package/src/commands/install-precommit.ts +354 -111
- package/tmpclaude-ea68-cwd +1 -0
|
@@ -8,19 +8,20 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const path_1 = require("path");
|
|
10
10
|
const PRE_COMMIT_SCRIPT = `#!/bin/sh
|
|
11
|
-
# CodeBakers Pre-Commit Hook -
|
|
12
|
-
#
|
|
11
|
+
# CodeBakers Pre-Commit Hook - Real Code Validation
|
|
12
|
+
# Actually scans code for pattern violations
|
|
13
13
|
|
|
14
14
|
# Run the validation script
|
|
15
|
-
node "$(dirname "$0")/validate-
|
|
15
|
+
node "$(dirname "$0")/validate-code.js"
|
|
16
16
|
exit $?
|
|
17
17
|
`;
|
|
18
|
-
const
|
|
18
|
+
const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
|
|
19
19
|
/**
|
|
20
|
-
* CodeBakers Pre-Commit
|
|
21
|
-
*
|
|
20
|
+
* CodeBakers Pre-Commit Code Validator
|
|
21
|
+
* Actually validates code against patterns - not just honor system
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
const { execSync } = require('child_process');
|
|
24
25
|
const fs = require('fs');
|
|
25
26
|
const path = require('path');
|
|
26
27
|
|
|
@@ -28,122 +29,365 @@ const RED = '\\x1b[31m';
|
|
|
28
29
|
const GREEN = '\\x1b[32m';
|
|
29
30
|
const YELLOW = '\\x1b[33m';
|
|
30
31
|
const CYAN = '\\x1b[36m';
|
|
32
|
+
const DIM = '\\x1b[2m';
|
|
31
33
|
const RESET = '\\x1b[0m';
|
|
32
34
|
|
|
33
35
|
function log(color, message) {
|
|
34
36
|
console.log(color + message + RESET);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
39
|
+
// Get staged files
|
|
40
|
+
function getStagedFiles() {
|
|
41
|
+
try {
|
|
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')));
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
44
46
|
}
|
|
47
|
+
}
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Pattern violations to check
|
|
50
|
+
const CHECKS = [
|
|
51
|
+
{
|
|
52
|
+
name: 'API Error Handling',
|
|
53
|
+
test: (content, file) => {
|
|
54
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
55
|
+
if (!content.includes('try {') && !content.includes('try{')) {
|
|
56
|
+
return 'API route missing try/catch error handling';
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Zod Validation',
|
|
63
|
+
test: (content, file) => {
|
|
64
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
65
|
+
// Check if it's a POST/PUT/PATCH that should have validation
|
|
66
|
+
if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
|
|
67
|
+
content.includes('req.json()') &&
|
|
68
|
+
!content.includes('z.object') &&
|
|
69
|
+
!content.includes('schema.parse') &&
|
|
70
|
+
!content.includes('Schema.parse')) {
|
|
71
|
+
return 'API route accepts body but missing Zod validation';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Console Statements',
|
|
78
|
+
test: (content, file) => {
|
|
79
|
+
// Allow in test files and scripts
|
|
80
|
+
if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
|
|
81
|
+
const lines = content.split('\\n');
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
// Skip commented lines
|
|
85
|
+
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\`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'Hardcoded Secrets',
|
|
95
|
+
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
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'Hardcoded URLs',
|
|
115
|
+
test: (content, file) => {
|
|
116
|
+
// Skip test files
|
|
117
|
+
if (file.includes('.test.') || file.includes('/tests/')) return null;
|
|
118
|
+
if (content.includes('localhost:') && !content.includes('process.env') && !content.includes('|| \\'http://localhost')) {
|
|
119
|
+
return 'Hardcoded localhost URL - use environment variable with fallback';
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'SQL Injection Risk',
|
|
126
|
+
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
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Untyped Function Parameters',
|
|
143
|
+
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';
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'Missing Async Error Handling',
|
|
154
|
+
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
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'Empty Catch Block',
|
|
170
|
+
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';
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'Direct DOM Manipulation in React',
|
|
182
|
+
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';
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'Missing Return Type',
|
|
194
|
+
test: (content, file) => {
|
|
195
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
196
|
+
// Check exported functions without return types
|
|
197
|
+
const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
|
|
198
|
+
for (const func of exportedFunctions) {
|
|
199
|
+
if (!func.includes(':') || func.match(/\\)\\s*\\{$/)) {
|
|
200
|
+
return 'Exported function missing return type annotation';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'Unsafe JSON Parse',
|
|
208
|
+
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';
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'Missing Auth Check',
|
|
217
|
+
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';
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'Unhandled Promise',
|
|
235
|
+
test: (content, file) => {
|
|
236
|
+
// Check for promises without await or .then/.catch
|
|
237
|
+
const lines = content.split('\\n');
|
|
238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
239
|
+
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}\`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'Sensitive Data in Logs',
|
|
255
|
+
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,
|
|
263
|
+
];
|
|
264
|
+
for (const pattern of sensitivePatterns) {
|
|
265
|
+
if (pattern.test(content)) {
|
|
266
|
+
return 'Possible sensitive data being logged';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
51
271
|
}
|
|
272
|
+
];
|
|
52
273
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
274
|
+
async function validateCode() {
|
|
275
|
+
const cwd = process.cwd();
|
|
276
|
+
const violations = [];
|
|
277
|
+
const warnings = [];
|
|
278
|
+
|
|
279
|
+
// Get staged files
|
|
280
|
+
const stagedFiles = getStagedFiles();
|
|
281
|
+
|
|
282
|
+
if (stagedFiles.length === 0) {
|
|
283
|
+
return { valid: true, message: 'No code files staged' };
|
|
56
284
|
}
|
|
57
285
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
286
|
+
log(CYAN, '\\n๐ช CodeBakers Pre-Commit Checks');
|
|
287
|
+
log(CYAN, '================================\\n');
|
|
288
|
+
|
|
289
|
+
log(DIM, \`๐ Step 1/2: Checking pattern compliance...\\n\`);
|
|
290
|
+
log(DIM, \`๐ Validating CodeBakers pattern compliance...\\n\`);
|
|
291
|
+
|
|
292
|
+
let filesChecked = 0;
|
|
293
|
+
|
|
294
|
+
for (const file of stagedFiles) {
|
|
295
|
+
const filePath = path.join(cwd, file);
|
|
296
|
+
if (!fs.existsSync(filePath)) continue;
|
|
297
|
+
|
|
298
|
+
let content;
|
|
299
|
+
try {
|
|
300
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
69
303
|
}
|
|
70
|
-
}
|
|
71
304
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
305
|
+
filesChecked++;
|
|
306
|
+
const fileViolations = [];
|
|
307
|
+
|
|
308
|
+
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
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (fileViolations.length > 0) {
|
|
320
|
+
violations.push(...fileViolations);
|
|
321
|
+
}
|
|
80
322
|
}
|
|
81
323
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!lastValidation) {
|
|
85
|
-
return {
|
|
86
|
-
valid: false,
|
|
87
|
-
reason: 'no-validation',
|
|
88
|
-
message: 'No validation completed.\\nAI must call validate_complete before committing.'
|
|
89
|
-
};
|
|
324
|
+
if (filesChecked === 0) {
|
|
325
|
+
log(DIM, 'No files to validate.\\n');
|
|
90
326
|
}
|
|
91
327
|
|
|
92
|
-
//
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
328
|
+
// Report results
|
|
329
|
+
if (violations.length > 0) {
|
|
330
|
+
log(RED, \`\\nโ Found \${violations.length} violation(s):\\n\`);
|
|
331
|
+
|
|
332
|
+
const byFile = {};
|
|
333
|
+
for (const v of violations) {
|
|
334
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
335
|
+
byFile[v.file].push(v);
|
|
336
|
+
}
|
|
337
|
+
|
|
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}\`);
|
|
342
|
+
}
|
|
343
|
+
console.log('');
|
|
344
|
+
}
|
|
345
|
+
|
|
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');
|
|
350
|
+
log(YELLOW, 'To bypass (not recommended): git commit --no-verify\\n');
|
|
351
|
+
|
|
352
|
+
return { valid: false, violations };
|
|
100
353
|
}
|
|
101
354
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
355
|
+
log(GREEN, 'โ
Pattern compliance passed!\\n');
|
|
356
|
+
|
|
357
|
+
// Step 2: Run tests if available
|
|
358
|
+
log(DIM, '๐งช Step 2/2: Running tests...\\n');
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Check if there's a test script
|
|
362
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
363
|
+
if (fs.existsSync(pkgPath)) {
|
|
364
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
365
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
366
|
+
execSync('npm test', { stdio: 'inherit', cwd });
|
|
367
|
+
log(GREEN, 'โ
Tests passed!\\n');
|
|
368
|
+
} else {
|
|
369
|
+
log(DIM, 'No test script found, skipping...\\n');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
log(RED, 'โ Tests failed!\\n');
|
|
374
|
+
log(YELLOW, 'Fix failing tests before committing.\\n');
|
|
375
|
+
return { valid: false, reason: 'tests-failed' };
|
|
111
376
|
}
|
|
112
377
|
|
|
113
|
-
|
|
378
|
+
log(GREEN, '================================');
|
|
379
|
+
log(GREEN, 'โ
All pre-commit checks passed!');
|
|
380
|
+
log(GREEN, '================================\\n');
|
|
381
|
+
|
|
382
|
+
return { valid: true };
|
|
114
383
|
}
|
|
115
384
|
|
|
116
385
|
async function main() {
|
|
117
|
-
|
|
118
|
-
log(CYAN, ' ๐ช CodeBakers Pre-Commit Validation');
|
|
119
|
-
console.log('');
|
|
120
|
-
|
|
121
|
-
const result = await validateSession();
|
|
386
|
+
const result = await validateCode();
|
|
122
387
|
|
|
123
388
|
if (result.valid) {
|
|
124
|
-
if (result.reason === 'not-codebakers-project') {
|
|
125
|
-
log(GREEN, ' โ Not a CodeBakers project - commit allowed');
|
|
126
|
-
} else if (result.reason === 'legacy-project') {
|
|
127
|
-
log(GREEN, ' โ Legacy project (pre-6.0) - commit allowed');
|
|
128
|
-
} else {
|
|
129
|
-
log(GREEN, ' โ Valid CodeBakers session - commit allowed');
|
|
130
|
-
}
|
|
131
|
-
console.log('');
|
|
132
389
|
process.exit(0);
|
|
133
390
|
} else {
|
|
134
|
-
log(RED, ' โ Commit blocked: ' + result.reason);
|
|
135
|
-
console.log('');
|
|
136
|
-
if (result.message) {
|
|
137
|
-
log(YELLOW, ' ' + result.message.split('\\n').join('\\n '));
|
|
138
|
-
}
|
|
139
|
-
console.log('');
|
|
140
|
-
log(CYAN, ' How to fix:');
|
|
141
|
-
log(RESET, ' 1. AI must call discover_patterns before writing code');
|
|
142
|
-
log(RESET, ' 2. AI must call validate_complete before saying "done"');
|
|
143
|
-
log(RESET, ' 3. Both tools must pass for commits to be allowed');
|
|
144
|
-
console.log('');
|
|
145
|
-
log(YELLOW, ' To bypass (not recommended): git commit --no-verify');
|
|
146
|
-
console.log('');
|
|
147
391
|
process.exit(1);
|
|
148
392
|
}
|
|
149
393
|
}
|
|
@@ -163,12 +407,6 @@ async function installPrecommit() {
|
|
|
163
407
|
console.log(chalk_1.default.gray(' Initialize git first: git init\n'));
|
|
164
408
|
process.exit(1);
|
|
165
409
|
}
|
|
166
|
-
// Check if this is a CodeBakers project
|
|
167
|
-
const stateFile = (0, path_1.join)(cwd, '.codebakers.json');
|
|
168
|
-
if (!(0, fs_1.existsSync)(stateFile)) {
|
|
169
|
-
console.log(chalk_1.default.yellow(' โ ๏ธ No .codebakers.json found'));
|
|
170
|
-
console.log(chalk_1.default.gray(' Run codebakers upgrade first to enable server enforcement\n'));
|
|
171
|
-
}
|
|
172
410
|
// Create hooks directory if it doesn't exist
|
|
173
411
|
const hooksDir = (0, path_1.join)(gitDir, 'hooks');
|
|
174
412
|
if (!(0, fs_1.existsSync)(hooksDir)) {
|
|
@@ -186,9 +424,9 @@ async function installPrecommit() {
|
|
|
186
424
|
}
|
|
187
425
|
console.log(chalk_1.default.green(' โ Created pre-commit hook'));
|
|
188
426
|
// Write the validation script
|
|
189
|
-
const validatePath = (0, path_1.join)(hooksDir, 'validate-
|
|
190
|
-
(0, fs_1.writeFileSync)(validatePath,
|
|
191
|
-
console.log(chalk_1.default.green(' โ Created validation script'));
|
|
427
|
+
const validatePath = (0, path_1.join)(hooksDir, 'validate-code.js');
|
|
428
|
+
(0, fs_1.writeFileSync)(validatePath, VALIDATE_CODE_SCRIPT);
|
|
429
|
+
console.log(chalk_1.default.green(' โ Created code validation script'));
|
|
192
430
|
// Check if husky is being used
|
|
193
431
|
const huskyDir = (0, path_1.join)(cwd, '.husky');
|
|
194
432
|
if ((0, fs_1.existsSync)(huskyDir)) {
|
|
@@ -197,8 +435,8 @@ async function installPrecommit() {
|
|
|
197
435
|
let huskyContent = '';
|
|
198
436
|
if ((0, fs_1.existsSync)(huskyPreCommit)) {
|
|
199
437
|
huskyContent = (0, fs_1.readFileSync)(huskyPreCommit, 'utf-8');
|
|
200
|
-
if (!huskyContent.includes('validate-
|
|
201
|
-
huskyContent += '\n# CodeBakers
|
|
438
|
+
if (!huskyContent.includes('validate-code')) {
|
|
439
|
+
huskyContent += '\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
202
440
|
(0, fs_1.writeFileSync)(huskyPreCommit, huskyContent);
|
|
203
441
|
console.log(chalk_1.default.green(' โ Added to existing husky pre-commit'));
|
|
204
442
|
}
|
|
@@ -207,7 +445,7 @@ async function installPrecommit() {
|
|
|
207
445
|
}
|
|
208
446
|
}
|
|
209
447
|
else {
|
|
210
|
-
huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers
|
|
448
|
+
huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
211
449
|
(0, fs_1.writeFileSync)(huskyPreCommit, huskyContent);
|
|
212
450
|
try {
|
|
213
451
|
(0, fs_1.chmodSync)(huskyPreCommit, '755');
|
|
@@ -219,11 +457,17 @@ async function installPrecommit() {
|
|
|
219
457
|
}
|
|
220
458
|
}
|
|
221
459
|
console.log(chalk_1.default.green('\n โ
Pre-commit hook installed!\n'));
|
|
222
|
-
console.log(chalk_1.default.cyan(' What this
|
|
223
|
-
console.log(chalk_1.default.gray(' -
|
|
224
|
-
console.log(chalk_1.default.gray(' -
|
|
225
|
-
console.log(chalk_1.default.gray(' -
|
|
226
|
-
console.log(chalk_1.default.gray(' -
|
|
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'));
|
|
227
471
|
console.log(chalk_1.default.yellow(' To bypass (not recommended):'));
|
|
228
472
|
console.log(chalk_1.default.gray(' git commit --no-verify\n'));
|
|
229
473
|
}
|
package/package.json
CHANGED
|
@@ -3,20 +3,21 @@ 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 -
|
|
7
|
-
#
|
|
6
|
+
# CodeBakers Pre-Commit Hook - Real Code Validation
|
|
7
|
+
# Actually scans code for pattern violations
|
|
8
8
|
|
|
9
9
|
# Run the validation script
|
|
10
|
-
node "$(dirname "$0")/validate-
|
|
10
|
+
node "$(dirname "$0")/validate-code.js"
|
|
11
11
|
exit $?
|
|
12
12
|
`;
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
|
|
15
15
|
/**
|
|
16
|
-
* CodeBakers Pre-Commit
|
|
17
|
-
*
|
|
16
|
+
* CodeBakers Pre-Commit Code Validator
|
|
17
|
+
* Actually validates code against patterns - not just honor system
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
const { execSync } = require('child_process');
|
|
20
21
|
const fs = require('fs');
|
|
21
22
|
const path = require('path');
|
|
22
23
|
|
|
@@ -24,122 +25,365 @@ const RED = '\\x1b[31m';
|
|
|
24
25
|
const GREEN = '\\x1b[32m';
|
|
25
26
|
const YELLOW = '\\x1b[33m';
|
|
26
27
|
const CYAN = '\\x1b[36m';
|
|
28
|
+
const DIM = '\\x1b[2m';
|
|
27
29
|
const RESET = '\\x1b[0m';
|
|
28
30
|
|
|
29
31
|
function log(color, message) {
|
|
30
32
|
console.log(color + message + RESET);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
35
|
+
// Get staged files
|
|
36
|
+
function getStagedFiles() {
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' });
|
|
39
|
+
return output.split('\\n').filter(f => f.trim() && (f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js') || f.endsWith('.jsx')));
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
40
42
|
}
|
|
43
|
+
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Pattern violations to check
|
|
46
|
+
const CHECKS = [
|
|
47
|
+
{
|
|
48
|
+
name: 'API Error Handling',
|
|
49
|
+
test: (content, file) => {
|
|
50
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
51
|
+
if (!content.includes('try {') && !content.includes('try{')) {
|
|
52
|
+
return 'API route missing try/catch error handling';
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Zod Validation',
|
|
59
|
+
test: (content, file) => {
|
|
60
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
61
|
+
// Check if it's a POST/PUT/PATCH that should have validation
|
|
62
|
+
if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
|
|
63
|
+
content.includes('req.json()') &&
|
|
64
|
+
!content.includes('z.object') &&
|
|
65
|
+
!content.includes('schema.parse') &&
|
|
66
|
+
!content.includes('Schema.parse')) {
|
|
67
|
+
return 'API route accepts body but missing Zod validation';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Console Statements',
|
|
74
|
+
test: (content, file) => {
|
|
75
|
+
// Allow in test files and scripts
|
|
76
|
+
if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
|
|
77
|
+
const lines = content.split('\\n');
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
// Skip commented lines
|
|
81
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
82
|
+
if (line.includes('console.log(') || line.includes('console.error(') || line.includes('console.warn(')) {
|
|
83
|
+
return \`Console statement found at line \${i + 1} - use proper logging\`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Hardcoded Secrets',
|
|
91
|
+
test: (content, file) => {
|
|
92
|
+
// Skip env files and configs
|
|
93
|
+
if (file.includes('.env') || file.includes('config')) return null;
|
|
94
|
+
const patterns = [
|
|
95
|
+
/api[_-]?key\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
96
|
+
/secret\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
97
|
+
/password\\s*[:=]\\s*['"][^'"]{8,}['"]/i,
|
|
98
|
+
/sk_live_[a-zA-Z0-9]+/,
|
|
99
|
+
/sk_test_[a-zA-Z0-9]+/,
|
|
100
|
+
];
|
|
101
|
+
for (const pattern of patterns) {
|
|
102
|
+
if (pattern.test(content)) {
|
|
103
|
+
return 'Possible hardcoded secret detected - use environment variables';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Hardcoded URLs',
|
|
111
|
+
test: (content, file) => {
|
|
112
|
+
// Skip test files
|
|
113
|
+
if (file.includes('.test.') || file.includes('/tests/')) return null;
|
|
114
|
+
if (content.includes('localhost:') && !content.includes('process.env') && !content.includes('|| \\'http://localhost')) {
|
|
115
|
+
return 'Hardcoded localhost URL - use environment variable with fallback';
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'SQL Injection Risk',
|
|
122
|
+
test: (content, file) => {
|
|
123
|
+
// Check for string concatenation in SQL
|
|
124
|
+
const sqlPatterns = [
|
|
125
|
+
/\\$\\{.*\\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/i,
|
|
126
|
+
/['"]\\s*\\+\\s*.*\\+\\s*['"].*(?:SELECT|INSERT|UPDATE|DELETE)/i,
|
|
127
|
+
/sql\\s*\\(\\s*\`[^\\)]*\\$\\{/,
|
|
128
|
+
];
|
|
129
|
+
for (const pattern of sqlPatterns) {
|
|
130
|
+
if (pattern.test(content)) {
|
|
131
|
+
return 'Possible SQL injection - use parameterized queries';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Untyped Function Parameters',
|
|
139
|
+
test: (content, file) => {
|
|
140
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
141
|
+
// Check for functions with 'any' type
|
|
142
|
+
if (content.includes(': any)') || content.includes(': any,') || content.includes(': any =')) {
|
|
143
|
+
return 'Using "any" type - provide proper TypeScript types';
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Missing Async Error Handling',
|
|
150
|
+
test: (content, file) => {
|
|
151
|
+
// Check for await without try/catch in the same function scope
|
|
152
|
+
const asyncFunctions = content.match(/async\\s+(?:function\\s+)?\\w*\\s*\\([^)]*\\)\\s*(?::\\s*[^{]+)?\\s*\\{[^}]+\\}/g) || [];
|
|
153
|
+
for (const func of asyncFunctions) {
|
|
154
|
+
if (func.includes('await') && !func.includes('try') && !func.includes('catch')) {
|
|
155
|
+
// Check if it's wrapped in a try/catch at a higher level
|
|
156
|
+
if (!content.includes('.catch(') && !content.includes('try {')) {
|
|
157
|
+
return 'Async function with await but no error handling';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Empty Catch Block',
|
|
166
|
+
test: (content, file) => {
|
|
167
|
+
if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}/.test(content)) {
|
|
168
|
+
return 'Empty catch block - handle or rethrow errors';
|
|
169
|
+
}
|
|
170
|
+
if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\/\\//.test(content)) {
|
|
171
|
+
return 'Catch block with only comment - properly handle errors';
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'Direct DOM Manipulation in React',
|
|
178
|
+
test: (content, file) => {
|
|
179
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
180
|
+
if (content.includes('document.getElementById') ||
|
|
181
|
+
content.includes('document.querySelector') ||
|
|
182
|
+
content.includes('document.createElement')) {
|
|
183
|
+
return 'Direct DOM manipulation in React - use refs or state instead';
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'Missing Return Type',
|
|
190
|
+
test: (content, file) => {
|
|
191
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
192
|
+
// Check exported functions without return types
|
|
193
|
+
const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
|
|
194
|
+
for (const func of exportedFunctions) {
|
|
195
|
+
if (!func.includes(':') || func.match(/\\)\\s*\\{$/)) {
|
|
196
|
+
return 'Exported function missing return type annotation';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'Unsafe JSON Parse',
|
|
204
|
+
test: (content, file) => {
|
|
205
|
+
if (content.includes('JSON.parse(') && !content.includes('try') && !content.includes('catch')) {
|
|
206
|
+
return 'JSON.parse without try/catch - can throw on invalid JSON';
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'Missing Auth Check',
|
|
213
|
+
test: (content, file) => {
|
|
214
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
215
|
+
// Skip public routes
|
|
216
|
+
if (file.includes('/public/') || file.includes('/auth/') || file.includes('/webhook')) return null;
|
|
217
|
+
// Check if it's accessing user data without auth
|
|
218
|
+
if ((content.includes('userId') || content.includes('user.id') || content.includes('session')) &&
|
|
219
|
+
!content.includes('getServerSession') &&
|
|
220
|
+
!content.includes('auth(') &&
|
|
221
|
+
!content.includes('requireAuth') &&
|
|
222
|
+
!content.includes('verifyToken') &&
|
|
223
|
+
!content.includes('validateSession')) {
|
|
224
|
+
return 'Route accesses user data but may be missing auth check';
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'Unhandled Promise',
|
|
231
|
+
test: (content, file) => {
|
|
232
|
+
// Check for promises without await or .then/.catch
|
|
233
|
+
const lines = content.split('\\n');
|
|
234
|
+
for (let i = 0; i < lines.length; i++) {
|
|
235
|
+
const line = lines[i].trim();
|
|
236
|
+
// Skip if line ends with await, .then, .catch, or is assigned
|
|
237
|
+
if (line.match(/(?:fetch|axios|db\\.|prisma\\.).*\\(/) &&
|
|
238
|
+
!line.includes('await') &&
|
|
239
|
+
!line.includes('.then') &&
|
|
240
|
+
!line.includes('.catch') &&
|
|
241
|
+
!line.includes('return') &&
|
|
242
|
+
!line.includes('=')) {
|
|
243
|
+
return \`Unhandled promise at line \${i + 1}\`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'Sensitive Data in Logs',
|
|
251
|
+
test: (content, file) => {
|
|
252
|
+
const sensitivePatterns = [
|
|
253
|
+
/console\\.log.*password/i,
|
|
254
|
+
/console\\.log.*token/i,
|
|
255
|
+
/console\\.log.*secret/i,
|
|
256
|
+
/console\\.log.*apiKey/i,
|
|
257
|
+
/console\\.log.*creditCard/i,
|
|
258
|
+
/console\\.log.*ssn/i,
|
|
259
|
+
];
|
|
260
|
+
for (const pattern of sensitivePatterns) {
|
|
261
|
+
if (pattern.test(content)) {
|
|
262
|
+
return 'Possible sensitive data being logged';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
47
267
|
}
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
async function validateCode() {
|
|
271
|
+
const cwd = process.cwd();
|
|
272
|
+
const violations = [];
|
|
273
|
+
const warnings = [];
|
|
48
274
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
275
|
+
// Get staged files
|
|
276
|
+
const stagedFiles = getStagedFiles();
|
|
277
|
+
|
|
278
|
+
if (stagedFiles.length === 0) {
|
|
279
|
+
return { valid: true, message: 'No code files staged' };
|
|
52
280
|
}
|
|
53
281
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
282
|
+
log(CYAN, '\\n๐ช CodeBakers Pre-Commit Checks');
|
|
283
|
+
log(CYAN, '================================\\n');
|
|
284
|
+
|
|
285
|
+
log(DIM, \`๐ Step 1/2: Checking pattern compliance...\\n\`);
|
|
286
|
+
log(DIM, \`๐ Validating CodeBakers pattern compliance...\\n\`);
|
|
287
|
+
|
|
288
|
+
let filesChecked = 0;
|
|
289
|
+
|
|
290
|
+
for (const file of stagedFiles) {
|
|
291
|
+
const filePath = path.join(cwd, file);
|
|
292
|
+
if (!fs.existsSync(filePath)) continue;
|
|
293
|
+
|
|
294
|
+
let content;
|
|
295
|
+
try {
|
|
296
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
filesChecked++;
|
|
302
|
+
const fileViolations = [];
|
|
303
|
+
|
|
304
|
+
for (const check of CHECKS) {
|
|
305
|
+
const result = check.test(content, file);
|
|
306
|
+
if (result) {
|
|
307
|
+
fileViolations.push({
|
|
308
|
+
check: check.name,
|
|
309
|
+
message: result,
|
|
310
|
+
file: file
|
|
311
|
+
});
|
|
312
|
+
}
|
|
65
313
|
}
|
|
66
|
-
}
|
|
67
314
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
valid: false,
|
|
73
|
-
reason: 'session-expired',
|
|
74
|
-
message: 'CodeBakers session has expired.\\nAI must call discover_patterns again.'
|
|
75
|
-
};
|
|
315
|
+
if (fileViolations.length > 0) {
|
|
316
|
+
violations.push(...fileViolations);
|
|
317
|
+
}
|
|
76
318
|
}
|
|
77
319
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!lastValidation) {
|
|
81
|
-
return {
|
|
82
|
-
valid: false,
|
|
83
|
-
reason: 'no-validation',
|
|
84
|
-
message: 'No validation completed.\\nAI must call validate_complete before committing.'
|
|
85
|
-
};
|
|
320
|
+
if (filesChecked === 0) {
|
|
321
|
+
log(DIM, 'No files to validate.\\n');
|
|
86
322
|
}
|
|
87
323
|
|
|
88
|
-
//
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
324
|
+
// Report results
|
|
325
|
+
if (violations.length > 0) {
|
|
326
|
+
log(RED, \`\\nโ Found \${violations.length} violation(s):\\n\`);
|
|
327
|
+
|
|
328
|
+
const byFile = {};
|
|
329
|
+
for (const v of violations) {
|
|
330
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
331
|
+
byFile[v.file].push(v);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const [file, fileViolations] of Object.entries(byFile)) {
|
|
335
|
+
log(YELLOW, \` \${file}:\`);
|
|
336
|
+
for (const v of fileViolations) {
|
|
337
|
+
log(RED, \` โ [\${v.check}] \${v.message}\`);
|
|
338
|
+
}
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
log(CYAN, '\\nHow to fix:');
|
|
343
|
+
log(RESET, ' 1. Address each violation listed above');
|
|
344
|
+
log(RESET, ' 2. Re-stage your changes: git add <files>');
|
|
345
|
+
log(RESET, ' 3. Try committing again\\n');
|
|
346
|
+
log(YELLOW, 'To bypass (not recommended): git commit --no-verify\\n');
|
|
347
|
+
|
|
348
|
+
return { valid: false, violations };
|
|
96
349
|
}
|
|
97
350
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
351
|
+
log(GREEN, 'โ
Pattern compliance passed!\\n');
|
|
352
|
+
|
|
353
|
+
// Step 2: Run tests if available
|
|
354
|
+
log(DIM, '๐งช Step 2/2: Running tests...\\n');
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Check if there's a test script
|
|
358
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
359
|
+
if (fs.existsSync(pkgPath)) {
|
|
360
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
361
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
362
|
+
execSync('npm test', { stdio: 'inherit', cwd });
|
|
363
|
+
log(GREEN, 'โ
Tests passed!\\n');
|
|
364
|
+
} else {
|
|
365
|
+
log(DIM, 'No test script found, skipping...\\n');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
log(RED, 'โ Tests failed!\\n');
|
|
370
|
+
log(YELLOW, 'Fix failing tests before committing.\\n');
|
|
371
|
+
return { valid: false, reason: 'tests-failed' };
|
|
107
372
|
}
|
|
108
373
|
|
|
109
|
-
|
|
374
|
+
log(GREEN, '================================');
|
|
375
|
+
log(GREEN, 'โ
All pre-commit checks passed!');
|
|
376
|
+
log(GREEN, '================================\\n');
|
|
377
|
+
|
|
378
|
+
return { valid: true };
|
|
110
379
|
}
|
|
111
380
|
|
|
112
381
|
async function main() {
|
|
113
|
-
|
|
114
|
-
log(CYAN, ' ๐ช CodeBakers Pre-Commit Validation');
|
|
115
|
-
console.log('');
|
|
116
|
-
|
|
117
|
-
const result = await validateSession();
|
|
382
|
+
const result = await validateCode();
|
|
118
383
|
|
|
119
384
|
if (result.valid) {
|
|
120
|
-
if (result.reason === 'not-codebakers-project') {
|
|
121
|
-
log(GREEN, ' โ Not a CodeBakers project - commit allowed');
|
|
122
|
-
} else if (result.reason === 'legacy-project') {
|
|
123
|
-
log(GREEN, ' โ Legacy project (pre-6.0) - commit allowed');
|
|
124
|
-
} else {
|
|
125
|
-
log(GREEN, ' โ Valid CodeBakers session - commit allowed');
|
|
126
|
-
}
|
|
127
|
-
console.log('');
|
|
128
385
|
process.exit(0);
|
|
129
386
|
} else {
|
|
130
|
-
log(RED, ' โ Commit blocked: ' + result.reason);
|
|
131
|
-
console.log('');
|
|
132
|
-
if (result.message) {
|
|
133
|
-
log(YELLOW, ' ' + result.message.split('\\n').join('\\n '));
|
|
134
|
-
}
|
|
135
|
-
console.log('');
|
|
136
|
-
log(CYAN, ' How to fix:');
|
|
137
|
-
log(RESET, ' 1. AI must call discover_patterns before writing code');
|
|
138
|
-
log(RESET, ' 2. AI must call validate_complete before saying "done"');
|
|
139
|
-
log(RESET, ' 3. Both tools must pass for commits to be allowed');
|
|
140
|
-
console.log('');
|
|
141
|
-
log(YELLOW, ' To bypass (not recommended): git commit --no-verify');
|
|
142
|
-
console.log('');
|
|
143
387
|
process.exit(1);
|
|
144
388
|
}
|
|
145
389
|
}
|
|
@@ -163,13 +407,6 @@ export async function installPrecommit(): Promise<void> {
|
|
|
163
407
|
process.exit(1);
|
|
164
408
|
}
|
|
165
409
|
|
|
166
|
-
// Check if this is a CodeBakers project
|
|
167
|
-
const stateFile = join(cwd, '.codebakers.json');
|
|
168
|
-
if (!existsSync(stateFile)) {
|
|
169
|
-
console.log(chalk.yellow(' โ ๏ธ No .codebakers.json found'));
|
|
170
|
-
console.log(chalk.gray(' Run codebakers upgrade first to enable server enforcement\n'));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
410
|
// Create hooks directory if it doesn't exist
|
|
174
411
|
const hooksDir = join(gitDir, 'hooks');
|
|
175
412
|
if (!existsSync(hooksDir)) {
|
|
@@ -190,10 +427,10 @@ export async function installPrecommit(): Promise<void> {
|
|
|
190
427
|
console.log(chalk.green(' โ Created pre-commit hook'));
|
|
191
428
|
|
|
192
429
|
// Write the validation script
|
|
193
|
-
const validatePath = join(hooksDir, 'validate-
|
|
194
|
-
writeFileSync(validatePath,
|
|
430
|
+
const validatePath = join(hooksDir, 'validate-code.js');
|
|
431
|
+
writeFileSync(validatePath, VALIDATE_CODE_SCRIPT);
|
|
195
432
|
|
|
196
|
-
console.log(chalk.green(' โ Created validation script'));
|
|
433
|
+
console.log(chalk.green(' โ Created code validation script'));
|
|
197
434
|
|
|
198
435
|
// Check if husky is being used
|
|
199
436
|
const huskyDir = join(cwd, '.husky');
|
|
@@ -204,15 +441,15 @@ export async function installPrecommit(): Promise<void> {
|
|
|
204
441
|
|
|
205
442
|
if (existsSync(huskyPreCommit)) {
|
|
206
443
|
huskyContent = readFileSync(huskyPreCommit, 'utf-8');
|
|
207
|
-
if (!huskyContent.includes('validate-
|
|
208
|
-
huskyContent += '\n# CodeBakers
|
|
444
|
+
if (!huskyContent.includes('validate-code')) {
|
|
445
|
+
huskyContent += '\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
209
446
|
writeFileSync(huskyPreCommit, huskyContent);
|
|
210
447
|
console.log(chalk.green(' โ Added to existing husky pre-commit'));
|
|
211
448
|
} else {
|
|
212
449
|
console.log(chalk.gray(' โ Husky hook already configured'));
|
|
213
450
|
}
|
|
214
451
|
} else {
|
|
215
|
-
huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers
|
|
452
|
+
huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
216
453
|
writeFileSync(huskyPreCommit, huskyContent);
|
|
217
454
|
try {
|
|
218
455
|
chmodSync(huskyPreCommit, '755');
|
|
@@ -224,11 +461,17 @@ export async function installPrecommit(): Promise<void> {
|
|
|
224
461
|
}
|
|
225
462
|
|
|
226
463
|
console.log(chalk.green('\n โ
Pre-commit hook installed!\n'));
|
|
227
|
-
console.log(chalk.cyan(' What this
|
|
228
|
-
console.log(chalk.gray(' -
|
|
229
|
-
console.log(chalk.gray(' -
|
|
230
|
-
console.log(chalk.gray(' -
|
|
231
|
-
console.log(chalk.gray(' -
|
|
464
|
+
console.log(chalk.cyan(' What this validates:'));
|
|
465
|
+
console.log(chalk.gray(' - API routes have error handling'));
|
|
466
|
+
console.log(chalk.gray(' - Request bodies are validated with Zod'));
|
|
467
|
+
console.log(chalk.gray(' - No console.log statements in production code'));
|
|
468
|
+
console.log(chalk.gray(' - No hardcoded secrets or URLs'));
|
|
469
|
+
console.log(chalk.gray(' - No SQL injection vulnerabilities'));
|
|
470
|
+
console.log(chalk.gray(' - Proper TypeScript types (no "any")'));
|
|
471
|
+
console.log(chalk.gray(' - Async functions have error handling'));
|
|
472
|
+
console.log(chalk.gray(' - No empty catch blocks'));
|
|
473
|
+
console.log(chalk.gray(' - Auth checks on protected routes'));
|
|
474
|
+
console.log(chalk.gray(' - Runs tests before commit\n'));
|
|
232
475
|
console.log(chalk.yellow(' To bypass (not recommended):'));
|
|
233
476
|
console.log(chalk.gray(' git commit --no-verify\n'));
|
|
234
477
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/c/dev/1 - CodeBakers/codebakers-server/cli
|