Skip to main content

cc_switch_lib/cli/commands/
config.rs

1use clap::Subcommand;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::app_config::AppType;
6use crate::cli::commands::config_common;
7use crate::cli::commands::config_webdav;
8use crate::cli::i18n::texts;
9use crate::cli::ui::{error, highlight, info, success, to_json};
10use crate::error::AppError;
11use crate::services::ConfigService;
12use crate::store::AppState;
13
14#[derive(Subcommand)]
15pub enum ConfigCommand {
16    /// Show current configuration
17    Show,
18    /// Show configuration file path
19    Path,
20    /// Export configuration to file
21    Export {
22        /// Output file path
23        file: PathBuf,
24    },
25    /// Import configuration from file
26    Import {
27        /// Input file path
28        file: PathBuf,
29    },
30    /// Create a backup of current configuration
31    Backup {
32        /// Optional custom name for the backup
33        #[arg(long)]
34        name: Option<String>,
35    },
36    /// Restore from a backup
37    Restore {
38        /// Backup ID to restore (from list)
39        #[arg(long, conflicts_with = "file")]
40        backup: Option<String>,
41
42        /// External file path to restore from
43        #[arg(long, conflicts_with = "backup")]
44        file: Option<PathBuf>,
45    },
46    /// Validate configuration file
47    Validate,
48    /// Reset to default configuration
49    Reset,
50
51    /// Manage common configuration snippet (per app)
52    #[command(subcommand)]
53    Common(config_common::CommonConfigCommand),
54
55    /// Manage WebDAV sync settings and operations
56    #[command(name = "webdav", subcommand)]
57    WebDav(config_webdav::WebDavCommand),
58}
59
60pub fn execute(cmd: ConfigCommand, app: Option<AppType>) -> Result<(), AppError> {
61    match cmd {
62        ConfigCommand::Show => show_config(),
63        ConfigCommand::Path => show_path(),
64        ConfigCommand::Export { file } => export_config(&file),
65        ConfigCommand::Import { file } => import_config(&file),
66        ConfigCommand::Backup { name } => backup_config(name.as_deref()),
67        ConfigCommand::Restore { backup, file } => {
68            restore_config(backup.as_deref(), file.as_deref())
69        }
70        ConfigCommand::Validate => validate_config(),
71        ConfigCommand::Reset => reset_config(),
72        ConfigCommand::Common(cmd) => config_common::execute(cmd, app.unwrap_or(AppType::Claude)),
73        ConfigCommand::WebDav(cmd) => config_webdav::execute(cmd),
74    }
75}
76
77fn get_state() -> Result<AppState, AppError> {
78    AppState::try_new()
79}
80
81fn show_config() -> Result<(), AppError> {
82    let state = get_state()?;
83    let config = state.config.read()?;
84
85    println!("{}", highlight("Current Configuration"));
86    println!("{}", "=".repeat(50));
87    println!();
88
89    // Display in pretty JSON format
90    let json = to_json(&*config).map_err(|e| AppError::Message(e.to_string()))?;
91    println!("{}", json);
92
93    Ok(())
94}
95
96fn show_path() -> Result<(), AppError> {
97    let config_dir = crate::config::get_app_config_dir();
98    let db_path = config_dir.join("cc-switch.db");
99    let legacy_config_path = config_dir.join("config.json");
100
101    println!("{}", highlight("Configuration Paths"));
102    println!("{}", "=".repeat(50));
103    println!("DB file:      {}", db_path.display());
104    println!("Legacy JSON:  {}", legacy_config_path.display());
105    println!("Config dir:   {}", config_dir.display());
106
107    // Check if DB file exists
108    if db_path.exists() {
109        println!("\n{} Database exists", success("✓"));
110
111        // Show file size
112        if let Ok(metadata) = fs::metadata(&db_path) {
113            println!("File size:    {} bytes", metadata.len());
114        }
115    } else {
116        println!("\n{} Database file does not exist", error("✗"));
117        println!("{}", info("Run cc-switch once to create the database."));
118    }
119
120    // Show backup directory
121    let backup_dir = config_dir.join("backups");
122    if backup_dir.exists() {
123        if let Ok(entries) = fs::read_dir(&backup_dir) {
124            let count = entries.filter_map(|e| e.ok()).count();
125            println!("\nBackups dir:  {}", backup_dir.display());
126            println!("Backups:      {} backup(s) found", count);
127        }
128    }
129
130    Ok(())
131}
132
133fn export_config(file: &PathBuf) -> Result<(), AppError> {
134    println!(
135        "{}",
136        info(&format!("Exporting configuration to {}...", file.display()))
137    );
138
139    // Check if target file already exists
140    if file.exists() {
141        let confirm = inquire::Confirm::new(&format!(
142            "File '{}' already exists. Overwrite?",
143            file.display()
144        ))
145        .with_default(false)
146        .prompt()
147        .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
148
149        if !confirm {
150            println!("{}", info("Cancelled."));
151            return Ok(());
152        }
153    }
154
155    // Ensure parent directory exists
156    if let Some(parent) = file.parent() {
157        fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
158    }
159
160    // Export configuration
161    ConfigService::export_config_to_path(file)?;
162
163    println!(
164        "{}",
165        success(&format!("✓ Configuration exported to {}", file.display()))
166    );
167
168    Ok(())
169}
170
171fn import_config(file: &PathBuf) -> Result<(), AppError> {
172    println!(
173        "{}",
174        info(&format!(
175            "Importing configuration from {}...",
176            file.display()
177        ))
178    );
179
180    // Check if source file exists
181    if !file.exists() {
182        return Err(AppError::Message(format!(
183            "File '{}' not found",
184            file.display()
185        )));
186    }
187
188    // Confirm import
189    println!();
190    println!("{}", highlight("Warning:"));
191    println!("This will replace your current database with the imported SQL backup.");
192    println!("A backup will be created automatically.");
193    println!();
194
195    let confirm = inquire::Confirm::new("Continue with import?")
196        .with_default(false)
197        .prompt()
198        .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
199
200    if !confirm {
201        println!("{}", info("Cancelled."));
202        return Ok(());
203    }
204
205    // Perform import
206    let state = get_state()?;
207    let backup_id = ConfigService::import_config_from_path(file, &state)?;
208
209    // 导入后同步 live 配置
210    if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
211        log::warn!("配置导入后同步 live 配置失败: {e}");
212    }
213
214    println!(
215        "{}",
216        success(&format!("✓ Configuration imported from {}", file.display()))
217    );
218    if !backup_id.is_empty() {
219        println!("{}", info(&format!("  Backup created: {}", backup_id)));
220    }
221    println!();
222    println!(
223        "{}",
224        info("Note: Restart your CLI clients to apply the changes.")
225    );
226
227    Ok(())
228}
229
230fn backup_config(custom_name: Option<&str>) -> Result<(), AppError> {
231    let config_path = crate::config::get_app_config_path();
232
233    if let Some(name) = custom_name {
234        println!(
235            "{}",
236            info(&format!("Creating backup with name '{}'...", name))
237        );
238    } else {
239        println!("{}", info("Creating backup of current configuration..."));
240    }
241
242    let backup_id = ConfigService::create_backup(&config_path, custom_name.map(|s| s.to_string()))?;
243
244    if backup_id.is_empty() {
245        println!("{}", error("Failed to create backup."));
246    } else {
247        let backup_dir = config_path.parent().unwrap().join("backups");
248        let backup_file = backup_dir.join(format!("{}.sql", backup_id));
249
250        println!("{}", success(&format!("✓ Backup created: {}", backup_id)));
251        println!("Location: {}", backup_file.display());
252    }
253
254    Ok(())
255}
256
257fn restore_config(backup_id: Option<&str>, file_path: Option<&Path>) -> Result<(), AppError> {
258    let config_path = crate::config::get_app_config_path();
259
260    // 情况1:指定了备份 ID
261    if let Some(id) = backup_id {
262        println!("{}", info(&format!("Restoring from backup '{}'...", id)));
263
264        let confirm =
265            inquire::Confirm::new("This will replace your current configuration. Continue?")
266                .with_default(false)
267                .prompt()
268                .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
269
270        if !confirm {
271            println!("{}", info("Cancelled."));
272            return Ok(());
273        }
274
275        let state = get_state()?;
276        let pre_restore_backup = ConfigService::restore_from_backup_id(id, &state)?;
277
278        // 恢复后同步 live 配置
279        if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
280            log::warn!("备份恢复后同步 live 配置失败: {e}");
281        }
282
283        println!(
284            "{}",
285            success(&format!("✓ Configuration restored from backup '{}'", id))
286        );
287        if !pre_restore_backup.is_empty() {
288            println!(
289                "{}",
290                info(&format!("  Pre-restore backup: {}", pre_restore_backup))
291            );
292        }
293        println!();
294        println!(
295            "{}",
296            info("Note: Restart your CLI clients to apply the changes.")
297        );
298
299        return Ok(());
300    }
301
302    // 情况2:指定了文件路径
303    if let Some(file) = file_path {
304        println!(
305            "{}",
306            info(&format!(
307                "Restoring configuration from {}...",
308                file.display()
309            ))
310        );
311
312        if !file.exists() {
313            return Err(AppError::Message(format!(
314                "File '{}' not found",
315                file.display()
316            )));
317        }
318
319        println!();
320        println!("{}", highlight("Warning:"));
321        println!("This will replace your current database with the SQL backup file.");
322        println!("A backup of the current state will be created first.");
323        println!();
324
325        let confirm = inquire::Confirm::new(texts::config_restore_confirm_prompt())
326            .with_default(false)
327            .prompt()
328            .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
329
330        if !confirm {
331            println!("{}", info("Cancelled."));
332            return Ok(());
333        }
334
335        let state = get_state()?;
336        let pre_restore_backup = ConfigService::import_config_from_path(file, &state)?;
337
338        // 恢复后同步 live 配置
339        if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
340            log::warn!("配置恢复后同步 live 配置失败: {e}");
341        }
342
343        println!(
344            "{}",
345            success(&format!("✓ Configuration restored from {}", file.display()))
346        );
347        if !pre_restore_backup.is_empty() {
348            println!(
349                "{}",
350                info(&format!("  Pre-restore backup: {}", pre_restore_backup))
351            );
352        }
353        println!();
354        println!(
355            "{}",
356            info("Note: Restart your CLI clients to apply the changes.")
357        );
358
359        return Ok(());
360    }
361
362    // 情况3:无参数,显示交互式列表
363    println!("{}", highlight(texts::available_backups()));
364    println!("{}", "=".repeat(50));
365
366    let backups = ConfigService::list_backups(&config_path)?;
367
368    if backups.is_empty() {
369        println!();
370        println!("{}", info(texts::no_backups_found()));
371        println!("{}", info(texts::create_backup_first_hint()));
372        return Ok(());
373    }
374
375    println!();
376    println!("{}", texts::found_backups(backups.len()));
377    println!();
378
379    let choices: Vec<String> = backups
380        .iter()
381        .map(|b| format!("{} - {}", b.display_name, b.id))
382        .collect();
383
384    let selection = inquire::Select::new(texts::select_backup_to_restore(), choices)
385        .prompt()
386        .map_err(|_| AppError::Message(texts::selection_cancelled().to_string()))?;
387
388    let selected_backup = backups
389        .iter()
390        .find(|b| selection.contains(&b.id))
391        .ok_or_else(|| AppError::Message(texts::invalid_selection().to_string()))?;
392
393    println!();
394    println!("{}", highlight(texts::warning_title()));
395    println!("{}", texts::config_restore_warning_replace());
396    println!("{}", texts::config_restore_warning_pre_backup());
397    println!();
398
399    let confirm = inquire::Confirm::new(texts::config_restore_confirm_prompt())
400        .with_default(false)
401        .prompt()
402        .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
403
404    if !confirm {
405        println!("{}", info(texts::cancelled()));
406        return Ok(());
407    }
408
409    let state = get_state()?;
410    let pre_restore_backup = ConfigService::restore_from_backup_id(&selected_backup.id, &state)?;
411
412    // 恢复后同步 live 配置
413    if let Err(e) = crate::services::provider::ProviderService::sync_current_to_live(&state) {
414        log::warn!("备份恢复后同步 live 配置失败: {e}");
415    }
416
417    println!(
418        "{}",
419        success(&format!(
420            "✓ Configuration restored from: {}",
421            selected_backup.display_name
422        ))
423    );
424    if !pre_restore_backup.is_empty() {
425        println!(
426            "{}",
427            info(&format!("  Pre-restore backup: {}", pre_restore_backup))
428        );
429    }
430    println!();
431    println!(
432        "{}",
433        info("Note: Restart your CLI clients to apply the changes.")
434    );
435
436    Ok(())
437}
438
439fn validate_config() -> Result<(), AppError> {
440    let config_dir = crate::config::get_app_config_dir();
441    let db_path = config_dir.join("cc-switch.db");
442
443    println!("{}", info("Validating database..."));
444    println!();
445
446    if !db_path.exists() {
447        println!("{}", error("✗ Database file does not exist"));
448        println!("Path: {}", db_path.display());
449        return Ok(());
450    }
451
452    println!("{} Database file exists", success("✓"));
453    println!("Path: {}", db_path.display());
454
455    let db = crate::Database::init()?;
456    println!("{} Database schema is readable", success("✓"));
457
458    // Show some stats
459    let claude_count = db.get_all_providers("claude")?.len();
460    let codex_count = db.get_all_providers("codex")?.len();
461    let gemini_count = db.get_all_providers("gemini")?.len();
462    let mcp_count = db.get_all_mcp_servers()?.len();
463    let skills_count = db.get_all_installed_skills()?.len();
464
465    println!();
466    println!("{}", highlight("Database Summary:"));
467    println!("Claude providers:  {}", claude_count);
468    println!("Codex providers:   {}", codex_count);
469    println!("Gemini providers:  {}", gemini_count);
470    println!("MCP servers:       {}", mcp_count);
471    println!("Skills installed:  {}", skills_count);
472
473    println!();
474    println!("{}", success("✓ Database validation passed"));
475
476    Ok(())
477}
478
479fn reset_config() -> Result<(), AppError> {
480    println!("{}", highlight("Reset Configuration"));
481    println!("{}", "=".repeat(50));
482    println!();
483    println!("{}", highlight("Warning:"));
484    println!("This will delete your current configuration and create a fresh default one.");
485    println!("All your providers, MCP servers, and settings will be lost.");
486    println!();
487    println!("{}", info("Consider creating a backup first:"));
488    println!("  cc-switch config backup");
489    println!();
490
491    let confirm = inquire::Confirm::new("Are you sure you want to reset to default configuration?")
492        .with_default(false)
493        .prompt()
494        .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
495
496    if !confirm {
497        println!("{}", info("Cancelled."));
498        return Ok(());
499    }
500
501    // Create a backup before reset (SQL)
502    let config_path = crate::config::get_app_config_path();
503    let backup_id = ConfigService::create_backup(&config_path, None)?;
504
505    // Delete the database file
506    let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
507    if db_path.exists() {
508        fs::remove_file(&db_path).map_err(|e| AppError::io(&db_path, e))?;
509    }
510
511    // Recreate empty DB
512    let _ = crate::Database::init()?;
513
514    println!("{}", success("✓ Configuration reset to defaults"));
515    if !backup_id.is_empty() {
516        println!("{}", info(&format!("  Backup created: {}", backup_id)));
517        println!(
518            "{}",
519            info("  You can restore it later using: cc-switch config restore")
520        );
521    }
522
523    Ok(())
524}