use std::path::Path;
use std::process::Command;
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
#[allow(dead_code)]
fn description(&self) -> &str;
fn run(&self, args: &serde_json::Value, work_dir: &Path) -> Result<String, String>;
}
#[derive(Default)]
pub struct ToolRegistry {
tools: Vec<Box<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self { tools: Vec::new() }
}
pub fn register(&mut self, tool: Box<dyn Tool>) {
self.tools.push(tool);
}
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.tools
.iter()
.find(|t| t.name() == name)
.map(|t| t.as_ref())
}
pub fn tool_names(&self) -> Vec<String> {
self.tools.iter().map(|t| t.name().to_string()).collect()
}
pub fn run_tool(
&self,
name: &str,
args: &serde_json::Value,
work_dir: &Path,
) -> Option<Result<String, String>> {
let t = self.get(name)?;
Some(t.run(args, work_dir))
}
pub fn run_auto(&self, enabled: &[String], work_dir: &Path) -> String {
let mut out = String::new();
for name in enabled {
let args = serde_json::json!({});
match self.run_tool(name, &args, work_dir) {
Some(Ok(s)) => out.push_str(&format!("=== {} ===\n{}\n", name, s.trim())),
Some(Err(e)) => out.push_str(&format!("=== {} ===\n(error: {})\n", name, e)),
None => out.push_str(&format!("=== {} (unknown) ===\n(skipped)\n", name)),
}
}
out
}
}
pub struct LsTool;
impl Tool for LsTool {
fn name(&self) -> &str {
"ls"
}
fn description(&self) -> &str {
"list directory; optional args: path (or dir) = directory to list"
}
fn run(&self, args: &serde_json::Value, work_dir: &Path) -> Result<String, String> {
let path = args
.get("path")
.or_else(|| args.get("dir"))
.and_then(|v| v.as_str())
.unwrap_or(".");
let target = if path == "." || path.is_empty() {
work_dir.to_path_buf()
} else {
let p = Path::new(path);
if p.is_absolute() {
p.to_path_buf()
} else {
work_dir.join(p)
}
};
let result = Command::new("ls")
.args(["-la"])
.current_dir(&target)
.output();
match result {
Ok(o) if o.status.success() => {
Ok(String::from_utf8_lossy(&o.stdout).trim().to_string())
}
Ok(o) => Err(format!(
"exit {:?}: {}",
o.status,
String::from_utf8_lossy(&o.stderr).trim()
)),
Err(e) => Err(format!("{}", e)),
}
}
}
pub struct PwdTool;
impl Tool for PwdTool {
fn name(&self) -> &str {
"pwd"
}
fn description(&self) -> &str {
"print working directory"
}
fn run(&self, _args: &serde_json::Value, work_dir: &Path) -> Result<String, String> {
Ok(work_dir.display().to_string())
}
}
pub struct PythonTool;
impl Tool for PythonTool {
fn name(&self) -> &str {
"python"
}
fn description(&self) -> &str {
"run a Python script; args: script (or path) = path to .py file"
}
fn run(&self, args: &serde_json::Value, work_dir: &Path) -> Result<String, String> {
let script = args
.get("script")
.or_else(|| args.get("path"))
.and_then(|v| v.as_str())
.ok_or_else(|| "missing script (or path)".to_string())?;
let script = script.trim();
if script.is_empty() {
return Err("script path is empty".to_string());
}
let script_path = Path::new(script);
let full = if script_path.is_absolute() {
script_path.to_path_buf()
} else {
work_dir.join(script_path)
};
if !full.exists() {
return Err(format!("script not found: {}", full.display()));
}
if full.extension().map(|e| e != "py").unwrap_or(true) {
return Err("only .py files are allowed".to_string());
}
let result = Command::new("python3")
.arg(&full)
.current_dir(work_dir)
.output();
match result {
Ok(o) if o.status.success() => {
let out = String::from_utf8_lossy(&o.stdout);
let err = String::from_utf8_lossy(&o.stderr);
let mut s = out.trim().to_string();
if !err.trim().is_empty() {
s.push_str("\n(stderr)\n");
s.push_str(err.trim());
}
Ok(s)
}
Ok(o) => Err(format!(
"exit {:?}: {}",
o.status,
String::from_utf8_lossy(&o.stderr).trim()
)),
Err(e) => Err(format!("{}", e)),
}
}
}
pub fn default_registry() -> ToolRegistry {
let mut r = ToolRegistry::new();
r.register(Box::new(LsTool));
r.register(Box::new(PwdTool));
r.register(Box::new(PythonTool));
r
}
pub fn enabled_auto_commands() -> Vec<String> {
let s = std::env::var("ZEBTUNE_AUTO_COMMANDS").unwrap_or_else(|_| "ls,pwd".into());
s.split(',')
.map(|x| x.trim().to_string())
.filter(|x| !x.is_empty())
.collect()
}