@eldrforge/kodrdriv 0.0.33 → 0.0.37

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 (69) hide show
  1. package/README.md +46 -69
  2. package/dist/application.js +146 -0
  3. package/dist/application.js.map +1 -0
  4. package/dist/arguments.js +22 -21
  5. package/dist/arguments.js.map +1 -1
  6. package/dist/commands/audio-commit.js +43 -21
  7. package/dist/commands/audio-commit.js.map +1 -1
  8. package/dist/commands/audio-review.js +46 -38
  9. package/dist/commands/audio-review.js.map +1 -1
  10. package/dist/commands/clean.js +28 -12
  11. package/dist/commands/clean.js.map +1 -1
  12. package/dist/commands/commit.js +132 -39
  13. package/dist/commands/commit.js.map +1 -1
  14. package/dist/commands/link.js +177 -159
  15. package/dist/commands/link.js.map +1 -1
  16. package/dist/commands/publish-tree.js +19 -6
  17. package/dist/commands/publish-tree.js.map +1 -1
  18. package/dist/commands/publish.js +152 -82
  19. package/dist/commands/publish.js.map +1 -1
  20. package/dist/commands/release.js +21 -16
  21. package/dist/commands/release.js.map +1 -1
  22. package/dist/commands/review.js +286 -60
  23. package/dist/commands/review.js.map +1 -1
  24. package/dist/commands/select-audio.js +25 -8
  25. package/dist/commands/select-audio.js.map +1 -1
  26. package/dist/commands/unlink.js +349 -159
  27. package/dist/commands/unlink.js.map +1 -1
  28. package/dist/constants.js +14 -5
  29. package/dist/constants.js.map +1 -1
  30. package/dist/content/diff.js +7 -5
  31. package/dist/content/diff.js.map +1 -1
  32. package/dist/content/log.js +4 -1
  33. package/dist/content/log.js.map +1 -1
  34. package/dist/error/CancellationError.js +9 -0
  35. package/dist/error/CancellationError.js.map +1 -0
  36. package/dist/error/CommandErrors.js +120 -0
  37. package/dist/error/CommandErrors.js.map +1 -0
  38. package/dist/logging.js +55 -12
  39. package/dist/logging.js.map +1 -1
  40. package/dist/main.js +6 -131
  41. package/dist/main.js.map +1 -1
  42. package/dist/prompt/commit.js +4 -0
  43. package/dist/prompt/commit.js.map +1 -1
  44. package/dist/prompt/instructions/commit.md +33 -24
  45. package/dist/prompt/instructions/release.md +39 -5
  46. package/dist/prompt/release.js +41 -1
  47. package/dist/prompt/release.js.map +1 -1
  48. package/dist/types.js +9 -2
  49. package/dist/types.js.map +1 -1
  50. package/dist/util/github.js +71 -4
  51. package/dist/util/github.js.map +1 -1
  52. package/dist/util/npmOptimizations.js +174 -0
  53. package/dist/util/npmOptimizations.js.map +1 -0
  54. package/dist/util/openai.js +4 -2
  55. package/dist/util/openai.js.map +1 -1
  56. package/dist/util/performance.js +202 -0
  57. package/dist/util/performance.js.map +1 -0
  58. package/dist/util/safety.js +166 -0
  59. package/dist/util/safety.js.map +1 -0
  60. package/dist/util/storage.js +10 -0
  61. package/dist/util/storage.js.map +1 -1
  62. package/dist/util/validation.js +81 -0
  63. package/dist/util/validation.js.map +1 -0
  64. package/package.json +19 -18
  65. package/packages/components/package.json +4 -0
  66. package/packages/tools/package.json +4 -0
  67. package/packages/utils/package.json +4 -0
  68. package/scripts/pre-commit-hook.sh +52 -0
  69. package/test-project/package.json +1 -0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Formatter } from '@riotprompt/riotprompt';
3
+ import { ValidationError, FileOperationError, CommandError } from '../error/CommandErrors.js';
3
4
  import { getLogger } from '../logging.js';
4
5
  import { createCompletion } from '../util/openai.js';
5
6
  import { createPrompt } from '../prompt/review.js';
