1use 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, 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 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(), session_config: self.session_config.clone(),
49 task_config: self.task_config.clone(),
50 claude_config: self.claude_config.clone(),
51 }
52 }
53
54 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 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
69pub struct ConfigDiscovery;
71
72impl ConfigDiscovery {
73 pub fn discover_config() -> Result<DefaultAgentConfig, Box<dyn std::error::Error>> {
75 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 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 fn get_config_candidates() -> Vec<PathBuf> {
103 let mut candidates = Vec::new();
104
105 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(¤t_dir));
109 }
110
111 if let Some(home_dir) = Self::get_home_dir() {
113 candidates.push(env::user_config_file_path(&home_dir));
114 }
115
116 #[cfg(unix)]
118 candidates.push(PathBuf::from("/etc/aca/config.toml"));
119
120 #[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 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 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 if !config_dir.exists() {
146 fs::create_dir_all(&config_dir)?;
147 info!("Created configuration directory: {:?}", config_dir);
148 }
149
150 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 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 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 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 original_config.to_toml_file(&config_path).unwrap();
224 assert!(config_path.exists());
225
226 let loaded_config = DefaultAgentConfig::from_toml_file(&config_path).unwrap();
228
229 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 assert!(!candidates.is_empty());
242
243 assert!(candidates[0].file_name().unwrap() == "aca.toml");
245 }
246}