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
386
src/cli/commands/add_prj.rs
Normal file
386
src/cli/commands/add_prj.rs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use crate::{
|
||||
error::{PakkerError, Result},
|
||||
model::{
|
||||
Config,
|
||||
LockFile,
|
||||
Project,
|
||||
enums::{ProjectSide, ProjectType, UpdateStrategy},
|
||||
},
|
||||
platform::create_platform,
|
||||
resolver::DependencyResolver,
|
||||
};
|
||||
|
||||
/// Parse a common project argument (slug or ID with optional file ID)
|
||||
/// Format: "input" or "`input#file_id`"
|
||||
fn parse_common_arg(input: &str) -> (String, Option<String>) {
|
||||
if let Some((project_input, file_id)) = input.split_once('#') {
|
||||
(project_input.to_string(), Some(file_id.to_string()))
|
||||
} else {
|
||||
(input.to_string(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a GitHub argument (owner/repo with optional tag)
|
||||
/// Format: "owner/repo" or "owner/repo#tag"
|
||||
fn parse_github_arg(input: &str) -> Result<(String, String, Option<String>)> {
|
||||
let (repo_part, tag) = if let Some((r, t)) = input.split_once('#') {
|
||||
(r, Some(t.to_string()))
|
||||
} else {
|
||||
(input, None)
|
||||
};
|
||||
|
||||
if let Some((owner, repo)) = repo_part.split_once('/') {
|
||||
Ok((owner.to_string(), repo.to_string(), tag))
|
||||
} else {
|
||||
Err(PakkerError::InvalidInput(format!(
|
||||
"Invalid GitHub format '{input}'. Expected: owner/repo or owner/repo#tag"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_loaders(lockfile: &LockFile) -> Vec<String> {
|
||||
lockfile.loaders.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
cf_arg: Option<String>,
|
||||
mr_arg: Option<String>,
|
||||
gh_arg: Option<String>,
|
||||
project_type: Option<ProjectType>,
|
||||
project_side: Option<ProjectSide>,
|
||||
update_strategy: Option<UpdateStrategy>,
|
||||
redistributable: Option<bool>,
|
||||
subpath: Option<String>,
|
||||
aliases: Vec<String>,
|
||||
export: Option<bool>,
|
||||
no_deps: bool,
|
||||
yes: bool,
|
||||
lockfile_path: &Path,
|
||||
config_path: &Path,
|
||||
) -> Result<()> {
|
||||
// At least one platform must be specified
|
||||
if cf_arg.is_none() && mr_arg.is_none() && gh_arg.is_none() {
|
||||
return Err(PakkerError::InvalidInput(
|
||||
"At least one platform must be specified (--cf, --mr, or --gh)"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
log::info!("Adding project with explicit platform specification");
|
||||
|
||||
// Load lockfile
|
||||
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||
|
||||
let mut lockfile = LockFile::load(lockfile_dir)?;
|
||||
|
||||
// Load config if available
|
||||
let _config = Config::load(config_dir).ok();
|
||||
|
||||
// Get MC versions and loaders from lockfile
|
||||
let mc_versions = &lockfile.mc_versions;
|
||||
let loaders = get_loaders(&lockfile);
|
||||
|
||||
// Fetch projects from each specified platform
|
||||
let mut projects_to_merge: Vec<Project> = Vec::new();
|
||||
|
||||
// CurseForge
|
||||
if let Some(cf_input) = cf_arg {
|
||||
log::info!("Fetching from CurseForge: {cf_input}");
|
||||
let (input, file_id) = parse_common_arg(&cf_input);
|
||||
|
||||
let cf_api_key = std::env::var("CURSEFORGE_API_KEY").ok();
|
||||
let platform = create_platform("curseforge", cf_api_key)?;
|
||||
|
||||
let mut project = platform
|
||||
.request_project_with_files(&input, mc_versions, &loaders)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
PakkerError::ProjectNotFound(format!(
|
||||
"CurseForge project '{input}': {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// If file_id specified, filter to that file
|
||||
if let Some(fid) = file_id {
|
||||
project.files.retain(|f| f.id == fid);
|
||||
if project.files.is_empty() {
|
||||
return Err(PakkerError::FileSelectionError(format!(
|
||||
"File ID '{fid}' not found for CurseForge project '{input}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
projects_to_merge.push(project);
|
||||
}
|
||||
|
||||
// Modrinth
|
||||
if let Some(mr_input) = mr_arg {
|
||||
log::info!("Fetching from Modrinth: {mr_input}");
|
||||
let (input, file_id) = parse_common_arg(&mr_input);
|
||||
|
||||
let platform = create_platform("modrinth", None)?;
|
||||
|
||||
let mut project = platform
|
||||
.request_project_with_files(&input, mc_versions, &loaders)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
PakkerError::ProjectNotFound(format!("Modrinth project '{input}': {e}"))
|
||||
})?;
|
||||
|
||||
// If file_id specified, filter to that file
|
||||
if let Some(fid) = file_id {
|
||||
project.files.retain(|f| f.id == fid);
|
||||
if project.files.is_empty() {
|
||||
return Err(PakkerError::FileSelectionError(format!(
|
||||
"File ID '{fid}' not found for Modrinth project '{input}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
projects_to_merge.push(project);
|
||||
}
|
||||
|
||||
// GitHub
|
||||
if let Some(gh_input) = gh_arg {
|
||||
log::info!("Fetching from GitHub: {gh_input}");
|
||||
let (owner, repo, tag) = parse_github_arg(&gh_input)?;
|
||||
|
||||
let gh_token = std::env::var("GITHUB_TOKEN").ok();
|
||||
let platform = create_platform("github", gh_token)?;
|
||||
|
||||
let repo_path = format!("{owner}/{repo}");
|
||||
let mut project = platform
|
||||
.request_project_with_files(&repo_path, mc_versions, &loaders)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
PakkerError::ProjectNotFound(format!(
|
||||
"GitHub repository '{owner}/{repo}': {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// If tag specified, filter to that tag
|
||||
if let Some(t) = tag {
|
||||
project.files.retain(|f| f.id == t);
|
||||
if project.files.is_empty() {
|
||||
return Err(PakkerError::FileSelectionError(format!(
|
||||
"Tag '{t}' not found for GitHub repository '{owner}/{repo}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
projects_to_merge.push(project);
|
||||
}
|
||||
|
||||
// Merge all fetched projects into one
|
||||
if projects_to_merge.is_empty() {
|
||||
return Err(PakkerError::ProjectNotFound(
|
||||
"No projects could be fetched from specified platforms".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut combined_project = projects_to_merge.remove(0);
|
||||
for project in projects_to_merge {
|
||||
combined_project.merge(project);
|
||||
}
|
||||
|
||||
// Apply user-specified properties
|
||||
if let Some(pt) = project_type {
|
||||
combined_project.r#type = pt;
|
||||
}
|
||||
if let Some(ps) = project_side {
|
||||
combined_project.side = ps;
|
||||
}
|
||||
if let Some(us) = update_strategy {
|
||||
combined_project.update_strategy = us;
|
||||
}
|
||||
if let Some(r) = redistributable {
|
||||
combined_project.redistributable = r;
|
||||
}
|
||||
if let Some(sp) = subpath {
|
||||
combined_project.subpath = Some(sp);
|
||||
}
|
||||
if let Some(e) = export {
|
||||
combined_project.export = e;
|
||||
}
|
||||
|
||||
// Add aliases
|
||||
for alias in aliases {
|
||||
combined_project.aliases.insert(alias);
|
||||
}
|
||||
|
||||
// Check if project already exists
|
||||
let existing_pos = lockfile.projects.iter().position(|p| {
|
||||
// Check if any platform ID matches
|
||||
combined_project.id.iter().any(|(platform, id)| {
|
||||
p.id
|
||||
.get(platform)
|
||||
.is_some_and(|existing_id| existing_id == id)
|
||||
})
|
||||
});
|
||||
|
||||
let project_name = combined_project.get_name();
|
||||
|
||||
if let Some(pos) = existing_pos {
|
||||
let existing_project = &lockfile.projects[pos];
|
||||
let existing_name = existing_project.get_name();
|
||||
|
||||
if !yes {
|
||||
let prompt_msg = format!(
|
||||
"Project '{existing_name}' already exists. Replace with \
|
||||
'{project_name}'?"
|
||||
);
|
||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, false)? {
|
||||
log::info!("Operation cancelled by user");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Replacing existing project: {existing_name}");
|
||||
lockfile.projects[pos] = combined_project.clone();
|
||||
println!("✓ Replaced '{existing_name}' with '{project_name}'");
|
||||
} else {
|
||||
if !yes {
|
||||
let prompt_msg = format!("Add project '{project_name}'?");
|
||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
||||
log::info!("Operation cancelled by user");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
lockfile.add_project(combined_project.clone());
|
||||
println!("✓ Added '{project_name}'");
|
||||
}
|
||||
|
||||
// Resolve dependencies unless --no-deps is specified
|
||||
if !no_deps {
|
||||
log::info!("Resolving dependencies...");
|
||||
|
||||
let platforms = create_all_platforms()?;
|
||||
let mut resolver = DependencyResolver::new();
|
||||
|
||||
let deps = resolver
|
||||
.resolve(&mut combined_project, &mut lockfile, &platforms)
|
||||
.await?;
|
||||
|
||||
for dep in deps {
|
||||
// Skip if already in lockfile
|
||||
if lockfile.projects.iter().any(|p| {
|
||||
dep.id.iter().any(|(platform, id)| {
|
||||
p.id
|
||||
.get(platform)
|
||||
.is_some_and(|existing_id| existing_id == id)
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dep_name = dep.get_name();
|
||||
|
||||
// Prompt user for confirmation unless --yes flag is set
|
||||
if !yes {
|
||||
let prompt_msg =
|
||||
format!("Add dependency '{dep_name}' required by '{project_name}'?");
|
||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
||||
log::info!("Skipping dependency: {dep_name}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Adding dependency: {dep_name}");
|
||||
lockfile.add_project(dep);
|
||||
println!(" ✓ Added dependency '{dep_name}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Save lockfile
|
||||
lockfile.save(lockfile_dir)?;
|
||||
|
||||
log::info!("Successfully completed add-prj operation");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_all_platforms()
|
||||
-> Result<HashMap<String, Box<dyn crate::platform::PlatformClient>>> {
|
||||
let mut platforms = HashMap::new();
|
||||
|
||||
if let Ok(platform) = create_platform("modrinth", None) {
|
||||
platforms.insert("modrinth".to_string(), platform);
|
||||
}
|
||||
if let Ok(platform) =
|
||||
create_platform("curseforge", std::env::var("CURSEFORGE_API_KEY").ok())
|
||||
{
|
||||
platforms.insert("curseforge".to_string(), platform);
|
||||
}
|
||||
if let Ok(platform) =
|
||||
create_platform("github", std::env::var("GITHUB_TOKEN").ok())
|
||||
{
|
||||
platforms.insert("github".to_string(), platform);
|
||||
}
|
||||
|
||||
Ok(platforms)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_common_arg_without_file_id() {
|
||||
let (input, file_id) = parse_common_arg("fabric-api");
|
||||
assert_eq!(input, "fabric-api");
|
||||
assert_eq!(file_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_common_arg_with_file_id() {
|
||||
let (input, file_id) = parse_common_arg("fabric-api#12345");
|
||||
assert_eq!(input, "fabric-api");
|
||||
assert_eq!(file_id, Some("12345".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_github_arg_owner_repo() {
|
||||
let result = parse_github_arg("FabricMC/fabric");
|
||||
assert!(result.is_ok());
|
||||
let (owner, repo, tag) = result.unwrap();
|
||||
assert_eq!(owner, "FabricMC");
|
||||
assert_eq!(repo, "fabric");
|
||||
assert_eq!(tag, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_github_arg_with_tag() {
|
||||
let result = parse_github_arg("FabricMC/fabric#v0.15.0");
|
||||
assert!(result.is_ok());
|
||||
let (owner, repo, tag) = result.unwrap();
|
||||
assert_eq!(owner, "FabricMC");
|
||||
assert_eq!(repo, "fabric");
|
||||
assert_eq!(tag, Some("v0.15.0".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_github_arg_invalid() {
|
||||
let result = parse_github_arg("invalid-format");
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Invalid GitHub format")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_github_arg_missing_repo() {
|
||||
let result = parse_github_arg("FabricMC/");
|
||||
assert!(result.is_ok());
|
||||
let (owner, repo, tag) = result.unwrap();
|
||||
assert_eq!(owner, "FabricMC");
|
||||
assert_eq!(repo, "");
|
||||
assert_eq!(tag, None);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue