lean_ctx/core/patterns/
poetry.rs1macro_rules! static_regex {
2 ($pattern:expr_2021) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10fn uv_installed_line_re() -> &'static regex::Regex {
11 static_regex!(r"^\s*\+\s+(\S+)")
12}
13fn uv_resolved_re() -> &'static regex::Regex {
14 static_regex!(r"(?i)^(Resolved|Prepared|Installed|Audited)\s+")
15}
16fn poetry_installing_re() -> &'static regex::Regex {
17 static_regex!(r"(?i)^\s*-\s+Installing\s+(\S+)\s+\(([^)]+)\)")
18}
19fn poetry_updating_re() -> &'static regex::Regex {
20 static_regex!(r"(?i)^\s*-\s+Updating\s+(\S+)\s+\(([^)]+)\)")
21}
22fn pip_style_success_re() -> &'static regex::Regex {
23 static_regex!(r"(?i)Successfully installed\s+(.+)")
24}
25fn percent_bar_re() -> &'static regex::Regex {
26 static_regex!(r"\d+%\|")
27}
28
29pub fn compress(command: &str, output: &str) -> Option<String> {
30 let cl = command.trim().to_ascii_lowercase();
31 if cl.starts_with("poetry ") {
32 let sub = cl.split_whitespace().nth(1).unwrap_or("");
33 return match sub {
34 "install" | "add" => Some(compress_poetry(output, false)),
35 "update" => Some(compress_poetry(output, true)),
36 _ => None,
37 };
38 }
39 let parts: Vec<&str> = cl.split_whitespace().collect();
40 if parts.first() == Some(&"uv") && uv_is_compressible(&parts) {
41 return Some(compress_uv(output));
42 }
43 if cl.starts_with("conda ") || cl.starts_with("mamba ") {
44 let sub = parts.get(1).copied().unwrap_or("");
45 return match sub {
46 "install" | "create" | "update" | "remove" => Some(compress_conda(output)),
47 "list" => Some(compress_conda_list(output)),
48 "info" => Some(compress_conda_info(output)),
49 _ => None,
50 };
51 }
52 if cl.starts_with("pipx ") {
53 return Some(compress_pipx(output));
54 }
55 None
56}
57
58fn uv_is_compressible(parts: &[&str]) -> bool {
63 let sub = parts.get(1).copied().unwrap_or("");
64 match sub {
65 "sync" | "lock" | "add" | "remove" | "venv" => true,
66 "tool" => matches!(
67 parts.get(2).copied().unwrap_or(""),
68 "install" | "upgrade" | "uninstall"
69 ),
70 "pip" => matches!(
71 parts.get(2).copied().unwrap_or(""),
72 "install" | "sync" | "uninstall"
73 ),
74 _ => false,
75 }
76}
77
78fn is_download_noise(line: &str) -> bool {
79 let t = line.trim();
80 let tl = t.to_ascii_lowercase();
81 if tl.contains("downloading ")
82 || tl.starts_with("downloading [")
83 || tl.contains("kiB/s")
84 || tl.contains("kib/s")
85 || tl.contains("mib/s")
86 || tl.contains('%') && (tl.contains("eta") || tl.contains('|') || tl.contains("of "))
87 {
88 return true;
89 }
90 if tl.starts_with("progress ") && tl.contains('/') {
91 return true;
92 }
93 if percent_bar_re().is_match(t) {
94 return true;
95 }
96 false
97}
98
99fn compress_poetry(output: &str, prefer_update: bool) -> String {
100 let mut packages = Vec::new();
101 let mut errors = Vec::new();
102
103 for line in output.lines() {
104 let t = line.trim_end();
105 if t.trim().is_empty() || is_download_noise(t) {
106 continue;
107 }
108 let trim = t.trim();
109 let tl = trim.to_ascii_lowercase();
110
111 if prefer_update && let Some(caps) = poetry_updating_re().captures(trim) {
112 packages.push(format!("{} {}", &caps[1], &caps[2]));
113 continue;
114 }
115 if let Some(caps) = poetry_installing_re().captures(trim) {
116 packages.push(format!("{} {}", &caps[1], &caps[2]));
117 continue;
118 }
119 if !prefer_update && let Some(caps) = poetry_updating_re().captures(trim) {
120 packages.push(format!("{} {}", &caps[1], &caps[2]));
121 continue;
122 }
123
124 if tl.contains("error")
125 && (tl.contains("because") || tl.contains("could not") || tl.contains("failed"))
126 {
127 errors.push(trim.to_string());
128 }
129 if tl.starts_with("solverproblemerror") || tl.contains("version solving failed") {
130 errors.push(trim.to_string());
131 }
132 }
133
134 let mut parts = Vec::new();
135 if !packages.is_empty() {
136 parts.push(format!("{} package(s):", packages.len()));
137 parts.extend(packages.into_iter().map(|p| format!(" {p}")));
138 }
139 if !errors.is_empty() {
140 parts.push(format!("{} error line(s):", errors.len()));
141 parts.extend(errors.into_iter().take(15).map(|e| format!(" {e}")));
142 }
143
144 if parts.is_empty() {
145 fallback_compact(output)
146 } else {
147 parts.join("\n")
148 }
149}
150
151fn compress_uv(output: &str) -> String {
152 let mut summary = Vec::new();
153 let mut installed = Vec::new();
154 let mut errors = Vec::new();
155
156 for line in output.lines() {
157 let t = line.trim_end();
158 if t.trim().is_empty() || is_download_noise(t) {
159 continue;
160 }
161 let trim = t.trim();
162 let tl = trim.to_ascii_lowercase();
163
164 if uv_resolved_re().is_match(trim) {
165 summary.push(trim.to_string());
166 continue;
167 }
168 if let Some(caps) = uv_installed_line_re().captures(trim) {
169 installed.push(caps[1].to_string());
170 continue;
171 }
172 if let Some(caps) = pip_style_success_re().captures(trim) {
173 let pkgs: Vec<&str> = caps[1].split_whitespace().collect();
174 summary.push(format!("Successfully installed {} packages", pkgs.len()));
175 for p in pkgs.into_iter().take(30) {
176 installed.push(p.to_string());
177 }
178 continue;
179 }
180
181 if tl.contains("error:")
182 || tl.starts_with("error:")
183 || tl.contains("failed to")
184 || tl.contains("resolution failed")
185 {
186 errors.push(trim.to_string());
187 }
188 }
189
190 let mut parts = Vec::new();
191 parts.extend(summary);
192 if !installed.is_empty() {
193 parts.push(format!("+ {} package(s):", installed.len()));
194 for p in installed.into_iter().take(40) {
195 parts.push(format!(" {p}"));
196 }
197 }
198 if !errors.is_empty() {
199 parts.push(format!("{} error line(s):", errors.len()));
200 parts.extend(errors.into_iter().take(15).map(|e| format!(" {e}")));
201 }
202
203 if parts.is_empty() {
204 fallback_compact(output)
205 } else {
206 parts.join("\n")
207 }
208}
209
210fn compress_conda(output: &str) -> String {
211 let mut packages = Vec::new();
212 let mut errors = Vec::new();
213 let mut action = String::new();
214
215 for line in output.lines() {
216 let t = line.trim();
217 if t.is_empty() || is_download_noise(t) {
218 continue;
219 }
220 let tl = t.to_ascii_lowercase();
221
222 if tl.starts_with("the following packages will be")
223 || tl.starts_with("the following new packages")
224 {
225 action = t.to_string();
226 continue;
227 }
228 if t.starts_with(" ") && t.contains("::") {
229 packages.push(t.trim().to_string());
230 continue;
231 }
232 if t.starts_with(" ") && !t.starts_with(" ") && packages.is_empty() {
233 let name = t.split_whitespace().next().unwrap_or(t);
234 packages.push(name.to_string());
235 continue;
236 }
237 if tl.contains("error")
238 || tl.contains("conflictingerror")
239 || tl.contains("unsatisfiableerror")
240 {
241 errors.push(t.to_string());
242 }
243 }
244
245 let mut parts = Vec::new();
246 if !action.is_empty() {
247 parts.push(action);
248 }
249 if !packages.is_empty() {
250 parts.push(format!("{} package(s)", packages.len()));
251 for p in packages.iter().take(20) {
252 parts.push(format!(" {p}"));
253 }
254 if packages.len() > 20 {
255 parts.push(format!(" ... +{} more", packages.len() - 20));
256 }
257 }
258 if !errors.is_empty() {
259 parts.push(format!("{} error(s):", errors.len()));
260 parts.extend(errors.into_iter().take(10).map(|e| format!(" {e}")));
261 }
262
263 if parts.is_empty() {
264 fallback_compact(output)
265 } else {
266 parts.join("\n")
267 }
268}
269
270fn compress_conda_list(output: &str) -> String {
271 let lines: Vec<&str> = output
272 .lines()
273 .filter(|l| !l.starts_with('#') && !l.trim().is_empty())
274 .collect();
275 if lines.is_empty() {
276 return "no packages".to_string();
277 }
278 if lines.len() <= 10 {
279 return lines.join("\n");
280 }
281 format!(
282 "{} packages installed\n{}\n... +{} more",
283 lines.len(),
284 lines[..10].join("\n"),
285 lines.len() - 10
286 )
287}
288
289fn compress_conda_info(output: &str) -> String {
290 let important = [
291 "active environment",
292 "conda version",
293 "platform",
294 "python version",
295 ];
296 let mut info = Vec::new();
297 for line in output.lines() {
298 let trimmed = line.trim();
299 for key in &important {
300 if trimmed.to_lowercase().starts_with(key) {
301 info.push(trimmed.to_string());
302 break;
303 }
304 }
305 }
306 if info.is_empty() {
307 fallback_compact(output)
308 } else {
309 info.join("\n")
310 }
311}
312
313fn compress_pipx(output: &str) -> String {
314 let mut parts = Vec::new();
315 for line in output.lines() {
316 let t = line.trim();
317 if t.is_empty() || is_download_noise(t) {
318 continue;
319 }
320 let tl = t.to_ascii_lowercase();
321 if tl.contains("installed package")
322 || tl.contains("done!")
323 || tl.contains("these apps are now")
324 {
325 parts.push(t.to_string());
326 }
327 }
328 if parts.is_empty() {
329 fallback_compact(output)
330 } else {
331 parts.join("\n")
332 }
333}
334
335fn fallback_compact(output: &str) -> String {
336 let lines: Vec<&str> = output
337 .lines()
338 .map(str::trim_end)
339 .filter(|l| !l.trim().is_empty() && !is_download_noise(l))
340 .collect();
341 if lines.is_empty() {
342 return "ok".to_string();
343 }
344 let max = 12usize;
345 if lines.len() <= max {
346 return lines.join("\n");
347 }
348 format!(
349 "{}\n... ({} more lines)",
350 lines[..max].join("\n"),
351 lines.len() - max
352 )
353}