Skip to main content

lean_ctx_patterns/
pnpm.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 pnpm_added_re() -> &'static regex::Regex {
13    static_regex!(r"(\d+) packages? (?:are )?(?:installed|added|updated)")
14}
15
16pub fn compress(command: &str, output: &str) -> Option<String> {
17    if command.contains("install") || command.contains("add") || command.contains("i ") {
18        return Some(compress_install(output));
19    }
20    if command.contains("list") || command.contains("ls") {
21        return Some(compress_list(output));
22    }
23    if command.contains("outdated") {
24        return Some(compress_outdated(output));
25    }
26    if command.contains("run") || command.contains("exec") {
27        return Some(compress_run(output));
28    }
29    if command.contains("test") {
30        return Some(compress_test(output));
31    }
32    if command.contains("why") {
33        return Some(compact_output(output, 10));
34    }
35    if command.contains("store") {
36        return Some(compress_store(output));
37    }
38    None
39}
40
41fn compress_install(output: &str) -> String {
42    let summary = compress_install_summary(output);
43    // Tail lines already shown verbatim must not be repeated.
44    let advisories: Vec<String> = super::npm::install_advisories(output)
45        .into_iter()
46        .filter(|a| !summary.lines().any(|l| l.trim() == a))
47        .collect();
48    if advisories.is_empty() {
49        summary
50    } else {
51        format!("{summary}\n{}", advisories.join("\n"))
52    }
53}
54
55fn compress_install_summary(output: &str) -> String {
56    let trimmed = output.trim();
57    if trimmed.is_empty() {
58        return "ok".to_string();
59    }
60
61    let mut pkg_count = 0u32;
62    if let Some(caps) = pnpm_added_re().captures(trimmed) {
63        pkg_count = caps[1].parse().unwrap_or(0);
64    }
65
66    let progress_free: Vec<&str> = trimmed
67        .lines()
68        .filter(|l| {
69            let t = l.trim();
70            !t.is_empty()
71                && !t.starts_with("Progress:")
72                && !t.starts_with("Already up to date")
73                && !t.contains("Downloading")
74                && !t.contains("fetched from")
75        })
76        .collect();
77
78    if pkg_count > 0 {
79        return format!("ok ({pkg_count} packages installed)");
80    }
81    if progress_free.len() <= 3 {
82        return progress_free.join("\n");
83    }
84    format!(
85        "ok\n{}",
86        progress_free[progress_free.len() - 3..].join("\n")
87    )
88}
89
90fn compress_list(output: &str) -> String {
91    let lines: Vec<&str> = output.lines().collect();
92    if lines.len() <= 5 {
93        return output.to_string();
94    }
95
96    let _deps: Vec<&str> = lines
97        .iter()
98        .filter(|l| l.contains("dependencies:") || l.starts_with(' '))
99        .copied()
100        .collect();
101
102    let top: Vec<String> = lines
103        .iter()
104        .filter(|l| {
105            let trimmed = l.trim();
106            !trimmed.is_empty()
107                && (trimmed.starts_with('+')
108                    || trimmed.starts_with("└")
109                    || trimmed.starts_with("├"))
110        })
111        .map(|l| {
112            l.replace("├──", "")
113                .replace("└──", "")
114                .replace("├─", "")
115                .replace("└─", "")
116                .trim()
117                .to_string()
118        })
119        .collect();
120
121    if !top.is_empty() {
122        return format!("{} packages:\n{}", top.len(), top.join("\n"));
123    }
124    compact_output(output, 15)
125}
126
127fn compress_outdated(output: &str) -> String {
128    let lines: Vec<&str> = output.lines().collect();
129    if lines.len() <= 1 {
130        return "all up-to-date".to_string();
131    }
132
133    let mut packages = Vec::new();
134    for line in &lines[1..] {
135        let parts: Vec<&str> = line.split_whitespace().collect();
136        if parts.len() >= 3 {
137            packages.push(format!("{}: {} → {}", parts[0], parts[1], parts[2]));
138        }
139    }
140
141    if packages.is_empty() {
142        return "all up-to-date".to_string();
143    }
144    format!("{} outdated:\n{}", packages.len(), packages.join("\n"))
145}
146
147fn compress_run(output: &str) -> String {
148    let lines: Vec<&str> = output
149        .lines()
150        .filter(|l| {
151            let t = l.trim();
152            !t.is_empty() && !t.starts_with('>')
153        })
154        .collect();
155
156    if lines.len() <= 5 {
157        return lines.join("\n");
158    }
159    let tail = &lines[lines.len() - 3..];
160    format!("...({} lines)\n{}", lines.len(), tail.join("\n"))
161}
162
163fn compress_test(output: &str) -> String {
164    compress_run(output)
165}
166
167fn compress_store(output: &str) -> String {
168    let trimmed = output.trim();
169    if trimmed.is_empty() {
170        return "ok".to_string();
171    }
172    compact_output(trimmed, 5)
173}
174
175fn compact_output(text: &str, max: usize) -> String {
176    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
177    if lines.len() <= max {
178        return lines.join("\n");
179    }
180    format!(
181        "{}\n{}",
182        lines[..max].join("\n"),
183        super::elision_marker(lines.len() - max)
184    )
185}
186
187#[cfg(test)]
188mod tests {
189    use super::compress;
190
191    #[test]
192    fn install_keeps_vulnerability_summary() {
193        let out = "Packages: +12\n++++++++++++\nProgress: resolved 12, reused 12, downloaded 0, added 12, done\n12 packages are installed\n2 vulnerabilities found\nSeverity: 1 low | 1 high\nRun pnpm audit --fix to resolve\n";
194        let c = compress("pnpm install", out).unwrap();
195        assert!(c.contains("2 vulnerabilities found"), "{c}");
196        assert!(c.contains("pnpm audit --fix"), "{c}");
197        assert_eq!(c.matches("2 vulnerabilities found").count(), 1, "{c}");
198    }
199}