use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub fn home_dir() -> Option<PathBuf> {
let var = |name: &str| std::env::var_os(name).filter(|value| !value.is_empty()).map(PathBuf::from);
var("HOME").or_else(|| if cfg!(windows) { var("USERPROFILE") } else { None })
}
pub fn exe_name(program: &str) -> String {
if cfg!(windows) && Path::new(program).extension().is_none() { format!("{program}.exe") } else { program.into() }
}
fn nvm_bins(home: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(home.join(".nvm/versions/node")) else { return Vec::new() };
let mut versions: Vec<PathBuf> = entries.flatten().map(|entry| entry.path().join("bin")).collect();
versions.sort_unstable_by(|a, b| b.cmp(a));
versions
}
fn under_env(name: &str, relative: &str) -> Option<PathBuf> {
std::env::var_os(name).map(|base| Path::new(&base).join(relative))
}
fn usual_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(home) = home_dir() {
let relative = [".local/bin", ".cargo/bin", ".bun/bin", ".npm-global/bin", ".volta/bin", "bin"];
dirs.extend(relative.map(|path| home.join(path)));
dirs.extend(nvm_bins(&home));
}
if cfg!(windows) {
dirs.extend(under_env("APPDATA", "npm"));
dirs.extend(under_env("ProgramFiles", "nodejs"));
dirs.extend(under_env("LOCALAPPDATA", "Microsoft/WinGet/Links"));
} else if cfg!(target_os = "macos") {
dirs.extend(["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"].map(PathBuf::from));
} else {
dirs.extend(["/usr/local/bin", "/usr/bin", "/snap/bin", "/home/linuxbrew/.linuxbrew/bin"].map(PathBuf::from));
}
dirs
}
pub fn tool_dirs() -> Vec<PathBuf> {
let inherited: Vec<PathBuf> =
std::env::var_os("PATH").map(|path| std::env::split_paths(&path).collect()).unwrap_or_default();
let mut dirs: Vec<PathBuf> = Vec::new();
for dir in inherited.into_iter().chain(usual_dirs()) {
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
dirs
}
fn candidates(command: &str) -> Vec<String> {
if !cfg!(windows) || Path::new(command).extension().is_some() {
return vec![command.into()];
}
["exe", "cmd", "bat"].iter().map(|extension| format!("{command}.{extension}")).collect()
}
pub fn find_program(command: &str) -> PathBuf {
if command.contains(['/', '\\']) {
return PathBuf::from(command);
}
let names = candidates(command);
tool_dirs()
.iter()
.flat_map(|dir| names.iter().map(move |name| dir.join(name)))
.find(|path| path.is_file())
.unwrap_or_else(|| command.into())
}
pub fn path_for_child() -> OsString {
std::env::join_paths(tool_dirs()).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn una_ruta_explicita_no_se_busca() {
assert_eq!(find_program("/opt/mi/agente"), PathBuf::from("/opt/mi/agente"));
assert_eq!(find_program(r"C:\mi\agente"), PathBuf::from(r"C:\mi\agente"));
}
#[test]
fn un_programa_inexistente_se_devuelve_tal_cual() {
assert_eq!(find_program("no-existe-aura-xyz"), PathBuf::from("no-existe-aura-xyz"));
}
#[test]
fn un_programa_del_cargo_se_resuelve_a_ruta_completa() {
let cargo = find_program("cargo");
assert!(cargo.is_absolute() && cargo.is_file(), "{cargo:?}");
}
#[test]
fn los_directorios_no_se_repiten() {
let dirs = tool_dirs();
let mut sorted = dirs.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), dirs.len());
assert!(!path_for_child().is_empty());
}
#[test]
fn el_nombre_del_ejecutable_lleva_exe_solo_en_windows() {
assert_eq!(exe_name("tsgo"), if cfg!(windows) { "tsgo.exe" } else { "tsgo" });
assert_eq!(exe_name("a.cmd"), "a.cmd");
}
}