1use crate::{GlobalToolManager, Result, VxEnvironment, VxError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SymlinkVenv {
15 pub name: String,
17 pub path: PathBuf,
19 pub linked_tools: HashMap<String, String>, pub created_at: chrono::DateTime<chrono::Utc>,
23 pub modified_at: chrono::DateTime<chrono::Utc>,
25}
26
27#[derive(Debug)]
29pub struct SymlinkVenvManager {
30 env: VxEnvironment,
32 global_manager: GlobalToolManager,
34 registry_path: PathBuf,
36}
37
38impl SymlinkVenvManager {
39 pub fn new() -> Result<Self> {
41 let env = VxEnvironment::new()?;
42 let global_manager = GlobalToolManager::new()?;
43
44 let data_dir = env
45 .get_base_install_dir()
46 .parent()
47 .ok_or_else(|| VxError::Other {
48 message: "Failed to get VX data directory".to_string(),
49 })?
50 .to_path_buf();
51
52 let registry_path = data_dir.join("symlink_venvs.json");
53
54 Ok(Self {
55 env,
56 global_manager,
57 registry_path,
58 })
59 }
60
61 pub async fn create_venv(&self, name: &str, path: &Path) -> Result<()> {
63 if self.venv_exists(name).await? {
65 return Err(VxError::Other {
66 message: format!("Virtual environment '{}' already exists", name),
67 });
68 }
69
70 fs::create_dir_all(path).await.map_err(|e| VxError::Other {
72 message: format!("Failed to create venv directory: {}", e),
73 })?;
74
75 let bin_dir = path.join("bin");
77 fs::create_dir_all(&bin_dir)
78 .await
79 .map_err(|e| VxError::Other {
80 message: format!("Failed to create venv bin directory: {}", e),
81 })?;
82
83 let venv = SymlinkVenv {
85 name: name.to_string(),
86 path: path.to_path_buf(),
87 linked_tools: HashMap::new(),
88 created_at: chrono::Utc::now(),
89 modified_at: chrono::Utc::now(),
90 };
91
92 let mut venvs = self.load_venvs().await?;
94 venvs.insert(name.to_string(), venv);
95 self.save_venvs(&venvs).await?;
96
97 Ok(())
98 }
99
100 pub async fn link_tool(&self, venv_name: &str, tool_name: &str, version: &str) -> Result<()> {
102 if !self.global_manager.is_tool_installed(tool_name).await? {
104 return Err(VxError::Other {
105 message: format!("Tool '{}' is not globally installed", tool_name),
106 });
107 }
108
109 let mut venvs = self.load_venvs().await?;
111 let venv = venvs.get_mut(venv_name).ok_or_else(|| VxError::Other {
112 message: format!("Virtual environment '{}' not found", venv_name),
113 })?;
114
115 let global_install_dir = self.env.get_version_install_dir(tool_name, version);
117 let global_exe = self
118 .env
119 .find_executable_in_dir(&global_install_dir, tool_name)?;
120
121 let venv_bin_dir = venv.path.join("bin");
123 let venv_exe = venv_bin_dir.join(tool_name);
124
125 if venv_exe.exists() {
127 fs::remove_file(&venv_exe)
128 .await
129 .map_err(|e| VxError::Other {
130 message: format!("Failed to remove existing symlink: {}", e),
131 })?;
132 }
133
134 self.create_symlink(&global_exe, &venv_exe).await?;
136
137 venv.linked_tools
139 .insert(tool_name.to_string(), version.to_string());
140 venv.modified_at = chrono::Utc::now();
141
142 self.global_manager
144 .add_venv_dependency(venv_name, tool_name)
145 .await?;
146
147 self.save_venvs(&venvs).await?;
149
150 Ok(())
151 }
152
153 pub async fn unlink_tool(&self, venv_name: &str, tool_name: &str) -> Result<()> {
155 let mut venvs = self.load_venvs().await?;
157 let venv = venvs.get_mut(venv_name).ok_or_else(|| VxError::Other {
158 message: format!("Virtual environment '{}' not found", venv_name),
159 })?;
160
161 let venv_exe = venv.path.join("bin").join(tool_name);
163 if venv_exe.exists() {
164 fs::remove_file(&venv_exe)
165 .await
166 .map_err(|e| VxError::Other {
167 message: format!("Failed to remove symlink: {}", e),
168 })?;
169 }
170
171 venv.linked_tools.remove(tool_name);
173 venv.modified_at = chrono::Utc::now();
174
175 self.global_manager
177 .remove_venv_dependency(venv_name, tool_name)
178 .await?;
179
180 self.save_venvs(&venvs).await?;
182
183 Ok(())
184 }
185
186 pub async fn remove_venv(&self, name: &str) -> Result<()> {
188 let mut venvs = self.load_venvs().await?;
189
190 if let Some(venv) = venvs.get(name) {
191 for tool_name in venv.linked_tools.keys() {
193 self.global_manager
194 .remove_venv_dependency(name, tool_name)
195 .await?;
196 }
197
198 if venv.path.exists() {
200 fs::remove_dir_all(&venv.path)
201 .await
202 .map_err(|e| VxError::Other {
203 message: format!("Failed to remove venv directory: {}", e),
204 })?;
205 }
206
207 venvs.remove(name);
209 self.save_venvs(&venvs).await?;
210 }
211
212 Ok(())
213 }
214
215 pub async fn list_venvs(&self) -> Result<Vec<SymlinkVenv>> {
217 let venvs = self.load_venvs().await?;
218 Ok(venvs.into_values().collect())
219 }
220
221 pub async fn get_venv(&self, name: &str) -> Result<Option<SymlinkVenv>> {
223 let venvs = self.load_venvs().await?;
224 Ok(venvs.get(name).cloned())
225 }
226
227 pub async fn venv_exists(&self, name: &str) -> Result<bool> {
229 let venvs = self.load_venvs().await?;
230 Ok(venvs.contains_key(name))
231 }
232
233 async fn create_symlink(&self, target: &Path, link: &Path) -> Result<()> {
235 #[cfg(unix)]
236 {
237 tokio::fs::symlink(target, link)
238 .await
239 .map_err(|e| VxError::Other {
240 message: format!("Failed to create symlink: {}", e),
241 })
242 }
243
244 #[cfg(windows)]
245 {
246 match tokio::fs::symlink_file(target, link).await {
248 Ok(()) => Ok(()),
249 Err(_) => {
250 tokio::fs::copy(target, link)
252 .await
253 .map_err(|e| VxError::Other {
254 message: format!("Failed to copy file (symlink fallback): {}", e),
255 })?;
256 Ok(())
257 }
258 }
259 }
260 }
261
262 async fn load_venvs(&self) -> Result<HashMap<String, SymlinkVenv>> {
264 if !self.registry_path.exists() {
265 return Ok(HashMap::new());
266 }
267
268 let content =
269 fs::read_to_string(&self.registry_path)
270 .await
271 .map_err(|e| VxError::Other {
272 message: format!("Failed to read venv registry: {}", e),
273 })?;
274
275 serde_json::from_str(&content).map_err(|e| VxError::Other {
276 message: format!("Failed to parse venv registry: {}", e),
277 })
278 }
279
280 async fn save_venvs(&self, venvs: &HashMap<String, SymlinkVenv>) -> Result<()> {
282 if let Some(parent) = self.registry_path.parent() {
284 fs::create_dir_all(parent)
285 .await
286 .map_err(|e| VxError::Other {
287 message: format!("Failed to create registry directory: {}", e),
288 })?;
289 }
290
291 let content = serde_json::to_string_pretty(venvs).map_err(|e| VxError::Other {
292 message: format!("Failed to serialize venv registry: {}", e),
293 })?;
294
295 fs::write(&self.registry_path, content)
296 .await
297 .map_err(|e| VxError::Other {
298 message: format!("Failed to write venv registry: {}", e),
299 })
300 }
301}
302
303impl Default for SymlinkVenvManager {
304 fn default() -> Self {
305 Self::new().expect("Failed to create SymlinkVenvManager")
306 }
307}