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 if let Some(result) = compress(&path) {
35 return Some(result);
36 }
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 if let Some(v) = extract_toml_string(trimmed) {
105 name = v;
106 }
107 }
108 if trimmed.starts_with("version") && !in_deps {
109 if let Some(v) = extract_toml_string(trimmed) {
110 version = v;
111 }
112 }
113
114 if in_deps && !trimmed.is_empty() && !trimmed.starts_with('#') {
115 if let Some(dep_name) = trimmed.split('=').next() {
116 deps.push(dep_name.trim().to_string());
117 }
118 }
119 }
120
121 if deps.is_empty() {
122 return None;
123 }
124
125 let mut result = format!("Rust ({name}@{version}):");
126 result.push_str(&format!("\n deps ({}): {}", deps.len(), deps.join(", ")));
127
128 Some(result)
129}
130
131fn compress_requirements(content: &str) -> Option<String> {
132 let deps: Vec<&str> = content
133 .lines()
134 .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#'))
135 .collect();
136
137 if deps.is_empty() {
138 return None;
139 }
140
141 let short: Vec<String> = deps
142 .iter()
143 .map(|d| {
144 d.split("==")
145 .next()
146 .unwrap_or(d)
147 .split(">=")
148 .next()
149 .unwrap_or(d)
150 .trim()
151 .to_string()
152 })
153 .collect();
154
155 Some(format!(
156 "Python:\n deps ({}): {}",
157 short.len(),
158 short.join(", ")
159 ))
160}
161
162fn compress_go_mod(content: &str) -> Option<String> {
163 let mut module = String::new();
164 let mut deps = Vec::new();
165 let mut in_require = false;
166
167 for line in content.lines() {
168 let trimmed = line.trim();
169
170 if trimmed.starts_with("module ") {
171 module = trimmed.strip_prefix("module ").unwrap_or("").to_string();
172 }
173 if trimmed == "require (" {
174 in_require = true;
175 continue;
176 }
177 if trimmed == ")" && in_require {
178 in_require = false;
179 }
180 if in_require && !trimmed.is_empty() {
181 if let Some(name) = trimmed.split_whitespace().next() {
182 if !name.contains("// indirect") {
183 let short = name.rsplit('/').next().unwrap_or(name);
184 deps.push(short.to_string());
185 }
186 }
187 }
188 }
189
190 if deps.is_empty() {
191 return None;
192 }
193
194 Some(format!(
195 "Go ({module}):\n deps ({}): {}",
196 deps.len(),
197 deps.join(", ")
198 ))
199}
200
201fn compress_gemfile(content: &str) -> Option<String> {
202 let gems: Vec<&str> = content
203 .lines()
204 .filter(|l| l.trim().starts_with("gem "))
205 .map(|l| {
206 l.trim()
207 .strip_prefix("gem ")
208 .unwrap_or(l)
209 .split(',')
210 .next()
211 .unwrap_or("")
212 .trim()
213 .trim_matches('\'')
214 .trim_matches('"')
215 })
216 .collect();
217
218 if gems.is_empty() {
219 return None;
220 }
221
222 Some(format!(
223 "Ruby:\n gems ({}): {}",
224 gems.len(),
225 gems.join(", ")
226 ))
227}
228
229fn compress_pyproject(content: &str) -> Option<String> {
230 let mut deps = Vec::new();
231 let mut in_deps = false;
232
233 for line in content.lines() {
234 let trimmed = line.trim();
235 if trimmed == "dependencies = [" || trimmed.starts_with("dependencies") {
236 in_deps = true;
237 continue;
238 }
239 if trimmed == "]" && in_deps {
240 in_deps = false;
241 }
242 if in_deps {
243 let clean = trimmed.trim_matches(|c: char| c == '"' || c == '\'' || c == ',');
244 let name = clean
245 .split(">=")
246 .next()
247 .unwrap_or(clean)
248 .split("==")
249 .next()
250 .unwrap_or(clean)
251 .trim();
252 if !name.is_empty() {
253 deps.push(name.to_string());
254 }
255 }
256 }
257
258 if deps.is_empty() {
259 return None;
260 }
261
262 Some(format!(
263 "Python:\n deps ({}): {}",
264 deps.len(),
265 deps.join(", ")
266 ))
267}
268
269fn extract_toml_string(line: &str) -> Option<String> {
270 let after_eq = line.split('=').nth(1)?.trim();
271 Some(after_eq.trim_matches('"').to_string())
272}