use anyhow::{Context, Result};
use async_trait::async_trait;
use std::process::Stdio;
use tokio::process::Command;
use super::{PackageManager, PackageManagerType};
pub struct PathManager;
impl PathManager {
pub fn new() -> Self {
Self
}
pub async fn which(&self, binary: &str) -> Result<Option<String>> {
let output = Command::new("which")
.arg(binary)
.output()
.await
.context("Failed to run which command")?;
if output.status.success() && !output.stdout.is_empty() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(Some(path))
} else {
Ok(None)
}
}
pub async fn get_version_output(
&self,
binary: &str,
args: &[String],
) -> Result<Option<String>> {
if self.which(binary).await?.is_none() {
return Ok(None);
}
let mut cmd = Command::new(binary);
for arg in args {
cmd.arg(arg);
}
let output = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await;
match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let version_output = if !stdout.trim().is_empty() {
stdout.to_string()
} else {
stderr.to_string()
};
Ok(Some(version_output.trim().to_string()))
}
_ => Ok(None),
}
}
pub fn extract_version(&self, output: &str, pattern: Option<&str>) -> Option<String> {
if let Some(_pattern) = pattern {
}
let lines = output.lines();
for line in lines {
for word in line.split_whitespace() {
let cleaned = word
.trim_start_matches('v')
.trim_start_matches("version")
.trim_start_matches("Version");
if cleaned.chars().next().is_some_and(|c| c.is_numeric()) && cleaned.contains('.') {
return Some(cleaned.to_string());
}
}
}
None
}
}
#[async_trait]
impl PackageManager for PathManager {
fn manager_type(&self) -> PackageManagerType {
unreachable!("PathManager should not be used as a regular PackageManager")
}
async fn is_available(&self) -> bool {
true
}
async fn check_installed(&self, package: &str) -> Result<bool> {
Ok(self.which(package).await?.is_some())
}
async fn install(&self, _package: &str, _args: &[String]) -> Result<()> {
anyhow::bail!("PathManager cannot install packages")
}
async fn get_version(&self, package: &str) -> Result<Option<String>> {
let version_flags = vec![
vec!["--version"],
vec!["-version"],
vec!["-v"],
vec!["version"],
];
for flags in version_flags {
if let Some(output) = self
.get_version_output(
package,
&flags.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
)
.await?
{
if let Some(version) = self.extract_version(&output, None) {
return Ok(Some(version));
}
}
}
Ok(None)
}
}