claude-git-hooks 2.1.0 → 2.3.1

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 (52) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +280 -78
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +164 -0
  5. package/lib/hooks/pre-commit.js +180 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +107 -16
  8. package/lib/utils/claude-diagnostics.js +266 -0
  9. package/lib/utils/file-operations.js +1 -65
  10. package/lib/utils/file-utils.js +65 -0
  11. package/lib/utils/installation-diagnostics.js +145 -0
  12. package/lib/utils/package-info.js +75 -0
  13. package/lib/utils/preset-loader.js +214 -0
  14. package/lib/utils/prompt-builder.js +83 -67
  15. package/lib/utils/resolution-prompt.js +12 -2
  16. package/package.json +49 -50
  17. package/templates/ANALYZE_DIFF.md +33 -0
  18. package/templates/COMMIT_MESSAGE.md +24 -0
  19. package/templates/CUSTOMIZATION_GUIDE.md +656 -0
  20. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  21. package/templates/config.example.json +41 -0
  22. package/templates/pre-commit +40 -2
  23. package/templates/prepare-commit-msg +40 -2
  24. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  25. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  26. package/templates/presets/ai/config.json +12 -0
  27. package/templates/presets/ai/preset.json +42 -0
  28. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  29. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  30. package/templates/presets/backend/config.json +12 -0
  31. package/templates/presets/backend/preset.json +49 -0
  32. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  33. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  34. package/templates/presets/database/config.json +12 -0
  35. package/templates/presets/database/preset.json +38 -0
  36. package/templates/presets/default/config.json +12 -0
  37. package/templates/presets/default/preset.json +53 -0
  38. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  39. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  40. package/templates/presets/frontend/config.json +12 -0
  41. package/templates/presets/frontend/preset.json +50 -0
  42. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  43. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  44. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  45. package/templates/presets/fullstack/config.json +12 -0
  46. package/templates/presets/fullstack/preset.json +55 -0
  47. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  48. package/templates/shared/ANALYZE_DIFF.md +33 -0
  49. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  50. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  51. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  52. package/templates/check-version.sh +0 -266
@@ -12,12 +12,16 @@
12
12
  * - child_process: For executing Claude CLI
13
13
  * - fs/promises: For debug file writing
14
14
  * - logger: Debug and error logging
15
+ * - claude-diagnostics: Error detection and formatting
15
16
  */
16
17
 
17
18
  import { spawn, execSync } from 'child_process';
18
19
  import fs from 'fs/promises';
20
+ import path from 'path';
19
21
  import os from 'os';
20
22
  import logger from './logger.js';
23
+ import config from '../config.js';
24
+ import { detectClaudeError, formatClaudeError, ClaudeErrorType } from './claude-diagnostics.js';
21
25
 
22
26
  /**
23
27
  * Custom error for Claude client failures
@@ -53,7 +57,7 @@ const isWSLAvailable = () => {
53
57
 
54
58
  try {
55
59
  // Try to run wsl --version to check if WSL is installed
56
- execSync('wsl --version', { stdio: 'ignore', timeout: 3000 });
60
+ execSync('wsl --version', { stdio: 'ignore', timeout: config.system.wslCheckTimeout });
57
61
  return true;
58
62
  } catch (error) {
59
63
  logger.debug('claude-client - isWSLAvailable', 'WSL not available', error);
@@ -139,18 +143,25 @@ const executeClaude = (prompt, { timeout = 120000 } = {}) => {
139
143
  );
140
144
  resolve(stdout);
141
145
  } else {
146
+ // Detect specific error type
147
+ const errorInfo = detectClaudeError(stdout, stderr, code);
148
+
142
149
  logger.error(
143
150
  'claude-client - executeClaude',
144
- `Claude CLI failed with exit code ${code}`,
145
- new ClaudeClientError('Claude CLI execution failed', {
151
+ `Claude CLI failed: ${errorInfo.type}`,
152
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
146
153
  output: { stdout, stderr },
147
- context: { exitCode: code, duration }
154
+ context: { exitCode: code, duration, errorType: errorInfo.type }
148
155
  })
149
156
  );
150
157
 
151
- reject(new ClaudeClientError('Claude CLI execution failed', {
158
+ // Show formatted error to user
159
+ const formattedError = formatClaudeError(errorInfo);
160
+ console.error('\n' + formattedError + '\n');
161
+
162
+ reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
152
163
  output: { stdout, stderr },
153
- context: { exitCode: code, duration }
164
+ context: { exitCode: code, duration, errorInfo }
154
165
  }));
155
166
  }
156
167
  });
@@ -294,15 +305,45 @@ const extractJSON = (response) => {
294
305
  };
295
306
 
296
307
  /**
297
- * Saves response to debug file
298
- * Why: Helps troubleshoot issues with Claude responses
308
+ * Saves prompt and response to debug file
309
+ * Why: Helps troubleshoot issues with Claude responses and verify prompts
299
310
  *
300
- * @param {string} response - Raw response to save
301
- * @param {string} filename - Debug filename (default: 'debug-claude-response.json')
311
+ * @param {string} prompt - Prompt sent to Claude
312
+ * @param {string} response - Raw response from Claude
313
+ * @param {string} filename - Debug filename (default: from config)
302
314
  */
