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
JavaScript
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] = [];