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.
@@ -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 with exit code ${code}`,
147
- new ClaudeClientError('Claude CLI execution failed', {
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
- 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}`, {
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
- { fileCount: files.length, maxSize, extensions }
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 validCount = fileMetadata.filter(f => f.valid).length;
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: validCount,
323
- invalidFiles: fileMetadata.length - validCount
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
+ };
@@ -16,7 +16,7 @@
16
16
 
17
17
  class Logger {
18
18
  constructor({ debugMode = false } = {}) {
19
- this.debugMode = debugMode || process.env.DEBUG === 'true';
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({ debugMode: process.env.DEBUG === 'true' });
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
- logger.warning('No presets directory found. Run "claude-hooks install" first.');
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {