use serde_json::Value;
use std::path::Path;
use std::process::Command;
pub fn execute_tool(tool_name: &str, input: &Value, project_path: &Path) -> String {
let km_binary = std::env::current_exe().unwrap_or_else(|_| "km".into());
let (subcmd, args) = match tool_name {
"km_loc" => ("loc", build_args(input, &[], project_path)),
"km_score" => ("score", build_args(input, &[], project_path)),
"km_hal" => ("hal", build_args(input, &["top"], project_path)),
"km_cycom" => ("cycom", build_args(input, &["top"], project_path)),
"km_indent" => ("indent", build_args(input, &[], project_path)),
"km_mi" => ("mi", build_args(input, &["top"], project_path)),
"km_miv" => ("miv", build_args(input, &["top"], project_path)),
"km_dups" => ("dups", build_args(input, &["min_lines"], project_path)),
"km_hotspots" => (
"hotspots",
build_args(input, &["top", "since"], project_path),
),
"km_knowledge" => (
"knowledge",
build_args(input, &["top", "since"], project_path),
),
"km_tc" => ("tc", build_args(input, &["top", "since"], project_path)),
_ => return format!("Unknown tool: {tool_name}"),
};
let mut cmd = Command::new(&km_binary);
cmd.arg(subcmd).arg("--json");
for arg in &args {
cmd.arg(arg);
}
match cmd.output() {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.success() {
stdout.into_owned()
} else {
format!("Error running km {subcmd}: {stderr}")
}
}
Err(e) => format!("Failed to execute km {subcmd}: {e}"),
}
}
fn resolve_path(input: &Value, project_path: &Path) -> String {
let raw = input.get("path").and_then(|v| v.as_str()).unwrap_or("");
if raw.is_empty() {
return project_path.to_string_lossy().into_owned();
}
let candidate = if Path::new(raw).is_absolute() {
Path::new(raw).to_path_buf()
} else {
project_path.join(raw)
};
let resolved = match candidate.canonicalize() {
Ok(p) => p,
Err(_) => return project_path.to_string_lossy().into_owned(),
};
if !resolved.starts_with(project_path) {
eprintln!(
" Warning: path '{}' is outside project root, using project root instead",
raw
);
return project_path.to_string_lossy().into_owned();
}
resolved.to_string_lossy().into_owned()
}
fn build_args(input: &Value, named: &[&str], project_path: &Path) -> Vec<String> {
let mut args = Vec::new();
for name in named {
if let Some(val) = input.get(name) {
let flag = format!("--{}", name.replace('_', "-"));
match val {
Value::Number(n) => {
args.push(flag);
args.push(n.to_string());
}
Value::String(s) => {
args.push(flag);
args.push(s.clone());
}
_ => {
eprintln!(" Warning: ignoring invalid value for --{name}: {val}");
}
}
}
}
args.push(resolve_path(input, project_path));
args
}
#[cfg(test)]
#[path = "tools_test.rs"]
mod tests;