use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use crate::commands::plugin::{self, PluginMeta};
const INSTALL_SH_URL: &str =
"https://raw.githubusercontent.com/Rushit/tonin/main/scripts/install.sh";
const TONIN_REPO: &str = "Rushit/tonin";
#[derive(clap::Args, Default)]
pub struct UpgradeArgs {
#[arg(long)]
pub check: bool,
#[arg(short, long)]
pub yes: bool,
#[arg(long, value_name = "vX.Y.Z")]
pub version: Option<String>,
#[arg(long, value_name = "DIR")]
pub dir: Option<PathBuf>,
#[arg(long)]
pub self_only: bool,
}
struct PlanRow {
label: String,
current: String,
target: String,
}
struct UpgradablePlugin {
repo: String,
}
pub fn run(args: UpgradeArgs) -> Result<()> {
let current_tonin = env!("CARGO_PKG_VERSION").to_string();
let dir = args.dir.clone().or_else(|| {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(Path::to_path_buf))
});
let want_tonin = args
.version
.clone()
.or_else(|| latest_tag(TONIN_REPO))
.unwrap_or_else(|| "latest".to_string());
let mut rows = vec![PlanRow {
label: "tonin".to_string(),
current: current_tonin,
target: want_tonin,
}];
let mut upgradable: Vec<UpgradablePlugin> = Vec::new();
let mut unmanaged: Vec<(String, String)> = Vec::new();
if !args.self_only {
for (name, path) in plugin::find_plugins() {
let meta = plugin::query_meta(&path);
let current = plugin_version(&meta, &path);
match meta.as_ref().and_then(|m| m.repo.clone()) {
Some(repo) => {
let target = latest_tag(&repo).unwrap_or_else(|| "latest".to_string());
rows.push(PlanRow {
label: format!("tonin-{name}"),
current,
target,
});
upgradable.push(UpgradablePlugin { repo });
}
None => unmanaged.push((name, current)),
}
}
}
print_plan(&rows, &unmanaged, dir.as_deref());
if args.check {
return Ok(());
}
if rows.iter().all(|r| !r.is_upgrade()) {
println!("\nEverything is up to date.");
return Ok(());
}
if !args.yes && !confirm("Proceed with upgrade?") {
eprintln!("aborted.");
return Ok(());
}
run_install_script(dir.as_deref(), args.version.as_deref(), &upgradable)
}
impl PlanRow {
fn is_upgrade(&self) -> bool {
!matches!(self.target.as_str(), "latest" | "?")
&& plugin::version_lt(&self.current, &self.target)
}
fn status(&self) -> &'static str {
match self.target.as_str() {
"latest" | "?" => "(latest unknown — will resolve)",
_ if plugin::version_lt(&self.current, &self.target) => "⬆ upgrade",
_ => "✓ up to date",
}
}
}
fn print_plan(rows: &[PlanRow], unmanaged: &[(String, String)], dir: Option<&Path>) {
println!("Upgrade plan:\n");
for r in rows {
let target = r.target.trim_start_matches('v');
println!(
" {:<18} {:<10} → {:<10} {}",
r.label,
r.current,
target,
r.status()
);
}
for (name, current) in unmanaged {
let label = format!("tonin-{name}");
println!(
" {label:<18} {current:<10} (no repo metadata — upgrade with `cargo binstall`)"
);
}
if let Some(d) = dir {
println!("\nInstall directory: {}", d.display());
}
}
fn plugin_version(meta: &Option<PluginMeta>, path: &Path) -> String {
meta.as_ref()
.map(|m| m.version.clone())
.or_else(|| plugin::query_version(path))
.unwrap_or_else(|| "?".to_string())
}
fn latest_tag(repo: &str) -> Option<String> {
let url = format!("https://api.github.com/repos/{repo}/releases/latest");
let output = Command::new("curl")
.args([
"-sSf",
"-H",
"User-Agent: tonin-cli",
"-H",
"Accept: application/vnd.github+json",
&url,
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let json: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
json.get("tag_name")?.as_str().map(str::to_string)
}
fn confirm(prompt: &str) -> bool {
dialoguer::Confirm::new()
.with_prompt(prompt)
.default(true)
.interact()
.unwrap_or(false)
}
fn run_install_script(
dir: Option<&Path>,
version: Option<&str>,
plugins: &[UpgradablePlugin],
) -> Result<()> {
let tmp = std::env::temp_dir().join("tonin-install.sh");
let status = Command::new("curl")
.args([
"-sSfL",
"--proto",
"=https",
"--tlsv1.2",
INSTALL_SH_URL,
"-o",
])
.arg(&tmp)
.status()
.context("running curl to download install.sh (is curl installed?)")?;
if !status.success() {
bail!("failed to download install.sh from {INSTALL_SH_URL}");
}
let mut cmd = Command::new("bash");
cmd.arg(&tmp);
if let Some(d) = dir {
cmd.arg("--dir").arg(d);
}
if let Some(v) = version {
cmd.arg("--version").arg(v);
}
for p in plugins {
cmd.arg("--plugin").arg(&p.repo);
}
let status = cmd.status().context("running install.sh")?;
let _ = std::fs::remove_file(&tmp);
if !status.success() {
bail!("install.sh exited with a failure");
}
println!("\n✓ Upgrade complete. Run `tonin --version` to verify.");
Ok(())
}