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.
- package/CHANGELOG.md +178 -0
- package/README.md +203 -79
- package/bin/claude-hooks +295 -119
- package/lib/config.js +163 -0
- package/lib/hooks/pre-commit.js +179 -67
- package/lib/hooks/prepare-commit-msg.js +47 -41
- package/lib/utils/claude-client.js +93 -11
- package/lib/utils/file-operations.js +1 -65
- package/lib/utils/file-utils.js +65 -0
- package/lib/utils/package-info.js +75 -0
- package/lib/utils/preset-loader.js +209 -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/SUBAGENT_INSTRUCTION.md +1 -0
- package/templates/config.example.json +41 -0
- 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
package/lib/config.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: config.js
|
|
3
|
+
* Purpose: Centralized configuration management
|
|
4
|
+
*
|
|
5
|
+
* Priority: .claude/config.json > defaults
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Single source of truth for all configurable values
|
|
9
|
+
* - No environment variables (except OS for platform detection)
|
|
10
|
+
* - Preset-aware (allowedExtensions come from preset templates)
|
|
11
|
+
* - Override via .claude/config.json per project
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default configuration
|
|
23
|
+
* All values can be overridden via .claude/config.json
|
|
24
|
+
*/
|
|
25
|
+
const defaults = {
|
|
26
|
+
// Analysis configuration
|
|
27
|
+
analysis: {
|
|
28
|
+
maxFileSize: 100000, // 100KB - max file size to analyze
|
|
29
|
+
maxFiles: 10, // Max number of files per commit
|
|
30
|
+
timeout: 120000, // 2 minutes - Claude analysis timeout
|
|
31
|
+
contextLines: 3, // Git diff context lines
|
|
32
|
+
ignoreExtensions: [], // Extensions to exclude (e.g., ['.min.js', '.lock'])
|
|
33
|
+
// NOTE: allowedExtensions comes from preset/template, not here
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Commit message generation
|
|
37
|
+
commitMessage: {
|
|
38
|
+
autoKeyword: 'auto', // Keyword to trigger auto-generation
|
|
39
|
+
timeout: 180000,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Subagent configuration (parallel analysis)
|
|
43
|
+
subagents: {
|
|
44
|
+
enabled: true, // Enable parallel file analysis
|
|
45
|
+
model: 'haiku', // haiku (fast), sonnet (balanced), opus (thorough)
|
|
46
|
+
batchSize: 3, // Parallel subagents per batch
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Template paths
|
|
50
|
+
templates: {
|
|
51
|
+
baseDir: '.claude', // Base directory for all templates
|
|
52
|
+
analysis: 'CLAUDE_ANALYSIS_PROMPT_SONAR.md', // Pre-commit analysis prompt
|
|
53
|
+
guidelines: 'CLAUDE_PRE_COMMIT_SONAR.md', // Analysis guidelines
|
|
54
|
+
commitMessage: 'COMMIT_MESSAGE.md', // Commit message prompt
|
|
55
|
+
analyzeDiff: 'ANALYZE_DIFF.md', // PR analysis prompt
|
|
56
|
+
resolution: 'CLAUDE_RESOLUTION_PROMPT.md', // Issue resolution prompt
|
|
57
|
+
subagentInstruction: 'SUBAGENT_INSTRUCTION.md', // Parallel analysis instruction
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Output file paths (relative to repo root)
|
|
61
|
+
output: {
|
|
62
|
+
outputDir: '.claude/out', // Output directory for all generated files
|
|
63
|
+
debugFile: '.claude/out/debug-claude-response.json', // Debug response dump
|
|
64
|
+
resolutionFile: '.claude/out/claude_resolution_prompt.md', // Generated resolution prompt
|
|
65
|
+
prAnalysisFile: '.claude/out/pr-analysis.json', // PR analysis result
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// System behavior
|
|
69
|
+
system: {
|
|
70
|
+
debug: false, // Enable debug logging and file dumps
|
|
71
|
+
wslCheckTimeout: 3000, // 3 seconds - quick WSL availability check
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Git operations
|
|
75
|
+
git: {
|
|
76
|
+
diffFilter: 'ACM', // Added, Copied, Modified (excludes Deleted)
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Loads user configuration from .claude/config.json
|
|
82
|
+
* Merges with defaults and preset config (preset -> user config takes priority)
|
|
83
|
+
*
|
|
84
|
+
* Priority: defaults < preset config < user config
|
|
85
|
+
*
|
|
86
|
+
* @param {string} baseDir - Base directory to search for config (default: cwd)
|
|
87
|
+
* @returns {Promise<Object>} Merged configuration
|
|
88
|
+
*/
|
|
89
|
+
const loadUserConfig = async (baseDir = process.cwd()) => {
|
|
90
|
+
const configPath = path.join(baseDir, '.claude', 'config.json');
|
|
91
|
+
|
|
92
|
+
let userConfig = {};
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(configPath)) {
|
|
95
|
+
userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`ā ļø Warning: Could not load .claude/config.json: ${error.message}`);
|
|
99
|
+
console.warn('Using default configuration');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Load preset if specified
|
|
103
|
+
let presetConfig = {};
|
|
104
|
+
if (userConfig.preset) {
|
|
105
|
+
try {
|
|
106
|
+
// Dynamic import to avoid circular dependency
|
|
107
|
+
const { loadPreset } = await import('./utils/preset-loader.js');
|
|
108
|
+
const { config } = await loadPreset(userConfig.preset);
|
|
109
|
+
presetConfig = config;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`ā ļø Warning: Preset "${userConfig.preset}" not found, using defaults`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Merge: defaults < preset < user
|
|
116
|
+
return deepMerge(deepMerge(defaults, presetConfig), userConfig);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deep merge two objects (user config overrides defaults)
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} target - Target object (defaults)
|
|
123
|
+
* @param {Object} source - Source object (user overrides)
|
|
124
|
+
* @returns {Object} Merged object
|
|
125
|
+
*/
|
|
126
|
+
const deepMerge = (target, source) => {
|
|
127
|
+
const result = { ...target };
|
|
128
|
+
|
|
129
|
+
for (const key in source) {
|
|
130
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
131
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
132
|
+
} else {
|
|
133
|
+
result[key] = source[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get configuration instance
|
|
142
|
+
* Loads once, caches result
|
|
143
|
+
*
|
|
144
|
+
* Usage:
|
|
145
|
+
* import { getConfig } from './lib/config.js';
|
|
146
|
+
* const config = await getConfig();
|
|
147
|
+
* const maxFiles = config.analysis.maxFiles;
|
|
148
|
+
*/
|
|
149
|
+
let configInstance = null;
|
|
150
|
+
|
|
151
|
+
const getConfig = async () => {
|
|
152
|
+
if (!configInstance) {
|
|
153
|
+
configInstance = await loadUserConfig();
|
|
154
|
+
}
|
|
155
|
+
return configInstance;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Export async loader
|
|
159
|
+
export { getConfig, loadUserConfig, defaults };
|
|
160
|
+
|
|
161
|
+
// Export a sync default for backwards compatibility (loads without preset)
|
|
162
|
+
// NOTE: This will be deprecated - prefer using getConfig() directly
|
|
163
|
+
export default defaults;
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -30,57 +30,28 @@ import {
|
|
|
30
30
|
filterSkipAnalysis,
|
|
31
31
|
readFile
|
|
32
32
|
} from '../utils/file-operations.js';
|
|
33
|
-
import { analyzeCode } from '../utils/claude-client.js';
|
|
33
|
+
import { analyzeCode, analyzeCodeParallel, chunkArray } from '../utils/claude-client.js';
|
|
34
34
|
import { buildAnalysisPrompt } from '../utils/prompt-builder.js';
|
|
35
35
|
import {
|
|
36
36
|
generateResolutionPrompt,
|
|
37
37
|
shouldGeneratePrompt
|
|
38
38
|
} from '../utils/resolution-prompt.js';
|
|
39
|
+
import { loadPreset } from '../utils/preset-loader.js';
|
|
40
|
+
import { getVersion, calculateBatches } from '../utils/package-info.js';
|
|
39
41
|
import logger from '../utils/logger.js';
|
|
42
|
+
import { getConfig } from '../config.js';
|
|
40
43
|
|
|
41
|
-
// Configuration constants
|
|
42
44
|
/**
|
|
43
|
-
*
|
|
45
|
+
* Configuration loaded from lib/config.js
|
|
46
|
+
* Override via .claude/config.json
|
|
44
47
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* Overhead
|
|
48
|
-
* -
|
|
49
|
-
* -
|
|
50
|
-
* -
|
|
51
|
-
* - Total overhead: 4.2KB ā 1,074 tokens
|
|
52
|
-
*
|
|
53
|
-
* Escenarios con lĆmites actuales:
|
|
54
|
-
*
|
|
55
|
-
* 1) 10 archivos modificados (solo diffs):
|
|
56
|
-
* - Diff promedio: 4KB/archivo Ć 10 = 40KB ā 10,240 tokens
|
|
57
|
-
* - Total: 44.2KB ā 11,314 tokens (5.6% de 200k) ā
|
|
58
|
-
*
|
|
59
|
-
* 2) 10 archivos nuevos (contenido completo):
|
|
60
|
-
* - Archivo promedio: 30KB Ć 10 = 300KB ā 76,800 tokens
|
|
61
|
-
* - Total: 304.2KB ā 77,874 tokens (38.9% de 200k) ā
|
|
62
|
-
*
|
|
63
|
-
* 3) MÔximo permitido (10 à 100KB):
|
|
64
|
-
* - Total: 1,004KB ā 257,074 tokens (128.5% de 200k) ā ļø EXCEDE
|
|
65
|
-
*
|
|
66
|
-
* Con SUBAGENTES (anƔlisis paralelo):
|
|
67
|
-
* - Cada archivo analizado independientemente con su propio overhead
|
|
68
|
-
* - Batch size default: 3 archivos en paralelo
|
|
69
|
-
* - Uso mĆ”ximo por subagente: ~26,265 tokens (13% de 200k) ā
|
|
70
|
-
*
|
|
71
|
-
* Recomendaciones:
|
|
72
|
-
* - LĆmites actuales seguros para archivos modificados (diffs)
|
|
73
|
-
* - Con subagentes: escala bien hasta 15-20 archivos
|
|
74
|
-
* - Sin subagentes: limitar a 5-7 archivos grandes
|
|
75
|
-
* - Considerar MAX_NEW_FILE_SIZE menor si se analizan muchos archivos nuevos
|
|
48
|
+
* TOKEN USAGE ANALYSIS (200k context window):
|
|
49
|
+
* - 1 token ā 4 chars, 1KB ā 256 tokens
|
|
50
|
+
* - Overhead: template(512) + guidelines(512) + metadata(50) ā 1,074 tokens
|
|
51
|
+
* - 10 modified files (diffs): ~11,314 tokens (5.6%) ā
|
|
52
|
+
* - 10 new files (100KB each): ~257,074 tokens (128%) ā ļø EXCEEDS
|
|
53
|
+
* - With subagents (batch=3): ~26,265 tokens per batch (13%) ā
|
|
76
54
|
*/
|
|
77
|
-
const CONFIG = {
|
|
78
|
-
MAX_FILE_SIZE: 100000, // 100KB
|
|
79
|
-
MAX_FILES: 10,
|
|
80
|
-
ALLOWED_EXTENSIONS: ['.java', '.xml', '.properties', '.yml', '.yaml'],
|
|
81
|
-
TEMPLATE_NAME: 'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
|
|
82
|
-
GUIDELINES_NAME: 'CLAUDE_PRE_COMMIT_SONAR.md'
|
|
83
|
-
};
|
|
84
55
|
|
|
85
56
|
/**
|
|
86
57
|
* Displays SonarQube-style analysis results
|
|
@@ -150,6 +121,56 @@ const displayResults = (result) => {
|
|
|
150
121
|
}
|
|
151
122
|
};
|
|
152
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Consolidates multiple analysis results into one
|
|
126
|
+
* @param {Array<Object>} results - Array of analysis results
|
|
127
|
+
* @returns {Object} - Consolidated result
|
|
128
|
+
*/
|
|
129
|
+
const consolidateResults = (results) => {
|
|
130
|
+
const consolidated = {
|
|
131
|
+
QUALITY_GATE: 'PASSED',
|
|
132
|
+
approved: true,
|
|
133
|
+
score: 10,
|
|
134
|
+
metrics: { reliability: 'A', security: 'A', maintainability: 'A', coverage: 100, duplications: 0, complexity: 0 },
|
|
135
|
+
issues: { blocker: 0, critical: 0, major: 0, minor: 0, info: 0 },
|
|
136
|
+
details: [],
|
|
137
|
+
blockingIssues: [],
|
|
138
|
+
securityHotspots: 0
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (const result of results) {
|
|
142
|
+
// Worst-case quality gate
|
|
143
|
+
if (result.QUALITY_GATE === 'FAILED') consolidated.QUALITY_GATE = 'FAILED';
|
|
144
|
+
if (result.approved === false) consolidated.approved = false;
|
|
145
|
+
if (result.score < consolidated.score) consolidated.score = result.score;
|
|
146
|
+
|
|
147
|
+
// Worst-case metrics
|
|
148
|
+
if (result.metrics) {
|
|
149
|
+
const metricOrder = { 'A': 5, 'B': 4, 'C': 3, 'D': 2, 'E': 1 };
|
|
150
|
+
['reliability', 'security', 'maintainability'].forEach(m => {
|
|
151
|
+
const current = metricOrder[consolidated.metrics[m]] || 5;
|
|
152
|
+
const incoming = metricOrder[result.metrics[m]] || 5;
|
|
153
|
+
if (incoming < current) consolidated.metrics[m] = result.metrics[m];
|
|
154
|
+
});
|
|
155
|
+
if (result.metrics.coverage !== undefined) consolidated.metrics.coverage = Math.min(consolidated.metrics.coverage, result.metrics.coverage);
|
|
156
|
+
if (result.metrics.duplications !== undefined) consolidated.metrics.duplications = Math.max(consolidated.metrics.duplications, result.metrics.duplications);
|
|
157
|
+
if (result.metrics.complexity !== undefined) consolidated.metrics.complexity = Math.max(consolidated.metrics.complexity, result.metrics.complexity);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Sum issue counts
|
|
161
|
+
if (result.issues) {
|
|
162
|
+
Object.keys(consolidated.issues).forEach(s => consolidated.issues[s] += (result.issues[s] || 0));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Merge arrays
|
|
166
|
+
if (Array.isArray(result.details)) consolidated.details.push(...result.details);
|
|
167
|
+
if (Array.isArray(result.blockingIssues)) consolidated.blockingIssues.push(...result.blockingIssues);
|
|
168
|
+
if (result.securityHotspots) consolidated.securityHotspots += result.securityHotspots;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return consolidated;
|
|
172
|
+
};
|
|
173
|
+
|
|
153
174
|
/**
|
|
154
175
|
* Main pre-commit hook execution
|
|
155
176
|
*/
|
|
@@ -157,18 +178,43 @@ const main = async () => {
|
|
|
157
178
|
const startTime = Date.now();
|
|
158
179
|
|
|
159
180
|
try {
|
|
181
|
+
// Load configuration
|
|
182
|
+
const config = await getConfig();
|
|
183
|
+
|
|
184
|
+
// Display configuration info
|
|
185
|
+
const version = await getVersion();
|
|
186
|
+
console.log(`\nš¤ claude-git-hooks v${version}`);
|
|
187
|
+
|
|
160
188
|
logger.info('Starting code quality analysis...');
|
|
161
189
|
|
|
162
190
|
logger.debug(
|
|
163
191
|
'pre-commit - main',
|
|
164
192
|
'Configuration',
|
|
165
|
-
{ ...
|
|
193
|
+
{ ...config }
|
|
166
194
|
);
|
|
167
195
|
|
|
168
|
-
//
|
|
196
|
+
// Load active preset
|
|
197
|
+
const presetName = config.preset || 'default';
|
|
198
|
+
const { metadata } = await loadPreset(presetName);
|
|
199
|
+
|
|
200
|
+
logger.info(`šÆ Analyzing with '${metadata.displayName}' preset`);
|
|
201
|
+
logger.debug(
|
|
202
|
+
'pre-commit - main',
|
|
203
|
+
'Preset loaded',
|
|
204
|
+
{
|
|
205
|
+
preset: presetName,
|
|
206
|
+
fileExtensions: metadata.fileExtensions,
|
|
207
|
+
techStack: metadata.techStack
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Use preset's file extensions
|
|
212
|
+
const allowedExtensions = metadata.fileExtensions;
|
|
213
|
+
|
|
214
|
+
// Step 1: Get staged files with preset extensions
|
|
169
215
|
logger.debug('pre-commit - main', 'Getting staged files');
|
|
170
216
|
const stagedFiles = getStagedFiles({
|
|
171
|
-
extensions:
|
|
217
|
+
extensions: allowedExtensions
|
|
172
218
|
});
|
|
173
219
|
|
|
174
220
|
if (stagedFiles.length === 0) {
|
|
@@ -177,12 +223,17 @@ const main = async () => {
|
|
|
177
223
|
}
|
|
178
224
|
|
|
179
225
|
logger.info(`Files to review: ${stagedFiles.length}`);
|
|
226
|
+
logger.debug(
|
|
227
|
+
'pre-commit - main',
|
|
228
|
+
'Files matched by preset',
|
|
229
|
+
{ count: stagedFiles.length, extensions: allowedExtensions }
|
|
230
|
+
);
|
|
180
231
|
|
|
181
232
|
// Step 2: Filter files by size
|
|
182
233
|
logger.debug('pre-commit - main', 'Filtering files by size');
|
|
183
234
|
const filteredFiles = await filterFiles(stagedFiles, {
|
|
184
|
-
maxSize:
|
|
185
|
-
extensions:
|
|
235
|
+
maxSize: config.analysis.maxFileSize,
|
|
236
|
+
extensions: allowedExtensions
|
|
186
237
|
});
|
|
187
238
|
|
|
188
239
|
const validFiles = filteredFiles.filter(f => f.valid);
|
|
@@ -192,7 +243,7 @@ const main = async () => {
|
|
|
192
243
|
process.exit(0);
|
|
193
244
|
}
|
|
194
245
|
|
|
195
|
-
if (validFiles.length >
|
|
246
|
+
if (validFiles.length > config.analysis.maxFiles) {
|
|
196
247
|
logger.warning(`Too many files to review (${validFiles.length})`);
|
|
197
248
|
logger.warning('Consider splitting the commit into smaller parts');
|
|
198
249
|
process.exit(0);
|
|
@@ -245,29 +296,90 @@ const main = async () => {
|
|
|
245
296
|
|
|
246
297
|
// Step 4: Build analysis prompt
|
|
247
298
|
logger.info(`Sending ${filesData.length} files for review...`);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
299
|
+
|
|
300
|
+
// Display subagent configuration
|
|
301
|
+
const subagentsEnabled = config.subagents?.enabled || false;
|
|
302
|
+
const subagentModel = config.subagents?.model || 'haiku';
|
|
303
|
+
const batchSize = config.subagents?.batchSize || 3;
|
|
304
|
+
|
|
305
|
+
if (subagentsEnabled && filesData.length >= 3) {
|
|
306
|
+
const { numBatches, shouldShowBatches } = calculateBatches(filesData.length, batchSize);
|
|
307
|
+
console.log(`ā” Batch optimization: ${subagentModel} model, ${batchSize} files per batch`);
|
|
308
|
+
if (shouldShowBatches) {
|
|
309
|
+
console.log(`š Analyzing ${filesData.length} files in ${numBatches} batch${numBatches > 1 ? 'es' : ''}`);
|
|
257
310
|
}
|
|
258
|
-
}
|
|
311
|
+
}
|
|
259
312
|
|
|
260
|
-
// Step 5: Analyze with Claude
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
313
|
+
// Step 5: Analyze with Claude (parallel or single)
|
|
314
|
+
let result;
|
|
315
|
+
|
|
316
|
+
if (subagentsEnabled && filesData.length >= 3) {
|
|
317
|
+
// Parallel execution: split files into batches
|
|
318
|
+
logger.info(`Using parallel execution with batch size ${batchSize}`);
|
|
319
|
+
|
|
320
|
+
const fileBatches = chunkArray(filesData, batchSize);
|
|
321
|
+
logger.debug('pre-commit - main', `Split into ${fileBatches.length} batches`);
|
|
322
|
+
|
|
323
|
+
// Build one prompt per batch
|
|
324
|
+
const prompts = await Promise.all(
|
|
325
|
+
fileBatches.map(async (batch) => {
|
|
326
|
+
return await buildAnalysisPrompt({
|
|
327
|
+
templateName: config.templates.analysis,
|
|
328
|
+
guidelinesName: config.templates.guidelines,
|
|
329
|
+
files: batch,
|
|
330
|
+
metadata: {
|
|
331
|
+
REPO_NAME: getRepoName(),
|
|
332
|
+
BRANCH_NAME: getCurrentBranch()
|
|
333
|
+
},
|
|
334
|
+
subagentConfig: null // Don't add subagent instruction for parallel
|
|
335
|
+
});
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Execute in parallel
|
|
340
|
+
const results = await analyzeCodeParallel(prompts, {
|
|
341
|
+
timeout: config.analysis.timeout,
|
|
342
|
+
saveDebug: false // Don't save debug for individual batches
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Simple consolidation: merge all results
|
|
346
|
+
result = consolidateResults(results);
|
|
347
|
+
|
|
348
|
+
// Save consolidated debug if enabled
|
|
349
|
+
if (config.system.debug) {
|
|
350
|
+
const { saveDebugResponse } = await import('../utils/claude-client.js');
|
|
351
|
+
await saveDebugResponse(
|
|
352
|
+
`PARALLEL ANALYSIS: ${fileBatches.length} batches`,
|
|
353
|
+
JSON.stringify(result, null, 2)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
266
356
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
} else {
|
|
358
|
+
// Single execution: original behavior
|
|
359
|
+
logger.debug('pre-commit - main', 'Building analysis prompt');
|
|
360
|
+
|
|
361
|
+
const prompt = await buildAnalysisPrompt({
|
|
362
|
+
templateName: config.templates.analysis,
|
|
363
|
+
guidelinesName: config.templates.guidelines,
|
|
364
|
+
files: filesData,
|
|
365
|
+
metadata: {
|
|
366
|
+
REPO_NAME: getRepoName(),
|
|
367
|
+
BRANCH_NAME: getCurrentBranch()
|
|
368
|
+
},
|
|
369
|
+
subagentConfig: config.subagents
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
logger.debug(
|
|
373
|
+
'pre-commit - main',
|
|
374
|
+
'Sending prompt to Claude',
|
|
375
|
+
{ promptLength: prompt.length }
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
result = await analyzeCode(prompt, {
|
|
379
|
+
timeout: config.analysis.timeout,
|
|
380
|
+
saveDebug: config.system.debug
|
|
381
|
+
});
|
|
382
|
+
}
|
|
271
383
|
|
|
272
384
|
// Step 6: Display results
|
|
273
385
|
displayResults(result);
|
|
@@ -21,13 +21,10 @@ import fs from 'fs/promises';
|
|
|
21
21
|
import { getStagedFiles, getStagedStats, getFileDiff } from '../utils/git-operations.js';
|
|
22
22
|
import { analyzeCode } from '../utils/claude-client.js';
|
|
23
23
|
import { filterSkipAnalysis } from '../utils/file-operations.js';
|
|
24
|
+
import { loadPrompt } from '../utils/prompt-builder.js';
|
|
25
|
+
import { getVersion, calculateBatches } from '../utils/package-info.js';
|
|
24
26
|
import logger from '../utils/logger.js';
|
|
25
|
-
|
|
26
|
-
// Configuration
|
|
27
|
-
const CONFIG = {
|
|
28
|
-
MAX_FILE_SIZE: 100000, // 100KB
|
|
29
|
-
AUTO_KEYWORD: 'auto'
|
|
30
|
-
};
|
|
27
|
+
import { getConfig } from '../config.js';
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* Builds commit message generation prompt
|
|
@@ -35,49 +32,36 @@ const CONFIG = {
|
|
|
35
32
|
*
|
|
36
33
|
* @param {Array<Object>} filesData - Array of file change data
|
|
37
34
|
* @param {Object} stats - Staging statistics
|
|
38
|
-
* @returns {string} Complete prompt
|
|
35
|
+
* @returns {Promise<string>} Complete prompt
|
|
39
36
|
*/
|
|
40
|
-
const buildCommitPrompt = (filesData, stats) => {
|
|
37
|
+
const buildCommitPrompt = async (filesData, stats) => {
|
|
41
38
|
logger.debug(
|
|
42
39
|
'prepare-commit-msg - buildCommitPrompt',
|
|
43
40
|
'Building commit message prompt',
|
|
44
41
|
{ fileCount: filesData.length }
|
|
45
42
|
);
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Respond ONLY with a valid JSON:
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
"type": "feat|fix|docs|style|refactor|test|chore|ci|perf",
|
|
53
|
-
"scope": "optional scope (e.g.: api, frontend, db)",
|
|
54
|
-
"title": "short description in present tense (max 50 chars)",
|
|
55
|
-
"body": "optional detailed description"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
CHANGES TO ANALYZE:
|
|
59
|
-
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
// Add file list
|
|
63
|
-
prompt += '\nModified files:\n';
|
|
64
|
-
filesData.forEach(({ path }) => {
|
|
65
|
-
prompt += `${path}\n`;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Add statistics
|
|
69
|
-
prompt += `\nSummary of changes:\n`;
|
|
70
|
-
prompt += `Files changed: ${stats.totalFiles || filesData.length}\n`;
|
|
71
|
-
prompt += `Insertions: ${stats.insertions || 0}, Deletions: ${stats.deletions || 0}\n`;
|
|
44
|
+
// Build file list
|
|
45
|
+
const fileList = filesData.map(({ path }) => path).join('\n');
|
|
72
46
|
|
|
73
|
-
//
|
|
47
|
+
// Build file diffs
|
|
48
|
+
let fileDiffs = '';
|
|
74
49
|
filesData.forEach(({ path, diff }) => {
|
|
75
50
|
if (diff) {
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
fileDiffs += `\n--- Diff of ${path} ---\n`;
|
|
52
|
+
fileDiffs += diff;
|
|
78
53
|
}
|
|
79
54
|
});
|
|
80
55
|
|
|
56
|
+
// Load prompt from template
|
|
57
|
+
const prompt = await loadPrompt('COMMIT_MESSAGE.md', {
|
|
58
|
+
FILE_LIST: fileList,
|
|
59
|
+
FILE_COUNT: stats.totalFiles || filesData.length,
|
|
60
|
+
INSERTIONS: stats.insertions || 0,
|
|
61
|
+
DELETIONS: stats.deletions || 0,
|
|
62
|
+
FILE_DIFFS: fileDiffs
|
|
63
|
+
});
|
|
64
|
+
|
|
81
65
|
return prompt;
|
|
82
66
|
};
|
|
83
67
|
|
|
@@ -133,6 +117,9 @@ const formatCommitMessage = (json) => {
|
|
|
133
117
|
const main = async () => {
|
|
134
118
|
const startTime = Date.now();
|
|
135
119
|
|
|
120
|
+
// Load configuration (includes preset + user overrides)
|
|
121
|
+
const config = await getConfig();
|
|
122
|
+
|
|
136
123
|
try {
|
|
137
124
|
// Get hook arguments
|
|
138
125
|
const args = process.argv.slice(2);
|
|
@@ -166,7 +153,7 @@ const main = async () => {
|
|
|
166
153
|
);
|
|
167
154
|
|
|
168
155
|
// Check if message is "auto"
|
|
169
|
-
if (firstLine !==
|
|
156
|
+
if (firstLine !== config.commitMessage.autoKeyword) {
|
|
170
157
|
logger.debug(
|
|
171
158
|
'prepare-commit-msg - main',
|
|
172
159
|
'Not generating: message is not "auto"'
|
|
@@ -174,6 +161,17 @@ const main = async () => {
|
|
|
174
161
|
process.exit(0);
|
|
175
162
|
}
|
|
176
163
|
|
|
164
|
+
// Display configuration info
|
|
165
|
+
const version = await getVersion();
|
|
166
|
+
const subagentsEnabled = config.subagents?.enabled || false;
|
|
167
|
+
const subagentModel = config.subagents?.model || 'haiku';
|
|
168
|
+
const batchSize = config.subagents?.batchSize || 3;
|
|
169
|
+
|
|
170
|
+
console.log(`\nš¤ claude-git-hooks v${version}`);
|
|
171
|
+
if (subagentsEnabled) {
|
|
172
|
+
console.log(`ā” Parallel analysis: ${subagentModel} model, batch size ${batchSize}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
177
175
|
logger.info('Generating commit message automatically...');
|
|
178
176
|
|
|
179
177
|
// Get staged files
|
|
@@ -203,7 +201,7 @@ const main = async () => {
|
|
|
203
201
|
|
|
204
202
|
// Only include diff for small files
|
|
205
203
|
let diff = null;
|
|
206
|
-
if (fileStats.size <
|
|
204
|
+
if (fileStats.size < config.analysis.maxFileSize) {
|
|
207
205
|
diff = getFileDiff(filePath);
|
|
208
206
|
diff = filterSkipAnalysis(diff);
|
|
209
207
|
}
|
|
@@ -231,7 +229,7 @@ const main = async () => {
|
|
|
231
229
|
);
|
|
232
230
|
|
|
233
231
|
// Build prompt
|
|
234
|
-
const prompt = buildCommitPrompt(filesData, stats);
|
|
232
|
+
const prompt = await buildCommitPrompt(filesData, stats);
|
|
235
233
|
|
|
236
234
|
logger.debug(
|
|
237
235
|
'prepare-commit-msg - main',
|
|
@@ -239,12 +237,20 @@ const main = async () => {
|
|
|
239
237
|
{ promptLength: prompt.length }
|
|
240
238
|
);
|
|
241
239
|
|
|
240
|
+
// Calculate batches if subagents enabled and applicable
|
|
241
|
+
if (subagentsEnabled && filesData.length >= 3) {
|
|
242
|
+
const { numBatches, shouldShowBatches } = calculateBatches(filesData.length, batchSize);
|
|
243
|
+
if (shouldShowBatches) {
|
|
244
|
+
console.log(`š Analyzing ${filesData.length} files in ${numBatches} batch${numBatches > 1 ? 'es' : ''}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
242
248
|
// Generate message with Claude
|
|
243
249
|
logger.info('Sending to Claude...');
|
|
244
250
|
|
|
245
251
|
const response = await analyzeCode(prompt, {
|
|
246
|
-
timeout:
|
|
247
|
-
saveDebug:
|
|
252
|
+
timeout: config.commitMessage.timeout,
|
|
253
|
+
saveDebug: config.system.debug
|
|
248
254
|
});
|
|
249
255
|
|
|
250
256
|
logger.debug(
|