Skip to main content

null_e/cleaners/
misc.rs

1//! Miscellaneous development tools cleanup module
2//!
3//! Handles cleanup of various development tools:
4//! - Vagrant boxes
5//! - Git LFS cache
6//! - Go modules
7//! - Ruby gems
8//! - NuGet packages (.NET)
9//! - Composer (PHP)
10
11use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
12use crate::error::Result;
13use std::path::PathBuf;
14
15/// Miscellaneous tools cleaner
16pub struct MiscCleaner {
17    home: PathBuf,
18}
19
20impl MiscCleaner {
21    /// Create a new misc cleaner
22    pub fn new() -> Option<Self> {
23        let home = dirs::home_dir()?;
24        Some(Self { home })
25    }
26
27    /// Detect all cleanable items
28    pub fn detect(&self) -> Result<Vec<CleanableItem>> {
29        let mut items = Vec::new();
30
31        // Vagrant
32        items.extend(self.detect_vagrant()?);
33
34        // Git LFS
35        items.extend(self.detect_git_lfs()?);
36
37        // Go
38        items.extend(self.detect_go()?);
39
40        // Ruby
41        items.extend(self.detect_ruby()?);
42
43        // NuGet (.NET)
44        items.extend(self.detect_nuget()?);
45
46        // Composer (PHP)
47        items.extend(self.detect_composer()?);
48
49        // Coursier (Scala)
50        items.extend(self.detect_coursier()?);
51
52        // Gradle (global)
53        items.extend(self.detect_gradle()?);
54
55        // Maven
56        items.extend(self.detect_maven()?);
57
58        // SBT (Scala)
59        items.extend(self.detect_sbt()?);
60
61        Ok(items)
62    }
63
64    /// Detect Vagrant boxes
65    fn detect_vagrant(&self) -> Result<Vec<CleanableItem>> {
66        let mut items = Vec::new();
67
68        let vagrant_home = std::env::var("VAGRANT_HOME")
69            .map(PathBuf::from)
70            .unwrap_or_else(|_| self.home.join(".vagrant.d"));
71
72        // Vagrant boxes
73        let boxes_path = vagrant_home.join("boxes");
74        if boxes_path.exists() {
75            if let Ok(entries) = std::fs::read_dir(&boxes_path) {
76                for entry in entries.filter_map(|e| e.ok()) {
77                    let path = entry.path();
78                    if path.is_dir() {
79                        let name = path.file_name()
80                            .map(|n| n.to_string_lossy().to_string())
81                            .unwrap_or_else(|| "Unknown".to_string());
82
83                        let (size, file_count) = calculate_dir_size(&path)?;
84                        if size < 100_000_000 { // Skip small boxes
85                            continue;
86                        }
87
88                        items.push(CleanableItem {
89                            name: format!("Vagrant Box: {}", name.replace("-VAGRANTSLASH-", "/")),
90                            category: "Vagrant".to_string(),
91                            subcategory: "Boxes".to_string(),
92                            icon: "📦",
93                            path,
94                            size,
95                            file_count: Some(file_count),
96                            last_modified: get_mtime(&entry.path()),
97                            description: "Vagrant base box. Can be re-downloaded if needed.",
98                            safe_to_delete: SafetyLevel::SafeWithCost,
99                            clean_command: Some(format!("vagrant box remove {}", name)),
100                        });
101                    }
102                }
103            }
104        }
105
106        // Vagrant temp files
107        let tmp_path = vagrant_home.join("tmp");
108        if tmp_path.exists() {
109            let (size, file_count) = calculate_dir_size(&tmp_path)?;
110            if size > 50_000_000 {
111                items.push(CleanableItem {
112                    name: "Vagrant Temp Files".to_string(),
113                    category: "Vagrant".to_string(),
114                    subcategory: "Cache".to_string(),
115                    icon: "📦",
116                    path: tmp_path,
117                    size,
118                    file_count: Some(file_count),
119                    last_modified: None,
120                    description: "Temporary Vagrant files. Safe to delete.",
121                    safe_to_delete: SafetyLevel::Safe,
122                    clean_command: None,
123                });
124            }
125        }
126
127        Ok(items)
128    }
129
130    /// Detect Git LFS cache
131    fn detect_git_lfs(&self) -> Result<Vec<CleanableItem>> {
132        let mut items = Vec::new();
133
134        let lfs_paths = [
135            self.home.join(".git-lfs"),
136            self.home.join("AppData/Local/git-lfs"), // Windows
137        ];
138
139        for lfs_path in lfs_paths {
140            if !lfs_path.exists() {
141                continue;
142            }
143
144            let (size, file_count) = calculate_dir_size(&lfs_path)?;
145            if size < 100_000_000 { // 100MB minimum
146                continue;
147            }
148
149            items.push(CleanableItem {
150                name: "Git LFS Cache".to_string(),
151                category: "Git".to_string(),
152                subcategory: "LFS".to_string(),
153                icon: "📁",
154                path: lfs_path,
155                size,
156                file_count: Some(file_count),
157                last_modified: None,
158                description: "Git Large File Storage cache. Will be re-downloaded when needed.",
159                safe_to_delete: SafetyLevel::SafeWithCost,
160                clean_command: Some("git lfs prune".to_string()),
161            });
162        }
163
164        Ok(items)
165    }
166
167    /// Detect Go module cache
168    fn detect_go(&self) -> Result<Vec<CleanableItem>> {
169        let mut items = Vec::new();
170
171        // GOPATH defaults to ~/go
172        let gopath = std::env::var("GOPATH")
173            .map(PathBuf::from)
174            .unwrap_or_else(|_| self.home.join("go"));
175
176        // Module cache
177        let mod_cache = gopath.join("pkg/mod/cache");
178        if mod_cache.exists() {
179            let (size, file_count) = calculate_dir_size(&mod_cache)?;
180            if size > 100_000_000 {
181                items.push(CleanableItem {
182                    name: "Go Module Cache".to_string(),
183                    category: "Go".to_string(),
184                    subcategory: "Modules".to_string(),
185                    icon: "🐹",
186                    path: mod_cache,
187                    size,
188                    file_count: Some(file_count),
189                    last_modified: None,
190                    description: "Downloaded Go modules. Will be re-downloaded when needed.",
191                    safe_to_delete: SafetyLevel::SafeWithCost,
192                    clean_command: Some("go clean -modcache".to_string()),
193                });
194            }
195        }
196
197        // Build cache
198        let gocache = std::env::var("GOCACHE")
199            .map(PathBuf::from)
200            .unwrap_or_else(|_| {
201                #[cfg(target_os = "macos")]
202                return self.home.join("Library/Caches/go-build");
203                #[cfg(target_os = "linux")]
204                return self.home.join(".cache/go-build");
205                #[cfg(target_os = "windows")]
206                return self.home.join("AppData/Local/go-build");
207                #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
208                return self.home.join(".cache/go-build");
209            });
210
211        if gocache.exists() {
212            let (size, file_count) = calculate_dir_size(&gocache)?;
213            if size > 500_000_000 { // 500MB
214                items.push(CleanableItem {
215                    name: "Go Build Cache".to_string(),
216                    category: "Go".to_string(),
217                    subcategory: "Build".to_string(),
218                    icon: "🐹",
219                    path: gocache,
220                    size,
221                    file_count: Some(file_count),
222                    last_modified: None,
223                    description: "Go build cache. Will slow down first build after deletion.",
224                    safe_to_delete: SafetyLevel::SafeWithCost,
225                    clean_command: Some("go clean -cache".to_string()),
226                });
227            }
228        }
229
230        Ok(items)
231    }
232
233    /// Detect Ruby gems
234    fn detect_ruby(&self) -> Result<Vec<CleanableItem>> {
235        let mut items = Vec::new();
236
237        // Global gems
238        let gem_paths = [
239            self.home.join(".gem"),
240            self.home.join(".local/share/gem"), // Linux
241            self.home.join("AppData/Local/gem"), // Windows
242        ];
243
244        for gem_path in gem_paths {
245            if !gem_path.exists() {
246                continue;
247            }
248
249            let (size, file_count) = calculate_dir_size(&gem_path)?;
250            if size < 100_000_000 {
251                continue;
252            }
253
254            items.push(CleanableItem {
255                name: "Ruby Gems".to_string(),
256                category: "Ruby".to_string(),
257                subcategory: "Gems".to_string(),
258                icon: "💎",
259                path: gem_path,
260                size,
261                file_count: Some(file_count),
262                last_modified: None,
263                description: "Installed Ruby gems. Will be reinstalled when needed.",
264                safe_to_delete: SafetyLevel::SafeWithCost,
265                clean_command: Some("gem cleanup".to_string()),
266            });
267        }
268
269        // Bundler cache
270        let bundler_cache = self.home.join(".bundle/cache");
271        if bundler_cache.exists() {
272            let (size, file_count) = calculate_dir_size(&bundler_cache)?;
273            if size > 50_000_000 {
274                items.push(CleanableItem {
275                    name: "Bundler Cache".to_string(),
276                    category: "Ruby".to_string(),
277                    subcategory: "Bundler".to_string(),
278                    icon: "💎",
279                    path: bundler_cache,
280                    size,
281                    file_count: Some(file_count),
282                    last_modified: None,
283                    description: "Bundler download cache. Safe to delete.",
284                    safe_to_delete: SafetyLevel::Safe,
285                    clean_command: Some("bundle clean --force".to_string()),
286                });
287            }
288        }
289
290        // rbenv versions
291        let rbenv_versions = self.home.join(".rbenv/versions");
292        if rbenv_versions.exists() {
293            if let Ok(entries) = std::fs::read_dir(&rbenv_versions) {
294                for entry in entries.filter_map(|e| e.ok()) {
295                    let path = entry.path();
296                    if path.is_dir() {
297                        let name = path.file_name()
298                            .map(|n| n.to_string_lossy().to_string())
299                            .unwrap_or_else(|| "Unknown".to_string());
300
301                        let (size, file_count) = calculate_dir_size(&path)?;
302                        if size < 100_000_000 {
303                            continue;
304                        }
305
306                        items.push(CleanableItem {
307                            name: format!("Ruby {}", name),
308                            category: "Ruby".to_string(),
309                            subcategory: "rbenv".to_string(),
310                            icon: "💎",
311                            path,
312                            size,
313                            file_count: Some(file_count),
314                            last_modified: get_mtime(&entry.path()),
315                            description: "Installed Ruby version via rbenv.",
316                            safe_to_delete: SafetyLevel::Caution,
317                            clean_command: Some(format!("rbenv uninstall {}", name)),
318                        });
319                    }
320                }
321            }
322        }
323
324        Ok(items)
325    }
326
327    /// Detect NuGet packages (.NET)
328    fn detect_nuget(&self) -> Result<Vec<CleanableItem>> {
329        let mut items = Vec::new();
330
331        // NuGet global packages
332        let nuget_paths = [
333            self.home.join(".nuget/packages"),
334            self.home.join("AppData/Local/NuGet/Cache"), // Windows
335        ];
336
337        for nuget_path in nuget_paths {
338            if !nuget_path.exists() {
339                continue;
340            }
341
342            let (size, file_count) = calculate_dir_size(&nuget_path)?;
343            if size < 500_000_000 { // 500MB
344                continue;
345            }
346
347            items.push(CleanableItem {
348                name: "NuGet Global Packages".to_string(),
349                category: ".NET".to_string(),
350                subcategory: "NuGet".to_string(),
351                icon: "🔷",
352                path: nuget_path,
353                size,
354                file_count: Some(file_count),
355                last_modified: None,
356                description: "NuGet package cache. Will be re-downloaded when needed.",
357                safe_to_delete: SafetyLevel::SafeWithCost,
358                clean_command: Some("dotnet nuget locals all --clear".to_string()),
359            });
360        }
361
362        // .NET SDK workloads
363        let workload_paths = [
364            PathBuf::from("/usr/local/share/dotnet/metadata"), // macOS/Linux
365            self.home.join("AppData/Local/Microsoft/dotnet"), // Windows
366        ];
367
368        for workload_path in workload_paths {
369            if !workload_path.exists() {
370                continue;
371            }
372
373            let (size, file_count) = calculate_dir_size(&workload_path)?;
374            if size < 500_000_000 {
375                continue;
376            }
377
378            items.push(CleanableItem {
379                name: ".NET Workloads/SDK".to_string(),
380                category: ".NET".to_string(),
381                subcategory: "SDK".to_string(),
382                icon: "🔷",
383                path: workload_path,
384                size,
385                file_count: Some(file_count),
386                last_modified: None,
387                description: ".NET SDK workloads. May affect installed SDKs.",
388                safe_to_delete: SafetyLevel::Caution,
389                clean_command: None,
390            });
391        }
392
393        Ok(items)
394    }
395
396    /// Detect Composer (PHP) cache
397    fn detect_composer(&self) -> Result<Vec<CleanableItem>> {
398        let mut items = Vec::new();
399
400        let composer_paths = [
401            self.home.join(".composer/cache"),
402            self.home.join(".cache/composer"), // Linux
403            self.home.join("AppData/Local/Composer/cache"), // Windows
404        ];
405
406        for composer_path in composer_paths {
407            if !composer_path.exists() {
408                continue;
409            }
410
411            let (size, file_count) = calculate_dir_size(&composer_path)?;
412            if size < 100_000_000 {
413                continue;
414            }
415
416            items.push(CleanableItem {
417                name: "Composer Cache".to_string(),
418                category: "PHP".to_string(),
419                subcategory: "Composer".to_string(),
420                icon: "🐘",
421                path: composer_path,
422                size,
423                file_count: Some(file_count),
424                last_modified: None,
425                description: "PHP Composer package cache. Will be re-downloaded when needed.",
426                safe_to_delete: SafetyLevel::Safe,
427                clean_command: Some("composer clear-cache".to_string()),
428            });
429        }
430
431        Ok(items)
432    }
433
434    /// Detect Coursier (Scala) cache
435    fn detect_coursier(&self) -> Result<Vec<CleanableItem>> {
436        let mut items = Vec::new();
437
438        let coursier_paths = [
439            self.home.join(".cache/coursier"),
440            self.home.join("Library/Caches/Coursier"), // macOS
441            self.home.join("AppData/Local/Coursier/Cache"), // Windows
442        ];
443
444        for coursier_path in coursier_paths {
445            if !coursier_path.exists() {
446                continue;
447            }
448
449            let (size, file_count) = calculate_dir_size(&coursier_path)?;
450            if size < 500_000_000 {
451                continue;
452            }
453
454            items.push(CleanableItem {
455                name: "Coursier Cache".to_string(),
456                category: "Scala".to_string(),
457                subcategory: "Coursier".to_string(),
458                icon: "⚡",
459                path: coursier_path,
460                size,
461                file_count: Some(file_count),
462                last_modified: None,
463                description: "Scala dependency cache. Will be re-downloaded when needed.",
464                safe_to_delete: SafetyLevel::SafeWithCost,
465                clean_command: None,
466            });
467        }
468
469        Ok(items)
470    }
471
472    /// Detect Gradle global cache
473    fn detect_gradle(&self) -> Result<Vec<CleanableItem>> {
474        let mut items = Vec::new();
475
476        let gradle_home = std::env::var("GRADLE_USER_HOME")
477            .map(PathBuf::from)
478            .unwrap_or_else(|_| self.home.join(".gradle"));
479
480        // Caches
481        let cache_path = gradle_home.join("caches");
482        if cache_path.exists() {
483            let (size, file_count) = calculate_dir_size(&cache_path)?;
484            if size > 1_000_000_000 { // 1GB
485                items.push(CleanableItem {
486                    name: "Gradle Cache".to_string(),
487                    category: "Java/Kotlin".to_string(),
488                    subcategory: "Gradle".to_string(),
489                    icon: "🐘",
490                    path: cache_path,
491                    size,
492                    file_count: Some(file_count),
493                    last_modified: None,
494                    description: "Gradle dependency cache. Will be re-downloaded when needed.",
495                    safe_to_delete: SafetyLevel::SafeWithCost,
496                    clean_command: Some("gradle --stop && rm -rf ~/.gradle/caches".to_string()),
497                });
498            }
499        }
500
501        // Wrapper distributions
502        let wrapper_path = gradle_home.join("wrapper/dists");
503        if wrapper_path.exists() {
504            let (size, file_count) = calculate_dir_size(&wrapper_path)?;
505            if size > 500_000_000 { // 500MB
506                items.push(CleanableItem {
507                    name: "Gradle Wrapper Distributions".to_string(),
508                    category: "Java/Kotlin".to_string(),
509                    subcategory: "Gradle".to_string(),
510                    icon: "🐘",
511                    path: wrapper_path,
512                    size,
513                    file_count: Some(file_count),
514                    last_modified: None,
515                    description: "Downloaded Gradle distributions. Will be re-downloaded when needed.",
516                    safe_to_delete: SafetyLevel::SafeWithCost,
517                    clean_command: None,
518                });
519            }
520        }
521
522        // Daemon files
523        let daemon_path = gradle_home.join("daemon");
524        if daemon_path.exists() {
525            let (size, file_count) = calculate_dir_size(&daemon_path)?;
526            if size > 100_000_000 {
527                items.push(CleanableItem {
528                    name: "Gradle Daemon Files".to_string(),
529                    category: "Java/Kotlin".to_string(),
530                    subcategory: "Gradle".to_string(),
531                    icon: "🐘",
532                    path: daemon_path,
533                    size,
534                    file_count: Some(file_count),
535                    last_modified: None,
536                    description: "Gradle daemon logs and state. Safe to delete.",
537                    safe_to_delete: SafetyLevel::Safe,
538                    clean_command: Some("gradle --stop".to_string()),
539                });
540            }
541        }
542
543        Ok(items)
544    }
545
546    /// Detect Maven cache
547    fn detect_maven(&self) -> Result<Vec<CleanableItem>> {
548        let mut items = Vec::new();
549
550        let m2_repo = self.home.join(".m2/repository");
551        if m2_repo.exists() {
552            let (size, file_count) = calculate_dir_size(&m2_repo)?;
553            if size > 1_000_000_000 { // 1GB
554                items.push(CleanableItem {
555                    name: "Maven Repository".to_string(),
556                    category: "Java/Kotlin".to_string(),
557                    subcategory: "Maven".to_string(),
558                    icon: "☕",
559                    path: m2_repo,
560                    size,
561                    file_count: Some(file_count),
562                    last_modified: None,
563                    description: "Maven local repository. Dependencies will be re-downloaded.",
564                    safe_to_delete: SafetyLevel::SafeWithCost,
565                    clean_command: Some("mvn dependency:purge-local-repository".to_string()),
566                });
567            }
568        }
569
570        Ok(items)
571    }
572
573    /// Detect SBT (Scala) cache
574    fn detect_sbt(&self) -> Result<Vec<CleanableItem>> {
575        let mut items = Vec::new();
576
577        let sbt_path = self.home.join(".sbt");
578        if sbt_path.exists() {
579            let (size, file_count) = calculate_dir_size(&sbt_path)?;
580            if size > 500_000_000 { // 500MB
581                items.push(CleanableItem {
582                    name: "SBT Cache".to_string(),
583                    category: "Scala".to_string(),
584                    subcategory: "SBT".to_string(),
585                    icon: "⚡",
586                    path: sbt_path,
587                    size,
588                    file_count: Some(file_count),
589                    last_modified: None,
590                    description: "SBT cache and plugins. Will slow down first build after deletion.",
591                    safe_to_delete: SafetyLevel::SafeWithCost,
592                    clean_command: None,
593                });
594            }
595        }
596
597        // Ivy cache (used by SBT)
598        let ivy_path = self.home.join(".ivy2/cache");
599        if ivy_path.exists() {
600            let (size, file_count) = calculate_dir_size(&ivy_path)?;
601            if size > 500_000_000 {
602                items.push(CleanableItem {
603                    name: "Ivy Cache".to_string(),
604                    category: "Scala".to_string(),
605                    subcategory: "Ivy".to_string(),
606                    icon: "⚡",
607                    path: ivy_path,
608                    size,
609                    file_count: Some(file_count),
610                    last_modified: None,
611                    description: "Ivy dependency cache (used by SBT). Will be re-downloaded.",
612                    safe_to_delete: SafetyLevel::SafeWithCost,
613                    clean_command: None,
614                });
615            }
616        }
617
618        Ok(items)
619    }
620}