1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Confirm, Input, Select};
4
5use crate::cli::SetupCommand;
6use crate::config::Config;
7
8
9struct ProviderOption {
11 name: &'static str,
12 display: &'static str,
13 default_model: &'static str,
14 requires_key: bool,
15 category: ProviderCategory,
16}
17
18#[derive(Clone, Copy, PartialEq)]
19enum ProviderCategory {
20 Popular,
21 Local,
22 Cloud,
23 Enterprise,
24}
25
26impl ProviderOption {
27 fn all() -> Vec<Self> {
28 vec![
29 ProviderOption {
31 name: "openai",
32 display: "OpenAI (GPT-4o, GPT-4o-mini)",
33 default_model: "gpt-4o-mini",
34 requires_key: true,
35 category: ProviderCategory::Popular,
36 },
37 ProviderOption {
38 name: "anthropic",
39 display: "Anthropic (Claude 3.5 Sonnet, Haiku)",
40 default_model: "claude-3-5-haiku-20241022",
41 requires_key: true,
42 category: ProviderCategory::Popular,
43 },
44 ProviderOption {
45 name: "gemini",
46 display: "Google Gemini (Flash, Pro)",
47 default_model: "gemini-1.5-flash",
48 requires_key: true,
49 category: ProviderCategory::Popular,
50 },
51 ProviderOption {
53 name: "ollama",
54 display: "Ollama (Local models - free, private)",
55 default_model: "llama3.2",
56 requires_key: false,
57 category: ProviderCategory::Local,
58 },
59 ProviderOption {
61 name: "xai",
62 display: "xAI (Grok)",
63 default_model: "grok-2",
64 requires_key: true,
65 category: ProviderCategory::Cloud,
66 },
67 ProviderOption {
68 name: "deepseek",
69 display: "DeepSeek (V3, Coder)",
70 default_model: "deepseek-chat",
71 requires_key: true,
72 category: ProviderCategory::Cloud,
73 },
74 ProviderOption {
75 name: "groq",
76 display: "Groq (Fast inference)",
77 default_model: "llama-3.1-8b-instant",
78 requires_key: true,
79 category: ProviderCategory::Cloud,
80 },
81 ProviderOption {
82 name: "openrouter",
83 display: "OpenRouter (Access many models)",
84 default_model: "anthropic/claude-3.5-haiku",
85 requires_key: true,
86 category: ProviderCategory::Cloud,
87 },
88 ProviderOption {
89 name: "mistral",
90 display: "Mistral AI",
91 default_model: "mistral-small-latest",
92 requires_key: true,
93 category: ProviderCategory::Cloud,
94 },
95 ProviderOption {
96 name: "perplexity",
97 display: "Perplexity AI",
98 default_model: "sonar",
99 requires_key: true,
100 category: ProviderCategory::Cloud,
101 },
102 ProviderOption {
104 name: "azure",
105 display: "Azure OpenAI",
106 default_model: "gpt-4o",
107 requires_key: true,
108 category: ProviderCategory::Enterprise,
109 },
110 ProviderOption {
111 name: "bedrock",
112 display: "AWS Bedrock",
113 default_model: "anthropic.claude-3-haiku-20240307-v1:0",
114 requires_key: true,
115 category: ProviderCategory::Enterprise,
116 },
117 ProviderOption {
118 name: "vertex",
119 display: "Google Vertex AI",
120 default_model: "gemini-1.5-flash-001",
121 requires_key: true,
122 category: ProviderCategory::Enterprise,
123 },
124 ]
125 }
126
127 fn by_name(name: &str) -> Option<Self> {
128 Self::all().into_iter().find(|p| p.name == name)
129 }
130}
131
132#[derive(Clone, Copy)]
134enum CommitFormat {
135 Conventional,
136 Gitmoji,
137 Simple,
138}
139
140impl CommitFormat {
141 fn display(&self) -> &'static str {
142 match self {
143 CommitFormat::Conventional => "Conventional Commits (feat:, fix:, docs:, etc.)",
144 CommitFormat::Gitmoji => "GitMoji (β¨ feat:, π fix:, π docs:, etc.)",
145 CommitFormat::Simple => "Simple (no prefix)",
146 }
147 }
148
149 fn as_str(&self) -> &'static str {
150 match self {
151 CommitFormat::Conventional => "conventional",
152 CommitFormat::Gitmoji => "gitmoji",
153 CommitFormat::Simple => "simple",
154 }
155 }
156
157 fn all() -> Vec<Self> {
158 vec![
159 CommitFormat::Conventional,
160 CommitFormat::Gitmoji,
161 CommitFormat::Simple,
162 ]
163 }
164}
165
166struct LanguageOption {
168 code: &'static str,
169 display: &'static str,
170}
171
172impl LanguageOption {
173 fn all() -> Vec<Self> {
174 vec![
175 LanguageOption {
176 code: "en",
177 display: "English",
178 },
179 LanguageOption {
180 code: "zh",
181 display: "Chinese (δΈζ)",
182 },
183 LanguageOption {
184 code: "es",
185 display: "Spanish (EspaΓ±ol)",
186 },
187 LanguageOption {
188 code: "fr",
189 display: "French (FranΓ§ais)",
190 },
191 LanguageOption {
192 code: "de",
193 display: "German (Deutsch)",
194 },
195 LanguageOption {
196 code: "ja",
197 display: "Japanese (ζ₯ζ¬θͺ)",
198 },
199 LanguageOption {
200 code: "ko",
201 display: "Korean (νκ΅μ΄)",
202 },
203 LanguageOption {
204 code: "ru",
205 display: "Russian (Π ΡΡΡΠΊΠΈΠΉ)",
206 },
207 LanguageOption {
208 code: "pt",
209 display: "Portuguese (PortuguΓͺs)",
210 },
211 LanguageOption {
212 code: "it",
213 display: "Italian (Italiano)",
214 },
215 LanguageOption {
216 code: "other",
217 display: "Other (specify)",
218 },
219 ]
220 }
221}
222
223pub async fn execute(cmd: SetupCommand) -> Result<()> {
224 print_welcome_header();
225
226 let is_advanced = if cmd.defaults {
228 return apply_defaults().await;
230 } else if cmd.advanced {
231 true
232 } else {
233 println!();
235 println!("{}", "Choose your setup mode:".bold());
236 println!();
237
238 let modes = vec![
239 "π Quick Setup - Just the essentials (recommended)",
240 "βοΈ Advanced Setup - Full configuration options",
241 ];
242
243 let selection = Select::new()
244 .with_prompt("Select mode")
245 .items(&modes)
246 .default(0)
247 .interact()?;
248
249 selection == 1
250 };
251
252 if is_advanced {
253 run_advanced_setup().await
254 } else {
255 run_quick_setup().await
256 }
257}
258
259fn print_welcome_header() {
260 println!();
261 println!(
262 "{} {}",
263 "π".green(),
264 "Welcome to Rusty Commit Setup!".bold().white()
265 );
266 println!();
267 println!(
268 "{}",
269 " Let's get you set up with AI-powered commit messages.".dimmed()
270 );
271 println!();
272}
273
274async fn run_quick_setup() -> Result<()> {
275 let mut config = Config::load()?;
276
277 let provider = select_provider_quick()?;
279 config.ai_provider = Some(provider.name.to_string());
280 config.model = Some(provider.default_model.to_string());
281
282 if provider.requires_key {
284 let api_key = prompt_for_api_key(provider.name)?;
285 if !api_key.is_empty() {
286 config.api_key = Some(api_key);
287 }
288 } else {
289 println!();
290 println!(
291 "{} {} doesn't require an API key - great for privacy!",
292 "βΉοΈ".blue(),
293 provider.name.bright_cyan()
294 );
295 }
296
297 let format = select_commit_format()?;
299 config.commit_type = Some(format.as_str().to_string());
300 config.emoji = Some(matches!(format, CommitFormat::Gitmoji));
301
302 config.save()?;
304
305 print_completion_message(&config, false);
306 Ok(())
307}
308
309async fn run_advanced_setup() -> Result<()> {
310 let mut config = Config::load()?;
311
312 print_section_header("π€ AI Provider Configuration");
314
315 let provider = select_provider_advanced()?;
316 config.ai_provider = Some(provider.name.to_string());
317
318 let default_model = provider.default_model;
320 let use_custom_model = Confirm::new()
321 .with_prompt(format!(
322 "Use default model ({}), or specify a custom one?",
323 default_model.bright_cyan()
324 ))
325 .default(true)
326 .interact()?;
327
328 if use_custom_model {
329 config.model = Some(default_model.to_string());
330 } else {
331 let custom_model: String = Input::new()
332 .with_prompt("Enter model name")
333 .default(default_model.to_string())
334 .interact()?;
335 config.model = Some(custom_model);
336 }
337
338 if provider.requires_key {
340 let api_key = prompt_for_api_key(provider.name)?;
341 if !api_key.is_empty() {
342 config.api_key = Some(api_key);
343 }
344
345 let use_custom_url = Confirm::new()
347 .with_prompt("Use a custom API endpoint URL?")
348 .default(false)
349 .interact()?;
350
351 if use_custom_url {
352 let custom_url: String = Input::new()
353 .with_prompt("Enter custom API URL")
354 .default(format!("https://api.{}.com/v1", provider.name))
355 .interact()?;
356 config.api_url = Some(custom_url);
357 }
358 }
359
360 print_section_header("π Commit Message Style");
362
363 let format = select_commit_format()?;
364 config.commit_type = Some(format.as_str().to_string());
365 config.emoji = Some(matches!(format, CommitFormat::Gitmoji));
366
367 config.description_capitalize = Some(
369 Confirm::new()
370 .with_prompt("Capitalize the first letter of commit messages?")
371 .default(true)
372 .interact()?,
373 );
374
375 config.description_add_period = Some(
377 Confirm::new()
378 .with_prompt("Add period at the end of commit messages?")
379 .default(false)
380 .interact()?,
381 );
382
383 let max_length: usize = Input::new()
385 .with_prompt("Maximum commit message length")
386 .default(100)
387 .validate_with(|input: &usize| -> Result<(), &str> {
388 if *input >= 50 && *input <= 200 {
389 Ok(())
390 } else {
391 Err("Please enter a value between 50 and 200")
392 }
393 })
394 .interact()?;
395 config.description_max_length = Some(max_length);
396
397 let language = select_language()?;
399 config.language = Some(language.to_string());
400
401 print_section_header("βοΈ Behavior Settings");
403
404 let generate_count: u8 = Input::new()
406 .with_prompt("Number of commit variations to generate (1-5)")
407 .default(1)
408 .validate_with(|input: &u8| -> Result<(), &str> {
409 if *input >= 1 && *input <= 5 {
410 Ok(())
411 } else {
412 Err("Please enter a value between 1 and 5")
413 }
414 })
415 .interact()?;
416 config.generate_count = Some(generate_count);
417
418 config.gitpush = Some(
420 Confirm::new()
421 .with_prompt("Automatically push commits to remote?")
422 .default(false)
423 .interact()?,
424 );
425
426 config.one_line_commit = Some(
428 Confirm::new()
429 .with_prompt("Always use one-line commits (no body)?")
430 .default(false)
431 .interact()?,
432 );
433
434 config.enable_commit_body = Some(
436 Confirm::new()
437 .with_prompt("Allow multi-line commit messages with body?")
438 .default(false)
439 .interact()?,
440 );
441
442 print_section_header("π§ Advanced Features");
444
445 config.learn_from_history = Some(
447 Confirm::new()
448 .with_prompt("Learn commit style from repository history?")
449 .default(false)
450 .interact()?,
451 );
452
453 if config.learn_from_history == Some(true) {
454 let history_count: usize = Input::new()
455 .with_prompt("Number of commits to analyze for style")
456 .default(50)
457 .validate_with(|input: &usize| -> Result<(), &str> {
458 if *input >= 10 && *input <= 200 {
459 Ok(())
460 } else {
461 Err("Please enter a value between 10 and 200")
462 }
463 })
464 .interact()?;
465 config.history_commits_count = Some(history_count);
466 }
467
468 config.clipboard_on_timeout = Some(
470 Confirm::new()
471 .with_prompt("Copy commit message to clipboard on timeout/error?")
472 .default(true)
473 .interact()?,
474 );
475
476 config.hook_strict = Some(
478 Confirm::new()
479 .with_prompt("Strict hook mode (fail on hook errors)?")
480 .default(true)
481 .interact()?,
482 );
483
484 let hook_timeout: u64 = Input::new()
485 .with_prompt("Hook timeout (milliseconds)")
486 .default(30000)
487 .validate_with(|input: &u64| -> Result<(), &str> {
488 if *input >= 1000 && *input <= 300000 {
489 Ok(())
490 } else {
491 Err("Please enter a value between 1000 and 300000")
492 }
493 })
494 .interact()?;
495 config.hook_timeout_ms = Some(hook_timeout);
496
497 print_section_header("π― Token Limits (Optional)");
499
500 let configure_tokens = Confirm::new()
501 .with_prompt("Configure token limits? (Most users can skip this)")
502 .default(false)
503 .interact()?;
504
505 if configure_tokens {
506 let max_input: usize = Input::new()
507 .with_prompt("Maximum input tokens")
508 .default(4096)
509 .interact()?;
510 config.tokens_max_input = Some(max_input);
511
512 let max_output: u32 = Input::new()
513 .with_prompt("Maximum output tokens")
514 .default(500)
515 .interact()?;
516 config.tokens_max_output = Some(max_output);
517 }
518
519 config.save()?;
521
522 print_completion_message(&config, true);
523 Ok(())
524}
525
526fn select_provider_quick() -> Result<ProviderOption> {
527 println!();
528 println!("{}", "Select your AI provider:".bold());
529 println!("{}", " This determines which AI will generate your commit messages.".dimmed());
530 println!();
531
532 let providers = ProviderOption::all();
533 let popular: Vec<_> = providers
534 .iter()
535 .filter(|p| p.category == ProviderCategory::Popular)
536 .map(|p| p.display)
537 .collect();
538
539 let local: Vec<_> = providers
540 .iter()
541 .filter(|p| p.category == ProviderCategory::Local)
542 .map(|p| p.display)
543 .collect();
544
545 let others: Vec<_> = providers
546 .iter()
547 .filter(|p| {
548 p.category == ProviderCategory::Cloud || p.category == ProviderCategory::Enterprise
549 })
550 .map(|p| p.display)
551 .collect();
552
553 let mut all_displays = Vec::new();
554 all_displays.push("βββ Popular Providers βββ".dimmed().to_string());
555 all_displays.extend(popular.iter().map(|s| s.to_string()));
556 all_displays.push("βββ Local/Private βββ".dimmed().to_string());
557 all_displays.extend(local.iter().map(|s| s.to_string()));
558 all_displays.push("βββ More Cloud Providers βββ".dimmed().to_string());
559 all_displays.extend(others.iter().map(|s| s.to_string()));
560
561 let selection = Select::new()
562 .with_prompt("AI Provider")
563 .items(&all_displays)
564 .default(1) .interact()?;
566
567 let provider_name = match selection {
569 1 => "openai",
570 2 => "anthropic",
571 3 => "gemini",
572 5 => "ollama",
573 7 => "xai",
574 8 => "deepseek",
575 9 => "groq",
576 10 => "openrouter",
577 11 => "mistral",
578 12 => "perplexity",
579 13 => "azure",
580 14 => "bedrock",
581 15 => "vertex",
582 _ => "openai",
583 };
584
585 let provider = ProviderOption::by_name(provider_name).unwrap();
586
587 println!();
588 println!(
589 "{} Selected: {} {}",
590 "β".green(),
591 provider.name.bright_cyan(),
592 format!("(model: {})", provider.default_model).dimmed()
593 );
594
595 Ok(provider)
596}
597
598fn select_provider_advanced() -> Result<ProviderOption> {
599 println!();
600 println!("{}", "Select your AI provider:".bold());
601 println!();
602
603 let providers = ProviderOption::all();
604 let items: Vec<_> = providers.iter().map(|p| p.display).collect();
605
606 let selection = Select::new()
607 .with_prompt("AI Provider")
608 .items(&items)
609 .default(0)
610 .interact()?;
611
612 let provider = providers.into_iter().nth(selection).unwrap();
613
614 println!();
615 println!(
616 "{} Selected: {}",
617 "β".green(),
618 provider.name.bright_cyan()
619 );
620
621 Ok(provider)
622}
623
624fn prompt_for_api_key(provider_name: &str) -> Result<String> {
625 println!();
626 println!("{}", "API Key Configuration".bold());
627 println!(
628 "{}",
629 format!(
630 " Get your API key from the {} dashboard",
631 provider_name.bright_cyan()
632 )
633 .dimmed()
634 );
635 println!(
636 "{}",
637 " Your key will be stored securely in your system's keychain.".dimmed()
638 );
639 println!();
640
641 let api_key: String = Input::new()
642 .with_prompt(format!("Enter your {} API key", provider_name.bright_cyan()))
643 .allow_empty(true)
644 .interact()?;
645
646 let trimmed = api_key.trim();
647
648 if trimmed.is_empty() {
649 println!();
650 println!(
651 "{} No API key provided. You can set it later with: {}",
652 "β οΈ".yellow(),
653 format!("rco config set RCO_API_KEY=<your_key>").bright_cyan()
654 );
655 } else {
656 let masked = if trimmed.len() > 4 {
658 format!("{}****", &trimmed[trimmed.len() - 4..])
659 } else {
660 "****".to_string()
661 };
662 println!();
663 println!("{} API key saved: {}", "β".green(), masked.dimmed());
664 }
665
666 Ok(trimmed.to_string())
667}
668
669fn select_commit_format() -> Result<CommitFormat> {
670 println!();
671 println!("{}", "Commit Message Format".bold());
672 println!(
673 "{}",
674 " Choose how your commit messages should be formatted.".dimmed()
675 );
676 println!();
677
678 let formats = CommitFormat::all();
679 let items: Vec<_> = formats.iter().map(|f| f.display()).collect();
680
681 let selection = Select::new()
682 .with_prompt("Commit format")
683 .items(&items)
684 .default(0)
685 .interact()?;
686
687 let format = formats.into_iter().nth(selection).unwrap();
688
689 println!();
690 println!(
691 "{} Selected: {}",
692 "β".green(),
693 format.as_str().bright_cyan()
694 );
695
696 let example = match format {
698 CommitFormat::Conventional => "feat(auth): Add login functionality",
699 CommitFormat::Gitmoji => "β¨ feat(auth): Add login functionality",
700 CommitFormat::Simple => "Add login functionality",
701 };
702 println!(" Example: {}", example.dimmed());
703
704 Ok(format)
705}
706
707fn select_language() -> Result<String> {
708 println!();
709 println!("{}", "Output Language".bold());
710 println!(
711 "{}",
712 " What language should commit messages be generated in?".dimmed()
713 );
714 println!();
715
716 let languages = LanguageOption::all();
717 let items: Vec<_> = languages.iter().map(|l| l.display).collect();
718
719 let selection = Select::new()
720 .with_prompt("Language")
721 .items(&items)
722 .default(0)
723 .interact()?;
724
725 let lang = &languages[selection];
726
727 if lang.code == "other" {
728 let custom: String = Input::new()
729 .with_prompt("Enter language code (e.g., 'nl' for Dutch)")
730 .interact()?;
731 Ok(custom)
732 } else {
733 Ok(lang.code.to_string())
734 }
735}
736
737fn print_section_header(title: &str) {
738 println!();
739 println!(
740 "{}",
741 "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()
742 );
743 println!("{}", title.bold());
744 println!(
745 "{}",
746 "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()
747 );
748}
749
750fn print_completion_message(config: &Config, is_advanced: bool) {
751 println!();
752 println!(
753 "{}",
754 "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()
755 );
756 println!();
757 println!("{} Setup complete! π", "β".green().bold());
758 println!();
759
760 println!("{}", "Configuration Summary:".bold());
762 println!();
763
764 if let Some(ref provider) = config.ai_provider {
765 println!(" {} Provider: {}", "β’".cyan(), provider.bright_white());
766 }
767 if let Some(ref model) = config.model {
768 println!(" {} Model: {}", "β’".cyan(), model.bright_white());
769 }
770 if let Some(ref commit_type) = config.commit_type {
771 println!(
772 " {} Commit format: {}",
773 "β’".cyan(),
774 commit_type.bright_white()
775 );
776 }
777 if let Some(ref language) = config.language {
778 if language != "en" {
779 println!(
780 " {} Language: {}",
781 "β’".cyan(),
782 language.bright_white()
783 );
784 }
785 }
786
787 println!();
788 println!("{} You're ready to go!", "β".cyan());
789 println!();
790 println!(
791 " Try it now: {}",
792 "rco".bold().bright_cyan().underline()
793 );
794 println!();
795
796 if is_advanced {
797 println!(
798 " Make a commit: {}",
799 "git add . && rco".dimmed()
800 );
801 println!();
802 println!(
803 "{} Modify settings anytime: {}",
804 "β".cyan(),
805 "rco setup --advanced".bright_cyan()
806 );
807 println!(
808 "{} Or use: {}",
809 "β".cyan(),
810 "rco config set <key>=<value>".bright_cyan()
811 );
812 } else {
813 println!(
814 " Want more options? Run: {}",
815 "rco setup --advanced".bright_cyan()
816 );
817 }
818
819 println!();
820 println!(
821 "{}",
822 "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ".dimmed()
823 );
824}
825
826async fn apply_defaults() -> Result<()> {
827 let mut config = Config::load()?;
828
829 config.ai_provider = Some("openai".to_string());
831 config.model = Some("gpt-4o-mini".to_string());
832 config.commit_type = Some("conventional".to_string());
833 config.description_capitalize = Some(true);
834 config.description_add_period = Some(false);
835 config.description_max_length = Some(100);
836 config.language = Some("en".to_string());
837 config.generate_count = Some(1);
838 config.emoji = Some(false);
839 config.gitpush = Some(false);
840 config.one_line_commit = Some(false);
841 config.enable_commit_body = Some(false);
842 config.learn_from_history = Some(false);
843 config.clipboard_on_timeout = Some(true);
844 config.hook_strict = Some(true);
845 config.hook_timeout_ms = Some(30000);
846 config.tokens_max_input = Some(4096);
847 config.tokens_max_output = Some(500);
848
849 config.save()?;
850
851 println!();
852 println!("{} Default configuration applied!", "β".green().bold());
853 println!();
854 println!(" Provider: openai (gpt-4o-mini)");
855 println!(" Format: conventional commits");
856 println!();
857 println!(
858 " Set your API key: {}",
859 "rco config set RCO_API_KEY=<your_key>".bright_cyan()
860 );
861 println!();
862
863 Ok(())
864}