codereview-aia 0.1.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/.cr-aia.yml +23 -0
- package/.crignore +0 -0
- package/dist/index.js +27 -0
- package/package.json +85 -0
- package/src/analysis/FindingsExtractor.ts +431 -0
- package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +267 -0
- package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +622 -0
- package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +430 -0
- package/src/analysis/ai-detection/core/AIDetectionEngine.ts +467 -0
- package/src/analysis/ai-detection/types/DetectionTypes.ts +406 -0
- package/src/analysis/ai-detection/utils/SubmissionConverter.ts +390 -0
- package/src/analysis/context/ReviewContext.ts +378 -0
- package/src/analysis/context/index.ts +7 -0
- package/src/analysis/index.ts +8 -0
- package/src/analysis/tokens/TokenAnalysisFormatter.ts +154 -0
- package/src/analysis/tokens/TokenAnalyzer.ts +747 -0
- package/src/analysis/tokens/index.ts +8 -0
- package/src/clients/base/abstractClient.ts +190 -0
- package/src/clients/base/httpClient.ts +160 -0
- package/src/clients/base/index.ts +12 -0
- package/src/clients/base/modelDetection.ts +107 -0
- package/src/clients/base/responseProcessor.ts +586 -0
- package/src/clients/factory/clientFactory.ts +55 -0
- package/src/clients/factory/index.ts +8 -0
- package/src/clients/implementations/index.ts +8 -0
- package/src/clients/implementations/openRouterClient.ts +411 -0
- package/src/clients/openRouterClient.ts +863 -0
- package/src/clients/openRouterClientWrapper.ts +44 -0
- package/src/clients/utils/directoryStructure.ts +52 -0
- package/src/clients/utils/index.ts +11 -0
- package/src/clients/utils/languageDetection.ts +44 -0
- package/src/clients/utils/promptFormatter.ts +105 -0
- package/src/clients/utils/promptLoader.ts +53 -0
- package/src/clients/utils/tokenCounter.ts +297 -0
- package/src/core/ApiClientSelector.ts +37 -0
- package/src/core/ConfigurationService.ts +591 -0
- package/src/core/ConsolidationService.ts +423 -0
- package/src/core/InteractiveDisplayManager.ts +81 -0
- package/src/core/OutputManager.ts +275 -0
- package/src/core/ReviewGenerator.ts +140 -0
- package/src/core/fileDiscovery.ts +237 -0
- package/src/core/handlers/EstimationHandler.ts +104 -0
- package/src/core/handlers/FileProcessingHandler.ts +204 -0
- package/src/core/handlers/OutputHandler.ts +125 -0
- package/src/core/handlers/ReviewExecutor.ts +104 -0
- package/src/core/reviewOrchestrator.ts +333 -0
- package/src/core/utils/ModelInfoUtils.ts +56 -0
- package/src/formatters/outputFormatter.ts +62 -0
- package/src/formatters/utils/IssueFormatters.ts +83 -0
- package/src/formatters/utils/JsonFormatter.ts +77 -0
- package/src/formatters/utils/MarkdownFormatters.ts +609 -0
- package/src/formatters/utils/MetadataFormatter.ts +269 -0
- package/src/formatters/utils/ModelInfoExtractor.ts +115 -0
- package/src/index.ts +27 -0
- package/src/plugins/PluginInterface.ts +50 -0
- package/src/plugins/PluginManager.ts +126 -0
- package/src/prompts/PromptManager.ts +69 -0
- package/src/prompts/cache/PromptCache.ts +50 -0
- package/src/prompts/promptText/common/variables/css-frameworks.json +33 -0
- package/src/prompts/promptText/common/variables/framework-versions.json +45 -0
- package/src/prompts/promptText/frameworks/react/comprehensive.hbs +19 -0
- package/src/prompts/promptText/languages/css/comprehensive.hbs +18 -0
- package/src/prompts/promptText/languages/generic/comprehensive.hbs +20 -0
- package/src/prompts/promptText/languages/html/comprehensive.hbs +18 -0
- package/src/prompts/promptText/languages/javascript/comprehensive.hbs +18 -0
- package/src/prompts/promptText/languages/python/comprehensive.hbs +18 -0
- package/src/prompts/promptText/languages/typescript/comprehensive.hbs +18 -0
- package/src/runtime/auth/service.ts +58 -0
- package/src/runtime/auth/session.ts +103 -0
- package/src/runtime/auth/types.ts +11 -0
- package/src/runtime/cliEntry.ts +196 -0
- package/src/runtime/errors.ts +13 -0
- package/src/runtime/fileCollector.ts +188 -0
- package/src/runtime/manifest.ts +64 -0
- package/src/runtime/openrouterProxy.ts +45 -0
- package/src/runtime/proxyConfig.ts +94 -0
- package/src/runtime/proxyEnvironment.ts +71 -0
- package/src/runtime/reportMerge.ts +102 -0
- package/src/runtime/reporting/markdownReportBuilder.ts +138 -0
- package/src/runtime/reporting/reportDataCollector.ts +234 -0
- package/src/runtime/reporting/summaryGenerator.ts +86 -0
- package/src/runtime/reviewPipeline.ts +155 -0
- package/src/runtime/runAiCodeReview.ts +153 -0
- package/src/runtime/runtimeConfig.ts +5 -0
- package/src/runtime/ui/Layout.tsx +57 -0
- package/src/runtime/ui/RuntimeApp.tsx +150 -0
- package/src/runtime/ui/inkModules.ts +73 -0
- package/src/runtime/ui/screens/AuthScreen.tsx +128 -0
- package/src/runtime/ui/screens/ModeSelection.tsx +52 -0
- package/src/runtime/ui/screens/ProgressScreen.tsx +55 -0
- package/src/runtime/ui/screens/ResultsScreen.tsx +76 -0
- package/src/strategies/ArchitecturalReviewStrategy.ts +54 -0
- package/src/strategies/CodingTestReviewStrategy.ts +920 -0
- package/src/strategies/ConsolidatedReviewStrategy.ts +59 -0
- package/src/strategies/ExtractPatternsReviewStrategy.ts +64 -0
- package/src/strategies/MultiPassReviewStrategy.ts +785 -0
- package/src/strategies/ReviewStrategy.ts +64 -0
- package/src/strategies/StrategyFactory.ts +79 -0
- package/src/strategies/index.ts +14 -0
- package/src/tokenizers/baseTokenizer.ts +61 -0
- package/src/tokenizers/gptTokenizer.ts +27 -0
- package/src/tokenizers/index.ts +8 -0
- package/src/types/apiResponses.ts +40 -0
- package/src/types/cli.ts +24 -0
- package/src/types/common.ts +39 -0
- package/src/types/configuration.ts +201 -0
- package/src/types/handlebars.d.ts +5 -0
- package/src/types/patch.d.ts +25 -0
- package/src/types/review.ts +294 -0
- package/src/types/reviewContext.d.ts +65 -0
- package/src/types/reviewSchema.ts +181 -0
- package/src/types/structuredReview.ts +167 -0
- package/src/types/tokenAnalysis.ts +56 -0
- package/src/utils/FileReader.ts +93 -0
- package/src/utils/FileWriter.ts +76 -0
- package/src/utils/PathGenerator.ts +97 -0
- package/src/utils/api/apiUtils.ts +14 -0
- package/src/utils/api/index.ts +1 -0
- package/src/utils/apiErrorHandler.ts +287 -0
- package/src/utils/ciDataCollector.ts +252 -0
- package/src/utils/codingTestConfigLoader.ts +466 -0
- package/src/utils/dependencies/aiDependencyAnalyzer.ts +454 -0
- package/src/utils/detection/frameworkDetector.ts +879 -0
- package/src/utils/detection/index.ts +10 -0
- package/src/utils/detection/projectTypeDetector.ts +518 -0
- package/src/utils/diagramGenerator.ts +206 -0
- package/src/utils/errorLogger.ts +60 -0
- package/src/utils/estimationUtils.ts +407 -0
- package/src/utils/fileFilters.ts +373 -0
- package/src/utils/fileSystem.ts +57 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/logger.ts +240 -0
- package/src/utils/pathValidator.ts +98 -0
- package/src/utils/priorityFilter.ts +59 -0
- package/src/utils/projectDocs.ts +189 -0
- package/src/utils/promptPaths.ts +29 -0
- package/src/utils/promptTemplateManager.ts +157 -0
- package/src/utils/review/consolidateReview.ts +553 -0
- package/src/utils/review/fixDisplay.ts +100 -0
- package/src/utils/review/fixImplementation.ts +61 -0
- package/src/utils/review/index.ts +36 -0
- package/src/utils/review/interactiveProcessing.ts +294 -0
- package/src/utils/review/progressTracker.ts +296 -0
- package/src/utils/review/reviewExtraction.ts +382 -0
- package/src/utils/review/types.ts +46 -0
- package/src/utils/reviewActionHandler.ts +18 -0
- package/src/utils/reviewParser.ts +253 -0
- package/src/utils/sanitizer.ts +238 -0
- package/src/utils/smartFileSelector.ts +255 -0
- package/src/utils/templateLoader.ts +514 -0
- package/src/utils/treeGenerator.ts +153 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +59 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Output manager module.
|
|
3
|
+
*
|
|
4
|
+
* This module is responsible for formatting and saving review outputs to files.
|
|
5
|
+
* It centralizes the logic for generating output paths and writing review results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { formatReviewOutput } from '../formatters/outputFormatter';
|
|
11
|
+
import type { FileInfo, ReviewOptions, ReviewResult } from '../types/review';
|
|
12
|
+
import { createAIDependencyAnalysis } from '../utils/dependencies/aiDependencyAnalyzer';
|
|
13
|
+
import { processDiagrams } from '../utils/diagramGenerator';
|
|
14
|
+
import { logError } from '../utils/errorLogger';
|
|
15
|
+
import { generateUniqueOutputPath, generateVersionedOutputPath } from '../utils/fileSystem';
|
|
16
|
+
import logger from '../utils/logger';
|
|
17
|
+
import { generateFileTree } from '../utils/treeGenerator';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add file tree visualization to a review
|
|
21
|
+
* @param formattedOutput Formatted review output (markdown or JSON)
|
|
22
|
+
* @param files Files included in the review
|
|
23
|
+
* @param outputFormat Output format (markdown or json)
|
|
24
|
+
* @returns Enhanced output with file tree
|
|
25
|
+
*/
|
|
26
|
+
export function addFileTreeToReview(
|
|
27
|
+
formattedOutput: string,
|
|
28
|
+
files: FileInfo[],
|
|
29
|
+
outputFormat: string,
|
|
30
|
+
): string {
|
|
31
|
+
if (!files || files.length === 0) {
|
|
32
|
+
logger.debug('No files provided for tree generation');
|
|
33
|
+
return formattedOutput;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.debug(`Adding file tree for ${files.length} files in ${outputFormat} format`);
|
|
37
|
+
|
|
38
|
+
// Extract relative paths from files
|
|
39
|
+
const relativePaths = files.map((file) => file.relativePath || file.path);
|
|
40
|
+
// Generate tree visualization
|
|
41
|
+
const fileTree = generateFileTree(relativePaths);
|
|
42
|
+
|
|
43
|
+
if (outputFormat === 'json') {
|
|
44
|
+
// For JSON, parse and add tree visualization as property
|
|
45
|
+
try {
|
|
46
|
+
const reviewObj = JSON.parse(formattedOutput);
|
|
47
|
+
// Add both a flat list and a tree structure
|
|
48
|
+
reviewObj.analyzedFiles = relativePaths;
|
|
49
|
+
reviewObj.fileTree = fileTree.replace(/```/g, '').trim();
|
|
50
|
+
|
|
51
|
+
return JSON.stringify(reviewObj, null, 2);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.warn(`Error enhancing JSON review with file tree: ${error}`);
|
|
54
|
+
return formattedOutput;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// For markdown, find appropriate position to insert file tree section
|
|
58
|
+
const fileListSection = `\n## Files Analyzed\n\nThe following ${files.length} files were included in this review:\n\n${fileTree}\n\n`;
|
|
59
|
+
|
|
60
|
+
// Find the position to insert (before cost information section if it exists)
|
|
61
|
+
const costSectionMatch = formattedOutput.match(/^## Cost Information/m);
|
|
62
|
+
|
|
63
|
+
if (costSectionMatch?.index) {
|
|
64
|
+
// Insert before cost information
|
|
65
|
+
const position = costSectionMatch.index;
|
|
66
|
+
logger.debug('Inserting file list before Cost Information section');
|
|
67
|
+
return (
|
|
68
|
+
formattedOutput.substring(0, position) +
|
|
69
|
+
fileListSection +
|
|
70
|
+
formattedOutput.substring(position)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// Find the position to insert before the closing markdown
|
|
74
|
+
const closingMatch = formattedOutput.match(/---\n\*Generated by Code Review Tool/);
|
|
75
|
+
|
|
76
|
+
if (closingMatch?.index) {
|
|
77
|
+
// Insert before the closing section
|
|
78
|
+
const position = closingMatch.index;
|
|
79
|
+
logger.debug('Inserting file list before closing section');
|
|
80
|
+
return (
|
|
81
|
+
formattedOutput.substring(0, position) +
|
|
82
|
+
fileListSection +
|
|
83
|
+
formattedOutput.substring(position)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
// If no suitable position found, append at the end
|
|
87
|
+
logger.debug('No insertion point found, appending file list to end');
|
|
88
|
+
return formattedOutput + fileListSection;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Format and save a review result to a file
|
|
94
|
+
* @param review Review result to save
|
|
95
|
+
* @param options Review options
|
|
96
|
+
* @param outputBaseDir Base directory for output
|
|
97
|
+
* @param modelName Name of the model used for the review
|
|
98
|
+
* @param targetName Name of the target file or directory
|
|
99
|
+
* @param files Optional array of file information to include in the review
|
|
100
|
+
* @returns Promise resolving to the path of the saved file
|
|
101
|
+
*/
|
|
102
|
+
export async function saveReviewOutput(
|
|
103
|
+
review: ReviewResult,
|
|
104
|
+
options: ReviewOptions,
|
|
105
|
+
outputBaseDir: string,
|
|
106
|
+
modelName: string,
|
|
107
|
+
targetName: string,
|
|
108
|
+
files?: FileInfo[],
|
|
109
|
+
): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
// Generate a versioned output path
|
|
112
|
+
const extension = options.output === 'json' ? '.json' : '.md';
|
|
113
|
+
|
|
114
|
+
// Create unique filenames for different types of output
|
|
115
|
+
// For formatted review output
|
|
116
|
+
let outputPath = await generateVersionedOutputPath(
|
|
117
|
+
outputBaseDir,
|
|
118
|
+
`${options.type}-review`,
|
|
119
|
+
extension,
|
|
120
|
+
modelName,
|
|
121
|
+
targetName,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// For raw review data (if needed for debugging)
|
|
125
|
+
const rawDataPath = await generateUniqueOutputPath(
|
|
126
|
+
outputBaseDir,
|
|
127
|
+
`${options.type}-review-raw-data-${path.basename(outputPath, extension)}.json`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Ensure costInfo is set if only cost is available
|
|
131
|
+
if (review.cost && !review.costInfo) {
|
|
132
|
+
review.costInfo = review.cost;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format the review output
|
|
136
|
+
logger.debug(`Formatting review output as ${options.output}`);
|
|
137
|
+
let formattedOutput = formatReviewOutput(review, options.output || 'markdown');
|
|
138
|
+
|
|
139
|
+
// Add file tree to all review types if files are provided
|
|
140
|
+
if (files && files.length > 0) {
|
|
141
|
+
logger.info(`Adding file tree visualization for ${files.length} files`);
|
|
142
|
+
formattedOutput = addFileTreeToReview(formattedOutput, files, options.output || 'markdown');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// For architectural and security reviews, dependency analysis is ON by default unless explicitly disabled
|
|
146
|
+
const reviewTypeNeedsDependencyAnalysis = ['architectural', 'security'].includes(options.type);
|
|
147
|
+
if (reviewTypeNeedsDependencyAnalysis && options.includeDependencyAnalysis !== false) {
|
|
148
|
+
console.log(
|
|
149
|
+
`=========== DEPENDENCY ANALYSIS FOR ${options.type.toUpperCase()} REVIEW ===========`,
|
|
150
|
+
);
|
|
151
|
+
logger.info(
|
|
152
|
+
`=========== DEPENDENCY ANALYSIS FOR ${options.type.toUpperCase()} REVIEW ===========`,
|
|
153
|
+
);
|
|
154
|
+
try {
|
|
155
|
+
// Use the AI-powered dependency analyzer (no external dependencies required)
|
|
156
|
+
logger.info(`Performing AI-powered dependency analysis for ${options.type} review...`);
|
|
157
|
+
|
|
158
|
+
// Use project directory path instead of current working directory to ensure correct analysis
|
|
159
|
+
const projectPath =
|
|
160
|
+
files && files.length > 0 ? path.dirname(files[0].path) : path.resolve(process.cwd());
|
|
161
|
+
console.log(`Project path for dependency analysis: ${projectPath}`);
|
|
162
|
+
logger.info(`Project path for dependency analysis: ${projectPath}`);
|
|
163
|
+
|
|
164
|
+
// Run the AI-powered dependency analysis
|
|
165
|
+
const dependencySection = await createAIDependencyAnalysis(projectPath);
|
|
166
|
+
|
|
167
|
+
// Append dependency analysis section to the review
|
|
168
|
+
if (options.output === 'json') {
|
|
169
|
+
try {
|
|
170
|
+
// Parse JSON, add dependency analysis section, and stringify again
|
|
171
|
+
const reviewObj = JSON.parse(formattedOutput);
|
|
172
|
+
reviewObj.dependencyAnalysis = dependencySection;
|
|
173
|
+
formattedOutput = JSON.stringify(reviewObj, null, 2);
|
|
174
|
+
logger.info('AI-powered dependency analysis added to JSON review output');
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.warn(`Error adding dependency analysis section to JSON review: ${error}`);
|
|
177
|
+
// If JSON parsing fails, append as text
|
|
178
|
+
formattedOutput += `\n\n${dependencySection}`;
|
|
179
|
+
logger.info(
|
|
180
|
+
'AI-powered dependency analysis appended as text to JSON review (JSON parsing failed)',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// For markdown, append at the end
|
|
185
|
+
formattedOutput += `\n\n${dependencySection}`;
|
|
186
|
+
logger.info('AI-powered dependency analysis added to markdown review output');
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logger.error(
|
|
190
|
+
`Error performing AI-powered dependency analysis for ${options.type} review: ${error instanceof Error ? error.message : String(error)}`,
|
|
191
|
+
);
|
|
192
|
+
logger.error(
|
|
193
|
+
error instanceof Error && error.stack ? error.stack : 'No stack trace available',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check if the output file already exists (to avoid overwriting)
|
|
199
|
+
try {
|
|
200
|
+
await fs.access(outputPath);
|
|
201
|
+
logger.warn(`Output file already exists: ${outputPath}`);
|
|
202
|
+
|
|
203
|
+
// Generate a new unique path to avoid overwriting
|
|
204
|
+
const uniqueOutputPath = await generateUniqueOutputPath(
|
|
205
|
+
outputBaseDir,
|
|
206
|
+
`${options.type}-review-${Date.now()}${extension}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
logger.info(`Using alternative output path to avoid overwriting: ${uniqueOutputPath}`);
|
|
210
|
+
|
|
211
|
+
// Update the output path
|
|
212
|
+
const originalPath = outputPath;
|
|
213
|
+
outputPath = uniqueOutputPath;
|
|
214
|
+
|
|
215
|
+
// Log this change for debugging
|
|
216
|
+
logger.debug(`Changed output path from ${originalPath} to ${outputPath} to avoid collision`);
|
|
217
|
+
} catch (_error) {
|
|
218
|
+
// File doesn't exist, which is good
|
|
219
|
+
logger.debug(`Output file doesn't exist yet, proceeding with: ${outputPath}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Write the formatted output to the file
|
|
223
|
+
logger.debug(`Writing formatted review output to: ${outputPath}`);
|
|
224
|
+
await fs.writeFile(outputPath, formattedOutput);
|
|
225
|
+
logger.info(`Review output saved to: ${outputPath}`);
|
|
226
|
+
|
|
227
|
+
// Process and save any Mermaid diagrams if requested
|
|
228
|
+
if (options.diagram && options.type === 'architectural') {
|
|
229
|
+
const diagramPaths = await processDiagrams(formattedOutput, outputPath, options);
|
|
230
|
+
if (diagramPaths.length > 0) {
|
|
231
|
+
logger.info(`Generated ${diagramPaths.length} architecture diagram file(s)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Optionally save raw review data for debugging (only if debug mode is enabled)
|
|
236
|
+
if (options.debug) {
|
|
237
|
+
logger.debug(`Saving raw review data for debugging to: ${rawDataPath}`);
|
|
238
|
+
await fs.writeFile(rawDataPath, JSON.stringify(review, null, 2));
|
|
239
|
+
logger.debug(`Raw review data saved to: ${rawDataPath}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return outputPath;
|
|
243
|
+
} catch (error: unknown) {
|
|
244
|
+
if (error instanceof Error) {
|
|
245
|
+
const errorLogPath = await logError(error, {
|
|
246
|
+
operation: 'writeFile',
|
|
247
|
+
outputPath: 'unknown',
|
|
248
|
+
reviewType: options.type,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
logger.error(`Error saving review output:`);
|
|
252
|
+
logger.error(` Message: ${error.message}`);
|
|
253
|
+
logger.error(` Error details logged to: ${errorLogPath}`);
|
|
254
|
+
|
|
255
|
+
// Add more detailed error information
|
|
256
|
+
if (error.stack) {
|
|
257
|
+
logger.debug(`Error stack trace: ${error.stack}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (error.name === 'EACCES') {
|
|
261
|
+
logger.error(
|
|
262
|
+
` This appears to be a permission error. Please check that you have write access to the output directory.`,
|
|
263
|
+
);
|
|
264
|
+
} else if (error.name === 'ENOSPC') {
|
|
265
|
+
logger.error(
|
|
266
|
+
` This appears to be a disk space error. Please free up some disk space and try again.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
logger.error(`Unknown error saving review output: ${String(error)}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Review generator module.
|
|
3
|
+
*
|
|
4
|
+
* This module coordinates OpenRouter reviews. All other provider implementations
|
|
5
|
+
* have been removed so we can focus on the only runtime dependency we actually use.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
generateOpenRouterConsolidatedReview,
|
|
10
|
+
initializeAnyOpenRouterModel,
|
|
11
|
+
} from '../clients/openRouterClientWrapper';
|
|
12
|
+
import type { FileInfo, ReviewOptions, ReviewResult, ReviewType } from '../types/review';
|
|
13
|
+
import logger from '../utils/logger';
|
|
14
|
+
import type { ProjectDocs } from '../utils/projectDocs';
|
|
15
|
+
import type { ApiClientConfig } from './ApiClientSelector';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate an OpenRouter review.
|
|
19
|
+
*/
|
|
20
|
+
export async function generateReview(
|
|
21
|
+
fileInfos: FileInfo[],
|
|
22
|
+
project: string,
|
|
23
|
+
reviewType: ReviewType,
|
|
24
|
+
projectDocs: ProjectDocs | null,
|
|
25
|
+
options: ReviewOptions,
|
|
26
|
+
apiClientConfig: ApiClientConfig,
|
|
27
|
+
): Promise<ReviewResult> {
|
|
28
|
+
logger.debug('generateReview called');
|
|
29
|
+
logger.debug(`generateReview: apiClientConfig=${JSON.stringify(apiClientConfig)}`);
|
|
30
|
+
|
|
31
|
+
if (apiClientConfig.clientType !== 'OpenRouter') {
|
|
32
|
+
throw new Error(`Unsupported client type: ${apiClientConfig.clientType ?? 'unknown'}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let result: ReviewResult;
|
|
36
|
+
try {
|
|
37
|
+
await initializeAnyOpenRouterModel();
|
|
38
|
+
result = await generateOpenRouterConsolidatedReview(
|
|
39
|
+
fileInfos,
|
|
40
|
+
project,
|
|
41
|
+
reviewType,
|
|
42
|
+
projectDocs,
|
|
43
|
+
options,
|
|
44
|
+
);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error(
|
|
47
|
+
`Error initializing or calling OpenRouter client: ${
|
|
48
|
+
error instanceof Error ? error.message : String(error)
|
|
49
|
+
}`,
|
|
50
|
+
);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reviewResult = result;
|
|
55
|
+
|
|
56
|
+
// Enhanced logging for debugging empty content issues
|
|
57
|
+
logger.debug(`generateReview completed:`);
|
|
58
|
+
logger.debug(` Client type: ${apiClientConfig.clientType}`);
|
|
59
|
+
logger.debug(` Model: ${apiClientConfig.modelName}`);
|
|
60
|
+
logger.debug(` Result exists: ${!!reviewResult}`);
|
|
61
|
+
logger.debug(` Content exists: ${!!(reviewResult && reviewResult.content)}`);
|
|
62
|
+
logger.debug(
|
|
63
|
+
` Content length: ${reviewResult && reviewResult.content ? reviewResult.content.length : 'N/A'}`,
|
|
64
|
+
);
|
|
65
|
+
logger.debug(
|
|
66
|
+
` Content preview: ${reviewResult && reviewResult.content ? reviewResult.content.substring(0, 100).replace(/\n/g, ' ') + '...' : 'N/A'}`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Get package version from process.env or hardcoded value
|
|
70
|
+
const packageVersion = process.env.npm_package_version || '2.1.1';
|
|
71
|
+
|
|
72
|
+
// Create a string representation of the command-line options
|
|
73
|
+
const commandOptions = Object.entries(options)
|
|
74
|
+
.filter(([key, value]) => {
|
|
75
|
+
// Filter out internal options and undefined values
|
|
76
|
+
if (key.startsWith('_') || value === undefined) return false;
|
|
77
|
+
|
|
78
|
+
// Filter out ciData which can be very large
|
|
79
|
+
if (key === 'ciData') return false;
|
|
80
|
+
|
|
81
|
+
// Filter out empty arrays and objects
|
|
82
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
83
|
+
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
})
|
|
88
|
+
.map(([key, value]) => {
|
|
89
|
+
// Format boolean options as flags without values
|
|
90
|
+
if (typeof value === 'boolean') {
|
|
91
|
+
return value ? `--${key}` : '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Format arrays and objects as JSON strings
|
|
95
|
+
if (typeof value === 'object' && value !== null) {
|
|
96
|
+
return `--${key}='${JSON.stringify(value)}'`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format other values normally
|
|
100
|
+
return `--${key}=${value}`;
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean) // Remove empty strings
|
|
103
|
+
.join(' ');
|
|
104
|
+
|
|
105
|
+
// Add metadata to the review result
|
|
106
|
+
reviewResult.toolVersion = packageVersion;
|
|
107
|
+
reviewResult.commandOptions = commandOptions;
|
|
108
|
+
|
|
109
|
+
// Ensure costInfo is set if only cost is available
|
|
110
|
+
if (reviewResult.cost && !reviewResult.costInfo) {
|
|
111
|
+
reviewResult.costInfo = reviewResult.cost;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Ensure required fields are set to avoid undefined values in output
|
|
115
|
+
if (!reviewResult.filePath) {
|
|
116
|
+
logger.warn('Review result has no filePath. Setting to default value.');
|
|
117
|
+
reviewResult.filePath = reviewType;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!reviewResult.modelUsed) {
|
|
121
|
+
logger.warn('Review result has no modelUsed. Setting to default value.');
|
|
122
|
+
reviewResult.modelUsed = `${apiClientConfig.clientType}:${apiClientConfig.modelName}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Final validation before returning
|
|
126
|
+
if (!reviewResult.content || reviewResult.content.trim() === '') {
|
|
127
|
+
logger.error('CRITICAL: generateReview is about to return a result with empty content!');
|
|
128
|
+
logger.error(` Client: ${apiClientConfig.clientType}:${apiClientConfig.modelName}`);
|
|
129
|
+
logger.error(` Files: ${fileInfos.length} files`);
|
|
130
|
+
logger.error(` Review type: ${reviewType}`);
|
|
131
|
+
logger.error(` Project: ${project}`);
|
|
132
|
+
|
|
133
|
+
// This is a critical error that should be investigated
|
|
134
|
+
throw new Error(
|
|
135
|
+
`generateReview produced empty content for ${apiClientConfig.clientType}:${apiClientConfig.modelName}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return reviewResult;
|
|
140
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview File discovery and filtering module.
|
|
3
|
+
*
|
|
4
|
+
* This module is responsible for finding, filtering, and reading files for review.
|
|
5
|
+
* It handles gitignore patterns, test exclusions, and file system operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { getFilesToReview as getFilteredFiles, loadGitignorePatterns } from '../utils/fileFilters';
|
|
11
|
+
import { isDirectory, isPathWithinCwd, pathExists } from '../utils/fileSystem';
|
|
12
|
+
import logger from '../utils/logger';
|
|
13
|
+
import { applySmartFiltering } from '../utils/smartFileSelector';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* File information structure
|
|
17
|
+
*/
|
|
18
|
+
export interface FileInfo {
|
|
19
|
+
path: string;
|
|
20
|
+
relativePath: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate target parameter and provide helpful error messages for common mistakes
|
|
26
|
+
* @param target The target parameter to validate
|
|
27
|
+
* @throws Error with helpful message if the target looks like a misformatted parameter
|
|
28
|
+
*/
|
|
29
|
+
function validateTargetParameter(target: string): void {
|
|
30
|
+
// Check for common parameter format mistakes
|
|
31
|
+
if (target.includes('=')) {
|
|
32
|
+
const [key, ...valueParts] = target.split('=');
|
|
33
|
+
const value = valueParts.join('='); // Rejoin in case value contains =
|
|
34
|
+
const commonOptions = [
|
|
35
|
+
'type',
|
|
36
|
+
'output',
|
|
37
|
+
'model',
|
|
38
|
+
'language',
|
|
39
|
+
'debug',
|
|
40
|
+
'interactive',
|
|
41
|
+
'estimate',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Only flag as parameter if the key part matches a known option
|
|
45
|
+
// This avoids false positives for file paths like "src/file=name.ts"
|
|
46
|
+
if (commonOptions.includes(key)) {
|
|
47
|
+
throw new Error(`Invalid parameter format: '${target}'
|
|
48
|
+
|
|
49
|
+
It looks like you're trying to set the '${key}' option.
|
|
50
|
+
Did you mean: --${key} ${value}
|
|
51
|
+
|
|
52
|
+
Example usage:
|
|
53
|
+
cr-aia --${key} ${value}
|
|
54
|
+
cr-aia src --${key} ${value}
|
|
55
|
+
cr-aia . --${key} ${value}
|
|
56
|
+
|
|
57
|
+
Run 'cr-aia --help' for more options.`);
|
|
58
|
+
}
|
|
59
|
+
if (!key.includes('/') && !key.includes('\\') && !key.includes('.')) {
|
|
60
|
+
// If the key doesn't look like a path (no slashes or dots), it's probably a parameter mistake
|
|
61
|
+
throw new Error(`Invalid parameter format: '${target}'
|
|
62
|
+
|
|
63
|
+
Parameters should use '--' prefix, not '=' format.
|
|
64
|
+
Example: --type performance
|
|
65
|
+
|
|
66
|
+
Common usage patterns:
|
|
67
|
+
cr-aia # Review current directory
|
|
68
|
+
cr-aia src # Review src directory
|
|
69
|
+
cr-aia src/index.ts # Review specific file
|
|
70
|
+
cr-aia --type security # Security review of current directory
|
|
71
|
+
cr-aia src --type performance # Performance review of src
|
|
72
|
+
|
|
73
|
+
Run 'cr-aia --help' for all options.`);
|
|
74
|
+
}
|
|
75
|
+
// If key looks like a path (contains / or \ or .), don't flag it as an error
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if the target looks like an option without proper prefix
|
|
79
|
+
const commonOptions = [
|
|
80
|
+
'type',
|
|
81
|
+
'output',
|
|
82
|
+
'model',
|
|
83
|
+
'language',
|
|
84
|
+
'debug',
|
|
85
|
+
'interactive',
|
|
86
|
+
'estimate',
|
|
87
|
+
'help',
|
|
88
|
+
'version',
|
|
89
|
+
];
|
|
90
|
+
if (commonOptions.includes(target)) {
|
|
91
|
+
throw new Error(`'${target}' looks like an option but is missing '--' prefix.
|
|
92
|
+
|
|
93
|
+
Did you mean: --${target}
|
|
94
|
+
|
|
95
|
+
Example usage:
|
|
96
|
+
cr-aia --${target}
|
|
97
|
+
cr-aia src --${target}
|
|
98
|
+
|
|
99
|
+
For options that require values:
|
|
100
|
+
cr-aia --type performance
|
|
101
|
+
cr-aia --output json
|
|
102
|
+
cr-aia --model openai:gpt-4
|
|
103
|
+
|
|
104
|
+
Run 'cr-aia --help' for more information.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for other common mistakes
|
|
108
|
+
if (target.startsWith('-') && !target.startsWith('--')) {
|
|
109
|
+
throw new Error(`Invalid option format: '${target}'
|
|
110
|
+
|
|
111
|
+
Options should use double dashes (--), not single dash (-).
|
|
112
|
+
Did you mean: -${target}?
|
|
113
|
+
|
|
114
|
+
Example usage:
|
|
115
|
+
cr-aia --type security
|
|
116
|
+
cr-aia --debug
|
|
117
|
+
cr-aia --help
|
|
118
|
+
|
|
119
|
+
Run 'cr-aia --help' for all available options.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Discover files for review based on the target path and options
|
|
125
|
+
* @param target The target file or directory path
|
|
126
|
+
* @param projectPath The project root path
|
|
127
|
+
* @param includeTests Whether to include test files
|
|
128
|
+
* @returns Array of file paths to review
|
|
129
|
+
*/
|
|
130
|
+
export async function discoverFiles(
|
|
131
|
+
target: string,
|
|
132
|
+
projectPath: string,
|
|
133
|
+
includeTests = false,
|
|
134
|
+
): Promise<string[]> {
|
|
135
|
+
try {
|
|
136
|
+
// First validate the target parameter for common mistakes
|
|
137
|
+
validateTargetParameter(target);
|
|
138
|
+
|
|
139
|
+
// Validate the target path
|
|
140
|
+
const resolvedTarget = path.resolve(projectPath, target);
|
|
141
|
+
|
|
142
|
+
// Check if the path is within the project directory
|
|
143
|
+
if (!isPathWithinCwd(resolvedTarget)) {
|
|
144
|
+
throw new Error(`Target must be within the project directory: ${projectPath}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targetPath = resolvedTarget;
|
|
148
|
+
|
|
149
|
+
// Check if the target exists
|
|
150
|
+
const isFileTarget = (await pathExists(targetPath)) && !(await isDirectory(targetPath));
|
|
151
|
+
const isDirectoryTarget = await isDirectory(targetPath);
|
|
152
|
+
|
|
153
|
+
if (!isFileTarget && !isDirectoryTarget) {
|
|
154
|
+
throw new Error(`Target not found: ${target}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Load gitignore patterns from target path, not project path
|
|
158
|
+
const gitignorePatterns = await loadGitignorePatterns(targetPath);
|
|
159
|
+
logger.debug(`Loaded ${gitignorePatterns.length} patterns from .gitignore in ${targetPath}`);
|
|
160
|
+
|
|
161
|
+
// Get files to review using the existing filter utility
|
|
162
|
+
let filesToReview = await getFilteredFiles(
|
|
163
|
+
targetPath,
|
|
164
|
+
isFileTarget,
|
|
165
|
+
includeTests,
|
|
166
|
+
gitignorePatterns,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Apply smart filtering (tsconfig.json and .eslintignore)
|
|
170
|
+
if (filesToReview.length > 0) {
|
|
171
|
+
logger.info('Applying smart filtering based on project configuration files...');
|
|
172
|
+
// Use the target directory for finding tsconfig.json, not the project root
|
|
173
|
+
const configDir = isFileTarget ? path.dirname(targetPath) : targetPath;
|
|
174
|
+
filesToReview = await applySmartFiltering(filesToReview, configDir);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (filesToReview.length === 0) {
|
|
178
|
+
logger.info('No files found to review.');
|
|
179
|
+
} else {
|
|
180
|
+
logger.info(`Found ${filesToReview.length} files to review.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return filesToReview;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.error(
|
|
186
|
+
`Error discovering files: ${error instanceof Error ? error.message : String(error)}`,
|
|
187
|
+
);
|
|
188
|
+
throw error; // Re-throw to allow the caller to handle it
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read file contents and prepare file information for review
|
|
194
|
+
* @param filePaths Array of file paths to read
|
|
195
|
+
* @param projectPath The project root path
|
|
196
|
+
* @returns Array of FileInfo objects with file contents
|
|
197
|
+
*/
|
|
198
|
+
export async function readFilesContent(
|
|
199
|
+
filePaths: string[],
|
|
200
|
+
projectPath: string,
|
|
201
|
+
): Promise<{ fileInfos: FileInfo[]; errors: Array<{ path: string; error: string }> }> {
|
|
202
|
+
const fileInfos: FileInfo[] = [];
|
|
203
|
+
const errors: Array<{ path: string; error: string }> = [];
|
|
204
|
+
|
|
205
|
+
for (const filePath of filePaths) {
|
|
206
|
+
try {
|
|
207
|
+
// Check if file exists and is readable
|
|
208
|
+
if (!(await pathExists(filePath))) {
|
|
209
|
+
errors.push({ path: filePath, error: 'File does not exist' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Read file content
|
|
214
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
215
|
+
|
|
216
|
+
// Get relative path from project root
|
|
217
|
+
const relativePath = path.relative(projectPath, filePath);
|
|
218
|
+
|
|
219
|
+
// Add to file infos
|
|
220
|
+
fileInfos.push({
|
|
221
|
+
path: filePath,
|
|
222
|
+
relativePath,
|
|
223
|
+
content: fileContent,
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
227
|
+
logger.error(`Error reading file ${filePath}: ${errorMessage}`);
|
|
228
|
+
errors.push({ path: filePath, error: errorMessage });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (errors.length > 0) {
|
|
233
|
+
logger.warn(`Failed to read ${errors.length} file(s)`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { fileInfos, errors };
|
|
237
|
+
}
|