Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,13 @@ enum Commands {
#[arg(short = 'n', long)]
num_batches: Option<usize>,
},

/// Update the s2 CLI to the latest version.
Update {
/// Force update even if already on the latest version.
#[arg(short = 'f', long)]
force: bool,
},
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -1524,8 +1531,329 @@ async fn run() -> Result<(), S2CliError> {
let client_config = client_config(cfg.access_token)?;
list_tokens(client_config, prefix, start_after, limit, no_auto_paginate).await?;
}
Commands::Update { force } => {
update_s2_cli(force).await?;
}
}

Ok(())
}

async fn update_s2_cli(force: bool) -> Result<(), S2CliError> {
use serde_json::Value;
use std::env;
use std::process::Command;

let current_version = env!("CARGO_PKG_VERSION");

println!();
println!(
"┌─────────────────────────────────────────────────────────────────────────────────────┐"
);
println!(
"│ s2.dev │"
);
println!(
"│ The serverless API for streaming data, backed by object storage. │"
);
println!(
"└─────────────────────────────────────────────────────────────────────────────────────┘"
);
println!();

println!(" Current version: {}", current_version.cyan().bold());
println!();

print!(" ");
print!("{}", "Checking for updates".dimmed());

let output = Command::new("curl")
.args([
"-s",
"-H",
"User-Agent: s2-cli-updater",
"https://api.github.com/repos/s2-streamstore/s2-cli/releases/latest",
])
.output()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to fetch latest version: {}", e))
})?;

if !output.status.success() {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Failed to fetch latest version: HTTP {}",
output.status
)));
}

let release_info: Value = serde_json::from_slice(&output.stdout)
.map_err(|e| S2CliError::InvalidArgs(miette::miette!("Failed to parse response: {}", e)))?;

let latest_version = release_info["tag_name"]
.as_str()
.ok_or_else(|| S2CliError::InvalidArgs(miette::miette!("No tag_name in response")))?
.trim_start_matches('v');

println!(" ... done");
println!(" Latest version: {}", latest_version.cyan().bold());
println!();

if current_version == latest_version && !force {
println!(" {}", "Already up to date!".green().bold());
println!();
return Ok(());
}

if !force {
println!(
" Update available: {} → {}",
current_version.dimmed(),
latest_version.green().bold()
);
print!(" Proceed with update? [y/N]: ");
std::io::Write::flush(&mut std::io::stdout()).map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to flush stdout: {}", e))
})?;

let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| S2CliError::InvalidArgs(miette::miette!("Failed to read input: {}", e)))?;

if !input.trim().to_lowercase().starts_with('y') {
println!(" Update cancelled.");
println!();
return Ok(());
}
}

let exe_path = env::current_exe().map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to get executable path: {}", e))
})?;

println!();
println!(" {}", "Starting update...".blue().bold());
println!();

let exe_path_str = exe_path.to_string_lossy();

if exe_path_str.contains("homebrew/bin")
|| exe_path_str.contains("Cellar")
|| exe_path_str.contains("/opt/homebrew/")
|| exe_path_str.contains("/usr/local/bin/")
{
println!(" Installation method: {}", "Homebrew".cyan());
println!(" Running: brew upgrade s2-streamstore/s2/s2");
println!();

let status = Command::new("brew")
.args(["upgrade", "s2-streamstore/s2/s2"])
.status()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to run brew upgrade: {}", e))
})?;

if status.success() {
println!(
" {} {}",
"Success:".green().bold(),
"Updated via Homebrew".green()
);
} else {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Homebrew upgrade failed with exit code: {}",
status
)));
}
} else if exe_path_str.contains(".cargo")
|| exe_path_str.contains("target/release")
|| exe_path_str.contains("target/debug")
{
println!(" Installation method: {}", "Cargo".cyan());
println!(" Running: cargo install --locked --force streamstore-cli");
println!();

let status = Command::new("cargo")
.args(["install", "--locked", "--force", "streamstore-cli"])
.status()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to run cargo install: {}", e))
})?;

