carp_cli/config/
settings.rs

1use crate::utils::error::{CarpError, CarpResult};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// Configuration structure for the Carp CLI
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    /// Registry API base URL
10    pub registry_url: String,
11    /// User API token for authentication
12    pub api_token: Option<String>,
13    /// Default timeout for API requests in seconds
14    pub timeout: u64,
15    /// Whether to verify SSL certificates
16    pub verify_ssl: bool,
17    /// Default output directory for pulled agents
18    pub default_output_dir: Option<String>,
19}
20
21impl Default for Config {
22    fn default() -> Self {
23        Self {
24            registry_url: "https://api.carp.refcell.org".to_string(),
25            api_token: None,
26            timeout: 30,
27            verify_ssl: true,
28            default_output_dir: None,
29        }
30    }
31}
32
33/// Configuration manager for loading and saving config
34pub struct ConfigManager;
35
36impl ConfigManager {
37    /// Get the path to the config file
38    pub fn config_path() -> CarpResult<PathBuf> {
39        let config_dir = dirs::config_dir()
40            .ok_or_else(|| CarpError::Config("Unable to find config directory".to_string()))?;
41        
42        let carp_dir = config_dir.join("carp");
43        if !carp_dir.exists() {
44            fs::create_dir_all(&carp_dir)?;
45        }
46        
47        Ok(carp_dir.join("config.toml"))
48    }
49    
50    /// Load configuration from file, creating default if it doesn't exist
51    pub fn load() -> CarpResult<Config> {
52        let config_path = Self::config_path()?;
53        
54        if !config_path.exists() {
55            let default_config = Config::default();
56            Self::save(&default_config)?;
57            return Ok(default_config);
58        }
59        
60        let contents = fs::read_to_string(&config_path)
61            .map_err(|e| CarpError::Config(format!("Failed to read config file: {}", e)))?;
62        
63        let config: Config = toml::from_str(&contents)?;
64        
65        // Validate registry URL
66        Self::validate_registry_url(&config.registry_url)?;
67        
68        // Ensure HTTPS for security
69        if !config.registry_url.starts_with("https://") {
70            eprintln!("Warning: Registry URL is not using HTTPS. This is insecure.");
71        }
72        
73        Ok(config)
74    }
75    
76    /// Validate registry URL format and security
77    fn validate_registry_url(url: &str) -> CarpResult<()> {
78        // Basic URL validation
79        if url.is_empty() {
80            return Err(CarpError::Config("Registry URL cannot be empty".to_string()));
81        }
82        
83        if !url.starts_with("http://") && !url.starts_with("https://") {
84            return Err(CarpError::Config(
85                "Registry URL must start with http:// or https://".to_string()
86            ));
87        }
88        
89        // Parse URL to validate format
90        if let Err(_) = url.parse::<reqwest::Url>() {
91            return Err(CarpError::Config(
92                "Invalid registry URL format".to_string()
93            ));
94        }
95        
96        Ok(())
97    }
98    
99    /// Save configuration to file
100    pub fn save(config: &Config) -> CarpResult<()> {
101        let config_path = Self::config_path()?;
102        let contents = toml::to_string_pretty(config)
103            .map_err(|e| CarpError::Config(format!("Failed to serialize config: {}", e)))?;
104        
105        fs::write(&config_path, contents)
106            .map_err(|e| CarpError::Config(format!("Failed to write config file: {}", e)))?;
107        
108        // Set restrictive permissions on config file (600 - owner read/write only)
109        #[cfg(unix)]
110        {
111            use std::os::unix::fs::PermissionsExt;
112            let mut perms = fs::metadata(&config_path)?.permissions();
113            perms.set_mode(0o600);
114            fs::set_permissions(&config_path, perms)?;
115        }
116        
117        Ok(())
118    }
119    
120    /// Update the API token in the config
121    pub fn set_api_token(token: String) -> CarpResult<()> {
122        let mut config = Self::load()?;
123        config.api_token = Some(token);
124        Self::save(&config)
125    }
126    
127    /// Clear the API token from the config
128    pub fn clear_api_token() -> CarpResult<()> {
129        let mut config = Self::load()?;
130        config.api_token = None;
131        Self::save(&config)
132    }
133    
134    /// Get the cache directory for storing downloaded agents
135    pub fn cache_dir() -> CarpResult<PathBuf> {
136        let cache_dir = dirs::cache_dir()
137            .ok_or_else(|| CarpError::Config("Unable to find cache directory".to_string()))?;
138        
139        let carp_cache = cache_dir.join("carp");
140        if !carp_cache.exists() {
141            fs::create_dir_all(&carp_cache)?;
142        }
143        
144        Ok(carp_cache)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use tempfile::TempDir;
152    
153    #[test]
154    fn test_default_config() {
155        let config = Config::default();
156        assert_eq!(config.registry_url, "https://api.carp.refcell.org");
157        assert!(config.api_token.is_none());
158        assert_eq!(config.timeout, 30);
159        assert!(config.verify_ssl);
160    }
161    
162    #[test]
163    fn test_config_serialization() {
164        let config = Config::default();
165        let toml_str = toml::to_string(&config).unwrap();
166        let deserialized: Config = toml::from_str(&toml_str).unwrap();
167        
168        assert_eq!(config.registry_url, deserialized.registry_url);
169        assert_eq!(config.timeout, deserialized.timeout);
170    }
171}