cli: add --all flag to update; wire UpdateStrategy enforcement

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9570557396ac46e82cbabbd8e39be0936a6a6964
This commit is contained in:
raf 2026-02-12 23:20:49 +03:00
commit 1ecf0fae00
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -4,10 +4,10 @@ use indicatif::{ProgressBar, ProgressStyle};
use crate::{ use crate::{
cli::UpdateArgs, cli::UpdateArgs,
error::PakkerError, error::{MultiError, PakkerError},
model::{Config, LockFile}, model::{Config, LockFile, UpdateStrategy},
platform::create_platform, platform::create_platform,
ui_utils::prompt_select, ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no},
}; };
pub async fn execute( pub async fn execute(
@ -33,6 +33,22 @@ pub async fn execute(
platforms.insert("curseforge".to_string(), platform); platforms.insert("curseforge".to_string(), platform);
} }
// Collect all known project identifiers for typo suggestions
let all_slugs: Vec<String> = 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() { let project_indices: Vec<_> = if args.inputs.is_empty() {
(0..lockfile.projects.len()).collect() (0..lockfile.projects.len()).collect()
} else { } else {
@ -46,14 +62,29 @@ pub async fn execute(
{ {
indices.push(idx); indices.push(idx);
} else { } 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())); return Err(PakkerError::ProjectNotFound(input.clone()));
} }
} }
indices indices
}; };
// Capture count before consuming the iterator
let total_projects = project_indices.len();
// Create progress bar // Create progress bar
let pb = ProgressBar::new(project_indices.len() as u64); let pb = ProgressBar::new(total_projects as u64);
pb.set_style( pb.set_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
@ -61,8 +92,23 @@ pub async fn execute(
.progress_chars("#>-"), .progress_chars("#>-"),
); );
let mut skipped_pinned = 0;
let mut update_errors = MultiError::new();
for idx in project_indices { for idx in project_indices {
let old_project = &lockfile.projects[idx]; 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())); pb.set_message(format!("Updating {}...", old_project.get_name()));
let slug = old_project let slug = old_project
@ -87,21 +133,46 @@ 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 if let Some(mut updated_project) = updated_project
&& !updated_project.files.is_empty() && !updated_project.files.is_empty()
&& let Some(old_file) = lockfile.projects[idx].files.first() && 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 { if new_file_id == old_file.id {
pb.println(format!( pb.println(format!(" {project_name} - Already up to date"));
" {} - Already up to date",
old_project.get_name()
));
} else { } else {
// Interactive version selection if not using --yes flag // Interactive confirmation and version selection if not using --yes
if !args.yes && updated_project.files.len() > 1 { // flag
let mut should_update = args.yes || args.all;
let mut selected_idx: Option<usize> = None;
if !args.yes && !args.all {
pb.suspend(|| { pb.suspend(|| {
// 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);
// If confirmed and multiple versions available, offer selection
if should_update && updated_project.files.len() > 1 {
let choices: Vec<String> = updated_project let choices: Vec<String> = updated_project
.files .files
.iter() .iter()
@ -111,30 +182,67 @@ pub async fn execute(
let choice_refs: Vec<&str> = let choice_refs: Vec<&str> =
choices.iter().map(std::string::String::as_str).collect(); choices.iter().map(std::string::String::as_str).collect();
if let Ok(selected_idx) = prompt_select( if let Ok(idx) = prompt_select(
&format!("Select version for {}:", old_project.get_name()), &format!("Select version for {project_name}:"),
&choice_refs, &choice_refs,
) { ) {
// Move selected file to front selected_idx = Some(idx);
if selected_idx > 0 {
updated_project.files.swap(0, selected_idx);
} }
} }
}); });
} }
// 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(); let selected_file = updated_project.files.first().unwrap();
pb.println(format!( pb.println(format!(
" {} -> {}", " {} -> {}",
old_file.file_name, selected_file.file_name old_file_name, selected_file.file_name
)); ));
lockfile.projects[idx] = updated_project; lockfile.projects[idx] = updated_project;
} else {
pb.println(format!(" {project_name} - Skipped by user"));
}
} }
} }
pb.inc(1); pb.inc(1);
} }
if skipped_pinned > 0 {
pb.finish_with_message(format!(
"Update complete ({skipped_pinned} pinned projects skipped)"
));
} else {
pb.finish_with_message("Update complete"); pb.finish_with_message("Update complete");
}
lockfile.save(lockfile_dir)?; 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(()) Ok(())
} }