From e9ab30bad72aeecd25719606ee890a09ce882da5 Mon Sep 17 00:00:00 2001 From: Brandon Shippy Date: Mon, 29 Jun 2026 14:11:10 -0700 Subject: [PATCH] sift-cli update tool boilerplate --- Cargo.toml | 1 + rust/crates/sift_cli/Cargo.toml | 1 + rust/crates/sift_cli/src/cli/mod.rs | 22 ++++ rust/crates/sift_cli/src/cmd/mod.rs | 1 + rust/crates/sift_cli/src/cmd/update.rs | 141 +++++++++++++++++++++++++ rust/crates/sift_cli/src/main.rs | 1 + 6 files changed, 167 insertions(+) create mode 100644 rust/crates/sift_cli/src/cmd/update.rs diff --git a/Cargo.toml b/Cargo.toml index 1efff87a2..5bfcdce78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ arrow-array = "58.3.0" arrow-schema = "58.3.0" async-channel = "2.2" async-trait = "^0.1" +axoupdater = "0.9" axum = "0.8" mockall = "0.14.0" bytes = "1.11.1" diff --git a/rust/crates/sift_cli/Cargo.toml b/rust/crates/sift_cli/Cargo.toml index b28014861..632e65589 100644 --- a/rust/crates/sift_cli/Cargo.toml +++ b/rust/crates/sift_cli/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } +axoupdater = { workspace = true } arrow-array = { workspace = true } arrow-schema = { workspace = true } chrono = { workspace = true } diff --git a/rust/crates/sift_cli/src/cli/mod.rs b/rust/crates/sift_cli/src/cli/mod.rs index 34a354aa1..ef031c265 100644 --- a/rust/crates/sift_cli/src/cli/mod.rs +++ b/rust/crates/sift_cli/src/cli/mod.rs @@ -59,6 +59,28 @@ pub enum Cmd { /// Ping the Sift API to verify credentials and connectivity Ping, + + /// Update sift-cli to the latest release (or a pinned version) + Update(UpdateArgs), +} + +#[derive(clap::Args)] +pub struct UpdateArgs { + /// Pin to a specific version (e.g. 0.3.0). Defaults to the latest release. + #[arg(long)] + pub version: Option, + + /// Include prereleases when picking the latest version + #[arg(long)] + pub pre: bool, + + /// Report what would happen without applying the update + #[arg(long)] + pub check: bool, + + /// Skip the confirmation prompt + #[arg(short = 'y', long)] + pub yes: bool, } /// Serve the bundled Sift CLI user documentation over HTTP. diff --git a/rust/crates/sift_cli/src/cmd/mod.rs b/rust/crates/sift_cli/src/cmd/mod.rs index 64d7704d2..b464f4485 100644 --- a/rust/crates/sift_cli/src/cmd/mod.rs +++ b/rust/crates/sift_cli/src/cmd/mod.rs @@ -11,6 +11,7 @@ pub mod import; pub mod install; pub mod mcp; pub mod ping; +pub mod update; pub struct Context { pub grpc_uri: String, diff --git a/rust/crates/sift_cli/src/cmd/update.rs b/rust/crates/sift_cli/src/cmd/update.rs new file mode 100644 index 000000000..5baf82acf --- /dev/null +++ b/rust/crates/sift_cli/src/cmd/update.rs @@ -0,0 +1,141 @@ +use std::{ + io::{self, Write}, + process::ExitCode, +}; + +use anyhow::{Context as _, Result}; +use axoupdater::{AxoUpdater, UpdateRequest, Version}; +use crossterm::style::Stylize; + +use crate::cli::UpdateArgs; +use crate::cmd::config::get_config_file_path; +use crate::util::tty::Output; + +pub async fn run(args: UpdateArgs) -> Result { + let UpdateArgs { + version, + pre, + check, + yes, + } = args; + + let mut updater = AxoUpdater::new_for("sift_cli"); + + if updater.load_receipt().is_err() { + Output::new() + .line("sift-cli wasn't installed by the official installer.".to_string()) + .tip( + "re-run the curl installer to update: \ + https://github.com/sift-stack/sift/releases/latest", + ) + .eprint(); + return Ok(ExitCode::FAILURE); + } + + updater.disable_installer_output(); + + let current = Version::parse(env!("CARGO_PKG_VERSION")) + .context("invalid CARGO_PKG_VERSION baked into binary")?; + let current_is_prerelease = !current.pre.is_empty(); + + let specifier = match version.as_deref() { + Some(v) => { + v.parse::() + .context("--version must be a valid semver string like 0.3.0")?; + UpdateRequest::SpecificVersion(v.to_string()) + } + None if pre || current_is_prerelease => UpdateRequest::LatestMaybePrerelease, + None => UpdateRequest::Latest, + }; + updater.configure_version_specifier(specifier); + + let target = updater + .query_new_version() + .await + .context("failed to query latest release from GitHub")? + .cloned(); + + let Some(target) = target else { + Output::new() + .line(format!( + "sift-cli {} is already the latest release.", + current.to_string().bold(), + )) + .print(); + return Ok(ExitCode::SUCCESS); + }; + + if target == current { + Output::new() + .line(format!( + "sift-cli {} is already at the requested version.", + current.to_string().bold(), + )) + .print(); + return Ok(ExitCode::SUCCESS); + } + + if target < current && version.is_none() { + Output::new() + .line(format!( + "Refusing to downgrade: latest release ({}) is older than installed ({}).", + target.to_string().yellow(), + current.to_string().bold(), + )) + .tip("pass `--version ` to pin a specific version, or `--pre` to include prereleases") + .eprint(); + return Ok(ExitCode::FAILURE); + } + + let arrow = if target < current { "↓" } else { "→" }; + println!( + "Update: {} {arrow} {}", + current.to_string().yellow(), + target.to_string().green(), + ); + + if check { + println!("({} passed — no changes applied)", "--check".bold()); + return Ok(ExitCode::SUCCESS); + } + + if !yes && !confirm()? { + println!("Update aborted."); + return Ok(ExitCode::SUCCESS); + } + + let result = updater + .run() + .await + .context("update failed")?; + + let mut out = Output::new(); + match result { + Some(outcome) => { + out.line(format!( + "Updated sift-cli to {}", + outcome.new_version.to_string().green(), + )); + } + None => { + out.line("Update completed.".to_string()); + } + } + if let Ok(path) = get_config_file_path() { + out.line(format!("Your config at {} is unchanged.", path.display())); + } + out.print(); + + Ok(ExitCode::SUCCESS) +} + +fn confirm() -> Result { + print!("Proceed? [y/N] "); + io::stdout().flush()?; + let mut buf = String::new(); + io::stdin().read_line(&mut buf)?; + Ok(matches!( + buf.trim().to_lowercase().as_str(), + "y" | "yes" + )) +} diff --git a/rust/crates/sift_cli/src/main.rs b/rust/crates/sift_cli/src/main.rs index 086f8eb31..036a312e9 100644 --- a/rust/crates/sift_cli/src/main.rs +++ b/rust/crates/sift_cli/src/main.rs @@ -65,6 +65,7 @@ fn run(clargs: cli::Args) -> Result { ConfigCmd::Update(args) => return cmd::config::update(clargs.profile, args), }, Cmd::Doc(args) => return run_future(cmd::doc::serve(args)), + Cmd::Update(args) => return run_future(cmd::update::run(args)), Cmd::Install(cmd) => match cmd { InstallCmd::Completions(cmd) => match cmd { cli::CompletionsCmd::Print(args) => return cmd::install::completions::print(args),