claude-git-hooks 2.6.3 → 2.8.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 +173 -0
- package/README.md +77 -58
- package/bin/claude-hooks +322 -27
- package/lib/config.js +176 -83
- package/lib/hooks/pre-commit.js +11 -14
- package/lib/hooks/prepare-commit-msg.js +2 -3
- package/lib/utils/claude-client.js +199 -34
- package/lib/utils/claude-diagnostics.js +102 -11
- package/lib/utils/prompt-builder.js +2 -2
- package/package.json +1 -1
- package/templates/{CLAUDE_ANALYSIS_PROMPT_SONAR.md → CLAUDE_ANALYSIS_PROMPT.md} +2 -1
- package/templates/{CLAUDE_PRE_COMMIT_SONAR.md → CLAUDE_PRE_COMMIT.md} +10 -1
- package/templates/CUSTOMIZATION_GUIDE.md +3 -3
- package/templates/config.advanced.example.json +113 -0
- package/templates/config.example.json +53 -36
- package/templates/presets/ai/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/ai/config.json +5 -12
- package/templates/presets/backend/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/backend/config.json +5 -12
- package/templates/presets/database/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/database/config.json +5 -12
- package/templates/presets/default/config.json +5 -12
- package/templates/presets/frontend/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/frontend/config.json +5 -12
- package/templates/presets/fullstack/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/fullstack/config.json +5 -12
- package/templates/shared/ANALYSIS_PROMPT.md +1 -1
package/lib/config.js
CHANGED
|
@@ -1,101 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File: config.js
|
|
3
|
-
* Purpose:
|
|
3
|
+
* Purpose: Simplified configuration management (v2.8.0)
|
|
4
4
|
*
|
|
5
|
-
* Priority: preset config > .claude/config.json > defaults
|
|
5
|
+
* Priority: preset config > .claude/config.json > hardcoded defaults
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* - Preset
|
|
11
|
-
* - User
|
|
12
|
-
* -
|
|
7
|
+
* v2.8.0 Changes:
|
|
8
|
+
* - Removed 21 redundant parameters (hardcoded sensible defaults)
|
|
9
|
+
* - Only 5 user-configurable parameters remain
|
|
10
|
+
* - Preset can override: subagents.batchSize
|
|
11
|
+
* - User can override: github.pr.*, subagents.batchSize
|
|
12
|
+
* - Advanced params moved to config.advanced.example.json
|
|
13
|
+
*
|
|
14
|
+
* User-configurable:
|
|
15
|
+
* - preset (required)
|
|
16
|
+
* - github.pr.defaultBase
|
|
17
|
+
* - github.pr.reviewers
|
|
18
|
+
* - github.pr.labelRules
|
|
19
|
+
* - subagents.batchSize
|
|
20
|
+
*
|
|
21
|
+
* Advanced (in example file only):
|
|
22
|
+
* - analysis.ignoreExtensions
|
|
23
|
+
* - commitMessage.taskIdPattern
|
|
24
|
+
* - subagents.model
|
|
13
25
|
*/
|
|
14
26
|
|
|
15
27
|
import fs from 'fs';
|
|
16
28
|
import path from 'path';
|
|
17
29
|
import { fileURLToPath } from 'url';
|
|
30
|
+
import logger from './utils/logger.js';
|
|
18
31
|
|
|
19
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
33
|
const __dirname = path.dirname(__filename);
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
36
|
+
* Hardcoded defaults (v3.0.0)
|
|
37
|
+
* These are NOT user-configurable - sensible defaults that work for everyone
|
|
25
38
|
*/
|
|
26
|
-
const
|
|
27
|
-
// Analysis configuration
|
|
39
|
+
const HARDCODED = {
|
|
28
40
|
analysis: {
|
|
29
|
-
maxFileSize: 1000000, // 1MB -
|
|
30
|
-
maxFiles: 20, //
|
|
31
|
-
timeout: 300000, //
|
|
32
|
-
contextLines: 3, // Git
|
|
33
|
-
ignoreExtensions: [], //
|
|
34
|
-
// NOTE: allowedExtensions comes from preset/template, not here
|
|
41
|
+
maxFileSize: 1000000, // 1MB - sufficient for most files
|
|
42
|
+
maxFiles: 20, // Reasonable limit per commit
|
|
43
|
+
timeout: 300000, // 5 minutes - adequate for Claude API
|
|
44
|
+
contextLines: 3, // Git default
|
|
45
|
+
ignoreExtensions: [], // Can be set in advanced config only
|
|
35
46
|
},
|
|
36
|
-
|
|
37
|
-
// Commit message generation
|
|
38
47
|
commitMessage: {
|
|
39
|
-
autoKeyword: 'auto', //
|
|
40
|
-
timeout: 300000,
|
|
41
|
-
|
|
42
|
-
// Default: 1-3 uppercase letters + separator + 3-5 digits
|
|
43
|
-
// Examples: "ABC-12345", "IX-123", "DE 4567"
|
|
44
|
-
// Regex is case-insensitive and extracted from branch name
|
|
45
|
-
taskIdPattern: '([A-Z]{1,3}[-\\s]\\d{3,5})'
|
|
48
|
+
autoKeyword: 'auto', // Standard keyword
|
|
49
|
+
timeout: 300000, // Use same timeout as analysis
|
|
50
|
+
taskIdPattern: '([A-Z]{1,3}[-\\s]\\d{3,5})', // Jira/GitHub/Linear pattern
|
|
46
51
|
},
|
|
47
|
-
|
|
48
|
-
// Subagent configuration (parallel analysis)
|
|
49
52
|
subagents: {
|
|
50
|
-
enabled:
|
|
51
|
-
model: 'haiku', //
|
|
52
|
-
batchSize:
|
|
53
|
+
enabled: true, // Enable by default (faster analysis)
|
|
54
|
+
model: 'haiku', // Fast and cost-effective
|
|
55
|
+
batchSize: 3, // Reasonable parallelization
|
|
53
56
|
},
|
|
54
|
-
|
|
55
|
-
// Template paths
|
|
56
57
|
templates: {
|
|
57
|
-
baseDir: '.claude',
|
|
58
|
-
analysis: '
|
|
59
|
-
guidelines: '
|
|
60
|
-
commitMessage: 'COMMIT_MESSAGE.md',
|
|
61
|
-
analyzeDiff: 'ANALYZE_DIFF.md',
|
|
62
|
-
resolution: 'CLAUDE_RESOLUTION_PROMPT.md',
|
|
63
|
-
subagentInstruction: 'SUBAGENT_INSTRUCTION.md',
|
|
64
|
-
createGithubPR: 'CREATE_GITHUB_PR.md',
|
|
58
|
+
baseDir: '.claude/prompts',
|
|
59
|
+
analysis: 'CLAUDE_ANALYSIS_PROMPT.md',
|
|
60
|
+
guidelines: 'CLAUDE_PRE_COMMIT.md',
|
|
61
|
+
commitMessage: 'COMMIT_MESSAGE.md',
|
|
62
|
+
analyzeDiff: 'ANALYZE_DIFF.md',
|
|
63
|
+
resolution: 'CLAUDE_RESOLUTION_PROMPT.md',
|
|
64
|
+
subagentInstruction: 'SUBAGENT_INSTRUCTION.md',
|
|
65
|
+
createGithubPR: 'CREATE_GITHUB_PR.md',
|
|
65
66
|
},
|
|
66
|
-
|
|
67
|
-
// Output file paths (relative to repo root)
|
|
68
67
|
output: {
|
|
69
|
-
outputDir: '.claude/out',
|
|
70
|
-
debugFile: '.claude/out/debug-claude-response.json',
|
|
71
|
-
resolutionFile: '.claude/out/claude_resolution_prompt.md',
|
|
72
|
-
prAnalysisFile: '.claude/out/pr-analysis.json',
|
|
68
|
+
outputDir: '.claude/out',
|
|
69
|
+
debugFile: '.claude/out/debug-claude-response.json',
|
|
70
|
+
resolutionFile: '.claude/out/claude_resolution_prompt.md',
|
|
71
|
+
prAnalysisFile: '.claude/out/pr-analysis.json',
|
|
73
72
|
},
|
|
74
|
-
|
|
75
|
-
// System behavior
|
|
76
73
|
system: {
|
|
77
|
-
debug: false, //
|
|
78
|
-
wslCheckTimeout:
|
|
74
|
+
debug: false, // Controlled by --debug flag
|
|
75
|
+
wslCheckTimeout: 15000, // System behavior
|
|
79
76
|
},
|
|
80
|
-
|
|
81
|
-
// Git operations
|
|
82
77
|
git: {
|
|
83
|
-
diffFilter: 'ACM', // Added, Copied, Modified
|
|
78
|
+
diffFilter: 'ACM', // Standard: Added, Copied, Modified
|
|
84
79
|
},
|
|
85
|
-
|
|
86
|
-
// GitHub integration (v2.5.0+)
|
|
87
80
|
github: {
|
|
88
|
-
enabled: true, //
|
|
81
|
+
enabled: true, // Always enabled
|
|
82
|
+
},
|
|
83
|
+
};
|
|
89
84
|
|
|
90
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Default user-configurable values (v2.8.0)
|
|
87
|
+
* Only these can be overridden in .claude/config.json
|
|
88
|
+
*/
|
|
89
|
+
const defaults = {
|
|
90
|
+
// GitHub PR configuration (user-specific)
|
|
91
|
+
github: {
|
|
91
92
|
pr: {
|
|
92
|
-
defaultBase: 'develop', //
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
reviewers: [], // Example: ["juan.perez", "maria.garcia"]
|
|
96
|
-
|
|
97
|
-
// Labels by preset
|
|
98
|
-
labelRules: {
|
|
93
|
+
defaultBase: 'develop', // Project default branch
|
|
94
|
+
reviewers: [], // Project reviewers
|
|
95
|
+
labelRules: { // Labels by preset
|
|
99
96
|
backend: ['backend', 'java'],
|
|
100
97
|
frontend: ['frontend', 'react'],
|
|
101
98
|
fullstack: ['fullstack'],
|
|
@@ -105,13 +102,21 @@ const defaults = {
|
|
|
105
102
|
},
|
|
106
103
|
},
|
|
107
104
|
},
|
|
105
|
+
|
|
106
|
+
// Subagent configuration (preset can override)
|
|
107
|
+
subagents: {
|
|
108
|
+
batchSize: 3, // Files per parallel batch
|
|
109
|
+
},
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
|
-
* Loads user configuration from .claude/config.json
|
|
112
|
-
*
|
|
113
|
+
* Loads user configuration from .claude/config.json (v2.8.0)
|
|
114
|
+
*
|
|
115
|
+
* Format detection:
|
|
116
|
+
* - v2.8.0: { version: "2.8.0", preset: "...", overrides: {...} }
|
|
117
|
+
* - Legacy: { preset: "...", analysis: {...}, ... } (auto-migrates with warning)
|
|
113
118
|
*
|
|
114
|
-
*
|
|
119
|
+
* Merge priority: HARDCODED < defaults < preset config < user overrides
|
|
115
120
|
*
|
|
116
121
|
* @param {string} baseDir - Base directory to search for config (default: cwd)
|
|
117
122
|
* @returns {Promise<Object>} Merged configuration
|
|
@@ -119,31 +124,119 @@ const defaults = {
|
|
|
119
124
|
const loadUserConfig = async (baseDir = process.cwd()) => {
|
|
120
125
|
const configPath = path.join(baseDir, '.claude', 'config.json');
|
|
121
126
|
|
|
122
|
-
let
|
|
127
|
+
let rawConfig = {};
|
|
128
|
+
let configFormat = 'default';
|
|
129
|
+
|
|
123
130
|
try {
|
|
124
131
|
if (fs.existsSync(configPath)) {
|
|
125
|
-
|
|
132
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
133
|
+
|
|
134
|
+
// Detect format
|
|
135
|
+
if (rawConfig.version === '2.8.0') {
|
|
136
|
+
configFormat = 'v3';
|
|
137
|
+
} else if (rawConfig.version) {
|
|
138
|
+
throw new Error(`Unsupported config version: ${rawConfig.version}. Please run 'claude-hooks migrate-config'`);
|
|
139
|
+
} else if (Object.keys(rawConfig).length > 0 && rawConfig.preset) {
|
|
140
|
+
configFormat = 'legacy';
|
|
141
|
+
logger.warning('⚠️ Legacy config format detected. Run "claude-hooks migrate-config" to update to v2.8.0');
|
|
142
|
+
}
|
|
126
143
|
}
|
|
127
144
|
} catch (error) {
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
if (error.message.includes('Unsupported config version')) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
logger.warning(`⚠️ Warning: Could not load .claude/config.json: ${error.message}`);
|
|
149
|
+
logger.warning('Using default configuration');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Extract user overrides based on format
|
|
153
|
+
let userOverrides = {};
|
|
154
|
+
let presetName = null;
|
|
155
|
+
|
|
156
|
+
if (configFormat === 'v3') {
|
|
157
|
+
presetName = rawConfig.preset;
|
|
158
|
+
userOverrides = rawConfig.overrides || {};
|
|
159
|
+
} else if (configFormat === 'legacy') {
|
|
160
|
+
presetName = rawConfig.preset;
|
|
161
|
+
// Legacy format: extract only allowed parameters
|
|
162
|
+
userOverrides = extractAllowedParams(rawConfig);
|
|
130
163
|
}
|
|
131
164
|
|
|
132
165
|
// Load preset if specified
|
|
133
166
|
let presetConfig = {};
|
|
134
|
-
if (
|
|
167
|
+
if (presetName) {
|
|
135
168
|
try {
|
|
136
|
-
// Dynamic import to avoid circular dependency
|
|
137
169
|
const { loadPreset } = await import('./utils/preset-loader.js');
|
|
138
|
-
const { config } = await loadPreset(
|
|
170
|
+
const { config } = await loadPreset(presetName);
|
|
139
171
|
presetConfig = config;
|
|
140
172
|
} catch (error) {
|
|
141
|
-
|
|
173
|
+
logger.warning(`⚠️ Warning: Preset "${presetName}" not found, using defaults`);
|
|
142
174
|
}
|
|
143
175
|
}
|
|
144
176
|
|
|
145
|
-
// Merge: defaults <
|
|
146
|
-
|
|
177
|
+
// Merge priority: HARDCODED < defaults < preset < user overrides
|
|
178
|
+
const baseConfig = deepMerge(HARDCODED, defaults);
|
|
179
|
+
const withPreset = deepMerge(baseConfig, presetConfig);
|
|
180
|
+
const final = deepMerge(withPreset, userOverrides);
|
|
181
|
+
|
|
182
|
+
// Add preset name to final config (so hooks can display it)
|
|
183
|
+
if (presetName) {
|
|
184
|
+
final.preset = presetName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return final;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extracts only allowed parameters from legacy config
|
|
192
|
+
* v2.8.0 only allows: github.pr.*, subagents.batchSize
|
|
193
|
+
* Advanced: analysis.ignoreExtensions, commitMessage.taskIdPattern, subagents.model
|
|
194
|
+
*
|
|
195
|
+
* @param {Object} legacyConfig - Legacy format config
|
|
196
|
+
* @returns {Object} Allowed parameters only
|
|
197
|
+
*/
|
|
198
|
+
const extractAllowedParams = (legacyConfig) => {
|
|
199
|
+
const allowed = {};
|
|
200
|
+
|
|
201
|
+
// GitHub PR config (fully allowed)
|
|
202
|
+
if (legacyConfig.github?.pr) {
|
|
203
|
+
allowed.github = { pr: {} };
|
|
204
|
+
if (legacyConfig.github.pr.defaultBase !== undefined) {
|
|
205
|
+
allowed.github.pr.defaultBase = legacyConfig.github.pr.defaultBase;
|
|
206
|
+
}
|
|
207
|
+
if (legacyConfig.github.pr.reviewers !== undefined) {
|
|
208
|
+
allowed.github.pr.reviewers = legacyConfig.github.pr.reviewers;
|
|
209
|
+
}
|
|
210
|
+
if (legacyConfig.github.pr.labelRules !== undefined) {
|
|
211
|
+
allowed.github.pr.labelRules = legacyConfig.github.pr.labelRules;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Subagent batchSize (allowed)
|
|
216
|
+
if (legacyConfig.subagents?.batchSize !== undefined) {
|
|
217
|
+
allowed.subagents = { batchSize: legacyConfig.subagents.batchSize };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Advanced params (allowed but warn)
|
|
221
|
+
if (legacyConfig.analysis?.ignoreExtensions !== undefined) {
|
|
222
|
+
if (!allowed.analysis) allowed.analysis = {};
|
|
223
|
+
allowed.analysis.ignoreExtensions = legacyConfig.analysis.ignoreExtensions;
|
|
224
|
+
logger.warning('ℹ️ Using advanced parameter: analysis.ignoreExtensions');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (legacyConfig.commitMessage?.taskIdPattern !== undefined) {
|
|
228
|
+
if (!allowed.commitMessage) allowed.commitMessage = {};
|
|
229
|
+
allowed.commitMessage.taskIdPattern = legacyConfig.commitMessage.taskIdPattern;
|
|
230
|
+
logger.warning('ℹ️ Using advanced parameter: commitMessage.taskIdPattern');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (legacyConfig.subagents?.model !== undefined) {
|
|
234
|
+
if (!allowed.subagents) allowed.subagents = {};
|
|
235
|
+
allowed.subagents.model = legacyConfig.subagents.model;
|
|
236
|
+
logger.warning('ℹ️ Using advanced parameter: subagents.model');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return allowed;
|
|
147
240
|
};
|
|
148
241
|
|
|
149
242
|
/**
|
|
@@ -185,9 +278,9 @@ const getConfig = async () => {
|
|
|
185
278
|
return configInstance;
|
|
186
279
|
};
|
|
187
280
|
|
|
188
|
-
// Export async loader
|
|
189
|
-
export { getConfig, loadUserConfig, defaults };
|
|
281
|
+
// Export async loader and constants
|
|
282
|
+
export { getConfig, loadUserConfig, defaults, HARDCODED };
|
|
190
283
|
|
|
191
284
|
// Export a sync default for backwards compatibility (loads without preset)
|
|
192
|
-
// NOTE:
|
|
193
|
-
export default defaults;
|
|
285
|
+
// NOTE: Returns HARDCODED merged with defaults
|
|
286
|
+
export default deepMerge(HARDCODED, defaults);
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 2. Build analysis prompt with file diffs
|
|
10
10
|
* 3. Send to Claude CLI for analysis
|
|
11
11
|
* 4. Parse JSON response
|
|
12
|
-
* 5. Display
|
|
12
|
+
* 5. Display structured analysis results
|
|
13
13
|
* 6. Generate resolution prompt if issues found
|
|
14
14
|
* 7. Block commit if quality gate fails
|
|
15
15
|
*
|
|
@@ -52,8 +52,8 @@ import { getConfig } from '../config.js';
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Displays
|
|
56
|
-
* Why: Provides
|
|
55
|
+
* Displays structured analysis results
|
|
56
|
+
* Why: Provides clear, visual feedback about code quality
|
|
57
57
|
*
|
|
58
58
|
* @param {Object} result - Analysis result from Claude
|
|
59
59
|
*/
|
|
@@ -73,19 +73,16 @@ const displayResults = (result) => {
|
|
|
73
73
|
}
|
|
74
74
|
console.log();
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
if (result.
|
|
78
|
-
|
|
79
|
-
console.log(
|
|
80
|
-
|
|
81
|
-
console.log(
|
|
82
|
-
console.log(`├─ Coverage: ${result.metrics.coverage || '?'}%`);
|
|
83
|
-
console.log(`├─ Duplications: ${result.metrics.duplications || '?'}%`);
|
|
84
|
-
console.log(`└─ Complexity: ${result.metrics.complexity || '?'}`);
|
|
85
|
-
console.log();
|
|
76
|
+
// Issues Summary - Simple count
|
|
77
|
+
if (Array.isArray(result.details) && result.details.length > 0) {
|
|
78
|
+
const fileCount = new Set(result.details.map(i => i.file)).size;
|
|
79
|
+
console.log(`📊 ${result.details.length} issue(s) found across ${fileCount} file(s)`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log('✅ No issues found!');
|
|
86
82
|
}
|
|
83
|
+
console.log();
|
|
87
84
|
|
|
88
|
-
// Issues
|
|
85
|
+
// Issues Breakdown (severity counts)
|
|
89
86
|
if (result.issues && typeof result.issues === 'object') {
|
|
90
87
|
console.log('📋 ISSUES SUMMARY');
|
|
91
88
|
|
|
@@ -187,9 +187,8 @@ const main = async () => {
|
|
|
187
187
|
config: config // Pass config for custom pattern
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
} else {
|
|
190
|
+
// Note: getOrPromptTaskId() already logs the task ID if found
|
|
191
|
+
if (!taskId) {
|
|
193
192
|
logger.debug('prepare-commit-msg - main', 'No task ID found in branch, continuing without it');
|
|
194
193
|
}
|
|
195
194
|
|