vx_core/
venv.rs

1//! Simplified virtual environment management for vx
2//!
3//! This module provides a simplified virtual environment system that:
4//! - Uses transparent proxy approach (no explicit activation needed)
5//! - Automatically detects project configuration (.vx.toml)
6//! - Manages tool versions through global installation + PATH manipulation
7//! - Provides seamless user experience similar to nvm/pnpm
8
9use crate::{GlobalToolManager, Result, VxEnvironment, VxError, VxShimManager};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::env;
13use std::path::PathBuf;
14
15/// Simplified virtual environment configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct VenvConfig {
18    /// Virtual environment name
19    pub name: String,
20    /// Tools and their versions
21    pub tools: HashMap<String, String>, // tool_name -> version
22    /// Creation timestamp
23    pub created_at: chrono::DateTime<chrono::Utc>,
24    /// Last modified timestamp
25    pub modified_at: chrono::DateTime<chrono::Utc>,
26    /// Whether this venv is currently active
27    pub is_active: bool,
28}
29
30/// Project configuration from .vx.toml
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct ProjectConfig {
33    /// Tools required by this project
34    pub tools: HashMap<String, String>,
35    /// Project settings
36    pub settings: ProjectSettings,
37}
38
39/// Project settings
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProjectSettings {
42    /// Automatically install missing tools
43    pub auto_install: bool,
44    /// Cache duration for version checks
45    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
57/// Simplified virtual environment manager
58pub struct VenvManager {
59    /// VX environment for path management
60    env: VxEnvironment,
61    /// Global tool manager for tool installation
62    #[allow(dead_code)]
63    global_manager: GlobalToolManager,
64    /// Path to venvs directory
65    venvs_dir: PathBuf,
66}
67
68impl VenvManager {
69    /// Create a new VenvManager instance
70    pub fn new() -> Result<Self> {
71        let env = VxEnvironment::new()?;
72        let global_manager = GlobalToolManager::new()?;
73
74        // Get venvs directory from VX environment
75        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        // Ensure venvs directory exists
84        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    /// Load project configuration from .vx.toml
96    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    /// Get the tool version for the current project context
119    pub async fn get_project_tool_version(&self, tool_name: &str) -> Result<Option<String>> {
120        // First check project configuration
121        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        // TODO: Check for tool-specific version files (.nvmrc, .python-version, etc.)
128
129        Ok(None)
130    }
131
132    /// Ensure a tool is available for the current project
133    pub async fn ensure_tool_available(&self, tool_name: &str) -> Result<PathBuf> {
134        // Get the required version for this project
135        let version = self
136            .get_project_tool_version(tool_name)
137            .await?
138            .unwrap_or_else(|| "latest".to_string());
139
140        // Check if the specific version is installed
141        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            // Auto-install if enabled
146            let should_auto_install = if let Some(config) = self.load_project_config()? {
147                config.settings.auto_install
148            } else {
149                true // Default to auto-install
150            };
151
152            if should_auto_install {
153                // TODO: Implement auto-installation through plugin system
154                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        // Get the executable path
171        self.env.find_executable_in_dir(&install_dir, tool_name)
172    }
173
174    /// Create a new virtual environment
175    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        // Create venv directory structure
185        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        // Create venv configuration
196        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        // Save configuration
210        self.save_venv_config(&venv_config)?;
211
212        // TODO: Install tools for this venv
213        for (tool, version) in tools {
214            self.install_tool_for_venv(name, tool, version)?;
215        }
216
217        Ok(())
218    }
219
220    /// Activate a virtual environment (returns shell commands to execute)
221    pub fn activate(&self, name: &str) -> Result<String> {
222        let _venv_config = self.load_venv_config(name)?;
223
224        // Generate activation script
225        let mut commands = Vec::new();
226
227        // Set VX_VENV environment variable
228        commands.push(format!("export VX_VENV={name}"));
229
230        // In the simplified design, we don't modify PATH directly
231        // Instead, vx commands will automatically use the correct tool versions
232        // based on the project configuration
233
234        // Set prompt indicator
235        commands.push(format!("export PS1=\"(vx:{name}) $PS1\""));
236
237        Ok(commands.join("\n"))
238    }
239
240    /// Deactivate current virtual environment
241    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    /// List all virtual environments
251    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    /// Remove a virtual environment
280    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    /// Get current active virtual environment
296    pub fn current() -> Option<String> {
297        env::var("VX_VENV").ok()
298    }
299
300    /// Check if we're in a virtual environment
301    pub fn is_active() -> bool {
302        env::var("VX_VENV").is_ok()
303    }
304
305    /// Install a tool for a specific virtual environment
306    fn install_tool_for_venv(&self, venv_name: &str, tool: &str, version: &str) -> Result<()> {
307        // Check if the tool version is already installed globally
308        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        // Get the installation info to find the executable path
316        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        // Create shim manager for this venv
325        let shim_manager = VxShimManager::new(self.env.clone())?;
326
327        // Create a venv-specific shim directory
328        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        // Create shim for this tool in the venv
333        shim_manager.create_tool_shim(tool, &installation.executable_path, version, None)?;
334
335        Ok(())
336    }
337
338    /// Save virtual environment configuration
339    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        // Ensure config directory exists
344        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    /// Load virtual environment configuration
358    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        // Should return None when not in a venv
396        assert!(!VenvManager::is_active());
397    }
398}