vx_core/
global_tool_manager.rs

1//! Global tool management system
2//!
3//! This module provides functionality for managing globally installed tools
4//! and tracking their usage by virtual environments.
5
6use crate::{Result, VxEnvironment, VxError};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12/// Information about a globally installed tool
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GlobalToolInfo {
15    /// Tool name
16    pub name: String,
17    /// Installed version
18    pub version: String,
19    /// Installation path
20    pub install_path: PathBuf,
21    /// Installation timestamp
22    pub installed_at: chrono::DateTime<chrono::Utc>,
23    /// Virtual environments that reference this tool
24    pub referenced_by: HashSet<String>,
25}
26
27/// Dependency tracking for virtual environments
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct VenvDependency {
30    /// Virtual environment name/path
31    pub venv_name: String,
32    /// Tools this venv depends on
33    pub dependencies: HashSet<String>,
34    /// Creation timestamp
35    pub created_at: chrono::DateTime<chrono::Utc>,
36}
37
38/// Global tool manager for handling tool installations and dependencies
39#[derive(Debug)]
40pub struct GlobalToolManager {
41    /// VX environment for path management
42    #[allow(dead_code)]
43    env: VxEnvironment,
44    /// Path to global tools registry file
45    registry_path: PathBuf,
46    /// Path to venv dependencies file
47    dependencies_path: PathBuf,
48}
49
50impl GlobalToolManager {
51    /// Create a new global tool manager
52    pub fn new() -> Result<Self> {
53        let env = VxEnvironment::new()?;
54        let data_dir = env
55            .get_base_install_dir()
56            .parent()
57            .ok_or_else(|| VxError::Other {
58                message: "Failed to get VX data directory".to_string(),
59            })?
60            .to_path_buf();
61
62        let registry_path = data_dir.join("global_tools.json");
63        let dependencies_path = data_dir.join("venv_dependencies.json");
64
65        Ok(Self {
66            env,
67            registry_path,
68            dependencies_path,
69        })
70    }
71
72    /// Register a globally installed tool
73    pub async fn register_global_tool(
74        &self,
75        name: &str,
76        version: &str,
77        install_path: &Path,
78    ) -> Result<()> {
79        let mut tools = self.load_global_tools().await?;
80
81        let tool_info = GlobalToolInfo {
82            name: name.to_string(),
83            version: version.to_string(),
84            install_path: install_path.to_path_buf(),
85            installed_at: chrono::Utc::now(),
86            referenced_by: HashSet::new(),
87        };
88
89        tools.insert(name.to_string(), tool_info);
90        self.save_global_tools(&tools).await?;
91
92        Ok(())
93    }
94
95    /// Check if a tool is globally installed
96    pub async fn is_tool_installed(&self, name: &str) -> Result<bool> {
97        let tools = self.load_global_tools().await?;
98        Ok(tools.contains_key(name))
99    }
100
101    /// Get information about a globally installed tool
102    pub async fn get_tool_info(&self, name: &str) -> Result<Option<GlobalToolInfo>> {
103        let tools = self.load_global_tools().await?;
104        Ok(tools.get(name).cloned())
105    }
106
107    /// List all globally installed tools
108    pub async fn list_global_tools(&self) -> Result<Vec<GlobalToolInfo>> {
109        let tools = self.load_global_tools().await?;
110        Ok(tools.into_values().collect())
111    }
112
113    /// Add a virtual environment dependency
114    pub async fn add_venv_dependency(&self, venv_name: &str, tool_name: &str) -> Result<()> {
115        // Update global tool references
116        let mut tools = self.load_global_tools().await?;
117        if let Some(tool) = tools.get_mut(tool_name) {
118            tool.referenced_by.insert(venv_name.to_string());
119        }
120        self.save_global_tools(&tools).await?;
121
122        // Update venv dependencies
123        let mut dependencies = self.load_venv_dependencies().await?;
124        let venv_dep = dependencies
125            .entry(venv_name.to_string())
126            .or_insert_with(|| VenvDependency {
127                venv_name: venv_name.to_string(),
128                dependencies: HashSet::new(),
129                created_at: chrono::Utc::now(),
130            });
131
132        venv_dep.dependencies.insert(tool_name.to_string());
133        self.save_venv_dependencies(&dependencies).await?;
134
135        Ok(())
136    }
137
138    /// Remove a virtual environment dependency
139    pub async fn remove_venv_dependency(&self, venv_name: &str, tool_name: &str) -> Result<()> {
140        // Update global tool references
141        let mut tools = self.load_global_tools().await?;
142        if let Some(tool) = tools.get_mut(tool_name) {
143            tool.referenced_by.remove(venv_name);
144        }
145        self.save_global_tools(&tools).await?;
146
147        // Update venv dependencies
148        let mut dependencies = self.load_venv_dependencies().await?;
149        if let Some(venv_dep) = dependencies.get_mut(venv_name) {
150            venv_dep.dependencies.remove(tool_name);
151
152            // Remove venv entry if no dependencies left
153            if venv_dep.dependencies.is_empty() {
154                dependencies.remove(venv_name);
155            }
156        }
157        self.save_venv_dependencies(&dependencies).await?;
158
159        Ok(())
160    }
161
162    /// Check if a tool can be safely removed (not referenced by any venv)
163    pub async fn can_remove_tool(&self, tool_name: &str) -> Result<bool> {
164        let tools = self.load_global_tools().await?;
165        if let Some(tool) = tools.get(tool_name) {
166            Ok(tool.referenced_by.is_empty())
167        } else {
168            Ok(true) // Tool doesn't exist, can be "removed"
169        }
170    }
171
172    /// Get virtual environments that depend on a tool
173    pub async fn get_tool_dependents(&self, tool_name: &str) -> Result<Vec<String>> {
174        let tools = self.load_global_tools().await?;
175        if let Some(tool) = tools.get(tool_name) {
176            Ok(tool.referenced_by.iter().cloned().collect())
177        } else {
178            Ok(vec![])
179        }
180    }
181
182    /// Remove a global tool (only if not referenced)
183    pub async fn remove_global_tool(&self, tool_name: &str) -> Result<()> {
184        if !self.can_remove_tool(tool_name).await? {
185            let dependents = self.get_tool_dependents(tool_name).await?;
186            return Err(VxError::Other {
187                message: format!(
188                    "Cannot remove tool '{}' - it is referenced by virtual environments: {}",
189                    tool_name,
190                    dependents.join(", ")
191                ),
192            });
193        }
194
195        let mut tools = self.load_global_tools().await?;
196        if let Some(tool_info) = tools.remove(tool_name) {
197            // Remove the actual installation directory
198            if tool_info.install_path.exists() {
199                fs::remove_dir_all(&tool_info.install_path)
200                    .await
201                    .map_err(|e| VxError::Other {
202                        message: format!(
203                            "Failed to remove tool directory {}: {}",
204                            tool_info.install_path.display(),
205                            e
206                        ),
207                    })?;
208            }
209        }
210
211        self.save_global_tools(&tools).await?;
212        Ok(())
213    }
214
215    /// Load global tools registry from disk
216    async fn load_global_tools(&self) -> Result<HashMap<String, GlobalToolInfo>> {
217        if !self.registry_path.exists() {
218            return Ok(HashMap::new());
219        }
220
221        let content =
222            fs::read_to_string(&self.registry_path)
223                .await
224                .map_err(|e| VxError::Other {
225                    message: format!("Failed to read global tools registry: {}", e),
226                })?;
227
228        serde_json::from_str(&content).map_err(|e| VxError::Other {
229            message: format!("Failed to parse global tools registry: {}", e),
230        })
231    }
232
233    /// Save global tools registry to disk
234    async fn save_global_tools(&self, tools: &HashMap<String, GlobalToolInfo>) -> Result<()> {
235        // Ensure parent directory exists
236        if let Some(parent) = self.registry_path.parent() {
237            fs::create_dir_all(parent)
238                .await
239                .map_err(|e| VxError::Other {
240                    message: format!("Failed to create registry directory: {}", e),
241                })?;
242        }
243
244        let content = serde_json::to_string_pretty(tools).map_err(|e| VxError::Other {
245            message: format!("Failed to serialize global tools registry: {}", e),
246        })?;
247
248        fs::write(&self.registry_path, content)
249            .await
250            .map_err(|e| VxError::Other {
251                message: format!("Failed to write global tools registry: {}", e),
252            })
253    }
254
255    /// Load venv dependencies from disk
256    async fn load_venv_dependencies(&self) -> Result<HashMap<String, VenvDependency>> {
257        if !self.dependencies_path.exists() {
258            return Ok(HashMap::new());
259        }
260
261        let content = fs::read_to_string(&self.dependencies_path)
262            .await
263            .map_err(|e| VxError::Other {
264                message: format!("Failed to read venv dependencies: {}", e),
265            })?;
266
267        serde_json::from_str(&content).map_err(|e| VxError::Other {
268            message: format!("Failed to parse venv dependencies: {}", e),
269        })
270    }
271
272    /// Save venv dependencies to disk
273    async fn save_venv_dependencies(&self, deps: &HashMap<String, VenvDependency>) -> Result<()> {
274        // Ensure parent directory exists
275        if let Some(parent) = self.dependencies_path.parent() {
276            fs::create_dir_all(parent)
277                .await
278                .map_err(|e| VxError::Other {
279                    message: format!("Failed to create dependencies directory: {}", e),
280                })?;
281        }
282
283        let content = serde_json::to_string_pretty(deps).map_err(|e| VxError::Other {
284            message: format!("Failed to serialize venv dependencies: {}", e),
285        })?;
286
287        fs::write(&self.dependencies_path, content)
288            .await
289            .map_err(|e| VxError::Other {
290                message: format!("Failed to write venv dependencies: {}", e),
291            })
292    }
293}
294
295impl Default for GlobalToolManager {
296    fn default() -> Self {
297        Self::new().expect("Failed to create GlobalToolManager")
298    }
299}