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_REMOTE       Remote to push to (default: origin)");
187            println!("  RCO_ONE_LINE_COMMIT    One-line format: true/false");
188
189            println!("\n{}", "Hooks:".bold().green());
190            println!("  RCO_PRE_GEN_HOOK       Command to run before generation");
191            println!("  RCO_PRE_COMMIT_HOOK    Command to run after generation");
192            println!("  RCO_POST_COMMIT_HOOK   Command to run after commit");
193            println!("  RCO_HOOK_STRICT        Fail on hook error: true/false");
194            println!("  RCO_HOOK_TIMEOUT_MS    Hook timeout in milliseconds");
195
196            println!("\n{}", "Examples:".bold().green());
197            println!("  rco config set RCO_AI_PROVIDER=anthropic");
198            println!("  rco config set RCO_MODEL=claude-3-5-haiku-20241022");
199            println!("  rco config set RCO_EMOJI=true RCO_LANGUAGE=es");
200            println!("  rco config set RCO_PRE_GEN_HOOK='just lint'");
201
202            println!("\n{}", "═".repeat(60).dimmed());
203        }
204        ConfigAction::AddProvider { provider: _, alias } => {
205            println!("\n{}", "🔧 Add Provider Wizard".bold().green());
206            println!("{}", "═".repeat(50).dimmed());
207
208            // Select provider
209            let provider_names = vec![
210                "OpenAI (GPT-4, GPT-3.5)",
211                "Anthropic Claude",
212                "Claude Code (OAuth)",
213                "Google Gemini",
214                "xAI Grok",
215                "Ollama (local)",
216                "Perplexity",
217                "Azure OpenAI",
218                "Qwen AI",
219            ];
220
221            let provider_selection = Select::new()
222                .with_prompt("Select AI provider")
223                .items(&provider_names)
224                .default(0)
225                .interact()?;
226
227            let (provider_name, provider_key) = match provider_selection {
228                0 => ("openai", Some("OPENAI_API_KEY")),
229                1 => ("anthropic", Some("ANTHROPIC_API_KEY")),
230                2 => ("claude-code", Some("CLAUDE_CODE_TOKEN")),
231                3 => ("gemini", Some("GEMINI_API_KEY")),
232                4 => ("xai", Some("XAI_API_KEY")),
233                5 => ("ollama", None),
234                6 => ("perplexity", Some("PERPLEXITY_API_KEY")),
235                7 => ("azure", Some("AZURE_API_KEY")),
236                8 => ("qwen", Some("QWEN_API_KEY")),
237                _ => ("openai", Some("OPENAI_API_KEY")),
238            };
239
240            // Get alias
241            let alias = alias.unwrap_or_else(|| {
242                Input::new()
243                    .with_prompt("Enter account alias (e.g., 'work', 'personal')")
244                    .with_initial_text(format!("{}-default", provider_name))
245                    .interact()
246                    .unwrap_or_else(|_| format!("{}-default", provider_name))
247            });
248
249            // Get optional model
250            let model_input: String = Input::new()
251                .with_prompt("Enter model name (optional, press Enter to use default)")
252                .allow_empty(true)
253                .interact()?;
254
255            let model = if model_input.trim().is_empty() {
256                None
257            } else {
258                Some(model_input.trim().to_string())
259            };
260
261            // Get optional API URL
262            let api_url_input: String = Input::new()
263                .with_prompt("Enter API URL (optional, press Enter to use default)")
264                .allow_empty(true)
265                .interact()?;
266
267            let api_url = if api_url_input.trim().is_empty() {
268                None
269            } else {
270                Some(api_url_input.trim().to_string())
271            };
272
273            // Get API key (skip for Ollama)
274            let api_key = if provider_selection == 5 {
275                None
276            } else {
277                let key_input: String = Input::new()
278                    .with_prompt(format!("Enter your {} API key", provider_name))
279                    .interact()?;
280
281                if key_input.trim().is_empty() {
282                    eprintln!(
283                        "{}",
284                        "⚠ No API key entered. You'll need to set it later.".yellow()
285                    );
286                    None
287                } else {
288                    Some(key_input.trim().to_string())
289                }
290            };
291
292            // Create the account config
293            let auth = if api_key.is_some() {
294                // Generate a key_id for this account
295                let key_id = format!("rco_{}", alias.to_lowercase().replace(' ', "_"));
296                accounts::AuthMethod::ApiKey {
297                    key_id: key_id.clone(),
298                }
299            } else {
300                // For Ollama or no key, use env var
301                if let Some(env_var) = provider_key {
302                    accounts::AuthMethod::EnvVar {
303                        name: env_var.to_string(),
304                    }
305                } else {
306                    // Fallback - no auth
307                    accounts::AuthMethod::EnvVar {
308                        name: "OLLAMA_HOST".to_string(),
309                    }
310                }
311            };
312
313            let account = accounts::AccountConfig {
314                alias: alias.to_lowercase().replace(' ', "_"),
315                provider: provider_name.to_string(),
316                api_url,
317                model,
318                auth,
319                tokens_max_input: None,
320                tokens_max_output: None,
321                is_default: false,
322            };
323
324            // Save the account
325            let mut accounts_config =
326                accounts::AccountsConfig::load()?.unwrap_or_else(|| accounts::AccountsConfig {
327                    active_account: None,
328                    accounts: std::collections::HashMap::new(),
329                });
330
331            // Check if alias already exists
332            if accounts_config.get_account(&account.alias).is_some() {
333                eprintln!(
334                    "{}",
335                    format!("❌ Account '{}' already exists", account.alias).red()
336                );
337            } else {
338                accounts_config.add_account(account.clone());
339
340                // Store API key in secure storage if provided
341                if let Some(key) = api_key {
342                    let key_id = match &account.auth {
343                        accounts::AuthMethod::ApiKey { key_id } => key_id.clone(),
344                        _ => unreachable!(),
345                    };
346                    if let Err(e) =
347                        crate::auth::token_storage::store_api_key_for_account(&key_id, &key)
348                    {
349                        eprintln!(
350                            "{}",
351                            format!("⚠ Failed to store API key securely: {e}").yellow()
352                        );
353                    }
354                }
355
356                accounts_config.save()?;
357                println!();
358                println!(
359                    "{}",
360                    format!("✅ Account '{}' added successfully!", account.alias).green()
361                );
362                println!();
363                println!(
364                    "{} To use this account: {}",
365                    "→".cyan(),
366                    format!("rco config use-account {}", account.alias)
367                        .bold()
368                        .white()
369                );
370            }
371        }
372        ConfigAction::ListAccounts => {
373            let out = ConfigOutput;
374            out.header("📋 Configured Accounts");
375            out.divider();
376
377            if config.has_accounts() {
378                match config.list_accounts() {
379                    Ok(accounts) => {
380                        for account in accounts {
381                            let default_marker = if account.is_default {
382                                " [DEFAULT]".bold().green()
383                            } else {
384                                "".normal()
385                            };
386                            println!(
387                                "{}: {}{}",
388                                account.alias.yellow(),
389                                account.provider,
390                                default_marker
391                            );
392                            if let Some(model) = &account.model {
393                                println!("   Model: {}", model.dimmed());
394                            }
395                            if let Some(api_url) = &account.api_url {
396                                println!("   URL: {}", api_url.dimmed());
397                            }
398                        }
399                    }
400                    Err(e) => {
401                        eprintln!("{}", format!("❌ Failed to list accounts: {e}").red());
402                    }
403                }
404            } else {
405                println!("\n{}", "No accounts configured yet.".dimmed());
406                println!(
407                    "{}",
408                    "Use: rco config add-provider to add an account".dimmed()
409                );
410            }
411        }
412        ConfigAction::UseAccount { alias } => {
413            println!(
414                "\n{}",
415                format!("🔄 Switching to account: {}", alias).bold().green()
416            );
417
418            match config.set_default_account(&alias) {
419                Ok(_) => {
420                    println!("{}", format!("✅ Now using account: {alias}").green());
421                    println!(
422                        "\n{}",
423                        "Note: Account switching requires restart of commands".dimmed()
424                    );
425                }
426                Err(e) => {
427                    eprintln!("{}", format!("❌ Failed to switch account: {e}").red());
428                }
429            }
430        }
431        ConfigAction::RemoveAccount { alias } => {
432            println!(
433                "\n{}",
434                format!("🗑️  Removing account: {}", alias).bold().yellow()
435            );
436
437            match config.remove_account(&alias) {
438                Ok(_) => {
439                    println!("{}", format!("✅ Account '{alias}' removed").green());
440                }
441                Err(e) => {
442                    eprintln!("{}", format!("❌ Failed to remove account: {e}").red());
443                }
444            }
445        }
446        ConfigAction::ShowAccount { alias } => {
447            let alias = alias.as_deref().unwrap_or("default");
448
449            println!("\n{}", format!("👤 Account: {}", alias).bold().green());
450            println!("{}", "═".repeat(50).dimmed());
451
452            match config.get_account(alias) {
453                Ok(Some(account)) => {
454                    println!("Alias: {}", account.alias.yellow());
455                    println!("Provider: {}", account.provider);
456                    println!("Default: {}", if account.is_default { "Yes" } else { "No" });
457
458                    if let Some(model) = &account.model {
459                        println!("Model: {}", model);
460                    }
461                    if let Some(api_url) = &account.api_url {
462                        println!("API URL: {}", api_url);
463                    }
464
465                    match &account.auth {
466                        crate::config::accounts::AuthMethod::ApiKey { .. } => {
467                            println!("Auth: API Key 🔑");
468                        }
469                        crate::config::accounts::AuthMethod::OAuth {
470                            provider,
471                            account_id,
472                        } => {
473                            println!("Auth: OAuth ({}) - Account: {}", provider, account_id);
474                        }
475                        crate::config::accounts::AuthMethod::EnvVar { name } => {
476                            println!("Auth: Environment Variable ({})", name);
477                        }
478                        crate::config::accounts::AuthMethod::Bearer { .. } => {
479                            println!("Auth: Bearer Token 🔖");
480                        }
481                    }
482                }
483                Ok(None) => {
484                    eprintln!("{}", format!("❌ Account '{alias}' not found").red());
485                }
486                Err(e) => {
487                    eprintln!("{}", format!("❌ Failed to get account: {e}").red());
488                }
489            }
490        }
491    }
492
493    Ok(())
494}