claude-git-hooks 2.0.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 +212 -0
- package/README.md +217 -92
- package/bin/claude-hooks +311 -149
- package/lib/config.js +163 -0
- package/lib/hooks/pre-commit.js +180 -68
- package/lib/hooks/prepare-commit-msg.js +47 -41
- package/lib/utils/claude-client.js +93 -11
- package/lib/utils/file-operations.js +23 -74
- 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/bin/claude-hooks
CHANGED
|
@@ -9,6 +9,9 @@ import https from 'https';
|
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname } from 'path';
|
|
11
11
|
import { executeClaude, extractJSON } from '../lib/utils/claude-client.js';
|
|
12
|
+
import { loadPrompt } from '../lib/utils/prompt-builder.js';
|
|
13
|
+
import { listPresets } from '../lib/utils/preset-loader.js';
|
|
14
|
+
import { getConfig } from '../lib/config.js';
|
|
12
15
|
|
|
13
16
|
// Why: ES6 modules don't have __dirname, need to recreate it
|
|
14
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -126,31 +129,6 @@ function warning(message) {
|
|
|
126
129
|
log(`⚠️ ${message}`, 'yellow');
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
// Function to read password securely
|
|
130
|
-
function readPassword(prompt) {
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
const rl = readline.createInterface({
|
|
133
|
-
input: process.stdin,
|
|
134
|
-
output: process.stdout
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Disable echo
|
|
138
|
-
rl.stdoutMuted = true;
|
|
139
|
-
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
140
|
-
if (rl.stdoutMuted)
|
|
141
|
-
rl.output.write("*");
|
|
142
|
-
else
|
|
143
|
-
rl.output.write(stringToWrite);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
rl.question(prompt, (password) => {
|
|
147
|
-
rl.close();
|
|
148
|
-
console.log(); // New line
|
|
149
|
-
resolve(password);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
132
|
// Entertainment system
|
|
155
133
|
class Entertainment {
|
|
156
134
|
static jokes = [
|
|
@@ -332,6 +310,11 @@ async function install(args) {
|
|
|
332
310
|
const isForce = args.includes('--force');
|
|
333
311
|
const skipAuth = args.includes('--skip-auth');
|
|
334
312
|
|
|
313
|
+
// Check for updates (unless --skip-auth flag)
|
|
314
|
+
if (!skipAuth && !isForce) {
|
|
315
|
+
await checkVersionAndPromptUpdate();
|
|
316
|
+
}
|
|
317
|
+
|
|
335
318
|
if (isForce) {
|
|
336
319
|
info('Installing Claude Git Hooks (force mode)...');
|
|
337
320
|
} else {
|
|
@@ -396,28 +379,79 @@ async function install(args) {
|
|
|
396
379
|
success('.claude directory created');
|
|
397
380
|
}
|
|
398
381
|
|
|
399
|
-
// Copy
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
382
|
+
// Copy ALL template files (.md and .json) to .claude directory
|
|
383
|
+
const templateFiles = fs.readdirSync(templatesPath)
|
|
384
|
+
.filter(file => {
|
|
385
|
+
const filePath = path.join(templatesPath, file);
|
|
386
|
+
return fs.statSync(filePath).isFile() && (file.endsWith('.md') || file.endsWith('.json'));
|
|
387
|
+
});
|
|
405
388
|
|
|
406
|
-
|
|
407
|
-
const destPath = path.join(claudeDir, file);
|
|
389
|
+
templateFiles.forEach(file => {
|
|
408
390
|
const sourcePath = path.join(templatesPath, file);
|
|
391
|
+
const destPath = path.join(claudeDir, file);
|
|
409
392
|
|
|
410
393
|
// In force mode or if it doesn't exist, copy the file
|
|
411
394
|
if (isForce || !fs.existsSync(destPath)) {
|
|
412
395
|
if (fs.existsSync(sourcePath)) {
|
|
413
396
|
fs.copyFileSync(sourcePath, destPath);
|
|
414
397
|
success(`${file} installed in .claude/`);
|
|
415
|
-
} else {
|
|
416
|
-
warning(`Template file not found: ${file}`);
|
|
417
398
|
}
|
|
399
|
+
} else {
|
|
400
|
+
info(`${file} already exists (skipped)`);
|
|
418
401
|
}
|
|
419
402
|
});
|
|
420
403
|
|
|
404
|
+
// Copy presets directory structure
|
|
405
|
+
const presetsSourcePath = path.join(templatesPath, 'presets');
|
|
406
|
+
const presetsDestPath = path.join(claudeDir, 'presets');
|
|
407
|
+
|
|
408
|
+
if (fs.existsSync(presetsSourcePath)) {
|
|
409
|
+
// Create presets directory in .claude
|
|
410
|
+
if (!fs.existsSync(presetsDestPath)) {
|
|
411
|
+
fs.mkdirSync(presetsDestPath, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Copy each preset directory
|
|
415
|
+
const presetDirs = fs.readdirSync(presetsSourcePath)
|
|
416
|
+
.filter(item => fs.statSync(path.join(presetsSourcePath, item)).isDirectory());
|
|
417
|
+
|
|
418
|
+
presetDirs.forEach(presetName => {
|
|
419
|
+
const presetSource = path.join(presetsSourcePath, presetName);
|
|
420
|
+
const presetDest = path.join(presetsDestPath, presetName);
|
|
421
|
+
|
|
422
|
+
// Create preset directory
|
|
423
|
+
if (!fs.existsSync(presetDest)) {
|
|
424
|
+
fs.mkdirSync(presetDest, { recursive: true });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Copy all files in preset directory
|
|
428
|
+
const presetFiles = fs.readdirSync(presetSource);
|
|
429
|
+
presetFiles.forEach(file => {
|
|
430
|
+
const sourceFile = path.join(presetSource, file);
|
|
431
|
+
const destFile = path.join(presetDest, file);
|
|
432
|
+
|
|
433
|
+
if (fs.statSync(sourceFile).isFile()) {
|
|
434
|
+
if (isForce || !fs.existsSync(destFile)) {
|
|
435
|
+
fs.copyFileSync(sourceFile, destFile);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
success(`${presetDirs.length} presets installed in .claude/presets/`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Special handling for config.json: rename config.example.json → config.json
|
|
445
|
+
const configExamplePath = path.join(claudeDir, 'config.example.json');
|
|
446
|
+
const configPath = path.join(claudeDir, 'config.json');
|
|
447
|
+
|
|
448
|
+
if (fs.existsSync(configExamplePath) && !fs.existsSync(configPath)) {
|
|
449
|
+
fs.copyFileSync(configExamplePath, configPath);
|
|
450
|
+
success('config.json created from example (customize as needed)');
|
|
451
|
+
} else if (!fs.existsSync(configPath)) {
|
|
452
|
+
warning('config.json not found - using defaults');
|
|
453
|
+
}
|
|
454
|
+
|
|
421
455
|
// Configure Git
|
|
422
456
|
configureGit();
|
|
423
457
|
|
|
@@ -429,17 +463,16 @@ async function install(args) {
|
|
|
429
463
|
console.log(' git commit -m "auto" # Generate message automatically');
|
|
430
464
|
console.log(' git commit -m "message" # Analyze code before commit');
|
|
431
465
|
console.log(' git commit --no-verify # Skip analysis completely');
|
|
432
|
-
console.log('\
|
|
433
|
-
console.log('
|
|
434
|
-
console.log('
|
|
435
|
-
console.log('
|
|
436
|
-
console.log('
|
|
437
|
-
console.log('\
|
|
438
|
-
console.log('
|
|
439
|
-
console.log('
|
|
440
|
-
console.log('
|
|
441
|
-
console.log('
|
|
442
|
-
console.log(' # Example: 4 files, BATCH_SIZE=3 → 2 batches (3 parallel + 1)');
|
|
466
|
+
console.log('\n💡 Configuration:');
|
|
467
|
+
console.log(' 📁 All templates installed in .claude/');
|
|
468
|
+
console.log(' 📝 Edit .claude/config.json to customize settings');
|
|
469
|
+
console.log(' 🎯 Use presets: backend, frontend, fullstack, database, ai, default');
|
|
470
|
+
console.log(' 🚀 Enable parallel analysis: set subagents.enabled = true');
|
|
471
|
+
console.log('\n📖 Example config.json:');
|
|
472
|
+
console.log(' {');
|
|
473
|
+
console.log(' "preset": "backend",');
|
|
474
|
+
console.log(' "subagents": { "enabled": true, "model": "haiku", "batchSize": 3 }');
|
|
475
|
+
console.log(' }');
|
|
443
476
|
console.log('\nFor more options: claude-hooks --help');
|
|
444
477
|
}
|
|
445
478
|
|
|
@@ -597,9 +630,6 @@ function updateGitignore() {
|
|
|
597
630
|
const claudeEntries = [
|
|
598
631
|
'# Claude Git Hooks',
|
|
599
632
|
'.claude/',
|
|
600
|
-
'debug-claude-response.json',
|
|
601
|
-
'claude_resolution_prompt.md',
|
|
602
|
-
'.claude-pr-analysis.json',
|
|
603
633
|
];
|
|
604
634
|
|
|
605
635
|
let gitignoreContent = '';
|
|
@@ -760,6 +790,9 @@ async function analyzeDiff(args) {
|
|
|
760
790
|
return;
|
|
761
791
|
}
|
|
762
792
|
|
|
793
|
+
// Load configuration
|
|
794
|
+
const config = await getConfig();
|
|
795
|
+
|
|
763
796
|
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
764
797
|
|
|
765
798
|
if (!currentBranch) {
|
|
@@ -863,9 +896,9 @@ async function analyzeDiff(args) {
|
|
|
863
896
|
}
|
|
864
897
|
|
|
865
898
|
// Check if subagents should be used
|
|
866
|
-
const useSubagents =
|
|
867
|
-
const subagentModel =
|
|
868
|
-
let subagentBatchSize =
|
|
899
|
+
const useSubagents = config.subagents.enabled;
|
|
900
|
+
const subagentModel = config.subagents.model;
|
|
901
|
+
let subagentBatchSize = config.subagents.batchSize;
|
|
869
902
|
// Validate batch size (must be >= 1)
|
|
870
903
|
if (subagentBatchSize < 1) {
|
|
871
904
|
subagentBatchSize = 1;
|
|
@@ -874,37 +907,19 @@ async function analyzeDiff(args) {
|
|
|
874
907
|
? `\n\nIMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${subagentBatchSize}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one file and provides insights. After ALL batches complete, consolidate into SINGLE JSON with ONE cohesive PR title/description. Model: ${subagentModel}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4).\n`
|
|
875
908
|
: '';
|
|
876
909
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
Respond EXCLUSIVELY with a valid JSON with this structure:
|
|
891
|
-
{
|
|
892
|
-
"prTitle": "Interesting PR title",
|
|
893
|
-
"prDescription": "detailed PR description with markdown",
|
|
894
|
-
"suggestedBranchName": "type/suggested-branch-name",
|
|
895
|
-
"changeType": "feature|fix|refactor|docs|test|chore",
|
|
896
|
-
"breakingChanges": false,
|
|
897
|
-
"testingNotes": "notes on necessary testing or 'None'"
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
=== COMMITS ===
|
|
901
|
-
${commits}
|
|
902
|
-
|
|
903
|
-
=== CHANGED FILES ===
|
|
904
|
-
${diffFiles}
|
|
905
|
-
|
|
906
|
-
=== FULL DIFF ===
|
|
907
|
-
${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated diff)' : ''}`;
|
|
910
|
+
// Truncate full diff if too large
|
|
911
|
+
const truncatedDiff = fullDiff.length > 50000
|
|
912
|
+
? fullDiff.substring(0, 50000) + '\n... (truncated diff)'
|
|
913
|
+
: fullDiff;
|
|
914
|
+
|
|
915
|
+
// Load prompt from template
|
|
916
|
+
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
917
|
+
CONTEXT_DESCRIPTION: contextDescription,
|
|
918
|
+
SUBAGENT_INSTRUCTION: subagentInstruction,
|
|
919
|
+
COMMITS: commits,
|
|
920
|
+
DIFF_FILES: diffFiles,
|
|
921
|
+
FULL_DIFF: truncatedDiff
|
|
922
|
+
});
|
|
908
923
|
|
|
909
924
|
info('Sending to Claude for analysis...');
|
|
910
925
|
const startTime = Date.now();
|
|
@@ -965,7 +980,13 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
965
980
|
}
|
|
966
981
|
};
|
|
967
982
|
|
|
968
|
-
|
|
983
|
+
// Ensure .claude/out directory exists
|
|
984
|
+
const outputDir = '.claude/out';
|
|
985
|
+
if (!fs.existsSync(outputDir)) {
|
|
986
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const outputFile = '.claude/out/pr-analysis.json';
|
|
969
990
|
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
970
991
|
|
|
971
992
|
const elapsed = Date.now() - startTime;
|
|
@@ -1036,20 +1057,13 @@ function status() {
|
|
|
1036
1057
|
const gitignorePath = '.gitignore';
|
|
1037
1058
|
if (fs.existsSync(gitignorePath)) {
|
|
1038
1059
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
1039
|
-
const
|
|
1040
|
-
let allPresent = true;
|
|
1060
|
+
const claudeIgnore = '.claude/';
|
|
1041
1061
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
warning(`${entry}: missing`);
|
|
1048
|
-
allPresent = false;
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
if (!allPresent) {
|
|
1062
|
+
const regex = new RegExp(`^${claudeIgnore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
1063
|
+
if (regex.test(gitignoreContent)) {
|
|
1064
|
+
success(`${claudeIgnore}: included (protects all Claude files)`);
|
|
1065
|
+
} else {
|
|
1066
|
+
warning(`${claudeIgnore}: missing`);
|
|
1053
1067
|
info('\nRun "claude-hooks install" to update .gitignore');
|
|
1054
1068
|
}
|
|
1055
1069
|
} else {
|
|
@@ -1057,43 +1071,24 @@ function status() {
|
|
|
1057
1071
|
}
|
|
1058
1072
|
}
|
|
1059
1073
|
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
error('You are not in a Git repository.');
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// Función para comparar versiones usando el script compartido
|
|
1074
|
+
// Cross-platform version comparison (semver)
|
|
1075
|
+
// Why: Pure JavaScript, no bash dependency
|
|
1076
|
+
// Returns: 0 if equal, 1 if v1 > v2, -1 if v1 < v2
|
|
1068
1077
|
function compareVersions(v1, v2) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
if (v1 === v2) return 0;
|
|
1081
|
-
const sorted = [v1, v2].sort((a, b) => {
|
|
1082
|
-
const aParts = a.split('.').map(Number);
|
|
1083
|
-
const bParts = b.split('.').map(Number);
|
|
1084
|
-
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
1085
|
-
const aPart = aParts[i] || 0;
|
|
1086
|
-
const bPart = bParts[i] || 0;
|
|
1087
|
-
if (aPart !== bPart) return aPart - bPart;
|
|
1088
|
-
}
|
|
1089
|
-
return 0;
|
|
1090
|
-
});
|
|
1091
|
-
return v1 === sorted[1] ? 1 : -1;
|
|
1092
|
-
} catch (e) {
|
|
1093
|
-
// Si falla todo, usar comparación simple
|
|
1094
|
-
if (v1 === v2) return 0;
|
|
1095
|
-
return v1 > v2 ? 1 : -1;
|
|
1078
|
+
if (v1 === v2) return 0;
|
|
1079
|
+
|
|
1080
|
+
const v1Parts = v1.split('.').map(Number);
|
|
1081
|
+
const v2Parts = v2.split('.').map(Number);
|
|
1082
|
+
|
|
1083
|
+
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
1084
|
+
const v1Part = v1Parts[i] || 0;
|
|
1085
|
+
const v2Part = v2Parts[i] || 0;
|
|
1086
|
+
|
|
1087
|
+
if (v1Part > v2Part) return 1;
|
|
1088
|
+
if (v1Part < v2Part) return -1;
|
|
1096
1089
|
}
|
|
1090
|
+
|
|
1091
|
+
return 0;
|
|
1097
1092
|
}
|
|
1098
1093
|
|
|
1099
1094
|
// Update command - update to the latest version
|
|
@@ -1145,6 +1140,13 @@ async function update() {
|
|
|
1145
1140
|
}
|
|
1146
1141
|
}
|
|
1147
1142
|
|
|
1143
|
+
// Show version command
|
|
1144
|
+
// Why: Reusable function to display current version from package.json
|
|
1145
|
+
function showVersion() {
|
|
1146
|
+
const pkg = getPackageJson();
|
|
1147
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1148
1150
|
// Comando help
|
|
1149
1151
|
function showHelp() {
|
|
1150
1152
|
console.log(`
|
|
@@ -1162,6 +1164,10 @@ Commands:
|
|
|
1162
1164
|
disable [hook] Disable hooks (all or one specific)
|
|
1163
1165
|
status Show the status of hooks
|
|
1164
1166
|
analyze-diff [base] Analyze differences between branches and generate PR info
|
|
1167
|
+
presets List all available presets
|
|
1168
|
+
--set-preset <name> Set the active preset
|
|
1169
|
+
preset current Show the current active preset
|
|
1170
|
+
--version, -v Show the current version
|
|
1165
1171
|
help Show this help
|
|
1166
1172
|
|
|
1167
1173
|
Available hooks:
|
|
@@ -1176,6 +1182,9 @@ Examples:
|
|
|
1176
1182
|
claude-hooks enable # Enable all hooks
|
|
1177
1183
|
claude-hooks status # View current status
|
|
1178
1184
|
claude-hooks analyze-diff main # Analyze differences with main
|
|
1185
|
+
claude-hooks presets # List available presets
|
|
1186
|
+
claude-hooks --set-preset backend # Set backend preset
|
|
1187
|
+
claude-hooks preset current # Show current preset
|
|
1179
1188
|
|
|
1180
1189
|
Commit use cases:
|
|
1181
1190
|
git commit -m "message" # Manual message + blocking analysis
|
|
@@ -1186,31 +1195,165 @@ Commit use cases:
|
|
|
1186
1195
|
Analyze-diff use case:
|
|
1187
1196
|
claude-hooks analyze-diff main # Analyze changes vs main and generate:
|
|
1188
1197
|
→ PR Title: "feat: add user authentication module"
|
|
1189
|
-
→ PR Description: "## Summary\n- Added JWT authentication..."
|
|
1198
|
+
→ PR Description: "## Summary\n- Added JWT authentication..."
|
|
1190
1199
|
→ Suggested branch: "feature/user-authentication"
|
|
1191
1200
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1201
|
+
Presets (v2.3.0+):
|
|
1202
|
+
Built-in tech-stack specific configurations:
|
|
1203
|
+
- backend: Spring Boot + SQL Server (.java, .xml, .yml)
|
|
1204
|
+
- frontend: React + Material-UI (.js, .jsx, .ts, .tsx, .css)
|
|
1205
|
+
- fullstack: Backend + Frontend with API consistency checks
|
|
1206
|
+
- database: SQL Server migrations and procedures (.sql)
|
|
1207
|
+
- ai: Node.js + Claude CLI integration (.js, .json, .md)
|
|
1208
|
+
- default: General-purpose mixed languages
|
|
1209
|
+
|
|
1210
|
+
Configuration (v2.2.0+):
|
|
1211
|
+
Create .claude/config.json in your project to customize:
|
|
1212
|
+
- Preset selection
|
|
1213
|
+
- Analysis settings (maxFileSize, maxFiles, timeout)
|
|
1214
|
+
- Commit message generation (autoKeyword, timeout)
|
|
1215
|
+
- Parallel execution (enabled, model, batchSize)
|
|
1216
|
+
- Template paths and output files
|
|
1217
|
+
- Debug mode
|
|
1218
|
+
|
|
1219
|
+
Example: .claude/config.json
|
|
1220
|
+
{
|
|
1221
|
+
"preset": "backend",
|
|
1222
|
+
"analysis": { "maxFiles": 30, "timeout": 180000 },
|
|
1223
|
+
"subagents": {
|
|
1224
|
+
"enabled": true, # Enable parallel execution
|
|
1225
|
+
"model": "haiku", # haiku (fast) | sonnet | opus
|
|
1226
|
+
"batchSize": 2 # Files per batch (1=fastest)
|
|
1227
|
+
},
|
|
1228
|
+
"system": { "debug": true }
|
|
1229
|
+
}
|
|
1207
1230
|
|
|
1208
|
-
|
|
1231
|
+
Parallel Analysis (v2.2.0+):
|
|
1232
|
+
When analyzing 3+ files, parallel execution runs multiple Claude CLI
|
|
1233
|
+
processes simultaneously for faster analysis:
|
|
1234
|
+
- batchSize: 1 → Maximum speed (1 file per process)
|
|
1235
|
+
- batchSize: 2 → Balanced (2 files per process)
|
|
1236
|
+
- batchSize: 4+ → Fewer API calls but slower
|
|
1237
|
+
- Speed improvement: up to 4x faster with batchSize: 1
|
|
1238
|
+
|
|
1239
|
+
Customization:
|
|
1240
|
+
Override prompts by copying to .claude/:
|
|
1241
|
+
cp templates/COMMIT_MESSAGE.md .claude/
|
|
1242
|
+
cp templates/ANALYZE_DIFF.md .claude/
|
|
1243
|
+
cp templates/CLAUDE_PRE_COMMIT_SONAR.md .claude/
|
|
1244
|
+
# Edit as needed - system uses .claude/ version if exists
|
|
1209
1245
|
|
|
1210
1246
|
More information: https://github.com/pablorovito/claude-git-hooks
|
|
1211
1247
|
`);
|
|
1212
1248
|
}
|
|
1213
1249
|
|
|
1250
|
+
// Preset Management Functions
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Shows all available presets
|
|
1254
|
+
*/
|
|
1255
|
+
async function showPresets() {
|
|
1256
|
+
try {
|
|
1257
|
+
const presets = await listPresets();
|
|
1258
|
+
|
|
1259
|
+
if (presets.length === 0) {
|
|
1260
|
+
warning('No presets found');
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
console.log('');
|
|
1265
|
+
info('Available presets:');
|
|
1266
|
+
console.log('');
|
|
1267
|
+
|
|
1268
|
+
presets.forEach(preset => {
|
|
1269
|
+
console.log(` ${colors.green}${preset.name}${colors.reset}`);
|
|
1270
|
+
console.log(` ${preset.displayName}`);
|
|
1271
|
+
console.log(` ${colors.blue}${preset.description}${colors.reset}`);
|
|
1272
|
+
console.log('');
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
info('To set a preset: claude-hooks --set-preset <name>');
|
|
1276
|
+
info('To see current preset: claude-hooks preset current');
|
|
1277
|
+
console.log('');
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
error(`Failed to list presets: ${err.message}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Sets the active preset
|
|
1285
|
+
*/
|
|
1286
|
+
async function setPreset(presetName) {
|
|
1287
|
+
if (!presetName) {
|
|
1288
|
+
error('Please specify a preset name: claude-hooks --set-preset <name>');
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
// Verify preset exists
|
|
1294
|
+
const presets = await listPresets();
|
|
1295
|
+
const preset = presets.find(p => p.name === presetName);
|
|
1296
|
+
|
|
1297
|
+
if (!preset) {
|
|
1298
|
+
error(`Preset "${presetName}" not found`);
|
|
1299
|
+
info('Available presets:');
|
|
1300
|
+
presets.forEach(p => console.log(` - ${p.name}`));
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Update .claude/config.json
|
|
1305
|
+
const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
1306
|
+
const configDir = path.join(repoRoot, '.claude');
|
|
1307
|
+
const configPath = path.join(configDir, 'config.json');
|
|
1308
|
+
|
|
1309
|
+
// Ensure .claude directory exists
|
|
1310
|
+
if (!fs.existsSync(configDir)) {
|
|
1311
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Load existing config or create new
|
|
1315
|
+
let config = {};
|
|
1316
|
+
if (fs.existsSync(configPath)) {
|
|
1317
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Set preset
|
|
1321
|
+
config.preset = presetName;
|
|
1322
|
+
|
|
1323
|
+
// Save config
|
|
1324
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1325
|
+
|
|
1326
|
+
success(`Preset '${preset.displayName}' activated`);
|
|
1327
|
+
info(`Configuration saved to ${configPath}`);
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
error(`Failed to set preset: ${err.message}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Shows the current active preset
|
|
1335
|
+
*/
|
|
1336
|
+
async function currentPreset() {
|
|
1337
|
+
try {
|
|
1338
|
+
const config = await getConfig();
|
|
1339
|
+
const presetName = config.preset || 'default';
|
|
1340
|
+
|
|
1341
|
+
const presets = await listPresets();
|
|
1342
|
+
const preset = presets.find(p => p.name === presetName);
|
|
1343
|
+
|
|
1344
|
+
if (preset) {
|
|
1345
|
+
console.log('');
|
|
1346
|
+
success(`Current preset: ${preset.displayName} (${preset.name})`);
|
|
1347
|
+
console.log(` ${colors.blue}${preset.description}${colors.reset}`);
|
|
1348
|
+
console.log('');
|
|
1349
|
+
} else {
|
|
1350
|
+
warning(`Current preset "${presetName}" not found`);
|
|
1351
|
+
}
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
error(`Failed to get current preset: ${err.message}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1214
1357
|
// Main
|
|
1215
1358
|
async function main() {
|
|
1216
1359
|
const args = process.argv.slice(2);
|
|
@@ -1238,6 +1381,25 @@ async function main() {
|
|
|
1238
1381
|
case 'analyze-diff':
|
|
1239
1382
|
await analyzeDiff(args.slice(1));
|
|
1240
1383
|
break;
|
|
1384
|
+
case 'presets':
|
|
1385
|
+
await showPresets();
|
|
1386
|
+
break;
|
|
1387
|
+
case '--set-preset':
|
|
1388
|
+
await setPreset(args[1]);
|
|
1389
|
+
break;
|
|
1390
|
+
case 'preset':
|
|
1391
|
+
// Handle subcommands: preset current
|
|
1392
|
+
if (args[1] === 'current') {
|
|
1393
|
+
await currentPreset();
|
|
1394
|
+
} else {
|
|
1395
|
+
error(`Unknown preset subcommand: ${args[1]}`);
|
|
1396
|
+
}
|
|
1397
|
+
break;
|
|
1398
|
+
case '--version':
|
|
1399
|
+
case '-v':
|
|
1400
|
+
case 'version':
|
|
1401
|
+
showVersion();
|
|
1402
|
+
break;
|
|
1241
1403
|
case 'help':
|
|
1242
1404
|
case '--help':
|
|
1243
1405
|
case '-h':
|