1use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12pub const NPM_PACKAGE: &str = "@withautonomi/ant";
14
15const NODE_MODULES: &str = "node_modules";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum InstallMethod {
22 Npm,
24 SelfManaged,
27}
28
29impl InstallMethod {
30 #[must_use]
32 pub fn can_self_replace(self) -> bool {
33 matches!(self, Self::SelfManaged)
34 }
35
36 #[must_use]
38 pub fn update_command(self) -> Option<String> {
39 match self {
40 Self::Npm => Some(format!("npm update -g {NPM_PACKAGE}")),
41 Self::SelfManaged => None,
42 }
43 }
44
45 #[must_use]
47 pub fn package_manager(self) -> Option<&'static str> {
48 match self {
49 Self::Npm => Some("npm"),
50 Self::SelfManaged => None,
51 }
52 }
53}
54
55#[must_use]
61pub fn detect() -> InstallMethod {
62 current_exe()
63 .as_deref()
64 .map_or(InstallMethod::SelfManaged, classify_path)
65}
66
67fn current_exe() -> Option<PathBuf> {
74 let exe = std::env::current_exe().ok()?;
75 Some(std::fs::canonicalize(&exe).unwrap_or(exe))
76}
77
78#[must_use]
90pub fn classify_path(exe: &Path) -> InstallMethod {
91 let under_node_modules = exe
92 .to_string_lossy()
93 .split(['/', '\\'])
94 .any(|segment| segment == NODE_MODULES);
95
96 if under_node_modules {
97 InstallMethod::Npm
98 } else {
99 InstallMethod::SelfManaged
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn npm_global_install_is_detected() {
109 let path = Path::new(
111 "/home/user/.nvm/versions/node/v22.11.0/lib/node_modules/@withautonomi/ant-linux-x64/bin/ant",
112 );
113 assert_eq!(classify_path(path), InstallMethod::Npm);
114 }
115
116 #[test]
117 fn npm_local_install_is_detected() {
118 let path = Path::new("/srv/project/node_modules/@withautonomi/ant-linux-x64/bin/ant");
119 assert_eq!(classify_path(path), InstallMethod::Npm);
120 }
121
122 #[test]
123 fn nested_node_modules_is_detected() {
124 let path = Path::new(
125 "/srv/project/node_modules/some-tool/node_modules/@withautonomi/ant-darwin-arm64/bin/ant",
126 );
127 assert_eq!(classify_path(path), InstallMethod::Npm);
128 }
129
130 #[test]
131 fn windows_npm_install_is_detected() {
132 let path = Path::new(
133 r"C:\Users\user\AppData\Roaming\npm\node_modules\@withautonomi\ant-win32-x64\bin\ant.exe",
134 );
135 assert_eq!(classify_path(path), InstallMethod::Npm);
136 }
137
138 #[test]
139 fn install_sh_locations_are_self_managed() {
140 for path in [
141 "/home/user/.local/bin/ant",
142 "/usr/local/bin/ant",
143 "/home/user/Library/Application Support/ant/ant",
144 "/opt/ant/bin/ant",
145 ] {
146 assert_eq!(
147 classify_path(Path::new(path)),
148 InstallMethod::SelfManaged,
149 "{path} should be self-managed"
150 );
151 }
152 }
153
154 #[test]
155 fn cargo_and_local_builds_are_self_managed() {
156 for path in [
157 "/home/user/.cargo/bin/ant",
158 "/home/user/dev/ant-client/target/release/ant",
159 ] {
160 assert_eq!(classify_path(Path::new(path)), InstallMethod::SelfManaged);
161 }
162 }
163
164 #[test]
165 fn a_directory_merely_containing_the_substring_is_not_npm() {
166 let path = Path::new("/home/user/my_node_modules_backup/bin/ant");
168 assert_eq!(classify_path(path), InstallMethod::SelfManaged);
169 }
170
171 #[test]
172 fn self_managed_installs_may_self_replace() {
173 assert!(InstallMethod::SelfManaged.can_self_replace());
174 assert!(InstallMethod::SelfManaged.update_command().is_none());
175 }
176
177 #[test]
178 fn npm_installs_defer_to_npm() {
179 assert!(!InstallMethod::Npm.can_self_replace());
180 assert_eq!(
181 InstallMethod::Npm.update_command().as_deref(),
182 Some("npm update -g @withautonomi/ant")
183 );
184 }
185
186 #[test]
187 fn only_package_managed_installs_name_a_manager() {
188 assert_eq!(InstallMethod::Npm.package_manager(), Some("npm"));
189 assert_eq!(InstallMethod::SelfManaged.package_manager(), None);
190 }
191
192 #[test]
193 fn install_method_serialises_as_snake_case() {
194 assert_eq!(
195 serde_json::to_string(&InstallMethod::Npm).unwrap(),
196 "\"npm\""
197 );
198 assert_eq!(
199 serde_json::to_string(&InstallMethod::SelfManaged).unwrap(),
200 "\"self_managed\""
201 );
202 }
203}