lean_ctx/core/patterns/
kubectl.rs1macro_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 log_ts_re() -> &'static regex::Regex {
11 static_regex!(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*\s+")
12}
13fn resource_action_re() -> &'static regex::Regex {
14 static_regex!(r"(\S+/\S+)\s+(configured|created|unchanged|deleted)")
15}
16
17pub fn compress(command: &str, output: &str) -> Option<String> {
18 if command.contains("logs") || command.contains("log ") {
19 return Some(compress_logs(output));
20 }
21 if command.contains("describe") {
22 return Some(compress_describe(output));
23 }
24 if command.contains("apply") {
25 return Some(compress_apply(output));
26 }
27 if command.contains("delete") {
28 return Some(compress_delete(output));
29 }
30 if command.contains("get") {
31 return Some(compress_get(output));
32 }
33 if command.contains("exec") {
34 return Some(compress_exec(output));
35 }
36 if command.contains("top") {
37 return Some(compress_top(output));
38 }
39 if command.contains("rollout") {
40 return Some(compress_rollout(output));
41 }
42 if command.contains("scale") {
43 return Some(compress_simple(output));
44 }
45 Some(compact_table(output))
46}
47
48fn compress_get(output: &str) -> String {
49 let lines: Vec<&str> = output.lines().collect();
50 if lines.is_empty() {
51 return "no resources".to_string();
52 }
53 if lines.len() == 1 && lines[0].starts_with("No resources") {
54 return "no resources".to_string();
55 }
56
57 if lines.len() <= 1 {
58 return output.trim().to_string();
59 }
60
61 let header = lines[0];
62 let cols: Vec<&str> = header.split_whitespace().collect();
63
64 let mut rows = Vec::new();
65 for line in &lines[1..] {
66 let parts: Vec<&str> = line.split_whitespace().collect();
67 if parts.is_empty() {
68 continue;
69 }
70
71 let name = parts[0];
72 let relevant: Vec<&str> = parts.iter().skip(1).take(4).copied().collect();
73 rows.push(format!("{name} {}", relevant.join(" ")));
74 }
75
76 if rows.is_empty() {
77 return "no resources".to_string();
78 }
79
80 let col_hint = cols
81 .iter()
82 .skip(1)
83 .take(4)
84 .copied()
85 .collect::<Vec<&str>>()
86 .join(" ");
87 format!("[{col_hint}]\n{}", rows.join("\n"))
88}
89
90fn compress_logs(output: &str) -> String {
91 let lines: Vec<&str> = output.lines().collect();
92 if lines.len() <= 10 {
93 return output.to_string();
94 }
95
96 let mut deduped: Vec<(String, u32)> = Vec::new();
97 for line in &lines {
98 let stripped = log_ts_re().replace(line, "").trim().to_string();
99 if stripped.is_empty() {
100 continue;
101 }
102
103 if let Some(last) = deduped.last_mut() {
104 if last.0 == stripped {
105 last.1 += 1;
106 continue;
107 }
108 }
109 deduped.push((stripped, 1));
110 }
111
112 let result: Vec<String> = deduped
113 .iter()
114 .map(|(line, count)| {
115 if *count > 1 {
116 format!("{line} (x{count})")
117 } else {
118 line.clone()
119 }
120 })
121 .collect();
122
123 if result.len() > 30 {
124 let tail = &result[result.len() - 20..];
125 return format!("... ({} lines total)\n{}", lines.len(), tail.join("\n"));
126 }
127 result.join("\n")
128}
129
130fn compress_describe(output: &str) -> String {
131 let lines: Vec<&str> = output.lines().collect();
132 if lines.len() <= 20 {
133 return output.to_string();
134 }
135
136 let mut sections = Vec::new();
137 let mut current_section = String::new();
138 let mut current_lines: Vec<&str> = Vec::new();
139 for line in &lines {
140 if !line.starts_with(' ')
141 && !line.starts_with('\t')
142 && line.ends_with(':')
143 && !line.contains(" ")
144 {
145 if !current_section.is_empty() {
146 let count = current_lines.len();
147 if count <= 3 {
148 sections.push(format!("{current_section}\n{}", current_lines.join("\n")));
149 } else {
150 sections.push(format!("{current_section} ({count} lines)"));
151 }
152 }
153 current_section = line.trim_end_matches(':').to_string();
154 current_lines.clear();
155 } else {
157 current_lines.push(line);
158 }
159 }
160
161 if !current_section.is_empty() {
162 let count = current_lines.len();
163 if current_section == "Events" && count > 5 {
164 let last_events = ¤t_lines[count.saturating_sub(5)..];
165 sections.push(format!(
166 "Events (last 5 of {count}):\n{}",
167 last_events.join("\n")
168 ));
169 } else if count <= 5 {
170 sections.push(format!("{current_section}\n{}", current_lines.join("\n")));
171 } else {
172 sections.push(format!("{current_section} ({count} lines)"));
173 }
174 }
175
176 sections.join("\n")
177}
178
179fn compress_apply(output: &str) -> String {
180 let trimmed = output.trim();
181 if trimmed.is_empty() {
182 return "ok".to_string();
183 }
184
185 let mut configured = 0u32;
186 let mut created = 0u32;
187 let mut unchanged = 0u32;
188 let mut deleted = 0u32;
189 let mut resources = Vec::new();
190
191 for line in trimmed.lines() {
192 if let Some(caps) = resource_action_re().captures(line) {
193 let resource = &caps[1];
194 let action = &caps[2];
195 match action {
196 "configured" => configured += 1,
197 "created" => created += 1,
198 "unchanged" => unchanged += 1,
199 "deleted" => deleted += 1,
200 _ => {}
201 }
202 resources.push(format!("{resource} {action}"));
203 }
204 }
205
206 let total = configured + created + unchanged + deleted;
207 if total == 0 {
208 return compact_output(trimmed, 5);
209 }
210
211 let mut summary = Vec::new();
212 if created > 0 {
213 summary.push(format!("{created} created"));
214 }
215 if configured > 0 {
216 summary.push(format!("{configured} configured"));
217 }
218 if unchanged > 0 {
219 summary.push(format!("{unchanged} unchanged"));
220 }
221 if deleted > 0 {
222 summary.push(format!("{deleted} deleted"));
223 }
224
225 format!("ok ({total} resources: {})", summary.join(", "))
226}
227
228fn compress_delete(output: &str) -> String {
229 let trimmed = output.trim();
230 if trimmed.is_empty() {
231 return "ok".to_string();
232 }
233
234 let deleted: Vec<&str> = trimmed.lines().filter(|l| l.contains("deleted")).collect();
235
236 if deleted.is_empty() {
237 return compact_output(trimmed, 3);
238 }
239 format!("deleted {} resources", deleted.len())
240}
241
242fn compress_exec(output: &str) -> String {
243 let trimmed = output.trim();
244 if trimmed.is_empty() {
245 return "ok".to_string();
246 }
247 let lines: Vec<&str> = trimmed.lines().collect();
248 if lines.len() > 20 {
249 let tail = &lines[lines.len() - 10..];
250 return format!("... ({} lines)\n{}", lines.len(), tail.join("\n"));
251 }
252 trimmed.to_string()
253}
254
255fn compress_top(output: &str) -> String {
256 compact_table(output)
257}
258
259fn compress_rollout(output: &str) -> String {
260 compact_output(output, 5)
261}
262
263fn compress_simple(output: &str) -> String {
264 let trimmed = output.trim();
265 if trimmed.is_empty() {
266 return "ok".to_string();
267 }
268 compact_output(trimmed, 3)
269}
270
271fn compact_table(text: &str) -> String {
272 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
273 if lines.len() <= 15 {
274 return lines.join("\n");
275 }
276 format!(
277 "{}\n... ({} more rows)",
278 lines[..15].join("\n"),
279 lines.len() - 15
280 )
281}
282
283fn compact_output(text: &str, max: usize) -> String {
284 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
285 if lines.len() <= max {
286 return lines.join("\n");
287 }
288 format!(
289 "{}\n... ({} more lines)",
290 lines[..max].join("\n"),
291 lines.len() - max
292 )
293}