1use anyhow::Result;
6use std::path::Path;
7use vx_plugin::{Ecosystem, PackageSpec, VxPackageManager, VxPlugin, VxTool};
8
9#[derive(Default)]
13pub struct SimpleNpmPackageManager;
14
15#[async_trait::async_trait]
16impl VxPackageManager for SimpleNpmPackageManager {
17 fn name(&self) -> &str {
18 "npm"
19 }
20
21 fn ecosystem(&self) -> Ecosystem {
22 Ecosystem::Node
23 }
24
25 fn description(&self) -> &str {
26 "Node Package Manager"
27 }
28
29 fn is_preferred_for_project(&self, project_path: &Path) -> bool {
31 project_path.join("package.json").exists()
32 }
33
34 fn get_config_files(&self) -> Vec<&str> {
35 vec!["package.json", "package-lock.json"]
36 }
37
38 async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()> {
40 if packages.is_empty() {
41 self.run_command(&["install"], &[], project_path).await
43 } else {
44 let package_names: Vec<String> = packages
46 .iter()
47 .map(|pkg| {
48 if let Some(version) = &pkg.version {
49 format!("{}@{}", pkg.name, version)
50 } else {
51 pkg.name.clone()
52 }
53 })
54 .collect();
55
56 let command = if packages.iter().any(|pkg| pkg.dev_dependency) {
57 vec!["install", "--save-dev"]
58 } else {
59 vec!["install", "--save"]
60 };
61
62 self.run_command(&command, &package_names, project_path)
63 .await
64 }
65 }
66
67 async fn remove_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
69 self.run_command(&["uninstall"], packages, project_path)
70 .await
71 }
72
73 async fn update_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
75 if packages.is_empty() {
76 self.run_command(&["update"], &[], project_path).await
77 } else {
78 self.run_command(&["update"], packages, project_path).await
79 }
80 }
81}
82
83#[derive(Default)]
85pub struct SimpleNpmPlugin;
86
87#[async_trait::async_trait]
88impl VxPlugin for SimpleNpmPlugin {
89 fn name(&self) -> &str {
90 "simple-npm"
91 }
92
93 fn description(&self) -> &str {
94 "Simple NPM package manager support for vx"
95 }
96
97 fn version(&self) -> &str {
98 "1.0.0"
99 }
100
101 fn tools(&self) -> Vec<Box<dyn VxTool>> {
102 vec![]
103 }
104
105 fn package_managers(&self) -> Vec<Box<dyn VxPackageManager>> {
106 vec![Box::new(SimpleNpmPackageManager)]
107 }
108
109 fn supports_tool(&self, _tool_name: &str) -> bool {
110 false
111 }
112}
113
114pub fn create_simple_npm_plugin() -> Box<dyn VxPlugin> {
116 Box::new(SimpleNpmPlugin)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_simple_npm_package_manager() {
125 let pm = SimpleNpmPackageManager;
126
127 assert_eq!(pm.name(), "npm");
128 assert_eq!(pm.ecosystem(), Ecosystem::Node);
129 assert_eq!(pm.description(), "Node Package Manager");
130
131 let temp_dir = std::env::temp_dir().join("test-npm-project");
133 std::fs::create_dir_all(&temp_dir).unwrap();
134
135 assert!(!pm.is_preferred_for_project(&temp_dir));
137
138 std::fs::write(temp_dir.join("package.json"), "{}").unwrap();
140 assert!(pm.is_preferred_for_project(&temp_dir));
141
142 let _ = std::fs::remove_dir_all(&temp_dir);
144 }
145
146 #[test]
147 fn test_simple_npm_plugin() {
148 let plugin = SimpleNpmPlugin;
149
150 assert_eq!(plugin.name(), "simple-npm");
151 assert_eq!(plugin.version(), "1.0.0");
152 assert!(plugin.supports_package_manager("npm"));
153 assert!(!plugin.supports_package_manager("yarn"));
154
155 let package_managers = plugin.package_managers();
156 assert_eq!(package_managers.len(), 1);
157 assert_eq!(package_managers[0].name(), "npm");
158 }
159
160 #[tokio::test]
161 async fn test_package_spec_formatting() {
162 let _pm = SimpleNpmPackageManager;
163
164 let packages = vec![
165 PackageSpec::new("express".to_string()),
166 PackageSpec::new("lodash".to_string()).with_version("4.17.21".to_string()),
167 PackageSpec::new("jest".to_string()).as_dev_dependency(),
168 ];
169
170 assert_eq!(packages.len(), 3);
173 assert_eq!(packages[0].name, "express");
174 assert_eq!(packages[1].version, Some("4.17.21".to_string()));
175 assert!(packages[2].dev_dependency);
176 }
177}
178
179