@eldrforge/kodrdriv 1.2.1 → 1.2.3
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/dist/application.js +7 -19
- package/dist/application.js.map +1 -1
- package/dist/arguments.js +345 -96
- package/dist/arguments.js.map +1 -1
- package/dist/commands/clean.js +1 -1
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/commit.js +59 -14
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/link.js +200 -11
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/release.js +9 -3
- package/dist/commands/release.js.map +1 -1
- package/dist/commands/review.js +356 -145
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/tree.js +111 -250
- package/dist/commands/tree.js.map +1 -1
- package/dist/commands/unlink.js +238 -10
- package/dist/commands/unlink.js.map +1 -1
- package/dist/constants.js +28 -12
- package/dist/constants.js.map +1 -1
- package/dist/content/issues.js +1 -1
- package/dist/error/CommandErrors.js +8 -1
- package/dist/error/CommandErrors.js.map +1 -1
- package/dist/prompt/commit.js +6 -0
- package/dist/prompt/commit.js.map +1 -1
- package/dist/prompt/instructions/review.md +13 -3
- package/dist/util/openai.js +70 -9
- package/dist/util/openai.js.map +1 -1
- package/dist/util/validation.js +24 -1
- package/dist/util/validation.js.map +1 -1
- package/package.json +2 -2
- package/test-external-unlink/package.json +16 -0
- package/test-externals/package.json +8 -0
- package/test-review-flow.sh +15 -0
- package/test-sort-files/alpha.md +3 -0
- package/test-sort-files/middle.txt +3 -0
- package/test-sort-files/zebra.txt +3 -0
- package/test_output.txt +161 -0
- package/dist/types.js +0 -140
- package/dist/types.js.map +0 -1
package/dist/commands/review.js
CHANGED
|
@@ -2,20 +2,122 @@
|
|
|
2
2
|
import { Formatter } from '@riotprompt/riotprompt';
|
|
3
3
|
import { ValidationError, FileOperationError, CommandError } from '../error/CommandErrors.js';
|
|
4
4
|
import { getLogger } from '../logging.js';
|
|
5
|
-
import { getModelForCommand, createCompletion } from '../util/openai.js';
|
|
5
|
+
import { getModelForCommand, createCompletion, getOpenAIMaxOutputTokensForCommand, getOpenAIReasoningForCommand } from '../util/openai.js';
|
|
6
6
|
import { createPrompt } from '../prompt/review.js';
|
|
7
|
-
import { create } from '../content/log.js';
|
|
7
|
+
import { create as create$1 } from '../content/log.js';
|
|
8
8
|
import { getReviewExcludedPatterns, getRecentDiffsForReview } from '../content/diff.js';
|
|
9
9
|
import { get } from '../content/releaseNotes.js';
|
|
10
|
-
import { get as get$1
|
|
10
|
+
import { handleIssueCreation, get as get$1 } from '../content/issues.js';
|
|
11
11
|
import { DEFAULT_EXCLUDED_PATTERNS, DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
|
|
12
|
-
import { getTimestampedReviewNotesFilename, getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFilename
|
|
13
|
-
import { create
|
|
12
|
+
import { getTimestampedReviewNotesFilename, getOutputPath, getTimestampedReviewFilename, getTimestampedResponseFilename, getTimestampedRequestFilename } from '../util/general.js';
|
|
13
|
+
import { create } from '../util/storage.js';
|
|
14
|
+
import { getUserChoice } from '../util/interactive.js';
|
|
14
15
|
import path__default from 'path';
|
|
15
16
|
import os__default from 'os';
|
|
16
17
|
import { spawn } from 'child_process';
|
|
17
18
|
import fs__default from 'fs/promises';
|
|
18
19
|
|
|
20
|
+
// Utility function to read a review note from a file
|
|
21
|
+
const readReviewNoteFromFile = async (filePath)=>{
|
|
22
|
+
const logger = getLogger();
|
|
23
|
+
try {
|
|
24
|
+
logger.debug(`Reading review note from file: ${filePath}`);
|
|
25
|
+
const content = await fs__default.readFile(filePath, 'utf8');
|
|
26
|
+
if (!content.trim()) {
|
|
27
|
+
throw new ValidationError(`Review file is empty: ${filePath}`);
|
|
28
|
+
}
|
|
29
|
+
logger.debug(`Successfully read review note from file: ${filePath} (${content.length} characters)`);
|
|
30
|
+
return content.trim();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT') {
|
|
33
|
+
throw new FileOperationError(`Review file not found: ${filePath}`, filePath, error);
|
|
34
|
+
}
|
|
35
|
+
if (error instanceof ValidationError) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
throw new FileOperationError(`Failed to read review file: ${error.message}`, filePath, error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// Utility function to get all review files in a directory
|
|
42
|
+
const getReviewFilesInDirectory = async (directoryPath)=>{
|
|
43
|
+
const logger = getLogger();
|
|
44
|
+
try {
|
|
45
|
+
logger.debug(`Scanning directory for review files: ${directoryPath}`);
|
|
46
|
+
const entries = await fs__default.readdir(directoryPath, {
|
|
47
|
+
withFileTypes: true
|
|
48
|
+
});
|
|
49
|
+
// Filter for regular files (not directories) and get full paths
|
|
50
|
+
const files = entries.filter((entry)=>entry.isFile()).map((entry)=>path__default.join(directoryPath, entry.name)).sort(); // Sort alphabetically
|
|
51
|
+
logger.debug(`Found ${files.length} files in directory: ${directoryPath}`);
|
|
52
|
+
return files;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code === 'ENOENT') {
|
|
55
|
+
throw new FileOperationError(`Directory not found: ${directoryPath}`, directoryPath, error);
|
|
56
|
+
}
|
|
57
|
+
throw new FileOperationError(`Failed to read directory: ${directoryPath}`, directoryPath, error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// New function for file selection phase
|
|
61
|
+
const selectFilesForProcessing = async (reviewFiles, senditMode)=>{
|
|
62
|
+
const logger = getLogger();
|
|
63
|
+
if (senditMode) {
|
|
64
|
+
logger.info(`Auto-selecting all ${reviewFiles.length} files for processing (--sendit mode)`);
|
|
65
|
+
return reviewFiles;
|
|
66
|
+
}
|
|
67
|
+
// Check if we're in an interactive environment
|
|
68
|
+
if (!isTTYSafe()) {
|
|
69
|
+
logger.warn(`Non-interactive environment detected, selecting all files for processing`);
|
|
70
|
+
return reviewFiles;
|
|
71
|
+
}
|
|
72
|
+
logger.info(`\n📁 File Selection Phase`);
|
|
73
|
+
logger.info(`Found ${reviewFiles.length} files to review. Select which ones to process:`);
|
|
74
|
+
logger.info(`[c] Confirm this file for processing`);
|
|
75
|
+
logger.info(`[s] Skip this file`);
|
|
76
|
+
logger.info(`[a] Abort the entire review process`);
|
|
77
|
+
logger.info(``);
|
|
78
|
+
const selectedFiles = [];
|
|
79
|
+
let shouldAbort = false;
|
|
80
|
+
for(let i = 0; i < reviewFiles.length; i++){
|
|
81
|
+
const filePath = reviewFiles[i];
|
|
82
|
+
logger.info(`File ${i + 1}/${reviewFiles.length}: ${filePath}`);
|
|
83
|
+
const choice = await getUserChoice(`Select action for this file:`, [
|
|
84
|
+
{
|
|
85
|
+
key: 'c',
|
|
86
|
+
label: 'Confirm and process'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 's',
|
|
90
|
+
label: 'Skip this file'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: 'a',
|
|
94
|
+
label: 'Abort entire review'
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
if (choice === 'a') {
|
|
98
|
+
logger.info(`🛑 Aborting review process as requested`);
|
|
99
|
+
shouldAbort = true;
|
|
100
|
+
break;
|
|
101
|
+
} else if (choice === 'c') {
|
|
102
|
+
selectedFiles.push(filePath);
|
|
103
|
+
logger.info(`✅ File selected for processing: ${filePath}`);
|
|
104
|
+
} else if (choice === 's') {
|
|
105
|
+
logger.info(`⏭️ File skipped: ${filePath}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (shouldAbort) {
|
|
109
|
+
throw new Error('Review process aborted by user');
|
|
110
|
+
}
|
|
111
|
+
if (selectedFiles.length === 0) {
|
|
112
|
+
throw new Error('No files were selected for processing');
|
|
113
|
+
}
|
|
114
|
+
logger.info(`\n📋 File selection complete. ${selectedFiles.length} files selected for processing:`);
|
|
115
|
+
selectedFiles.forEach((file, index)=>{
|
|
116
|
+
logger.info(` ${index + 1}. ${file}`);
|
|
117
|
+
});
|
|
118
|
+
logger.info(``);
|
|
119
|
+
return selectedFiles;
|
|
120
|
+
};
|
|
19
121
|
// Safe temp file handling with proper permissions and validation
|
|
20
122
|
const createSecureTempFile = async ()=>{
|
|
21
123
|
const logger = getLogger();
|
|
@@ -192,99 +294,10 @@ const safeWriteFile = async (filePath, content, encoding = 'utf-8')=>{
|
|
|
192
294
|
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
|
|
193
295
|
}
|
|
194
296
|
};
|
|
195
|
-
|
|
196
|
-
|
|
297
|
+
// Helper function to process a single review note
|
|
298
|
+
const processSingleReview = async (reviewNote, runConfig, outputDirectory)=>{
|
|
299
|
+
var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review_context, _runConfig_review4, _runConfig_review5, _analysisResult_issues;
|
|
197
300
|
const logger = getLogger();
|
|
198
|
-
const isDryRun = runConfig.dryRun || false;
|
|
199
|
-
// Show configuration even in dry-run mode
|
|
200
|
-
logger.debug('Review context configuration:');
|
|
201
|
-
logger.debug(' Include commit history: %s', (_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory);
|
|
202
|
-
logger.debug(' Include recent diffs: %s', (_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs);
|
|
203
|
-
logger.debug(' Include release notes: %s', (_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes);
|
|
204
|
-
logger.debug(' Include GitHub issues: %s', (_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues);
|
|
205
|
-
logger.debug(' Commit history limit: %d', (_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : _runConfig_review4.commitHistoryLimit);
|
|
206
|
-
logger.debug(' Diff history limit: %d', (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.diffHistoryLimit);
|
|
207
|
-
logger.debug(' Release notes limit: %d', (_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.releaseNotesLimit);
|
|
208
|
-
logger.debug(' GitHub issues limit: %d', (_runConfig_review7 = runConfig.review) === null || _runConfig_review7 === void 0 ? void 0 : _runConfig_review7.githubIssuesLimit);
|
|
209
|
-
logger.debug(' Sendit mode (auto-create issues): %s', (_runConfig_review8 = runConfig.review) === null || _runConfig_review8 === void 0 ? void 0 : _runConfig_review8.sendit);
|
|
210
|
-
if (isDryRun) {
|
|
211
|
-
var _runConfig_review18, _runConfig_review19;
|
|
212
|
-
logger.info('DRY RUN: Would analyze provided note for review');
|
|
213
|
-
logger.info('DRY RUN: Would gather additional context based on configuration above');
|
|
214
|
-
logger.info('DRY RUN: Would analyze note and identify issues');
|
|
215
|
-
if ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) {
|
|
216
|
-
logger.info('DRY RUN: Would automatically create GitHub issues (sendit mode enabled)');
|
|
217
|
-
} else {
|
|
218
|
-
logger.info('DRY RUN: Would prompt for confirmation before creating GitHub issues');
|
|
219
|
-
}
|
|
220
|
-
// Show what exclusion patterns would be used in dry-run mode
|
|
221
|
-
if ((_runConfig_review19 = runConfig.review) === null || _runConfig_review19 === void 0 ? void 0 : _runConfig_review19.includeRecentDiffs) {
|
|
222
|
-
var _runConfig_excludedPatterns;
|
|
223
|
-
const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
|
|
224
|
-
const reviewExcluded = getReviewExcludedPatterns(basePatterns);
|
|
225
|
-
logger.info('DRY RUN: Would use %d exclusion patterns for diff context', reviewExcluded.length);
|
|
226
|
-
logger.debug('DRY RUN: Sample exclusions: %s', reviewExcluded.slice(0, 15).join(', ') + (reviewExcluded.length > 15 ? '...' : ''));
|
|
227
|
-
}
|
|
228
|
-
return 'DRY RUN: Review command would analyze note, gather context, and create GitHub issues';
|
|
229
|
-
}
|
|
230
|
-
// Enhanced TTY check with proper error handling
|
|
231
|
-
const isInteractive = isTTYSafe();
|
|
232
|
-
if (!isInteractive && !((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.sendit)) {
|
|
233
|
-
logger.error('❌ STDIN is piped but --sendit flag is not enabled');
|
|
234
|
-
logger.error(' Interactive prompts cannot be used when input is piped');
|
|
235
|
-
logger.error(' Solutions:');
|
|
236
|
-
logger.error(' • Add --sendit flag to auto-create all issues');
|
|
237
|
-
logger.error(' • Use terminal input instead of piping');
|
|
238
|
-
logger.error(' • Example: echo "note" | kodrdriv review --sendit');
|
|
239
|
-
throw new ValidationError('Piped input requires --sendit flag for non-interactive operation');
|
|
240
|
-
}
|
|
241
|
-
// Get the review note from configuration
|
|
242
|
-
let reviewNote = (_runConfig_review10 = runConfig.review) === null || _runConfig_review10 === void 0 ? void 0 : _runConfig_review10.note;
|
|
243
|
-
// If no review note was provided via CLI arg or STDIN, open the user's editor to capture it.
|
|
244
|
-
if (!reviewNote || !reviewNote.trim()) {
|
|
245
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
246
|
-
let tmpFilePath = null;
|
|
247
|
-
try {
|
|
248
|
-
var _runConfig_review20;
|
|
249
|
-
// Create secure temporary file
|
|
250
|
-
tmpFilePath = await createSecureTempFile();
|
|
251
|
-
// Pre-populate the file with a helpful header so users know what to do.
|
|
252
|
-
const templateContent = [
|
|
253
|
-
'# Kodrdriv Review Note',
|
|
254
|
-
'',
|
|
255
|
-
'# Please enter your review note below. Lines starting with "#" will be ignored.',
|
|
256
|
-
'# Save and close the editor when you are done.',
|
|
257
|
-
'',
|
|
258
|
-
''
|
|
259
|
-
].join('\n');
|
|
260
|
-
await safeWriteFile(tmpFilePath, templateContent);
|
|
261
|
-
logger.info(`No review note provided – opening ${editor} to capture input...`);
|
|
262
|
-
// Open the editor with optional timeout protection
|
|
263
|
-
const editorTimeout = (_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : _runConfig_review20.editorTimeout; // No default timeout - let user take their time
|
|
264
|
-
await openEditorWithTimeout(editor, tmpFilePath, editorTimeout);
|
|
265
|
-
// Read the file back in, stripping comment lines and whitespace.
|
|
266
|
-
const fileContent = (await fs__default.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
|
|
267
|
-
if (!fileContent) {
|
|
268
|
-
throw new ValidationError('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
|
|
269
|
-
}
|
|
270
|
-
reviewNote = fileContent;
|
|
271
|
-
// If the original runConfig.review object exists, update it so downstream code has the note.
|
|
272
|
-
if (runConfig.review) {
|
|
273
|
-
runConfig.review.note = reviewNote;
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
logger.error(`Failed to capture review note via editor: ${error.message}`);
|
|
277
|
-
throw error;
|
|
278
|
-
} finally{
|
|
279
|
-
// Always clean up the temp file
|
|
280
|
-
if (tmpFilePath) {
|
|
281
|
-
await cleanupTempFile(tmpFilePath);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
logger.info('📝 Starting review analysis...');
|
|
286
|
-
logger.debug('Review note: %s', reviewNote);
|
|
287
|
-
logger.debug('Review note length: %d characters', reviewNote.length);
|
|
288
301
|
// Gather additional context based on configuration with improved error handling
|
|
289
302
|
let logContext = '';
|
|
290
303
|
let diffContext = '';
|
|
@@ -292,10 +305,10 @@ const executeInternal = async (runConfig)=>{
|
|
|
292
305
|
let issuesContext = '';
|
|
293
306
|
const contextErrors = [];
|
|
294
307
|
// Fetch commit history if enabled
|
|
295
|
-
if ((
|
|
308
|
+
if ((_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory) {
|
|
296
309
|
try {
|
|
297
310
|
logger.debug('Fetching recent commit history...');
|
|
298
|
-
const log = await create({
|
|
311
|
+
const log = await create$1({
|
|
299
312
|
limit: runConfig.review.commitHistoryLimit
|
|
300
313
|
});
|
|
301
314
|
const logContent = await log.get();
|
|
@@ -310,11 +323,11 @@ const executeInternal = async (runConfig)=>{
|
|
|
310
323
|
}
|
|
311
324
|
}
|
|
312
325
|
// Fetch recent diffs if enabled
|
|
313
|
-
if ((
|
|
326
|
+
if ((_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs) {
|
|
314
327
|
try {
|
|
315
328
|
logger.debug('Fetching recent commit diffs...');
|
|
316
|
-
var
|
|
317
|
-
const basePatterns = (
|
|
329
|
+
var _runConfig_excludedPatterns;
|
|
330
|
+
const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
|
|
318
331
|
const recentDiffs = await getRecentDiffsForReview({
|
|
319
332
|
limit: runConfig.review.diffHistoryLimit,
|
|
320
333
|
baseExcludedPatterns: basePatterns
|
|
@@ -330,7 +343,7 @@ const executeInternal = async (runConfig)=>{
|
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
// Fetch release notes if enabled
|
|
333
|
-
if ((
|
|
346
|
+
if ((_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes) {
|
|
334
347
|
try {
|
|
335
348
|
logger.debug('Fetching recent release notes from GitHub...');
|
|
336
349
|
const releaseNotesContent = await get({
|
|
@@ -347,7 +360,7 @@ const executeInternal = async (runConfig)=>{
|
|
|
347
360
|
}
|
|
348
361
|
}
|
|
349
362
|
// Fetch GitHub issues if enabled
|
|
350
|
-
if ((
|
|
363
|
+
if ((_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues) {
|
|
351
364
|
try {
|
|
352
365
|
logger.debug('Fetching open GitHub issues...');
|
|
353
366
|
issuesContext = await get$1({
|
|
@@ -364,11 +377,11 @@ const executeInternal = async (runConfig)=>{
|
|
|
364
377
|
}
|
|
365
378
|
// Report context gathering results
|
|
366
379
|
if (contextErrors.length > 0) {
|
|
367
|
-
var
|
|
380
|
+
var _runConfig_review6;
|
|
368
381
|
logger.warn(`Context gathering completed with ${contextErrors.length} error(s):`);
|
|
369
382
|
contextErrors.forEach((error)=>logger.warn(` - ${error}`));
|
|
370
383
|
// For critical operations, consider failing if too many context sources fail
|
|
371
|
-
const maxContextErrors = ((
|
|
384
|
+
const maxContextErrors = ((_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.maxContextErrors) || contextErrors.length; // Default: allow all errors
|
|
372
385
|
if (contextErrors.length > maxContextErrors) {
|
|
373
386
|
throw new Error(`Too many context gathering errors (${contextErrors.length}), aborting review. Consider checking your configuration and network connectivity.`);
|
|
374
387
|
}
|
|
@@ -381,7 +394,7 @@ const executeInternal = async (runConfig)=>{
|
|
|
381
394
|
logger.debug(' - Diff context: %d chars', diffContext.length);
|
|
382
395
|
logger.debug(' - Release notes context: %d chars', releaseNotesContext.length);
|
|
383
396
|
logger.debug(' - Issues context: %d chars', issuesContext.length);
|
|
384
|
-
logger.debug(' - User context: %d chars', ((
|
|
397
|
+
logger.debug(' - User context: %d chars', ((_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : (_runConfig_review_context = _runConfig_review4.context) === null || _runConfig_review_context === void 0 ? void 0 : _runConfig_review_context.length) || 0);
|
|
385
398
|
const promptConfig = {
|
|
386
399
|
overridePaths: runConfig.discoveredConfigDirs || [],
|
|
387
400
|
overrides: runConfig.overrides || false
|
|
@@ -390,47 +403,13 @@ const executeInternal = async (runConfig)=>{
|
|
|
390
403
|
notes: reviewNote
|
|
391
404
|
};
|
|
392
405
|
const promptContext = {
|
|
393
|
-
context: (
|
|
406
|
+
context: (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.context,
|
|
394
407
|
logContext,
|
|
395
408
|
diffContext,
|
|
396
409
|
releaseNotesContext,
|
|
397
410
|
issuesContext
|
|
398
411
|
};
|
|
399
412
|
const prompt = await createPrompt(promptConfig, promptContent, promptContext);
|
|
400
|
-
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
401
|
-
const storage = create$1({
|
|
402
|
-
log: logger.info
|
|
403
|
-
});
|
|
404
|
-
await storage.ensureDirectory(outputDirectory);
|
|
405
|
-
// Save timestamped copy of review notes and context to output directory
|
|
406
|
-
try {
|
|
407
|
-
var _runConfig_review_context1, _runConfig_review22;
|
|
408
|
-
// Save the original review note
|
|
409
|
-
const reviewNotesFilename = getTimestampedReviewNotesFilename();
|
|
410
|
-
const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
|
|
411
|
-
let reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
|
|
412
|
-
// Add all context sections if they exist
|
|
413
|
-
if (logContext.trim()) {
|
|
414
|
-
reviewNotesContent += `# Commit History Context\n\n${logContext}\n\n`;
|
|
415
|
-
}
|
|
416
|
-
if (diffContext.trim()) {
|
|
417
|
-
reviewNotesContent += `# Recent Diffs Context\n\n${diffContext}\n\n`;
|
|
418
|
-
}
|
|
419
|
-
if (releaseNotesContext.trim()) {
|
|
420
|
-
reviewNotesContent += `# Release Notes Context\n\n${releaseNotesContext}\n\n`;
|
|
421
|
-
}
|
|
422
|
-
if (issuesContext.trim()) {
|
|
423
|
-
reviewNotesContent += `# GitHub Issues Context\n\n${issuesContext}\n\n`;
|
|
424
|
-
}
|
|
425
|
-
if ((_runConfig_review22 = runConfig.review) === null || _runConfig_review22 === void 0 ? void 0 : (_runConfig_review_context1 = _runConfig_review22.context) === null || _runConfig_review_context1 === void 0 ? void 0 : _runConfig_review_context1.trim()) {
|
|
426
|
-
reviewNotesContent += `# User Context\n\n${runConfig.review.context}\n\n`;
|
|
427
|
-
}
|
|
428
|
-
await safeWriteFile(reviewNotesPath, reviewNotesContent);
|
|
429
|
-
logger.debug('Saved timestamped review notes and context: %s', reviewNotesPath);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
logger.warn('Failed to save timestamped review notes: %s', error.message);
|
|
432
|
-
// Don't fail the entire operation for this
|
|
433
|
-
}
|
|
434
413
|
const modelToUse = getModelForCommand(runConfig, 'review');
|
|
435
414
|
const request = Formatter.create({
|
|
436
415
|
logger
|
|
@@ -439,6 +418,8 @@ const executeInternal = async (runConfig)=>{
|
|
|
439
418
|
try {
|
|
440
419
|
const rawResult = await createCompletion(request.messages, {
|
|
441
420
|
model: modelToUse,
|
|
421
|
+
openaiReasoning: getOpenAIReasoningForCommand(runConfig, 'review'),
|
|
422
|
+
openaiMaxOutputTokens: getOpenAIMaxOutputTokensForCommand(runConfig, 'review'),
|
|
442
423
|
responseFormat: {
|
|
443
424
|
type: 'json_object'
|
|
444
425
|
},
|
|
@@ -473,8 +454,238 @@ const executeInternal = async (runConfig)=>{
|
|
|
473
454
|
logger.warn('Failed to save timestamped review analysis: %s', error.message);
|
|
474
455
|
// Don't fail the entire operation for this
|
|
475
456
|
}
|
|
457
|
+
return analysisResult;
|
|
458
|
+
};
|
|
459
|
+
const executeInternal = async (runConfig)=>{
|
|
460
|
+
var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review4, _runConfig_review5, _runConfig_review6, _runConfig_review7, _runConfig_review8, _runConfig_review9, _runConfig_review10, _runConfig_review11, _runConfig_review12, _runConfig_review13, _runConfig_review14, _runConfig_review15, _runConfig_review16, _runConfig_review17, _runConfig_review18;
|
|
461
|
+
const logger = getLogger();
|
|
462
|
+
const isDryRun = runConfig.dryRun || false;
|
|
463
|
+
// Show configuration even in dry-run mode
|
|
464
|
+
logger.debug('Review context configuration:');
|
|
465
|
+
logger.debug(' Include commit history: %s', (_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory);
|
|
466
|
+
logger.debug(' Include recent diffs: %s', (_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs);
|
|
467
|
+
logger.debug(' Include release notes: %s', (_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes);
|
|
468
|
+
logger.debug(' Include GitHub issues: %s', (_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues);
|
|
469
|
+
logger.debug(' Commit history limit: %d', (_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : _runConfig_review4.commitHistoryLimit);
|
|
470
|
+
logger.debug(' Diff history limit: %d', (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.diffHistoryLimit);
|
|
471
|
+
logger.debug(' Release notes limit: %d', (_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.releaseNotesLimit);
|
|
472
|
+
logger.debug(' GitHub issues limit: %d', (_runConfig_review7 = runConfig.review) === null || _runConfig_review7 === void 0 ? void 0 : _runConfig_review7.githubIssuesLimit);
|
|
473
|
+
logger.debug(' Sendit mode (auto-create issues): %s', (_runConfig_review8 = runConfig.review) === null || _runConfig_review8 === void 0 ? void 0 : _runConfig_review8.sendit);
|
|
474
|
+
logger.debug(' File: %s', ((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.file) || 'not specified');
|
|
475
|
+
logger.debug(' Directory: %s', ((_runConfig_review10 = runConfig.review) === null || _runConfig_review10 === void 0 ? void 0 : _runConfig_review10.directory) || 'not specified');
|
|
476
|
+
if (isDryRun) {
|
|
477
|
+
var _runConfig_review19, _runConfig_review20, _runConfig_review21, _runConfig_review22, _runConfig_review23;
|
|
478
|
+
if ((_runConfig_review19 = runConfig.review) === null || _runConfig_review19 === void 0 ? void 0 : _runConfig_review19.file) {
|
|
479
|
+
logger.info('DRY RUN: Would read review note from file: %s', runConfig.review.file);
|
|
480
|
+
} else if ((_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : _runConfig_review20.directory) {
|
|
481
|
+
logger.info('DRY RUN: Would process review files in directory: %s', runConfig.review.directory);
|
|
482
|
+
logger.info('DRY RUN: Would first select which files to process, then analyze selected files');
|
|
483
|
+
} else if ((_runConfig_review21 = runConfig.review) === null || _runConfig_review21 === void 0 ? void 0 : _runConfig_review21.note) {
|
|
484
|
+
logger.info('DRY RUN: Would analyze provided note for review');
|
|
485
|
+
} else {
|
|
486
|
+
logger.info('DRY RUN: Would open editor to capture review note');
|
|
487
|
+
}
|
|
488
|
+
logger.info('DRY RUN: Would gather additional context based on configuration above');
|
|
489
|
+
logger.info('DRY RUN: Would analyze note and identify issues');
|
|
490
|
+
if ((_runConfig_review22 = runConfig.review) === null || _runConfig_review22 === void 0 ? void 0 : _runConfig_review22.sendit) {
|
|
491
|
+
logger.info('DRY RUN: Would automatically create GitHub issues (sendit mode enabled)');
|
|
492
|
+
} else {
|
|
493
|
+
logger.info('DRY RUN: Would prompt for confirmation before creating GitHub issues');
|
|
494
|
+
}
|
|
495
|
+
// Show what exclusion patterns would be used in dry-run mode
|
|
496
|
+
if ((_runConfig_review23 = runConfig.review) === null || _runConfig_review23 === void 0 ? void 0 : _runConfig_review23.includeRecentDiffs) {
|
|
497
|
+
var _runConfig_excludedPatterns;
|
|
498
|
+
const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
|
|
499
|
+
const reviewExcluded = getReviewExcludedPatterns(basePatterns);
|
|
500
|
+
logger.info('DRY RUN: Would use %d exclusion patterns for diff context', reviewExcluded.length);
|
|
501
|
+
logger.debug('DRY RUN: Sample exclusions: %s', reviewExcluded.slice(0, 15).join(', ') + (reviewExcluded.length > 15 ? '...' : ''));
|
|
502
|
+
}
|
|
503
|
+
return 'DRY RUN: Review command would analyze note, gather context, and create GitHub issues';
|
|
504
|
+
}
|
|
505
|
+
// Enhanced TTY check with proper error handling
|
|
506
|
+
const isInteractive = isTTYSafe();
|
|
507
|
+
if (!isInteractive && !((_runConfig_review11 = runConfig.review) === null || _runConfig_review11 === void 0 ? void 0 : _runConfig_review11.sendit)) {
|
|
508
|
+
logger.error('❌ STDIN is piped but --sendit flag is not enabled');
|
|
509
|
+
logger.error(' Interactive prompts cannot be used when input is piped');
|
|
510
|
+
logger.error(' Solutions:');
|
|
511
|
+
logger.error(' • Add --sendit flag to auto-create all issues');
|
|
512
|
+
logger.error(' • Use terminal input instead of piping');
|
|
513
|
+
logger.error(' • Example: echo "note" | kodrdriv review --sendit');
|
|
514
|
+
throw new ValidationError('Piped input requires --sendit flag for non-interactive operation');
|
|
515
|
+
}
|
|
516
|
+
// Get the review note from configuration
|
|
517
|
+
let reviewNote = (_runConfig_review12 = runConfig.review) === null || _runConfig_review12 === void 0 ? void 0 : _runConfig_review12.note;
|
|
518
|
+
let reviewFiles = [];
|
|
519
|
+
// Check if we should process a single file
|
|
520
|
+
if ((_runConfig_review13 = runConfig.review) === null || _runConfig_review13 === void 0 ? void 0 : _runConfig_review13.file) {
|
|
521
|
+
logger.info(`📁 Reading review note from file: ${runConfig.review.file}`);
|
|
522
|
+
reviewNote = await readReviewNoteFromFile(runConfig.review.file);
|
|
523
|
+
reviewFiles = [
|
|
524
|
+
runConfig.review.file
|
|
525
|
+
];
|
|
526
|
+
} else if ((_runConfig_review14 = runConfig.review) === null || _runConfig_review14 === void 0 ? void 0 : _runConfig_review14.directory) {
|
|
527
|
+
var _runConfig_review24;
|
|
528
|
+
logger.info(`📁 Processing review files in directory: ${runConfig.review.directory}`);
|
|
529
|
+
reviewFiles = await getReviewFilesInDirectory(runConfig.review.directory);
|
|
530
|
+
if (reviewFiles.length === 0) {
|
|
531
|
+
throw new ValidationError(`No review files found in directory: ${runConfig.review.directory}`);
|
|
532
|
+
}
|
|
533
|
+
logger.info(`📁 Found ${reviewFiles.length} files to process`);
|
|
534
|
+
// Set a dummy reviewNote for directory mode to satisfy validation
|
|
535
|
+
// The actual review notes will be read from each file during processing
|
|
536
|
+
reviewNote = `Processing ${reviewFiles.length} files from directory`;
|
|
537
|
+
// If not in sendit mode, explain the two-phase process
|
|
538
|
+
if (!((_runConfig_review24 = runConfig.review) === null || _runConfig_review24 === void 0 ? void 0 : _runConfig_review24.sendit)) {
|
|
539
|
+
logger.info(`📝 Interactive mode: You will first select which files to process, then they will be analyzed in order.`);
|
|
540
|
+
logger.info(`📝 Use --sendit to process all files automatically without confirmation.`);
|
|
541
|
+
}
|
|
542
|
+
} else if ((_runConfig_review15 = runConfig.review) === null || _runConfig_review15 === void 0 ? void 0 : _runConfig_review15.note) {
|
|
543
|
+
reviewNote = runConfig.review.note;
|
|
544
|
+
reviewFiles = [
|
|
545
|
+
'provided note'
|
|
546
|
+
];
|
|
547
|
+
} else {
|
|
548
|
+
// Open editor to capture review note
|
|
549
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
550
|
+
let tmpFilePath = null;
|
|
551
|
+
try {
|
|
552
|
+
var _runConfig_review25;
|
|
553
|
+
// Create secure temporary file
|
|
554
|
+
tmpFilePath = await createSecureTempFile();
|
|
555
|
+
// Pre-populate the file with a helpful header so users know what to do.
|
|
556
|
+
const templateContent = [
|
|
557
|
+
'# Kodrdriv Review Note',
|
|
558
|
+
'',
|
|
559
|
+
'# Please enter your review note below. Lines starting with "#" will be ignored.',
|
|
560
|
+
'# Save and close the editor when you are done.',
|
|
561
|
+
'',
|
|
562
|
+
''
|
|
563
|
+
].join('\n');
|
|
564
|
+
await safeWriteFile(tmpFilePath, templateContent);
|
|
565
|
+
logger.info(`No review note provided – opening ${editor} to capture input...`);
|
|
566
|
+
// Open the editor with optional timeout protection
|
|
567
|
+
const editorTimeout = (_runConfig_review25 = runConfig.review) === null || _runConfig_review25 === void 0 ? void 0 : _runConfig_review25.editorTimeout; // No default timeout - let user take their time
|
|
568
|
+
await openEditorWithTimeout(editor, tmpFilePath, editorTimeout);
|
|
569
|
+
// Read the file back in, stripping comment lines and whitespace.
|
|
570
|
+
const fileContent = (await fs__default.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
|
|
571
|
+
if (!fileContent) {
|
|
572
|
+
throw new ValidationError('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
|
|
573
|
+
}
|
|
574
|
+
reviewNote = fileContent;
|
|
575
|
+
// If the original runConfig.review object exists, update it so downstream code has the note.
|
|
576
|
+
if (runConfig.review) {
|
|
577
|
+
runConfig.review.note = reviewNote;
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
logger.error(`Failed to capture review note via editor: ${error.message}`);
|
|
581
|
+
throw error;
|
|
582
|
+
} finally{
|
|
583
|
+
// Always clean up the temp file
|
|
584
|
+
if (tmpFilePath) {
|
|
585
|
+
await cleanupTempFile(tmpFilePath);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
reviewFiles = [
|
|
589
|
+
'editor input'
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
if (!reviewNote || !reviewNote.trim()) {
|
|
593
|
+
throw new ValidationError('No review note provided or captured');
|
|
594
|
+
}
|
|
595
|
+
logger.info('📝 Starting review analysis...');
|
|
596
|
+
logger.debug('Review note: %s', reviewNote);
|
|
597
|
+
logger.debug('Review note length: %d characters', reviewNote.length);
|
|
598
|
+
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
599
|
+
const storage = create({
|
|
600
|
+
log: logger.info
|
|
601
|
+
});
|
|
602
|
+
await storage.ensureDirectory(outputDirectory);
|
|
603
|
+
// Save timestamped copy of review notes to output directory
|
|
604
|
+
try {
|
|
605
|
+
const reviewNotesFilename = getTimestampedReviewNotesFilename();
|
|
606
|
+
const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
|
|
607
|
+
const reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
|
|
608
|
+
await safeWriteFile(reviewNotesPath, reviewNotesContent);
|
|
609
|
+
logger.debug('Saved timestamped review notes: %s', reviewNotesPath);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logger.warn('Failed to save review notes: %s', error.message);
|
|
612
|
+
}
|
|
613
|
+
// Phase 1: File selection (only for directory mode)
|
|
614
|
+
let selectedFiles;
|
|
615
|
+
if ((_runConfig_review16 = runConfig.review) === null || _runConfig_review16 === void 0 ? void 0 : _runConfig_review16.directory) {
|
|
616
|
+
var _runConfig_review26;
|
|
617
|
+
selectedFiles = await selectFilesForProcessing(reviewFiles, ((_runConfig_review26 = runConfig.review) === null || _runConfig_review26 === void 0 ? void 0 : _runConfig_review26.sendit) || false);
|
|
618
|
+
} else {
|
|
619
|
+
// For single note mode, just use the note directly
|
|
620
|
+
selectedFiles = [
|
|
621
|
+
'single note'
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
// Phase 2: Process selected files in order
|
|
625
|
+
logger.info(`\n📝 Starting analysis phase...`);
|
|
626
|
+
const results = [];
|
|
627
|
+
const processedFiles = [];
|
|
628
|
+
if ((_runConfig_review17 = runConfig.review) === null || _runConfig_review17 === void 0 ? void 0 : _runConfig_review17.directory) {
|
|
629
|
+
// Directory mode: process each selected file
|
|
630
|
+
for(let i = 0; i < selectedFiles.length; i++){
|
|
631
|
+
const filePath = selectedFiles[i];
|
|
632
|
+
try {
|
|
633
|
+
logger.info(`📝 Processing file ${i + 1}/${selectedFiles.length}: ${filePath}`);
|
|
634
|
+
const fileNote = await readReviewNoteFromFile(filePath);
|
|
635
|
+
const fileResult = await processSingleReview(fileNote, runConfig, outputDirectory);
|
|
636
|
+
results.push(fileResult);
|
|
637
|
+
processedFiles.push(filePath);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
// Check if this is a critical error that should be propagated
|
|
640
|
+
if (error.message.includes('Too many context gathering errors')) {
|
|
641
|
+
throw error; // Propagate critical context errors
|
|
642
|
+
}
|
|
643
|
+
logger.warn(`Failed to process file ${filePath}: ${error.message}`);
|
|
644
|
+
// Continue with other files for non-critical errors
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
// Single note mode: process the note directly
|
|
649
|
+
try {
|
|
650
|
+
logger.info(`📝 Processing single review note`);
|
|
651
|
+
const fileResult = await processSingleReview(reviewNote, runConfig, outputDirectory);
|
|
652
|
+
results.push(fileResult);
|
|
653
|
+
processedFiles.push('single note');
|
|
654
|
+
} catch (error) {
|
|
655
|
+
logger.warn(`Failed to process review note: ${error.message}`);
|
|
656
|
+
throw error; // Re-throw for single note mode since there's only one item
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (results.length === 0) {
|
|
660
|
+
throw new ValidationError('No files were processed successfully');
|
|
661
|
+
}
|
|
662
|
+
// Combine results if we processed multiple files
|
|
663
|
+
let analysisResult;
|
|
664
|
+
if (results.length === 1) {
|
|
665
|
+
analysisResult = results[0];
|
|
666
|
+
} else {
|
|
667
|
+
logger.info(`✅ Successfully processed ${results.length} review files`);
|
|
668
|
+
// Create a combined summary
|
|
669
|
+
const totalIssues = results.reduce((sum, result)=>sum + result.totalIssues, 0);
|
|
670
|
+
const allIssues = results.flatMap((result)=>result.issues || []);
|
|
671
|
+
analysisResult = {
|
|
672
|
+
summary: `Combined analysis of ${results.length} review files. Total issues found: ${totalIssues}`,
|
|
673
|
+
totalIssues,
|
|
674
|
+
issues: allIssues
|
|
675
|
+
};
|
|
676
|
+
// Save combined results
|
|
677
|
+
try {
|
|
678
|
+
const combinedFilename = getTimestampedReviewFilename();
|
|
679
|
+
const combinedPath = getOutputPath(outputDirectory, combinedFilename);
|
|
680
|
+
const combinedContent = `# Combined Review Analysis Result\n\n` + `## Summary\n${analysisResult.summary}\n\n` + `## Total Issues Found\n${totalIssues}\n\n` + `## Files Processed\n${processedFiles.join('\n')}\n\n` + `## Issues\n\n${JSON.stringify(allIssues, null, 2)}\n\n` + `---\n\n*Combined analysis completed at ${new Date().toISOString()}*`;
|
|
681
|
+
await safeWriteFile(combinedPath, combinedContent);
|
|
682
|
+
logger.debug('Saved combined review analysis: %s', combinedPath);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.warn('Failed to save combined review analysis: %s', error.message);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
476
687
|
// Handle GitHub issue creation using the issues module
|
|
477
|
-
const senditMode = ((
|
|
688
|
+
const senditMode = ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) || false;
|
|
478
689
|
return await handleIssueCreation(analysisResult, senditMode);
|
|
479
690
|
};
|
|
480
691
|
const execute = async (runConfig)=>{
|
|
@@ -488,14 +699,14 @@ const execute = async (runConfig)=>{
|
|
|
488
699
|
}
|
|
489
700
|
if (error instanceof FileOperationError) {
|
|
490
701
|
logger.error(`review failed: ${error.message}`);
|
|
491
|
-
if (error.cause) {
|
|
702
|
+
if (error.cause && typeof error.cause === 'object' && 'message' in error.cause) {
|
|
492
703
|
logger.debug(`Caused by: ${error.cause.message}`);
|
|
493
704
|
}
|
|
494
705
|
throw error;
|
|
495
706
|
}
|
|
496
707
|
if (error instanceof CommandError) {
|
|
497
708
|
logger.error(`review failed: ${error.message}`);
|
|
498
|
-
if (error.cause) {
|
|
709
|
+
if (error.cause && typeof error.cause === 'object' && 'message' in error.cause) {
|
|
499
710
|
logger.debug(`Caused by: ${error.cause.message}`);
|
|
500
711
|
}
|
|
501
712
|
throw error;
|