claude-git-hooks 2.1.0 → 2.3.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +203 -79
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +163 -0
  5. package/lib/hooks/pre-commit.js +179 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +93 -11
  8. package/lib/utils/file-operations.js +1 -65
  9. package/lib/utils/file-utils.js +65 -0
  10. package/lib/utils/package-info.js +75 -0
  11. package/lib/utils/preset-loader.js +209 -0
  12. package/lib/utils/prompt-builder.js +83 -67
  13. package/lib/utils/resolution-prompt.js +12 -2
  14. package/package.json +49 -50
  15. package/templates/ANALYZE_DIFF.md +33 -0
  16. package/templates/COMMIT_MESSAGE.md +24 -0
  17. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  18. package/templates/config.example.json +41 -0
  19. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  20. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  21. package/templates/presets/ai/config.json +12 -0
  22. package/templates/presets/ai/preset.json +42 -0
  23. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  24. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  25. package/templates/presets/backend/config.json +12 -0
  26. package/templates/presets/backend/preset.json +49 -0
  27. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  28. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  29. package/templates/presets/database/config.json +12 -0
  30. package/templates/presets/database/preset.json +38 -0
  31. package/templates/presets/default/config.json +12 -0
  32. package/templates/presets/default/preset.json +53 -0
  33. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  34. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  35. package/templates/presets/frontend/config.json +12 -0
  36. package/templates/presets/frontend/preset.json +50 -0
  37. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  38. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  39. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  40. package/templates/presets/fullstack/config.json +12 -0
  41. package/templates/presets/fullstack/preset.json +55 -0
  42. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  43. package/templates/shared/ANALYZE_DIFF.md +33 -0
  44. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  45. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  46. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  47. package/templates/check-version.sh +0 -266
@@ -16,8 +16,10 @@
16
16
 
17
17
  import { spawn, execSync } from 'child_process';
18
18
  import fs from 'fs/promises';
19
+ import path from 'path';
19
20
  import os from 'os';
20
21
  import logger from './logger.js';
22
+ import config from '../config.js';
21
23
 
22
24
  /**
23
25
  * Custom error for Claude client failures
@@ -53,7 +55,7 @@ const isWSLAvailable = () => {
53
55
 
54
56
  try {
55
57
  // Try to run wsl --version to check if WSL is installed
56
- execSync('wsl --version', { stdio: 'ignore', timeout: 3000 });
58
+ execSync('wsl --version', { stdio: 'ignore', timeout: config.system.wslCheckTimeout });
57
59
  return true;
58
60
  } catch (error) {
59
61
  logger.debug('claude-client - isWSLAvailable', 'WSL not available', error);
@@ -294,15 +296,45 @@ const extractJSON = (response) => {
294
296
  };
295
297
 
296
298
  /**
297
- * Saves response to debug file
298
- * Why: Helps troubleshoot issues with Claude responses
299
+ * Saves prompt and response to debug file
300
+ * Why: Helps troubleshoot issues with Claude responses and verify prompts
299
301
  *
300
- * @param {string} response - Raw response to save
301
- * @param {string} filename - Debug filename (default: 'debug-claude-response.json')
302
+ * @param {string} prompt - Prompt sent to Claude
303
+ * @param {string} response - Raw response from Claude
304
+ * @param {string} filename - Debug filename (default: from config)
302
305
  */
