use super::FileOpsTool;
use crate::tools::error_helpers::{deserialize_tool_args, with_file_context, with_path_context};
use crate::tools::traits::FileTool;
use crate::tools::types::{CopyInput, CreateInput, DeleteInput, MoveInput};
use crate::utils::file_utils::ensure_dir_exists;
use anyhow::{Context, Result, anyhow};
use serde_json::{Value, json};
use tracing::info;
use vtcode_commons::canonicalize_async;
impl FileOpsTool {
async fn validate_move_copy_args(
&self,
path: &str,
destination: &str,
operation: &str,
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
let from_path = self.normalize_and_validate_user_path(path).await?;
let to_path = self.normalize_and_validate_user_path(destination).await?;
if self.should_exclude(&from_path).await {
return Err(anyhow!("Error: Path '{path}' is excluded by .vtcodegitignore and cannot be {operation}."));
}
if self.should_exclude(&to_path).await {
return Err(anyhow!("Error: Destination '{destination}' is excluded by .vtcodegitignore."));
}
if !tokio::fs::try_exists(&from_path).await? {
return Err(anyhow!("Source path '{path}' does not exist"));
}
if let Some(parent) = to_path.parent() {
ensure_dir_exists(parent).await?;
}
Ok((from_path, to_path))
}
pub async fn create_file(&self, args: Value) -> Result<Value> {
let input: CreateInput = deserialize_tool_args(&args, "create_file")?;
let CreateInput { path, content, encoding } = input;
let file_path = self.normalize_and_validate_user_path(&path).await?;
if self.should_exclude(&file_path).await {
return Err(anyhow!("Error: Path '{path}' is excluded by .vtcodegitignore"));
}
if tokio::fs::try_exists(&file_path).await? {
return Err(anyhow!(
"Error: File '{path}' already exists. Use write_file with mode='overwrite' to replace it."
));
}
if let Some(parent) = file_path.parent() {
ensure_dir_exists(parent).await?;
}
let mut payload = json!({
"path": path,
"content": content,
"mode": "fail_if_exists"
});
if let Some(encoding) = encoding {
payload["encoding"] = Value::String(encoding);
}
let mut result = self.write_file(payload).await?;
if let Some(map) = result.as_object_mut() {
map.insert("created".to_string(), Value::Bool(true));
}
Ok(result)
}
pub async fn delete_file(&self, args: Value) -> Result<Value> {
let input: DeleteInput = deserialize_tool_args(&args, "delete_file")?;
let DeleteInput { path, recursive, force } = input;
let target_path = self.workspace_root.join(&path);
let exists = with_path_context(tokio::fs::try_exists(&target_path).await, "check if exists", &path)?;
if !exists {
if force {
return Ok(json!({
"success": true,
"deleted": false,
"skipped": true,
"reason": "not_found",
"path": path,
}));
}
return Err(anyhow!("Error: Path '{path}' does not exist. Provide force=true to ignore missing files."));
}
let canonical = with_path_context(canonicalize_async(&target_path).await, "resolve canonical path for", &path)?;
if !canonical.starts_with(self.canonical_workspace_root()) {
return Err(anyhow!("Error: Path '{path}' resolves outside the workspace and cannot be deleted."));
}
if self.should_exclude(&canonical).await {
return Err(anyhow!("Error: Path '{path}' is excluded by .vtcodegitignore and cannot be deleted."));
}
let metadata = with_path_context(tokio::fs::metadata(&canonical).await, "read metadata for", &path)?;
let deleted_kind = if metadata.is_dir() {
if !recursive {
return Err(anyhow!("Error: '{path}' is a directory. Pass recursive=true to remove directories."));
}
with_path_context(tokio::fs::remove_dir_all(&canonical).await, "remove directory", &path)?;
"directory"
} else {
with_path_context(tokio::fs::remove_file(&canonical).await, "remove file", &path)?;
"file"
};
Ok(json!({
"success": true,
"deleted": true,
"path": self.workspace_relative_display(&canonical),
"kind": deleted_kind,
}))
}
pub async fn move_file(&self, args: Value) -> Result<Value> {
let input: MoveInput = deserialize_tool_args(&args, "move_file")?;
let MoveInput { path, destination, force } = input;
let (from_path, to_path) = self.validate_move_copy_args(&path, &destination, "moved").await?;
if tokio::fs::try_exists(&to_path).await? && !force {
return Err(anyhow!("Destination path '{destination}' already exists. Use force=true to overwrite."));
}
tokio::fs::rename(&from_path, &to_path)
.await
.with_context(|| format!("Failed to move '{path}' to '{destination}'"))?;
info!(from = %path, to = %destination, "Moved successfully");
Ok(json!({
"success": true,
"from": self.workspace_relative_display(&from_path),
"to": self.workspace_relative_display(&to_path),
}))
}
pub async fn copy_file(&self, args: Value) -> Result<Value> {
let input: CopyInput = deserialize_tool_args(&args, "copy_file")?;
let CopyInput { path, destination, recursive } = input;
let (from_path, to_path) = self.validate_move_copy_args(&path, &destination, "copied").await?;
let metadata = tokio::fs::metadata(&from_path).await?;
if metadata.is_dir() {
if !recursive {
return Err(anyhow!("Path '{path}' is a directory. Use recursive=true to copy directories."));
}
use vtcode_commons::walk::build_walker_single_threaded;
for entry in build_walker_single_threaded(&from_path).build().filter_map(|e| e.ok()) {
let entry_path = entry.path();
let relative = entry_path.strip_prefix(&from_path).unwrap_or(entry_path);
let target = to_path.join(relative);
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
ensure_dir_exists(&target).await?;
} else {
with_file_context(tokio::fs::copy(entry_path, &target).await, "copy", entry_path)?;
}
}
} else {
tokio::fs::copy(&from_path, &to_path)
.await
.with_context(|| format!("Failed to copy '{path}' to '{destination}'"))?;
}
info!(from = %path, to = %destination, "Copied successfully");
Ok(json!({
"success": true,
"from": self.workspace_relative_display(&from_path),
"to": self.workspace_relative_display(&to_path),
}))
}
}