1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Input, Select};
4
5use crate::cli::{ConfigAction, ConfigCommand};
6use crate::config::{self, accounts, Config};
7
8#[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 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 #[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 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 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 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 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 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 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 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 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 let auth = if api_key.is_some() {
293 let key_id = format!("rco_{}", alias.to_lowercase().replace(' ', "_"));
295 accounts::AuthMethod::ApiKey {
296 key_id: key_id.clone(),
297 }
298 } else {
299 if let Some(env_var) = provider_key {
301 accounts::AuthMethod::EnvVar {
302 name: env_var.to_string(),
303 }
304 } else {
305 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 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 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 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}