use leo_errors::Result;
use colored::Colorize;
use std::{
convert::Infallible,
ffi::OsString,
path::{Path, PathBuf},
process,
};
const PLUGIN_PREFIX: &str = "leo-";
pub fn find_exe(name: &str) -> Option<PathBuf> {
let filename = format!("{name}{}", std::env::consts::EXE_SUFFIX);
let var = std::env::var_os("PATH")?;
std::env::split_paths(&var).find_map(|dir| {
let candidate = dir.join(&filename);
if is_executable(&candidate) { Some(candidate) } else { None }
})
}
pub fn exec(name: &str, args: &[OsString], cwd: Option<&Path>) -> Result<Infallible> {
let path = find_exe(name).ok_or_else(|| -> leo_errors::LeoError {
crate::errors::custom(format!(
"'{name}' not found. Install the plugin and ensure it is available on your PATH."
))
.into()
})?;
let mut cmd = process::Command::new(&path);
cmd.args(args);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = cmd.exec();
Err(crate::errors::custom(format!("failed to exec '{name}': {err}")).into())
}
#[cfg(not(unix))]
{
let status = cmd
.stdin(process::Stdio::inherit())
.stdout(process::Stdio::inherit())
.stderr(process::Stdio::inherit())
.status()
.map_err(|err| -> leo_errors::LeoError {
crate::errors::custom(format!("failed to spawn '{name}': {err}")).into()
})?;
process::exit(status.code().unwrap_or(1));
}
}
pub fn all() -> Vec<(String, PathBuf)> {
let Some(var) = std::env::var_os("PATH") else {
return Vec::new();
};
let mut seen = std::collections::HashSet::new();
let mut plugins = Vec::new();
for dir in std::env::split_paths(&var) {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let stem = name.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(name);
if let Some(subcmd) = stem.strip_prefix(PLUGIN_PREFIX)
&& !subcmd.is_empty()
&& is_executable(&path)
&& seen.insert(subcmd.to_string())
{
plugins.push((subcmd.to_string(), path));
}
}
}
plugins.sort_by(|(a, _), (b, _)| a.cmp(b));
plugins
}
pub fn print_all() {
let plugins = all();
if plugins.is_empty() {
println!("No leo plugins detected on PATH.");
return;
}
println!("Installed plugins:\n");
let name_col_w = plugins.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);
for (cmd, path) in &plugins {
println!(" {:<name_col_w$} {}", cmd.bold(), path.display());
}
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.is_file() && std::fs::metadata(path).map(|m| m.permissions().mode() & 0o111 != 0).unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.is_file()
}