Skip to main content

uira_cli/
config.rs

1//! CLI configuration
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use uira_core::{atomic_write_secure, UIRA_DIR};
6
7/// CLI-specific configuration
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct CliConfig {
10    /// Default provider
11    #[serde(default)]
12    pub default_provider: Option<String>,
13
14    /// Default model
15    #[serde(default)]
16    pub default_model: Option<String>,
17
18    /// API keys by provider
19    #[serde(default)]
20    pub api_keys: std::collections::HashMap<String, String>,
21
22    /// Default working directory
23    #[serde(default)]
24    pub working_directory: Option<PathBuf>,
25
26    /// Enable colors in output
27    #[serde(default = "default_true")]
28    pub colors: bool,
29
30    /// Enable verbose output
31    #[serde(default)]
32    pub verbose: bool,
33}
34
35fn default_true() -> bool {
36    true
37}
38
39impl Default for CliConfig {
40    fn default() -> Self {
41        Self {
42            default_provider: None,
43            default_model: None,
44            api_keys: std::collections::HashMap::new(),
45            working_directory: None,
46            colors: true,
47            verbose: false,
48        }
49    }
50}
51
52#[allow(dead_code)] // Public API methods
53impl CliConfig {
54    /// Load configuration from default locations
55    pub fn load() -> Self {
56        // Try to load from:
57        // 1. ~/.config/uira/config.toml
58        // 2. ~/.uira/config.toml
59        // 3. Use defaults
60
61        if let Some(config_dir) = dirs::config_dir() {
62            let config_path = config_dir.join("uira").join("config.toml");
63            if config_path.exists() {
64                if let Ok(content) = std::fs::read_to_string(&config_path) {
65                    if let Ok(config) = toml::from_str(&content) {
66                        return config;
67                    }
68                }
69            }
70        }
71
72        if let Some(home) = dirs::home_dir() {
73            let config_path = home.join(UIRA_DIR).join("config.toml");
74            if config_path.exists() {
75                if let Ok(content) = std::fs::read_to_string(&config_path) {
76                    if let Ok(config) = toml::from_str(&content) {
77                        return config;
78                    }
79                }
80            }
81        }
82
83        Self::default()
84    }
85
86    /// Save configuration to disk
87    pub fn save(&self) -> std::io::Result<()> {
88        let config_dir = dirs::config_dir()
89            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "No config dir"))?
90            .join("uira");
91
92        std::fs::create_dir_all(&config_dir)?;
93
94        let config_path = config_dir.join("config.toml");
95        let content = toml::to_string_pretty(self)
96            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
97
98        atomic_write_secure(&config_path, content.as_bytes())
99    }
100
101    /// Get API key for a provider
102    pub fn get_api_key(&self, provider: &str) -> Option<&String> {
103        self.api_keys.get(provider)
104    }
105
106    /// Set API key for a provider
107    pub fn set_api_key(&mut self, provider: impl Into<String>, key: impl Into<String>) {
108        self.api_keys.insert(provider.into(), key.into());
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_default_config() {
118        let config = CliConfig::default();
119        assert!(config.colors);
120        assert!(!config.verbose);
121    }
122
123    #[test]
124    fn test_api_key_management() {
125        let mut config = CliConfig::default();
126        config.set_api_key("anthropic", "sk-test-key");
127        assert_eq!(
128            config.get_api_key("anthropic"),
129            Some(&"sk-test-key".to_string())
130        );
131    }
132}