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.
Files changed (47) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/README.md +217 -92
  3. package/bin/claude-hooks +311 -149
  4. package/lib/config.js +163 -0
  5. package/lib/hooks/pre-commit.js +180 -68
  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 +23 -74
  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
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 guidelines and prompts files to .claude
400
- const claudeFiles = [
401
- 'CLAUDE_PRE_COMMIT_SONAR.md',
402
- 'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
403
- 'CLAUDE_RESOLUTION_PROMPT.md'
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
- claudeFiles.forEach(file => {
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('\nExclude code from analysis:');
433
- console.log(' // SKIP-ANALYSIS # Exclude the next line');
434
- console.log(' // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one');
435
- console.log(' ...excluded code...');
436
- console.log(' // SKIP_ANALYSIS_BLOCK');
437
- console.log('\nNEW: Parallel analysis for faster multi-file commits:');
438
- console.log(' export CLAUDE_USE_SUBAGENTS=true # Enable subagents');
439
- console.log(' export CLAUDE_SUBAGENT_MODEL=haiku # haiku/sonnet/opus');
440
- console.log(' export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel per batch (default: 3)');
441
- console.log(' # Example: 4 files, BATCH_SIZE=1 → 4 sequential batches');
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 = process.env.CLAUDE_USE_SUBAGENTS === 'true';
867
- const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
868
- let subagentBatchSize = parseInt(process.env.CLAUDE_SUBAGENT_BATCH_SIZE || '3');
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
- const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
878
- ${subagentInstruction}
879
- Please generate:
880
- 1. A concise and descriptive PR title (maximum 72 characters)
881
- 2. A detailed PR description that includes:
882
- - Summary of changes
883
- - Motivation/context
884
- - Type of change (feature/fix/refactor/docs/etc)
885
- - Recommended testing
886
- 3. A suggested branch name following the format: type/short-description (example: feature/add-user-auth, fix/memory-leak)
887
-
888
- IMPORTANT: If these are local changes without push, the suggested branch name should be for creating a new branch from the current one.
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
- const outputFile = '.claude-pr-analysis.json';
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 claudeIgnores = ['.claude/', 'debug-claude-response.json', '.claude-pr-analysis.json'];
1040
- let allPresent = true;
1060
+ const claudeIgnore = '.claude/';
1041
1061
 
1042
- claudeIgnores.forEach(entry => {
1043
- const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
1044
- if (regex.test(gitignoreContent)) {
1045
- success(`${entry}: included`);
1046
- } else {
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
- // Comando set-mode
1061
- function setMode(mode) {
1062
- if (!checkGitRepo()) {
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
- try {
1070
- // Usar el script compartido para mantener consistencia
1071
- const result = execSync(`bash -c 'source "${getTemplatesPath()}/check-version.sh" && compare_versions "${v1}" "${v2}"; echo $?'`, { encoding: 'utf8' }).trim();
1072
- const exitCode = parseInt(result);
1073
-
1074
- // Convertir los códigos de retorno del script bash a valores JS
1075
- if (exitCode === 0) return 0; // iguales
1076
- if (exitCode === 1) return 1; // v1 > v2
1077
- if (exitCode === 2) return -1; // v1 < v2
1078
-
1079
- // Fallback: comparación simple si el script falla
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
- Exclude code from analysis:
1193
- // SKIP-ANALYSIS # Exclude the next line from analysis
1194
- // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one
1195
- ...excluded code...
1196
- // SKIP_ANALYSIS_BLOCK
1197
-
1198
- Performance optimization (NEW in v1.5.5):
1199
- export CLAUDE_USE_SUBAGENTS=true # Enable parallel analysis for 3+ files
1200
- export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet, opus
1201
- export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
1202
-
1203
- # Batching examples:
1204
- # BATCH_SIZE=1 with 4 files → 4 sequential batches (1 subagent each)
1205
- # BATCH_SIZE=3 with 4 files → 2 batches (3 parallel, then 1)
1206
- # BATCH_SIZE=4 with 4 files → 1 batch (4 parallel subagents)
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
- # Benefits: Faster for multi-file commits, shows execution time
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':