1use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11pub type VxResult<T> = Result<T, VxError>;
13
14#[derive(thiserror::Error, Debug)]
16pub enum VxError {
17 #[error("Tool '{tool}' not found")]
18 ToolNotFound { tool: String },
19
20 #[error("Version '{version}' not found for tool '{tool}'")]
21 VersionNotFound { tool: String, version: String },
22
23 #[error("Installation failed for '{tool}': {reason}")]
24 InstallationFailed { tool: String, reason: String },
25
26 #[error("Execution failed: {message}")]
27 ExecutionFailed { message: String },
28
29 #[error("Configuration error: {message}")]
30 ConfigError { message: String },
31
32 #[error("IO error: {0}")]
33 Io(#[from] std::io::Error),
34
35 #[error("Other error: {0}")]
36 Other(#[from] anyhow::Error),
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct Platform {
42 pub os: String,
44 pub arch: String,
46}
47
48impl Platform {
49 pub fn current() -> Self {
51 Self {
52 os: std::env::consts::OS.to_string(),
53 arch: std::env::consts::ARCH.to_string(),
54 }
55 }
56}
57
58impl std::fmt::Display for Platform {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 let platform_str = match (self.os.as_str(), self.arch.as_str()) {
61 ("windows", "x86_64") => "win-x64".to_string(),
62 ("windows", "aarch64") => "win-arm64".to_string(),
63 ("macos", "x86_64") => "darwin-x64".to_string(),
64 ("macos", "aarch64") => "darwin-arm64".to_string(),
65 ("linux", "x86_64") => "linux-x64".to_string(),
66 ("linux", "aarch64") => "linux-arm64".to_string(),
67 _ => format!("{}-{}", self.os, self.arch),
68 };
69 write!(f, "{}", platform_str)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Version {
76 pub version: String,
78 pub prerelease: bool,
80 pub metadata: HashMap<String, String>,
82}
83
84impl Version {
85 pub fn new(version: impl Into<String>) -> Self {
87 Self {
88 version: version.into(),
89 prerelease: false,
90 metadata: HashMap::new(),
91 }
92 }
93
94 pub fn prerelease(version: impl Into<String>) -> Self {
96 Self {
97 version: version.into(),
98 prerelease: true,
99 metadata: HashMap::new(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ToolSpec {
107 pub name: String,
109 pub description: String,
111 pub platforms: Vec<Platform>,
113 pub versions: Vec<Version>,
115 pub install_methods: Vec<String>,
117 pub dependencies: Vec<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct InstallConfig {
124 pub tool: String,
126 pub version: String,
128 pub platform: Platform,
130 pub install_dir: PathBuf,
132 pub download_url: Option<String>,
134 pub method: InstallMethod,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub enum InstallMethod {
141 Archive { format: ArchiveFormat },
143 Binary,
145 PackageManager { manager: String },
147 Custom { script: String },
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub enum ArchiveFormat {
154 Zip,
155 TarGz,
156 TarXz,
157}
158
159#[derive(Debug, Clone)]
161pub struct ExecutionContext {
162 pub working_dir: PathBuf,
164 pub env_vars: HashMap<String, String>,
166 pub args: Vec<String>,
168}
169
170impl Default for ExecutionContext {
171 fn default() -> Self {
172 Self {
173 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
174 env_vars: HashMap::new(),
175 args: Vec::new(),
176 }
177 }
178}
179
180#[derive(Debug)]
182pub struct ExecutionResult {
183 pub exit_code: i32,
185 pub duration: std::time::Duration,
187 pub success: bool,
189}
190
191#[async_trait]
193pub trait ToolManager: Send + Sync {
194 async fn is_available(&self, tool: &str) -> VxResult<bool>;
196
197 async fn get_version(&self, tool: &str) -> VxResult<Option<Version>>;
199
200 async fn install(&self, config: &InstallConfig) -> VxResult<()>;
202
203 async fn execute(&self, tool: &str, context: &ExecutionContext) -> VxResult<ExecutionResult>;
205
206 async fn list_tools(&self) -> VxResult<Vec<String>>;
208}
209
210#[async_trait]
212pub trait ToolResolver: Send + Sync {
213 async fn resolve(&self, tool: &str) -> VxResult<ToolSpec>;
215
216 async fn get_install_config(&self, tool: &str, version: &str) -> VxResult<InstallConfig>;
218}
219
220#[async_trait]
222pub trait VersionManager: Send + Sync {
223 async fn list_versions(&self, tool: &str) -> VxResult<Vec<Version>>;
225
226 async fn get_latest(&self, tool: &str) -> VxResult<Version>;
228
229 fn satisfies(&self, version: &Version, constraint: &str) -> bool;
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct VxConfig {
236 pub install_dir: PathBuf,
238 pub cache_dir: PathBuf,
240 pub platform: Platform,
242 pub registries: Vec<String>,
244 pub tools: HashMap<String, serde_json::Value>,
246}
247
248impl Default for VxConfig {
249 fn default() -> Self {
250 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
251 let vx_dir = home_dir.join(".vx");
252
253 Self {
254 install_dir: vx_dir.join("tools"),
255 cache_dir: vx_dir.join("cache"),
256 platform: Platform::current(),
257 registries: vec!["https://registry.vx.dev".to_string()],
258 tools: HashMap::new(),
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_platform_current() {
269 let platform = Platform::current();
270 assert!(!platform.os.is_empty());
271 assert!(!platform.arch.is_empty());
272 }
273
274 #[test]
275 fn test_platform_to_string() {
276 let platform = Platform {
277 os: "linux".to_string(),
278 arch: "x86_64".to_string(),
279 };
280 assert_eq!(platform.to_string(), "linux-x64");
281 }
282
283 #[test]
284 fn test_version_creation() {
285 let version = Version::new("1.0.0");
286 assert_eq!(version.version, "1.0.0");
287 assert!(!version.prerelease);
288
289 let prerelease = Version::prerelease("2.0.0-beta.1");
290 assert!(prerelease.prerelease);
291 }
292
293 #[test]
294 fn test_vx_config_default() {
295 let config = VxConfig::default();
296 assert!(config.install_dir.to_string_lossy().contains(".vx"));
297 assert!(!config.registries.is_empty());
298 }
299}