use anyhow::Result; use clap::Parser; use colored::Colorize; use dialoguer::{Confirm, Input, Select}; use std::path::PathBuf; use crate::config::{GitProfile, Language}; use crate::config::manager::ConfigManager; use crate::config::profile::{GpgConfig, SshConfig}; use crate::i18n::Messages; use crate::utils::validators::validate_email; /// Initialize quicommit configuration #[derive(Parser)] pub struct InitCommand { /// Skip interactive setup #[arg(short, long)] yes: bool, /// Reset existing configuration #[arg(long)] reset: bool, } impl InitCommand { pub async fn execute(&self, config_path: Option) -> Result<()> { let messages = Messages::new(Language::English); println!("{}", messages.initializing().bold().cyan()); let config_path = config_path.unwrap_or_else(|| { crate::config::AppConfig::default_path().unwrap() }); // Check if config already exists if config_path.exists() && !self.reset { if !self.yes { let overwrite = Confirm::new() .with_prompt("Configuration already exists. Overwrite?") .default(false) .interact()?; if !overwrite { println!("{}", "Initialization cancelled.".yellow()); return Ok(()); } } else { println!("{}", "Configuration already exists. Use --reset to overwrite.".yellow()); return Ok(()); } } // Create parent directory if needed if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| anyhow::anyhow!("Failed to create config directory: {}", e))?; } // Create new config manager with fresh config let mut manager = ConfigManager::with_path_fresh(&config_path)?; if self.yes { self.quick_setup(&mut manager).await?; } else { self.interactive_setup(&mut manager).await?; } manager.save()?; // Get configured language for final messages let language = manager.get_language().unwrap_or(Language::English); let messages = Messages::new(language); println!("{}", messages.init_success().bold().green()); println!("\n{}: {}", messages.config_file(), config_path.display()); println!("\n{}:", messages.next_steps()); println!(" 1. Create a profile: {}", "quicommit profile add".cyan()); println!(" 2. Configure LLM: {}", "quicommit config set-llm".cyan()); println!(" 3. Start committing: {}", "quicommit commit".cyan()); Ok(()) } async fn quick_setup(&self, manager: &mut ConfigManager) -> Result<()> { // Try to get git user info let git_config = git2::Config::open_default()?; let user_name = git_config.get_string("user.name").unwrap_or_else(|_| "User".to_string()); let user_email = git_config.get_string("user.email").unwrap_or_else(|_| "user@example.com".to_string()); let profile = GitProfile::new( "default".to_string(), user_name, user_email, ); manager.add_profile("default".to_string(), profile)?; manager.set_default_profile(Some("default".to_string()))?; // Set default LLM to Ollama manager.set_llm_provider("ollama".to_string()); Ok(()) } async fn interactive_setup(&self, manager: &mut ConfigManager) -> Result<()> { let messages = Messages::new(Language::English); println!("\n{}", messages.setup_profile().bold()); // Language selection println!("\n{}", messages.select_output_language().bold()); let languages = vec![ Language::English, Language::Chinese, Language::Japanese, Language::Korean, Language::Spanish, Language::French, Language::German, ]; let language_names: Vec = languages.iter().map(|l| l.display_name().to_string()).collect(); let language_idx = Select::new() .items(&language_names) .default(0) .interact()?; let selected_language = languages[language_idx]; manager.set_output_language(selected_language.to_code().to_string()); // Update messages to selected language let messages = Messages::new(selected_language); // Profile name let profile_name: String = Input::new() .with_prompt(messages.profile_name()) .default("personal".to_string()) .interact_text()?; // User info let git_config = git2::Config::open_default().ok(); let default_name = git_config.as_ref() .and_then(|c| c.get_string("user.name").ok()) .unwrap_or_default(); let default_email = git_config.as_ref() .and_then(|c| c.get_string("user.email").ok()) .unwrap_or_default(); let user_name: String = Input::new() .with_prompt(messages.git_user_name()) .default(default_name) .interact_text()?; let user_email: String = Input::new() .with_prompt(messages.git_user_email()) .default(default_email) .validate_with(|input: &String| { validate_email(input).map_err(|e| e.to_string()) }) .interact_text()?; let description: String = Input::new() .with_prompt(messages.profile_description()) .allow_empty(true) .interact_text()?; let is_work = Confirm::new() .with_prompt(messages.is_work_profile()) .default(false) .interact()?; let organization = if is_work { Some(Input::new() .with_prompt(messages.organization_name()) .interact_text()?) } else { None }; // SSH configuration let setup_ssh = Confirm::new() .with_prompt(messages.configure_ssh()) .default(false) .interact()?; let ssh_config = if setup_ssh { Some(self.setup_ssh_interactive(&messages).await?) } else { None }; // GPG configuration let setup_gpg = Confirm::new() .with_prompt(messages.configure_gpg()) .default(false) .interact()?; let gpg_config = if setup_gpg { Some(self.setup_gpg_interactive(&messages).await?) } else { None }; // Create profile let mut profile = GitProfile::new( profile_name.clone(), user_name, user_email, ); if !description.is_empty() { profile.description = Some(description); } profile.is_work = is_work; profile.organization = organization; profile.ssh = ssh_config; profile.gpg = gpg_config; manager.add_profile(profile_name.clone(), profile)?; manager.set_default_profile(Some(profile_name))?; // LLM provider selection println!("\n{}", messages.select_llm_provider().bold()); let providers = vec![ "Ollama (local)", "OpenAI", "Anthropic Claude", "Kimi (Moonshot AI)", "DeepSeek", "OpenRouter" ]; let provider_idx = Select::new() .items(&providers) .default(0) .interact()?; let provider = match provider_idx { 0 => "ollama", 1 => "openai", 2 => "anthropic", 3 => "kimi", 4 => "deepseek", 5 => "openrouter", _ => "ollama", }; manager.set_llm_provider(provider.to_string()); // Configure API key if needed if provider == "openai" { let api_key: String = Input::new() .with_prompt(messages.openai_api_key()) .interact_text()?; manager.set_openai_api_key(api_key); } else if provider == "anthropic" { let api_key: String = Input::new() .with_prompt(messages.anthropic_api_key()) .interact_text()?; manager.set_anthropic_api_key(api_key); } else if provider == "kimi" { let api_key: String = Input::new() .with_prompt(messages.kimi_api_key()) .interact_text()?; manager.set_kimi_api_key(api_key); } else if provider == "deepseek" { let api_key: String = Input::new() .with_prompt(messages.deepseek_api_key()) .interact_text()?; manager.set_deepseek_api_key(api_key); } else if provider == "openrouter" { let api_key: String = Input::new() .with_prompt(messages.openrouter_api_key()) .interact_text()?; manager.set_openrouter_api_key(api_key); } Ok(()) } async fn setup_ssh_interactive(&self, messages: &Messages) -> Result { use std::path::PathBuf; let ssh_dir = dirs::home_dir() .map(|h| h.join(".ssh")) .unwrap_or_else(|| PathBuf::from("~/.ssh")); let key_path: String = Input::new() .with_prompt(messages.ssh_private_key_path()) .default(ssh_dir.join("id_rsa").display().to_string()) .interact_text()?; let has_passphrase = Confirm::new() .with_prompt(messages.has_passphrase()) .default(false) .interact()?; let passphrase = if has_passphrase { Some(crate::utils::password_input(messages.ssh_key_passphrase())?) } else { None }; Ok(SshConfig { private_key_path: Some(PathBuf::from(key_path)), public_key_path: None, passphrase, agent_forwarding: false, ssh_command: None, known_hosts_file: None, }) } async fn setup_gpg_interactive(&self, messages: &Messages) -> Result { let key_id: String = Input::new() .with_prompt(messages.gpg_key_id()) .interact_text()?; let use_agent = Confirm::new() .with_prompt(messages.use_gpg_agent()) .default(true) .interact()?; Ok(GpgConfig { key_id, program: "gpg".to_string(), home_dir: None, passphrase: None, use_agent, }) } }