use crate::cloud;
use crate::daemon;
use crate::errors::{Result, TokenSaveError};
fn asset_name(version: &str, is_beta: bool) -> String {
let prefix = if is_beta { "tokensave-beta" } else { "tokensave" };
let platform = current_platform();
let ext = if cfg!(windows) { "zip" } else { "tar.gz" };
format!("{prefix}-v{version}-{platform}.{ext}")
}
fn current_platform() -> &'static str {
if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
"aarch64-macos"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
"x86_64-macos"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-linux"
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
"aarch64-linux"
} else if cfg!(target_os = "windows") {
"x86_64-windows"
} else {
"unknown"
}
}
fn release_tag(version: &str) -> String {
format!("v{version}")
}
pub fn run_upgrade() -> Result<String> {
let current = env!("CARGO_PKG_VERSION");
let is_beta = cloud::is_beta();
let channel = if is_beta { "beta" } else { "stable" };
eprintln!("Current version: v{current} ({channel} channel)");
eprintln!("Checking for updates...");
let latest = cloud::fetch_latest_version().ok_or_else(|| TokenSaveError::Config {
message: "failed to check for updates — could not reach GitHub".to_string(),
})?;
if !cloud::is_newer_version(current, &latest) {
eprintln!("\x1b[32m✔\x1b[0m Already up to date (v{current}).");
return Err(TokenSaveError::Config {
message: format!("already at latest version v{current}"),
});
}
let tag = release_tag(&latest);
let expected_asset = asset_name(&latest, is_beta);
let bin_name = if cfg!(windows) { "tokensave.exe" } else { "tokensave" };
eprintln!("Upgrading v{current} → v{latest}...");
eprintln!(" Asset: {expected_asset}");
let daemon_was_running = daemon::running_daemon_pid().is_some();
if daemon_was_running {
eprintln!(" Stopping daemon...");
daemon::stop().ok(); }
let target_suffix = if is_beta {
format!("beta-v{}-{}", latest, current_platform())
} else {
format!("v{}-{}", latest, current_platform())
};
let result = self_update::backends::github::Update::configure()
.repo_owner("aovestdipaperino")
.repo_name("tokensave")
.bin_name(bin_name)
.target(&target_suffix)
.current_version(current)
.target_version_tag(&tag)
.show_download_progress(true)
.no_confirm(true)
.build()
.map_err(|e| TokenSaveError::Config {
message: format!("failed to configure updater: {e}"),
})?
.update();
match result {
Ok(status) => {
eprintln!(
"\x1b[32m✔\x1b[0m Successfully upgraded to v{}!",
status.version()
);
if daemon_was_running {
eprintln!(" Restarting daemon...");
restart_daemon();
}
Ok(status.version().to_string())
}
Err(e) => {
if daemon_was_running {
eprintln!(" Restarting daemon (upgrade failed, old version still in place)...");
restart_daemon();
}
let err_str = e.to_string();
let message = if err_str.contains("No asset found") {
format!(
"upgrade failed: release v{latest} exists but binaries are not yet available \
for your platform ({}).\n \
This usually means the CI build is still in progress — try again in a few minutes.\n \
If the problem persists, download manually from:\n \
https://github.com/aovestdipaperino/tokensave/releases/tag/{tag}",
current_platform(), tag = tag,
)
} else {
format!("upgrade failed: {e}")
};
Err(TokenSaveError::Config { message })
}
}
}
pub fn show_channel() {
let current = env!("CARGO_PKG_VERSION");
let channel = if cloud::is_beta() { "beta" } else { "stable" };
eprintln!("v{current} ({channel})");
}
pub fn switch_channel(target_channel: &str) -> Result<String> {
let current = env!("CARGO_PKG_VERSION");
let current_is_beta = cloud::is_beta();
let current_channel = if current_is_beta { "beta" } else { "stable" };
let target_is_beta = match target_channel {
"beta" => true,
"stable" => false,
other => {
return Err(TokenSaveError::Config {
message: format!(
"unknown channel '{other}'. Valid channels: stable, beta"
),
});
}
};
if target_is_beta == current_is_beta {
eprintln!("Already on the {current_channel} channel (v{current}).");
eprintln!("Run `tokensave upgrade` to check for updates within this channel.");
return Err(TokenSaveError::Config {
message: format!("already on {current_channel} channel"),
});
}
eprintln!("Switching from {current_channel} to {target_channel}...");
let latest = if target_is_beta {
cloud::fetch_latest_beta_version()
} else {
cloud::fetch_latest_stable_version()
}
.ok_or_else(|| TokenSaveError::Config {
message: format!(
"failed to find latest {target_channel} release — could not reach GitHub"
),
})?;
let tag = release_tag(&latest);
let expected_asset = asset_name(&latest, target_is_beta);
let bin_name = if cfg!(windows) { "tokensave.exe" } else { "tokensave" };
eprintln!(" Target: v{latest}");
eprintln!(" Asset: {expected_asset}");
let daemon_was_running = daemon::running_daemon_pid().is_some();
if daemon_was_running {
eprintln!(" Stopping daemon...");
daemon::stop().ok();
}
let target_suffix = if target_is_beta {
format!("beta-v{}-{}", latest, current_platform())
} else {
format!("v{}-{}", latest, current_platform())
};
let result = self_update::backends::github::Update::configure()
.repo_owner("aovestdipaperino")
.repo_name("tokensave")
.bin_name(bin_name)
.target(&target_suffix)
.current_version(current)
.target_version_tag(&tag)
.show_download_progress(true)
.no_confirm(true)
.build()
.map_err(|e| TokenSaveError::Config {
message: format!("failed to configure updater: {e}"),
})?
.update();
match result {
Ok(status) => {
eprintln!(
"\x1b[32m✔\x1b[0m Switched to {target_channel} channel: v{}",
status.version()
);
if daemon_was_running {
eprintln!(" Restarting daemon...");
restart_daemon();
}
Ok(status.version().to_string())
}
Err(e) => {
if daemon_was_running {
eprintln!(" Restarting daemon (switch failed, old version still in place)...");
restart_daemon();
}
let err_str = e.to_string();
let message = if err_str.contains("No asset found") {
format!(
"channel switch failed: v{latest} binaries not yet available for {}.\n \
CI build may still be in progress — try again in a few minutes.",
current_platform(),
)
} else {
format!("channel switch failed: {e}")
};
Err(TokenSaveError::Config { message })
}
}
}
fn restart_daemon() {
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!(
" \x1b[33mwarning:\x1b[0m could not determine executable path to restart daemon: {e}"
);
return;
}
};
match std::process::Command::new(&exe)
.arg("daemon")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(_) => eprintln!(" \x1b[32m✔\x1b[0m Daemon restarted"),
Err(e) => eprintln!(
" \x1b[33mwarning:\x1b[0m failed to restart daemon: {e}"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_asset_name_stable() {
let name = asset_name("3.3.3", false);
assert!(name.starts_with("tokensave-v3.3.3-"));
assert!(!name.contains("beta"));
if cfg!(windows) {
assert!(name.ends_with(".zip"));
} else {
assert!(name.ends_with(".tar.gz"));
}
}
#[test]
fn test_asset_name_beta() {
let name = asset_name("4.0.2-beta.1", true);
assert!(name.starts_with("tokensave-beta-v4.0.2-beta.1-"));
if cfg!(windows) {
assert!(name.ends_with(".zip"));
} else {
assert!(name.ends_with(".tar.gz"));
}
}
#[test]
fn test_release_tag() {
assert_eq!(release_tag("3.3.3"), "v3.3.3");
assert_eq!(release_tag("4.0.2-beta.1"), "v4.0.2-beta.1");
}
#[test]
fn test_current_platform_not_unknown() {
assert_ne!(current_platform(), "unknown");
}
#[test]
fn test_asset_name_matches_ci_convention() {
let stable = asset_name("3.3.3", false);
let platform = current_platform();
if cfg!(windows) {
assert_eq!(stable, format!("tokensave-v3.3.3-{platform}.zip"));
} else {
assert_eq!(stable, format!("tokensave-v3.3.3-{platform}.tar.gz"));
}
let beta = asset_name("4.0.2-beta.1", true);
if cfg!(windows) {
assert_eq!(beta, format!("tokensave-beta-v4.0.2-beta.1-{platform}.zip"));
} else {
assert_eq!(beta, format!("tokensave-beta-v4.0.2-beta.1-{platform}.tar.gz"));
}
}
}