237 lines
7.0 KiB
Rust
237 lines
7.0 KiB
Rust
//! 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<String> {
|
||
// 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<InstallResult, InstallationError> {
|
||
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<bool, InstallationError> {
|
||
// 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<InstallResult, InstallationError> {
|
||
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");
|
||
}
|
||
}
|