1use crate::{GlobalToolManager, Result, VxEnvironment, VxError, VxShimManager};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::env;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct VenvConfig {
18 pub name: String,
20 pub tools: HashMap<String, String>, pub created_at: chrono::DateTime<chrono::Utc>,
24 pub modified_at: chrono::DateTime<chrono::Utc>,
26 pub is_active: bool,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct ProjectConfig {
33 pub tools: HashMap<String, String>,
35 pub settings: ProjectSettings,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProjectSettings {
42 pub auto_install: bool,
44 pub cache_duration: String,
46}
47
48impl Default for ProjectSettings {
49 fn default() -> Self {
50 Self {
51 auto_install: true,
52 cache_duration: "7d".to_string(),
53 }
54 }
55}
56
57pub struct VenvManager {
59 env: VxEnvironment,
61 #[allow(dead_code)]
63 global_manager: GlobalToolManager,
64 venvs_dir: PathBuf,
66}
67
68impl VenvManager {
69 pub fn new() -> Result<Self> {
71 let env = VxEnvironment::new()?;
72 let global_manager = GlobalToolManager::new()?;
73
74 let venvs_dir = env
76 .get_base_install_dir()
77 .parent()
78 .ok_or_else(|| VxError::Other {
79 message: "Failed to get VX data directory".to_string(),
80 })?
81 .join("venvs");
82
83 std::fs::create_dir_all(&venvs_dir).map_err(|e| VxError::Other {
85 message: format!("Failed to create venvs directory: {}", e),
86 })?;
87
88 Ok(Self {
89 env,
90 global_manager,
91 venvs_dir,
92 })
93 }
94
95 pub fn load_project_config(&self) -> Result<Option<ProjectConfig>> {
97 let config_path = std::env::current_dir()
98 .map_err(|e| VxError::Other {
99 message: format!("Failed to get current directory: {}", e),
100 })?
101 .join(".vx.toml");
102
103 if !config_path.exists() {
104 return Ok(None);
105 }
106
107 let content = std::fs::read_to_string(&config_path).map_err(|e| VxError::Other {
108 message: format!("Failed to read .vx.toml: {}", e),
109 })?;
110
111 let config: ProjectConfig = toml::from_str(&content).map_err(|e| VxError::Other {
112 message: format!("Failed to parse .vx.toml: {}", e),
113 })?;
114
115 Ok(Some(config))
116 }
117
118 pub async fn get_project_tool_version(&self, tool_name: &str) -> Result<Option<String>> {
120 if let Some(config) = self.load_project_config()? {
122 if let Some(version) = config.tools.get(tool_name) {
123 return Ok(Some(version.clone()));
124 }
125 }
126
127 Ok(None)
130 }
131
132 pub async fn ensure_tool_available(&self, tool_name: &str) -> Result<PathBuf> {
134 let version = self
136 .get_project_tool_version(tool_name)
137 .await?
138 .unwrap_or_else(|| "latest".to_string());
139
140 let install_dir = self.env.get_version_install_dir(tool_name, &version);
142 let is_installed = self.env.is_version_installed(tool_name, &version);
143
144 if !is_installed {
145 let should_auto_install = if let Some(config) = self.load_project_config()? {
147 config.settings.auto_install
148 } else {
149 true };
151
152 if should_auto_install {
153 return Err(VxError::Other {
155 message: format!(
156 "Tool '{}' version '{}' is not installed. Auto-installation not yet implemented. Run 'vx install {}@{}' to install it.",
157 tool_name, version, tool_name, version
158 ),
159 });
160 } else {
161 return Err(VxError::Other {
162 message: format!(
163 "Tool '{}' version '{}' is not installed. Run 'vx install {}@{}' to install it.",
164 tool_name, version, tool_name, version
165 ),
166 });
167 }
168 }
169
170 self.env.find_executable_in_dir(&install_dir, tool_name)
172 }
173
174 pub fn create(&self, name: &str, tools: &[(String, String)]) -> Result<()> {
176 let venv_dir = self.venvs_dir.join(name);
177
178 if venv_dir.exists() {
179 return Err(VxError::Other {
180 message: format!("Virtual environment '{}' already exists", name),
181 });
182 }
183
184 std::fs::create_dir_all(&venv_dir).map_err(|e| VxError::Other {
186 message: format!("Failed to create venv directory: {}", e),
187 })?;
188 std::fs::create_dir_all(venv_dir.join("bin")).map_err(|e| VxError::Other {
189 message: format!("Failed to create bin directory: {}", e),
190 })?;
191 std::fs::create_dir_all(venv_dir.join("config")).map_err(|e| VxError::Other {
192 message: format!("Failed to create config directory: {}", e),
193 })?;
194
195 let mut tool_versions = HashMap::new();
197 for (tool, version) in tools {
198 tool_versions.insert(tool.clone(), version.clone());
199 }
200
201 let venv_config = VenvConfig {
202 name: name.to_string(),
203 tools: tool_versions,
204 created_at: chrono::Utc::now(),
205 modified_at: chrono::Utc::now(),
206 is_active: false,
207 };
208
209 self.save_venv_config(&venv_config)?;
211
212 for (tool, version) in tools {
214 self.install_tool_for_venv(name, tool, version)?;
215 }
216
217 Ok(())
218 }
219
220 pub fn activate(&self, name: &str) -> Result<String> {
222 let _venv_config = self.load_venv_config(name)?;
223
224 let mut commands = Vec::new();
226
227 commands.push(format!("export VX_VENV={name}"));
229
230 commands.push(format!("export PS1=\"(vx:{name}) $PS1\""));
236
237 Ok(commands.join("\n"))
238 }
239
240 pub fn deactivate() -> String {
242 [
243 "unset VX_VENV",
244 "# Restore original PATH (implementation needed)",
245 "# Restore original PS1 (implementation needed)",
246 ]
247 .join("\n")
248 }
249
250 pub fn list(&self) -> Result<Vec<String>> {
252 let mut venvs = Vec::new();
253
254 if self.venvs_dir.exists() {
255 for entry in std::fs::read_dir(&self.venvs_dir).map_err(|e| VxError::Other {
256 message: format!("Failed to read venvs directory: {}", e),
257 })? {
258 let entry = entry.map_err(|e| VxError::Other {
259 message: format!("Failed to read directory entry: {}", e),
260 })?;
261 if entry
262 .file_type()
263 .map_err(|e| VxError::Other {
264 message: format!("Failed to get file type: {}", e),
265 })?
266 .is_dir()
267 {
268 if let Some(name) = entry.file_name().to_str() {
269 venvs.push(name.to_string());
270 }
271 }
272 }
273 }
274
275 venvs.sort();
276 Ok(venvs)
277 }
278
279 pub fn remove(&self, name: &str) -> Result<()> {
281 let venv_dir = self.venvs_dir.join(name);
282
283 if !venv_dir.exists() {
284 return Err(VxError::Other {
285 message: format!("Virtual environment '{}' does not exist", name),
286 });
287 }
288
289 std::fs::remove_dir_all(&venv_dir).map_err(|e| VxError::Other {
290 message: format!("Failed to remove venv directory: {}", e),
291 })?;
292 Ok(())
293 }
294
295 pub fn current() -> Option<String> {
297 env::var("VX_VENV").ok()
298 }
299
300 pub fn is_active() -> bool {
302 env::var("VX_VENV").is_ok()
303 }
304
305 fn install_tool_for_venv(&self, venv_name: &str, tool: &str, version: &str) -> Result<()> {
307 if !self.env.is_version_installed(tool, version) {
309 return Err(VxError::VersionNotInstalled {
310 tool_name: tool.to_string(),
311 version: version.to_string(),
312 });
313 }
314
315 let installation = self
317 .env
318 .get_installation_info(tool, version)?
319 .ok_or_else(|| VxError::VersionNotInstalled {
320 tool_name: tool.to_string(),
321 version: version.to_string(),
322 })?;
323
324 let shim_manager = VxShimManager::new(self.env.clone())?;
326
327 let venv_dir = self.venvs_dir.join(venv_name);
329 let venv_bin_dir = venv_dir.join("bin");
330 std::fs::create_dir_all(&venv_bin_dir)?;
331
332 shim_manager.create_tool_shim(tool, &installation.executable_path, version, None)?;
334
335 Ok(())
336 }
337
338 fn save_venv_config(&self, config: &VenvConfig) -> Result<()> {
340 let config_dir = self.venvs_dir.join(&config.name).join("config");
341 let config_path = config_dir.join("venv.toml");
342
343 std::fs::create_dir_all(&config_dir).map_err(|e| VxError::Other {
345 message: format!("Failed to create config directory: {}", e),
346 })?;
347
348 let toml_content = toml::to_string_pretty(config).map_err(|e| VxError::Other {
349 message: format!("Failed to serialize venv config: {}", e),
350 })?;
351 std::fs::write(config_path, toml_content).map_err(|e| VxError::Other {
352 message: format!("Failed to write venv config: {}", e),
353 })?;
354 Ok(())
355 }
356
357 fn load_venv_config(&self, name: &str) -> Result<VenvConfig> {
359 let config_path = self.venvs_dir.join(name).join("config").join("venv.toml");
360
361 if !config_path.exists() {
362 return Err(VxError::Other {
363 message: format!("Virtual environment '{}' configuration not found", name),
364 });
365 }
366
367 let content = std::fs::read_to_string(config_path).map_err(|e| VxError::Other {
368 message: format!("Failed to read venv config: {}", e),
369 })?;
370 let config: VenvConfig = toml::from_str(&content).map_err(|e| VxError::Other {
371 message: format!("Failed to parse venv config: {}", e),
372 })?;
373 Ok(config)
374 }
375}
376
377impl Default for VenvManager {
378 fn default() -> Self {
379 Self::new().expect("Failed to create VenvManager")
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_venv_manager_creation() {
389 let manager = VenvManager::new();
390 assert!(manager.is_ok());
391 }
392
393 #[test]
394 fn test_current_venv() {
395 assert!(!VenvManager::is_active());
397 }
398}