Skip to main content

lean_ctx/core/patterns/
deps_cmd.rs

1use std::path::Path;
2
3pub fn compress(path: &str) -> Option<String> {
4    let content = std::fs::read_to_string(path).ok()?;
5    let filename = Path::new(path)
6        .file_name()
7        .and_then(|n| n.to_str())
8        .unwrap_or(path);
9
10    match filename {
11        "package.json" => compress_package_json(&content),
12        "Cargo.toml" => compress_cargo_toml(&content),
13        "requirements.txt" => compress_requirements(&content),
14        "go.mod" => compress_go_mod(&content),
15        "Gemfile" => compress_gemfile(&content),
16        "pyproject.toml" => compress_pyproject(&content),
17        _ => None,
18    }
19}
20
21pub fn detect_and_compress(dir: &str) -> Option<String> {
22    let candidates = [
23        "package.json",
24        "Cargo.toml",
25        "requirements.txt",
26        "go.mod",
27        "Gemfile",
28        "pyproject.toml",
29    ];
30
31    for name in &candidates {
32        let path = format!("{}/{}", dir.trim_end_matches('/'), name);
33        if Path::new(&path).exists()
34            && let Some(result) = compress(&path)
35        {
36            return Some(result);
37        }
38    }
39
40    None
41}
42
43fn compress_package_json(content: &str) -> Option<String> {
44    let val: serde_json::Value = serde_json::from_str(content).ok()?;
45    let obj = val.as_object()?;
46
47    let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("?");
48    let version = obj.get("version").and_then(|v| v.as_str()).unwrap_or("?");
49
50    let mut result = format!("Node ({name}@{version}):");
51
52    if let Some(deps) = obj.get("dependencies").and_then(|v| v.as_object()) {
53        let dep_list: Vec<String> = deps
54            .iter()
55            .map(|(k, v)| format!("{k} ({})", v.as_str().unwrap_or("?")))
56            .collect();
57        result.push_str(&format!(
58            "\n  deps ({}): {}",
59            dep_list.len(),
60            dep_list.join(", ")
61        ));
62    }
63
64    if let Some(deps) = obj.get("devDependencies").and_then(|v| v.as_object()) {
65        let dep_list: Vec<String> = deps
66            .iter()
67            .take(10)
68            .map(|(k, v)| format!("{k} ({})", v.as_str().unwrap_or("?")))
69            .collect();
70        let suffix = if deps.len() > 10 {
71            format!(", ... +{} more", deps.len() - 10)
72        } else {
73            String::new()
74        };
75        result.push_str(&format!(
76            "\n  devDeps ({}): {}{}",
77            deps.len(),
78            dep_list.join(", "),
79            suffix
80        ));
81    }
82
83    Some(result)
84}
85
86fn compress_cargo_toml(content: &str) -> Option<String> {
87    let mut name = String::new();
88    let mut version = String::new();
89    let mut deps = Vec::new();
90    let mut in_deps = false;
91
92    for line in content.lines() {
93        let trimmed = line.trim();
94
95        if trimmed.starts_with("[dependencies]") {
96            in_deps = true;
97            continue;
98        }
99        if trimmed.starts_with('[') && in_deps {
100            in_deps = false;
101        }
102
103        if trimmed.starts_with("name")
104            && let Some(v) = extract_toml_string(trimmed)
105        {
106            name = v;
107        }
108        if trimmed.starts_with("version")
109            && !in_deps
110            && let Some(v) = extract_toml_string(trimmed)
111        {
112            version = v;
113        }
114
115        if in_deps
116            && !trimmed.is_empty()
117            && !trimmed.starts_with('#')
118            && let Some(dep_name) = trimmed.split('=').next()
119        {
120            deps.push(dep_name.trim().to_string());
121        }
122    }
123
124    if deps.is_empty() {
125        return None;
126    }
127
128    let mut result = format!("Rust ({name}@{version}):");
129    result.push_str(&format!("\n  deps ({}): {}", deps.len(), deps.join(", ")));
130
131    Some(result)
132}
133
134fn compress_requirements(content: &str) -> Option<String> {
135    let deps: Vec<&str> = content
136        .lines()
137        .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#'))
138        .collect();
139
140    if deps.is_empty() {
141        return None;
142    }
143
144    let short: Vec<String> = deps
145        .iter()
146        .map(|d| {
147            d.split("==")
148                .next()
149                .unwrap_or(d)
150                .split(">=")
151                .next()
152                .unwrap_or(d)
153                .trim()
154                .to_string()
155        })
156        .collect();
157
158    Some(format!(
159        "Python:\n  deps ({}): {}",
160        short.len(),
161        short.join(", ")
162    ))
163}
164
165fn compress_go_mod(content: &str) -> Option<String> {
166    let mut module = String::new();
167    let mut deps = Vec::new();
168    let mut in_require = false;
169
170    for line in content.lines() {
171        let trimmed = line.trim();
172
173        if trimmed.starts_with("module ") {
174            module = trimmed.strip_prefix("module ").unwrap_or("").to_string();
175        }
176        if trimmed == "require (" {
177            in_require = true;
178            continue;
179        }
180        if trimmed == ")" && in_require {
181            in_require = false;
182        }
183        if in_require
184            && !trimmed.is_empty()
185            && let Some(name) = trimmed.split_whitespace().next()
186            && !name.contains("// indirect")
187        {
188            let short = name.rsplit('/').next().unwrap_or(name);
189            deps.push(short.to_string());
190        }
191    }
192
193    if deps.is_empty() {
194        return None;
195    }
196
197    Some(format!(
198        "Go ({module}):\n  deps ({}): {}",
199        deps.len(),
200        deps.join(", ")
201    ))
202}
203
204fn compress_gemfile(content: &str) -> Option<String> {
205    let gems: Vec<&str> = content
206        .lines()
207        .filter(|l| l.trim().starts_with("gem "))
208        .map(|l| {
209            l.trim()
210                .strip_prefix("gem ")
211                .unwrap_or(l)
212                .split(',')
213                .next()
214                .unwrap_or("")
215                .trim()
216                .trim_matches('\'')
217                .trim_matches('"')
218        })
219        .collect();
220
221    if gems.is_empty() {
222        return None;
223    }
224
225    Some(format!(
226        "Ruby:\n  gems ({}): {}",
227        gems.len(),
228        gems.join(", ")
229    ))
230}
231
232fn compress_pyproject(content: &str) -> Option<String> {
233    let mut deps = Vec::new();
234    let mut in_deps = false;
235
236    for line in content.lines() {
237        let trimmed = line.trim();
238        if trimmed == "dependencies = [" || trimmed.starts_with("dependencies") {
239            in_deps = true;
240            continue;
241        }
242        if trimmed == "]" && in_deps {
243            in_deps = false;
244        }
245        if in_deps {
246            let clean = trimmed.trim_matches(|c: char| c == '"' || c == '\'' || c == ',');
247            let name = clean
248                .split(">=")
249                .next()
250                .unwrap_or(clean)
251                .split("==")
252                .next()
253                .unwrap_or(clean)
254                .trim();
255            if !name.is_empty() {
256                deps.push(name.to_string());
257            }
258        }
259    }
260
261    if deps.is_empty() {
262        return None;
263    }
264
265    Some(format!(
266        "Python:\n  deps ({}): {}",
267        deps.len(),
268        deps.join(", ")
269    ))
270}
271
272fn extract_toml_string(line: &str) -> Option<String> {
273    let after_eq = line.split('=').nth(1)?.trim();
274    Some(after_eq.trim_matches('"').to_string())
275}