@@ -12,10 +13,171 @@ import { getTimestampedReviewNotesFilename, getOutputPath, getTimestampedRespons
12
13
  import { create as create$1 } from '../util/storage.js';
13
14
  import path from 'path';
14
15
  import os from 'os';
15
- import { spawnSync } from 'child_process';
16
+ import { spawn } from 'child_process';
16
17
  import fs from 'fs/promises';
17
18
 
18
- const execute = async (runConfig)=>{
19
+ // Safe temp file handling with proper permissions and validation
20
+ const createSecureTempFile = async ()=>{
21
+ const logger = getLogger();
22
+ const tmpDir = os.tmpdir();
23
+ // Ensure temp directory exists and is writable
24
+ try {
25
+ // Use constant value directly to avoid import restrictions
26
+ const W_OK = 2; // fs.constants.W_OK value
27
+ await fs.access(tmpDir, W_OK);
28
+ } catch (error) {
29
+ logger.error(`Temp directory not writable: ${tmpDir}`);
30
+ throw new FileOperationError(`Temp directory not writable: ${error.message}`, tmpDir, error);
31
+ }
32
+ const tmpFilePath = path.join(tmpDir, `kodrdriv_review_${Date.now()}_${Math.random().toString(36).substring(7)}.md`);
33
+ // Create file with restrictive permissions (owner read/write only)
34
+ try {
35
+ const fd = await fs.open(tmpFilePath, 'w', 0o600);
36
+ await fd.close();
37
+ logger.debug(`Created secure temp file: ${tmpFilePath}`);
38
+ return tmpFilePath;
39
+ } catch (error) {
40
+ logger.error(`Failed to create temp file: ${error.message}`);
41
+ throw new FileOperationError(`Failed to create temp file: ${error.message}`, 'temporary file', error);
42
+ }
43
+ };
44
+ // Safe file cleanup with proper error handling
45
+ const cleanupTempFile = async (filePath)=>{
46
+ const logger = getLogger();
47
+ try {
48
+ await fs.unlink(filePath);
49
+ logger.debug(`Cleaned up temp file: ${filePath}`);
50
+ } catch (error) {
51
+ // Only ignore ENOENT (file not found) errors, log others
52
+ if (error.code !== 'ENOENT') {
53
+ logger.warn(`Failed to cleanup temp file ${filePath}: ${error.message}`);
54
+ // Don't throw here to avoid masking the main operation
55
+ }
56
+ }
57
+ };
58
+ // Editor with timeout and proper error handling
59
+ const openEditorWithTimeout = async (editorCmd, filePath, timeoutMs = 300000)=>{
60
+ const logger = getLogger();
61
+ return new Promise((resolve, reject)=>{
62
+ logger.debug(`Opening editor: ${editorCmd} ${filePath} (timeout: ${timeoutMs}ms)`);
63
+ const child = spawn(editorCmd, [
64
+ filePath
65
+ ], {
66
+ stdio: 'inherit',
67
+ shell: false // Prevent shell injection
68
+ });
69
+ const timeout = setTimeout(()=>{
70
+ logger.warn(`Editor timed out after ${timeoutMs}ms, terminating...`);
71
+ child.kill('SIGTERM');
72
+ // Give it a moment to terminate gracefully, then force kill
73
+ setTimeout(()=>{
74
+ if (!child.killed) {
75
+ logger.warn('Editor did not terminate gracefully, force killing...');
76
+ child.kill('SIGKILL');
77
+ }
78
+ }, 5000);
79
+ reject(new Error(`Editor '${editorCmd}' timed out after ${timeoutMs}ms. Consider using a different editor or increasing the timeout.`));
80
+ }, timeoutMs);
81
+ child.on('exit', (code, signal)=>{
82
+ clearTimeout(timeout);
83
+ logger.debug(`Editor exited with code ${code}, signal ${signal}`);
84
+ if (signal === 'SIGTERM' || signal === 'SIGKILL') {
85
+ reject(new Error(`Editor was terminated (${signal})`));
86
+ } else if (code === 0) {
87
+ resolve();
88
+ } else {
89
+ reject(new Error(`Editor exited with non-zero code: ${code}`));
90
+ }
91
+ });
92
+ child.on('error', (error)=>{
93
+ clearTimeout(timeout);
94
+ logger.error(`Editor error: ${error.message}`);
95
+ reject(new Error(`Failed to launch editor '${editorCmd}': ${error.message}`));
96
+ });
97
+ });
98
+ };
99
+ // Validate API response format before use
100
+ const validateReviewResult = (data)=>{
101
+ if (!data || typeof data !== 'object') {
102
+ throw new Error('Invalid API response: expected object, got ' + typeof data);
103
+ }
104
+ if (typeof data.summary !== 'string') {
105
+ throw new Error('Invalid API response: missing or invalid summary field');
106
+ }
107
+ if (typeof data.totalIssues !== 'number' || data.totalIssues < 0) {
108
+ throw new Error('Invalid API response: missing or invalid totalIssues field');
109
+ }
110
+ if (data.issues && !Array.isArray(data.issues)) {
111
+ throw new Error('Invalid API response: issues field must be an array');
112
+ }
113
+ // Validate each issue if present
114
+ if (data.issues) {
115
+ for(let i = 0; i < data.issues.length; i++){
116
+ const issue = data.issues[i];
117
+ if (!issue || typeof issue !== 'object') {
118
+ throw new Error(`Invalid API response: issue ${i} is not an object`);
119
+ }
120
+ if (typeof issue.title !== 'string') {
121
+ throw new Error(`Invalid API response: issue ${i} missing title`);
122
+ }
123
+ if (typeof issue.priority !== 'string') {
124
+ throw new Error(`Invalid API response: issue ${i} missing priority`);
125
+ }
126
+ }
127
+ }
128
+ return data;
129
+ };
130
+ // Enhanced TTY detection with fallback handling
131
+ const isTTYSafe = ()=>{
132
+ try {
133
+ // Primary check
134
+ if (process.stdin.isTTY === false) {
135
+ return false;
136
+ }
137
+ // Additional checks for edge cases
138
+ if (process.stdin.isTTY === true) {
139
+ return true;
140
+ }
141
+ // Handle undefined case (some environments)
142
+ if (process.stdin.isTTY === undefined) {
143
+ // Check if we can reasonably assume interactive mode
144
+ return process.stdout.isTTY === true && process.stderr.isTTY === true;
145
+ }
146
+ return false;
147
+ } catch (error) {
148
+ // If TTY detection fails entirely, assume non-interactive
149
+ getLogger().debug(`TTY detection failed: ${error}, assuming non-interactive`);
150
+ return false;
151
+ }
152
+ };
153
+ // Safe file write with disk space and permission validation
154
+ const safeWriteFile = async (filePath, content, encoding = 'utf-8')=>{
155
+ const logger = getLogger();
156
+ try {
157
+ // Check if parent directory exists and is writable
158
+ const parentDir = path.dirname(filePath);
159
+ const W_OK = 2; // fs.constants.W_OK value
160
+ await fs.access(parentDir, W_OK);
161
+ // Check available disk space (basic check by writing a small test)
162
+ const testFile = `${filePath}.test`;
163
+ try {
164
+ await fs.writeFile(testFile, 'test', encoding);
165
+ await fs.unlink(testFile);
166
+ } catch (error) {
167
+ if (error.code === 'ENOSPC') {
168
+ throw new Error(`Insufficient disk space to write file: ${filePath}`);
169
+ }
170
+ throw error;
171
+ }
172
+ // Write the actual file
173
+ await fs.writeFile(filePath, content, encoding);
174
+ logger.debug(`Successfully wrote file: ${filePath} (${content.length} characters)`);
175
+ } catch (error) {
176
+ logger.error(`Failed to write file ${filePath}: ${error.message}`);
177
+ throw new Error(`Failed to write file ${filePath}: ${error.message}`);
178
+ }
179
+ };
180
+ const executeInternal = async (runConfig)=>{
19
181
  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_review_context, _runConfig_review15, _runConfig_review16, _analysisResult_issues, _runConfig_review17;
20
182
  const logger = getLogger();
21
183
  const isDryRun = runConfig.dryRun || false;
@@ -50,68 +212,70 @@ const execute = async (runConfig)=>{
50
212
  }
51
213
  return 'DRY RUN: Review command would analyze note, gather context, and create GitHub issues';
52
214
  }
53
- // Fail fast if STDIN is piped but sendit mode is not enabled
54
- if (!process.stdin.isTTY && !((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.sendit)) {
215
+ // Enhanced TTY check with proper error handling
216
+ const isInteractive = isTTYSafe();
217
+ if (!isInteractive && !((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.sendit)) {
55
218
  logger.error('❌ STDIN is piped but --sendit flag is not enabled');
56
219
  logger.error(' Interactive prompts cannot be used when input is piped');
57
220
  logger.error(' Solutions:');
58
221
  logger.error(' • Add --sendit flag to auto-create all issues');
59
222
  logger.error(' • Use terminal input instead of piping');
60
223
  logger.error(' • Example: echo "note" | kodrdriv review --sendit');
61
- throw new Error('Piped input requires --sendit flag for non-interactive operation');
224
+ throw new ValidationError('Piped input requires --sendit flag for non-interactive operation');
62
225
  }
63
226
  // Get the review note from configuration
64
227
  let reviewNote = (_runConfig_review10 = runConfig.review) === null || _runConfig_review10 === void 0 ? void 0 : _runConfig_review10.note;
65
228
  // If no review note was provided via CLI arg or STDIN, open the user's editor to capture it.
66
229
  if (!reviewNote || !reviewNote.trim()) {
67
230
  const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
68
- // Create a temporary file for the user to edit.
69
- const tmpDir = os.tmpdir();
70
- const tmpFilePath = path.join(tmpDir, `kodrdriv_review_${Date.now()}.md`);
71
- // Pre-populate the file with a helpful header so users know what to do.
72
- const templateContent = [
73
- '# Kodrdriv Review Note',
74
- '',
75
- '# Please enter your review note below. Lines starting with "#" will be ignored.',
76
- '# Save and close the editor when you are done.',
77
- '',
78
- ''
79
- ].join('\n');
80
- await fs.writeFile(tmpFilePath, templateContent, 'utf8');
81
- logger.info(`No review note provided – opening ${editor} to capture input...`);
82
- // Open the editor synchronously so execution resumes after the user closes it.
83
- const result = spawnSync(editor, [
84
- tmpFilePath
85
- ], {
86
- stdio: 'inherit'
87
- });
88
- if (result.error) {
89
- throw new Error(`Failed to launch editor '${editor}': ${result.error.message}`);
90
- }
91
- // Read the file back in, stripping comment lines and whitespace.
92
- const fileContent = (await fs.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
93
- // Clean up the temporary file (best-effort – ignore errors).
231
+ let tmpFilePath = null;
94
232
  try {
95
- await fs.unlink(tmpFilePath);
96
- } catch {
97
- /* ignore */ }
98
- if (!fileContent) {
99
- throw new Error('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
100
- }
101
- reviewNote = fileContent;
102
- // If the original runConfig.review object exists, update it so downstream code has the note.
103
- if (runConfig.review) {
104
- runConfig.review.note = reviewNote;
233
+ var _runConfig_review20;
234
+ // Create secure temporary file
235
+ tmpFilePath = await createSecureTempFile();
236
+ // Pre-populate the file with a helpful header so users know what to do.
237
+ const templateContent = [
238
+ '# Kodrdriv Review Note',
239
+ '',
240
+ '# Please enter your review note below. Lines starting with "#" will be ignored.',
241
+ '# Save and close the editor when you are done.',
242
+ '',
243
+ ''
244
+ ].join('\n');
245
+ await safeWriteFile(tmpFilePath, templateContent);
246
+ logger.info(`No review note provided – opening ${editor} to capture input...`);
247
+ // Open the editor with timeout protection
248
+ const editorTimeout = ((_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : _runConfig_review20.editorTimeout) || 300000; // 5 minutes default
249
+ await openEditorWithTimeout(editor, tmpFilePath, editorTimeout);
250
+ // Read the file back in, stripping comment lines and whitespace.
251
+ const fileContent = (await fs.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
252
+ if (!fileContent) {
253
+ throw new ValidationError('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
254
+ }
255
+ reviewNote = fileContent;
256
+ // If the original runConfig.review object exists, update it so downstream code has the note.
257
+ if (runConfig.review) {
258
+ runConfig.review.note = reviewNote;
259
+ }
260
+ } catch (error) {
261
+ logger.error(`Failed to capture review note via editor: ${error.message}`);
262
+ throw error;
263
+ } finally{
264
+ // Always clean up the temp file
265
+ if (tmpFilePath) {
266
+ await cleanupTempFile(tmpFilePath);
267
+ }
105
268
  }
106
269
  }
107
270
  logger.info('📝 Starting review analysis...');
108
271
  logger.debug('Review note: %s', reviewNote);
109
272
  logger.debug('Review note length: %d characters', reviewNote.length);
110
- // Gather additional context based on configuration
273
+ // Gather additional context based on configuration with improved error handling
111
274
  let logContext = '';
112
275
  let diffContext = '';
113
276
  let releaseNotesContext = '';
114
277
  let issuesContext = '';
278
+ const contextErrors = [];
115
279
  // Fetch commit history if enabled
116
280
  if ((_runConfig_review11 = runConfig.review) === null || _runConfig_review11 === void 0 ? void 0 : _runConfig_review11.includeCommitHistory) {
117
281
  try {
@@ -125,7 +289,9 @@ const execute = async (runConfig)=>{
125
289
  logger.debug('Added commit history to context (%d characters)', logContent.length);
126
290
  }
127
291
  } catch (error) {
128
- logger.warn('Failed to fetch commit history: %s', error.message);
292
+ const errorMsg = `Failed to fetch commit history: ${error.message}`;
293
+ logger.warn(errorMsg);
294
+ contextErrors.push(errorMsg);
129
295
  }
130
296
  }
131
297
  // Fetch recent diffs if enabled
@@ -139,8 +305,13 @@ const execute = async (runConfig)=>{
139
305
  baseExcludedPatterns: basePatterns
140
306
  });
141
307
  diffContext += recentDiffs;
308
+ if (recentDiffs.trim()) {
309
+ logger.debug('Added recent diffs to context (%d characters)', recentDiffs.length);
310
+ }
142
311
  } catch (error) {
143
- logger.warn('Failed to fetch recent diffs: %s', error.message);
312
+ const errorMsg = `Failed to fetch recent diffs: ${error.message}`;
313
+ logger.warn(errorMsg);
314
+ contextErrors.push(errorMsg);
144
315
  }
145
316
  }
146
317
  // Fetch release notes if enabled
@@ -155,7 +326,9 @@ const execute = async (runConfig)=>{
155
326
  logger.debug('Added release notes to context (%d characters)', releaseNotesContent.length);
156
327
  }
157
328
  } catch (error) {
158
- logger.warn('Failed to fetch release notes: %s', error.message);
329
+ const errorMsg = `Failed to fetch release notes: ${error.message}`;
330
+ logger.warn(errorMsg);
331
+ contextErrors.push(errorMsg);
159
332
  }
160
333
  }
161
334
  // Fetch GitHub issues if enabled
@@ -165,9 +338,24 @@ const execute = async (runConfig)=>{
165
338
  issuesContext = await get$1({
166
339
  limit: runConfig.review.githubIssuesLimit || 20
167
340
  });
168
- logger.debug('Added GitHub issues to context (%d characters)', issuesContext.length);
341
+ if (issuesContext.trim()) {
342
+ logger.debug('Added GitHub issues to context (%d characters)', issuesContext.length);
343
+ }
169
344
  } catch (error) {
170
- logger.warn('Failed to fetch GitHub issues: %s', error.message);
345
+ const errorMsg = `Failed to fetch GitHub issues: ${error.message}`;
346
+ logger.warn(errorMsg);
347
+ contextErrors.push(errorMsg);
348
+ }
349
+ }
350
+ // Report context gathering results
351
+ if (contextErrors.length > 0) {
352
+ var _runConfig_review21;
353
+ logger.warn(`Context gathering completed with ${contextErrors.length} error(s):`);
354
+ contextErrors.forEach((error)=>logger.warn(` - ${error}`));
355
+ // For critical operations, consider failing if too many context sources fail
356
+ const maxContextErrors = ((_runConfig_review21 = runConfig.review) === null || _runConfig_review21 === void 0 ? void 0 : _runConfig_review21.maxContextErrors) || contextErrors.length; // Default: allow all errors
357
+ if (contextErrors.length > maxContextErrors) {
358
+ throw new Error(`Too many context gathering errors (${contextErrors.length}), aborting review. Consider checking your configuration and network connectivity.`);
171
359
  }
172
360
  }
173
361
  // Analyze review note for issues using OpenAI
@@ -201,7 +389,7 @@ const execute = async (runConfig)=>{
201
389
  await storage.ensureDirectory(outputDirectory);
202
390
  // Save timestamped copy of review notes and context to output directory
203
391
  try {
204
- var _runConfig_review_context1, _runConfig_review20;
392
+ var _runConfig_review_context1, _runConfig_review22;
205
393
  // Save the original review note
206
394
  const reviewNotesFilename = getTimestampedReviewNotesFilename();
207
395
  const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
@@ -219,26 +407,35 @@ const execute = async (runConfig)=>{
219
407
  if (issuesContext.trim()) {
220
408
  reviewNotesContent += `# GitHub Issues Context\n\n${issuesContext}\n\n`;
221
409
  }
222
- if ((_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : (_runConfig_review_context1 = _runConfig_review20.context) === null || _runConfig_review_context1 === void 0 ? void 0 : _runConfig_review_context1.trim()) {
410
+ 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()) {
223
411
  reviewNotesContent += `# User Context\n\n${runConfig.review.context}\n\n`;
224
412
  }
225
- await storage.writeFile(reviewNotesPath, reviewNotesContent, 'utf-8');
413
+ await safeWriteFile(reviewNotesPath, reviewNotesContent);
226
414
  logger.debug('Saved timestamped review notes and context: %s', reviewNotesPath);
227
415
  } catch (error) {
228
416
  logger.warn('Failed to save timestamped review notes: %s', error.message);
417
+ // Don't fail the entire operation for this
229
418
  }
230
419
  const request = Formatter.create({
231
420
  logger
232
421
  }).formatPrompt(runConfig.model, prompt);
233
- const analysisResult = await createCompletion(request.messages, {
234
- model: runConfig.model,
235
- responseFormat: {
236
- type: 'json_object'
237
- },
238
- debug: runConfig.debug,
239
- debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('review-analysis')),
240
- debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('review-analysis'))
241
- });
422
+ let analysisResult;
423
+ try {
424
+ const rawResult = await createCompletion(request.messages, {
425
+ model: runConfig.model,
426
+ responseFormat: {
427
+ type: 'json_object'
428
+ },
429
+ debug: runConfig.debug,
430
+ debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('review-analysis')),
431
+ debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('review-analysis'))
432
+ });
433
+ // Validate the API response before using it
434
+ analysisResult = validateReviewResult(rawResult);
435
+ } catch (error) {
436
+ logger.error(`Failed to analyze review note: ${error.message}`);
437
+ throw new Error(`Review analysis failed: ${error.message}`);
438
+ }
242
439
  logger.info('✅ Analysis completed');
243
440
  logger.debug('Analysis result summary: %s', analysisResult.summary);
244
441
  logger.debug('Total issues found: %d', analysisResult.totalIssues);
@@ -254,15 +451,44 @@ const execute = async (runConfig)=>{
254
451
  const reviewPath = getOutputPath(outputDirectory, reviewFilename);
255
452
  // Format the analysis result as markdown
256
453
  const reviewContent = `# Review Analysis Result\n\n` + `## Summary\n${analysisResult.summary}\n\n` + `## Total Issues Found\n${analysisResult.totalIssues}\n\n` + `## Issues\n\n${JSON.stringify(analysisResult.issues, null, 2)}\n\n` + `---\n\n*Analysis completed at ${new Date().toISOString()}*`;
257
- await storage.writeFile(reviewPath, reviewContent, 'utf-8');
454
+ await safeWriteFile(reviewPath, reviewContent);
258
455
  logger.debug('Saved timestamped review analysis: %s', reviewPath);
259
456
  } catch (error) {
260
457
  logger.warn('Failed to save timestamped review analysis: %s', error.message);
458
+ // Don't fail the entire operation for this
261
459
  }
262
460
  // Handle GitHub issue creation using the issues module
263
461
  const senditMode = ((_runConfig_review17 = runConfig.review) === null || _runConfig_review17 === void 0 ? void 0 : _runConfig_review17.sendit) || false;
264
462
  return await handleIssueCreation(analysisResult, senditMode);
265
463
  };
464
+ const execute = async (runConfig)=>{
465
+ try {
466
+ return await executeInternal(runConfig);
467
+ } catch (error) {
468
+ const logger = getLogger();
469
+ if (error instanceof ValidationError) {
470
+ logger.error(`review failed: ${error.message}`);
471
+ process.exit(1);
472
+ }
473
+ if (error instanceof FileOperationError) {
474
+ logger.error(`review failed: ${error.message}`);
475
+ if (error.cause) {
476
+ logger.debug(`Caused by: ${error.cause.message}`);
477
+ }
478
+ process.exit(1);
479
+ }
480
+ if (error instanceof CommandError) {
481
+ logger.error(`review failed: ${error.message}`);
482
+ if (error.cause) {
483
+ logger.debug(`Caused by: ${error.cause.message}`);
484
+ }
485
+ process.exit(1);
486
+ }
487
+ // Unexpected errors
488
+ logger.error(`review encountered unexpected error: ${error.message}`);
489
+ process.exit(1);
490
+ }
491
+ };
266
492
 
267
493
  export { execute };
268
494
  //# sourceMappingURL=review.js.map