enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Git operations tool

use crate::tool::Tool;
use async_trait::async_trait;
use serde_json::json;
use tokio::process::Command;

/// Git operations tool
pub struct GitTool;

impl GitTool {
    pub fn new() -> Self {
        Self
    }
}

impl Default for GitTool {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl Tool for GitTool {
    fn name(&self) -> &str {
        "git"
    }

    fn description(&self) -> &str {
        "Execute git commands (clone, status, add, commit, push, pull, etc.)"
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "operation": {
                    "type": "string",
                    "enum": ["clone", "status", "add", "commit", "push", "pull", "branch", "checkout", "log"],
                    "description": "Git operation to perform"
                },
                "args": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Additional arguments for the git command"
                },
                "message": {
                    "type": "string",
                    "description": "Commit message (for commit operation)"
                },
                "repo_url": {
                    "type": "string",
                    "description": "Repository URL (for clone operation)"
                },
                "directory": {
                    "type": "string",
                    "description": "Target directory (for clone) or repository directory"
                }
            },
            "required": ["operation"]
        })
    }

    fn requires_network(&self) -> bool {
        true // Git often requires network access
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
        let operation = args
            .get("operation")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'operation' parameter"))?;

        let operation_args = args
            .get("args")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str())
                    .map(|s| s.to_string())
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        let mut cmd = Command::new("git");
        cmd.arg(operation);

        // Handle specific operations with special parameters
        match operation {
            "clone" => {
                if let Some(url) = args.get("repo_url").and_then(|v| v.as_str()) {
                    cmd.arg(url);
                } else {
                    anyhow::bail!("clone operation requires 'repo_url' parameter");
                }
                if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
                    cmd.arg(dir);
                }
            }
            "commit" => {
                if let Some(msg) = args.get("message").and_then(|v| v.as_str()) {
                    cmd.arg("-m").arg(msg);
                }
            }
            _ => {}
        }

        // Add additional args
        for arg in operation_args {
            cmd.arg(arg);
        }

        // Set working directory if specified
        if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
            cmd.current_dir(dir);
        }

        let output = cmd.output().await?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();

        Ok(json!({
            "success": output.status.success(),
            "stdout": stdout,
            "stderr": stderr,
            "exit_code": output.status.code(),
            "operation": operation
        }))
    }
}

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

    #[tokio::test]
    async fn test_git_status() {
        let tool = GitTool::new();
        let result = tool
            .execute(json!({
                "operation": "status",
                "directory": "."
            }))
            .await
            .unwrap();

        // Should succeed if we're in a git repo
        if result["success"].as_bool().unwrap_or(false) {
            assert!(result["stdout"].as_str().is_some());
        }
    }

    #[tokio::test]
    async fn test_git_version() {
        let tool = GitTool::new();
        let result = tool
            .execute(json!({
                "operation": "version"
            }))
            .await
            .unwrap();

        assert_eq!(result["success"], true);
        assert!(result["stdout"].as_str().unwrap().contains("git version"));
    }
}