cli: make init and cfg interactive by default

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7596bb9913a8d98133bdf3c531241bf06a6a6964
This commit is contained in:
raf 2026-02-12 23:20:30 +03:00
commit cce952698a
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 179 additions and 18 deletions

View file

@ -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

View file

@ -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<String, String> = 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::<Vec<_>>()
.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(())
}