UNPKG

claude-code-templates

Version:

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

1,281 lines (1,094 loc) • 139 kB
const inquirer = require('inquirer'); const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); const ora = require('ora'); const { detectProject } = require('./utils'); const { getTemplateConfig, TEMPLATES_CONFIG } = require('./templates'); const { createPrompts, interactivePrompts } = require('./prompts'); const { copyTemplateFiles, runPostInstallationValidation } = require('./file-operations'); const { getHooksForLanguage, getMCPsForLanguage } = require('./hook-scanner'); const { installAgents } = require('./agents'); const { runCommandStats } = require('./command-stats'); const { runHookStats } = require('./hook-stats'); const { runMCPStats } = require('./mcp-stats'); const { runAnalytics } = require('./analytics'); const { startChatsMobile } = require('./chats-mobile'); const { runHealthCheck } = require('./health-check'); const { runPluginDashboard } = require('./plugin-dashboard'); const { runSkillDashboard } = require('./skill-dashboard'); const { trackingService } = require('./tracking-service'); const { createGlobalAgent, listGlobalAgents, removeGlobalAgent, updateGlobalAgent } = require('./sdk/global-agent-manager'); const SessionSharing = require('./session-sharing'); const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer'); /** * Get platform-appropriate Python command candidates * Returns array of commands to try in order * @returns {string[]} Array of Python commands to try */ function getPlatformPythonCandidates() { if (process.platform === 'win32') { // Windows: Try py launcher (PEP 397) first, then python, then python3 return ['py', 'python', 'python3']; } else { // Unix/Linux/Mac: Try python3 first, then python return ['python3', 'python']; } } /** * Replace python3 commands with platform-appropriate Python command in configuration * Windows typically uses 'python' or 'py', while Unix/Linux uses 'python3' * @param {Object} config - Configuration object to process * @returns {Object} Processed configuration with platform-appropriate Python commands */ function replacePythonCommands(config) { if (!config || typeof config !== 'object') { return config; } // On Windows, replace python3 with python for better compatibility if (process.platform === 'win32') { const configString = JSON.stringify(config); const replacedString = configString.replace(/python3\s/g, 'python '); return JSON.parse(replacedString); } return config; } async function showMainMenu() { console.log(''); const initialChoice = await inquirer.prompt([{ type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'šŸ“Š Analytics Dashboard - Monitor your Claude Code usage and sessions', value: 'analytics', short: 'Analytics Dashboard' }, { name: 'šŸ’¬ Chats Mobile - AI-first mobile interface for conversations', value: 'chats', short: 'Chats Mobile' }, { name: 'šŸ¤– Agents Dashboard - View and analyze Claude conversations with agent tools', value: 'agents', short: 'Agents Dashboard' }, { name: 'āš™ļø Project Setup - Configure Claude Code for your project', value: 'setup', short: 'Project Setup' }, { name: 'šŸ” Health Check - Verify your Claude Code setup and configuration', value: 'health', short: 'Health Check' } ], default: 'analytics' }]); if (initialChoice.action === 'analytics') { console.log(chalk.blue('šŸ“Š Launching Claude Code Analytics Dashboard...')); trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'interactive_menu' }); await runAnalytics({}); return; } if (initialChoice.action === 'chats') { console.log(chalk.blue('šŸ’¬ Launching Claude Code Mobile Chats...')); trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'interactive_menu' }); await startChatsMobile({}); return; } if (initialChoice.action === 'agents') { console.log(chalk.blue('šŸ¤– Launching Claude Code Agents Dashboard...')); trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'interactive_menu' }); await runAnalytics({ openTo: 'agents' }); return; } if (initialChoice.action === 'health') { console.log(chalk.blue('šŸ” Running Health Check...')); const healthResult = await runHealthCheck(); // Track health check usage trackingService.trackHealthCheck({ setup_recommended: healthResult.runSetup, issues_found: healthResult.issues || 0 }); if (healthResult.runSetup) { console.log(chalk.blue('āš™ļø Starting Project Setup...')); // Continue with setup flow return await createClaudeConfig({}); } else { console.log(chalk.green('šŸ‘ Health check completed. Returning to main menu...')); return await showMainMenu(); } } // Continue with setup if user chose 'setup' console.log(chalk.blue('āš™ļø Setting up Claude Code configuration...')); return await createClaudeConfig({ setupFromMenu: true }); } async function createClaudeConfig(options = {}) { const targetDir = options.directory || process.cwd(); // Validate --tunnel usage if (options.tunnel && !options.analytics && !options.chats && !options.agents && !options.chatsMobile && !options['2025']) { console.log(chalk.red('āŒ Error: --tunnel can only be used with --analytics, --chats, --2025, or --chats-mobile')); console.log(chalk.yellow('šŸ’” Examples:')); console.log(chalk.gray(' cct --analytics --tunnel')); console.log(chalk.gray(' cct --chats --tunnel')); console.log(chalk.gray(' cct --2025 --tunnel')); console.log(chalk.gray(' cct --chats-mobile')); return; } // Handle Claude Code Studio launch if (options.studio) { await launchClaudeCodeStudio(options, targetDir); return; } // Handle sandbox execution FIRST (before individual components) if (options.sandbox) { trackingService.trackCommandExecution('sandbox', { provider: options.sandbox, hasPrompt: !!options.prompt }); await executeSandbox(options, targetDir); return; } // Handle multiple components installation (new approach) if (options.agent || options.command || options.mcp || options.setting || options.hook || options.skill) { // If --workflow is used with components, treat it as YAML if (options.workflow) { options.yaml = options.workflow; } await installMultipleComponents(options, targetDir); return; } // Handle workflow installation (hash-based) if (options.workflow) { await installWorkflow(options.workflow, targetDir, options); return; } // Handle global agent creation if (options.createAgent) { await createGlobalAgent(options.createAgent, options); return; } // Handle global agent listing if (options.listAgents) { await listGlobalAgents(options); return; } // Handle global agent removal if (options.removeAgent) { await removeGlobalAgent(options.removeAgent, options); return; } // Handle global agent update if (options.updateAgent) { await updateGlobalAgent(options.updateAgent, options); return; } // (Sandbox execution handled earlier) // Handle command stats analysis (both singular and plural) if (options.commandStats || options.commandsStats) { trackingService.trackCommandExecution('command-stats'); await runCommandStats(options); return; } // Handle hook stats analysis (both singular and plural) if (options.hookStats || options.hooksStats) { trackingService.trackCommandExecution('hook-stats'); await runHookStats(options); return; } // Handle MCP stats analysis (both singular and plural) if (options.mcpStats || options.mcpsStats) { trackingService.trackCommandExecution('mcp-stats'); await runMCPStats(options); return; } // Handle analytics dashboard if (options.analytics) { trackingService.trackCommandExecution('analytics', { tunnel: options.tunnel || false }); trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'command_line' }); await runAnalytics(options); return; } // Handle 2025 Year in Review dashboard if (options['2025']) { trackingService.trackCommandExecution('2025-year-in-review'); trackingService.trackAnalyticsDashboard({ page: '2025', source: 'command_line' }); await runAnalytics({ ...options, openTo: '2025' }); return; } // Handle plugin dashboard if (options.plugins) { trackingService.trackCommandExecution('plugins'); trackingService.trackAnalyticsDashboard({ page: 'plugins', source: 'command_line' }); await runPluginDashboard(options); return; } // Handle skills dashboard if (options.skillsManager) { trackingService.trackCommandExecution('skills-manager'); trackingService.trackAnalyticsDashboard({ page: 'skills-manager', source: 'command_line' }); await runSkillDashboard(options); return; } // Handle chats dashboard (now points to mobile chats interface) if (options.chats) { trackingService.trackCommandExecution('chats', { tunnel: options.tunnel || false }); trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'command_line' }); await startChatsMobile(options); return; } // Handle agents dashboard (separate from chats) if (options.agents) { trackingService.trackCommandExecution('agents', { tunnel: options.tunnel || false }); trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'command_line' }); await runAnalytics({ ...options, openTo: 'agents' }); return; } // Handle mobile chats interface if (options.chatsMobile) { trackingService.trackCommandExecution('chats-mobile', { tunnel: options.tunnel || false }); trackingService.trackAnalyticsDashboard({ page: 'chats-mobile', source: 'command_line' }); await startChatsMobile(options); return; } // Handle session clone (download and import shared session) if (options.cloneSession) { console.log(chalk.blue('šŸ“„ Cloning shared Claude Code session...')); try { const os = require('os'); const homeDir = os.homedir(); const claudeDir = path.join(homeDir, '.claude'); // Initialize ConversationAnalyzer and SessionSharing const conversationAnalyzer = new ConversationAnalyzer(claudeDir); const sessionSharing = new SessionSharing(conversationAnalyzer); // Clone the session (cloneSession method handles all console output) const result = await sessionSharing.cloneSession(options.cloneSession, { projectPath: options.directory || process.cwd() }); // Track session clone trackingService.trackAnalyticsDashboard({ page: 'session-clone', source: 'command_line', success: true }); } catch (error) { console.error(chalk.red('āŒ Failed to clone session:'), error.message); // Track failed clone trackingService.trackAnalyticsDashboard({ page: 'session-clone', source: 'command_line', success: false, error: error.message }); process.exit(1); } return; } // Handle health check let shouldRunSetup = false; if (options.healthCheck || options.health || options.check || options.verify) { trackingService.trackCommandExecution('health-check'); const healthResult = await runHealthCheck(); // Track health check usage trackingService.trackHealthCheck({ setup_recommended: healthResult.runSetup, issues_found: healthResult.issues || 0, source: 'command_line' }); if (healthResult.runSetup) { console.log(chalk.blue('āš™ļø Starting Project Setup...')); shouldRunSetup = true; } else { console.log(chalk.green('šŸ‘ Health check completed. Returning to main menu...')); return await showMainMenu(); } } // Add initial choice prompt (only if no specific options are provided and not continuing from health check or menu) if (!shouldRunSetup && !options.setupFromMenu && !options.yes && !options.language && !options.framework && !options.dryRun) { return await showMainMenu(); } else { console.log(chalk.blue('šŸš€ Setting up Claude Code configuration...')); } console.log(chalk.gray(`Target directory: ${targetDir}`)); // Detect existing project const spinner = ora('Detecting project type...').start(); const projectInfo = await detectProject(targetDir); spinner.succeed('Project detection complete'); let config; if (options.yes) { // Use defaults - prioritize --template over --language for backward compatibility const selectedLanguage = options.template || options.language || projectInfo.detectedLanguage || 'common'; // Check if selected language is coming soon if (selectedLanguage && TEMPLATES_CONFIG[selectedLanguage] && TEMPLATES_CONFIG[selectedLanguage].comingSoon) { console.log(chalk.red(`āŒ ${selectedLanguage} is not available yet. Coming soon!`)); console.log(chalk.yellow('Available languages: common, javascript-typescript, python')); return; } const availableHooks = getHooksForLanguage(selectedLanguage); const defaultHooks = availableHooks.filter(hook => hook.checked).map(hook => hook.id); const availableMCPs = getMCPsForLanguage(selectedLanguage); const defaultMCPs = availableMCPs.filter(mcp => mcp.checked).map(mcp => mcp.id); config = { language: selectedLanguage, framework: options.framework || projectInfo.detectedFramework || 'none', features: [], hooks: defaultHooks, mcps: defaultMCPs }; } else { // Interactive prompts with back navigation config = await interactivePrompts(projectInfo, options); } // Check if user confirmed the setup if (config.confirm === false) { console.log(chalk.yellow('ā¹ļø Setup cancelled by user.')); return; } // Handle analytics option from onboarding if (config.analytics) { console.log(chalk.blue('šŸ“Š Launching Claude Code Analytics Dashboard...')); await runAnalytics(options); return; } // Get template configuration const templateConfig = getTemplateConfig(config); // Add selected hooks to template config if (config.hooks) { templateConfig.selectedHooks = config.hooks; templateConfig.language = config.language; // Ensure language is available for hook filtering } // Add selected MCPs to template config if (config.mcps) { templateConfig.selectedMCPs = config.mcps; templateConfig.language = config.language; // Ensure language is available for MCP filtering } // Install selected agents if (config.agents && config.agents.length > 0) { console.log(chalk.blue('šŸ¤– Installing Claude Code agents...')); await installAgents(config.agents, targetDir); } if (options.dryRun) { console.log(chalk.yellow('šŸ” Dry run - showing what would be copied:')); templateConfig.files.forEach(file => { console.log(chalk.gray(` - ${file.source} → ${file.destination}`)); }); return; } // Copy template files const copySpinner = ora('Copying template files...').start(); try { const result = await copyTemplateFiles(templateConfig, targetDir, options); if (result === false) { copySpinner.info('Setup cancelled by user'); return; // Exit early if user cancelled } copySpinner.succeed('Template files copied successfully'); } catch (error) { copySpinner.fail('Failed to copy template files'); throw error; } // Show success message console.log(chalk.green('āœ… Claude Code configuration setup complete!')); console.log(chalk.cyan('šŸ“š Next steps:')); console.log(chalk.white(' 1. Review the generated CLAUDE.md file')); console.log(chalk.white(' 2. Customize the configuration for your project')); console.log(chalk.white(' 3. Start using Claude Code with: claude')); console.log(''); console.log(chalk.blue('🌐 View all available templates at: https://aitmpl.com/')); console.log(chalk.blue('šŸ“– Read the complete documentation at: https://docs.aitmpl.com/')); if (config.language !== 'common') { console.log(chalk.yellow(`šŸ’” Language-specific features for ${config.language} have been configured`)); } if (config.framework !== 'none') { console.log(chalk.yellow(`šŸŽÆ Framework-specific commands for ${config.framework} are available`)); } if (config.hooks && config.hooks.length > 0) { console.log(chalk.magenta(`šŸ”§ ${config.hooks.length} automation hooks have been configured`)); } if (config.mcps && config.mcps.length > 0) { console.log(chalk.blue(`šŸ”§ ${config.mcps.length} MCP servers have been configured`)); } // Track successful template installation if (!options.agent && !options.command && !options.mcp) { trackingService.trackTemplateInstallation(config.language, config.framework, { installation_method: options.setupFromMenu ? 'interactive_menu' : 'command_line', dry_run: options.dryRun || false, hooks_count: config.hooks ? config.hooks.length : 0, mcps_count: config.mcps ? config.mcps.length : 0, project_detected: !!options.detectedProject }); } // Run post-installation validation if (!options.dryRun) { await runPostInstallationValidation(targetDir, templateConfig); } // Handle prompt execution if provided (but not in sandbox mode) if (options.prompt && !options.sandbox) { await handlePromptExecution(options.prompt, targetDir); } } // Individual component installation functions async function installIndividualAgent(agentName, targetDir, options) { console.log(chalk.blue(`šŸ¤– Installing agent: ${agentName}`)); try { // Support both category/agent-name and direct agent-name formats let githubUrl; if (agentName.includes('/')) { // Category/agent format: deep-research-team/academic-researcher githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`; } else { // Direct agent format: api-security-audit githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`; } console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`)); const response = await fetch(githubUrl); if (!response.ok) { if (response.status === 404) { console.log(chalk.red(`āŒ Agent "${agentName}" not found`)); await showAvailableAgents(); return; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const agentContent = await response.text(); // Create .claude/agents directory if it doesn't exist const agentsDir = path.join(targetDir, '.claude', 'agents'); await fs.ensureDir(agentsDir); // Write the agent file - always to flat .claude/agents directory let fileName; if (agentName.includes('/')) { const [category, filename] = agentName.split('/'); fileName = filename; // Extract just the filename, ignore category for installation } else { fileName = agentName; } const targetFile = path.join(agentsDir, `${fileName}.md`); await fs.writeFile(targetFile, agentContent, 'utf8'); if (!options.silent) { console.log(chalk.green(`āœ… Agent "${agentName}" installed successfully!`)); console.log(chalk.cyan(`šŸ“ Installed to: ${path.relative(targetDir, targetFile)}`)); console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`)); } // Track successful agent installation trackingService.trackDownload('agent', agentName, { installation_type: 'individual_component', target_directory: path.relative(process.cwd(), targetDir), source: 'github_main' }); return true; } catch (error) { console.log(chalk.red(`āŒ Error installing agent: ${error.message}`)); return false; } } async function installIndividualCommand(commandName, targetDir, options) { console.log(chalk.blue(`⚔ Installing command: ${commandName}`)); try { // Support both category/command-name and direct command-name formats let githubUrl; if (commandName.includes('/')) { // Category/command format: security/vulnerability-scan githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/commands/${commandName}.md`; } else { // Direct command format: check-file githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/commands/${commandName}.md`; } console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`)); const response = await fetch(githubUrl); if (!response.ok) { if (response.status === 404) { console.log(chalk.red(`āŒ Command "${commandName}" not found`)); console.log(chalk.yellow('Available commands: check-file, generate-tests')); return; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const commandContent = await response.text(); // Create .claude/commands directory if it doesn't exist const commandsDir = path.join(targetDir, '.claude', 'commands'); await fs.ensureDir(commandsDir); // Write the command file - always to flat .claude/commands directory let fileName; if (commandName.includes('/')) { const [category, filename] = commandName.split('/'); fileName = filename; // Extract just the filename, ignore category for installation } else { fileName = commandName; } const targetFile = path.join(commandsDir, `${fileName}.md`); await fs.writeFile(targetFile, commandContent, 'utf8'); if (!options.silent) { console.log(chalk.green(`āœ… Command "${commandName}" installed successfully!`)); console.log(chalk.cyan(`šŸ“ Installed to: ${path.relative(targetDir, targetFile)}`)); console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`)); } // Track successful command installation trackingService.trackDownload('command', commandName, { installation_type: 'individual_command', target_directory: path.relative(process.cwd(), targetDir), source: 'github_main' }); return true; } catch (error) { console.log(chalk.red(`āŒ Error installing command: ${error.message}`)); return false; } } async function installIndividualMCP(mcpName, targetDir, options) { console.log(chalk.blue(`šŸ”Œ Installing MCP: ${mcpName}`)); try { // Support both category/mcp-name and direct mcp-name formats let githubUrl; if (mcpName.includes('/')) { // Category/mcp format: database/mysql-integration githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/mcps/${mcpName}.json`; } else { // Direct mcp format: web-fetch githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/mcps/${mcpName}.json`; } console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`)); const response = await fetch(githubUrl); if (!response.ok) { if (response.status === 404) { console.log(chalk.red(`āŒ MCP "${mcpName}" not found`)); console.log(chalk.yellow('Available MCPs: web-fetch, filesystem-access, github-integration, memory-integration, mysql-integration, postgresql-integration, deepgraph-react, deepgraph-nextjs, deepgraph-typescript, deepgraph-vue')); return; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const mcpConfigText = await response.text(); const mcpConfig = JSON.parse(mcpConfigText); // Remove description field from each MCP server before merging if (mcpConfig.mcpServers) { for (const serverName in mcpConfig.mcpServers) { if (mcpConfig.mcpServers[serverName] && typeof mcpConfig.mcpServers[serverName] === 'object') { delete mcpConfig.mcpServers[serverName].description; } } } // Check if .mcp.json exists in target directory const targetMcpFile = path.join(targetDir, '.mcp.json'); let existingConfig = {}; if (await fs.pathExists(targetMcpFile)) { existingConfig = await fs.readJson(targetMcpFile); console.log(chalk.yellow('šŸ“ Existing .mcp.json found, merging configurations...')); } // Merge configurations with deep merge for mcpServers const mergedConfig = { ...existingConfig, ...mcpConfig }; // Deep merge mcpServers specifically to avoid overwriting existing servers if (existingConfig.mcpServers && mcpConfig.mcpServers) { mergedConfig.mcpServers = { ...existingConfig.mcpServers, ...mcpConfig.mcpServers }; } // Write the merged configuration await fs.writeJson(targetMcpFile, mergedConfig, { spaces: 2 }); if (!options.silent) { console.log(chalk.green(`āœ… MCP "${mcpName}" installed successfully!`)); console.log(chalk.cyan(`šŸ“ Configuration merged into: ${path.relative(targetDir, targetMcpFile)}`)); console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`)); } // Track successful MCP installation trackingService.trackDownload('mcp', mcpName, { installation_type: 'individual_mcp', merged_with_existing: existingConfig !== null, servers_count: Object.keys(mergedConfig.mcpServers || {}).length, source: 'github_main' }); return true; } catch (error) { console.log(chalk.red(`āŒ Error installing MCP: ${error.message}`)); return false; } } async function installIndividualSetting(settingName, targetDir, options) { console.log(chalk.blue(`āš™ļø Installing setting: ${settingName}`)); try { // Support both category/setting-name and direct setting-name formats let githubUrl; if (settingName.includes('/')) { // Category/setting format: permissions/allow-npm-commands githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/settings/${settingName}.json`; } else { // Direct setting format: allow-npm-commands githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/settings/${settingName}.json`; } console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`)); const response = await fetch(githubUrl); if (!response.ok) { if (response.status === 404) { console.log(chalk.red(`āŒ Setting "${settingName}" not found`)); console.log(chalk.yellow('Available settings: enable-telemetry, disable-telemetry, allow-npm-commands, deny-sensitive-files, use-sonnet, use-haiku, retention-7-days, retention-90-days')); console.log(chalk.yellow('Available statuslines: statusline/context-monitor')); return; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const settingConfigText = await response.text(); let settingConfig = JSON.parse(settingConfigText); // Replace python3 with platform-appropriate command for Windows compatibility settingConfig = replacePythonCommands(settingConfig); // Check if there are additional files to download (e.g., Python scripts) const additionalFiles = {}; // For statusline settings, check if there's a corresponding Python file if (settingName.includes('statusline/')) { const pythonFileName = settingName.split('/')[1] + '.py'; const pythonUrl = githubUrl.replace('.json', '.py'); try { console.log(chalk.gray(`šŸ“„ Downloading Python script: ${pythonFileName}...`)); const pythonResponse = await fetch(pythonUrl); if (pythonResponse.ok) { const pythonContent = await pythonResponse.text(); additionalFiles['.claude/scripts/' + pythonFileName] = { content: pythonContent, executable: true }; } } catch (error) { console.log(chalk.yellow(`āš ļø Could not download Python script: ${error.message}`)); } } // Extract and handle additional files before removing them from config const configFiles = settingConfig.files || {}; // Merge downloaded files with config files Object.assign(additionalFiles, configFiles); // Remove description and files fields before merging if (settingConfig && typeof settingConfig === 'object') { delete settingConfig.description; delete settingConfig.files; } // Use shared locations if provided (batch mode), otherwise ask user let installLocations = options.sharedInstallLocations || ['local']; // default to local settings if (!options.silent && !options.sharedInstallLocations) { const inquirer = require('inquirer'); const { selectedLocations } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedLocations', message: 'Where would you like to install this setting? (Select one or more)', choices: [ { name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects', value: 'user' }, { name: 'šŸ“ Project settings (.claude/settings.json) - Shared with team', value: 'project' }, { name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed', value: 'local', checked: true // Default selection }, { name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)', value: 'enterprise' } ], validate: function(answer) { if (answer.length < 1) { return 'You must choose at least one installation location.'; } return true; } }]); installLocations = selectedLocations; } // Install the setting in each selected location let successfulInstallations = 0; for (const installLocation of installLocations) { console.log(chalk.blue(`\nšŸ“ Installing "${settingName}" in ${installLocation} settings...`)); let currentTargetDir = targetDir; let settingsFile = 'settings.local.json'; // default if (installLocation === 'user') { const os = require('os'); currentTargetDir = os.homedir(); settingsFile = 'settings.json'; } else if (installLocation === 'project') { settingsFile = 'settings.json'; } else if (installLocation === 'local') { settingsFile = 'settings.local.json'; } else if (installLocation === 'enterprise') { const os = require('os'); const platform = os.platform(); if (platform === 'darwin') { // macOS currentTargetDir = '/Library/Application Support/ClaudeCode'; settingsFile = 'managed-settings.json'; } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) { // Linux and WSL currentTargetDir = '/etc/claude-code'; settingsFile = 'managed-settings.json'; } else if (platform === 'win32') { // Windows currentTargetDir = 'C:\\ProgramData\\ClaudeCode'; settingsFile = 'managed-settings.json'; } else { console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.')); const os = require('os'); currentTargetDir = os.homedir(); settingsFile = 'settings.json'; } console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`)); console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`)); } // Determine target directory and file based on selection const claudeDir = path.join(currentTargetDir, '.claude'); const targetSettingsFile = path.join(claudeDir, settingsFile); let existingConfig = {}; // For enterprise settings, create directory structure directly (not under .claude) if (settingsFile === 'managed-settings.json') { // Ensure enterprise directory exists (requires admin privileges) try { await fs.ensureDir(currentTargetDir); } catch (error) { console.log(chalk.red(`āŒ Failed to create enterprise directory: ${error.message}`)); console.log(chalk.yellow('šŸ’” Try running with administrator privileges or choose a different installation location.')); continue; // Skip this location and continue with others } } else { // Ensure .claude directory exists for regular settings await fs.ensureDir(claudeDir); } // Read existing configuration const actualTargetFile = settingsFile === 'managed-settings.json' ? path.join(currentTargetDir, settingsFile) : targetSettingsFile; if (await fs.pathExists(actualTargetFile)) { existingConfig = await fs.readJson(actualTargetFile); console.log(chalk.yellow(`šŸ“ Existing ${settingsFile} found, merging configurations...`)); } // Check for conflicts before merging const conflicts = []; // Check for conflicting environment variables if (existingConfig.env && settingConfig.env) { Object.keys(settingConfig.env).forEach(key => { if (existingConfig.env[key] && existingConfig.env[key] !== settingConfig.env[key]) { conflicts.push(`Environment variable "${key}" (current: "${existingConfig.env[key]}", new: "${settingConfig.env[key]}")`); } }); } // Check for conflicting top-level settings Object.keys(settingConfig).forEach(key => { if (key !== 'permissions' && key !== 'env' && key !== 'hooks' && existingConfig[key] !== undefined && JSON.stringify(existingConfig[key]) !== JSON.stringify(settingConfig[key])) { // For objects, just indicate the setting name without showing the complex values if (typeof existingConfig[key] === 'object' && existingConfig[key] !== null && typeof settingConfig[key] === 'object' && settingConfig[key] !== null) { conflicts.push(`Setting "${key}" (will be overwritten with new configuration)`); } else { conflicts.push(`Setting "${key}" (current: "${existingConfig[key]}", new: "${settingConfig[key]}")`); } } }); // Ask user about conflicts if any exist if (conflicts.length > 0) { console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing setting "${settingName}" in ${installLocation}:`)); conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`))); const inquirer = require('inquirer'); const { shouldOverwrite } = await inquirer.prompt([{ type: 'confirm', name: 'shouldOverwrite', message: `Do you want to overwrite the existing configuration in ${installLocation}?`, default: false }]); if (!shouldOverwrite) { console.log(chalk.yellow(`ā¹ļø Installation of setting "${settingName}" in ${installLocation} cancelled by user.`)); continue; // Skip this location and continue with others } } // Deep merge configurations const mergedConfig = { ...existingConfig, ...settingConfig }; // Deep merge specific sections (only if no conflicts or user approved overwrite) if (existingConfig.permissions && settingConfig.permissions) { mergedConfig.permissions = { ...existingConfig.permissions, ...settingConfig.permissions }; // Merge arrays for allow, deny, ask (no conflicts here, just merge) ['allow', 'deny', 'ask'].forEach(key => { if (existingConfig.permissions[key] && settingConfig.permissions[key]) { mergedConfig.permissions[key] = [ ...new Set([...existingConfig.permissions[key], ...settingConfig.permissions[key]]) ]; } }); } if (existingConfig.env && settingConfig.env) { mergedConfig.env = { ...existingConfig.env, ...settingConfig.env }; } if (existingConfig.hooks && settingConfig.hooks) { mergedConfig.hooks = { ...existingConfig.hooks, ...settingConfig.hooks }; } // Write the merged configuration await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 }); // Install additional files if any exist if (Object.keys(additionalFiles).length > 0) { console.log(chalk.blue(`šŸ“„ Installing ${Object.keys(additionalFiles).length} additional file(s)...`)); for (const [filePath, fileConfig] of Object.entries(additionalFiles)) { try { // Resolve tilde (~) to home directory const resolvedFilePath = filePath.startsWith('~') ? path.join(require('os').homedir(), filePath.slice(1)) : path.resolve(currentTargetDir, filePath); // Ensure directory exists await fs.ensureDir(path.dirname(resolvedFilePath)); // Write file content await fs.writeFile(resolvedFilePath, fileConfig.content, 'utf8'); // Make file executable if specified if (fileConfig.executable) { await fs.chmod(resolvedFilePath, 0o755); console.log(chalk.gray(`šŸ”§ Made executable: ${resolvedFilePath}`)); } console.log(chalk.green(`āœ… File installed: ${resolvedFilePath}`)); } catch (fileError) { console.log(chalk.red(`āŒ Failed to install file ${filePath}: ${fileError.message}`)); } } } if (!options.silent) { console.log(chalk.green(`āœ… Setting "${settingName}" installed successfully in ${installLocation}!`)); console.log(chalk.cyan(`šŸ“ Configuration merged into: ${actualTargetFile}`)); console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`)); } // Track successful setting installation for this location trackingService.trackDownload('setting', settingName, { installation_type: 'individual_setting', installation_location: installLocation, merged_with_existing: Object.keys(existingConfig).length > 0, source: 'github_main' }); // Increment successful installations counter successfulInstallations++; } // Summary after all installations if (!options.silent) { if (successfulInstallations === installLocations.length) { console.log(chalk.green(`\nšŸŽ‰ Setting "${settingName}" successfully installed in ${successfulInstallations} location(s)!`)); } else { console.log(chalk.yellow(`\nāš ļø Setting "${settingName}" installed in ${successfulInstallations} of ${installLocations.length} location(s).`)); const failedCount = installLocations.length - successfulInstallations; console.log(chalk.red(`āŒ ${failedCount} installation(s) failed due to permission or other errors.`)); } } return successfulInstallations; } catch (error) { console.log(chalk.red(`āŒ Error installing setting: ${error.message}`)); return 0; } } async function installIndividualHook(hookName, targetDir, options) { console.log(chalk.blue(`šŸŖ Installing hook: ${hookName}`)); try { // Support both category/hook-name and direct hook-name formats let githubUrl; if (hookName.includes('/')) { // Category/hook format: pre-tool/backup-before-edit githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/hooks/${hookName}.json`; } else { // Direct hook format: backup-before-edit githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/hooks/${hookName}.json`; } console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`)); const response = await fetch(githubUrl); if (!response.ok) { if (response.status === 404) { console.log(chalk.red(`āŒ Hook "${hookName}" not found`)); console.log(chalk.yellow('Available hooks: notify-before-bash, format-python-files, format-javascript-files, git-add-changes, backup-before-edit, run-tests-after-changes')); return; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const hookConfigText = await response.text(); let hookConfig = JSON.parse(hookConfigText); // Replace python3 with platform-appropriate command for Windows compatibility hookConfig = replacePythonCommands(hookConfig); // Check if there are additional files to download (e.g., Python scripts for hooks) const additionalFiles = {}; // Check if there's a corresponding Python file for ANY hook const pythonUrl = githubUrl.replace('.json', '.py'); const hookBaseName = hookName.includes('/') ? hookName.split('/').pop() : hookName; try { console.log(chalk.gray(`šŸ“„ Checking for additional Python script...`)); const pythonResponse = await fetch(pythonUrl); if (pythonResponse.ok) { const pythonContent = await pythonResponse.text(); additionalFiles[`.claude/hooks/${hookBaseName}.py`] = { content: pythonContent, executable: true }; console.log(chalk.green(`āœ“ Found Python script: ${hookBaseName}.py`)); } } catch (error) { // Python file is optional, silently continue if not found } // Remove description field before merging if (hookConfig && typeof hookConfig === 'object') { delete hookConfig.description; } // Use shared locations if provided (batch mode), otherwise ask user let installLocations = options.sharedInstallLocations || ['local']; // default to local settings if (!options.silent && !options.sharedInstallLocations) { const inquirer = require('inquirer'); const { selectedLocations } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedLocations', message: 'Where would you like to install this hook? (Select one or more)', choices: [ { name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects', value: 'user' }, { name: 'šŸ“ Project settings (.claude/settings.json) - Shared with team', value: 'project' }, { name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed', value: 'local', checked: true // Default selection }, { name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)', value: 'enterprise' } ], validate: function(answer) { if (answer.length < 1) { return 'You must choose at least one installation location.'; } return true; } }]); installLocations = selectedLocations; } // Install the hook in each selected location let successfulInstallations = 0; for (const installLocation of installLocations) { console.log(chalk.blue(`\nšŸ“ Installing "${hookName}" in ${installLocation} settings...`)); let currentTargetDir = targetDir; let settingsFile = 'settings.local.json'; // default if (installLocation === 'user') { const os = require('os'); currentTargetDir = os.homedir(); settingsFile = 'settings.json'; } else if (installLocation === 'project') { settingsFile = 'settings.json'; } else if (installLocation === 'local') { settingsFile = 'settings.local.json'; } else if (installLocation === 'enterprise') { const os = require('os'); const platform = os.platform(); if (platform === 'darwin') { // macOS currentTargetDir = '/Library/Application Support/ClaudeCode'; settingsFile = 'managed-settings.json'; } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) { // Linux and WSL currentTargetDir = '/etc/claude-code'; settingsFile = 'managed-settings.json'; } else if (platform === 'win32') { // Windows currentTargetDir = 'C:\\ProgramData\\ClaudeCode'; settingsFile = 'managed-settings.json'; } else { console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.')); const os = require('os'); currentTargetDir = os.homedir(); settingsFile = 'settings.json'; } console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`)); console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`)); } // Determine target directory and file based on selection const claudeDir = path.join(currentTargetDir, '.claude'); const targetSettingsFile = path.join(claudeDir, settingsFile); let existingConfig = {}; // For enterprise settings, create directory structure directly (not under .claude) if (settingsFile === 'managed-settings.json') { // Ensure enterprise directory exists (requires admin privileges) try { await fs.ensureDir(currentTargetDir); } catch (error) { console.log(chalk.red(`āŒ Failed to create enterprise directory: ${error.message}`)); console.log(chalk.yellow('šŸ’” Try running with administrator privileges or choose a different installation location.')); continue; // Skip this location and continue with others } } else { // Ensure .claude directory exists for regular settings await fs.ensureDir(claudeDir); } // Read existing configuration const actualTargetFile = settingsFile === 'managed-settings.json' ? path.join(currentTargetDir, settingsFile) : targetSettingsFile; if (await fs.pathExists(actualTargetFile)) { existingConfig = await fs.readJson(actualTargetFile); console.log(chalk.yellow(`šŸ“ Existing ${settingsFile} found, merging hook configurations...`)); } // Check for conflicts before merging (simplified for new array format) const conflicts = []; // For the new array format, we'll allow appending rather than conflict detection // This is because Claude Code's array format naturally supports multiple hooks // Conflicts are less likely and generally hooks can coexist // Ask user about conflicts if any exist if (conflicts.length > 0) { console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing hook "${hookName}" in ${installLocation}:`)); conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`))); const inquirer = require('inquirer'); const { shouldOverwrite } = await inquirer.prompt([{ type: 'confirm', name: 'shouldOverwrite', message: `Do you want to overwrite the existing hook configuration in ${installLocation}?`, default: false }]); if (!shouldOverwrite) { console.log(chalk.yellow(`ā¹ļø Installation of hook "${hookName}" in ${installLocation} cancelled by user.`)); continue; // Skip this location and continue with others } } // Deep merge configurations with proper hook array structure const mergedConfig = { ...existingConfig }; // Initialize hooks structure if it doesn't exist if (!mergedConfig.hooks) { mergedConfig.hooks = {}; } // Merge hook configurations properly (Claude Code expects arrays) if (hookConfig.hooks) { Object.keys(hookConfig.hooks).forEach(hookType => { if (!mergedConfig.hooks[hookType]) { // If hook type doesn't exist, just copy the array mergedConfig.hooks[hookType] = hookConfig.hooks[hookType]; } else { // If hook type exists, append to the array (Claude Code format) if (Array.isArray(hookConfig.hooks[hookType])) { // New format: array of hook objects if (!Array.isArray(mergedConfig.hooks[hookType])) { // Convert old format to new format mergedConfig.hooks[hookType] = []; } // Append new hooks to existing array mergedConfig.hooks[hookType] = mergedConfig.hooks[hookType].concat(hookConfig.hooks[hookType]); } else { // Old format compatibility: convert to new format console.log(chalk.yellow(`āš ļø Converting old hook format to new Claude Code format for ${hookType}`)); if (!Array.isArray(mergedConfig.hooks[hookType])) { mergedConfig.hooks[hookType] = [];