soar-cli 0.12.1

A modern package manager for Linux
use std::{
    env::{self, consts::ARCH},
    fs,
    io::{self, Write},
};

use semver::Version;
use soar_core::{
    error::{ErrorContext, SoarError},
    SoarResult,
};
use soar_dl::{
    download::Download,
    github::Github,
    traits::{Asset as _, Platform as _, Release as _},
    types::OverwriteMode,
};
use soar_utils::bytes::format_bytes;
use tracing::{debug, error, info};

use crate::{
    cli::SelfAction,
    progress::{create_download_job, handle_download_progress},
};

pub async fn process_self_action(action: &SelfAction) -> SoarResult<()> {
    let self_bin =
        env::current_exe().with_context(|| "Failed to get executable path".to_string())?;
    let self_version = env!("CARGO_PKG_VERSION");

    debug!("Executable path: {}", self_bin.display());

    match action {
        SelfAction::Update {
            yes,
        } => {
            let is_nightly = self_version.starts_with("nightly");
            debug!("Current version: {}", self_version);

            let target_nightly = match (env::var("SOAR_NIGHTLY"), env::var("SOAR_RELEASE")) {
                (Ok(_), Err(_)) => true,
                (Err(_), Ok(_)) => false,
                _ => is_nightly,
            };

            let releases = Github::fetch_releases("pkgforge/soar", None)?;

            let release = releases.iter().find(|release| {
                let is_nightly_release = release.tag().starts_with("nightly");
                debug!(
                    "Checking release: {}, Release Channel: {}",
                    release.tag(),
                    if is_nightly_release {
                        "nightly"
                    } else {
                        "stable"
                    }
                );

                if target_nightly {
                    is_nightly_release && release.name() != self_version
                } else {
                    let release_version = release.tag().trim_start_matches("v");
                    let parsed_release_version = Version::parse(release_version).ok();
                    let parsed_self_version = Version::parse(self_version).ok();

                    match (parsed_release_version, parsed_self_version) {
                        (Some(release_ver), Some(self_ver)) => {
                            let should_update = !is_nightly_release && release_ver > self_ver;
                            debug!(
                                "Comparing versions: release_ver={}, self_ver={}, should_update={}",
                                release_ver, self_ver, should_update
                            );
                            should_update
                        }
                        (_, None) => is_nightly,
                        _ => {
                            debug!(
                                "Skipping release {} due to invalid version.",
                                release_version
                            );
                            false
                        }
                    }
                }
            });

            if let Some(release) = release {
                let new_version = release.tag();

                if let Some(body) = release.body() {
                    if !body.is_empty() {
                        println!("\nRelease Notes for {}:\n", new_version);
                        println!("{}", body);
                        println!();
                    } else {
                        println!("No release notes available for this update.");
                    }
                } else {
                    println!("No release notes available for this update.");
                }

                if !yes {
                    print!("Update to {}? [y/N] ", new_version);
                    io::stdout().flush().map_err(|e| {
                        SoarError::IoError {
                            action: "flushing stdout".to_string(),
                            source: e,
                        }
                    })?;

                    let mut input = String::new();
                    io::stdin().read_line(&mut input).map_err(|e| {
                        SoarError::IoError {
                            action: "reading user input".to_string(),
                            source: e,
                        }
                    })?;

                    if !input.trim().to_lowercase().starts_with('y') {
                        info!("Update cancelled.");
                        return Ok(());
                    }
                }

                if target_nightly != is_nightly {
                    info!(
                        "Switching from {} to {} channel",
                        if is_nightly { "nightly" } else { "stable" },
                        if target_nightly { "nightly" } else { "stable" }
                    );
                } else {
                    info!("Found new update: {}", release.tag());
                }
                let assets = release.assets();
                let asset = assets
                    .iter()
                    .find(|a| {
                        a.name.contains(ARCH) && !a.name.contains("tar") && !a.name.contains("sum")
                    })
                    .ok_or_else(|| {
                        SoarError::Custom(format!("No matching asset found for {}", ARCH))
                    })?;

                debug!("Selected asset: {}", asset.name());

                if let Some(size) = asset.size() {
                    info!("Download size: {}", format_bytes(size, 2));
                }

                let pb = create_download_job("Downloading");

                let dl = Download::new(asset.url())
                    .output(self_bin.to_string_lossy())
                    .overwrite(OverwriteMode::Force)
                    .progress({
                        let pb = pb.clone();
                        move |p| handle_download_progress(p, &pb)
                    });

                debug!("Downloading update from: {}", asset.url());
                dl.execute()?;
                info!("Soar updated to {}", release.tag());
            } else {
                eprintln!("No updates found.");
            }
        }
        SelfAction::Uninstall => {
            match fs::remove_file(self_bin) {
                Ok(_) => {
                    info!("Soar has been uninstalled successfully.");
                    info!("You should remove soar config and data files manually.");
                }
                Err(err) => {
                    error!("{}\nFailed to uninstall soar.", err.to_string());
                }
            };
        }
    };

    Ok(())
}