mod cargo_pkg;
pub mod common;
mod config;
mod npm;
mod nuget;
mod pip;
pub use common::PackageError;
#[allow(unused_imports)] pub use config::{
CARGO_KNOBS, CargoConfig, NPM_KNOBS, NUGET_KNOBS, NpmConfig, NugetConfig, PIP_KNOBS,
PackagesConfig, PackagesConfigRef, PipConfig,
};
use cargo_pkg::CargoHandler;
use npm::NpmHandler;
use nuget::NugetHandler;
use pip::PipHandler;
use std::path::Path;
pub fn install_packages(
config: PackagesConfigRef<'_>,
project_dir: &Path,
) -> Result<(), PackageError> {
if config.origin == crate::ai_hooks::ConfigOrigin::Remote && !config.allow_remote_packages {
let any_configured = config.npm.is_some()
|| config.pip.is_some()
|| config.cargo.is_some()
|| config.nuget.is_some();
if any_configured {
tracing::warn!(
event = "packages.remote_refused",
reason = "allow_remote_packages_not_set",
);
eprintln!(
"\n Refusing to install packages from a remote config (`jarvy setup --from <url>`).\n \
Set `[packages] allow_remote = true` in the source config — or copy it locally —\n \
to authorize npm/pip/cargo/nuget installations from this origin."
);
return Ok(());
}
}
let telemetry_on = crate::observability::telemetry_gate::is_enabled();
if let Some(npm_config) = config.npm {
println!("\n Installing npm packages...");
let handler = NpmHandler::new(npm_config.clone(), project_dir.to_path_buf());
if let Err(e) = handler.install() {
if telemetry_on {
tracing::warn!(
event = "packages.install_failed",
ecosystem = "npm",
error_kind = e.kind(),
error = %e,
);
}
eprintln!(" Warning: npm install failed (see output above)");
}
}
if let Some(pip_config) = config.pip {
println!("\n Installing pip packages...");
let handler = PipHandler::new(pip_config.clone(), project_dir.to_path_buf());
if let Err(e) = handler.install() {
if telemetry_on {
tracing::warn!(
event = "packages.install_failed",
ecosystem = "pip",
error_kind = e.kind(),
error = %e,
);
}
eprintln!(" Warning: pip install failed (see output above)");
}
}
if let Some(cargo_config) = config.cargo {
println!("\n Installing cargo binaries...");
let handler = CargoHandler::new(cargo_config.clone());
if let Err(e) = handler.install() {
if telemetry_on {
tracing::warn!(
event = "packages.install_failed",
ecosystem = "cargo",
error_kind = e.kind(),
error = %e,
);
}
eprintln!(" Warning: cargo install failed (see output above)");
}
}
if let Some(nuget_config) = config.nuget {
println!("\n Installing .NET global tools...");
let handler = NugetHandler::new(nuget_config.clone());
if let Err(e) = handler.install() {
if telemetry_on {
tracing::warn!(
event = "packages.install_failed",
ecosystem = "nuget",
error_kind = e.kind(),
error = %e,
);
}
eprintln!(" Warning: nuget install failed (see output above)");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ai_hooks::ConfigOrigin;
fn ref_with_nuget_only(
nuget: &NugetConfig,
origin: ConfigOrigin,
allow_remote_packages: bool,
) -> PackagesConfigRef<'_> {
PackagesConfigRef {
npm: None,
pip: None,
cargo: None,
nuget: Some(nuget),
origin,
allow_remote_packages,
}
}
#[test]
fn remote_config_refused_without_opt_in() {
let nuget = NugetConfig::default();
let config = ref_with_nuget_only(&nuget, ConfigOrigin::Remote, false);
let tmp = tempfile::tempdir().unwrap();
let result = install_packages(config, tmp.path());
assert!(result.is_ok(), "trust gate should refuse silently");
}
#[test]
fn remote_config_allowed_with_opt_in_proceeds() {
let nuget = NugetConfig::default();
let config = ref_with_nuget_only(&nuget, ConfigOrigin::Remote, true);
let tmp = tempfile::tempdir().unwrap();
let result = install_packages(config, tmp.path());
assert!(result.is_ok());
}
#[test]
fn local_config_always_allowed() {
let nuget = NugetConfig::default();
let config = ref_with_nuget_only(&nuget, ConfigOrigin::Local, false);
let tmp = tempfile::tempdir().unwrap();
let result = install_packages(config, tmp.path());
assert!(result.is_ok());
}
}