claude-git-hooks 2.7.1 → 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';
@@ -398,28 +398,64 @@ async function install(args) {
398
398
  }
399
399
  });
400
400
 
401
- // Copy ALL template files (.md and .json) to .claude directory
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
402
409
  const templateFiles = fs.readdirSync(templatesPath)
403
410
  .filter(file => {
404
411
  const filePath = path.join(templatesPath, file);
405
- 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');
406
416
  });
407
417
 
408
418
  templateFiles.forEach(file => {
409
419
  const sourcePath = path.join(templatesPath, file);
410
- 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
+ }
411
431
 
412
432
  // In force mode or if it doesn't exist, copy the file
413
433
  if (isForce || !fs.existsSync(destPath)) {
414
434
  if (fs.existsSync(sourcePath)) {
415
435
  fs.copyFileSync(sourcePath, destPath);
416
- success(`${file} installed in .claude/`);
436
+ success(`${file} installed in ${destLocation}`);
417
437
  }
418
438
  } else {
419
439
  info(`${file} already exists (skipped)`);
420
440
  }
421
441
  });
422
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
+
423
459
  // Copy presets directory structure
424
460
  const presetsSourcePath = path.join(templatesPath, 'presets');
425
461
  const presetsDestPath = path.join(claudeDir, 'presets');
@@ -460,15 +496,74 @@ async function install(args) {
460
496
  success(`${presetDirs.length} presets installed in .claude/presets/`);
461
497
  }
462
498
 
463
- // Special handling for config.json: rename config.example.json config.json
464
- const configExamplePath = path.join(claudeDir, 'config.example.json');
499
+ // Special handling for config.json (v2.8.0+): backup old, create new simplified
465
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
+ }
466
508
 
467
- if (fs.existsSync(configExamplePath) && !fs.existsSync(configPath)) {
468
- fs.copyFileSync(configExamplePath, configPath);
469
- success('config.json created from example (customize as needed)');
470
- } else if (!fs.existsSync(configPath)) {
471
- 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
+ }
472
567
  }
473
568
 
474
569
  // Create settings.local.json for sensitive data (gitignored)
@@ -493,21 +588,31 @@ async function install(args) {
493
588
  console.log(' git commit -m "auto" # Generate message automatically');
494
589
  console.log(' git commit -m "message" # Analyze code before commit');
495
590
  console.log(' git commit --no-verify # Skip analysis completely');
496
- console.log('\n💡 Configuration:');
591
+ console.log('\n💡 Configuration (v2.8.0):');
497
592
  console.log(' 📁 All templates installed in .claude/');
498
- console.log(' 📝 Edit .claude/config.json to customize settings');
499
- console.log(' 🎯 Use presets: backend, frontend, fullstack, database, ai, default');
500
- console.log(' 🚀 Enable parallel analysis: set subagents.enabled = true');
501
- 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');
502
599
  console.log('\n🔗 GitHub PR Creation (v2.5.0+):');
503
- console.log(' claude-hooks setup-github # Configure GitHub token for create-pr');
504
- console.log(' claude-hooks create-pr main # Create PR with auto-generated metadata');
505
- 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:');
506
608
  console.log(' {');
609
+ console.log(' "version": "2.8.0",');
507
610
  console.log(' "preset": "backend",');
508
- console.log(' "subagents": { "enabled": true, "model": "haiku", "batchSize": 3 },');
509
- console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
611
+ console.log(' "overrides": {');
612
+ console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
613
+ console.log(' }');
510
614
  console.log(' }');
615
+ console.log('\n🔧 Advanced: see .claude/config_example/config.advanced.example.json');
511
616
  console.log('\nFor more options: claude-hooks --help');
512
617
  }
513
618
 
@@ -968,8 +1073,8 @@ async function analyzeDiff(args) {
968
1073
  const startTime = Date.now();
969
1074
 
970
1075
  try {
971
- // Use cross-platform executeClaude from claude-client.js
972
- 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
973
1078
 
974
1079
  // Extract JSON from response using claude-client utility
975
1080
  const result = extractJSON(response);
@@ -1231,7 +1336,7 @@ async function createPr(args) {
1231
1336
 
1232
1337
  showInfo('Generating PR metadata with Claude...');
1233
1338
  logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
1234
- const response = await executeClaude(prompt, { timeout: 180000 });
1339
+ const response = await executeClaudeWithRetry(prompt, { timeout: 180000 });
1235
1340
  logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
1236
1341
 
1237
1342
  const analysisResult = extractJSON(response);
@@ -1853,6 +1958,179 @@ async function currentPreset() {
1853
1958
  }
1854
1959
  }
1855
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
+
1856
2134
  /**
1857
2135
  * Sets debug mode
1858
2136
  * Why: Enables detailed logging for troubleshooting
@@ -1942,6 +2220,9 @@ async function main() {
1942
2220
  error(`Unknown preset subcommand: ${args[1]}`);
1943
2221
  }
1944
2222
  break;
2223
+ case 'migrate-config':
2224
+ await migrateConfig();
2225
+ break;
1945
2226
  case '--debug':
1946
2227
  await setDebug(args[1]);
1947
2228
  break;