Skip to main content

zoi_cli/cmd/
update.rs

1use crate::cmd::utils as cmd_utils;
2use crate::cmd::ux;
3use crate::pkg::merge::handle_backup_files;
4use crate::pkg::{config, db, hooks, install, local, pin, resolve, transaction, types};
5use anyhow::{Result, anyhow};
6use colored::*;
7use dialoguer::{MultiSelect, theme::ColorfulTheme};
8use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
9use rayon::prelude::*;
10use semver::Version;
11use serde_json::json;
12use std::fs;
13use std::sync::Mutex;
14
15/// The primary high-level orchestration for the `zoi update` command.
16///
17/// This function handles:
18/// - Batch Updates: When `--all` is specified, it scans all installed packages.
19/// - Targeted Updates: Updates specific packages provided by name.
20/// - Advisory Deltas: Calculates and displays changes in security vulnerabilities.
21/// - Cleanup: Automatically removes old versions after a successful upgrade
22///   (if rollbacks are not required).
23pub fn run(
24    all: bool,
25    package_names: &[String],
26    yes: bool,
27    dry_run: bool,
28    explain: bool,
29    plan_json: bool,
30    interactive: bool,
31) -> Result<()> {
32    if all {
33        return run_update_all_logic(yes, dry_run, explain, plan_json, interactive);
34    }
35
36    let expanded_package_names = cmd_utils::expand_split_packages(package_names, "Updating")?;
37
38    let mut failed_packages = Vec::new();
39
40    for (i, package_name) in expanded_package_names.iter().enumerate() {
41        if i > 0 {
42            println!();
43        }
44        if let Err(e) = run_update_single_logic(package_name, yes, dry_run, explain, plan_json) {
45            eprintln!(
46                "{}: Failed to update '{}': {}",
47                "Error".red().bold(),
48                package_name,
49                e
50            );
51            failed_packages.push(package_name.clone());
52        }
53    }
54
55    if !failed_packages.is_empty() {
56        return Err(anyhow!(
57            "The following packages failed to update: {}",
58            failed_packages.join(", ")
59        ));
60    } else if !package_names.is_empty() && !dry_run {
61        println!("\n{}", "Success:".green());
62    }
63    Ok(())
64}
65
66fn run_update_single_logic(
67    package_name: &str,
68    yes: bool,
69    dry_run: bool,
70    explain: bool,
71    plan_json: bool,
72) -> Result<()> {
73    println!(
74        "{} Updating package '{}'...",
75        "::".bold().blue(),
76        package_name.cyan().bold()
77    );
78
79    let (new_pkg, new_version, _, _, registry_handle, _, _) =
80        resolve::resolve_package_and_version(package_name, None, false, yes)?;
81
82    if pin::is_pinned(package_name)? {
83        println!(
84            "Package '{}' is pinned. Skipping update.",
85            package_name.yellow()
86        );
87        return Ok(());
88    }
89
90    let installed_source = if let Some(sub) = &new_pkg.sub_package {
91        format!(
92            "#{}@{}/{}:{}",
93            registry_handle.as_deref().unwrap_or("local"),
94            new_pkg.repo,
95            new_pkg.name,
96            sub
97        )
98    } else {
99        format!(
100            "#{}@{}/{}",
101            registry_handle.as_deref().unwrap_or("local"),
102            new_pkg.repo,
103            new_pkg.name
104        )
105    };
106    let installed_request = resolve::parse_source_string(&installed_source)?;
107    let mut candidates = Vec::new();
108    candidates.extend(local::find_installed_manifests_matching(
109        &installed_request,
110        types::Scope::User,
111    )?);
112    candidates.extend(local::find_installed_manifests_matching(
113        &installed_request,
114        types::Scope::System,
115    )?);
116
117    let old_manifest =
118        crate::cmd::installed_select::choose_installed_manifest(package_name, &candidates, yes)
119            .map_err(|e| {
120                if candidates.is_empty() {
121                    anyhow!("Package '{package_name}' is not installed. Use 'zoi install' instead.")
122                } else {
123                    e
124                }
125            })?;
126
127    println!(
128        "Currently installed version: {}",
129        old_manifest.version.yellow()
130    );
131    if old_manifest.revision != "1" || new_pkg.revision != "1" {
132        println!(
133            "Currently installed revision: {}",
134            old_manifest.revision.yellow()
135        );
136    }
137    println!("Available version: {}", new_version.green());
138    if old_manifest.revision != "1" || new_pkg.revision != "1" {
139        println!("Available revision: {}", new_pkg.revision.green());
140    }
141
142    if old_manifest.version == new_version && old_manifest.revision == new_pkg.revision {
143        println!("\nPackage is already up to date.");
144        ux::print_transaction_summary(&ux::TransactionSummary {
145            command: "update".to_string(),
146            success: 0,
147            failed: 0,
148            skipped: 1,
149        });
150        return Ok(());
151    }
152
153    let (download_size, new_installed_size) = install::util::get_package_sizes(
154        &new_pkg,
155        registry_handle.as_deref().unwrap_or("local"),
156        &new_version,
157    );
158    let old_installed_size = old_manifest.installed_size.unwrap_or(0);
159    let installed_size_diff = new_installed_size as i64 - old_installed_size as i64;
160
161    println!();
162    if download_size > 0 {
163        println!(
164            "Total Download Size: {}",
165            crate::pkg::utils::format_bytes(download_size)
166        );
167    }
168    if installed_size_diff != 0 {
169        println!(
170            "Net Upgrade Size:    {}",
171            crate::pkg::utils::format_size_diff(installed_size_diff)
172        );
173    }
174    println!();
175
176    let preflight = ux::PreflightSummary::new("Update preflight")
177        .row("Package", new_pkg.name.clone())
178        .row("From", old_manifest.version.clone())
179        .row("To", new_version.clone())
180        .row("Scope", format!("{:?}", old_manifest.scope))
181        .row(
182            "Download size",
183            crate::pkg::utils::format_bytes(download_size),
184        )
185        .row(
186            "Net size",
187            crate::pkg::utils::format_size_diff(installed_size_diff),
188        );
189    ux::print_preflight(&preflight);
190
191    if explain {
192        let mut report = ux::ExplainReport::new("Update explanation");
193        report = report.item(
194            new_pkg.name.clone(),
195            format!(
196                "selected because newer version {} is available over installed {}",
197                new_version, old_manifest.version
198            ),
199            Vec::new(),
200        );
201        if let Ok((old_adv, new_adv)) = advisory_counts(
202            &old_manifest.registry_handle,
203            &new_pkg.name,
204            old_manifest.sub_package.as_deref(),
205            &old_manifest.version,
206            &new_version,
207        ) {
208            report = report.item(
209                "advisories",
210                format!(
211                    "old={}, new={}, delta={}",
212                    old_adv,
213                    new_adv,
214                    (new_adv as i64 - old_adv as i64)
215                ),
216                Vec::new(),
217            );
218        }
219        ux::print_explain(&report);
220    }
221
222    if plan_json {
223        let plan = json!({
224            "dry_run": dry_run,
225            "package": {
226                "name": new_pkg.name,
227                "sub_package": old_manifest.sub_package,
228                "registry": old_manifest.registry_handle,
229                "repo": old_manifest.repo,
230                "scope": format!("{:?}", old_manifest.scope),
231                "from_version": old_manifest.version,
232                "to_version": new_version,
233                "download_bytes": download_size,
234                "net_size_bytes": installed_size_diff,
235            }
236        });
237        ux::emit_plan_json_v1("update", plan)?;
238    }
239
240    if dry_run {
241        println!(
242            "{} Dry-run: would upgrade {} from {} to {}",
243            "::".bold().yellow(),
244            new_pkg.name,
245            old_manifest.version,
246            new_version
247        );
248        ux::print_transaction_summary(&ux::TransactionSummary {
249            command: "update".to_string(),
250            success: 0,
251            failed: 0,
252            skipped: 1,
253        });
254        return Ok(());
255    }
256
257    if !crate::utils::ask_for_confirmation(
258        &format!("Update from {} to {}?", old_manifest.version, new_version),
259        yes,
260    ) {
261        ux::print_transaction_summary(&ux::TransactionSummary {
262            command: "update".to_string(),
263            success: 0,
264            failed: 0,
265            skipped: 1,
266        });
267        return Ok(());
268    }
269
270    let mut transaction = transaction::begin()?;
271
272    if let Some(hooks) = &new_pkg.hooks {
273        hooks::run_hooks(hooks, hooks::HookType::PreUpgrade, old_manifest.scope)?;
274    }
275
276    let (graph, _) = install::resolver::resolve_dependency_graph(
277        &[package_name.to_string()],
278        Some(old_manifest.scope),
279        true,
280        yes,
281        false,
282        None,
283        false,
284    )?;
285
286    install::util::check_policy_compliance(&graph)?;
287    install::util::check_scope_compliance(&graph)?;
288    for node in graph.nodes.values() {
289        if !install::util::display_updates(&node.pkg, yes)? {
290            return Err(anyhow!("Update aborted by user."));
291        }
292    }
293    install::util::check_for_vulnerabilities(&graph, yes)?;
294
295    let install_plan = install::plan::create_install_plan(&graph.nodes, None, false)?;
296
297    let stages = graph.toposort()?;
298    let mut new_manifest_option: Option<types::InstallManifest> = None;
299
300    for stage in stages {
301        for pkg_id in stage {
302            let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
303                anyhow!("Package node '{}' missing from graph during update", pkg_id)
304            })?;
305            if let Some(action) = install_plan.get(&pkg_id) {
306                match install::installer::install_node(
307                    node, action, None, None, yes, true, true, false,
308                ) {
309                    Ok(m) => {
310                        if m.name == new_pkg.name {
311                            new_manifest_option = Some(m);
312                        }
313                    }
314                    Err(e) => {
315                        eprintln!("\nError: Update failed during installation. Rolling back...");
316                        transaction::rollback(&transaction.id)?;
317                        ux::print_transaction_summary(&ux::TransactionSummary {
318                            command: "update".to_string(),
319                            success: 0,
320                            failed: 1,
321                            skipped: 0,
322                        });
323                        return Err(anyhow!("Update failed: {}", e));
324                    }
325                }
326            }
327        }
328    }
329
330    if let Some(new_manifest) = new_manifest_option {
331        if let Err(e) = transaction::record_operation(
332            &mut transaction,
333            types::TransactionOperation::Upgrade {
334                old_manifest: Box::new(old_manifest.clone()),
335                new_manifest: Box::new(new_manifest.clone()),
336            },
337        ) {
338            eprintln!("Warning: Failed to record transaction for update: {}", e);
339            transaction::delete_log(&transaction.id)?;
340        } else {
341            if let Ok(modified_files) = transaction::get_modified_files(&transaction.id) {
342                let modified_packages =
343                    transaction::get_modified_packages(&transaction.id).unwrap_or_default();
344                let _ = crate::pkg::hooks::global::run_global_hooks(
345                    crate::pkg::hooks::global::HookWhen::PostTransaction,
346                    &modified_files,
347                    &modified_packages,
348                    "upgrade",
349                    old_manifest.scope,
350                );
351            }
352            transaction::commit(&transaction.id)?;
353        }
354
355        if let Some(backup_files) = &old_manifest.backup {
356            println!("Restoring configuration files...");
357            let old_version_dir = local::get_package_version_dir(
358                old_manifest.scope,
359                &old_manifest.registry_handle,
360                &old_manifest.repo,
361                &old_manifest.name,
362                &old_manifest.version,
363            )?;
364            let new_version_dir = local::get_package_version_dir(
365                new_manifest.scope,
366                &new_manifest.registry_handle,
367                &new_manifest.repo,
368                &new_manifest.name,
369                &new_manifest.version,
370            )?;
371
372            handle_backup_files(
373                &old_version_dir,
374                &new_version_dir,
375                backup_files,
376                old_manifest.scope,
377            )?;
378        }
379
380        cleanup_old_versions(
381            &new_pkg.name,
382            old_manifest.scope,
383            &new_pkg.repo,
384            registry_handle.as_deref().unwrap_or("local"),
385        )?;
386
387        let handle = registry_handle.as_deref().unwrap_or("local");
388        if let Ok(conn) = db::open_connection("local") {
389            let _ = db::update_package(
390                &conn,
391                &new_pkg,
392                handle,
393                Some(new_pkg.scope),
394                new_pkg.sub_package.as_deref(),
395                Some(&types::InstallReason::Direct),
396            );
397        }
398
399        if let Some(hooks) = &new_pkg.hooks {
400            hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_pkg.scope)?;
401        }
402
403        println!("\n{}", "Success:".green());
404        ux::print_transaction_summary(&ux::TransactionSummary {
405            command: "update".to_string(),
406            success: 1,
407            failed: 0,
408            skipped: 0,
409        });
410        Ok(())
411    } else {
412        eprintln!("\nError: Update failed to produce a new manifest. Rolling back...");
413        transaction::rollback(&transaction.id)?;
414        ux::print_transaction_summary(&ux::TransactionSummary {
415            command: "update".to_string(),
416            success: 0,
417            failed: 1,
418            skipped: 0,
419        });
420        Err(anyhow!("Update failed: could not get new manifest"))
421    }
422}
423
424fn run_update_all_logic(
425    yes: bool,
426    dry_run: bool,
427    explain: bool,
428    plan_json: bool,
429    interactive: bool,
430) -> Result<()> {
431    #[derive(Clone)]
432    struct UpdateCandidate {
433        source: String,
434        new_pkg: types::Package,
435        new_version: String,
436        old_manifest: types::InstallManifest,
437        old_advisories: usize,
438        new_advisories: usize,
439        download_size: u64,
440        new_installed_size: u64,
441    }
442
443    let installed_packages = local::get_installed_packages()?;
444    let mut pinned_sources = Vec::new();
445    let mut skipped_sources = Vec::new();
446    let mut up_to_date_sources = Vec::new();
447    let mut packages_to_upgrade: Vec<UpdateCandidate> = Vec::new();
448
449    // --- Phase 1: Upgrade Scanning ---
450    // We scan all installed packages and compare them against the latest registry metadata.
451    println!("{} Checking for upgrades...", "::".bold().blue());
452    let pb = ProgressBar::new(installed_packages.len() as u64);
453    pb.set_style(
454        ProgressStyle::default_bar()
455            .template(
456                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({msg})",
457            )?
458            .progress_chars("#>-"),
459    );
460    pb.set_message("Checking packages...");
461
462    for manifest in installed_packages {
463        let source = if let Some(sub) = &manifest.sub_package {
464            format!(
465                "#{}@{}/{}:{}",
466                manifest.registry_handle, manifest.repo, manifest.name, sub
467            )
468        } else {
469            format!(
470                "#{}@{}/{}",
471                manifest.registry_handle, manifest.repo, manifest.name
472            )
473        };
474
475        if pin::is_pinned(&source).unwrap_or(false)
476            || pin::is_pinned(&manifest.name).unwrap_or(false)
477        {
478            pinned_sources.push(source.clone());
479            pb.inc(1);
480            continue;
481        }
482
483        let (new_pkg, new_version, _, _, _registry_handle, _, _) =
484            match resolve::resolve_package_and_version(&source, Some(manifest.scope), true, false) {
485                Ok(result) => result,
486                Err(e) => {
487                    eprintln!(
488                        "{}: Failed to resolve '{}': {}",
489                        "Warning".yellow(),
490                        source,
491                        e
492                    );
493                    skipped_sources.push(format!("{} ({})", source, e));
494                    pb.inc(1);
495                    continue;
496                }
497            };
498
499        if manifest.version == new_version && manifest.revision == new_pkg.revision {
500            up_to_date_sources.push(source.clone());
501            pb.inc(1);
502            continue;
503        }
504
505        let (old_adv, new_adv) = advisory_counts(
506            &manifest.registry_handle,
507            &manifest.name,
508            manifest.sub_package.as_deref(),
509            &manifest.version,
510            &new_version,
511        )
512        .unwrap_or((0, 0));
513
514        let (download_size, new_installed_size) =
515            install::util::get_package_sizes(&new_pkg, &manifest.registry_handle, &new_version);
516
517        packages_to_upgrade.push(UpdateCandidate {
518            source,
519            new_pkg,
520            new_version,
521            old_manifest: manifest,
522            old_advisories: old_adv,
523            new_advisories: new_adv,
524            download_size,
525            new_installed_size,
526        });
527        pb.inc(1);
528    }
529    pb.finish_and_clear();
530
531    if !pinned_sources.is_empty() {
532        println!("\n{} Pinned (skipped)", "::".bold().blue());
533        for source in &pinned_sources {
534            println!("  - {}", source.yellow());
535        }
536    }
537    if !skipped_sources.is_empty() {
538        println!("\n{} Skipped (resolve failures)", "::".bold().blue());
539        for source in &skipped_sources {
540            println!("  - {}", source.red());
541        }
542    }
543    if !packages_to_upgrade.is_empty() {
544        println!("\n{} Upgrade candidates", "::".bold().blue());
545        for candidate in &packages_to_upgrade {
546            let delta = candidate.new_advisories as i64 - candidate.old_advisories as i64;
547            let advisory_suffix = if delta > 0 {
548                format!(" (advisories +{})", delta).red().to_string()
549            } else if delta < 0 {
550                format!(" (advisories {})", delta).green().to_string()
551            } else {
552                String::new()
553            };
554
555            let old_display = if candidate.old_manifest.revision != "1" {
556                format!(
557                    "{}-{}",
558                    candidate.old_manifest.version, candidate.old_manifest.revision
559                )
560            } else {
561                candidate.old_manifest.version.clone()
562            };
563
564            let new_display = if candidate.new_pkg.revision != "1" {
565                format!("{}-{}", candidate.new_version, candidate.new_pkg.revision)
566            } else {
567                candidate.new_version.clone()
568            };
569
570            println!(
571                "  - {}: {} -> {}{}",
572                candidate.source.cyan(),
573                old_display.yellow(),
574                new_display.green(),
575                advisory_suffix
576            );
577
578            let _ = install::util::display_updates(&candidate.new_pkg, true);
579        }
580    }
581
582    if packages_to_upgrade.is_empty() {
583        println!("\nAll packages are up to date.");
584        ux::print_transaction_summary(&ux::TransactionSummary {
585            command: "update".to_string(),
586            success: 0,
587            failed: 0,
588            skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
589        });
590        return Ok(());
591    }
592
593    if explain {
594        let mut report = ux::ExplainReport::new("Update explanation");
595        for candidate in &packages_to_upgrade {
596            report = report.item(
597                candidate.source.clone(),
598                format!(
599                    "selected because {} -> {}",
600                    candidate.old_manifest.version, candidate.new_version
601                ),
602                Vec::new(),
603            );
604        }
605        ux::print_explain(&report);
606    }
607
608    if interactive && !dry_run {
609        let items: Vec<String> = packages_to_upgrade
610            .iter()
611            .map(|c| {
612                format!(
613                    "{}  {} -> {}",
614                    c.source, c.old_manifest.version, c.new_version
615                )
616            })
617            .collect();
618        let selected = MultiSelect::with_theme(&ColorfulTheme::default())
619            .with_prompt("Select packages to update")
620            .items(&items)
621            .interact()
622            .map_err(|e| anyhow!("Interactive selection failed: {}", e))?;
623
624        if selected.is_empty() {
625            println!("No packages selected.");
626            ux::print_transaction_summary(&ux::TransactionSummary {
627                command: "update".to_string(),
628                success: 0,
629                failed: 0,
630                skipped: packages_to_upgrade.len()
631                    + pinned_sources.len()
632                    + skipped_sources.len()
633                    + up_to_date_sources.len(),
634            });
635            return Ok(());
636        }
637
638        let selected_set: std::collections::HashSet<usize> = selected.into_iter().collect();
639        let mut filtered = Vec::new();
640        for (idx, candidate) in packages_to_upgrade.into_iter().enumerate() {
641            if selected_set.contains(&idx) {
642                filtered.push(candidate);
643            }
644        }
645        packages_to_upgrade = filtered;
646    }
647
648    let total_download_size: u64 = packages_to_upgrade.iter().map(|c| c.download_size).sum();
649    let total_installed_size_diff: i64 = packages_to_upgrade
650        .iter()
651        .map(|c| c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64)
652        .sum();
653
654    let preflight = ux::PreflightSummary::new("Update preflight")
655        .row("Candidates", packages_to_upgrade.len().to_string())
656        .row("Pinned skipped", pinned_sources.len().to_string())
657        .row("Other skipped", skipped_sources.len().to_string())
658        .row("Up-to-date", up_to_date_sources.len().to_string())
659        .row(
660            "Download size",
661            crate::pkg::utils::format_bytes(total_download_size),
662        )
663        .row(
664            "Net size",
665            crate::pkg::utils::format_size_diff(total_installed_size_diff),
666        );
667    ux::print_preflight(&preflight);
668
669    if plan_json {
670        let packages: Vec<_> = packages_to_upgrade
671            .iter()
672            .map(|c| {
673                json!({
674                    "source": c.source,
675                    "name": c.new_pkg.name,
676                    "sub_package": c.old_manifest.sub_package,
677                    "from_version": c.old_manifest.version,
678                    "to_version": c.new_version,
679                    "download_bytes": c.download_size,
680                    "net_size_bytes": c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64,
681                    "advisories_old": c.old_advisories,
682                    "advisories_new": c.new_advisories,
683                })
684            })
685            .collect();
686
687        let plan = json!({
688            "dry_run": dry_run,
689            "interactive": interactive,
690            "totals": {
691                "candidates": packages_to_upgrade.len(),
692                "pinned_skipped": pinned_sources.len(),
693                "other_skipped": skipped_sources.len(),
694                "up_to_date": up_to_date_sources.len(),
695                "download_bytes": total_download_size,
696                "net_size_bytes": total_installed_size_diff,
697            },
698            "pinned": pinned_sources,
699            "skipped": skipped_sources,
700            "packages": packages,
701        });
702        ux::emit_plan_json_v1("update", plan)?;
703    }
704
705    if dry_run {
706        println!(
707            "\n{} Dry-run: upgrade plan above would be executed.",
708            "::".bold().yellow()
709        );
710        ux::print_transaction_summary(&ux::TransactionSummary {
711            command: "update".to_string(),
712            success: 0,
713            failed: 0,
714            skipped: packages_to_upgrade.len()
715                + pinned_sources.len()
716                + skipped_sources.len()
717                + up_to_date_sources.len(),
718        });
719        return Ok(());
720    }
721
722    println!();
723    if !crate::utils::ask_for_confirmation("Do you want to upgrade these packages?", yes) {
724        ux::print_transaction_summary(&ux::TransactionSummary {
725            command: "update".to_string(),
726            success: 0,
727            failed: 0,
728            skipped: packages_to_upgrade.len()
729                + pinned_sources.len()
730                + skipped_sources.len()
731                + up_to_date_sources.len(),
732        });
733        return Ok(());
734    }
735
736    // --- Phase 2: Transactional Upgrade ---
737    // We execute the upgrade plan. Each package is processed in parallel
738    // where possible, but all are wrapped in a single machine-wide transaction.
739    let transaction = Mutex::new(transaction::begin()?);
740    let transaction_id = transaction.lock().unwrap().id.clone();
741    let failed_updates = Mutex::new(Vec::new());
742    let successful_upgrades = Mutex::new(Vec::new());
743
744    let m = MultiProgress::new();
745
746    packages_to_upgrade
747        .par_iter()
748        .try_for_each(|candidate| -> Result<()> {
749            println!(
750                "\n{} Upgrading {} to {}...",
751                "::".bold().blue(),
752                candidate.source.cyan(),
753                candidate.new_version.green()
754            );
755
756            if let Some(hooks) = &candidate.new_pkg.hooks {
757                hooks::run_hooks(
758                    hooks,
759                    hooks::HookType::PreUpgrade,
760                    candidate.old_manifest.scope,
761                )?;
762            }
763
764            let (graph, _) = match install::resolver::resolve_dependency_graph(
765                std::slice::from_ref(&candidate.source),
766                Some(candidate.old_manifest.scope),
767                true,
768                yes,
769                false,
770                None,
771                false,
772            ) {
773                Ok(res) => res,
774                Err(e) => {
775                    eprintln!(
776                        "{}: Failed to resolve dependencies for '{}': {}",
777                        "Error".red().bold(),
778                        candidate.source,
779                        e
780                    );
781                    failed_updates
782                        .lock()
783                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
784                        .push(candidate.source.clone());
785                    return Ok(());
786                }
787            };
788
789            if let Err(e) = install::util::check_policy_compliance(&graph) {
790                eprintln!(
791                    "{}: Policy check failed for '{}': {}",
792                    "Error".red().bold(),
793                    candidate.source,
794                    e
795                );
796                failed_updates
797                    .lock()
798                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
799                    .push(candidate.source.clone());
800                return Ok(());
801            }
802
803            if let Err(e) = install::util::check_scope_compliance(&graph) {
804                eprintln!(
805                    "{}: Scope check failed for '{}': {}",
806                    "Error".red().bold(),
807                    candidate.source,
808                    e
809                );
810                failed_updates
811                    .lock()
812                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
813                    .push(candidate.source.clone());
814                return Ok(());
815            }
816
817            if let Err(e) = install::util::check_for_vulnerabilities(&graph, yes) {
818                eprintln!(
819                    "{}: Security check failed for '{}': {}",
820                    "Error".red().bold(),
821                    candidate.source,
822                    e
823                );
824                failed_updates
825                    .lock()
826                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
827                    .push(candidate.source.clone());
828                return Ok(());
829            }
830
831            let install_plan = match install::plan::create_install_plan(&graph.nodes, None, false) {
832                Ok(plan) => plan,
833                Err(e) => {
834                    eprintln!(
835                        "{}: Failed to create install plan for '{}': {}",
836                        "Error".red().bold(),
837                        candidate.source,
838                        e
839                    );
840                    failed_updates
841                        .lock()
842                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
843                        .push(candidate.source.clone());
844                    return Ok(());
845                }
846            };
847
848            let stages = match graph.toposort() {
849                Ok(s) => s,
850                Err(e) => {
851                    eprintln!(
852                        "{}: Failed to sort dependency graph for '{}': {}",
853                        "Error".red().bold(),
854                        candidate.source,
855                        e
856                    );
857                    failed_updates
858                        .lock()
859                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
860                        .push(candidate.source.clone());
861                    return Ok(());
862                }
863            };
864
865            let mut new_manifest_option: Option<types::InstallManifest> = None;
866            for stage in stages {
867                for pkg_id in stage {
868                    let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
869                        anyhow!("Package node '{}' missing from graph during update", pkg_id)
870                    })?;
871                    if let Some(action) = install_plan.get(&pkg_id) {
872                        match install::installer::install_node(
873                            node,
874                            action,
875                            Some(&m),
876                            None,
877                            yes,
878                            true,
879                            true,
880                            false,
881                        ) {
882                            Ok(m) => {
883                                if m.name == candidate.new_pkg.name
884                                    && m.sub_package == candidate.old_manifest.sub_package
885                                {
886                                    new_manifest_option = Some(m);
887                                }
888                            }
889                            Err(e) => {
890                                eprintln!(
891                                    "{}: Failed to upgrade '{}': {}",
892                                    "Error".red().bold(),
893                                    candidate.source,
894                                    e
895                                );
896                                failed_updates
897                                    .lock()
898                                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
899                                    .push(candidate.source.clone());
900                                return Ok(());
901                            }
902                        }
903                    }
904                }
905            }
906
907            if let Some(new_manifest) = new_manifest_option {
908                let mut tx_lock = transaction
909                    .lock()
910                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
911                if let Err(e) = transaction::record_operation(
912                    &mut tx_lock,
913                    types::TransactionOperation::Upgrade {
914                        old_manifest: Box::new(candidate.old_manifest.clone()),
915                        new_manifest: Box::new(new_manifest.clone()),
916                    },
917                ) {
918                    eprintln!(
919                        "Error: Failed to record transaction for {}: {}",
920                        candidate.source, e
921                    );
922                    failed_updates
923                        .lock()
924                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
925                        .push(candidate.source.clone());
926                } else {
927                    successful_upgrades
928                        .lock()
929                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
930                        .push((
931                            candidate.old_manifest.clone(),
932                            new_manifest.clone(),
933                            candidate.new_pkg.clone(),
934                        ));
935                }
936            } else {
937                eprintln!("Failed to get new manifest for {}", candidate.source);
938                failed_updates
939                    .lock()
940                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
941                    .push(candidate.source.clone());
942            }
943            Ok(())
944        })?;
945
946    let failed = failed_updates
947        .into_inner()
948        .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
949    if !failed.is_empty() {
950        eprintln!("\nError: Some packages failed to upgrade. Rolling back all changes...");
951        for pkg in &failed {
952            eprintln!("  - {}", pkg);
953        }
954        transaction::rollback(&transaction_id)?;
955        ux::print_transaction_summary(&ux::TransactionSummary {
956            command: "update".to_string(),
957            success: 0,
958            failed: failed.len(),
959            skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
960        });
961        return Err(anyhow!("Update failed for some packages."));
962    }
963
964    if let Ok(modified_files) = transaction::get_modified_files(&transaction_id) {
965        let modified_packages =
966            transaction::get_modified_packages(&transaction_id).unwrap_or_default();
967        let upgrades_lock = successful_upgrades
968            .lock()
969            .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
970        let first_scope = upgrades_lock
971            .first()
972            .map(|(old, _, _)| old.scope)
973            .unwrap_or(types::Scope::User);
974        let _ = crate::pkg::hooks::global::run_global_hooks(
975            crate::pkg::hooks::global::HookWhen::PostTransaction,
976            &modified_files,
977            &modified_packages,
978            "upgrade",
979            first_scope,
980        );
981    }
982    transaction::commit(&transaction_id)?;
983
984    println!("\n{}", "Success:".green());
985    let successful_upgrades = successful_upgrades
986        .into_inner()
987        .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
988    for (old_manifest, new_manifest, new_pkg) in &successful_upgrades {
989        if let Some(backup_files) = &old_manifest.backup {
990            println!(
991                "Restoring configuration for {}...",
992                old_manifest.name.cyan()
993            );
994            let old_version_dir = local::get_package_version_dir(
995                old_manifest.scope,
996                &old_manifest.registry_handle,
997                &old_manifest.repo,
998                &old_manifest.name,
999                &old_manifest.version,
1000            )?;
1001            let new_version_dir = local::get_package_version_dir(
1002                new_manifest.scope,
1003                &new_manifest.registry_handle,
1004                &new_manifest.repo,
1005                &new_manifest.name,
1006                &new_manifest.version,
1007            )?;
1008            handle_backup_files(
1009                &old_version_dir,
1010                &new_version_dir,
1011                backup_files,
1012                old_manifest.scope,
1013            )?;
1014        }
1015
1016        if let Err(e) = cleanup_old_versions(
1017            &new_manifest.name,
1018            new_manifest.scope,
1019            &new_manifest.repo,
1020            &new_manifest.registry_handle,
1021        ) {
1022            eprintln!(
1023                "Failed to clean up old versions for {}: {}",
1024                new_manifest.name, e
1025            );
1026        }
1027
1028        if let Ok(conn) = db::open_connection("local") {
1029            let _ = db::update_package(
1030                &conn,
1031                new_pkg,
1032                &new_manifest.registry_handle,
1033                Some(new_manifest.scope),
1034                new_manifest.sub_package.as_deref(),
1035                Some(&old_manifest.reason),
1036            );
1037        }
1038
1039        if let Some(hooks) = &new_pkg.hooks
1040            && let Err(e) =
1041                hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_manifest.scope)
1042        {
1043            eprintln!(
1044                "{}: Post-upgrade hook failed for '{}': {}",
1045                "Error".red().bold(),
1046                new_manifest.name,
1047                e
1048            );
1049        }
1050    }
1051
1052    ux::print_transaction_summary(&ux::TransactionSummary {
1053        command: "update".to_string(),
1054        success: successful_upgrades.len(),
1055        failed: 0,
1056        skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
1057    });
1058    println!("\n{}", "Success:".green());
1059    Ok(())
1060}
1061
1062fn advisory_counts(
1063    registry_handle: &str,
1064    package: &str,
1065    sub_package: Option<&str>,
1066    old_version: &str,
1067    new_version: &str,
1068) -> Result<(usize, usize)> {
1069    let advisories = db::get_advisories_for_package(registry_handle, package, sub_package)?;
1070    let old_ver = Version::parse(old_version)
1071        .map_err(|e| anyhow!("failed to parse old version '{}': {}", old_version, e))?;
1072    let new_ver = Version::parse(new_version)
1073        .map_err(|e| anyhow!("failed to parse new version '{}': {}", new_version, e))?;
1074
1075    let mut old_count = 0usize;
1076    let mut new_count = 0usize;
1077    for adv in advisories {
1078        if let Ok(req) = semver::VersionReq::parse(&adv.affected_range) {
1079            if req.matches(&old_ver) {
1080                old_count += 1;
1081            }
1082            if req.matches(&new_ver) {
1083                new_count += 1;
1084            }
1085        }
1086    }
1087    Ok((old_count, new_count))
1088}
1089
1090fn cleanup_old_versions(
1091    package_name: &str,
1092    scope: types::Scope,
1093    repo: &str,
1094    registry_handle: &str,
1095) -> Result<()> {
1096    let config = config::read_config()?;
1097    let rollback_enabled = config.rollback_enabled;
1098
1099    let package_dir = local::get_package_dir(scope, registry_handle, repo, package_name)?;
1100
1101    let mut versions = Vec::new();
1102    if let Ok(entries) = fs::read_dir(&package_dir) {
1103        for entry in entries.flatten() {
1104            let path = entry.path();
1105            if path.is_dir()
1106                && let Some(version_str) = path.file_name().and_then(|s| s.to_str())
1107                && version_str != "latest"
1108                && let Ok(version) = Version::parse(version_str)
1109            {
1110                versions.push(version);
1111            }
1112        }
1113    }
1114
1115    if versions.is_empty() {
1116        return Ok(());
1117    }
1118
1119    versions.sort();
1120
1121    let versions_to_keep = if rollback_enabled { 3 } else { 1 };
1122
1123    if versions.len() > versions_to_keep {
1124        let num_to_delete = versions.len() - versions_to_keep;
1125        let versions_to_delete = &versions[..num_to_delete];
1126
1127        println!("Cleaning up old versions...");
1128        for version in versions_to_delete {
1129            let version_dir_to_delete = package_dir.join(version.to_string());
1130            println!(" - Removing {}", version_dir_to_delete.display());
1131            if version_dir_to_delete.exists() {
1132                fs::remove_dir_all(version_dir_to_delete)?;
1133            }
1134        }
1135    }
1136
1137    Ok(())
1138}