diff --git a/src/cli/commands/cfg.rs b/src/cli/commands/cfg.rs index 0e1068a..6d42b0d 100644 --- a/src/cli/commands/cfg.rs +++ b/src/cli/commands/cfg.rs @@ -2,7 +2,11 @@ use std::path::Path; use yansi::Paint; -use crate::{error::Result, model::config::Config}; +use crate::{ + error::Result, + model::config::Config, + ui_utils::prompt_input_optional, +}; pub fn execute( config_path: &Path, @@ -85,11 +89,50 @@ pub fn execute( } if !changed { - eprintln!( + // Interactive mode: prompt for values if none were specified + println!( "{}", - "No changes specified. Use --help for options.".yellow() + "No changes specified. Enter values interactively (press Enter to skip):" + .yellow() ); - return Ok(()); + println!(); + + // Prompt for each configurable field + if let Ok(Some(new_name)) = prompt_input_optional(" Name") { + config.name = new_name.clone(); + println!("{}", format!(" ✓ 'name' set to '{new_name}'").green()); + changed = true; + } + + if let Ok(Some(new_version)) = prompt_input_optional(" Version") { + config.version = new_version.clone(); + println!( + "{}", + format!(" ✓ 'version' set to '{new_version}'").green() + ); + changed = true; + } + + if let Ok(Some(new_description)) = prompt_input_optional(" Description") { + config.description = Some(new_description.clone()); + println!( + "{}", + format!(" ✓ 'description' set to '{new_description}'").green() + ); + changed = true; + } + + if let Ok(Some(new_author)) = prompt_input_optional(" Author") { + config.author = Some(new_author.clone()); + println!("{}", format!(" ✓ 'author' set to '{new_author}'").green()); + changed = true; + } + + if !changed { + println!(); + println!("{}", "No changes made.".dim()); + return Ok(()); + } } // Config::save expects directory path, not file path diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index b79fb6a..d0da1ed 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -3,7 +3,13 @@ use std::{collections::HashMap, path::Path}; use crate::{ cli::InitArgs, error::PakkerError, - model::{Config, LockFile, Target}, + model::{Config, LockFile, ResolvedCredentials, Target}, + ui_utils::{ + prompt_curseforge_api_key, + prompt_input, + prompt_select, + prompt_yes_no, + }, }; pub async fn execute( @@ -17,8 +23,42 @@ pub async fn execute( )); } - let target = args.target.as_str(); - let target_enum = match target { + // Interactive mode: prompt for values not provided via CLI and --yes not set + let is_interactive = !args.yes && args.name.is_none(); + + // Get modpack name + let name = if let Some(name) = args.name.clone() { + name + } else if is_interactive { + prompt_input("Modpack name", Some("My Modpack")) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))? + } else { + "My Modpack".to_string() + }; + + // Get modpack version + let version = if let Some(version) = args.version.clone() { + version + } else if is_interactive { + prompt_input("Version", Some("1.0.0")) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))? + } else { + "1.0.0".to_string() + }; + + // Get target platform + let target = if let Some(target) = args.target.clone() { + target + } else if is_interactive { + let targets = ["multiplatform", "curseforge", "modrinth"]; + let idx = prompt_select("Target platform", &targets) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))?; + targets[idx].to_string() + } else { + "multiplatform".to_string() + }; + + let target_enum = match target.as_str() { "curseforge" => Target::CurseForge, "modrinth" => Target::Modrinth, "multiplatform" => Target::Multiplatform, @@ -29,17 +69,56 @@ pub async fn execute( }, }; - let mc_versions = vec![args.mc_version]; + // Get Minecraft versions (supports multiple) + let mc_versions = if let Some(versions) = args.mc_versions.clone() { + versions + } else if is_interactive { + let input = + prompt_input("Minecraft versions (space-separated)", Some("1.20.1")) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))?; + input.split_whitespace().map(String::from).collect() + } else { + vec!["1.20.1".to_string()] + }; - let mut loaders = HashMap::new(); - loaders.insert(args.loader, args.loader_version); + // Get mod loaders (supports multiple in name=version format) + let loaders: HashMap = if let Some(loader_strs) = args.loaders + { + let mut map = HashMap::new(); + for loader_str in loader_strs { + let parts: Vec<&str> = loader_str.splitn(2, '=').collect(); + if parts.len() == 2 { + map.insert(parts[0].to_string(), parts[1].to_string()); + } else { + // If no version specified, use "latest" + map.insert(loader_str, "latest".to_string()); + } + } + map + } else if is_interactive { + let loader_options = ["fabric", "forge", "neoforge", "quilt"]; + let idx = prompt_select("Mod loader", &loader_options) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))?; + let loader = loader_options[idx].to_string(); + + let loader_version = prompt_input("Loader version", Some("latest")) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))?; + + let mut map = HashMap::new(); + map.insert(loader, loader_version); + map + } else { + let mut map = HashMap::new(); + map.insert("fabric".to_string(), "latest".to_string()); + map + }; let lockfile = LockFile { target: Some(target_enum), mc_versions, loaders, projects: Vec::new(), - lockfile_version: 1, + lockfile_version: 2, }; // Save expects directory path, so get parent directory @@ -47,12 +126,8 @@ pub async fn execute( lockfile.save(lockfile_dir)?; let config = Config { - name: args - .name - .unwrap_or_else(|| "My Modpack".to_string()), - version: args - .version - .unwrap_or_else(|| "1.0.0".to_string()), + name: name.clone(), + version: version.clone(), description: None, author: None, overrides: vec!["overrides".to_string()], @@ -67,6 +142,49 @@ pub async fn execute( let config_dir = config_path.parent().unwrap_or(Path::new(".")); config.save(config_dir)?; - println!("Initialized new modpack with target: {target}"); + println!("Initialized new modpack '{name}' v{version}"); + println!(" Target: {target}"); + println!(" Minecraft: {}", lockfile.mc_versions.join(", ")); + println!( + " Loaders: {}", + lockfile + .loaders + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(", ") + ); + + // Check if CurseForge API key is needed and prompt if interactive + if is_interactive && (target == "curseforge" || target == "multiplatform") { + let credentials = ResolvedCredentials::load().ok(); + let has_cf_key = credentials + .as_ref() + .is_some_and(|c| c.curseforge_api_key().is_some()); + + if !has_cf_key { + println!(); + if prompt_yes_no("Would you like to set up CurseForge API key now?", true) + .map_err(|e| PakkerError::InvalidInput(e.to_string()))? + && let Ok(Some(api_key)) = prompt_curseforge_api_key() + { + // Save to credentials file + let creds_path = std::env::var("HOME").map_or_else( + |_| Path::new(".pakku").to_path_buf(), + |h| Path::new(&h).join(".pakku"), + ); + + std::fs::create_dir_all(&creds_path).ok(); + + let creds_file = creds_path.join("credentials"); + let content = + format!("# Pakku/Pakker credentials\nCURSEFORGE_API_KEY={api_key}\n"); + if std::fs::write(&creds_file, content).is_ok() { + println!("CurseForge API key saved to ~/.pakku/credentials"); + } + } + } + } + Ok(()) }