initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ife1391ed23a1e7f388b1b5eca90b9ea76a6a6964
This commit is contained in:
commit
ef28bdaeb4
63 changed files with 17292 additions and 0 deletions
370
src/cli/commands/status.rs
Normal file
370
src/cli/commands/status.rs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use tokio::sync::Semaphore;
|
||||
use yansi::Paint;
|
||||
|
||||
use crate::{
|
||||
error::Result,
|
||||
model::{Config, LockFile, Project},
|
||||
platform::create_platform,
|
||||
};
|
||||
|
||||
pub async fn execute(
|
||||
parallel: bool,
|
||||
lockfile_path: &Path,
|
||||
config_path: &Path,
|
||||
) -> Result<()> {
|
||||
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||
|
||||
let lockfile = LockFile::load(lockfile_dir)?;
|
||||
let config = Config::load(config_dir)?;
|
||||
|
||||
// Display modpack metadata
|
||||
display_modpack_info(&lockfile, &config);
|
||||
println!();
|
||||
|
||||
// Check for updates (sequential or parallel)
|
||||
let (updates, errors) = if parallel {
|
||||
check_updates_parallel(&lockfile).await?
|
||||
} else {
|
||||
check_updates_sequential(&lockfile).await?
|
||||
};
|
||||
|
||||
// Display results
|
||||
display_update_results(&updates);
|
||||
|
||||
// Display errors if any
|
||||
if !errors.is_empty() {
|
||||
println!();
|
||||
println!("{}", "Errors encountered:".red());
|
||||
for (project, error) in &errors {
|
||||
println!(" - {}: {}", project.yellow(), error.red());
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt to update if there are updates available
|
||||
if !updates.is_empty() {
|
||||
println!();
|
||||
if crate::ui_utils::prompt_yes_no("Update now?", false)? {
|
||||
// Call update command programmatically (update all projects)
|
||||
let update_args = crate::cli::UpdateArgs {
|
||||
inputs: vec![],
|
||||
yes: true, // Auto-yes for status command
|
||||
};
|
||||
crate::cli::commands::update::execute(
|
||||
update_args,
|
||||
lockfile_path,
|
||||
config_path,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_modpack_info(lockfile: &LockFile, config: &Config) {
|
||||
let author = config.author.as_deref().unwrap_or("Unknown");
|
||||
println!(
|
||||
"Managing {} modpack, version {}, by {}",
|
||||
config.name.cyan(),
|
||||
config.version.cyan(),
|
||||
author.cyan()
|
||||
);
|
||||
|
||||
let mc_versions = lockfile.mc_versions.join(", ");
|
||||
let loaders: Vec<String> = lockfile
|
||||
.loaders
|
||||
.iter()
|
||||
.map(|(loader, version)| format!("{loader}-{version}"))
|
||||
.collect();
|
||||
let loaders_str = loaders.join(", ");
|
||||
|
||||
println!(
|
||||
"on Minecraft version {}, loader {}, targeting platform {:?}.",
|
||||
mc_versions.cyan(),
|
||||
loaders_str.cyan(),
|
||||
lockfile.target
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProjectUpdate {
|
||||
slug: HashMap<String, String>,
|
||||
name: String,
|
||||
project_type: String,
|
||||
side: String,
|
||||
file_updates: Vec<FileUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileUpdate {
|
||||
platform: String,
|
||||
old_filename: String,
|
||||
new_filename: String,
|
||||
}
|
||||
|
||||
async fn check_updates_sequential(
|
||||
lockfile: &LockFile,
|
||||
) -> Result<(Vec<ProjectUpdate>, Vec<(String, String)>)> {
|
||||
let total = lockfile.projects.len();
|
||||
let mut updates = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// Create progress bar
|
||||
let pb = ProgressBar::new(total as u64);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb.set_message("Checking for updates...");
|
||||
|
||||
for project in &lockfile.projects {
|
||||
let project_name = project
|
||||
.name
|
||||
.values()
|
||||
.next()
|
||||
.unwrap_or(&"Unknown".to_string())
|
||||
.clone();
|
||||
pb.set_message(format!("Checking {project_name}..."));
|
||||
|
||||
match check_project_update(project, lockfile).await {
|
||||
Ok(update_opt) => {
|
||||
if let Some(update) = update_opt {
|
||||
updates.push(update);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
errors.push((project_name.clone(), e.to_string()));
|
||||
},
|
||||
}
|
||||
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
pb.finish_with_message(format!("Checked {total} projects"));
|
||||
println!(); // Add blank line after progress bar
|
||||
|
||||
Ok((updates, errors))
|
||||
}
|
||||
|
||||
async fn check_updates_parallel(
|
||||
lockfile: &LockFile,
|
||||
) -> Result<(Vec<ProjectUpdate>, Vec<(String, String)>)> {
|
||||
let total = lockfile.projects.len();
|
||||
let semaphore = Arc::new(Semaphore::new(10));
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
// Create progress bar
|
||||
let pb = Arc::new(ProgressBar::new(total as u64));
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb.set_message("Checking for updates (parallel)...");
|
||||
|
||||
for project in &lockfile.projects {
|
||||
let project = project.clone();
|
||||
let sem = semaphore.clone();
|
||||
let pb_clone = pb.clone();
|
||||
let lockfile_clone = lockfile.clone();
|
||||
|
||||
futures.push(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
let result = check_project_update(&project, &lockfile_clone).await;
|
||||
pb_clone.inc(1);
|
||||
(project, result)
|
||||
});
|
||||
}
|
||||
|
||||
let mut updates = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
while let Some((project, result)) = futures.next().await {
|
||||
match result {
|
||||
Ok(update_opt) => {
|
||||
if let Some(update) = update_opt {
|
||||
updates.push(update);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let project_name = project
|
||||
.name
|
||||
.values()
|
||||
.next()
|
||||
.unwrap_or(&"Unknown".to_string())
|
||||
.clone();
|
||||
errors.push((project_name, e.to_string()));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pb.finish_with_message(format!("Checked {total} projects"));
|
||||
println!(); // Add blank line after progress bar
|
||||
|
||||
Ok((updates, errors))
|
||||
}
|
||||
|
||||
async fn check_project_update(
|
||||
project: &Project,
|
||||
lockfile: &LockFile,
|
||||
) -> Result<Option<ProjectUpdate>> {
|
||||
// Get primary slug
|
||||
let slug = project
|
||||
.slug
|
||||
.values()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
crate::error::PakkerError::InvalidProject("No slug found".to_string())
|
||||
})?
|
||||
.clone();
|
||||
|
||||
// Try each platform in project
|
||||
for platform_name in project.id.keys() {
|
||||
let api_key = get_api_key(platform_name);
|
||||
let platform = match create_platform(platform_name, api_key) {
|
||||
Ok(p) => p,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let loaders: Vec<String> = lockfile.loaders.keys().cloned().collect();
|
||||
|
||||
match platform
|
||||
.request_project_with_files(&slug, &lockfile.mc_versions, &loaders)
|
||||
.await
|
||||
{
|
||||
Ok(updated_project) => {
|
||||
// Compare files to detect updates
|
||||
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); // No updates
|
||||
},
|
||||
Err(_) => {
|
||||
// Try next platform
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err(crate::error::PakkerError::PlatformApiError(
|
||||
"Failed to check for updates on any platform".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn detect_file_updates(
|
||||
current: &Project,
|
||||
updated: &Project,
|
||||
) -> Vec<FileUpdate> {
|
||||
let mut updates = Vec::new();
|
||||
|
||||
for old_file in ¤t.files {
|
||||
if let Some(new_file) = updated
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.file_type == old_file.file_type)
|
||||
{
|
||||
// Check if file ID changed (indicates update)
|
||||
if new_file.id != old_file.id {
|
||||
updates.push(FileUpdate {
|
||||
platform: old_file.file_type.clone(),
|
||||
old_filename: old_file.file_name.clone(),
|
||||
new_filename: new_file.file_name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updates
|
||||
}
|
||||
|
||||
fn display_update_results(updates: &[ProjectUpdate]) {
|
||||
if updates.is_empty() {
|
||||
println!("{}", "✓ All projects are up to date".green());
|
||||
return;
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "📦 Updates Available:".cyan().bold());
|
||||
println!();
|
||||
|
||||
for update in updates {
|
||||
// Create hyperlink for project name using ui_utils
|
||||
let project_url = if let Some((platform, slug)) = update.slug.iter().next()
|
||||
{
|
||||
match platform.as_str() {
|
||||
"modrinth" => crate::ui_utils::modrinth_project_url(slug),
|
||||
"curseforge" => crate::ui_utils::curseforge_project_url(slug),
|
||||
_ => String::new(),
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if project_url.is_empty() {
|
||||
println!(
|
||||
"{} ({}, {})",
|
||||
update.name.yellow(),
|
||||
update.project_type,
|
||||
update.side
|
||||
);
|
||||
} else {
|
||||
let hyperlinked = crate::ui_utils::hyperlink(
|
||||
&project_url,
|
||||
&update.name.yellow().to_string(),
|
||||
);
|
||||
println!("{} ({}, {})", hyperlinked, update.project_type, update.side);
|
||||
}
|
||||
|
||||
for file_update in &update.file_updates {
|
||||
println!(
|
||||
" • {}: {} → {}",
|
||||
file_update.platform.cyan(),
|
||||
file_update.old_filename.dim(),
|
||||
file_update.new_filename.green()
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("{} project(s) need updates", updates.len()).yellow()
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_project_display_name(project: &Project) -> String {
|
||||
project
|
||||
.name
|
||||
.values()
|
||||
.next()
|
||||
.or_else(|| project.slug.values().next())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
}
|
||||
|
||||
fn get_api_key(platform: &str) -> Option<String> {
|
||||
match platform {
|
||||
"modrinth" => std::env::var("MODRINTH_TOKEN").ok(),
|
||||
"curseforge" => std::env::var("CURSEFORGE_API_KEY").ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue