@codebakers/cli 1.6.0 → 2.0.0
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/audit.d.ts +19 -0
- package/dist/commands/audit.js +730 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +176 -0
- package/dist/commands/doctor.js +59 -4
- package/dist/commands/heal.d.ts +41 -0
- package/dist/commands/heal.js +734 -0
- package/dist/commands/login.js +12 -16
- package/dist/commands/provision.d.ts +55 -3
- package/dist/commands/provision.js +243 -74
- package/dist/commands/scaffold.js +158 -80
- package/dist/commands/setup.js +60 -19
- package/dist/commands/upgrade.d.ts +4 -0
- package/dist/commands/upgrade.js +90 -0
- package/dist/config.d.ts +61 -5
- package/dist/config.js +268 -5
- package/dist/index.js +44 -3
- package/dist/lib/api.d.ts +45 -0
- package/dist/lib/api.js +159 -0
- package/dist/mcp/server.js +146 -0
- package/package.json +1 -1
- package/src/commands/audit.ts +827 -0
- package/src/commands/config.ts +216 -0
- package/src/commands/doctor.ts +69 -4
- package/src/commands/heal.ts +889 -0
- package/src/commands/login.ts +14 -18
- package/src/commands/provision.ts +323 -101
- package/src/commands/scaffold.ts +188 -81
- package/src/commands/setup.ts +65 -20
- package/src/commands/upgrade.ts +110 -0
- package/src/config.ts +320 -11
- package/src/index.ts +48 -3
- package/src/lib/api.ts +183 -0
- package/src/mcp/server.ts +160 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
interface AuditCheck {
|
|
8
|
+
name: string;
|
|
9
|
+
category: string;
|
|
10
|
+
passed: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
details?: string[];
|
|
13
|
+
severity: 'error' | 'warning' | 'info';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AuditResult {
|
|
17
|
+
checks: AuditCheck[];
|
|
18
|
+
score: number;
|
|
19
|
+
maxScore: number;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run automated code quality audit
|
|
25
|
+
*/
|
|
26
|
+
export async function audit(): Promise<AuditResult> {
|
|
27
|
+
console.log(chalk.blue('\n CodeBakers Audit\n'));
|
|
28
|
+
console.log(chalk.gray(' Running automated checks...\n'));
|
|
29
|
+
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const checks: AuditCheck[] = [];
|
|
32
|
+
|
|
33
|
+
// ============================================================
|
|
34
|
+
// BUILD & TYPES
|
|
35
|
+
// ============================================================
|
|
36
|
+
console.log(chalk.white(' Build & Types:'));
|
|
37
|
+
|
|
38
|
+
// Check TypeScript
|
|
39
|
+
const tsCheck = await checkTypeScript(cwd);
|
|
40
|
+
checks.push(tsCheck);
|
|
41
|
+
printCheck(tsCheck);
|
|
42
|
+
|
|
43
|
+
// Check ESLint
|
|
44
|
+
const eslintCheck = await checkESLint(cwd);
|
|
45
|
+
checks.push(eslintCheck);
|
|
46
|
+
printCheck(eslintCheck);
|
|
47
|
+
|
|
48
|
+
// Check Build
|
|
49
|
+
const buildCheck = await checkBuild(cwd);
|
|
50
|
+
checks.push(buildCheck);
|
|
51
|
+
printCheck(buildCheck);
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// SECURITY
|
|
55
|
+
// ============================================================
|
|
56
|
+
console.log(chalk.white('\n Security:'));
|
|
57
|
+
|
|
58
|
+
// Check for secrets in code
|
|
59
|
+
const secretsCheck = checkSecretsInCode(cwd);
|
|
60
|
+
checks.push(secretsCheck);
|
|
61
|
+
printCheck(secretsCheck);
|
|
62
|
+
|
|
63
|
+
// Check npm audit
|
|
64
|
+
const npmAuditCheck = await checkNpmAudit(cwd);
|
|
65
|
+
checks.push(npmAuditCheck);
|
|
66
|
+
printCheck(npmAuditCheck);
|
|
67
|
+
|
|
68
|
+
// Check .env.local not committed
|
|
69
|
+
const envGitCheck = checkEnvNotCommitted(cwd);
|
|
70
|
+
checks.push(envGitCheck);
|
|
71
|
+
printCheck(envGitCheck);
|
|
72
|
+
|
|
73
|
+
// ============================================================
|
|
74
|
+
// CODE QUALITY
|
|
75
|
+
// ============================================================
|
|
76
|
+
console.log(chalk.white('\n Code Quality:'));
|
|
77
|
+
|
|
78
|
+
// Check for console.log
|
|
79
|
+
const consoleLogCheck = checkConsoleLog(cwd);
|
|
80
|
+
checks.push(consoleLogCheck);
|
|
81
|
+
printCheck(consoleLogCheck);
|
|
82
|
+
|
|
83
|
+
// Check for API route validation
|
|
84
|
+
const validationCheck = checkApiValidation(cwd);
|
|
85
|
+
checks.push(validationCheck);
|
|
86
|
+
printCheck(validationCheck);
|
|
87
|
+
|
|
88
|
+
// Check for error boundaries
|
|
89
|
+
const errorBoundaryCheck = checkErrorBoundary(cwd);
|
|
90
|
+
checks.push(errorBoundaryCheck);
|
|
91
|
+
printCheck(errorBoundaryCheck);
|
|
92
|
+
|
|
93
|
+
// ============================================================
|
|
94
|
+
// ENVIRONMENT
|
|
95
|
+
// ============================================================
|
|
96
|
+
console.log(chalk.white('\n Environment:'));
|
|
97
|
+
|
|
98
|
+
// Check .env.example exists
|
|
99
|
+
const envExampleCheck = checkEnvExample(cwd);
|
|
100
|
+
checks.push(envExampleCheck);
|
|
101
|
+
printCheck(envExampleCheck);
|
|
102
|
+
|
|
103
|
+
// Check env vars match
|
|
104
|
+
const envMatchCheck = checkEnvMatch(cwd);
|
|
105
|
+
checks.push(envMatchCheck);
|
|
106
|
+
printCheck(envMatchCheck);
|
|
107
|
+
|
|
108
|
+
// ============================================================
|
|
109
|
+
// PROJECT STRUCTURE
|
|
110
|
+
// ============================================================
|
|
111
|
+
console.log(chalk.white('\n Project Structure:'));
|
|
112
|
+
|
|
113
|
+
// Check for CodeBakers patterns
|
|
114
|
+
const patternsCheck = checkCodeBakersPatterns(cwd);
|
|
115
|
+
checks.push(patternsCheck);
|
|
116
|
+
printCheck(patternsCheck);
|
|
117
|
+
|
|
118
|
+
// Check for tests
|
|
119
|
+
const testsCheck = checkTests(cwd);
|
|
120
|
+
checks.push(testsCheck);
|
|
121
|
+
printCheck(testsCheck);
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// SUMMARY
|
|
125
|
+
// ============================================================
|
|
126
|
+
const passed = checks.filter(c => c.passed).length;
|
|
127
|
+
const total = checks.length;
|
|
128
|
+
const score = Math.round((passed / total) * 100);
|
|
129
|
+
|
|
130
|
+
console.log(chalk.white('\n ─────────────────────────────────────────────────\n'));
|
|
131
|
+
|
|
132
|
+
if (score >= 90) {
|
|
133
|
+
console.log(chalk.green(` ✅ Score: ${passed}/${total} checks passed (${score}%)\n`));
|
|
134
|
+
console.log(chalk.green(' Excellent! Your project is production-ready.\n'));
|
|
135
|
+
} else if (score >= 70) {
|
|
136
|
+
console.log(chalk.yellow(` ⚠️ Score: ${passed}/${total} checks passed (${score}%)\n`));
|
|
137
|
+
console.log(chalk.yellow(' Good progress. Fix the issues above before deploying.\n'));
|
|
138
|
+
} else {
|
|
139
|
+
console.log(chalk.red(` ❌ Score: ${passed}/${total} checks passed (${score}%)\n`));
|
|
140
|
+
console.log(chalk.red(' Needs attention. Address critical issues before deploying.\n'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Show critical issues summary
|
|
144
|
+
const criticalIssues = checks.filter(c => !c.passed && c.severity === 'error');
|
|
145
|
+
if (criticalIssues.length > 0) {
|
|
146
|
+
console.log(chalk.red(' Critical Issues:'));
|
|
147
|
+
for (const issue of criticalIssues) {
|
|
148
|
+
console.log(chalk.red(` • ${issue.message}`));
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show warnings
|
|
154
|
+
const warnings = checks.filter(c => !c.passed && c.severity === 'warning');
|
|
155
|
+
if (warnings.length > 0) {
|
|
156
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
157
|
+
for (const warning of warnings) {
|
|
158
|
+
console.log(chalk.yellow(` • ${warning.message}`));
|
|
159
|
+
}
|
|
160
|
+
console.log('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.gray(' Tip: Run /audit in Claude for full 100-point inspection.\n'));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
checks,
|
|
167
|
+
score,
|
|
168
|
+
maxScore: 100,
|
|
169
|
+
passed: score >= 70,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// CHECK FUNCTIONS
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
function printCheck(check: AuditCheck): void {
|
|
178
|
+
const icon = check.passed ? chalk.green('✓') : (check.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠'));
|
|
179
|
+
console.log(` ${icon} ${check.message}`);
|
|
180
|
+
if (!check.passed && check.details && check.details.length > 0) {
|
|
181
|
+
for (const detail of check.details.slice(0, 3)) {
|
|
182
|
+
console.log(chalk.gray(` └─ ${detail}`));
|
|
183
|
+
}
|
|
184
|
+
if (check.details.length > 3) {
|
|
185
|
+
console.log(chalk.gray(` └─ ...and ${check.details.length - 3} more`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function checkTypeScript(cwd: string): Promise<AuditCheck> {
|
|
191
|
+
const tsconfigPath = join(cwd, 'tsconfig.json');
|
|
192
|
+
|
|
193
|
+
if (!existsSync(tsconfigPath)) {
|
|
194
|
+
return {
|
|
195
|
+
name: 'typescript',
|
|
196
|
+
category: 'build',
|
|
197
|
+
passed: false,
|
|
198
|
+
message: 'No tsconfig.json found',
|
|
199
|
+
severity: 'warning',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
execSync('npx tsc --noEmit', { cwd, stdio: 'pipe' });
|
|
205
|
+
return {
|
|
206
|
+
name: 'typescript',
|
|
207
|
+
category: 'build',
|
|
208
|
+
passed: true,
|
|
209
|
+
message: 'TypeScript compiles (0 errors)',
|
|
210
|
+
severity: 'info',
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const output = error instanceof Error && 'stdout' in error
|
|
214
|
+
? (error as { stdout?: Buffer }).stdout?.toString() || ''
|
|
215
|
+
: '';
|
|
216
|
+
const errorCount = (output.match(/error TS/g) || []).length;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
name: 'typescript',
|
|
220
|
+
category: 'build',
|
|
221
|
+
passed: false,
|
|
222
|
+
message: `TypeScript errors (${errorCount} errors)`,
|
|
223
|
+
details: ['Run: npx tsc --noEmit to see details'],
|
|
224
|
+
severity: 'error',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function checkESLint(cwd: string): Promise<AuditCheck> {
|
|
230
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
231
|
+
|
|
232
|
+
if (!existsSync(packageJsonPath)) {
|
|
233
|
+
return {
|
|
234
|
+
name: 'eslint',
|
|
235
|
+
category: 'build',
|
|
236
|
+
passed: false,
|
|
237
|
+
message: 'No package.json found',
|
|
238
|
+
severity: 'warning',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
244
|
+
const hasEslint = packageJson.devDependencies?.eslint || packageJson.dependencies?.eslint;
|
|
245
|
+
|
|
246
|
+
if (!hasEslint) {
|
|
247
|
+
return {
|
|
248
|
+
name: 'eslint',
|
|
249
|
+
category: 'build',
|
|
250
|
+
passed: false,
|
|
251
|
+
message: 'ESLint not installed',
|
|
252
|
+
details: ['Run: npm install -D eslint'],
|
|
253
|
+
severity: 'warning',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
execSync('npx eslint . --max-warnings=0', { cwd, stdio: 'pipe' });
|
|
258
|
+
return {
|
|
259
|
+
name: 'eslint',
|
|
260
|
+
category: 'build',
|
|
261
|
+
passed: true,
|
|
262
|
+
message: 'ESLint passes (0 warnings)',
|
|
263
|
+
severity: 'info',
|
|
264
|
+
};
|
|
265
|
+
} catch {
|
|
266
|
+
return {
|
|
267
|
+
name: 'eslint',
|
|
268
|
+
category: 'build',
|
|
269
|
+
passed: false,
|
|
270
|
+
message: 'ESLint has warnings or errors',
|
|
271
|
+
details: ['Run: npx eslint . to see details'],
|
|
272
|
+
severity: 'warning',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function checkBuild(cwd: string): Promise<AuditCheck> {
|
|
278
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
279
|
+
|
|
280
|
+
if (!existsSync(packageJsonPath)) {
|
|
281
|
+
return {
|
|
282
|
+
name: 'build',
|
|
283
|
+
category: 'build',
|
|
284
|
+
passed: false,
|
|
285
|
+
message: 'No package.json found',
|
|
286
|
+
severity: 'error',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
292
|
+
const hasBuildScript = packageJson.scripts?.build;
|
|
293
|
+
|
|
294
|
+
if (!hasBuildScript) {
|
|
295
|
+
return {
|
|
296
|
+
name: 'build',
|
|
297
|
+
category: 'build',
|
|
298
|
+
passed: true,
|
|
299
|
+
message: 'No build script (skipped)',
|
|
300
|
+
severity: 'info',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if .next exists (Next.js) or dist exists
|
|
305
|
+
const hasNextBuild = existsSync(join(cwd, '.next'));
|
|
306
|
+
const hasDistBuild = existsSync(join(cwd, 'dist'));
|
|
307
|
+
|
|
308
|
+
if (hasNextBuild || hasDistBuild) {
|
|
309
|
+
return {
|
|
310
|
+
name: 'build',
|
|
311
|
+
category: 'build',
|
|
312
|
+
passed: true,
|
|
313
|
+
message: 'Build output exists',
|
|
314
|
+
severity: 'info',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
name: 'build',
|
|
320
|
+
category: 'build',
|
|
321
|
+
passed: false,
|
|
322
|
+
message: 'No build output found',
|
|
323
|
+
details: ['Run: npm run build'],
|
|
324
|
+
severity: 'warning',
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
return {
|
|
328
|
+
name: 'build',
|
|
329
|
+
category: 'build',
|
|
330
|
+
passed: false,
|
|
331
|
+
message: 'Could not check build status',
|
|
332
|
+
severity: 'warning',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function checkSecretsInCode(cwd: string): AuditCheck {
|
|
338
|
+
const srcDir = join(cwd, 'src');
|
|
339
|
+
if (!existsSync(srcDir)) {
|
|
340
|
+
return {
|
|
341
|
+
name: 'secrets',
|
|
342
|
+
category: 'security',
|
|
343
|
+
passed: true,
|
|
344
|
+
message: 'No src/ directory (skipped)',
|
|
345
|
+
severity: 'info',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const secretPatterns = [
|
|
350
|
+
/sk_live_[a-zA-Z0-9]+/, // Stripe live key
|
|
351
|
+
/sk_test_[a-zA-Z0-9]+/, // Stripe test key
|
|
352
|
+
/ghp_[a-zA-Z0-9]+/, // GitHub token
|
|
353
|
+
/AKIA[A-Z0-9]{16}/, // AWS access key
|
|
354
|
+
/-----BEGIN RSA PRIVATE KEY-----/, // RSA key
|
|
355
|
+
/-----BEGIN PRIVATE KEY-----/, // Generic private key
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const issues: string[] = [];
|
|
359
|
+
|
|
360
|
+
function scanDir(dir: string): void {
|
|
361
|
+
try {
|
|
362
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
|
|
365
|
+
scanDir(join(dir, file.name));
|
|
366
|
+
} else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.tsx') || file.name.endsWith('.js'))) {
|
|
367
|
+
const content = readFileSync(join(dir, file.name), 'utf-8');
|
|
368
|
+
for (const pattern of secretPatterns) {
|
|
369
|
+
if (pattern.test(content)) {
|
|
370
|
+
issues.push(join(dir, file.name).replace(cwd, ''));
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// Ignore read errors
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
scanDir(srcDir);
|
|
382
|
+
|
|
383
|
+
if (issues.length > 0) {
|
|
384
|
+
return {
|
|
385
|
+
name: 'secrets',
|
|
386
|
+
category: 'security',
|
|
387
|
+
passed: false,
|
|
388
|
+
message: `Possible secrets in code (${issues.length} files)`,
|
|
389
|
+
details: issues,
|
|
390
|
+
severity: 'error',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
name: 'secrets',
|
|
396
|
+
category: 'security',
|
|
397
|
+
passed: true,
|
|
398
|
+
message: 'No secrets detected in code',
|
|
399
|
+
severity: 'info',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function checkNpmAudit(cwd: string): Promise<AuditCheck> {
|
|
404
|
+
try {
|
|
405
|
+
execSync('npm audit --audit-level=high --json', { cwd, stdio: 'pipe' });
|
|
406
|
+
return {
|
|
407
|
+
name: 'npm-audit',
|
|
408
|
+
category: 'security',
|
|
409
|
+
passed: true,
|
|
410
|
+
message: 'No high/critical vulnerabilities',
|
|
411
|
+
severity: 'info',
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
try {
|
|
415
|
+
const output = error instanceof Error && 'stdout' in error
|
|
416
|
+
? (error as { stdout?: Buffer }).stdout?.toString() || '{}'
|
|
417
|
+
: '{}';
|
|
418
|
+
const auditResult = JSON.parse(output);
|
|
419
|
+
const high = auditResult.metadata?.vulnerabilities?.high || 0;
|
|
420
|
+
const critical = auditResult.metadata?.vulnerabilities?.critical || 0;
|
|
421
|
+
|
|
422
|
+
if (high > 0 || critical > 0) {
|
|
423
|
+
return {
|
|
424
|
+
name: 'npm-audit',
|
|
425
|
+
category: 'security',
|
|
426
|
+
passed: false,
|
|
427
|
+
message: `Vulnerabilities: ${critical} critical, ${high} high`,
|
|
428
|
+
details: ['Run: npm audit to see details', 'Run: npm audit fix to auto-fix'],
|
|
429
|
+
severity: 'error',
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Parse failed, assume passed
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
name: 'npm-audit',
|
|
438
|
+
category: 'security',
|
|
439
|
+
passed: true,
|
|
440
|
+
message: 'No high/critical vulnerabilities',
|
|
441
|
+
severity: 'info',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function checkEnvNotCommitted(cwd: string): AuditCheck {
|
|
447
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
448
|
+
|
|
449
|
+
if (!existsSync(gitignorePath)) {
|
|
450
|
+
return {
|
|
451
|
+
name: 'env-git',
|
|
452
|
+
category: 'security',
|
|
453
|
+
passed: false,
|
|
454
|
+
message: 'No .gitignore file',
|
|
455
|
+
details: ['Create .gitignore and add .env.local'],
|
|
456
|
+
severity: 'warning',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
461
|
+
const hasEnvLocal = gitignore.includes('.env.local') || gitignore.includes('.env*.local');
|
|
462
|
+
|
|
463
|
+
if (!hasEnvLocal) {
|
|
464
|
+
return {
|
|
465
|
+
name: 'env-git',
|
|
466
|
+
category: 'security',
|
|
467
|
+
passed: false,
|
|
468
|
+
message: '.env.local not in .gitignore',
|
|
469
|
+
details: ['Add .env.local to .gitignore'],
|
|
470
|
+
severity: 'error',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
name: 'env-git',
|
|
476
|
+
category: 'security',
|
|
477
|
+
passed: true,
|
|
478
|
+
message: '.env.local properly gitignored',
|
|
479
|
+
severity: 'info',
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function checkConsoleLog(cwd: string): AuditCheck {
|
|
484
|
+
const srcDir = join(cwd, 'src');
|
|
485
|
+
if (!existsSync(srcDir)) {
|
|
486
|
+
return {
|
|
487
|
+
name: 'console-log',
|
|
488
|
+
category: 'quality',
|
|
489
|
+
passed: true,
|
|
490
|
+
message: 'No src/ directory (skipped)',
|
|
491
|
+
severity: 'info',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const issues: string[] = [];
|
|
496
|
+
|
|
497
|
+
function scanDir(dir: string): void {
|
|
498
|
+
try {
|
|
499
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
500
|
+
for (const file of files) {
|
|
501
|
+
if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
|
|
502
|
+
scanDir(join(dir, file.name));
|
|
503
|
+
} else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.tsx'))) {
|
|
504
|
+
const content = readFileSync(join(dir, file.name), 'utf-8');
|
|
505
|
+
// Match console.log but not console.error or console.warn
|
|
506
|
+
if (/console\.log\s*\(/.test(content)) {
|
|
507
|
+
issues.push(join(dir, file.name).replace(cwd, ''));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
// Ignore read errors
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
scanDir(srcDir);
|
|
517
|
+
|
|
518
|
+
if (issues.length > 0) {
|
|
519
|
+
return {
|
|
520
|
+
name: 'console-log',
|
|
521
|
+
category: 'quality',
|
|
522
|
+
passed: false,
|
|
523
|
+
message: `console.log found (${issues.length} files)`,
|
|
524
|
+
details: issues,
|
|
525
|
+
severity: 'warning',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
name: 'console-log',
|
|
531
|
+
category: 'quality',
|
|
532
|
+
passed: true,
|
|
533
|
+
message: 'No console.log in production code',
|
|
534
|
+
severity: 'info',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function checkApiValidation(cwd: string): AuditCheck {
|
|
539
|
+
const apiDir = join(cwd, 'src', 'app', 'api');
|
|
540
|
+
if (!existsSync(apiDir)) {
|
|
541
|
+
return {
|
|
542
|
+
name: 'api-validation',
|
|
543
|
+
category: 'quality',
|
|
544
|
+
passed: true,
|
|
545
|
+
message: 'No API routes (skipped)',
|
|
546
|
+
severity: 'info',
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const issues: string[] = [];
|
|
551
|
+
let totalRoutes = 0;
|
|
552
|
+
|
|
553
|
+
function scanDir(dir: string): void {
|
|
554
|
+
try {
|
|
555
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
556
|
+
for (const file of files) {
|
|
557
|
+
if (file.isDirectory()) {
|
|
558
|
+
scanDir(join(dir, file.name));
|
|
559
|
+
} else if (file.name === 'route.ts' || file.name === 'route.tsx') {
|
|
560
|
+
totalRoutes++;
|
|
561
|
+
const content = readFileSync(join(dir, file.name), 'utf-8');
|
|
562
|
+
// Check for Zod validation
|
|
563
|
+
if (!content.includes('zod') && !content.includes('.parse(') && !content.includes('.safeParse(')) {
|
|
564
|
+
issues.push(join(dir, file.name).replace(cwd, ''));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
// Ignore read errors
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
scanDir(apiDir);
|
|
574
|
+
|
|
575
|
+
if (issues.length > 0) {
|
|
576
|
+
return {
|
|
577
|
+
name: 'api-validation',
|
|
578
|
+
category: 'quality',
|
|
579
|
+
passed: false,
|
|
580
|
+
message: `API routes without validation (${issues.length}/${totalRoutes})`,
|
|
581
|
+
details: issues,
|
|
582
|
+
severity: 'warning',
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
name: 'api-validation',
|
|
588
|
+
category: 'quality',
|
|
589
|
+
passed: true,
|
|
590
|
+
message: `All ${totalRoutes} API routes have validation`,
|
|
591
|
+
severity: 'info',
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function checkErrorBoundary(cwd: string): AuditCheck {
|
|
596
|
+
const appDir = join(cwd, 'src', 'app');
|
|
597
|
+
if (!existsSync(appDir)) {
|
|
598
|
+
return {
|
|
599
|
+
name: 'error-boundary',
|
|
600
|
+
category: 'quality',
|
|
601
|
+
passed: true,
|
|
602
|
+
message: 'No app/ directory (skipped)',
|
|
603
|
+
severity: 'info',
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const errorPath = join(appDir, 'error.tsx');
|
|
608
|
+
const globalErrorPath = join(appDir, 'global-error.tsx');
|
|
609
|
+
|
|
610
|
+
if (existsSync(errorPath) || existsSync(globalErrorPath)) {
|
|
611
|
+
return {
|
|
612
|
+
name: 'error-boundary',
|
|
613
|
+
category: 'quality',
|
|
614
|
+
passed: true,
|
|
615
|
+
message: 'Error boundary exists',
|
|
616
|
+
severity: 'info',
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
name: 'error-boundary',
|
|
622
|
+
category: 'quality',
|
|
623
|
+
passed: false,
|
|
624
|
+
message: 'No error boundary (error.tsx)',
|
|
625
|
+
details: ['Create src/app/error.tsx for error handling'],
|
|
626
|
+
severity: 'warning',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function checkEnvExample(cwd: string): AuditCheck {
|
|
631
|
+
const envExamplePath = join(cwd, '.env.example');
|
|
632
|
+
|
|
633
|
+
if (existsSync(envExamplePath)) {
|
|
634
|
+
return {
|
|
635
|
+
name: 'env-example',
|
|
636
|
+
category: 'environment',
|
|
637
|
+
passed: true,
|
|
638
|
+
message: '.env.example exists',
|
|
639
|
+
severity: 'info',
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
name: 'env-example',
|
|
645
|
+
category: 'environment',
|
|
646
|
+
passed: false,
|
|
647
|
+
message: 'No .env.example file',
|
|
648
|
+
details: ['Create .env.example with all required variables'],
|
|
649
|
+
severity: 'warning',
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function checkEnvMatch(cwd: string): AuditCheck {
|
|
654
|
+
const envExamplePath = join(cwd, '.env.example');
|
|
655
|
+
const envLocalPath = join(cwd, '.env.local');
|
|
656
|
+
|
|
657
|
+
if (!existsSync(envExamplePath)) {
|
|
658
|
+
return {
|
|
659
|
+
name: 'env-match',
|
|
660
|
+
category: 'environment',
|
|
661
|
+
passed: true,
|
|
662
|
+
message: 'No .env.example to compare (skipped)',
|
|
663
|
+
severity: 'info',
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!existsSync(envLocalPath)) {
|
|
668
|
+
return {
|
|
669
|
+
name: 'env-match',
|
|
670
|
+
category: 'environment',
|
|
671
|
+
passed: false,
|
|
672
|
+
message: 'No .env.local file',
|
|
673
|
+
details: ['Copy .env.example to .env.local and fill in values'],
|
|
674
|
+
severity: 'warning',
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const exampleVars = parseEnvFile(readFileSync(envExamplePath, 'utf-8'));
|
|
679
|
+
const localVars = parseEnvFile(readFileSync(envLocalPath, 'utf-8'));
|
|
680
|
+
|
|
681
|
+
const missingInLocal = exampleVars.filter(v => !localVars.includes(v));
|
|
682
|
+
const extraInLocal = localVars.filter(v => !exampleVars.includes(v) && !v.startsWith('#'));
|
|
683
|
+
|
|
684
|
+
if (missingInLocal.length > 0) {
|
|
685
|
+
return {
|
|
686
|
+
name: 'env-match',
|
|
687
|
+
category: 'environment',
|
|
688
|
+
passed: false,
|
|
689
|
+
message: `Missing ${missingInLocal.length} vars from .env.example`,
|
|
690
|
+
details: missingInLocal,
|
|
691
|
+
severity: 'warning',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (extraInLocal.length > 0) {
|
|
696
|
+
return {
|
|
697
|
+
name: 'env-match',
|
|
698
|
+
category: 'environment',
|
|
699
|
+
passed: false,
|
|
700
|
+
message: `${extraInLocal.length} vars in .env.local not in .env.example`,
|
|
701
|
+
details: extraInLocal,
|
|
702
|
+
severity: 'info',
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
name: 'env-match',
|
|
708
|
+
category: 'environment',
|
|
709
|
+
passed: true,
|
|
710
|
+
message: 'Environment variables match',
|
|
711
|
+
severity: 'info',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function parseEnvFile(content: string): string[] {
|
|
716
|
+
return content
|
|
717
|
+
.split('\n')
|
|
718
|
+
.filter(line => line.trim() && !line.startsWith('#'))
|
|
719
|
+
.map(line => line.split('=')[0].trim())
|
|
720
|
+
.filter(Boolean);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function checkCodeBakersPatterns(cwd: string): AuditCheck {
|
|
724
|
+
const claudeDir = join(cwd, '.claude');
|
|
725
|
+
const claudeMd = join(cwd, 'CLAUDE.md');
|
|
726
|
+
|
|
727
|
+
if (!existsSync(claudeDir) && !existsSync(claudeMd)) {
|
|
728
|
+
return {
|
|
729
|
+
name: 'patterns',
|
|
730
|
+
category: 'structure',
|
|
731
|
+
passed: false,
|
|
732
|
+
message: 'CodeBakers patterns not installed',
|
|
733
|
+
details: ['Run: codebakers init'],
|
|
734
|
+
severity: 'info',
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (existsSync(claudeDir)) {
|
|
739
|
+
const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
|
|
740
|
+
return {
|
|
741
|
+
name: 'patterns',
|
|
742
|
+
category: 'structure',
|
|
743
|
+
passed: true,
|
|
744
|
+
message: `${files.length} CodeBakers modules installed`,
|
|
745
|
+
severity: 'info',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
name: 'patterns',
|
|
751
|
+
category: 'structure',
|
|
752
|
+
passed: true,
|
|
753
|
+
message: 'CLAUDE.md exists',
|
|
754
|
+
severity: 'info',
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function checkTests(cwd: string): AuditCheck {
|
|
759
|
+
const testDirs = [
|
|
760
|
+
join(cwd, '__tests__'),
|
|
761
|
+
join(cwd, 'tests'),
|
|
762
|
+
join(cwd, 'test'),
|
|
763
|
+
join(cwd, 'src', '__tests__'),
|
|
764
|
+
];
|
|
765
|
+
|
|
766
|
+
const testFiles: string[] = [];
|
|
767
|
+
|
|
768
|
+
for (const dir of testDirs) {
|
|
769
|
+
if (existsSync(dir)) {
|
|
770
|
+
try {
|
|
771
|
+
const files = readdirSync(dir, { recursive: true }) as string[];
|
|
772
|
+
testFiles.push(...files.filter(f =>
|
|
773
|
+
f.endsWith('.test.ts') ||
|
|
774
|
+
f.endsWith('.test.tsx') ||
|
|
775
|
+
f.endsWith('.spec.ts') ||
|
|
776
|
+
f.endsWith('.spec.tsx')
|
|
777
|
+
));
|
|
778
|
+
} catch {
|
|
779
|
+
// Ignore
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Also check for test files in src
|
|
785
|
+
const srcDir = join(cwd, 'src');
|
|
786
|
+
if (existsSync(srcDir)) {
|
|
787
|
+
function findTestFiles(dir: string): void {
|
|
788
|
+
try {
|
|
789
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
790
|
+
for (const file of files) {
|
|
791
|
+
if (file.isDirectory() && !file.name.startsWith('.') && file.name !== 'node_modules') {
|
|
792
|
+
findTestFiles(join(dir, file.name));
|
|
793
|
+
} else if (
|
|
794
|
+
file.name.endsWith('.test.ts') ||
|
|
795
|
+
file.name.endsWith('.test.tsx') ||
|
|
796
|
+
file.name.endsWith('.spec.ts') ||
|
|
797
|
+
file.name.endsWith('.spec.tsx')
|
|
798
|
+
) {
|
|
799
|
+
testFiles.push(file.name);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
// Ignore
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
findTestFiles(srcDir);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (testFiles.length === 0) {
|
|
810
|
+
return {
|
|
811
|
+
name: 'tests',
|
|
812
|
+
category: 'structure',
|
|
813
|
+
passed: false,
|
|
814
|
+
message: 'No test files found',
|
|
815
|
+
details: ['Add tests for critical paths'],
|
|
816
|
+
severity: 'warning',
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
name: 'tests',
|
|
822
|
+
category: 'structure',
|
|
823
|
+
passed: true,
|
|
824
|
+
message: `${testFiles.length} test files found`,
|
|
825
|
+
severity: 'info',
|
|
826
|
+
};
|
|
827
|
+
}
|