use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
Hybrid,
Parallel,
Sequential,
}
impl std::fmt::Display for ExecutionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hybrid => write!(f, "hybrid"),
Self::Parallel => write!(f, "parallel"),
Self::Sequential => write!(f, "sequential"),
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub default_agent: Option<String>,
#[serde(default)]
pub agents: BTreeMap<String, AgentConfig>,
#[serde(default)]
pub claude_args: Option<String>,
#[serde(default)]
pub execution_mode: Option<ExecutionMode>,
#[serde(default)]
pub terminal: Option<TerminalConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TerminalConfig {
pub command: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub result_path: Option<String>,
#[serde(default)]
pub session_path: Option<String>,
}
pub fn load() -> Result<Config> {
let path = config_path();
if path.exists() {
let content = std::fs::read_to_string(&path)?;
Ok(toml::from_str(&content)?)
} else {
Ok(Config::default())
}
}
fn config_path() -> PathBuf {
dirs_config_dir()
.join("agent-doc")
.join("config.toml")
}
fn dirs_config_dir() -> PathBuf {
std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".config")
})
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
pub tmux_session: Option<String>,
}
pub fn load_project() -> ProjectConfig {
let path = project_config_path();
if path.exists() {
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("warning: failed to parse {}: {}", path.display(), e);
ProjectConfig::default()
}
},
Err(e) => {
eprintln!("warning: failed to read {}: {}", path.display(), e);
ProjectConfig::default()
}
}
} else {
ProjectConfig::default()
}
}
pub fn project_tmux_session() -> Option<String> {
load_project().tmux_session
}
pub fn save_project(config: &ProjectConfig) -> Result<()> {
let path = project_config_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(config)?;
std::fs::write(&path, content)?;
Ok(())
}
pub fn update_project_tmux_session(new_session: &str) -> Result<()> {
let mut config = load_project();
let old = config.tmux_session.clone();
config.tmux_session = Some(new_session.to_string());
save_project(&config)?;
eprintln!(
"[config] updated tmux_session: {} → {}",
old.as_deref().unwrap_or("(none)"),
new_session
);
Ok(())
}
fn project_config_path() -> PathBuf {
PathBuf::from(".agent-doc").join("config.toml")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn load_project_from(dir: &Path) -> ProjectConfig {
let path = dir.join(".agent-doc").join("config.toml");
if path.exists() {
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(_) => ProjectConfig::default(),
},
Err(_) => ProjectConfig::default(),
}
} else {
ProjectConfig::default()
}
}
fn save_project_to(dir: &Path, config: &ProjectConfig) -> Result<()> {
let path = dir.join(".agent-doc").join("config.toml");
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(config)?;
std::fs::write(&path, content)?;
Ok(())
}
#[test]
fn save_and_load_project_config() {
let dir = tempfile::tempdir().unwrap();
let cfg = load_project_from(dir.path());
assert!(cfg.tmux_session.is_none());
let cfg = ProjectConfig { tmux_session: Some("5".to_string()) };
save_project_to(dir.path(), &cfg).unwrap();
let loaded = load_project_from(dir.path());
assert_eq!(loaded.tmux_session.as_deref(), Some("5"));
let mut updated = load_project_from(dir.path());
updated.tmux_session = Some("9".to_string());
save_project_to(dir.path(), &updated).unwrap();
let loaded = load_project_from(dir.path());
assert_eq!(loaded.tmux_session.as_deref(), Some("9"));
}
#[test]
fn update_creates_config_if_missing() {
let dir = tempfile::tempdir().unwrap();
let cfg = ProjectConfig { tmux_session: Some("3".to_string()) };
save_project_to(dir.path(), &cfg).unwrap();
let loaded = load_project_from(dir.path());
assert_eq!(loaded.tmux_session.as_deref(), Some("3"));
}
#[test]
fn load_malformed_config_returns_defaults() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".agent-doc")).unwrap();
std::fs::write(dir.path().join(".agent-doc/config.toml"), "not valid { toml").unwrap();
let cfg = load_project_from(dir.path());
assert!(cfg.tmux_session.is_none(), "malformed config should return defaults");
}
}