diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7b1ed04..e42ffee 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,10 +1,6 @@ name: Package and Publish on: - push: - branches: - - master - - release/* workflow_dispatch: permissions: diff --git a/Cargo.lock b/Cargo.lock index 3e30163..c2d36e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,7 +461,7 @@ dependencies = [ [[package]] name = "needs" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "atty", diff --git a/Cargo.toml b/Cargo.toml index e978239..146b432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "needs" description = "Check if given bin(s) are available in the PATH" -version = "0.6.0" +version = "0.7.0" edition = "2024" authors = ["Noah "] license = "GPL-3.0-or-later" diff --git a/justfile b/justfile index 1d8ae83..f7c48c8 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,7 @@ release_build := "./target/release/needs" @_default: just --list - needs gum freeze hr + needs gum freeze hr agg @gif: agg demo.cast --font-family "JetBrainsMono Nerd Font Mono" --speed 2 demo.gif diff --git a/src/binary.rs b/src/binary.rs index 02a64f3..126d48e 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -9,6 +9,7 @@ pub struct Binary<'a> { pub name: Cow<'a, str>, // TODO: use a custom version type pub version: Option, + pub package_manager: Option, } impl<'a> Binary<'a> { @@ -16,6 +17,15 @@ impl<'a> Binary<'a> { Self { name, version: None, + package_manager: None, + } + } + + pub fn new_with_package_manager(name: Cow<'a, str>, package_manager: Option) -> Self { + Self { + name, + version: None, + package_manager, } } } @@ -25,6 +35,7 @@ impl Default for Binary<'_> { Self { name: Cow::borrowed(""), version: Some(unknown_version()), + package_manager: None, } } } @@ -32,14 +43,18 @@ impl Default for Binary<'_> { impl Display for Binary<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.version.is_none() { - write!(f, "{} ?", self.name) + if let Some(ref pm) = self.package_manager { + write!(f, "{} ? ({})", self.name, pm) + } else { + write!(f, "{} ?", self.name) + } } else { - write!( - f, - "{} {}", - self.name, - format_version(self.version.as_ref().unwrap(), false) - ) + let version_str = format_version(self.version.as_ref().unwrap(), false); + if let Some(ref pm) = self.package_manager { + write!(f, "{} {} ({})", self.name, version_str, pm) + } else { + write!(f, "{} {}", self.name, version_str) + } } } } diff --git a/src/discovery.rs b/src/discovery.rs index 1afbf37..59cb70c 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -2,6 +2,97 @@ use crate::binary::Binary; use crate::error::DiscoveryError; use log::{info, warn}; use miette::Result; +use std::path::Path; + +/// Detect which package manager is responsible for managing a binary based on its path +fn detect_package_manager(binary_path: &Path) -> Option { + let path_str = binary_path.to_string_lossy(); + let path_str_lower = path_str.to_lowercase(); + + // Check if it's a cargo binary specifically + if path_str_lower.contains("/.cargo/bin/") { + return Some("cargo".to_string()); + } + // Check if it's other rustup toolchain binaries + if path_str_lower.contains("/.rustup/") { + return Some("rustup".to_string()); + } + + // Check for Homebrew (macOS/Linux) + if path_str_lower.starts_with("/opt/homebrew/") + || path_str_lower.starts_with("/usr/local/cellar/") + || (path_str_lower.contains("/usr/local/") && path_str_lower.contains("homebrew")) + { + return Some("homebrew".to_string()); + } + + // Check for npm global installs + if path_str_lower.contains("/node_modules/.bin/") + || path_str_lower.contains("/npm/") + || path_str_lower.contains("/.npm/") + || path_str_lower.contains("/npm-global/") + { + return Some("npm".to_string()); + } + + // Check for Go binaries + if path_str_lower.contains("/go/bin/") || path_str_lower.contains("/gopath/bin/") { + return Some("go".to_string()); + } + + // Check for Python pip/pipx installs + if path_str_lower.contains("/.local/bin/") + || path_str_lower.contains("/python") + || path_str_lower.contains("/pip/") + || path_str_lower.contains("/pipx/") + { + return Some("pip".to_string()); + } + + // DEV: adding ? to these because i havnt tested them yet + // Check for snap packages + if path_str_lower.contains("/snap/") { + return Some("snap?".to_string()); + } + + // Check for flatpak + if path_str_lower.contains("/flatpak/") { + return Some("flatpak?".to_string()); + } + + // Check for AppImage + if path_str_lower.contains("/appimage/") || path_str_lower.ends_with(".appimage") { + return Some("appimage?".to_string()); + } + + // check for bun + if path_str_lower.contains("/bun/") || path_str_lower.contains("/.bun/") { + return Some("bun".to_string()); + } + + // check for deno + if path_str_lower.contains("/deno/") || path_str_lower.contains("/.deno/") { + return Some("deno".to_string()); + } + + // check for yarn + if path_str_lower.contains("/yarn/") || path_str_lower.contains("/.yarn/") { + return Some("yarn".to_string()); + } + + // Check for system package managers (dpkg/apt on Debian/Ubuntu, etc.) + // This should be last as it's the most generic + // if path_str_lower.starts_with("/usr/bin/") + // || path_str_lower.starts_with("/bin/") + // || path_str_lower.starts_with("/usr/local/bin/") + // || path_str_lower.starts_with("/sbin/") + // || path_str_lower.starts_with("/usr/sbin/") + // { + // return Some("system".to_string()); + // } + + None +} pub fn partition_binaries( binaries_to_check: Vec>, @@ -16,9 +107,11 @@ pub fn partition_binaries( for binary in binaries_to_check { let name = binary.name.as_ref(); match which::which(name) { - Ok(_) => { + Ok(path) => { info!(SCOPE = "which", bin = name; "found"); - available.push(binary); + let package_manager = detect_package_manager(&path); + let updated_binary = Binary::new_with_package_manager(binary.name, package_manager); + available.push(updated_binary); } Err(err) => { info!(SCOPE = "which", bin = name; "not found"); @@ -30,7 +123,7 @@ pub fn partition_binaries( return Err( DiscoveryError::BinaryCheck { name: name.to_string(), - source: std::io::Error::new(std::io::ErrorKind::Other, err), + source: std::io::Error::other(err), } .into(), ); @@ -44,9 +137,57 @@ pub fn partition_binaries( #[cfg(test)] mod tests { use beef::Cow; + use std::path::Path; use super::*; + #[test] + fn test_detect_package_manager() { + // Test rustup/cargo detection + assert_eq!( + detect_package_manager(Path::new("/home/user/.cargo/bin/cargo")), + Some("cargo".to_string()) + ); + assert_eq!( + detect_package_manager(Path::new("/home/user/.rustup/toolchains/stable/bin/rustc")), + Some("rustup".to_string()) + ); + + // Test system package detection + assert_eq!(detect_package_manager(Path::new("/usr/bin/grep")), None); + assert_eq!(detect_package_manager(Path::new("/bin/ls")), None); + + // Test homebrew detection + assert_eq!( + detect_package_manager(Path::new("/opt/homebrew/bin/brew")), + Some("homebrew".to_string()) + ); + + // Test npm detection + assert_eq!( + detect_package_manager(Path::new("/usr/local/lib/node_modules/.bin/npm")), + Some("npm".to_string()) + ); + + // Test go detection + assert_eq!( + detect_package_manager(Path::new("/home/user/go/bin/gofmt")), + Some("go".to_string()) + ); + + // Test pip detection + assert_eq!( + detect_package_manager(Path::new("/home/user/.local/bin/pip")), + Some("pip".to_string()) + ); + + // Test unknown path + assert_eq!( + detect_package_manager(Path::new("/some/unknown/path/binary")), + None + ); + } + #[test] fn test_partition_binaries() { let cargo_exists = which::which("cargo").is_ok(); @@ -69,6 +210,9 @@ mod tests { if cargo_exists { assert_eq!(available.len(), 1); assert_eq!(available[0].name, "cargo"); + // Check that package manager was detected + assert!(available[0].package_manager.is_some()); + assert_eq!(available[0].package_manager.as_ref().unwrap(), "cargo"); assert_eq!(not_available.len(), 1); assert_eq!( not_available[0].name, diff --git a/src/output.rs b/src/output.rs index e309a38..9a53876 100644 --- a/src/output.rs +++ b/src/output.rs @@ -15,11 +15,28 @@ pub fn print_center_aligned( let padding_needed = max_len.saturating_sub(bin.name.len()); let padding = " ".repeat(padding_needed); let version_display = if always_found { - "found".to_string() + if let Some(ref pm) = bin.package_manager { + format!("found {}", format!("via {}", pm).dimmed()) + } else { + "found".to_string() + } } else { match bin.version { - Some(ref version) => format!("{}", format_version(version, full_versions)), - None => "?".to_string(), + Some(ref version) => { + let version_str = format!("{}", format_version(version, full_versions)); + if let Some(ref pm) = bin.package_manager { + format!("{} {}", version_str, format!("via {}", pm).dimmed()) + } else { + version_str + } + } + None => { + if let Some(ref pm) = bin.package_manager { + format!("? {}", format!("via {}", pm).dimmed()) + } else { + "?".to_string() + } + } } }; println!("{}{} {}", padding, bin.name.green(), version_display); @@ -32,7 +49,12 @@ pub fn print_center_aligned(binaries: Vec, max_len: usize) -> Result<()> for bin in &binaries { let padding_needed = max_len.saturating_sub(bin.name.len()); let padding = " ".repeat(padding_needed); - println!("{}{} found", padding, bin.name.green()); + let display_text = if let Some(ref pm) = bin.package_manager { + format!("found {}", format!("via {}", pm).dimmed()) + } else { + "found".to_string() + }; + println!("{}{} {}", padding, bin.name.green(), display_text); } Ok(()) } diff --git a/src/versions.rs b/src/versions.rs index ad3618e..5ae166a 100644 --- a/src/versions.rs +++ b/src/versions.rs @@ -188,7 +188,7 @@ pub fn execute_binary<'a>(binary_name: &str) -> Result> { return Err( VersionError::Execution { name: binary_name.to_string(), - source: std::io::Error::new(std::io::ErrorKind::Other, e), + source: std::io::Error::other(e), } .into(), ); @@ -317,6 +317,7 @@ pub fn get_versions_for_bins(binaries: Vec) -> Vec { return Binary { name: binary.name, version: None, + package_manager: binary.package_manager, }; } @@ -332,6 +333,7 @@ pub fn get_versions_for_bins(binaries: Vec) -> Vec { Binary { name: binary.name, version, + package_manager: binary.package_manager, } }) .collect()