vx_tool_node/
node_tool.rs

1//! Node.js tool implementations - JavaScript runtime and package management tools
2
3use std::collections::HashMap;
4use vx_core::{
5    HttpUtils, NodeUrlBuilder, NodeVersionParser, Result, ToolContext, ToolExecutionResult,
6    VersionInfo, VxEnvironment, VxError, VxTool,
7};
8// use vx_core::{UrlBuilder, VersionParser};
9
10/// Macro to generate Node.js tool implementations using VxTool trait
11macro_rules! node_vx_tool {
12    ($name:ident, $cmd:literal, $desc:literal, $homepage:expr) => {
13        #[derive(Debug, Clone)]
14        pub struct $name {
15            _url_builder: NodeUrlBuilder,
16            _version_parser: NodeVersionParser,
17        }
18
19        impl $name {
20            pub fn new() -> Self {
21                Self {
22                    _url_builder: NodeUrlBuilder::new(),
23                    _version_parser: NodeVersionParser::new(),
24                }
25            }
26        }
27
28        #[async_trait::async_trait]
29        impl VxTool for $name {
30            fn name(&self) -> &str {
31                $cmd
32            }
33
34            fn description(&self) -> &str {
35                $desc
36            }
37
38            fn aliases(&self) -> Vec<&str> {
39                match $cmd {
40                    "node" => vec!["nodejs"],
41                    "npm" => vec![],
42                    "npx" => vec![],
43                    _ => vec![],
44                }
45            }
46
47            async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
48                // For Node.js, fetch from official API
49                let json = HttpUtils::fetch_json(NodeUrlBuilder::versions_url()).await?;
50                NodeVersionParser::parse_versions(&json, include_prerelease)
51            }
52
53            async fn install_version(&self, version: &str, force: bool) -> Result<()> {
54                if !force && self.is_version_installed(version).await? {
55                    return Err(VxError::VersionAlreadyInstalled {
56                        tool_name: self.name().to_string(),
57                        version: version.to_string(),
58                    });
59                }
60
61                let install_dir = self.get_version_install_dir(version);
62                let _exe_path = self.default_install_workflow(version, &install_dir).await?;
63
64                // Verify installation
65                if !self.is_version_installed(version).await? {
66                    return Err(VxError::InstallationFailed {
67                        tool_name: self.name().to_string(),
68                        version: version.to_string(),
69                        message: "Installation verification failed".to_string(),
70                    });
71                }
72
73                Ok(())
74            }
75
76            async fn is_version_installed(&self, version: &str) -> Result<bool> {
77                let env = VxEnvironment::new().expect("Failed to create VX environment");
78
79                // For npm and npx, check if Node.js is installed (they come bundled)
80                if self.name() == "npm" || self.name() == "npx" {
81                    return Ok(env.is_version_installed("node", version));
82                }
83
84                Ok(env.is_version_installed(self.name(), version))
85            }
86
87            async fn execute(
88                &self,
89                args: &[String],
90                context: &ToolContext,
91            ) -> Result<ToolExecutionResult> {
92                // For npm and npx, find executable in Node.js installation
93                if (self.name() == "npm" || self.name() == "npx") && !context.use_system_path {
94                    let active_version = self.get_active_version().await?;
95                    let env = VxEnvironment::new().expect("Failed to create VX environment");
96                    let node_install_dir = env.get_version_install_dir("node", &active_version);
97                    let exe_path = env.find_executable_in_dir(&node_install_dir, self.name())?;
98
99                    // Execute the tool
100                    let mut cmd = std::process::Command::new(&exe_path);
101                    cmd.args(args);
102
103                    if let Some(cwd) = &context.working_directory {
104                        cmd.current_dir(cwd);
105                    }
106
107                    for (key, value) in &context.environment_variables {
108                        cmd.env(key, value);
109                    }
110
111                    let status = cmd.status().map_err(|e| VxError::Other {
112                        message: format!("Failed to execute {}: {}", self.name(), e),
113                    })?;
114
115                    return Ok(ToolExecutionResult {
116                        exit_code: status.code().unwrap_or(1),
117                        stdout: None,
118                        stderr: None,
119                    });
120                }
121
122                // For node or system path execution, use default workflow
123                self.default_execute_workflow(args, context).await
124            }
125
126            async fn get_active_version(&self) -> Result<String> {
127                let env = VxEnvironment::new().expect("Failed to create VX environment");
128
129                // For npm and npx, use Node.js version
130                if self.name() == "npm" || self.name() == "npx" {
131                    if let Some(active_version) = env.get_active_version("node")? {
132                        return Ok(active_version);
133                    }
134
135                    let installed_versions = env.list_installed_versions("node")?;
136                    return installed_versions.first().cloned().ok_or_else(|| {
137                        VxError::ToolNotInstalled {
138                            tool_name: "node".to_string(),
139                        }
140                    });
141                }
142
143                // For node, use default implementation
144                if let Some(active_version) = env.get_active_version(self.name())? {
145                    return Ok(active_version);
146                }
147
148                let installed_versions = env.list_installed_versions(self.name())?;
149                installed_versions
150                    .first()
151                    .cloned()
152                    .ok_or_else(|| VxError::ToolNotInstalled {
153                        tool_name: self.name().to_string(),
154                    })
155            }
156
157            async fn get_installed_versions(&self) -> Result<Vec<String>> {
158                let env = VxEnvironment::new().expect("Failed to create VX environment");
159
160                // For npm and npx, use Node.js versions
161                if self.name() == "npm" || self.name() == "npx" {
162                    return env.list_installed_versions("node");
163                }
164
165                env.list_installed_versions(self.name())
166            }
167
168            async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
169                Ok(NodeUrlBuilder::download_url(version))
170            }
171
172            fn metadata(&self) -> HashMap<String, String> {
173                let mut meta = HashMap::new();
174                meta.insert("homepage".to_string(), $homepage.unwrap_or("").to_string());
175                meta.insert("ecosystem".to_string(), "javascript".to_string());
176                meta
177            }
178        }
179
180        impl Default for $name {
181            fn default() -> Self {
182                Self::new()
183            }
184        }
185    };
186}
187
188// Define Node.js tools using the VxTool macro
189node_vx_tool!(
190    NodeTool,
191    "node",
192    "Node.js JavaScript runtime",
193    Some("https://nodejs.org/")
194);
195node_vx_tool!(
196    NpmTool,
197    "npm",
198    "Node.js package manager",
199    Some("https://www.npmjs.com/")
200);
201node_vx_tool!(
202    NpxTool,
203    "npx",
204    "Node.js package runner",
205    Some("https://www.npmjs.com/package/npx")
206);
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_node_tool_creation() {
214        let tool = NodeTool::new();
215        assert_eq!(tool.name(), "node");
216        assert!(!tool.description().is_empty());
217        assert!(tool.aliases().contains(&"nodejs"));
218    }
219
220    #[test]
221    fn test_npm_tool_creation() {
222        let tool = NpmTool::new();
223        assert_eq!(tool.name(), "npm");
224        assert!(!tool.description().is_empty());
225    }
226
227    #[test]
228    fn test_npx_tool_creation() {
229        let tool = NpxTool::new();
230        assert_eq!(tool.name(), "npx");
231        assert!(!tool.description().is_empty());
232    }
233
234    #[test]
235    fn test_node_tool_metadata() {
236        let tool = NodeTool::new();
237        let metadata = tool.metadata();
238
239        assert!(metadata.contains_key("homepage"));
240        assert!(metadata.contains_key("ecosystem"));
241        assert_eq!(metadata.get("ecosystem"), Some(&"javascript".to_string()));
242    }
243}