use std::{collections::HashMap, path::Path}; use indicatif::{ProgressBar, ProgressStyle}; use crate::{ cli::UpdateArgs, error::{MultiError, PakkerError}, model::{Config, LockFile, UpdateStrategy}, platform::create_platform, ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no}, }; pub async fn execute( args: UpdateArgs, lockfile_path: &Path, config_path: &Path, ) -> Result<(), PakkerError> { // Load expects directory path, so get parent directory let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new(".")); let config_dir = config_path.parent().unwrap_or(Path::new(".")); let mut lockfile = LockFile::load(lockfile_dir)?; let _config = Config::load(config_dir)?; // Create platforms let mut platforms = HashMap::new(); if let Ok(platform) = create_platform("modrinth", None) { platforms.insert("modrinth".to_string(), platform); } if let Ok(platform) = create_platform("curseforge", std::env::var("CURSEFORGE_API_KEY").ok()) { 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 { let mut indices = Vec::new(); for input in &args.inputs { if let Some((idx, _)) = lockfile .projects .iter() .enumerate() .find(|(_, p)| p.matches_input(input)) { 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(total_projects as u64); pb.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") .unwrap() .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 .slug .values() .next() .ok_or_else(|| PakkerError::InvalidProject("No slug found".into()))?; // Find updated project from one of the platforms let mut updated_project = None; for platform in platforms.values() { if let Ok(project) = platform .request_project_with_files( slug, &lockfile.mc_versions, &lockfile.loaders.keys().cloned().collect::>(), ) .await { updated_project = Some(project); break; } } 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() { // 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!(" {project_name} - Already up to date")); } else { // 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(|| { // 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 = updated_project .files .iter() .map(|f| format!("{} ({})", f.file_name, f.id)) .collect(); 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); } } }); } // 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); } 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(()) }