Skip to main content

rusty_commit/
config.rs

1pub mod accounts;
2pub mod format;
3pub mod migrations;
4pub mod secure_storage;
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use dirs::home_dir;
9use serde::{Deserialize, Serialize};
10use std::env;
11use std::path::PathBuf;
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Config {
15    // API Configuration
16    pub api_key: Option<String>,
17    pub api_url: Option<String>,
18    pub ai_provider: Option<String>,
19    pub model: Option<String>,
20
21    // Token limits
22    pub tokens_max_input: Option<usize>,
23    pub tokens_max_output: Option<u32>,
24
25    // Commit message configuration
26    pub commit_type: Option<String>,
27    pub emoji: Option<bool>,
28    pub description: Option<bool>,
29    pub description_capitalize: Option<bool>,
30    pub description_add_period: Option<bool>,
31    pub description_max_length: Option<usize>,
32
33    // Language and customization
34    pub language: Option<String>,
35    pub message_template_placeholder: Option<String>,
36    pub prompt_module: Option<String>,
37
38    // Behavior
39    pub gitpush: Option<bool>,
40    pub one_line_commit: Option<bool>,
41    pub why: Option<bool>,
42    pub omit_scope: Option<bool>,
43    pub generate_count: Option<u8>,
44    pub clipboard_on_timeout: Option<bool>,
45
46    // GitHub Actions
47    pub action_enabled: Option<bool>,
48
49    // Testing
50    pub test_mock_type: Option<String>,
51
52    // Hooks
53    pub hook_auto_uncomment: Option<bool>,
54    pub pre_gen_hook: Option<Vec<String>>,
55    pub pre_commit_hook: Option<Vec<String>>,
56    pub post_commit_hook: Option<Vec<String>>,
57    pub hook_strict: Option<bool>,
58    pub hook_timeout_ms: Option<u64>,
59
60    // Global commitlint configuration
61    pub commitlint_config: Option<String>,
62    pub custom_prompt: Option<String>,
63
64    // Commit style learning from history
65    pub learn_from_history: Option<bool>,
66    pub history_commits_count: Option<usize>,
67    pub style_profile: Option<String>,
68}
69
70impl Default for Config {
71    fn default() -> Self {
72        Self {
73            api_key: None,
74            api_url: None,
75            ai_provider: Some("openai".to_string()),
76            model: Some("gpt-3.5-turbo".to_string()),
77            tokens_max_input: Some(4096),
78            tokens_max_output: Some(500),
79            commit_type: Some("conventional".to_string()),
80            emoji: Some(false),
81            description: Some(false),
82            description_capitalize: Some(true),
83            description_add_period: Some(false),
84            description_max_length: Some(100),
85            language: Some("en".to_string()),
86            message_template_placeholder: Some("$msg".to_string()),
87            prompt_module: Some("conventional-commit".to_string()),
88            gitpush: Some(false),
89            one_line_commit: Some(false),
90            why: Some(false),
91            omit_scope: Some(false),
92            generate_count: Some(1),
93            clipboard_on_timeout: Some(true),
94            action_enabled: Some(false),
95            test_mock_type: None,
96            hook_auto_uncomment: Some(false),
97            pre_gen_hook: None,
98            pre_commit_hook: None,
99            post_commit_hook: None,
100            hook_strict: Some(true),
101            hook_timeout_ms: Some(30000),
102            commitlint_config: None,
103            custom_prompt: None,
104            learn_from_history: Some(false),
105            history_commits_count: Some(10),
106            style_profile: None,
107        }
108    }
109}
110
111impl Config {
112    /// Get the new global config path
113    #[allow(dead_code)]
114    pub fn global_config_path() -> Result<PathBuf> {
115        if let Ok(config_home) = env::var("RCO_CONFIG_HOME") {
116            Ok(PathBuf::from(config_home).join("config.toml"))
117        } else {
118            let home = home_dir().context("Could not find home directory")?;
119            Ok(home.join(".config").join("rustycommit").join("config.toml"))
120        }
121    }
122
123    /// Load configuration with proper priority handling
124    pub fn load() -> Result<Self> {
125        // Use the new format system to load with priority
126        format::ConfigLocations::load_merged()
127    }
128
129    pub fn save(&self) -> Result<()> {
130        // Save to global config by default
131        self.save_to(format::ConfigLocation::Global)
132    }
133
134    /// Save configuration to a specific location
135    pub fn save_to(&self, location: format::ConfigLocation) -> Result<()> {
136        // Create a copy for saving (without sensitive data)
137        let mut save_config = self.clone();
138
139        // If we have an API key and secure storage is available, store it securely
140        if let Some(ref api_key) = self.api_key {
141            if secure_storage::is_available() {
142                match secure_storage::store_secret("RCO_API_KEY", api_key) {
143                    Ok(_) => {
144                        // Don't save API key to file if stored securely
145                        save_config.api_key = None;
146                    }
147                    Err(e) => {
148                        // Fall back to file storage; keep api_key in file
149                        eprintln!("Warning: Secure storage unavailable, falling back to file: {e}");
150                    }
151                }
152            }
153        }
154
155        format::ConfigLocations::save(&save_config, location)
156    }
157
158    /// Helper function to get environment variable with RCO_ prefix
159    fn get_env_var(base_name: &str) -> Option<String> {
160        let rco_key = format!("RCO_{}", base_name);
161
162        // Check RCO_ prefix
163        env::var(&rco_key).ok()
164    }
165
166    pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
167        // Handle undefined/null values
168        if value == "undefined" || value == "null" {
169            return Ok(());
170        }
171
172        match key {
173            // Support RCO_ prefix
174            "RCO_API_KEY" => {
175                self.api_key = Some(value.to_string());
176                // Also try to store in secure storage (use RCO_ key)
177                if secure_storage::is_available() {
178                    let _ = secure_storage::store_secret("RCO_API_KEY", value);
179                }
180            }
181            "RCO_API_URL" => self.api_url = Some(value.to_string()),
182            "RCO_AI_PROVIDER" => self.ai_provider = Some(value.to_string()),
183            "RCO_MODEL" => self.model = Some(value.to_string()),
184            "RCO_TOKENS_MAX_INPUT" => {
185                self.tokens_max_input = Some(
186                    value
187                        .parse()
188                        .context("Invalid number for TOKENS_MAX_INPUT")?,
189                );
190            }
191            "RCO_TOKENS_MAX_OUTPUT" => {
192                self.tokens_max_output = Some(
193                    value
194                        .parse()
195                        .context("Invalid number for TOKENS_MAX_OUTPUT")?,
196                );
197            }
198            "RCO_COMMIT_TYPE" => {
199                self.commit_type = Some(value.to_string());
200            }
201            "RCO_PROMPT_MODULE" => {
202                // Map legacy prompt module to commit type
203                let commit_type = match value {
204                    "conventional-commit" => "conventional",
205                    _ => value,
206                };
207                self.commit_type = Some(commit_type.to_string());
208                self.prompt_module = Some(value.to_string());
209            }
210            "RCO_EMOJI" => {
211                self.emoji = Some(value.parse().context("Invalid boolean for EMOJI")?);
212            }
213            "RCO_DESCRIPTION_CAPITALIZE" => {
214                self.description_capitalize = Some(
215                    value
216                        .parse()
217                        .context("Invalid boolean for DESCRIPTION_CAPITALIZE")?,
218                );
219            }
220            "RCO_DESCRIPTION_ADD_PERIOD" => {
221                self.description_add_period = Some(
222                    value
223                        .parse()
224                        .context("Invalid boolean for DESCRIPTION_ADD_PERIOD")?,
225                );
226            }
227            "RCO_DESCRIPTION_MAX_LENGTH" => {
228                self.description_max_length = Some(
229                    value
230                        .parse()
231                        .context("Invalid number for DESCRIPTION_MAX_LENGTH")?,
232                );
233            }
234            "RCO_LANGUAGE" => self.language = Some(value.to_string()),
235            "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
236                self.message_template_placeholder = Some(value.to_string());
237            }
238            "RCO_GITPUSH" => {
239                self.gitpush = Some(value.parse().context("Invalid boolean for GITPUSH")?);
240            }
241            "RCO_ONE_LINE_COMMIT" => {
242                self.one_line_commit = Some(
243                    value
244                        .parse()
245                        .context("Invalid boolean for ONE_LINE_COMMIT")?,
246                );
247            }
248            "RCO_ACTION_ENABLED" => {
249                self.action_enabled = Some(
250                    value
251                        .parse()
252                        .context("Invalid boolean for ACTION_ENABLED")?,
253                );
254            }
255            "RCO_DESCRIPTION" => {
256                self.description = Some(value.parse().context("Invalid boolean for DESCRIPTION")?);
257            }
258            "RCO_WHY" => {
259                self.why = Some(value.parse().context("Invalid boolean for WHY")?);
260            }
261            "RCO_OMIT_SCOPE" => {
262                self.omit_scope = Some(value.parse().context("Invalid boolean for OMIT_SCOPE")?);
263            }
264            "RCO_TEST_MOCK_TYPE" => {
265                self.test_mock_type = Some(value.to_string());
266            }
267            "RCO_HOOK_AUTO_UNCOMMENT" => {
268                self.hook_auto_uncomment = Some(
269                    value
270                        .parse()
271                        .context("Invalid boolean for HOOK_AUTO_UNCOMMENT")?,
272                );
273            }
274            "RCO_PRE_GEN_HOOK" => {
275                let items = value
276                    .split(';')
277                    .map(|s| s.trim().to_string())
278                    .filter(|s| !s.is_empty())
279                    .collect();
280                self.pre_gen_hook = Some(items);
281            }
282            "RCO_PRE_COMMIT_HOOK" => {
283                let items = value
284                    .split(';')
285                    .map(|s| s.trim().to_string())
286                    .filter(|s| !s.is_empty())
287                    .collect();
288                self.pre_commit_hook = Some(items);
289            }
290            "RCO_POST_COMMIT_HOOK" => {
291                let items = value
292                    .split(';')
293                    .map(|s| s.trim().to_string())
294                    .filter(|s| !s.is_empty())
295                    .collect();
296                self.post_commit_hook = Some(items);
297            }
298            "RCO_HOOK_STRICT" => {
299                self.hook_strict = Some(value.parse().context("Invalid boolean for HOOK_STRICT")?);
300            }
301            "RCO_HOOK_TIMEOUT_MS" => {
302                self.hook_timeout_ms = Some(
303                    value
304                        .parse()
305                        .context("Invalid number for HOOK_TIMEOUT_MS")?,
306                );
307            }
308            "RCO_COMMITLINT_CONFIG" => {
309                self.commitlint_config = Some(value.to_string());
310            }
311            "RCO_CUSTOM_PROMPT" => {
312                self.custom_prompt = Some(value.to_string());
313            }
314            "RCO_GENERATE_COUNT" => {
315                self.generate_count = Some(
316                    value
317                        .parse()
318                        .context("Invalid number for GENERATE_COUNT (1-5)")?,
319                );
320            }
321            "RCO_CLIPBOARD_ON_TIMEOUT" => {
322                self.clipboard_on_timeout = Some(
323                    value
324                        .parse()
325                        .context("Invalid boolean for CLIPBOARD_ON_TIMEOUT")?,
326                );
327            }
328            "RCO_LEARN_FROM_HISTORY" => {
329                self.learn_from_history = Some(
330                    value
331                        .parse()
332                        .context("Invalid boolean for LEARN_FROM_HISTORY")?,
333                );
334            }
335            "RCO_HISTORY_COMMITS_COUNT" => {
336                self.history_commits_count = Some(
337                    value
338                        .parse()
339                        .context("Invalid number for HISTORY_COMMITS_COUNT")?,
340                );
341            }
342            "RCO_STYLE_PROFILE" => {
343                self.style_profile = Some(value.to_string());
344            }
345            // Ignore unsupported keys
346            "RCO_API_CUSTOM_HEADERS" => {
347                // Silently ignore these legacy keys
348                return Ok(());
349            }
350            _ => anyhow::bail!("Unknown configuration key: {}", key),
351        }
352
353        self.save()?;
354        Ok(())
355    }
356
357    pub fn get(&self, key: &str) -> Result<String> {
358        let value = match key {
359            "RCO_API_KEY" => {
360                // Try to get from memory first, then from secure storage
361                self.api_key
362                    .as_ref()
363                    .map(|s| s.to_string())
364                    .or_else(|| secure_storage::get_secret("RCO_API_KEY").ok().flatten())
365            }
366            "RCO_API_URL" => self.api_url.as_ref().map(|s| s.to_string()),
367            "RCO_AI_PROVIDER" => self.ai_provider.as_ref().map(|s| s.to_string()),
368            "RCO_MODEL" => self.model.as_ref().map(|s| s.to_string()),
369            "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input.map(|v| v.to_string()),
370            "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output.map(|v| v.to_string()),
371            "RCO_COMMIT_TYPE" => self.commit_type.as_ref().map(|s| s.to_string()),
372            "RCO_EMOJI" => self.emoji.map(|v| v.to_string()),
373            "RCO_DESCRIPTION_CAPITALIZE" => self.description_capitalize.map(|v| v.to_string()),
374            "RCO_DESCRIPTION_ADD_PERIOD" => self.description_add_period.map(|v| v.to_string()),
375            "RCO_DESCRIPTION_MAX_LENGTH" => self.description_max_length.map(|v| v.to_string()),
376            "RCO_LANGUAGE" => self.language.as_ref().map(|s| s.to_string()),
377            "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => self
378                .message_template_placeholder
379                .as_ref()
380                .map(|s| s.to_string()),
381            "RCO_GITPUSH" => self.gitpush.map(|v| v.to_string()),
382            "RCO_ONE_LINE_COMMIT" => self.one_line_commit.map(|v| v.to_string()),
383            "RCO_ACTION_ENABLED" => self.action_enabled.map(|v| v.to_string()),
384            "RCO_COMMITLINT_CONFIG" => self.commitlint_config.as_ref().map(|s| s.to_string()),
385            "RCO_CUSTOM_PROMPT" => self.custom_prompt.as_ref().map(|s| s.to_string()),
386            "RCO_GENERATE_COUNT" => self.generate_count.map(|v| v.to_string()),
387            "RCO_CLIPBOARD_ON_TIMEOUT" => self.clipboard_on_timeout.map(|v| v.to_string()),
388            _ => None,
389        };
390
391        value.ok_or_else(|| anyhow::anyhow!("Configuration key '{}' not found or not set", key))
392    }
393
394    pub fn reset(&mut self, keys: Option<&[String]>) -> Result<()> {
395        if let Some(key_list) = keys {
396            let default = Self::default();
397            for key in key_list {
398                match key.as_str() {
399                    "RCO_API_KEY" => {
400                        self.api_key = default.api_key.clone();
401                        // Also clear from secure storage
402                        let _ = secure_storage::delete_secret("RCO_API_KEY");
403                    }
404                    "RCO_API_URL" => self.api_url = default.api_url.clone(),
405                    "RCO_AI_PROVIDER" => self.ai_provider = default.ai_provider.clone(),
406                    "RCO_MODEL" => self.model = default.model.clone(),
407                    "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input = default.tokens_max_input,
408                    "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output = default.tokens_max_output,
409                    "RCO_COMMIT_TYPE" => self.commit_type = default.commit_type.clone(),
410                    "RCO_EMOJI" => self.emoji = default.emoji,
411                    "RCO_DESCRIPTION_CAPITALIZE" => {
412                        self.description_capitalize = default.description_capitalize
413                    }
414                    "RCO_DESCRIPTION_ADD_PERIOD" => {
415                        self.description_add_period = default.description_add_period
416                    }
417                    "RCO_DESCRIPTION_MAX_LENGTH" => {
418                        self.description_max_length = default.description_max_length
419                    }
420                    "RCO_LANGUAGE" => self.language = default.language.clone(),
421                    "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
422                        self.message_template_placeholder =
423                            default.message_template_placeholder.clone()
424                    }
425                    "RCO_GITPUSH" => self.gitpush = default.gitpush,
426                    "RCO_ONE_LINE_COMMIT" => self.one_line_commit = default.one_line_commit,
427                    "RCO_ACTION_ENABLED" => self.action_enabled = default.action_enabled,
428                    "RCO_PRE_GEN_HOOK" => self.pre_gen_hook = default.pre_gen_hook.clone(),
429                    "RCO_PRE_COMMIT_HOOK" => self.pre_commit_hook = default.pre_commit_hook.clone(),
430                    "RCO_POST_COMMIT_HOOK" => {
431                        self.post_commit_hook = default.post_commit_hook.clone()
432                    }
433                    "RCO_HOOK_STRICT" => self.hook_strict = default.hook_strict,
434                    "RCO_HOOK_TIMEOUT_MS" => self.hook_timeout_ms = default.hook_timeout_ms,
435                    "RCO_GENERATE_COUNT" => self.generate_count = default.generate_count,
436                    "RCO_CLIPBOARD_ON_TIMEOUT" => {
437                        self.clipboard_on_timeout = default.clipboard_on_timeout
438                    }
439                    _ => anyhow::bail!("Unknown configuration key: {}", key),
440                }
441            }
442        } else {
443            *self = Self::default();
444        }
445
446        self.save()?;
447        Ok(())
448    }
449
450    /// Load and merge global commitlint configuration
451    pub fn load_with_commitlint(&mut self) -> Result<()> {
452        // First check for COMMITLINT_CONFIG environment variable
453        if let Ok(commitlint_path) = env::var("COMMITLINT_CONFIG") {
454            self.commitlint_config = Some(commitlint_path);
455        }
456
457        // If no explicit config path, check default locations
458        if self.commitlint_config.is_none() {
459            let home = home_dir().context("Could not find home directory")?;
460
461            // Check for global commitlint configs in priority order
462            let possible_paths = [
463                home.join(".commitlintrc.js"),
464                home.join(".commitlintrc.json"),
465                home.join(".commitlintrc.yml"),
466                home.join(".commitlintrc.yaml"),
467                home.join("commitlint.config.js"),
468            ];
469
470            for path in &possible_paths {
471                if path.exists() {
472                    self.commitlint_config = Some(path.to_string_lossy().to_string());
473                    break;
474                }
475            }
476        }
477
478        Ok(())
479    }
480
481    /// Load commitlint rules and modify commit type accordingly
482    pub fn apply_commitlint_rules(&mut self) -> Result<()> {
483        if let Some(ref config_path) = self.commitlint_config.clone() {
484            let path = PathBuf::from(config_path);
485            if path.exists() {
486                // For now, just set to conventional commits if commitlint config exists
487                // Full commitlint parsing would require a JS engine or specific parsing
488                if self.commit_type.is_none() {
489                    self.commit_type = Some("conventional".to_string());
490                }
491
492                // In a full implementation, we would parse the commitlint config
493                // and extract rules, but for now we'll use conventional commits
494                println!("📋 Found commitlint config at: {}", config_path);
495                println!("🔧 Using conventional commit format for consistency");
496            }
497        }
498        Ok(())
499    }
500
501    /// Get the effective prompt (custom or generated)
502    pub fn get_effective_prompt(
503        &self,
504        diff: &str,
505        context: Option<&str>,
506        full_gitmoji: bool,
507    ) -> String {
508        if let Some(ref custom_prompt) = self.custom_prompt {
509            // Security warning: custom prompts receive diff content
510            tracing::warn!(
511                "Using custom prompt template - diff content will be included in the prompt. \
512                Ensure your custom prompt does not exfiltrate or log sensitive code."
513            );
514            eprintln!(
515                "{}",
516                "Warning: Using custom prompt template. Your diff content will be sent to the AI provider."
517                    .yellow()
518            );
519
520            // Replace placeholders in custom prompt
521            let mut prompt = custom_prompt.clone();
522            prompt = prompt.replace("$diff", diff);
523            if let Some(ctx) = context {
524                prompt = prompt.replace("$context", ctx);
525            }
526            prompt
527        } else {
528            // Use the standard prompt generation
529            super::providers::build_prompt(diff, context, self, full_gitmoji)
530        }
531    }
532
533    /// Merge another config into this one (other takes priority over self)
534    pub fn merge(&mut self, other: Config) {
535        macro_rules! merge_field {
536            ($field:ident) => {
537                if other.$field.is_some() {
538                    self.$field = other.$field;
539                }
540            };
541        }
542
543        merge_field!(api_key);
544        merge_field!(api_url);
545        merge_field!(ai_provider);
546        merge_field!(model);
547        merge_field!(tokens_max_input);
548        merge_field!(tokens_max_output);
549        merge_field!(commit_type);
550        merge_field!(emoji);
551        merge_field!(description);
552        merge_field!(description_capitalize);
553        merge_field!(description_add_period);
554        merge_field!(description_max_length);
555        merge_field!(language);
556        merge_field!(message_template_placeholder);
557        merge_field!(prompt_module);
558        merge_field!(gitpush);
559        merge_field!(one_line_commit);
560        merge_field!(why);
561        merge_field!(omit_scope);
562        merge_field!(action_enabled);
563        merge_field!(test_mock_type);
564        merge_field!(hook_auto_uncomment);
565        merge_field!(pre_gen_hook);
566        merge_field!(pre_commit_hook);
567        merge_field!(post_commit_hook);
568        merge_field!(hook_strict);
569        merge_field!(hook_timeout_ms);
570        merge_field!(commitlint_config);
571        merge_field!(custom_prompt);
572        merge_field!(generate_count);
573        merge_field!(clipboard_on_timeout);
574        merge_field!(learn_from_history);
575        merge_field!(history_commits_count);
576        merge_field!(style_profile);
577    }
578
579    /// Load configuration values from environment variables
580    /// Uses RCO_ environment variables
581    pub fn load_from_environment(&mut self) {
582        // Macro to reduce code duplication
583        macro_rules! load_env_var {
584            ($field:ident, $base_name:expr) => {
585                if let Some(value) = Self::get_env_var($base_name) {
586                    self.$field = Some(value);
587                }
588            };
589        }
590
591        macro_rules! load_env_var_parse {
592            ($field:ident, $base_name:expr, $type:ty) => {
593                if let Some(value) = Self::get_env_var($base_name) {
594                    if let Ok(parsed) = value.parse::<$type>() {
595                        self.$field = Some(parsed);
596                    }
597                }
598            };
599        }
600
601        load_env_var!(api_key, "API_KEY");
602        load_env_var!(api_url, "API_URL");
603        load_env_var!(ai_provider, "AI_PROVIDER");
604        load_env_var!(model, "MODEL");
605        load_env_var_parse!(tokens_max_input, "TOKENS_MAX_INPUT", usize);
606        load_env_var_parse!(tokens_max_output, "TOKENS_MAX_OUTPUT", u32);
607        load_env_var!(commit_type, "COMMIT_TYPE");
608        load_env_var_parse!(emoji, "EMOJI", bool);
609        load_env_var_parse!(description, "DESCRIPTION", bool);
610        load_env_var_parse!(description_capitalize, "DESCRIPTION_CAPITALIZE", bool);
611        load_env_var_parse!(description_add_period, "DESCRIPTION_ADD_PERIOD", bool);
612        load_env_var_parse!(description_max_length, "DESCRIPTION_MAX_LENGTH", usize);
613        load_env_var!(language, "LANGUAGE");
614        load_env_var!(message_template_placeholder, "MESSAGE_TEMPLATE_PLACEHOLDER");
615        load_env_var!(prompt_module, "PROMPT_MODULE");
616        load_env_var_parse!(gitpush, "GITPUSH", bool);
617        load_env_var_parse!(one_line_commit, "ONE_LINE_COMMIT", bool);
618        load_env_var_parse!(why, "WHY", bool);
619        load_env_var_parse!(omit_scope, "OMIT_SCOPE", bool);
620        load_env_var_parse!(action_enabled, "ACTION_ENABLED", bool);
621        load_env_var!(test_mock_type, "TEST_MOCK_TYPE");
622        load_env_var_parse!(hook_auto_uncomment, "HOOK_AUTO_UNCOMMENT", bool);
623        load_env_var!(commitlint_config, "COMMITLINT_CONFIG");
624        load_env_var!(custom_prompt, "CUSTOM_PROMPT");
625        load_env_var_parse!(generate_count, "GENERATE_COUNT", u8);
626        load_env_var_parse!(clipboard_on_timeout, "CLIPBOARD_ON_TIMEOUT", bool);
627        load_env_var_parse!(learn_from_history, "LEARN_FROM_HISTORY", bool);
628        load_env_var_parse!(history_commits_count, "HISTORY_COMMITS_COUNT", usize);
629        load_env_var!(style_profile, "STYLE_PROFILE");
630    }
631}
632
633// ============================================
634// Multi-account support methods
635// ============================================
636
637#[allow(dead_code)]
638impl Config {
639    /// Get the active account config, if available
640    pub fn get_active_account(&self) -> Result<Option<accounts::AccountConfig>> {
641        if let Some(accounts_config) = accounts::AccountsConfig::load()? {
642            if let Some(account) = accounts_config.get_active_account() {
643                return Ok(Some(account.clone()));
644            }
645        }
646        Ok(None)
647    }
648
649    /// Check if we have any accounts configured
650    pub fn has_accounts(&self) -> bool {
651        accounts::AccountsConfig::load()
652            .map(|c| c.map(|ac| !ac.accounts.is_empty()).unwrap_or(false))
653            .unwrap_or(false)
654    }
655
656    /// Get a specific account by alias
657    pub fn get_account(&self, alias: &str) -> Result<Option<accounts::AccountConfig>> {
658        if let Some(accounts_config) = accounts::AccountsConfig::load()? {
659            if let Some(account) = accounts_config.get_account(alias) {
660                return Ok(Some(account.clone()));
661            }
662        }
663        Ok(None)
664    }
665
666    /// List all accounts
667    pub fn list_accounts(&self) -> Result<Vec<accounts::AccountConfig>> {
668        if let Some(accounts_config) = accounts::AccountsConfig::load()? {
669            Ok(accounts_config
670                .list_accounts()
671                .into_iter()
672                .cloned()
673                .collect())
674        } else {
675            Ok(Vec::new())
676        }
677    }
678
679    /// Set an account as the default (active) account
680    pub fn set_default_account(&mut self, alias: &str) -> Result<()> {
681        let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
682        accounts_config.set_active_account(alias)?;
683        accounts_config.save()?;
684        Ok(())
685    }
686
687    /// Remove an account
688    pub fn remove_account(&mut self, alias: &str) -> Result<()> {
689        let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
690        if accounts_config.remove_account(alias) {
691            accounts_config.save()?;
692        }
693        Ok(())
694    }
695}