lean_ctx/core/patterns/
pip.rs1use regex::Regex;
2use std::sync::OnceLock;
3
4static PIP_INSTALLED_RE: OnceLock<Regex> = OnceLock::new();
5static PIP_OUTDATED_RE: OnceLock<Regex> = OnceLock::new();
6
7fn pip_installed_re() -> &'static Regex {
8 PIP_INSTALLED_RE.get_or_init(|| Regex::new(r"Successfully installed\s+(.+)").unwrap())
9}
10fn pip_outdated_re() -> &'static Regex {
11 PIP_OUTDATED_RE.get_or_init(|| Regex::new(r"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)").unwrap())
12}
13
14pub fn compress(command: &str, output: &str) -> Option<String> {
15 if command.contains("install") {
16 return Some(compress_install(output));
17 }
18 if command.contains("list") || command.contains("freeze") {
19 if command.contains("outdated") || command.contains("--outdated") {
20 return Some(compress_outdated(output));
21 }
22 return Some(compress_list(output));
23 }
24 if command.contains("uninstall") {
25 return Some(compress_uninstall(output));
26 }
27 if command.contains("show") {
28 return Some(compress_show(output));
29 }
30 if command.contains("check") {
31 return Some(compress_check(output));
32 }
33 None
34}
35
36fn compress_install(output: &str) -> String {
37 let trimmed = output.trim();
38 if trimmed.is_empty() {
39 return "ok".to_string();
40 }
41
42 if let Some(caps) = pip_installed_re().captures(trimmed) {
43 let packages: Vec<&str> = caps[1].split_whitespace().collect();
44 return format!("ok (+{} packages): {}", packages.len(), packages.join(", "));
45 }
46
47 if trimmed.contains("already satisfied") {
48 return "ok (already satisfied)".to_string();
49 }
50
51 compact_output(trimmed, 5)
52}
53
54fn compress_list(output: &str) -> String {
55 let lines: Vec<&str> = output.lines().collect();
56 if lines.len() <= 2 {
57 return output.to_string();
58 }
59
60 let skip = if lines[0].starts_with("Package") || lines[0].starts_with("---") {
61 2
62 } else {
63 0
64 };
65 let packages: Vec<String> = lines[skip..]
66 .iter()
67 .filter_map(|l| {
68 let parts: Vec<&str> = l.split_whitespace().collect();
69 if parts.len() >= 2 {
70 Some(format!("{}=={}", parts[0], parts[1]))
71 } else {
72 None
73 }
74 })
75 .collect();
76
77 if packages.is_empty() {
78 return output.to_string();
79 }
80 format!("{} packages:\n{}", packages.len(), packages.join("\n"))
81}
82
83fn compress_outdated(output: &str) -> String {
84 let lines: Vec<&str> = output.lines().collect();
85 let skip = if lines
86 .first()
87 .map(|l| l.starts_with("Package"))
88 .unwrap_or(false)
89 {
90 2
91 } else {
92 0
93 };
94
95 let mut outdated = Vec::new();
96 for line in lines.iter().skip(skip) {
97 if let Some(caps) = pip_outdated_re().captures(line) {
98 let name = &caps[1];
99 let current = &caps[2];
100 let latest = &caps[3];
101 outdated.push(format!("{name}: {current} → {latest}"));
102 }
103 }
104
105 if outdated.is_empty() {
106 return "all up-to-date".to_string();
107 }
108 format!("{} outdated:\n{}", outdated.len(), outdated.join("\n"))
109}
110
111fn compress_uninstall(output: &str) -> String {
112 let trimmed = output.trim();
113 if trimmed.is_empty() {
114 return "ok".to_string();
115 }
116
117 let removed: Vec<&str> = trimmed
118 .lines()
119 .filter(|l| l.contains("Successfully uninstalled"))
120 .collect();
121
122 if removed.is_empty() {
123 return compact_output(trimmed, 3);
124 }
125 format!("ok (removed {} packages)", removed.len())
126}
127
128fn compress_show(output: &str) -> String {
129 compact_output(output, 10)
130}
131
132fn compress_check(output: &str) -> String {
133 let trimmed = output.trim();
134 if trimmed.is_empty() || trimmed.contains("No broken requirements") {
135 return "ok (no broken dependencies)".to_string();
136 }
137
138 let broken: Vec<&str> = trimmed
139 .lines()
140 .filter(|l| l.contains("requires") || l.contains("has requirement"))
141 .collect();
142
143 if broken.is_empty() {
144 return compact_output(trimmed, 5);
145 }
146 format!(
147 "{} broken dependencies:\n{}",
148 broken.len(),
149 broken.join("\n")
150 )
151}
152
153fn compact_output(text: &str, max: usize) -> String {
154 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
155 if lines.len() <= max {
156 return lines.join("\n");
157 }
158 format!(
159 "{}\n... ({} more lines)",
160 lines[..max].join("\n"),
161 lines.len() - max
162 )
163}