cc_switch_lib/cli/commands/
config.rs1use 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,
18 Path,
20 Export {
22 file: PathBuf,
24 },
25 Import {
27 file: PathBuf,
29 },
30 Backup {
32 #[arg(long)]
34 name: Option<String>,
35 },
36 Restore {
38 #[arg(long, conflicts_with = "file")]
40 backup: Option<String>,
41
42 #[arg(long, conflicts_with = "backup")]
44 file: Option<PathBuf>,
45 },
46 Validate,
48 Reset,
50
51 #[command(subcommand)]
53 Common(config_common::CommonConfigCommand),
54
55 #[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 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 if db_path.exists() {
109 println!("\n{} Database exists", success("✓"));
110
111 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 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 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 if let Some(parent) = file.parent() {
157 fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
158 }
159
160 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 if !file.exists() {
182 return Err(AppError::Message(format!(
183 "File '{}' not found",
184 file.display()
185 )));
186 }
187
188 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 let state = get_state()?;
207 let backup_id = ConfigService::import_config_from_path(file, &state)?;
208
209 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 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 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 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 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 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 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 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 let config_path = crate::config::get_app_config_path();
503 let backup_id = ConfigService::create_backup(&config_path, None)?;
504
505 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 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}