1use crate::cli::completion::{generate_aliases, generate_completion, list_aliases_for_completion};
2use crate::cli::{Cli, Commands};
3use crate::config::types::AddCommandParams;
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
15#[allow(clippy::type_complexity)]
26fn parse_config_from_file(
27 file_path: &str,
28) -> Result<(
29 String,
30 String,
31 String,
32 Option<String>,
33 Option<String>,
34 Option<u32>,
35 Option<u32>,
36 Option<u32>,
37 Option<String>,
38 Option<String>,
39 Option<String>,
40)> {
41 let file_content = fs::read_to_string(file_path)
43 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
44
45 let json: serde_json::Value = serde_json::from_str(&file_content)
47 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
48
49 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
51 anyhow!(
52 "File '{}' does not contain a valid 'env' section",
53 file_path
54 )
55 })?;
56
57 let path = Path::new(file_path);
59 let alias_name = path
60 .file_stem()
61 .and_then(|s| s.to_str())
62 .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
63 .to_string();
64
65 let token = env
67 .get("ANTHROPIC_AUTH_TOKEN")
68 .and_then(|v| v.as_str())
69 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
70 .to_string();
71
72 let url = env
73 .get("ANTHROPIC_BASE_URL")
74 .and_then(|v| v.as_str())
75 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
76 .to_string();
77
78 let model = env
79 .get("ANTHROPIC_MODEL")
80 .and_then(|v| v.as_str())
81 .map(|s| s.to_string());
82
83 let small_fast_model = env
84 .get("ANTHROPIC_SMALL_FAST_MODEL")
85 .and_then(|v| v.as_str())
86 .map(|s| s.to_string());
87
88 let max_thinking_tokens = env
89 .get("ANTHROPIC_MAX_THINKING_TOKENS")
90 .and_then(|v| v.as_u64())
91 .map(|u| u as u32);
92
93 let api_timeout_ms = env
94 .get("API_TIMEOUT_MS")
95 .and_then(|v| v.as_u64())
96 .map(|u| u as u32);
97
98 let claude_code_disable_nonessential_traffic = env
99 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
100 .and_then(|v| v.as_u64())
101 .map(|u| u as u32);
102
103 let anthropic_default_sonnet_model = env
104 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
105 .and_then(|v| v.as_str())
106 .map(|s| s.to_string());
107
108 let anthropic_default_opus_model = env
109 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
110 .and_then(|v| v.as_str())
111 .map(|s| s.to_string());
112
113 let anthropic_default_haiku_model = env
114 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
115 .and_then(|v| v.as_str())
116 .map(|s| s.to_string());
117
118 Ok((
119 alias_name,
120 token,
121 url,
122 model,
123 small_fast_model,
124 max_thinking_tokens,
125 api_timeout_ms,
126 claude_code_disable_nonessential_traffic,
127 anthropic_default_sonnet_model,
128 anthropic_default_opus_model,
129 anthropic_default_haiku_model,
130 ))
131}
132
133fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
142 if let Some(file_path) = ¶ms.from_file {
144 println!("Importing configuration from file: {}", file_path);
145
146 let (
147 file_alias_name,
148 file_token,
149 file_url,
150 file_model,
151 file_small_fast_model,
152 file_max_thinking_tokens,
153 file_api_timeout_ms,
154 file_claude_disable_nonessential_traffic,
155 file_sonnet_model,
156 file_opus_model,
157 file_haiku_model,
158 ) = parse_config_from_file(file_path)?;
159
160 params.alias_name = file_alias_name;
162
163 params.token = Some(file_token);
165 params.url = Some(file_url);
166 params.model = file_model;
167 params.small_fast_model = file_small_fast_model;
168 params.max_thinking_tokens = file_max_thinking_tokens;
169 params.api_timeout_ms = file_api_timeout_ms;
170 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
171 params.anthropic_default_sonnet_model = file_sonnet_model;
172 params.anthropic_default_opus_model = file_opus_model;
173 params.anthropic_default_haiku_model = file_haiku_model;
174
175 println!(
176 "Configuration '{}' will be imported from file",
177 params.alias_name
178 );
179 }
180
181 validate_alias_name(¶ms.alias_name)?;
183
184 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
186 eprintln!("Configuration '{}' already exists.", params.alias_name);
187 eprintln!("Use --force to overwrite or choose a different alias name.");
188 return Ok(());
189 }
190
191 if params.interactive && params.from_file.is_some() {
193 anyhow::bail!("Cannot use --interactive mode with --from-file");
194 }
195
196 let final_token = if params.interactive {
198 if params.token.is_some() || params.token_arg.is_some() {
199 eprintln!(
200 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
201 );
202 }
203 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
204 } else {
205 match (¶ms.token, ¶ms.token_arg) {
206 (Some(t), _) => t.clone(),
207 (None, Some(t)) => t.clone(),
208 (None, None) => {
209 anyhow::bail!(
210 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
211 );
212 }
213 }
214 };
215
216 let final_url = if params.interactive {
218 if params.url.is_some() || params.url_arg.is_some() {
219 eprintln!(
220 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
221 );
222 }
223 read_input("Enter API URL (default: https://api.anthropic.com): ")?
224 } else {
225 match (¶ms.url, ¶ms.url_arg) {
226 (Some(u), _) => u.clone(),
227 (None, Some(u)) => u.clone(),
228 (None, None) => "https://api.anthropic.com".to_string(),
229 }
230 };
231
232 let final_url = if final_url.is_empty() {
234 "https://api.anthropic.com".to_string()
235 } else {
236 final_url
237 };
238
239 let final_model = if params.interactive {
241 if params.model.is_some() {
242 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
243 }
244 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
245 if model_input.is_empty() {
246 None
247 } else {
248 Some(model_input)
249 }
250 } else {
251 params.model
252 };
253
254 let final_small_fast_model = if params.interactive {
256 if params.small_fast_model.is_some() {
257 eprintln!(
258 "Warning: Small fast model provided via flags will be ignored in interactive mode"
259 );
260 }
261 let small_model_input =
262 read_input("Enter small fast model name (optional, press enter to skip): ")?;
263 if small_model_input.is_empty() {
264 None
265 } else {
266 Some(small_model_input)
267 }
268 } else {
269 params.small_fast_model
270 };
271
272 let final_max_thinking_tokens = if params.interactive {
274 if params.max_thinking_tokens.is_some() {
275 eprintln!(
276 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
277 );
278 }
279 let tokens_input = read_input(
280 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
281 )?;
282 if tokens_input.is_empty() {
283 None
284 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
285 if tokens == 0 { None } else { Some(tokens) }
286 } else {
287 eprintln!("Warning: Invalid max thinking tokens value, skipping");
288 None
289 }
290 } else {
291 params.max_thinking_tokens
292 };
293
294 let final_api_timeout_ms = if params.interactive {
296 if params.api_timeout_ms.is_some() {
297 eprintln!(
298 "Warning: API timeout provided via flags will be ignored in interactive mode"
299 );
300 }
301 let timeout_input = read_input(
302 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
303 )?;
304 if timeout_input.is_empty() {
305 None
306 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
307 if timeout == 0 { None } else { Some(timeout) }
308 } else {
309 eprintln!("Warning: Invalid API timeout value, skipping");
310 None
311 }
312 } else {
313 params.api_timeout_ms
314 };
315
316 let final_claude_code_disable_nonessential_traffic = if params.interactive {
318 if params.claude_code_disable_nonessential_traffic.is_some() {
319 eprintln!(
320 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
321 );
322 }
323 let flag_input = read_input(
324 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
325 )?;
326 if flag_input.is_empty() {
327 None
328 } else if let Ok(flag) = flag_input.parse::<u32>() {
329 if flag == 0 { None } else { Some(flag) }
330 } else {
331 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
332 None
333 }
334 } else {
335 params.claude_code_disable_nonessential_traffic
336 };
337
338 let final_anthropic_default_sonnet_model = if params.interactive {
340 if params.anthropic_default_sonnet_model.is_some() {
341 eprintln!(
342 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
343 );
344 }
345 let model_input =
346 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
347 if model_input.is_empty() {
348 None
349 } else {
350 Some(model_input)
351 }
352 } else {
353 params.anthropic_default_sonnet_model
354 };
355
356 let final_anthropic_default_opus_model = if params.interactive {
358 if params.anthropic_default_opus_model.is_some() {
359 eprintln!(
360 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
361 );
362 }
363 let model_input =
364 read_input("Enter default Opus 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_opus_model
372 };
373
374 let final_anthropic_default_haiku_model = if params.interactive {
376 if params.anthropic_default_haiku_model.is_some() {
377 eprintln!(
378 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
379 );
380 }
381 let model_input =
382 read_input("Enter default Haiku 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_haiku_model
390 };
391
392 let is_anthropic_official = final_url.contains("api.anthropic.com");
394 if is_anthropic_official {
395 if !final_token.starts_with("sk-ant-api03-") {
396 eprintln!(
397 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
398 );
399 }
400 } else {
401 if final_token.starts_with("sk-ant-api03-") {
403 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
404 }
405 }
407
408 let config = Configuration {
410 alias_name: params.alias_name.clone(),
411 token: final_token,
412 url: final_url,
413 model: final_model,
414 small_fast_model: final_small_fast_model,
415 max_thinking_tokens: final_max_thinking_tokens,
416 api_timeout_ms: final_api_timeout_ms,
417 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
418 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
419 anthropic_default_opus_model: final_anthropic_default_opus_model,
420 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
421 };
422
423 storage.add_configuration(config);
424 storage.save()?;
425
426 println!("Configuration '{}' added successfully", params.alias_name);
427 if params.force {
428 println!("(Overwrote existing configuration)");
429 }
430
431 Ok(())
432}
433
434pub fn handle_switch_command(alias_name: Option<&str>) -> Result<()> {
449 let storage = ConfigStorage::load()?;
450
451 if alias_name.is_none() {
453 return handle_interactive_selection(&storage);
454 }
455
456 let alias_name = alias_name.unwrap();
457
458 let env_config = if alias_name == "cc" {
459 println!("Using default Claude configuration (no custom API settings)");
461 println!("Current URL: Default (api.anthropic.com)");
462 EnvironmentConfig::empty()
463 } else if let Some(config) = storage.get_configuration(alias_name) {
464 let mut config_info = format!("token: {}, url: {}", config.token, config.url);
465 if let Some(model) = &config.model {
466 config_info.push_str(&format!(", model: {model}"));
467 }
468 if let Some(small_fast_model) = &config.small_fast_model {
469 config_info.push_str(&format!(", small_fast_model: {small_fast_model}"));
470 }
471 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
472 config_info.push_str(&format!(", max_thinking_tokens: {max_thinking_tokens}"));
473 }
474 if let Some(api_timeout_ms) = config.api_timeout_ms {
475 config_info.push_str(&format!(", api_timeout_ms: {api_timeout_ms}"));
476 }
477 if let Some(claude_code_disable_nonessential_traffic) =
478 config.claude_code_disable_nonessential_traffic
479 {
480 config_info.push_str(&format!(
481 ", disable_nonessential_traffic: {claude_code_disable_nonessential_traffic}"
482 ));
483 }
484 if let Some(sonnet_model) = &config.anthropic_default_sonnet_model {
485 config_info.push_str(&format!(", default_sonnet_model: {sonnet_model}"));
486 }
487 if let Some(opus_model) = &config.anthropic_default_opus_model {
488 config_info.push_str(&format!(", default_opus_model: {opus_model}"));
489 }
490 if let Some(haiku_model) = &config.anthropic_default_haiku_model {
491 config_info.push_str(&format!(", default_haiku_model: {haiku_model}"));
492 }
493 println!("Switched to configuration '{alias_name}' ({config_info})");
494 println!("Current URL: {}", config.url);
495 EnvironmentConfig::from_config(config)
496 } else {
497 anyhow::bail!(
498 "Configuration '{}' not found. Use 'list' command to see available configurations.",
499 alias_name
500 );
501 };
502
503 println!("Waiting 0.2 second before launching Claude...");
505 println!(
506 "Executing: claude {}",
507 "--dangerously-skip-permissions".red()
508 );
509 thread::sleep(Duration::from_millis(200));
510
511 for (key, value) in env_config.as_env_tuples() {
513 unsafe {
514 std::env::set_var(&key, &value);
515 }
516 }
517
518 println!("Launching Claude CLI...");
520
521 #[cfg(unix)]
523 {
524 use std::os::unix::process::CommandExt;
525 let error = Command::new("claude")
526 .arg("--dangerously-skip-permissions")
527 .exec();
528 anyhow::bail!("Failed to exec claude: {}", error);
530 }
531
532 #[cfg(not(unix))]
534 {
535 use std::process::Stdio;
536 let mut child = Command::new("claude")
537 .arg("--dangerously-skip-permissions")
538 .stdin(Stdio::inherit())
539 .stdout(Stdio::inherit())
540 .stderr(Stdio::inherit())
541 .spawn()
542 .context(
543 "Failed to launch Claude CLI. Make sure 'claude' command is available in PATH",
544 )?;
545
546 let status = child
548 .wait()
549 .context("Failed to wait for Claude CLI process")?;
550
551 if !status.success() {
552 anyhow::bail!("Claude CLI exited with error status: {}", status);
553 }
554 }
555}
556
557pub fn run() -> Result<()> {
567 let cli = Cli::parse();
568
569 if cli.migrate {
571 ConfigStorage::migrate_from_old_path()?;
572 return Ok(());
573 }
574
575 if cli.list_aliases {
577 list_aliases_for_completion()?;
578 return Ok(());
579 }
580
581 if let Some(command) = cli.command {
583 let mut storage = ConfigStorage::load()?;
584
585 match command {
586 Commands::Add {
587 alias_name,
588 token,
589 url,
590 model,
591 small_fast_model,
592 max_thinking_tokens,
593 api_timeout_ms,
594 claude_code_disable_nonessential_traffic,
595 anthropic_default_sonnet_model,
596 anthropic_default_opus_model,
597 anthropic_default_haiku_model,
598 force,
599 interactive,
600 token_arg,
601 url_arg,
602 from_file,
603 } => {
604 let final_alias_name = if from_file.is_some() {
607 "placeholder".to_string()
609 } else {
610 alias_name.unwrap_or_else(|| {
611 eprintln!("Error: alias_name is required when not using --from-file");
612 std::process::exit(1);
613 })
614 };
615
616 let params = AddCommandParams {
617 alias_name: final_alias_name,
618 token,
619 url,
620 model,
621 small_fast_model,
622 max_thinking_tokens,
623 api_timeout_ms,
624 claude_code_disable_nonessential_traffic,
625 anthropic_default_sonnet_model,
626 anthropic_default_opus_model,
627 anthropic_default_haiku_model,
628 force,
629 interactive,
630 token_arg,
631 url_arg,
632 from_file,
633 };
634 handle_add_command(params, &mut storage)?;
635 }
636 Commands::Remove { alias_names } => {
637 let mut removed_count = 0;
638 let mut not_found_aliases = Vec::new();
639
640 for alias_name in &alias_names {
641 if storage.remove_configuration(alias_name) {
642 removed_count += 1;
643 println!("Configuration '{alias_name}' removed successfully");
644 } else {
645 not_found_aliases.push(alias_name.clone());
646 println!("Configuration '{alias_name}' not found");
647 }
648 }
649
650 if removed_count > 0 {
651 storage.save()?;
652 }
653
654 if !not_found_aliases.is_empty() {
655 eprintln!(
656 "Warning: The following configurations were not found: {}",
657 not_found_aliases.join(", ")
658 );
659 }
660
661 if removed_count > 0 {
662 println!("Successfully removed {removed_count} configuration(s)");
663 }
664 }
665 Commands::List { plain } => {
666 if plain {
667 if storage.configurations.is_empty() {
669 println!("No configurations stored");
670 } else {
671 println!("Stored configurations:");
672 for (alias_name, config) in &storage.configurations {
673 let mut info = format!("token={}, url={}", config.token, config.url);
674 if let Some(model) = &config.model {
675 info.push_str(&format!(", model={model}"));
676 }
677 if let Some(small_fast_model) = &config.small_fast_model {
678 info.push_str(&format!(", small_fast_model={small_fast_model}"));
679 }
680 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
681 info.push_str(&format!(
682 ", max_thinking_tokens={max_thinking_tokens}"
683 ));
684 }
685 println!(" {alias_name}: {info}");
686 }
687 }
688 } else {
689 println!(
691 "{}",
692 serde_json::to_string_pretty(&storage.configurations)
693 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
694 );
695 }
696 }
697 Commands::Completion { shell } => {
698 generate_completion(&shell)?;
699 }
700 Commands::Alias { shell } => {
701 generate_aliases(&shell)?;
702 }
703 Commands::Use { alias_name } => {
704 handle_switch_command(Some(&alias_name))?;
705 }
706 Commands::Version => {
707 println!("{}", env!("CARGO_PKG_VERSION"));
708 }
709 }
710 } else {
711 let storage = ConfigStorage::load()?;
713 handle_interactive_selection(&storage)?;
714 }
715
716 Ok(())
717}