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/bin/claude-hooks CHANGED
@@ -8,7 +8,7 @@ import readline from 'readline';
8
8
  import https from 'https';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { dirname } from 'path';
11
- import { executeClaude, extractJSON } from '../lib/utils/claude-client.js';
11
+ import { executeClaude, executeClaudeWithRetry, extractJSON, analyzeCode } from '../lib/utils/claude-client.js';
12
12
  import { loadPrompt } from '../lib/utils/prompt-builder.js';
13
13
  import { listPresets } from '../lib/utils/preset-loader.js';
14
14
  import { getConfig } from '../lib/config.js';
@@ -384,28 +384,78 @@ async function install(args) {
384
384
  success('.claude directory created');
385
385
  }
386
386
 
387
- // Copy ALL template files (.md and .json) to .claude directory
387
+ // Remove old SONAR template files if they exist (migration from v2.6.x to v2.7.0+)
388
+ const oldSonarFiles = [
389
+ 'CLAUDE_PRE_COMMIT_SONAR.md',
390
+ 'CLAUDE_ANALYSIS_PROMPT_SONAR.md'
391
+ ];
392
+
393
+ oldSonarFiles.forEach(oldFile => {
394
+ const oldPath = path.join(claudeDir, oldFile);
395
+ if (fs.existsSync(oldPath)) {
396
+ fs.unlinkSync(oldPath);
397
+ info(`Removed old template: ${oldFile}`);
398
+ }
399
+ });
400
+
401
+ // Create .claude/prompts directory for markdown templates
402
+ const promptsDir = path.join(claudeDir, 'prompts');
403
+ if (!fs.existsSync(promptsDir)) {
404
+ fs.mkdirSync(promptsDir, { recursive: true });
405
+ success('.claude/prompts directory created');
406
+ }
407
+
408
+ // Copy template files (.md and .json) to appropriate locations
388
409
  const templateFiles = fs.readdirSync(templatesPath)
389
410
  .filter(file => {
390
411
  const filePath = path.join(templatesPath, file);
391
- return fs.statSync(filePath).isFile() && (file.endsWith('.md') || file.endsWith('.json'));
412
+ // Exclude example.json files and only include .md and .json files
413
+ return fs.statSync(filePath).isFile() &&
414
+ (file.endsWith('.md') || file.endsWith('.json')) &&
415
+ !file.includes('example.json');
392
416
  });
393
417
 
394
418
  templateFiles.forEach(file => {
395
419
  const sourcePath = path.join(templatesPath, file);
396
- const destPath = path.join(claudeDir, file);
420
+ let destPath;
421
+ let destLocation;
422
+
423
+ // .md files go to .claude/prompts/, .json files go to .claude/
424
+ if (file.endsWith('.md')) {
425
+ destPath = path.join(promptsDir, file);
426
+ destLocation = '.claude/prompts/';
427
+ } else {
428
+ destPath = path.join(claudeDir, file);
429
+ destLocation = '.claude/';
430
+ }
397
431
 
398
432
  // In force mode or if it doesn't exist, copy the file
399
433
  if (isForce || !fs.existsSync(destPath)) {
400
434
  if (fs.existsSync(sourcePath)) {
401
435
  fs.copyFileSync(sourcePath, destPath);
402
- success(`${file} installed in .claude/`);
436
+ success(`${file} installed in ${destLocation}`);
403
437
  }
404
438
  } else {
405
439
  info(`${file} already exists (skipped)`);
406
440
  }
407
441
  });
408
442
 
443
+ // Clean up old .md files from .claude/ root (v2.8.0 migration)
444
+ // .md files should now be in .claude/prompts/, not .claude/
445
+ const oldMdFiles = fs.readdirSync(claudeDir)
446
+ .filter(file => {
447
+ const filePath = path.join(claudeDir, file);
448
+ return fs.statSync(filePath).isFile() && file.endsWith('.md');
449
+ });
450
+
451
+ if (oldMdFiles.length > 0) {
452
+ oldMdFiles.forEach(file => {
453
+ const oldPath = path.join(claudeDir, file);
454
+ fs.unlinkSync(oldPath);
455
+ info(`Removed old template from .claude/: ${file} (now in prompts/)`);
456
+ });
457
+ }
458
+
409
459
  // Copy presets directory structure
410
460
  const presetsSourcePath = path.join(templatesPath, 'presets');
411
461
  const presetsDestPath = path.join(claudeDir, 'presets');
