Compare commits
No commits in common. "notashelf/push-kyklmwpmqkqx" and "main" have entirely different histories.
notashelf/
...
main
14 changed files with 128 additions and 831 deletions
|
|
@ -9,11 +9,7 @@ use crate::model::{
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(name = "pakker")]
|
#[clap(name = "pakker")]
|
||||||
#[clap(
|
#[clap(about = "A multiplatform modpack manager for Minecraft", long_about = None)]
|
||||||
about = "A multiplatform modpack manager for Minecraft",
|
|
||||||
long_about = None,
|
|
||||||
version
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Enable verbose output (-v for info, -vv for debug, -vvv for trace)
|
/// Enable verbose output (-v for info, -vv for debug, -vvv for trace)
|
||||||
#[clap(short, long, action = clap::ArgAction::Count)]
|
#[clap(short, long, action = clap::ArgAction::Count)]
|
||||||
|
|
@ -92,9 +88,6 @@ pub enum Commands {
|
||||||
|
|
||||||
/// Manage fork configuration
|
/// Manage fork configuration
|
||||||
Fork(ForkArgs),
|
Fork(ForkArgs),
|
||||||
|
|
||||||
/// Check and repair lockfile integrity
|
|
||||||
Lockfile(LockfileArgs),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -224,33 +217,17 @@ pub struct UpdateArgs {
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct LsArgs {
|
pub struct LsArgs {
|
||||||
/// Show all optional columns (equivalent to enabling all --show-* flags)
|
/// Show detailed information
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
pub detailed: bool,
|
pub detailed: bool,
|
||||||
|
|
||||||
/// Show project type column (mod, resourcepack, shader, etc.)
|
/// Add update information for projects
|
||||||
#[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)]
|
||||||
|
|
@ -646,24 +623,3 @@ 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 = merged.merged(project)?;
|
merged.merge(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 = combined_project.merged(project)?;
|
combined_project.merge(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply user-specified properties
|
// Apply user-specified properties
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,8 @@ 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";
|
||||||
|
|
@ -692,54 +689,6 @@ 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)?;
|
||||||
|
|
@ -816,24 +765,7 @@ fn execute_promote(projects: &[String]) -> Result<(), PakkerError> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut project = project.clone();
|
local_lockfile.add_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);
|
||||||
|
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use yansi::Paint;
|
|
||||||
|
|
||||||
use crate::{cli::LsArgs, error::Result, model::LockFile};
|
use crate::{cli::LsArgs, error::Result, model::LockFile};
|
||||||
|
|
||||||
const COL_GAP: usize = 3; // spaces between columns
|
/// Truncate a name to fit within `max_len` characters, adding "..." if
|
||||||
|
/// 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,231 +14,80 @@ 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)?;
|
||||||
|
|
||||||
if lockfile.projects.is_empty() {
|
if lockfile.projects.is_empty() {
|
||||||
println!("{}", "No projects installed".yellow());
|
println!("No projects installed");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let project_count = lockfile.projects.len();
|
println!("Installed projects ({}):", 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!(
|
|
||||||
"{} ({})",
|
|
||||||
"Installed projects".bold(),
|
|
||||||
project_count.to_string().cyan().bold()
|
|
||||||
);
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Print header
|
// Calculate max name length for alignment
|
||||||
let header_line: Vec<String> = header_cols
|
let max_name_len = args.name_max_length.unwrap_or_else(|| {
|
||||||
.iter()
|
lockfile
|
||||||
.map(|(text, width)| {
|
.projects
|
||||||
if text == &header_cols.last().unwrap().0 {
|
.iter()
|
||||||
format!("{text}")
|
.map(|p| p.get_name().len())
|
||||||
} else {
|
.max()
|
||||||
format!("{:<width$}", text, width = *width)
|
.unwrap_or(20)
|
||||||
}
|
.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 {
|
||||||
let name = truncate_name(&project.get_name(), widths.name.min(50));
|
// Check for version mismatch across providers
|
||||||
let warning_marker = if !project.versions_match_across_providers() {
|
let version_warning = 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 {
|
|
||||||
""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let file_name = project
|
if args.detailed {
|
||||||
.files
|
let id = project.pakku_id.as_deref().unwrap_or("unknown");
|
||||||
.first()
|
let name = truncate_name(&project.get_name(), max_name_len);
|
||||||
.map(|f| f.file_name.as_str())
|
println!(" {name} ({id}){version_warning}");
|
||||||
.unwrap_or("-");
|
println!(" Type: {:?}", project.r#type);
|
||||||
|
println!(" Side: {:?}", project.side);
|
||||||
|
|
||||||
let name_display = format!("{name}{warning_marker}");
|
if let Some(file) = project.files.first() {
|
||||||
print!(" ");
|
println!(" File: {}", file.file_name);
|
||||||
if warning_marker.is_empty() {
|
println!(
|
||||||
print!(
|
" Version: {} ({})",
|
||||||
"{}",
|
file.release_type, file.date_published
|
||||||
format!("{:<width$}", name_display, width = widths.name).bold()
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
// Show version details if there's a mismatch
|
||||||
|
if !version_warning.is_empty() {
|
||||||
|
println!(" Provider versions:");
|
||||||
|
for file in &project.files {
|
||||||
|
println!(" {}: {}", file.file_type, file.file_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !project.pakku_links.is_empty() {
|
||||||
|
println!(" Dependencies: {}", project.pakku_links.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
} else {
|
} else {
|
||||||
print!(
|
let name = truncate_name(&project.get_name(), max_name_len);
|
||||||
"{}",
|
let file_info = project
|
||||||
format!("{:<width$}", name_display, width = widths.name).yellow()
|
.files
|
||||||
);
|
.first()
|
||||||
}
|
.map(|f| format!(" ({})", f.file_name))
|
||||||
print!("{}", " ".repeat(COL_GAP));
|
.unwrap_or_default();
|
||||||
print!(
|
|
||||||
"{}",
|
|
||||||
format!("{:<width$}", file_name, width = widths.file).dim()
|
|
||||||
);
|
|
||||||
|
|
||||||
if show_type {
|
println!(" {name}{file_info}{version_warning}");
|
||||||
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,7 +13,6 @@ 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,88 +255,49 @@ async fn check_project_update(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
lockfile: &LockFile,
|
lockfile: &LockFile,
|
||||||
) -> Result<Option<ProjectUpdate>> {
|
) -> Result<Option<ProjectUpdate>> {
|
||||||
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
// Get primary slug
|
||||||
let mc_versions = &lockfile.mc_versions;
|
let slug = project
|
||||||
|
.slug
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::error::PakkerError::InvalidProject("No slug found".to_string())
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
let mut errors: Vec<String> = Vec::new();
|
// Try each platform in project
|
||||||
|
|
||||||
for (platform_name, platform_slug) in &project.slug {
|
|
||||||
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_slug, 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}: {e}"));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also try platforms that have IDs but no slugs (uncommon edge case)
|
|
||||||
for platform_name in project.id.keys() {
|
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 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
match platform
|
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
||||||
.request_project_with_files(platform_id, mc_versions, &loaders)
|
|
||||||
|
if let Ok(updated_project) = platform
|
||||||
|
.request_project_with_files(&slug, &lockfile.mc_versions, &loaders)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(updated_project) => {
|
// Compare files to detect updates
|
||||||
let file_updates = detect_file_updates(project, &updated_project);
|
let file_updates = detect_file_updates(project, &updated_project);
|
||||||
if !file_updates.is_empty() {
|
|
||||||
return Ok(Some(ProjectUpdate {
|
if !file_updates.is_empty() {
|
||||||
slug: project.slug.clone(),
|
return Ok(Some(ProjectUpdate {
|
||||||
name: project.name.values().next().cloned().unwrap_or_default(),
|
slug: project.slug.clone(),
|
||||||
project_type: format!("{:?}", project.r#type),
|
name: project.name.values().next().cloned().unwrap_or_default(),
|
||||||
side: format!("{:?}", project.side),
|
project_type: format!("{:?}", project.r#type),
|
||||||
file_updates,
|
side: format!("{:?}", project.side),
|
||||||
}));
|
file_updates,
|
||||||
}
|
}));
|
||||||
return Ok(None);
|
}
|
||||||
},
|
|
||||||
Err(e) => {
|
return Ok(None); // No updates
|
||||||
errors.push(format!("{platform_name}(by id): {e}"));
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let error_detail = if errors.is_empty() {
|
Err(crate::error::PakkerError::PlatformApiError(
|
||||||
"no platform slugs or IDs available".to_string()
|
"Failed to check for updates on any platform".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,9 +238,5 @@ 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,7 +1,4 @@
|
||||||
use std::{
|
use std::{collections::HashMap, path::Path};
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -659,7 +656,6 @@ impl LockFile {
|
||||||
lockfile.validate()?;
|
lockfile.validate()?;
|
||||||
}
|
}
|
||||||
lockfile.sort_projects();
|
lockfile.sort_projects();
|
||||||
lockfile.deduplicate_projects();
|
|
||||||
|
|
||||||
Ok(lockfile)
|
Ok(lockfile)
|
||||||
}
|
}
|
||||||
|
|
@ -758,84 +754,7 @@ 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,6 +156,32 @@ 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,9 +322,7 @@ impl PlatformClient for CurseForgePlatform {
|
||||||
query_params.push(("modLoaderTypes", loader_str));
|
query_params.push(("modLoaderTypes", loader_str));
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_filters = !query_params.is_empty();
|
if !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}"))
|
||||||
|
|
@ -352,30 +350,6 @@ 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,16 +222,7 @@ impl PlatformClient for ModrinthPlatform {
|
||||||
url.push_str(¶ms.join("&"));
|
url.push_str(¶ms.join("&"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let files = self.request_project_files_url(&url).await?;
|
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 = merged.merged(project)?;
|
merged.merge(project);
|
||||||
}
|
}
|
||||||
Ok(merged)
|
Ok(merged)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue