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.
Files changed (153) hide show
  1. package/.cr-aia.yml +23 -0
  2. package/.crignore +0 -0
  3. package/dist/index.js +27 -0
  4. package/package.json +85 -0
  5. package/src/analysis/FindingsExtractor.ts +431 -0
  6. package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +267 -0
  7. package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +622 -0
  8. package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +430 -0
  9. package/src/analysis/ai-detection/core/AIDetectionEngine.ts +467 -0
  10. package/src/analysis/ai-detection/types/DetectionTypes.ts +406 -0
  11. package/src/analysis/ai-detection/utils/SubmissionConverter.ts +390 -0
  12. package/src/analysis/context/ReviewContext.ts +378 -0
  13. package/src/analysis/context/index.ts +7 -0
  14. package/src/analysis/index.ts +8 -0
  15. package/src/analysis/tokens/TokenAnalysisFormatter.ts +154 -0
  16. package/src/analysis/tokens/TokenAnalyzer.ts +747 -0
  17. package/src/analysis/tokens/index.ts +8 -0
  18. package/src/clients/base/abstractClient.ts +190 -0
  19. package/src/clients/base/httpClient.ts +160 -0
  20. package/src/clients/base/index.ts +12 -0
  21. package/src/clients/base/modelDetection.ts +107 -0
  22. package/src/clients/base/responseProcessor.ts +586 -0
  23. package/src/clients/factory/clientFactory.ts +55 -0
  24. package/src/clients/factory/index.ts +8 -0
  25. package/src/clients/implementations/index.ts +8 -0
  26. package/src/clients/implementations/openRouterClient.ts +411 -0
  27. package/src/clients/openRouterClient.ts +863 -0
  28. package/src/clients/openRouterClientWrapper.ts +44 -0
  29. package/src/clients/utils/directoryStructure.ts +52 -0
  30. package/src/clients/utils/index.ts +11 -0
  31. package/src/clients/utils/languageDetection.ts +44 -0
  32. package/src/clients/utils/promptFormatter.ts +105 -0
  33. package/src/clients/utils/promptLoader.ts +53 -0
  34. package/src/clients/utils/tokenCounter.ts +297 -0
  35. package/src/core/ApiClientSelector.ts +37 -0
  36. package/src/core/ConfigurationService.ts +591 -0
  37. package/src/core/ConsolidationService.ts +423 -0
  38. package/src/core/InteractiveDisplayManager.ts +81 -0
  39. package/src/core/OutputManager.ts +275 -0
  40. package/src/core/ReviewGenerator.ts +140 -0
  41. package/src/core/fileDiscovery.ts +237 -0
  42. package/src/core/handlers/EstimationHandler.ts +104 -0
  43. package/src/core/handlers/FileProcessingHandler.ts +204 -0
  44. package/src/core/handlers/OutputHandler.ts +125 -0
  45. package/src/core/handlers/ReviewExecutor.ts +104 -0
  46. package/src/core/reviewOrchestrator.ts +333 -0
  47. package/src/core/utils/ModelInfoUtils.ts +56 -0
  48. package/src/formatters/outputFormatter.ts +62 -0
  49. package/src/formatters/utils/IssueFormatters.ts +83 -0
  50. package/src/formatters/utils/JsonFormatter.ts +77 -0
  51. package/src/formatters/utils/MarkdownFormatters.ts +609 -0
  52. package/src/formatters/utils/MetadataFormatter.ts +269 -0
  53. package/src/formatters/utils/ModelInfoExtractor.ts +115 -0
  54. package/src/index.ts +27 -0
  55. package/src/plugins/PluginInterface.ts +50 -0
  56. package/src/plugins/PluginManager.ts +126 -0
  57. package/src/prompts/PromptManager.ts +69 -0
  58. package/src/prompts/cache/PromptCache.ts +50 -0
  59. package/src/prompts/promptText/common/variables/css-frameworks.json +33 -0
  60. package/src/prompts/promptText/common/variables/framework-versions.json +45 -0
  61. package/src/prompts/promptText/frameworks/react/comprehensive.hbs +19 -0
  62. package/src/prompts/promptText/languages/css/comprehensive.hbs +18 -0
  63. package/src/prompts/promptText/languages/generic/comprehensive.hbs +20 -0
  64. package/src/prompts/promptText/languages/html/comprehensive.hbs +18 -0
  65. package/src/prompts/promptText/languages/javascript/comprehensive.hbs +18 -0
  66. package/src/prompts/promptText/languages/python/comprehensive.hbs +18 -0
  67. package/src/prompts/promptText/languages/typescript/comprehensive.hbs +18 -0
  68. package/src/runtime/auth/service.ts +58 -0
  69. package/src/runtime/auth/session.ts +103 -0
  70. package/src/runtime/auth/types.ts +11 -0
  71. package/src/runtime/cliEntry.ts +196 -0
  72. package/src/runtime/errors.ts +13 -0
  73. package/src/runtime/fileCollector.ts +188 -0
  74. package/src/runtime/manifest.ts +64 -0
  75. package/src/runtime/openrouterProxy.ts +45 -0
  76. package/src/runtime/proxyConfig.ts +94 -0
  77. package/src/runtime/proxyEnvironment.ts +71 -0
  78. package/src/runtime/reportMerge.ts +102 -0
  79. package/src/runtime/reporting/markdownReportBuilder.ts +138 -0
  80. package/src/runtime/reporting/reportDataCollector.ts +234 -0
  81. package/src/runtime/reporting/summaryGenerator.ts +86 -0
  82. package/src/runtime/reviewPipeline.ts +155 -0
  83. package/src/runtime/runAiCodeReview.ts +153 -0
  84. package/src/runtime/runtimeConfig.ts +5 -0
  85. package/src/runtime/ui/Layout.tsx +57 -0
  86. package/src/runtime/ui/RuntimeApp.tsx +150 -0
  87. package/src/runtime/ui/inkModules.ts +73 -0
  88. package/src/runtime/ui/screens/AuthScreen.tsx +128 -0
  89. package/src/runtime/ui/screens/ModeSelection.tsx +52 -0
  90. package/src/runtime/ui/screens/ProgressScreen.tsx +55 -0
  91. package/src/runtime/ui/screens/ResultsScreen.tsx +76 -0
  92. package/src/strategies/ArchitecturalReviewStrategy.ts +54 -0
  93. package/src/strategies/CodingTestReviewStrategy.ts +920 -0
  94. package/src/strategies/ConsolidatedReviewStrategy.ts +59 -0
  95. package/src/strategies/ExtractPatternsReviewStrategy.ts +64 -0
  96. package/src/strategies/MultiPassReviewStrategy.ts +785 -0
  97. package/src/strategies/ReviewStrategy.ts +64 -0
  98. package/src/strategies/StrategyFactory.ts +79 -0
  99. package/src/strategies/index.ts +14 -0
  100. package/src/tokenizers/baseTokenizer.ts +61 -0
  101. package/src/tokenizers/gptTokenizer.ts +27 -0
  102. package/src/tokenizers/index.ts +8 -0
  103. package/src/types/apiResponses.ts +40 -0
  104. package/src/types/cli.ts +24 -0
  105. package/src/types/common.ts +39 -0
  106. package/src/types/configuration.ts +201 -0
  107. package/src/types/handlebars.d.ts +5 -0
  108. package/src/types/patch.d.ts +25 -0
  109. package/src/types/review.ts +294 -0
  110. package/src/types/reviewContext.d.ts +65 -0
  111. package/src/types/reviewSchema.ts +181 -0
  112. package/src/types/structuredReview.ts +167 -0
  113. package/src/types/tokenAnalysis.ts +56 -0
  114. package/src/utils/FileReader.ts +93 -0
  115. package/src/utils/FileWriter.ts +76 -0
  116. package/src/utils/PathGenerator.ts +97 -0
  117. package/src/utils/api/apiUtils.ts +14 -0
  118. package/src/utils/api/index.ts +1 -0
  119. package/src/utils/apiErrorHandler.ts +287 -0
  120. package/src/utils/ciDataCollector.ts +252 -0
  121. package/src/utils/codingTestConfigLoader.ts +466 -0
  122. package/src/utils/dependencies/aiDependencyAnalyzer.ts +454 -0
  123. package/src/utils/detection/frameworkDetector.ts +879 -0
  124. package/src/utils/detection/index.ts +10 -0
  125. package/src/utils/detection/projectTypeDetector.ts +518 -0
  126. package/src/utils/diagramGenerator.ts +206 -0
  127. package/src/utils/errorLogger.ts +60 -0
  128. package/src/utils/estimationUtils.ts +407 -0
  129. package/src/utils/fileFilters.ts +373 -0
  130. package/src/utils/fileSystem.ts +57 -0
  131. package/src/utils/index.ts +36 -0
  132. package/src/utils/logger.ts +240 -0
  133. package/src/utils/pathValidator.ts +98 -0
  134. package/src/utils/priorityFilter.ts +59 -0
  135. package/src/utils/projectDocs.ts +189 -0
  136. package/src/utils/promptPaths.ts +29 -0
  137. package/src/utils/promptTemplateManager.ts +157 -0
  138. package/src/utils/review/consolidateReview.ts +553 -0
  139. package/src/utils/review/fixDisplay.ts +100 -0
  140. package/src/utils/review/fixImplementation.ts +61 -0
  141. package/src/utils/review/index.ts +36 -0
  142. package/src/utils/review/interactiveProcessing.ts +294 -0
  143. package/src/utils/review/progressTracker.ts +296 -0
  144. package/src/utils/review/reviewExtraction.ts +382 -0
  145. package/src/utils/review/types.ts +46 -0
  146. package/src/utils/reviewActionHandler.ts +18 -0
  147. package/src/utils/reviewParser.ts +253 -0
  148. package/src/utils/sanitizer.ts +238 -0
  149. package/src/utils/smartFileSelector.ts +255 -0
  150. package/src/utils/templateLoader.ts +514 -0
  151. package/src/utils/treeGenerator.ts +153 -0
  152. package/tsconfig.build.json +14 -0
  153. 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
+ }