1use anyhow::Result;
23use serde::{Deserialize, Serialize};
24use std::collections::BTreeMap;
25use std::path::PathBuf;
26
27use agent_doc_frontmatter::frontmatter::{CodexNetworkAccess, FreeTextExecutionMode};
28use agent_doc_model_tier::ModelConfig;
29
30pub mod env;
31
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum ExecutionMode {
36 #[default]
38 Hybrid,
39 Parallel,
41 Sequential,
43}
44
45impl std::fmt::Display for ExecutionMode {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Hybrid => write!(f, "hybrid"),
49 Self::Parallel => write!(f, "parallel"),
50 Self::Sequential => write!(f, "sequential"),
51 }
52 }
53}
54
55#[derive(Debug, Default, Clone, Serialize, Deserialize)]
56pub struct Config {
57 #[serde(default)]
58 pub default_agent: Option<String>,
59 #[serde(default)]
60 pub agents: BTreeMap<String, AgentConfig>,
61 #[serde(default)]
64 pub agent_args: Option<String>,
65 #[serde(default)]
68 pub claude_args: Option<String>,
69 #[serde(default)]
72 pub codex_args: Option<String>,
73 #[serde(default)]
76 pub opencode_args: Option<String>,
77 #[serde(default)]
81 pub codex_network_access: Option<CodexNetworkAccess>,
82 #[serde(default)]
85 pub managed_proof_max_attempts: Option<u32>,
86 #[serde(default)]
89 pub managed_proof_retry_backoff_secs: Option<u64>,
90 #[serde(default)]
93 pub managed_proof_probe_timeout_secs: Option<u64>,
94 #[serde(default, alias = "free_text_execution")]
98 pub agent_doc_free_text_execution: Option<FreeTextExecutionMode>,
99 #[serde(default)]
102 pub execution_mode: Option<ExecutionMode>,
103 #[serde(default)]
105 pub terminal: Option<TerminalConfig>,
106 #[serde(default)]
110 pub model: ModelConfig,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TerminalConfig {
115 pub command: Option<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct AgentConfig {
123 pub command: String,
124 #[serde(default)]
125 pub args: Vec<String>,
126 #[serde(default)]
127 pub result_path: Option<String>,
128 #[serde(default)]
129 pub session_path: Option<String>,
130}
131
132pub fn load() -> Result<Config> {
134 let path = config_path();
135 if path.exists() {
136 let content = std::fs::read_to_string(&path)?;
137 Ok(toml::from_str(&content)?)
138 } else {
139 Ok(Config::default())
140 }
141}
142
143fn config_path() -> PathBuf {
144 dirs_config_dir().join("agent-doc").join("config.toml")
145}
146
147fn dirs_config_dir() -> PathBuf {
148 std::env::var("XDG_CONFIG_HOME")
149 .map(PathBuf::from)
150 .unwrap_or_else(|_| {
151 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
152 PathBuf::from(home).join(".config")
153 })
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_load_missing_global_config() {
162 let _cfg = Config::default();
165 assert!(_cfg.agents.is_empty());
166 }
167
168 #[test]
169 fn test_execution_mode_display() {
170 assert_eq!(ExecutionMode::Hybrid.to_string(), "hybrid");
171 assert_eq!(ExecutionMode::Parallel.to_string(), "parallel");
172 assert_eq!(ExecutionMode::Sequential.to_string(), "sequential");
173 }
174
175 #[test]
176 fn test_config_agent_args_deserialization() {
177 let toml_str = r#"
178agent_args = "--json -s workspace-write"
179claude_args = "--dangerously-skip-permissions"
180codex_args = "-s danger-full-access"
181opencode_args = "--dangerously-skip-permissions"
182codex_network_access = "enabled"
183agent_doc_free_text_execution = "queue"
184"#;
185 let cfg: Config = toml::from_str(toml_str).unwrap();
186 assert_eq!(cfg.agent_args.as_deref(), Some("--json -s workspace-write"));
187 assert_eq!(
188 cfg.claude_args.as_deref(),
189 Some("--dangerously-skip-permissions")
190 );
191 assert_eq!(cfg.codex_args.as_deref(), Some("-s danger-full-access"));
192 assert_eq!(
193 cfg.opencode_args.as_deref(),
194 Some("--dangerously-skip-permissions")
195 );
196 assert_eq!(cfg.codex_network_access, Some(CodexNetworkAccess::Enabled));
197 assert_eq!(
198 cfg.agent_doc_free_text_execution,
199 Some(FreeTextExecutionMode::Queue)
200 );
201 let alias: Config = toml::from_str("free_text_execution = \"goal\"").unwrap();
202 assert_eq!(
203 alias.agent_doc_free_text_execution,
204 Some(FreeTextExecutionMode::Goal)
205 );
206 }
207
208 #[test]
209 fn test_config_agent_args_precedence_resolution() {
210 let cfg = Config {
212 agent_args: Some("--json".to_string()),
213 claude_args: Some("--old-flag".to_string()),
214 ..Default::default()
215 };
216 let resolved = cfg.agent_args.or(cfg.claude_args);
217 assert_eq!(resolved.as_deref(), Some("--json"));
218 }
219
220 #[test]
221 fn test_config_claude_args_fallback() {
222 let cfg = Config {
224 agent_args: None,
225 claude_args: Some("--old-flag".to_string()),
226 ..Default::default()
227 };
228 let resolved = cfg.agent_args.or(cfg.claude_args);
229 assert_eq!(resolved.as_deref(), Some("--old-flag"));
230 }
231
232 #[test]
233 fn test_config_codex_args_fallback() {
234 let cfg = Config {
236 agent_args: None,
237 codex_args: Some("-s danger-full-access".to_string()),
238 ..Default::default()
239 };
240 let resolved = cfg.agent_args.or(cfg.codex_args);
241 assert_eq!(resolved.as_deref(), Some("-s danger-full-access"));
242 }
243
244 #[test]
245 fn test_config_opencode_args_fallback() {
246 let cfg = Config {
248 agent_args: None,
249 opencode_args: Some("--dangerously-skip-permissions".to_string()),
250 ..Default::default()
251 };
252 let resolved = cfg.agent_args.or(cfg.opencode_args);
253 assert_eq!(resolved.as_deref(), Some("--dangerously-skip-permissions"));
254 }
255}