From 79a82d6ab843b2995ec63150c8935cdef11e3c6e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 12 Feb 2026 23:20:34 +0300 Subject: [PATCH] cli: wire `MultiError` in add/rm; add typo suggestions Signed-off-by: NotAShelf Change-Id: I98240ec0f9e3932a46e79f82f32cd5d36a6a6964 --- src/cli/commands/add.rs | 40 ++++++++++++++++++++++++++++++++--- src/cli/commands/rm.rs | 46 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/add.rs b/src/cli/commands/add.rs index 751c941..1ea653e 100644 --- a/src/cli/commands/add.rs +++ b/src/cli/commands/add.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::{ - error::{PakkerError, Result}, + error::{MultiError, PakkerError, Result}, model::{Config, LockFile, Project}, platform::create_platform, resolver::DependencyResolver, @@ -139,10 +139,19 @@ pub async fn execute( let platforms = create_all_platforms()?; let mut new_projects = Vec::new(); + let mut errors = MultiError::new(); // Resolve each input for input in &args.inputs { - let project = resolve_input(input, &platforms, &lockfile).await?; + let project = match resolve_input(input, &platforms, &lockfile).await { + Ok(p) => p, + Err(e) => { + // Collect error but continue with other inputs + log::warn!("Failed to resolve '{input}': {e}"); + errors.push(e); + continue; + }, + }; // Check if already exists by matching platform IDs (not pakku_id which is // random) @@ -174,6 +183,15 @@ pub async fn execute( continue; } + // Prompt for confirmation unless --yes flag is set + if !args.yes { + let prompt_msg = format!("Add project '{}'?", project.get_name()); + if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? { + log::info!("Skipping project: {}", project.get_name()); + continue; + } + } + new_projects.push(project); } @@ -213,6 +231,9 @@ pub async fn execute( new_projects = all_new_projects; } + // Track count before moving + let added_count = new_projects.len(); + // Add projects to lockfile (updates already handled above) for project in new_projects { lockfile.add_project(project); @@ -221,7 +242,20 @@ pub async fn execute( // Save lockfile lockfile.save(lockfile_dir)?; - log::info!("Successfully added {} project(s)", args.inputs.len()); + log::info!("Successfully added {added_count} project(s)"); + + // Return aggregated errors if any occurred + if !errors.is_empty() { + let error_count = errors.len(); + log::warn!( + "{error_count} project(s) failed to resolve (see warnings above)" + ); + // Return success if at least some projects were added, otherwise return + // errors + if added_count == 0 && args.inputs.len() == error_count { + return errors.into_result(()); + } + } Ok(()) } diff --git a/src/cli/commands/rm.rs b/src/cli/commands/rm.rs index cee440a..a147d66 100644 --- a/src/cli/commands/rm.rs +++ b/src/cli/commands/rm.rs @@ -4,7 +4,7 @@ use crate::{ cli::RmArgs, error::{PakkerError, Result}, model::LockFile, - ui_utils::prompt_yes_no, + ui_utils::{prompt_typo_suggestion, prompt_yes_no}, }; pub async fn execute( @@ -44,15 +44,52 @@ pub async fn execute( }; } - log::info!("Removing projects: {:?}", inputs); + log::info!("Removing projects: {inputs:?}"); let mut removed_count = 0; let mut removed_ids = Vec::new(); let mut projects_to_remove = Vec::new(); + // 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(); + // First, identify all projects to remove + let mut resolved_inputs = Vec::new(); for input in &inputs { // Find project by various identifiers + if lockfile.projects.iter().any(|p| { + p.pakku_id.as_deref() == Some(input) + || p.slug.values().any(|s| s == input) + || p.name.values().any(|n| n.eq_ignore_ascii_case(input)) + || p.aliases.contains(input) + }) { + resolved_inputs.push(input.clone()); + } else if !args.all { + // Try typo suggestion + if let Ok(Some(suggestion)) = prompt_typo_suggestion(input, &all_slugs) { + log::info!("Using suggested project: {suggestion}"); + resolved_inputs.push(suggestion); + } else { + log::warn!("Project not found: {input}"); + } + } + } + + // Now find the actual projects from resolved inputs + for input in &resolved_inputs { if let Some(project) = lockfile.projects.iter().find(|p| { p.pakku_id.as_deref() == Some(input) || p.slug.values().any(|s| s == input) @@ -60,11 +97,12 @@ pub async fn execute( || p.aliases.contains(input) }) { projects_to_remove.push(project.get_name()); - } else if !args.all { - log::warn!("Project not found: {input}"); } } + // Replace inputs with resolved_inputs for actual removal + let inputs = resolved_inputs; + if projects_to_remove.is_empty() { return Err(PakkerError::ProjectNotFound( "None of the specified projects found".to_string(),