darwincode 1.9.104

The open source terminal AI coding agent
use serde::Deserialize;
use std::collections::HashMap;

use crate::config::StoredConfig;

#[derive(Debug, Deserialize, Clone)]
pub struct CustomCommandConfig {
    pub description: String,
    pub model: Option<String>,
    pub context: Option<HashMap<String, String>>,
    pub prompt: CustomPromptConfig,
}

#[derive(Debug, Deserialize, Clone)]
pub struct CustomPromptConfig {
    pub template: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct CustomAgentConfig {
    pub name: String,
    pub model: Option<String>,
    pub allowed_tools: Option<Vec<String>>,
    pub system_prompt: String,
}

impl CustomCommandConfig {
    pub fn execute(&self) -> anyhow::Result<String> {
        let mut template = self.prompt.template.clone();
        if let Some(ref context_map) = self.context {
            for (key, cmd) in context_map {
                let output = if cfg!(target_os = "windows") {
                    std::process::Command::new("cmd").args(["/C", cmd]).output()
                } else {
                    std::process::Command::new("sh").args(["-c", cmd]).output()
                };

                let result_str = match output {
                    Ok(out) => {
                        let out_str = String::from_utf8_lossy(&out.stdout).to_string();
                        let err_str = String::from_utf8_lossy(&out.stderr).to_string();
                        if out.status.success() {
                            out_str
                        } else {
                            format!("Error running command '{}':\n{}", cmd, err_str)
                        }
                    }
                    Err(e) => format!("Failed to execute command '{}': {}", cmd, e),
                };

                let placeholder = format!("{{{{{}}}}}", key);
                template = template.replace(&placeholder, &result_str);
            }
        }
        Ok(template)
    }
}

pub fn find_global_commands_dir() -> Option<std::path::PathBuf> {
    crate::config::config_path()
        .ok()
        .and_then(|p| p.parent().map(|p| p.join("commands")))
}

pub fn load_custom_commands(config: &StoredConfig) -> HashMap<String, (CustomCommandConfig, bool)> {
    let workspace_trusted = config.is_workspace_trusted();
    load_custom_commands_with_trust(workspace_trusted)
}

pub fn load_custom_commands_all() -> HashMap<String, (CustomCommandConfig, bool)> {
    load_custom_commands_with_trust(true)
}

fn load_custom_commands_with_trust(
    workspace_trusted: bool,
) -> HashMap<String, (CustomCommandConfig, bool)> {
    let mut commands = HashMap::new();

    if let Some(global_dir) = find_global_commands_dir()
        && let Ok(entries) = std::fs::read_dir(global_dir)
    {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
                let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
                if !file_name.contains("schema")
                    && !file_name.contains("template")
                    && !file_name.contains("example")
                    && let Ok(toml_content) = std::fs::read_to_string(&path)
                    && let Ok(cfg) = toml::from_str::<CustomCommandConfig>(&toml_content)
                {
                    commands.insert(file_name.to_owned(), (cfg, false));
                }
            }
        }
    }

    if let Some(proj_root) = crate::config::find_project_root() {
        let cmd_dir = proj_root.join(".darwincode").join("commands");
        if let Ok(entries) = std::fs::read_dir(cmd_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
                    let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
                    if !file_name.contains("schema")
                        && !file_name.contains("template")
                        && !file_name.contains("example")
                        && let Ok(toml_content) = std::fs::read_to_string(&path)
                        && let Ok(cfg) = toml::from_str::<CustomCommandConfig>(&toml_content)
                    {
                        if commands.contains_key(file_name) {
                            if workspace_trusted {
                                commands.insert(file_name.to_owned(), (cfg, true));
                            }
                        } else {
                            commands.insert(file_name.to_owned(), (cfg, true));
                        }
                    }
                }
            }
        }
    }
    commands
}

pub fn load_custom_agents() -> HashMap<String, CustomAgentConfig> {
    let mut agents = HashMap::new();
    if let Some(proj_root) = crate::config::find_project_root() {
        let agent_dir = proj_root.join(".darwincode").join("agents");
        if let Ok(entries) = std::fs::read_dir(agent_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
                    let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
                    if !file_name.contains("schema")
                        && !file_name.contains("template")
                        && !file_name.contains("example")
                        && let Ok(toml_content) = std::fs::read_to_string(&path)
                        && let Ok(config) = toml::from_str::<CustomAgentConfig>(&toml_content)
                    {
                        agents.insert(file_name.to_owned(), config);
                    }
                }
            }
        }
    }
    agents
}

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

    #[test]
    fn test_custom_command_execution() {
        let config_toml = r#"
description = "Test command"
prompt.template = "Hello, {{name}}! The status is {{status}}."
"#;
        let mut config: CustomCommandConfig = toml::from_str(config_toml).unwrap();

        let result = config.execute().unwrap();
        assert_eq!(result, "Hello, {{name}}! The status is {{status}}.");

        let mut context = HashMap::new();
        context.insert("name".to_owned(), "echo Antigravity".to_owned());
        context.insert("status".to_owned(), "echo Active".to_owned());
        config.context = Some(context);

        let result2 = config.execute().unwrap();
        assert!(result2.contains("Antigravity"));
        assert!(result2.contains("Active"));
    }

    #[test]
    fn test_custom_agent_deserialization() {
        let agent_toml = r#"
name = "Reviewer Agent"
model = "gemini-2.5-pro"
allowed_tools = ["read", "grep"]
system_prompt = "You are a code reviewer."
"#;
        let config: CustomAgentConfig = toml::from_str(agent_toml).unwrap();
        assert_eq!(config.name, "Reviewer Agent");
        assert_eq!(config.model, Some("gemini-2.5-pro".to_owned()));
        assert_eq!(
            config.allowed_tools,
            Some(vec!["read".to_owned(), "grep".to_owned()])
        );
        assert_eq!(config.system_prompt, "You are a code reviewer.");
    }
}