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