1use crate::{Result, VersionInfo};
7use std::path::Path;
8
9pub fn is_command_available(command: &str) -> bool {
14 which::which(command).is_ok()
15}
16
17pub fn get_exe_extension() -> &'static str {
21 if cfg!(target_os = "windows") {
22 ".exe"
23 } else {
24 ""
25 }
26}
27
28pub fn get_exe_name(base_name: &str) -> String {
32 format!("{}{}", base_name, get_exe_extension())
33}
34
35pub fn is_executable(path: &Path) -> bool {
40 if !path.exists() {
41 return false;
42 }
43
44 #[cfg(unix)]
45 {
46 use std::os::unix::fs::PermissionsExt;
47 if let Ok(metadata) = path.metadata() {
48 let permissions = metadata.permissions();
49 return permissions.mode() & 0o111 != 0;
50 }
51 false
52 }
53
54 #[cfg(not(unix))]
55 {
56 true
59 }
60}
61
62pub fn find_executable_in_dir(dir: &Path, exe_name: &str) -> Option<std::path::PathBuf> {
67 let exe_name_with_ext = get_exe_name(exe_name);
68
69 let candidates = vec![
71 dir.join(&exe_name_with_ext),
72 dir.join("bin").join(&exe_name_with_ext),
73 dir.join("Scripts").join(&exe_name_with_ext), dir.join("sbin").join(&exe_name_with_ext), ];
76
77 candidates
78 .into_iter()
79 .find(|candidate| is_executable(candidate))
80}
81pub fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
86 let parts: Vec<&str> = version.trim_start_matches('v').split('.').collect();
87
88 if parts.len() >= 3 {
89 let major = parts[0].parse().ok()?;
90 let minor = parts[1].parse().ok()?;
91 let patch_str = parts[2].split('-').next().unwrap_or(parts[2]);
93 let patch = patch_str.parse().ok()?;
94
95 Some((major, minor, patch))
96 } else {
97 None
98 }
99}
100
101pub fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
109 let (a_major, a_minor, a_patch) = parse_version(a)?;
110 let (b_major, b_minor, b_patch) = parse_version(b)?;
111
112 Some((a_major, a_minor, a_patch).cmp(&(b_major, b_minor, b_patch)))
113}
114
115pub fn sort_versions_desc(versions: &mut [String]) {
120 versions.sort_by(|a, b| {
121 match compare_versions(a, b) {
122 Some(ordering) => ordering.reverse(), None => std::cmp::Ordering::Equal,
124 }
125 });
126}
127
128pub fn is_prerelease(version: &str) -> bool {
133 let version_lower = version.to_lowercase();
134 version_lower.contains("alpha")
135 || version_lower.contains("beta")
136 || version_lower.contains("rc")
137 || version_lower.contains("pre")
138 || version_lower.contains("dev")
139 || version_lower.contains("snapshot")
140}
141
142pub fn create_version_info(version: &str, download_url: Option<String>) -> VersionInfo {
147 VersionInfo {
148 version: version.to_string(),
149 prerelease: is_prerelease(version),
150 release_date: None,
151 release_notes: None,
152 download_url,
153 checksum: None,
154 file_size: None,
155 metadata: std::collections::HashMap::new(),
156 }
157}
158pub fn validate_tool_name(name: &str) -> Result<()> {
165 if name.is_empty() {
166 return Err(anyhow::anyhow!("Tool name cannot be empty"));
167 }
168
169 if name.len() > 64 {
170 return Err(anyhow::anyhow!(
171 "Tool name cannot be longer than 64 characters"
172 ));
173 }
174
175 if !name.chars().next().unwrap().is_ascii_alphabetic() {
176 return Err(anyhow::anyhow!("Tool name must start with a letter"));
177 }
178
179 for ch in name.chars() {
180 if !ch.is_ascii_alphanumeric() && ch != '-' && ch != '_' {
181 return Err(anyhow::anyhow!(
182 "Tool name can only contain letters, numbers, hyphens, and underscores"
183 ));
184 }
185 }
186
187 Ok(())
188}
189
190pub fn validate_version(version: &str) -> Result<()> {
195 if version.is_empty() {
196 return Err(anyhow::anyhow!("Version cannot be empty"));
197 }
198
199 let version = version.strip_prefix('v').unwrap_or(version);
201
202 if !version.chars().any(|c| c.is_ascii_digit()) {
204 return Err(anyhow::anyhow!("Version must contain at least one number"));
205 }
206
207 Ok(())
209}
210
211pub fn get_vx_dir() -> std::path::PathBuf {
216 dirs::home_dir()
217 .unwrap_or_else(|| std::path::PathBuf::from("."))
218 .join(".vx")
219}
220
221pub fn get_tools_dir() -> std::path::PathBuf {
225 get_vx_dir().join("tools")
226}
227
228pub fn get_plugins_dir() -> std::path::PathBuf {
232 get_vx_dir().join("plugins")
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_parse_version() {
241 assert_eq!(parse_version("1.2.3"), Some((1, 2, 3)));
242 assert_eq!(parse_version("v1.2.3"), Some((1, 2, 3)));
243 assert_eq!(parse_version("1.2.3-beta"), Some((1, 2, 3)));
244 assert_eq!(parse_version("invalid"), None);
245 }
246
247 #[test]
248 fn test_is_prerelease() {
249 assert!(is_prerelease("1.0.0-alpha"));
250 assert!(is_prerelease("1.0.0-beta.1"));
251 assert!(is_prerelease("1.0.0-rc.1"));
252 assert!(!is_prerelease("1.0.0"));
253 }
254
255 #[test]
256 fn test_validate_tool_name() {
257 assert!(validate_tool_name("node").is_ok());
258 assert!(validate_tool_name("my-tool").is_ok());
259 assert!(validate_tool_name("tool_name").is_ok());
260 assert!(validate_tool_name("").is_err());
261 assert!(validate_tool_name("123tool").is_err());
262 assert!(validate_tool_name("tool@name").is_err());
263 }
264}