Skip to main content

lean_ctx/core/patterns/
kubectl.rs

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