Files
qiming/qiming-rcoder/crates/agent_config/src/installer/npm_installer.rs
2026-06-01 13:54:52 +08:00

237 lines
7.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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");
}
}