Skip to main content

lean_ctx_patterns/
gem.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! RubyGems (`gem`) output compression.
4//!
5//! `gem install`/`update` interleaves the few lines that matter
6//! (`Successfully installed …`, gem count) with documentation/fetch noise
7//! (`Fetching`, `Parsing documentation`, `Installing ri/rdoc`).
8//!
9//! NOTE: `gem list` is classified **Verbatim** by the output policy
10//! (`is_package_manager_info`, alongside `npm list`/`pip list`/`cargo tree`) —
11//! installed-package inventories are reference data the agent reads in full, so
12//! they never reach this compressor. The `name (versions)` count+cap path here
13//! therefore serves `gem search` (remote listing noise), not `gem list`.
14
15use crate::ansi::strip_ansi;
16
17pub fn compress(cmd: &str, output: &str) -> Option<String> {
18    let trimmed = output.trim();
19    if trimmed.is_empty() {
20        return Some("gem: ok".to_string());
21    }
22    if cmd.contains(" list") || cmd.contains(" search") {
23        return Some(compress_list(trimmed));
24    }
25    Some(compress_install(trimmed))
26}
27
28fn compress_list(output: &str) -> String {
29    let rows: Vec<&str> = output
30        .lines()
31        .map(str::trim)
32        .filter(|l| !l.is_empty() && !l.starts_with("***") && l.contains('('))
33        .collect();
34    if rows.is_empty() {
35        return fallback(output);
36    }
37    let n = rows.len();
38    let cap = 30.min(n);
39    let mut s = format!("gem: {n} gem(s)\n{}", rows[..cap].join("\n"));
40    if n > cap {
41        s.push_str(&format!("\n... +{} more", n - cap));
42    }
43    s
44}
45
46fn compress_install(output: &str) -> String {
47    let mut kept: Vec<String> = Vec::new();
48    for raw in output.lines() {
49        let line = strip_ansi(raw);
50        let t = line.trim();
51        if t.is_empty() {
52            continue;
53        }
54        let l = t.to_ascii_lowercase();
55        if l.starts_with("fetching")
56            || l.starts_with("parsing documentation")
57            || l.starts_with("installing ri")
58            || l.starts_with("installing rdoc")
59            || l.starts_with("done installing documentation")
60            || l.starts_with("building native extensions")
61        {
62            continue;
63        }
64        if l.starts_with("successfully installed")
65            || l.starts_with("successfully uninstalled")
66            || l.contains("gems installed")
67            || l.contains("gem installed")
68            || l.contains("error")
69            || l.contains("could not")
70            || l.contains("conflict")
71        {
72            kept.push(t.to_string());
73        }
74    }
75    if kept.is_empty() {
76        return "gem: ok".to_string();
77    }
78    kept.join("\n")
79}
80
81fn fallback(text: &str) -> String {
82    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
83    let n = lines.len().min(10);
84    let mut s = lines[..n].join("\n");
85    if lines.len() > n {
86        s.push_str(&format!("\n... (+{} lines)", lines.len() - n));
87    }
88    s
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn install_keeps_success_drops_docs() {
97        let out = "Fetching rails-7.1.0.gem\nFetching activesupport-7.1.0.gem\nSuccessfully installed activesupport-7.1.0\nSuccessfully installed rails-7.1.0\nParsing documentation for rails-7.1.0\nInstalling ri documentation for rails-7.1.0\nDone installing documentation for rails after 3 seconds\n2 gems installed";
98        let r = compress("gem install rails", out).unwrap();
99        assert!(r.contains("Successfully installed rails-7.1.0"), "{r}");
100        assert!(r.contains("2 gems installed"), "{r}");
101        assert!(!r.contains("Fetching"), "drops fetch noise: {r}");
102        assert!(!r.contains("Parsing documentation"), "drops doc noise: {r}");
103    }
104
105    #[test]
106    fn search_counts_and_caps() {
107        // `gem search` (remote) is the reachable list path — `gem list` is
108        // intercepted upstream as Verbatim and never lands here.
109        let out = "*** REMOTE GEMS ***\n\nbundler (2.5.0)\nrails (7.1.0)\nrake (13.1.0)";
110        let r = compress("gem search rails", out).unwrap();
111        assert!(r.contains("gem: 3 gem(s)"), "{r}");
112        assert!(r.contains("rails (7.1.0)"), "{r}");
113    }
114
115    #[test]
116    fn empty_is_ok() {
117        assert_eq!(compress("gem install x", "").unwrap(), "gem: ok");
118    }
119}