mana-core 0.3.2

Core library for mana — task tracker for AI coding agents
Documentation
use std::path::Path;

use anyhow::{anyhow, Result};

use crate::config::{Config, GlobalConfig, DEFAULT_COMMIT_TEMPLATE};

/// Get a configuration value by key.
///
/// Returns the string representation of the config value.
pub fn config_get(mana_dir: &Path, key: &str) -> Result<String> {
    let config = Config::load_with_extends(mana_dir)?;

    let value = match key {
        "project" => config.project,
        "next_id" => config.next_id.to_string(),
        "auto_close_parent" => config.auto_close_parent.to_string(),
        "run" => config.run.unwrap_or_default(),
        "plan" => config.plan.unwrap_or_default(),
        "max_concurrent" => config.max_concurrent.to_string(),
        "poll_interval" => config.poll_interval.to_string(),
        "rules_file" => config.rules_file.unwrap_or_else(|| "RULES.md".to_string()),
        "auto_commit" => config.auto_commit.to_string(),
        "commit_template" => config
            .commit_template
            .unwrap_or_else(|| DEFAULT_COMMIT_TEMPLATE.to_string()),
        "research" => config.research.unwrap_or_default(),
        "run_model" => config.run_model.unwrap_or_default(),
        "plan_model" => config.plan_model.unwrap_or_default(),
        "review_model" => config.review_model.unwrap_or_default(),
        "research_model" => config.research_model.unwrap_or_default(),
        "on_close" => config.on_close.unwrap_or_default(),
        "on_fail" => config.on_fail.unwrap_or_default(),
        "memory_reserve_mb" => config.memory_reserve_mb.to_string(),
        "user" => {
            if let Some(user) = config.user {
                user
            } else if let Ok(global) = GlobalConfig::load() {
                global.user.unwrap_or_default()
            } else {
                String::new()
            }
        }
        "user.email" => {
            if let Some(email) = config.user_email {
                email
            } else if let Ok(global) = GlobalConfig::load() {
                global.user_email.unwrap_or_default()
            } else {
                String::new()
            }
        }
        _ => return Err(anyhow!("Unknown config key: {}", key)),
    };

    Ok(value)
}

/// Set a configuration value by key, persisting to disk.
pub fn config_set(mana_dir: &Path, key: &str, value: &str) -> Result<()> {
    let mut config = Config::load(mana_dir)?;

    match key {
        "project" => {
            config.project = value.to_string();
        }
        "next_id" => {
            config.next_id = value
                .parse()
                .map_err(|_| anyhow!("Invalid value for next_id: {}", value))?;
        }
        "auto_close_parent" => {
            config.auto_close_parent = value.parse().map_err(|_| {
                anyhow!(
                    "Invalid value for auto_close_parent: {} (expected true/false)",
                    value
                )
            })?;
        }
        "run" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.run = None;
            } else {
                config.run = Some(value.to_string());
            }
        }
        "plan" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.plan = None;
            } else {
                config.plan = Some(value.to_string());
            }
        }
        "max_concurrent" => {
            config.max_concurrent = value.parse().map_err(|_| {
                anyhow!(
                    "Invalid value for max_concurrent: {} (expected positive integer)",
                    value
                )
            })?;
        }
        "poll_interval" => {
            config.poll_interval = value.parse().map_err(|_| {
                anyhow!(
                    "Invalid value for poll_interval: {} (expected positive integer)",
                    value
                )
            })?;
        }
        "rules_file" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.rules_file = None;
            } else {
                config.rules_file = Some(value.to_string());
            }
        }
        "auto_commit" => {
            config.auto_commit = value.parse().map_err(|_| {
                anyhow!(
                    "Invalid value for auto_commit: {} (expected true/false)",
                    value
                )
            })?;
        }
        "commit_template" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.commit_template = None;
            } else {
                config.commit_template = Some(value.to_string());
            }
        }
        "research" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.research = None;
            } else {
                config.research = Some(value.to_string());
            }
        }
        "run_model" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.run_model = None;
            } else {
                config.run_model = Some(value.to_string());
            }
        }
        "plan_model" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.plan_model = None;
            } else {
                config.plan_model = Some(value.to_string());
            }
        }
        "review_model" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.review_model = None;
            } else {
                config.review_model = Some(value.to_string());
            }
        }
        "research_model" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.research_model = None;
            } else {
                config.research_model = Some(value.to_string());
            }
        }
        "on_close" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.on_close = None;
            } else {
                config.on_close = Some(value.to_string());
            }
        }
        "on_fail" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.on_fail = None;
            } else {
                config.on_fail = Some(value.to_string());
            }
        }
        "memory_reserve_mb" => {
            config.memory_reserve_mb = value.parse().map_err(|_| {
                anyhow!(
                    "Invalid value for memory_reserve_mb: {} (expected non-negative integer in MB)",
                    value
                )
            })?;
        }
        "user" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.user = None;
            } else {
                config.user = Some(value.to_string());
            }
        }
        "user.email" => {
            if value.is_empty() || value == "none" || value == "unset" {
                config.user_email = None;
            } else {
                config.user_email = Some(value.to_string());
            }
        }
        _ => return Err(anyhow!("Unknown config key: {}", key)),
    }

    config.save(mana_dir)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    fn setup_test_dir() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        fs::write(
            dir.path().join("config.yaml"),
            "project: test\nnext_id: 1\nauto_close_parent: true\n",
        )
        .unwrap();
        dir
    }

    #[test]
    fn get_unknown_key_returns_error() {
        let dir = setup_test_dir();
        let result = config_get(dir.path(), "unknown_key");
        assert!(result.is_err());
    }

    #[test]
    fn set_unknown_key_returns_error() {
        let dir = setup_test_dir();
        let result = config_set(dir.path(), "unknown_key", "value");
        assert!(result.is_err());
    }

    #[test]
    fn get_project() {
        let dir = setup_test_dir();
        let value = config_get(dir.path(), "project").unwrap();
        assert_eq!(value, "test");
    }

    #[test]
    fn set_run_stores_and_clears() {
        let dir = setup_test_dir();
        config_set(dir.path(), "run", "claude -p 'implement {id}'").unwrap();

        let config = Config::load(dir.path()).unwrap();
        assert_eq!(config.run, Some("claude -p 'implement {id}'".to_string()));

        config_set(dir.path(), "run", "none").unwrap();
        let config = Config::load(dir.path()).unwrap();
        assert_eq!(config.run, None);
    }

    #[test]
    fn get_commit_template_returns_default_when_unset() {
        let dir = setup_test_dir();
        let value = config_get(dir.path(), "commit_template").unwrap();
        assert_eq!(value, DEFAULT_COMMIT_TEMPLATE);
    }
}