use crate::{Result, VersionInfo};
use std::path::Path;
pub fn is_command_available(command: &str) -> bool {
which::which(command).is_ok()
}
pub fn get_exe_extension() -> &'static str {
if cfg!(target_os = "windows") {
".exe"
} else {
""
}
}
pub fn get_exe_name(base_name: &str) -> String {
format!("{}{}", base_name, get_exe_extension())
}
pub fn is_executable(path: &Path) -> bool {
if !path.exists() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let permissions = metadata.permissions();
return permissions.mode() & 0o111 != 0;
}
false
}
#[cfg(not(unix))]
{
true
}
}
pub fn find_executable_in_dir(dir: &Path, exe_name: &str) -> Option<std::path::PathBuf> {
let exe_name_with_ext = get_exe_name(exe_name);
let candidates = vec![
dir.join(&exe_name_with_ext),
dir.join("bin").join(&exe_name_with_ext),
dir.join("Scripts").join(&exe_name_with_ext), dir.join("sbin").join(&exe_name_with_ext), ];
candidates
.into_iter()
.find(|candidate| is_executable(candidate))
}
pub fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
let parts: Vec<&str> = version.trim_start_matches('v').split('.').collect();
if parts.len() >= 3 {
let major = parts[0].parse().ok()?;
let minor = parts[1].parse().ok()?;
let patch_str = parts[2].split('-').next().unwrap_or(parts[2]);
let patch = patch_str.parse().ok()?;
Some((major, minor, patch))
} else {
None
}
}
pub fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
let (a_major, a_minor, a_patch) = parse_version(a)?;
let (b_major, b_minor, b_patch) = parse_version(b)?;
Some((a_major, a_minor, a_patch).cmp(&(b_major, b_minor, b_patch)))
}
pub fn sort_versions_desc(versions: &mut [String]) {
versions.sort_by(|a, b| {
match compare_versions(a, b) {
Some(ordering) => ordering.reverse(), None => std::cmp::Ordering::Equal,
}
});
}
pub fn is_prerelease(version: &str) -> bool {
let version_lower = version.to_lowercase();
version_lower.contains("alpha")
|| version_lower.contains("beta")
|| version_lower.contains("rc")
|| version_lower.contains("pre")
|| version_lower.contains("dev")
|| version_lower.contains("snapshot")
}
pub fn create_version_info(version: &str, download_url: Option<String>) -> VersionInfo {
VersionInfo {
version: version.to_string(),
prerelease: is_prerelease(version),
release_date: None,
release_notes: None,
download_url,
checksum: None,
file_size: None,
metadata: std::collections::HashMap::new(),
}
}
pub fn validate_tool_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow::anyhow!("Tool name cannot be empty"));
}
if name.len() > 64 {
return Err(anyhow::anyhow!(
"Tool name cannot be longer than 64 characters"
));
}
if !name.chars().next().unwrap().is_ascii_alphabetic() {
return Err(anyhow::anyhow!("Tool name must start with a letter"));
}
for ch in name.chars() {
if !ch.is_ascii_alphanumeric() && ch != '-' && ch != '_' {
return Err(anyhow::anyhow!(
"Tool name can only contain letters, numbers, hyphens, and underscores"
));
}
}
Ok(())
}
pub fn validate_version(version: &str) -> Result<()> {
if version.is_empty() {
return Err(anyhow::anyhow!("Version cannot be empty"));
}
let version = version.strip_prefix('v').unwrap_or(version);
if !version.chars().any(|c| c.is_ascii_digit()) {
return Err(anyhow::anyhow!("Version must contain at least one number"));
}
Ok(())
}
pub fn get_vx_dir() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".vx")
}
pub fn get_tools_dir() -> std::path::PathBuf {
get_vx_dir().join("tools")
}
pub fn get_plugins_dir() -> std::path::PathBuf {
get_vx_dir().join("plugins")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
assert_eq!(parse_version("1.2.3"), Some((1, 2, 3)));
assert_eq!(parse_version("v1.2.3"), Some((1, 2, 3)));
assert_eq!(parse_version("1.2.3-beta"), Some((1, 2, 3)));
assert_eq!(parse_version("invalid"), None);
}
#[test]
fn test_is_prerelease() {
assert!(is_prerelease("1.0.0-alpha"));
assert!(is_prerelease("1.0.0-beta.1"));
assert!(is_prerelease("1.0.0-rc.1"));
assert!(!is_prerelease("1.0.0"));
}
#[test]
fn test_validate_tool_name() {
assert!(validate_tool_name("node").is_ok());
assert!(validate_tool_name("my-tool").is_ok());
assert!(validate_tool_name("tool_name").is_ok());
assert!(validate_tool_name("").is_err());
assert!(validate_tool_name("123tool").is_err());
assert!(validate_tool_name("tool@name").is_err());
}
}