vx_tool_node/
node_tool.rs1use std::collections::HashMap;
4use vx_core::{
5 HttpUtils, NodeUrlBuilder, NodeVersionParser, Result, ToolContext, ToolExecutionResult,
6 VersionInfo, VxEnvironment, VxError, VxTool,
7};
8macro_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 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 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 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 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 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 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 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 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 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
188node_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}