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 deepseek-tui",
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/deepseek-tui/0.9.4/bin/codewhale",
171 "/usr/local/Cellar/deepseek-tui/0.9.4/bin/codewhale",
172 "/home/linuxbrew/.linuxbrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
173 ] {
174 assert_eq!(
175 InstallMethod::from_path(&PathBuf::from(exe)),
176 InstallMethod::Homebrew,
177 "{exe} should read as Homebrew"
178 );
179 }
180 assert_eq!(
182 InstallMethod::Homebrew.update_command(),
183 "brew upgrade deepseek-tui"
184 );
185 assert!(!InstallMethod::Homebrew.supports_self_update());
186 }
187
188 #[test]
189 fn cargo_install_requires_the_cargo_bin_pair() {
190 assert_eq!(
191 InstallMethod::from_path(&PathBuf::from("/home/u/.cargo/bin/codewhale")),
192 InstallMethod::Cargo
193 );
194 assert_eq!(
196 InstallMethod::from_path(&PathBuf::from("/home/u/bin/codewhale")),
197 InstallMethod::Binary
198 );
199 assert!(!InstallMethod::Cargo.supports_self_update());
200 }
201
202 #[test]
203 fn npm_wins_over_an_enclosing_manager_prefix() {
204 let exe = PathBuf::from("/opt/homebrew/lib/node_modules/codewhale/bin/codewhale");
206 assert_eq!(InstallMethod::from_path(&exe), InstallMethod::Npm);
207 }
208
209 #[test]
210 fn termux_and_plain_release_binaries_self_update() {
211 for exe in [
212 "/data/data/com.termux/files/usr/bin/codewhale",
213 "/usr/local/bin/codewhale",
214 "/home/u/Downloads/codewhale",
215 ] {
216 let method = InstallMethod::from_path(&PathBuf::from(exe));
217 assert_eq!(method, InstallMethod::Binary, "{exe} should self-update");
218 assert!(method.supports_self_update());
219 assert_eq!(method.update_command(), "codewhale update");
220 }
221 }
222
223 #[test]
224 fn env_tokens_map_to_methods_and_junk_is_ignored() {
225 assert_eq!(InstallMethod::from_token("npm"), Some(InstallMethod::Npm));
226 assert_eq!(
227 InstallMethod::from_token(" BREW "),
228 Some(InstallMethod::Homebrew)
229 );
230 assert_eq!(
231 InstallMethod::from_token("homebrew"),
232 Some(InstallMethod::Homebrew)
233 );
234 assert_eq!(
235 InstallMethod::from_token("cargo"),
236 Some(InstallMethod::Cargo)
237 );
238 assert_eq!(
239 InstallMethod::from_token("binary"),
240 Some(InstallMethod::Binary)
241 );
242 assert_eq!(InstallMethod::from_token("apt"), None);
243 }
244}