vx_core/
venv.rs

1// Virtual environment management for vx
2// Similar to Python's venv, allows users to enter an isolated environment
3
4use crate::{Result, VxError};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::env;
8use std::path::PathBuf;
9
10/// Virtual environment configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct VenvConfig {
13    pub name: String,
14    pub tools: HashMap<String, String>, // tool_name -> version
15    pub path_entries: Vec<PathBuf>,
16    pub env_vars: HashMap<String, String>,
17}
18
19/// Virtual environment manager
20pub struct VenvManager {
21    venvs_dir: PathBuf,
22}
23
24impl VenvManager {
25    pub fn new() -> Result<Self> {
26        // Get venvs directory from VX_HOME or config or use default
27        let venvs_dir = if let Ok(vx_home) = env::var("VX_HOME") {
28            PathBuf::from(vx_home).join("venvs")
29        } else if let Some(config_dir) = dirs::config_dir() {
30            config_dir.join("vx").join("venvs")
31        } else {
32            dirs::home_dir()
33                .unwrap_or_else(|| std::env::current_dir().unwrap())
34                .join(".vx")
35                .join("venvs")
36        };
37
38        // Ensure venvs directory exists
39        std::fs::create_dir_all(&venvs_dir).map_err(|e| VxError::Other {
40            message: format!("Failed to create venvs directory: {}", e),
41        })?;
42
43        Ok(Self { venvs_dir })
44    }
45
46    /// Create a new virtual environment
47    pub fn create(&self, name: &str, tools: &[(String, String)]) -> Result<()> {
48        let venv_dir = self.venvs_dir.join(name);
49
50        if venv_dir.exists() {
51            return Err(VxError::Other {
52                message: format!("Virtual environment '{}' already exists", name),
53            });
54        }
55
56        // Create venv directory structure
57        std::fs::create_dir_all(&venv_dir).map_err(|e| VxError::Other {
58            message: format!("Failed to create venv directory: {}", e),
59        })?;
60        std::fs::create_dir_all(venv_dir.join("bin")).map_err(|e| VxError::Other {
61            message: format!("Failed to create bin directory: {}", e),
62        })?;
63        std::fs::create_dir_all(venv_dir.join("config")).map_err(|e| VxError::Other {
64            message: format!("Failed to create config directory: {}", e),
65        })?;
66
67        // Create venv configuration
68        let mut tool_versions = HashMap::new();
69        for (tool, version) in tools {
70            tool_versions.insert(tool.clone(), version.clone());
71        }
72
73        let venv_config = VenvConfig {
74            name: name.to_string(),
75            tools: tool_versions,
76            path_entries: vec![venv_dir.join("bin")],
77            env_vars: HashMap::new(),
78        };
79
80        // Save configuration
81        self.save_venv_config(&venv_config)?;
82
83        // TODO: Install tools for this venv
84        for (tool, version) in tools {
85            self.install_tool_for_venv(name, tool, version)?;
86        }
87
88        Ok(())
89    }
90
91    /// Activate a virtual environment (returns shell commands to execute)
92    pub fn activate(&self, name: &str) -> Result<String> {
93        let venv_config = self.load_venv_config(name)?;
94
95        // Generate activation script
96        let mut commands = Vec::new();
97
98        // Set VX_VENV environment variable
99        commands.push(format!("export VX_VENV={name}"));
100
101        // Prepend venv bin directory to PATH
102        for path_entry in &venv_config.path_entries {
103            commands.push(format!("export PATH={}:$PATH", path_entry.display()));
104        }
105
106        // Set custom environment variables
107        for (key, value) in &venv_config.env_vars {
108            commands.push(format!("export {key}={value}"));
109        }
110
111        // Set prompt indicator
112        commands.push(format!("export PS1=\"(vx:{name}) $PS1\""));
113
114        Ok(commands.join("\n"))
115    }
116
117    /// Deactivate current virtual environment
118    pub fn deactivate() -> String {
119        [
120            "unset VX_VENV",
121            "# Restore original PATH (implementation needed)",
122            "# Restore original PS1 (implementation needed)",
123        ]
124        .join("\n")
125    }
126
127    /// List all virtual environments
128    pub fn list(&self) -> Result<Vec<String>> {
129        let mut venvs = Vec::new();
130
131        if self.venvs_dir.exists() {
132            for entry in std::fs::read_dir(&self.venvs_dir).map_err(|e| VxError::Other {
133                message: format!("Failed to read venvs directory: {}", e),
134            })? {
135                let entry = entry.map_err(|e| VxError::Other {
136                    message: format!("Failed to read directory entry: {}", e),
137                })?;
138                if entry
139                    .file_type()
140                    .map_err(|e| VxError::Other {
141                        message: format!("Failed to get file type: {}", e),
142                    })?
143                    .is_dir()
144                {
145                    if let Some(name) = entry.file_name().to_str() {
146                        venvs.push(name.to_string());
147                    }
148                }
149            }
150        }
151
152        venvs.sort();
153        Ok(venvs)
154    }
155
156    /// Remove a virtual environment
157    pub fn remove(&self, name: &str) -> Result<()> {
158        let venv_dir = self.venvs_dir.join(name);
159
160        if !venv_dir.exists() {
161            return Err(VxError::Other {
162                message: format!("Virtual environment '{}' does not exist", name),
163            });
164        }
165
166        std::fs::remove_dir_all(&venv_dir).map_err(|e| VxError::Other {
167            message: format!("Failed to remove venv directory: {}", e),
168        })?;
169        Ok(())
170    }
171
172    /// Get current active virtual environment
173    pub fn current() -> Option<String> {
174        env::var("VX_VENV").ok()
175    }
176
177    /// Check if we're in a virtual environment
178    pub fn is_active() -> bool {
179        env::var("VX_VENV").is_ok()
180    }
181
182    /// Install a tool for a specific virtual environment
183    fn install_tool_for_venv(&self, _venv_name: &str, _tool: &str, _version: &str) -> Result<()> {
184        // TODO: Implement actual tool installation
185        Ok(())
186    }
187
188    /// Save virtual environment configuration
189    fn save_venv_config(&self, config: &VenvConfig) -> Result<()> {
190        let config_path = self
191            .venvs_dir
192            .join(&config.name)
193            .join("config")
194            .join("venv.toml");
195        let toml_content = toml::to_string_pretty(config).map_err(|e| VxError::Other {
196            message: format!("Failed to serialize venv config: {}", e),
197        })?;
198        std::fs::write(config_path, toml_content).map_err(|e| VxError::Other {
199            message: format!("Failed to write venv config: {}", e),
200        })?;
201        Ok(())
202    }
203
204    /// Load virtual environment configuration
205    fn load_venv_config(&self, name: &str) -> Result<VenvConfig> {
206        let config_path = self.venvs_dir.join(name).join("config").join("venv.toml");
207
208        if !config_path.exists() {
209            return Err(VxError::Other {
210                message: format!("Virtual environment '{}' configuration not found", name),
211            });
212        }
213
214        let content = std::fs::read_to_string(config_path).map_err(|e| VxError::Other {
215            message: format!("Failed to read venv config: {}", e),
216        })?;
217        let config: VenvConfig = toml::from_str(&content).map_err(|e| VxError::Other {
218            message: format!("Failed to parse venv config: {}", e),
219        })?;
220        Ok(config)
221    }
222}
223
224impl Default for VenvManager {
225    fn default() -> Self {
226        Self::new().expect("Failed to create VenvManager")
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_venv_manager_creation() {
236        let manager = VenvManager::new();
237        assert!(manager.is_ok());
238    }
239
240    #[test]
241    fn test_current_venv() {
242        // Should return None when not in a venv
243        assert!(!VenvManager::is_active());
244    }
245}