vx_core/
environment.rs

1//! Environment management for VX tool manager
2//!
3//! This module handles:
4//! - Tool installation directories
5//! - Version management
6//! - Environment isolation
7//! - PATH management
8
9use crate::{Result, VxError};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14/// VX environment manager
15#[derive(Debug, Clone)]
16pub struct VxEnvironment {
17    /// Base directory for all VX installations
18    base_dir: PathBuf,
19    /// Configuration directory
20    config_dir: PathBuf,
21    /// Cache directory
22    cache_dir: PathBuf,
23}
24
25/// Tool installation information
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ToolInstallation {
28    /// Tool name
29    pub tool_name: String,
30    /// Installed version
31    pub version: String,
32    /// Installation directory
33    pub install_dir: PathBuf,
34    /// Executable path
35    pub executable_path: PathBuf,
36    /// Installation timestamp
37    pub installed_at: chrono::DateTime<chrono::Utc>,
38    /// Whether this is the active version
39    pub is_active: bool,
40}
41
42/// Environment configuration
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EnvironmentConfig {
45    /// Active versions for each tool
46    pub active_versions: HashMap<String, String>,
47    /// Global settings
48    pub global_settings: HashMap<String, String>,
49}
50
51impl VxEnvironment {
52    /// Create a new VX environment
53    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        // Ensure directories exist
59        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    /// Get VX home directory
71    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    /// Get base installation directory
84    pub fn get_base_install_dir(&self) -> PathBuf {
85        self.base_dir.join("tools")
86    }
87
88    /// Get tool installation directory
89    pub fn get_tool_install_dir(&self, tool_name: &str) -> PathBuf {
90        self.get_base_install_dir().join(tool_name)
91    }
92
93    /// Get version installation directory
94    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    /// Get cache directory for downloads
99    pub fn get_cache_dir(&self) -> PathBuf {
100        self.cache_dir.clone()
101    }
102
103    /// Get download cache directory for a tool
104    pub fn get_tool_cache_dir(&self, tool_name: &str) -> PathBuf {
105        self.cache_dir.join("downloads").join(tool_name)
106    }
107
108    /// Get configuration file path
109    pub fn get_config_file(&self) -> PathBuf {
110        self.config_dir.join("environment.toml")
111    }
112
113    /// Load environment configuration
114    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    /// Save environment configuration
134    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    /// Get active version for a tool
145    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    /// Set active version for a tool
151    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    /// List all installed versions for a tool
161    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        // Sort versions (simple string sort for now, could be improved with semver)
179        versions.sort();
180        Ok(versions)
181    }
182
183    /// Check if a version is installed
184    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    /// Get installation info for a tool version
190    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        // Try to find the executable
204        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(), // TODO: Get actual installation time
212            is_active,
213        }))
214    }
215
216    /// Find executable in installation directory
217    pub fn find_executable_in_dir(&self, dir: &Path, tool_name: &str) -> Result<PathBuf> {
218        // Common executable patterns (including nested directories)
219        // On Windows, prioritize .cmd and .exe files over files without extensions
220        let mut patterns = vec![];
221
222        // Add Windows-specific patterns first (higher priority)
223        #[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        // Add generic patterns
234        #[cfg(not(windows))]
235        {
236            patterns.extend(vec![format!("{}.exe", tool_name), tool_name.to_string()]);
237        }
238
239        // On Windows, add the no-extension pattern last (lowest priority)
240        #[cfg(windows)]
241        {
242            patterns.push(tool_name.to_string());
243        }
244
245        // Add bin subdirectory patterns
246        #[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        // First try direct patterns
266        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 not found, search recursively in subdirectories (common for archives)
274        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                    // Try patterns in subdirectory
280                    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    /// Check if a path is an executable (handles symlinks)
297    fn is_executable(&self, path: &Path) -> bool {
298        if !path.exists() {
299            return false;
300        }
301
302        // Check if it's a regular file or symlink
303        if let Ok(metadata) = std::fs::metadata(path) {
304            if metadata.is_file() {
305                return true;
306            }
307        }
308
309        // Check if it's a symlink that points to an executable
310        if let Ok(metadata) = std::fs::symlink_metadata(path) {
311            if metadata.file_type().is_symlink() {
312                // For symlinks, we consider them executable if they exist
313                // (the target might be executable even if we can't check it directly)
314                return true;
315            }
316        }
317
318        false
319    }
320
321    /// Clean up unused installations
322    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    /// Clean up old versions of a specific tool
344    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        // Sort versions (newest first)
358        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            // Always keep the active version
366            if Some(&version) == active_version {
367                continue;
368            }
369
370            if kept_count < keep_latest {
371                kept_count += 1;
372                continue;
373            }
374
375            // Remove this version
376            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}