UNPKG

zcf

Version:

Zero-Config Code Flow - One-click configuration tool for Code Cli

732 lines (729 loc) 23 kB
import dayjs from 'dayjs'; import { join } from 'pathe'; import { q as ZCF_CONFIG_FILE, Z as ZCF_CONFIG_DIR, ap as ensureDir, aq as readDefaultTomlConfig, ar as createDefaultTomlConfig, as as exists, at as readJsonConfig, au as writeTomlConfig, S as SETTINGS_FILE, av as clearModelEnv, aw as copyFile } from './simple-config.mjs'; import 'node:fs'; import 'node:process'; import 'ansis'; import 'inquirer'; import 'node:child_process'; import 'node:os'; import 'node:util'; import 'node:url'; import 'inquirer-toggle'; import 'ora'; import 'tinyexec'; import 'semver'; import 'smol-toml'; import 'node:fs/promises'; import 'i18next'; import 'i18next-fs-backend'; class ClaudeCodeConfigManager { static CONFIG_FILE = ZCF_CONFIG_FILE; static LEGACY_CONFIG_FILE = join(ZCF_CONFIG_DIR, "claude-code-configs.json"); /** * Ensure configuration directory exists */ static ensureConfigDir() { ensureDir(ZCF_CONFIG_DIR); } /** * Read TOML configuration */ static readTomlConfig() { return readDefaultTomlConfig(); } /** * Load TOML configuration, falling back to default when missing */ static loadTomlConfig() { const existingConfig = this.readTomlConfig(); if (existingConfig) { return existingConfig; } return createDefaultTomlConfig(); } /** * Migrate legacy JSON-based configuration into TOML storage */ static migrateFromLegacyConfig() { if (!exists(this.LEGACY_CONFIG_FILE)) { return null; } try { const legacyConfig = readJsonConfig(this.LEGACY_CONFIG_FILE); if (!legacyConfig) { return null; } const normalizedProfiles = {}; const existingKeys = /* @__PURE__ */ new Set(); let migratedCurrentKey = ""; Object.entries(legacyConfig.profiles || {}).forEach(([legacyKey, profile]) => { const sourceProfile = profile; const name = sourceProfile.name?.trim() || legacyKey; const baseKey = this.generateProfileId(name); let uniqueKey = baseKey || legacyKey; let suffix = 2; while (existingKeys.has(uniqueKey)) { uniqueKey = `${baseKey || legacyKey}-${suffix++}`; } existingKeys.add(uniqueKey); const sanitizedProfile = this.sanitizeProfile({ ...sourceProfile, name }); normalizedProfiles[uniqueKey] = { ...sanitizedProfile, id: uniqueKey }; if (legacyConfig.currentProfileId === legacyKey || legacyConfig.currentProfileId === sourceProfile.id) { migratedCurrentKey = uniqueKey; } }); if (!migratedCurrentKey && legacyConfig.currentProfileId) { const fallbackKey = this.generateProfileId(legacyConfig.currentProfileId); if (existingKeys.has(fallbackKey)) { migratedCurrentKey = fallbackKey; } } if (!migratedCurrentKey && existingKeys.size > 0) { migratedCurrentKey = Array.from(existingKeys)[0]; } const migratedConfig = { currentProfileId: migratedCurrentKey, profiles: normalizedProfiles }; this.writeConfig(migratedConfig); return migratedConfig; } catch (error) { console.error("Failed to migrate legacy Claude Code config:", error); return null; } } /** * Read configuration */ static readConfig() { try { const tomlConfig = readDefaultTomlConfig(); if (!tomlConfig || !tomlConfig.claudeCode) { return this.migrateFromLegacyConfig(); } const { claudeCode } = tomlConfig; const rawProfiles = claudeCode.profiles || {}; const sanitizedProfiles = Object.fromEntries( Object.entries(rawProfiles).map(([key, profile]) => { const storedProfile = this.sanitizeProfile({ ...profile, name: profile.name || key }); return [key, { ...storedProfile, id: key }]; }) ); const configData = { currentProfileId: claudeCode.currentProfile || "", profiles: sanitizedProfiles }; if (Object.keys(configData.profiles).length === 0) { const migrated = this.migrateFromLegacyConfig(); if (migrated) { return migrated; } } return configData; } catch (error) { console.error("Failed to read Claude Code config:", error); return null; } } /** * Write configuration */ static writeConfig(config) { try { this.ensureConfigDir(); const keyMap = /* @__PURE__ */ new Map(); const sanitizedProfiles = Object.fromEntries( Object.entries(config.profiles).map(([key, profile]) => { const normalizedName = profile.name?.trim() || key; const profileKey = this.generateProfileId(normalizedName); keyMap.set(key, profileKey); const sanitizedProfile = this.sanitizeProfile({ ...profile, name: normalizedName }); return [profileKey, sanitizedProfile]; }) ); const tomlConfig = this.loadTomlConfig(); const nextTomlConfig = { ...tomlConfig, claudeCode: { ...tomlConfig.claudeCode, currentProfile: keyMap.get(config.currentProfileId) || config.currentProfileId, profiles: sanitizedProfiles } }; writeTomlConfig(this.CONFIG_FILE, nextTomlConfig); } catch (error) { console.error("Failed to write Claude Code config:", error); throw new Error(`Failed to write config: ${error instanceof Error ? error.message : String(error)}`); } } /** * Create empty configuration */ static createEmptyConfig() { return { currentProfileId: "", profiles: {} }; } /** * Apply profile settings to Claude Code runtime */ static async applyProfileSettings(profile) { const { ensureI18nInitialized, i18n } = await import('./simple-config.mjs').then(function (n) { return n.bg; }); ensureI18nInitialized(); try { if (!profile) { const { switchToOfficialLogin } = await import('./simple-config.mjs').then(function (n) { return n.bk; }); switchToOfficialLogin(); return; } const { readJsonConfig: readJsonConfig2, writeJsonConfig } = await import('./simple-config.mjs').then(function (n) { return n.bi; }); const settings = readJsonConfig2(SETTINGS_FILE) || {}; if (!settings.env) settings.env = {}; clearModelEnv(settings.env); let shouldRestartCcr = false; if (profile.authType === "api_key") { settings.env.ANTHROPIC_API_KEY = profile.apiKey; delete settings.env.ANTHROPIC_AUTH_TOKEN; } else if (profile.authType === "auth_token") { settings.env.ANTHROPIC_AUTH_TOKEN = profile.apiKey; delete settings.env.ANTHROPIC_API_KEY; } else if (profile.authType === "ccr_proxy") { const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.bl; }); const ccrConfig = readCcrConfig(); if (!ccrConfig) { throw new Error(i18n.t("ccr:ccrNotConfigured") || "CCR proxy configuration not found"); } const host = ccrConfig.HOST || "127.0.0.1"; const port = ccrConfig.PORT || 3456; const apiKey = ccrConfig.APIKEY || "sk-zcf-x-ccr"; settings.env.ANTHROPIC_BASE_URL = `http://${host}:${port}`; settings.env.ANTHROPIC_API_KEY = apiKey; delete settings.env.ANTHROPIC_AUTH_TOKEN; shouldRestartCcr = true; } if (profile.authType !== "ccr_proxy") { if (profile.baseUrl) settings.env.ANTHROPIC_BASE_URL = profile.baseUrl; else delete settings.env.ANTHROPIC_BASE_URL; } const hasModelConfig = Boolean( profile.primaryModel || profile.defaultHaikuModel || profile.defaultSonnetModel || profile.defaultOpusModel ); if (hasModelConfig) { if (profile.primaryModel) settings.env.ANTHROPIC_MODEL = profile.primaryModel; if (profile.defaultHaikuModel) settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.defaultHaikuModel; if (profile.defaultSonnetModel) settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = profile.defaultSonnetModel; if (profile.defaultOpusModel) settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = profile.defaultOpusModel; } else { clearModelEnv(settings.env); } writeJsonConfig(SETTINGS_FILE, settings); const { setPrimaryApiKey, addCompletedOnboarding } = await import('./simple-config.mjs').then(function (n) { return n.bj; }); setPrimaryApiKey(); addCompletedOnboarding(); if (shouldRestartCcr) { const { runCcrRestart } = await import('./commands.mjs'); await runCcrRestart(); } } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw new Error(`${i18n.t("multi-config:failedToApplySettings")}: ${reason}`); } } static async applyCurrentProfile() { const currentProfile = this.getCurrentProfile(); await this.applyProfileSettings(currentProfile); } /** * Remove unsupported fields from profile payload */ static sanitizeProfile(profile) { const sanitized = { name: profile.name, authType: profile.authType }; if (profile.apiKey) sanitized.apiKey = profile.apiKey; if (profile.baseUrl) sanitized.baseUrl = profile.baseUrl; if (profile.primaryModel) sanitized.primaryModel = profile.primaryModel; if (profile.defaultHaikuModel) sanitized.defaultHaikuModel = profile.defaultHaikuModel; if (profile.defaultSonnetModel) sanitized.defaultSonnetModel = profile.defaultSonnetModel; if (profile.defaultOpusModel) sanitized.defaultOpusModel = profile.defaultOpusModel; return sanitized; } /** * Backup configuration */ static backupConfig() { try { if (!exists(this.CONFIG_FILE)) { return null; } const timestamp = dayjs().format("YYYY-MM-DD_HH-mm-ss"); const backupPath = join(ZCF_CONFIG_DIR, `config.backup.${timestamp}.toml`); copyFile(this.CONFIG_FILE, backupPath); return backupPath; } catch (error) { console.error("Failed to backup Claude Code config:", error); return null; } } /** * Add configuration */ static async addProfile(profile) { try { const validationErrors = this.validateProfile(profile); if (validationErrors.length > 0) { return { success: false, error: `Validation failed: ${validationErrors.join(", ")}` }; } const backupPath = this.backupConfig(); let config = this.readConfig(); if (!config) { config = this.createEmptyConfig(); } if (profile.id && config.profiles[profile.id]) { return { success: false, error: `Profile with ID "${profile.id}" already exists`, backupPath: backupPath || void 0 }; } const normalizedName = profile.name.trim(); const profileKey = this.generateProfileId(normalizedName); const existingNames = Object.values(config.profiles).map((p) => p.name || ""); if (config.profiles[profileKey] || existingNames.some((name) => name.toLowerCase() === normalizedName.toLowerCase())) { return { success: false, error: `Profile with name "${profile.name}" already exists`, backupPath: backupPath || void 0 }; } const sanitizedProfile = this.sanitizeProfile({ ...profile, name: normalizedName }); const runtimeProfile = { ...sanitizedProfile, id: profileKey }; config.profiles[profileKey] = runtimeProfile; if (!config.currentProfileId) { config.currentProfileId = profileKey; } this.writeConfig(config); return { success: true, backupPath: backupPath || void 0, addedProfile: runtimeProfile }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Update configuration */ static async updateProfile(id, data) { try { const validationErrors = this.validateProfile(data, true); if (validationErrors.length > 0) { return { success: false, error: `Validation failed: ${validationErrors.join(", ")}` }; } const backupPath = this.backupConfig(); const config = this.readConfig(); if (!config || !config.profiles[id]) { return { success: false, error: `Profile with ID "${id}" not found`, backupPath: backupPath || void 0 }; } const existingProfile = config.profiles[id]; const nextName = data.name !== void 0 ? data.name.trim() : existingProfile.name; const nextKey = this.generateProfileId(nextName); const nameChanged = nextKey !== id; if (nameChanged) { const duplicateName = Object.entries(config.profiles).some(([key, profile]) => key !== id && (profile.name || "").toLowerCase() === nextName.toLowerCase()); if (duplicateName || config.profiles[nextKey]) { return { success: false, error: `Profile with name "${data.name}" already exists`, backupPath: backupPath || void 0 }; } } const mergedProfile = this.sanitizeProfile({ ...existingProfile, ...data, name: nextName }); if (nameChanged) { delete config.profiles[id]; config.profiles[nextKey] = { ...mergedProfile, id: nextKey }; if (config.currentProfileId === id) { config.currentProfileId = nextKey; } } else { config.profiles[id] = { ...mergedProfile, id }; } this.writeConfig(config); return { success: true, backupPath: backupPath || void 0, updatedProfile: { ...mergedProfile, id: nameChanged ? nextKey : id } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Delete configuration */ static async deleteProfile(id) { try { const backupPath = this.backupConfig(); const config = this.readConfig(); if (!config || !config.profiles[id]) { return { success: false, error: `Profile with ID "${id}" not found`, backupPath: backupPath || void 0 }; } const profileCount = Object.keys(config.profiles).length; if (profileCount === 1) { return { success: false, error: "Cannot delete the last profile. At least one profile must remain.", backupPath: backupPath || void 0 }; } delete config.profiles[id]; if (config.currentProfileId === id) { const remainingIds = Object.keys(config.profiles); config.currentProfileId = remainingIds[0]; } this.writeConfig(config); return { success: true, backupPath: backupPath || void 0, remainingProfiles: Object.entries(config.profiles).map(([key, profile]) => ({ ...profile, id: key })) }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Delete multiple configurations */ static async deleteProfiles(ids) { try { const backupPath = this.backupConfig(); const config = this.readConfig(); if (!config) { return { success: false, error: "No configuration found", backupPath: backupPath || void 0 }; } const missingIds = ids.filter((id) => !config.profiles[id]); if (missingIds.length > 0) { return { success: false, error: `Profiles not found: ${missingIds.join(", ")}`, backupPath: backupPath || void 0 }; } const remainingCount = Object.keys(config.profiles).length - ids.length; if (remainingCount === 0) { return { success: false, error: "Cannot delete all profiles. At least one profile must remain.", backupPath: backupPath || void 0 }; } let newCurrentProfileId; ids.forEach((id) => { delete config.profiles[id]; }); if (ids.includes(config.currentProfileId)) { const remainingIds = Object.keys(config.profiles); config.currentProfileId = remainingIds[0]; newCurrentProfileId = config.currentProfileId; } this.writeConfig(config); return { success: true, backupPath: backupPath || void 0, newCurrentProfileId, deletedProfiles: ids, remainingProfiles: Object.entries(config.profiles).map(([key, profile]) => ({ ...profile, id: key })) }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Generate profile ID from name */ static generateProfileId(name) { return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "profile"; } /** * Switch configuration */ static async switchProfile(id) { try { const config = this.readConfig(); if (!config || !config.profiles[id]) { return { success: false, error: "Profile not found" }; } if (config.currentProfileId === id) { return { success: true }; } config.currentProfileId = id; this.writeConfig(config); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * List all configurations */ static listProfiles() { const config = this.readConfig(); if (!config) { return []; } return Object.values(config.profiles); } /** * Get current configuration */ static getCurrentProfile() { const config = this.readConfig(); if (!config || !config.currentProfileId) { return null; } return config.profiles[config.currentProfileId] || null; } /** * Get configuration by ID */ static getProfileById(id) { const config = this.readConfig(); if (!config) { return null; } return config.profiles[id] || null; } /** * Get configuration by name */ static getProfileByName(name) { const config = this.readConfig(); if (!config) { return null; } return Object.values(config.profiles).find((p) => p.name === name) || null; } /** * Sync CCR configuration */ static async syncCcrProfile() { try { const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.bl; }); const ccrConfig = readCcrConfig(); if (!ccrConfig) { await this.ensureCcrProfileExists(ccrConfig); return; } await this.ensureCcrProfileExists(ccrConfig); } catch (error) { console.error("Failed to sync CCR profile:", error); } } /** * 确保CCR配置文件存在 */ static async ensureCcrProfileExists(ccrConfig) { const config = this.readConfig() || this.createEmptyConfig(); const ccrProfileId = "ccr-proxy"; const existingCcrProfile = config.profiles[ccrProfileId]; if (!ccrConfig) { if (existingCcrProfile) { delete config.profiles[ccrProfileId]; if (config.currentProfileId === ccrProfileId) { const remainingIds = Object.keys(config.profiles); config.currentProfileId = remainingIds[0] || ""; } this.writeConfig(config); } return; } const host = ccrConfig.HOST || "127.0.0.1"; const port = ccrConfig.PORT || 3456; const apiKey = ccrConfig.APIKEY || "sk-zcf-x-ccr"; const baseUrl = `http://${host}:${port}`; const ccrProfile = { name: "CCR Proxy", authType: "ccr_proxy", baseUrl, apiKey }; config.profiles[ccrProfileId] = { ...ccrProfile, id: ccrProfileId }; if (!config.currentProfileId) { config.currentProfileId = ccrProfileId; } this.writeConfig(config); } /** * Switch to official login */ static async switchToOfficial() { try { const config = this.readConfig(); if (!config) { return { success: true }; } config.currentProfileId = ""; this.writeConfig(config); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Switch to CCR proxy */ static async switchToCcr() { try { await this.syncCcrProfile(); const config = this.readConfig(); if (!config || !config.profiles["ccr-proxy"]) { return { success: false, error: "CCR proxy configuration not found. Please configure CCR first." }; } return await this.switchProfile("ccr-proxy"); } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Validate configuration */ static validateProfile(profile, isUpdate = false) { const errors = []; if (!isUpdate && (!profile.name || typeof profile.name !== "string" || profile.name.trim() === "")) { errors.push("Profile name is required"); } if (profile.name && typeof profile.name !== "string") { errors.push("Profile name must be a string"); } if (profile.authType && !["api_key", "auth_token", "ccr_proxy"].includes(profile.authType)) { errors.push("Invalid auth type. Must be one of: api_key, auth_token, ccr_proxy"); } if (profile.authType === "api_key" || profile.authType === "auth_token") { if (!profile.apiKey || typeof profile.apiKey !== "string" || profile.apiKey.trim() === "") { errors.push("API key is required for api_key and auth_token types"); } } if (profile.baseUrl) { try { new URL(profile.baseUrl); } catch { errors.push("Invalid base URL format"); } } return errors; } /** * 检查是否为最后一个配置 */ static isLastProfile(id) { const config = this.readConfig(); if (!config || !config.profiles[id]) { return false; } return Object.keys(config.profiles).length === 1; } } export { ClaudeCodeConfigManager };