use async_trait::async_trait;
use std::path::Path;
use crate::ast::Value;
use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode};
use crate::tools::{ExecContext, ParamSchema, Tool, ToolArgs, ToolSchema};
pub struct Stat;
#[async_trait]
impl Tool for Stat {
fn name(&self) -> &str {
"stat"
}
fn schema(&self) -> ToolSchema {
ToolSchema::new("stat", "Display file status")
.param(ParamSchema::required("path", "string", "File to stat"))
.param(ParamSchema::optional(
"format",
"string",
Value::Null,
"Output format (--format). Supports: %n (name), %s (size), %F (type)",
))
.example("Show file info", "stat README.md")
.example("Just the size", "stat --format '%s' file.txt")
}
async fn execute(&self, args: ToolArgs, ctx: &mut ExecContext) -> ExecResult {
let path_str = match args.get_string("path", 0) {
Some(p) => p,
None => return ExecResult::failure(1, "stat: missing path argument"),
};
let resolved = ctx.resolve_path(&path_str);
let format = args
.get_string("format", usize::MAX)
.or_else(|| args.get_string("c", usize::MAX));
match ctx.backend.stat(Path::new(&resolved)).await {
Ok(info) => {
if let Some(fmt) = format {
let output = format_stat(&fmt, &path_str, &info);
ExecResult::with_output(OutputData::text(output))
} else {
let is_dir = info.is_dir();
let file_type = if is_dir {
"directory"
} else {
"regular file"
};
let entry_type = if is_dir {
EntryType::Directory
} else {
EntryType::File
};
let node = OutputNode::new(&path_str)
.with_cells(vec![
info.size.to_string(),
file_type.to_string(),
])
.with_entry_type(entry_type);
let headers = vec![
"FILE".to_string(),
"SIZE".to_string(),
"TYPE".to_string(),
];
ExecResult::with_output(OutputData::table(headers, vec![node]))
}
}
Err(e) => ExecResult::failure(1, format!("stat: {}: {}", path_str, e)),
}
}
}
fn format_stat(fmt: &str, name: &str, info: &crate::vfs::DirEntry) -> String {
let mut result = fmt.to_string();
result = result.replace("%n", name);
result = result.replace("%s", &info.size.to_string());
let file_type = if info.is_dir() { "directory" } else { "regular file" };
result = result.replace("%F", file_type);
if !result.ends_with('\n') {
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;
async fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
let mem = MemoryFs::new();
mem.write(Path::new("file.txt"), b"hello world")
.await
.unwrap();
mem.mkdir(Path::new("mydir")).await.unwrap();
vfs.mount("/", mem);
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_stat_file() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/file.txt".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.has_output());
let output = result.output().unwrap();
assert!(output.headers.is_some());
assert_eq!(output.root.len(), 1);
assert!(result.text_out().contains("file.txt"));
assert!(result.text_out().contains("11")); }
#[tokio::test]
async fn test_stat_directory() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/mydir".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.has_output());
let output = result.output().unwrap();
assert_eq!(output.root.len(), 1);
assert!(result.text_out().contains("directory"));
}
#[tokio::test]
async fn test_stat_format() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/file.txt".into()));
args.named
.insert("format".to_string(), Value::String("%n: %s bytes".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().trim(), "/file.txt: 11 bytes");
}
#[tokio::test]
async fn test_stat_not_found() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/nonexistent".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(!result.ok());
}
#[tokio::test]
async fn test_stat_missing_path() {
let mut ctx = make_ctx().await;
let result = Stat.execute(ToolArgs::new(), &mut ctx).await;
assert!(!result.ok());
}
}