claude_code_switcher/
commands.rs

1use crate::{
2    Configurable, CredentialManager,
3    credentials::{CredentialStore, get_api_key_interactively},
4    settings::{ClaudeSettings, format_settings_for_display},
5    snapshots::{SnapshotScope, SnapshotStore},
6    templates::{TemplateType, get_template, get_template_type},
7    utils::{
8        backup_settings, confirm_action, get_credentials_dir, get_settings_path, get_snapshots_dir,
9    },
10};
11use anyhow::{Result, anyhow};
12use console::style;
13use std::path::PathBuf;
14
15/// Run a command based on CLI arguments
16pub fn run_command(args: &crate::Cli) -> Result<()> {
17    match &args.command {
18        crate::Commands::List { verbose } => list_command(*verbose)?,
19        crate::Commands::Snap {
20            name,
21            scope,
22            settings_path,
23            description,
24            overwrite,
25        } => snap_command(name, scope, settings_path, description, *overwrite)?,
26        crate::Commands::Apply {
27            target,
28            scope,
29            model,
30            settings_path,
31            backup,
32            yes,
33        } => apply_command(target, scope, model, settings_path, *backup, *yes)?,
34        crate::Commands::Delete { name, yes } => delete_command(name, *yes)?,
35        crate::Commands::Credentials(credential_commands) => match credential_commands {
36            crate::CredentialCommands::List => credentials_list_command()?,
37            crate::CredentialCommands::Delete { id } => credentials_delete_command(id)?,
38            crate::CredentialCommands::Clear { yes } => credentials_clear_command(*yes)?,
39        },
40    }
41    Ok(())
42}
43
44/// List available snapshots
45pub fn list_command(verbose: bool) -> Result<()> {
46    let snapshots_dir = crate::utils::get_snapshots_dir();
47    let store = SnapshotStore::new(snapshots_dir);
48    let snapshots = store.list()?;
49
50    if snapshots.is_empty() {
51        println!("No snapshots found.");
52        return Ok(());
53    }
54
55    println!("Available snapshots ({} total):", snapshots.len());
56
57    for snapshot in &snapshots {
58        if verbose {
59            println!("\n{} {}", style("Name:").bold(), snapshot.name);
60            println!("{} {}", style("ID:").bold(), snapshot.id);
61            if let Some(ref desc) = snapshot.description {
62                println!("{} {}", style("Description:").bold(), desc);
63            }
64            println!("{} {}", style("Scope:").bold(), snapshot.scope);
65            println!("{} {}", style("Created:").bold(), snapshot.created_at);
66            println!("{} {}", style("Updated:").bold(), snapshot.updated_at);
67
68            let masked_settings = snapshot.settings.clone().mask_sensitive_data();
69            println!(
70                "{}\n{}",
71                style("Settings:").bold(),
72                format_settings_for_display(&masked_settings, true)
73            );
74        } else {
75            println!(
76                "{}: {} (scope: {}, created: {})",
77                style(&snapshot.name).cyan().bold(),
78                snapshot.id,
79                snapshot.scope,
80                snapshot.created_at
81            );
82        }
83        println!();
84    }
85
86    Ok(())
87}
88
89/// Create a snapshot
90pub fn snap_command(
91    name: &str,
92    scope: &SnapshotScope,
93    settings_path: &Option<PathBuf>,
94    description: &Option<String>,
95    overwrite: bool,
96) -> Result<()> {
97    let settings_path = get_settings_path(settings_path.clone());
98    let settings = ClaudeSettings::from_file(&settings_path)?;
99
100    // Capture environment variables if needed
101    let mut snapshot_settings = settings;
102
103    if matches!(scope, SnapshotScope::All | SnapshotScope::Env) {
104        snapshot_settings.environment = Some(ClaudeSettings::capture_environment());
105    }
106
107    let snapshots_dir = crate::utils::get_snapshots_dir();
108    let store = SnapshotStore::new(snapshots_dir);
109
110    if store.exists_by_name(name) && !overwrite {
111        if !confirm_action(
112            &format!("Snapshot '{}' already exists. Overwrite?", name),
113            false,
114        )? {
115            return Ok(());
116        }
117    }
118
119    let snapshot = crate::Snapshot::new(
120        name.to_string(),
121        snapshot_settings,
122        scope.clone(),
123        description.clone(),
124    );
125
126    store.save(&snapshot)?;
127    println!(
128        "{} Snapshot '{}' created successfully!",
129        style("✓").green().bold(),
130        name
131    );
132
133    Ok(())
134}
135
136/// Apply a snapshot or template
137pub fn apply_command(
138    target: &str,
139    scope: &SnapshotScope,
140    model: &Option<String>,
141    settings_path: &Option<PathBuf>,
142    backup: bool,
143    yes: bool,
144) -> Result<()> {
145    let settings_path = get_settings_path(settings_path.clone());
146
147    // Try to parse as template type first
148    if let Ok(template_type) = get_template_type(target) {
149        return apply_template_command(&template_type, scope, model, &settings_path, backup, yes);
150    }
151
152    // Otherwise treat as snapshot name
153    apply_snapshot_command(target, scope, model, &settings_path, backup, yes)
154}
155
156/// Apply a template
157fn apply_template_command(
158    template_type: &TemplateType,
159    scope: &SnapshotScope,
160    model: &Option<String>,
161    settings_path: &PathBuf,
162    backup: bool,
163    yes: bool,
164) -> Result<()> {
165    let template = get_template(template_type);
166    // Get API key
167    let api_key = get_api_key_interactively(template_type.clone())?;
168
169    let mut settings = template(&api_key, scope);
170
171    // Override model if specified
172    if let Some(model_name) = model {
173        if let Some(ref mut model_config) = settings.model {
174            model_config.name = model_name.clone();
175        }
176    }
177
178    // Load existing settings and merge
179    let existing_settings = ClaudeSettings::from_file(settings_path)?;
180
181    // Backup current settings if requested
182    if backup {
183        backup_settings(settings_path)?;
184    }
185
186    // Confirm overwrite
187    if !yes {
188        let existing_masked = existing_settings.clone().mask_sensitive_data();
189        let new_masked = settings.clone().mask_sensitive_data();
190
191        println!("Current settings:");
192        println!("{}", format_settings_for_display(&existing_masked, false));
193        println!("\nNew settings:");
194        println!("{}", format_settings_for_display(&new_masked, false));
195
196        if !confirm_action("Apply these settings?", false)? {
197            return Ok(());
198        }
199    }
200
201    let final_settings = settings.merge_with(existing_settings);
202
203    // Save settings
204    final_settings.to_file(settings_path)?;
205
206    println!(
207        "{} Applied template '{}' successfully!",
208        style("✓").green().bold(),
209        template_type
210    );
211
212    Ok(())
213}
214
215/// Apply a snapshot
216fn apply_snapshot_command(
217    snapshot_name: &str,
218    scope: &SnapshotScope,
219    model: &Option<String>,
220    settings_path: &PathBuf,
221    backup: bool,
222    yes: bool,
223) -> Result<()> {
224    let snapshots_dir = get_snapshots_dir();
225    let store = SnapshotStore::new(snapshots_dir);
226
227    let mut snapshot = store.load_by_name(snapshot_name)?;
228
229    // Filter settings by scope
230    snapshot.settings = snapshot.settings.filter_by_scope(scope);
231
232    // Override model if specified
233    if let Some(model_name) = model {
234        if let Some(ref mut model_config) = snapshot.settings.model {
235            model_config.name = model_name.clone();
236        }
237    }
238
239    // Load existing settings and merge
240    let existing_settings = ClaudeSettings::from_file(settings_path)?;
241
242    // Backup current settings if requested
243    if backup {
244        backup_settings(settings_path)?;
245    }
246
247    // Confirm overwrite
248    if !yes {
249        let existing_masked = existing_settings.clone().mask_sensitive_data();
250        let snapshot_masked = snapshot.settings.clone().mask_sensitive_data();
251
252        println!("Current settings:");
253        println!("{}", format_settings_for_display(&existing_masked, false));
254        println!("\nSnapshot settings:");
255        println!("{}", format_settings_for_display(&snapshot_masked, false));
256
257        if !confirm_action("Apply these settings?", false)? {
258            return Ok(());
259        }
260    }
261
262    let final_settings = snapshot.settings.merge_with(existing_settings);
263
264    // Save settings
265    final_settings.to_file(settings_path)?;
266
267    println!(
268        "{} Applied snapshot '{}' successfully!",
269        style("✓").green().bold(),
270        snapshot_name
271    );
272
273    Ok(())
274}
275
276/// Delete a snapshot
277pub fn delete_command(name: &str, yes: bool) -> Result<()> {
278    let snapshots_dir = get_snapshots_dir();
279    let store = SnapshotStore::new(snapshots_dir);
280
281    if !store.exists_by_name(name) {
282        return Err(anyhow!("Snapshot '{}' not found", name));
283    }
284
285    if !yes {
286        if !confirm_action(&format!("Delete snapshot '{}'?", name), false)? {
287            return Ok(());
288        }
289    }
290
291    store.delete_by_name(name)?;
292    println!(
293        "{} Deleted snapshot '{}' successfully!",
294        style("✓").green().bold(),
295        name
296    );
297
298    Ok(())
299}
300
301/// List saved credentials
302pub fn credentials_list_command() -> Result<()> {
303    let _credentials_dir = get_credentials_dir();
304    let credential_store = CredentialStore::new()?;
305
306    let credentials = credential_store.load_credentials()?;
307
308    if credentials.is_empty() {
309        println!("No saved credentials found.");
310        return Ok(());
311    }
312
313    println!("Saved credentials ({} total):", credentials.len());
314
315    for credential in &credentials {
316        let template_type = credential.template_type();
317        let masked_key = mask_api_key(credential.api_key());
318
319        println!(
320            "{}: {} ({} - {})",
321            style(credential.id()).cyan().bold(),
322            credential.name(),
323            template_type,
324            masked_key
325        );
326    }
327
328    Ok(())
329}
330
331/// Delete a credential
332pub fn credentials_delete_command(id: &str) -> Result<()> {
333    let _credentials_dir = get_credentials_dir();
334    let credential_store = CredentialStore::new()?;
335
336    if credential_store.delete_credential(id).is_err() {
337        return Err(anyhow!("Credential '{}' not found", id));
338    }
339
340    println!(
341        "{} Deleted credential '{}' successfully!",
342        style("✓").green().bold(),
343        id
344    );
345
346    Ok(())
347}
348
349/// Clear all credentials
350pub fn credentials_clear_command(yes: bool) -> Result<()> {
351    if !yes {
352        if !confirm_action("Clear all saved credentials?", false)? {
353            return Ok(());
354        }
355    }
356
357    let _credentials_dir = get_credentials_dir();
358    let credential_store = CredentialStore::new()?;
359
360    credential_store.clear_credentials()?;
361
362    println!("{} Cleared all credentials!", style("✓").green().bold());
363
364    Ok(())
365}
366
367/// Helper function to mask API key for display
368fn mask_api_key(api_key: &str) -> String {
369    if api_key.len() <= 8 {
370        "••••••••".to_string()
371    } else {
372        format!("{}••••••••", &api_key[..api_key.len().min(8)])
373    }
374}