lean_ctx/core/patterns/
deps_cmd.rs1use 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}