aca/cli/
config.rs

1//! Configuration discovery and loading
2//!
3//! This module handles the configuration discovery hierarchy:
4//! 1. Current directory: ./aca.toml or ./.aca/config.toml
5//! 2. User config: ~/.aca/config.toml
6//! 3. System config: /etc/aca/config.toml
7//! 4. Built-in defaults
8
9use crate::{
10    AgentConfig, claude::ClaudeConfig, env, session::SessionManagerConfig, task::TaskManagerConfig,
11};
12use serde::{Deserialize, Serialize};
13use std::env as std_env;
14use std::fs;
15use std::path::{Path, PathBuf};
16use tracing::{debug, info, warn};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DefaultAgentConfig {
20    pub workspace_path: Option<PathBuf>,
21    pub session_config: SessionManagerConfig,
22    pub task_config: TaskManagerConfig,
23    pub claude_config: ClaudeConfig,
24}
25
26impl Default for DefaultAgentConfig {
27    fn default() -> Self {
28        let default_agent = AgentConfig::default();
29        Self {
30            workspace_path: None, // Will be set to current dir if not specified
31            session_config: default_agent.session_config,
32            task_config: default_agent.task_config,
33            claude_config: default_agent.claude_config,
34        }
35    }
36}
37
38impl DefaultAgentConfig {
39    /// Convert to AgentConfig with specified workspace and empty setup commands
40    pub fn to_agent_config(&self, workspace_override: Option<PathBuf>) -> AgentConfig {
41        let workspace_path = workspace_override
42            .or_else(|| self.workspace_path.clone())
43            .unwrap_or_else(|| std_env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
44
45        AgentConfig {
46            workspace_path,
47            setup_commands: Vec::new(), // Will be populated by task processing
48            session_config: self.session_config.clone(),
49            task_config: self.task_config.clone(),
50            claude_config: self.claude_config.clone(),
51        }
52    }
53
54    /// Load from TOML file
55    pub fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
56        let content = fs::read_to_string(path)?;
57        let config: DefaultAgentConfig = toml::from_str(&content)?;
58        Ok(config)
59    }
60
61    /// Save to TOML file
62    pub fn to_toml_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
63        let content = toml::to_string_pretty(self)?;
64        fs::write(path, content)?;
65        Ok(())
66    }
67}
68
69/// Configuration discovery system
70pub struct ConfigDiscovery;
71
72impl ConfigDiscovery {
73    /// Discover and load configuration using the hierarchy
74    pub fn discover_config() -> Result<DefaultAgentConfig, Box<dyn std::error::Error>> {
75        // Try discovery hierarchy
76        if let Some(config_path) = Self::find_config_file() {
77            info!("Loading configuration from: {:?}", config_path);
78            return DefaultAgentConfig::from_toml_file(config_path);
79        }
80
81        info!("No configuration file found, using defaults");
82        Ok(DefaultAgentConfig::default())
83    }
84
85    /// Find configuration file using discovery hierarchy
86    pub fn find_config_file() -> Option<PathBuf> {
87        let candidates = Self::get_config_candidates();
88
89        for candidate in candidates {
90            debug!("Checking for config file: {:?}", candidate);
91            if candidate.exists() && candidate.is_file() {
92                debug!("Found config file: {:?}", candidate);
93                return Some(candidate);
94            }
95        }
96
97        debug!("No config file found in discovery hierarchy");
98        None
99    }
100
101    /// Get list of configuration file candidates in priority order
102    fn get_config_candidates() -> Vec<PathBuf> {
103        let mut candidates = Vec::new();
104
105        // 1. Current directory: ./aca.toml
106        if let Ok(current_dir) = std_env::current_dir() {
107            candidates.push(current_dir.join("aca.toml"));
108            candidates.push(env::local_config_file_path(&current_dir));
109        }
110
111        // 2. User config: ~/.aca/config.toml
112        if let Some(home_dir) = Self::get_home_dir() {
113            candidates.push(env::user_config_file_path(&home_dir));
114        }
115
116        // 3. System config: /etc/aca/config.toml (Unix-like systems)
117        #[cfg(unix)]
118        candidates.push(PathBuf::from("/etc/aca/config.toml"));
119
120        // Windows system config: C:\ProgramData\aca\config.toml
121        #[cfg(windows)]
122        if let Ok(program_data) = std_env::var("PROGRAMDATA") {
123            candidates.push(PathBuf::from(program_data).join("aca").join("config.toml"));
124        }
125
126        candidates
127    }
128
129    /// Get home directory path
130    fn get_home_dir() -> Option<PathBuf> {
131        std_env::var("HOME")
132            .ok()
133            .or_else(|| std_env::var("USERPROFILE").ok())
134            .map(PathBuf::from)
135    }
136
137    /// Create a default config file in the user's home directory
138    pub fn create_default_user_config() -> Result<PathBuf, Box<dyn std::error::Error>> {
139        let home_dir = Self::get_home_dir().ok_or("Could not determine home directory")?;
140
141        let config_dir = env::user_config_dir_path(&home_dir);
142        let config_path = env::user_config_file_path(&home_dir);
143
144        // Create directory if it doesn't exist
145        if !config_dir.exists() {
146            fs::create_dir_all(&config_dir)?;
147            info!("Created configuration directory: {:?}", config_dir);
148        }
149
150        // Create default config if it doesn't exist
151        if !config_path.exists() {
152            let default_config = DefaultAgentConfig::default();
153            default_config.to_toml_file(&config_path)?;
154            info!("Created default configuration file: {:?}", config_path);
155        } else {
156            warn!("Configuration file already exists: {:?}", config_path);
157        }
158
159        Ok(config_path)
160    }
161
162    /// Show configuration discovery information for debugging
163    pub fn show_discovery_info() {
164        println!("Configuration Discovery Hierarchy:");
165        println!();
166
167        let candidates = Self::get_config_candidates();
168        for (i, candidate) in candidates.iter().enumerate() {
169            let status = if candidate.exists() {
170                if candidate.is_file() {
171                    "✓ EXISTS"
172                } else {
173                    "✗ NOT A FILE"
174                }
175            } else {
176                "✗ NOT FOUND"
177            };
178
179            println!("  {}. {:?} - {}", i + 1, candidate, status);
180        }
181
182        println!();
183        if let Some(found) = Self::find_config_file() {
184            println!("Active configuration: {:?}", found);
185        } else {
186            println!("Active configuration: Built-in defaults");
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use tempfile::TempDir;
195
196    #[test]
197    fn test_default_agent_config() {
198        let config = DefaultAgentConfig::default();
199        let agent_config = config.to_agent_config(None);
200
201        // Should have current directory as workspace if not specified
202        assert!(!agent_config.workspace_path.as_os_str().is_empty());
203        assert_eq!(agent_config.setup_commands.len(), 0);
204    }
205
206    #[test]
207    fn test_config_serialization() {
208        let config = DefaultAgentConfig::default();
209        let toml_string = toml::to_string(&config).unwrap();
210
211        // Should be able to deserialize back
212        let _deserialized: DefaultAgentConfig = toml::from_str(&toml_string).unwrap();
213    }
214
215    #[test]
216    fn test_config_file_operations() {
217        let temp_dir = TempDir::new().unwrap();
218        let config_path = temp_dir.path().join("test_config.toml");
219
220        let original_config = DefaultAgentConfig::default();
221
222        // Save config
223        original_config.to_toml_file(&config_path).unwrap();
224        assert!(config_path.exists());
225
226        // Load config
227        let loaded_config = DefaultAgentConfig::from_toml_file(&config_path).unwrap();
228
229        // Compare key fields (session config should match)
230        assert_eq!(
231            original_config.session_config.auto_save_interval_minutes,
232            loaded_config.session_config.auto_save_interval_minutes
233        );
234    }
235
236    #[test]
237    fn test_config_candidates() {
238        let candidates = ConfigDiscovery::get_config_candidates();
239
240        // Should have at least current directory candidates
241        assert!(!candidates.is_empty());
242
243        // First candidates should be current directory
244        assert!(candidates[0].file_name().unwrap() == "aca.toml");
245    }
246}