Phase 0 - Quick fixes:
- Fix catalog entries() return type (removed extra indirection)
- Fix welcome string (mpv-mgr → empeve)
- Fix HEAD detachment on update (branch-aware fast-forward)
- Add fetch_rev with branch detection
Phase 1 - Git model ("rev means rev"):
- Add RevType enum (Commit/Tag/Branch/Default)
- Add UpdateResult enum for update outcomes
- Implement clone_with_rev for proper revision checkout
- Pinned repos (commits/tags) skip auto-update
Phase 2 - Discovery & install fidelity:
- Support init.lua and named entry points for multi-file scripts
- Better asset mapping with prefix matching for configs
- Proactive target directory creation
Phase 3 - UX and quality-of-life:
- Add --verbose flag to status command
- Add 'empeve doctor' diagnostic command
- Improve error messages with actionable hints
Phase 4 - Feature expansion:
- External TOML catalog system (extensible)
- Import --convert-local for local script management
- Lockfile support for reproducible installations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
6.3 KiB
Rust
212 lines
6.3 KiB
Rust
use clap::Parser;
|
|
use colored::Colorize;
|
|
use empeve::{cli::Cli, commands, config::Config, error::Result, paths::{detect_mpv_configs, Paths}};
|
|
use std::io::{self, Write};
|
|
|
|
fn main() {
|
|
if let Err(error) = run() {
|
|
eprintln!("{}: {}", "error".red().bold(), error);
|
|
|
|
// Show hint if available
|
|
if let Some(hint) = error.hint() {
|
|
eprintln!("{}: {}", "hint".yellow(), hint);
|
|
}
|
|
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
|
|
// Check for first-run scenario before executing commands
|
|
check_first_run(&cli.command)?;
|
|
|
|
match cli.command {
|
|
commands::Commands::Add { repo, rev, scripts } => {
|
|
commands::add::execute(&repo, rev, scripts)?;
|
|
}
|
|
commands::Commands::Remove { repo, purge } => {
|
|
commands::remove::execute(&repo, purge)?;
|
|
}
|
|
commands::Commands::Install { force, repos, locked } => {
|
|
commands::install::execute(force, repos, cli.target, locked)?;
|
|
}
|
|
commands::Commands::Update { repos } => {
|
|
commands::update::execute(repos)?;
|
|
}
|
|
commands::Commands::Clean { yes } => {
|
|
commands::clean::execute(yes, cli.target)?;
|
|
}
|
|
commands::Commands::Status { verbose } => {
|
|
commands::status::execute(verbose)?;
|
|
}
|
|
commands::Commands::List { detailed } => {
|
|
commands::list::execute(detailed, cli.target)?;
|
|
}
|
|
commands::Commands::Import { convert_local, script } => {
|
|
commands::import::execute(convert_local, script)?;
|
|
}
|
|
commands::Commands::Browse { category, interactive } => {
|
|
commands::browse::execute(category, interactive)?;
|
|
}
|
|
commands::Commands::Doctor { fix } => {
|
|
commands::doctor::execute(fix)?;
|
|
}
|
|
commands::Commands::Lock => {
|
|
commands::lock::execute()?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if this is the first run and set up targets
|
|
fn check_first_run(command: &commands::Commands) -> Result<()> {
|
|
// Only check for certain commands
|
|
let should_check = matches!(
|
|
command,
|
|
commands::Commands::Status { .. }
|
|
| commands::Commands::List { .. }
|
|
| commands::Commands::Install { .. }
|
|
| commands::Commands::Browse { .. }
|
|
| commands::Commands::Doctor { .. }
|
|
);
|
|
|
|
if !should_check {
|
|
return Ok(());
|
|
}
|
|
|
|
let paths = Paths::new()?;
|
|
|
|
// If config already exists, skip first-run check
|
|
if paths.config_file.exists() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Detect mpv config folders
|
|
let detected = detect_mpv_configs();
|
|
|
|
if detected.is_empty() {
|
|
// No mpv configs found - create default config
|
|
println!("{}", "Welcome to empeve!".green().bold());
|
|
println!();
|
|
println!("{}", "No mpv configuration folders detected.".yellow());
|
|
println!("Creating default configuration...");
|
|
|
|
paths.ensure_directories()?;
|
|
Config::default().save(&paths.config_file)?;
|
|
return Ok(());
|
|
}
|
|
|
|
// Show welcome and detected targets
|
|
println!("{}", "Welcome to empeve!".green().bold());
|
|
println!();
|
|
println!("{}", "Detected mpv configuration folders:".bold());
|
|
println!();
|
|
|
|
for (i, target) in detected.iter().enumerate() {
|
|
let script_info = if target.has_scripts {
|
|
format!("({} scripts)", target.script_count).cyan().to_string()
|
|
} else {
|
|
"(empty)".dimmed().to_string()
|
|
};
|
|
|
|
println!(
|
|
" {:2}. {} {} {}",
|
|
i + 1,
|
|
target.name.cyan(),
|
|
target.path.display().to_string().dimmed(),
|
|
script_info
|
|
);
|
|
}
|
|
|
|
println!();
|
|
println!("{}", "Select targets to manage (e.g., 1 2 3 or 1-3 or 'all'):".bold());
|
|
print!("> ");
|
|
io::stdout().flush()?;
|
|
|
|
let mut input = String::new();
|
|
io::stdin().read_line(&mut input)?;
|
|
let input = input.trim();
|
|
|
|
// Parse selection
|
|
let selected_indices = if input.eq_ignore_ascii_case("all") || input.is_empty() {
|
|
(0..detected.len()).collect::<Vec<_>>()
|
|
} else {
|
|
parse_target_selection(input, detected.len())
|
|
};
|
|
|
|
if selected_indices.is_empty() {
|
|
println!("{}", "No targets selected. Using all detected targets.".yellow());
|
|
}
|
|
|
|
// Create config with selected targets
|
|
let mut config = Config::default();
|
|
|
|
let indices_to_use = if selected_indices.is_empty() {
|
|
(0..detected.len()).collect::<Vec<_>>()
|
|
} else {
|
|
selected_indices
|
|
};
|
|
|
|
for i in indices_to_use {
|
|
if let Some(target) = detected.get(i) {
|
|
let target_config = target.to_target_config();
|
|
if let Err(e) = target_config.ensure_directories() {
|
|
eprintln!(" {}: Could not create directories for {}: {}", "Warning".yellow(), target.name, e);
|
|
}
|
|
config.add_target(target_config);
|
|
println!(" {} {}", "✓".green(), target.name.cyan());
|
|
}
|
|
}
|
|
|
|
// Save config
|
|
paths.ensure_directories()?;
|
|
config.save(&paths.config_file)?;
|
|
|
|
println!();
|
|
println!(
|
|
"{} Configuration saved. Run {} to browse and add scripts.",
|
|
"Done!".green().bold(),
|
|
"empeve browse -i".cyan()
|
|
);
|
|
println!();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Parse target selection input (e.g., "1 2 3", "1-3", "all")
|
|
fn parse_target_selection(input: &str, max: usize) -> Vec<usize> {
|
|
let mut result = Vec::new();
|
|
|
|
for part in input.split(|c| c == ',' || c == ' ') {
|
|
let part = part.trim();
|
|
if part.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
if let Some((start, end)) = part.split_once('-') {
|
|
if let (Ok(s), Ok(e)) = (start.trim().parse::<usize>(), end.trim().parse::<usize>()) {
|
|
for i in s..=e {
|
|
if i >= 1 && i <= max {
|
|
let idx = i - 1; // Convert to 0-based
|
|
if !result.contains(&idx) {
|
|
result.push(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if let Ok(n) = part.parse::<usize>() {
|
|
if n >= 1 && n <= max {
|
|
let idx = n - 1; // Convert to 0-based
|
|
if !result.contains(&idx) {
|
|
result.push(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|