//! NPM Package Installer //! //! Implements agent installation via npm. use std::path::Path; use tracing::{debug, info, warn}; use super::manager::{InstallResult, InstallationError}; use super::traits::AgentInstaller; use crate::types::installation::InstallationConfig; /// NPM Installer /// /// Installs agents via npm global installation. pub struct NpmInstaller { /// Use pnpm instead of npm if available prefer_pnpm: bool, } impl NpmInstaller { /// Create a new NPM installer pub fn new() -> Self { Self { prefer_pnpm: false } } /// Create NPM installer that prefers pnpm pub fn with_pnpm_preference() -> Self { Self { prefer_pnpm: true } } /// Get the npm command to use (npm or pnpm) async fn get_npm_command(&self) -> &'static str { if self.prefer_pnpm && which::which("pnpm").is_ok() { "pnpm" } else if which::which("npm").is_ok() { "npm" } else { "npm" // Default, will fail if not available } } /// Parse version from npm output fn parse_installed_version(&self, output: &str) -> Option { // npm list output format: "package@version" for line in output.lines() { if line.contains('@') { if let Some(version) = line.split('@').last() { return Some(version.trim().to_string()); } } } None } } impl Default for NpmInstaller { fn default() -> Self { Self::new() } } #[async_trait::async_trait] impl AgentInstaller for NpmInstaller { async fn install( &self, config: &InstallationConfig, _install_dir: Option<&Path>, ) -> Result { let npm_cmd = self.get_npm_command().await; // Check if npm is available if !self.command_exists(npm_cmd).await { return Err(InstallationError::PackageManagerNotAvailable( npm_cmd.to_string(), )); } let package_name = config.package_name.as_ref().ok_or_else(|| { InstallationError::ConfigError("Package name is required".to_string()) })?; // Build package spec with version let package_spec = if let Some(version) = &config.version { if version == "latest" { package_name.clone() } else { format!("{}@{}", package_name, version) } } else { package_name.clone() }; info!(" running {} : {}", npm_cmd, package_spec); // Run npm install -g // 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc) let output = if package_name == "nuwaxcode" { info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/"); self.run_command( npm_cmd, &[ "install", "-g", &package_spec, "--registry=https://registry.npmjs.org/", ], ) .await? } else { self.run_command(npm_cmd, &["install", "-g", &package_spec]) .await? }; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); debug!("npm install stdout: {}", stdout); if !stderr.is_empty() { debug!("npm install stderr: {}", stderr); } // Try to get installed version let version = self.parse_installed_version(&stdout); info!("Package installed: {}", package_spec); Ok(InstallResult::success( version, format!("Successfully installed {}", package_spec), )) } else { let stderr = String::from_utf8_lossy(&output.stderr); warn!("Installation failed: {}", stderr); Err(InstallationError::InstallFailed { package: package_spec, reason: stderr.to_string(), }) } } async fn validate(&self, config: &InstallationConfig) -> Result { // Check if the package is globally installed let npm_cmd = self.get_npm_command().await; if !self.command_exists(npm_cmd).await { return Ok(false); } let package_name = match &config.package_name { Some(name) => name, None => return Ok(false), }; // Check global list let output = self .run_command(npm_cmd, &["list", "-g", "--depth=0", package_name]) .await?; Ok(output.status.success()) } async fn update( &self, config: &InstallationConfig, _install_dir: Option<&Path>, ) -> Result { let npm_cmd = self.get_npm_command().await; if !self.command_exists(npm_cmd).await { return Err(InstallationError::PackageManagerNotAvailable( npm_cmd.to_string(), )); } let package_name = config.package_name.as_ref().ok_or_else(|| { InstallationError::ConfigError("Package name is required".to_string()) })?; info!("Updating global package: {}", package_name); // Run npm update -g // 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc) let output = if package_name == "nuwaxcode" { info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/"); self.run_command( npm_cmd, &[ "update", "-g", package_name, "--registry=https://registry.npmjs.org/", ], ) .await? } else { self.run_command(npm_cmd, &["update", "-g", package_name]) .await? }; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let version = self.parse_installed_version(&stdout); info!("Update succeeded: {}", package_name); Ok(InstallResult::success( version, format!("Successfully updated {}", package_name), )) } else { let stderr = String::from_utf8_lossy(&output.stderr); warn!("Update failed: {}", stderr); Err(InstallationError::InstallFailed { package: package_name.clone(), reason: stderr.to_string(), }) } } fn package_manager_name(&self) -> &'static str { "npm" } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_npm_command_detection() { let installer = NpmInstaller::new(); let cmd = installer.get_npm_command().await; // Should return "npm" or "pnpm" assert!(cmd == "npm" || cmd == "pnpm"); } }