1use crate::cli::completion::{generate_completion, list_aliases_for_completion};
2use crate::cli::{Cli, Commands};
3use crate::config::types::{AddCommandParams, StorageMode};
4use crate::config::{ConfigStorage, Configuration, validate_alias_name};
5use crate::interactive::{handle_interactive_selection, read_input, read_sensitive_input};
6use anyhow::{Result, anyhow};
7use clap::Parser;
8use std::fs;
9use std::path::Path;
10
11fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
19 match store_str.to_lowercase().as_str() {
20 "env" => Ok(StorageMode::Env),
21 "config" => Ok(StorageMode::Config),
22 _ => Err(anyhow!(
23 "Invalid storage mode '{}'. Use 'env' or 'config'",
24 store_str
25 )),
26 }
27}
28
29#[allow(clippy::type_complexity)]
40fn parse_config_from_file(
41 file_path: &str,
42) -> Result<(
43 String,
44 String,
45 String,
46 Option<String>,
47 Option<String>,
48 Option<u32>,
49 Option<u32>,
50 Option<u32>,
51 Option<String>,
52 Option<String>,
53 Option<String>,
54)> {
55 let file_content = fs::read_to_string(file_path)
57 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
58
59 let json: serde_json::Value = serde_json::from_str(&file_content)
61 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
62
63 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
65 anyhow!(
66 "File '{}' does not contain a valid 'env' section",
67 file_path
68 )
69 })?;
70
71 let path = Path::new(file_path);
73 let alias_name = path
74 .file_stem()
75 .and_then(|s| s.to_str())
76 .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
77 .to_string();
78
79 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 Ok((
133 alias_name,
134 token,
135 url,
136 model,
137 small_fast_model,
138 max_thinking_tokens,
139 api_timeout_ms,
140 claude_code_disable_nonessential_traffic,
141 anthropic_default_sonnet_model,
142 anthropic_default_opus_model,
143 anthropic_default_haiku_model,
144 ))
145}
146
147fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
156 if let Some(file_path) = ¶ms.from_file {
158 println!("Importing configuration from file: {}", file_path);
159
160 let (
161 file_alias_name,
162 file_token,
163 file_url,
164 file_model,
165 file_small_fast_model,
166 file_max_thinking_tokens,
167 file_api_timeout_ms,
168 file_claude_disable_nonessential_traffic,
169 file_sonnet_model,
170 file_opus_model,
171 file_haiku_model,
172 ) = parse_config_from_file(file_path)?;
173
174 params.alias_name = file_alias_name;
176
177 params.token = Some(file_token);
179 params.url = Some(file_url);
180 params.model = file_model;
181 params.small_fast_model = file_small_fast_model;
182 params.max_thinking_tokens = file_max_thinking_tokens;
183 params.api_timeout_ms = file_api_timeout_ms;
184 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
185 params.anthropic_default_sonnet_model = file_sonnet_model;
186 params.anthropic_default_opus_model = file_opus_model;
187 params.anthropic_default_haiku_model = file_haiku_model;
188
189 println!(
190 "Configuration '{}' will be imported from file",
191 params.alias_name
192 );
193 }
194
195 validate_alias_name(¶ms.alias_name)?;
197
198 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
200 eprintln!("Configuration '{}' already exists.", params.alias_name);
201 eprintln!("Use --force to overwrite or choose a different alias name.");
202 return Ok(());
203 }
204
205 if params.interactive && params.from_file.is_some() {
207 anyhow::bail!("Cannot use --interactive mode with --from-file");
208 }
209
210 let final_token = if params.interactive {
212 if params.token.is_some() || params.token_arg.is_some() {
213 eprintln!(
214 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
215 );
216 }
217 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
218 } else {
219 match (¶ms.token, ¶ms.token_arg) {
220 (Some(t), _) => t.clone(),
221 (None, Some(t)) => t.clone(),
222 (None, None) => {
223 anyhow::bail!(
224 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
225 );
226 }
227 }
228 };
229
230 let final_url = if params.interactive {
232 if params.url.is_some() || params.url_arg.is_some() {
233 eprintln!(
234 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
235 );
236 }
237 read_input("Enter API URL (default: https://api.anthropic.com): ")?
238 } else {
239 match (¶ms.url, ¶ms.url_arg) {
240 (Some(u), _) => u.clone(),
241 (None, Some(u)) => u.clone(),
242 (None, None) => "https://api.anthropic.com".to_string(),
243 }
244 };
245
246 let final_url = if final_url.is_empty() {
248 "https://api.anthropic.com".to_string()
249 } else {
250 final_url
251 };
252
253 let final_model = if params.interactive {
255 if params.model.is_some() {
256 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
257 }
258 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
259 if model_input.is_empty() {
260 None
261 } else {
262 Some(model_input)
263 }
264 } else {
265 params.model
266 };
267
268 let final_small_fast_model = if params.interactive {
270 if params.small_fast_model.is_some() {
271 eprintln!(
272 "Warning: Small fast model provided via flags will be ignored in interactive mode"
273 );
274 }
275 let small_model_input =
276 read_input("Enter small fast model name (optional, press enter to skip): ")?;
277 if small_model_input.is_empty() {
278 None
279 } else {
280 Some(small_model_input)
281 }
282 } else {
283 params.small_fast_model
284 };
285
286 let final_max_thinking_tokens = if params.interactive {
288 if params.max_thinking_tokens.is_some() {
289 eprintln!(
290 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
291 );
292 }
293 let tokens_input = read_input(
294 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
295 )?;
296 if tokens_input.is_empty() {
297 None
298 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
299 if tokens == 0 { None } else { Some(tokens) }
300 } else {
301 eprintln!("Warning: Invalid max thinking tokens value, skipping");
302 None
303 }
304 } else {
305 params.max_thinking_tokens
306 };
307
308 let final_api_timeout_ms = if params.interactive {
310 if params.api_timeout_ms.is_some() {
311 eprintln!(
312 "Warning: API timeout provided via flags will be ignored in interactive mode"
313 );
314 }
315 let timeout_input = read_input(
316 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
317 )?;
318 if timeout_input.is_empty() {
319 None
320 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
321 if timeout == 0 { None } else { Some(timeout) }
322 } else {
323 eprintln!("Warning: Invalid API timeout value, skipping");
324 None
325 }
326 } else {
327 params.api_timeout_ms
328 };
329
330 let final_claude_code_disable_nonessential_traffic = if params.interactive {
332 if params.claude_code_disable_nonessential_traffic.is_some() {
333 eprintln!(
334 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
335 );
336 }
337 let flag_input = read_input(
338 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
339 )?;
340 if flag_input.is_empty() {
341 None
342 } else if let Ok(flag) = flag_input.parse::<u32>() {
343 if flag == 0 { None } else { Some(flag) }
344 } else {
345 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
346 None
347 }
348 } else {
349 params.claude_code_disable_nonessential_traffic
350 };
351
352 let final_anthropic_default_sonnet_model = if params.interactive {
354 if params.anthropic_default_sonnet_model.is_some() {
355 eprintln!(
356 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
357 );
358 }
359 let model_input =
360 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
361 if model_input.is_empty() {
362 None
363 } else {
364 Some(model_input)
365 }
366 } else {
367 params.anthropic_default_sonnet_model
368 };
369
370 let final_anthropic_default_opus_model = if params.interactive {
372 if params.anthropic_default_opus_model.is_some() {
373 eprintln!(
374 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
375 );
376 }
377 let model_input =
378 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
379 if model_input.is_empty() {
380 None
381 } else {
382 Some(model_input)
383 }
384 } else {
385 params.anthropic_default_opus_model
386 };
387
388 let final_anthropic_default_haiku_model = if params.interactive {
390 if params.anthropic_default_haiku_model.is_some() {
391 eprintln!(
392 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
393 );
394 }
395 let model_input =
396 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
397 if model_input.is_empty() {
398 None
399 } else {
400 Some(model_input)
401 }
402 } else {
403 params.anthropic_default_haiku_model
404 };
405
406 let is_anthropic_official = final_url.contains("api.anthropic.com");
408 if is_anthropic_official {
409 if !final_token.starts_with("sk-ant-api03-") {
410 eprintln!(
411 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
412 );
413 }
414 } else {
415 if final_token.starts_with("sk-ant-api03-") {
417 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
418 }
419 }
421
422 let config = Configuration {
424 alias_name: params.alias_name.clone(),
425 token: final_token,
426 url: final_url,
427 model: final_model,
428 small_fast_model: final_small_fast_model,
429 max_thinking_tokens: final_max_thinking_tokens,
430 api_timeout_ms: final_api_timeout_ms,
431 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
432 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
433 anthropic_default_opus_model: final_anthropic_default_opus_model,
434 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
435 };
436
437 storage.add_configuration(config);
438 storage.save()?;
439
440 println!("Configuration '{}' added successfully", params.alias_name);
441 if params.force {
442 println!("(Overwrote existing configuration)");
443 }
444
445 Ok(())
446}
447
448pub fn run() -> Result<()> {
458 let cli = Cli::parse();
459
460 if cli.migrate {
462 ConfigStorage::migrate_from_old_path()?;
463 return Ok(());
464 }
465
466 if cli.list_aliases {
468 list_aliases_for_completion()?;
469 return Ok(());
470 }
471
472 if let Some(ref store_str) = cli.store
474 && cli.command.is_none()
475 {
476 let mode = match parse_storage_mode(store_str) {
478 Ok(mode) => mode,
479 Err(e) => {
480 eprintln!("Error: {}", e);
481 std::process::exit(1);
482 }
483 };
484
485 let mut storage = ConfigStorage::load()?;
486 storage.default_storage_mode = Some(mode.clone());
487 storage.save()?;
488
489 let mode_str = match mode {
490 StorageMode::Env => "env",
491 StorageMode::Config => "config",
492 };
493
494 println!("Default storage mode set to: {}", mode_str);
495 return Ok(());
496 }
497
498 if let Some(command) = cli.command {
500 let mut storage = ConfigStorage::load()?;
501
502 match command {
503 Commands::Add {
504 alias_name,
505 token,
506 url,
507 model,
508 small_fast_model,
509 max_thinking_tokens,
510 api_timeout_ms,
511 claude_code_disable_nonessential_traffic,
512 anthropic_default_sonnet_model,
513 anthropic_default_opus_model,
514 anthropic_default_haiku_model,
515 force,
516 interactive,
517 token_arg,
518 url_arg,
519 from_file,
520 } => {
521 let final_alias_name = if from_file.is_some() {
524 "placeholder".to_string()
526 } else {
527 alias_name.unwrap_or_else(|| {
528 eprintln!("Error: alias_name is required when not using --from-file");
529 std::process::exit(1);
530 })
531 };
532
533 let params = AddCommandParams {
534 alias_name: final_alias_name,
535 token,
536 url,
537 model,
538 small_fast_model,
539 max_thinking_tokens,
540 api_timeout_ms,
541 claude_code_disable_nonessential_traffic,
542 anthropic_default_sonnet_model,
543 anthropic_default_opus_model,
544 anthropic_default_haiku_model,
545 force,
546 interactive,
547 token_arg,
548 url_arg,
549 from_file,
550 };
551 handle_add_command(params, &mut storage)?;
552 }
553 Commands::Remove { alias_names } => {
554 let mut removed_count = 0;
555 let mut not_found_aliases = Vec::new();
556
557 for alias_name in &alias_names {
558 if storage.remove_configuration(alias_name) {
559 removed_count += 1;
560 println!("Configuration '{alias_name}' removed successfully");
561 } else {
562 not_found_aliases.push(alias_name.clone());
563 println!("Configuration '{alias_name}' not found");
564 }
565 }
566
567 if removed_count > 0 {
568 storage.save()?;
569 }
570
571 if !not_found_aliases.is_empty() {
572 eprintln!(
573 "Warning: The following configurations were not found: {}",
574 not_found_aliases.join(", ")
575 );
576 }
577
578 if removed_count > 0 {
579 println!("Successfully removed {removed_count} configuration(s)");
580 }
581 }
582 Commands::List { plain } => {
583 if plain {
584 if storage.configurations.is_empty() {
586 println!("No configurations stored");
587 } else {
588 println!("Stored configurations:");
589 for (alias_name, config) in &storage.configurations {
590 let mut info = format!("token={}, url={}", config.token, config.url);
591 if let Some(model) = &config.model {
592 info.push_str(&format!(", model={model}"));
593 }
594 if let Some(small_fast_model) = &config.small_fast_model {
595 info.push_str(&format!(", small_fast_model={small_fast_model}"));
596 }
597 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
598 info.push_str(&format!(
599 ", max_thinking_tokens={max_thinking_tokens}"
600 ));
601 }
602 println!(" {alias_name}: {info}");
603 }
604 }
605 } else {
606 println!(
608 "{}",
609 serde_json::to_string_pretty(&storage.configurations)
610 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
611 );
612 }
613 }
614 Commands::Completion { shell } => {
615 generate_completion(&shell)?;
616 }
617 }
618 } else {
619 let storage = ConfigStorage::load()?;
621 handle_interactive_selection(&storage)?;
622 }
623
624 Ok(())
625}