Skip to main content

cc_switch/cli/
main.rs

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