agtrace_runtime/
config.rs

1use crate::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Resolve the workspace data directory path based on priority:
7/// 1. Explicit path (with tilde expansion)
8/// 2. AGTRACE_PATH environment variable (with tilde expansion)
9/// 3. XDG data directory (recommended default)
10/// 4. ~/.agtrace (fallback for systems without XDG)
11pub fn resolve_workspace_path(explicit_path: Option<&str>) -> Result<PathBuf> {
12    agtrace_core::resolve_workspace_path(explicit_path).map_err(|e| match e {
13        agtrace_core::path::Error::Io(io_err) => Error::Io(io_err),
14        agtrace_core::path::Error::Config(msg) => Error::Config(msg),
15    })
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProviderConfig {
20    pub enabled: bool,
21    pub log_root: PathBuf,
22    #[serde(default)]
23    pub context_window_override: Option<u64>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct Config {
28    #[serde(default)]
29    pub providers: HashMap<String, ProviderConfig>,
30}
31
32impl Config {
33    pub fn load() -> Result<Self> {
34        let config_path = Self::default_path()?;
35        Self::load_from(&config_path)
36    }
37
38    pub fn load_from(path: &PathBuf) -> Result<Self> {
39        if !path.exists() {
40            return Ok(Self::default());
41        }
42
43        let content = std::fs::read_to_string(path)?;
44        let config: Config = toml::from_str(&content)?;
45        Ok(config)
46    }
47
48    pub fn save(&self) -> Result<()> {
49        let config_path = Self::default_path()?;
50        self.save_to(&config_path)
51    }
52
53    pub fn save_to(&self, path: &PathBuf) -> Result<()> {
54        if let Some(parent) = path.parent() {
55            std::fs::create_dir_all(parent)?;
56        }
57
58        let content = toml::to_string_pretty(self)?;
59        std::fs::write(path, content)?;
60        Ok(())
61    }
62
63    pub fn default_path() -> Result<PathBuf> {
64        Ok(resolve_workspace_path(None)?.join("config.toml"))
65    }
66
67    pub fn detect_providers() -> Result<Self> {
68        let mut providers = HashMap::new();
69
70        for (name, path) in agtrace_providers::get_default_log_paths() {
71            if path.exists() {
72                providers.insert(
73                    name,
74                    ProviderConfig {
75                        enabled: true,
76                        log_root: path,
77                        context_window_override: None,
78                    },
79                );
80            }
81        }
82
83        Ok(Config { providers })
84    }
85
86    pub fn enabled_providers(&self) -> Vec<(&String, &ProviderConfig)> {
87        self.providers
88            .iter()
89            .filter(|(_, config)| config.enabled)
90            .collect()
91    }
92
93    pub fn set_provider(&mut self, name: String, config: ProviderConfig) {
94        self.providers.insert(name, config);
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use tempfile::TempDir;
102
103    #[test]
104    fn test_config_default() {
105        let config = Config::default();
106        assert_eq!(config.providers.len(), 0);
107    }
108
109    #[test]
110    fn test_config_save_and_load() -> Result<()> {
111        let temp_dir = TempDir::new()?;
112        let config_path = temp_dir.path().join("config.toml");
113
114        let mut config = Config::default();
115        config.set_provider(
116            "claude".to_string(),
117            ProviderConfig {
118                enabled: true,
119                log_root: PathBuf::from("/home/user/.claude/projects"),
120                context_window_override: None,
121            },
122        );
123
124        config.save_to(&config_path)?;
125        assert!(config_path.exists());
126
127        let loaded = Config::load_from(&config_path)?;
128        assert_eq!(loaded.providers.len(), 1);
129        assert!(loaded.providers.contains_key("claude"));
130        assert!(loaded.providers.get("claude").unwrap().enabled);
131
132        Ok(())
133    }
134
135    #[test]
136    fn test_enabled_providers() {
137        let mut config = Config::default();
138        config.set_provider(
139            "claude".to_string(),
140            ProviderConfig {
141                enabled: true,
142                log_root: PathBuf::from("/test/claude"),
143                context_window_override: None,
144            },
145        );
146        config.set_provider(
147            "codex".to_string(),
148            ProviderConfig {
149                enabled: false,
150                log_root: PathBuf::from("/test/codex"),
151                context_window_override: None,
152            },
153        );
154
155        let enabled = config.enabled_providers();
156        assert_eq!(enabled.len(), 1);
157        assert_eq!(enabled[0].0, "claude");
158    }
159
160    #[test]
161    fn test_load_nonexistent_returns_default() -> Result<()> {
162        let temp_dir = TempDir::new()?;
163        let config_path = temp_dir.path().join("nonexistent.toml");
164
165        let config = Config::load_from(&config_path)?;
166        assert_eq!(config.providers.len(), 0);
167
168        Ok(())
169    }
170}