warren-cli 0.1.6

Install any CLI tool unlimited times. Every instance is its own world.
use anyhow::Result;
use chrono::Utc;

use crate::config::WarrenConfig;
use crate::instance::InstanceMetadata;
use crate::ui::theme::Theme;

pub async fn execute(config: &WarrenConfig, theme: &Theme) -> Result<()> {
    let instances_dir = &config.paths.instances_dir;
    if !instances_dir.exists() {
        theme.dim("No instances found.");
        theme.dim("Run 'warren dig <source> --as <alias>' to get started.");
        return Ok(());
    }
    let mut entries: Vec<InstanceMetadata> = Vec::new();
    for entry in std::fs::read_dir(instances_dir)? {
        let entry = entry?;
        if !entry.path().is_dir() {
            continue;
        }
        let metadata_path = entry.path().join("metadata.toml");
        if metadata_path.exists()
            && let Ok(meta) = InstanceMetadata::load(&metadata_path)
        {
            entries.push(meta);
        }
    }
    if entries.is_empty() {
        theme.dim("No instances found.");
        theme.dim("Run 'warren dig <source> --as <alias>' to get started.");
        return Ok(());
    }
    entries.sort_by(|a, b| a.instance.alias.cmp(&b.instance.alias));
    theme.blank();
    let header = console::Style::new().bold().dim();
    eprintln!(
        "  {:<20}{:<14}{:<12}{:<8}{}",
        header.apply_to("ALIAS"),
        header.apply_to("APP"),
        header.apply_to("VERSION"),
        header.apply_to("KIND"),
        header.apply_to("CREATED"),
    );
    for meta in &entries {
        let age = format_relative_time(&meta.instance.created_at);
        let version = meta.instance.version.as_deref().unwrap_or("-");
        let kind = if meta.instance.gui { "gui" } else { "cli" };
        eprintln!(
            "  {:<20}{:<14}{:<12}{:<8}{}",
            theme.brand.apply_to(&meta.instance.alias),
            theme.value.apply_to(&meta.instance.app_name),
            version,
            kind,
            theme.dim.apply_to(&age),
        );
    }
    theme.blank();
    theme.dim(&format!(
        "{} instance{}{}",
        entries.len(),
        if entries.len() == 1 { "" } else { "s" },
        InstanceMetadata::display_path(&instances_dir.to_string_lossy()),
    ));
    theme.blank();
    Ok(())
}

fn format_relative_time(dt: &chrono::DateTime<Utc>) -> String {
    let now = Utc::now();
    let diff = now.signed_duration_since(dt);
    let seconds = diff.num_seconds();
    if seconds < 60 {
        return "just now".to_string();
    }
    let minutes = diff.num_minutes();
    if minutes < 60 {
        return format!("{} min{} ago", minutes, if minutes == 1 { "" } else { "s" });
    }
    let hours = diff.num_hours();
    if hours < 24 {
        return format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" });
    }
    let days = diff.num_days();
    if days < 7 {
        return format!("{} day{} ago", days, if days == 1 { "" } else { "s" });
    }
    let weeks = days / 7;
    if weeks < 5 {
        return format!("{} week{} ago", weeks, if weeks == 1 { "" } else { "s" });
    }
    let months = days / 30;
    if months < 12 {
        return format!("{} month{} ago", months, if months == 1 { "" } else { "s" });
    }
    let years = days / 365;
    format!("{} year{} ago", years, if years == 1 { "" } else { "s" })
}