cargo-fresh 0.9.9

A Rust tool for checking and updating globally installed Cargo packages with interactive mode and smart prerelease detection
use anyhow::Result;
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::process::{Command, Output};

use crate::locale::detection::detect_language;
use crate::models::{
    UpdateResult, MAX_RETRY_ATTEMPTS, PROGRESS_BAR_WIDTH, PROGRESS_TICK_MS, RETRY_DELAY_MS,
    VERSION_UPDATE_DELAY_MS,
};
use crate::package::{ensure_binstall_available, get_installed_version};

pub fn create_progress_bar(package_name: &str) -> ProgressBar {
    let language = detect_language();
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {msg}")
            .unwrap()
            .tick_strings(&["", "", "", "", "", "", "", "", "", ""]),
    );
    pb.set_message(format!(
        "{} {}",
        language
            .get_text("updating_package_progress")
            .replace("{}", "")
            .trim(),
        package_name.cyan()
    ));
    pb
}

pub fn create_main_progress_bar(total: usize) -> ProgressBar {
    let language = detect_language();
    let pb = ProgressBar::new(total as u64);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(&format!(
                "{{spinner:.green}} {{bar:{}.green/blue}} {{pos}}/{{len}} {{msg}}",
                PROGRESS_BAR_WIDTH
            ))
            .unwrap()
            .progress_chars("█▉▊▋▌▍▎▏  ")
            .tick_strings(&["", "", "", "", "", "", "", "", "", ""]),
    );
    pb.set_message(
        language
            .get_text("updating_package_progress")
            .replace("{}", "packages"),
    );
    pb
}

fn build_args<'a>(use_binstall: bool, package_name: &'a str, version: Option<&'a str>) -> Vec<&'a str> {
    let subcmd = if use_binstall { "binstall" } else { "install" };
    match version {
        Some(v) => vec![subcmd, "--force", package_name, "--version", v],
        None => vec![subcmd, "--force", package_name],
    }
}

fn run_cargo(pb: &ProgressBar, args: &[&str]) -> Result<Output> {
    let language = detect_language();
    pb.println(format!(
        "{} cargo {}",
        language.get_text("executing_command"),
        args.join(" ")
    ));
    pb.enable_steady_tick(std::time::Duration::from_millis(PROGRESS_TICK_MS));
    let output = Command::new("cargo")
        .args(args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .output()?;
    pb.disable_steady_tick();
    Ok(output)
}

/// 命令执行成功后,确认安装版本并打印对应文案,返回 UpdateResult。
///
/// `new_version: None` 表示命令成功但无法读到安装后的版本(例:cargo install --list 失败)。
/// 调用方可据此决定是否重试。
async fn verify_and_report_update(
    pb: &ProgressBar,
    package_name: &str,
    old_version: &Option<String>,
) -> UpdateResult {
    let language = detect_language();
    pb.println(
        language.format_text(
            "package_update_success",
            &[("name", &package_name.green().to_string())],
        ),
    );

    tokio::time::sleep(tokio::time::Duration::from_millis(VERSION_UPDATE_DELAY_MS)).await;

    match get_installed_version(package_name).await {
        Ok(Some(new_version)) if old_version.as_ref() != Some(&new_version) => {
            let unknown = language.get_text("unknown_version").to_string();
            let old_str = old_version.as_ref().unwrap_or(&unknown);
            pb.println(language.format_text(
                "package_updated_version",
                &[
                    ("name", &package_name.green().to_string()),
                    ("old", &old_str.red().to_string()),
                    ("new", &new_version.green().to_string()),
                ],
            ));
            UpdateResult::new(
                package_name.to_string(),
                old_version.clone(),
                Some(new_version),
                true,
            )
        }
        Ok(Some(_)) => {
            pb.println(
                language.format_text(
                    "package_version_unchanged",
                    &[("name", &package_name.yellow().to_string())],
                ),
            );
            UpdateResult::new(
                package_name.to_string(),
                old_version.clone(),
                old_version.clone(),
                true,
            )
        }
        _ => {
            pb.println(
                language.format_text(
                    "package_update_verification_failed",
                    &[("name", &package_name.yellow().to_string())],
                ),
            );
            UpdateResult::new(package_name.to_string(), old_version.clone(), None, true)
        }
    }
}

fn report_command_failure(pb: &ProgressBar, package_name: &str, output: &Output) {
    let language = detect_language();
    pb.println(language.format_text(
        "package_update_failed",
        &[
            ("name", &package_name.red().to_string()),
            ("code", &output.status.code().unwrap_or(-1).to_string()),
        ],
    ));
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !stderr.is_empty() {
        pb.println(format!(
            "{} {}",
            language.get_text("error_details"),
            stderr.red()
        ));
    }
}

pub async fn update_package(
    package_name: &str,
    target_version: Option<&str>,
) -> Result<UpdateResult> {
    let language = detect_language();
    let pb = create_progress_bar(package_name);

    let old_version = get_installed_version(package_name).await.ok().flatten();
    if let Some(ref version) = old_version {
        pb.println(format!(
            "{} {}",
            language.get_text("current_version_label"),
            version.blue()
        ));
    }

    let use_binstall = ensure_binstall_available().await.unwrap_or(false);

    if use_binstall {
        pb.println(format!("{}", language.get_text("using_binstall").cyan()));
    } else {
        pb.println(format!(
            "🔄 {}",
            language.get_text("using_install_fallback").yellow()
        ));
    }

    let primary_args = build_args(use_binstall, package_name, target_version);
    // 只有走 binstall 时才有 install 回退;纯 install 路径失败就只能重试。
    let fallback_args = if use_binstall {
        Some(build_args(false, package_name, target_version))
    } else {
        None
    };

    for attempt in 1..=MAX_RETRY_ATTEMPTS {
        if attempt > 1 {
            pb.set_message(language.format_text(
                "retry_attempt",
                &[
                    ("attempt", &attempt.to_string()),
                    ("name", &package_name.cyan().to_string()),
                ],
            ));
        }

        let output = run_cargo(&pb, &primary_args)?;

        if output.status.success() {
            let result = verify_and_report_update(&pb, package_name, &old_version).await;
            // 命令成功但读不到新版本时,给主路径一次重试机会(保留原行为)
            if result.new_version.is_none() && attempt < MAX_RETRY_ATTEMPTS {
                pb.println(language.get_text("waiting_retry"));
                tokio::time::sleep(tokio::time::Duration::from_millis(RETRY_DELAY_MS)).await;
                continue;
            }
            return Ok(result);
        }

        // binstall 第一次失败:立刻尝试 install 回退(不消耗 attempt 计数器中的剩余次数)
        if let (Some(args), 1) = (fallback_args.as_ref(), attempt) {
            pb.println(
                language
                    .get_text("binstall_failed_fallback")
                    .yellow()
                    .to_string(),
            );
            let fb_output = run_cargo(&pb, args)?;
            if fb_output.status.success() {
                return Ok(verify_and_report_update(&pb, package_name, &old_version).await);
            }
            report_command_failure(&pb, package_name, &fb_output);
        } else {
            report_command_failure(&pb, package_name, &output);
        }

        if attempt < MAX_RETRY_ATTEMPTS {
            pb.println(language.get_text("waiting_retry"));
            tokio::time::sleep(tokio::time::Duration::from_millis(RETRY_DELAY_MS)).await;
            continue;
        }

        return Ok(UpdateResult::new(
            package_name.to_string(),
            old_version,
            None,
            false,
        ));
    }

    Ok(UpdateResult::new(
        package_name.to_string(),
        old_version,
        None,
        false,
    ))
}