Skip to main content

lean_ctx/core/patterns/
poetry.rs

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