if status.success() {
println!(
" {} {}",
"Success:".green().bold(),
"Updated via Cargo".green()
);
} else {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Cargo install failed with exit code: {}",
status
)));
}
} else {
println!(" Installation method: {}", "Binary".cyan());

let (_os, arch) = if cfg!(target_os = "macos") {
if cfg!(target_arch = "aarch64") {
("aarch64-apple-darwin", "aarch64-apple-darwin.zip")
} else {
("x86_64-apple-darwin", "x86_64-apple-darwin.zip")
}
} else if cfg!(target_os = "linux") {
if cfg!(target_arch = "aarch64") {
("aarch64-unknown-linux-gnu", "aarch64-unknown-linux-gnu.zip")
} else {
("x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu.zip")
}
} else {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Unsupported OS: {}",
std::env::consts::OS
)));
};

let download_url = format!(
"https://github.com/s2-streamstore/s2-cli/releases/download/{latest_version}/s2-{arch}"
);

println!(" Downloading: {}", download_url.dimmed());

let temp_dir = std::env::temp_dir();
let extract_dir = temp_dir.join(format!("s2-update-{latest_version}"));
let zip_file = temp_dir.join(format!("s2-{latest_version}.zip"));

tokio::fs::create_dir_all(&extract_dir).await.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to create temp directory: {}", e))
})?;

let download_status = Command::new("curl")
.args([
"-L",
"-o",
zip_file.to_str().unwrap(),
"-H",
"User-Agent: s2-cli-updater",
&download_url,
])
.status()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to download binary: {}", e))
})?;

if !download_status.success() {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Failed to download binary with exit code: {}",
download_status
)));
}

println!(" Extracting binary...");

let unzip_status = Command::new("unzip")
.args([
"-q",
"-o",
zip_file.to_str().unwrap(),
"-d",
extract_dir.to_str().unwrap(),
])
.status()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to extract zip file: {}", e))
})?;

if !unzip_status.success() {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Failed to extract zip file with exit code: {}",
unzip_status
)));
}

let extracted_binary = extract_dir.join("s2");

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = tokio::fs::metadata(&extracted_binary)
.await
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to get file metadata: {}", e))
})?
.permissions();
perms.set_mode(0o755);
tokio::fs::set_permissions(&extracted_binary, perms)
.await
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!(
"Failed to set executable permissions: {}",
e
))
})?;
}

println!(" Replacing binary...");

let status = Command::new("mv")
.args([
extracted_binary.to_str().unwrap(),
exe_path.to_str().unwrap(),
])
.status();

if let Ok(status) = status {
if status.success() {
println!(
" {} {}",
"Success:".green().bold(),
"Binary updated".green()
);

let _ = tokio::fs::remove_file(zip_file).await;
let _ = tokio::fs::remove_dir_all(extract_dir).await;
} else {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Failed to replace binary with exit code: {}",
status
)));
}
} else {
println!(" Trying alternative update method...");
let cp_status = Command::new("cp")
.args([
extracted_binary.to_str().unwrap(),
exe_path.to_str().unwrap(),
])
.status()
.map_err(|e| {
S2CliError::InvalidArgs(miette::miette!("Failed to copy binary: {}", e))
})?;

if cp_status.success() {
println!(
" {} {}",
"Success:".green().bold(),
"Binary updated (using copy method)".green()
);

let _ = tokio::fs::remove_file(zip_file).await;
let _ = tokio::fs::remove_dir_all(extract_dir).await;
} else {
return Err(S2CliError::InvalidArgs(miette::miette!(
"Failed to copy binary with exit code: {}",
cp_status
)));
}
}
}

println!();
println!(
" {} Update completed successfully!",
"Success:".green().bold()
);
println!();
Ok(())
}

Expand Down
Loading