use clap::Subcommand;
use std::fs;
use std::path::{Path, PathBuf};
use crate::app_config::AppType;
use crate::cli::commands::config_common;
use crate::cli::commands::config_webdav;
use crate::cli::i18n::texts;
use crate::cli::ui::{error, highlight, info, success, to_json};
use crate::error::AppError;
use crate::services::ConfigService;
use crate::store::AppState;
#[derive(Subcommand)]
pub enum ConfigCommand {
Show,
Path,
Export {
file: PathBuf,
},
Import {
file: PathBuf,
},
Backup {
#[arg(long)]
name: Option<String>,
},
Restore {
#[arg(long, conflicts_with = "file")]
backup: Option<String>,
#[arg(long, conflicts_with = "backup")]
file: Option<PathBuf>,
},
Validate,
Reset,
#[command(subcommand)]
Common(config_common::CommonConfigCommand),
#[command(name = "webdav", subcommand)]
WebDav(config_webdav::WebDavCommand),
}
pub fn execute(cmd: ConfigCommand, app: Option<AppType>) -> Result<(), AppError> {
match cmd {
ConfigCommand::Show => show_config(),
ConfigCommand::Path => show_path(),
ConfigCommand::Export { file } => export_config(&file),
ConfigCommand::Import { file } => import_config(&file),
ConfigCommand::Backup { name } => backup_config(name.as_deref()),
ConfigCommand::Restore { backup, file } => {
restore_config(backup.as_deref(), file.as_deref())
}
ConfigCommand::Validate => validate_config(),
ConfigCommand::Reset => reset_config(),
ConfigCommand::Common(cmd) => config_common::execute(cmd, app.unwrap_or(AppType::Claude)),
ConfigCommand::WebDav(cmd) => config_webdav::execute(cmd),
}
}
fn get_state() -> Result<AppState, AppError> {
AppState::try_new()
}
fn show_config() -> Result<(), AppError> {
let state = get_state()?;
let config = state.config.read()?;
println!("{}", highlight("Current Configuration"));
println!("{}", "=".repeat(50));
println!();
let json = to_json(&*config).map_err(|e| AppError::Message(e.to_string()))?;
println!("{}", json);
Ok(())
}
fn show_path() -> Result<(), AppError> {
let config_dir = crate::config::get_app_config_dir();
let db_path = config_dir.join("cc-switch.db");
let legacy_config_path = config_dir.join("config.json");
println!("{}", highlight("Configuration Paths"));
println!("{}", "=".repeat(50));
println!("DB file: {}", db_path.display());
println!("Legacy JSON: {}", legacy_config_path.display());
println!("Config dir: {}", config_dir.display());
if db_path.exists() {
println!("\n{} Database exists", success("✓"));
if let Ok(metadata) = fs::metadata(&db_path) {
println!("File size: {} bytes", metadata.len());
}
} else {
println!("\n{} Database file does not exist", error("✗"));
println!("{}", info("Run cc-switch once to create the database."));
}
let backup_dir = config_dir.join("backups");
if backup_dir.exists() {
if let Ok(entries) = fs::read_dir(&backup_dir) {
let count = entries.filter_map(|e| e.ok()).count();
println!("\nBackups dir: {}", backup_dir.display());
println!("Backups: {} backup(s) found", count);
}
}
Ok(())
}
fn export_config(file: &PathBuf) -> Result<(), AppError> {
println!(
"{}",
info(&format!("Exporting configuration to {}...", file.display()))
);
if file.exists() {
let confirm = inquire::Confirm::new(&format!(
"File '{}' already exists. Overwrite?",
file.display()
))
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info("Cancelled."));
return Ok(());
}
}
if let Some(parent) = file.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
ConfigService::export_config_to_path(file)?;
println!(
"{}",
success(&format!("✓ Configuration exported to {}", file.display()))
);
Ok(())
}
fn import_config(file: &PathBuf) -> Result<(), AppError> {
println!(
"{}",
info(&format!(
"Importing configuration from {}...",
file.display()
))
);
if !file.exists() {
return Err(AppError::Message(format!(
"File '{}' not found",
file.display()
)));
}
println!();
println!("{}", highlight("Warning:"));
println!("This will replace your current database with the imported SQL backup.");
println!("A backup will be created automatically.");
println!();
let confirm = inquire::Confirm::new("Continue with import?")
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info("Cancelled."));
return Ok(());
}
let state = get_state()?;
let backup_id = ConfigService::import_config_from_path(file, &state)?;
if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
log::warn!("配置导入后同步 live 配置失败: {e}");
}
println!(
"{}",
success(&format!("✓ Configuration imported from {}", file.display()))
);
if !backup_id.is_empty() {
println!("{}", info(&format!(" Backup created: {}", backup_id)));
}
println!();
println!(
"{}",
info("Note: Restart your CLI clients to apply the changes.")
);
Ok(())
}
fn backup_config(custom_name: Option<&str>) -> Result<(), AppError> {
let config_path = crate::config::get_app_config_path();
if let Some(name) = custom_name {
println!(
"{}",
info(&format!("Creating backup with name '{}'...", name))
);
} else {
println!("{}", info("Creating backup of current configuration..."));
}
let backup_id = ConfigService::create_backup(&config_path, custom_name.map(|s| s.to_string()))?;
if backup_id.is_empty() {
println!("{}", error("Failed to create backup."));
} else {
let backup_dir = config_path.parent().unwrap().join("backups");
let backup_file = backup_dir.join(format!("{}.sql", backup_id));
println!("{}", success(&format!("✓ Backup created: {}", backup_id)));
println!("Location: {}", backup_file.display());
}
Ok(())
}
fn restore_config(backup_id: Option<&str>, file_path: Option<&Path>) -> Result<(), AppError> {
let config_path = crate::config::get_app_config_path();
if let Some(id) = backup_id {
println!("{}", info(&format!("Restoring from backup '{}'...", id)));
let confirm =
inquire::Confirm::new("This will replace your current configuration. Continue?")
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info("Cancelled."));
return Ok(());
}
let state = get_state()?;
let pre_restore_backup = ConfigService::restore_from_backup_id(id, &state)?;
if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
log::warn!("备份恢复后同步 live 配置失败: {e}");
}
println!(
"{}",
success(&format!("✓ Configuration restored from backup '{}'", id))
);
if !pre_restore_backup.is_empty() {
println!(
"{}",
info(&format!(" Pre-restore backup: {}", pre_restore_backup))
);
}
println!();
println!(
"{}",
info("Note: Restart your CLI clients to apply the changes.")
);
return Ok(());
}
if let Some(file) = file_path {
println!(
"{}",
info(&format!(
"Restoring configuration from {}...",
file.display()
))
);
if !file.exists() {
return Err(AppError::Message(format!(
"File '{}' not found",
file.display()
)));
}
println!();
println!("{}", highlight("Warning:"));
println!("This will replace your current database with the SQL backup file.");
println!("A backup of the current state will be created first.");
println!();
let confirm = inquire::Confirm::new(texts::config_restore_confirm_prompt())
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info("Cancelled."));
return Ok(());
}
let state = get_state()?;
let pre_restore_backup = ConfigService::import_config_from_path(file, &state)?;
if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
log::warn!("配置恢复后同步 live 配置失败: {e}");
}
println!(
"{}",
success(&format!("✓ Configuration restored from {}", file.display()))
);
if !pre_restore_backup.is_empty() {
println!(
"{}",
info(&format!(" Pre-restore backup: {}", pre_restore_backup))
);
}
println!();
println!(
"{}",
info("Note: Restart your CLI clients to apply the changes.")
);
return Ok(());
}
println!("{}", highlight(texts::available_backups()));
println!("{}", "=".repeat(50));
let backups = ConfigService::list_backups(&config_path)?;
if backups.is_empty() {
println!();
println!("{}", info(texts::no_backups_found()));
println!("{}", info(texts::create_backup_first_hint()));
return Ok(());
}
println!();
println!("{}", texts::found_backups(backups.len()));
println!();
let choices: Vec<String> = backups
.iter()
.map(|b| format!("{} - {}", b.display_name, b.id))
.collect();
let selection = inquire::Select::new(texts::select_backup_to_restore(), choices)
.prompt()
.map_err(|_| AppError::Message(texts::selection_cancelled().to_string()))?;
let selected_backup = backups
.iter()
.find(|b| selection.contains(&b.id))
.ok_or_else(|| AppError::Message(texts::invalid_selection().to_string()))?;
println!();
println!("{}", highlight(texts::warning_title()));
println!("{}", texts::config_restore_warning_replace());
println!("{}", texts::config_restore_warning_pre_backup());
println!();
let confirm = inquire::Confirm::new(texts::config_restore_confirm_prompt())
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info(texts::cancelled()));
return Ok(());
}
let state = get_state()?;
let pre_restore_backup = ConfigService::restore_from_backup_id(&selected_backup.id, &state)?;
if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
log::warn!("备份恢复后同步 live 配置失败: {e}");
}
println!(
"{}",
success(&format!(
"✓ Configuration restored from: {}",
selected_backup.display_name
))
);
if !pre_restore_backup.is_empty() {
println!(
"{}",
info(&format!(" Pre-restore backup: {}", pre_restore_backup))
);
}
println!();
println!(
"{}",
info("Note: Restart your CLI clients to apply the changes.")
);
Ok(())
}
fn validate_config() -> Result<(), AppError> {
let config_dir = crate::config::get_app_config_dir();
let db_path = config_dir.join("cc-switch.db");
println!("{}", info("Validating database..."));
println!();
if !db_path.exists() {
println!("{}", error("✗ Database file does not exist"));
println!("Path: {}", db_path.display());
return Ok(());
}
println!("{} Database file exists", success("✓"));
println!("Path: {}", db_path.display());
let db = crate::Database::init()?;
println!("{} Database schema is readable", success("✓"));
let claude_count = db.get_all_providers("claude")?.len();
let codex_count = db.get_all_providers("codex")?.len();
let gemini_count = db.get_all_providers("gemini")?.len();
let mcp_count = db.get_all_mcp_servers()?.len();
let skills_count = db.get_all_installed_skills()?.len();
println!();
println!("{}", highlight("Database Summary:"));
println!("Claude providers: {}", claude_count);
println!("Codex providers: {}", codex_count);
println!("Gemini providers: {}", gemini_count);
println!("MCP servers: {}", mcp_count);
println!("Skills installed: {}", skills_count);
println!();
println!("{}", success("✓ Database validation passed"));
Ok(())
}
fn reset_config() -> Result<(), AppError> {
println!("{}", highlight("Reset Configuration"));
println!("{}", "=".repeat(50));
println!();
println!("{}", highlight("Warning:"));
println!("This will delete your current configuration and create a fresh default one.");
println!("All your providers, MCP servers, and settings will be lost.");
println!();
println!("{}", info("Consider creating a backup first:"));
println!(" cc-switch config backup");
println!();
let confirm = inquire::Confirm::new("Are you sure you want to reset to default configuration?")
.with_default(false)
.prompt()
.map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
if !confirm {
println!("{}", info("Cancelled."));
return Ok(());
}
let config_path = crate::config::get_app_config_path();
let backup_id = ConfigService::create_backup(&config_path, None)?;
let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
if db_path.exists() {
fs::remove_file(&db_path).map_err(|e| AppError::io(&db_path, e))?;
}
let _ = crate::Database::init()?;
println!("{}", success("✓ Configuration reset to defaults"));
if !backup_id.is_empty() {
println!("{}", info(&format!(" Backup created: {}", backup_id)));
println!(
"{}",
info(" You can restore it later using: cc-switch config restore")
);
}
Ok(())
}