flatland3 0.2.32

Flatland3 terminal play client and account CLI
Documentation
//! Client binary self-update CLI (`flatland3 update check|apply`).

use clap::Subcommand;
use flatland_client_lib::{
    apply_update, check_for_update, default_latest_url, local_version, UpdateAvailable,
};

#[derive(Debug, Subcommand)]
pub enum UpdateCommand {
    /// Compare the installed client version to the public latest.json
    Check {
        #[arg(long, env = "FLATLAND_UPDATE_URL")]
        update_url: Option<String>,
    },
    /// Download, verify, and swap flatland3 + flatland3-gfx from the release archive
    Apply {
        #[arg(long, env = "FLATLAND_UPDATE_URL")]
        update_url: Option<String>,
        /// Apply even when the remote version is not newer (reinstall current latest)
        #[arg(long)]
        force: bool,
    },
}

pub async fn run(command: UpdateCommand) -> anyhow::Result<()> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(60))
        .build()?;
    match command {
        UpdateCommand::Check { update_url } => {
            let local = local_version();
            match check_for_update(&client, local, update_url.as_deref()).await? {
                Some(offer) => {
                    println!(
                        "update available: {}{} ({})",
                        offer.local_version, offer.remote_version, offer.platform
                    );
                    println!("archive: {}", offer.archive_url);
                    println!("run: flatland3 update apply");
                }
                None => {
                    println!(
                        "up to date: v{local} (checked {})",
                        update_url.unwrap_or_else(default_latest_url)
                    );
                }
            }
            Ok(())
        }
        UpdateCommand::Apply { update_url, force } => {
            let local = local_version();
            let offer = if force {
                force_offer(&client, local, update_url.as_deref()).await?
            } else {
                check_for_update(&client, local, update_url.as_deref())
                    .await?
                    .ok_or_else(|| anyhow::anyhow!("already up to date (v{local})"))?
            };
            println!(
                "downloading {} ({} bytes)…",
                offer.remote_version, offer.artifact.size
            );
            let applied = apply_update(&client, &offer).await?;
            println!(
                "updated to v{} in {}",
                applied.version,
                applied.install_dir.display()
            );
            println!("restart flatland3 / flatland3-gfx to run the new binaries");
            Ok(())
        }
    }
}

async fn force_offer(
    client: &reqwest::Client,
    local: &str,
    latest_url: Option<&str>,
) -> anyhow::Result<UpdateAvailable> {
    use flatland_client_lib::{fetch_latest_manifest, platform_tag};

    let url = latest_url
        .map(str::to_string)
        .unwrap_or_else(default_latest_url);
    let platform = platform_tag().ok_or_else(|| {
        anyhow::anyhow!(
            "unsupported platform ({}-{})",
            std::env::consts::OS,
            std::env::consts::ARCH
        )
    })?;
    let manifest = fetch_latest_manifest(client, &url).await?;
    let artifact = manifest
        .artifacts
        .get(platform)
        .cloned()
        .ok_or_else(|| anyhow::anyhow!("latest.json missing artifact for {platform}"))?;
    let archive_url = if let Some((base, _)) = url.rsplit_once('/') {
        format!("{}/{}", base, artifact.path.trim_start_matches('/'))
    } else {
        artifact.path.clone()
    };
    Ok(UpdateAvailable {
        local_version: local.to_string(),
        remote_version: manifest.version,
        platform: platform.to_string(),
        artifact,
        archive_url,
    })
}