use crate::io::read_lock;
use crate::resolver::fetch_packagist_versions_bulk;
use crate::utils::is_prerelease_version;
use crate::utils::{print_error, print_info, print_success};
use anyhow::Result;
use semver::Version;
use std::path::Path;
pub async fn check_outdated_packages(working_dir: &Path, quiet: bool) -> Result<()> {
if !quiet {
print_info("🔍 Checking for outdated packages...");
}
let lock_path = working_dir.join("composer.lock");
if !lock_path.exists() {
print_error("❌ No composer.lock found. Run 'lectern install' first.");
return Ok(());
}
let lock = read_lock(&lock_path)?;
let total_packages = lock.packages.len() + lock.packages_dev.len();
if total_packages == 0 {
if !quiet {
print_info("📦 No packages installed.");
}
return Ok(());
}
let mut package_names: Vec<String> = Vec::new();
for pkg in lock.packages.iter().chain(lock.packages_dev.iter()) {
if pkg.name.starts_with("php")
|| pkg.name.starts_with("ext-")
|| pkg.name.starts_with("lib-")
|| pkg.name == "hhvm"
{
continue;
}
package_names.push(pkg.name.clone());
}
if package_names.is_empty() {
if !quiet {
print_success("✅ All packages are up to date!");
}
return Ok(());
}
let versions_map = fetch_packagist_versions_bulk(&package_names).await?;
let mut outdated_count = 0;
let mut table_rows = Vec::new();
for package_name in package_names.clone() {
let locked_pkg = lock
.packages
.iter()
.find(|p| p.name == package_name)
.or_else(|| lock.packages_dev.iter().find(|p| p.name == package_name));
if let Some(locked_pkg) = locked_pkg {
if let Some(versions) = versions_map.get(&package_name) {
let mut latest_version = None;
let mut latest_parsed: Option<Version> = None;
let current_version_str = locked_pkg.version.trim_start_matches('v');
let current_parsed = Version::parse(current_version_str).ok();
let mut version_list: Vec<_> = versions.iter().collect();
version_list.sort_by(|a, b| {
let a_clean = a.version.trim_start_matches('v');
let b_clean = b.version.trim_start_matches('v');
match (Version::parse(a_clean), Version::parse(b_clean)) {
(Ok(va), Ok(vb)) => vb.cmp(&va), _ => std::cmp::Ordering::Equal,
}
});
for version_data in version_list {
let version_str = &version_data.version;
if is_prerelease_version(version_str.as_str()) {
continue;
}
let clean_version = version_str.trim_start_matches('v');
if let Ok(parsed_version) = Version::parse(clean_version) {
latest_parsed = Some(parsed_version);
latest_version = Some(version_str.clone());
break; }
}
if let (Some(current), Some(latest_ver), Some(latest_str)) =
(current_parsed, latest_parsed, latest_version)
{
if latest_ver > current {
outdated_count += 1;
let description = versions
.iter()
.find(|v| v.version == latest_str)
.and_then(|v| v.other.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
table_rows.push((
package_name.clone(),
locked_pkg.version.clone(),
latest_str,
description,
));
}
}
}
}
}
if outdated_count == 0 {
if !quiet {
print_success("✅ All packages are up to date!");
}
} else if !quiet {
println!("\n📊 Outdated Packages ({outdated_count} found):");
println!(
"{:<30} {:<15} {:<15} Description",
"Package", "Current", "Latest"
);
println!("{}", "-".repeat(100));
for (name, current, latest, desc) in table_rows {
let short_desc = if desc.len() > 30 {
format!("{}...", &desc[..27])
} else {
desc
};
println!("{name:<30} {current:<15} {latest:<15} {short_desc}");
}
println!("\nRun 'lectern update' to update packages.");
}
Ok(())
}