use std::collections::HashMap; use crate::{ error::{MultiError, PakkerError, Result}, model::{Config, LockFile, Project}, platform::create_platform, resolver::DependencyResolver, }; fn get_loaders(lockfile: &LockFile) -> Vec { lockfile.loaders.keys().cloned().collect() } pub fn create_all_platforms() -> Result>> { const MODRINTH: &str = "modrinth"; const CURSEFORGE: &str = "curseforge"; let mut platforms = HashMap::new(); if let Ok(platform) = create_platform(MODRINTH, None) { platforms.insert(MODRINTH.to_owned(), platform); } if let Ok(platform) = create_platform(CURSEFORGE, std::env::var("CURSEFORGE_API_KEY").ok()) { platforms.insert(CURSEFORGE.to_owned(), platform); } Ok(platforms) } async fn resolve_input( input: &str, platforms: &HashMap>, lockfile: &LockFile, ) -> Result { for platform in platforms.values() { if let Ok(project) = platform .request_project_with_files( input, &lockfile.mc_versions, &get_loaders(lockfile), ) .await { return Ok(project); } } Err(PakkerError::ProjectNotFound(input.to_string())) } use std::path::Path; use crate::{cli::AddArgs, model::fork::LocalConfig}; pub async fn execute( args: AddArgs, lockfile_path: &Path, config_path: &Path, ) -> Result<()> { log::info!("Adding projects: {:?}", args.inputs); // Load lockfile // 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(".")); // Check if lockfile exists (try both pakker-lock.json and pakku-lock.json) let lockfile_exists = lockfile_path.exists() || lockfile_dir.join("pakku-lock.json").exists(); if !lockfile_exists { // Try to load config from both pakker.json and pakku.json let local_config = LocalConfig::load(config_dir).or_else(|_| { let legacy_config_path = config_dir.join("pakku.json"); if legacy_config_path.exists() { LocalConfig::load(&config_dir.join("pakku.json")) } else { Err(PakkerError::IoError(std::io::Error::new( std::io::ErrorKind::NotFound, "No pakker.json found", ))) } })?; if local_config.has_parent() { log::info!("Creating minimal fork lockfile with parent metadata..."); // Check for parent lockfile (try both pakker-lock.json and // pakku-lock.json) let parent_paths = [ lockfile_dir.join(".pakku/parent/pakker-lock.json"), lockfile_dir.join(".pakku/parent/pakku-lock.json"), ]; let parent_found = parent_paths.iter().any(|path| path.exists()); if !parent_found { return Err(PakkerError::IoError(std::io::Error::new( std::io::ErrorKind::NotFound, "Fork configured but parent lockfile not found at \ .pakku/parent/pakker-lock.json or .pakku/parent/pakku-lock.json", ))); } // Load parent lockfile to get metadata let parent_lockfile = parent_paths .iter() .find(|path| path.exists()) .and_then(|path| LockFile::load(path.parent().unwrap()).ok()) .ok_or_else(|| { PakkerError::IoError(std::io::Error::new( std::io::ErrorKind::NotFound, "Failed to load parent lockfile metadata", )) })?; let minimal_lockfile = LockFile { target: parent_lockfile.target, mc_versions: parent_lockfile.mc_versions, loaders: parent_lockfile.loaders, projects: Vec::new(), lockfile_version: 1, }; minimal_lockfile.save_without_validation(lockfile_dir)?; } else { return Err(PakkerError::IoError(std::io::Error::new( std::io::ErrorKind::NotFound, "pakker-lock.json not found and no fork configured. Run 'pakker init' \ first.", ))); } } let mut lockfile = LockFile::load_with_validation(lockfile_dir, false)?; // Load config if available let _config = Config::load(config_dir).ok(); // Create platforms 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 = 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) let already_exists = lockfile.projects.iter().any(|p| { // Check if any platform ID matches project.id.iter().any(|(platform, id)| { p.id .get(platform) .is_some_and(|existing_id| existing_id == id) }) }); if already_exists { if args.update { log::info!("Updating existing project: {}", project.get_name()); // Find and replace the existing project if let Some(pos) = lockfile.projects.iter().position(|p| { project.id.iter().any(|(platform, id)| { p.id .get(platform) .is_some_and(|existing_id| existing_id == id) }) }) { lockfile.projects[pos] = project; } continue; } log::info!("Project already exists: {}", project.get_name()); 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); } // Resolve dependencies unless --no-deps is specified if !args.no_deps { log::info!("Resolving dependencies..."); let mut resolver = DependencyResolver::new(); let mut all_new_projects = new_projects.clone(); for project in &mut new_projects { let deps = resolver.resolve(project, &mut lockfile, &platforms).await?; for dep in deps { if !lockfile.projects.iter().any(|p| p.pakku_id == dep.pakku_id) && !all_new_projects.iter().any(|p| p.pakku_id == dep.pakku_id) { // Prompt user for confirmation unless --yes flag is set if !args.yes { let prompt_msg = format!( "Add dependency '{}' required by '{}'?", dep.get_name(), project.get_name() ); if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? { log::info!("Skipping dependency: {}", dep.get_name()); continue; } } log::info!("Adding dependency: {}", dep.get_name()); all_new_projects.push(dep); } } } 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); } // Save lockfile lockfile.save(lockfile_dir)?; 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(()) }