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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +123 -48
  3. package/src/llm.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
- console.log(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
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
- console.log(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
225
+ diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
225
226
  await embeddingsSystem.cleanup();
226
- console.log(chalk.green('embeddingsSystem.cleanup() completed.'));
227
+ diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
227
228
  clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
228
- console.log(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
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
- console.log(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
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
- console.log(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
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
- console.log(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
249
+ diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
249
250
  await embeddingsSystem.cleanup();
250
- console.log(chalk.green('embeddingsSystem.cleanup() completed.'));
251
+ diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
251
252
  clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
252
- console.log(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
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
- console.log(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
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
- console.log(chalk.cyan('Exiting...'));
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
- console.log(chalk.gray(`Using project path for analysis: ${projectPath}`));
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
- console.log(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
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
- console.log(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
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
- console.log(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
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
- console.log(chalk.yellow(`No changed files found compared to branch '${options.diffWith}'. Exiting.`));
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
- console.log(chalk.yellow('No files found matching the specified patterns. Exiting.'));
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
- console.log(chalk.cyan(`Starting review for ${operationDescription}...`));
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
- console.log(chalk.green(`Found ${reviewResult.results.length} result items to display`));
425
- // Determine output function based on format option
426
- const outputFn = options.output === 'json' ? outputJson : options.output === 'markdown' ? outputMarkdown : outputText;
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
- console.log(chalk.yellow(message));
437
+ diagnosticLog(chalk.yellow(message));
435
438
 
436
- // Still output empty results if outputFile is specified (for CI/CD pipelines)
437
- if (options.outputFile) {
438
- const outputFn = options.output === 'json' ? outputJson : options.output === 'markdown' ? outputMarkdown : outputText;
439
- outputFn([], options, reviewResult);
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
- console.log(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
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
- if (reviewResult && reviewResult.error) {
449
- console.error(chalk.red(`Error: ${reviewResult.error}`));
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
- console.log(chalk.cyan('Cleaning up resources...'));
462
+ diagnosticLog(chalk.cyan('Cleaning up resources...'));
455
463
  try {
456
464
  await embeddingsSystem.cleanup();
457
465
  await cleanupClassifier();
458
- console.log(chalk.green('All resources cleaned up successfully'));
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.exit(1);
470
+ process.exitCode = 1;
471
+ return;
463
472
  }
464
473
  }
465
474
  catch (err) {
466
- console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
467
- console.error(err.stack);
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
- console.log(chalk.green('All resources cleaned up successfully'));
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.exit(1);
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
- console.log(chalk.gray(`Git repository: ${gitRoot}`));
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
- console.log(chalk.gray(`Comparing ${branch} against ${baseBranch}...`));
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
- console.log(chalk.gray(`Found ${changedFiles.length} changed files in ${branch} vs ${baseBranch}`));
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
- process.stdout.write(jsonOutput);
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
- process.stdout.write(markdownOutput);
1242
+ await writeStdout(markdownOutput);
1168
1243
  }
1169
1244
 
1170
1245
  /**
package/src/llm.js CHANGED
@@ -18,7 +18,7 @@ import dotenv from 'dotenv';
18
18
  import { verboseLog } from './utils/logging.js';
19
19
 
20
20
  // Load env variables if present; do not enforce key at import time
21
- dotenv.config();
21
+ dotenv.config({ quiet: true });
22
22
 
23
23
  let anthropic = null;
24
24