stepshots-cli 1.0.1

CLI for recording, bundling, and uploading Stepshots demos
use std::time::Duration;

use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use semver::Version;

use crate::error::CliError;

const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
const GITHUB_REPO: &str = "hauju/stepshots";
const CARGO_INSTALL_URL: &str = "https://github.com/hauju/stepshots.git";
const PACKAGE_NAME: &str = "stepshots-cli";

pub async fn run(force: bool, check_only: bool) -> Result<(), CliError> {
    let current = Version::parse(CURRENT_VERSION)
        .map_err(|e| CliError::Upgrade(format!("Failed to parse current version: {e}")))?;

    println!("  {} Checking for updates...", style("").dim(),);

    let latest_str = match fetch_latest_version().await {
        Ok(v) => v,
        Err(e) => {
            eprintln!(
                "  {} Could not check for updates: {e}",
                style("").red().bold(),
            );
            if force {
                eprintln!(
                    "  {} Proceeding with reinstall (--force)",
                    style("").cyan(),
                );
                return do_install(CURRENT_VERSION, None).await;
            }
            return Err(e);
        }
    };

    let latest = Version::parse(&latest_str).map_err(|e| {
        CliError::Upgrade(format!(
            "Failed to parse latest version '{latest_str}': {e}"
        ))
    })?;

    if current >= latest && !force {
        println!(
            "  {} stepshots is already at the latest version (v{})",
            style("").green().bold(),
            current,
        );
        return Ok(());
    }

    if current < latest {
        println!(
            "  {} Update available: v{} → v{}",
            style("").green().bold(),
            current,
            latest,
        );
    }

    if check_only {
        return Ok(());
    }

    do_install(CURRENT_VERSION, Some(&latest_str)).await
}

async fn do_install(current: &str, latest: Option<&str>) -> Result<(), CliError> {
    verify_cargo_available()?;

    let target = latest.unwrap_or(current);
    println!(
        "  {} Upgrading stepshots... v{} → v{}",
        style("").cyan(),
        current,
        target,
    );

    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {msg}")
            .expect("valid template"),
    );
    spinner.set_message("Compiling from source (this may take a minute)...");
    spinner.enable_steady_tick(Duration::from_millis(100));

    let output = tokio::process::Command::new("cargo")
        .args([
            "install",
            "--git",
            CARGO_INSTALL_URL,
            PACKAGE_NAME,
            "--force",
        ])
        .output()
        .await
        .map_err(|e| CliError::Upgrade(format!("Failed to run cargo install: {e}")))?;

    spinner.finish_and_clear();

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(CliError::Upgrade(format!(
            "cargo install failed:\n{stderr}"
        )));
    }

    println!(
        "  {} Successfully upgraded to stepshots v{}",
        style("").green().bold(),
        target,
    );

    Ok(())
}

async fn fetch_latest_version() -> Result<String, CliError> {
    let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases/latest");
    let client = reqwest::Client::new();
    let resp = client
        .get(&url)
        .header("User-Agent", format!("stepshots-cli/{CURRENT_VERSION}"))
        .header("Accept", "application/vnd.github.v3+json")
        .send()
        .await
        .map_err(|e| CliError::Upgrade(format!("Could not reach GitHub: {e}")))?;

    if resp.status().as_u16() == 404 {
        return Err(CliError::Upgrade(
            "No releases found on GitHub. Upgrade manually with: cargo install --git https://github.com/hauju/stepshots.git stepshots-cli --force".into(),
        ));
    }

    if !resp.status().is_success() {
        return Err(CliError::Upgrade(format!(
            "GitHub API returned status {}",
            resp.status()
        )));
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| CliError::Upgrade(format!("Failed to parse GitHub response: {e}")))?;

    let tag = body
        .get("tag_name")
        .and_then(|v| v.as_str())
        .ok_or_else(|| CliError::Upgrade("No tag_name in GitHub release".into()))?;

    Ok(tag.strip_prefix('v').unwrap_or(tag).to_string())
}

fn verify_cargo_available() -> Result<(), CliError> {
    match std::process::Command::new("cargo")
        .arg("--version")
        .output()
    {
        Ok(output) if output.status.success() => Ok(()),
        _ => Err(CliError::Upgrade(
            "cargo not found. Install Rust from https://rustup.rs".into(),
        )),
    }
}