1use crate::{with_executable_extension, VxPaths};
4use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub struct PathManager {
10 paths: VxPaths,
11}
12
13impl PathManager {
14 pub fn new() -> Result<Self> {
16 let paths = VxPaths::new()?;
17 paths.ensure_dirs()?;
18 Ok(Self { paths })
19 }
20
21 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 pub fn from_paths(paths: VxPaths) -> Self {
30 Self { paths }
31 }
32
33 pub fn base_dir(&self) -> &Path {
35 &self.paths.base_dir
36 }
37
38 pub fn tools_dir(&self) -> &Path {
40 &self.paths.tools_dir
41 }
42
43 pub fn cache_dir(&self) -> &Path {
45 &self.paths.cache_dir
46 }
47
48 pub fn config_dir(&self) -> &Path {
50 &self.paths.config_dir
51 }
52
53 pub fn tmp_dir(&self) -> &Path {
55 &self.paths.tmp_dir
56 }
57 pub fn tool_dir(&self, tool_name: &str) -> PathBuf {
60 self.paths.tools_dir.join(tool_name)
61 }
62
63 pub fn tool_version_dir(&self, tool_name: &str, version: &str) -> PathBuf {
66 self.tool_dir(tool_name).join(version)
67 }
68
69 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 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 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 if self.is_tool_version_installed(tool_name, version) {
99 versions.push(version.to_string());
100 }
101 }
102 }
103 }
104
105 versions.sort();
107 Ok(versions)
108 }
109
110 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 let latest = versions.into_iter().max();
121 Ok(latest)
122 }
123 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 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 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 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 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 pub fn tool_cache_dir(&self, tool_name: &str) -> PathBuf {
179 self.paths.cache_dir.join(tool_name)
180 }
181
182 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 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 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 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 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}