vx_plugin/package_manager.rs
1//! Package manager plugin trait and related functionality
2//!
3//! This module defines the `VxPackageManager` trait, which provides a unified
4//! interface for different package managers across various ecosystems.
5
6use crate::{Ecosystem, IsolationLevel, PackageInfo, PackageManagerConfig, PackageSpec, Result};
7use async_trait::async_trait;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11/// Simplified trait for implementing package manager support
12///
13/// This trait provides a high-level interface for package managers,
14/// with sensible defaults for common operations.
15///
16/// # Required Methods
17///
18/// - `name()`: Return the package manager name
19/// - `ecosystem()`: Return the ecosystem this package manager belongs to
20/// - `install_packages()`: Install packages in a project
21///
22/// # Optional Methods
23///
24/// All other methods have default implementations, but can be overridden
25/// for package manager-specific behavior.
26///
27/// # Example
28///
29/// ```rust,no_run
30/// use vx_plugin::{VxPackageManager, Ecosystem, PackageSpec, Result};
31/// use async_trait::async_trait;
32/// use std::path::Path;
33///
34/// struct MyPackageManager;
35///
36/// #[async_trait]
37/// impl VxPackageManager for MyPackageManager {
38/// fn name(&self) -> &str {
39/// "mypm"
40/// }
41///
42/// fn ecosystem(&self) -> Ecosystem {
43/// Ecosystem::Node
44/// }
45///
46/// async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()> {
47/// // Install packages using your package manager
48/// Ok(())
49/// }
50/// }
51/// ```
52#[async_trait]
53pub trait VxPackageManager: Send + Sync {
54 /// Package manager name (required)
55 ///
56 /// This should be the command name used to invoke the package manager,
57 /// such as "npm", "pip", "cargo", etc.
58 fn name(&self) -> &str;
59
60 /// Ecosystem this package manager belongs to (required)
61 ///
62 /// Indicates which programming language or platform ecosystem
63 /// this package manager serves.
64 fn ecosystem(&self) -> Ecosystem;
65
66 /// Description of the package manager (optional)
67 ///
68 /// A human-readable description of what this package manager does.
69 fn description(&self) -> &str {
70 "A package manager"
71 }
72
73 /// Check if this package manager is available on the system
74 ///
75 /// Default implementation checks if the executable exists in PATH.
76 async fn is_available(&self) -> Result<bool> {
77 Ok(which::which(self.name()).is_ok())
78 }
79
80 /// Check if this package manager should be used for a project
81 ///
82 /// Override this to detect project-specific files (package.json, Cargo.toml, etc.)
83 /// The default implementation checks for common configuration files.
84 fn is_preferred_for_project(&self, project_path: &Path) -> bool {
85 let config_files = self.get_config_files();
86 config_files
87 .iter()
88 .any(|file| project_path.join(file).exists())
89 }
90
91 /// Get configuration files that indicate this package manager should be used
92 ///
93 /// Override this to return the configuration files specific to your package manager.
94 /// For example, npm would return ["package.json"], cargo would return ["Cargo.toml"].
95 fn get_config_files(&self) -> Vec<&str> {
96 vec![]
97 }
98 /// Install packages (main method to implement)
99 ///
100 /// This is the primary method that package manager implementations must provide.
101 /// It should install the specified packages in the given project directory.
102 ///
103 /// # Arguments
104 ///
105 /// * `packages` - List of package specifications to install
106 /// * `project_path` - Path to the project directory
107 async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()>;
108
109 /// Remove packages
110 ///
111 /// Default implementation uses the "remove" command.
112 /// Override if your package manager uses different commands.
113 async fn remove_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
114 self.run_command(&["remove"], packages, project_path).await
115 }
116
117 /// Update packages
118 ///
119 /// Default implementation uses the "update" command.
120 /// If no packages are specified, updates all packages.
121 async fn update_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
122 if packages.is_empty() {
123 self.run_command(&["update"], &[], project_path).await
124 } else {
125 self.run_command(&["update"], packages, project_path).await
126 }
127 }
128
129 /// List installed packages
130 ///
131 /// Default implementation attempts to parse common configuration files
132 /// or run list commands. Override for package manager-specific logic.
133 async fn list_packages(&self, project_path: &Path) -> Result<Vec<PackageInfo>> {
134 self.default_list_packages(project_path).await
135 }
136
137 /// Search for packages
138 ///
139 /// Default implementation runs the package manager's search command.
140 /// Override for custom search logic or API integration.
141 async fn search_packages(&self, query: &str) -> Result<Vec<PackageInfo>> {
142 self.run_search_command(query).await
143 }
144 /// Run a package manager command with arguments
145 ///
146 /// This is a utility method that executes the package manager with
147 /// the specified command and arguments in the given project directory.
148 async fn run_command(
149 &self,
150 command: &[&str],
151 args: &[String],
152 project_path: &Path,
153 ) -> Result<()> {
154 let mut cmd = std::process::Command::new(self.name());
155 cmd.args(command);
156 cmd.args(args);
157 cmd.current_dir(project_path);
158
159 let status = cmd
160 .status()
161 .map_err(|e| anyhow::anyhow!("Failed to run {} command: {}", self.name(), e))?;
162
163 if !status.success() {
164 return Err(anyhow::anyhow!(
165 "{} command failed with exit code: {:?}",
166 self.name(),
167 status.code()
168 ));
169 }
170
171 Ok(())
172 }
173
174 /// Default implementation for listing packages
175 ///
176 /// This method provides a fallback implementation that attempts
177 /// to parse common configuration files. Override for better integration.
178 async fn default_list_packages(&self, _project_path: &Path) -> Result<Vec<PackageInfo>> {
179 // Default implementation returns empty list
180 // Real implementations would parse lock files, run list commands, etc.
181 Ok(vec![])
182 }
183
184 /// Default implementation for searching packages
185 ///
186 /// This method runs the package manager's search command and attempts
187 /// to parse the output. Override for API-based search or custom parsing.
188 async fn run_search_command(&self, _query: &str) -> Result<Vec<PackageInfo>> {
189 // Default implementation returns empty list
190 // Real implementations would run search commands and parse output
191 Ok(vec![])
192 }
193 /// Get the command to install packages
194 ///
195 /// Override this if your package manager uses a different command for installation.
196 /// Most package managers use "install", but some might use "add" or other commands.
197 fn get_install_command(&self) -> Vec<&str> {
198 vec!["install"]
199 }
200
201 /// Get the command to add new packages
202 ///
203 /// Override this if your package manager distinguishes between installing
204 /// existing dependencies and adding new ones. Some package managers use "add"
205 /// for new packages and "install" for existing dependencies.
206 fn get_add_command(&self) -> Vec<&str> {
207 vec!["add"]
208 }
209
210 /// Get the command to remove packages
211 ///
212 /// Override this if your package manager uses a different command for removal.
213 fn get_remove_command(&self) -> Vec<&str> {
214 vec!["remove"]
215 }
216
217 /// Get the command to update packages
218 ///
219 /// Override this if your package manager uses a different command for updates.
220 fn get_update_command(&self) -> Vec<&str> {
221 vec!["update"]
222 }
223
224 /// Get the command to list packages
225 ///
226 /// Override this if your package manager has a specific list command.
227 fn get_list_command(&self) -> Vec<&str> {
228 vec!["list"]
229 }
230
231 /// Get the command to search packages
232 ///
233 /// Override this if your package manager uses a different search command.
234 fn get_search_command(&self) -> Vec<&str> {
235 vec!["search"]
236 }
237
238 /// Get package manager configuration
239 ///
240 /// Returns configuration information about this package manager.
241 fn get_config(&self) -> PackageManagerConfig {
242 PackageManagerConfig {
243 name: self.name().to_string(),
244 version: None,
245 executable_path: which::which(self.name()).ok(),
246 config_files: self.get_config_files().iter().map(PathBuf::from).collect(),
247 cache_directory: None,
248 supports_lockfiles: true,
249 supports_workspaces: false,
250 isolation_level: IsolationLevel::Project,
251 }
252 }
253
254 /// Run a package manager command and return the exit code
255 ///
256 /// Similar to run_command but returns the exit code instead of failing on non-zero codes.
257 async fn run_command_with_code(
258 &self,
259 command: &[&str],
260 args: &[String],
261 project_path: &Path,
262 ) -> Result<i32> {
263 let mut cmd = std::process::Command::new(self.name());
264 cmd.args(command);
265 cmd.args(args);
266 cmd.current_dir(project_path);
267
268 let status = cmd
269 .status()
270 .map_err(|e| anyhow::anyhow!("Failed to run {} command: {}", self.name(), e))?;
271
272 Ok(status.code().unwrap_or(-1))
273 }
274
275 /// Additional metadata for the package manager (optional)
276 ///
277 /// Override this to provide package manager-specific metadata such as
278 /// supported features, configuration options, etc.
279 fn metadata(&self) -> HashMap<String, String> {
280 HashMap::new()
281 }
282}
283
284/// Standard package manager implementation
285///
286/// This is a convenience implementation for package managers that follow
287/// common patterns. It provides sensible defaults and can be customized
288/// through configuration.
289pub struct StandardPackageManager {
290 name: String,
291 description: String,
292 ecosystem: Ecosystem,
293 config_files: Vec<String>,
294 install_command: Vec<String>,
295 remove_command: Vec<String>,
296 update_command: Vec<String>,
297 list_command: Vec<String>,
298 search_command: Vec<String>,
299}
300
301impl StandardPackageManager {
302 /// Create a new standard package manager
303 pub fn new(
304 name: impl Into<String>,
305 description: impl Into<String>,
306 ecosystem: Ecosystem,
307 ) -> Self {
308 let name = name.into();
309 Self {
310 name: name.clone(),
311 description: description.into(),
312 ecosystem,
313 config_files: Vec::new(),
314 install_command: vec!["install".to_string()],
315 remove_command: vec!["remove".to_string()],
316 update_command: vec!["update".to_string()],
317 list_command: vec!["list".to_string()],
318 search_command: vec!["search".to_string()],
319 }
320 }
321
322 /// Add a configuration file that indicates this package manager should be used
323 pub fn with_config_file(mut self, config_file: impl Into<String>) -> Self {
324 self.config_files.push(config_file.into());
325 self
326 }
327
328 /// Set custom install command
329 pub fn with_install_command(mut self, command: Vec<String>) -> Self {
330 self.install_command = command;
331 self
332 }
333
334 /// Set custom remove command
335 pub fn with_remove_command(mut self, command: Vec<String>) -> Self {
336 self.remove_command = command;
337 self
338 }
339
340 /// Set custom update command
341 pub fn with_update_command(mut self, command: Vec<String>) -> Self {
342 self.update_command = command;
343 self
344 }
345}
346
347#[async_trait]
348impl VxPackageManager for StandardPackageManager {
349 fn name(&self) -> &str {
350 &self.name
351 }
352
353 fn ecosystem(&self) -> Ecosystem {
354 self.ecosystem
355 }
356
357 fn description(&self) -> &str {
358 &self.description
359 }
360
361 fn get_config_files(&self) -> Vec<&str> {
362 self.config_files.iter().map(|s| s.as_str()).collect()
363 }
364
365 async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()> {
366 let package_names: Vec<String> = packages
367 .iter()
368 .map(|p| {
369 if let Some(version) = &p.version {
370 format!("{}@{}", p.name, version)
371 } else {
372 p.name.clone()
373 }
374 })
375 .collect();
376
377 let command_strs: Vec<&str> = self.install_command.iter().map(|s| s.as_str()).collect();
378 self.run_command(&command_strs, &package_names, project_path)
379 .await
380 }
381
382 fn get_install_command(&self) -> Vec<&str> {
383 self.install_command.iter().map(|s| s.as_str()).collect()
384 }
385
386 fn get_remove_command(&self) -> Vec<&str> {
387 self.remove_command.iter().map(|s| s.as_str()).collect()
388 }
389
390 fn get_update_command(&self) -> Vec<&str> {
391 self.update_command.iter().map(|s| s.as_str()).collect()
392 }
393
394 fn get_list_command(&self) -> Vec<&str> {
395 self.list_command.iter().map(|s| s.as_str()).collect()
396 }
397
398 fn get_search_command(&self) -> Vec<&str> {
399 self.search_command.iter().map(|s| s.as_str()).collect()
400 }
401}