use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const NPM_PACKAGE: &str = "@withautonomi/ant";
const NODE_MODULES: &str = "node_modules";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InstallMethod {
Npm,
SelfManaged,
}
impl InstallMethod {
#[must_use]
pub fn can_self_replace(self) -> bool {
matches!(self, Self::SelfManaged)
}
#[must_use]
pub fn update_command(self) -> Option<String> {
match self {
Self::Npm => Some(format!("npm update -g {NPM_PACKAGE}")),
Self::SelfManaged => None,
}
}
#[must_use]
pub fn package_manager(self) -> Option<&'static str> {
match self {
Self::Npm => Some("npm"),
Self::SelfManaged => None,
}
}
}
#[must_use]
pub fn detect() -> InstallMethod {
current_exe()
.as_deref()
.map_or(InstallMethod::SelfManaged, classify_path)
}
fn current_exe() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
Some(std::fs::canonicalize(&exe).unwrap_or(exe))
}
#[must_use]
pub fn classify_path(exe: &Path) -> InstallMethod {
let under_node_modules = exe
.to_string_lossy()
.split(['/', '\\'])
.any(|segment| segment == NODE_MODULES);
if under_node_modules {
InstallMethod::Npm
} else {
InstallMethod::SelfManaged
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn npm_global_install_is_detected() {
let path = Path::new(
"/home/user/.nvm/versions/node/v22.11.0/lib/node_modules/@withautonomi/ant-linux-x64/bin/ant",
);
assert_eq!(classify_path(path), InstallMethod::Npm);
}
#[test]
fn npm_local_install_is_detected() {
let path = Path::new("/srv/project/node_modules/@withautonomi/ant-linux-x64/bin/ant");
assert_eq!(classify_path(path), InstallMethod::Npm);
}
#[test]
fn nested_node_modules_is_detected() {
let path = Path::new(
"/srv/project/node_modules/some-tool/node_modules/@withautonomi/ant-darwin-arm64/bin/ant",
);
assert_eq!(classify_path(path), InstallMethod::Npm);
}
#[test]
fn windows_npm_install_is_detected() {
let path = Path::new(
r"C:\Users\user\AppData\Roaming\npm\node_modules\@withautonomi\ant-win32-x64\bin\ant.exe",
);
assert_eq!(classify_path(path), InstallMethod::Npm);
}
#[test]
fn install_sh_locations_are_self_managed() {
for path in [
"/home/user/.local/bin/ant",
"/usr/local/bin/ant",
"/home/user/Library/Application Support/ant/ant",
"/opt/ant/bin/ant",
] {
assert_eq!(
classify_path(Path::new(path)),
InstallMethod::SelfManaged,
"{path} should be self-managed"
);
}
}
#[test]
fn cargo_and_local_builds_are_self_managed() {
for path in [
"/home/user/.cargo/bin/ant",
"/home/user/dev/ant-client/target/release/ant",
] {
assert_eq!(classify_path(Path::new(path)), InstallMethod::SelfManaged);
}
}
#[test]
fn a_directory_merely_containing_the_substring_is_not_npm() {
let path = Path::new("/home/user/my_node_modules_backup/bin/ant");
assert_eq!(classify_path(path), InstallMethod::SelfManaged);
}
#[test]
fn self_managed_installs_may_self_replace() {
assert!(InstallMethod::SelfManaged.can_self_replace());
assert!(InstallMethod::SelfManaged.update_command().is_none());
}
#[test]
fn npm_installs_defer_to_npm() {
assert!(!InstallMethod::Npm.can_self_replace());
assert_eq!(
InstallMethod::Npm.update_command().as_deref(),
Some("npm update -g @withautonomi/ant")
);
}
#[test]
fn only_package_managed_installs_name_a_manager() {
assert_eq!(InstallMethod::Npm.package_manager(), Some("npm"));
assert_eq!(InstallMethod::SelfManaged.package_manager(), None);
}
#[test]
fn install_method_serialises_as_snake_case() {
assert_eq!(
serde_json::to_string(&InstallMethod::Npm).unwrap(),
"\"npm\""
);
assert_eq!(
serde_json::to_string(&InstallMethod::SelfManaged).unwrap(),
"\"self_managed\""
);
}
}