claude-git-hooks 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +203 -79
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +163 -0
  5. package/lib/hooks/pre-commit.js +179 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +93 -11
  8. package/lib/utils/file-operations.js +1 -65
  9. package/lib/utils/file-utils.js +65 -0
  10. package/lib/utils/package-info.js +75 -0
  11. package/lib/utils/preset-loader.js +209 -0
  12. package/lib/utils/prompt-builder.js +83 -67
  13. package/lib/utils/resolution-prompt.js +12 -2
  14. package/package.json +49 -50
  15. package/templates/ANALYZE_DIFF.md +33 -0
  16. package/templates/COMMIT_MESSAGE.md +24 -0
  17. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  18. package/templates/config.example.json +41 -0
  19. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  20. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  21. package/templates/presets/ai/config.json +12 -0
  22. package/templates/presets/ai/preset.json +42 -0
  23. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  24. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  25. package/templates/presets/backend/config.json +12 -0
  26. package/templates/presets/backend/preset.json +49 -0
  27. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  28. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  29. package/templates/presets/database/config.json +12 -0
  30. package/templates/presets/database/preset.json +38 -0
  31. package/templates/presets/default/config.json +12 -0
  32. package/templates/presets/default/preset.json +53 -0
  33. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  34. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  35. package/templates/presets/frontend/config.json +12 -0
  36. package/templates/presets/frontend/preset.json +50 -0
  37. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  38. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  39. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  40. package/templates/presets/fullstack/config.json +12 -0
  41. package/templates/presets/fullstack/preset.json +55 -0
  42. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  43. package/templates/shared/ANALYZE_DIFF.md +33 -0
  44. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  45. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  46. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  47. package/templates/check-version.sh +0 -266
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,16 +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('\n⚠️ Exclude code from analysis (EXPERIMENTAL/BROKEN):');
433
- console.log(' // SKIP_ANALYSIS_LINE # Does NOT work reliably');
434
- console.log(' // SKIP_ANALYSIS_BLOCK # Does NOT work reliably');
435
- console.log(' // Reason: Analyzes git diff, not full file');
436
- console.log('\nNEW: Parallel analysis for faster multi-file commits:');
437
- console.log(' export CLAUDE_USE_SUBAGENTS=true # Enable subagents');
438
- console.log(' export CLAUDE_SUBAGENT_MODEL=haiku # haiku/sonnet/opus');
439
- console.log(' export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel per batch (default: 3)');
440
- console.log(' # Example: 4 files, BATCH_SIZE=1 4 sequential batches');
441
- 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(' }');
442
476
  console.log('\nFor more options: claude-hooks --help');
443
477
  }
444
478
 
