1use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
12use crate::error::Result;
13use std::path::PathBuf;
14
15pub struct MiscCleaner {
17 home: PathBuf,
18}
19
20impl MiscCleaner {
21 pub fn new() -> Option<Self> {
23 let home = dirs::home_dir()?;
24 Some(Self { home })
25 }
26
27 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
29 let mut items = Vec::new();
30
31 items.extend(self.detect_vagrant()?);
33
34 items.extend(self.detect_git_lfs()?);
36
37 items.extend(self.detect_go()?);
39
40 items.extend(self.detect_ruby()?);
42
43 items.extend(self.detect_nuget()?);
45
46 items.extend(self.detect_composer()?);
48
49 items.extend(self.detect_coursier()?);
51
52 items.extend(self.detect_gradle()?);
54
55 items.extend(self.detect_maven()?);
57
58 items.extend(self.detect_sbt()?);
60
61 Ok(items)
62 }
63
64 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 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 { 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 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 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"), ];
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 { 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 fn detect_go(&self) -> Result<Vec<CleanableItem>> {
169 let mut items = Vec::new();
170
171 let gopath = std::env::var("GOPATH")
173 .map(PathBuf::from)
174 .unwrap_or_else(|_| self.home.join("go"));
175
176 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 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 { 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 fn detect_ruby(&self) -> Result<Vec<CleanableItem>> {
235 let mut items = Vec::new();
236
237 let gem_paths = [
239 self.home.join(".gem"),
240 self.home.join(".local/share/gem"), self.home.join("AppData/Local/gem"), ];
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 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 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 fn detect_nuget(&self) -> Result<Vec<CleanableItem>> {
329 let mut items = Vec::new();
330
331 let nuget_paths = [
333 self.home.join(".nuget/packages"),
334 self.home.join("AppData/Local/NuGet/Cache"), ];
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 { 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 let workload_paths = [
364 PathBuf::from("/usr/local/share/dotnet/metadata"), self.home.join("AppData/Local/Microsoft/dotnet"), ];
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 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"), self.home.join("AppData/Local/Composer/cache"), ];
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 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"), self.home.join("AppData/Local/Coursier/Cache"), ];
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 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 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 { 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 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 { 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 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 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 { 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 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 { 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 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}