codewhale_release/
install.rs1use std::path::Path;
16
17pub const INSTALL_METHOD_ENV: &str = "CODEWHALE_INSTALL_METHOD";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum InstallMethod {
28 Npm,
30 Homebrew,
32 Cargo,
34 Binary,
37}
38
39impl InstallMethod {
40 #[must_use]
47 pub fn detect(exe: &Path) -> Self {
48 if let Some(forced) = std::env::var(INSTALL_METHOD_ENV)
49 .ok()
50 .and_then(|raw| Self::from_token(&raw))
51 {
52 return forced;
53 }
54 Self::from_path(exe)
55 }
56
57 #[must_use]
61 pub fn from_path(exe: &Path) -> Self {
62 let components: Vec<String> = exe
63 .components()
64 .filter_map(|c| c.as_os_str().to_str())
65 .map(str::to_ascii_lowercase)
66 .collect();
67
68 let has = |name: &str| components.iter().any(|c| c == name);
69
70 if has("node_modules") {
73 return Self::Npm;
74 }
75 if has("cellar") || has(".linuxbrew") || has("linuxbrew") {
76 return Self::Homebrew;
77 }
78 if components
81 .windows(2)
82 .any(|pair| pair[0] == ".cargo" && pair[1] == "bin")
83 {
84 return Self::Cargo;
85 }
86 Self::Binary
87 }
88
89 fn from_token(raw: &str) -> Option<Self> {
90 match raw.trim().to_ascii_lowercase().as_str() {
91 "npm" => Some(Self::Npm),
92 "homebrew" | "brew" => Some(Self::Homebrew),
93 "cargo" => Some(Self::Cargo),
94 "binary" | "release" => Some(Self::Binary),
95 _ => None,
96 }
97 }
98
99 #[must_use]
106 pub fn update_command(self) -> &'static str {
107 match self {
108 Self::Npm => "npm install -g codewhale@latest",
109 Self::Homebrew => "brew upgrade codewhale",
110 Self::Cargo => "cargo install codewhale-cli --locked --force",
111 Self::Binary => "codewhale update",
112 }
113 }
114
115 #[must_use]
120 pub fn supports_self_update(self) -> bool {
121 matches!(self, Self::Binary)
122 }
123
124 #[must_use]
126 pub fn label(self) -> &'static str {
127 match self {
128 Self::Npm => "npm",
129 Self::Homebrew => "Homebrew",
130 Self::Cargo => "cargo",
131 Self::Binary => "release binary",
132 }
133 }
134}
135
136#[must_use]
143pub fn current_install_method() -> InstallMethod {
144 match std::env::current_exe() {
145 Ok(exe) => InstallMethod::detect(&exe),
146 Err(_) => InstallMethod::Binary,
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use std::path::PathBuf;
153
154 use super::*;
155
156 #[test]
157 fn npm_global_install_is_detected_from_node_modules() {
158 let exe = PathBuf::from("/usr/local/lib/node_modules/codewhale/bin/codewhale");
159 assert_eq!(InstallMethod::from_path(&exe), InstallMethod::Npm);
160 assert_eq!(
161 InstallMethod::Npm.update_command(),
162 "npm install -g codewhale@latest"
163 );
164 assert!(!InstallMethod::Npm.supports_self_update());
165 }
166
167 #[test]
168 fn homebrew_install_is_detected_from_cellar_on_both_prefixes() {
169 for exe in [
170 "/opt/homebrew/Cellar/codewhale/0.9.8/bin/codewhale",
171 "/usr/local/Cellar/codewhale/0.9.8/bin/codewhale",
172 "/home/linuxbrew/.linuxbrew/Cellar/codewhale/0.9.8/bin/codewhale",
173 "/opt/homebrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
174 "/usr/local/Cellar/deepseek-tui/0.9.4/bin/codewhale",
175 "/home/linuxbrew/.linuxbrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
176 ] {
177 assert_eq!(
178 InstallMethod::from_path(&PathBuf::from(exe)),
179 InstallMethod::Homebrew,
180 "{exe} should read as Homebrew"
181 );
182 }
183 assert_eq!(
184 InstallMethod::Homebrew.update_command(),
185 "brew upgrade codewhale"
186 );
187 assert!(!InstallMethod::Homebrew.supports_self_update());
188 }
189
190 #[test]
191 fn cargo_install_requires_the_cargo_bin_pair() {
192 assert_eq!(
193 InstallMethod::from_path(&PathBuf::from("/home/u/.cargo/bin/codewhale")),
194 InstallMethod::Cargo
195 );
196 assert_eq!(
198 InstallMethod::from_path(&PathBuf::from("/home/u/bin/codewhale")),
199 InstallMethod::Binary
200 );
201 assert!(!InstallMethod::Cargo.supports_self_update());
202 }
203
204 #[test]
205 fn npm_wins_over_an_enclosing_manager_prefix() {
206 let exe = PathBuf::from("/opt/homebrew/lib/node_modules/codewhale/bin/codewhale");
208 assert_eq!(InstallMethod::from_path(&exe), InstallMethod::Npm);
209 }
210
211 #[test]
212 fn termux_and_plain_release_binaries_self_update() {
213 for exe in [
214 "/data/data/com.termux/files/usr/bin/codewhale",
215 "/usr/local/bin/codewhale",
216 "/home/u/Downloads/codewhale",
217 ] {
218 let method = InstallMethod::from_path(&PathBuf::from(exe));
219 assert_eq!(method, InstallMethod::Binary, "{exe} should self-update");
220 assert!(method.supports_self_update());
221 assert_eq!(method.update_command(), "codewhale update");
222 }
223 }
224
225 #[test]
226 fn env_tokens_map_to_methods_and_junk_is_ignored() {
227 assert_eq!(InstallMethod::from_token("npm"), Some(InstallMethod::Npm));
228 assert_eq!(
229 InstallMethod::from_token(" BREW "),
230 Some(InstallMethod::Homebrew)
231 );
232 assert_eq!(
233 InstallMethod::from_token("homebrew"),
234 Some(InstallMethod::Homebrew)
235 );
236 assert_eq!(
237 InstallMethod::from_token("cargo"),
238 Some(InstallMethod::Cargo)
239 );
240 assert_eq!(
241 InstallMethod::from_token("binary"),
242 Some(InstallMethod::Binary)
243 );
244 assert_eq!(InstallMethod::from_token("apt"), None);
245 }
246}