use colored::Colorize;
use std::io::{self, Write};
use std::process::Command;
const CRATE: &str = "whypkg";
#[derive(clap::Subcommand)]
pub enum Cmd {
#[command(verbatim_doc_comment)]
Update(UpdateArgs),
Check,
}
#[derive(clap::Args)]
pub struct UpdateArgs {
#[arg(short, long)]
pub yes: bool,
}
pub fn run(cmd: Cmd) {
match cmd {
Cmd::Update(args) => update(args),
Cmd::Check => check(),
}
}
fn update(args: UpdateArgs) {
if !args.yes && !confirm() {
println!("{}", "Aborted.".dimmed());
return;
}
println!(
"{} {}\n",
"Updating whypkg via".dimmed(),
"cargo install whypkg --force".bold()
);
match Command::new("cargo")
.args(["install", CRATE, "--force"])
.status()
{
Ok(status) if status.success() => {
println!("\n{}", "✓ whypkg is up to date.".green());
}
Ok(status) => {
eprintln!("\n{}", "✗ update failed.".red());
std::process::exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("{} {e}", "whypkg: could not run cargo:".red());
eprintln!(
"{}",
"is cargo installed and on your PATH? (https://rustup.rs)".dimmed()
);
std::process::exit(127);
}
}
}
fn check() {
let current = env!("CARGO_PKG_VERSION");
match latest() {
Ok(latest) if newer(&latest, current) => {
println!(
"{} {} {}",
format!("whypkg {latest}").bold(),
"is available, you have".dimmed(),
current.bold()
);
println!("{} {}", "run".dimmed(), "whypkg self update".bold());
}
Ok(_) => println!(
"{} {}",
format!("whypkg {current}").bold(),
"is the latest release.".dimmed()
),
Err(e) => {
eprintln!("{} {e}", "whypkg: could not reach crates.io:".red());
std::process::exit(1);
}
}
}
fn latest() -> Result<String, String> {
let out = Command::new("cargo")
.args(["search", CRATE, "--limit", "1"])
.output()
.map_err(|e| format!("could not run cargo: {e}"))?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
}
let text = String::from_utf8_lossy(&out.stdout);
let prefix = format!("{CRATE} = \"");
text.lines()
.find_map(|l| l.strip_prefix(&prefix))
.and_then(|rest| rest.split('"').next())
.map(str::to_string)
.ok_or_else(|| format!("the registry did not list `{CRATE}`"))
}
fn newer(a: &str, b: &str) -> bool {
let fields = |v: &str| {
v.split(['.', '-'])
.map(|p| p.parse::<u64>().unwrap_or(0))
.collect::<Vec<_>>()
};
fields(a) > fields(b)
}
fn confirm() -> bool {
print!(
"{} {} ",
"Update whypkg to the latest release via cargo?".bold(),
"[y/N]".dimmed()
);
io::stdout().flush().ok();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
}
#[cfg(test)]
mod tests {
use super::newer;
#[test]
fn a_newer_release_is_recognised_field_by_field() {
assert!(newer("0.10.0", "0.9.9"));
assert!(newer("1.0.0", "0.9.9"));
assert!(newer("0.4.2", "0.4.1"));
assert!(!newer("0.4.1", "0.4.1"));
assert!(!newer("0.4.0", "0.4.1"));
assert!(!newer("0.9.9", "0.10.0"));
}
}