use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result, bail};
use crate::addons::manifest::PackagesStep;
use super::Rollback;
enum PackageManager {
Npm,
Bun,
Pnpm,
Yarn,
Cargo,
}
impl PackageManager {
fn program(&self) -> &'static str {
match self {
PackageManager::Npm => "npm",
PackageManager::Bun => "bun",
PackageManager::Pnpm => "pnpm",
PackageManager::Yarn => "yarn",
PackageManager::Cargo => "cargo",
}
}
fn add_args(&self) -> &'static [&'static str] {
match self {
PackageManager::Npm => &["install"],
_ => &["add"],
}
}
fn dev_flag(&self) -> &'static str {
match self {
PackageManager::Npm | PackageManager::Pnpm => "--save-dev",
_ => "--dev",
}
}
fn snapshot_files(&self) -> &'static [&'static str] {
match self {
PackageManager::Cargo => &["Cargo.toml", "Cargo.lock"],
PackageManager::Npm => &["package.json", "package-lock.json"],
PackageManager::Bun => &["package.json", "bun.lock", "bun.lockb"],
PackageManager::Pnpm => &["package.json", "pnpm-lock.yaml"],
PackageManager::Yarn => &["package.json", "yarn.lock"],
}
}
}
fn detect_pm(root: &Path) -> Result<PackageManager> {
if root.join("bun.lock").exists() || root.join("bun.lockb").exists() {
Ok(PackageManager::Bun)
} else if root.join("pnpm-lock.yaml").exists() {
Ok(PackageManager::Pnpm)
} else if root.join("yarn.lock").exists() {
Ok(PackageManager::Yarn)
} else if root.join("package.json").exists() {
Ok(PackageManager::Npm)
} else if root.join("Cargo.toml").exists() {
Ok(PackageManager::Cargo)
} else {
bail!("No package manager detected (no package.json or Cargo.toml in the project root)")
}
}
pub fn execute_packages(step: &PackagesStep, project_root: &Path) -> Result<Vec<Rollback>> {
if step.dependencies.is_empty() && step.dev_dependencies.is_empty() {
return Ok(Vec::new());
}
let pm = detect_pm(project_root)?;
let mut rollbacks = Vec::new();
for name in pm.snapshot_files() {
let path = project_root.join(name);
if path.exists() {
rollbacks.push(Rollback::RestoreFile {
path: path.clone(),
original: std::fs::read(&path)?,
});
}
}
let run = |extra: &[&str], specs: &[String]| -> Result<()> {
let status = Command::new(pm.program())
.args(pm.add_args())
.args(extra)
.args(specs)
.current_dir(project_root)
.status()
.with_context(|| {
format!(
"failed to run '{}' — is it installed and on PATH?",
pm.program()
)
})?;
if !status.success() {
bail!("'{}' exited with {}", pm.program(), status);
}
Ok(())
};
let result = (|| {
if !step.dependencies.is_empty() {
run(&[], &step.dependencies)?;
}
if !step.dev_dependencies.is_empty() {
run(&[pm.dev_flag()], &step.dev_dependencies)?;
}
Ok(())
})();
if let Err(err) = result {
for rb in rollbacks.iter().rev() {
if let Rollback::RestoreFile { path, original } = rb {
let _ = std::fs::write(path, original);
}
}
return Err(err);
}
Ok(rollbacks)
}
pub fn detect_pm_for_tests(root: &Path) -> Result<&'static str> {
detect_pm(root).map(|pm| pm.program())
}