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
221    // Override model if specified
222    if let Some(model_name) = model {
223        settings.model = Some(model_name.clone());
224    }
225
226    // Load existing settings and merge
227    let existing_settings = ClaudeSettings::from_file(settings_path)?;
228
229    // Backup current settings if requested
230    if backup {
231        backup_settings(settings_path)?;
232    }
233
234    // Confirm overwrite
235    if !yes {
236        let existing_masked = existing_settings.clone().mask_sensitive_data();
237        let new_masked = settings.clone().mask_sensitive_data();
238
239        let comparison = format_settings_comparison(&existing_masked, &new_masked);
240
241        if comparison == "Settings are identical." {
242            println!(
243                "{}",
244                style("Settings are already configured as requested.").green()
245            );
246            // Even if settings are identical, we still need to save them in case the user
247            // explicitly wanted to ensure these settings are applied
248            let final_settings = settings.merge_with(existing_settings);
249            final_settings.to_file(settings_path)?;
250            return Ok(());
251        }
252
253        println!("Changes to be applied:");
254        println!("{}", comparison);
255
256        if !confirm_action("Apply these changes?", false)? {
257            return Ok(());
258        }
259    }
260
261    let final_settings = settings.merge_with(existing_settings);
262
263    // Save settings
264    final_settings.to_file(settings_path)?;
265
266    println!(
267        "{} Applied template '{}' successfully!",
268        style("✓").green().bold(),
269        template_type
270    );
271
272    Ok(())
273}
274
275/// Apply a snapshot
276fn apply_snapshot_command(
277    snapshot_name: &str,
278    scope: &SnapshotScope,
279    model: &Option<String>,
280    settings_path: &PathBuf,
281    backup: bool,
282    yes: bool,
283) -> Result<()> {
284    let snapshots_dir = get_snapshots_dir();
285    let store = SnapshotStore::new(snapshots_dir);
286
287    let mut snapshot = store.load_by_name(snapshot_name)?;
288
289    // Filter settings by scope
290    snapshot.settings = snapshot.settings.filter_by_scope(scope);
291
292    // Override model if specified
293    if let Some(model_name) = model {
294        snapshot.settings.model = Some(model_name.clone());
295    }
296
297    // Load existing settings and merge
298    let existing_settings = ClaudeSettings::from_file(settings_path)?;
299
300    // Backup current settings if requested
301    if backup {
302        backup_settings(settings_path)?;
303    }
304
305    // Confirm overwrite
306    if !yes {
307        let existing_masked = existing_settings.clone().mask_sensitive_data();
308        let snapshot_masked = snapshot.settings.clone().mask_sensitive_data();
309
310        println!("Current settings:");
311        println!("{}", format_settings_for_display(&existing_masked, false));
312        println!("\nSnapshot settings:");
313        println!("{}", format_settings_for_display(&snapshot_masked, false));
314
315        if !confirm_action("Apply these settings?", false)? {
316            return Ok(());
317        }
318    }
319
320    let final_settings = snapshot.settings.merge_with(existing_settings);
321
322    // Save settings
323    final_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}