use std::fs::File;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
const CRATE: &str = "oxframe-cli";
const INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
pub fn run() {
if std::env::var_os("OXFRAME_NO_UPDATE_CHECK").is_some() || std::env::var_os("CI").is_some() {
return;
}
let Some(file) = cache_file() else { return };
let text = std::fs::read_to_string(&file).unwrap_or_default();
if let Some(latest) = parse_search(&text)
&& is_newer(&latest, env!("CARGO_PKG_VERSION"))
{
crate::note(format!(
"oxframe-cli {latest} is available (you have {}): cargo install {CRATE} --force",
env!("CARGO_PKG_VERSION")
));
}
let stale = std::fs::metadata(&file)
.and_then(|m| m.modified())
.ok()
.and_then(|t| SystemTime::now().duration_since(t).ok())
.is_none_or(|age| age > INTERVAL);
if stale {
refresh(&file);
}
}
fn refresh(file: &PathBuf) {
if let Some(dir) = file.parent()
&& std::fs::create_dir_all(dir).is_err()
{
return;
}
let Ok(out) = File::create(file) else { return };
let _ = Command::new(crate::cargo::cargo())
.args(["search", CRATE, "--limit", "1", "--color", "never"])
.stdin(Stdio::null())
.stdout(out)
.stderr(Stdio::null())
.spawn();
}
fn cache_file() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("LOCALAPPDATA").map(PathBuf::from))
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))?;
Some(base.join("oxframe").join("latest-cli.txt"))
}
fn parse_search(text: &str) -> Option<String> {
let line = text
.lines()
.find(|l| l.starts_with(&format!("{CRATE} = ")))?;
let v = line.split('"').nth(1)?;
Some(v.to_owned())
}
pub fn is_newer(candidate: &str, current: &str) -> bool {
match (parse_version(candidate), parse_version(current)) {
(Some(a), Some(b)) => a > b,
_ => false,
}
}
pub fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
let mut it = v.split('.').map(|p| p.parse::<u64>().ok());
let r = (it.next()??, it.next()??, it.next()??);
it.next().is_none().then_some(r)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_cargo_search_output() {
let out = "oxframe-cli = \"0.3.1\" # The `oxframe` command\n... and 2 crates more\n";
assert_eq!(parse_search(out).as_deref(), Some("0.3.1"));
assert_eq!(parse_search(""), None);
assert_eq!(parse_search("oxframe = \"9.0.0\" # other crate"), None);
}
#[test]
fn compares_versions() {
assert!(is_newer("0.2.0", "0.1.9"));
assert!(is_newer("0.10.0", "0.9.0"));
assert!(!is_newer("0.1.0", "0.1.0"));
assert!(!is_newer("0.2.0-beta.1", "0.1.0"));
assert!(!is_newer("garbage", "0.1.0"));
}
}