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        None,
285    )?;
286
287    install::util::check_policy_compliance(&graph)?;
288    install::util::check_scope_compliance(&graph)?;
289    for node in graph.nodes.values() {
290        if !install::util::display_updates(&node.pkg, yes)? {
291            return Err(anyhow!("Update aborted by user."));
292        }
293    }
294    install::util::check_for_vulnerabilities(&graph, yes)?;
295
296    let install_plan = install::plan::create_install_plan(&graph.nodes, None, false)?;
297
298    let stages = graph.toposort()?;
299    let mut new_manifest_option: Option<types::InstallManifest> = None;
300
301    for stage in stages {
302        for pkg_id in stage {
303            let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
304                anyhow!("Package node '{}' missing from graph during update", pkg_id)
305            })?;
306            if let Some(action) = install_plan.get(&pkg_id) {
307                match install::installer::install_node(
308                    node, action, None, None, yes, true, true, false,
309                ) {
310                    Ok(m) => {
311                        if m.name == new_pkg.name {
312                            new_manifest_option = Some(m);
313                        }
314                    }
315                    Err(e) => {
316                        eprintln!("\nError: Update failed during installation. Rolling back...");
317                        transaction::rollback(&transaction.id)?;
318                        ux::print_transaction_summary(&ux::TransactionSummary {
319                            command: "update".to_string(),
320                            success: 0,
321                            failed: 1,
322                            skipped: 0,
323                        });
324                        return Err(anyhow!("Update failed: {}", e));
325                    }
326                }
327            }
328        }
329    }
330
331    if let Some(new_manifest) = new_manifest_option {
332        if let Err(e) = transaction::record_operation(
333            &mut transaction,
334            types::TransactionOperation::Upgrade {
335                old_manifest: Box::new(old_manifest.clone()),
336                new_manifest: Box::new(new_manifest.clone()),
337            },
338        ) {
339            eprintln!("Warning: Failed to record transaction for update: {}", e);
340            transaction::delete_log(&transaction.id)?;
341        } else {
342            if let Ok(modified_files) = transaction::get_modified_files(&transaction.id) {
343                let modified_packages =
344                    transaction::get_modified_packages(&transaction.id).unwrap_or_default();
345                let _ = crate::pkg::hooks::global::run_global_hooks(
346                    crate::pkg::hooks::global::HookWhen::PostTransaction,
347                    &modified_files,
348                    &modified_packages,
349                    "upgrade",
350                    old_manifest.scope,
351                );
352            }
353            transaction::commit(&transaction.id)?;
354        }
355
356        if let Some(backup_files) = &old_manifest.backup {
357            println!("Restoring configuration files...");
358            let old_version_dir = local::get_package_version_dir(
359                old_manifest.scope,
360                &old_manifest.registry_handle,
361                &old_manifest.repo,
362                &old_manifest.name,
363                &old_manifest.version,
364            )?;
365            let new_version_dir = local::get_package_version_dir(
366                new_manifest.scope,
367                &new_manifest.registry_handle,
368                &new_manifest.repo,
369                &new_manifest.name,
370                &new_manifest.version,
371            )?;
372
373            handle_backup_files(
374                &old_version_dir,
375                &new_version_dir,
376                backup_files,
377                old_manifest.scope,
378            )?;
379        }
380
381        cleanup_old_versions(
382            &new_pkg.name,
383            old_manifest.scope,
384            &new_pkg.repo,
385            registry_handle.as_deref().unwrap_or("local"),
386        )?;
387
388        let handle = registry_handle.as_deref().unwrap_or("local");
389        if let Ok(conn) = db::open_connection("local") {
390            let _ = db::update_package(
391                &conn,
392                &new_pkg,
393                handle,
394                Some(new_pkg.scope),
395                new_pkg.sub_package.as_deref(),
396                Some(&types::InstallReason::Direct),
397            );
398        }
399
400        if let Some(hooks) = &new_pkg.hooks {
401            hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_pkg.scope)?;
402        }
403
404        println!("\n{}", "Success:".green());
405        ux::print_transaction_summary(&ux::TransactionSummary {
406            command: "update".to_string(),
407            success: 1,
408            failed: 0,
409            skipped: 0,
410        });
411        Ok(())
412    } else {
413        eprintln!("\nError: Update failed to produce a new manifest. Rolling back...");
414        transaction::rollback(&transaction.id)?;
415        ux::print_transaction_summary(&ux::TransactionSummary {
416            command: "update".to_string(),
417            success: 0,
418            failed: 1,
419            skipped: 0,
420        });
421        Err(anyhow!("Update failed: could not get new manifest"))
422    }
423}
424
425fn run_update_all_logic(
426    yes: bool,
427    dry_run: bool,
428    explain: bool,
429    plan_json: bool,
430    interactive: bool,
431) -> Result<()> {
432    #[derive(Clone)]
433    struct UpdateCandidate {
434        source: String,
435        new_pkg: types::Package,
436        new_version: String,
437        old_manifest: types::InstallManifest,
438        old_advisories: usize,
439        new_advisories: usize,
440        download_size: u64,
441        new_installed_size: u64,
442    }
443
444    let installed_packages = local::get_installed_packages()?;
445    let mut pinned_sources = Vec::new();
446    let mut skipped_sources = Vec::new();
447    let mut up_to_date_sources = Vec::new();
448    let mut packages_to_upgrade: Vec<UpdateCandidate> = Vec::new();
449
450    // --- Phase 1: Upgrade Scanning ---
451    // We scan all installed packages and compare them against the latest registry metadata.
452    println!("{} Checking for upgrades...", "::".bold().blue());
453    let pb = ProgressBar::new(installed_packages.len() as u64);
454    pb.set_style(
455        ProgressStyle::default_bar()
456            .template(
457                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({msg})",
458            )?
459            .progress_chars("#>-"),
460    );
461    pb.set_message("Checking packages...");
462
463    for manifest in installed_packages {
464        let source = if let Some(sub) = &manifest.sub_package {
465            format!(
466                "#{}@{}/{}:{}",
467                manifest.registry_handle, manifest.repo, manifest.name, sub
468            )
469        } else {
470            format!(
471                "#{}@{}/{}",
472                manifest.registry_handle, manifest.repo, manifest.name
473            )
474        };
475
476        if pin::is_pinned(&source).unwrap_or(false)
477            || pin::is_pinned(&manifest.name).unwrap_or(false)
478        {
479            pinned_sources.push(source.clone());
480            pb.inc(1);
481            continue;
482        }
483
484        let (new_pkg, new_version, _, _, _registry_handle, _, _) =
485            match resolve::resolve_package_and_version(&source, Some(manifest.scope), true, false) {
486                Ok(result) => result,
487                Err(e) => {
488                    eprintln!(
489                        "{}: Failed to resolve '{}': {}",
490                        "Warning".yellow(),
491                        source,
492                        e
493                    );
494                    skipped_sources.push(format!("{} ({})", source, e));
495                    pb.inc(1);
496                    continue;
497                }
498            };
499
500        if manifest.version == new_version && manifest.revision == new_pkg.revision {
501            up_to_date_sources.push(source.clone());
502            pb.inc(1);
503            continue;
504        }
505
506        let (old_adv, new_adv) = advisory_counts(
507            &manifest.registry_handle,
508            &manifest.name,
509            manifest.sub_package.as_deref(),
510            &manifest.version,
511            &new_version,
512        )
513        .unwrap_or((0, 0));
514
515        let (download_size, new_installed_size) =
516            install::util::get_package_sizes(&new_pkg, &manifest.registry_handle, &new_version);
517
518        packages_to_upgrade.push(UpdateCandidate {
519            source,
520            new_pkg,
521            new_version,
522            old_manifest: manifest,
523            old_advisories: old_adv,
524            new_advisories: new_adv,
525            download_size,
526            new_installed_size,
527        });
528        pb.inc(1);
529    }
530    pb.finish_and_clear();
531
532    if !pinned_sources.is_empty() {
533        println!("\n{} Pinned (skipped)", "::".bold().blue());
534        for source in &pinned_sources {
535            println!("  - {}", source.yellow());
536        }
537    }
538    if !skipped_sources.is_empty() {
539        println!("\n{} Skipped (resolve failures)", "::".bold().blue());
540        for source in &skipped_sources {
541            println!("  - {}", source.red());
542        }
543    }
544    if !packages_to_upgrade.is_empty() {
545        println!("\n{} Upgrade candidates", "::".bold().blue());
546        for candidate in &packages_to_upgrade {
547            let delta = candidate.new_advisories as i64 - candidate.old_advisories as i64;
548            let advisory_suffix = if delta > 0 {
549                format!(" (advisories +{})", delta).red().to_string()
550            } else if delta < 0 {
551                format!(" (advisories {})", delta).green().to_string()
552            } else {
553                String::new()
554            };
555
556            let old_display = if candidate.old_manifest.revision != "1" {
557                format!(
558                    "{}-{}",
559                    candidate.old_manifest.version, candidate.old_manifest.revision
560                )
561            } else {
562                candidate.old_manifest.version.clone()
563            };
564
565            let new_display = if candidate.new_pkg.revision != "1" {
566                format!("{}-{}", candidate.new_version, candidate.new_pkg.revision)
567            } else {
568                candidate.new_version.clone()
569            };
570
571            println!(
572                "  - {}: {} -> {}{}",
573                candidate.source.cyan(),
574                old_display.yellow(),
575                new_display.green(),
576                advisory_suffix
577            );
578
579            let _ = install::util::display_updates(&candidate.new_pkg, true);
580        }
581    }
582
583    if packages_to_upgrade.is_empty() {
584        println!("\nAll packages are up to date.");
585        ux::print_transaction_summary(&ux::TransactionSummary {
586            command: "update".to_string(),
587            success: 0,
588            failed: 0,
589            skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
590        });
591        return Ok(());
592    }
593
594    if explain {
595        let mut report = ux::ExplainReport::new("Update explanation");
596        for candidate in &packages_to_upgrade {
597            report = report.item(
598                candidate.source.clone(),
599                format!(
600                    "selected because {} -> {}",
601                    candidate.old_manifest.version, candidate.new_version
602                ),
603                Vec::new(),
604            );
605        }
606        ux::print_explain(&report);
607    }
608
609    if interactive && !dry_run {
610        let items: Vec<String> = packages_to_upgrade
611            .iter()
612            .map(|c| {
613                format!(
614                    "{}  {} -> {}",
615                    c.source, c.old_manifest.version, c.new_version
616                )
617            })
618            .collect();
619        let selected = MultiSelect::with_theme(&ColorfulTheme::default())
620            .with_prompt("Select packages to update")
621            .items(&items)
622            .interact()
623            .map_err(|e| anyhow!("Interactive selection failed: {}", e))?;
624
625        if selected.is_empty() {
626            println!("No packages selected.");
627            ux::print_transaction_summary(&ux::TransactionSummary {
628                command: "update".to_string(),
629                success: 0,
630                failed: 0,
631                skipped: packages_to_upgrade.len()
632                    + pinned_sources.len()
633                    + skipped_sources.len()
634                    + up_to_date_sources.len(),
635            });
636            return Ok(());
637        }
638
639        let selected_set: std::collections::HashSet<usize> = selected.into_iter().collect();
640        let mut filtered = Vec::new();
641        for (idx, candidate) in packages_to_upgrade.into_iter().enumerate() {
642            if selected_set.contains(&idx) {
643                filtered.push(candidate);
644            }
645        }
646        packages_to_upgrade = filtered;
647    }
648
649    let total_download_size: u64 = packages_to_upgrade.iter().map(|c| c.download_size).sum();
650    let total_installed_size_diff: i64 = packages_to_upgrade
651        .iter()
652        .map(|c| c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64)
653        .sum();
654
655    let preflight = ux::PreflightSummary::new("Update preflight")
656        .row("Candidates", packages_to_upgrade.len().to_string())
657        .row("Pinned skipped", pinned_sources.len().to_string())
658        .row("Other skipped", skipped_sources.len().to_string())
659        .row("Up-to-date", up_to_date_sources.len().to_string())
660        .row(
661            "Download size",
662            crate::pkg::utils::format_bytes(total_download_size),
663        )
664        .row(
665            "Net size",
666            crate::pkg::utils::format_size_diff(total_installed_size_diff),
667        );
668    ux::print_preflight(&preflight);
669
670    if plan_json {
671        let packages: Vec<_> = packages_to_upgrade
672            .iter()
673            .map(|c| {
674                json!({
675                    "source": c.source,
676                    "name": c.new_pkg.name,
677                    "sub_package": c.old_manifest.sub_package,
678                    "from_version": c.old_manifest.version,
679                    "to_version": c.new_version,
680                    "download_bytes": c.download_size,
681                    "net_size_bytes": c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64,
682                    "advisories_old": c.old_advisories,
683                    "advisories_new": c.new_advisories,
684                })
685            })
686            .collect();
687
688        let plan = json!({
689            "dry_run": dry_run,
690            "interactive": interactive,
691            "totals": {
692                "candidates": packages_to_upgrade.len(),
693                "pinned_skipped": pinned_sources.len(),
694                "other_skipped": skipped_sources.len(),
695                "up_to_date": up_to_date_sources.len(),
696                "download_bytes": total_download_size,
697                "net_size_bytes": total_installed_size_diff,
698            },
699            "pinned": pinned_sources,
700            "skipped": skipped_sources,
701            "packages": packages,
702        });
703        ux::emit_plan_json_v1("update", plan)?;
704    }
705
706    if dry_run {
707        println!(
708            "\n{} Dry-run: upgrade plan above would be executed.",
709            "::".bold().yellow()
710        );
711        ux::print_transaction_summary(&ux::TransactionSummary {
712            command: "update".to_string(),
713            success: 0,
714            failed: 0,
715            skipped: packages_to_upgrade.len()
716                + pinned_sources.len()
717                + skipped_sources.len()
718                + up_to_date_sources.len(),
719        });
720        return Ok(());
721    }
722
723    println!();
724    if !crate::utils::ask_for_confirmation("Do you want to upgrade these packages?", yes) {
725        ux::print_transaction_summary(&ux::TransactionSummary {
726            command: "update".to_string(),
727            success: 0,
728            failed: 0,
729            skipped: packages_to_upgrade.len()
730                + pinned_sources.len()
731                + skipped_sources.len()
732                + up_to_date_sources.len(),
733        });
734        return Ok(());
735    }
736
737    // --- Phase 2: Transactional Upgrade ---
738    // We execute the upgrade plan. Each package is processed in parallel
739    // where possible, but all are wrapped in a single machine-wide transaction.
740    let transaction = Mutex::new(transaction::begin()?);
741    let transaction_id = transaction.lock().unwrap().id.clone();
742    let failed_updates = Mutex::new(Vec::new());
743    let successful_upgrades = Mutex::new(Vec::new());
744
745    let m = MultiProgress::new();
746
747    packages_to_upgrade
748        .par_iter()
749        .try_for_each(|candidate| -> Result<()> {
750            println!(
751                "\n{} Upgrading {} to {}...",
752                "::".bold().blue(),
753                candidate.source.cyan(),
754                candidate.new_version.green()
755            );
756
757            if let Some(hooks) = &candidate.new_pkg.hooks {
758                hooks::run_hooks(
759                    hooks,
760                    hooks::HookType::PreUpgrade,
761                    candidate.old_manifest.scope,
762                )?;
763            }
764
765            let (graph, _) = match install::resolver::resolve_dependency_graph(
766                std::slice::from_ref(&candidate.source),
767                Some(candidate.old_manifest.scope),
768                true,
769                yes,
770                false,
771                None,
772                false,
773                None,
774            ) {
775                Ok(res) => res,
776                Err(e) => {
777                    eprintln!(
778                        "{}: Failed to resolve dependencies for '{}': {}",
779                        "Error".red().bold(),
780                        candidate.source,
781                        e
782                    );
783                    failed_updates
784                        .lock()
785                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
786                        .push(candidate.source.clone());
787                    return Ok(());
788                }
789            };
790
791            if let Err(e) = install::util::check_policy_compliance(&graph) {
792                eprintln!(
793                    "{}: Policy check failed for '{}': {}",
794                    "Error".red().bold(),
795                    candidate.source,
796                    e
797                );
798                failed_updates
799                    .lock()
800                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
801                    .push(candidate.source.clone());
802                return Ok(());
803            }
804
805            if let Err(e) = install::util::check_scope_compliance(&graph) {
806                eprintln!(
807                    "{}: Scope check failed for '{}': {}",
808                    "Error".red().bold(),
809                    candidate.source,
810                    e
811                );
812                failed_updates
813                    .lock()
814                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
815                    .push(candidate.source.clone());
816                return Ok(());
817            }
818
819            if let Err(e) = install::util::check_for_vulnerabilities(&graph, yes) {
820                eprintln!(
821                    "{}: Security check failed for '{}': {}",
822                    "Error".red().bold(),
823                    candidate.source,
824                    e
825                );
826                failed_updates
827                    .lock()
828                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
829                    .push(candidate.source.clone());
830                return Ok(());
831            }
832
833            let install_plan = match install::plan::create_install_plan(&graph.nodes, None, false) {
834                Ok(plan) => plan,
835                Err(e) => {
836                    eprintln!(
837                        "{}: Failed to create install plan for '{}': {}",
838                        "Error".red().bold(),
839                        candidate.source,
840                        e
841                    );
842                    failed_updates
843                        .lock()
844                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
845                        .push(candidate.source.clone());
846                    return Ok(());
847                }
848            };
849
850            let stages = match graph.toposort() {
851                Ok(s) => s,
852                Err(e) => {
853                    eprintln!(
854                        "{}: Failed to sort dependency graph for '{}': {}",
855                        "Error".red().bold(),
856                        candidate.source,
857                        e
858                    );
859                    failed_updates
860                        .lock()
861                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
862                        .push(candidate.source.clone());
863                    return Ok(());
864                }
865            };
866
867            let mut new_manifest_option: Option<types::InstallManifest> = None;
868            for stage in stages {
869                for pkg_id in stage {
870                    let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
871                        anyhow!("Package node '{}' missing from graph during update", pkg_id)
872                    })?;
873                    if let Some(action) = install_plan.get(&pkg_id) {
874                        match install::installer::install_node(
875                            node,
876                            action,
877                            Some(&m),
878                            None,
879                            yes,
880                            true,
881                            true,
882                            false,
883                        ) {
884                            Ok(m) => {
885                                if m.name == candidate.new_pkg.name
886                                    && m.sub_package == candidate.old_manifest.sub_package
887                                {
888                                    new_manifest_option = Some(m);
889                                }
890                            }
891                            Err(e) => {
892                                eprintln!(
893                                    "{}: Failed to upgrade '{}': {}",
894                                    "Error".red().bold(),
895                                    candidate.source,
896                                    e
897                                );
898                                failed_updates
899                                    .lock()
900                                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
901                                    .push(candidate.source.clone());
902                                return Ok(());
903                            }
904                        }
905                    }
906                }
907            }
908
909            if let Some(new_manifest) = new_manifest_option {
910                let mut tx_lock = transaction
911                    .lock()
912                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
913                if let Err(e) = transaction::record_operation(
914                    &mut tx_lock,
915                    types::TransactionOperation::Upgrade {
916                        old_manifest: Box::new(candidate.old_manifest.clone()),
917                        new_manifest: Box::new(new_manifest.clone()),
918                    },
919                ) {
920                    eprintln!(
921                        "Error: Failed to record transaction for {}: {}",
922                        candidate.source, e
923                    );
924                    failed_updates
925                        .lock()
926                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
927                        .push(candidate.source.clone());
928                } else {
929                    successful_upgrades
930                        .lock()
931                        .map_err(|e| anyhow!("mutex poisoned: {}", e))?
932                        .push((
933                            candidate.old_manifest.clone(),
934                            new_manifest.clone(),
935                            candidate.new_pkg.clone(),
936                        ));
937                }
938            } else {
939                eprintln!("Failed to get new manifest for {}", candidate.source);
940                failed_updates
941                    .lock()
942                    .map_err(|e| anyhow!("mutex poisoned: {}", e))?
943                    .push(candidate.source.clone());
944            }
945            Ok(())
946        })?;
947
948    let failed = failed_updates
949        .into_inner()
950        .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
951    if !failed.is_empty() {
952        eprintln!("\nError: Some packages failed to upgrade. Rolling back all changes...");
953        for pkg in &failed {
954            eprintln!("  - {}", pkg);
955        }
956        transaction::rollback(&transaction_id)?;
957        ux::print_transaction_summary(&ux::TransactionSummary {
958            command: "update".to_string(),
959            success: 0,
960            failed: failed.len(),
961            skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
962        });
963        return Err(anyhow!("Update failed for some packages."));
964    }
965
966    if let Ok(modified_files) = transaction::get_modified_files(&transaction_id) {
967        let modified_packages =
968            transaction::get_modified_packages(&transaction_id).unwrap_or_default();
969        let upgrades_lock = successful_upgrades
970            .lock()
971            .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
972        let first_scope = upgrades_lock
973            .first()
974            .map(|(old, _, _)| old.scope)
975            .unwrap_or(types::Scope::User);
976        let _ = crate::pkg::hooks::global::run_global_hooks(
977            crate::pkg::hooks::global::HookWhen::PostTransaction,
978            &modified_files,
979            &modified_packages,
980            "upgrade",
981            first_scope,
982        );
983    }
984    transaction::commit(&transaction_id)?;
985
986    println!("\n{}", "Success:".green());
987    let successful_upgrades = successful_upgrades
988        .into_inner()
989        .map_err(|e| anyhow!("mutex poisoned: {}", e))?;
990    for (old_manifest, new_manifest, new_pkg) in &successful_upgrades {
991        if let Some(backup_files) = &old_manifest.backup {
992            println!(
993                "Restoring configuration for {}...",
994                old_manifest.name.cyan()
995            );
996            let old_version_dir = local::get_package_version_dir(
997                old_manifest.scope,
998                &old_manifest.registry_handle,
999                &old_manifest.repo,
1000                &old_manifest.name,
1001                &old_manifest.version,
1002            )?;
1003            let new_version_dir = local::get_package_version_dir(
1004                new_manifest.scope,
1005                &new_manifest.registry_handle,
1006                &new_manifest.repo,
1007                &new_manifest.name,
1008                &new_manifest.version,
1009            )?;
1010            handle_backup_files(
1011                &old_version_dir,
1012                &new_version_dir,
1013                backup_files,
1014                old_manifest.scope,
1015            )?;
1016        }
1017
1018        if let Err(e) = cleanup_old_versions(
1019            &new_manifest.name,
1020            new_manifest.scope,
1021            &new_manifest.repo,
1022            &new_manifest.registry_handle,
1023        ) {
1024            eprintln!(
1025                "Failed to clean up old versions for {}: {}",
1026                new_manifest.name, e
1027            );
1028        }
1029
1030        if let Ok(conn) = db::open_connection("local") {
1031            let _ = db::update_package(
1032                &conn,
1033                new_pkg,
1034                &new_manifest.registry_handle,
1035                Some(new_manifest.scope),
1036                new_manifest.sub_package.as_deref(),
1037                Some(&old_manifest.reason),
1038            );
1039        }
1040
1041        if let Some(hooks) = &new_pkg.hooks
1042            && let Err(e) =
1043                hooks::run_hooks(hooks, hooks::HookType::PostUpgrade, new_manifest.scope)
1044        {
1045            eprintln!(
1046                "{}: Post-upgrade hook failed for '{}': {}",
1047                "Error".red().bold(),
1048                new_manifest.name,
1049                e
1050            );
1051        }
1052    }
1053
1054    ux::print_transaction_summary(&ux::TransactionSummary {
1055        command: "update".to_string(),
1056        success: successful_upgrades.len(),
1057        failed: 0,
1058        skipped: pinned_sources.len() + skipped_sources.len() + up_to_date_sources.len(),
1059    });
1060    println!("\n{}", "Success:".green());
1061    Ok(())
1062}
1063
1064fn advisory_counts(
1065    registry_handle: &str,
1066    package: &str,
1067    sub_package: Option<&str>,
1068    old_version: &str,
1069    new_version: &str,
1070) -> Result<(usize, usize)> {
1071    let advisories = db::get_advisories_for_package(registry_handle, package, sub_package)?;
1072    let old_ver = Version::parse(old_version)
1073        .map_err(|e| anyhow!("failed to parse old version '{}': {}", old_version, e))?;
1074    let new_ver = Version::parse(new_version)
1075        .map_err(|e| anyhow!("failed to parse new version '{}': {}", new_version, e))?;
1076
1077    let mut old_count = 0usize;
1078    let mut new_count = 0usize;
1079    for adv in advisories {
1080        if let Ok(req) = semver::VersionReq::parse(&adv.affected_range) {
1081            if req.matches(&old_ver) {
1082                old_count += 1;
1083            }
1084            if req.matches(&new_ver) {
1085                new_count += 1;
1086            }
1087        }
1088    }
1089    Ok((old_count, new_count))
1090}
1091
1092fn cleanup_old_versions(
1093    package_name: &str,
1094    scope: types::Scope,
1095    repo: &str,
1096    registry_handle: &str,
1097) -> Result<()> {
1098    let config = config::read_config()?;
1099    let rollback_enabled = config.rollback_enabled;
1100
1101    let package_dir = local::get_package_dir(scope, registry_handle, repo, package_name)?;
1102
1103    let mut versions = Vec::new();
1104    if let Ok(entries) = fs::read_dir(&package_dir) {
1105        for entry in entries.flatten() {
1106            let path = entry.path();
1107            if path.is_dir()
1108                && let Some(version_str) = path.file_name().and_then(|s| s.to_str())
1109                && version_str != "latest"
1110                && let Ok(version) = Version::parse(version_str)
1111            {
1112                versions.push(version);
1113            }
1114        }
1115    }
1116
1117    if versions.is_empty() {
1118        return Ok(());
1119    }
1120
1121    versions.sort();
1122
1123    let versions_to_keep = if rollback_enabled { 3 } else { 1 };
1124
1125    if versions.len() > versions_to_keep {
1126        let num_to_delete = versions.len() - versions_to_keep;
1127        let versions_to_delete = &versions[..num_to_delete];
1128
1129        println!("Cleaning up old versions...");
1130        for version in versions_to_delete {
1131            let version_dir_to_delete = package_dir.join(version.to_string());
1132            println!(" - Removing {}", version_dir_to_delete.display());
1133            if version_dir_to_delete.exists() {
1134                fs::remove_dir_all(version_dir_to_delete)?;
1135            }
1136        }
1137    }
1138
1139    Ok(())
1140}