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}