Skip to main content

rusty_commit/commands/
setup.rs

1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Confirm, Input, Select};
4
5use crate::cli::SetupCommand;
6use crate::config::Config;
7
8/// Provider option for the setup wizard
9struct 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    Specialized,
24}
25
26impl ProviderOption {
27    fn all() -> Vec<Self> {
28        vec![
29            // ═════════════════════════════════════════════════════════════════
30            // Popular providers
31            // ═════════════════════════════════════════════════════════════════
32            ProviderOption {
33                name: "openai",
34                display: "OpenAI (GPT-4o, GPT-4o-mini, GPT-5)",
35                default_model: "gpt-4o-mini",
36                requires_key: true,
37                category: ProviderCategory::Popular,
38            },
39            ProviderOption {
40                name: "anthropic",
41                display: "Anthropic (Claude 3.5/4 Sonnet, Haiku, Opus)",
42                default_model: "claude-3-5-haiku-20241022",
43                requires_key: true,
44                category: ProviderCategory::Popular,
45            },
46            ProviderOption {
47                name: "gemini",
48                display: "Google Gemini (2.5 Flash, 2.5 Pro)",
49                default_model: "gemini-2.5-flash",
50                requires_key: true,
51                category: ProviderCategory::Popular,
52            },
53            // ═════════════════════════════════════════════════════════════════
54            // Local/Self-hosted
55            // ═════════════════════════════════════════════════════════════════
56            ProviderOption {
57                name: "ollama",
58                display: "Ollama (Local models - free, private)",
59                default_model: "llama3.2",
60                requires_key: false,
61                category: ProviderCategory::Local,
62            },
63            ProviderOption {
64                name: "lmstudio",
65                display: "LM Studio (Local GUI for LLMs)",
66                default_model: "local-model",
67                requires_key: false,
68                category: ProviderCategory::Local,
69            },
70            ProviderOption {
71                name: "llamacpp",
72                display: "llama.cpp (Local inference)",
73                default_model: "local-model",
74                requires_key: false,
75                category: ProviderCategory::Local,
76            },
77            // ═════════════════════════════════════════════════════════════════
78            // Cloud providers - Fast Inference
79            // ═════════════════════════════════════════════════════════════════
80            ProviderOption {
81                name: "groq",
82                display: "Groq (Ultra-fast inference)",
83                default_model: "llama-3.3-70b-versatile",
84                requires_key: true,
85                category: ProviderCategory::Cloud,
86            },
87            ProviderOption {
88                name: "cerebras",
89                display: "Cerebras (Fast inference)",
90                default_model: "llama-3.3-70b",
91                requires_key: true,
92                category: ProviderCategory::Cloud,
93            },
94            ProviderOption {
95                name: "sambanova",
96                display: "SambaNova (Fast inference)",
97                default_model: "Meta-Llama-3.3-70B-Instruct",
98                requires_key: true,
99                category: ProviderCategory::Cloud,
100            },
101            ProviderOption {
102                name: "nebius",
103                display: "Nebius (GPU cloud inference)",
104                default_model: "meta-llama/Llama-3.3-70B-Instruct",
105                requires_key: true,
106                category: ProviderCategory::Cloud,
107            },
108            // ═════════════════════════════════════════════════════════════════
109            // Cloud providers - General
110            // ═════════════════════════════════════════════════════════════════
111            ProviderOption {
112                name: "xai",
113                display: "xAI (Grok)",
114                default_model: "grok-2",
115                requires_key: true,
116                category: ProviderCategory::Cloud,
117            },
118            ProviderOption {
119                name: "deepseek",
120                display: "DeepSeek (V3, R1 Reasoner)",
121                default_model: "deepseek-chat",
122                requires_key: true,
123                category: ProviderCategory::Cloud,
124            },
125            ProviderOption {
126                name: "openrouter",
127                display: "OpenRouter (Access 100+ models)",
128                default_model: "anthropic/claude-3.5-haiku",
129                requires_key: true,
130                category: ProviderCategory::Cloud,
131            },
132            ProviderOption {
133                name: "mistral",
134                display: "Mistral AI",
135                default_model: "mistral-small-latest",
136                requires_key: true,
137                category: ProviderCategory::Cloud,
138            },
139            ProviderOption {
140                name: "perplexity",
141                display: "Perplexity AI",
142                default_model: "sonar",
143                requires_key: true,
144                category: ProviderCategory::Cloud,
145            },
146            ProviderOption {
147                name: "together",
148                display: "Together AI",
149                default_model: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
150                requires_key: true,
151                category: ProviderCategory::Cloud,
152            },
153            ProviderOption {
154                name: "fireworks",
155                display: "Fireworks AI",
156                default_model: "accounts/fireworks/models/llama-v3p3-70b-instruct",
157                requires_key: true,
158                category: ProviderCategory::Cloud,
159            },
160            ProviderOption {
161                name: "replicate",
162                display: "Replicate",
163                default_model: "meta/meta-llama-3-70b-instruct",
164                requires_key: true,
165                category: ProviderCategory::Cloud,
166            },
167            // ═════════════════════════════════════════════════════════════════
168            // Enterprise
169            // ═════════════════════════════════════════════════════════════════
170            ProviderOption {
171                name: "azure",
172                display: "Azure OpenAI",
173                default_model: "gpt-4o",
174                requires_key: true,
175                category: ProviderCategory::Enterprise,
176            },
177            ProviderOption {
178                name: "bedrock",
179                display: "AWS Bedrock",
180                default_model: "anthropic.claude-3-haiku-20240307-v1:0",
181                requires_key: true,
182                category: ProviderCategory::Enterprise,
183            },
184            ProviderOption {
185                name: "vertex",
186                display: "Google Vertex AI",
187                default_model: "gemini-2.5-flash-001",
188                requires_key: true,
189                category: ProviderCategory::Enterprise,
190            },
191            ProviderOption {
192                name: "cohere",
193                display: "Cohere",
194                default_model: "command-r",
195                requires_key: true,
196                category: ProviderCategory::Enterprise,
197            },
198            ProviderOption {
199                name: "ai21",
200                display: "AI21 Labs (Jamba)",
201                default_model: "jamba-1.5-mini",
202                requires_key: true,
203                category: ProviderCategory::Enterprise,
204            },
205            // ═════════════════════════════════════════════════════════════════
206            // China-based Providers
207            // ═════════════════════════════════════════════════════════════════
208            ProviderOption {
209                name: "siliconflow",
210                display: "SiliconFlow (China)",
211                default_model: "deepseek-ai/DeepSeek-V3",
212                requires_key: true,
213                category: ProviderCategory::Cloud,
214            },
215            ProviderOption {
216                name: "zhipu",
217                display: "Zhipu AI / ChatGLM (China)",
218                default_model: "glm-4-flash",
219                requires_key: true,
220                category: ProviderCategory::Cloud,
221            },
222            ProviderOption {
223                name: "moonshot",
224                display: "Moonshot AI / Kimi (China)",
225                default_model: "moonshot-v1-8k",
226                requires_key: true,
227                category: ProviderCategory::Cloud,
228            },
229            // ═════════════════════════════════════════════════════════════════
230            // Specialized Providers
231            // ═════════════════════════════════════════════════════════════════
232            ProviderOption {
233                name: "jina",
234                display: "Jina AI (Embeddings & LLMs)",
235                default_model: "jina-embeddings-v3",
236                requires_key: true,
237                category: ProviderCategory::Specialized,
238            },
239            ProviderOption {
240                name: "helicone",
241                display: "Helicone (LLM Observability)",
242                default_model: "gpt-4o-mini",
243                requires_key: true,
244                category: ProviderCategory::Specialized,
245            },
246        ]
247    }
248
249    #[allow(dead_code)]
250    fn by_name(name: &str) -> Option<Self> {
251        Self::all().into_iter().find(|p| p.name == name)
252    }
253}
254
255/// Commit format options
256#[derive(Clone, Copy)]
257enum CommitFormat {
258    Conventional,
259    Gitmoji,
260    Simple,
261}
262
263impl CommitFormat {
264    fn display(&self) -> &'static str {
265        match self {
266            CommitFormat::Conventional => "Conventional Commits (feat:, fix:, docs:, etc.)",
267            CommitFormat::Gitmoji => "GitMoji (✨ feat:, 🐛 fix:, 📝 docs:, etc.)",
268            CommitFormat::Simple => "Simple (no prefix)",
269        }
270    }
271
272    fn as_str(&self) -> &'static str {
273        match self {
274            CommitFormat::Conventional => "conventional",
275            CommitFormat::Gitmoji => "gitmoji",
276            CommitFormat::Simple => "simple",
277        }
278    }
279
280    fn all() -> Vec<Self> {
281        vec![
282            CommitFormat::Conventional,
283            CommitFormat::Gitmoji,
284            CommitFormat::Simple,
285        ]
286    }
287}
288
289/// Language options
290struct LanguageOption {
291    code: &'static str,
292    display: &'static str,
293}
294
295impl LanguageOption {
296    fn all() -> Vec<Self> {
297        vec![
298            LanguageOption {
299                code: "en",
300                display: "English",
301            },
302            LanguageOption {
303                code: "zh",
304                display: "Chinese (中文)",
305            },
306            LanguageOption {
307                code: "es",
308                display: "Spanish (Español)",
309            },
310            LanguageOption {
311                code: "fr",
312                display: "French (Français)",
313            },
314            LanguageOption {
315                code: "de",
316                display: "German (Deutsch)",
317            },
318            LanguageOption {
319                code: "ja",
320                display: "Japanese (日本語)",
321            },
322            LanguageOption {
323                code: "ko",
324                display: "Korean (한국어)",
325            },
326            LanguageOption {
327                code: "ru",
328                display: "Russian (Русский)",
329            },
330            LanguageOption {
331                code: "pt",
332                display: "Portuguese (Português)",
333            },
334            LanguageOption {
335                code: "it",
336                display: "Italian (Italiano)",
337            },
338            LanguageOption {
339                code: "other",
340                display: "Other (specify)",
341            },
342        ]
343    }
344}
345
346pub async fn execute(cmd: SetupCommand) -> Result<()> {
347    print_welcome_header();
348
349    // Determine if we're doing quick or advanced setup
350    let is_advanced = if cmd.defaults {
351        // Non-interactive defaults mode
352        return apply_defaults().await;
353    } else if cmd.advanced {
354        true
355    } else {
356        // Ask user which mode they prefer
357        println!();
358        println!("{}", "Choose your setup mode:".bold());
359        println!();
360
361        let modes = vec![
362            "🚀 Quick Setup - Just the essentials (recommended)",
363            "⚙️  Advanced Setup - Full configuration options",
364        ];
365
366        let selection = Select::new()
367            .with_prompt("Select mode")
368            .items(&modes)
369            .default(0)
370            .interact()?;
371
372        selection == 1
373    };
374
375    if is_advanced {
376        run_advanced_setup().await
377    } else {
378        run_quick_setup().await
379    }
380}
381
382fn print_welcome_header() {
383    println!();
384    println!(
385        "{} {}",
386        "🚀".green(),
387        "Welcome to Rusty Commit Setup!".bold().white()
388    );
389    println!();
390    println!(
391        "{}",
392        "   Let's get you set up with AI-powered commit messages.".dimmed()
393    );
394    println!();
395}
396
397async fn run_quick_setup() -> Result<()> {
398    let mut config = Config::load()?;
399
400    // Step 1: Provider selection
401    let provider = select_provider_quick()?;
402    config.ai_provider = Some(provider.name.to_string());
403    config.model = Some(provider.default_model.to_string());
404
405    // Step 2: API key (if needed)
406    if provider.requires_key {
407        let api_key = prompt_for_api_key(provider.name)?;
408        if !api_key.is_empty() {
409            config.api_key = Some(api_key);
410        }
411    } else {
412        println!();
413        println!(
414            "{} {} doesn't require an API key - great for privacy!",
415            "ℹ️".blue(),
416            provider.name.bright_cyan()
417        );
418    }
419
420    // Step 3: Commit format
421    let format = select_commit_format()?;
422    config.commit_type = Some(format.as_str().to_string());
423    config.emoji = Some(matches!(format, CommitFormat::Gitmoji));
424
425    // Save configuration
426    config.save()?;
427
428    print_completion_message(&config, false);
429    Ok(())
430}
431
432async fn run_advanced_setup() -> Result<()> {
433    let mut config = Config::load()?;
434
435    // Section 1: AI Provider Configuration
436    print_section_header("🤖 AI Provider Configuration");
437
438    let provider = select_provider_advanced()?;
439    config.ai_provider = Some(provider.name.to_string());
440
441    // Custom model selection
442    let default_model = provider.default_model;
443    let use_custom_model = Confirm::new()
444        .with_prompt(format!(
445            "Use default model ({}), or specify a custom one?",
446            default_model.bright_cyan()
447        ))
448        .default(true)
449        .interact()?;
450
451    if use_custom_model {
452        config.model = Some(default_model.to_string());
453    } else {
454        let custom_model: String = Input::new()
455            .with_prompt("Enter model name")
456            .default(default_model.to_string())
457            .interact()?;
458        config.model = Some(custom_model);
459    }
460
461    // API key or custom endpoint
462    if provider.requires_key {
463        let api_key = prompt_for_api_key(provider.name)?;
464        if !api_key.is_empty() {
465            config.api_key = Some(api_key);
466        }
467
468        // Custom API URL option
469        let use_custom_url = Confirm::new()
470            .with_prompt("Use a custom API endpoint URL?")
471            .default(false)
472            .interact()?;
473
474        if use_custom_url {
475            let custom_url: String = Input::new()
476                .with_prompt("Enter custom API URL")
477                .default(format!("https://api.{}.com/v1", provider.name))
478                .interact()?;
479            config.api_url = Some(custom_url);
480        }
481    }
482
483    // Section 2: Commit Message Style
484    print_section_header("📝 Commit Message Style");
485
486    let format = select_commit_format()?;
487    config.commit_type = Some(format.as_str().to_string());
488    config.emoji = Some(matches!(format, CommitFormat::Gitmoji));
489
490    // Capitalization
491    config.description_capitalize = Some(
492        Confirm::new()
493            .with_prompt("Capitalize the first letter of commit messages?")
494            .default(true)
495            .interact()?,
496    );
497
498    // Period at end
499    config.description_add_period = Some(
500        Confirm::new()
501            .with_prompt("Add period at the end of commit messages?")
502            .default(false)
503            .interact()?,
504    );
505
506    // Max length
507    let max_length: usize = Input::new()
508        .with_prompt("Maximum commit message length")
509        .default(100)
510        .validate_with(|input: &usize| -> Result<(), &str> {
511            if *input >= 50 && *input <= 200 {
512                Ok(())
513            } else {
514                Err("Please enter a value between 50 and 200")
515            }
516        })
517        .interact()?;
518    config.description_max_length = Some(max_length);
519
520    // Language selection
521    let language = select_language()?;
522    config.language = Some(language.to_string());
523
524    // Section 3: Behavior Settings
525    print_section_header("⚙️  Behavior Settings");
526
527    // Generate count
528    let generate_count: u8 = Input::new()
529        .with_prompt("Number of commit variations to generate (1-5)")
530        .default(1)
531        .validate_with(|input: &u8| -> Result<(), &str> {
532            if *input >= 1 && *input <= 5 {
533                Ok(())
534            } else {
535                Err("Please enter a value between 1 and 5")
536            }
537        })
538        .interact()?;
539    config.generate_count = Some(generate_count);
540
541    // Git push option
542    config.gitpush = Some(
543        Confirm::new()
544            .with_prompt("Automatically push commits to remote?")
545            .default(false)
546            .interact()?,
547    );
548
549    // One-line commits
550    config.one_line_commit = Some(
551        Confirm::new()
552            .with_prompt("Always use one-line commits (no body)?")
553            .default(false)
554            .interact()?,
555    );
556
557    // Enable commit body
558    config.enable_commit_body = Some(
559        Confirm::new()
560            .with_prompt("Allow multi-line commit messages with body?")
561            .default(false)
562            .interact()?,
563    );
564
565    // Section 4: Advanced Features
566    print_section_header("🔧 Advanced Features");
567
568    // Learn from history
569    config.learn_from_history = Some(
570        Confirm::new()
571            .with_prompt("Learn commit style from repository history?")
572            .default(false)
573            .interact()?,
574    );
575
576    if config.learn_from_history == Some(true) {
577        let history_count: usize = Input::new()
578            .with_prompt("Number of commits to analyze for style")
579            .default(50)
580            .validate_with(|input: &usize| -> Result<(), &str> {
581                if *input >= 10 && *input <= 200 {
582                    Ok(())
583                } else {
584                    Err("Please enter a value between 10 and 200")
585                }
586            })
587            .interact()?;
588        config.history_commits_count = Some(history_count);
589    }
590
591    // Clipboard on timeout
592    config.clipboard_on_timeout = Some(
593        Confirm::new()
594            .with_prompt("Copy commit message to clipboard on timeout/error?")
595            .default(true)
596            .interact()?,
597    );
598
599    // Hook settings
600    config.hook_strict = Some(
601        Confirm::new()
602            .with_prompt("Strict hook mode (fail on hook errors)?")
603            .default(true)
604            .interact()?,
605    );
606
607    let hook_timeout: u64 = Input::new()
608        .with_prompt("Hook timeout (milliseconds)")
609        .default(30000)
610        .validate_with(|input: &u64| -> Result<(), &str> {
611            if *input >= 1000 && *input <= 300000 {
612                Ok(())
613            } else {
614                Err("Please enter a value between 1000 and 300000")
615            }
616        })
617        .interact()?;
618    config.hook_timeout_ms = Some(hook_timeout);
619
620    // Section 5: Token Limits (for advanced users)
621    print_section_header("🎯 Token Limits (Optional)");
622
623    let configure_tokens = Confirm::new()
624        .with_prompt("Configure token limits? (Most users can skip this)")
625        .default(false)
626        .interact()?;
627
628    if configure_tokens {
629        let max_input: usize = Input::new()
630            .with_prompt("Maximum input tokens")
631            .default(4096)
632            .interact()?;
633        config.tokens_max_input = Some(max_input);
634
635        let max_output: u32 = Input::new()
636            .with_prompt("Maximum output tokens")
637            .default(500)
638            .interact()?;
639        config.tokens_max_output = Some(max_output);
640    }
641
642    // Save configuration
643    config.save()?;
644
645    print_completion_message(&config, true);
646    Ok(())
647}
648
649fn select_provider_quick() -> Result<ProviderOption> {
650    println!();
651    println!("{}", "Select your AI provider:".bold());
652    println!(
653        "{}",
654        "   This determines which AI will generate your commit messages.".dimmed()
655    );
656    println!();
657
658    let providers = ProviderOption::all();
659    let _popular: Vec<_> = providers
660        .iter()
661        .filter(|p| p.category == ProviderCategory::Popular)
662        .map(|p| p.display)
663        .collect();
664
665    let _local: Vec<_> = providers
666        .iter()
667        .filter(|p| p.category == ProviderCategory::Local)
668        .map(|p| p.display)
669        .collect();
670
671    let _cloud: Vec<_> = providers
672        .iter()
673        .filter(|p| p.category == ProviderCategory::Cloud)
674        .map(|p| p.display)
675        .collect();
676
677    let _enterprise: Vec<_> = providers
678        .iter()
679        .filter(|p| p.category == ProviderCategory::Enterprise)
680        .map(|p| p.display)
681        .collect();
682
683    let specialized: Vec<_> = providers
684        .iter()
685        .filter(|p| p.category == ProviderCategory::Specialized)
686        .map(|p| p.display)
687        .collect();
688
689    let mut all_displays = Vec::new();
690    let mut provider_indices: Vec<usize> = Vec::new();
691
692    // Popular section
693    all_displays.push("─── Popular Providers ───".dimmed().to_string());
694    for (idx, p) in providers.iter().enumerate() {
695        if p.category == ProviderCategory::Popular {
696            all_displays.push(p.display.to_string());
697            provider_indices.push(idx);
698        }
699    }
700
701    // Local section
702    all_displays.push("─── Local/Private ───".dimmed().to_string());
703    for (idx, p) in providers.iter().enumerate() {
704        if p.category == ProviderCategory::Local {
705            all_displays.push(p.display.to_string());
706            provider_indices.push(idx);
707        }
708    }
709
710    // Cloud section
711    all_displays.push("─── Cloud Providers ───".dimmed().to_string());
712    for (idx, p) in providers.iter().enumerate() {
713        if p.category == ProviderCategory::Cloud {
714            all_displays.push(p.display.to_string());
715            provider_indices.push(idx);
716        }
717    }
718
719    // Enterprise section
720    all_displays.push("─── Enterprise ───".dimmed().to_string());
721    for (idx, p) in providers.iter().enumerate() {
722        if p.category == ProviderCategory::Enterprise {
723            all_displays.push(p.display.to_string());
724            provider_indices.push(idx);
725        }
726    }
727
728    // Specialized section
729    if !specialized.is_empty() {
730        all_displays.push("─── Specialized ───".dimmed().to_string());
731        for (idx, p) in providers.iter().enumerate() {
732            if p.category == ProviderCategory::Specialized {
733                all_displays.push(p.display.to_string());
734                provider_indices.push(idx);
735            }
736        }
737    }
738
739    let selection = Select::new()
740        .with_prompt("AI Provider")
741        .items(&all_displays)
742        .default(1) // First real item (after header)
743        .interact()?;
744
745    // Count headers before the selected item to determine actual provider index
746    let header_count = all_displays[..=selection]
747        .iter()
748        .filter(|s| s.starts_with('─'))
749        .count();
750
751    let provider_idx = if selection > 0 {
752        selection.saturating_sub(header_count)
753    } else {
754        0
755    };
756
757    let provider = if provider_idx < provider_indices.len() {
758        providers
759            .into_iter()
760            .nth(provider_indices[provider_idx])
761            .unwrap()
762    } else {
763        // Fallback to first popular provider
764        providers
765            .into_iter()
766            .find(|p| p.category == ProviderCategory::Popular)
767            .unwrap()
768    };
769
770    println!();
771    println!(
772        "{} Selected: {} {}",
773        "✓".green(),
774        provider.name.bright_cyan(),
775        format!("(model: {})", provider.default_model).dimmed()
776    );
777
778    Ok(provider)
779}
780
781fn select_provider_advanced() -> Result<ProviderOption> {
782    println!();
783    println!("{}", "Select your AI provider:".bold());
784    println!();
785
786    let providers = ProviderOption::all();
787    let items: Vec<_> = providers.iter().map(|p| p.display).collect();
788
789    let selection = Select::new()
790        .with_prompt("AI Provider")
791        .items(&items)
792        .default(0)
793        .interact()?;
794
795    let provider = providers.into_iter().nth(selection).unwrap();
796
797    println!();
798    println!("{} Selected: {}", "✓".green(), provider.name.bright_cyan());
799
800    Ok(provider)
801}
802
803fn prompt_for_api_key(provider_name: &str) -> Result<String> {
804    println!();
805    println!("{}", "API Key Configuration".bold());
806    println!(
807        "{}",
808        format!(
809            "   Get your API key from the {} dashboard",
810            provider_name.bright_cyan()
811        )
812        .dimmed()
813    );
814    println!(
815        "{}",
816        "   Your key will be stored securely in your system's keychain.".dimmed()
817    );
818    println!();
819
820    let api_key: String = Input::new()
821        .with_prompt(format!(
822            "Enter your {} API key",
823            provider_name.bright_cyan()
824        ))
825        .allow_empty(true)
826        .interact()?;
827
828    let trimmed = api_key.trim();
829
830    if trimmed.is_empty() {
831        println!();
832        println!(
833            "{} No API key provided. You can set it later with: {}",
834            "⚠️".yellow(),
835            "rco config set RCO_API_KEY=<your_key>".bright_cyan()
836        );
837    } else {
838        // Show last 4 characters for confirmation
839        let masked = if trimmed.len() > 4 {
840            format!("{}****", &trimmed[trimmed.len() - 4..])
841        } else {
842            "****".to_string()
843        };
844        println!();
845        println!("{} API key saved: {}", "✓".green(), masked.dimmed());
846    }
847
848    Ok(trimmed.to_string())
849}
850
851fn select_commit_format() -> Result<CommitFormat> {
852    println!();
853    println!("{}", "Commit Message Format".bold());
854    println!(
855        "{}",
856        "   Choose how your commit messages should be formatted.".dimmed()
857    );
858    println!();
859
860    let formats = CommitFormat::all();
861    let items: Vec<_> = formats.iter().map(|f| f.display()).collect();
862
863    let selection = Select::new()
864        .with_prompt("Commit format")
865        .items(&items)
866        .default(0)
867        .interact()?;
868
869    let format = formats.into_iter().nth(selection).unwrap();
870
871    println!();
872    println!(
873        "{} Selected: {}",
874        "✓".green(),
875        format.as_str().bright_cyan()
876    );
877
878    // Show example
879    let example = match format {
880        CommitFormat::Conventional => "feat(auth): Add login functionality",
881        CommitFormat::Gitmoji => "✨ feat(auth): Add login functionality",
882        CommitFormat::Simple => "Add login functionality",
883    };
884    println!("  Example: {}", example.dimmed());
885
886    Ok(format)
887}
888
889fn select_language() -> Result<String> {
890    println!();
891    println!("{}", "Output Language".bold());
892    println!(
893        "{}",
894        "   What language should commit messages be generated in?".dimmed()
895    );
896    println!();
897
898    let languages = LanguageOption::all();
899    let items: Vec<_> = languages.iter().map(|l| l.display).collect();
900
901    let selection = Select::new()
902        .with_prompt("Language")
903        .items(&items)
904        .default(0)
905        .interact()?;
906
907    let lang = &languages[selection];
908
909    if lang.code == "other" {
910        let custom: String = Input::new()
911            .with_prompt("Enter language code (e.g., 'nl' for Dutch)")
912            .interact()?;
913        Ok(custom)
914    } else {
915        Ok(lang.code.to_string())
916    }
917}
918
919fn print_section_header(title: &str) {
920    println!();
921    println!(
922        "{}",
923        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
924    );
925    println!("{}", title.bold());
926    println!(
927        "{}",
928        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
929    );
930}
931
932fn print_completion_message(config: &Config, is_advanced: bool) {
933    println!();
934    println!(
935        "{}",
936        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
937    );
938    println!();
939    println!("{} Setup complete! 🎉", "✓".green().bold());
940    println!();
941
942    // Show summary
943    println!("{}", "Configuration Summary:".bold());
944    println!();
945
946    if let Some(provider) = &config.ai_provider {
947        println!("  {} Provider: {}", "•".cyan(), provider.bright_white());
948    }
949    if let Some(model) = &config.model {
950        println!("  {} Model: {}", "•".cyan(), model.bright_white());
951    }
952    if let Some(commit_type) = &config.commit_type {
953        println!(
954            "  {} Commit format: {}",
955            "•".cyan(),
956            commit_type.bright_white()
957        );
958    }
959    if let Some(language) = &config.language {
960        if language != "en" {
961            println!("  {} Language: {}", "•".cyan(), language.bright_white());
962        }
963    }
964
965    println!();
966    println!("{} You're ready to go!", "→".cyan());
967    println!();
968    println!("   Try it now:  {}", "rco".bold().bright_cyan().underline());
969    println!();
970
971    if is_advanced {
972        println!("   Make a commit:  {}", "git add . && rco".dimmed());
973        println!();
974        println!(
975            "{} Modify settings anytime: {}",
976            "→".cyan(),
977            "rco setup --advanced".bright_cyan()
978        );
979        println!(
980            "{} Or use: {}",
981            "→".cyan(),
982            "rco config set <key>=<value>".bright_cyan()
983        );
984    } else {
985        println!(
986            "   Want more options? Run: {}",
987            "rco setup --advanced".bright_cyan()
988        );
989    }
990
991    println!();
992    println!(
993        "{}",
994        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
995    );
996}
997
998async fn apply_defaults() -> Result<()> {
999    let mut config = Config::load()?;
1000
1001    // Apply sensible defaults without prompting
1002    config.ai_provider = Some("openai".to_string());
1003    config.model = Some("gpt-4o-mini".to_string());
1004    config.commit_type = Some("conventional".to_string());
1005    config.description_capitalize = Some(true);
1006    config.description_add_period = Some(false);
1007    config.description_max_length = Some(100);
1008    config.language = Some("en".to_string());
1009    config.generate_count = Some(1);
1010    config.emoji = Some(false);
1011    config.gitpush = Some(false);
1012    config.one_line_commit = Some(false);
1013    config.enable_commit_body = Some(false);
1014    config.learn_from_history = Some(false);
1015    config.clipboard_on_timeout = Some(true);
1016    config.hook_strict = Some(true);
1017    config.hook_timeout_ms = Some(30000);
1018    config.tokens_max_input = Some(4096);
1019    config.tokens_max_output = Some(500);
1020
1021    config.save()?;
1022
1023    println!();
1024    println!("{} Default configuration applied!", "✓".green().bold());
1025    println!();
1026    println!("   Provider: openai (gpt-4o-mini)");
1027    println!("   Format: conventional commits");
1028    println!();
1029    println!(
1030        "   Set your API key: {}",
1031        "rco config set RCO_API_KEY=<your_key>".bright_cyan()
1032    );
1033    println!();
1034
1035    Ok(())
1036}