use std::path::{Path, PathBuf};
use std::sync::Arc;
pub const SANDBOX_PATH: [&str; 3] = ["/usr/local/bin", "/usr/bin", "/bin"];
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AgentRuntime {
pub read_paths: Vec<String>,
pub path_entries: Vec<String>,
}
pub type Lookup = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
fn is_executable(path: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.is_ok_and(|info| info.is_file() && (info.permissions().mode() & 0o111) != 0)
}
pub fn which(name: &str) -> Option<String> {
which_on_path(name, &std::env::var("PATH").unwrap_or_default())
}
pub fn which_on_path(name: &str, path: &str) -> Option<String> {
for directory in path.split(':') {
if directory.is_empty() {
continue;
}
let candidate = Path::new(directory)
.join(name)
.to_string_lossy()
.into_owned();
if is_executable(&candidate) {
return Some(candidate);
}
}
None
}
fn install_root(directory: &Path) -> Option<PathBuf> {
let mut current = directory.to_path_buf();
loop {
if current
.file_name()
.is_some_and(|name| name == "node_modules")
{
return Some(current);
}
if !current.pop() {
return None;
}
}
}
fn package_root(file: &Path) -> Option<PathBuf> {
let mut directory = file.parent()?.to_path_buf();
loop {
if directory.join("package.json").exists() {
return Some(directory);
}
if !directory.pop() {
return None;
}
}
}
fn add(into: &mut Vec<String>, value: Option<String>) {
if let Some(value) = value
&& !into.contains(&value)
{
into.push(value);
}
}
fn real_path(path: &str) -> String {
std::fs::canonicalize(path).map_or_else(
|_| path.to_owned(),
|resolved| resolved.to_string_lossy().into_owned(),
)
}
pub fn agent_runtime(lookup: &Lookup) -> Option<AgentRuntime> {
let launcher = lookup("pi")?;
let mut runtime = AgentRuntime::default();
add(&mut runtime.path_entries, Some(parent_of(&launcher)));
add(&mut runtime.read_paths, Some(parent_of(&launcher)));
let real = real_path(&launcher);
let real_file = PathBuf::from(&real);
let root = package_root(&real_file).unwrap_or_else(|| {
real_file
.parent()
.map(Path::to_path_buf)
.unwrap_or_default()
});
add(
&mut runtime.read_paths,
Some(root.to_string_lossy().into_owned()),
);
if let Some(installed) = install_root(&root) {
add(
&mut runtime.read_paths,
Some(installed.to_string_lossy().into_owned()),
);
}
if let Some(node) = lookup("node") {
let interpreter = parent_of(&real_path(&node));
add(&mut runtime.path_entries, Some(interpreter.clone()));
add(&mut runtime.read_paths, Some(interpreter));
}
Some(runtime)
}
fn parent_of(path: &str) -> String {
Path::new(path).parent().map_or_else(
|| path.to_owned(),
|parent| parent.to_string_lossy().into_owned(),
)
}
#[cfg(test)]
mod tests;