1use serde::Deserialize;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct UpdateNotice {
7 pub name: String,
8 pub current: String,
9 pub latest: String,
10 pub command: String,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct UpdateWarning {
15 pub message: String,
16 pub command: String,
17}
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct UpdateReport {
21 pub notices: Vec<UpdateNotice>,
22 pub warnings: Vec<UpdateWarning>,
23}
24
25impl UpdateReport {
26 pub fn is_empty(&self) -> bool {
27 self.notices.is_empty() && self.warnings.is_empty()
28 }
29}
30
31#[derive(Debug, Deserialize)]
32struct CratesResponse {
33 #[serde(rename = "crate")]
34 crate_info: CrateInfo,
35}
36#[derive(Debug, Deserialize)]
37struct CrateInfo {
38 max_version: String,
39}
40
41fn updates_disabled() -> bool {
42 crate::args::offline_env_enabled() || std::env::var_os("RPI_DISABLE_UPDATE_CHECK").is_some()
43}
44
45pub async fn check_rpi_startup() -> UpdateReport {
46 if updates_disabled() {
47 return UpdateReport::default();
48 }
49 let client = match reqwest::Client::builder()
50 .timeout(std::time::Duration::from_secs(2))
51 .user_agent(format!("rpi/{}", crate::VERSION))
52 .build()
53 {
54 Ok(c) => c,
55 Err(_) => return UpdateReport::default(),
56 };
57 let url = "https://crates.io/api/v1/crates/rpi-cli";
58 let latest = match client
59 .get(url)
60 .send()
61 .await
62 .and_then(|r| r.error_for_status())
63 {
64 Ok(response) => match response.json::<CratesResponse>().await {
65 Ok(v) => v.crate_info.max_version,
66 Err(_) => return UpdateReport::default(),
67 },
68 Err(_) => return UpdateReport::default(),
69 };
70 if is_newer(crate::VERSION, &latest) {
71 UpdateReport {
72 notices: vec![UpdateNotice {
73 name: "rpi".into(),
74 current: crate::VERSION.into(),
75 latest,
76 command: "rpi update".into(),
77 }],
78 warnings: Vec::new(),
79 }
80 } else {
81 UpdateReport::default()
82 }
83}
84
85pub fn is_newer(current: &str, latest: &str) -> bool {
86 match (
87 semver::Version::parse(current),
88 semver::Version::parse(latest),
89 ) {
90 (Ok(a), Ok(b)) => b > a,
91 _ => current != latest,
92 }
93}
94
95pub fn print_startup_notices(report: &UpdateReport) {
96 for notice in &report.notices {
97 eprintln!(
98 "update available: {} {} -> {} ({})",
99 notice.name, notice.current, notice.latest, notice.command
100 );
101 }
102 for warning in &report.warnings {
103 eprintln!("warning: {} ({})", warning.message, warning.command);
104 }
105}
106
107pub fn run_self_update(_args: &[String]) -> i32 {
108 let status = std::process::Command::new("cargo")
109 .args(["install", "rpi-cli", "--locked"])
110 .status();
111 match status {
112 Ok(s) if s.success() => 0,
113 Ok(s) => s.code().unwrap_or(1),
114 Err(e) => {
115 eprintln!("rpi update failed: {e}");
116 1
117 }
118 }
119}