1use crate::config::NodeUrlBuilder;
4use anyhow::Result;
5use std::collections::HashMap;
6use vx_plugin::{ToolContext, ToolExecutionResult, VersionInfo, VxTool};
7use vx_tool_standard::StandardUrlBuilder;
8use vx_version::{NodeVersionFetcher, VersionFetcher};
9macro_rules! node_vx_tool {
13 ($name:ident, $cmd:literal, $desc:literal, $homepage:expr) => {
14 #[derive(Debug, Clone)]
15 pub struct $name {
16 version_fetcher: NodeVersionFetcher,
17 }
18
19 impl $name {
20 pub fn new() -> Self {
21 Self {
22 version_fetcher: NodeVersionFetcher::new(),
23 }
24 }
25 }
26
27 #[async_trait::async_trait]
28 impl VxTool for $name {
29 fn name(&self) -> &str {
30 $cmd
31 }
32
33 fn description(&self) -> &str {
34 $desc
35 }
36
37 fn aliases(&self) -> Vec<&str> {
38 match $cmd {
39 "node" => vec!["nodejs"],
40 "npm" => vec![],
41 "npx" => vec![],
42 _ => vec![],
43 }
44 }
45
46 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
47 self.version_fetcher
49 .fetch_versions(include_prerelease)
50 .await
51 .map_err(|e| anyhow::anyhow!("Failed to fetch versions: {}", e))
52 }
53
54 async fn install_version(&self, version: &str, force: bool) -> Result<()> {
55 if !force && self.is_version_installed(version).await? {
56 return Err(anyhow::anyhow!(
57 "Version {} of {} is already installed",
58 version,
59 self.name()
60 ));
61 }
62
63 let install_dir = self.get_version_install_dir(version);
64 let _exe_path = self.default_install_workflow(version, &install_dir).await?;
65
66 if !self.is_version_installed(version).await? {
68 return Err(anyhow::anyhow!(
69 "Installation verification failed for {} version {}",
70 self.name(),
71 version
72 ));
73 }
74
75 Ok(())
76 }
77
78 async fn is_version_installed(&self, version: &str) -> Result<bool> {
79 let install_dir = self.get_version_install_dir(version);
81 Ok(install_dir.exists())
82 }
83
84 async fn execute(
85 &self,
86 args: &[String],
87 context: &ToolContext,
88 ) -> Result<ToolExecutionResult> {
89 let mut cmd = std::process::Command::new($cmd);
91 cmd.args(args);
92
93 if let Some(cwd) = &context.working_directory {
94 cmd.current_dir(cwd);
95 }
96
97 for (key, value) in &context.environment_variables {
98 cmd.env(key, value);
99 }
100
101 let status = cmd
102 .status()
103 .map_err(|e| anyhow::anyhow!("Failed to execute {}: {}", $cmd, e))?;
104
105 Ok(ToolExecutionResult {
106 exit_code: status.code().unwrap_or(1),
107 stdout: None,
108 stderr: None,
109 })
110 }
111
112 async fn get_active_version(&self) -> Result<String> {
113 Ok("latest".to_string())
115 }
116
117 async fn get_installed_versions(&self) -> Result<Vec<String>> {
118 Ok(vec![])
120 }
121
122 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
123 Ok(NodeUrlBuilder::download_url(version))
124 }
125
126 fn metadata(&self) -> HashMap<String, String> {
127 let mut meta = HashMap::new();
128 meta.insert("homepage".to_string(), $homepage.unwrap_or("").to_string());
129 meta.insert("ecosystem".to_string(), "javascript".to_string());
130 meta
131 }
132 }
133
134 impl Default for $name {
135 fn default() -> Self {
136 Self::new()
137 }
138 }
139 };
140}
141
142node_vx_tool!(
144 NodeTool,
145 "node",
146 "Node.js JavaScript runtime",
147 Some("https://nodejs.org/")
148);
149node_vx_tool!(
150 NpmTool,
151 "npm",
152 "Node.js package manager",
153 Some("https://www.npmjs.com/")
154);
155node_vx_tool!(
156 NpxTool,
157 "npx",
158 "Node.js package runner",
159 Some("https://www.npmjs.com/package/npx")
160);
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_node_tool_creation() {
168 let tool = NodeTool::new();
169 assert_eq!(tool.name(), "node");
170 assert!(!tool.description().is_empty());
171 assert!(tool.aliases().contains(&"nodejs"));
172 }
173
174 #[test]
175 fn test_npm_tool_creation() {
176 let tool = NpmTool::new();
177 assert_eq!(tool.name(), "npm");
178 assert!(!tool.description().is_empty());
179 }
180
181 #[test]
182 fn test_npx_tool_creation() {
183 let tool = NpxTool::new();
184 assert_eq!(tool.name(), "npx");
185 assert!(!tool.description().is_empty());
186 }
187
188 #[test]
189 fn test_node_tool_metadata() {
190 let tool = NodeTool::new();
191 let metadata = tool.metadata();
192
193 assert!(metadata.contains_key("homepage"));
194 assert!(metadata.contains_key("ecosystem"));
195 assert_eq!(metadata.get("ecosystem"), Some(&"javascript".to_string()));
196 }
197}