vx_core/
global_tool_manager.rs1use crate::{Result, VxEnvironment, VxError};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GlobalToolInfo {
15 pub name: String,
17 pub version: String,
19 pub install_path: PathBuf,
21 pub installed_at: chrono::DateTime<chrono::Utc>,
23 pub referenced_by: HashSet<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct VenvDependency {
30 pub venv_name: String,
32 pub dependencies: HashSet<String>,
34 pub created_at: chrono::DateTime<chrono::Utc>,
36}
37
38#[derive(Debug)]
40pub struct GlobalToolManager {
41 #[allow(dead_code)]
43 env: VxEnvironment,
44 registry_path: PathBuf,
46 dependencies_path: PathBuf,
48}
49
50impl GlobalToolManager {
51 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 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 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 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 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 pub async fn add_venv_dependency(&self, venv_name: &str, tool_name: &str) -> Result<()> {
115 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 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 pub async fn remove_venv_dependency(&self, venv_name: &str, tool_name: &str) -> Result<()> {
140 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 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 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 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) }
170 }
171
172 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 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 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 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 async fn save_global_tools(&self, tools: &HashMap<String, GlobalToolInfo>) -> Result<()> {
235 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 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 async fn save_venv_dependencies(&self, deps: &HashMap<String, VenvDependency>) -> Result<()> {
274 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}