use anyhow::{Context, Result};
use check_updates_core::{DependencyResolver, Version};
use clap::Parser;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::{HashMap, HashSet};
use ncu::cli::Args;
use ncu::detector::ProjectDetector;
use ncu::global::{generate_upgrade_commands, GlobalCheck, GlobalPackageDiscovery};
use ncu::npm::NpmClient;
use ncu::output::{GlobalTableRenderer, TableRenderer};
use ncu::parsers::{LockfileParser, PackageJsonParser};
use ncu::updater::FileUpdater;
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.global {
run_global_mode(&args).await
} else {
run_project_mode(&args).await
}
}
async fn run_global_mode(args: &Args) -> Result<()> {
if args.update {
println!(
"Note: --update flag is ignored in global mode. Commands will be shown instead.\n"
);
}
let discovery = GlobalPackageDiscovery::new();
let packages = discovery.discover();
if packages.is_empty() {
println!("No globally installed npm packages found.");
return Ok(());
}
let client = NpmClient::new(args.pre_release);
let package_names: Vec<String> = packages
.iter()
.map(|p| p.name.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect();
let progress = ProgressBar::new(package_names.len() as u64);
progress.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.expect("valid progress template")
.progress_chars("=>-"),
);
let pb = progress.clone();
let results = client
.get_packages(&package_names, move |done, _total| {
pb.set_position(done as u64);
})
.await;
progress.finish_and_clear();
let mut package_infos: HashMap<String, _> = HashMap::new();
let mut errors: Vec<(String, String)> = Vec::new();
for (name, result) in results {
match result {
Ok(info) => {
package_infos.insert(name, info);
}
Err(e) => {
errors.push((name, e.to_string()));
}
}
}
let mut checks: Vec<GlobalCheck> = Vec::new();
for package in packages {
if let Some(info) = package_infos.get(&package.name) {
let target = if args.minor {
info.versions
.iter()
.filter(|v| v.major == package.installed_version.major)
.max()
.cloned()
.unwrap_or_else(|| package.installed_version.clone())
} else {
info.latest.clone()
};
let has_update = target > package.installed_version;
checks.push(GlobalCheck {
package,
latest: target,
has_update,
});
}
}
let renderer = GlobalTableRenderer::new(true);
renderer.render(&checks);
let commands = generate_upgrade_commands(&checks);
if !commands.is_empty() {
println!();
println!("To upgrade, run:\n");
for cmd in &commands {
println!(" $ {cmd}");
}
}
if !errors.is_empty() {
println!();
println!("{}", "Packages not found on npm:".dimmed());
for (name, error) in errors {
println!(" {}: {}", name.dimmed(), error.dimmed());
}
}
Ok(())
}
async fn run_project_mode(args: &Args) -> Result<()> {
let project_path = args.project_path();
if !project_path.exists() {
anyhow::bail!("Project path does not exist: {project_path:?}");
}
let detector = ProjectDetector::new(project_path.clone());
let detected_files = detector.detect()?;
if detected_files.is_empty() {
println!("No package.json files found in {project_path:?}");
return Ok(());
}
let installed_versions: HashMap<String, Version> =
if let Some(lockfile_type) = detector.detect_lockfile() {
let lockfile_path = detector.lockfile_path(lockfile_type);
LockfileParser::new()
.parse(&lockfile_path, lockfile_type)
.unwrap_or_default()
} else {
HashMap::new()
};
let parser = PackageJsonParser::new();
let mut all_deps = Vec::new();
for file in &detected_files {
let deps = parser
.parse(&file.path)
.with_context(|| format!("Failed to parse {}", file.path.display()))?;
all_deps.extend(deps);
}
if all_deps.is_empty() {
println!("No dependencies found");
return Ok(());
}
let mut seen = HashSet::new();
all_deps.retain(|d| seen.insert(d.name.clone()));
let client = NpmClient::new(args.pre_release);
let package_names: Vec<String> = all_deps.iter().map(|d| d.name.clone()).collect();
let progress = ProgressBar::new(package_names.len() as u64);
progress.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.expect("valid progress template")
.progress_chars("=>-"),
);
let pb = progress.clone();
let results = client
.get_packages(&package_names, move |done, _total| {
pb.set_position(done as u64);
})
.await;
progress.finish_and_clear();
let mut package_infos: HashMap<String, _> = HashMap::new();
let mut errors: Vec<(String, String)> = Vec::new();
for (name, result) in results {
match result {
Ok(info) => {
package_infos.insert(name, info);
}
Err(e) => {
errors.push((name, e.to_string()));
}
}
}
let resolver = DependencyResolver::new();
let mut checks = Vec::new();
for dep in &all_deps {
if let Some(info) = package_infos.get(&dep.name) {
let installed = installed_versions.get(&dep.name);
let check = resolver.resolve(dep, info, installed);
checks.push(check);
}
}
let renderer = TableRenderer::new(true);
renderer.render(&checks);
if args.update {
let updater = FileUpdater::new();
let result = updater.apply_updates(&checks, args.minor, args.force)?;
result.print_summary();
} else if checks.iter().any(check_updates_core::DependencyCheck::has_update) {
println!();
println!("Run -u to upgrade patch, -um to upgrade patch+minors, and -uf to force upgrade all.");
}
if !errors.is_empty() {
println!();
println!("Packages not found on npm:");
for (name, error) in errors {
println!(" {name}: {error}");
}
}
Ok(())
}