lean_ctx_patterns/
deps_cmd.rs1use 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
19pub 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}