malvin 0.2.4

Non-interactive research and coding agent
use std::path::Path;

use super::{
    AgentConfig, MalvinConfig, ensure_config_parent_dir, merge_missing_keys, parse_agent_config,
    parse_malvin_config, parse_template_value, write_config_value,
};
use crate::workspace_paths::malvin_config_path;

pub(super) fn create_malvin_config_from_template(
    path: &Path,
    template: &toml::Value,
) -> Result<MalvinConfig, String> {
    let mut on_disk = toml::Value::Table(toml::map::Map::new());
    merge_missing_keys(&mut on_disk, template);
    write_config_value(path, &on_disk)?;
    Ok(parse_malvin_config(
        &toml::to_string(&on_disk).map_err(|e| e.to_string())?,
    ))
}

pub fn ensure_malvin_config_file_if_missing(work_dir: &Path) -> Result<(), String> {
    let path = malvin_config_path(work_dir);
    ensure_config_parent_dir(&path)?;
    if path.is_file() && !file_is_empty(&path)? {
        return Ok(());
    }
    if path.is_file() {
        std::fs::remove_file(&path).map_err(|e| format!("remove empty {}: {e}", path.display()))?;
    }
    let template = parse_template_value()?;
    let _ = create_malvin_config_from_template(&path, &template)?;
    Ok(())
}

fn file_is_empty(path: &Path) -> Result<bool, String> {
    let meta = std::fs::metadata(path).map_err(|e| format!("stat {}: {e}", path.display()))?;
    Ok(meta.len() == 0)
}

pub fn load_agent_config_strict(work_dir: &Path) -> Result<AgentConfig, String> {
    let path = malvin_config_path(work_dir);
    let Ok(text) = std::fs::read_to_string(&path) else {
        return Ok(AgentConfig::default());
    };
    let Ok(template) = parse_template_value() else {
        return parse_agent_config(&text);
    };
    let Ok(mut on_disk) = text.parse::<toml::Value>() else {
        return Err(format!("invalid TOML in {}", path.display()));
    };
    let _ = merge_missing_keys(&mut on_disk, &template);
    let merged = toml::to_string(&on_disk).map_err(|e| e.to_string())?;
    parse_agent_config(&merged)
}

#[must_use]
pub fn load_agent_config_lenient(work_dir: &Path) -> AgentConfig {
    if let Ok(agent) = load_agent_config_strict(work_dir) {
        return agent;
    }
    let path = malvin_config_path(work_dir);
    let Ok(text) = std::fs::read_to_string(&path) else {
        return AgentConfig::default();
    };
    let Ok(template) = parse_template_value() else {
        return AgentConfig::default();
    };
    let Ok(mut on_disk) = text.parse::<toml::Value>() else {
        return AgentConfig::default();
    };
    let _ = merge_missing_keys(&mut on_disk, &template);
    if let Some(table) = on_disk.get_mut("agent").and_then(toml::Value::as_table_mut) {
        table.insert(
            "model".into(),
            toml::Value::String(AgentConfig::default().model),
        );
    }
    let Ok(merged) = toml::to_string(&on_disk) else {
        return AgentConfig::default();
    };
    parse_agent_config(&merged).unwrap_or_default()
}