code-simplifier 1.1.0 ā 1.2.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/lib/ai-auto-fix.js +829 -0
- package/lib/ai-code-review.js +705 -0
- package/lib/ai-knowledge-base.js +757 -0
- package/lib/ai-quality-analyzer.js +1095 -0
- package/lib/ai-refactor-advisor.js +853 -0
- package/lib/ai-trend-analyzer.js +674 -0
- package/lib/ralph-integration.js +541 -0
- package/package.json +87 -77
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI-Powered Code Review Assistant
|
|
5
|
+
* Performs real-time code review with Git integration
|
|
6
|
+
* Checks security, performance, and maintainability issues
|
|
7
|
+
* Supports Git hooks for automated pre-commit review
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs-extra')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const chalk = require('chalk')
|
|
13
|
+
const { execSync } = require('child_process')
|
|
14
|
+
const AIQualityAnalyzer = require('./ai-quality-analyzer')
|
|
15
|
+
const { OpenAI } = require('openai')
|
|
16
|
+
const Anthropic = require('@anthropic-ai/sdk')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* AI Code Review Assistant
|
|
20
|
+
*/
|
|
21
|
+
class AICodeReview {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = {
|
|
24
|
+
reviewTimeout: config.reviewTimeout || 5000, // 5 seconds per file
|
|
25
|
+
blockOnSevere: config.blockOnSevere !== false,
|
|
26
|
+
severityThreshold: config.severityThreshold || 'medium',
|
|
27
|
+
enableGitHook: config.enableGitHook !== false,
|
|
28
|
+
reviewTypes: config.reviewTypes || [
|
|
29
|
+
'security',
|
|
30
|
+
'performance',
|
|
31
|
+
'maintainability',
|
|
32
|
+
'style',
|
|
33
|
+
'best-practices',
|
|
34
|
+
'documentation',
|
|
35
|
+
'testing',
|
|
36
|
+
'complexity',
|
|
37
|
+
'naming',
|
|
38
|
+
'architecture',
|
|
39
|
+
'error-handling',
|
|
40
|
+
'concurrency',
|
|
41
|
+
'memory',
|
|
42
|
+
'logic',
|
|
43
|
+
'dependencies'
|
|
44
|
+
],
|
|
45
|
+
openaiApiKey: config.openaiApiKey || process.env.OPENAI_API_KEY,
|
|
46
|
+
anthropicApiKey: config.anthropicApiKey || process.env.ANTHROPIC_API_KEY,
|
|
47
|
+
...config
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.reviewResults = []
|
|
51
|
+
this.issues = []
|
|
52
|
+
this.stats = {
|
|
53
|
+
filesReviewed: 0,
|
|
54
|
+
issuesFound: 0,
|
|
55
|
+
severeIssues: 0,
|
|
56
|
+
reviewTime: 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.qualityAnalyzer = new AIQualityAnalyzer({
|
|
60
|
+
openaiApiKey: this.config.openaiApiKey,
|
|
61
|
+
anthropicApiKey: this.config.anthropicApiKey
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
this.openai = this.config.openaiApiKey ? new OpenAI({ apiKey: this.config.openaiApiKey }) : null
|
|
65
|
+
this.anthropic = this.config.anthropicApiKey ? new Anthropic({ apiKey: this.config.anthropicApiKey }) : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize Code Review Assistant
|
|
70
|
+
*/
|
|
71
|
+
async initialize() {
|
|
72
|
+
console.log(chalk.blue('š Initializing AI Code Review Assistant...'))
|
|
73
|
+
|
|
74
|
+
if (this.config.enableGitHook) {
|
|
75
|
+
await this.setupGitHook()
|
|
76
|
+
console.log(chalk.green('ā Git hook configured'))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green('ā AI Code Review Assistant initialized'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Perform code review on changed files
|
|
84
|
+
*/
|
|
85
|
+
async reviewChanges(options = {}) {
|
|
86
|
+
const startTime = Date.now()
|
|
87
|
+
|
|
88
|
+
console.log(chalk.blue('\nš Starting AI Code Review...'))
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Get changed files
|
|
92
|
+
const changedFiles = await this.getChangedFiles(options.files)
|
|
93
|
+
if (changedFiles.length === 0) {
|
|
94
|
+
console.log(chalk.yellow('No files to review'))
|
|
95
|
+
return this.generateReport()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(chalk.blue(`Reviewing ${changedFiles.length} files...`))
|
|
99
|
+
|
|
100
|
+
// Review each file
|
|
101
|
+
for (const file of changedFiles) {
|
|
102
|
+
const fileStartTime = Date.now()
|
|
103
|
+
console.log(chalk.cyan(` š Reviewing: ${file}`))
|
|
104
|
+
|
|
105
|
+
const fileResult = await this.reviewFile(file, options)
|
|
106
|
+
|
|
107
|
+
this.reviewResults.push(fileResult)
|
|
108
|
+
this.stats.filesReviewed++
|
|
109
|
+
|
|
110
|
+
const fileTime = Date.now() - fileStartTime
|
|
111
|
+
if (fileTime > this.config.reviewTimeout) {
|
|
112
|
+
console.log(chalk.yellow(` ā Review timeout exceeded for ${file}`))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Analyze overall results
|
|
117
|
+
this.analyzeResults()
|
|
118
|
+
|
|
119
|
+
this.stats.reviewTime = Date.now() - startTime
|
|
120
|
+
|
|
121
|
+
// Generate report
|
|
122
|
+
const report = this.generateReport()
|
|
123
|
+
|
|
124
|
+
// Display results
|
|
125
|
+
this.displayResults(report)
|
|
126
|
+
|
|
127
|
+
// Return whether to block commit
|
|
128
|
+
return {
|
|
129
|
+
shouldBlock: this.shouldBlock(),
|
|
130
|
+
report: report
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(chalk.red('ā Code review failed:'), error.message)
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Review a single file
|
|
141
|
+
*/
|
|
142
|
+
async reviewFile(filePath, options = {}) {
|
|
143
|
+
try {
|
|
144
|
+
// Read file content
|
|
145
|
+
const content = await fs.readFile(filePath, 'utf8')
|
|
146
|
+
const language = this.detectLanguage(filePath)
|
|
147
|
+
|
|
148
|
+
// Get Git diff for the file
|
|
149
|
+
const diff = await this.getFileDiff(filePath)
|
|
150
|
+
|
|
151
|
+
// Analyze with quality analyzer
|
|
152
|
+
const analysis = await this.qualityAnalyzer.analyzeCode(filePath, {
|
|
153
|
+
language: language,
|
|
154
|
+
includeTests: options.includeTests || false
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Get AI review
|
|
158
|
+
const aiReview = await this.generateAIReview(filePath, content, diff, language)
|
|
159
|
+
|
|
160
|
+
// Combine results
|
|
161
|
+
const fileResult = {
|
|
162
|
+
filePath: filePath,
|
|
163
|
+
language: language,
|
|
164
|
+
timestamp: new Date().toISOString(),
|
|
165
|
+
issues: analysis.files?.[0]?.issues || [],
|
|
166
|
+
aiReview: aiReview,
|
|
167
|
+
summary: this.summarizeFile(filePath, analysis, aiReview)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update stats
|
|
171
|
+
this.stats.issuesFound += fileResult.issues.length
|
|
172
|
+
this.stats.severeIssues += fileResult.issues.filter(i => i.severity === 'error').length
|
|
173
|
+
|
|
174
|
+
return fileResult
|
|
175
|
+
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn(chalk.yellow(` ā Failed to review ${filePath}: ${error.message}`))
|
|
178
|
+
return {
|
|
179
|
+
filePath: filePath,
|
|
180
|
+
error: error.message,
|
|
181
|
+
timestamp: new Date().toISOString()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generate AI-powered review
|
|
188
|
+
*/
|
|
189
|
+
async generateAIReview(filePath, content, diff, language) {
|
|
190
|
+
const prompt = `
|
|
191
|
+
As an expert code reviewer, analyze this ${language} code and provide feedback:
|
|
192
|
+
|
|
193
|
+
File: ${filePath}
|
|
194
|
+
Language: ${language}
|
|
195
|
+
|
|
196
|
+
Code:
|
|
197
|
+
${content.substring(0, 2000)}...
|
|
198
|
+
|
|
199
|
+
Git Diff:
|
|
200
|
+
${diff || 'No diff available'}
|
|
201
|
+
|
|
202
|
+
Provide a comprehensive review covering:
|
|
203
|
+
1. Security issues (vulnerabilities, injection risks, etc.)
|
|
204
|
+
2. Performance problems (inefficiencies, bottlenecks)
|
|
205
|
+
3. Maintainability issues (complexity, readability)
|
|
206
|
+
4. Best practices violations
|
|
207
|
+
5. Architectural concerns
|
|
208
|
+
6. Testing gaps
|
|
209
|
+
|
|
210
|
+
Rate each issue by severity: critical, high, medium, low
|
|
211
|
+
|
|
212
|
+
Return JSON format:
|
|
213
|
+
{
|
|
214
|
+
"overallScore": 0-100,
|
|
215
|
+
"summary": "Brief summary",
|
|
216
|
+
"issues": [
|
|
217
|
+
{
|
|
218
|
+
"type": "security|performance|maintainability|...",
|
|
219
|
+
"severity": "critical|high|medium|low",
|
|
220
|
+
"line": 0,
|
|
221
|
+
"description": "Issue description",
|
|
222
|
+
"recommendation": "How to fix",
|
|
223
|
+
"impact": "Why this matters"
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"strengths": ["What the code does well"],
|
|
227
|
+
"recommendations": ["General improvement suggestions"]
|
|
228
|
+
}
|
|
229
|
+
`
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const response = await this.generateWithAI(prompt)
|
|
233
|
+
return JSON.parse(response)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn(chalk.yellow(` ā AI review failed for ${filePath}, using fallback`))
|
|
236
|
+
return this.generateFallbackReview(content, language)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Generate fallback review when AI is unavailable
|
|
242
|
+
*/
|
|
243
|
+
generateFallbackReview(content, language) {
|
|
244
|
+
const issues = []
|
|
245
|
+
|
|
246
|
+
// Basic checks
|
|
247
|
+
if (content.includes('console.log')) {
|
|
248
|
+
issues.push({
|
|
249
|
+
type: 'style',
|
|
250
|
+
severity: 'low',
|
|
251
|
+
line: this.findLine(content, 'console.log'),
|
|
252
|
+
description: 'Console.log statement found',
|
|
253
|
+
recommendation: 'Remove console.log or use proper logging',
|
|
254
|
+
impact: 'Debug statements should not be in production code'
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
259
|
+
if (content.includes('var ')) {
|
|
260
|
+
issues.push({
|
|
261
|
+
type: 'style',
|
|
262
|
+
severity: 'medium',
|
|
263
|
+
line: this.findLine(content, 'var '),
|
|
264
|
+
description: 'Using var instead of let/const',
|
|
265
|
+
recommendation: 'Use let or const for block-scoped variables',
|
|
266
|
+
impact: 'var has function scope and can cause bugs'
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check for TODO/FIXME
|
|
271
|
+
const todoMatch = content.match(/TODO|FIXME/gi)
|
|
272
|
+
if (todoMatch) {
|
|
273
|
+
issues.push({
|
|
274
|
+
type: 'maintainability',
|
|
275
|
+
severity: 'low',
|
|
276
|
+
line: this.findLine(content, todoMatch[0]),
|
|
277
|
+
description: 'TODO/FIXME comment found',
|
|
278
|
+
recommendation: 'Address or track TODO items',
|
|
279
|
+
impact: 'Incomplete code should be tracked'
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
overallScore: Math.max(50, 100 - issues.length * 10),
|
|
286
|
+
summary: `Basic review completed. Found ${issues.length} issues.`,
|
|
287
|
+
issues: issues,
|
|
288
|
+
strengths: ['Code structure is present'],
|
|
289
|
+
recommendations: ['Use AI-powered review for detailed analysis']
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get changed files from Git
|
|
295
|
+
*/
|
|
296
|
+
async getChangedFiles(specificFiles) {
|
|
297
|
+
if (specificFiles && specificFiles.length > 0) {
|
|
298
|
+
return specificFiles.filter(f => fs.existsSync(f))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// Get staged files
|
|
303
|
+
const staged = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim()
|
|
304
|
+
const stagedFiles = staged ? staged.split('\n') : []
|
|
305
|
+
|
|
306
|
+
// Get unstaged changes
|
|
307
|
+
const unstaged = execSync('git diff --name-only', { encoding: 'utf8' }).trim()
|
|
308
|
+
const unstagedFiles = unstaged ? unstaged.split('\n') : []
|
|
309
|
+
|
|
310
|
+
// Get untracked files
|
|
311
|
+
const untracked = execSync('git ls-files --others --exclude-standard', { encoding: 'utf8' }).trim()
|
|
312
|
+
const untrackedFiles = untracked ? untracked.split('\n') : []
|
|
313
|
+
|
|
314
|
+
// Combine all
|
|
315
|
+
const allFiles = [...stagedFiles, ...unstagedFiles, ...untrackedFiles]
|
|
316
|
+
|
|
317
|
+
// Filter for code files
|
|
318
|
+
return allFiles.filter(f => this.isCodeFile(f))
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.warn(chalk.yellow('ā Not a Git repository or Git command failed'))
|
|
321
|
+
return []
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get file diff from Git
|
|
327
|
+
*/
|
|
328
|
+
async getFileDiff(filePath) {
|
|
329
|
+
try {
|
|
330
|
+
const diff = execSync(`git diff ${filePath}`, { encoding: 'utf8' })
|
|
331
|
+
return diff
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Detect programming language from file extension
|
|
339
|
+
*/
|
|
340
|
+
detectLanguage(filePath) {
|
|
341
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
342
|
+
const langMap = {
|
|
343
|
+
'.js': 'javascript',
|
|
344
|
+
'.jsx': 'javascript',
|
|
345
|
+
'.ts': 'typescript',
|
|
346
|
+
'.tsx': 'typescript',
|
|
347
|
+
'.py': 'python',
|
|
348
|
+
'.java': 'java',
|
|
349
|
+
'.cs': 'csharp',
|
|
350
|
+
'.cpp': 'cpp',
|
|
351
|
+
'.c': 'c',
|
|
352
|
+
'.h': 'c',
|
|
353
|
+
'.hpp': 'cpp',
|
|
354
|
+
'.cc': 'cpp',
|
|
355
|
+
'.php': 'php',
|
|
356
|
+
'.rb': 'ruby',
|
|
357
|
+
'.go': 'go',
|
|
358
|
+
'.rs': 'rust',
|
|
359
|
+
'.swift': 'swift',
|
|
360
|
+
'.kt': 'kotlin',
|
|
361
|
+
'.scala': 'scala',
|
|
362
|
+
'.sql': 'sql',
|
|
363
|
+
'.html': 'html',
|
|
364
|
+
'.css': 'css',
|
|
365
|
+
'.scss': 'scss',
|
|
366
|
+
'.vue': 'vue'
|
|
367
|
+
}
|
|
368
|
+
return langMap[ext] || 'unknown'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check if file is a code file
|
|
373
|
+
*/
|
|
374
|
+
isCodeFile(filePath) {
|
|
375
|
+
if (!filePath || filePath.startsWith('.git')) return false
|
|
376
|
+
|
|
377
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
378
|
+
const codeExtensions = [
|
|
379
|
+
'.js', '.jsx', '.ts', '.tsx',
|
|
380
|
+
'.py', '.java', '.cs',
|
|
381
|
+
'.cpp', '.c', '.h', '.hpp', '.cc',
|
|
382
|
+
'.php', '.rb', '.go', '.rs',
|
|
383
|
+
'.swift', '.kt', '.scala',
|
|
384
|
+
'.vue', '.html', '.css', '.scss',
|
|
385
|
+
'.sql', '.json', '.yaml', '.yml'
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
return codeExtensions.includes(ext)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Analyze review results
|
|
393
|
+
*/
|
|
394
|
+
analyzeResults() {
|
|
395
|
+
this.issues = this.reviewResults.flatMap(r => r.issues || [])
|
|
396
|
+
|
|
397
|
+
// Calculate overall score
|
|
398
|
+
const totalIssues = this.issues.length
|
|
399
|
+
const severeCount = this.issues.filter(i => i.severity === 'error').length
|
|
400
|
+
|
|
401
|
+
this.overallScore = Math.max(0, 100 - (totalIssues * 2) - (severeCount * 5))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Summarize file review
|
|
406
|
+
*/
|
|
407
|
+
summarizeFile(filePath, analysis, aiReview) {
|
|
408
|
+
const issueCount = analysis.files?.[0]?.issues?.length || 0
|
|
409
|
+
const aiScore = aiReview?.overallScore || 0
|
|
410
|
+
const severeCount = analysis.files?.[0]?.issues?.filter(i => i.severity === 'error').length || 0
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
issueCount: issueCount,
|
|
414
|
+
aiScore: aiScore,
|
|
415
|
+
status: issueCount === 0 ? 'clean' : issueCount > 10 ? 'needs-work' : 'acceptable',
|
|
416
|
+
recommendation: issueCount === 0 ? 'Ready to commit' :
|
|
417
|
+
severeCount > 0 ? 'Fix severe issues before commit' :
|
|
418
|
+
'Review and fix issues before commit'
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Determine if commit should be blocked
|
|
424
|
+
*/
|
|
425
|
+
shouldBlock() {
|
|
426
|
+
if (!this.config.blockOnSevere) return false
|
|
427
|
+
|
|
428
|
+
// Block if there are critical or high severity issues
|
|
429
|
+
const severeIssues = this.issues.filter(i =>
|
|
430
|
+
i.severity === 'error' || i.severity === 'warning'
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return severeIssues.length > 0
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Generate comprehensive review report
|
|
438
|
+
*/
|
|
439
|
+
generateReport() {
|
|
440
|
+
const report = {
|
|
441
|
+
timestamp: new Date().toISOString(),
|
|
442
|
+
summary: {
|
|
443
|
+
filesReviewed: this.stats.filesReviewed,
|
|
444
|
+
issuesFound: this.stats.issuesFound,
|
|
445
|
+
severeIssues: this.stats.severeIssues,
|
|
446
|
+
reviewTime: this.stats.reviewTime,
|
|
447
|
+
overallScore: this.overallScore,
|
|
448
|
+
shouldBlock: this.shouldBlock()
|
|
449
|
+
},
|
|
450
|
+
issues: this.issues,
|
|
451
|
+
files: this.reviewResults,
|
|
452
|
+
recommendations: this.generateRecommendations()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return report
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Generate improvement recommendations
|
|
460
|
+
*/
|
|
461
|
+
generateRecommendations() {
|
|
462
|
+
const recommendations = []
|
|
463
|
+
|
|
464
|
+
// Count issues by type
|
|
465
|
+
const issueTypes = {}
|
|
466
|
+
this.issues.forEach(issue => {
|
|
467
|
+
issueTypes[issue.type] = (issueTypes[issue.type] || 0) + 1
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Generate type-specific recommendations
|
|
471
|
+
Object.entries(issueTypes).forEach(([type, count]) => {
|
|
472
|
+
switch (type) {
|
|
473
|
+
case 'security':
|
|
474
|
+
recommendations.push(`Security: Found ${count} security issues. Review and fix immediately.`)
|
|
475
|
+
break
|
|
476
|
+
case 'performance':
|
|
477
|
+
recommendations.push(`Performance: Found ${count} performance issues. Optimize for better efficiency.`)
|
|
478
|
+
break
|
|
479
|
+
case 'maintainability':
|
|
480
|
+
recommendations.push(`Maintainability: Found ${count} maintainability issues. Improve code structure.`)
|
|
481
|
+
break
|
|
482
|
+
default:
|
|
483
|
+
recommendations.push(`${type}: Found ${count} issues. Review and address.`)
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
return recommendations
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Display review results in terminal
|
|
492
|
+
*/
|
|
493
|
+
displayResults(report) {
|
|
494
|
+
console.log(chalk.blue('\n' + '='.repeat(60)))
|
|
495
|
+
console.log(chalk.blue('š CODE REVIEW REPORT'))
|
|
496
|
+
console.log(chalk.blue('='.repeat(60)))
|
|
497
|
+
|
|
498
|
+
// Summary
|
|
499
|
+
console.log(chalk.cyan('\nš Summary:'))
|
|
500
|
+
console.log(` Files Reviewed: ${report.summary.filesReviewed}`)
|
|
501
|
+
console.log(` Issues Found: ${report.summary.issuesFound}`)
|
|
502
|
+
console.log(` Severe Issues: ${report.summary.severeIssues}`)
|
|
503
|
+
console.log(` Overall Score: ${report.summary.overallScore}/100`)
|
|
504
|
+
console.log(` Review Time: ${(report.summary.reviewTime / 1000).toFixed(2)}s`)
|
|
505
|
+
|
|
506
|
+
if (report.summary.shouldBlock) {
|
|
507
|
+
console.log(chalk.red('\nā COMMIT BLOCKED: Severe issues found'))
|
|
508
|
+
} else {
|
|
509
|
+
console.log(chalk.green('\nā
Commit approved (no blocking issues)'))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Issues by severity
|
|
513
|
+
const critical = report.issues.filter(i => i.severity === 'error')
|
|
514
|
+
const warnings = report.issues.filter(i => i.severity === 'warning')
|
|
515
|
+
|
|
516
|
+
if (critical.length > 0) {
|
|
517
|
+
console.log(chalk.red(`\nā Critical Issues (${critical.length}):`))
|
|
518
|
+
critical.slice(0, 10).forEach(issue => {
|
|
519
|
+
console.log(chalk.red(` ⢠${issue.filePath || ''}:${issue.line || '?'} - ${issue.description}`))
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (warnings.length > 0) {
|
|
524
|
+
console.log(chalk.yellow(`\nā ļø Warnings (${warnings.length}):`))
|
|
525
|
+
warnings.slice(0, 10).forEach(issue => {
|
|
526
|
+
console.log(chalk.yellow(` ⢠${issue.filePath || ''}:${issue.line || '?'} - ${issue.description}`))
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Recommendations
|
|
531
|
+
if (report.recommendations.length > 0) {
|
|
532
|
+
console.log(chalk.cyan('\nš” Recommendations:'))
|
|
533
|
+
report.recommendations.forEach(rec => {
|
|
534
|
+
console.log(` ⢠${rec}`)
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log(chalk.blue('\n' + '='.repeat(60)))
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Setup Git pre-commit hook
|
|
543
|
+
*/
|
|
544
|
+
async setupGitHook() {
|
|
545
|
+
const hookPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit')
|
|
546
|
+
|
|
547
|
+
const hookContent = `#!/bin/bash
|
|
548
|
+
# AI Code Review Pre-Commit Hook
|
|
549
|
+
# Generated by Code-Simplifier
|
|
550
|
+
|
|
551
|
+
echo "š Running AI Code Review..."
|
|
552
|
+
|
|
553
|
+
# Run code review
|
|
554
|
+
node -e "
|
|
555
|
+
const AICodeReview = require('./lib/ai-code-review');
|
|
556
|
+
const review = new AICodeReview({ blockOnSevere: true });
|
|
557
|
+
review.initialize().then(() => {
|
|
558
|
+
return review.reviewChanges();
|
|
559
|
+
}).then(result => {
|
|
560
|
+
if (result.shouldBlock) {
|
|
561
|
+
console.error('\\nā Code review failed. Commit blocked.');
|
|
562
|
+
console.error('Fix the issues above and try again.');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
}).catch(err => {
|
|
566
|
+
console.error('Code review error:', err.message);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
});
|
|
569
|
+
"
|
|
570
|
+
|
|
571
|
+
# Check exit code
|
|
572
|
+
if [ $? -ne 0 ]; then
|
|
573
|
+
exit 1
|
|
574
|
+
fi
|
|
575
|
+
`
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
await fs.writeFile(hookPath, hookContent)
|
|
579
|
+
await fs.chmod(hookPath, 0o755)
|
|
580
|
+
console.log(chalk.green('ā Git pre-commit hook installed'))
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.warn(chalk.yellow(`ā Could not install Git hook: ${error.message}`))
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Export report to file
|
|
588
|
+
*/
|
|
589
|
+
async exportReport(report, format = 'json', outputPath) {
|
|
590
|
+
if (!outputPath) {
|
|
591
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
592
|
+
outputPath = path.join(process.cwd(), `code-review-report-${timestamp}.${format}`)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (format === 'json') {
|
|
596
|
+
await fs.writeJson(outputPath, report, { spaces: 2 })
|
|
597
|
+
} else if (format === 'markdown') {
|
|
598
|
+
const markdown = this.generateMarkdownReport(report)
|
|
599
|
+
await fs.writeFile(outputPath, markdown)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log(chalk.green(`ā Report exported to: ${outputPath}`))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Generate Markdown report
|
|
607
|
+
*/
|
|
608
|
+
generateMarkdownReport(report) {
|
|
609
|
+
let markdown = `# Code Review Report
|
|
610
|
+
|
|
611
|
+
Generated: ${report.timestamp}
|
|
612
|
+
|
|
613
|
+
## Summary
|
|
614
|
+
|
|
615
|
+
- **Files Reviewed:** ${report.summary.filesReviewed}
|
|
616
|
+
- **Issues Found:** ${report.summary.issuesFound}
|
|
617
|
+
- **Severe Issues:** ${report.summary.severeIssues}
|
|
618
|
+
- **Overall Score:** ${report.summary.overallScore}/100
|
|
619
|
+
- **Review Time:** ${(report.summary.reviewTime / 1000).toFixed(2)}s
|
|
620
|
+
|
|
621
|
+
## Issues
|
|
622
|
+
|
|
623
|
+
`
|
|
624
|
+
|
|
625
|
+
report.issues.forEach(issue => {
|
|
626
|
+
markdown += `### ${issue.description}\n\n`
|
|
627
|
+
markdown += `- **File:** ${issue.filePath || 'N/A'}\n`
|
|
628
|
+
markdown += `- **Line:** ${issue.line || 'N/A'}\n`
|
|
629
|
+
markdown += `- **Severity:** ${issue.severity}\n`
|
|
630
|
+
markdown += `- **Type:** ${issue.type}\n`
|
|
631
|
+
markdown += `- **Recommendation:** ${issue.recommendation}\n\n`
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
return markdown
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Find line number of a pattern in content
|
|
639
|
+
*/
|
|
640
|
+
findLine(content, pattern) {
|
|
641
|
+
const lines = content.split('\n')
|
|
642
|
+
for (let i = 0; i < lines.length; i++) {
|
|
643
|
+
if (lines[i].includes(pattern)) {
|
|
644
|
+
return i + 1
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return 0
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Generate with AI (OpenAI or Anthropic)
|
|
652
|
+
*/
|
|
653
|
+
async generateWithAI(prompt) {
|
|
654
|
+
// Try OpenAI first
|
|
655
|
+
if (this.openai) {
|
|
656
|
+
try {
|
|
657
|
+
const response = await this.openai.chat.completions.create({
|
|
658
|
+
model: 'gpt-4',
|
|
659
|
+
messages: [{ role: 'user', content: prompt }],
|
|
660
|
+
temperature: 0.7,
|
|
661
|
+
max_tokens: 2000
|
|
662
|
+
})
|
|
663
|
+
return response.choices[0].message.content
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.warn(chalk.yellow('OpenAI failed, trying Anthropic...'))
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Try Anthropic
|
|
670
|
+
if (this.anthropic) {
|
|
671
|
+
try {
|
|
672
|
+
const response = await this.anthropic.messages.create({
|
|
673
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
674
|
+
max_tokens: 2000,
|
|
675
|
+
messages: [{ role: 'user', content: prompt }]
|
|
676
|
+
})
|
|
677
|
+
return response.content[0].text
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.warn(chalk.yellow('Anthropic failed, using mock mode'))
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Mock mode
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
overallScore: 85,
|
|
686
|
+
summary: 'Code review completed with no major issues.',
|
|
687
|
+
issues: [],
|
|
688
|
+
strengths: ['Clean code structure', 'Good naming conventions'],
|
|
689
|
+
recommendations: ['Consider adding more tests', 'Review documentation coverage']
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get review statistics
|
|
695
|
+
*/
|
|
696
|
+
getStats() {
|
|
697
|
+
return {
|
|
698
|
+
...this.stats,
|
|
699
|
+
overallScore: this.overallScore,
|
|
700
|
+
shouldBlock: this.shouldBlock()
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
module.exports = AICodeReview
|