Skip to main content

lean_ctx_patterns/
poetry.rs

1// SPDX-License-Identifier: Apache-2.0
2
3macro_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
60/// uv subcommands whose output is the `Resolved/Prepared/Installed + pkg==ver`
61/// progress format we can safely compress. Excludes commands that emit an
62/// artifact or child-process output (`run`, `pip compile`, `export`, `tree`,
63/// `pip freeze`, `pip list`) which must pass through untouched.
64fn 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}