pakker/src/cli/commands/add.rs
NotAShelf 79a82d6ab8
cli: wire MultiError in add/rm; add typo suggestions
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I98240ec0f9e3932a46e79f82f32cd5d36a6a6964
2026-02-19 00:22:38 +03:00

261 lines
7.6 KiB
Rust

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<String> {
lockfile.loaders.keys().cloned().collect()
}
pub fn create_all_platforms()
-> Result<HashMap<String, Box<dyn crate::platform::PlatformClient>>> {
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);
}
Ok(platforms)
}
async fn resolve_input(
input: &str,
platforms: &HashMap<String, Box<dyn crate::platform::PlatformClient>>,
lockfile: &LockFile,
) -> Result<Project> {
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(())
}