claude-git-hooks 2.3.0 ā 2.4.0
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 +119 -0
- package/README.md +99 -15
- package/bin/claude-hooks +132 -38
- package/lib/config.js +13 -12
- package/lib/hooks/pre-commit.js +36 -1
- package/lib/hooks/prepare-commit-msg.js +5 -0
- package/lib/utils/claude-client.js +14 -5
- package/lib/utils/claude-diagnostics.js +266 -0
- package/lib/utils/file-operations.js +38 -4
- package/lib/utils/installation-diagnostics.js +145 -0
- package/lib/utils/logger.js +2 -2
- package/lib/utils/preset-loader.js +6 -1
- package/package.json +1 -1
- package/templates/CUSTOMIZATION_GUIDE.md +656 -0
- package/templates/config.example.json +39 -39
- package/templates/pre-commit +40 -2
- package/templates/prepare-commit-msg +40 -2
- package/templates/presets/ai/config.json +10 -10
- package/templates/presets/backend/config.json +10 -10
- package/templates/presets/database/config.json +10 -10
- package/templates/presets/default/config.json +10 -10
- package/templates/presets/frontend/config.json +10 -10
- package/templates/presets/fullstack/config.json +10 -10
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -181,12 +181,38 @@ const main = async () => {
|
|
|
181
181
|
// Load configuration
|
|
182
182
|
const config = await getConfig();
|
|
183
183
|
|
|
184
|
+
// Enable debug mode from config
|
|
185
|
+
if (config.system.debug) {
|
|
186
|
+
logger.setDebugMode(true);
|
|
187
|
+
}
|
|
188
|
+
|
|
184
189
|
// Display configuration info
|
|
185
190
|
const version = await getVersion();
|
|
186
191
|
console.log(`\nš¤ claude-git-hooks v${version}`);
|
|
187
192
|
|
|
188
193
|
logger.info('Starting code quality analysis...');
|
|
189
194
|
|
|
195
|
+
// DEBUG: Log working directories
|
|
196
|
+
const repoRoot = await import('../utils/git-operations.js').then(m => m.getRepoRoot());
|
|
197
|
+
const { getRepoRoot } = await import('../utils/git-operations.js');
|
|
198
|
+
|
|
199
|
+
// Normalize paths for comparison (handle Windows backslash vs forward slash)
|
|
200
|
+
const normalizePath = (p) => p.replace(/\\/g, '/').toLowerCase();
|
|
201
|
+
const cwdNormalized = normalizePath(process.cwd());
|
|
202
|
+
const repoRootNormalized = normalizePath(repoRoot);
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
'pre-commit - main',
|
|
206
|
+
'Working directory info',
|
|
207
|
+
{
|
|
208
|
+
'process.cwd()': process.cwd(),
|
|
209
|
+
'repo root': repoRoot,
|
|
210
|
+
'cwd (normalized)': cwdNormalized,
|
|
211
|
+
'repo root (normalized)': repoRootNormalized,
|
|
212
|
+
'match': cwdNormalized === repoRootNormalized
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
190
216
|
logger.debug(
|
|
191
217
|
'pre-commit - main',
|
|
192
218
|
'Configuration',
|
|
@@ -237,6 +263,14 @@ const main = async () => {
|
|
|
237
263
|
});
|
|
238
264
|
|
|
239
265
|
const validFiles = filteredFiles.filter(f => f.valid);
|
|
266
|
+
const invalidFiles = filteredFiles.filter(f => !f.valid);
|
|
267
|
+
|
|
268
|
+
// Show user-facing warnings for rejected files
|
|
269
|
+
if (invalidFiles.length > 0) {
|
|
270
|
+
invalidFiles.forEach(file => {
|
|
271
|
+
logger.warning(`Skipping ${file.path}: ${file.reason}`);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
240
274
|
|
|
241
275
|
if (validFiles.length === 0) {
|
|
242
276
|
logger.warning('No valid files found to review');
|
|
@@ -312,7 +346,8 @@ const main = async () => {
|
|
|
312
346
|
|
|
313
347
|
// Step 5: Analyze with Claude (parallel or single)
|
|
314
348
|
let result;
|
|
315
|
-
|
|
349
|
+
// TODO: This can be refactored so no conditional is needed.
|
|
350
|
+
// Lists can have 0...N items, e.g. iterating a list of 1 element is akin to single execution.
|
|
316
351
|
if (subagentsEnabled && filesData.length >= 3) {
|
|
317
352
|
// Parallel execution: split files into batches
|
|
318
353
|
logger.info(`Using parallel execution with batch size ${batchSize}`);
|
|
@@ -120,6 +120,11 @@ const main = async () => {
|
|
|
120
120
|
// Load configuration (includes preset + user overrides)
|
|
121
121
|
const config = await getConfig();
|
|
122
122
|
|
|
123
|
+
// Enable debug mode from config
|
|
124
|
+
if (config.system.debug) {
|
|
125
|
+
logger.setDebugMode(true);
|
|
126
|
+
}
|
|
127
|
+
|
|
123
128
|
try {
|
|
124
129
|
// Get hook arguments
|
|
125
130
|
const args = process.argv.slice(2);
|
|
@@ -12,6 +12,7 @@
|
|
|
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';
|
|
@@ -20,6 +21,7 @@ import path from 'path';
|
|
|
20
21
|
import os from 'os';
|
|
21
22
|
import logger from './logger.js';
|
|
22
23
|
import config from '../config.js';
|
|
24
|
+
import { detectClaudeError, formatClaudeError, ClaudeErrorType } from './claude-diagnostics.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Custom error for Claude client failures
|
|
@@ -141,18 +143,25 @@ const executeClaude = (prompt, { timeout = 120000 } = {}) => {
|
|
|
141
143
|
);
|
|
142
144
|
resolve(stdout);
|
|
143
145
|
} else {
|
|
146
|
+
// Detect specific error type
|
|
147
|
+
const errorInfo = detectClaudeError(stdout, stderr, code);
|
|
148
|
+
|
|
144
149
|
logger.error(
|
|
145
150
|
'claude-client - executeClaude',
|
|
146
|
-
`Claude CLI failed
|
|
147
|
-
new ClaudeClientError(
|
|
151
|
+
`Claude CLI failed: ${errorInfo.type}`,
|
|
152
|
+
new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
148
153
|
output: { stdout, stderr },
|
|
149
|
-
context: { exitCode: code, duration }
|
|
154
|
+
context: { exitCode: code, duration, errorType: errorInfo.type }
|
|
150
155
|
})
|
|
151
156
|
);
|
|
152
157
|
|
|
153
|
-
|
|
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}`, {
|
|
154
163
|
output: { stdout, stderr },
|
|
155
|
-
context: { exitCode: code, duration }
|
|
164
|
+
context: { exitCode: code, duration, errorInfo }
|
|
156
165
|
}));
|
|
157
166
|
}
|
|
158
167
|
});
|
|
@@ -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
|
+
};
|
|
@@ -250,13 +250,24 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
250
250
|
logger.debug(
|
|
251
251
|
'file-operations - filterFiles',
|
|
252
252
|
'Filtering files',
|
|
253
|
-
{
|
|
253
|
+
{
|
|
254
|
+
fileCount: files.length,
|
|
255
|
+
maxSize,
|
|
256
|
+
extensions,
|
|
257
|
+
'process.cwd()': process.cwd(),
|
|
258
|
+
files: files
|
|
259
|
+
}
|
|
254
260
|
);
|
|
255
261
|
|
|
256
262
|
const results = await Promise.allSettled(
|
|
257
263
|
files.map(async (filePath) => {
|
|
258
264
|
// Check extension first (fast)
|
|
259
265
|
if (!hasAllowedExtension(filePath, extensions)) {
|
|
266
|
+
logger.debug(
|
|
267
|
+
'file-operations - filterFiles',
|
|
268
|
+
'Extension rejected',
|
|
269
|
+
{ filePath }
|
|
270
|
+
);
|
|
260
271
|
return {
|
|
261
272
|
path: filePath,
|
|
262
273
|
size: 0,
|
|
@@ -267,6 +278,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
267
278
|
|
|
268
279
|
// Check if file exists
|
|
269
280
|
const exists = await fileExists(filePath);
|
|
281
|
+
logger.debug(
|
|
282
|
+
'file-operations - filterFiles',
|
|
283
|
+
'File exists check',
|
|
284
|
+
{ filePath, exists }
|
|
285
|
+
);
|
|
270
286
|
if (!exists) {
|
|
271
287
|
return {
|
|
272
288
|
path: filePath,
|
|
@@ -281,6 +297,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
281
297
|
const size = await getFileSize(filePath);
|
|
282
298
|
|
|
283
299
|
if (size > maxSize) {
|
|
300
|
+
logger.debug(
|
|
301
|
+
'file-operations - filterFiles',
|
|
302
|
+
'File too large',
|
|
303
|
+
{ filePath, size, maxSize, 'size (KB)': Math.round(size / 1024), 'maxSize (KB)': Math.round(maxSize / 1024) }
|
|
304
|
+
);
|
|
284
305
|
return {
|
|
285
306
|
path: filePath,
|
|
286
307
|
size,
|
|
@@ -289,6 +310,12 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
289
310
|
};
|
|
290
311
|
}
|
|
291
312
|
|
|
313
|
+
logger.debug(
|
|
314
|
+
'file-operations - filterFiles',
|
|
315
|
+
'File passed size check',
|
|
316
|
+
{ filePath, size, maxSize, 'size (KB)': Math.round(size / 1024) }
|
|
317
|
+
);
|
|
318
|
+
|
|
292
319
|
return {
|
|
293
320
|
path: filePath,
|
|
294
321
|
size,
|
|
@@ -297,6 +324,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
297
324
|
};
|
|
298
325
|
|
|
299
326
|
} catch (error) {
|
|
327
|
+
logger.debug(
|
|
328
|
+
'file-operations - filterFiles',
|
|
329
|
+
'Error reading file',
|
|
330
|
+
{ filePath, error: error.message }
|
|
331
|
+
);
|
|
300
332
|
return {
|
|
301
333
|
path: filePath,
|
|
302
334
|
size: 0,
|
|
@@ -312,15 +344,17 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
|
|
|
312
344
|
.filter(r => r.status === 'fulfilled')
|
|
313
345
|
.map(r => r.value);
|
|
314
346
|
|
|
315
|
-
const
|
|
347
|
+
const validFiles = fileMetadata.filter(f => f.valid);
|
|
348
|
+
const invalidFiles = fileMetadata.filter(f => !f.valid);
|
|
316
349
|
|
|
317
350
|
logger.debug(
|
|
318
351
|
'file-operations - filterFiles',
|
|
319
352
|
'Filtering complete',
|
|
320
353
|
{
|
|
321
354
|
totalFiles: files.length,
|
|
322
|
-
validFiles:
|
|
323
|
-
invalidFiles:
|
|
355
|
+
validFiles: validFiles.length,
|
|
356
|
+
invalidFiles: invalidFiles.length,
|
|
357
|
+
rejectedFiles: invalidFiles.map(f => ({ path: f.path, reason: f.reason }))
|
|
324
358
|
}
|
|
325
359
|
);
|
|
326
360
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: installation-diagnostics.js
|
|
3
|
+
* Purpose: Reusable error diagnostics and formatting utilities
|
|
4
|
+
*
|
|
5
|
+
* Key features:
|
|
6
|
+
* - Generic error formatting with installation diagnostics
|
|
7
|
+
* - Detects common installation issues
|
|
8
|
+
* - Provides actionable remediation steps
|
|
9
|
+
* - Extensible for future diagnostic checks
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { formatError } from './installation-diagnostics.js';
|
|
13
|
+
*
|
|
14
|
+
* try {
|
|
15
|
+
* // ... operation that might fail
|
|
16
|
+
* } catch (error) {
|
|
17
|
+
* console.error(formatError('Presets not found'));
|
|
18
|
+
* process.exit(1);
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { getRepoRoot } from './git-operations.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets installation diagnostics
|
|
28
|
+
* Why: Centralized logic to detect common installation issues
|
|
29
|
+
*
|
|
30
|
+
* Future enhancements:
|
|
31
|
+
* - Check file permissions
|
|
32
|
+
* - Verify Claude CLI installation
|
|
33
|
+
* - Check Node.js version compatibility
|
|
34
|
+
* - Validate .gitignore entries
|
|
35
|
+
* - Check hook file integrity
|
|
36
|
+
* - Verify template files exist
|
|
37
|
+
* - Check config.json validity
|
|
38
|
+
*
|
|
39
|
+
* @returns {Object} Diagnostic information
|
|
40
|
+
*/
|
|
41
|
+
export const getInstallationDiagnostics = () => {
|
|
42
|
+
const diagnostics = {
|
|
43
|
+
currentDir: process.cwd(),
|
|
44
|
+
repoRoot: null,
|
|
45
|
+
isInRepoRoot: false,
|
|
46
|
+
claudeDirExists: false,
|
|
47
|
+
claudeDirPath: null,
|
|
48
|
+
presetsDirExists: false,
|
|
49
|
+
presetsDirPath: null,
|
|
50
|
+
gitHooksExists: false,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
diagnostics.repoRoot = getRepoRoot();
|
|
55
|
+
diagnostics.isInRepoRoot = diagnostics.currentDir === diagnostics.repoRoot;
|
|
56
|
+
|
|
57
|
+
diagnostics.claudeDirPath = path.join(diagnostics.repoRoot, '.claude');
|
|
58
|
+
diagnostics.claudeDirExists = fs.existsSync(diagnostics.claudeDirPath);
|
|
59
|
+
|
|
60
|
+
diagnostics.presetsDirPath = path.join(diagnostics.claudeDirPath, 'presets');
|
|
61
|
+
diagnostics.presetsDirExists = fs.existsSync(diagnostics.presetsDirPath);
|
|
62
|
+
|
|
63
|
+
const gitHooksPath = path.join(diagnostics.repoRoot, '.git', 'hooks');
|
|
64
|
+
diagnostics.gitHooksExists = fs.existsSync(gitHooksPath);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Not in a git repository - diagnostics.repoRoot will be null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return diagnostics;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats error message with diagnostics and remediation steps
|
|
74
|
+
* Why: Provides consistent, actionable error messages across all errors
|
|
75
|
+
*
|
|
76
|
+
* @param {string} errorMessage - Description of what failed (e.g., "Presets not found", "Template file missing")
|
|
77
|
+
* @param {string[]} additionalContext - Optional additional context lines
|
|
78
|
+
* @returns {string} Formatted error message with diagnostics and remediation steps
|
|
79
|
+
*/
|
|
80
|
+
export const formatError = (errorMessage, additionalContext = []) => {
|
|
81
|
+
const diagnostics = getInstallationDiagnostics();
|
|
82
|
+
const lines = [];
|
|
83
|
+
|
|
84
|
+
lines.push(`ā ļø ${errorMessage}`);
|
|
85
|
+
lines.push('');
|
|
86
|
+
|
|
87
|
+
// Add any additional context first
|
|
88
|
+
if (additionalContext.length > 0) {
|
|
89
|
+
lines.push(...additionalContext);
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Diagnostic information
|
|
94
|
+
lines.push('Installation diagnostics:');
|
|
95
|
+
lines.push(` Current directory: ${diagnostics.currentDir}`);
|
|
96
|
+
if (diagnostics.repoRoot) {
|
|
97
|
+
lines.push(` Repository root: ${diagnostics.repoRoot}`);
|
|
98
|
+
lines.push(` .claude/ exists: ${diagnostics.claudeDirExists ? 'ā' : 'ā'}`);
|
|
99
|
+
lines.push(` presets/ exists: ${diagnostics.presetsDirExists ? 'ā' : 'ā'}`);
|
|
100
|
+
} else {
|
|
101
|
+
lines.push(` Repository root: [Not in a git repository]`);
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
// Remediation steps based on detected issues
|
|
106
|
+
lines.push('Recommended solution:');
|
|
107
|
+
if (!diagnostics.repoRoot) {
|
|
108
|
+
lines.push(' Not in a git repository');
|
|
109
|
+
lines.push(' ā Navigate to your repository and try again');
|
|
110
|
+
} else if (!diagnostics.claudeDirExists) {
|
|
111
|
+
lines.push(' claude-hooks not installed');
|
|
112
|
+
if (!diagnostics.isInRepoRoot) {
|
|
113
|
+
lines.push(` ā cd ${diagnostics.repoRoot}`);
|
|
114
|
+
lines.push(' ā claude-hooks install');
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(' ā claude-hooks install');
|
|
117
|
+
}
|
|
118
|
+
} else if (!diagnostics.isInRepoRoot) {
|
|
119
|
+
lines.push(' Running from subdirectory (may cause path issues)');
|
|
120
|
+
lines.push(` ā cd ${diagnostics.repoRoot}`);
|
|
121
|
+
lines.push(' ā claude-hooks uninstall');
|
|
122
|
+
lines.push(' ā claude-hooks install');
|
|
123
|
+
} else if (!diagnostics.presetsDirExists) {
|
|
124
|
+
lines.push(' Incomplete installation (presets missing)');
|
|
125
|
+
lines.push(' ā claude-hooks install --force');
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(' Unknown issue detected');
|
|
128
|
+
lines.push(' ā claude-hooks install --force');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks if installation appears healthy
|
|
136
|
+
* Why: Quick validation before operations that require full installation
|
|
137
|
+
*
|
|
138
|
+
* @returns {boolean} True if installation looks healthy
|
|
139
|
+
*/
|
|
140
|
+
export const isInstallationHealthy = () => {
|
|
141
|
+
const diagnostics = getInstallationDiagnostics();
|
|
142
|
+
return diagnostics.claudeDirExists &&
|
|
143
|
+
diagnostics.presetsDirExists &&
|
|
144
|
+
diagnostics.gitHooksExists;
|
|
145
|
+
};
|
package/lib/utils/logger.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
class Logger {
|
|
18
18
|
constructor({ debugMode = false } = {}) {
|
|
19
|
-
this.debugMode = debugMode
|
|
19
|
+
this.debugMode = debugMode;
|
|
20
20
|
this.colors = {
|
|
21
21
|
reset: '\x1b[0m',
|
|
22
22
|
red: '\x1b[31m',
|
|
@@ -138,4 +138,4 @@ class Logger {
|
|
|
138
138
|
|
|
139
139
|
// Export singleton instance
|
|
140
140
|
// Why: Single logger instance ensures consistent debug mode across entire application
|
|
141
|
-
export default new Logger(
|
|
141
|
+
export default new Logger();
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
* - path: Cross-platform path handling
|
|
14
14
|
* - git-operations: For getRepoRoot()
|
|
15
15
|
* - logger: Debug and error logging
|
|
16
|
+
* - installation-diagnostics: Error formatting with remediation steps
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import fs from 'fs/promises';
|
|
19
20
|
import path from 'path';
|
|
20
21
|
import { getRepoRoot } from './git-operations.js';
|
|
21
22
|
import logger from './logger.js';
|
|
23
|
+
import { formatError } from './installation-diagnostics.js';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Custom error for preset loading failures
|
|
@@ -189,7 +191,10 @@ export async function listPresets() {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
} catch (error) {
|
|
192
|
-
|
|
194
|
+
const errorMsg = formatError('No presets directory found', [
|
|
195
|
+
`Expected location: ${presetsDir}`
|
|
196
|
+
]);
|
|
197
|
+
logger.warning(errorMsg);
|
|
193
198
|
logger.debug(
|
|
194
199
|
'preset-loader - listPresets',
|
|
195
200
|
'Failed to read presets directory',
|