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.
- package/CHANGELOG.md +240 -0
- package/README.md +280 -78
- package/bin/claude-hooks +295 -119
- package/lib/config.js +164 -0
- package/lib/hooks/pre-commit.js +180 -67
- package/lib/hooks/prepare-commit-msg.js +47 -41
- package/lib/utils/claude-client.js +107 -16
- package/lib/utils/claude-diagnostics.js +266 -0
- package/lib/utils/file-operations.js +1 -65
- package/lib/utils/file-utils.js +65 -0
- package/lib/utils/installation-diagnostics.js +145 -0
- package/lib/utils/package-info.js +75 -0
- package/lib/utils/preset-loader.js +214 -0
- package/lib/utils/prompt-builder.js +83 -67
- package/lib/utils/resolution-prompt.js +12 -2
- package/package.json +49 -50
- package/templates/ANALYZE_DIFF.md +33 -0
- package/templates/COMMIT_MESSAGE.md +24 -0
- package/templates/CUSTOMIZATION_GUIDE.md +656 -0
- package/templates/SUBAGENT_INSTRUCTION.md +1 -0
- package/templates/config.example.json +41 -0
- package/templates/pre-commit +40 -2
- package/templates/prepare-commit-msg +40 -2
- package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
- package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
- package/templates/presets/ai/config.json +12 -0
- package/templates/presets/ai/preset.json +42 -0
- package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
- package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
- package/templates/presets/backend/config.json +12 -0
- package/templates/presets/backend/preset.json +49 -0
- package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
- package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
- package/templates/presets/database/config.json +12 -0
- package/templates/presets/database/preset.json +38 -0
- package/templates/presets/default/config.json +12 -0
- package/templates/presets/default/preset.json +53 -0
- package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
- package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
- package/templates/presets/frontend/config.json +12 -0
- package/templates/presets/frontend/preset.json +50 -0
- package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
- package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
- package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
- package/templates/presets/fullstack/config.json +12 -0
- package/templates/presets/fullstack/preset.json +55 -0
- package/templates/shared/ANALYSIS_PROMPT.md +103 -0
- package/templates/shared/ANALYZE_DIFF.md +33 -0
- package/templates/shared/COMMIT_MESSAGE.md +24 -0
- package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
- package/templates/shared/RESOLUTION_PROMPT.md +32 -0
- 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:
|
|
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
|
|
145
|
-
new ClaudeClientError(
|
|
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
|
-
|
|
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}
|
|
301
|
-
* @param {string}
|
|
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 =
|
|
315
|
+
const saveDebugResponse = async (prompt, response, filename = config.output.debugFile) => {
|
|
304
316
|
try {
|
|
305
|
-
|
|
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
|
|
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 =
|
|
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
|
+
};
|