upstream-rs 1.15.0

Fetch package updates directly from the source.
Documentation
use anyhow::{Result, anyhow};
use console::style;

use crate::services::packaging::RollbackManager;
use crate::services::storage::{
    metadata_storage::MetadataStorage, package_storage::PackageStorage,
    rollback_storage::RollbackStorage,
};
use crate::utils::static_paths::UpstreamPaths;

pub fn run(names: Vec<String>, prune: bool, dry_run: bool) -> Result<()> {
    let paths = UpstreamPaths::new()?;
    let mut package_storage = PackageStorage::new(&paths.config.packages_file)?;
    let mut metadata_storage = MetadataStorage::new(&paths.config.metadata_file)?;
    let rollback_file = RollbackManager::rollback_file_path(&paths);
    let mut rollback_storage = RollbackStorage::new(&rollback_file)?;

    let mut manager = RollbackManager::new(
        &paths,
        &mut package_storage,
        &mut metadata_storage,
        &mut rollback_storage,
    );

    if prune {
        return run_prune(names, dry_run, &mut manager);
    }

    if names.is_empty() {
        return Err(anyhow!(
            "At least one package name is required unless --prune is provided"
        ));
    }

    if dry_run {
        println!("{}", style("Dry run: rollback preview").bold());
        for name in &names {
            let Some(record) = manager.rollback_record(name) else {
                println!("{:<7} {:<28} no rollback data found", "[x]", name);
                continue;
            };

            let target = record
                .package_snapshot
                .install_path
                .as_ref()
                .map(|p| p.display().to_string())
                .unwrap_or_else(|| "<missing>".to_string());
            println!(
                "{:<7} {:<28} would restore rollback from {} ({:?})",
                "[plan]", name, target, record.source
            );
        }
        println!("  actions: resolve only (no restore, no prune, no metadata changes)");
        return Ok(());
    }

    let mut restored = 0_u32;
    let mut failed = 0_u32;
    for name in &names {
        let mut msg = Some(|line: &str| println!("{line}"));
        match manager.restore_package(name, &mut msg) {
            Ok(_) => {
                println!("{:<7} {:<28} restored", "[✓]", name);
                restored += 1;
            }
            Err(err) => {
                println!("{:<7} {:<28} {}", "[!]", name, err);
                failed += 1;
            }
        }
    }

    if failed > 0 {
        println!(
            "{}",
            style(format!(
                "Rollback complete: {} restored, {} failed.",
                restored, failed
            ))
            .yellow()
        );
    } else {
        println!(
            "{}",
            style(format!(
                "Rollback complete: {} restored, 0 failed.",
                restored
            ))
            .green()
        );
    }

    Ok(())
}

fn run_prune(names: Vec<String>, dry_run: bool, manager: &mut RollbackManager<'_>) -> Result<()> {
    let target_names = if names.is_empty() {
        manager.rollback_packages()
    } else {
        names
    };

    if dry_run {
        println!("{}", style("Dry run: rollback prune preview").bold());
        if target_names.is_empty() {
            println!("No rollback artifacts to prune.");
            println!("  actions: resolve only (no prune, no metadata changes)");
            return Ok(());
        }

        for name in &target_names {
            if manager.rollback_record(name).is_some() {
                println!("{:<7} {:<28} would prune rollback artifact", "[plan]", name);
            } else {
                println!("{:<7} {:<28} no rollback data found", "[x]", name);
            }
        }
        println!("  actions: resolve only (no prune, no metadata changes)");
        return Ok(());
    }

    let mut pruned = 0_u32;
    let mut missing = 0_u32;
    for name in &target_names {
        if manager.prune_package(name)? {
            println!("{:<7} {:<28} pruned", "[✓]", name);
            pruned += 1;
        } else {
            println!("{:<7} {:<28} no rollback data found", "[x]", name);
            missing += 1;
        }
    }

    if target_names.is_empty() {
        println!("No rollback artifacts to prune.");
    } else {
        println!(
            "{}",
            style(format!(
                "Rollback prune complete: {} pruned, {} missing.",
                pruned, missing
            ))
            .green()
        );
    }

    Ok(())
}