Skip to main content

lean_ctx_patterns/
deps_cmd.rs

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