codebakers 1.0.45
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/.vscodeignore +18 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/codebakers-1.0.0.vsix +0 -0
- package/codebakers-1.0.10.vsix +0 -0
- package/codebakers-1.0.11.vsix +0 -0
- package/codebakers-1.0.12.vsix +0 -0
- package/codebakers-1.0.13.vsix +0 -0
- package/codebakers-1.0.14.vsix +0 -0
- package/codebakers-1.0.15.vsix +0 -0
- package/codebakers-1.0.16.vsix +0 -0
- package/codebakers-1.0.17.vsix +0 -0
- package/codebakers-1.0.18.vsix +0 -0
- package/codebakers-1.0.19.vsix +0 -0
- package/codebakers-1.0.20.vsix +0 -0
- package/codebakers-1.0.21.vsix +0 -0
- package/codebakers-1.0.22.vsix +0 -0
- package/codebakers-1.0.23.vsix +0 -0
- package/codebakers-1.0.24.vsix +0 -0
- package/codebakers-1.0.25.vsix +0 -0
- package/codebakers-1.0.26.vsix +0 -0
- package/codebakers-1.0.27.vsix +0 -0
- package/codebakers-1.0.28.vsix +0 -0
- package/codebakers-1.0.29.vsix +0 -0
- package/codebakers-1.0.30.vsix +0 -0
- package/codebakers-1.0.31.vsix +0 -0
- package/codebakers-1.0.32.vsix +0 -0
- package/codebakers-1.0.35.vsix +0 -0
- package/codebakers-1.0.36.vsix +0 -0
- package/codebakers-1.0.37.vsix +0 -0
- package/codebakers-1.0.38.vsix +0 -0
- package/codebakers-1.0.39.vsix +0 -0
- package/codebakers-1.0.40.vsix +0 -0
- package/codebakers-1.0.41.vsix +0 -0
- package/codebakers-1.0.42.vsix +0 -0
- package/codebakers-1.0.43.vsix +0 -0
- package/codebakers-1.0.44.vsix +0 -0
- package/codebakers-1.0.45.vsix +0 -0
- package/dist/extension.js +1394 -0
- package/esbuild.js +63 -0
- package/media/icon.png +0 -0
- package/media/icon.svg +7 -0
- package/nul +1 -0
- package/package.json +127 -0
- package/preview.html +547 -0
- package/src/ChatPanelProvider.ts +1815 -0
- package/src/ChatViewProvider.ts +749 -0
- package/src/CodeBakersClient.ts +1146 -0
- package/src/CodeValidator.ts +645 -0
- package/src/FileOperations.ts +410 -0
- package/src/ProjectContext.ts +526 -0
- package/src/extension.ts +332 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { FileOperation } from './CodeBakersClient';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
passed: boolean;
|
|
12
|
+
errors: ValidationError[];
|
|
13
|
+
warnings: ValidationWarning[];
|
|
14
|
+
suggestions: string[];
|
|
15
|
+
tscResult?: TypeScriptCheckResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TypeScriptCheckResult {
|
|
19
|
+
passed: boolean;
|
|
20
|
+
errors: Array<{
|
|
21
|
+
file: string;
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
message: string;
|
|
25
|
+
code: string;
|
|
26
|
+
}>;
|
|
27
|
+
errorCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ValidationError {
|
|
31
|
+
file: string;
|
|
32
|
+
line?: number;
|
|
33
|
+
message: string;
|
|
34
|
+
type: 'type' | 'import' | 'syntax' | 'security';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ValidationWarning {
|
|
38
|
+
file: string;
|
|
39
|
+
message: string;
|
|
40
|
+
type: 'any-type' | 'missing-error-handling' | 'no-test' | 'console-log';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DependencyCheck {
|
|
44
|
+
missing: string[];
|
|
45
|
+
available: string[];
|
|
46
|
+
suggestions: { package: string; command: string }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TypeInventory {
|
|
50
|
+
types: Map<string, TypeInfo>;
|
|
51
|
+
exports: Map<string, ExportInfo[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TypeInfo {
|
|
55
|
+
name: string;
|
|
56
|
+
file: string;
|
|
57
|
+
kind: 'interface' | 'type' | 'enum' | 'class';
|
|
58
|
+
exported: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ExportInfo {
|
|
62
|
+
name: string;
|
|
63
|
+
file: string;
|
|
64
|
+
kind: 'function' | 'const' | 'class' | 'type' | 'default';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class CodeValidator {
|
|
68
|
+
private workspaceRoot: string | undefined;
|
|
69
|
+
private typeInventory: TypeInventory | null = null;
|
|
70
|
+
private installedPackages: Set<string> = new Set();
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
this.workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize the validator by scanning the project
|
|
78
|
+
*/
|
|
79
|
+
async initialize(): Promise<void> {
|
|
80
|
+
if (!this.workspaceRoot) return;
|
|
81
|
+
|
|
82
|
+
await Promise.all([
|
|
83
|
+
this.scanInstalledPackages(),
|
|
84
|
+
this.scanTypeInventory()
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Scan package.json for installed packages
|
|
90
|
+
*/
|
|
91
|
+
private async scanInstalledPackages(): Promise<void> {
|
|
92
|
+
if (!this.workspaceRoot) return;
|
|
93
|
+
|
|
94
|
+
const packageJsonPath = path.join(this.workspaceRoot, 'package.json');
|
|
95
|
+
if (!fs.existsSync(packageJsonPath)) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
99
|
+
const pkg = JSON.parse(content);
|
|
100
|
+
|
|
101
|
+
this.installedPackages.clear();
|
|
102
|
+
|
|
103
|
+
// Add all dependencies
|
|
104
|
+
for (const dep of Object.keys(pkg.dependencies || {})) {
|
|
105
|
+
this.installedPackages.add(dep);
|
|
106
|
+
}
|
|
107
|
+
for (const dep of Object.keys(pkg.devDependencies || {})) {
|
|
108
|
+
this.installedPackages.add(dep);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`CodeValidator: Found ${this.installedPackages.size} installed packages`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('CodeValidator: Failed to scan packages:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scan project for type definitions and exports
|
|
119
|
+
*/
|
|
120
|
+
private async scanTypeInventory(): Promise<void> {
|
|
121
|
+
if (!this.workspaceRoot) return;
|
|
122
|
+
|
|
123
|
+
this.typeInventory = {
|
|
124
|
+
types: new Map(),
|
|
125
|
+
exports: new Map()
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Find all TypeScript files
|
|
129
|
+
const files = await vscode.workspace.findFiles(
|
|
130
|
+
'**/*.{ts,tsx}',
|
|
131
|
+
'**/node_modules/**',
|
|
132
|
+
200
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
try {
|
|
137
|
+
const content = fs.readFileSync(file.fsPath, 'utf-8');
|
|
138
|
+
const relativePath = vscode.workspace.asRelativePath(file);
|
|
139
|
+
|
|
140
|
+
// Extract types and interfaces
|
|
141
|
+
this.extractTypes(content, relativePath);
|
|
142
|
+
|
|
143
|
+
// Extract exports
|
|
144
|
+
this.extractExports(content, relativePath);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Skip files we can't read
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`CodeValidator: Found ${this.typeInventory.types.size} types, ${this.typeInventory.exports.size} files with exports`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract type definitions from file content
|
|
155
|
+
*/
|
|
156
|
+
private extractTypes(content: string, filePath: string): void {
|
|
157
|
+
if (!this.typeInventory) return;
|
|
158
|
+
|
|
159
|
+
// Match interfaces
|
|
160
|
+
const interfaceRegex = /export\s+interface\s+(\w+)/g;
|
|
161
|
+
let match;
|
|
162
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
163
|
+
this.typeInventory.types.set(match[1], {
|
|
164
|
+
name: match[1],
|
|
165
|
+
file: filePath,
|
|
166
|
+
kind: 'interface',
|
|
167
|
+
exported: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Match type aliases
|
|
172
|
+
const typeRegex = /export\s+type\s+(\w+)/g;
|
|
173
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
174
|
+
this.typeInventory.types.set(match[1], {
|
|
175
|
+
name: match[1],
|
|
176
|
+
file: filePath,
|
|
177
|
+
kind: 'type',
|
|
178
|
+
exported: true
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Match enums
|
|
183
|
+
const enumRegex = /export\s+enum\s+(\w+)/g;
|
|
184
|
+
while ((match = enumRegex.exec(content)) !== null) {
|
|
185
|
+
this.typeInventory.types.set(match[1], {
|
|
186
|
+
name: match[1],
|
|
187
|
+
file: filePath,
|
|
188
|
+
kind: 'enum',
|
|
189
|
+
exported: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract exports from file content
|
|
196
|
+
*/
|
|
197
|
+
private extractExports(content: string, filePath: string): void {
|
|
198
|
+
if (!this.typeInventory) return;
|
|
199
|
+
|
|
200
|
+
const exports: ExportInfo[] = [];
|
|
201
|
+
|
|
202
|
+
// Match exported functions
|
|
203
|
+
const funcRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
206
|
+
exports.push({ name: match[1], file: filePath, kind: 'function' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Match exported consts
|
|
210
|
+
const constRegex = /export\s+const\s+(\w+)/g;
|
|
211
|
+
while ((match = constRegex.exec(content)) !== null) {
|
|
212
|
+
exports.push({ name: match[1], file: filePath, kind: 'const' });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Match exported classes
|
|
216
|
+
const classRegex = /export\s+class\s+(\w+)/g;
|
|
217
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
218
|
+
exports.push({ name: match[1], file: filePath, kind: 'class' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Match default exports
|
|
222
|
+
if (/export\s+default/.test(content)) {
|
|
223
|
+
exports.push({ name: 'default', file: filePath, kind: 'default' });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (exports.length > 0) {
|
|
227
|
+
this.typeInventory.exports.set(filePath, exports);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if required packages are installed
|
|
233
|
+
*/
|
|
234
|
+
checkDependencies(fileOperations: FileOperation[]): DependencyCheck {
|
|
235
|
+
const result: DependencyCheck = {
|
|
236
|
+
missing: [],
|
|
237
|
+
available: [],
|
|
238
|
+
suggestions: []
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const requiredPackages = new Set<string>();
|
|
242
|
+
|
|
243
|
+
for (const op of fileOperations) {
|
|
244
|
+
if (!op.content) continue;
|
|
245
|
+
|
|
246
|
+
// Extract import statements
|
|
247
|
+
const importRegex = /import\s+.*?\s+from\s+['"]([^'"./][^'"]*)['"]/g;
|
|
248
|
+
let match;
|
|
249
|
+
while ((match = importRegex.exec(op.content)) !== null) {
|
|
250
|
+
const pkg = match[1].split('/')[0]; // Handle scoped packages
|
|
251
|
+
if (pkg.startsWith('@')) {
|
|
252
|
+
// Scoped package like @tanstack/react-query
|
|
253
|
+
const scopedPkg = match[1].split('/').slice(0, 2).join('/');
|
|
254
|
+
requiredPackages.add(scopedPkg);
|
|
255
|
+
} else {
|
|
256
|
+
requiredPackages.add(pkg);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check against installed packages
|
|
262
|
+
for (const pkg of requiredPackages) {
|
|
263
|
+
if (this.installedPackages.has(pkg)) {
|
|
264
|
+
result.available.push(pkg);
|
|
265
|
+
} else {
|
|
266
|
+
// Skip Node.js built-ins
|
|
267
|
+
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'util', 'stream', 'events', 'url', 'querystring', 'os', 'child_process'];
|
|
268
|
+
if (!builtins.includes(pkg)) {
|
|
269
|
+
result.missing.push(pkg);
|
|
270
|
+
result.suggestions.push({
|
|
271
|
+
package: pkg,
|
|
272
|
+
command: `npm install ${pkg}`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Validate generated file operations
|
|
283
|
+
*/
|
|
284
|
+
async validateFileOperations(fileOperations: FileOperation[]): Promise<ValidationResult> {
|
|
285
|
+
const result: ValidationResult = {
|
|
286
|
+
passed: true,
|
|
287
|
+
errors: [],
|
|
288
|
+
warnings: [],
|
|
289
|
+
suggestions: []
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
for (const op of fileOperations) {
|
|
293
|
+
if (!op.content || op.action === 'delete') continue;
|
|
294
|
+
|
|
295
|
+
// Check imports resolve
|
|
296
|
+
const importErrors = this.validateImports(op);
|
|
297
|
+
result.errors.push(...importErrors);
|
|
298
|
+
|
|
299
|
+
// Check for type issues
|
|
300
|
+
const typeWarnings = this.checkTypeUsage(op);
|
|
301
|
+
result.warnings.push(...typeWarnings);
|
|
302
|
+
|
|
303
|
+
// Check for security issues
|
|
304
|
+
const securityErrors = this.checkSecurity(op);
|
|
305
|
+
result.errors.push(...securityErrors);
|
|
306
|
+
|
|
307
|
+
// Check for best practices
|
|
308
|
+
const practiceWarnings = this.checkBestPractices(op);
|
|
309
|
+
result.warnings.push(...practiceWarnings);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if tests are included
|
|
313
|
+
const hasTestFile = fileOperations.some(op =>
|
|
314
|
+
op.path.includes('.test.') ||
|
|
315
|
+
op.path.includes('.spec.') ||
|
|
316
|
+
op.path.includes('__tests__')
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!hasTestFile && fileOperations.length > 0) {
|
|
320
|
+
result.warnings.push({
|
|
321
|
+
file: 'project',
|
|
322
|
+
message: 'No test file included with this feature',
|
|
323
|
+
type: 'no-test'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add suggestions for existing types
|
|
328
|
+
const suggestions = this.suggestExistingTypes(fileOperations);
|
|
329
|
+
result.suggestions.push(...suggestions);
|
|
330
|
+
|
|
331
|
+
result.passed = result.errors.length === 0;
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validate that imports can resolve
|
|
338
|
+
*/
|
|
339
|
+
private validateImports(op: FileOperation): ValidationError[] {
|
|
340
|
+
const errors: ValidationError[] = [];
|
|
341
|
+
if (!op.content || !this.workspaceRoot) return errors;
|
|
342
|
+
|
|
343
|
+
// Match relative imports
|
|
344
|
+
const relativeImportRegex = /import\s+.*?\s+from\s+['"](\.[^'"]+)['"]/g;
|
|
345
|
+
let match;
|
|
346
|
+
|
|
347
|
+
while ((match = relativeImportRegex.exec(op.content)) !== null) {
|
|
348
|
+
const importPath = match[1];
|
|
349
|
+
const opDir = path.dirname(op.path);
|
|
350
|
+
const resolvedPath = path.join(this.workspaceRoot, opDir, importPath);
|
|
351
|
+
|
|
352
|
+
// Check if file exists (try various extensions)
|
|
353
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];
|
|
354
|
+
const exists = extensions.some(ext => {
|
|
355
|
+
const fullPath = resolvedPath + ext;
|
|
356
|
+
return fs.existsSync(fullPath);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Also check if it's being created in this batch
|
|
360
|
+
const isBeingCreated = importPath.replace(/^\.\//, '').replace(/^\.\.\//, '');
|
|
361
|
+
// Skip validation for imports that might be to other new files
|
|
362
|
+
|
|
363
|
+
if (!exists) {
|
|
364
|
+
// This might be a new file being created, so just warn
|
|
365
|
+
// Don't add as error since we might be creating multiple files
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return errors;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check for problematic type usage
|
|
374
|
+
*/
|
|
375
|
+
private checkTypeUsage(op: FileOperation): ValidationWarning[] {
|
|
376
|
+
const warnings: ValidationWarning[] = [];
|
|
377
|
+
if (!op.content) return warnings;
|
|
378
|
+
|
|
379
|
+
// Check for excessive 'any' usage
|
|
380
|
+
const anyMatches = op.content.match(/:\s*any\b/g) || [];
|
|
381
|
+
if (anyMatches.length > 2) {
|
|
382
|
+
warnings.push({
|
|
383
|
+
file: op.path,
|
|
384
|
+
message: `Found ${anyMatches.length} uses of 'any' type - consider proper typing`,
|
|
385
|
+
type: 'any-type'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return warnings;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check for security issues
|
|
394
|
+
*/
|
|
395
|
+
private checkSecurity(op: FileOperation): ValidationError[] {
|
|
396
|
+
const errors: ValidationError[] = [];
|
|
397
|
+
if (!op.content) return errors;
|
|
398
|
+
|
|
399
|
+
// Check for hardcoded secrets
|
|
400
|
+
const secretPatterns = [
|
|
401
|
+
{ pattern: /['"]sk-[a-zA-Z0-9]{20,}['"]/, name: 'OpenAI API key' },
|
|
402
|
+
{ pattern: /['"]sk_live_[a-zA-Z0-9]+['"]/, name: 'Stripe live key' },
|
|
403
|
+
{ pattern: /['"]ghp_[a-zA-Z0-9]+['"]/, name: 'GitHub token' },
|
|
404
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]+['"]/, name: 'Hardcoded password' }
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
for (const { pattern, name } of secretPatterns) {
|
|
408
|
+
if (pattern.test(op.content)) {
|
|
409
|
+
errors.push({
|
|
410
|
+
file: op.path,
|
|
411
|
+
message: `Potential ${name} found in code - use environment variables`,
|
|
412
|
+
type: 'security'
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check client-side files for server secrets
|
|
418
|
+
if (op.path.includes('/components/') || op.path.includes('/app/') && !op.path.includes('/api/')) {
|
|
419
|
+
if (/process\.env\.((?!NEXT_PUBLIC_)[A-Z_]+)/.test(op.content)) {
|
|
420
|
+
errors.push({
|
|
421
|
+
file: op.path,
|
|
422
|
+
message: 'Server-side env var accessed in client component - use NEXT_PUBLIC_ prefix or move to API route',
|
|
423
|
+
type: 'security'
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return errors;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check for best practices
|
|
433
|
+
*/
|
|
434
|
+
private checkBestPractices(op: FileOperation): ValidationWarning[] {
|
|
435
|
+
const warnings: ValidationWarning[] = [];
|
|
436
|
+
if (!op.content) return warnings;
|
|
437
|
+
|
|
438
|
+
// Check for console.log in production code
|
|
439
|
+
if (!/\.test\.|\.spec\./.test(op.path)) {
|
|
440
|
+
const consoleCount = (op.content.match(/console\.(log|debug|info)\(/g) || []).length;
|
|
441
|
+
if (consoleCount > 0) {
|
|
442
|
+
warnings.push({
|
|
443
|
+
file: op.path,
|
|
444
|
+
message: `Found ${consoleCount} console.log statement(s) - remove before production`,
|
|
445
|
+
type: 'console-log'
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check async functions have error handling
|
|
451
|
+
if (op.content.includes('async ') && op.content.includes('await ')) {
|
|
452
|
+
if (!op.content.includes('try') && !op.content.includes('catch')) {
|
|
453
|
+
warnings.push({
|
|
454
|
+
file: op.path,
|
|
455
|
+
message: 'Async function without try/catch error handling',
|
|
456
|
+
type: 'missing-error-handling'
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check API routes have proper error responses
|
|
462
|
+
if (op.path.includes('/api/')) {
|
|
463
|
+
if (!op.content.includes('catch') && !op.content.includes('error')) {
|
|
464
|
+
warnings.push({
|
|
465
|
+
file: op.path,
|
|
466
|
+
message: 'API route may lack error handling',
|
|
467
|
+
type: 'missing-error-handling'
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return warnings;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Suggest using existing types instead of creating new ones
|
|
477
|
+
*/
|
|
478
|
+
private suggestExistingTypes(fileOperations: FileOperation[]): string[] {
|
|
479
|
+
const suggestions: string[] = [];
|
|
480
|
+
if (!this.typeInventory) return suggestions;
|
|
481
|
+
|
|
482
|
+
for (const op of fileOperations) {
|
|
483
|
+
if (!op.content) continue;
|
|
484
|
+
|
|
485
|
+
// Look for new interface/type definitions
|
|
486
|
+
const newTypeRegex = /(?:interface|type)\s+(\w+)/g;
|
|
487
|
+
let match;
|
|
488
|
+
while ((match = newTypeRegex.exec(op.content)) !== null) {
|
|
489
|
+
const typeName = match[1];
|
|
490
|
+
|
|
491
|
+
// Check if similar type already exists
|
|
492
|
+
if (this.typeInventory.types.has(typeName)) {
|
|
493
|
+
const existing = this.typeInventory.types.get(typeName)!;
|
|
494
|
+
suggestions.push(
|
|
495
|
+
`Type '${typeName}' already exists in ${existing.file} - consider importing instead of redefining`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return suggestions;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get inventory of existing types for AI context
|
|
506
|
+
*/
|
|
507
|
+
getTypeInventoryForContext(): string {
|
|
508
|
+
if (!this.typeInventory || this.typeInventory.types.size === 0) {
|
|
509
|
+
return 'No existing types found';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const lines: string[] = ['Existing types in project:'];
|
|
513
|
+
|
|
514
|
+
// Group by file
|
|
515
|
+
const byFile = new Map<string, TypeInfo[]>();
|
|
516
|
+
for (const [, info] of this.typeInventory.types) {
|
|
517
|
+
const types = byFile.get(info.file) || [];
|
|
518
|
+
types.push(info);
|
|
519
|
+
byFile.set(info.file, types);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Output grouped by file
|
|
523
|
+
for (const [file, types] of byFile) {
|
|
524
|
+
if (types.length > 0) {
|
|
525
|
+
lines.push(` ${file}:`);
|
|
526
|
+
for (const t of types.slice(0, 10)) { // Limit per file
|
|
527
|
+
lines.push(` - ${t.kind} ${t.name}`);
|
|
528
|
+
}
|
|
529
|
+
if (types.length > 10) {
|
|
530
|
+
lines.push(` ... and ${types.length - 10} more`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return lines.slice(0, 50).join('\n'); // Limit total output
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get installed packages for AI context
|
|
540
|
+
*/
|
|
541
|
+
getInstalledPackagesForContext(): string[] {
|
|
542
|
+
return Array.from(this.installedPackages).slice(0, 50); // Limit for context
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Run TypeScript compile check on the project
|
|
547
|
+
* This catches type errors that the AI might introduce
|
|
548
|
+
*/
|
|
549
|
+
async runTypeScriptCheck(): Promise<TypeScriptCheckResult> {
|
|
550
|
+
if (!this.workspaceRoot) {
|
|
551
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Check if tsconfig.json exists
|
|
555
|
+
const tsconfigPath = path.join(this.workspaceRoot, 'tsconfig.json');
|
|
556
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
557
|
+
console.log('CodeValidator: No tsconfig.json found, skipping TypeScript check');
|
|
558
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
// Run tsc --noEmit to check for type errors without emitting
|
|
563
|
+
await execAsync('npx tsc --noEmit', {
|
|
564
|
+
cwd: this.workspaceRoot,
|
|
565
|
+
timeout: 60000 // 60 second timeout
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// If we get here, no errors
|
|
569
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
570
|
+
} catch (error: any) {
|
|
571
|
+
// Parse TypeScript errors from stdout/stderr
|
|
572
|
+
const output = error.stdout || error.stderr || '';
|
|
573
|
+
const errors = this.parseTypeScriptErrors(output);
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
passed: false,
|
|
577
|
+
errors,
|
|
578
|
+
errorCount: errors.length
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Parse TypeScript compiler errors from output
|
|
585
|
+
*/
|
|
586
|
+
private parseTypeScriptErrors(output: string): TypeScriptCheckResult['errors'] {
|
|
587
|
+
const errors: TypeScriptCheckResult['errors'] = [];
|
|
588
|
+
const lines = output.split('\n');
|
|
589
|
+
|
|
590
|
+
// TypeScript error format: src/file.ts(10,5): error TS2339: Property 'x' does not exist
|
|
591
|
+
const errorRegex = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
|
|
592
|
+
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
const match = line.match(errorRegex);
|
|
595
|
+
if (match) {
|
|
596
|
+
errors.push({
|
|
597
|
+
file: match[1],
|
|
598
|
+
line: parseInt(match[2], 10),
|
|
599
|
+
column: parseInt(match[3], 10),
|
|
600
|
+
code: match[4],
|
|
601
|
+
message: match[5]
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Limit to first 20 errors to avoid overwhelming output
|
|
607
|
+
return errors.slice(0, 20);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Quick TypeScript check - only checks specific files
|
|
612
|
+
* Faster than full project check, good for validating just the generated code
|
|
613
|
+
*/
|
|
614
|
+
async checkSpecificFiles(filePaths: string[]): Promise<TypeScriptCheckResult> {
|
|
615
|
+
if (!this.workspaceRoot || filePaths.length === 0) {
|
|
616
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Filter to only TypeScript files
|
|
620
|
+
const tsFiles = filePaths.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
621
|
+
if (tsFiles.length === 0) {
|
|
622
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
// Check specific files with tsc
|
|
627
|
+
const filesArg = tsFiles.map(f => `"${f}"`).join(' ');
|
|
628
|
+
await execAsync(`npx tsc --noEmit ${filesArg}`, {
|
|
629
|
+
cwd: this.workspaceRoot,
|
|
630
|
+
timeout: 30000 // 30 second timeout for specific files
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
return { passed: true, errors: [], errorCount: 0 };
|
|
634
|
+
} catch (error: any) {
|
|
635
|
+
const output = error.stdout || error.stderr || '';
|
|
636
|
+
const errors = this.parseTypeScriptErrors(output);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
passed: false,
|
|
640
|
+
errors,
|
|
641
|
+
errorCount: errors.length
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|