various: clean up multiplatform mod resolution; add lockfile management
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If1fed3ad9f9273266ae6e0e24d57b6996a6a6964
This commit is contained in:
parent
6ffcfb5af6
commit
da15ebf9bd
14 changed files with 818 additions and 141 deletions
|
|
@ -92,6 +92,9 @@ pub enum Commands {
|
||||||
|
|
||||||
/// Manage fork configuration
|
/// Manage fork configuration
|
||||||
Fork(ForkArgs),
|
Fork(ForkArgs),
|
||||||
|
|
||||||
|
/// Check and repair lockfile integrity
|
||||||
|
Lockfile(LockfileArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -221,17 +224,33 @@ pub struct UpdateArgs {
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct LsArgs {
|
pub struct LsArgs {
|
||||||
/// Show detailed information
|
/// Show all optional columns (equivalent to enabling all --show-* flags)
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
pub detailed: bool,
|
pub detailed: bool,
|
||||||
|
|
||||||
/// Add update information for projects
|
/// Show project type column (mod, resourcepack, shader, etc.)
|
||||||
|
#[clap(long = "show-type")]
|
||||||
|
pub show_type: bool,
|
||||||
|
|
||||||
|
/// Show project side column (client, server, both)
|
||||||
|
#[clap(long = "show-side")]
|
||||||
|
pub show_side: bool,
|
||||||
|
|
||||||
|
/// Show first slug column
|
||||||
|
#[clap(long = "show-slug")]
|
||||||
|
pub show_slug: bool,
|
||||||
|
|
||||||
|
/// Show dependency count column
|
||||||
|
#[clap(long = "show-links")]
|
||||||
|
pub show_links: bool,
|
||||||
|
|
||||||
|
/// Show provider versions (when present) column
|
||||||
|
#[clap(long = "show-versions")]
|
||||||
|
pub show_versions: bool,
|
||||||
|
|
||||||
|
/// Include update information for projects
|
||||||
#[clap(short = 'c', long = "check-updates")]
|
#[clap(short = 'c', long = "check-updates")]
|
||||||
pub check_updates: bool,
|
pub check_updates: bool,
|
||||||
|
|
||||||
/// Maximum length for project names
|
|
||||||
#[clap(long = "name-max-length")]
|
|
||||||
pub name_max_length: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -627,3 +646,24 @@ pub enum ForkSubcommand {
|
||||||
projects: Vec<String>,
|
projects: Vec<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lockfile management subcommand arguments
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
#[command(args_conflicts_with_subcommands = true)]
|
||||||
|
pub struct LockfileArgs {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub subcommand: LockfileSubcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum LockfileSubcommand {
|
||||||
|
/// Check the lockfile for known issues
|
||||||
|
Doctor,
|
||||||
|
|
||||||
|
/// Repair known lockfile issues
|
||||||
|
Repair {
|
||||||
|
/// Skip operations that require network access
|
||||||
|
#[clap(long)]
|
||||||
|
offline: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ async fn resolve_input(
|
||||||
|
|
||||||
let mut merged = projects.remove(0);
|
let mut merged = projects.remove(0);
|
||||||
for project in projects {
|
for project in projects {
|
||||||
merged.merge(project);
|
merged = merged.merged(project)?;
|
||||||
}
|
}
|
||||||
Ok(merged)
|
Ok(merged)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ pub async fn execute(
|
||||||
|
|
||||||
let mut combined_project = projects_to_merge.remove(0);
|
let mut combined_project = projects_to_merge.remove(0);
|
||||||
for project in projects_to_merge {
|
for project in projects_to_merge {
|
||||||
combined_project.merge(project);
|
combined_project = combined_project.merged(project)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply user-specified properties
|
// Apply user-specified properties
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@ use crate::{
|
||||||
git::{self, VcsType},
|
git::{self, VcsType},
|
||||||
model::{
|
model::{
|
||||||
LockFile,
|
LockFile,
|
||||||
|
Project,
|
||||||
|
credentials::ResolvedCredentials,
|
||||||
fork::{ForkIntegrity, LocalConfig, ParentConfig, RefType, hash_content},
|
fork::{ForkIntegrity, LocalConfig, ParentConfig, RefType, hash_content},
|
||||||
},
|
},
|
||||||
|
platform::create_platform,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAKKU_DIR: &str = ".pakku";
|
const PAKKU_DIR: &str = ".pakku";
|
||||||
|
|
@ -689,6 +692,54 @@ fn execute_sync() -> Result<(), PakkerError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_project_files(
|
||||||
|
project: &mut Project,
|
||||||
|
slug: &str,
|
||||||
|
mc_versions: &[String],
|
||||||
|
loaders: &[String],
|
||||||
|
) -> Result<(), PakkerError> {
|
||||||
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
|
||||||
|
if let Ok(platform) = create_platform("modrinth", None)
|
||||||
|
&& let Ok(mut resolved) = handle
|
||||||
|
.block_on(platform.request_project_with_files(slug, mc_versions, loaders))
|
||||||
|
&& !resolved.files.is_empty()
|
||||||
|
&& resolved.select_file(mc_versions, loaders, None).is_ok()
|
||||||
|
{
|
||||||
|
project.files = resolved.files;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let creds = ResolvedCredentials::load();
|
||||||
|
let cf_key = creds.curseforge_api_key().map(String::from);
|
||||||
|
if let Ok(platform) = create_platform("curseforge", cf_key)
|
||||||
|
&& let Ok(mut resolved) = handle
|
||||||
|
.block_on(platform.request_project_with_files(slug, mc_versions, loaders))
|
||||||
|
&& !resolved.files.is_empty()
|
||||||
|
&& resolved.select_file(mc_versions, loaders, None).is_ok()
|
||||||
|
{
|
||||||
|
project.files = resolved.files;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cf_key2 = ResolvedCredentials::load()
|
||||||
|
.curseforge_api_key()
|
||||||
|
.map(String::from);
|
||||||
|
if let Ok(platform) = create_platform("multiplatform", cf_key2)
|
||||||
|
&& let Ok(mut resolved) = handle
|
||||||
|
.block_on(platform.request_project_with_files(slug, mc_versions, loaders))
|
||||||
|
&& !resolved.files.is_empty()
|
||||||
|
&& resolved.select_file(mc_versions, loaders, None).is_ok()
|
||||||
|
{
|
||||||
|
project.files = resolved.files;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(PakkerError::FileSelectionError(format!(
|
||||||
|
"Could not resolve files for '{slug}'"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_promote(projects: &[String]) -> Result<(), PakkerError> {
|
fn execute_promote(projects: &[String]) -> Result<(), PakkerError> {
|
||||||
let config_dir = Path::new(".");
|
let config_dir = Path::new(".");
|
||||||
let local_config = LocalConfig::load(config_dir)?;
|
let local_config = LocalConfig::load(config_dir)?;
|
||||||
|
|
@ -765,7 +816,24 @@ fn execute_promote(projects: &[String]) -> Result<(), PakkerError> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
local_lockfile.add_project(project.clone());
|
let mut project = project.clone();
|
||||||
|
if project.files.is_empty() {
|
||||||
|
// Try to resolve files from platforms
|
||||||
|
if let Some(slug) = project.slug.values().next().cloned() {
|
||||||
|
let mc_versions = parent_lockfile.mc_versions.clone();
|
||||||
|
let loaders: Vec<String> =
|
||||||
|
parent_lockfile.loaders.keys().cloned().collect();
|
||||||
|
if let Err(e) =
|
||||||
|
resolve_project_files(&mut project, &slug, &mc_versions, &loaders)
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"Failed to resolve files for '{}': {e}",
|
||||||
|
project.get_name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_lockfile.add_project(project);
|
||||||
promoted.push(project_arg);
|
promoted.push(project_arg);
|
||||||
} else {
|
} else {
|
||||||
not_found.push(project_arg);
|
not_found.push(project_arg);
|
||||||
|
|
|
||||||
304
crates/pakker-cli/src/cli/commands/lockfile.rs
Normal file
304
crates/pakker-cli/src/cli/commands/lockfile.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
use std::{collections::HashSet, path::Path};
|
||||||
|
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::LockfileSubcommand,
|
||||||
|
error::Result,
|
||||||
|
model::{LockFile, Project, credentials::ResolvedCredentials},
|
||||||
|
platform::create_platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn execute(args: &crate::cli::LockfileArgs) -> Result<()> {
|
||||||
|
match &args.subcommand {
|
||||||
|
LockfileSubcommand::Doctor => execute_doctor(),
|
||||||
|
LockfileSubcommand::Repair { offline } => execute_repair(*offline),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_doctor() -> Result<()> {
|
||||||
|
let config_dir = Path::new(".");
|
||||||
|
let lockfile = LockFile::load(config_dir)?;
|
||||||
|
|
||||||
|
let issues = diagnose(&lockfile);
|
||||||
|
|
||||||
|
if issues.is_empty() {
|
||||||
|
println!("{}", "✓ Lockfile is healthy".green());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "Lockfile Issues:".yellow().bold());
|
||||||
|
println!();
|
||||||
|
for issue in &issues {
|
||||||
|
println!(" {} {}", "✗".red(), issue);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
format!(
|
||||||
|
"{} issue(s) found. Run 'pakker lockfile repair' to fix.",
|
||||||
|
issues.len()
|
||||||
|
)
|
||||||
|
.dim()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_repair(offline: bool) -> Result<()> {
|
||||||
|
let config_dir = Path::new(".");
|
||||||
|
let mut lockfile = LockFile::load(config_dir)?;
|
||||||
|
let issues_before = diagnose(&lockfile);
|
||||||
|
|
||||||
|
if issues_before.is_empty() {
|
||||||
|
println!("{}", "✓ Lockfile is healthy — nothing to repair".green());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Found {} issue(s). Attempting repair...",
|
||||||
|
issues_before.len()
|
||||||
|
)
|
||||||
|
.yellow()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let mut fixed = Vec::new();
|
||||||
|
let mut skipped = Vec::new();
|
||||||
|
|
||||||
|
// Fix 1: Resolve projects with empty files
|
||||||
|
let empty_file_count = lockfile
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.files.is_empty())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if empty_file_count > 0 {
|
||||||
|
if offline {
|
||||||
|
skipped.push(format!(
|
||||||
|
"{empty_file_count} project(s) with missing files (requires network)"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let resolved = resolve_empty_files(&mut lockfile);
|
||||||
|
if resolved > 0 {
|
||||||
|
fixed.push(format!(
|
||||||
|
"{resolved}/{empty_file_count} project(s) with missing files \
|
||||||
|
resolved"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if resolved < empty_file_count {
|
||||||
|
skipped.push(format!(
|
||||||
|
"{} project(s) could not be resolved (check slugs or network)",
|
||||||
|
empty_file_count - resolved
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix 2: Deduplicate
|
||||||
|
let project_count_before = lockfile.projects.len();
|
||||||
|
lockfile.deduplicate_projects();
|
||||||
|
let removed = project_count_before - lockfile.projects.len();
|
||||||
|
if removed > 0 {
|
||||||
|
fixed.push(format!("{removed} duplicate project(s) merged"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save repaired lockfile
|
||||||
|
lockfile.save(config_dir)?;
|
||||||
|
|
||||||
|
// Report
|
||||||
|
if !fixed.is_empty() {
|
||||||
|
println!("{}", "Fixed:".green());
|
||||||
|
for item in &fixed {
|
||||||
|
println!(" {} {}", "✓".green(), item);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
if !skipped.is_empty() {
|
||||||
|
println!("{}", "Skipped (requires attention):".yellow());
|
||||||
|
for item in &skipped {
|
||||||
|
println!(" {} {}", "!".yellow(), item);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnose(lockfile: &LockFile) -> Vec<String> {
|
||||||
|
let mut issues = Vec::new();
|
||||||
|
|
||||||
|
// Check for empty mc_versions
|
||||||
|
if lockfile.mc_versions.is_empty() {
|
||||||
|
issues.push("No Minecraft versions configured".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty loaders
|
||||||
|
if lockfile.loaders.is_empty() {
|
||||||
|
issues.push("No mod loaders configured".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check lockfile version
|
||||||
|
const LOCKFILE_VERSION: u32 = 2;
|
||||||
|
if lockfile.lockfile_version < LOCKFILE_VERSION {
|
||||||
|
issues.push(format!(
|
||||||
|
"Lockfile version {} is outdated (current: {LOCKFILE_VERSION})",
|
||||||
|
lockfile.lockfile_version
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check projects
|
||||||
|
let mut seen_slugs: HashSet<&str> = HashSet::new();
|
||||||
|
let mut duplicate_count = 0u32;
|
||||||
|
|
||||||
|
for project in &lockfile.projects {
|
||||||
|
let name = project.get_name();
|
||||||
|
|
||||||
|
// Empty files
|
||||||
|
if project.files.is_empty() && !project.slug.is_empty() {
|
||||||
|
issues.push(format!("'{name}' has no resolved files"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No slugs at all
|
||||||
|
if project.slug.is_empty() {
|
||||||
|
issues.push(format!("'{name}' has no platform slugs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No platform IDs
|
||||||
|
if project.id.is_empty() && !project.slug.is_empty() {
|
||||||
|
issues.push(format!("'{name}' has no platform IDs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate slugs
|
||||||
|
for slug in project.slug.values() {
|
||||||
|
if !seen_slugs.insert(slug.as_str()) {
|
||||||
|
duplicate_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if duplicate_count > 0 {
|
||||||
|
issues.push(format!(
|
||||||
|
"{duplicate_count} duplicate slug conflict(s) across projects"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
issues
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_empty_files(lockfile: &mut LockFile) -> usize {
|
||||||
|
let mut resolved = 0usize;
|
||||||
|
|
||||||
|
for project in lockfile.projects.iter_mut() {
|
||||||
|
if !project.files.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (platform_name, platform_slug) in &project.slug.clone() {
|
||||||
|
let mc_versions = lockfile.mc_versions.clone();
|
||||||
|
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
||||||
|
|
||||||
|
if resolve_project_from_platform(
|
||||||
|
project,
|
||||||
|
platform_name,
|
||||||
|
platform_slug,
|
||||||
|
&mc_versions,
|
||||||
|
&loaders,
|
||||||
|
) {
|
||||||
|
resolved += 1;
|
||||||
|
println!(
|
||||||
|
" {} Resolved files for '{}' via {platform_name}",
|
||||||
|
"✓".green(),
|
||||||
|
project.get_name()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try by platform ID if slug resolution failed
|
||||||
|
if project.files.is_empty() {
|
||||||
|
for (platform_name, platform_id) in &project.id.clone() {
|
||||||
|
if project.slug.contains_key(platform_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mc_versions = lockfile.mc_versions.clone();
|
||||||
|
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
||||||
|
|
||||||
|
if resolve_project_from_platform(
|
||||||
|
project,
|
||||||
|
platform_name,
|
||||||
|
platform_id,
|
||||||
|
&mc_versions,
|
||||||
|
&loaders,
|
||||||
|
) {
|
||||||
|
resolved += 1;
|
||||||
|
println!(
|
||||||
|
" {} Resolved files for '{}' via {platform_name} (by ID)",
|
||||||
|
"✓".green(),
|
||||||
|
project.get_name()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_project_from_platform(
|
||||||
|
project: &mut Project,
|
||||||
|
platform_name: &str,
|
||||||
|
identifier: &str,
|
||||||
|
mc_versions: &[String],
|
||||||
|
loaders: &[String],
|
||||||
|
) -> bool {
|
||||||
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
|
||||||
|
let api_key = match platform_name {
|
||||||
|
"curseforge" => {
|
||||||
|
ResolvedCredentials::load()
|
||||||
|
.curseforge_api_key()
|
||||||
|
.map(String::from)
|
||||||
|
},
|
||||||
|
"modrinth" => None,
|
||||||
|
"github" => {
|
||||||
|
ResolvedCredentials::load()
|
||||||
|
.github_access_token()
|
||||||
|
.map(String::from)
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let platform = match create_platform(platform_name, api_key) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("Failed to create platform '{platform_name}'");
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match handle.block_on(platform.request_project_with_files(
|
||||||
|
identifier,
|
||||||
|
mc_versions,
|
||||||
|
loaders,
|
||||||
|
)) {
|
||||||
|
Ok(mut resolved) => {
|
||||||
|
if resolved.files.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if resolved.select_file(mc_versions, loaders, None).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
project.files = resolved.files;
|
||||||
|
true
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Platform '{platform_name}' failed for '{identifier}': {e}");
|
||||||
|
false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,8 @@ use yansi::Paint;
|
||||||
|
|
||||||
use crate::{cli::LsArgs, error::Result, model::LockFile};
|
use crate::{cli::LsArgs, error::Result, model::LockFile};
|
||||||
|
|
||||||
/// Truncate a name to fit within `max_len` characters, adding "..." if
|
const COL_GAP: usize = 3; // spaces between columns
|
||||||
/// truncated
|
|
||||||
fn truncate_name(name: &str, max_len: usize) -> String {
|
fn truncate_name(name: &str, max_len: usize) -> String {
|
||||||
if name.len() <= max_len {
|
if name.len() <= max_len {
|
||||||
name.to_string()
|
name.to_string()
|
||||||
|
|
@ -16,8 +16,74 @@ fn truncate_name(name: &str, max_len: usize) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ColumnWidths {
|
||||||
|
name: usize,
|
||||||
|
file: usize,
|
||||||
|
r#type: usize,
|
||||||
|
side: usize,
|
||||||
|
slug: usize,
|
||||||
|
links: usize,
|
||||||
|
versions: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_widths(
|
||||||
|
lockfile: &LockFile,
|
||||||
|
show_type: bool,
|
||||||
|
show_side: bool,
|
||||||
|
show_slug: bool,
|
||||||
|
show_links: bool,
|
||||||
|
show_versions: bool,
|
||||||
|
) -> ColumnWidths {
|
||||||
|
let mut w = ColumnWidths {
|
||||||
|
name: "Name".len(),
|
||||||
|
file: "File".len(),
|
||||||
|
r#type: "Type".len(),
|
||||||
|
side: "Side".len(),
|
||||||
|
slug: "Slug".len(),
|
||||||
|
links: "Links".len(),
|
||||||
|
versions: "Versions".len(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for p in &lockfile.projects {
|
||||||
|
w.name = w.name.max(p.get_name().len().min(50));
|
||||||
|
let file_len = p.files.first().map_or(1, |f| f.file_name.len());
|
||||||
|
w.file = w.file.max(file_len);
|
||||||
|
|
||||||
|
if show_type {
|
||||||
|
let t = format!("{:?}", p.r#type).to_lowercase();
|
||||||
|
w.r#type = w.r#type.max(t.len());
|
||||||
|
}
|
||||||
|
if show_side {
|
||||||
|
let s = format!("{:?}", p.side).to_lowercase();
|
||||||
|
w.side = w.side.max(s.len());
|
||||||
|
}
|
||||||
|
if show_slug {
|
||||||
|
let slug_len = p.slug.values().next().map_or(1, String::len);
|
||||||
|
w.slug = w.slug.max(slug_len);
|
||||||
|
}
|
||||||
|
if show_links {
|
||||||
|
let links_len = p.pakku_links.len().to_string().len();
|
||||||
|
w.links = w.links.max(links_len);
|
||||||
|
}
|
||||||
|
if show_versions {
|
||||||
|
let v = if p.files.len() > 1 {
|
||||||
|
p.files
|
||||||
|
.iter()
|
||||||
|
.map(|f| format!("{}: {}", f.file_type, f.file_name))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
.len()
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
w.versions = w.versions.max(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
pub fn execute(args: &LsArgs, lockfile_path: &Path) -> Result<()> {
|
pub fn execute(args: &LsArgs, lockfile_path: &Path) -> Result<()> {
|
||||||
// Load expects directory path, so get parent directory
|
|
||||||
let lockfile_dir = lockfile_path.parent().unwrap_or_else(|| Path::new("."));
|
let lockfile_dir = lockfile_path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
let lockfile = LockFile::load(lockfile_dir)?;
|
let lockfile = LockFile::load(lockfile_dir)?;
|
||||||
|
|
||||||
|
|
@ -26,90 +92,155 @@ pub fn execute(args: &LsArgs, lockfile_path: &Path) -> Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let project_count = lockfile.projects.len();
|
||||||
|
|
||||||
|
let show_type = args.detailed || args.show_type;
|
||||||
|
let show_side = args.detailed || args.show_side;
|
||||||
|
let show_slug = args.detailed || args.show_slug;
|
||||||
|
let show_links = args.detailed || args.show_links;
|
||||||
|
let show_versions = args.detailed || args.show_versions;
|
||||||
|
|
||||||
|
let widths = compute_widths(
|
||||||
|
&lockfile,
|
||||||
|
show_type,
|
||||||
|
show_side,
|
||||||
|
show_slug,
|
||||||
|
show_links,
|
||||||
|
show_versions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build header
|
||||||
|
let mut header_cols: Vec<(&str, usize)> =
|
||||||
|
vec![("Name", widths.name), ("File", widths.file)];
|
||||||
|
if show_type {
|
||||||
|
header_cols.push(("Type", widths.r#type));
|
||||||
|
}
|
||||||
|
if show_side {
|
||||||
|
header_cols.push(("Side", widths.side));
|
||||||
|
}
|
||||||
|
if show_slug {
|
||||||
|
header_cols.push(("Slug", widths.slug));
|
||||||
|
}
|
||||||
|
if show_links {
|
||||||
|
header_cols.push(("Links", widths.links));
|
||||||
|
}
|
||||||
|
if show_versions {
|
||||||
|
header_cols.push(("Versions", widths.versions));
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{} ({})",
|
"{} ({})",
|
||||||
"Installed projects".bold(),
|
"Installed projects".bold(),
|
||||||
lockfile.projects.len().to_string().cyan().bold()
|
project_count.to_string().cyan().bold()
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Calculate max name length for alignment
|
// Print header
|
||||||
let max_name_len = args.name_max_length.unwrap_or_else(|| {
|
let header_line: Vec<String> = header_cols
|
||||||
lockfile
|
.iter()
|
||||||
.projects
|
.map(|(text, width)| {
|
||||||
.iter()
|
if text == &header_cols.last().unwrap().0 {
|
||||||
.map(|p| p.get_name().len())
|
format!("{text}")
|
||||||
.max()
|
} else {
|
||||||
.unwrap_or(20)
|
format!("{:<width$}", text, width = *width)
|
||||||
.min(50)
|
}
|
||||||
});
|
})
|
||||||
|
.collect();
|
||||||
|
println!("{}", header_line.join(&" ".repeat(COL_GAP)).cyan());
|
||||||
|
|
||||||
|
// Underline with dashes
|
||||||
|
let dash_line: String = header_cols
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (_, width))| {
|
||||||
|
let dashes = "-".repeat(*width);
|
||||||
|
if i == header_cols.len() - 1 {
|
||||||
|
dashes
|
||||||
|
} else {
|
||||||
|
format!("{:<width$}", dashes, width = *width)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(&" ".repeat(COL_GAP));
|
||||||
|
println!("{}", dash_line.dim());
|
||||||
|
|
||||||
for project in &lockfile.projects {
|
for project in &lockfile.projects {
|
||||||
// Check for version mismatch across providers
|
let name = truncate_name(&project.get_name(), widths.name.min(50));
|
||||||
let version_warning = if project.versions_match_across_providers() {
|
let warning_marker = if !project.versions_match_across_providers() {
|
||||||
""
|
|
||||||
} else {
|
|
||||||
// Use the detailed check_version_mismatch for logging
|
|
||||||
if let Some(mismatch_detail) = project.check_version_mismatch() {
|
if let Some(mismatch_detail) = project.check_version_mismatch() {
|
||||||
log::warn!("{mismatch_detail}");
|
log::warn!("{mismatch_detail}");
|
||||||
}
|
}
|
||||||
" [!] versions do not match across providers"
|
" [!]"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.detailed {
|
let file_name = project
|
||||||
let id = project.pakku_id.as_deref().unwrap_or("unknown");
|
.files
|
||||||
let name = truncate_name(&project.get_name(), max_name_len);
|
.first()
|
||||||
let name_line = format!(" {name} ({id})");
|
.map(|f| f.file_name.as_str())
|
||||||
if version_warning.is_empty() {
|
.unwrap_or("-");
|
||||||
println!("{}", name_line.bold());
|
|
||||||
} else {
|
|
||||||
println!("{}{}", name_line.bold(), version_warning.yellow());
|
|
||||||
}
|
|
||||||
println!(" {} {:?}", "Type:".dim(), project.r#type);
|
|
||||||
println!(" {} {:?}", "Side:".dim(), project.side);
|
|
||||||
|
|
||||||
if let Some(file) = project.files.first() {
|
let name_display = format!("{name}{warning_marker}");
|
||||||
println!(" {} {}", "File:".dim(), file.file_name);
|
print!(" ");
|
||||||
println!(
|
if warning_marker.is_empty() {
|
||||||
" {} {} ({})",
|
print!(
|
||||||
"Version:".dim(),
|
"{}",
|
||||||
file.release_type,
|
format!("{:<width$}", name_display, width = widths.name).bold()
|
||||||
file.date_published
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show version details if there's a mismatch
|
|
||||||
if !version_warning.is_empty() {
|
|
||||||
println!(" {}:", "Provider versions".dim());
|
|
||||||
for file in &project.files {
|
|
||||||
println!(" {}: {}", file.file_type, file.file_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !project.pakku_links.is_empty() {
|
|
||||||
println!(
|
|
||||||
" {} {}",
|
|
||||||
"Dependencies:".dim(),
|
|
||||||
project.pakku_links.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
} else {
|
} else {
|
||||||
let name = truncate_name(&project.get_name(), max_name_len);
|
print!(
|
||||||
let file_info = project
|
"{}",
|
||||||
.files
|
format!("{:<width$}", name_display, width = widths.name).yellow()
|
||||||
.first()
|
|
||||||
.map(|f| format!(" ({})", f.file_name))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
println!(
|
|
||||||
" {}{}{}",
|
|
||||||
name.bold(),
|
|
||||||
file_info.dim(),
|
|
||||||
version_warning.yellow()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!(
|
||||||
|
"{}",
|
||||||
|
format!("{:<width$}", file_name, width = widths.file).dim()
|
||||||
|
);
|
||||||
|
|
||||||
|
if show_type {
|
||||||
|
let t = format!("{:?}", project.r#type).to_lowercase();
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!("{:<width$}", t, width = widths.r#type);
|
||||||
|
}
|
||||||
|
if show_side {
|
||||||
|
let s = format!("{:?}", project.side).to_lowercase();
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!("{:<width$}", s, width = widths.side);
|
||||||
|
}
|
||||||
|
if show_slug {
|
||||||
|
let slug = project
|
||||||
|
.slug
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!("{:<width$}", slug, width = widths.slug);
|
||||||
|
}
|
||||||
|
if show_links {
|
||||||
|
let links = project.pakku_links.len().to_string();
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!("{:<width$}", links, width = widths.links);
|
||||||
|
}
|
||||||
|
if show_versions {
|
||||||
|
let v = if project.files.len() > 1 {
|
||||||
|
project
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.map(|f| format!("{}: {}", f.file_type, f.file_name))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
} else {
|
||||||
|
String::from("-")
|
||||||
|
};
|
||||||
|
print!("{}", " ".repeat(COL_GAP));
|
||||||
|
print!("{v}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub mod import;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod inspect;
|
pub mod inspect;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
|
pub mod lockfile;
|
||||||
pub mod ls;
|
pub mod ls;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod remote_update;
|
pub mod remote_update;
|
||||||
|
|
|
||||||
|
|
@ -255,49 +255,88 @@ async fn check_project_update(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
lockfile: &LockFile,
|
lockfile: &LockFile,
|
||||||
) -> Result<Option<ProjectUpdate>> {
|
) -> Result<Option<ProjectUpdate>> {
|
||||||
// Get primary slug
|
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
||||||
let slug = project
|
let mc_versions = &lockfile.mc_versions;
|
||||||
.slug
|
|
||||||
.values()
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
crate::error::PakkerError::InvalidProject("No slug found".to_string())
|
|
||||||
})?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// Try each platform in project
|
let mut errors: Vec<String> = Vec::new();
|
||||||
for platform_name in project.id.keys() {
|
|
||||||
|
for (platform_name, platform_slug) in &project.slug {
|
||||||
let api_key = get_api_key(platform_name);
|
let api_key = get_api_key(platform_name);
|
||||||
let Ok(platform) = create_platform(platform_name, api_key) else {
|
let Ok(platform) = create_platform(platform_name, api_key) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
match platform
|
||||||
|
.request_project_with_files(platform_slug, mc_versions, &loaders)
|
||||||
if let Ok(updated_project) = platform
|
|
||||||
.request_project_with_files(&slug, &lockfile.mc_versions, &loaders)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Compare files to detect updates
|
Ok(updated_project) => {
|
||||||
let file_updates = detect_file_updates(project, &updated_project);
|
let file_updates = detect_file_updates(project, &updated_project);
|
||||||
|
|
||||||
if !file_updates.is_empty() {
|
if !file_updates.is_empty() {
|
||||||
return Ok(Some(ProjectUpdate {
|
return Ok(Some(ProjectUpdate {
|
||||||
slug: project.slug.clone(),
|
slug: project.slug.clone(),
|
||||||
name: project.name.values().next().cloned().unwrap_or_default(),
|
name: project.name.values().next().cloned().unwrap_or_default(),
|
||||||
project_type: format!("{:?}", project.r#type),
|
project_type: format!("{:?}", project.r#type),
|
||||||
side: format!("{:?}", project.side),
|
side: format!("{:?}", project.side),
|
||||||
file_updates,
|
file_updates,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(None); // No updates
|
return Ok(None);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
errors.push(format!("{platform_name}: {e}"));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(crate::error::PakkerError::PlatformApiError(
|
// Also try platforms that have IDs but no slugs (uncommon edge case)
|
||||||
"Failed to check for updates on any platform".to_string(),
|
for platform_name in project.id.keys() {
|
||||||
))
|
if project.slug.contains_key(platform_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let platform_id = project
|
||||||
|
.id
|
||||||
|
.get(platform_name)
|
||||||
|
.expect("key must exist in id map");
|
||||||
|
let api_key = get_api_key(platform_name);
|
||||||
|
let Ok(platform) = create_platform(platform_name, api_key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match platform
|
||||||
|
.request_project_with_files(platform_id, mc_versions, &loaders)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(updated_project) => {
|
||||||
|
let file_updates = detect_file_updates(project, &updated_project);
|
||||||
|
if !file_updates.is_empty() {
|
||||||
|
return Ok(Some(ProjectUpdate {
|
||||||
|
slug: project.slug.clone(),
|
||||||
|
name: project.name.values().next().cloned().unwrap_or_default(),
|
||||||
|
project_type: format!("{:?}", project.r#type),
|
||||||
|
side: format!("{:?}", project.side),
|
||||||
|
file_updates,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
errors.push(format!("{platform_name}(by id): {e}"));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_detail = if errors.is_empty() {
|
||||||
|
"no platform slugs or IDs available".to_string()
|
||||||
|
} else {
|
||||||
|
errors.join(" | ")
|
||||||
|
};
|
||||||
|
|
||||||
|
Err(crate::error::PakkerError::PlatformApiError(format!(
|
||||||
|
"Failed to check for updates on any platform ({error_detail})"
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_file_updates(
|
fn detect_file_updates(
|
||||||
|
|
|
||||||
|
|
@ -238,5 +238,9 @@ pub async fn run() -> Result<(), PakkerError> {
|
||||||
cli::commands::fork::execute(&args)?;
|
cli::commands::fork::execute(&args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
Commands::Lockfile(args) => {
|
||||||
|
cli::commands::lockfile::execute(&args)?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use std::{collections::HashMap, path::Path};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -656,6 +659,7 @@ impl LockFile {
|
||||||
lockfile.validate()?;
|
lockfile.validate()?;
|
||||||
}
|
}
|
||||||
lockfile.sort_projects();
|
lockfile.sort_projects();
|
||||||
|
lockfile.deduplicate_projects();
|
||||||
|
|
||||||
Ok(lockfile)
|
Ok(lockfile)
|
||||||
}
|
}
|
||||||
|
|
@ -754,7 +758,84 @@ impl LockFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_project(&mut self, project: Project) {
|
pub fn add_project(&mut self, project: Project) {
|
||||||
|
// Check for existing project with overlapping slugs
|
||||||
|
if let Some(existing) = self.projects.iter_mut().find(|p| {
|
||||||
|
p.slug
|
||||||
|
.values()
|
||||||
|
.any(|s| project.slug.values().any(|ps| ps == s))
|
||||||
|
}) {
|
||||||
|
// Merge data into existing project
|
||||||
|
for (platform, slug) in &project.slug {
|
||||||
|
existing
|
||||||
|
.slug
|
||||||
|
.entry(platform.clone())
|
||||||
|
.or_insert_with(|| slug.clone());
|
||||||
|
}
|
||||||
|
for (platform, name) in &project.name {
|
||||||
|
existing
|
||||||
|
.name
|
||||||
|
.entry(platform.clone())
|
||||||
|
.or_insert_with(|| name.clone());
|
||||||
|
}
|
||||||
|
for (platform, id) in &project.id {
|
||||||
|
existing
|
||||||
|
.id
|
||||||
|
.entry(platform.clone())
|
||||||
|
.or_insert_with(|| id.clone());
|
||||||
|
}
|
||||||
|
for file in &project.files {
|
||||||
|
if !existing.files.iter().any(|f| f.file_name == file.file_name) {
|
||||||
|
existing.files.push(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"Merged duplicate project '{}' into existing entry",
|
||||||
|
project.get_name()
|
||||||
|
);
|
||||||
|
self.projects.sort_by_key(super::project::Project::get_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.projects.push(project);
|
self.projects.push(project);
|
||||||
self.projects.sort_by_key(super::project::Project::get_name);
|
self.projects.sort_by_key(super::project::Project::get_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove duplicate projects that share overlapping slugs.
|
||||||
|
/// When duplicates are found, files from the duplicate are merged into
|
||||||
|
/// the kept project. This handles lockfiles that were corrupted before
|
||||||
|
/// `add_project` enforced slug uniqueness.
|
||||||
|
pub fn deduplicate_projects(&mut self) {
|
||||||
|
let mut seen_slugs: HashSet<String> = HashSet::new();
|
||||||
|
let mut slug_to_idx: HashMap<String, usize> = HashMap::new();
|
||||||
|
let mut unique: Vec<Project> = Vec::with_capacity(self.projects.len());
|
||||||
|
|
||||||
|
for project in self.projects.drain(..) {
|
||||||
|
let duplicate_slug =
|
||||||
|
project.slug.values().find(|s| seen_slugs.contains(*s));
|
||||||
|
|
||||||
|
if let Some(dup_slug) = duplicate_slug {
|
||||||
|
log::debug!(
|
||||||
|
"Removed duplicate project '{}' (slug collision: {dup_slug})",
|
||||||
|
project.get_name()
|
||||||
|
);
|
||||||
|
if let Some(&existing_idx) = slug_to_idx.get(dup_slug) {
|
||||||
|
if let Some(existing) = unique.get_mut(existing_idx) {
|
||||||
|
for file in &project.files {
|
||||||
|
if !existing.files.iter().any(|f| f.file_name == file.file_name) {
|
||||||
|
existing.files.push(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for slug in project.slug.values() {
|
||||||
|
seen_slugs.insert(slug.clone());
|
||||||
|
slug_to_idx.insert(slug.clone(), unique.len());
|
||||||
|
}
|
||||||
|
unique.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.projects = unique;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,32 +156,6 @@ impl Project {
|
||||||
self.name.insert(platform, name);
|
self.name.insert(platform, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&mut self, other: Self) {
|
|
||||||
// Merge platform identifiers
|
|
||||||
for (platform, id) in other.id {
|
|
||||||
self.id.entry(platform).or_insert(id);
|
|
||||||
}
|
|
||||||
for (platform, slug) in other.slug {
|
|
||||||
self.slug.entry(platform).or_insert(slug);
|
|
||||||
}
|
|
||||||
for (platform, name) in other.name {
|
|
||||||
self.name.entry(platform).or_insert(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge pakku links
|
|
||||||
self.pakku_links.extend(other.pakku_links);
|
|
||||||
|
|
||||||
// Merge files
|
|
||||||
for file in other.files {
|
|
||||||
if !self.files.iter().any(|f| f.id == file.id) {
|
|
||||||
self.files.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge aliases
|
|
||||||
self.aliases.extend(other.aliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merge this project with another, returning a new combined project.
|
/// Merge this project with another, returning a new combined project.
|
||||||
/// Like Pakku's `Project.plus()`, this is a pure operation that doesn't
|
/// Like Pakku's `Project.plus()`, this is a pure operation that doesn't
|
||||||
/// modify either project.
|
/// modify either project.
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,9 @@ impl PlatformClient for CurseForgePlatform {
|
||||||
query_params.push(("modLoaderTypes", loader_str));
|
query_params.push(("modLoaderTypes", loader_str));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
let has_filters = !query_params.is_empty();
|
||||||
|
|
||||||
|
if has_filters {
|
||||||
let query_string = query_params
|
let query_string = query_params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| format!("{k}={v}"))
|
.map(|(k, v)| format!("{k}={v}"))
|
||||||
|
|
@ -350,6 +352,30 @@ impl PlatformClient for CurseForgePlatform {
|
||||||
.map(|f| Self::convert_file(f, project_id))
|
.map(|f| Self::convert_file(f, project_id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// If server-side filters eliminated all results, retry without them
|
||||||
|
if files.is_empty() && has_filters {
|
||||||
|
let bare_url = format!("{CURSEFORGE_API_BASE}/mods/{project_id}/files");
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(&bare_url)
|
||||||
|
.headers(self.get_headers()?)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(Self::map_http_error(response.status(), project_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: CurseForgeFilesResponse = response.json().await?;
|
||||||
|
return Ok(
|
||||||
|
result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| Self::convert_file(f, project_id))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,16 @@ impl PlatformClient for ModrinthPlatform {
|
||||||
url.push_str(¶ms.join("&"));
|
url.push_str(¶ms.join("&"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.request_project_files_url(&url).await
|
let files = self.request_project_files_url(&url).await?;
|
||||||
|
|
||||||
|
// If server-side filters eliminated all results, retry without them
|
||||||
|
if files.is_empty() && !params.is_empty() {
|
||||||
|
let bare_url =
|
||||||
|
format!("{MODRINTH_API_BASE}/project/{project_id}/version");
|
||||||
|
return self.request_project_files_url(&bare_url).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_project_with_files(
|
async fn request_project_with_files(
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ impl DependencyResolver {
|
||||||
} else {
|
} else {
|
||||||
let mut merged = projects.remove(0);
|
let mut merged = projects.remove(0);
|
||||||
for project in projects {
|
for project in projects {
|
||||||
merged.merge(project);
|
merged = merged.merged(project)?;
|
||||||
}
|
}
|
||||||
Ok(merged)
|
Ok(merged)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue