use anyhow::{anyhow, bail, Context, Result};
use std::cmp::Ordering;
use std::io::{IsTerminal, Write};
use std::process::Command;
use std::time::Duration;
const RELEASES_API_URL: &str = "https://api.github.com/repos/zilliztech/zilliz-cli/releases/latest";
const INSTALL_SH_URL: &str =
"https://raw.githubusercontent.com/zilliztech/zilliz-cli/master/install.sh";
const INSTALL_PS1_URL: &str =
"https://raw.githubusercontent.com/zilliztech/zilliz-cli/master/install.ps1";
const MANUAL_INSTALL_HINT: &str = "\
Manual install:
macOS/Linux: curl -fsSL https://raw.githubusercontent.com/zilliztech/zilliz-cli/master/install.sh | bash
Windows: irm https://raw.githubusercontent.com/zilliztech/zilliz-cli/master/install.ps1 | iex";
pub async fn run(check: bool, yes: bool, force: bool) -> Result<()> {
let current = env!("CARGO_PKG_VERSION").to_string();
let latest = match fetch_latest_version().await {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to query latest version: {}", e);
eprintln!("\n{}", MANUAL_INSTALL_HINT);
bail!("upgrade aborted");
}
};
println!("Current version: {}", current);
println!("Latest version: {}", latest);
let cmp = compare_versions(¤t, &latest);
if check {
match cmp {
Ordering::Less => {
println!("\nA newer version is available. Run `zilliz upgrade` to update.")
}
_ => println!("\nAlready on the latest version."),
}
return Ok(());
}
if !force && cmp != Ordering::Less {
println!("\nAlready on the latest version.");
return Ok(());
}
let installer_cmd = installer_command_str();
println!("\nInstaller command:\n {}\n", installer_cmd);
if !yes {
if !std::io::stdin().is_terminal() {
eprintln!("Refusing to run installer non-interactively. Pass --yes to proceed.");
bail!("upgrade aborted");
}
if !prompt_confirm("Proceed with upgrade? [y/N]: ")? {
println!("Upgrade cancelled.");
return Ok(());
}
}
let status = run_installer()?;
if !status.success() {
eprintln!("\nInstaller exited with status {}.", status);
eprintln!("{}", MANUAL_INSTALL_HINT);
let code = status.code().unwrap_or(1);
std::process::exit(code);
}
println!("\nUpgrade complete.");
if let Err(e) = path_hint() {
tracing::debug!("path_hint error: {}", e);
}
Ok(())
}
pub(crate) async fn fetch_latest_version() -> Result<String> {
let ua = format!("zilliz-tui/{}", env!("CARGO_PKG_VERSION"));
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()
.context("build http client")?;
let resp = client
.get(RELEASES_API_URL)
.header("User-Agent", ua)
.header("Accept", "application/vnd.github+json")
.send()
.await
.context("request GitHub releases API")?;
let status = resp.status();
if !status.is_success() {
return Err(anyhow!("GitHub API returned HTTP {}", status));
}
let body: serde_json::Value = resp.json().await.context("parse GitHub response")?;
let tag = body
.get("tag_name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing tag_name in GitHub response"))?;
Ok(normalize_tag(tag))
}
fn normalize_tag(tag: &str) -> String {
match tag.find(|c: char| c.is_ascii_digit()) {
Some(idx) => tag[idx..].to_string(),
None => tag.to_string(),
}
}
pub fn compare_versions(a: &str, b: &str) -> Ordering {
let a_owned = normalize_tag(a);
let b_owned = normalize_tag(b);
let a = a_owned.as_str();
let b = b_owned.as_str();
let a_parts: Vec<&str> = a.split('.').collect();
let b_parts: Vec<&str> = b.split('.').collect();
let len = a_parts.len().max(b_parts.len());
for i in 0..len {
let ap = a_parts.get(i).copied().unwrap_or("0");
let bp = b_parts.get(i).copied().unwrap_or("0");
match (ap.parse::<u64>(), bp.parse::<u64>()) {
(Ok(x), Ok(y)) => match x.cmp(&y) {
Ordering::Equal => continue,
other => return other,
},
_ => match ap.cmp(bp) {
Ordering::Equal => continue,
other => return other,
},
}
}
Ordering::Equal
}
fn installer_command_str() -> String {
if cfg!(target_os = "windows") {
format!(
"powershell -NoProfile -Command \"irm {} | iex\"",
INSTALL_PS1_URL
)
} else {
format!("bash -c \"curl -fsSL {} | bash\"", INSTALL_SH_URL)
}
}
fn run_installer() -> Result<std::process::ExitStatus> {
let mut cmd = if cfg!(target_os = "windows") {
let mut c = Command::new("powershell");
c.args([
"-NoProfile",
"-Command",
&format!("irm {} | iex", INSTALL_PS1_URL),
]);
c
} else {
let mut c = Command::new("bash");
c.args(["-c", &format!("curl -fsSL {} | bash", INSTALL_SH_URL)]);
c
};
let status = cmd.status().context("failed to spawn installer process")?;
Ok(status)
}
fn prompt_confirm(prompt: &str) -> Result<bool> {
print!("{}", prompt);
std::io::stdout().flush().ok();
let mut buf = String::new();
std::io::stdin().read_line(&mut buf).context("read stdin")?;
let trimmed = buf.trim();
Ok(trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes"))
}
fn path_hint() -> Result<()> {
let exe = std::env::current_exe().context("current_exe")?;
let exe_str = exe.to_string_lossy().to_string();
let home = dirs::home_dir().map(|p| p.to_string_lossy().to_string());
let expected = home.as_ref().map(|h| format!("{}/.zilliz/bin", h));
if let Some(expected) = expected {
if !exe_str.starts_with(&expected) {
println!(
"Note: the running binary is at {}. The installer writes to {}. \
If `zilliz` still reports the old version after this command, \
reopen your shell or ensure {} is first on PATH.",
exe_str, expected, expected
);
}
}
Ok(())
}