@@ -446,15 +496,74 @@ async function install(args) {
446
496
  success(`${presetDirs.length} presets installed in .claude/presets/`);
447
497
  }
448
498
 
449
- // Special handling for config.json: rename config.example.json config.json
450
- const configExamplePath = path.join(claudeDir, 'config.example.json');
499
+ // Special handling for config.json (v2.8.0+): backup old, create new simplified
451
500
  const configPath = path.join(claudeDir, 'config.json');
501
+ const configOldDir = path.join(claudeDir, 'config_old');
502
+ const configExampleDir = path.join(claudeDir, 'config_example');
503
+
504
+ // Create config_old directory if needed
505
+ if (!fs.existsSync(configOldDir)) {
506
+ fs.mkdirSync(configOldDir, { recursive: true });
507
+ }
452
508
 
453
- if (fs.existsSync(configExamplePath) && !fs.existsSync(configPath)) {
454
- fs.copyFileSync(configExamplePath, configPath);
455
- success('config.json created from example (customize as needed)');
456
- } else if (!fs.existsSync(configPath)) {
457
- warning('config.json not found - using defaults');
509
+ // Create config_example directory
510
+ if (!fs.existsSync(configExampleDir)) {
511
+ fs.mkdirSync(configExampleDir, { recursive: true });
512
+ }
513
+
514
+ // Copy example configs to config_example/ directly from templates/
515
+ const exampleConfigs = ['config.example.json', 'config.advanced.example.json'];
516
+ exampleConfigs.forEach(exampleFile => {
517
+ const sourcePath = path.join(templatesPath, exampleFile);
518
+ const destPath = path.join(configExampleDir, exampleFile);
519
+ if (fs.existsSync(sourcePath)) {
520
+ fs.copyFileSync(sourcePath, destPath);
521
+ }
522
+ });
523
+ success('Example configs installed in .claude/config_example/');
524
+
525
+ // Backup existing config if it exists (legacy format migration)
526
+ let needsMigration = false;
527
+ if (fs.existsSync(configPath)) {
528
+ const backupPath = path.join(configOldDir, `config.json.${Date.now()}`);
529
+ fs.copyFileSync(configPath, backupPath);
530
+ info(`Existing config backed up: ${backupPath}`);
531
+
532
+ // Read old config to check if it's legacy format
533
+ const oldConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
534
+ if (!oldConfig.version || oldConfig.version !== '2.8.0') {
535
+ warning('Legacy config detected - will be replaced with v2.8.0 format');
536
+ needsMigration = true;
537
+
538
+ // Delete old config to force new format
539
+ fs.unlinkSync(configPath);
540
+ } else {
541
+ info('Config already in v2.8.0 format - keeping existing file');
542
+ }
543
+ }
544
+
545
+ // Create new config.json from minimal example if it doesn't exist
546
+ if (!fs.existsSync(configPath)) {
547
+ // Read example and extract minimal config
548
+ const examplePath = path.join(configExampleDir, 'config.example.json');
549
+ const exampleContent = fs.readFileSync(examplePath, 'utf8');
550
+ const exampleJson = JSON.parse(exampleContent);
551
+
552
+ // Create minimal config: just version and preset
553
+ const minimalConfig = {
554
+ version: exampleJson.version,
555
+ preset: exampleJson.preset
556
+ };
557
+
558
+ fs.writeFileSync(configPath, JSON.stringify(minimalConfig, null, 4));
559
+ success('config.json created with minimal v2.8.0 format');
560
+ info('📝 Customize: .claude/config.json (see config_example/ for examples)');
561
+
562
+ // Auto-run migration if needed to preserve settings
563
+ if (needsMigration) {
564
+ info('🔄 Auto-migrating settings from backup...');
565
+ await autoMigrateConfig(configPath, path.join(configOldDir, fs.readdirSync(configOldDir).sort().pop()));
566
+ }
458
567
  }
459
568
 
460
569
  // Create settings.local.json for sensitive data (gitignored)
@@ -479,21 +588,31 @@ async function install(args) {
479
588
  console.log(' git commit -m "auto" # Generate message automatically');
480
589
  console.log(' git commit -m "message" # Analyze code before commit');
481
590
  console.log(' git commit --no-verify # Skip analysis completely');
482
- console.log('\n💡 Configuration:');
591
+ console.log('\n💡 Configuration (v2.8.0):');
483
592
  console.log(' 📁 All templates installed in .claude/');
484
- console.log(' 📝 Edit .claude/config.json to customize settings');
485
- console.log(' 🎯 Use presets: backend, frontend, fullstack, database, ai, default');
486
- console.log(' 🚀 Enable parallel analysis: set subagents.enabled = true');
487
- console.log(' 🐛 Enable debug mode: claude-hooks --debug true');
593
+ console.log(' 📝 Edit .claude/config.json (minimal by default)');
594
+ console.log(' 📂 Examples: .claude/config_example/');
595
+ console.log(' 📦 Backups: .claude/config_old/');
596
+ console.log(' 🎯 Presets: backend, frontend, fullstack, database, ai, default');
597
+ console.log(' 🚀 Parallel analysis enabled by default (hardcoded)');
598
+ console.log(' 🐛 Debug mode: claude-hooks --debug true');
488
599
  console.log('\n🔗 GitHub PR Creation (v2.5.0+):');
489
- console.log(' claude-hooks setup-github # Configure GitHub token for create-pr');
490
- console.log(' claude-hooks create-pr main # Create PR with auto-generated metadata');
491
- console.log('\n📖 Example config.json:');
600
+ console.log(' claude-hooks setup-github # Configure GitHub token');
601
+ console.log(' claude-hooks create-pr main # Create PR with auto-metadata');
602
+ console.log('\n📖 Minimal config.json (v2.8.0):');
603
+ console.log(' {');
604
+ console.log(' "version": "2.8.0",');
605
+ console.log(' "preset": "backend"');
606
+ console.log(' }');
607
+ console.log('\n📖 With GitHub customization:');
492
608
  console.log(' {');
609
+ console.log(' "version": "2.8.0",');
493
610
  console.log(' "preset": "backend",');
494
- console.log(' "subagents": { "enabled": true, "model": "haiku", "batchSize": 3 },');
495
- console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
611
+ console.log(' "overrides": {');
612
+ console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
613
+ console.log(' }');
496
614
  console.log(' }');
615
+ console.log('\n🔧 Advanced: see .claude/config_example/config.advanced.example.json');
497
616
  console.log('\nFor more options: claude-hooks --help');
498
617
  }
499
618
 
@@ -954,8 +1073,8 @@ async function analyzeDiff(args) {
954
1073
  const startTime = Date.now();
955
1074
 
956
1075
  try {
957
- // Use cross-platform executeClaude from claude-client.js
958
- const response = await executeClaude(prompt, { timeout: 180000 }); // 3 minutes for diff analysis
1076
+ // Use cross-platform executeClaudeWithRetry from claude-client.js
1077
+ const response = await executeClaudeWithRetry(prompt, { timeout: 180000 }); // 3 minutes for diff analysis
959
1078
 
960
1079
  // Extract JSON from response using claude-client utility
961
1080
  const result = extractJSON(response);
@@ -1217,7 +1336,7 @@ async function createPr(args) {
1217
1336
 
1218
1337
  showInfo('Generating PR metadata with Claude...');
1219
1338
  logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
1220
- const response = await executeClaude(prompt, { timeout: 180000 });
1339
+ const response = await executeClaudeWithRetry(prompt, { timeout: 180000 });
1221
1340
  logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
1222
1341
 
1223
1342
  const analysisResult = extractJSON(response);
@@ -1455,7 +1574,7 @@ function status() {
1455
1574
 
1456
1575
  // Check guidelines files
1457
1576
  console.log('\nGuidelines files:');
1458
- const guidelines = ['CLAUDE_PRE_COMMIT_SONAR.md'];
1577
+ const guidelines = ['CLAUDE_PRE_COMMIT.md'];
1459
1578
  guidelines.forEach(guideline => {
1460
1579
  const claudePath = path.join('.claude', guideline);
1461
1580
  if (fs.existsSync(claudePath)) {
@@ -1683,7 +1802,7 @@ Customization:
1683
1802
  Override prompts by copying to .claude/:
1684
1803
  cp templates/COMMIT_MESSAGE.md .claude/
1685
1804
  cp templates/ANALYZE_DIFF.md .claude/
1686
- cp templates/CLAUDE_PRE_COMMIT_SONAR.md .claude/
1805
+ cp templates/CLAUDE_PRE_COMMIT.md .claude/
1687
1806
  # Edit as needed - system uses .claude/ version if exists
1688
1807
 
1689
1808
  More information: https://github.com/pablorovito/claude-git-hooks
@@ -1839,6 +1958,179 @@ async function currentPreset() {
1839
1958
  }
1840
1959
  }
1841
1960
 
1961
+ // ============================================================================
1962
+ // DEPRECATED CODE SECTION - Will be removed in v3.0.0
1963
+ // ============================================================================
1964
+ // This section contains migration code for legacy configs (pre-v2.8.0)
1965
+ // Auto-executed during install when legacy config detected
1966
+ // Manual command: claude-hooks migrate-config
1967
+ // ============================================================================
1968
+
1969
+ /**
1970
+ * Extracts allowed settings from legacy config format
1971
+ * Shared by both autoMigrateConfig and migrateConfig
1972
+ *
1973
+ * @param {Object} rawConfig - Legacy format config
1974
+ * @returns {Object} Allowed overrides only
1975
+ */
1976
+ function extractLegacySettings(rawConfig) {
1977
+ const allowedOverrides = {};
1978
+
1979
+ // GitHub PR config (fully allowed)
1980
+ if (rawConfig.github?.pr) {
1981
+ allowedOverrides.github = { pr: {} };
1982
+ if (rawConfig.github.pr.defaultBase !== undefined) {
1983
+ allowedOverrides.github.pr.defaultBase = rawConfig.github.pr.defaultBase;
1984
+ }
1985
+ if (rawConfig.github.pr.reviewers !== undefined) {
1986
+ allowedOverrides.github.pr.reviewers = rawConfig.github.pr.reviewers;
1987
+ }
1988
+ if (rawConfig.github.pr.labelRules !== undefined) {
1989
+ allowedOverrides.github.pr.labelRules = rawConfig.github.pr.labelRules;
1990
+ }
1991
+ }
1992
+
1993
+ // Subagent batchSize (allowed)
1994
+ if (rawConfig.subagents?.batchSize !== undefined) {
1995
+ allowedOverrides.subagents = { batchSize: rawConfig.subagents.batchSize };
1996
+ }
1997
+
1998
+ // Advanced params (preserved with warning in manual migration)
1999
+ if (rawConfig.analysis?.ignoreExtensions !== undefined) {
2000
+ if (!allowedOverrides.analysis) allowedOverrides.analysis = {};
2001
+ allowedOverrides.analysis.ignoreExtensions = rawConfig.analysis.ignoreExtensions;
2002
+ }
2003
+
2004
+ if (rawConfig.commitMessage?.taskIdPattern !== undefined) {
2005
+ if (!allowedOverrides.commitMessage) allowedOverrides.commitMessage = {};
2006
+ allowedOverrides.commitMessage.taskIdPattern = rawConfig.commitMessage.taskIdPattern;
2007
+ }
2008
+
2009
+ if (rawConfig.subagents?.model !== undefined) {
2010
+ if (!allowedOverrides.subagents) allowedOverrides.subagents = {};
2011
+ allowedOverrides.subagents.model = rawConfig.subagents.model;
2012
+ }
2013
+
2014
+ return allowedOverrides;
2015
+ }
2016
+
2017
+ /**
2018
+ * Auto-migrates legacy config during installation
2019
+ * Called automatically by installer when legacy format detected
2020
+ *
2021
+ * @param {string} newConfigPath - Path to new config.json
2022
+ * @param {string} backupConfigPath - Path to backup of old config
2023
+ */
2024
+ async function autoMigrateConfig(newConfigPath, backupConfigPath) {
2025
+ try {
2026
+ const rawConfig = JSON.parse(fs.readFileSync(backupConfigPath, 'utf8'));
2027
+ const allowedOverrides = extractLegacySettings(rawConfig);
2028
+
2029
+ // Read the newly created minimal config
2030
+ const newConfig = JSON.parse(fs.readFileSync(newConfigPath, 'utf8'));
2031
+
2032
+ // Add overrides if any
2033
+ if (Object.keys(allowedOverrides).length > 0) {
2034
+ newConfig.overrides = allowedOverrides;
2035
+ fs.writeFileSync(newConfigPath, JSON.stringify(newConfig, null, 4));
2036
+ success('✅ Settings migrated from legacy config');
2037
+ info(`📋 Preserved ${Object.keys(allowedOverrides).length} custom settings`);
2038
+ }
2039
+ } catch (err) {
2040
+ warning(`⚠️ Could not auto-migrate settings: ${err.message}`);
2041
+ info('💡 Run "claude-hooks migrate-config" manually if needed');
2042
+ }
2043
+ }
2044
+
2045
+ /**
2046
+ * Migrates legacy config.json to v2.8.0 format (Manual command)
2047
+ * Why: Simplifies configuration, reduces redundancy
2048
+ *
2049
+ * DEPRECATED: Will be removed in v3.0.0 (most users will have migrated by then)
2050
+ */
2051
+ async function migrateConfig() {
2052
+ const claudeDir = '.claude';
2053
+ const configPath = path.join(claudeDir, 'config.json');
2054
+
2055
+ if (!fs.existsSync(configPath)) {
2056
+ info('ℹ️ No config file found. Nothing to migrate.');
2057
+ console.log('\n💡 To create a new config:');
2058
+ console.log(' 1. Run: claude-hooks install --force');
2059
+ console.log(' 2. Or copy from: .claude/config_example/config.example.json');
2060
+ return;
2061
+ }
2062
+
2063
+ try {
2064
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
2065
+
2066
+ // Check if already in v2.8.0 format
2067
+ if (rawConfig.version === '2.8.0') {
2068
+ success('✅ Config is already in v2.8.0 format.');
2069
+ return;
2070
+ }
2071
+
2072
+ info('📦 Starting config migration to v2.8.0...');
2073
+
2074
+ // Create backup in config_old/
2075
+ const configOldDir = path.join(claudeDir, 'config_old');
2076
+ if (!fs.existsSync(configOldDir)) {
2077
+ fs.mkdirSync(configOldDir, { recursive: true });
2078
+ }
2079
+ const backupPath = path.join(configOldDir, `config.json.${Date.now()}`);
2080
+ fs.copyFileSync(configPath, backupPath);
2081
+ success(`Backup created: ${backupPath}`);
2082
+
2083
+ // Extract only allowed parameters
2084
+ const allowedOverrides = extractLegacySettings(rawConfig);
2085
+
2086
+ // Check for advanced params
2087
+ const hasAdvancedParams = allowedOverrides.analysis?.ignoreExtensions ||
2088
+ allowedOverrides.commitMessage?.taskIdPattern ||
2089
+ allowedOverrides.subagents?.model;
2090
+
2091
+ // Build new config
2092
+ const newConfig = {
2093
+ version: '2.8.0',
2094
+ preset: rawConfig.preset || 'default'
2095
+ };
2096
+
2097
+ // Only add overrides if there are any
2098
+ if (Object.keys(allowedOverrides).length > 0) {
2099
+ newConfig.overrides = allowedOverrides;
2100
+ }
2101
+
2102
+ // Show diff
2103
+ console.log('\n📝 Migration preview:');
2104
+ console.log(` Old format: ${Object.keys(rawConfig).length} top-level keys`);
2105
+ console.log(` New format: ${Object.keys(newConfig).length} top-level keys`);
2106
+ if (Object.keys(allowedOverrides).length > 0) {
2107
+ console.log(` Preserved: ${Object.keys(allowedOverrides).length} override sections`);
2108
+ }
2109
+
2110
+ // Write new config
2111
+ fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 4));
2112
+ success('✅ Config migrated to v2.8.0 successfully!');
2113
+
2114
+ if (hasAdvancedParams) {
2115
+ warning('⚠️ Advanced parameters detected and preserved');
2116
+ info('📖 See .claude/config.advanced.example.json for documentation');
2117
+ }
2118
+
2119
+ console.log(`\n✨ New config:`);
2120
+ console.log(JSON.stringify(newConfig, null, 2));
2121
+ console.log(`\n💾 Old config backed up to: ${backupPath}`);
2122
+ console.log('\n💡 Many parameters are now hardcoded with sensible defaults');
2123
+ console.log(' See CHANGELOG.md for full list of changes');
2124
+
2125
+ } catch (error) {
2126
+ error(`Failed to migrate config: ${error.message}`);
2127
+ console.log('\n💡 Manual migration:');
2128
+ console.log(' 1. Backup your current config');
2129
+ console.log(' 2. See .claude/config.example.json for new format');
2130
+ console.log(' 3. Copy minimal example and customize');
2131
+ }
2132
+ }
2133
+
1842
2134
  /**
1843
2135
  * Sets debug mode
1844
2136
  * Why: Enables detailed logging for troubleshooting
@@ -1928,6 +2220,9 @@ async function main() {
1928
2220
  error(`Unknown preset subcommand: ${args[1]}`);
1929
2221
  }
1930
2222
  break;
2223
+ case 'migrate-config':
2224
+ await migrateConfig();
2225
+ break;
1931
2226
  case '--debug':
1932
2227
  await setDebug(args[1]);
1933
2228
  break;