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
18fn 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#[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)> {
64 let file_content = fs::read_to_string(file_path)
65 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
66
67 let json: serde_json::Value = serde_json::from_str(&file_content)
68 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
69
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 let token = env
78 .get("ANTHROPIC_AUTH_TOKEN")
79 .and_then(|v| v.as_str())
80 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
81 .to_string();
82
83 let url = env
84 .get("ANTHROPIC_BASE_URL")
85 .and_then(|v| v.as_str())
86 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
87 .to_string();
88
89 let model = env
90 .get("ANTHROPIC_MODEL")
91 .and_then(|v| v.as_str())
92 .map(|s| s.to_string());
93
94 let small_fast_model = env
95 .get("ANTHROPIC_SMALL_FAST_MODEL")
96 .and_then(|v| v.as_str())
97 .map(|s| s.to_string());
98
99 let max_thinking_tokens = env
100 .get("ANTHROPIC_MAX_THINKING_TOKENS")
101 .and_then(|v| v.as_u64())
102 .map(|u| u as u32);
103
104 let api_timeout_ms = env
105 .get("API_TIMEOUT_MS")
106 .and_then(|v| v.as_u64())
107 .map(|u| u as u32);
108
109 let claude_code_disable_nonessential_traffic = env
110 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
111 .and_then(|v| v.as_u64())
112 .map(|u| u as u32);
113
114 let anthropic_default_sonnet_model = env
115 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
116 .and_then(|v| v.as_str())
117 .map(|s| s.to_string());
118
119 let anthropic_default_opus_model = env
120 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
121 .and_then(|v| v.as_str())
122 .map(|s| s.to_string());
123
124 let anthropic_default_haiku_model = env
125 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string());
128
129 let claude_code_subagent_model = env
130 .get("CLAUDE_CODE_SUBAGENT_MODEL")
131 .and_then(|v| v.as_str())
132 .map(|s| s.to_string());
133
134 let claude_code_disable_nonstreaming_fallback = env
135 .get("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
136 .and_then(|v| v.as_u64())
137 .map(|u| u as u32);
138
139 let claude_code_effort_level = env
140 .get("CLAUDE_CODE_EFFORT_LEVEL")
141 .and_then(|v| v.as_str())
142 .map(|s| s.to_string());
143
144 Ok((
145 token,
146 url,
147 model,
148 small_fast_model,
149 max_thinking_tokens,
150 api_timeout_ms,
151 claude_code_disable_nonessential_traffic,
152 anthropic_default_sonnet_model,
153 anthropic_default_opus_model,
154 anthropic_default_haiku_model,
155 claude_code_subagent_model,
156 claude_code_disable_nonstreaming_fallback,
157 claude_code_effort_level,
158 ))
159}
160
161fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
170 if let Some(file_path) = ¶ms.from_file {
172 println!("Importing configuration from file: {}", file_path);
173
174 let (
175 file_token,
176 file_url,
177 file_model,
178 file_small_fast_model,
179 file_max_thinking_tokens,
180 file_api_timeout_ms,
181 file_claude_disable_nonessential_traffic,
182 file_sonnet_model,
183 file_opus_model,
184 file_haiku_model,
185 file_subagent_model,
186 file_disable_nonstreaming_fallback,
187 file_effort_level,
188 ) = parse_config_from_file(file_path)?;
189
190 params.token = Some(file_token);
191 params.url = Some(file_url);
192 params.model = file_model;
193 params.small_fast_model = file_small_fast_model;
194 params.max_thinking_tokens = file_max_thinking_tokens;
195 params.api_timeout_ms = file_api_timeout_ms;
196 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
197 params.anthropic_default_sonnet_model = file_sonnet_model;
198 params.anthropic_default_opus_model = file_opus_model;
199 params.anthropic_default_haiku_model = file_haiku_model;
200 params.claude_code_subagent_model = file_subagent_model;
201 params.claude_code_disable_nonstreaming_fallback = file_disable_nonstreaming_fallback;
202 params.claude_code_effort_level = file_effort_level;
203
204 println!(
205 "Configuration '{}' will be imported from file",
206 params.alias_name
207 );
208 }
209
210 validate_alias_name(¶ms.alias_name)?;
212
213 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
215 eprintln!("Configuration '{}' already exists.", params.alias_name);
216 eprintln!("Use --force to overwrite or choose a different alias name.");
217 return Ok(());
218 }
219
220 if params.interactive && params.from_file.is_some() {
222 anyhow::bail!("Cannot use --interactive mode with --from-file");
223 }
224
225 let final_token = if params.interactive {
227 if params.token.is_some() || params.token_arg.is_some() {
228 eprintln!(
229 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
230 );
231 }
232 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
233 } else {
234 match (¶ms.token, ¶ms.token_arg) {
235 (Some(t), _) => t.clone(),
236 (None, Some(t)) => t.clone(),
237 (None, None) => {
238 anyhow::bail!(
239 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
240 );
241 }
242 }
243 };
244
245 let final_url = if params.interactive {
247 if params.url.is_some() || params.url_arg.is_some() {
248 eprintln!(
249 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
250 );
251 }
252 read_input("Enter API URL (default: https://api.anthropic.com): ")?
253 } else {
254 match (¶ms.url, ¶ms.url_arg) {
255 (Some(u), _) => u.clone(),
256 (None, Some(u)) => u.clone(),
257 (None, None) => "https://api.anthropic.com".to_string(),
258 }
259 };
260
261 let final_url = if final_url.is_empty() {
263 "https://api.anthropic.com".to_string()
264 } else {
265 final_url
266 };
267
268 let final_model = if params.interactive {
270 if params.model.is_some() {
271 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
272 }
273 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
274 if model_input.is_empty() {
275 None
276 } else {
277 Some(model_input)
278 }
279 } else {
280 params.model
281 };
282
283 let final_small_fast_model = if params.interactive {
285 if params.small_fast_model.is_some() {
286 eprintln!(
287 "Warning: Small fast model provided via flags will be ignored in interactive mode"
288 );
289 }
290 let small_model_input =
291 read_input("Enter small fast model name (optional, press enter to skip): ")?;
292 if small_model_input.is_empty() {
293 None
294 } else {
295 Some(small_model_input)
296 }
297 } else {
298 params.small_fast_model
299 };
300
301 let final_max_thinking_tokens = if params.interactive {
303 if params.max_thinking_tokens.is_some() {
304 eprintln!(
305 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
306 );
307 }
308 let tokens_input = read_input(
309 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
310 )?;
311 if tokens_input.is_empty() {
312 None
313 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
314 if tokens == 0 { None } else { Some(tokens) }
315 } else {
316 eprintln!("Warning: Invalid max thinking tokens value, skipping");
317 None
318 }
319 } else {
320 params.max_thinking_tokens
321 };
322
323 let final_api_timeout_ms = if params.interactive {
325 if params.api_timeout_ms.is_some() {
326 eprintln!(
327 "Warning: API timeout provided via flags will be ignored in interactive mode"
328 );
329 }
330 let timeout_input = read_input(
331 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
332 )?;
333 if timeout_input.is_empty() {
334 None
335 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
336 if timeout == 0 { None } else { Some(timeout) }
337 } else {
338 eprintln!("Warning: Invalid API timeout value, skipping");
339 None
340 }
341 } else {
342 params.api_timeout_ms
343 };
344
345 let final_claude_code_disable_nonessential_traffic = if params.interactive {
347 if params.claude_code_disable_nonessential_traffic.is_some() {
348 eprintln!(
349 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
350 );
351 }
352 let flag_input = read_input(
353 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
354 )?;
355 if flag_input.is_empty() {
356 None
357 } else if let Ok(flag) = flag_input.parse::<u32>() {
358 if flag == 0 { None } else { Some(flag) }
359 } else {
360 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
361 None
362 }
363 } else {
364 params.claude_code_disable_nonessential_traffic
365 };
366
367 let final_anthropic_default_sonnet_model = if params.interactive {
369 if params.anthropic_default_sonnet_model.is_some() {
370 eprintln!(
371 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
372 );
373 }
374 let model_input =
375 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
376 if model_input.is_empty() {
377 None
378 } else {
379 Some(model_input)
380 }
381 } else {
382 params.anthropic_default_sonnet_model
383 };
384
385 let final_anthropic_default_opus_model = if params.interactive {
387 if params.anthropic_default_opus_model.is_some() {
388 eprintln!(
389 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
390 );
391 }
392 let model_input =
393 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
394 if model_input.is_empty() {
395 None
396 } else {
397 Some(model_input)
398 }
399 } else {
400 params.anthropic_default_opus_model
401 };
402
403 let final_anthropic_default_haiku_model = if params.interactive {
405 if params.anthropic_default_haiku_model.is_some() {
406 eprintln!(
407 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
408 );
409 }
410 let model_input =
411 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
412 if model_input.is_empty() {
413 None
414 } else {
415 Some(model_input)
416 }
417 } else {
418 params.anthropic_default_haiku_model
419 };
420
421 let final_claude_code_subagent_model = if params.interactive {
423 if params.claude_code_subagent_model.is_some() {
424 eprintln!(
425 "Warning: Subagent model provided via flags will be ignored in interactive mode"
426 );
427 }
428 let model_input =
429 read_input("Enter subagent model name (optional, press enter to skip): ")?;
430 if model_input.is_empty() {
431 None
432 } else {
433 Some(model_input)
434 }
435 } else {
436 params.claude_code_subagent_model
437 };
438
439 let final_claude_code_disable_nonstreaming_fallback = if params.interactive {
441 if params.claude_code_disable_nonstreaming_fallback.is_some() {
442 eprintln!(
443 "Warning: Disable non-streaming fallback flag provided via flags will be ignored in interactive mode"
444 );
445 }
446 let flag_input = read_input(
447 "Enter disable non-streaming fallback flag (optional, press enter to skip, enter 0 to clear): ",
448 )?;
449 if flag_input.is_empty() {
450 None
451 } else if let Ok(flag) = flag_input.parse::<u32>() {
452 if flag == 0 { None } else { Some(flag) }
453 } else {
454 eprintln!("Warning: Invalid disable non-streaming fallback flag value, skipping");
455 None
456 }
457 } else {
458 params.claude_code_disable_nonstreaming_fallback
459 };
460
461 let final_claude_code_effort_level = if params.interactive {
463 if params.claude_code_effort_level.is_some() {
464 eprintln!(
465 "Warning: Effort level provided via flags will be ignored in interactive mode"
466 );
467 }
468 let level_input = read_input("Enter effort level (optional, press enter to skip): ")?;
469 if level_input.is_empty() {
470 None
471 } else {
472 Some(level_input)
473 }
474 } else {
475 params.claude_code_effort_level
476 };
477
478 let is_anthropic_official = final_url.contains("api.anthropic.com");
480 if is_anthropic_official {
481 if !final_token.starts_with("sk-ant-api03-") {
482 eprintln!(
483 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
484 );
485 }
486 } else {
487 if final_token.starts_with("sk-ant-api03-") {
489 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
490 }
491 }
493
494 let config = Configuration {
496 alias_name: params.alias_name.clone(),
497 token: final_token,
498 url: final_url,
499 model: final_model,
500 small_fast_model: final_small_fast_model,
501 max_thinking_tokens: final_max_thinking_tokens,
502 api_timeout_ms: final_api_timeout_ms,
503 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
504 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
505 anthropic_default_opus_model: final_anthropic_default_opus_model,
506 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
507 claude_code_subagent_model: final_claude_code_subagent_model,
508 claude_code_disable_nonstreaming_fallback: final_claude_code_disable_nonstreaming_fallback,
509 claude_code_effort_level: final_claude_code_effort_level,
510 claude_code_experimental_agent_teams: None,
511 claude_code_disable_1m_context: None,
512 };
513
514 storage.add_configuration(config);
515 storage.save()?;
516
517 println!("Configuration '{}' added successfully", params.alias_name);
518 if params.force {
519 println!("(Overwrote existing configuration)");
520 }
521
522 Ok(())
523}
524
525pub fn run() -> Result<()> {
535 let cli = Cli::parse();
536
537 if cli.migrate {
539 ConfigStorage::migrate_from_old_path()?;
540 return Ok(());
541 }
542
543 if cli.list_aliases {
545 list_aliases_for_completion()?;
546 return Ok(());
547 }
548
549 if cli.list_codex_aliases {
551 list_codex_aliases_for_completion()?;
552 return Ok(());
553 }
554
555 let _ = ClaudeSettings::cleanup_orphan_alias_files();
558
559 if let Some(ref store_str) = cli.store
561 && cli.command.is_none()
562 {
563 let mode = match parse_storage_mode(store_str) {
565 Ok(mode) => mode,
566 Err(e) => {
567 eprintln!("Error: {}", e);
568 std::process::exit(1);
569 }
570 };
571
572 let mut storage = ConfigStorage::load()?;
573 storage.default_storage_mode = Some(mode.clone());
574 storage.save()?;
575
576 let mode_str = match mode {
577 StorageMode::Env => "env",
578 StorageMode::Config => "config",
579 };
580
581 println!("Default storage mode set to: {}", mode_str);
582 return Ok(());
583 }
584
585 if let Some(command) = cli.command {
587 let mut storage = ConfigStorage::load()?;
588
589 match command {
590 Commands::Add {
591 alias_name,
592 token,
593 url,
594 model,
595 small_fast_model,
596 max_thinking_tokens,
597 api_timeout_ms,
598 claude_code_disable_nonessential_traffic,
599 anthropic_default_sonnet_model,
600 anthropic_default_opus_model,
601 anthropic_default_haiku_model,
602 claude_code_subagent_model,
603 claude_code_disable_nonstreaming_fallback,
604 claude_code_effort_level,
605 force,
606 interactive,
607 token_arg,
608 url_arg,
609 from_file,
610 } => {
611 let resolved_from_file: Option<String> = match from_file {
612 Some(Some(path)) => {
613 if !std::path::Path::new(&path).exists() {
614 anyhow::bail!("Config file not found: {}", path);
615 }
616 Some(path)
617 }
618 Some(None) => {
619 let custom_dir = storage.get_claude_settings_dir().map(|s| s.as_str());
620 let default_path = crate::utils::get_claude_settings_path(custom_dir)
621 .map(|p| p.to_string_lossy().into_owned())
622 .map_err(|e| {
623 anyhow!("Failed to resolve default Claude settings path: {}", e)
624 })?;
625 if !std::path::Path::new(&default_path).exists() {
626 anyhow::bail!(
627 "Default Claude config not found at: {}\n\
628 Run `claude` once to create it, or pass an explicit path: --from-file <path>",
629 default_path
630 );
631 }
632 Some(default_path)
633 }
634 None => None,
635 };
636
637 let params = AddCommandParams {
638 alias_name,
639 token,
640 url,
641 model,
642 small_fast_model,
643 max_thinking_tokens,
644 api_timeout_ms,
645 claude_code_disable_nonessential_traffic,
646 anthropic_default_sonnet_model,
647 anthropic_default_opus_model,
648 anthropic_default_haiku_model,
649 claude_code_subagent_model,
650 claude_code_disable_nonstreaming_fallback,
651 claude_code_effort_level,
652 force,
653 interactive,
654 token_arg,
655 url_arg,
656 from_file: resolved_from_file,
657 };
658 handle_add_command(params, &mut storage)?;
659 }
660 Commands::Remove { alias_names } => {
661 let mut removed_count = 0;
662 let mut not_found_aliases = Vec::new();
663
664 for alias_name in &alias_names {
665 if storage.remove_configuration(alias_name) {
666 removed_count += 1;
667 println!("Configuration '{alias_name}' removed successfully");
668 } else {
669 not_found_aliases.push(alias_name.clone());
670 println!("Configuration '{alias_name}' not found");
671 }
672 }
673
674 if removed_count > 0 {
675 storage.save()?;
676 }
677
678 if !not_found_aliases.is_empty() {
679 eprintln!(
680 "Warning: The following configurations were not found: {}",
681 not_found_aliases.join(", ")
682 );
683 }
684
685 if removed_count > 0 {
686 println!("Successfully removed {removed_count} configuration(s)");
687 }
688 }
689 Commands::List { plain, name } => {
690 if name {
691 if storage.configurations.is_empty() {
692 println!("No configurations stored");
693 } else {
694 for (alias_name, config) in &storage.configurations {
695 println!("{}: {}", alias_name, config.url);
696 }
697 }
698 } else if plain {
699 if storage.configurations.is_empty() {
701 println!("No configurations stored");
702 } else {
703 println!("Stored configurations:");
704 for (alias_name, config) in &storage.configurations {
705 let mut info = format!("token={}, url={}", config.token, config.url);
706 if let Some(model) = &config.model {
707 info.push_str(&format!(", model={model}"));
708 }
709 if let Some(small_fast_model) = &config.small_fast_model {
710 info.push_str(&format!(", small_fast_model={small_fast_model}"));
711 }
712 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
713 info.push_str(&format!(
714 ", max_thinking_tokens={max_thinking_tokens}"
715 ));
716 }
717 if let Some(subagent_model) = &config.claude_code_subagent_model {
718 info.push_str(&format!(", subagent_model={subagent_model}"));
719 }
720 if let Some(flag) = config.claude_code_disable_nonstreaming_fallback {
721 info.push_str(&format!(", disable_nonstreaming_fallback={flag}"));
722 }
723 if let Some(effort_level) = &config.claude_code_effort_level {
724 info.push_str(&format!(", effort_level={effort_level}"));
725 }
726 println!(" {alias_name}: {info}");
727 }
728 }
729 } else {
730 println!(
732 "{}",
733 serde_json::to_string_pretty(&storage.configurations)
734 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
735 );
736 }
737 }
738 Commands::Completion { shell } => {
739 generate_completion(&shell)?;
740 }
741 Commands::Use {
742 alias_name,
743 resume,
744 r#continue,
745 prompt,
746 } => {
747 if alias_name == "cc" || alias_name == "official" {
749 println!("Using official Claude configuration");
750
751 let mut settings = ClaudeSettings::load(
752 storage.get_claude_settings_dir().map(|s| s.as_str()),
753 )?;
754 settings.remove_anthropic_env();
755 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
756
757 launch_claude_with_env(
758 EnvironmentConfig::empty().with_alias("official"),
759 None,
760 None,
761 r#continue,
762 )?;
763 return Ok(());
764 }
765
766 let config = storage
767 .configurations
768 .get(&alias_name)
769 .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
770 .clone();
771
772 let env_config = EnvironmentConfig::from_config(&config).with_alias(&alias_name);
773 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
774
775 let mut settings =
777 ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
778 settings.switch_to_config_with_mode(
779 &config,
780 storage_mode,
781 storage.get_claude_settings_dir().map(|s| s.as_str()),
782 )?;
783
784 println!("Switched to configuration '{}'", alias_name);
785 println!(" URL: {}", config.url);
786 println!(
787 " Token: {}",
788 crate::cli::display_utils::format_token_for_display(&config.token)
789 );
790
791 let prompt_str = if prompt.is_empty() {
792 None
793 } else {
794 Some(prompt.join(" "))
795 };
796
797 launch_claude_with_env(
798 env_config,
799 prompt_str.as_deref(),
800 resume.as_deref(),
801 r#continue,
802 )?;
803 }
804 Commands::Codex { command } => match command {
805 Some(crate::cli::CodexCommands::Add {
806 alias_name,
807 api_key,
808 force,
809 interactive,
810 from_file,
811 }) => {
812 let resolved_from_file: Option<String> = match from_file {
813 Some(Some(path)) => {
814 if !std::path::Path::new(&path).exists() {
815 anyhow::bail!("Codex auth file not found: {}", path);
816 }
817 Some(path)
818 }
819 Some(None) => {
820 let default_path = crate::codex::default_codex_auth_path()
821 .map(|p| p.to_string_lossy().into_owned())
822 .map_err(|e| {
823 anyhow!("Failed to resolve default Codex auth path: {}", e)
824 })?;
825 if !std::path::Path::new(&default_path).exists() {
826 anyhow::bail!(
827 "Default Codex auth file not found at: {}\n\
828 Run `codex login` first, or pass an explicit path: --from-file <path>",
829 default_path
830 );
831 }
832 Some(default_path)
833 }
834 None => None,
835 };
836
837 handle_codex_add(
838 alias_name,
839 api_key,
840 force,
841 interactive,
842 resolved_from_file,
843 &mut storage,
844 )?;
845 }
846 Some(crate::cli::CodexCommands::List { plain, name }) => {
847 handle_codex_list(plain, name, &storage)?;
848 }
849 Some(crate::cli::CodexCommands::Use {
850 alias_name,
851 r#continue,
852 resume,
853 prompt,
854 }) => {
855 handle_codex_use(alias_name, r#continue, resume, prompt, &mut storage)?;
856 }
857 Some(crate::cli::CodexCommands::Remove { alias_names }) => {
858 handle_codex_remove(alias_names, &mut storage)?;
859 }
860 None => {
861 handle_codex_interactive(&storage)?;
863 }
864 },
865 Commands::Statusline { action } => {
866 let custom_dir = storage.get_claude_settings_dir().map(|s| s.as_str());
867 match action {
868 crate::cli::StatuslineAction::Install => {
869 crate::statusline::install(custom_dir)?;
870 }
871 crate::cli::StatuslineAction::Uninstall => {
872 crate::statusline::uninstall(custom_dir)?;
873 }
874 }
875 }
876 }
877 } else {
878 let storage = ConfigStorage::load()?;
880 handle_interactive_selection(&storage)?;
881 }
882
883 Ok(())
884}