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 new_with_base_dir<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
72 let base_dir = base_dir.as_ref().to_path_buf();
73 let config_dir = base_dir.join("config");
74 let cache_dir = base_dir.join("cache");
75
76 std::fs::create_dir_all(&base_dir)?;
78 std::fs::create_dir_all(&config_dir)?;
79 std::fs::create_dir_all(&cache_dir)?;
80
81 Ok(Self {
82 base_dir,
83 config_dir,
84 cache_dir,
85 })
86 }
87
88 pub fn get_vx_home() -> Result<PathBuf> {
90 if let Ok(vx_home) = std::env::var("VX_HOME") {
91 Ok(PathBuf::from(vx_home))
92 } else if let Some(home) = dirs::home_dir() {
93 Ok(home.join(".vx"))
94 } else {
95 Err(VxError::ConfigurationError {
96 message: "Cannot determine VX home directory".to_string(),
97 })
98 }
99 }
100
101 pub fn get_base_install_dir(&self) -> PathBuf {
103 self.base_dir.join("tools")
104 }
105
106 pub fn get_tool_install_dir(&self, tool_name: &str) -> PathBuf {
108 self.get_base_install_dir().join(tool_name)
109 }
110
111 pub fn get_version_install_dir(&self, tool_name: &str, version: &str) -> PathBuf {
113 self.get_tool_install_dir(tool_name).join(version)
114 }
115
116 pub fn get_cache_dir(&self) -> PathBuf {
118 self.cache_dir.clone()
119 }
120
121 pub fn get_tool_cache_dir(&self, tool_name: &str) -> PathBuf {
123 self.cache_dir.join("downloads").join(tool_name)
124 }
125
126 pub fn shim_dir(&self) -> Result<PathBuf> {
128 let shim_dir = self.base_dir.join("shims");
129 std::fs::create_dir_all(&shim_dir)?;
130 Ok(shim_dir)
131 }
132
133 pub fn bin_dir(&self) -> Result<PathBuf> {
135 let bin_dir = self.base_dir.join("bin");
136 std::fs::create_dir_all(&bin_dir)?;
137 Ok(bin_dir)
138 }
139
140 pub fn get_config_file(&self) -> PathBuf {
142 self.config_dir.join("environment.toml")
143 }
144
145 pub fn load_config(&self) -> Result<EnvironmentConfig> {
147 let config_file = self.get_config_file();
148
149 if !config_file.exists() {
150 return Ok(EnvironmentConfig {
151 active_versions: HashMap::new(),
152 global_settings: HashMap::new(),
153 });
154 }
155
156 let content = std::fs::read_to_string(&config_file)?;
157 let config: EnvironmentConfig =
158 toml::from_str(&content).map_err(|e| VxError::ConfigurationError {
159 message: format!("Failed to parse environment config: {}", e),
160 })?;
161
162 Ok(config)
163 }
164
165 pub fn save_config(&self, config: &EnvironmentConfig) -> Result<()> {
167 let config_file = self.get_config_file();
168 let content = toml::to_string_pretty(config).map_err(|e| VxError::ConfigurationError {
169 message: format!("Failed to serialize environment config: {}", e),
170 })?;
171
172 std::fs::write(&config_file, content)?;
173 Ok(())
174 }
175
176 pub fn get_active_version(&self, tool_name: &str) -> Result<Option<String>> {
178 let config = self.load_config()?;
179 Ok(config.active_versions.get(tool_name).cloned())
180 }
181
182 pub fn set_active_version(&self, tool_name: &str, version: &str) -> Result<()> {
184 let mut config = self.load_config()?;
185 config
186 .active_versions
187 .insert(tool_name.to_string(), version.to_string());
188 self.save_config(&config)?;
189 Ok(())
190 }
191
192 pub fn list_installed_versions(&self, tool_name: &str) -> Result<Vec<String>> {
194 let tool_dir = self.get_tool_install_dir(tool_name);
195
196 if !tool_dir.exists() {
197 return Ok(Vec::new());
198 }
199
200 let mut versions = Vec::new();
201 for entry in std::fs::read_dir(&tool_dir)? {
202 let entry = entry?;
203 if entry.file_type()?.is_dir() {
204 if let Some(version) = entry.file_name().to_str() {
205 versions.push(version.to_string());
206 }
207 }
208 }
209
210 versions.sort();
212 Ok(versions)
213 }
214
215 pub fn is_version_installed(&self, tool_name: &str, version: &str) -> bool {
217 let version_dir = self.get_version_install_dir(tool_name, version);
218 version_dir.exists()
219 }
220
221 pub fn get_installation_info(
223 &self,
224 tool_name: &str,
225 version: &str,
226 ) -> Result<Option<ToolInstallation>> {
227 if !self.is_version_installed(tool_name, version) {
228 return Ok(None);
229 }
230
231 let install_dir = self.get_version_install_dir(tool_name, version);
232 let config = self.load_config()?;
233 let is_active = config.active_versions.get(tool_name) == Some(&version.to_string());
234
235 let executable_path = self.find_executable_in_dir(&install_dir, tool_name)?;
237
238 Ok(Some(ToolInstallation {
239 tool_name: tool_name.to_string(),
240 version: version.to_string(),
241 install_dir,
242 executable_path,
243 installed_at: chrono::Utc::now(), is_active,
245 }))
246 }
247
248 pub fn find_executable_in_dir(&self, dir: &Path, tool_name: &str) -> Result<PathBuf> {
250 let mut patterns = vec![];
253
254 #[cfg(windows)]
256 {
257 patterns.extend(vec![
258 format!("{}.cmd", tool_name),
259 format!("{}.bat", tool_name),
260 format!("{}.exe", tool_name),
261 format!("{}.ps1", tool_name),
262 ]);
263 }
264
265 #[cfg(not(windows))]
267 {
268 patterns.extend(vec![format!("{}.exe", tool_name), tool_name.to_string()]);
269 }
270
271 #[cfg(windows)]
273 {
274 patterns.push(tool_name.to_string());
275 }
276
277 #[cfg(windows)]
279 {
280 patterns.extend(vec![
281 format!("bin/{}.cmd", tool_name),
282 format!("bin/{}.bat", tool_name),
283 format!("bin/{}.exe", tool_name),
284 format!("bin/{}.ps1", tool_name),
285 format!("bin/{}", tool_name),
286 ]);
287 }
288
289 #[cfg(not(windows))]
290 {
291 patterns.extend(vec![
292 format!("bin/{}.exe", tool_name),
293 format!("bin/{}", tool_name),
294 ]);
295 }
296
297 for pattern in &patterns {
299 let exe_path = dir.join(pattern);
300 if self.is_executable(&exe_path) {
301 return Ok(exe_path);
302 }
303 }
304
305 if let Ok(entries) = std::fs::read_dir(dir) {
307 for entry in entries.flatten() {
308 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
309 let subdir = entry.path();
310
311 for pattern in &patterns {
313 let exe_path = subdir.join(pattern);
314 if self.is_executable(&exe_path) {
315 return Ok(exe_path);
316 }
317 }
318 }
319 }
320 }
321
322 Err(VxError::ExecutableNotFound {
323 tool_name: tool_name.to_string(),
324 install_dir: dir.to_path_buf(),
325 })
326 }
327
328 fn is_executable(&self, path: &Path) -> bool {
330 if !path.exists() {
331 return false;
332 }
333
334 if let Ok(metadata) = std::fs::metadata(path) {
336 if metadata.is_file() {
337 return true;
338 }
339 }
340
341 if let Ok(metadata) = std::fs::symlink_metadata(path) {
343 if metadata.file_type().is_symlink() {
344 return true;
347 }
348 }
349
350 false
351 }
352
353 pub fn cleanup_unused(&self, keep_latest: usize) -> Result<Vec<String>> {
355 let mut cleaned = Vec::new();
356 let tools_dir = self.get_base_install_dir();
357
358 if !tools_dir.exists() {
359 return Ok(cleaned);
360 }
361
362 for entry in std::fs::read_dir(&tools_dir)? {
363 let entry = entry?;
364 if entry.file_type()?.is_dir() {
365 if let Some(tool_name) = entry.file_name().to_str() {
366 let removed = self.cleanup_tool_versions(tool_name, keep_latest)?;
367 cleaned.extend(removed);
368 }
369 }
370 }
371
372 Ok(cleaned)
373 }
374
375 pub fn cleanup_tool_versions(
377 &self,
378 tool_name: &str,
379 keep_latest: usize,
380 ) -> Result<Vec<String>> {
381 let mut versions = self.list_installed_versions(tool_name)?;
382 let config = self.load_config()?;
383 let active_version = config.active_versions.get(tool_name);
384
385 if versions.len() <= keep_latest {
386 return Ok(Vec::new());
387 }
388
389 versions.sort();
391 versions.reverse();
392
393 let mut removed = Vec::new();
394 let mut kept_count = 0;
395
396 for version in versions {
397 if Some(&version) == active_version {
399 continue;
400 }
401
402 if kept_count < keep_latest {
403 kept_count += 1;
404 continue;
405 }
406
407 let version_dir = self.get_version_install_dir(tool_name, &version);
409 if version_dir.exists() {
410 std::fs::remove_dir_all(&version_dir)?;
411 removed.push(format!("{}@{}", tool_name, version));
412 }
413 }
414
415 Ok(removed)
416 }
417}
418
419impl Default for VxEnvironment {
420 fn default() -> Self {
421 Self::new().expect("Failed to create VX environment")
422 }
423}