1use crate::cli::completion::{generate_completion, list_aliases_for_completion};
2use crate::cli::{Cli, Commands};
3use crate::config::types::{AddCommandParams, ClaudeSettings, StorageMode};
4use crate::config::{ConfigStorage, Configuration, EnvironmentConfig, validate_alias_name};
5use crate::interactive::{
6 handle_interactive_selection, launch_claude_with_env, read_input, read_sensitive_input,
7};
8use anyhow::{Result, anyhow};
9use clap::Parser;
10use std::fs;
11use std::path::Path;
12
13fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
21 match store_str.to_lowercase().as_str() {
22 "env" => Ok(StorageMode::Env),
23 "config" => Ok(StorageMode::Config),
24 _ => Err(anyhow!(
25 "Invalid storage mode '{}'. Use 'env' or 'config'",
26 store_str
27 )),
28 }
29}
30
31#[allow(clippy::type_complexity)]
42fn parse_config_from_file(
43 file_path: &str,
44) -> Result<(
45 String,
46 String,
47 String,
48 Option<String>,
49 Option<String>,
50 Option<u32>,
51 Option<u32>,
52 Option<u32>,
53 Option<String>,
54 Option<String>,
55 Option<String>,
56)> {
57 let file_content = fs::read_to_string(file_path)
59 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
60
61 let json: serde_json::Value = serde_json::from_str(&file_content)
63 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
64
65 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
67 anyhow!(
68 "File '{}' does not contain a valid 'env' section",
69 file_path
70 )
71 })?;
72
73 let path = Path::new(file_path);
75 let alias_name = path
76 .file_stem()
77 .and_then(|s| s.to_str())
78 .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
79 .to_string();
80
81 let token = env
83 .get("ANTHROPIC_AUTH_TOKEN")
84 .and_then(|v| v.as_str())
85 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
86 .to_string();
87
88 let url = env
89 .get("ANTHROPIC_BASE_URL")
90 .and_then(|v| v.as_str())
91 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
92 .to_string();
93
94 let model = env
95 .get("ANTHROPIC_MODEL")
96 .and_then(|v| v.as_str())
97 .map(|s| s.to_string());
98
99 let small_fast_model = env
100 .get("ANTHROPIC_SMALL_FAST_MODEL")
101 .and_then(|v| v.as_str())
102 .map(|s| s.to_string());
103
104 let max_thinking_tokens = env
105 .get("ANTHROPIC_MAX_THINKING_TOKENS")
106 .and_then(|v| v.as_u64())
107 .map(|u| u as u32);
108
109 let api_timeout_ms = env
110 .get("API_TIMEOUT_MS")
111 .and_then(|v| v.as_u64())
112 .map(|u| u as u32);
113
114 let claude_code_disable_nonessential_traffic = env
115 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
116 .and_then(|v| v.as_u64())
117 .map(|u| u as u32);
118
119 let anthropic_default_sonnet_model = env
120 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
121 .and_then(|v| v.as_str())
122 .map(|s| s.to_string());
123
124 let anthropic_default_opus_model = env
125 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string());
128
129 let anthropic_default_haiku_model = env
130 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
131 .and_then(|v| v.as_str())
132 .map(|s| s.to_string());
133
134 Ok((
135 alias_name,
136 token,
137 url,
138 model,
139 small_fast_model,
140 max_thinking_tokens,
141 api_timeout_ms,
142 claude_code_disable_nonessential_traffic,
143 anthropic_default_sonnet_model,
144 anthropic_default_opus_model,
145 anthropic_default_haiku_model,
146 ))
147}
148
149fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
158 if let Some(file_path) = ¶ms.from_file {
160 println!("Importing configuration from file: {}", file_path);
161
162 let (
163 file_alias_name,
164 file_token,
165 file_url,
166 file_model,
167 file_small_fast_model,
168 file_max_thinking_tokens,
169 file_api_timeout_ms,
170 file_claude_disable_nonessential_traffic,
171 file_sonnet_model,
172 file_opus_model,
173 file_haiku_model,
174 ) = parse_config_from_file(file_path)?;
175
176 params.alias_name = file_alias_name;
178
179 params.token = Some(file_token);
181 params.url = Some(file_url);
182 params.model = file_model;
183 params.small_fast_model = file_small_fast_model;
184 params.max_thinking_tokens = file_max_thinking_tokens;
185 params.api_timeout_ms = file_api_timeout_ms;
186 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
187 params.anthropic_default_sonnet_model = file_sonnet_model;
188 params.anthropic_default_opus_model = file_opus_model;
189 params.anthropic_default_haiku_model = file_haiku_model;
190
191 println!(
192 "Configuration '{}' will be imported from file",
193 params.alias_name
194 );
195 }
196
197 validate_alias_name(¶ms.alias_name)?;
199
200 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
202 eprintln!("Configuration '{}' already exists.", params.alias_name);
203 eprintln!("Use --force to overwrite or choose a different alias name.");
204 return Ok(());
205 }
206
207 if params.interactive && params.from_file.is_some() {
209 anyhow::bail!("Cannot use --interactive mode with --from-file");
210 }
211
212 let final_token = if params.interactive {
214 if params.token.is_some() || params.token_arg.is_some() {
215 eprintln!(
216 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
217 );
218 }
219 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
220 } else {
221 match (¶ms.token, ¶ms.token_arg) {
222 (Some(t), _) => t.clone(),
223 (None, Some(t)) => t.clone(),
224 (None, None) => {
225 anyhow::bail!(
226 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
227 );
228 }
229 }
230 };
231
232 let final_url = if params.interactive {
234 if params.url.is_some() || params.url_arg.is_some() {
235 eprintln!(
236 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
237 );
238 }
239 read_input("Enter API URL (default: https://api.anthropic.com): ")?
240 } else {
241 match (¶ms.url, ¶ms.url_arg) {
242 (Some(u), _) => u.clone(),
243 (None, Some(u)) => u.clone(),
244 (None, None) => "https://api.anthropic.com".to_string(),
245 }
246 };
247
248 let final_url = if final_url.is_empty() {
250 "https://api.anthropic.com".to_string()
251 } else {
252 final_url
253 };
254
255 let final_model = if params.interactive {
257 if params.model.is_some() {
258 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
259 }
260 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
261 if model_input.is_empty() {
262 None
263 } else {
264 Some(model_input)
265 }
266 } else {
267 params.model
268 };
269
270 let final_small_fast_model = if params.interactive {
272 if params.small_fast_model.is_some() {
273 eprintln!(
274 "Warning: Small fast model provided via flags will be ignored in interactive mode"
275 );
276 }
277 let small_model_input =
278 read_input("Enter small fast model name (optional, press enter to skip): ")?;
279 if small_model_input.is_empty() {
280 None
281 } else {
282 Some(small_model_input)
283 }
284 } else {
285 params.small_fast_model
286 };
287
288 let final_max_thinking_tokens = if params.interactive {
290 if params.max_thinking_tokens.is_some() {
291 eprintln!(
292 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
293 );
294 }
295 let tokens_input = read_input(
296 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
297 )?;
298 if tokens_input.is_empty() {
299 None
300 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
301 if tokens == 0 { None } else { Some(tokens) }
302 } else {
303 eprintln!("Warning: Invalid max thinking tokens value, skipping");
304 None
305 }
306 } else {
307 params.max_thinking_tokens
308 };
309
310 let final_api_timeout_ms = if params.interactive {
312 if params.api_timeout_ms.is_some() {
313 eprintln!(
314 "Warning: API timeout provided via flags will be ignored in interactive mode"
315 );
316 }
317 let timeout_input = read_input(
318 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
319 )?;
320 if timeout_input.is_empty() {
321 None
322 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
323 if timeout == 0 { None } else { Some(timeout) }
324 } else {
325 eprintln!("Warning: Invalid API timeout value, skipping");
326 None
327 }
328 } else {
329 params.api_timeout_ms
330 };
331
332 let final_claude_code_disable_nonessential_traffic = if params.interactive {
334 if params.claude_code_disable_nonessential_traffic.is_some() {
335 eprintln!(
336 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
337 );
338 }
339 let flag_input = read_input(
340 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
341 )?;
342 if flag_input.is_empty() {
343 None
344 } else if let Ok(flag) = flag_input.parse::<u32>() {
345 if flag == 0 { None } else { Some(flag) }
346 } else {
347 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
348 None
349 }
350 } else {
351 params.claude_code_disable_nonessential_traffic
352 };
353
354 let final_anthropic_default_sonnet_model = if params.interactive {
356 if params.anthropic_default_sonnet_model.is_some() {
357 eprintln!(
358 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
359 );
360 }
361 let model_input =
362 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
363 if model_input.is_empty() {
364 None
365 } else {
366 Some(model_input)
367 }
368 } else {
369 params.anthropic_default_sonnet_model
370 };
371
372 let final_anthropic_default_opus_model = if params.interactive {
374 if params.anthropic_default_opus_model.is_some() {
375 eprintln!(
376 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
377 );
378 }
379 let model_input =
380 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
381 if model_input.is_empty() {
382 None
383 } else {
384 Some(model_input)
385 }
386 } else {
387 params.anthropic_default_opus_model
388 };
389
390 let final_anthropic_default_haiku_model = if params.interactive {
392 if params.anthropic_default_haiku_model.is_some() {
393 eprintln!(
394 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
395 );
396 }
397 let model_input =
398 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
399 if model_input.is_empty() {
400 None
401 } else {
402 Some(model_input)
403 }
404 } else {
405 params.anthropic_default_haiku_model
406 };
407
408 let is_anthropic_official = final_url.contains("api.anthropic.com");
410 if is_anthropic_official {
411 if !final_token.starts_with("sk-ant-api03-") {
412 eprintln!(
413 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
414 );
415 }
416 } else {
417 if final_token.starts_with("sk-ant-api03-") {
419 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
420 }
421 }
423
424 let config = Configuration {
426 alias_name: params.alias_name.clone(),
427 token: final_token,
428 url: final_url,
429 model: final_model,
430 small_fast_model: final_small_fast_model,
431 max_thinking_tokens: final_max_thinking_tokens,
432 api_timeout_ms: final_api_timeout_ms,
433 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
434 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
435 anthropic_default_opus_model: final_anthropic_default_opus_model,
436 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
437 };
438
439 storage.add_configuration(config);
440 storage.save()?;
441
442 println!("Configuration '{}' added successfully", params.alias_name);
443 if params.force {
444 println!("(Overwrote existing configuration)");
445 }
446
447 Ok(())
448}
449
450pub fn run() -> Result<()> {
460 let cli = Cli::parse();
461
462 if cli.migrate {
464 ConfigStorage::migrate_from_old_path()?;
465 return Ok(());
466 }
467
468 if cli.list_aliases {
470 list_aliases_for_completion()?;
471 return Ok(());
472 }
473
474 if let Some(ref store_str) = cli.store
476 && cli.command.is_none()
477 {
478 let mode = match parse_storage_mode(store_str) {
480 Ok(mode) => mode,
481 Err(e) => {
482 eprintln!("Error: {}", e);
483 std::process::exit(1);
484 }
485 };
486
487 let mut storage = ConfigStorage::load()?;
488 storage.default_storage_mode = Some(mode.clone());
489 storage.save()?;
490
491 let mode_str = match mode {
492 StorageMode::Env => "env",
493 StorageMode::Config => "config",
494 };
495
496 println!("Default storage mode set to: {}", mode_str);
497 return Ok(());
498 }
499
500 if let Some(command) = cli.command {
502 let mut storage = ConfigStorage::load()?;
503
504 match command {
505 Commands::Add {
506 alias_name,
507 token,
508 url,
509 model,
510 small_fast_model,
511 max_thinking_tokens,
512 api_timeout_ms,
513 claude_code_disable_nonessential_traffic,
514 anthropic_default_sonnet_model,
515 anthropic_default_opus_model,
516 anthropic_default_haiku_model,
517 force,
518 interactive,
519 token_arg,
520 url_arg,
521 from_file,
522 } => {
523 let final_alias_name = if from_file.is_some() {
526 "placeholder".to_string()
528 } else {
529 alias_name.unwrap_or_else(|| {
530 eprintln!("Error: alias_name is required when not using --from-file");
531 std::process::exit(1);
532 })
533 };
534
535 let params = AddCommandParams {
536 alias_name: final_alias_name,
537 token,
538 url,
539 model,
540 small_fast_model,
541 max_thinking_tokens,
542 api_timeout_ms,
543 claude_code_disable_nonessential_traffic,
544 anthropic_default_sonnet_model,
545 anthropic_default_opus_model,
546 anthropic_default_haiku_model,
547 force,
548 interactive,
549 token_arg,
550 url_arg,
551 from_file,
552 };
553 handle_add_command(params, &mut storage)?;
554 }
555 Commands::Remove { alias_names } => {
556 let mut removed_count = 0;
557 let mut not_found_aliases = Vec::new();
558
559 for alias_name in &alias_names {
560 if storage.remove_configuration(alias_name) {
561 removed_count += 1;
562 println!("Configuration '{alias_name}' removed successfully");
563 } else {
564 not_found_aliases.push(alias_name.clone());
565 println!("Configuration '{alias_name}' not found");
566 }
567 }
568
569 if removed_count > 0 {
570 storage.save()?;
571 }
572
573 if !not_found_aliases.is_empty() {
574 eprintln!(
575 "Warning: The following configurations were not found: {}",
576 not_found_aliases.join(", ")
577 );
578 }
579
580 if removed_count > 0 {
581 println!("Successfully removed {removed_count} configuration(s)");
582 }
583 }
584 Commands::List { plain } => {
585 if plain {
586 if storage.configurations.is_empty() {
588 println!("No configurations stored");
589 } else {
590 println!("Stored configurations:");
591 for (alias_name, config) in &storage.configurations {
592 let mut info = format!("token={}, url={}", config.token, config.url);
593 if let Some(model) = &config.model {
594 info.push_str(&format!(", model={model}"));
595 }
596 if let Some(small_fast_model) = &config.small_fast_model {
597 info.push_str(&format!(", small_fast_model={small_fast_model}"));
598 }
599 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
600 info.push_str(&format!(
601 ", max_thinking_tokens={max_thinking_tokens}"
602 ));
603 }
604 println!(" {alias_name}: {info}");
605 }
606 }
607 } else {
608 println!(
610 "{}",
611 serde_json::to_string_pretty(&storage.configurations)
612 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
613 );
614 }
615 }
616 Commands::Completion { shell } => {
617 generate_completion(&shell)?;
618 }
619 Commands::Use {
620 alias_name,
621 prompt,
622 } => {
623 let config = storage
624 .configurations
625 .get(&alias_name)
626 .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
627 .clone();
628
629 let env_config = EnvironmentConfig::from_config(&config);
630 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
631
632 let mut settings = ClaudeSettings::load(
634 storage.get_claude_settings_dir().map(|s| s.as_str()),
635 )?;
636 settings.switch_to_config_with_mode(
637 &config,
638 storage_mode,
639 storage.get_claude_settings_dir().map(|s| s.as_str()),
640 )?;
641
642 println!("Switched to configuration '{}'", alias_name);
643
644 let prompt_str = if prompt.is_empty() {
645 None
646 } else {
647 Some(prompt.join(" "))
648 };
649
650 launch_claude_with_env(env_config, prompt_str.as_deref())?;
651 }
652 }
653 } else {
654 let storage = ConfigStorage::load()?;
656 handle_interactive_selection(&storage)?;
657 }
658
659 Ok(())
660}