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
JavaScript
;
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;