Skip to main content

cc_switch/cli/
main.rs

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