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_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 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 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 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 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 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 let auth = if api_key.is_some() {
294 let key_id = format!("rco_{}", alias.to_lowercase().replace(' ', "_"));
296 accounts::AuthMethod::ApiKey {
297 key_id: key_id.clone(),
298 }
299 } else {
300 if let Some(env_var) = provider_key {
302 accounts::AuthMethod::EnvVar {
303 name: env_var.to_string(),
304 }
305 } else {
306 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 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 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 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}