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