claude_code_switcher/
commands.rs

1use crate::{
2    Configurable, CredentialManager,
3    credentials::{CredentialStore, get_api_key_interactively},
4    settings::{ClaudeSettings, format_settings_comparison, format_settings_for_display},
5    snapshots::{SnapshotScope, SnapshotStore},
6    templates::{Template, TemplateType, get_template_instance_with_input, 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.env = 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)
111        && !overwrite
112        && !confirm_action(
113            &format!("Snapshot '{}' already exists. Overwrite?", name),
114            false,
115        )?
116    {
117        return Ok(());
118    }
119
120    let snapshot = crate::Snapshot::new(
121        name.to_string(),
122        snapshot_settings,
123        scope.clone(),
124        description.clone(),
125    );
126
127    store.save(&snapshot)?;
128    println!(
129        "{} Snapshot '{}' created successfully!",
130        style("✓").green().bold(),
131        name
132    );
133
134    Ok(())
135}
136
137/// Apply a snapshot or template
138pub fn apply_command(
139    target: &str,
140    scope: &SnapshotScope,
141    model: &Option<String>,
142    settings_path: &Option<PathBuf>,
143    backup: bool,
144    yes: bool,
145) -> Result<()> {
146    let settings_path = get_settings_path(settings_path.clone());
147
148    // Try to parse as template type first
149    if let Ok(template_type) = get_template_type(target) {
150        return apply_template_command(
151            &template_type,
152            target,
153            scope,
154            model,
155            &settings_path,
156            backup,
157            yes,
158        );
159    }
160
161    // Otherwise treat as snapshot name
162    apply_snapshot_command(target, scope, model, &settings_path, backup, yes)
163}
164
165/// Apply a template
166fn apply_template_command(
167    template_type: &TemplateType,
168    target: &str,
169    scope: &SnapshotScope,
170    model: &Option<String>,
171    settings_path: &PathBuf,
172    backup: bool,
173    yes: bool,
174) -> Result<()> {
175    // Get template instance with the original input to handle specific variants
176    let initial_template = get_template_instance_with_input(template_type, target);
177
178    // If template has variants and user didn't specify a specific one, let user choose interactively
179    let template_instance = if initial_template.has_variants()
180        && ((target == "kat-coder" || target == "katcoder" || target == "kat")
181            || (target == "kimi")
182            || (target == "zai" || target == "glm" || target == "zhipu"))
183    {
184        // Use template's interactive creation method
185        match template_type {
186            crate::templates::TemplateType::KatCoder => {
187                let kat_coder_template =
188                    crate::templates::kat_coder::KatCoderTemplate::create_interactively()?;
189                Box::new(kat_coder_template) as Box<dyn Template>
190            }
191            crate::templates::TemplateType::Kimi => {
192                let kimi_template = crate::templates::kimi::KimiTemplate::create_interactively()?;
193                Box::new(kimi_template) as Box<dyn Template>
194            }
195            crate::templates::TemplateType::Zai => {
196                let zai_template = crate::templates::zai::ZaiTemplate::create_interactively()?;
197                Box::new(zai_template) as Box<dyn Template>
198            }
199            _ => initial_template,
200        }
201    } else {
202        initial_template
203    };
204
205    // Get API key - use the template instance's env var name for accuracy
206    let api_key = {
207        let env_var_name = template_instance.env_var_name();
208        if let Ok(api_key) = std::env::var(env_var_name)
209            && !api_key.trim().is_empty()
210        {
211            println!("✓ Using API key from environment variable {}", env_var_name);
212            api_key
213        } else {
214            // Fallback to general API key selection
215            get_api_key_interactively(template_type.clone())?
216        }
217    };
218
219    let mut settings = template_instance.create_settings(&api_key, scope);
220    // Debug: Print settings before applying
221    println!(
222        "{}",
223        style("DEBUG: Settings to be applied:").yellow().bold()
224    );
225    println!("{}", format_settings_for_display(&settings, true));
226    // Override model if specified
227    if let Some(model_name) = model {
228        settings.model = Some(model_name.clone());
229    }
230
231    // Load existing settings for comparison (will be replaced, not merged)
232    let existing_settings = ClaudeSettings::from_file(settings_path)?;
233
234    // Backup current settings if requested
235    if backup {
236        backup_settings(settings_path)?;
237    }
238
239    // Confirm overwrite
240    if !yes {
241        let existing_masked = existing_settings.clone().mask_sensitive_data();
242        let new_masked = settings.clone().mask_sensitive_data();
243
244        let comparison = format_settings_comparison(&existing_masked, &new_masked);
245
246        if comparison == "Settings are identical." {
247            println!(
248                "{}",
249                style("Settings are already configured as requested.").green()
250            );
251            // Even if settings are identical, we still need to save them in case the user
252            // explicitly wanted to ensure these settings are applied (replace mode)
253            settings.to_file(settings_path)?;
254            return Ok(());
255        }
256
257        println!("Changes to be applied:");
258        println!("{}", comparison);
259
260        if !confirm_action("Apply these changes?", false)? {
261            return Ok(());
262        }
263    }
264
265    // Save settings (replace mode - no merging)
266    settings.to_file(settings_path)?;
267
268    println!(
269        "{} Applied template '{}' successfully!",
270        style("✓").green().bold(),
271        template_type
272    );
273
274    Ok(())
275}
276
277/// Apply a snapshot
278fn apply_snapshot_command(
279    snapshot_name: &str,
280    scope: &SnapshotScope,
281    model: &Option<String>,
282    settings_path: &PathBuf,
283    backup: bool,
284    yes: bool,
285) -> Result<()> {
286    let snapshots_dir = get_snapshots_dir();
287    let store = SnapshotStore::new(snapshots_dir);
288
289    let mut snapshot = store.load_by_name(snapshot_name)?;
290
291    // Filter settings by scope
292    snapshot.settings = snapshot.settings.filter_by_scope(scope);
293
294    // Override model if specified
295    if let Some(model_name) = model {
296        snapshot.settings.model = Some(model_name.clone());
297    }
298
299    // Load existing settings for comparison (will be replaced, not merged)
300    let existing_settings = ClaudeSettings::from_file(settings_path)?;
301
302    // Backup current settings if requested
303    if backup {
304        backup_settings(settings_path)?;
305    }
306
307    // Confirm overwrite
308    if !yes {
309        let existing_masked = existing_settings.clone().mask_sensitive_data();
310        let snapshot_masked = snapshot.settings.clone().mask_sensitive_data();
311
312        println!("Current settings:");
313        println!("{}", format_settings_for_display(&existing_masked, false));
314        println!("\nSnapshot settings:");
315        println!("{}", format_settings_for_display(&snapshot_masked, false));
316
317        if !confirm_action("Apply these settings?", false)? {
318            return Ok(());
319        }
320    }
321
322    // Save settings (replace mode - no merging)
323    snapshot.settings.to_file(settings_path)?;
324
325    println!(
326        "{} Applied snapshot '{}' successfully!",
327        style("✓").green().bold(),
328        snapshot_name
329    );
330
331    Ok(())
332}
333
334/// Delete a snapshot
335pub fn delete_command(name: &str, yes: bool) -> Result<()> {
336    let snapshots_dir = get_snapshots_dir();
337    let store = SnapshotStore::new(snapshots_dir);
338
339    if !store.exists_by_name(name) {
340        return Err(anyhow!("Snapshot '{}' not found", name));
341    }
342
343    if !yes && !confirm_action(&format!("Delete snapshot '{}'?", name), false)? {
344        return Ok(());
345    }
346
347    store.delete_by_name(name)?;
348    println!(
349        "{} Deleted snapshot '{}' successfully!",
350        style("✓").green().bold(),
351        name
352    );
353
354    Ok(())
355}
356
357/// List saved credentials
358pub fn credentials_list_command() -> Result<()> {
359    let _credentials_dir = get_credentials_dir();
360    let credential_store = CredentialStore::new()?;
361
362    let credentials = credential_store.load_credentials()?;
363
364    if credentials.is_empty() {
365        println!("No saved credentials found.");
366        return Ok(());
367    }
368
369    println!("Saved credentials ({} total):", credentials.len());
370
371    for credential in &credentials {
372        let template_type = credential.template_type();
373        let masked_key = mask_api_key(credential.api_key());
374
375        println!(
376            "{}: {} ({} - {})",
377            style(credential.id()).cyan().bold(),
378            credential.name(),
379            template_type,
380            masked_key
381        );
382    }
383
384    Ok(())
385}
386
387/// Delete a credential
388pub fn credentials_delete_command(id: &str) -> Result<()> {
389    let _credentials_dir = get_credentials_dir();
390    let credential_store = CredentialStore::new()?;
391
392    if credential_store.delete_credential(id).is_err() {
393        return Err(anyhow!("Credential '{}' not found", id));
394    }
395
396    println!(
397        "{} Deleted credential '{}' successfully!",
398        style("✓").green().bold(),
399        id
400    );
401
402    Ok(())
403}
404
405/// Clear all credentials
406pub fn credentials_clear_command(yes: bool) -> Result<()> {
407    if !yes && !confirm_action("Clear all saved credentials?", false)? {
408        return Ok(());
409    }
410
411    let _credentials_dir = get_credentials_dir();
412    let credential_store = CredentialStore::new()?;
413
414    credential_store.clear_credentials()?;
415
416    println!("{} Cleared all credentials!", style("✓").green().bold());
417
418    Ok(())
419}
420
421/// Helper function to mask API key for display
422fn mask_api_key(api_key: &str) -> String {
423    if api_key.len() <= 8 {
424        "••••••••".to_string()
425    } else {
426        format!("{}••••••••", &api_key[..api_key.len().min(8)])
427    }
428}