Skip to main content

lean_ctx/core/patterns/
poetry.rs

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