cargo_upgrade_command/
lib.rs1use std::env;
2use std::fs::{create_dir, remove_dir_all, rename};
3use std::io::{BufRead, BufReader, Result};
4
5use std::process::{Command, Stdio};
6use std::time::Instant;
7
8use colored::Colorize;
9use spinoff::{spinners, Color, Spinner};
10
11pub struct Package {
12 name: String,
13 version: String,
14 new_version: Option<String>,
15}
16
17impl Package {
18 fn to_formatted(&self) -> Self {
19 let name = self.name.to_string();
20 let old_version = format!("v{}", self.version).bright_red();
21 let new_version = format!("v{}", self.new_version.as_ref().unwrap()).bright_green();
22
23 Self {
24 name,
25 version: old_version.to_string(),
26 new_version: Some(new_version.to_string()),
27 }
28 }
29}
30
31pub fn get_installed_packages() -> Result<Vec<Package>> {
37 let output = Command::new("cargo").args(["install", "--list"]).output()?;
38
39 let text = String::from_utf8_lossy(&output.stdout);
40
41 let mut packages = Vec::new();
42 for line in text.lines() {
43 if line.ends_with(':') {
44 let parts: Vec<_> = line.splitn(2, ' ').collect();
45 if parts.len() == 2 && parts[1].starts_with('v') {
46 let name = parts[0].trim().to_string();
47 let version = parts[1]
48 .trim()
49 .trim_end_matches(':')
50 .trim_start_matches('v')
51 .to_string();
52 packages.push(Package {
53 name,
54 version,
55 new_version: None,
56 });
57 }
58 }
59 }
60
61 Ok(packages)
62}
63
64pub fn get_outdated_packages() -> Result<Vec<Package>> {
70 let spinner = Spinner::new(
71 spinners::Dots,
72 "Scanning for outdated crates...",
73 Color::Cyan,
74 );
75
76 let packages = get_installed_packages()?;
77
78 let mut outdated_packages = Vec::new();
79
80 for package in &packages {
81 let output = Command::new("cargo")
82 .args(["search", &package.name, "--limit=1", "--color=never", "-q"])
83 .output()?;
84 let text = String::from_utf8_lossy(&output.stdout);
85
86 let prefix = format!("{} = \"", package.name);
87
88 if !text.starts_with(&prefix) {
89 continue;
90 }
91
92 let value_start = prefix.len();
93 let quote_end = text[value_start..].find('"').unwrap();
94 let latest_version = text[value_start..value_start + quote_end].to_string();
95
96 if latest_version != package.version {
97 outdated_packages.push(Package {
98 name: package.name.to_string(),
99 version: package.version.clone(),
100 new_version: Some(latest_version),
101 });
102 }
103 }
104
105 spinner.clear();
106
107 Ok(outdated_packages)
108}
109
110pub fn show_outdated_packages() -> Result<()> {
116 let outdated_packages = get_outdated_packages()?;
117 if outdated_packages.is_empty() {
118 return Ok(());
119 }
120 println!("Outdated global cargo crates:");
121 println!("===============================");
122 for package in outdated_packages {
123 let formatted = package.to_formatted();
124
125 println!(
126 "📦 {}: {} -> {}",
127 formatted.name,
128 formatted.version,
129 formatted.new_version.unwrap()
130 );
131 }
132
133 Ok(())
134}
135
136pub fn update_package(name: &str) -> Result<()> {
143 let mut spinner = Spinner::new(spinners::Dots, "Loading...", Color::Cyan);
144
145 let start_time = Instant::now();
146
147 let mut cmd = Command::new("cargo")
148 .args(["install", name, "--locked"])
149 .stderr(Stdio::piped())
150 .stdout(Stdio::piped())
151 .stdin(Stdio::piped())
152 .spawn()?;
153
154 let reader = BufReader::new(cmd.stderr.take().unwrap());
155
156 let mut last_line = String::new();
157
158 for line in reader.lines() {
159 last_line = line?;
160 spinner.update_text(last_line.trim().to_string());
161 }
162
163 let status_code = cmd.wait()?;
164
165 let status = format!("{} [{:.2?}]", last_line.trim(), start_time.elapsed());
166
167 match status_code.code().unwrap_or(1) {
168 0 => spinner.success(&status),
169 1 => spinner.fail(&status),
170 _ => spinner.warn(&status),
171 }
172
173 Ok(())
174}
175
176pub fn update_all_packages() -> Result<()> {
182 let packages = get_outdated_packages()?;
183
184 let mut done_one = false;
185
186 for package in packages {
187 if package.new_version.is_none() {
188 continue;
189 }
190 let formatted = package.to_formatted();
191 if done_one {
192 println!();
193 }
194 if package.name == env!("CARGO_PKG_NAME") {
195 if cfg!(debug_assertions) {
196 println!("Skipping self update in debug mode");
197 continue;
198 }
199 move_executable_to_temp_folder()?;
200 }
201 println!(
202 "Upgrading {} from {} to {}",
203 formatted.name,
204 formatted.version,
205 formatted.new_version.unwrap()
206 );
207 update_package(&package.name)?;
208 done_one = true;
209 }
210
211 Ok(())
212}
213
214fn move_executable_to_temp_folder() -> Result<()> {
215 let current_exe = env::current_exe()?;
216 let temp_dir = env::temp_dir();
217
218 let cloned_exe_dir = temp_dir.join(env!("CARGO_PKG_NAME"));
220 if cloned_exe_dir.exists() {
221 remove_dir_all(&cloned_exe_dir)?;
222 }
223
224 create_dir(&cloned_exe_dir)?;
225
226 let mut cloned_exe_path = cloned_exe_dir.join(current_exe.file_name().unwrap());
227 let mut i = 0;
228 while cloned_exe_path.exists() {
229 i += 1;
230
231 cloned_exe_path = cloned_exe_dir.join(format!(
232 "{}-{i}",
233 current_exe.file_stem().unwrap().to_str().unwrap()
234 ));
235 if cfg!(windows) {
236 cloned_exe_path.set_extension("exe");
237 }
238 }
239
240 rename(¤t_exe, &cloned_exe_path)?;
242
243 Ok(())
244}