1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::sync::Mutex;
4
5use anyhow::{Result, anyhow};
6use colored::Colorize;
7use dialoguer::MultiSelect;
8use dialoguer::theme::ColorfulTheme;
9use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
10use rayon::prelude::*;
11use semver::Version;
12use serde_json::json;
13
14use crate::cmd::{utils as cmd_utils, ux};
15use crate::pkg::merge::handle_backup_files;
16use crate::pkg::{
17 config, db, hooks, install, local, pin, resolve, transaction, types
18};
19
20pub fn run(
34 all: bool,
35 package_names: &[String],
36 yes: bool,
37 dry_run: bool,
38 explain: bool,
39 plan_json: bool,
40 verbose: bool,
41 interactive: bool
42) -> Result<()> {
43 if all {
44 return run_update_all_logic(
45 yes,
46 dry_run,
47 explain,
48 plan_json,
49 verbose,
50 interactive
51 );
52 }
53
54 if plan_json && !dry_run {
55 return Err(anyhow!("--plan-json requires --dry-run"));
56 }
57
58 let expanded_package_names =
59 cmd_utils::expand_split_packages(package_names, "Updating")?;
60
61 let mut failed_packages = Vec::new();
62
63 for (i, package_name) in expanded_package_names.iter().enumerate() {
64 if i > 0 {
65 println!();
66 }
67 if let Err(e) = run_update_single_logic(
68 package_name,
69 yes,
70 dry_run,
71 explain,
72 plan_json,
73 verbose
74 ) {
75 eprintln!(
76 "{}: Failed to update '{}': {}",
77 "Error".red().bold(),
78 package_name,
79 e
80 );
81 failed_packages.push(package_name.clone());
82 }
83 }
84
85 if !failed_packages.is_empty() {
86 return Err(anyhow!(
87 "The following packages failed to update: {}",
88 failed_packages.join(", ")
89 ));
90 } else if !package_names.is_empty() && !dry_run {
91 println!("\n{}", "Success:".green());
92 }
93 Ok(())
94}
95
96fn run_update_single_logic(
98 package_name: &str,
99 yes: bool,
100 dry_run: bool,
101 explain: bool,
102 plan_json: bool,
103 verbose: bool
104) -> Result<()> {
105 if !plan_json {
106 println!("{} Resolving dependencies...", "::".bold().blue());
107 }
108
109 let (new_pkg, new_version, _, _, registry_handle, _, _) =
110 resolve::resolve_package_and_version(package_name, None, true, yes)?;
111
112 if pin::is_pinned(package_name)? {
113 if !plan_json {
114 println!(
115 "Package '{}' is pinned. Skipping update.",
116 package_name.yellow()
117 );
118 }
119 return Ok(());
120 }
121
122 let installed_source = if let Some(sub) = &new_pkg.sub_package {
123 format!(
124 "#{}@{}/{}:{}",
125 registry_handle.as_deref().unwrap_or("local"),
126 new_pkg.repo,
127 new_pkg.name,
128 sub
129 )
130 } else {
131 format!(
132 "#{}@{}/{}",
133 registry_handle.as_deref().unwrap_or("local"),
134 new_pkg.repo,
135 new_pkg.name
136 )
137 };
138 let installed_request = resolve::parse_source_string(&installed_source)?;
139 let mut candidates = Vec::new();
140 candidates.extend(local::find_installed_manifests_matching(
141 &installed_request,
142 types::Scope::User
143 )?);
144 candidates.extend(local::find_installed_manifests_matching(
145 &installed_request,
146 types::Scope::System
147 )?);
148
149 let old_manifest = crate::cmd::installed_select::choose_installed_manifest(
150 package_name,
151 &candidates,
152 yes
153 )
154 .map_err(|e| {
155 if candidates.is_empty() {
156 anyhow!(
157 "Package '{package_name}' is not installed. Use 'zoi install' \
158 instead."
159 )
160 } else {
161 e
162 }
163 })?;
164
165 if old_manifest.version == new_version
166 && old_manifest.revision == new_pkg.revision
167 {
168 if !plan_json {
169 println!("\nPackage is already up to date.");
170 ux::print_transaction_summary(&ux::TransactionSummary {
171 command: "update".to_string(),
172 success: 0,
173 failed: 0,
174 skipped: 1
175 });
176 }
177 return Ok(());
178 }
179
180 if !plan_json {
181 println!("{} Looking for conflicts...", "::".bold().blue());
182 }
183
184 let (graph, non_zoi_deps) = install::resolver::resolve_dependency_graph(
185 &[package_name.to_string()],
186 Some(old_manifest.scope),
187 true,
188 yes,
189 false,
190 None,
191 true,
192 None
193 )?;
194
195 if !plan_json {
196 println!("{} Checking available disk space...", "::".bold().blue());
197 }
198
199 if !dry_run {
200 let pkgs_to_check: Vec<&types::Package> =
201 graph.nodes.values().map(|n| &n.pkg).collect();
202 install::preflight::check_for_conflicts(&pkgs_to_check, yes)?;
203 for pkg in &pkgs_to_check {
204 if !install::util::display_updates(pkg, yes)? {
205 return Err(anyhow!("Update aborted by user."));
206 }
207 }
208 install::preflight::check_policy_compliance(&graph)?;
209 install::preflight::check_scope_compliance(&graph)?;
210 install::preflight::check_zoios_compliance(&graph)?;
211 install::preflight::check_for_vulnerabilities(&graph, yes)?;
212
213 let m_for_conflict_check = MultiProgress::new();
214 install::preflight::check_file_conflicts(
215 &graph,
216 yes,
217 &m_for_conflict_check
218 )?;
219 let _ = m_for_conflict_check.clear();
220 }
221
222 let install_plan =
223 install::plan::create_install_plan(&graph.nodes, None, false)?;
224
225 let mut total_download_size: u64 = 0;
226 let mut total_installed_size_diff: i64 = 0;
227 let mut packages_to_upgrade = Vec::new();
228 let config = config::read_config().unwrap_or_default();
229
230 for (pkg_id, node) in &graph.nodes {
231 let is_requested = node.pkg.name == new_pkg.name
232 && node.sub_package == old_manifest.sub_package;
233
234 let request_source = local::package_source_string(
235 &node.registry_handle,
236 &node.pkg.repo,
237 &node.pkg.name,
238 node.sub_package.as_deref(),
239 &node.version
240 );
241 let request = resolve::parse_source_string(&request_source)?;
242 let installed = local::find_installed_manifests_matching(
243 &request,
244 old_manifest.scope
245 )?;
246
247 let (current_version_display, needs_update) = if installed.is_empty() {
248 if is_requested {
249 let display = if old_manifest.revision == "1" {
250 old_manifest.version.clone()
251 } else {
252 format!(
253 "{}-{}",
254 old_manifest.version, old_manifest.revision
255 )
256 };
257 (Some(display), true)
258 } else {
259 (None, true)
260 }
261 } else {
262 let m = installed
263 .first()
264 .ok_or_else(|| anyhow!("installed list unexpectedly empty"))?;
265 let display = if m.revision == "1" {
266 m.version.clone()
267 } else {
268 format!("{}-{}", m.version, m.revision)
269 };
270 (
271 Some(display),
272 m.version != node.version || m.revision != node.revision
273 )
274 };
275
276 if needs_update {
277 let (down_size, inst_size) = install::util::get_package_sizes(
278 &node.pkg,
279 &node.registry_handle,
280 &node.version
281 );
282 total_download_size += down_size;
283
284 let old_size = installed
285 .first()
286 .and_then(|m| m.installed_size)
287 .unwrap_or(0);
288 total_installed_size_diff += inst_size as i64 - old_size as i64;
289
290 let display_name = ux::format_display_name(
291 &node.registry_handle,
292 &node.pkg.repo,
293 &node.pkg.name,
294 node.sub_package.as_deref(),
295 &config
296 );
297
298 let new_version_display = if node.revision == "1" {
299 node.version.clone()
300 } else {
301 format!("{}-{}", node.version, node.revision)
302 };
303
304 packages_to_upgrade.push((
305 display_name,
306 current_version_display,
307 new_version_display,
308 pkg_id.clone(),
309 !is_requested
310 ));
311 }
312 }
313
314 if !plan_json {
315 println!(
316 "\n{} Packages ({})\n",
317 "::".bold().blue(),
318 packages_to_upgrade.len()
319 );
320 }
321
322 for (name, old_ver, new_ver, pkg_id, is_dep) in &packages_to_upgrade {
323 let transition = if let Some(old) = old_ver {
324 format!(
325 "{} -> {}",
326 format!("{name}@{old}").cyan(),
327 format!("{name}@{new_ver}").cyan()
328 )
329 } else {
330 format!("{} (new dependency)", format!("{name}@{new_ver}").cyan())
331 };
332 if !plan_json {
333 println!(" {transition}");
334 }
335
336 if *is_dep {
337 let node = graph.nodes.get(pkg_id).ok_or_else(|| {
338 anyhow!("Package node '{pkg_id}' missing from graph")
339 })?;
340 let pkg_dir = local::get_package_dir(
341 old_manifest.scope,
342 &node.registry_handle,
343 &node.pkg.repo,
344 &node.pkg.name
345 )?;
346 if let Ok(dependents) = local::get_dependents(&pkg_dir) {
347 let external_dependents: Vec<_> = dependents
348 .into_iter()
349 .filter(|dep_id| !graph.nodes.contains_key(dep_id))
350 .collect();
351
352 if !external_dependents.is_empty() && !plan_json {
353 println!(
354 " {} Updating {} may affect: {}",
355 "Warning:".bold().yellow(),
356 node.pkg.name.cyan(),
357 external_dependents.join(", ").dimmed()
358 );
359 }
360 }
361 }
362 }
363
364 let down_str = zoi_core::utils::format_bytes(total_download_size);
365 let net_str = zoi_core::utils::format_size_diff(total_installed_size_diff);
366
367 if !plan_json {
368 println!("\nTotal Download Size: {down_str:>10}");
369 println!("Net Upgrade Size: {net_str:>10}");
370 }
371
372 if verbose && !plan_json {
373 let preflight = ux::PreflightSummary::new("Update preflight")
374 .row("Candidates", packages_to_upgrade.len().to_string())
375 .row("Scope", format!("{:?}", old_manifest.scope))
376 .row("Download size", &down_str)
377 .row("Net size", &net_str);
378 ux::print_preflight(&preflight);
379 }
380
381 if explain && !plan_json {
382 let mut report = ux::ExplainReport::new("Update explanation");
383 report = report.item(
384 new_pkg.name.clone(),
385 format!(
386 "selected because newer version {} is available over \
387 installed {}",
388 new_version, old_manifest.version
389 ),
390 Vec::new()
391 );
392 if let Ok((old_adv, new_adv)) = advisory_counts(
393 &old_manifest.registry_handle,
394 &new_pkg.name,
395 old_manifest.sub_package.as_deref(),
396 &old_manifest.version,
397 &new_version
398 ) {
399 report = report.item(
400 "advisories",
401 format!(
402 "old={}, new={}, delta={}",
403 old_adv,
404 new_adv,
405 (new_adv as i64 - old_adv as i64)
406 ),
407 Vec::new()
408 );
409 }
410 ux::print_explain(&report);
411 }
412
413 if plan_json {
414 let plan = json!({
415 "dry_run": dry_run,
416 "package": {
417 "name": new_pkg.name,
418 "sub_package": old_manifest.sub_package,
419 "registry": old_manifest.registry_handle,
420 "repo": old_manifest.repo,
421 "scope": format!("{:?}", old_manifest.scope),
422 "from_version": old_manifest.version,
423 "to_version": new_version,
424 "download_bytes": total_download_size,
425 "net_size_bytes": total_installed_size_diff,
426 }
427 });
428 ux::emit_plan_json_v1("update", plan)?;
429 return Ok(());
430 }
431
432 if dry_run {
433 println!(
434 "\n{} Dry-run: update plan above would be executed.",
435 "::".bold().yellow()
436 );
437 return Ok(());
438 }
439
440 println!();
441 let prompt = if packages_to_upgrade.len() > 1 {
442 "Do you want to upgrade these packages?".to_string()
443 } else {
444 format!("Update from {} to {}?", old_manifest.version, new_version)
445 };
446
447 if !crate::utils::ask_for_confirmation(&prompt, yes) {
448 ux::print_transaction_summary(&ux::TransactionSummary {
449 command: "update".to_string(),
450 success: 0,
451 failed: 0,
452 skipped: packages_to_upgrade.len()
453 });
454 return Ok(());
455 }
456
457 perform_transaction(
458 &graph,
459 &install_plan,
460 &non_zoi_deps,
461 old_manifest.scope,
462 yes,
463 verbose,
464 &new_pkg.name,
465 &old_manifest,
466 &new_pkg,
467 plan_json
468 )
469}
470
471fn perform_transaction(
476 graph: &install::resolver::DependencyGraph,
477 install_plan: &HashMap<String, install::plan::InstallAction>,
478 non_zoi_deps: &[String],
479 scope: types::Scope,
480 yes: bool,
481 verbose: bool,
482 target_pkg_name: &str,
483 old_manifest_ref: &types::InstallManifest,
484 new_pkg_ref: &types::Package,
485 plan_json: bool
486) -> Result<()> {
487 let mut transaction = transaction::begin()?;
488 let stages = graph.toposort()?;
489 let mut new_manifest_option: Option<types::InstallManifest> = None;
490 let m = MultiProgress::new();
491 if plan_json {
492 m.set_draw_target(indicatif::ProgressDrawTarget::hidden());
493 }
494
495 if !non_zoi_deps.is_empty() {
497 let processed_deps = Mutex::new(HashSet::new());
498 let mut installed_deps_ext = Vec::new();
499 for dep_str in non_zoi_deps {
500 let dep =
501 crate::pkg::dependencies::parse_dependency_string(dep_str)?;
502 crate::pkg::install::dep_install::install_dependency(
503 &dep,
504 "update",
505 scope,
506 yes,
507 false,
508 &processed_deps,
509 &mut installed_deps_ext,
510 Some(&m)
511 )?;
512 }
513 }
514
515 for stage in stages {
516 for pkg_id in stage {
517 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
518 anyhow!("Package node '{pkg_id}' missing from graph")
519 })?;
520 if let Some(action) = install_plan.get(&pkg_id) {
521 match install::installer::install_node(
522 node,
523 action,
524 Some(&m),
525 None,
526 yes,
527 true,
528 true,
529 verbose
530 ) {
531 Ok(m) => {
532 if m.name == target_pkg_name {
533 new_manifest_option = Some(m);
534 }
535 }
536 Err(e) => {
537 eprintln!("\nError: Update failed. Rolling back...");
538 transaction::rollback(&transaction.id)?;
539 return Err(anyhow!("Update failed: {e}"));
540 }
541 }
542 }
543 }
544 }
545
546 if let Some(new_manifest) = new_manifest_option {
547 transaction::record_operation(
549 &mut transaction,
550 types::TransactionOperation::Upgrade {
551 old_manifest: Box::new(old_manifest_ref.clone()),
552 new_manifest: Box::new(new_manifest.clone())
553 }
554 )?;
555
556 if let Ok(modified_files) =
557 transaction::get_modified_files(&transaction.id)
558 {
559 let modified_packages =
560 transaction::get_modified_packages(&transaction.id)
561 .unwrap_or_default();
562 let _ = crate::pkg::hooks::global::run_global_hooks(
563 crate::pkg::hooks::global::HookWhen::PostTransaction,
564 &modified_files,
565 &modified_packages,
566 "upgrade",
567 scope
568 );
569 }
570
571 transaction::commit(&transaction.id)?;
572
573 if let Some(backup_files) = &old_manifest_ref.backup {
574 println!("Restoring configuration files...");
575 let old_version_dir = local::get_package_version_dir(
576 old_manifest_ref.scope,
577 &old_manifest_ref.registry_handle,
578 &old_manifest_ref.repo,
579 &old_manifest_ref.name,
580 &old_manifest_ref.version
581 )?;
582 let new_version_dir = local::get_package_version_dir(
583 new_manifest.scope,
584 &new_manifest.registry_handle,
585 &new_manifest.repo,
586 &new_manifest.name,
587 &new_manifest.version
588 )?;
589
590 crate::pkg::merge::handle_backup_files(
591 &old_version_dir,
592 &new_version_dir,
593 backup_files,
594 old_manifest_ref.scope
595 )?;
596 }
597
598 cleanup_old_versions(
600 &new_manifest.name,
601 scope,
602 &new_manifest.repo,
603 &new_manifest.registry_handle
604 )?;
605
606 if let Ok(conn) = db::open_connection("local") {
607 let _ = db::update_package(
608 &conn,
609 new_pkg_ref,
610 &new_manifest.registry_handle,
611 Some(new_manifest.scope),
612 new_manifest.sub_package.as_deref(),
613 Some(&types::InstallReason::Direct)
614 );
615 }
616
617 if let Some(hooks) = &new_pkg_ref.hooks {
618 crate::pkg::hooks::run_hooks(
619 hooks,
620 crate::pkg::hooks::HookType::PostUpgrade,
621 new_manifest.scope
622 )?;
623 }
624
625 if !plan_json {
626 println!("\n{}", "Success:".green());
627 }
628 }
629
630 Ok(())
631}
632
633fn run_update_all_logic(
635 yes: bool,
636 dry_run: bool,
637 explain: bool,
638 plan_json: bool,
639 verbose: bool,
640 interactive: bool
641) -> Result<()> {
642 #[derive(Clone)]
643 struct UpdateCandidate {
644 source: String,
645 new_pkg: types::Package,
646 new_version: String,
647 old_manifest: types::InstallManifest,
648 old_advisories: usize,
649 new_advisories: usize,
650 download_size: u64,
651 new_installed_size: u64
652 }
653
654 let installed_packages = local::get_installed_packages()?;
655 let mut pinned_sources = Vec::new();
656 let mut skipped_sources = Vec::new();
657 let mut up_to_date_sources = Vec::new();
658 let mut candidates: Vec<UpdateCandidate> = Vec::new();
659
660 if !plan_json {
664 println!("{} Checking for upgrades...", "::".bold().blue());
665 }
666 let pb = ProgressBar::new(installed_packages.len() as u64);
667 if plan_json {
668 pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
669 }
670 pb.set_style(
671 ProgressStyle::default_bar()
672 .template(
673 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] \
674 {pos}/{len} ({msg})"
675 )?
676 .progress_chars("#>-")
677 );
678 pb.set_message("Checking packages...");
679
680 for manifest in installed_packages {
681 let source = if let Some(sub) = &manifest.sub_package {
682 format!(
683 "#{}@{}/{}:{}",
684 manifest.registry_handle, manifest.repo, manifest.name, sub
685 )
686 } else {
687 format!(
688 "#{}@{}/{}",
689 manifest.registry_handle, manifest.repo, manifest.name
690 )
691 };
692
693 if pin::is_pinned(&source).unwrap_or(false)
694 || pin::is_pinned(&manifest.name).unwrap_or(false)
695 {
696 pinned_sources.push(source);
697 pb.inc(1);
698 continue;
699 }
700
701 let (new_pkg, new_version, _, _, _registry_handle, _, _) =
702 match resolve::resolve_package_and_version(
703 &source,
704 Some(manifest.scope),
705 true,
706 false
707 ) {
708 Ok(result) => result,
709 Err(e) => {
710 skipped_sources.push(format!("{source} ({e})"));
711 pb.inc(1);
712 continue;
713 }
714 };
715
716 if manifest.version == new_version
717 && manifest.revision == new_pkg.revision
718 {
719 up_to_date_sources.push(source);
720 pb.inc(1);
721 continue;
722 }
723
724 let (old_adv, new_adv) = advisory_counts(
725 &manifest.registry_handle,
726 &manifest.name,
727 manifest.sub_package.as_deref(),
728 &manifest.version,
729 &new_version
730 )
731 .unwrap_or((0, 0));
732
733 let (download_size, new_installed_size) =
734 install::util::get_package_sizes(
735 &new_pkg,
736 &manifest.registry_handle,
737 &new_version
738 );
739
740 candidates.push(UpdateCandidate {
741 source,
742 new_pkg,
743 new_version,
744 old_manifest: manifest,
745 old_advisories: old_adv,
746 new_advisories: new_adv,
747 download_size,
748 new_installed_size
749 });
750 pb.inc(1);
751 }
752 pb.finish_and_clear();
753
754 if candidates.is_empty() {
755 if !plan_json {
756 println!("\nAll packages are up to date.");
757 }
758 return Ok(());
759 }
760
761 if interactive && !dry_run {
762 let items: Vec<String> = candidates
763 .iter()
764 .map(|c| {
765 format!(
766 "{} {} -> {}",
767 c.source, c.old_manifest.version, c.new_version
768 )
769 })
770 .collect();
771 let selected = MultiSelect::with_theme(&ColorfulTheme::default())
772 .with_prompt("Select packages to update")
773 .items(&items)
774 .interact()
775 .map_err(|e| anyhow!("Interactive selection failed: {e}"))?;
776
777 if selected.is_empty() {
778 if !plan_json {
779 println!("No packages selected.");
780 }
781 return Ok(());
782 }
783
784 let selected_set: HashSet<usize> = selected.into_iter().collect();
785 candidates = candidates
786 .into_iter()
787 .enumerate()
788 .filter(|(idx, _)| selected_set.contains(idx))
789 .map(|(_, c)| c)
790 .collect();
791 }
792
793 if !plan_json {
794 println!("{} Resolving dependencies...", "::".bold().blue());
795 println!("{} Looking for conflicts...", "::".bold().blue());
796 println!("{} Checking available disk space...", "::".bold().blue());
797
798 if !pinned_sources.is_empty() {
799 println!("\n{} Pinned (skipped)", "::".bold().blue());
800 for s in &pinned_sources {
801 println!(" - {}", s.yellow());
802 }
803 }
804
805 println!("\n{} Packages ({})", "::".bold().blue(), candidates.len());
806 }
807 let config = config::read_config().unwrap_or_default();
808
809 for candidate in &candidates {
810 let delta =
811 candidate.new_advisories as i64 - candidate.old_advisories as i64;
812 let advisory_suffix = match delta.cmp(&0) {
813 std::cmp::Ordering::Greater => {
814 format!(" (advisories +{delta})").red().to_string()
815 }
816 std::cmp::Ordering::Less => {
817 format!(" (advisories {delta})").green().to_string()
818 }
819 std::cmp::Ordering::Equal => String::new()
820 };
821
822 let display_name = ux::format_display_name(
823 &candidate.old_manifest.registry_handle,
824 &candidate.old_manifest.repo,
825 &candidate.old_manifest.name,
826 candidate.old_manifest.sub_package.as_deref(),
827 &config
828 );
829 let old_display = if candidate.old_manifest.revision == "1" {
830 candidate.old_manifest.version.clone()
831 } else {
832 format!(
833 "{}-{}",
834 candidate.old_manifest.version, candidate.old_manifest.revision
835 )
836 };
837 let new_display = if candidate.new_pkg.revision == "1" {
838 candidate.new_version.clone()
839 } else {
840 format!("{}-{}", candidate.new_version, candidate.new_pkg.revision)
841 };
842
843 if !plan_json {
844 println!(
845 " {} -> {}{}",
846 format!("{display_name}@{old_display}").cyan(),
847 format!("{display_name}@{new_display}").cyan(),
848 advisory_suffix
849 );
850 }
851 }
852
853 let total_download_size: u64 =
854 candidates.iter().map(|c| c.download_size).sum();
855 let total_installed_size_diff: i64 = candidates
856 .iter()
857 .map(|c| {
858 c.new_installed_size as i64
859 - c.old_manifest.installed_size.unwrap_or(0) as i64
860 })
861 .sum();
862
863 let down_str = zoi_core::utils::format_bytes(total_download_size);
864 let net_str = zoi_core::utils::format_size_diff(total_installed_size_diff);
865
866 if !plan_json {
867 println!("\nTotal Download Size: {down_str:>10}");
868 println!("Net Upgrade Size: {net_str:>10}");
869 }
870
871 if verbose && !plan_json {
872 let preflight = ux::PreflightSummary::new("Update preflight")
873 .row("Candidates", candidates.len().to_string())
874 .row("Download size", &down_str)
875 .row("Net size", &net_str);
876 ux::print_preflight(&preflight);
877 }
878
879 if explain && !plan_json {
880 let mut report = ux::ExplainReport::new("Update explanation");
881 for candidate in &candidates {
882 report = report.item(
883 candidate.source.clone(),
884 format!(
885 "selected because {} -> {}",
886 candidate.old_manifest.version, candidate.new_version
887 ),
888 Vec::new()
889 );
890 }
891 ux::print_explain(&report);
892 }
893
894 if plan_json {
895 let packages: Vec<_> = candidates
896 .iter()
897 .map(|c| {
898 json!({
899 "source": c.source,
900 "name": c.new_pkg.name,
901 "sub_package": c.old_manifest.sub_package,
902 "from_version": c.old_manifest.version,
903 "to_version": c.new_version,
904 "download_bytes": c.download_size,
905 "net_size_bytes": c.new_installed_size as i64 - c.old_manifest.installed_size.unwrap_or(0) as i64,
906 "advisories_old": c.old_advisories,
907 "advisories_new": c.new_advisories,
908 })
909 })
910 .collect();
911
912 let plan = json!({
913 "dry_run": dry_run,
914 "interactive": interactive,
915 "totals": {
916 "candidates": candidates.len(),
917 "pinned_skipped": pinned_sources.len(),
918 "other_skipped": skipped_sources.len(),
919 "up_to_date": up_to_date_sources.len(),
920 "download_bytes": total_download_size,
921 "net_size_bytes": total_installed_size_diff,
922 },
923 "pinned": pinned_sources,
924 "skipped": skipped_sources,
925 "packages": packages,
926 });
927 ux::emit_plan_json_v1("update", plan)?;
928 return Ok(());
929 }
930
931 if dry_run {
932 println!(
933 "\n{} Dry-run: upgrade plan above would be executed.",
934 "::".bold().yellow()
935 );
936 return Ok(());
937 }
938
939 if !crate::utils::ask_for_confirmation(
940 "Do you want to upgrade these packages?",
941 yes
942 ) {
943 return Ok(());
944 }
945
946 let transaction = Mutex::new(transaction::begin()?);
948 let transaction_id = transaction.lock().expect("mutex poisoned").id.clone();
949 let failed_updates = Mutex::new(Vec::new());
950 let successful_upgrades = Mutex::new(Vec::new());
951
952 let m = MultiProgress::new();
953
954 candidates
955 .par_iter()
956 .try_for_each(|candidate| -> Result<()> {
957 println!(
958 "\n{} Upgrading {} to {}...",
959 "::".bold().blue(),
960 candidate.source.cyan(),
961 candidate.new_version.green()
962 );
963
964 if let Some(hooks) = &candidate.new_pkg.hooks {
965 hooks::run_hooks(
966 hooks,
967 hooks::HookType::PreUpgrade,
968 candidate.old_manifest.scope
969 )?;
970 }
971
972 let (graph, _) = match install::resolver::resolve_dependency_graph(
973 std::slice::from_ref(&candidate.source),
974 Some(candidate.old_manifest.scope),
975 true,
976 yes,
977 false,
978 None,
979 false,
980 None
981 ) {
982 Ok(res) => res,
983 Err(e) => {
984 eprintln!(
985 "{}: Failed to resolve dependencies for '{}': {}",
986 "Error".red().bold(),
987 candidate.source,
988 e
989 );
990 failed_updates
991 .lock()
992 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
993 .push(candidate.source.clone());
994 return Ok(());
995 }
996 };
997
998 if let Err(e) =
999 zoi_install::preflight::check_policy_compliance(&graph)
1000 {
1001 eprintln!(
1002 "{}: Policy check failed for '{}': {}",
1003 "Error".red().bold(),
1004 candidate.source,
1005 e
1006 );
1007 failed_updates
1008 .lock()
1009 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1010 .push(candidate.source.clone());
1011 return Ok(());
1012 }
1013
1014 if let Err(e) =
1015 zoi_install::preflight::check_scope_compliance(&graph)
1016 {
1017 eprintln!(
1018 "{}: Scope check failed for '{}': {}",
1019 "Error".red().bold(),
1020 candidate.source,
1021 e
1022 );
1023 failed_updates
1024 .lock()
1025 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1026 .push(candidate.source.clone());
1027 return Ok(());
1028 }
1029
1030 if let Err(e) =
1031 zoi_install::preflight::check_zoios_compliance(&graph)
1032 {
1033 eprintln!(
1034 "{}: ZoiOS check failed for '{}': {}",
1035 "Error".red().bold(),
1036 candidate.source,
1037 e
1038 );
1039 failed_updates
1040 .lock()
1041 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1042 .push(candidate.source.clone());
1043 return Ok(());
1044 }
1045
1046 if let Err(e) =
1047 zoi_install::preflight::check_for_vulnerabilities(&graph, yes)
1048 {
1049 eprintln!(
1050 "{}: Security check failed for '{}': {}",
1051 "Error".red().bold(),
1052 candidate.source,
1053 e
1054 );
1055 failed_updates
1056 .lock()
1057 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1058 .push(candidate.source.clone());
1059 return Ok(());
1060 }
1061
1062 let install_plan = match install::plan::create_install_plan(
1063 &graph.nodes,
1064 None,
1065 false
1066 ) {
1067 Ok(plan) => plan,
1068 Err(e) => {
1069 eprintln!(
1070 "{}: Failed to create install plan for '{}': {}",
1071 "Error".red().bold(),
1072 candidate.source,
1073 e
1074 );
1075 failed_updates
1076 .lock()
1077 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1078 .push(candidate.source.clone());
1079 return Ok(());
1080 }
1081 };
1082
1083 let stages = match graph.toposort() {
1084 Ok(s) => s,
1085 Err(e) => {
1086 eprintln!(
1087 "{}: Failed to sort dependency graph for '{}': {}",
1088 "Error".red().bold(),
1089 candidate.source,
1090 e
1091 );
1092 failed_updates
1093 .lock()
1094 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1095 .push(candidate.source.clone());
1096 return Ok(());
1097 }
1098 };
1099
1100 let mut new_manifest_option: Option<types::InstallManifest> = None;
1101 for stage in stages {
1102 for pkg_id in stage {
1103 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
1104 anyhow!(
1105 "Package node '{pkg_id}' missing from graph \
1106 during update"
1107 )
1108 })?;
1109 if let Some(action) = install_plan.get(&pkg_id) {
1110 match install::installer::install_node(
1111 node,
1112 action,
1113 Some(&m),
1114 None,
1115 yes,
1116 true,
1117 true,
1118 false
1119 ) {
1120 Ok(m) => {
1121 if m.name == candidate.new_pkg.name
1122 && m.sub_package
1123 == candidate.old_manifest.sub_package
1124 {
1125 new_manifest_option = Some(m);
1126 }
1127 }
1128 Err(e) => {
1129 eprintln!(
1130 "{}: Failed to upgrade '{}': {}",
1131 "Error".red().bold(),
1132 candidate.source,
1133 e
1134 );
1135 failed_updates
1136 .lock()
1137 .map_err(|e| {
1138 anyhow!("mutex poisoned: {e}")
1139 })?
1140 .push(candidate.source.clone());
1141 return Ok(());
1142 }
1143 }
1144 }
1145 }
1146 }
1147
1148 if let Some(new_manifest) = new_manifest_option {
1149 if let Err(e) = transaction::record_operation(
1150 &mut *transaction
1151 .lock()
1152 .map_err(|e| anyhow!("mutex poisoned: {e}"))?,
1153 types::TransactionOperation::Upgrade {
1154 old_manifest: Box::new(candidate.old_manifest.clone()),
1155 new_manifest: Box::new(new_manifest.clone())
1156 }
1157 ) {
1158 eprintln!(
1159 "Error: Failed to record transaction for {}: {}",
1160 candidate.source, e
1161 );
1162 failed_updates
1163 .lock()
1164 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1165 .push(candidate.source.clone());
1166 } else {
1167 successful_upgrades
1168 .lock()
1169 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1170 .push((
1171 candidate.old_manifest.clone(),
1172 new_manifest.clone(),
1173 candidate.new_pkg.clone()
1174 ));
1175 }
1176 } else {
1177 eprintln!(
1178 "Failed to get new manifest for {}",
1179 candidate.source
1180 );
1181 failed_updates
1182 .lock()
1183 .map_err(|e| anyhow!("mutex poisoned: {e}"))?
1184 .push(candidate.source.clone());
1185 }
1186 Ok(())
1187 })?;
1188
1189 let failed = failed_updates
1190 .into_inner()
1191 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1192 if !failed.is_empty() {
1193 eprintln!(
1194 "\nError: Some packages failed to upgrade. Rolling back all \
1195 changes..."
1196 );
1197 for pkg in &failed {
1198 eprintln!(" - {pkg}");
1199 }
1200 transaction::rollback(&transaction_id)?;
1201 ux::print_transaction_summary(&ux::TransactionSummary {
1202 command: "update".to_string(),
1203 success: 0,
1204 failed: failed.len(),
1205 skipped: pinned_sources.len()
1206 + skipped_sources.len()
1207 + up_to_date_sources.len()
1208 });
1209 return Err(anyhow!("Update failed for some packages."));
1210 }
1211
1212 if let Ok(modified_files) = transaction::get_modified_files(&transaction_id)
1213 {
1214 let modified_packages =
1215 transaction::get_modified_packages(&transaction_id)
1216 .unwrap_or_default();
1217 let upgrades_lock = successful_upgrades
1218 .lock()
1219 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1220 let first_scope = upgrades_lock
1221 .first()
1222 .map_or(types::Scope::User, |(old, _, _)| old.scope);
1223 let _ = crate::pkg::hooks::global::run_global_hooks(
1224 crate::pkg::hooks::global::HookWhen::PostTransaction,
1225 &modified_files,
1226 &modified_packages,
1227 "upgrade",
1228 first_scope
1229 );
1230 }
1231 transaction::commit(&transaction_id)?;
1232
1233 if !plan_json {
1234 println!("\n{}", "Success:".green());
1235 }
1236 let successful_upgrades = successful_upgrades
1237 .into_inner()
1238 .map_err(|e| anyhow!("mutex poisoned: {e}"))?;
1239 for (old_manifest, new_manifest, new_pkg) in &successful_upgrades {
1240 if let Some(backup_files) = &old_manifest.backup {
1241 if !plan_json {
1242 println!(
1243 "Restoring configuration for {}...",
1244 old_manifest.name.cyan()
1245 );
1246 }
1247 let old_version_dir = local::get_package_version_dir(
1248 old_manifest.scope,
1249 &old_manifest.registry_handle,
1250 &old_manifest.repo,
1251 &old_manifest.name,
1252 &old_manifest.version
1253 )?;
1254 let new_version_dir = local::get_package_version_dir(
1255 new_manifest.scope,
1256 &new_manifest.registry_handle,
1257 &new_manifest.repo,
1258 &new_manifest.name,
1259 &new_manifest.version
1260 )?;
1261 handle_backup_files(
1262 &old_version_dir,
1263 &new_version_dir,
1264 backup_files,
1265 old_manifest.scope
1266 )?;
1267 }
1268
1269 if let Err(e) = cleanup_old_versions(
1270 &new_manifest.name,
1271 new_manifest.scope,
1272 &new_manifest.repo,
1273 &new_manifest.registry_handle
1274 ) {
1275 eprintln!(
1276 "Failed to clean up old versions for {}: {}",
1277 new_manifest.name, e
1278 );
1279 }
1280
1281 if let Ok(conn) = db::open_connection("local") {
1282 let _ = db::update_package(
1283 &conn,
1284 new_pkg,
1285 &new_manifest.registry_handle,
1286 Some(new_manifest.scope),
1287 new_manifest.sub_package.as_deref(),
1288 Some(&old_manifest.reason)
1289 );
1290 }
1291
1292 if let Some(hooks) = &new_pkg.hooks
1293 && let Err(e) = hooks::run_hooks(
1294 hooks,
1295 hooks::HookType::PostUpgrade,
1296 new_manifest.scope
1297 )
1298 {
1299 eprintln!(
1300 "{}: Post-upgrade hook failed for '{}': {}",
1301 "Error".red().bold(),
1302 new_manifest.name,
1303 e
1304 );
1305 }
1306 }
1307
1308 if !plan_json {
1309 ux::print_transaction_summary(&ux::TransactionSummary {
1310 command: "update".to_string(),
1311 success: successful_upgrades.len(),
1312 failed: 0,
1313 skipped: pinned_sources.len()
1314 + skipped_sources.len()
1315 + up_to_date_sources.len()
1316 });
1317 }
1318 Ok(())
1319}
1320
1321fn advisory_counts(
1324 registry_handle: &str,
1325 package: &str,
1326 sub_package: Option<&str>,
1327 old_version: &str,
1328 new_version: &str
1329) -> Result<(usize, usize)> {
1330 let advisories =
1331 db::get_advisories_for_package(registry_handle, package, sub_package)?;
1332 let old_ver = Version::parse(old_version).map_err(|e| {
1333 anyhow!("failed to parse old version '{old_version}': {e}")
1334 })?;
1335 let new_ver = Version::parse(new_version).map_err(|e| {
1336 anyhow!("failed to parse new version '{new_version}': {e}")
1337 })?;
1338
1339 let mut old_count = 0usize;
1340 let mut new_count = 0usize;
1341 for adv in advisories {
1342 if let Ok(req) = semver::VersionReq::parse(&adv.affected_range) {
1343 if req.matches(&old_ver) {
1344 old_count += 1;
1345 }
1346 if req.matches(&new_ver) {
1347 new_count += 1;
1348 }
1349 }
1350 }
1351 Ok((old_count, new_count))
1352}
1353
1354fn cleanup_old_versions(
1357 package_name: &str,
1358 scope: types::Scope,
1359 repo: &str,
1360 registry_handle: &str
1361) -> Result<()> {
1362 let config = config::read_config()?;
1363 let rollback_enabled = config.rollback_enabled;
1364 let package_dir =
1365 local::get_package_dir(scope, registry_handle, repo, package_name)?;
1366
1367 let mut versions = Vec::new();
1368 if let Ok(entries) = fs::read_dir(&package_dir) {
1369 for entry in entries.flatten() {
1370 let path = entry.path();
1371 if path.is_dir()
1372 && let Some(version_str) =
1373 path.file_name().and_then(|s| s.to_str())
1374 && version_str != "latest"
1375 && let Ok(version) = Version::parse(version_str)
1376 {
1377 versions.push(version);
1378 }
1379 }
1380 }
1381
1382 if versions.is_empty() {
1383 return Ok(());
1384 }
1385 versions.sort();
1386
1387 let versions_to_keep = if rollback_enabled { 3 } else { 1 };
1388 if versions.len() > versions_to_keep {
1389 let num_to_delete = versions.len() - versions_to_keep;
1390 println!("Cleaning up old versions...");
1391 let to_delete = versions
1392 .get(..num_to_delete)
1393 .ok_or_else(|| anyhow!("Version slice out of bounds"))?;
1394 for version in to_delete {
1395 let version_dir_to_delete = package_dir.join(version.to_string());
1396 println!(" - Removing {}", version_dir_to_delete.display());
1397 let _ = fs::remove_dir_all(version_dir_to_delete);
1398 }
1399 }
1400 Ok(())
1401}