303
- const saveDebugResponse = async (response, filename = 'debug-claude-response.json') => {
306
+ const saveDebugResponse = async (prompt, response, filename = config.output.debugFile) => {
304
307
  try {
305
- await fs.writeFile(filename, response, 'utf8');
308
+ // Ensure output directory exists
309
+ const outputDir = path.dirname(filename);
310
+ await fs.mkdir(outputDir, { recursive: true });
311
+
312
+ // Save full debug information
313
+ const debugData = {
314
+ timestamp: new Date().toISOString(),
315
+ promptLength: prompt.length,
316
+ responseLength: response.length,
317
+ prompt: prompt,
318
+ response: response
319
+ };
320
+
321
+ await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8');
322
+
323
+ // Display batch optimization status
324
+ try {
325
+ if (prompt.includes('OPTIMIZATION')) {
326
+ console.log('\n' + '='.repeat(70));
327
+ console.log('✅ BATCH OPTIMIZATION ENABLED');
328
+ console.log('='.repeat(70));
329
+ console.log('Multi-file analysis organized for efficient processing');
330
+ console.log('Check debug file for full prompt and response details');
331
+ console.log('='.repeat(70) + '\n');
332
+ }
333
+ } catch (parseError) {
334
+ // Ignore parsing errors, just skip the display
335
+ }
336
+
337
+ logger.info(`📝 Debug output saved to ${filename}`);
306
338
  logger.debug(
307
339
  'claude-client - saveDebugResponse',
308
340
  `Debug response saved to ${filename}`
@@ -323,11 +355,11 @@ const saveDebugResponse = async (response, filename = 'debug-claude-response.jso
323
355
  * @param {string} prompt - Analysis prompt
324
356
  * @param {Object} options - Analysis options
325
357
  * @param {number} options.timeout - Timeout in milliseconds
326
- * @param {boolean} options.saveDebug - Save response to debug file (default: from DEBUG env)
358
+ * @param {boolean} options.saveDebug - Save response to debug file (default: from config)
327
359
  * @returns {Promise<Object>} Parsed analysis result
328
360
  * @throws {ClaudeClientError} If analysis fails
329
361
  */
330
- const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.DEBUG === 'true' } = {}) => {
362
+ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug } = {}) => {
331
363
  logger.debug(
332
364
  'claude-client - analyzeCode',
333
365
  'Starting code analysis',
@@ -340,7 +372,7 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
340
372
 
341
373
  // Save debug if requested
342
374
  if (saveDebug) {
343
- await saveDebugResponse(response);
375
+ await saveDebugResponse(prompt, response);
344
376
  }
345
377
 
346
378
  // Extract and parse JSON
@@ -364,10 +396,60 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.D
364
396
  }
365
397
  };
366
398
 
399
+ /**
400
+ * Splits array into chunks
401
+ * @param {Array} array - Array to split
402
+ * @param {number} size - Chunk size
403
+ * @returns {Array<Array>} Array of chunks
404
+ */
405
+ const chunkArray = (array, size) => {
406
+ const chunks = [];
407
+ for (let i = 0; i < array.length; i += size) {
408
+ chunks.push(array.slice(i, i + size));
409
+ }
410
+ return chunks;
411
+ };
412
+
413
+ /**
414
+ * Runs multiple analyzeCode calls in parallel
415
+ * @param {Array<string>} prompts - Array of prompts to analyze
416
+ * @param {Object} options - Same options as analyzeCode
417
+ * @returns {Promise<Array<Object>>} Array of results
418
+ */
419
+ const analyzeCodeParallel = async (prompts, options = {}) => {
420
+ const startTime = Date.now();
421
+
422
+ console.log('\n' + '='.repeat(70));
423
+ console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
424
+ console.log('='.repeat(70));
425
+
426
+ logger.info(`Starting parallel analysis: ${prompts.length} prompts`);
427
+
428
+ const promises = prompts.map((prompt, index) => {
429
+ console.log(` ⚡ Launching batch ${index + 1}/${prompts.length}...`);
430
+ logger.debug('claude-client - analyzeCodeParallel', `Starting batch ${index + 1}`);
431
+ return analyzeCode(prompt, options);
432
+ });
433
+
434
+ console.log(` ⏳ Waiting for all batches to complete...\n`);
435
+ const results = await Promise.all(promises);
436
+
437
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
438
+
439
+ console.log('='.repeat(70));
440
+ console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
441
+ console.log('='.repeat(70) + '\n');
442
+
443
+ logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
444
+ return results;
445
+ };
446
+
367
447
  export {
368
448
  ClaudeClientError,
369
449
  executeClaude,
370
450
  extractJSON,
371
451
  saveDebugResponse,
372
- analyzeCode
452
+ analyzeCode,
453
+ analyzeCodeParallel,
454
+ chunkArray
373
455
  };
@@ -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
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * File: package-info.js
3
+ * Purpose: Utility for reading package.json information
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ /**
14
+ * @typedef {Object} PackageData
15
+ * @property {string} name - Package name
16
+ * @property {string} version - Package version
17
+ * @property {string} [description] - Package description
18
+ */
19
+
20
+ /**
21
+ * Cache for package.json to avoid repeated file reads
22
+ * @type {PackageData|undefined}
23
+ */
24
+ let packageCache;
25
+
26
+ /**
27
+ * Gets package.json data with caching (async)
28
+ * @returns {Promise<PackageData>} Package data with name and version
29
+ * @throws {Error} If package.json cannot be read
30
+ */
31
+ export const getPackageJson = async () => {
32
+ // Return cached value if available
33
+ if (packageCache) {
34
+ return packageCache;
35
+ }
36
+
37
+ try {
38
+ const packagePath = path.join(__dirname, '..', '..', 'package.json');
39
+ const content = await fs.readFile(packagePath, 'utf8');
40
+ packageCache = JSON.parse(content);
41
+ return packageCache;
42
+ } catch (error) {
43
+ throw new Error(`Failed to read package.json: ${error.message}`);
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Gets package version (async)
49
+ * @returns {Promise<string>} Package version
50
+ */
51
+ export const getVersion = async () => {
52
+ const pkg = await getPackageJson();
53
+ return pkg.version;
54
+ };
55
+
56
+ /**
57
+ * Gets package name (async)
58
+ * @returns {Promise<string>} Package name
59
+ */
60
+ export const getPackageName = async () => {
61
+ const pkg = await getPackageJson();
62
+ return pkg.name;
63
+ };
64
+
65
+ /**
66
+ * Calculates batch information for subagent processing
67
+ * @param {number} fileCount - Number of files to process
68
+ * @param {number} batchSize - Size of each batch
69
+ * @returns {Object} Batch information { numBatches, shouldShowBatches }
70
+ */
71
+ export const calculateBatches = (fileCount, batchSize) => {
72
+ const numBatches = Math.ceil(fileCount / batchSize);
73
+ const shouldShowBatches = fileCount > batchSize;
74
+ return { numBatches, shouldShowBatches };
75
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * File: preset-loader.js
3
+ * Purpose: Loads preset configurations and metadata
4
+ *
5
+ * Key responsibilities:
6
+ * - Load preset.json (metadata)
7
+ * - Load config.json (overrides)
8
+ * - Resolve template paths
9
+ * - Replace placeholders in templates
10
+ *
11
+ * Dependencies:
12
+ * - fs/promises: Async file operations
13
+ * - path: Cross-platform path handling
14
+ * - git-operations: For getRepoRoot()
15
+ * - logger: Debug and error logging
16
+ */
17
+
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import { getRepoRoot } from './git-operations.js';
21
+ import logger from './logger.js';
22
+
23
+ /**
24
+ * Custom error for preset loading failures
25
+ */
26
+ class PresetError extends Error {
27
+ constructor(message, { presetName, cause } = {}) {
28
+ super(message);
29
+ this.name = 'PresetError';
30
+ this.presetName = presetName;
31
+ this.cause = cause;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Loads preset metadata + config
37
+ * @param {string} presetName - Name of preset (backend, frontend, etc.)
38
+ * @returns {Promise<Object>} { metadata, config, templates }
39
+ */
40
+ export async function loadPreset(presetName) {
41
+ logger.debug(
42
+ 'preset-loader - loadPreset',
43
+ 'Loading preset',
44
+ { presetName }
45
+ );
46
+
47
+ const repoRoot = getRepoRoot();
48
+
49
+ // Only try .claude/presets/{name} (installed by claude-hooks install)
50
+ const presetDir = path.join(repoRoot, '.claude', 'presets', presetName);
51
+
52
+ logger.debug('preset-loader - loadPreset', 'Loading preset from', { presetDir });
53
+
54
+ try {
55
+ // Load preset.json (metadata)
56
+ const presetJsonPath = path.join(presetDir, 'preset.json');
57
+ const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
58
+ const metadata = JSON.parse(metadataRaw);
59
+
60
+ // Load config.json (overrides)
61
+ const configJsonPath = path.join(presetDir, 'config.json');
62
+ const configRaw = await fs.readFile(configJsonPath, 'utf8');
63
+ const config = JSON.parse(configRaw);
64
+
65
+ // Resolve template paths
66
+ const templates = {};
67
+ for (const [key, templatePath] of Object.entries(metadata.templates)) {
68
+ templates[key] = path.join(presetDir, templatePath);
69
+ }
70
+
71
+ logger.debug(
72
+ 'preset-loader - loadPreset',
73
+ 'Preset loaded successfully',
74
+ {
75
+ presetName,
76
+ displayName: metadata.displayName,
77
+ fileExtensions: metadata.fileExtensions,
78
+ templateCount: Object.keys(templates).length
79
+ }
80
+ );
81
+
82
+ return { metadata, config, templates };
83
+
84
+ } catch (error) {
85
+ logger.error(
86
+ 'preset-loader - loadPreset',
87
+ `Failed to load preset: ${presetName}`,
88
+ error
89
+ );
90
+
91
+ throw new PresetError(`Preset "${presetName}" not found or invalid`, {
92
+ presetName,
93
+ cause: error
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Loads a template file and replaces placeholders
100
+ * @param {string} templatePath - Absolute path to template file
101
+ * @param {Object} metadata - Preset metadata for placeholder replacement
102
+ * @returns {Promise<string>} Template content with placeholders replaced
103
+ */
104
+ export async function loadTemplate(templatePath, metadata) {
105
+ logger.debug(
106
+ 'preset-loader - loadTemplate',
107
+ 'Loading template',
108
+ { templatePath }
109
+ );
110
+
111
+ try {
112
+ let template = await fs.readFile(templatePath, 'utf8');
113
+
114
+ // Replace placeholders
115
+ template = template.replace(
116
+ /{{TECH_STACK}}/g,
117
+ metadata.techStack.join(', ')
118
+ );
119
+ template = template.replace(
120
+ /{{FOCUS_AREAS}}/g,
121
+ metadata.focusAreas.join(', ')
122
+ );
123
+ template = template.replace(
124
+ /{{FILE_EXTENSIONS}}/g,
125
+ metadata.fileExtensions.join(', ')
126
+ );
127
+ template = template.replace(
128
+ /{{PRESET_NAME}}/g,
129
+ metadata.name
130
+ );
131
+
132
+ logger.debug(
133
+ 'preset-loader - loadTemplate',
134
+ 'Template loaded and processed',
135
+ {
136
+ templatePath,
137
+ originalLength: template.length
138
+ }
139
+ );
140
+
141
+ return template;
142
+
143
+ } catch (error) {
144
+ logger.error(
145
+ 'preset-loader - loadTemplate',
146
+ `Failed to load template: ${templatePath}`,
147
+ error
148
+ );
149
+
150
+ throw new PresetError(`Template not found: ${templatePath}`, {
151
+ cause: error
152
+ });
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Lists all available presets in .claude/presets/
158
+ * @returns {Promise<Array<Object>>} Array of preset metadata
159
+ * Returns: [{ name, displayName, description }]
160
+ */
161
+ export async function listPresets() {
162
+ logger.debug('preset-loader - listPresets', 'Listing available presets');
163
+
164
+ const repoRoot = getRepoRoot();
165
+ const presets = [];
166
+
167
+ // Load all presets from .claude/presets/ (installed by claude-hooks install)
168
+ const presetsDir = path.join(repoRoot, '.claude', 'presets');
169
+ try {
170
+ const presetNames = await fs.readdir(presetsDir);
171
+
172
+ for (const name of presetNames) {
173
+ const presetJsonPath = path.join(presetsDir, name, 'preset.json');
174
+ try {
175
+ const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
176
+ const metadata = JSON.parse(metadataRaw);
177
+
178
+ presets.push({
179
+ name: metadata.name,
180
+ displayName: metadata.displayName,
181
+ description: metadata.description
182
+ });
183
+ } catch (error) {
184
+ logger.debug(
185
+ 'preset-loader - listPresets',
186
+ `Skipping invalid preset: ${name}`,
187
+ error
188
+ );
189
+ }
190
+ }
191
+ } catch (error) {
192
+ logger.warning('No presets directory found. Run "claude-hooks install" first.');
193
+ logger.debug(
194
+ 'preset-loader - listPresets',
195
+ 'Failed to read presets directory',
196
+ error
197
+ );
198
+ }
199
+
200
+ logger.debug(
201
+ 'preset-loader - listPresets',
202
+ 'Presets listed',
203
+ { count: presets.length }
204
+ );
205
+
206
+ return presets;
207
+ }
208
+
209
+ export { PresetError };