use anyhow::{Context, Result, bail};
use colored::Colorize;
use self_update::cargo_crate_version;
use std::process::Command;
const REPO_OWNER: &str = "ScreamZ";
const REPO_NAME: &str = "ai-igniter";
const BIN_NAME: &str = "ai-igniter";
fn current_target_triple() -> &'static str {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
"aarch64-apple-darwin"
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
{
"x86_64-apple-darwin"
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
{
"x86_64-unknown-linux-musl"
}
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
{
"x86_64-unknown-linux-gnu"
}
#[cfg(target_os = "windows")]
{
"x86_64-pc-windows-msvc"
}
#[cfg(not(any(
all(
target_os = "macos",
any(target_arch = "aarch64", target_arch = "x86_64")
),
target_os = "linux",
target_os = "windows"
)))]
{
"unsupported"
}
}
pub fn execute_update(check_only: bool, force_cargo: bool) -> Result<()> {
let current_v = cargo_crate_version!();
println!(
"{} Checking for updates (current version: {})...",
"[update]".cyan().bold(),
current_v.bold()
);
if force_cargo {
if check_only {
println!("{} Querying crates.io / cargo...", "[info]".blue().bold());
}
return update_via_cargo(check_only);
}
match update_via_github(check_only) {
Ok(true) => Ok(()),
Ok(false) => {
Ok(())
}
Err(e) => {
println!(
"{} GitHub release update unavailable: {:#}",
"[warn]".yellow().bold(),
e
);
println!(
"{} Falling back to 'cargo install'...",
"[info]".blue().bold()
);
update_via_cargo(check_only)
}
}
}
fn update_via_github(check_only: bool) -> Result<bool> {
let target = current_target_triple();
let current_v = cargo_crate_version!();
let mut builder = self_update::backends::github::Update::configure();
builder
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name(BIN_NAME)
.current_version(current_v)
.target(target)
.show_download_progress(true)
.show_output(false);
let updater = builder
.build()
.context("Failed to configure GitHub updater")?;
if check_only {
let releases = updater
.get_latest_release()
.context("Failed to fetch latest release")?;
let latest = match releases.latest() {
Some(r) => r,
None => {
println!(
"{} ai-igniter is already up to date ({})",
"✓".green(),
current_v.bold()
);
return Ok(false);
}
};
let latest_ver = latest.version();
if self_update::version::bump_is_greater(current_v, latest_ver).unwrap_or(false) {
println!(
"{} A new version is available: {} -> {}",
"✨".green(),
current_v.yellow(),
latest_ver.green().bold()
);
println!("Run {} to upgrade.", "ai-igniter update".bold().cyan());
return Ok(true);
} else {
println!(
"{} ai-igniter is already up to date ({})",
"✓".green(),
current_v.bold()
);
return Ok(false);
}
}
let status = updater
.update()
.context("Failed to perform binary update")?;
if status.is_updated() {
println!(
"{} Successfully updated ai-igniter to version {}!",
"✨".green().bold(),
status.version().green().bold()
);
Ok(true)
} else {
println!(
"{} ai-igniter is already up to date ({})",
"✓".green(),
status.version().bold()
);
Ok(false)
}
}
fn update_via_cargo(check_only: bool) -> Result<()> {
let cargo_check = Command::new("cargo").arg("--version").output();
if cargo_check.is_err() {
bail!(
"'cargo' executable not found in PATH. Please install Rust or download pre-built binaries from GitHub Releases."
);
}
if check_only {
println!(
"{} To update via cargo, run: {}",
"[info]".blue().bold(),
"cargo install ai-igniter --force".cyan().bold()
);
return Ok(());
}
println!(
"{} Running {}...",
"[cargo]".cyan().bold(),
"cargo install ai-igniter --force".bold()
);
let mut child = Command::new("cargo")
.args(["install", "ai-igniter", "--force"])
.spawn()
.context("Failed to spawn 'cargo install'")?;
let exit_status = child.wait().context("Failed waiting for 'cargo install'")?;
if exit_status.success() {
println!(
"{} Successfully updated ai-igniter via cargo!",
"✨".green().bold()
);
Ok(())
} else {
bail!("'cargo install' exited with status {}", exit_status);
}
}
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
const CACHE_TTL_SECS: u64 = 24 * 60 * 60;
#[derive(Serialize, Deserialize, Debug)]
struct UpdateCache {
last_checked_at: u64,
latest_version: String,
}
fn cache_file_path() -> Option<PathBuf> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()?;
Some(
PathBuf::from(home)
.join(".ai-igniter")
.join("update_cache.json"),
)
}
fn read_cache() -> Option<UpdateCache> {
let path = cache_file_path()?;
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn write_cache(latest_version: &str) {
if let Some(path) = cache_file_path() {
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let cache = UpdateCache {
last_checked_at: now,
latest_version: latest_version.to_string(),
};
if let Ok(json) = serde_json::to_string(&cache) {
let _ = fs::write(path, json);
}
}
}
pub struct UpdateNotifierGuard {
newer_version: Option<String>,
}
impl Drop for UpdateNotifierGuard {
fn drop(&mut self) {
if let Some(ref new_ver) = self.newer_version {
if std::io::stderr().is_terminal() {
let current_v = cargo_crate_version!();
eprintln!();
eprintln!(
"{}",
"╭─────────────────────────────────────────────────────────────╮".yellow()
);
eprintln!(
"{} Update available: {} {} {}",
"│".yellow(),
current_v.dimmed(),
"→".cyan(),
new_ver.green().bold()
);
eprintln!(
"{} Run {} to upgrade to the latest version {}",
"│".yellow(),
"ai-igniter update".bold().cyan(),
"│".yellow()
);
eprintln!(
"{}",
"╰─────────────────────────────────────────────────────────────╯".yellow()
);
eprintln!();
}
}
}
}
pub fn spawn_background_update_checker() -> UpdateNotifierGuard {
let current_v = cargo_crate_version!();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let cache = read_cache();
let mut newer_version = None;
let should_fetch = match &cache {
Some(c) => {
if self_update::version::bump_is_greater(current_v, &c.latest_version).unwrap_or(false)
{
newer_version = Some(c.latest_version.clone());
}
now.saturating_sub(c.last_checked_at) >= CACHE_TTL_SECS
}
None => true,
};
if should_fetch {
thread::spawn(move || {
let target = current_target_triple();
let mut builder = self_update::backends::github::Update::configure();
builder
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name(BIN_NAME)
.current_version(current_v)
.target(target)
.show_download_progress(false)
.show_output(false);
if let Some(latest) = builder
.build()
.ok()
.and_then(|u| u.get_latest_release().ok())
.and_then(|r| r.latest().map(|l| l.version().to_string()))
{
write_cache(&latest);
}
});
}
UpdateNotifierGuard { newer_version }
}