Skip to main content

rusty_commit/commands/
config.rs

1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Input, Select};
4
5use crate::cli::{ConfigAction, ConfigCommand};
6use crate::config::{self, accounts, Config};
7
8/// Unified output helper for config commands.
9#[allow(dead_code)]
10struct ConfigOutput;
11
12#[allow(dead_code)]
13impl ConfigOutput {
14    fn header(&self, text: &str) {
15        println!("\n{}", text.bold());
16    }
17
18    fn subheader(&self, text: &str) {
19        println!("{}", text.dimmed());
20    }
21
22    fn success(&self, message: &str) {
23        println!("{}", format!("✅ {}", message).green());
24    }
25
26    fn error(&self, message: &str) {
27        println!("{}", format!("❌ {}", message).red());
28    }
29
30    fn warning(&self, message: &str) {
31        println!("{}", format!("⚠️  {}", message).yellow());
32    }
33
34    fn info(&self, message: &str) {
35        println!("{}", message.cyan());
36    }
37
38    fn divider(&self) {
39        println!("{}", "─".repeat(50).dimmed());
40    }
41
42    fn section(&self, title: &str) {
43        self.divider();
44        println!("{}", title.cyan().bold());
45        self.divider();
46    }
47
48    fn key_value(&self, key: &str, value: &str) {
49        println!("  {}: {}", key.dimmed(), value);
50    }
51}
52
53pub async fn execute(cmd: ConfigCommand) -> Result<()> {
54    let mut config = Config::load()?;
55
56    match cmd.action {
57        ConfigAction::Set { pairs } => {
58            for pair in pairs {
59                let parts: Vec<&str> = pair.splitn(2, '=').collect();
60                if parts.len() != 2 {
61                    eprintln!("{}", format!("Invalid format: {pair}. Use KEY=value").red());
62                    continue;
63                }
64
65                let key = parts[0];
66                let value = parts[1];
67
68                match config.set(key, value) {
69                    Ok(_) => {
70                        println!("{}", format!("✅ {key} set to: {value}").green());
71                    }
72                    Err(e) => {
73                        eprintln!("{}", format!("❌ Failed to set {key}: {e}").red());
74                    }
75                }
76            }
77        }
78        ConfigAction::Get { key } => match config.get(&key) {
79            Ok(value) => {
80                println!("{key}: {value}");
81            }
82            Err(e) => {
83                eprintln!("{}", format!("❌ {e}").red());
84            }
85        },
86        ConfigAction::Reset { all, keys } => {
87            if all {
88                config.reset(None)?;
89                println!("{}", "✅ All configuration reset to defaults".green());
90            } else if !keys.is_empty() {
91                config.reset(Some(&keys))?;
92                println!("{}", format!("✅ Reset keys: {}", keys.join(", ")).green());
93            } else {
94                eprintln!("{}", "Please specify --all or provide keys to reset".red());
95            }
96        }
97        ConfigAction::Status => {
98            let out = ConfigOutput;
99            out.header("🔐 Secure Storage Status");
100            out.divider();
101
102            // Show platform info
103            out.key_value("Platform", &config::secure_storage::get_platform_info());
104
105            let status = config::secure_storage::status_message();
106            out.key_value("Status", &status);
107
108            if config::secure_storage::is_available() {
109                println!("\n{}", "✅ API keys will be stored securely".green());
110                out.subheader("Your API keys are encrypted and protected by your system");
111
112                // Platform-specific information
113                #[cfg(target_os = "macos")]
114                out.subheader("Stored in: macOS Keychain (login keychain)");
115
116                #[cfg(target_os = "linux")]
117                out.subheader("Stored in: Secret Service (GNOME Keyring/KWallet)");
118
119                #[cfg(target_os = "windows")]
120                out.subheader("Stored in: Windows Credential Manager");
121            } else {
122                out.warning("API keys will be stored in the configuration file");
123                out.subheader("Location: ~/.config/rustycommit/config.toml");
124
125                #[cfg(not(feature = "secure-storage"))]
126                {
127                    out.subheader("To enable secure storage:");
128                    out.subheader("cargo install rustycommit --features secure-storage");
129                }
130
131                #[cfg(feature = "secure-storage")]
132                {
133                    out.subheader("Note: Secure storage is not available on this system");
134                    out.subheader("Falling back to file-based storage");
135                }
136            }
137
138            // Show current API key status
139            println!("\n{}", "Current Configuration:".bold());
140            if config.api_key.is_some()
141                || config::secure_storage::get_secret("RCO_API_KEY")?.is_some()
142            {
143                println!("{}", "🔑 API key is configured".green());
144
145                // Show which storage method is being used
146                if config::secure_storage::is_available()
147                    && config::secure_storage::get_secret("RCO_API_KEY")?.is_some()
148                {
149                    println!("{}", "   Stored securely in system keychain".dimmed());
150                } else if config.api_key.is_some() {
151                    println!("{}", "   Stored in configuration file".dimmed());
152                }
153            } else {
154                println!("{}", "❌ No API key configured".red());
155                println!(
156                    "{}",
157                    "   Run: rco config set RCO_API_KEY=<your_key>".dimmed()
158                );
159            }
160
161            // Show AI provider
162            if let Some(provider) = &config.ai_provider {
163                println!("🤖 AI Provider: {}", provider);
164            }
165        }
166        ConfigAction::Describe => {
167            println!("\n{}", "📖 Configuration Options".bold());
168            println!("{}", "═".repeat(60).dimmed());
169
170            println!("\n{}", "Core Settings:".bold().green());
171            println!("  RCO_AI_PROVIDER    AI provider to use (openai, anthropic, ollama, etc.)");
172            println!("  RCO_MODEL          Model name for the provider");
173            println!("  RCO_API_KEY        API key for the provider");
174            println!("  RCO_API_URL        Custom API endpoint URL");
175
176            println!("\n{}", "Commit Style:".bold().green());
177            println!("  RCO_COMMIT_TYPE    Format: 'conventional' or 'gitmoji'");
178            println!("  RCO_EMOJI          Include emojis: true/false");
179            println!("  RCO_LANGUAGE       Output language (en, es, fr, etc.)");
180            println!("  RCO_DESCRIPTION    Include description: true/false");
181
182            println!("\n{}", "Behavior:".bold().green());
183            println!("  RCO_TOKENS_MAX_INPUT   Max input tokens (default: 4096)");
184            println!("  RCO_TOKENS_MAX_OUTPUT  Max output tokens (default: 500)");
185            println!("  RCO_GITPUSH      Auto-push after commit: true/false");
186            println!("  RCO_ONE_LINE_COMMIT    One-line format: true/false");
187
188            println!("\n{}", "Hooks:".bold().green());
189            println!("  RCO_PRE_GEN_HOOK       Command to run before generation");
190            println!("  RCO_PRE_COMMIT_HOOK    Command to run after generation");
191            println!("  RCO_POST_COMMIT_HOOK   Command to run after commit");
192            println!("  RCO_HOOK_STRICT        Fail on hook error: true/false");
193            println!("  RCO_HOOK_TIMEOUT_MS    Hook timeout in milliseconds");
194
195            println!("\n{}", "Examples:".bold().green());
196            println!("  rco config set RCO_AI_PROVIDER=anthropic");
197            println!("  rco config set RCO_MODEL=claude-3-5-haiku-20241022");
198            println!("  rco config set RCO_EMOJI=true RCO_LANGUAGE=es");
199            println!("  rco config set RCO_PRE_GEN_HOOK='just lint'");
200
201            println!("\n{}", "═".repeat(60).dimmed());
202        }
203        ConfigAction::AddProvider { provider: _, alias } => {
204            println!("\n{}", "🔧 Add Provider Wizard".bold().green());
205            println!("{}", "═".repeat(50).dimmed());
206
207            // Select provider
208            let provider_names = vec![
209                "OpenAI (GPT-4, GPT-3.5)",
210                "Anthropic Claude",
211                "Claude Code (OAuth)",
212                "Google Gemini",
213                "xAI Grok",
214                "Ollama (local)",
215                "Perplexity",
216                "Azure OpenAI",
217                "Qwen AI",
218            ];
219
220            let provider_selection = Select::new()
221                .with_prompt("Select AI provider")
222                .items(&provider_names)
223                .default(0)
224                .interact()?;
225
226            let (provider_name, provider_key) = match provider_selection {
227                0 => ("openai", Some("OPENAI_API_KEY")),
228                1 => ("anthropic", Some("ANTHROPIC_API_KEY")),
229                2 => ("claude-code", Some("CLAUDE_CODE_TOKEN")),
230                3 => ("gemini", Some("GEMINI_API_KEY")),
231                4 => ("xai", Some("XAI_API_KEY")),
232                5 => ("ollama", None),
233                6 => ("perplexity", Some("PERPLEXITY_API_KEY")),
234                7 => ("azure", Some("AZURE_API_KEY")),
235                8 => ("qwen", Some("QWEN_API_KEY")),
236                _ => ("openai", Some("OPENAI_API_KEY")),
237            };
238
239            // Get alias
240            let alias = alias.unwrap_or_else(|| {
241                Input::new()
242                    .with_prompt("Enter account alias (e.g., 'work', 'personal')")
243                    .with_initial_text(format!("{}-default", provider_name))
244                    .interact()
245                    .unwrap_or_else(|_| format!("{}-default", provider_name))
246            });
247
248            // Get optional model
249            let model_input: String = Input::new()
250                .with_prompt("Enter model name (optional, press Enter to use default)")
251                .allow_empty(true)
252                .interact()?;
253
254            let model = if model_input.trim().is_empty() {
255                None
256            } else {
257                Some(model_input.trim().to_string())
258            };
259
260            // Get optional API URL
261            let api_url_input: String = Input::new()
262                .with_prompt("Enter API URL (optional, press Enter to use default)")
263                .allow_empty(true)
264                .interact()?;
265
266            let api_url = if api_url_input.trim().is_empty() {
267                None
268            } else {
269                Some(api_url_input.trim().to_string())
270            };
271
272            // Get API key (skip for Ollama)
273            let api_key = if provider_selection == 5 {
274                None
275            } else {
276                let key_input: String = Input::new()
277                    .with_prompt(format!("Enter your {} API key", provider_name))
278                    .interact()?;
279
280                if key_input.trim().is_empty() {
281                    eprintln!(
282                        "{}",
283                        "⚠ No API key entered. You'll need to set it later.".yellow()
284                    );
285                    None
286                } else {
287                    Some(key_input.trim().to_string())
288                }
289            };
290
291            // Create the account config
292            let auth = if api_key.is_some() {
293                // Generate a key_id for this account
294                let key_id = format!("rco_{}", alias.to_lowercase().replace(' ', "_"));
295                accounts::AuthMethod::ApiKey {
296                    key_id: key_id.clone(),
297                }
298            } else {
299                // For Ollama or no key, use env var
300                if let Some(env_var) = provider_key {
301                    accounts::AuthMethod::EnvVar {
302                        name: env_var.to_string(),
303                    }
304                } else {
305                    // Fallback - no auth
306                    accounts::AuthMethod::EnvVar {
307                        name: "OLLAMA_HOST".to_string(),
308                    }
309                }
310            };
311
312            let account = accounts::AccountConfig {
313                alias: alias.to_lowercase().replace(' ', "_"),
314                provider: provider_name.to_string(),
315                api_url,
316                model,
317                auth,
318                tokens_max_input: None,
319                tokens_max_output: None,
320                is_default: false,
321            };
322
323            // Save the account
324            let mut accounts_config =
325                accounts::AccountsConfig::load()?.unwrap_or_else(|| accounts::AccountsConfig {
326                    active_account: None,
327                    accounts: std::collections::HashMap::new(),
328                });
329
330            // Check if alias already exists
331            if accounts_config.get_account(&account.alias).is_some() {
332                eprintln!(
333                    "{}",
334                    format!("❌ Account '{}' already exists", account.alias).red()
335                );
336            } else {
337                accounts_config.add_account(account.clone());
338
339                // Store API key in secure storage if provided
340                if let Some(key) = api_key {
341                    let key_id = match &account.auth {
342                        accounts::AuthMethod::ApiKey { key_id } => key_id.clone(),
343                        _ => unreachable!(),
344                    };
345                    if let Err(e) =
346                        crate::auth::token_storage::store_api_key_for_account(&key_id, &key)
347                    {
348                        eprintln!(
349                            "{}",
350                            format!("⚠ Failed to store API key securely: {e}").yellow()
351                        );
352                    }
353                }
354
355                accounts_config.save()?;
356                println!();
357                println!(
358                    "{}",
359                    format!("✅ Account '{}' added successfully!", account.alias).green()
360                );
361                println!();
362                println!(
363                    "{} To use this account: {}",
364                    "→".cyan(),
365                    format!("rco config use-account {}", account.alias)
366                        .bold()
367                        .white()
368                );
369            }
370        }
371        ConfigAction::ListAccounts => {
372            let out = ConfigOutput;
373            out.header("📋 Configured Accounts");
374            out.divider();
375
376            if config.has_accounts() {
377                match config.list_accounts() {
378                    Ok(accounts) => {
379                        for account in accounts {
380                            let default_marker = if account.is_default {
381                                " [DEFAULT]".bold().green()
382                            } else {
383                                "".normal()
384                            };
385                            println!(
386                                "{}: {}{}",
387                                account.alias.yellow(),
388                                account.provider,
389                                default_marker
390                            );
391                            if let Some(model) = &account.model {
392                                println!("   Model: {}", model.dimmed());
393                            }
394                            if let Some(api_url) = &account.api_url {
395                                println!("   URL: {}", api_url.dimmed());
396                            }
397                        }
398                    }
399                    Err(e) => {
400                        eprintln!("{}", format!("❌ Failed to list accounts: {e}").red());
401                    }
402                }
403            } else {
404                println!("\n{}", "No accounts configured yet.".dimmed());
405                println!(
406                    "{}",
407                    "Use: rco config add-provider to add an account".dimmed()
408                );
409            }
410        }
411        ConfigAction::UseAccount { alias } => {
412            println!(
413                "\n{}",
414                format!("🔄 Switching to account: {}", alias).bold().green()
415            );
416
417            match config.set_default_account(&alias) {
418                Ok(_) => {
419                    println!("{}", format!("✅ Now using account: {alias}").green());
420                    println!(
421                        "\n{}",
422                        "Note: Account switching requires restart of commands".dimmed()
423                    );
424                }
425                Err(e) => {
426                    eprintln!("{}", format!("❌ Failed to switch account: {e}").red());
427                }
428            }
429        }
430        ConfigAction::RemoveAccount { alias } => {
431            println!(
432                "\n{}",
433                format!("🗑️  Removing account: {}", alias).bold().yellow()
434            );
435
436            match config.remove_account(&alias) {
437                Ok(_) => {
438                    println!("{}", format!("✅ Account '{alias}' removed").green());
439                }
440                Err(e) => {
441                    eprintln!("{}", format!("❌ Failed to remove account: {e}").red());
442                }
443            }
444        }
445        ConfigAction::ShowAccount { alias } => {
446            let alias = alias.as_deref().unwrap_or("default");
447
448            println!("\n{}", format!("👤 Account: {}", alias).bold().green());
449            println!("{}", "═".repeat(50).dimmed());
450
451            match config.get_account(alias) {
452                Ok(Some(account)) => {
453                    println!("Alias: {}", account.alias.yellow());
454                    println!("Provider: {}", account.provider);
455                    println!("Default: {}", if account.is_default { "Yes" } else { "No" });
456
457                    if let Some(model) = &account.model {
458                        println!("Model: {}", model);
459                    }
460                    if let Some(api_url) = &account.api_url {
461                        println!("API URL: {}", api_url);
462                    }
463
464                    match &account.auth {
465                        crate::config::accounts::AuthMethod::ApiKey { .. } => {
466                            println!("Auth: API Key 🔑");
467                        }
468                        crate::config::accounts::AuthMethod::OAuth {
469                            provider,
470                            account_id,
471                        } => {
472                            println!("Auth: OAuth ({}) - Account: {}", provider, account_id);
473                        }
474                        crate::config::accounts::AuthMethod::EnvVar { name } => {
475                            println!("Auth: Environment Variable ({})", name);
476                        }
477                        crate::config::accounts::AuthMethod::Bearer { .. } => {
478                            println!("Auth: Bearer Token 🔖");
479                        }
480                    }
481                }
482                Ok(None) => {
483                    eprintln!("{}", format!("❌ Account '{alias}' not found").red());
484                }
485                Err(e) => {
486                    eprintln!("{}", format!("❌ Failed to get account: {e}").red());
487                }
488            }
489        }
490    }
491
492    Ok(())
493}