@@ -596,9 +630,6 @@ function updateGitignore() {
596
630
  const claudeEntries = [
597
631
  '# Claude Git Hooks',
598
632
  '.claude/',
599
- 'debug-claude-response.json',
600
- 'claude_resolution_prompt.md',
601
- '.claude-pr-analysis.json',
602
633
  ];
603
634
 
604
635
  let gitignoreContent = '';
@@ -759,6 +790,9 @@ async function analyzeDiff(args) {
759
790
  return;
760
791
  }
761
792
 
793
+ // Load configuration
794
+ const config = await getConfig();
795
+
762
796
  const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
763
797
 
764
798
  if (!currentBranch) {
@@ -862,9 +896,9 @@ async function analyzeDiff(args) {
862
896
  }
863
897
 
864
898
  // Check if subagents should be used
865
- const useSubagents = process.env.CLAUDE_USE_SUBAGENTS === 'true';
866
- const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
867
- 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;
868
902
  // Validate batch size (must be >= 1)
869
903
  if (subagentBatchSize < 1) {
870
904
  subagentBatchSize = 1;
@@ -873,37 +907,19 @@ async function analyzeDiff(args) {
873
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`
874
908
  : '';
875
909
 
876
- const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
877
- ${subagentInstruction}
878
- Please generate:
879
- 1. A concise and descriptive PR title (maximum 72 characters)
880
- 2. A detailed PR description that includes:
881
- - Summary of changes
882
- - Motivation/context
883
- - Type of change (feature/fix/refactor/docs/etc)
884
- - Recommended testing
885
- 3. A suggested branch name following the format: type/short-description (example: feature/add-user-auth, fix/memory-leak)
886
-
887
- IMPORTANT: If these are local changes without push, the suggested branch name should be for creating a new branch from the current one.
888
-
889
- Respond EXCLUSIVELY with a valid JSON with this structure:
890
- {
891
- "prTitle": "Interesting PR title",
892
- "prDescription": "detailed PR description with markdown",
893
- "suggestedBranchName": "type/suggested-branch-name",
894
- "changeType": "feature|fix|refactor|docs|test|chore",
895
- "breakingChanges": false,
896
- "testingNotes": "notes on necessary testing or 'None'"
897
- }
898
-
899
- === COMMITS ===
900
- ${commits}
901
-
902
- === CHANGED FILES ===
903
- ${diffFiles}
904
-
905
- === FULL DIFF ===
906
- ${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
+ });
907
923
 
908
924
  info('Sending to Claude for analysis...');
909
925
  const startTime = Date.now();
@@ -964,7 +980,13 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
964
980
  }
965
981
  };
966
982
 
967
- 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';
968
990
  fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
969
991
 
970
992
  const elapsed = Date.now() - startTime;
@@ -1035,20 +1057,13 @@ function status() {
1035
1057
  const gitignorePath = '.gitignore';
1036
1058
  if (fs.existsSync(gitignorePath)) {
1037
1059
  const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
1038
- const claudeIgnores = ['.claude/', 'debug-claude-response.json', '.claude-pr-analysis.json'];
1039
- let allPresent = true;
1060
+ const claudeIgnore = '.claude/';
1040
1061
 
1041
- claudeIgnores.forEach(entry => {
1042
- const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
1043
- if (regex.test(gitignoreContent)) {
1044
- success(`${entry}: included`);
1045
- } else {
1046
- warning(`${entry}: missing`);
1047
- allPresent = false;
1048
- }
1049
- });
1050
-
1051
- 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`);
1052
1067
  info('\nRun "claude-hooks install" to update .gitignore');
1053
1068
  }
1054
1069
  } else {
@@ -1056,13 +1071,6 @@ function status() {
1056
1071
  }
1057
1072
  }
1058
1073
 
1059
- // Comando set-mode
1060
- function setMode(mode) {
1061
- if (!checkGitRepo()) {
1062
- error('You are not in a Git repository.');
1063
- }
1064
- }
1065
-
1066
1074
  // Cross-platform version comparison (semver)
1067
1075
  // Why: Pure JavaScript, no bash dependency
1068
1076
  // Returns: 0 if equal, 1 if v1 > v2, -1 if v1 < v2
@@ -1132,6 +1140,13 @@ async function update() {
1132
1140
  }
1133
1141
  }
1134
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
+
1135
1150
  // Comando help
1136
1151
  function showHelp() {
1137
1152
  console.log(`
@@ -1149,6 +1164,10 @@ Commands:
1149
1164
  disable [hook] Disable hooks (all or one specific)
1150
1165
  status Show the status of hooks
1151
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
1152
1171
  help Show this help
1153
1172
 
1154
1173
  Available hooks:
@@ -1163,6 +1182,9 @@ Examples:
1163
1182
  claude-hooks enable # Enable all hooks
1164
1183
  claude-hooks status # View current status
1165
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
1166
1188
 
1167
1189
  Commit use cases:
1168
1190
  git commit -m "message" # Manual message + blocking analysis
@@ -1173,30 +1195,165 @@ Commit use cases:
1173
1195
  Analyze-diff use case:
1174
1196
  claude-hooks analyze-diff main # Analyze changes vs main and generate:
1175
1197
  → PR Title: "feat: add user authentication module"
1176
- → PR Description: "## Summary\n- Added JWT authentication..."
1198
+ → PR Description: "## Summary\n- Added JWT authentication..."
1177
1199
  → Suggested branch: "feature/user-authentication"
1178
1200
 
1179
- ⚠️ Exclude code from analysis (EXPERIMENTAL/BROKEN):
1180
- // SKIP_ANALYSIS_LINE # Does NOT work reliably
1181
- // SKIP_ANALYSIS_BLOCK # Does NOT work reliably
1182
- Reason: Analyzes git diff, not full file
1183
-
1184
- Performance optimization (NEW in v1.5.5):
1185
- export CLAUDE_USE_SUBAGENTS=true # Enable parallel analysis for 3+ files
1186
- export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet, opus
1187
- export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
1188
-
1189
- # Batching examples:
1190
- # BATCH_SIZE=1 with 4 files → 4 sequential batches (1 subagent each)
1191
- # BATCH_SIZE=3 with 4 files → 2 batches (3 parallel, then 1)
1192
- # 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
+ }
1193
1230
 
1194
- # 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
1195
1245
 
1196
1246
  More information: https://github.com/pablorovito/claude-git-hooks
1197
1247
  `);
1198
1248
  }
1199
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
+
1200
1357
  // Main
1201
1358
  async function main() {
1202
1359
  const args = process.argv.slice(2);
@@ -1224,6 +1381,25 @@ async function main() {
1224
1381
  case 'analyze-diff':
1225
1382
  await analyzeDiff(args.slice(1));
1226
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;
1227
1403
  case 'help':
1228
1404
  case '--help':
1229
1405
  case '-h':