Files
empeve/src/main.rs
vikingowl 6f714e58fa Implement comprehensive improvement roadmap (Phases 0-4)
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>
2025-12-15 03:44:37 +01:00

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
}