vx_paths/
manager.rs

1//! Path manager for vx tool installations
2
3use crate::{with_executable_extension, VxPaths};
4use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7/// Manages paths for vx tool installations with standardized structure
8#[derive(Debug, Clone)]
9pub struct PathManager {
10    paths: VxPaths,
11}
12
13impl PathManager {
14    /// Create a new PathManager with default paths
15    pub fn new() -> Result<Self> {
16        let paths = VxPaths::new()?;
17        paths.ensure_dirs()?;
18        Ok(Self { paths })
19    }
20
21    /// Create a new PathManager with custom base directory
22    pub fn with_base_dir<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
23        let paths = VxPaths::with_base_dir(base_dir);
24        paths.ensure_dirs()?;
25        Ok(Self { paths })
26    }
27
28    /// Create a PathManager from existing VxPaths
29    pub fn from_paths(paths: VxPaths) -> Self {
30        Self { paths }
31    }
32
33    /// Get the base vx directory
34    pub fn base_dir(&self) -> &Path {
35        &self.paths.base_dir
36    }
37
38    /// Get the tools directory
39    pub fn tools_dir(&self) -> &Path {
40        &self.paths.tools_dir
41    }
42
43    /// Get the cache directory
44    pub fn cache_dir(&self) -> &Path {
45        &self.paths.cache_dir
46    }
47
48    /// Get the config directory
49    pub fn config_dir(&self) -> &Path {
50        &self.paths.config_dir
51    }
52
53    /// Get the temporary directory
54    pub fn tmp_dir(&self) -> &Path {
55        &self.paths.tmp_dir
56    }
57    /// Get the installation directory for a specific tool
58    /// Returns: ~/.vx/tools/<tool>
59    pub fn tool_dir(&self, tool_name: &str) -> PathBuf {
60        self.paths.tools_dir.join(tool_name)
61    }
62
63    /// Get the installation directory for a specific tool version
64    /// Returns: ~/.vx/tools/<tool>/<version>
65    pub fn tool_version_dir(&self, tool_name: &str, version: &str) -> PathBuf {
66        self.tool_dir(tool_name).join(version)
67    }
68
69    /// Get the executable path for a specific tool version
70    /// Returns: ~/.vx/tools/<tool>/<version>/<tool>.exe (Windows) or ~/.vx/tools/<tool>/<version>/<tool> (Unix)
71    pub fn tool_executable_path(&self, tool_name: &str, version: &str) -> PathBuf {
72        let version_dir = self.tool_version_dir(tool_name, version);
73        let executable_name = with_executable_extension(tool_name);
74        version_dir.join(executable_name)
75    }
76
77    /// Check if a tool version is installed
78    pub fn is_tool_version_installed(&self, tool_name: &str, version: &str) -> bool {
79        let exe_path = self.tool_executable_path(tool_name, version);
80        exe_path.exists()
81    }
82
83    /// List all installed versions of a tool
84    pub fn list_tool_versions(&self, tool_name: &str) -> Result<Vec<String>> {
85        let tool_dir = self.tool_dir(tool_name);
86
87        if !tool_dir.exists() {
88            return Ok(Vec::new());
89        }
90
91        let mut versions = Vec::new();
92
93        for entry in std::fs::read_dir(&tool_dir)? {
94            let entry = entry?;
95            if entry.file_type()?.is_dir() {
96                if let Some(version) = entry.file_name().to_str() {
97                    // Verify that the executable exists in this version directory
98                    if self.is_tool_version_installed(tool_name, version) {
99                        versions.push(version.to_string());
100                    }
101                }
102            }
103        }
104
105        // Sort versions (simple string sort for now, could be improved with semver)
106        versions.sort();
107        Ok(versions)
108    }
109
110    /// Get the latest installed version of a tool
111    pub fn get_latest_tool_version(&self, tool_name: &str) -> Result<Option<String>> {
112        let versions = self.list_tool_versions(tool_name)?;
113
114        if versions.is_empty() {
115            return Ok(None);
116        }
117
118        // For now, use simple string comparison
119        // TODO: Implement proper semantic version comparison
120        let latest = versions.into_iter().max();
121        Ok(latest)
122    }
123    /// List all installed tools
124    pub fn list_installed_tools(&self) -> Result<Vec<String>> {
125        let tools_dir = &self.paths.tools_dir;
126
127        if !tools_dir.exists() {
128            return Ok(Vec::new());
129        }
130
131        let mut tools = Vec::new();
132
133        for entry in std::fs::read_dir(tools_dir)? {
134            let entry = entry?;
135            if entry.file_type()?.is_dir() {
136                if let Some(tool_name) = entry.file_name().to_str() {
137                    // Check if this tool has any valid versions installed
138                    let versions = self.list_tool_versions(tool_name)?;
139                    if !versions.is_empty() {
140                        tools.push(tool_name.to_string());
141                    }
142                }
143            }
144        }
145
146        tools.sort();
147        Ok(tools)
148    }
149
150    /// Create the directory structure for a tool version
151    pub fn create_tool_version_dir(&self, tool_name: &str, version: &str) -> Result<PathBuf> {
152        let version_dir = self.tool_version_dir(tool_name, version);
153        std::fs::create_dir_all(&version_dir)?;
154        Ok(version_dir)
155    }
156
157    /// Remove a specific tool version
158    pub fn remove_tool_version(&self, tool_name: &str, version: &str) -> Result<()> {
159        let version_dir = self.tool_version_dir(tool_name, version);
160
161        if version_dir.exists() {
162            std::fs::remove_dir_all(&version_dir)?;
163        }
164
165        // If this was the last version, remove the tool directory
166        let tool_dir = self.tool_dir(tool_name);
167        if tool_dir.exists() {
168            let remaining_versions = self.list_tool_versions(tool_name)?;
169            if remaining_versions.is_empty() {
170                std::fs::remove_dir_all(&tool_dir)?;
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Get cache path for a tool
178    pub fn tool_cache_dir(&self, tool_name: &str) -> PathBuf {
179        self.paths.cache_dir.join(tool_name)
180    }
181
182    /// Get temporary path for a tool installation
183    pub fn tool_tmp_dir(&self, tool_name: &str, version: &str) -> PathBuf {
184        self.paths
185            .tmp_dir
186            .join(format!("{}-{}", tool_name, version))
187    }
188}
189impl Default for PathManager {
190    fn default() -> Self {
191        Self::new().unwrap_or_else(|_| {
192            // Fallback to current directory if home directory is not available
193            Self::with_base_dir(".vx")
194                .expect("Failed to create PathManager with fallback directory")
195        })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use tempfile::TempDir;
203
204    #[test]
205    fn test_path_manager_creation() {
206        let temp_dir = TempDir::new().unwrap();
207        let manager = PathManager::with_base_dir(temp_dir.path()).unwrap();
208
209        assert!(manager.tools_dir().exists());
210        assert!(manager.cache_dir().exists());
211        assert!(manager.config_dir().exists());
212        assert!(manager.tmp_dir().exists());
213    }
214
215    #[test]
216    fn test_tool_paths() {
217        let temp_dir = TempDir::new().unwrap();
218        let manager = PathManager::with_base_dir(temp_dir.path()).unwrap();
219
220        let tool_dir = manager.tool_dir("node");
221        let version_dir = manager.tool_version_dir("node", "18.17.0");
222        let exe_path = manager.tool_executable_path("node", "18.17.0");
223
224        assert_eq!(tool_dir, temp_dir.path().join(".vx/tools/node"));
225        assert_eq!(version_dir, temp_dir.path().join(".vx/tools/node/18.17.0"));
226
227        if cfg!(target_os = "windows") {
228            assert_eq!(
229                exe_path,
230                temp_dir.path().join(".vx/tools/node/18.17.0/node.exe")
231            );
232        } else {
233            assert_eq!(
234                exe_path,
235                temp_dir.path().join(".vx/tools/node/18.17.0/node")
236            );
237        }
238    }
239
240    #[test]
241    fn test_tool_version_management() {
242        let temp_dir = TempDir::new().unwrap();
243        let manager = PathManager::with_base_dir(temp_dir.path()).unwrap();
244
245        // Initially no versions
246        assert!(!manager.is_tool_version_installed("node", "18.17.0"));
247        assert_eq!(
248            manager.list_tool_versions("node").unwrap(),
249            Vec::<String>::new()
250        );
251
252        // Create version directory and executable
253        let _version_dir = manager.create_tool_version_dir("node", "18.17.0").unwrap();
254        let exe_path = manager.tool_executable_path("node", "18.17.0");
255        std::fs::write(&exe_path, "fake executable").unwrap();
256
257        // Now it should be detected
258        assert!(manager.is_tool_version_installed("node", "18.17.0"));
259        assert_eq!(manager.list_tool_versions("node").unwrap(), vec!["18.17.0"]);
260        assert_eq!(
261            manager.get_latest_tool_version("node").unwrap(),
262            Some("18.17.0".to_string())
263        );
264        assert_eq!(manager.list_installed_tools().unwrap(), vec!["node"]);
265    }
266}