Skip to main content

cc_switch/cli/
main.rs

1use crate::cli::completion::{generate_completion, list_aliases_for_completion, list_codex_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 --list-codex-aliases flag for completion
565    if cli.list_codex_aliases {
566        list_codex_aliases_for_completion()?;
567        return Ok(());
568    }
569
570    // Handle --store flag: set default storage mode and exit
571    if let Some(ref store_str) = cli.store
572        && cli.command.is_none()
573    {
574        // No command provided, so --store is a setter
575        let mode = match parse_storage_mode(store_str) {
576            Ok(mode) => mode,
577            Err(e) => {
578                eprintln!("Error: {}", e);
579                std::process::exit(1);
580            }
581        };
582
583        let mut storage = ConfigStorage::load()?;
584        storage.default_storage_mode = Some(mode.clone());
585        storage.save()?;
586
587        let mode_str = match mode {
588            StorageMode::Env => "env",
589            StorageMode::Config => "config",
590        };
591
592        println!("Default storage mode set to: {}", mode_str);
593        return Ok(());
594    }
595
596    // Handle subcommands
597    if let Some(command) = cli.command {
598        let mut storage = ConfigStorage::load()?;
599
600        match command {
601            Commands::Add {
602                alias_name,
603                token,
604                url,
605                model,
606                small_fast_model,
607                max_thinking_tokens,
608                api_timeout_ms,
609                claude_code_disable_nonessential_traffic,
610                anthropic_default_sonnet_model,
611                anthropic_default_opus_model,
612                anthropic_default_haiku_model,
613                claude_code_subagent_model,
614                claude_code_disable_nonstreaming_fallback,
615                claude_code_effort_level,
616                force,
617                interactive,
618                token_arg,
619                url_arg,
620                from_file,
621            } => {
622                // When from_file is provided, alias_name will be extracted from the file
623                // For other cases, use the provided alias_name or provide a default
624                let final_alias_name = if from_file.is_some() {
625                    // Will be set from file parsing, use a placeholder for now
626                    "placeholder".to_string()
627                } else {
628                    alias_name.unwrap_or_else(|| {
629                        eprintln!("Error: alias_name is required when not using --from-file");
630                        std::process::exit(1);
631                    })
632                };
633
634                let params = AddCommandParams {
635                    alias_name: final_alias_name,
636                    token,
637                    url,
638                    model,
639                    small_fast_model,
640                    max_thinking_tokens,
641                    api_timeout_ms,
642                    claude_code_disable_nonessential_traffic,
643                    anthropic_default_sonnet_model,
644                    anthropic_default_opus_model,
645                    anthropic_default_haiku_model,
646                    claude_code_subagent_model,
647                    claude_code_disable_nonstreaming_fallback,
648                    claude_code_effort_level,
649                    force,
650                    interactive,
651                    token_arg,
652                    url_arg,
653                    from_file,
654                };
655                handle_add_command(params, &mut storage)?;
656            }
657            Commands::Remove { alias_names } => {
658                let mut removed_count = 0;
659                let mut not_found_aliases = Vec::new();
660
661                for alias_name in &alias_names {
662                    if storage.remove_configuration(alias_name) {
663                        removed_count += 1;
664                        println!("Configuration '{alias_name}' removed successfully");
665                    } else {
666                        not_found_aliases.push(alias_name.clone());
667                        println!("Configuration '{alias_name}' not found");
668                    }
669                }
670
671                if removed_count > 0 {
672                    storage.save()?;
673                }
674
675                if !not_found_aliases.is_empty() {
676                    eprintln!(
677                        "Warning: The following configurations were not found: {}",
678                        not_found_aliases.join(", ")
679                    );
680                }
681
682                if removed_count > 0 {
683                    println!("Successfully removed {removed_count} configuration(s)");
684                }
685            }
686            Commands::List { plain } => {
687                if plain {
688                    // Text output when -p flag is used
689                    if storage.configurations.is_empty() {
690                        println!("No configurations stored");
691                    } else {
692                        println!("Stored configurations:");
693                        for (alias_name, config) in &storage.configurations {
694                            let mut info = format!("token={}, url={}", config.token, config.url);
695                            if let Some(model) = &config.model {
696                                info.push_str(&format!(", model={model}"));
697                            }
698                            if let Some(small_fast_model) = &config.small_fast_model {
699                                info.push_str(&format!(", small_fast_model={small_fast_model}"));
700                            }
701                            if let Some(max_thinking_tokens) = config.max_thinking_tokens {
702                                info.push_str(&format!(
703                                    ", max_thinking_tokens={max_thinking_tokens}"
704                                ));
705                            }
706                            if let Some(subagent_model) = &config.claude_code_subagent_model {
707                                info.push_str(&format!(", subagent_model={subagent_model}"));
708                            }
709                            if let Some(flag) = config.claude_code_disable_nonstreaming_fallback {
710                                info.push_str(&format!(", disable_nonstreaming_fallback={flag}"));
711                            }
712                            if let Some(effort_level) = &config.claude_code_effort_level {
713                                info.push_str(&format!(", effort_level={effort_level}"));
714                            }
715                            println!("  {alias_name}: {info}");
716                        }
717                    }
718                } else {
719                    // JSON output (default)
720                    println!(
721                        "{}",
722                        serde_json::to_string_pretty(&storage.configurations)
723                            .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
724                    );
725                }
726            }
727            Commands::Completion { shell } => {
728                generate_completion(&shell)?;
729            }
730            Commands::Use {
731                alias_name,
732                resume,
733                r#continue,
734                prompt,
735            } => {
736                // Handle special reset aliases
737                if alias_name == "cc" || alias_name == "official" {
738                    println!("Using official Claude configuration");
739
740                    let mut settings = ClaudeSettings::load(
741                        storage.get_claude_settings_dir().map(|s| s.as_str()),
742                    )?;
743                    settings.remove_anthropic_env();
744                    settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
745
746                    launch_claude_with_env(EnvironmentConfig::empty(), None, None, r#continue)?;
747                    return Ok(());
748                }
749
750                let config = storage
751                    .configurations
752                    .get(&alias_name)
753                    .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
754                    .clone();
755
756                let env_config = EnvironmentConfig::from_config(&config);
757                let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
758
759                // Update settings.json with the configuration
760                let mut settings =
761                    ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
762                settings.switch_to_config_with_mode(
763                    &config,
764                    storage_mode,
765                    storage.get_claude_settings_dir().map(|s| s.as_str()),
766                )?;
767
768                println!("Switched to configuration '{}'", alias_name);
769                println!("  URL:   {}", config.url);
770                println!(
771                    "  Token: {}",
772                    crate::cli::display_utils::format_token_for_display(&config.token)
773                );
774
775                let prompt_str = if prompt.is_empty() {
776                    None
777                } else {
778                    Some(prompt.join(" "))
779                };
780
781                launch_claude_with_env(
782                    env_config,
783                    prompt_str.as_deref(),
784                    resume.as_deref(),
785                    r#continue,
786                )?;
787            }
788            Commands::Codex { command } => match command {
789                Some(crate::cli::CodexCommands::Add {
790                    alias_name,
791                    api_key,
792                    force,
793                    interactive,
794                    from_file,
795                }) => {
796                    handle_codex_add(
797                        alias_name,
798                        api_key,
799                        force,
800                        interactive,
801                        from_file,
802                        &mut storage,
803                    )?;
804                }
805                Some(crate::cli::CodexCommands::List { plain }) => {
806                    handle_codex_list(plain, &storage)?;
807                }
808                Some(crate::cli::CodexCommands::Use {
809                    alias_name,
810                    r#continue,
811                    resume,
812                    prompt,
813                }) => {
814                    handle_codex_use(alias_name, r#continue, resume, prompt, &mut storage)?;
815                }
816                Some(crate::cli::CodexCommands::Remove { alias_names }) => {
817                    handle_codex_remove(alias_names, &mut storage)?;
818                }
819                None => {
820                    // Enter interactive mode for Codex configurations
821                    handle_codex_interactive(&storage)?;
822                }
823            },
824        }
825    } else {
826        // No command provided, show interactive configuration selection
827        let storage = ConfigStorage::load()?;
828        handle_interactive_selection(&storage)?;
829    }
830
831    Ok(())
832}