use crate::error::{CliError, CliResult};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
const PREFIX: &str = "armature-";
const RESERVED: &[&str] = &["armature-cli"];
pub fn discover_external_commands() -> BTreeMap<String, PathBuf> {
let mut found = BTreeMap::new();
let Some(path_var) = std::env::var_os("PATH") else {
return found;
};
for dir in std::env::split_paths(&path_var) {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(file_name) = file_name.to_str() else {
continue;
};
let stem = file_name.strip_suffix(".exe").unwrap_or(file_name);
let Some(subcommand_name) = stem.strip_prefix(PREFIX) else {
continue;
};
if subcommand_name.is_empty() || RESERVED.contains(&stem) {
continue;
}
let full_path = entry.path();
if !is_executable(&full_path) {
continue;
}
found
.entry(subcommand_name.to_string())
.or_insert(full_path);
}
}
found
}
fn is_executable(path: &Path) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
pub fn run_external_command(name: &str, args: &[String]) -> CliResult<i32> {
let discovered = discover_external_commands();
let Some(binary_path) = discovered.get(name) else {
let suggestion = if discovered.is_empty() {
"No armature-* extension binaries were found on $PATH.".to_string()
} else {
let names: Vec<&str> = discovered.keys().map(String::as_str).collect();
format!(
"No binary named '{PREFIX}{name}' was found on $PATH. Available extensions: {}",
names.join(", ")
)
};
return Err(CliError::Tool(format!(
"unknown command '{name}'. {suggestion}"
)));
};
let status = Command::new(binary_path)
.args(args)
.status()
.map_err(|e| CliError::Tool(format!("failed to launch {}: {e}", binary_path.display())))?;
Ok(status.code().unwrap_or(128))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_external_commands_never_includes_the_cli_itself() {
let discovered = discover_external_commands();
assert!(!discovered.contains_key("cli"));
}
#[test]
fn run_external_command_reports_a_helpful_error_for_an_unknown_name() {
let result = run_external_command("this-definitely-does-not-exist-as-a-binary", &[]);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("this-definitely-does-not-exist-as-a-binary"));
}
#[test]
fn is_executable_rejects_directories() {
assert!(!is_executable(Path::new(".")));
}
}