UNPKG

claude-git-hooks

Version:

Git hooks with Claude CLI for code analysis and automatic commit messages

288 lines (258 loc) 10.1 kB
/** * File: config.js * Purpose: Simplified configuration management (v2.8.0) * * Priority: preset config > .claude/config.json > hardcoded defaults * * v2.8.0 Changes: * - Removed 21 redundant parameters (hardcoded sensible defaults) * - Only 5 user-configurable parameters remain * - Preset can override: subagents.batchSize * - User can override: github.pr.*, subagents.batchSize * - Advanced params moved to config.advanced.example.json * * User-configurable: * - preset (required) * - github.pr.defaultBase * - github.pr.reviewers * - github.pr.labelRules * - subagents.batchSize * * Advanced (in example file only): * - analysis.ignoreExtensions * - commitMessage.taskIdPattern * - subagents.model */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import logger from './utils/logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Hardcoded defaults (v3.0.0) * These are NOT user-configurable - sensible defaults that work for everyone */ const HARDCODED = { analysis: { maxFileSize: 1000000, // 1MB - sufficient for most files maxFiles: 20, // Reasonable limit per commit timeout: 300000, // 5 minutes - adequate for Claude API contextLines: 3, // Git default ignoreExtensions: [], // Can be set in advanced config only }, commitMessage: { autoKeyword: 'auto', // Standard keyword timeout: 300000, // Use same timeout as analysis taskIdPattern: '([A-Z]{1,3}[-\\s]\\d{3,5})', // Jira/GitHub/Linear pattern }, subagents: { enabled: true, // Enable by default (faster analysis) model: 'haiku', // Fast and cost-effective batchSize: 3, // Reasonable parallelization }, templates: { baseDir: '.claude/prompts', analysis: 'CLAUDE_ANALYSIS_PROMPT.md', guidelines: 'CLAUDE_PRE_COMMIT.md', commitMessage: 'COMMIT_MESSAGE.md', analyzeDiff: 'ANALYZE_DIFF.md', resolution: 'CLAUDE_RESOLUTION_PROMPT.md', subagentInstruction: 'SUBAGENT_INSTRUCTION.md', createGithubPR: 'CREATE_GITHUB_PR.md', }, output: { outputDir: '.claude/out', debugFile: '.claude/out/debug-claude-response.json', resolutionFile: '.claude/out/claude_resolution_prompt.md', prAnalysisFile: '.claude/out/pr-analysis.json', }, system: { debug: false, // Controlled by --debug flag telemetry: true, // Opt-out telemetry for debugging (local only, set to false to disable) wslCheckTimeout: 15000, // System behavior }, git: { diffFilter: 'ACM', // Standard: Added, Copied, Modified }, github: { enabled: true, // Always enabled }, }; /** * Default user-configurable values (v2.8.0) * Only these can be overridden in .claude/config.json */ const defaults = { // GitHub PR configuration (user-specific) github: { pr: { defaultBase: 'develop', // Project default branch reviewers: [], // Project reviewers labelRules: { // Labels by preset backend: ['backend', 'java'], frontend: ['frontend', 'react'], fullstack: ['fullstack'], database: ['database', 'sql'], ai: ['ai', 'tooling'], default: [] }, }, }, // Subagent configuration (preset can override) subagents: { batchSize: 3, // Files per parallel batch }, }; /** * Loads user configuration from .claude/config.json (v2.8.0) * * Format detection: * - v2.8.0: { version: "2.8.0", preset: "...", overrides: {...} } * - Legacy: { preset: "...", analysis: {...}, ... } (auto-migrates with warning) * * Merge priority: HARDCODED < defaults < preset config < user overrides * * @param {string} baseDir - Base directory to search for config (default: cwd) * @returns {Promise<Object>} Merged configuration */ const loadUserConfig = async (baseDir = process.cwd()) => { const configPath = path.join(baseDir, '.claude', 'config.json'); let rawConfig = {}; let configFormat = 'default'; try { if (fs.existsSync(configPath)) { rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); // Detect format if (rawConfig.version === '2.8.0') { configFormat = 'v3'; } else if (rawConfig.version) { throw new Error(`Unsupported config version: ${rawConfig.version}. Please run 'claude-hooks migrate-config'`); } else if (Object.keys(rawConfig).length > 0 && rawConfig.preset) { configFormat = 'legacy'; logger.warning('⚠️ Legacy config format detected. Run "claude-hooks migrate-config" to update to v2.8.0'); } } } catch (error) { if (error.message.includes('Unsupported config version')) { throw error; } logger.warning(`⚠️ Warning: Could not load .claude/config.json: ${error.message}`); logger.warning('Using default configuration'); } // Extract user overrides based on format let userOverrides = {}; let presetName = null; if (configFormat === 'v3') { presetName = rawConfig.preset; userOverrides = rawConfig.overrides || {}; } else if (configFormat === 'legacy') { presetName = rawConfig.preset; // Legacy format: extract only allowed parameters userOverrides = extractAllowedParams(rawConfig); } // Load preset if specified let presetConfig = {}; if (presetName) { try { const { loadPreset } = await import('./utils/preset-loader.js'); const { config } = await loadPreset(presetName); presetConfig = config; } catch (error) { logger.warning(`⚠️ Warning: Preset "${presetName}" not found, using defaults`); } } // Merge priority: HARDCODED < defaults < preset < user overrides const baseConfig = deepMerge(HARDCODED, defaults); const withPreset = deepMerge(baseConfig, presetConfig); const final = deepMerge(withPreset, userOverrides); // Add preset name to final config (so hooks can display it) if (presetName) { final.preset = presetName; } return final; }; /** * Extracts only allowed parameters from legacy config * v2.8.0 only allows: github.pr.*, subagents.batchSize * Advanced: analysis.ignoreExtensions, commitMessage.taskIdPattern, subagents.model * * @param {Object} legacyConfig - Legacy format config * @returns {Object} Allowed parameters only */ const extractAllowedParams = (legacyConfig) => { const allowed = {}; // GitHub PR config (fully allowed) if (legacyConfig.github?.pr) { allowed.github = { pr: {} }; if (legacyConfig.github.pr.defaultBase !== undefined) { allowed.github.pr.defaultBase = legacyConfig.github.pr.defaultBase; } if (legacyConfig.github.pr.reviewers !== undefined) { allowed.github.pr.reviewers = legacyConfig.github.pr.reviewers; } if (legacyConfig.github.pr.labelRules !== undefined) { allowed.github.pr.labelRules = legacyConfig.github.pr.labelRules; } } // Subagent batchSize (allowed) if (legacyConfig.subagents?.batchSize !== undefined) { allowed.subagents = { batchSize: legacyConfig.subagents.batchSize }; } // Advanced params (allowed but warn) if (legacyConfig.analysis?.ignoreExtensions !== undefined) { if (!allowed.analysis) allowed.analysis = {}; allowed.analysis.ignoreExtensions = legacyConfig.analysis.ignoreExtensions; logger.warning('ℹ️ Using advanced parameter: analysis.ignoreExtensions'); } if (legacyConfig.commitMessage?.taskIdPattern !== undefined) { if (!allowed.commitMessage) allowed.commitMessage = {}; allowed.commitMessage.taskIdPattern = legacyConfig.commitMessage.taskIdPattern; logger.warning('ℹ️ Using advanced parameter: commitMessage.taskIdPattern'); } if (legacyConfig.subagents?.model !== undefined) { if (!allowed.subagents) allowed.subagents = {}; allowed.subagents.model = legacyConfig.subagents.model; logger.warning('ℹ️ Using advanced parameter: subagents.model'); } return allowed; }; /** * Deep merge two objects (user config overrides defaults) * * @param {Object} target - Target object (defaults) * @param {Object} source - Source object (user overrides) * @returns {Object} Merged object */ const deepMerge = (target, source) => { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; }; /** * Get configuration instance * Loads once, caches result * * Usage: * import { getConfig } from './lib/config.js'; * const config = await getConfig(); * const maxFiles = config.analysis.maxFiles; */ let configInstance = null; const getConfig = async () => { if (!configInstance) { configInstance = await loadUserConfig(); } return configInstance; }; // Export async loader and constants export { getConfig, loadUserConfig, defaults, HARDCODED }; // Export a sync default for backwards compatibility (loads without preset) // NOTE: Returns HARDCODED merged with defaults export default deepMerge(HARDCODED, defaults);