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 base_dir = temp_dir.path().join(".vx");
208 let manager = PathManager::with_base_dir(&base_dir).unwrap();
209
210 assert!(manager.tools_dir().exists());
211 assert!(manager.cache_dir().exists());
212 assert!(manager.config_dir().exists());
213 assert!(manager.tmp_dir().exists());
214 }
215
216 #[test]
217 fn test_tool_paths() {
218 let temp_dir = TempDir::new().unwrap();
219 let base_dir = temp_dir.path().join(".vx");
220 let manager = PathManager::with_base_dir(&base_dir).unwrap();
221
222 let tool_dir = manager.tool_dir("node");
223 let version_dir = manager.tool_version_dir("node", "18.17.0");
224 let exe_path = manager.tool_executable_path("node", "18.17.0");
225
226 assert_eq!(tool_dir, base_dir.join("tools/node"));
227 assert_eq!(version_dir, base_dir.join("tools/node/18.17.0"));
228
229 if cfg!(target_os = "windows") {
230 assert_eq!(exe_path, base_dir.join("tools/node/18.17.0/node.exe"));
231 } else {
232 assert_eq!(exe_path, base_dir.join("tools/node/18.17.0/node"));
233 }
234 }
235
236 #[test]
237 fn test_tool_version_management() {
238 let temp_dir = TempDir::new().unwrap();
239 let base_dir = temp_dir.path().join(".vx");
240 let manager = PathManager::with_base_dir(&base_dir).unwrap();
241
242 assert!(!manager.is_tool_version_installed("node", "18.17.0"));
244 assert_eq!(
245 manager.list_tool_versions("node").unwrap(),
246 Vec::<String>::new()
247 );
248
249 let _version_dir = manager.create_tool_version_dir("node", "18.17.0").unwrap();
251 let exe_path = manager.tool_executable_path("node", "18.17.0");
252 std::fs::write(&exe_path, "fake executable").unwrap();
253
254 assert!(manager.is_tool_version_installed("node", "18.17.0"));
256 assert_eq!(manager.list_tool_versions("node").unwrap(), vec!["18.17.0"]);
257 assert_eq!(
258 manager.get_latest_tool_version("node").unwrap(),
259 Some("18.17.0".to_string())
260 );
261 assert_eq!(manager.list_installed_tools().unwrap(), vec!["node"]);
262 }
263}