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