@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.
@@ -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 - Session Enforcement
12
- # Blocks commits unless AI called discover_patterns and validate_complete
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-session.js"
15
+ node "$(dirname "$0")/validate-code.js"
16
16
  exit $?
17
17
  `;
18
- const VALIDATE_SESSION_SCRIPT = `#!/usr/bin/env node
18
+ const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
19
19
  /**
20
- * CodeBakers Pre-Commit Validation
21
- * Blocks commits unless a valid session exists
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
- async function validateSession() {
38
- const cwd = process.cwd();
39
- const stateFile = path.join(cwd, '.codebakers.json');
40
-
41
- // Check if this is a CodeBakers project
42
- if (!fs.existsSync(stateFile)) {
43
- return { valid: true, reason: 'not-codebakers-project' };
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
- let state;
47
- try {
48
- state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
49
- } catch (error) {
50
- return { valid: false, reason: 'invalid-state-file' };
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
- // Check if v6.0 server-enforced mode
54
- if (!state.serverEnforced) {
55
- return { valid: true, reason: 'legacy-project' };
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
- // Check for session token (means discover_patterns was called)
59
- const sessionToken = state.currentSessionToken;
60
- if (!sessionToken) {
61
- // Check if there's a recent passed validation
62
- const lastValidation = state.lastValidation;
63
- if (!lastValidation || !lastValidation.passed) {
64
- return {
65
- valid: false,
66
- reason: 'no-session',
67
- message: 'No active CodeBakers session.\\nAI must call discover_patterns before writing code.'
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
- // Check session expiry
73
- const sessionExpiry = state.sessionExpiresAt;
74
- if (sessionExpiry && new Date(sessionExpiry) < new Date()) {
75
- return {
76
- valid: false,
77
- reason: 'session-expired',
78
- message: 'CodeBakers session has expired.\\nAI must call discover_patterns again.'
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
- // Check if validation was completed
83
- const lastValidation = state.lastValidation;
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
- // Check if validation passed
93
- if (!lastValidation.passed) {
94
- const issues = lastValidation.issues?.map(i => i.message || i).join(', ') || 'Unknown issues';
95
- return {
96
- valid: false,
97
- reason: 'validation-failed',
98
- message: 'Validation failed: ' + issues + '\\nAI must fix issues and call validate_complete again.'
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
- // Check if validation is recent (within last 30 minutes)
103
- const validationTime = new Date(lastValidation.timestamp);
104
- const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
105
- if (validationTime < thirtyMinutesAgo) {
106
- return {
107
- valid: false,
108
- reason: 'validation-stale',
109
- message: 'Validation is stale (older than 30 minutes).\\nAI must call validate_complete again.'
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
- return { valid: true, reason: 'session-valid' };
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
- console.log('');
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-session.js');
190
- (0, fs_1.writeFileSync)(validatePath, VALIDATE_SESSION_SCRIPT);
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-session')) {
201
- huskyContent += '\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
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 session enforcement\nnode .git/hooks/validate-session.js\n';
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 does:'));
223
- console.log(chalk_1.default.gray(' - Blocks commits unless AI called discover_patterns'));
224
- console.log(chalk_1.default.gray(' - Blocks commits unless AI called validate_complete'));
225
- console.log(chalk_1.default.gray(' - Requires validation to pass before committing'));
226
- console.log(chalk_1.default.gray(' - Validation expires after 30 minutes\n'));
460
+ console.log(chalk_1.default.cyan(' What this validates:'));
461
+ console.log(chalk_1.default.gray(' - API routes have error handling'));
462
+ console.log(chalk_1.default.gray(' - Request bodies are validated with Zod'));
463
+ console.log(chalk_1.default.gray(' - No console.log statements in production code'));
464
+ console.log(chalk_1.default.gray(' - No hardcoded secrets or URLs'));
465
+ console.log(chalk_1.default.gray(' - No SQL injection vulnerabilities'));
466
+ console.log(chalk_1.default.gray(' - Proper TypeScript types (no "any")'));
467
+ console.log(chalk_1.default.gray(' - Async functions have error handling'));
468
+ console.log(chalk_1.default.gray(' - No empty catch blocks'));
469
+ console.log(chalk_1.default.gray(' - Auth checks on protected routes'));
470
+ console.log(chalk_1.default.gray(' - Runs tests before commit\n'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.9.38",
3
+ "version": "3.9.39",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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 - Session Enforcement
7
- # Blocks commits unless AI called discover_patterns and validate_complete
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-session.js"
10
+ node "$(dirname "$0")/validate-code.js"
11
11
  exit $?
12
12
  `;
13
13
 
14
- const VALIDATE_SESSION_SCRIPT = `#!/usr/bin/env node
14
+ const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
15
15
  /**
16
- * CodeBakers Pre-Commit Validation
17
- * Blocks commits unless a valid session exists
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
- async function validateSession() {
34
- const cwd = process.cwd();
35
- const stateFile = path.join(cwd, '.codebakers.json');
36
-
37
- // Check if this is a CodeBakers project
38
- if (!fs.existsSync(stateFile)) {
39
- return { valid: true, reason: 'not-codebakers-project' };
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
- let state;
43
- try {
44
- state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
45
- } catch (error) {
46
- return { valid: false, reason: 'invalid-state-file' };
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
- // Check if v6.0 server-enforced mode
50
- if (!state.serverEnforced) {
51
- return { valid: true, reason: 'legacy-project' };
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
- // Check for session token (means discover_patterns was called)
55
- const sessionToken = state.currentSessionToken;
56
- if (!sessionToken) {
57
- // Check if there's a recent passed validation
58
- const lastValidation = state.lastValidation;
59
- if (!lastValidation || !lastValidation.passed) {
60
- return {
61
- valid: false,
62
- reason: 'no-session',
63
- message: 'No active CodeBakers session.\\nAI must call discover_patterns before writing code.'
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
- // Check session expiry
69
- const sessionExpiry = state.sessionExpiresAt;
70
- if (sessionExpiry && new Date(sessionExpiry) < new Date()) {
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
- // Check if validation was completed
79
- const lastValidation = state.lastValidation;
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
- // Check if validation passed
89
- if (!lastValidation.passed) {
90
- const issues = lastValidation.issues?.map(i => i.message || i).join(', ') || 'Unknown issues';
91
- return {
92
- valid: false,
93
- reason: 'validation-failed',
94
- message: 'Validation failed: ' + issues + '\\nAI must fix issues and call validate_complete again.'
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
- // Check if validation is recent (within last 30 minutes)
99
- const validationTime = new Date(lastValidation.timestamp);
100
- const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
101
- if (validationTime < thirtyMinutesAgo) {
102
- return {
103
- valid: false,
104
- reason: 'validation-stale',
105
- message: 'Validation is stale (older than 30 minutes).\\nAI must call validate_complete again.'
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
- return { valid: true, reason: 'session-valid' };
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
- console.log('');
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-session.js');
194
- writeFileSync(validatePath, VALIDATE_SESSION_SCRIPT);
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-session')) {
208
- huskyContent += '\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
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 session enforcement\nnode .git/hooks/validate-session.js\n';
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 does:'));
228
- console.log(chalk.gray(' - Blocks commits unless AI called discover_patterns'));
229
- console.log(chalk.gray(' - Blocks commits unless AI called validate_complete'));
230
- console.log(chalk.gray(' - Requires validation to pass before committing'));
231
- console.log(chalk.gray(' - Validation expires after 30 minutes\n'));
464
+ console.log(chalk.cyan(' What this validates:'));
465
+ console.log(chalk.gray(' - API routes have error handling'));
466
+ console.log(chalk.gray(' - Request bodies are validated with Zod'));
467
+ console.log(chalk.gray(' - No console.log statements in production code'));
468
+ console.log(chalk.gray(' - No hardcoded secrets or URLs'));
469
+ console.log(chalk.gray(' - No SQL injection vulnerabilities'));
470
+ console.log(chalk.gray(' - Proper TypeScript types (no "any")'));
471
+ console.log(chalk.gray(' - Async functions have error handling'));
472
+ console.log(chalk.gray(' - No empty catch blocks'));
473
+ console.log(chalk.gray(' - Auth checks on protected routes'));
474
+ console.log(chalk.gray(' - Runs tests before commit\n'));
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