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