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