use crate::{Result, ToolContext, ToolExecutionResult, ToolStatus, VersionInfo};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[async_trait]
pub trait VxTool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str {
"A development tool"
}
fn aliases(&self) -> Vec<&str> {
vec![]
}
async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
async fn install_version(&self, version: &str, force: bool) -> Result<()> {
if !force && self.is_version_installed(version).await? {
return Err(anyhow::anyhow!(
"Version {} of {} is already installed. Use --force to reinstall.",
version,
self.name()
));
}
let install_dir = self.get_version_install_dir(version);
let _exe_path = self.default_install_workflow(version, &install_dir).await?;
if !self.is_version_installed(version).await? {
return Err(anyhow::anyhow!(
"Installation verification failed for {} version {}",
self.name(),
version
));
}
Ok(())
}
async fn is_version_installed(&self, version: &str) -> Result<bool> {
let install_dir = self.get_version_install_dir(version);
Ok(install_dir.exists())
}
async fn execute(&self, args: &[String], context: &ToolContext) -> Result<ToolExecutionResult> {
let _ = (args, context);
Ok(ToolExecutionResult::success())
}
async fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf> {
let exe_name = if cfg!(target_os = "windows") {
format!("{}.exe", self.name())
} else {
self.name().to_string()
};
let candidates = vec![
install_dir.join(&exe_name),
install_dir.join("bin").join(&exe_name),
install_dir.join("Scripts").join(&exe_name), ];
for candidate in candidates {
if candidate.exists() {
return Ok(candidate);
}
}
Ok(install_dir.join("bin").join(exe_name))
}
async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
let versions = self.fetch_versions(true).await?;
Ok(versions
.iter()
.find(|v| v.version == version)
.and_then(|v| v.download_url.clone()))
}
fn get_version_install_dir(&self, version: &str) -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".vx")
.join("tools")
.join(self.name())
.join(version)
}
fn get_base_install_dir(&self) -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".vx")
.join("tools")
.join(self.name())
}
async fn get_active_version(&self) -> Result<String> {
let installed_versions = self.get_installed_versions().await?;
installed_versions
.first()
.cloned()
.ok_or_else(|| anyhow::anyhow!("No versions installed for {}", self.name()))
}
async fn get_installed_versions(&self) -> Result<Vec<String>> {
let base_dir = self.get_base_install_dir();
if !base_dir.exists() {
return Ok(vec![]);
}
let mut versions = Vec::new();
for entry in std::fs::read_dir(base_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
if let Some(name) = entry.file_name().to_str() {
versions.push(name.to_string());
}
}
}
versions.sort_by(|a, b| b.cmp(a));
Ok(versions)
}
async fn remove_version(&self, version: &str, force: bool) -> Result<()> {
let version_dir = self.get_version_install_dir(version);
if !version_dir.exists() {
if !force {
return Err(anyhow::anyhow!(
"Version {} of {} is not installed",
version,
self.name()
));
}
return Ok(());
}
std::fs::remove_dir_all(&version_dir)?;
Ok(())
}
async fn get_status(&self) -> Result<ToolStatus> {
let installed_versions = self.get_installed_versions().await?;
let current_version = if !installed_versions.is_empty() {
self.get_active_version().await.ok()
} else {
None
};
Ok(ToolStatus {
installed: !installed_versions.is_empty(),
current_version,
installed_versions,
})
}
async fn default_install_workflow(&self, version: &str, install_dir: &Path) -> Result<PathBuf> {
let _download_url = self.get_download_url(version).await?.ok_or_else(|| {
anyhow::anyhow!(
"No download URL found for {} version {}",
self.name(),
version
)
})?;
std::fs::create_dir_all(install_dir)?;
let exe_path = self.get_executable_path(install_dir).await?;
if let Some(parent) = exe_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(
&exe_path,
format!(
"#!/bin/bash\necho 'This is {} version {}'\n",
self.name(),
version
),
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&exe_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&exe_path, perms)?;
}
Ok(exe_path)
}
fn metadata(&self) -> HashMap<String, String> {
HashMap::new()
}
}
pub trait UrlBuilder: Send + Sync {
fn download_url(&self, version: &str) -> Option<String>;
fn versions_url(&self) -> &str;
}
pub trait VersionParser: Send + Sync {
fn parse_versions(
&self,
json: &serde_json::Value,
include_prerelease: bool,
) -> Result<Vec<VersionInfo>>;
}
pub struct ConfigurableTool {
metadata: crate::ToolMetadata,
url_builder: Box<dyn UrlBuilder>,
#[allow(dead_code)]
version_parser: Box<dyn VersionParser>,
}
impl ConfigurableTool {
pub fn new(
metadata: crate::ToolMetadata,
url_builder: Box<dyn UrlBuilder>,
version_parser: Box<dyn VersionParser>,
) -> Self {
Self {
metadata,
url_builder,
version_parser,
}
}
pub fn metadata(&self) -> &crate::ToolMetadata {
&self.metadata
}
}
#[async_trait]
impl VxTool for ConfigurableTool {
fn name(&self) -> &str {
&self.metadata.name
}
fn description(&self) -> &str {
&self.metadata.description
}
fn aliases(&self) -> Vec<&str> {
self.metadata.aliases.iter().map(|s| s.as_str()).collect()
}
async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
let _ = include_prerelease;
Ok(vec![
VersionInfo::new("1.0.0"),
VersionInfo::new("1.1.0"),
VersionInfo::new("2.0.0"),
])
}
async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
Ok(self.url_builder.download_url(version))
}
fn metadata(&self) -> HashMap<String, String> {
self.metadata.metadata.clone()
}
}