Skip to main content

lean_ctx/core/patterns/
kubectl.rs

1use 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            // Events section detected
154        } 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 = &current_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}