From 1ecf0fae0025fa3410e1bc324ba1c705c139d25c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 12 Feb 2026 23:20:49 +0300 Subject: [PATCH] cli: add `--all` flag to update; wire `UpdateStrategy` enforcement Signed-off-by: NotAShelf Change-Id: I9570557396ac46e82cbabbd8e39be0936a6a6964 --- src/cli/commands/update.rs | 174 ++++++++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 33 deletions(-) diff --git a/src/cli/commands/update.rs b/src/cli/commands/update.rs index 88f5a43..f33caf2 100644 --- a/src/cli/commands/update.rs +++ b/src/cli/commands/update.rs @@ -4,10 +4,10 @@ use indicatif::{ProgressBar, ProgressStyle}; use crate::{ cli::UpdateArgs, - error::PakkerError, - model::{Config, LockFile}, + error::{MultiError, PakkerError}, + model::{Config, LockFile, UpdateStrategy}, platform::create_platform, - ui_utils::prompt_select, + ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no}, }; pub async fn execute( @@ -33,6 +33,22 @@ pub async fn execute( platforms.insert("curseforge".to_string(), platform); } + // Collect all known project identifiers for typo suggestions + let all_slugs: Vec = lockfile + .projects + .iter() + .flat_map(|p| { + let mut ids = Vec::new(); + if let Some(ref pakku_id) = p.pakku_id { + ids.push(pakku_id.clone()); + } + ids.extend(p.slug.values().cloned()); + ids.extend(p.name.values().cloned()); + ids.extend(p.aliases.iter().cloned()); + ids + }) + .collect(); + let project_indices: Vec<_> = if args.inputs.is_empty() { (0..lockfile.projects.len()).collect() } else { @@ -46,14 +62,29 @@ pub async fn execute( { indices.push(idx); } else { + // Try typo suggestion + if let Ok(Some(suggestion)) = prompt_typo_suggestion(input, &all_slugs) + && let Some((idx, _)) = lockfile + .projects + .iter() + .enumerate() + .find(|(_, p)| p.matches_input(&suggestion)) + { + log::info!("Using suggested project: {suggestion}"); + indices.push(idx); + continue; + } return Err(PakkerError::ProjectNotFound(input.clone())); } } indices }; + // Capture count before consuming the iterator + let total_projects = project_indices.len(); + // Create progress bar - let pb = ProgressBar::new(project_indices.len() as u64); + let pb = ProgressBar::new(total_projects as u64); pb.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") @@ -61,8 +92,23 @@ pub async fn execute( .progress_chars("#>-"), ); + let mut skipped_pinned = 0; + let mut update_errors = MultiError::new(); + for idx in project_indices { let old_project = &lockfile.projects[idx]; + + // Skip projects with UpdateStrategy::None (pinned) + if old_project.update_strategy == UpdateStrategy::None { + pb.println(format!( + " {} - Skipped (update strategy: NONE)", + old_project.get_name() + )); + skipped_pinned += 1; + pb.inc(1); + continue; + } + pb.set_message(format!("Updating {}...", old_project.get_name())); let slug = old_project @@ -87,54 +133,116 @@ pub async fn execute( } } + if updated_project.is_none() { + // Failed to fetch update info from any platform + update_errors.push(PakkerError::PlatformApiError(format!( + "Failed to check updates for '{}'", + old_project.get_name() + ))); + pb.inc(1); + continue; + } + if let Some(mut updated_project) = updated_project && !updated_project.files.is_empty() && let Some(old_file) = lockfile.projects[idx].files.first() { - let new_file = updated_project.files.first().unwrap(); + // Clone data needed for comparisons to avoid borrow issues + let new_file_id = updated_project.files.first().unwrap().id.clone(); + let new_file_name = + updated_project.files.first().unwrap().file_name.clone(); + let old_file_name = old_file.file_name.clone(); + let project_name = old_project.get_name(); - if new_file.id == old_file.id { - pb.println(format!( - " {} - Already up to date", - old_project.get_name() - )); + if new_file_id == old_file.id { + pb.println(format!(" {project_name} - Already up to date")); } else { - // Interactive version selection if not using --yes flag - if !args.yes && updated_project.files.len() > 1 { + // Interactive confirmation and version selection if not using --yes + // flag + let mut should_update = args.yes || args.all; + let mut selected_idx: Option = None; + + if !args.yes && !args.all { pb.suspend(|| { - let choices: Vec = updated_project - .files - .iter() - .map(|f| format!("{} ({})", f.file_name, f.id)) - .collect(); + // First, confirm the update + let prompt_msg = format!( + "Update '{project_name}' from {old_file_name} to \ + {new_file_name}?" + ); + should_update = prompt_yes_no(&prompt_msg, true).unwrap_or(false); - let choice_refs: Vec<&str> = - choices.iter().map(std::string::String::as_str).collect(); + // If confirmed and multiple versions available, offer selection + if should_update && updated_project.files.len() > 1 { + let choices: Vec = updated_project + .files + .iter() + .map(|f| format!("{} ({})", f.file_name, f.id)) + .collect(); - if let Ok(selected_idx) = prompt_select( - &format!("Select version for {}:", old_project.get_name()), - &choice_refs, - ) { - // Move selected file to front - if selected_idx > 0 { - updated_project.files.swap(0, selected_idx); + let choice_refs: Vec<&str> = + choices.iter().map(std::string::String::as_str).collect(); + + if let Ok(idx) = prompt_select( + &format!("Select version for {project_name}:"), + &choice_refs, + ) { + selected_idx = Some(idx); } } }); } - let selected_file = updated_project.files.first().unwrap(); - pb.println(format!( - " {} -> {}", - old_file.file_name, selected_file.file_name - )); - lockfile.projects[idx] = updated_project; + // Apply file selection outside the closure + if let Some(idx) = selected_idx + && idx > 0 + { + updated_project.files.swap(0, idx); + } + + if should_update { + let selected_file = updated_project.files.first().unwrap(); + pb.println(format!( + " {} -> {}", + old_file_name, selected_file.file_name + )); + lockfile.projects[idx] = updated_project; + } else { + pb.println(format!(" {project_name} - Skipped by user")); + } } } pb.inc(1); } - pb.finish_with_message("Update complete"); + if skipped_pinned > 0 { + pb.finish_with_message(format!( + "Update complete ({skipped_pinned} pinned projects skipped)" + )); + } else { + pb.finish_with_message("Update complete"); + } lockfile.save(lockfile_dir)?; + + // Report any errors that occurred during updates + if !update_errors.is_empty() { + let error_list = update_errors.errors(); + log::warn!( + "{} project(s) encountered errors during update check", + error_list.len() + ); + for err in error_list { + log::warn!(" - {err}"); + } + + // Extend with any additional collected errors and check if we should fail + let all_errors = update_errors.into_errors(); + if all_errors.len() == total_projects { + // All projects failed - return error + let mut multi = MultiError::new(); + multi.extend(all_errors); + return multi.into_result(()); + } + } + Ok(()) }