1use crate::error::{DevSweepError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::time::SystemTime;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GlobalCache {
14 pub name: String,
16 pub id: &'static str,
18 pub icon: &'static str,
20 pub path: PathBuf,
22 pub size: u64,
24 pub file_count: u64,
26 pub last_modified: Option<SystemTime>,
28 pub clean_command: Option<&'static str>,
30 pub description: &'static str,
32}
33
34impl GlobalCache {
35 pub fn exists(&self) -> bool {
37 self.path.exists() && self.path.is_dir()
38 }
39
40 pub fn age_days(&self) -> Option<u64> {
42 self.last_modified
43 .and_then(|t| t.elapsed().ok())
44 .map(|d| d.as_secs() / 86400)
45 }
46
47 pub fn last_used_display(&self) -> String {
49 match self.age_days() {
50 Some(0) => "today".to_string(),
51 Some(1) => "yesterday".to_string(),
52 Some(d) if d < 7 => format!("{} days ago", d),
53 Some(d) if d < 30 => format!("{} weeks ago", d / 7),
54 Some(d) if d < 365 => format!("{} months ago", d / 30),
55 Some(d) => format!("{} years ago", d / 365),
56 None => "unknown".to_string(),
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct CacheDefinition {
64 pub id: &'static str,
65 pub name: &'static str,
66 pub icon: &'static str,
67 pub paths: &'static [&'static str],
69 pub clean_command: Option<&'static str>,
71 pub description: &'static str,
72}
73
74pub fn known_caches() -> Vec<CacheDefinition> {
76 vec![
77 CacheDefinition {
81 id: "npm",
82 name: "npm cache",
83 icon: "📦",
84 paths: &[".npm/_cacache"],
85 clean_command: Some("npm cache clean --force"),
86 description: "Cached npm packages and metadata",
87 },
88 CacheDefinition {
89 id: "yarn",
90 name: "Yarn cache",
91 icon: "🧶",
92 paths: &[".yarn/cache", ".cache/yarn"],
93 clean_command: Some("yarn cache clean"),
94 description: "Cached Yarn packages",
95 },
96 CacheDefinition {
97 id: "pnpm",
98 name: "pnpm store",
99 icon: "📦",
100 paths: &[".pnpm-store", ".local/share/pnpm/store"],
101 clean_command: Some("pnpm store prune"),
102 description: "Global pnpm content-addressable store",
103 },
104 CacheDefinition {
105 id: "bun",
106 name: "Bun cache",
107 icon: "🥟",
108 paths: &[".bun/install/cache"],
109 clean_command: None,
110 description: "Cached Bun packages",
111 },
112 CacheDefinition {
113 id: "deno",
114 name: "Deno cache",
115 icon: "🦕",
116 paths: &[".cache/deno", ".deno"],
117 clean_command: Some("deno cache --reload"),
118 description: "Cached Deno modules and compiled scripts",
119 },
120
121 CacheDefinition {
125 id: "pip",
126 name: "pip cache",
127 icon: "🐍",
128 paths: &[".cache/pip", "Library/Caches/pip"],
129 clean_command: Some("pip cache purge"),
130 description: "Cached pip wheels and HTTP responses",
131 },
132 CacheDefinition {
133 id: "uv",
134 name: "uv cache",
135 icon: "⚡",
136 paths: &[".cache/uv"],
137 clean_command: Some("uv cache clean"),
138 description: "Cached uv packages (fast Python installer)",
139 },
140 CacheDefinition {
141 id: "poetry",
142 name: "Poetry cache",
143 icon: "📜",
144 paths: &[".cache/pypoetry", "Library/Caches/pypoetry"],
145 clean_command: Some("poetry cache clear --all ."),
146 description: "Cached Poetry packages and virtualenvs",
147 },
148 CacheDefinition {
149 id: "pipenv",
150 name: "Pipenv cache",
151 icon: "🐍",
152 paths: &[".cache/pipenv"],
153 clean_command: None,
154 description: "Cached Pipenv packages",
155 },
156 CacheDefinition {
157 id: "conda",
158 name: "Conda cache",
159 icon: "🐍",
160 paths: &[".conda/pkgs", "anaconda3/pkgs", "miniconda3/pkgs"],
161 clean_command: Some("conda clean --all"),
162 description: "Cached Conda packages",
163 },
164
165 CacheDefinition {
169 id: "cargo-registry",
170 name: "Cargo registry",
171 icon: "🦀",
172 paths: &[".cargo/registry"],
173 clean_command: None, description: "Downloaded crate sources and indices",
175 },
176 CacheDefinition {
177 id: "cargo-git",
178 name: "Cargo git",
179 icon: "🦀",
180 paths: &[".cargo/git"],
181 clean_command: None,
182 description: "Git dependencies cache",
183 },
184
185 CacheDefinition {
189 id: "go-mod",
190 name: "Go modules",
191 icon: "🐹",
192 paths: &["go/pkg/mod"],
193 clean_command: Some("go clean -modcache"),
194 description: "Downloaded Go module cache",
195 },
196 CacheDefinition {
197 id: "go-build",
198 name: "Go build cache",
199 icon: "🐹",
200 paths: &[".cache/go-build", "Library/Caches/go-build"],
201 clean_command: Some("go clean -cache"),
202 description: "Go build artifacts cache",
203 },
204
205 CacheDefinition {
209 id: "gradle",
210 name: "Gradle cache",
211 icon: "🐘",
212 paths: &[".gradle/caches"],
213 clean_command: None, description: "Gradle dependencies and build cache",
215 },
216 CacheDefinition {
217 id: "maven",
218 name: "Maven repository",
219 icon: "🪶",
220 paths: &[".m2/repository"],
221 clean_command: None,
222 description: "Maven local repository",
223 },
224 CacheDefinition {
225 id: "sbt",
226 name: "SBT cache",
227 icon: "📦",
228 paths: &[".sbt", ".ivy2/cache"],
229 clean_command: None,
230 description: "SBT and Ivy dependency cache",
231 },
232
233 CacheDefinition {
237 id: "nuget",
238 name: "NuGet cache",
239 icon: "🔷",
240 paths: &[".nuget/packages"],
241 clean_command: Some("dotnet nuget locals all --clear"),
242 description: "NuGet package cache",
243 },
244
245 CacheDefinition {
249 id: "gem",
250 name: "Ruby gems",
251 icon: "💎",
252 paths: &[".gem", ".local/share/gem"],
253 clean_command: Some("gem cleanup"),
254 description: "Installed Ruby gems",
255 },
256 CacheDefinition {
257 id: "bundler",
258 name: "Bundler cache",
259 icon: "💎",
260 paths: &[".bundle/cache"],
261 clean_command: Some("bundle clean --force"),
262 description: "Bundler gem cache",
263 },
264
265 CacheDefinition {
269 id: "composer",
270 name: "Composer cache",
271 icon: "🎼",
272 paths: &[".composer/cache", ".cache/composer"],
273 clean_command: Some("composer clear-cache"),
274 description: "Composer package cache",
275 },
276
277 CacheDefinition {
281 id: "cocoapods",
282 name: "CocoaPods cache",
283 icon: "🍫",
284 paths: &["Library/Caches/CocoaPods"],
285 clean_command: Some("pod cache clean --all"),
286 description: "CocoaPods specs and pod cache",
287 },
288 CacheDefinition {
289 id: "pub",
290 name: "Dart/Flutter pub",
291 icon: "🎯",
292 paths: &[".pub-cache"],
293 clean_command: None,
294 description: "Dart and Flutter package cache",
295 },
296 CacheDefinition {
297 id: "android-gradle",
298 name: "Android Gradle",
299 icon: "🤖",
300 paths: &[".android/cache", ".android/build-cache"],
301 clean_command: None,
302 description: "Android build cache",
303 },
304
305 CacheDefinition {
309 id: "huggingface",
310 name: "Hugging Face cache",
311 icon: "🤗",
312 paths: &[".cache/huggingface"],
313 clean_command: None,
314 description: "Downloaded ML models and datasets",
315 },
316 CacheDefinition {
317 id: "torch",
318 name: "PyTorch cache",
319 icon: "🔥",
320 paths: &[".cache/torch"],
321 clean_command: None,
322 description: "PyTorch model hub cache",
323 },
324
325 CacheDefinition {
329 id: "homebrew",
330 name: "Homebrew cache",
331 icon: "🍺",
332 paths: &["Library/Caches/Homebrew"],
333 clean_command: Some("brew cleanup --prune=all"),
334 description: "Downloaded Homebrew bottles and source",
335 },
336 CacheDefinition {
337 id: "cypress",
338 name: "Cypress cache",
339 icon: "🌲",
340 paths: &[".cache/Cypress", "Library/Caches/Cypress"],
341 clean_command: Some("cypress cache clear"),
342 description: "Cypress browser binaries",
343 },
344 CacheDefinition {
345 id: "playwright",
346 name: "Playwright cache",
347 icon: "🎭",
348 paths: &[".cache/ms-playwright", "Library/Caches/ms-playwright"],
349 clean_command: None,
350 description: "Playwright browser binaries",
351 },
352 CacheDefinition {
353 id: "electron",
354 name: "Electron cache",
355 icon: "⚛️",
356 paths: &[".cache/electron", "Library/Caches/electron"],
357 clean_command: None,
358 description: "Electron framework binaries",
359 },
360 ]
361}
362
363pub fn detect_caches() -> Result<Vec<GlobalCache>> {
365 let home = dirs::home_dir()
366 .ok_or_else(|| DevSweepError::Config("Could not find home directory".into()))?;
367
368 let definitions = known_caches();
369 let mut caches = Vec::new();
370
371 for def in definitions {
372 for rel_path in def.paths {
374 let full_path = home.join(rel_path);
375
376 if full_path.exists() && full_path.is_dir() {
377 let mut cache = GlobalCache {
379 name: def.name.to_string(),
380 id: def.id,
381 icon: def.icon,
382 path: full_path.clone(),
383 size: 0,
384 file_count: 0,
385 last_modified: None,
386 clean_command: def.clean_command,
387 description: def.description,
388 };
389
390 if let Ok(meta) = std::fs::metadata(&full_path) {
392 cache.last_modified = meta.modified().ok();
393 }
394
395 caches.push(cache);
396 break; }
398 }
399 }
400
401 Ok(caches)
402}
403
404pub fn calculate_cache_size(cache: &mut GlobalCache) -> Result<()> {
406 use rayon::prelude::*;
407 use walkdir::WalkDir;
408
409 if !cache.path.exists() {
410 return Ok(());
411 }
412
413 let entries: Vec<_> = WalkDir::new(&cache.path)
414 .into_iter()
415 .filter_map(|e| e.ok())
416 .collect();
417
418 let (size, count): (u64, u64) = entries
419 .par_iter()
420 .filter_map(|entry| entry.metadata().ok())
421 .filter(|m| m.is_file())
422 .fold(
423 || (0u64, 0u64),
424 |(size, count), m| (size + m.len(), count + 1),
425 )
426 .reduce(|| (0, 0), |(s1, c1), (s2, c2)| (s1 + s2, c1 + c2));
427
428 cache.size = size;
429 cache.file_count = count;
430
431 Ok(())
432}
433
434pub fn calculate_all_sizes(caches: &mut [GlobalCache]) -> Result<()> {
436 use rayon::prelude::*;
437
438 caches.par_iter_mut().for_each(|cache| {
440 let _ = calculate_cache_size(cache);
441 });
442
443 Ok(())
444}
445
446pub fn clean_cache(cache: &GlobalCache, use_official_command: bool) -> Result<CleanResult> {
448 if !cache.path.exists() {
449 return Ok(CleanResult {
450 success: true,
451 bytes_freed: 0,
452 method: CleanMethod::NotFound,
453 });
454 }
455
456 let size_before = cache.size;
457
458 if use_official_command {
460 if let Some(cmd) = cache.clean_command {
461 let result = run_clean_command(cmd);
462 if result.is_ok() {
463 return Ok(CleanResult {
464 success: true,
465 bytes_freed: size_before,
466 method: CleanMethod::OfficialCommand(cmd.to_string()),
467 });
468 }
469 }
471 }
472
473 match crate::trash::delete_path(&cache.path, crate::trash::DeleteMethod::Permanent) {
475 Ok(_) => Ok(CleanResult {
476 success: true,
477 bytes_freed: size_before,
478 method: CleanMethod::ManualDelete,
479 }),
480 Err(e) => Err(e),
481 }
482}
483
484fn run_clean_command(cmd: &str) -> Result<()> {
486 use std::process::Command;
487
488 let parts: Vec<&str> = cmd.split_whitespace().collect();
489 if parts.is_empty() {
490 return Err(DevSweepError::Config("Empty clean command".into()));
491 }
492
493 let output = Command::new(parts[0])
494 .args(&parts[1..])
495 .output()
496 .map_err(|e| DevSweepError::Io(e))?;
497
498 if output.status.success() {
499 Ok(())
500 } else {
501 Err(DevSweepError::CleanFailed {
502 path: PathBuf::from(cmd),
503 reason: String::from_utf8_lossy(&output.stderr).to_string(),
504 })
505 }
506}
507
508#[derive(Debug)]
510pub struct CleanResult {
511 pub success: bool,
512 pub bytes_freed: u64,
513 pub method: CleanMethod,
514}
515
516#[derive(Debug)]
518pub enum CleanMethod {
519 OfficialCommand(String),
520 ManualDelete,
521 NotFound,
522}
523
524#[derive(Debug, Default)]
526pub struct CachesSummary {
527 pub total_caches: usize,
528 pub total_size: u64,
529 pub total_files: u64,
530 pub cleaned_count: usize,
531 pub bytes_freed: u64,
532}
533
534impl CachesSummary {
535 pub fn from_caches(caches: &[GlobalCache]) -> Self {
536 Self {
537 total_caches: caches.len(),
538 total_size: caches.iter().map(|c| c.size).sum(),
539 total_files: caches.iter().map(|c| c.file_count).sum(),
540 ..Default::default()
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_known_caches_not_empty() {
551 let caches = known_caches();
552 assert!(!caches.is_empty());
553 assert!(caches.len() > 20); }
555
556 #[test]
557 fn test_cache_age_display() {
558 let mut cache = GlobalCache {
559 name: "test".into(),
560 id: "test",
561 icon: "📦",
562 path: PathBuf::from("/tmp/test"),
563 size: 0,
564 file_count: 0,
565 last_modified: Some(SystemTime::now()),
566 clean_command: None,
567 description: "test",
568 };
569
570 assert_eq!(cache.last_used_display(), "today");
571 }
572
573 #[test]
574 fn test_detect_caches() {
575 let caches = detect_caches().unwrap();
577 println!("Found {} caches", caches.len());
580 }
581}