Skip to main content

hostcraft_cli/update/
mod.rs

1mod utils;
2
3use crate::display::{print_success, print_up_to_date, print_updating};
4use std::error::Error;
5use std::sync::mpsc;
6use std::thread;
7use utils::{
8    binary_download_url, current_target, download_bytes, fetch_latest_cli_release,
9    is_newer_version, record_last_checked, replace_binary, should_check_for_update,
10    version_from_tag,
11};
12
13// ── Public API ─────────────────────────────────────────────────────────────────
14
15/// Spawns a background thread to check GitHub for a newer CLI version.
16/// Returns a lazy handle — calling the handle blocks until the result is ready.
17/// The check is skipped entirely if one ran within the last 24 hours.
18/// Network failures are silently swallowed so they never interrupt normal usage.
19pub fn check_for_update() -> Box<dyn FnOnce() -> Option<String>> {
20    if !should_check_for_update() {
21        return Box::new(|| None);
22    }
23
24    let (tx, rx) = mpsc::channel();
25
26    thread::spawn(move || {
27        let result = fetch_latest_version().ok().flatten();
28        record_last_checked();
29        let _ = tx.send(result);
30    });
31
32    Box::new(move || rx.recv().ok().flatten())
33}
34
35/// Asks GitHub releases whether there is a newer version.
36/// Returns Ok(Some(version_string)) if there is, Ok(None) if already current,
37/// or Err if the network request failed or the response was unparseable.
38pub fn fetch_latest_version() -> Result<Option<String>, Box<dyn Error>> {
39    let release = fetch_latest_cli_release()
40        .ok_or("Could not fetch release info from GitHub. Check your connection.")?;
41
42    let version = version_from_tag(&release.tag_name)
43        .ok_or_else(|| format!("Unexpected tag format: '{}'", release.tag_name))?;
44
45    let is_newer = is_newer_version(version)
46        .ok_or_else(|| format!("Could not parse version string: '{}'", version))?;
47
48    Ok(if is_newer {
49        Some(version.to_owned())
50    } else {
51        None
52    })
53}
54
55/// Full update flow for the `hostcraft update` command.
56pub fn handle_update() -> Result<(), Box<dyn Error>> {
57    let latest =
58        fetch_latest_version().map_err(|e| format!("Failed to check for updates: {}", e))?;
59
60    match latest {
61        None => print_up_to_date(),
62        Some(version) => {
63            print_updating(&version);
64            install_latest(&version).map_err(|e| format!("Update failed: {}", e))?;
65            print_success(&format!("Updated to v{}", version));
66        }
67    }
68    Ok(())
69}
70
71// ── Private ────────────────────────────────────────────────────────────────────
72
73/// Downloads the binary for the current platform from GitHub and self-replaces.
74/// Falls back to `cargo install` if no pre-built binary matches this platform
75/// (e.g. someone compiled from source on a niche target).
76fn install_latest(version: &str) -> Result<(), Box<dyn Error>> {
77    match current_target() {
78        Some(target) => {
79            let url = binary_download_url(version, target);
80            let bytes = download_bytes(&url)?;
81            replace_binary(&bytes)
82        }
83        None => install_via_cargo(),
84    }
85}
86
87fn install_via_cargo() -> Result<(), Box<dyn Error>> {
88    let status = std::process::Command::new("cargo")
89        .args(["install", "hostcraft-cli"])
90        .status()
91        .map_err(|e| format!("Failed to run cargo: {}", e))?;
92
93    if status.success() {
94        Ok(())
95    } else {
96        Err(format!("cargo install exited with status: {}", status).into())
97    }
98}