303
- const saveDebugResponse = async (response, filename = 'debug-claude-response.json') => {
315
+ const saveDebugResponse = async (prompt, response, filename = config.output.debugFile) => {
304
316
  try {
305
- await fs.writeFile(filename, response, 'utf8');
317
+ // Ensure output directory exists
318
+ const outputDir = path.dirname(filename);
319
+ await fs.mkdir(outputDir, { recursive: true });
320
+
321
+ // Save full debug information
322
+ const debugData = {
323
+ timestamp: new Date().toISOString(),
324
+ promptLength: prompt.length,
325
+ responseLength: response.length,
326
+ prompt: prompt,
327
+ response: response
328
+ };
329
+
330
+ await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8');
331
+
332
+ // Display batch optimization status
333
+ try {
334
+ if (prompt.includes('OPTIMIZATION')) {
335
+ console.log('\n' + '='.repeat(70));
336
+ console.log('✅ BATCH OPTIMIZATION ENABLED');
337
+ console.log('='.repeat(70));
338
+ console.log('Multi-file analysis organized for efficient processing');
339
+ console.log('Check debug file for full prompt and response details');
340
+ console.log('='.repeat(70) + '\n');
341
+ }
342
+ } catch (parseError) {
343
+ // Ignore parsing errors, just skip the display
344
+ }
345
+
346
+ logger.info(`📝 Debug output saved to ${filename}`);
306
347
  logger.debug(
307
348
  'claude-client - saveDebugResponse',
308
349
  `Debug response saved to ${filename}`
@@ -323,11 +364,11 @@ const saveDebugResponse = async (response, filename = 'debug-claude-response.jso
323
364
  * @param {string} prompt - Analysis prompt
324
365
  * @param {Object} options - Analysis options
325
366
  * @param {number} options.timeout - Timeout in milliseconds
326
- * @param {boolean} options.saveDebug - Save response to debug file (default: from DEBUG env)
367
+ * @param {boolean} options.saveDebug - Save response to debug file (default: from config)
327
368
  * @returns {Promise<Object>} Parsed analysis result
328
369
  * @throws {ClaudeClientError} If analysis fails
329
370
  */
330
- const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.DEBUG === 'true' } = {}) => {
371
+ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug } = {}) => {
331
372
  logger.debug(
332
373
  'claude-client - analyzeCode',
333
374
  'Starting code analysis',
@@ -340,7 +381,7 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
340
381
 
341
382
  // Save debug if requested
342
383
  if (saveDebug) {
343
- await saveDebugResponse(response);
384
+ await saveDebugResponse(prompt, response);
344
385
  }
345
386
 
346
387
  // Extract and parse JSON
@@ -364,10 +405,60 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
364
405
  }
365
406
  };
366
407
 
408
+ /**
409
+ * Splits array into chunks
410
+ * @param {Array} array - Array to split
411
+ * @param {number} size - Chunk size
412
+ * @returns {Array<Array>} Array of chunks
413
+ */
414
+ const chunkArray = (array, size) => {
415
+ const chunks = [];
416
+ for (let i = 0; i < array.length; i += size) {
417
+ chunks.push(array.slice(i, i + size));
418
+ }
419
+ return chunks;
420
+ };
421
+
422
+ /**
423
+ * Runs multiple analyzeCode calls in parallel
424
+ * @param {Array<string>} prompts - Array of prompts to analyze
425
+ * @param {Object} options - Same options as analyzeCode
426
+ * @returns {Promise<Array<Object>>} Array of results
427
+ */
428
+ const analyzeCodeParallel = async (prompts, options = {}) => {
429
+ const startTime = Date.now();
430
+
431
+ console.log('\n' + '='.repeat(70));
432
+ console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
433
+ console.log('='.repeat(70));
434
+
435
+ logger.info(`Starting parallel analysis: ${prompts.length} prompts`);
436
+
437
+ const promises = prompts.map((prompt, index) => {
438
+ console.log(` ⚡ Launching batch ${index + 1}/${prompts.length}...`);
439
+ logger.debug('claude-client - analyzeCodeParallel', `Starting batch ${index + 1}`);
440
+ return analyzeCode(prompt, options);
441
+ });
442
+
443
+ console.log(` ⏳ Waiting for all batches to complete...\n`);
444
+ const results = await Promise.all(promises);
445
+
446
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
447
+
448
+ console.log('='.repeat(70));
449
+ console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
450
+ console.log('='.repeat(70) + '\n');
451
+
452
+ logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
453
+ return results;
454
+ };
455
+
367
456
  export {
368
457
  ClaudeClientError,
369
458
  executeClaude,
370
459
  extractJSON,
371
460
  saveDebugResponse,
372
- analyzeCode
461
+ analyzeCode,
462
+ analyzeCodeParallel,
463
+ chunkArray
373
464
  };
@@ -0,0 +1,266 @@
1
+ /**
2
+ * File: claude-diagnostics.js
3
+ * Purpose: Reusable Claude CLI error diagnostics and formatting
4
+ *
5
+ * Key features:
6
+ * - Detects common Claude CLI error patterns
7
+ * - Provides actionable remediation steps
8
+ * - Extensible for future error types
9
+ *
10
+ * Usage:
11
+ * import { detectClaudeError, formatClaudeError } from './claude-diagnostics.js';
12
+ *
13
+ * const errorInfo = detectClaudeError(stdout, stderr, exitCode);
14
+ * if (errorInfo) {
15
+ * console.error(formatClaudeError(errorInfo));
16
+ * }
17
+ */
18
+
19
+ /**
20
+ * Error types that can be detected
21
+ */
22
+ export const ClaudeErrorType = {
23
+ RATE_LIMIT: 'RATE_LIMIT',
24
+ AUTH_FAILED: 'AUTH_FAILED',
25
+ TIMEOUT: 'TIMEOUT',
26
+ NETWORK: 'NETWORK',
27
+ INVALID_RESPONSE: 'INVALID_RESPONSE',
28
+ GENERIC: 'GENERIC'
29
+ };
30
+
31
+ /**
32
+ * Detects Claude CLI error type and extracts relevant information
33
+ * Why: Centralized error detection for consistent handling
34
+ *
35
+ * Future enhancements:
36
+ * - Network connectivity errors
37
+ * - Authentication expiration
38
+ * - Model availability errors
39
+ * - Token limit exceeded errors
40
+ *
41
+ * @param {string} stdout - Claude CLI stdout
42
+ * @param {string} stderr - Claude CLI stderr
43
+ * @param {number} exitCode - Process exit code
44
+ * @returns {Object|null} Error information or null if no specific error detected
45
+ */
46
+ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
47
+ // 1. Rate limit detection
48
+ const rateLimitMatch = stdout.match(/Claude AI usage limit reached\|(\d+)/);
49
+ if (rateLimitMatch) {
50
+ const resetTimestamp = parseInt(rateLimitMatch[1], 10);
51
+ const resetDate = new Date(resetTimestamp * 1000);
52
+ const now = new Date();
53
+ const minutesUntilReset = Math.ceil((resetDate - now) / 60000);
54
+
55
+ return {
56
+ type: ClaudeErrorType.RATE_LIMIT,
57
+ exitCode,
58
+ resetTimestamp,
59
+ resetDate,
60
+ minutesUntilReset
61
+ };
62
+ }
63
+
64
+ // 2. Authentication failure detection
65
+ if (stdout.includes('not authenticated') || stderr.includes('not authenticated') ||
66
+ stdout.includes('authentication failed') || stderr.includes('authentication failed')) {
67
+ return {
68
+ type: ClaudeErrorType.AUTH_FAILED,
69
+ exitCode
70
+ };
71
+ }
72
+
73
+ // 3. Network errors
74
+ if (stderr.includes('ENOTFOUND') || stderr.includes('ECONNREFUSED') ||
75
+ stderr.includes('network error') || stderr.includes('connection refused')) {
76
+ return {
77
+ type: ClaudeErrorType.NETWORK,
78
+ exitCode
79
+ };
80
+ }
81
+
82
+ // 4. Invalid response (JSON parsing errors)
83
+ if (stdout.includes('SyntaxError') || stdout.includes('Unexpected token')) {
84
+ return {
85
+ type: ClaudeErrorType.INVALID_RESPONSE,
86
+ exitCode
87
+ };
88
+ }
89
+
90
+ // 5. Generic error
91
+ return {
92
+ type: ClaudeErrorType.GENERIC,
93
+ exitCode,
94
+ stdout: stdout.substring(0, 200), // First 200 chars
95
+ stderr: stderr.substring(0, 200)
96
+ };
97
+ };
98
+
99
+ /**
100
+ * Formats Claude error message with diagnostics and remediation steps
101
+ * Why: Provides consistent, actionable error messages
102
+ *
103
+ * @param {Object} errorInfo - Output from detectClaudeError()
104
+ * @returns {string} Formatted error message
105
+ */
106
+ export const formatClaudeError = (errorInfo) => {
107
+ const lines = [];
108
+
109
+ switch (errorInfo.type) {
110
+ case ClaudeErrorType.RATE_LIMIT:
111
+ return formatRateLimitError(errorInfo);
112
+
113
+ case ClaudeErrorType.AUTH_FAILED:
114
+ return formatAuthError(errorInfo);
115
+
116
+ case ClaudeErrorType.NETWORK:
117
+ return formatNetworkError(errorInfo);
118
+
119
+ case ClaudeErrorType.INVALID_RESPONSE:
120
+ return formatInvalidResponseError(errorInfo);
121
+
122
+ case ClaudeErrorType.GENERIC:
123
+ default:
124
+ return formatGenericError(errorInfo);
125
+ }
126
+ };
127
+
128
+ /**
129
+ * Format rate limit error
130
+ */
131
+ const formatRateLimitError = ({ resetDate, minutesUntilReset }) => {
132
+ const lines = [];
133
+
134
+ lines.push('❌ Claude API usage limit reached');
135
+ lines.push('');
136
+ lines.push('Rate limit details:');
137
+ lines.push(` Reset time: ${resetDate.toLocaleString()}`);
138
+
139
+ if (minutesUntilReset > 60) {
140
+ const hours = Math.ceil(minutesUntilReset / 60);
141
+ lines.push(` Time until reset: ~${hours} hour${hours > 1 ? 's' : ''}`);
142
+ } else if (minutesUntilReset > 0) {
143
+ lines.push(` Time until reset: ~${minutesUntilReset} minute${minutesUntilReset !== 1 ? 's' : ''}`);
144
+ } else {
145
+ lines.push(' Limit should be reset now');
146
+ }
147
+
148
+ lines.push('');
149
+ lines.push('Options:');
150
+ lines.push(' 1. Wait for rate limit to reset');
151
+ lines.push(' 2. Skip analysis for now:');
152
+ lines.push(' git commit --no-verify -m "your message"');
153
+ lines.push(' 3. Reduce API usage by switching to haiku model:');
154
+ lines.push(' Edit .claude/config.json:');
155
+ lines.push(' { "subagents": { "model": "haiku" } }');
156
+
157
+ return lines.join('\n');
158
+ };
159
+
160
+ /**
161
+ * Format authentication error
162
+ */
163
+ const formatAuthError = ({ exitCode }) => {
164
+ const lines = [];
165
+
166
+ lines.push('❌ Claude CLI authentication failed');
167
+ lines.push('');
168
+ lines.push('Possible causes:');
169
+ lines.push(' 1. Not logged in to Claude CLI');
170
+ lines.push(' 2. Authentication token expired');
171
+ lines.push(' 3. Invalid API credentials');
172
+ lines.push('');
173
+ lines.push('Solution:');
174
+ lines.push(' claude auth login');
175
+ lines.push('');
176
+ lines.push('Then try your commit again.');
177
+
178
+ return lines.join('\n');
179
+ };
180
+
181
+ /**
182
+ * Format network error
183
+ */
184
+ const formatNetworkError = ({ exitCode }) => {
185
+ const lines = [];
186
+
187
+ lines.push('❌ Network error connecting to Claude API');
188
+ lines.push('');
189
+ lines.push('Possible causes:');
190
+ lines.push(' 1. No internet connection');
191
+ lines.push(' 2. Firewall blocking Claude API');
192
+ lines.push(' 3. Claude API temporarily unavailable');
193
+ lines.push('');
194
+ lines.push('Solutions:');
195
+ lines.push(' 1. Check your internet connection');
196
+ lines.push(' 2. Verify firewall settings');
197
+ lines.push(' 3. Try again in a few moments');
198
+ lines.push(' 4. Skip analysis: git commit --no-verify -m "message"');
199
+
200
+ return lines.join('\n');
201
+ };
202
+
203
+ /**
204
+ * Format invalid response error
205
+ */
206
+ const formatInvalidResponseError = ({ exitCode }) => {
207
+ const lines = [];
208
+
209
+ lines.push('❌ Claude returned invalid response');
210
+ lines.push('');
211
+ lines.push('This usually means:');
212
+ lines.push(' - Claude did not return valid JSON');
213
+ lines.push(' - Response format does not match expected schema');
214
+ lines.push('');
215
+ lines.push('Solutions:');
216
+ lines.push(' 1. Check debug output: .claude/out/debug-claude-response.json');
217
+ lines.push(' 2. Try again (may be temporary issue)');
218
+ lines.push(' 3. Skip analysis: git commit --no-verify -m "message"');
219
+
220
+ return lines.join('\n');
221
+ };
222
+
223
+ /**
224
+ * Format generic error
225
+ */
226
+ const formatGenericError = ({ exitCode, stdout, stderr }) => {
227
+ const lines = [];
228
+
229
+ lines.push('❌ Claude CLI execution failed');
230
+ lines.push('');
231
+ lines.push(`Exit code: ${exitCode}`);
232
+
233
+ if (stdout && stdout.trim()) {
234
+ lines.push('');
235
+ lines.push('Output:');
236
+ lines.push(` ${stdout.trim()}`);
237
+ }
238
+
239
+ if (stderr && stderr.trim()) {
240
+ lines.push('');
241
+ lines.push('Error:');
242
+ lines.push(` ${stderr.trim()}`);
243
+ }
244
+
245
+ lines.push('');
246
+ lines.push('Solutions:');
247
+ lines.push(' 1. Verify Claude CLI is installed: claude --version');
248
+ lines.push(' 2. Check authentication: claude auth login');
249
+ lines.push(' 3. Enable debug mode in .claude/config.json:');
250
+ lines.push(' { "system": { "debug": true } }');
251
+ lines.push(' 4. Skip analysis: git commit --no-verify -m "message"');
252
+
253
+ return lines.join('\n');
254
+ };
255
+
256
+ /**
257
+ * Checks if Claude CLI error is recoverable
258
+ * Why: Some errors (rate limit) should wait, others (auth) should fail immediately
259
+ *
260
+ * @param {Object} errorInfo - Output from detectClaudeError()
261
+ * @returns {boolean} True if error might resolve with retry
262
+ */
263
+ export const isRecoverableError = (errorInfo) => {
264
+ return errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
265
+ errorInfo.type === ClaudeErrorType.NETWORK;
266
+ };
@@ -347,69 +347,6 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
347
347
  * }
