Skip to main content

lean_ctx/core/patterns/
poetry.rs

1macro_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}