cc_switch/cli/
main.rs

1use crate::cli::completion::{generate_aliases, generate_completion, list_aliases_for_completion};
2use crate::cli::{Cli, Commands};
3use crate::config::types::{AddCommandParams, StorageMode};
4use crate::config::{ConfigStorage, Configuration, EnvironmentConfig, validate_alias_name};
5use crate::interactive::{handle_interactive_selection, read_input, read_sensitive_input};
6use anyhow::{Result, anyhow};
7use clap::Parser;
8use colored::*;
9use std::fs;
10use std::path::Path;
11use std::process::Command;
12use std::thread;
13use std::time::Duration;
14
15/// Parse storage mode string to StorageMode enum
16///
17/// # Arguments
18/// * `store_str` - String representation of storage mode ("env" or "config")
19///
20/// # Returns
21/// Result containing StorageMode or error if invalid
22fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
23    match store_str.to_lowercase().as_str() {
24        "env" => Ok(StorageMode::Env),
25        "config" => Ok(StorageMode::Config),
26        _ => Err(anyhow!(
27            "Invalid storage mode '{}'. Use 'env' or 'config'",
28            store_str
29        )),
30    }
31}
32
33/// Parse a configuration from a JSON file
34///
35/// # Arguments
36/// * `file_path` - Path to the JSON configuration file
37///
38/// # Returns
39/// Result containing a tuple of (alias_name, token, url, model, small_fast_model, max_thinking_tokens, api_timeout_ms, claude_code_disable_nonessential_traffic, anthropic_default_sonnet_model, anthropic_default_opus_model, anthropic_default_haiku_model)
40///
41/// # Errors
42/// Returns error if file cannot be read or parsed
43#[allow(clippy::type_complexity)]
44fn parse_config_from_file(
45    file_path: &str,
46) -> Result<(
47    String,
48    String,
49    String,
50    Option<String>,
51    Option<String>,
52    Option<u32>,
53    Option<u32>,
54    Option<u32>,
55    Option<String>,
56    Option<String>,
57    Option<String>,
58)> {
59    // Read the file
60    let file_content = fs::read_to_string(file_path)
61        .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
62
63    // Parse JSON
64    let json: serde_json::Value = serde_json::from_str(&file_content)
65        .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
66
67    // Extract env section
68    let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
69        anyhow!(
70            "File '{}' does not contain a valid 'env' section",
71            file_path
72        )
73    })?;
74
75    // Extract alias name from filename (without extension)
76    let path = Path::new(file_path);
77    let alias_name = path
78        .file_stem()
79        .and_then(|s| s.to_str())
80        .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
81        .to_string();
82
83    // Extract and map environment variables
84    let token = env
85        .get("ANTHROPIC_AUTH_TOKEN")
86        .and_then(|v| v.as_str())
87        .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
88        .to_string();
89
90    let url = env
91        .get("ANTHROPIC_BASE_URL")
92        .and_then(|v| v.as_str())
93        .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
94        .to_string();
95
96    let model = env
97        .get("ANTHROPIC_MODEL")
98        .and_then(|v| v.as_str())
99        .map(|s| s.to_string());
100
101    let small_fast_model = env
102        .get("ANTHROPIC_SMALL_FAST_MODEL")
103        .and_then(|v| v.as_str())
104        .map(|s| s.to_string());
105
106    let max_thinking_tokens = env
107        .get("ANTHROPIC_MAX_THINKING_TOKENS")
108        .and_then(|v| v.as_u64())
109        .map(|u| u as u32);
110
111    let api_timeout_ms = env
112        .get("API_TIMEOUT_MS")
113        .and_then(|v| v.as_u64())
114        .map(|u| u as u32);
115
116    let claude_code_disable_nonessential_traffic = env
117        .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
118        .and_then(|v| v.as_u64())
119        .map(|u| u as u32);
120
121    let anthropic_default_sonnet_model = env
122        .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
123        .and_then(|v| v.as_str())
124        .map(|s| s.to_string());
125
126    let anthropic_default_opus_model = env
127        .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
128        .and_then(|v| v.as_str())
129        .map(|s| s.to_string());
130
131    let anthropic_default_haiku_model = env
132        .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
133        .and_then(|v| v.as_str())
134        .map(|s| s.to_string());
135
136    Ok((
137        alias_name,
138        token,
139        url,
140        model,
141        small_fast_model,
142        max_thinking_tokens,
143        api_timeout_ms,
144        claude_code_disable_nonessential_traffic,
145        anthropic_default_sonnet_model,
146        anthropic_default_opus_model,
147        anthropic_default_haiku_model,
148    ))
149}
150
151/// Handle adding a configuration with all the new features
152///
153/// # Arguments
154/// * `params` - Parameters for the add command
155/// * `storage` - Mutable reference to config storage
156///
157/// # Errors
158/// Returns error if validation fails or user cancels interactive input
159fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
160    // If from-file is provided, parse the file and use those values
161    if let Some(file_path) = &params.from_file {
162        println!("Importing configuration from file: {}", file_path);
163
164        let (
165            file_alias_name,
166            file_token,
167            file_url,
168            file_model,
169            file_small_fast_model,
170            file_max_thinking_tokens,
171            file_api_timeout_ms,
172            file_claude_disable_nonessential_traffic,
173            file_sonnet_model,
174            file_opus_model,
175            file_haiku_model,
176        ) = parse_config_from_file(file_path)?;
177
178        // Use the file's alias name (ignoring the one provided via command line)
179        params.alias_name = file_alias_name;
180
181        // Override params with file values
182        params.token = Some(file_token);
183        params.url = Some(file_url);
184        params.model = file_model;
185        params.small_fast_model = file_small_fast_model;
186        params.max_thinking_tokens = file_max_thinking_tokens;
187        params.api_timeout_ms = file_api_timeout_ms;
188        params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
189        params.anthropic_default_sonnet_model = file_sonnet_model;
190        params.anthropic_default_opus_model = file_opus_model;
191        params.anthropic_default_haiku_model = file_haiku_model;
192
193        println!(
194            "Configuration '{}' will be imported from file",
195            params.alias_name
196        );
197    }
198
199    // Validate alias name
200    validate_alias_name(&params.alias_name)?;
201
202    // Check if alias already exists
203    if storage.get_configuration(&params.alias_name).is_some() && !params.force {
204        eprintln!("Configuration '{}' already exists.", params.alias_name);
205        eprintln!("Use --force to overwrite or choose a different alias name.");
206        return Ok(());
207    }
208
209    // Cannot use interactive mode with --from-file
210    if params.interactive && params.from_file.is_some() {
211        anyhow::bail!("Cannot use --interactive mode with --from-file");
212    }
213
214    // Determine token value
215    let final_token = if params.interactive {
216        if params.token.is_some() || params.token_arg.is_some() {
217            eprintln!(
218                "Warning: Token provided via flags/arguments will be ignored in interactive mode"
219            );
220        }
221        read_sensitive_input("Enter API token (sk-ant-xxx): ")?
222    } else {
223        match (&params.token, &params.token_arg) {
224            (Some(t), _) => t.clone(),
225            (None, Some(t)) => t.clone(),
226            (None, None) => {
227                anyhow::bail!(
228                    "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
229                );
230            }
231        }
232    };
233
234    // Determine URL value
235    let final_url = if params.interactive {
236        if params.url.is_some() || params.url_arg.is_some() {
237            eprintln!(
238                "Warning: URL provided via flags/arguments will be ignored in interactive mode"
239            );
240        }
241        read_input("Enter API URL (default: https://api.anthropic.com): ")?
242    } else {
243        match (&params.url, &params.url_arg) {
244            (Some(u), _) => u.clone(),
245            (None, Some(u)) => u.clone(),
246            (None, None) => "https://api.anthropic.com".to_string(),
247        }
248    };
249
250    // Use default URL if empty
251    let final_url = if final_url.is_empty() {
252        "https://api.anthropic.com".to_string()
253    } else {
254        final_url
255    };
256
257    // Determine model value
258    let final_model = if params.interactive {
259        if params.model.is_some() {
260            eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
261        }
262        let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
263        if model_input.is_empty() {
264            None
265        } else {
266            Some(model_input)
267        }
268    } else {
269        params.model
270    };
271
272    // Determine small fast model value
273    let final_small_fast_model = if params.interactive {
274        if params.small_fast_model.is_some() {
275            eprintln!(
276                "Warning: Small fast model provided via flags will be ignored in interactive mode"
277            );
278        }
279        let small_model_input =
280            read_input("Enter small fast model name (optional, press enter to skip): ")?;
281        if small_model_input.is_empty() {
282            None
283        } else {
284            Some(small_model_input)
285        }
286    } else {
287        params.small_fast_model
288    };
289
290    // Determine max thinking tokens value
291    let final_max_thinking_tokens = if params.interactive {
292        if params.max_thinking_tokens.is_some() {
293            eprintln!(
294                "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
295            );
296        }
297        let tokens_input = read_input(
298            "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
299        )?;
300        if tokens_input.is_empty() {
301            None
302        } else if let Ok(tokens) = tokens_input.parse::<u32>() {
303            if tokens == 0 { None } else { Some(tokens) }
304        } else {
305            eprintln!("Warning: Invalid max thinking tokens value, skipping");
306            None
307        }
308    } else {
309        params.max_thinking_tokens
310    };
311
312    // Determine API timeout value
313    let final_api_timeout_ms = if params.interactive {
314        if params.api_timeout_ms.is_some() {
315            eprintln!(
316                "Warning: API timeout provided via flags will be ignored in interactive mode"
317            );
318        }
319        let timeout_input = read_input(
320            "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
321        )?;
322        if timeout_input.is_empty() {
323            None
324        } else if let Ok(timeout) = timeout_input.parse::<u32>() {
325            if timeout == 0 { None } else { Some(timeout) }
326        } else {
327            eprintln!("Warning: Invalid API timeout value, skipping");
328            None
329        }
330    } else {
331        params.api_timeout_ms
332    };
333
334    // Determine disable nonessential traffic flag value
335    let final_claude_code_disable_nonessential_traffic = if params.interactive {
336        if params.claude_code_disable_nonessential_traffic.is_some() {
337            eprintln!(
338                "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
339            );
340        }
341        let flag_input = read_input(
342            "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
343        )?;
344        if flag_input.is_empty() {
345            None
346        } else if let Ok(flag) = flag_input.parse::<u32>() {
347            if flag == 0 { None } else { Some(flag) }
348        } else {
349            eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
350            None
351        }
352    } else {
353        params.claude_code_disable_nonessential_traffic
354    };
355
356    // Determine default Sonnet model value
357    let final_anthropic_default_sonnet_model = if params.interactive {
358        if params.anthropic_default_sonnet_model.is_some() {
359            eprintln!(
360                "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
361            );
362        }
363        let model_input =
364            read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
365        if model_input.is_empty() {
366            None
367        } else {
368            Some(model_input)
369        }
370    } else {
371        params.anthropic_default_sonnet_model
372    };
373
374    // Determine default Opus model value
375    let final_anthropic_default_opus_model = if params.interactive {
376        if params.anthropic_default_opus_model.is_some() {
377            eprintln!(
378                "Warning: Default Opus model provided via flags will be ignored in interactive mode"
379            );
380        }
381        let model_input =
382            read_input("Enter default Opus model name (optional, press enter to skip): ")?;
383        if model_input.is_empty() {
384            None
385        } else {
386            Some(model_input)
387        }
388    } else {
389        params.anthropic_default_opus_model
390    };
391
392    // Determine default Haiku model value
393    let final_anthropic_default_haiku_model = if params.interactive {
394        if params.anthropic_default_haiku_model.is_some() {
395            eprintln!(
396                "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
397            );
398        }
399        let model_input =
400            read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
401        if model_input.is_empty() {
402            None
403        } else {
404            Some(model_input)
405        }
406    } else {
407        params.anthropic_default_haiku_model
408    };
409
410    // Validate token format with flexible API provider support
411    let is_anthropic_official = final_url.contains("api.anthropic.com");
412    if is_anthropic_official {
413        if !final_token.starts_with("sk-ant-api03-") {
414            eprintln!(
415                "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
416            );
417        }
418    } else {
419        // For non-official APIs, provide general guidance
420        if final_token.starts_with("sk-ant-api03-") {
421            eprintln!("Warning: Using official Claude token format with non-official API endpoint");
422        }
423        // Don't validate format for third-party APIs as they may use different formats
424    }
425
426    // Create and add configuration
427    let config = Configuration {
428        alias_name: params.alias_name.clone(),
429        token: final_token,
430        url: final_url,
431        model: final_model,
432        small_fast_model: final_small_fast_model,
433        max_thinking_tokens: final_max_thinking_tokens,
434        api_timeout_ms: final_api_timeout_ms,
435        claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
436        anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
437        anthropic_default_opus_model: final_anthropic_default_opus_model,
438        anthropic_default_haiku_model: final_anthropic_default_haiku_model,
439    };
440
441    storage.add_configuration(config);
442    storage.save()?;
443
444    println!("Configuration '{}' added successfully", params.alias_name);
445    if params.force {
446        println!("(Overwrote existing configuration)");
447    }
448
449    Ok(())
450}
451
452/// Handle configuration switching command
453///
454/// Processes the switch subcommand to switch Claude API configuration:
455/// - None: Enter interactive selection mode
456/// - "cc": Launch Claude without custom environment variables (default behavior)
457/// - Other alias: Launch Claude with the specified configuration's environment variables
458///
459/// After switching, displays the current URL and automatically launches Claude CLI
460///
461/// # Arguments
462/// * `alias_name` - Optional name of configuration to switch to, or "cc" for default
463/// * `store` - Storage mode to use (None = default to env)
464///
465/// # Errors
466/// Returns error if configuration is not found or Claude CLI fails to launch
467pub fn handle_switch_command(alias_name: Option<&str>, store: Option<StorageMode>) -> Result<()> {
468    let storage = ConfigStorage::load()?;
469
470    // If no alias provided, enter interactive mode
471    if alias_name.is_none() {
472        return handle_interactive_selection(&storage);
473    }
474
475    let alias_name = alias_name.unwrap();
476
477    // Use global storage mode: --store flag > saved default > env default
478    let mode_to_use = store.unwrap_or_else(|| {
479        storage
480            .default_storage_mode
481            .clone()
482            .unwrap_or_else(StorageMode::default)
483    });
484
485    let env_config = if alias_name == "cc" {
486        // Default operation: launch Claude without custom environment variables
487        println!("Using default Claude configuration (no custom API settings)");
488        println!("Current URL: Default (api.anthropic.com)");
489
490        // Remove any existing Anthropic settings from settings.json
491        let mut settings = crate::config::types::ClaudeSettings::load(
492            storage.get_claude_settings_dir().map(|s| s.as_str()),
493        )?;
494        settings.remove_anthropic_env();
495        settings.remove_anthropic_config_mode();
496        settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
497
498        EnvironmentConfig::empty()
499    } else if let Some(config) = storage.get_configuration(alias_name) {
500        let mut config_info = format!("token: {}, url: {}", config.token, config.url);
501        if let Some(model) = &config.model {
502            config_info.push_str(&format!(", model: {model}"));
503        }
504        if let Some(small_fast_model) = &config.small_fast_model {
505            config_info.push_str(&format!(", small_fast_model: {small_fast_model}"));
506        }
507        if let Some(max_thinking_tokens) = config.max_thinking_tokens {
508            config_info.push_str(&format!(", max_thinking_tokens: {max_thinking_tokens}"));
509        }
510        if let Some(api_timeout_ms) = config.api_timeout_ms {
511            config_info.push_str(&format!(", api_timeout_ms: {api_timeout_ms}"));
512        }
513        if let Some(claude_code_disable_nonessential_traffic) =
514            config.claude_code_disable_nonessential_traffic
515        {
516            config_info.push_str(&format!(
517                ", disable_nonessential_traffic: {claude_code_disable_nonessential_traffic}"
518            ));
519        }
520        if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
521            config_info.push_str(&format!(", default_sonnet_model: {sonnet_model}"));
522        }
523        if let Some(opus_model) = &config.anthropic_default_opus_model {
524            config_info.push_str(&format!(", default_opus_model: {opus_model}"));
525        }
526        if let Some(haiku_model) = &config.anthropic_default_haiku_model {
527            config_info.push_str(&format!(", default_haiku_model: {haiku_model}"));
528        }
529
530        println!("Switched to configuration '{alias_name}' ({config_info})");
531        println!("Current URL: {}", config.url);
532
533        let mut settings = crate::config::types::ClaudeSettings::load(
534            storage.get_claude_settings_dir().map(|s| s.as_str()),
535        )?;
536        settings.switch_to_config_with_mode(
537            config,
538            mode_to_use,
539            storage.get_claude_settings_dir().map(|s| s.as_str()),
540        )?;
541
542        EnvironmentConfig::from_config(config)
543    } else {
544        anyhow::bail!(
545            "Configuration '{}' not found. Use 'list' command to see available configurations.",
546            alias_name
547        );
548    };
549
550    // Wait 0.2 second
551    println!("Waiting 0.2 second before launching Claude...");
552    println!(
553        "Executing: claude {}",
554        "--dangerously-skip-permissions".red()
555    );
556    thread::sleep(Duration::from_millis(200));
557
558    // Set environment variables for current process
559    for (key, value) in env_config.as_env_tuples() {
560        unsafe {
561            std::env::set_var(&key, &value);
562        }
563    }
564
565    // Launch Claude CLI with exec to replace current process
566    println!("Launching Claude CLI...");
567
568    // On Unix systems, use exec to replace current process
569    #[cfg(unix)]
570    {
571        use std::os::unix::process::CommandExt;
572        let error = Command::new("claude")
573            .arg("--dangerously-skip-permissions")
574            .exec();
575        // exec never returns on success, so if we get here, it failed
576        anyhow::bail!("Failed to exec claude: {}", error);
577    }
578
579    // On non-Unix systems, fallback to spawn and wait
580    #[cfg(not(unix))]
581    {
582        use std::process::Stdio;
583        let mut child = Command::new("claude")
584            .arg("--dangerously-skip-permissions")
585            .stdin(Stdio::inherit())
586            .stdout(Stdio::inherit())
587            .stderr(Stdio::inherit())
588            .spawn()
589            .context(
590                "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
591            )?;
592
593        // Wait for the Claude process to finish and pass control to it
594        let status = child
595            .wait()
596            .context("Failed to wait for Claude CLI process")?;
597
598        if !status.success() {
599            anyhow::bail!("Claude CLI exited with error status: {}", status);
600        }
601    }
602}
603
604/// Main entry point for the CLI application
605///
606/// Parses command-line arguments and executes the appropriate action:
607/// - Switch configuration with `-c` flag
608/// - Execute subcommands (add, remove, list)
609/// - Show help if no arguments provided
610///
611/// # Errors
612/// Returns error if any operation fails (file I/O, parsing, etc.)
613pub fn run() -> Result<()> {
614    let cli = Cli::parse();
615
616    // Handle --migrate flag: migrate old path to new path and exit
617    if cli.migrate {
618        ConfigStorage::migrate_from_old_path()?;
619        return Ok(());
620    }
621
622    // Handle --list-aliases flag for completion
623    if cli.list_aliases {
624        list_aliases_for_completion()?;
625        return Ok(());
626    }
627
628    // Handle --store flag: set default storage mode and exit
629    if let Some(ref store_str) = cli.store
630        && cli.command.is_none()
631    {
632        // No command provided, so --store is a setter
633        let mode = match parse_storage_mode(store_str) {
634            Ok(mode) => mode,
635            Err(e) => {
636                eprintln!("Error: {}", e);
637                std::process::exit(1);
638            }
639        };
640
641        let mut storage = ConfigStorage::load()?;
642        storage.default_storage_mode = Some(mode.clone());
643        storage.save()?;
644
645        let mode_str = match mode {
646            StorageMode::Env => "env",
647            StorageMode::Config => "config",
648        };
649
650        println!("Default storage mode set to: {}", mode_str);
651        return Ok(());
652    }
653
654    // Handle subcommands
655    if let Some(command) = cli.command {
656        let mut storage = ConfigStorage::load()?;
657
658        match command {
659            Commands::Add {
660                alias_name,
661                token,
662                url,
663                model,
664                small_fast_model,
665                max_thinking_tokens,
666                api_timeout_ms,
667                claude_code_disable_nonessential_traffic,
668                anthropic_default_sonnet_model,
669                anthropic_default_opus_model,
670                anthropic_default_haiku_model,
671                force,
672                interactive,
673                token_arg,
674                url_arg,
675                from_file,
676            } => {
677                // When from_file is provided, alias_name will be extracted from the file
678                // For other cases, use the provided alias_name or provide a default
679                let final_alias_name = if from_file.is_some() {
680                    // Will be set from file parsing, use a placeholder for now
681                    "placeholder".to_string()
682                } else {
683                    alias_name.unwrap_or_else(|| {
684                        eprintln!("Error: alias_name is required when not using --from-file");
685                        std::process::exit(1);
686                    })
687                };
688
689                let params = AddCommandParams {
690                    alias_name: final_alias_name,
691                    token,
692                    url,
693                    model,
694                    small_fast_model,
695                    max_thinking_tokens,
696                    api_timeout_ms,
697                    claude_code_disable_nonessential_traffic,
698                    anthropic_default_sonnet_model,
699                    anthropic_default_opus_model,
700                    anthropic_default_haiku_model,
701                    force,
702                    interactive,
703                    token_arg,
704                    url_arg,
705                    from_file,
706                };
707                handle_add_command(params, &mut storage)?;
708            }
709            Commands::Remove { alias_names } => {
710                let mut removed_count = 0;
711                let mut not_found_aliases = Vec::new();
712
713                for alias_name in &alias_names {
714                    if storage.remove_configuration(alias_name) {
715                        removed_count += 1;
716                        println!("Configuration '{alias_name}' removed successfully");
717                    } else {
718                        not_found_aliases.push(alias_name.clone());
719                        println!("Configuration '{alias_name}' not found");
720                    }
721                }
722
723                if removed_count > 0 {
724                    storage.save()?;
725                }
726
727                if !not_found_aliases.is_empty() {
728                    eprintln!(
729                        "Warning: The following configurations were not found: {}",
730                        not_found_aliases.join(", ")
731                    );
732                }
733
734                if removed_count > 0 {
735                    println!("Successfully removed {removed_count} configuration(s)");
736                }
737            }
738            Commands::List { plain } => {
739                if plain {
740                    // Text output when -p flag is used
741                    if storage.configurations.is_empty() {
742                        println!("No configurations stored");
743                    } else {
744                        println!("Stored configurations:");
745                        for (alias_name, config) in &storage.configurations {
746                            let mut info = format!("token={}, url={}", config.token, config.url);
747                            if let Some(model) = &config.model {
748                                info.push_str(&format!(", model={model}"));
749                            }
750                            if let Some(small_fast_model) = &config.small_fast_model {
751                                info.push_str(&format!(", small_fast_model={small_fast_model}"));
752                            }
753                            if let Some(max_thinking_tokens) = config.max_thinking_tokens {
754                                info.push_str(&format!(
755                                    ", max_thinking_tokens={max_thinking_tokens}"
756                                ));
757                            }
758                            println!("  {alias_name}: {info}");
759                        }
760                    }
761                } else {
762                    // JSON output (default)
763                    println!(
764                        "{}",
765                        serde_json::to_string_pretty(&storage.configurations)
766                            .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
767                    );
768                }
769            }
770            Commands::Completion { shell } => {
771                generate_completion(&shell)?;
772            }
773            Commands::Alias { shell } => {
774                generate_aliases(&shell)?;
775            }
776            Commands::Use { alias_name } => {
777                // Determine storage mode: --store flag > saved default > default
778                let parsed_store = match cli.store {
779                    Some(ref store_str) => match parse_storage_mode(store_str) {
780                        Ok(mode) => Some(mode),
781                        Err(e) => {
782                            eprintln!("Error: {}", e);
783                            std::process::exit(1);
784                        }
785                    },
786                    None => storage.default_storage_mode.clone(),
787                };
788                handle_switch_command(Some(&alias_name), parsed_store)?;
789            }
790            Commands::Version => {
791                println!("{}", env!("CARGO_PKG_VERSION"));
792            }
793        }
794    } else {
795        // No command provided, show interactive configuration selection
796        let storage = ConfigStorage::load()?;
797        handle_interactive_selection(&storage)?;
798    }
799
800    Ok(())
801}