xcodeai 2.1.0

Autonomous AI coding agent — zero human intervention, sbox sandboxed, OpenAI-compatible
Documentation
use crate::tools::{Tool, ToolContext, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use std::path::{Path, PathBuf};

pub struct FileWriteTool;

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

    fn description(&self) -> &str {
        "Write content to a file, creating parent directories if needed."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to write (relative to working_dir or absolute)"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file"
                }
            },
            "required": ["path", "content"]
        })
    }

    async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> Result<ToolResult> {
        let path_str = match args["path"].as_str() {
            Some(p) => p.to_string(),
            None => {
                return Ok(ToolResult {
                    output: "Error: 'path' parameter is required".to_string(),
                    is_error: true,
                });
            }
        };

        let content = match args["content"].as_str() {
            Some(c) => c.to_string(),
            None => {
                return Ok(ToolResult {
                    output: "Error: 'content' parameter is required".to_string(),
                    is_error: true,
                });
            }
        };

        let path = resolve_path(&path_str, &ctx.working_dir);

        if let Some(parent) = path.parent() {
            if let Err(e) = std::fs::create_dir_all(parent) {
                return Ok(ToolResult {
                    output: format!(
                        "Error: failed to create directories for '{}': {}",
                        path_str, e
                    ),
                    is_error: true,
                });
            }
        }

        let bytes = content.len();
        if let Err(e) = std::fs::write(&path, &content) {
            return Ok(ToolResult {
                output: format!("Error: failed to write file '{}': {}", path_str, e),
                is_error: true,
            });
        }

        // ── Run formatter if configured for this file extension ──────────
        if let Some(ext) = std::path::Path::new(&path_str).extension().and_then(|e| e.to_str()) {
            if let Some(fmt_cmd) = ctx.formatters.get(ext) {
                let cmd = fmt_cmd.replace("{}", &path.to_string_lossy());
                let _ = std::process::Command::new("sh")
                    .arg("-c")
                    .arg(&cmd)
                    .current_dir(&ctx.working_dir)
                    .output();
            }
        }

        Ok(ToolResult {
            output: format!("Written {} bytes to {}", bytes, path_str),
            is_error: false,
        })
    }
}

fn resolve_path(path_str: &str, working_dir: &Path) -> PathBuf {
    let p = PathBuf::from(path_str);
    if p.is_absolute() {
        p
    } else {
        working_dir.join(p)
    }
}

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

    fn make_ctx(dir: &std::path::Path) -> ToolContext {
        ToolContext {
            working_dir: dir.to_path_buf(),
            sandbox_enabled: false,
            io: std::sync::Arc::new(crate::io::NullIO),
            compact_mode: false,
            lsp_client: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
            mcp_client: None,
            nesting_depth: 0,
            llm: std::sync::Arc::new(crate::llm::NullLlmProvider),
            tools: std::sync::Arc::new(crate::tools::ToolRegistry::new()),
            permissions: vec![],
            formatters: std::collections::HashMap::new(),
        }
    }
    #[tokio::test]
    async fn test_file_write_creates_file() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("output.txt");
        let ctx = make_ctx(dir.path());

        let tool = FileWriteTool;
        let result = tool
            .execute(
                serde_json::json!({
                    "path": path.to_string_lossy().to_string(),
                    "content": "hello world\n"
                }),
                &ctx,
            )
            .await
            .unwrap();

        assert!(!result.is_error);
        assert!(result.output.contains("bytes"));
        let written = std::fs::read_to_string(&path).unwrap();
        assert_eq!(written, "hello world\n");
    }

    #[tokio::test]
    async fn test_file_write_creates_parent_dirs() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("a").join("b").join("c.txt");
        let ctx = make_ctx(dir.path());

        let tool = FileWriteTool;
        let result = tool
            .execute(
                serde_json::json!({
                    "path": path.to_string_lossy().to_string(),
                    "content": "nested content"
                }),
                &ctx,
            )
            .await
            .unwrap();

        assert!(!result.is_error);
        assert!(path.exists());
        let written = std::fs::read_to_string(&path).unwrap();
        assert_eq!(written, "nested content");
    }
}