tuff-core 0.14.0

Core lifecycle engine and adapter contract for Tuff.
Documentation
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::error::Result;
use crate::paths;

pub const DEFAULT_AGENT: &str = "open-agents";

#[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct TuffConfig {
    #[serde(rename = "agents", alias = "targets", alias = "harnesses")]
    pub agents: Vec<String>,
    #[serde(rename = "defaultAgent", alias = "default_agent")]
    pub default_agent: String,
}

impl Default for TuffConfig {
    fn default() -> Self {
        Self {
            agents: vec![],
            default_agent: DEFAULT_AGENT.to_string(),
        }
    }
}

pub fn read_config(repo_root: &Path) -> Result<TuffConfig> {
    read_config_at(&paths::project_config(repo_root))
}

pub fn read_global_config(home: &Path) -> Result<TuffConfig> {
    read_config_at(&paths::user_config(home))
}

fn read_config_at(path: &Path) -> Result<TuffConfig> {
    if path.exists() {
        let config: TuffConfig = serde_json::from_str(&std::fs::read_to_string(path)?)?;
        Ok(config)
    } else {
        let default = TuffConfig::default();
        write_config_at(path, &default)?;
        Ok(default)
    }
}

pub fn write_config(repo_root: &Path, config: &TuffConfig) -> Result<()> {
    write_config_at(&paths::project_config(repo_root), config)
}

pub fn write_global_config(home: &Path, config: &TuffConfig) -> Result<()> {
    write_config_at(&paths::user_config(home), config)
}

fn write_config_at(path: &Path, config: &TuffConfig) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, serde_json::to_string_pretty(config)? + "\n")?;
    Ok(())
}