348
348
  * ]
349
349
  */
350
- const readMultipleFiles = async (files, { maxSize = 100000, applySkipFilter = true } = {}) => {
351
- logger.debug(
352
- 'file-operations - readMultipleFiles',
353
- 'Reading multiple files',
354
- { fileCount: files.length, maxSize, applySkipFilter }
355
- );
356
-
357
- const results = await Promise.allSettled(
358
- files.map(async (filePath) => {
359
- try {
360
- let content = await readFile(filePath, { maxSize });
361
-
362
- // Apply SKIP-ANALYSIS filtering if requested
363
- if (applySkipFilter) {
364
- content = filterSkipAnalysis(content);
365
- }
366
-
367
- const size = await getFileSize(filePath);
368
-
369
- return {
370
- path: filePath,
371
- content,
372
- size,
373
- error: null
374
- };
375
-
376
- } catch (error) {
377
- logger.error(
378
- 'file-operations - readMultipleFiles',
379
- `Failed to read file: ${filePath}`,
380
- error
381
- );
382
-
383
- return {
384
- path: filePath,
385
- content: null,
386
- size: 0,
387
- error
388
- };
389
- }
390
- })
391
- );
392
-
393
- // Extract all results (both successful and failed)
394
- const fileContents = results.map(r =>
395
- r.status === 'fulfilled' ? r.value : r.reason
396
- );
397
-
398
- const successCount = fileContents.filter(f => f.error === null).length;
399
-
400
- logger.debug(
401
- 'file-operations - readMultipleFiles',
402
- 'Reading complete',
403
- {
404
- totalFiles: files.length,
405
- successfulReads: successCount,
406
- failedReads: files.length - successCount
407
- }
408
- );
409
-
410
- return fileContents;
411
- };
412
-
413
350
  export {
414
351
  FileOperationError,
415
352
  getFileSize,
@@ -417,6 +354,5 @@ export {
417
354
  readFile,
418
355
  filterSkipAnalysis,
419
356
  hasAllowedExtension,
420
- filterFiles,
421
- readMultipleFiles
357
+ filterFiles
422
358
  };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * File: file-utils.js
3
+ * Purpose: Utility functions for file system operations
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import fsSync from 'fs';
8
+ import path from 'path';
9
+ import { getRepoRoot } from './git-operations.js';
10
+ import logger from './logger.js';
11
+
12
+ /**
13
+ * Ensures a directory exists (creates if not present)
14
+ *
15
+ * @param {string} dirPath - Directory path to ensure
16
+ * @returns {Promise<void>}
17
+ */
18
+ export const ensureDir = async (dirPath) => {
19
+ const absolutePath = path.isAbsolute(dirPath)
20
+ ? dirPath
21
+ : path.join(getRepoRoot(), dirPath);
22
+
23
+ try {
24
+ await fs.mkdir(absolutePath, { recursive: true });
25
+ logger.debug('file-utils - ensureDir', 'Directory ensured', { path: absolutePath });
26
+ } catch (error) {
27
+ logger.error('file-utils - ensureDir', 'Failed to create directory', error);
28
+ throw error;
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Ensures the output directory exists before writing files
34
+ * Creates .claude/out/ if it doesn't exist
35
+ *
36
+ * @param {Object} config - Configuration object with output.outputDir
37
+ * @returns {Promise<void>}
38
+ */
39
+ export const ensureOutputDir = async (config) => {
40
+ const outputDir = config?.output?.outputDir || '.claude/out';
41
+ await ensureDir(outputDir);
42
+ };
43
+
44
+ /**
45
+ * Writes a file ensuring its directory exists
46
+ *
47
+ * @param {string} filePath - File path to write
48
+ * @param {string} content - File content
49
+ * @param {Object} config - Configuration object
50
+ * @returns {Promise<void>}
51
+ */
52
+ export const writeOutputFile = async (filePath, content, config) => {
53
+ await ensureOutputDir(config);
54
+
55
+ const absolutePath = path.isAbsolute(filePath)
56
+ ? filePath
57
+ : path.join(getRepoRoot(), filePath);
58
+
59
+ await fs.writeFile(absolutePath, content, 'utf8');
60
+
61
+ logger.debug('file-utils - writeOutputFile', 'File written', {
62
+ path: absolutePath,
63
+ size: content.length
64
+ });
65
+ };