codecritique 2.0.4 → 2.0.5
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/package.json +1 -1
- package/src/index.js +123 -48
- package/src/llm.js +1 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -30,8 +30,9 @@ import { ProjectAnalyzer } from './project-analyzer.js';
|
|
|
30
30
|
import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
|
|
31
31
|
import { execGitSafe } from './utils/command.js';
|
|
32
32
|
import { ensureBranchExists, findBaseBranch } from './utils/git.js';
|
|
33
|
-
import { verboseLog } from './utils/logging.js';
|
|
33
|
+
import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './utils/logging.js';
|
|
34
34
|
import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
|
|
35
|
+
import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
|
|
35
36
|
|
|
36
37
|
// Create a default embeddings system instance
|
|
37
38
|
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
@@ -213,7 +214,7 @@ else {
|
|
|
213
214
|
|
|
214
215
|
// Register process event handlers for cleanup (embeddings cleanup primarily)
|
|
215
216
|
process.on('SIGINT', async () => {
|
|
216
|
-
|
|
217
|
+
diagnosticLog(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
|
|
217
218
|
// Set a timeout to force exit if cleanup hangs
|
|
218
219
|
const forceExitTimeout = setTimeout(() => {
|
|
219
220
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -221,23 +222,23 @@ process.on('SIGINT', async () => {
|
|
|
221
222
|
}, 10000); // 10 seconds timeout
|
|
222
223
|
|
|
223
224
|
try {
|
|
224
|
-
|
|
225
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
|
|
225
226
|
await embeddingsSystem.cleanup();
|
|
226
|
-
|
|
227
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
227
228
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
228
|
-
|
|
229
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
|
|
229
230
|
process.exit(0); // Exit normally
|
|
230
231
|
}
|
|
231
232
|
catch (err) {
|
|
232
233
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
233
234
|
clearTimeout(forceExitTimeout);
|
|
234
|
-
|
|
235
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
|
|
235
236
|
process.exit(1); // Exit with error code
|
|
236
237
|
}
|
|
237
238
|
});
|
|
238
239
|
|
|
239
240
|
process.on('SIGTERM', async () => {
|
|
240
|
-
|
|
241
|
+
diagnosticLog(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
|
|
241
242
|
// Set a timeout to force exit if cleanup hangs
|
|
242
243
|
const forceExitTimeout = setTimeout(() => {
|
|
243
244
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -245,17 +246,17 @@ process.on('SIGTERM', async () => {
|
|
|
245
246
|
}, 10000);
|
|
246
247
|
|
|
247
248
|
try {
|
|
248
|
-
|
|
249
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
|
|
249
250
|
await embeddingsSystem.cleanup();
|
|
250
|
-
|
|
251
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
251
252
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
252
|
-
|
|
253
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
|
|
253
254
|
process.exit(0); // Exit normally
|
|
254
255
|
}
|
|
255
256
|
catch (err) {
|
|
256
257
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
257
258
|
clearTimeout(forceExitTimeout);
|
|
258
|
-
|
|
259
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
|
|
259
260
|
process.exit(1); // Exit with error code
|
|
260
261
|
}
|
|
261
262
|
});
|
|
@@ -263,7 +264,7 @@ process.on('SIGTERM', async () => {
|
|
|
263
264
|
// Ensure cleanup on normal exit
|
|
264
265
|
process.on('exit', () => {
|
|
265
266
|
// Note: Async cleanup might not fully complete here
|
|
266
|
-
|
|
267
|
+
diagnosticLog(chalk.cyan('Exiting...'));
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
// REMOVED: Old options processing logic for ignore/severity
|
|
@@ -273,6 +274,7 @@ process.on('exit', () => {
|
|
|
273
274
|
|
|
274
275
|
// Main function to run the code review (Refactored to use cag-review.js)
|
|
275
276
|
async function runCodeReview(options) {
|
|
277
|
+
configureCleanStdoutForDataOutput(options);
|
|
276
278
|
let reviewTask = null;
|
|
277
279
|
let operationDescription = '';
|
|
278
280
|
const startTime = Date.now();
|
|
@@ -281,7 +283,7 @@ async function runCodeReview(options) {
|
|
|
281
283
|
// If --directory is specified, use that as the project directory
|
|
282
284
|
// Otherwise, use the current working directory
|
|
283
285
|
const projectPath = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
284
|
-
|
|
286
|
+
diagnosticLog(chalk.gray(`Using project path for analysis: ${projectPath}`));
|
|
285
287
|
|
|
286
288
|
// Parse custom documents
|
|
287
289
|
const customDocs = [];
|
|
@@ -322,12 +324,12 @@ async function runCodeReview(options) {
|
|
|
322
324
|
.trim(); // Remove leading/trailing whitespace
|
|
323
325
|
|
|
324
326
|
customDocs.push({ title, content });
|
|
325
|
-
|
|
327
|
+
diagnosticLog(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
|
|
326
328
|
|
|
327
329
|
// Debug: Show a clean preview of the content
|
|
328
330
|
const previewLength = 200;
|
|
329
331
|
const preview = content.substring(0, previewLength).replace(/\n/g, '\\n');
|
|
330
|
-
|
|
332
|
+
diagnosticLog(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
|
|
331
333
|
}
|
|
332
334
|
}
|
|
333
335
|
|
|
@@ -352,7 +354,7 @@ async function runCodeReview(options) {
|
|
|
352
354
|
};
|
|
353
355
|
|
|
354
356
|
try {
|
|
355
|
-
|
|
357
|
+
diagnosticLog(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
|
|
356
358
|
|
|
357
359
|
// Determine the review mode based on options
|
|
358
360
|
// Only support: single file, specific files, or diff with branch
|
|
@@ -361,7 +363,9 @@ async function runCodeReview(options) {
|
|
|
361
363
|
const gitWorkingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
362
364
|
const changedFiles = getChangedFiles(options.diffWith, gitWorkingDir);
|
|
363
365
|
if (changedFiles.length === 0) {
|
|
364
|
-
|
|
366
|
+
const message = `No changed files found compared to branch '${options.diffWith}'. Exiting.`;
|
|
367
|
+
diagnosticLog(chalk.yellow(message));
|
|
368
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
365
369
|
return;
|
|
366
370
|
}
|
|
367
371
|
operationDescription = `${changedFiles.length} files changed vs ${options.diffWith}`;
|
|
@@ -383,7 +387,9 @@ async function runCodeReview(options) {
|
|
|
383
387
|
else if (options.files && options.files.length > 0) {
|
|
384
388
|
const filesToAnalyze = await expandFilePatterns(options.files);
|
|
385
389
|
if (filesToAnalyze.length === 0) {
|
|
386
|
-
|
|
390
|
+
const message = 'No files found matching the specified patterns. Exiting.';
|
|
391
|
+
diagnosticLog(chalk.yellow(message));
|
|
392
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
387
393
|
return;
|
|
388
394
|
}
|
|
389
395
|
operationDescription = `${filesToAnalyze.length} specific files/patterns`;
|
|
@@ -408,7 +414,7 @@ async function runCodeReview(options) {
|
|
|
408
414
|
process.exit(1);
|
|
409
415
|
}
|
|
410
416
|
|
|
411
|
-
|
|
417
|
+
diagnosticLog(chalk.cyan(`Starting review for ${operationDescription}...`));
|
|
412
418
|
|
|
413
419
|
// Execute the selected review task
|
|
414
420
|
const reviewResult = await reviewTask;
|
|
@@ -421,60 +427,77 @@ async function runCodeReview(options) {
|
|
|
421
427
|
// Process and output results
|
|
422
428
|
if (reviewResult && reviewResult.success) {
|
|
423
429
|
if (reviewResult.results && reviewResult.results.length > 0) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Pass the detailed results array to the output function
|
|
428
|
-
outputFn(reviewResult.results, options, reviewResult);
|
|
429
|
-
console.log(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
430
|
+
diagnosticLog(chalk.green(`Found ${reviewResult.results.length} result items to display`));
|
|
431
|
+
await outputReviewResults(reviewResult.results, options, reviewResult);
|
|
432
|
+
diagnosticLog(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
430
433
|
}
|
|
431
434
|
else {
|
|
432
435
|
// No results to display (e.g., all files were excluded/skipped)
|
|
433
436
|
const message = reviewResult.message || 'All files were excluded from review (e.g., config files, lock files).';
|
|
434
|
-
|
|
437
|
+
diagnosticLog(chalk.yellow(message));
|
|
435
438
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
439
|
+
if (await emitEmptyReviewOutputIfNeeded(options, reviewResult)) {
|
|
440
|
+
if (options.outputFile) {
|
|
441
|
+
diagnosticLog(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
442
|
+
}
|
|
441
443
|
}
|
|
442
444
|
|
|
443
|
-
|
|
445
|
+
diagnosticLog(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
|
|
444
446
|
}
|
|
445
447
|
}
|
|
446
448
|
else {
|
|
449
|
+
const error = reviewResult?.error || 'Code review process failed.';
|
|
447
450
|
console.error(chalk.red('\nCode review process failed.'));
|
|
448
|
-
|
|
449
|
-
|
|
451
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
452
|
+
|
|
453
|
+
if (await emitErrorReviewOutputIfNeeded(options, error, reviewResult || {})) {
|
|
454
|
+
if (options.outputFile) {
|
|
455
|
+
diagnosticLog(chalk.yellow(`Error results written to: ${options.outputFile}`));
|
|
456
|
+
}
|
|
450
457
|
}
|
|
458
|
+
// AI review output is advisory; report review-task failures without failing CI.
|
|
451
459
|
}
|
|
452
460
|
|
|
453
461
|
// Clean up resources
|
|
454
|
-
|
|
462
|
+
diagnosticLog(chalk.cyan('Cleaning up resources...'));
|
|
455
463
|
try {
|
|
456
464
|
await embeddingsSystem.cleanup();
|
|
457
465
|
await cleanupClassifier();
|
|
458
|
-
|
|
466
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
459
467
|
}
|
|
460
468
|
catch (cleanupErr) {
|
|
461
469
|
console.error(chalk.yellow('Error during cleanup:'), cleanupErr.message);
|
|
462
|
-
process.
|
|
470
|
+
process.exitCode = 1;
|
|
471
|
+
return;
|
|
463
472
|
}
|
|
464
473
|
}
|
|
465
474
|
catch (err) {
|
|
466
|
-
|
|
467
|
-
|
|
475
|
+
if (!isBrokenStdoutPipeError(err)) {
|
|
476
|
+
console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
|
|
477
|
+
if (shouldPrintErrorStack(options, err)) {
|
|
478
|
+
console.error(err.stack);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
await emitErrorReviewOutputIfNeeded(options, err.message, { success: false, error: err.message });
|
|
484
|
+
}
|
|
485
|
+
catch (outputErr) {
|
|
486
|
+
if (!isBrokenStdoutPipeError(outputErr)) {
|
|
487
|
+
console.error(chalk.red('Error writing review error output:'), outputErr.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
468
491
|
// Clean up resources even on error
|
|
469
492
|
try {
|
|
470
493
|
await embeddingsSystem.cleanup();
|
|
471
494
|
await cleanupClassifier();
|
|
472
|
-
|
|
495
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
473
496
|
}
|
|
474
497
|
catch (cleanupErr) {
|
|
475
498
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
476
499
|
}
|
|
477
|
-
process.
|
|
500
|
+
process.exitCode = 1;
|
|
478
501
|
}
|
|
479
502
|
}
|
|
480
503
|
|
|
@@ -961,7 +984,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
961
984
|
try {
|
|
962
985
|
// Get git root directory
|
|
963
986
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
|
|
964
|
-
|
|
987
|
+
diagnosticLog(chalk.gray(`Git repository: ${gitRoot}`));
|
|
965
988
|
|
|
966
989
|
// Ensure the branch exists locally (fetch if needed)
|
|
967
990
|
ensureBranchExists(branch, workingDir);
|
|
@@ -978,7 +1001,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
978
1001
|
// Continue with the original baseBranch name, it might work with remote refs
|
|
979
1002
|
}
|
|
980
1003
|
|
|
981
|
-
|
|
1004
|
+
diagnosticLog(chalk.gray(`Comparing ${branch} against ${baseBranch}...`));
|
|
982
1005
|
|
|
983
1006
|
// Use three-dot notation to get changes in branch compared to base
|
|
984
1007
|
// This shows commits that are in 'branch' but not in 'baseBranch'
|
|
@@ -992,7 +1015,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
992
1015
|
.map((file) => path.resolve(gitRoot, file)); // Get absolute path
|
|
993
1016
|
|
|
994
1017
|
if (changedFiles.length > 0) {
|
|
995
|
-
|
|
1018
|
+
diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in ${branch} vs ${baseBranch}`));
|
|
996
1019
|
}
|
|
997
1020
|
|
|
998
1021
|
return changedFiles;
|
|
@@ -1009,6 +1032,58 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1009
1032
|
// --- Output Formatting Functions --- //
|
|
1010
1033
|
// These consume the normalized results returned by the RAG review pipeline
|
|
1011
1034
|
|
|
1035
|
+
function getReviewOutputFunction(outputFormat) {
|
|
1036
|
+
if (outputFormat === 'json') {
|
|
1037
|
+
return outputJson;
|
|
1038
|
+
}
|
|
1039
|
+
if (outputFormat === 'markdown') {
|
|
1040
|
+
return outputMarkdown;
|
|
1041
|
+
}
|
|
1042
|
+
return outputText;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function shouldEmitReviewOutput(options = {}) {
|
|
1046
|
+
return Boolean(options.outputFile || options.output === 'json' || options.output === 'markdown');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function shouldPrintErrorStack(options, error) {
|
|
1050
|
+
return Boolean(error?.stack && (isVerboseEnabled(options) || isDebugEnabled()));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function outputReviewResults(reviewResults, options, aggregateResult = {}) {
|
|
1054
|
+
await getReviewOutputFunction(options.output)(reviewResults, options, aggregateResult);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function emitEmptyReviewOutputIfNeeded(options, aggregateResult = {}) {
|
|
1058
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
await outputReviewResults([], options, aggregateResult);
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function emitErrorReviewOutputIfNeeded(options, error, aggregateResult = {}) {
|
|
1067
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await outputReviewResults([createReviewErrorResult(error)], options, {
|
|
1072
|
+
...aggregateResult,
|
|
1073
|
+
success: false,
|
|
1074
|
+
error,
|
|
1075
|
+
});
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function createReviewErrorResult(error) {
|
|
1080
|
+
return {
|
|
1081
|
+
filePath: 'review',
|
|
1082
|
+
success: false,
|
|
1083
|
+
error,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1012
1087
|
/**
|
|
1013
1088
|
* Output results in JSON format
|
|
1014
1089
|
*
|
|
@@ -1016,7 +1091,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1016
1091
|
* @param {Object} options - Command line options
|
|
1017
1092
|
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1018
1093
|
*/
|
|
1019
|
-
function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
1094
|
+
async function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
1020
1095
|
const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
|
|
1021
1096
|
const fileLevelIssues = getFileLevelIssueCount(reviewResults);
|
|
1022
1097
|
|
|
@@ -1069,7 +1144,7 @@ function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
|
1069
1144
|
}
|
|
1070
1145
|
else {
|
|
1071
1146
|
// Write JSON output to stdout (process.stdout is not buffered)
|
|
1072
|
-
|
|
1147
|
+
await writeStdout(jsonOutput);
|
|
1073
1148
|
}
|
|
1074
1149
|
}
|
|
1075
1150
|
|
|
@@ -1080,7 +1155,7 @@ function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
|
1080
1155
|
* @param {Object} options - Command line options
|
|
1081
1156
|
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1082
1157
|
*/
|
|
1083
|
-
function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
1158
|
+
async function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
1084
1159
|
const totalFiles = reviewResults.length;
|
|
1085
1160
|
const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
|
|
1086
1161
|
const skippedFiles = reviewResults.filter((r) => r.skipped).length;
|
|
@@ -1164,7 +1239,7 @@ function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
|
1164
1239
|
return;
|
|
1165
1240
|
}
|
|
1166
1241
|
|
|
1167
|
-
|
|
1242
|
+
await writeStdout(markdownOutput);
|
|
1168
1243
|
}
|
|
1169
1244
|
|
|
1170
1245
|
/**
|
package/src/llm.js
CHANGED