UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

468 lines (397 loc) 13.9 kB
const fs = require('fs'); const path = require('path'); /** * Extracts and describes hooks from a settings.json file * @param {string} settingsPath - Path to the settings.json file * @returns {Array} Array of hook descriptions */ function getHooksFromSettings(settingsPath) { if (!fs.existsSync(settingsPath)) { return []; } try { const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); const hooks = []; if (settings.hooks) { // Process PreToolUse hooks if (settings.hooks.PreToolUse) { settings.hooks.PreToolUse.forEach((hookGroup, index) => { hookGroup.hooks.forEach((hook, hookIndex) => { const hookId = `pre-${index}-${hookIndex}`; const description = getHookDescription(hook, hookGroup.matcher, 'PreToolUse'); hooks.push({ id: hookId, type: 'PreToolUse', matcher: hookGroup.matcher, description, originalHook: hook, originalGroup: hookGroup, checked: true // Default to checked }); }); }); } // Process PostToolUse hooks if (settings.hooks.PostToolUse) { settings.hooks.PostToolUse.forEach((hookGroup, index) => { hookGroup.hooks.forEach((hook, hookIndex) => { const hookId = `post-${index}-${hookIndex}`; const description = getHookDescription(hook, hookGroup.matcher, 'PostToolUse'); hooks.push({ id: hookId, type: 'PostToolUse', matcher: hookGroup.matcher, description, originalHook: hook, originalGroup: hookGroup, checked: true // Default to checked }); }); }); } // Process Notification hooks if (settings.hooks.Notification) { settings.hooks.Notification.forEach((hookGroup, index) => { hookGroup.hooks.forEach((hook, hookIndex) => { const hookId = `notification-${index}-${hookIndex}`; const description = getHookDescription(hook, hookGroup.matcher, 'Notification'); hooks.push({ id: hookId, type: 'Notification', matcher: hookGroup.matcher, description, originalHook: hook, originalGroup: hookGroup, checked: false // Default to unchecked for notifications }); }); }); } // Process Stop hooks if (settings.hooks.Stop) { settings.hooks.Stop.forEach((hookGroup, index) => { hookGroup.hooks.forEach((hook, hookIndex) => { const hookId = `stop-${index}-${hookIndex}`; const description = getHookDescription(hook, hookGroup.matcher, 'Stop'); hooks.push({ id: hookId, type: 'Stop', matcher: hookGroup.matcher, description, originalHook: hook, originalGroup: hookGroup, checked: true // Default to checked }); }); }); } } return hooks; } catch (error) { console.error(`Error parsing settings file ${settingsPath}:`, error.message); return []; } } /** * Generates a human-readable description for a hook * @param {Object} hook - The hook object * @param {string} matcher - The matcher pattern * @param {string} type - The hook type * @returns {string} Human-readable description */ function getHookDescription(hook, matcher, type) { const command = hook.command || ''; // Extract key patterns for more specific descriptions if (command.includes('jq -r') && command.includes('bash-command-log')) { return 'Log all Bash commands for debugging'; } if (command.includes('console\\.log')) { return 'Block console.log statements in JS/TS files'; } if (command.includes('print(') && command.includes('py$')) { return 'Block print() statements in Python files'; } if (command.includes('puts\\|p ') && command.includes('rb$')) { return 'Block puts/p statements in Ruby files'; } if (command.includes('fmt.Print') && command.includes('go$')) { return 'Block fmt.Print statements in Go files'; } if (command.includes('println!') && command.includes('rs$')) { return 'Block println! macros in Rust files'; } if (command.includes('npm audit') || command.includes('pip-audit') || command.includes('bundle audit') || command.includes('cargo audit')) { return 'Security audit for dependencies'; } if (command.includes('prettier --write')) { return 'Auto-format JS/TS files with Prettier'; } if (command.includes('black') && command.includes('py$')) { return 'Auto-format Python files with Black'; } if (command.includes('rubocop -A') && command.includes('rb$')) { return 'Auto-format Ruby files with RuboCop'; } if (command.includes('rubocop') && command.includes('rb$') && !command.includes('-A')) { return 'Run Ruby linting with RuboCop'; } if (command.includes('brakeman')) { return 'Run Ruby security scan with Brakeman'; } if (command.includes('isort') && command.includes('py$')) { return 'Auto-sort Python imports with isort'; } if (command.includes('gofmt') && command.includes('go$')) { return 'Auto-format Go files with gofmt'; } if (command.includes('goimports')) { return 'Auto-format Go imports with goimports'; } if (command.includes('rustfmt') && command.includes('rs$')) { return 'Auto-format Rust files with rustfmt'; } if (command.includes('tsc --noEmit')) { return 'Run TypeScript type checking'; } if (command.includes('flake8') && !command.includes('git diff')) { return 'Run Python linting with flake8'; } if (command.includes('mypy')) { return 'Run Python type checking with mypy'; } if (command.includes('go vet') && !command.includes('git diff')) { return 'Run Go static analysis with go vet'; } if (command.includes('cargo check')) { return 'Run Rust compilation checks'; } if (command.includes('cargo clippy') && !command.includes('git diff')) { return 'Run Rust linting with clippy'; } if (command.includes('import \\* from')) { return 'Warn about wildcard imports'; } if (command.includes('jest') || command.includes('vitest')) { return 'Auto-run tests for modified files'; } if (command.includes('pytest')) { return 'Auto-run Python tests for modified files'; } if (command.includes('rspec')) { return 'Auto-run Ruby tests with RSpec'; } if (command.includes('go test')) { return 'Auto-run Go tests for modified files'; } if (command.includes('cargo test')) { return 'Auto-run Rust tests for modified files'; } if (command.includes('eslint') && command.includes('git diff')) { return 'Run ESLint on changed files'; } if (command.includes('flake8') && command.includes('git diff')) { return 'Run Python linting on changed files'; } if (command.includes('bandit')) { return 'Run Python security analysis'; } if (command.includes('go vet') && command.includes('git diff')) { return 'Run Go analysis on changed files'; } if (command.includes('staticcheck')) { return 'Run Go static analysis on changed files'; } if (command.includes('cargo clippy') && command.includes('git diff')) { return 'Run Rust linting on changed files'; } if (command.includes('bundlesize') || command.includes('webpack-bundle-analyzer')) { return 'Analyze bundle size impact'; } if (command.includes('notifications.log')) { return 'Log Claude Code notifications'; } // Generate description based on command analysis if (command.includes('eslint')) { return 'Run ESLint linting'; } else if (command.includes('prettier')) { return 'Format code with Prettier'; } else if (command.includes('tsc')) { return 'TypeScript type checking'; } else if (command.includes('jest') || command.includes('vitest')) { return 'Run tests automatically'; } else if (command.includes('audit')) { return 'Security audit for dependencies'; } else if (command.includes('bundlesize') || command.includes('bundle')) { return 'Bundle size analysis'; } else if (command.includes('console.log')) { return 'Detect console.log statements'; } else if (command.includes('import')) { return 'Import statement validation'; } else if (command.includes('log')) { return 'Logging functionality'; } else { // Fallback: use type and matcher const matcherDesc = matcher || 'all tools'; return `${type} hook for ${matcherDesc}`; } } /** * Gets hooks for a specific language * @param {string} language - The programming language * @returns {Array} Array of available hooks for the language */ function getHooksForLanguage(language) { const templateDir = path.join(__dirname, '../templates', language); const settingsPath = path.join(templateDir, '.claude', 'settings.json'); return getHooksFromSettings(settingsPath); } /** * Filters hooks based on user selection * @param {Object} originalSettings - Original settings object * @param {Array} selectedHookIds - Array of selected hook IDs * @param {Array} availableHooks - Array of available hooks * @returns {Object} Filtered settings object */ function filterHooksBySelection(originalSettings, selectedHookIds, availableHooks) { if (!originalSettings.hooks) { return originalSettings; } const filteredSettings = JSON.parse(JSON.stringify(originalSettings)); filteredSettings.hooks = {}; // Create a map of selected hooks for quick lookup const selectedHooks = new Map(); availableHooks.forEach(hook => { if (selectedHookIds.includes(hook.id)) { selectedHooks.set(hook.id, hook); } }); // Group selected hooks by type const hooksByType = { PreToolUse: [], PostToolUse: [], Notification: [], Stop: [] }; selectedHooks.forEach(hook => { if (hooksByType[hook.type]) { hooksByType[hook.type].push(hook); } }); // Rebuild hook structure Object.keys(hooksByType).forEach(type => { if (hooksByType[type].length > 0) { filteredSettings.hooks[type] = []; // Group hooks by matcher and originalGroup const groupMap = new Map(); hooksByType[type].forEach(hook => { const groupKey = `${hook.matcher}-${JSON.stringify(hook.originalGroup.matcher)}`; if (!groupMap.has(groupKey)) { groupMap.set(groupKey, { matcher: hook.originalGroup.matcher, hooks: [] }); } groupMap.get(groupKey).hooks.push(hook.originalHook); }); filteredSettings.hooks[type] = Array.from(groupMap.values()); } }); return filteredSettings; } /** * Extracts and describes MCPs from a .mcp.json file * @param {string} mcpPath - Path to the .mcp.json file * @returns {Array} Array of MCP descriptions */ function getMCPsFromFile(mcpPath) { if (!fs.existsSync(mcpPath)) { return []; } try { const mcpData = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); const mcps = []; if (mcpData.mcpServers) { Object.keys(mcpData.mcpServers).forEach((serverId) => { const server = mcpData.mcpServers[serverId]; mcps.push({ id: serverId, name: server.name || serverId, description: server.description || 'No description available', command: server.command, args: server.args || [], env: server.env || {}, originalServer: server, checked: getDefaultMCPSelection(serverId) // Default selection logic }); }); } return mcps; } catch (error) { console.error(`Error parsing MCP file ${mcpPath}:`, error.message); return []; } } /** * Determines default selection for MCP servers * @param {string} serverId - The MCP server ID * @returns {boolean} Whether the MCP should be selected by default */ function getDefaultMCPSelection(serverId) { // Default to checked for commonly useful MCPs const defaultSelected = [ 'filesystem', 'memory-bank', 'sequential-thinking', 'typescript-sdk', 'python-sdk', 'rust-sdk', 'go-sdk' ]; return defaultSelected.includes(serverId); } /** * Gets MCPs for a specific language * @param {string} language - The programming language * @returns {Array} Array of available MCPs for the language */ function getMCPsForLanguage(language) { const templateDir = path.join(__dirname, '../templates', language); const mcpPath = path.join(templateDir, '.mcp.json'); return getMCPsFromFile(mcpPath); } /** * Filters MCPs based on user selection * @param {Object} originalMCPData - Original MCP data object * @param {Array} selectedMCPIds - Array of selected MCP IDs * @param {Array} availableMCPs - Array of available MCPs * @returns {Object} Filtered MCP data object */ function filterMCPsBySelection(originalMCPData, selectedMCPIds, availableMCPs) { if (!originalMCPData.mcpServers) { return originalMCPData; } const filteredMCPData = { mcpServers: {} }; // Create a map of selected MCPs for quick lookup const selectedMCPs = new Map(); availableMCPs.forEach(mcp => { if (selectedMCPIds.includes(mcp.id)) { selectedMCPs.set(mcp.id, mcp); } }); // Add selected MCPs to filtered data selectedMCPs.forEach((mcp, mcpId) => { filteredMCPData.mcpServers[mcpId] = mcp.originalServer; }); return filteredMCPData; } module.exports = { getHooksFromSettings, getHooksForLanguage, filterHooksBySelection, getHookDescription, getMCPsFromFile, getMCPsForLanguage, filterMCPsBySelection };