1use crate::{Result, VxError};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone)]
16pub struct VxEnvironment {
17 base_dir: PathBuf,
19 config_dir: PathBuf,
21 cache_dir: PathBuf,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ToolInstallation {
28 pub tool_name: String,
30 pub version: String,
32 pub install_dir: PathBuf,
34 pub executable_path: PathBuf,
36 pub installed_at: chrono::DateTime<chrono::Utc>,
38 pub is_active: bool,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EnvironmentConfig {
45 pub active_versions: HashMap<String, String>,
47 pub global_settings: HashMap<String, String>,
49}
50
51impl VxEnvironment {
52 pub fn new() -> Result<Self> {
54 let base_dir = Self::get_vx_home()?;
55 let config_dir = base_dir.join("config");
56 let cache_dir = base_dir.join("cache");
57
58 std::fs::create_dir_all(&base_dir)?;
60 std::fs::create_dir_all(&config_dir)?;
61 std::fs::create_dir_all(&cache_dir)?;
62
63 Ok(Self {
64 base_dir,
65 config_dir,
66 cache_dir,
67 })
68 }
69
70 pub fn get_vx_home() -> Result<PathBuf> {
72 if let Ok(vx_home) = std::env::var("VX_HOME") {
73 Ok(PathBuf::from(vx_home))
74 } else if let Some(home) = dirs::home_dir() {
75 Ok(home.join(".vx"))
76 } else {
77 Err(VxError::ConfigurationError {
78 message: "Cannot determine VX home directory".to_string(),
79 })
80 }
81 }
82
83 pub fn get_base_install_dir(&self) -> PathBuf {
85 self.base_dir.join("tools")
86 }
87
88 pub fn get_tool_install_dir(&self, tool_name: &str) -> PathBuf {
90 self.get_base_install_dir().join(tool_name)
91 }
92
93 pub fn get_version_install_dir(&self, tool_name: &str, version: &str) -> PathBuf {
95 self.get_tool_install_dir(tool_name).join(version)
96 }
97
98 pub fn get_cache_dir(&self) -> PathBuf {
100 self.cache_dir.clone()
101 }
102
103 pub fn get_tool_cache_dir(&self, tool_name: &str) -> PathBuf {
105 self.cache_dir.join("downloads").join(tool_name)
106 }
107
108 pub fn get_config_file(&self) -> PathBuf {
110 self.config_dir.join("environment.toml")
111 }
112
113 pub fn load_config(&self) -> Result<EnvironmentConfig> {
115 let config_file = self.get_config_file();
116
117 if !config_file.exists() {
118 return Ok(EnvironmentConfig {
119 active_versions: HashMap::new(),
120 global_settings: HashMap::new(),
121 });
122 }
123
124 let content = std::fs::read_to_string(&config_file)?;
125 let config: EnvironmentConfig =
126 toml::from_str(&content).map_err(|e| VxError::ConfigurationError {
127 message: format!("Failed to parse environment config: {}", e),
128 })?;
129
130 Ok(config)
131 }
132
133 pub fn save_config(&self, config: &EnvironmentConfig) -> Result<()> {
135 let config_file = self.get_config_file();
136 let content = toml::to_string_pretty(config).map_err(|e| VxError::ConfigurationError {
137 message: format!("Failed to serialize environment config: {}", e),
138 })?;
139
140 std::fs::write(&config_file, content)?;
141 Ok(())
142 }
143
144 pub fn get_active_version(&self, tool_name: &str) -> Result<Option<String>> {
146 let config = self.load_config()?;
147 Ok(config.active_versions.get(tool_name).cloned())
148 }
149
150 pub fn set_active_version(&self, tool_name: &str, version: &str) -> Result<()> {
152 let mut config = self.load_config()?;
153 config
154 .active_versions
155 .insert(tool_name.to_string(), version.to_string());
156 self.save_config(&config)?;
157 Ok(())
158 }
159
160 pub fn list_installed_versions(&self, tool_name: &str) -> Result<Vec<String>> {
162 let tool_dir = self.get_tool_install_dir(tool_name);
163
164 if !tool_dir.exists() {
165 return Ok(Vec::new());
166 }
167
168 let mut versions = Vec::new();
169 for entry in std::fs::read_dir(&tool_dir)? {
170 let entry = entry?;
171 if entry.file_type()?.is_dir() {
172 if let Some(version) = entry.file_name().to_str() {
173 versions.push(version.to_string());
174 }
175 }
176 }
177
178 versions.sort();
180 Ok(versions)
181 }
182
183 pub fn is_version_installed(&self, tool_name: &str, version: &str) -> bool {
185 let version_dir = self.get_version_install_dir(tool_name, version);
186 version_dir.exists()
187 }
188
189 pub fn get_installation_info(
191 &self,
192 tool_name: &str,
193 version: &str,
194 ) -> Result<Option<ToolInstallation>> {
195 if !self.is_version_installed(tool_name, version) {
196 return Ok(None);
197 }
198
199 let install_dir = self.get_version_install_dir(tool_name, version);
200 let config = self.load_config()?;
201 let is_active = config.active_versions.get(tool_name) == Some(&version.to_string());
202
203 let executable_path = self.find_executable_in_dir(&install_dir, tool_name)?;
205
206 Ok(Some(ToolInstallation {
207 tool_name: tool_name.to_string(),
208 version: version.to_string(),
209 install_dir,
210 executable_path,
211 installed_at: chrono::Utc::now(), is_active,
213 }))
214 }
215
216 pub fn find_executable_in_dir(&self, dir: &Path, tool_name: &str) -> Result<PathBuf> {
218 let mut patterns = vec![];
221
222 #[cfg(windows)]
224 {
225 patterns.extend(vec![
226 format!("{}.cmd", tool_name),
227 format!("{}.bat", tool_name),
228 format!("{}.exe", tool_name),
229 format!("{}.ps1", tool_name),
230 ]);
231 }
232
233 #[cfg(not(windows))]
235 {
236 patterns.extend(vec![format!("{}.exe", tool_name), tool_name.to_string()]);
237 }
238
239 #[cfg(windows)]
241 {
242 patterns.push(tool_name.to_string());
243 }
244
245 #[cfg(windows)]
247 {
248 patterns.extend(vec![
249 format!("bin/{}.cmd", tool_name),
250 format!("bin/{}.bat", tool_name),
251 format!("bin/{}.exe", tool_name),
252 format!("bin/{}.ps1", tool_name),
253 format!("bin/{}", tool_name),
254 ]);
255 }
256
257 #[cfg(not(windows))]
258 {
259 patterns.extend(vec![
260 format!("bin/{}.exe", tool_name),
261 format!("bin/{}", tool_name),
262 ]);
263 }
264
265 for pattern in &patterns {
267 let exe_path = dir.join(pattern);
268 if self.is_executable(&exe_path) {
269 return Ok(exe_path);
270 }
271 }
272
273 if let Ok(entries) = std::fs::read_dir(dir) {
275 for entry in entries.flatten() {
276 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
277 let subdir = entry.path();
278
279 for pattern in &patterns {
281 let exe_path = subdir.join(pattern);
282 if self.is_executable(&exe_path) {
283 return Ok(exe_path);
284 }
285 }
286 }
287 }
288 }
289
290 Err(VxError::ExecutableNotFound {
291 tool_name: tool_name.to_string(),
292 install_dir: dir.to_path_buf(),
293 })
294 }
295
296 fn is_executable(&self, path: &Path) -> bool {
298 if !path.exists() {
299 return false;
300 }
301
302 if let Ok(metadata) = std::fs::metadata(path) {
304 if metadata.is_file() {
305 return true;
306 }
307 }
308
309 if let Ok(metadata) = std::fs::symlink_metadata(path) {
311 if metadata.file_type().is_symlink() {
312 return true;
315 }
316 }
317
318 false
319 }
320
321 pub fn cleanup_unused(&self, keep_latest: usize) -> Result<Vec<String>> {
323 let mut cleaned = Vec::new();
324 let tools_dir = self.get_base_install_dir();
325
326 if !tools_dir.exists() {
327 return Ok(cleaned);
328 }
329
330 for entry in std::fs::read_dir(&tools_dir)? {
331 let entry = entry?;
332 if entry.file_type()?.is_dir() {
333 if let Some(tool_name) = entry.file_name().to_str() {
334 let removed = self.cleanup_tool_versions(tool_name, keep_latest)?;
335 cleaned.extend(removed);
336 }
337 }
338 }
339
340 Ok(cleaned)
341 }
342
343 pub fn cleanup_tool_versions(
345 &self,
346 tool_name: &str,
347 keep_latest: usize,
348 ) -> Result<Vec<String>> {
349 let mut versions = self.list_installed_versions(tool_name)?;
350 let config = self.load_config()?;
351 let active_version = config.active_versions.get(tool_name);
352
353 if versions.len() <= keep_latest {
354 return Ok(Vec::new());
355 }
356
357 versions.sort();
359 versions.reverse();
360
361 let mut removed = Vec::new();
362 let mut kept_count = 0;
363
364 for version in versions {
365 if Some(&version) == active_version {
367 continue;
368 }
369
370 if kept_count < keep_latest {
371 kept_count += 1;
372 continue;
373 }
374
375 let version_dir = self.get_version_install_dir(tool_name, &version);
377 if version_dir.exists() {
378 std::fs::remove_dir_all(&version_dir)?;
379 removed.push(format!("{}@{}", tool_name, version));
380 }
381 }
382
383 Ok(removed)
384 }
385}
386
387impl Default for VxEnvironment {
388 fn default() -> Self {
389 Self::new().expect("Failed to create VX environment")
390 }
391}