Skip to main content

diskforge_core/rules/
dev_artifacts.rs

1use std::path::Path;
2use std::sync::mpsc;
3
4use ignore::{WalkBuilder, WalkState};
5
6use crate::detector;
7use crate::sizing;
8use crate::types::{Category, CleanableItem, Risk};
9
10/// Scan a directory tree for development project artifacts using parallel traversal.
11pub fn scan_dev_artifacts(root: &Path) -> Vec<CleanableItem> {
12    let (tx, rx) = mpsc::channel::<CleanableItem>();
13
14    let walker = WalkBuilder::new(root)
15        .follow_links(false)
16        .git_ignore(false)
17        .git_global(false)
18        .git_exclude(false)
19        .hidden(false) // we handle hidden dirs ourselves
20        .build_parallel();
21
22    walker.run(|| {
23        let tx = tx.clone();
24        Box::new(move |entry_result| {
25            let entry = match entry_result {
26                Ok(e) => e,
27                Err(_) => return WalkState::Continue,
28            };
29
30            // Only process directories
31            if entry.file_type().map(|t| !t.is_dir()).unwrap_or(true) {
32                return WalkState::Continue;
33            }
34
35            let dir_name = entry.file_name().to_string_lossy();
36
37            // Skip hidden dirs except known project-specific ones
38            if dir_name.starts_with('.')
39                && dir_name != ".next"
40                && dir_name != ".dart_tool"
41                && dir_name != ".angular"
42                && dir_name != ".turbo"
43                && dir_name != ".expo"
44                && dir_name != ".venv"
45                && dir_name != ".gradle"
46                && dir_name != ".build"
47            {
48                return WalkState::Skip;
49            }
50
51            // Skip artifact dirs — don't recurse into them
52            if is_artifact_dir(&dir_name) {
53                return WalkState::Skip;
54            }
55
56            let path = entry.path();
57
58            // Detect project type at this directory
59            let Some(project_type) = detector::detect_project_type(path) else {
60                return WalkState::Continue;
61            };
62
63            let artifact_dirs = detector::artifact_dirs(&project_type);
64            let hint = detector::rebuild_hint(&project_type);
65
66            for artifact_dir_name in artifact_dirs {
67                let artifact_path = path.join(artifact_dir_name);
68                if !artifact_path.is_dir() {
69                    continue;
70                }
71                let stats = sizing::dir_stats(&artifact_path);
72                if stats.size <= 1024 * 1024 {
73                    continue;
74                }
75                let _ = tx.send(CleanableItem {
76                    category: Category::DevArtifact(project_type.clone()),
77                    path: artifact_path,
78                    size: stats.size,
79                    risk: Risk::Low,
80                    regenerates: true,
81                    regeneration_hint: Some(hint.to_string()),
82                    last_modified: stats.last_modified,
83                    description: format!("{artifact_dir_name} ({project_type})"),
84                    cleanup_command: None,
85                });
86            }
87
88            // Don't recurse into this project root
89            WalkState::Skip
90        })
91    });
92
93    // Drop the original tx so the channel closes when all worker clones are dropped
94    drop(tx);
95
96    rx.into_iter().collect()
97}
98
99fn is_artifact_dir(name: &str) -> bool {
100    matches!(
101        name,
102        "node_modules"
103            | "target"
104            | "build"
105            | "dist"
106            | "Pods"
107            | ".next"
108            | ".nuxt"
109            | ".dart_tool"
110            | ".turbo"
111            | ".expo"
112            | ".angular"
113            | ".venv"
114            | "venv"
115            | "__pycache__"
116            | "zig-cache"
117            | "zig-out"
118            | ".build"
119            | ".gradle"
120    )
121}