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