UNPKG

claude-switcher

Version:

Cross-platform CLI tool for switching between different Claude AI model configurations. Supports automatic backup, rollback, and multi-platform configuration management for Claude API integrations.

344 lines (343 loc) 15.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigManager = void 0; const path = __importStar(require("path")); const os = __importStar(require("os")); const FileOperations_1 = require("../utils/FileOperations"); class ConfigManager { constructor(claudeDir) { this.claudeDir = claudeDir || ConfigManager.detectClaudeConfigDir(); this.settingsFile = path.join(this.claudeDir, 'settings.json'); } static detectClaudeConfigDir() { const platform = os.platform(); const homeDir = os.homedir(); switch (platform) { case 'win32': return path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), '.claude'); case 'darwin': return path.join(homeDir, '.claude'); case 'linux': const configDir = process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'); return path.join(configDir, 'claude'); default: return path.join(homeDir, '.claude'); } } static async findClaudeConfigDir() { const platform = os.platform(); const homeDir = os.homedir(); const possiblePaths = []; switch (platform) { case 'win32': possiblePaths.push(path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), '.claude'), path.join(homeDir, '.claude'), path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), '.claude')); break; case 'darwin': possiblePaths.push(path.join(homeDir, '.claude'), path.join(homeDir, 'Library', 'Application Support', 'claude'), path.join(homeDir, 'Library', 'Preferences', 'claude')); break; case 'linux': const configDir = process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'); possiblePaths.push(path.join(configDir, 'claude'), path.join(homeDir, '.claude'), path.join(homeDir, '.local', 'share', 'claude')); break; default: possiblePaths.push(path.join(homeDir, '.claude'), path.join(homeDir, '.config', 'claude')); break; } for (const claudePath of possiblePaths) { if (await FileOperations_1.FileOperations.fileExists(claudePath)) { try { const files = await FileOperations_1.FileOperations.getFileList(claudePath); const hasSettingsFiles = files.some(file => file === 'settings.json' || file.match(/^settings_.*\.json$/)); if (hasSettingsFiles) { return claudePath; } } catch { continue; } } } throw new Error(`Claude configuration directory not found. Searched paths: ${possiblePaths.join(', ')}`); } static async createWithAutoDetection() { try { const claudeDir = await ConfigManager.findClaudeConfigDir(); return new ConfigManager(claudeDir); } catch { const defaultDir = ConfigManager.detectClaudeConfigDir(); return new ConfigManager(defaultDir); } } async getAvailableConfigs() { try { if (!(await FileOperations_1.FileOperations.fileExists(this.claudeDir))) { throw new Error(`Claude directory not found: ${this.claudeDir}`); } const configPattern = /^settings_(.+)\.json$/; const files = await FileOperations_1.FileOperations.getFileList(this.claudeDir, configPattern); const configs = []; let currentConfig = null; try { currentConfig = await this.getCurrentConfig(); } catch { currentConfig = null; } for (const fileName of files) { const match = fileName.match(configPattern); if (match) { const configName = match[1]; const filePath = path.join(this.claudeDir, fileName); try { const configData = await this.loadConfigFile(filePath); const model = this.extractModelName(configData); configs.push({ name: configName, fileName, displayName: this.formatDisplayName(configName), model, isActive: currentConfig ? this.compareConfigs(configData, currentConfig) : false, filePath }); } catch (error) { console.warn(`Warning: Could not load config ${fileName}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } return configs.sort((a, b) => a.name.localeCompare(b.name)); } catch (error) { throw new Error(`Failed to get available configs: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getCurrentConfig() { try { if (!(await FileOperations_1.FileOperations.fileExists(this.settingsFile))) { return null; } const configData = await FileOperations_1.FileOperations.readJsonFile(this.settingsFile); this.validateConfigFormat(configData); return configData; } catch (error) { throw new Error(`Failed to get current config: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async loadConfigFile(filePath) { const configData = await FileOperations_1.FileOperations.readJsonFile(filePath); this.validateConfigFormat(configData); return configData; } validateConfigFormat(config) { if (!config || typeof config !== 'object') { throw new Error('Configuration must be a valid JSON object'); } if (!config.env || typeof config.env !== 'object') { throw new Error('Configuration must contain an "env" object'); } const requiredFields = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL']; const hasRequiredField = requiredFields.some(field => config.env[field] && typeof config.env[field] === 'string'); if (!hasRequiredField) { throw new Error(`Configuration must contain at least one of: ${requiredFields.join(', ')}`); } } extractModelName(config) { return config.env.ANTHROPIC_MODEL || config.env.ANTHROPIC_SMALL_FAST_MODEL || 'Unknown Model'; } formatDisplayName(configName) { return configName .split(/[-_]/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); } compareConfigs(config1, config2) { const env1 = config1.env; const env2 = config2.env; const keysToCompare = [ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL' ]; return keysToCompare.every(key => env1[key] === env2[key]); } async isClaudeDirAccessible() { return await FileOperations_1.FileOperations.fileExists(this.claudeDir); } getClaudeDir() { return this.claudeDir; } getSettingsFile() { return this.settingsFile; } async switchToConfig(configName) { try { const availableConfigs = await this.getAvailableConfigs(); const targetConfig = availableConfigs.find(config => config.name === configName); if (!targetConfig) { throw new Error(`Configuration '${configName}' not found. Available configurations: ${availableConfigs.map(c => c.name).join(', ')}`); } if (targetConfig.isActive) { return targetConfig; } if (await FileOperations_1.FileOperations.fileExists(this.settingsFile)) { await this.createBackup(); } const targetConfigData = await this.loadConfigFile(targetConfig.filePath); await this.performSafeSwitch(targetConfigData); return { ...targetConfig, isActive: true }; } catch (error) { throw new Error(`Failed to switch to config '${configName}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createBackup() { try { if (!(await FileOperations_1.FileOperations.fileExists(this.settingsFile))) { throw new Error('No settings.json file to backup'); } const backupFileName = FileOperations_1.FileOperations.generateTimestampedName('settings.json'); const backupPath = path.join(this.claudeDir, backupFileName); await FileOperations_1.FileOperations.copyFile(this.settingsFile, backupPath); return backupPath; } catch (error) { throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async restoreFromBackup(backupFileName) { try { const backupPath = path.join(this.claudeDir, backupFileName); if (!(await FileOperations_1.FileOperations.fileExists(backupPath))) { throw new Error(`Backup file not found: ${backupFileName}`); } const backupConfig = await this.loadConfigFile(backupPath); if (await FileOperations_1.FileOperations.fileExists(this.settingsFile)) { await this.createBackup(); } await FileOperations_1.FileOperations.copyFile(backupPath, this.settingsFile); return backupConfig; } catch (error) { throw new Error(`Failed to restore from backup '${backupFileName}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getBackupList() { try { const backupPattern = /^settings\.json\.backup\.\d{8}_\d{6}$/; const files = await FileOperations_1.FileOperations.getFileList(this.claudeDir, backupPattern); return files.sort((a, b) => { const timestampA = a.match(/(\d{8}_\d{6})$/)?.[1] || ''; const timestampB = b.match(/(\d{8}_\d{6})$/)?.[1] || ''; return timestampB.localeCompare(timestampA); }); } catch (error) { throw new Error(`Failed to get backup list: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async performSafeSwitch(targetConfigData) { let backupPath = null; try { if (await FileOperations_1.FileOperations.fileExists(this.settingsFile)) { const tempBackupName = `settings.json.temp.${Date.now()}`; backupPath = path.join(this.claudeDir, tempBackupName); await FileOperations_1.FileOperations.copyFile(this.settingsFile, backupPath); } await FileOperations_1.FileOperations.writeJsonFile(this.settingsFile, targetConfigData); const verificationConfig = await FileOperations_1.FileOperations.readJsonFile(this.settingsFile); this.validateConfigFormat(verificationConfig); if (!this.compareConfigs(targetConfigData, verificationConfig)) { throw new Error('Configuration verification failed: written config does not match source'); } if (backupPath && await FileOperations_1.FileOperations.fileExists(backupPath)) { await FileOperations_1.FileOperations.deleteFile(backupPath); } } catch (error) { if (backupPath && await FileOperations_1.FileOperations.fileExists(backupPath)) { try { await FileOperations_1.FileOperations.copyFile(backupPath, this.settingsFile); await FileOperations_1.FileOperations.deleteFile(backupPath); } catch (rollbackError) { throw new Error(`Switch failed and rollback also failed: ${error instanceof Error ? error.message : 'Unknown error'}. Rollback error: ${rollbackError instanceof Error ? rollbackError.message : 'Unknown rollback error'}`); } } throw error; } } validateConfigIntegrity(config) { const errors = []; try { this.validateConfigFormat(config); } catch (error) { errors.push(error instanceof Error ? error.message : 'Unknown validation error'); } const env = config.env; if (env.ANTHROPIC_BASE_URL && !this.isValidUrl(env.ANTHROPIC_BASE_URL)) { errors.push('ANTHROPIC_BASE_URL is not a valid URL'); } if (env.ANTHROPIC_AUTH_TOKEN && env.ANTHROPIC_AUTH_TOKEN.trim().length === 0) { errors.push('ANTHROPIC_AUTH_TOKEN cannot be empty'); } if (env.ANTHROPIC_MODEL !== undefined && env.ANTHROPIC_MODEL.trim().length === 0) { errors.push('ANTHROPIC_MODEL cannot be empty'); } return { isValid: errors.length === 0, errors }; } isValidUrl(urlString) { try { new URL(urlString); return true; } catch { return false; } } } exports.ConfigManager = ConfigManager;