1use leo_errors::Result;
18
19use colored::Colorize;
20use std::{
21 convert::Infallible,
22 ffi::OsString,
23 path::{Path, PathBuf},
24 process,
25};
26
27const PLUGIN_PREFIX: &str = "leo-";
28
29pub fn find_exe(name: &str) -> Option<PathBuf> {
31 let filename = format!("{name}{}", std::env::consts::EXE_SUFFIX);
32 let var = std::env::var_os("PATH")?;
33 std::env::split_paths(&var).find_map(|dir| {
34 let candidate = dir.join(&filename);
35 if is_executable(&candidate) { Some(candidate) } else { None }
36 })
37}
38
39pub fn exec(name: &str, args: &[OsString], cwd: Option<&Path>) -> Result<Infallible> {
45 let path = find_exe(name).ok_or_else(|| -> leo_errors::LeoError {
46 crate::errors::custom(format!(
47 "'{name}' not found. Install the plugin and ensure it is available on your PATH."
48 ))
49 .into()
50 })?;
51
52 let mut cmd = process::Command::new(&path);
53 cmd.args(args);
54 if let Some(dir) = cwd {
55 cmd.current_dir(dir);
56 }
57
58 #[cfg(unix)]
59 {
60 use std::os::unix::process::CommandExt;
61 let err = cmd.exec();
63 Err(crate::errors::custom(format!("failed to exec '{name}': {err}")).into())
64 }
65
66 #[cfg(not(unix))]
67 {
68 let status = cmd
69 .stdin(process::Stdio::inherit())
70 .stdout(process::Stdio::inherit())
71 .stderr(process::Stdio::inherit())
72 .status()
73 .map_err(|err| -> leo_errors::LeoError {
74 crate::errors::custom(format!("failed to spawn '{name}': {err}")).into()
75 })?;
76 process::exit(status.code().unwrap_or(1));
77 }
78}
79
80pub fn all() -> Vec<(String, PathBuf)> {
85 let Some(var) = std::env::var_os("PATH") else {
86 return Vec::new();
87 };
88 let mut seen = std::collections::HashSet::new();
89 let mut plugins = Vec::new();
90 for dir in std::env::split_paths(&var) {
91 let Ok(entries) = std::fs::read_dir(&dir) else {
92 continue;
93 };
94 for entry in entries.filter_map(|e| e.ok()) {
95 let path = entry.path();
96 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
97 continue;
98 };
99 let stem = name.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(name);
100 if let Some(subcmd) = stem.strip_prefix(PLUGIN_PREFIX)
101 && !subcmd.is_empty()
102 && is_executable(&path)
103 && seen.insert(subcmd.to_string())
104 {
105 plugins.push((subcmd.to_string(), path));
106 }
107 }
108 }
109 plugins.sort_by(|(a, _), (b, _)| a.cmp(b));
110 plugins
111}
112
113pub fn print_all() {
115 let plugins = all();
116 if plugins.is_empty() {
117 println!("No leo plugins detected on PATH.");
118 return;
119 }
120 println!("Installed plugins:\n");
121 let name_col_w = plugins.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);
122 for (cmd, path) in &plugins {
123 println!(" {:<name_col_w$} {}", cmd.bold(), path.display());
124 }
125}
126
127#[cfg(unix)]
128fn is_executable(path: &Path) -> bool {
129 use std::os::unix::fs::PermissionsExt;
130 path.is_file() && std::fs::metadata(path).map(|m| m.permissions().mode() & 0o111 != 0).unwrap_or(false)
131}
132
133#[cfg(not(unix))]
134fn is_executable(path: &Path) -> bool {
135 path.is_file()
136}