use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static ANNOTATION_BLOCK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?ms)^Annotations:\s+\S[^\n]*(\n\s+\S[^\n]*)*\n?").unwrap());
static LABEL_BLOCK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?ms)^Labels:\s+\S[^\n]*(\n\s+\S[^\n]*)*\n?").unwrap());
static MANAGED_FIELDS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?ms)^Managed Fields:\n(\s+[^\n]+\n)+").unwrap());
pub fn compress_get(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 30 {
return cleaned;
}
format!(
"{}\n... [{} more rows] ...",
lines[..30].join("\n"),
lines.len() - 30
)
}
pub fn compress_describe(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = ANNOTATION_BLOCK_RE.replace_all(&cleaned, "");
let s = LABEL_BLOCK_RE.replace_all(&s, "");
let s = MANAGED_FIELDS_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_apply(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let out: Vec<&str> = cleaned
.lines()
.filter(|l| {
l.contains(" created")
|| l.contains(" configured")
|| l.contains(" unchanged")
|| l.contains(" deleted")
|| l.starts_with("Error")
|| l.starts_with("error")
})
.collect();
if out.is_empty() {
cleaned
} else {
out.join("\n")
}
}
pub fn compress_rollout(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if let Some(&last) = lines.last() {
if lines.len() > 3 {
return format!("... [{} status lines] ...\n{}", lines.len() - 1, last);
}
}
cleaned
}
pub fn compress_logs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
super::super::sys::log_dedup(&cleaned)
}
pub fn compress_events(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 20 {
return cleaned;
}
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut out: Vec<&str> = Vec::new();
for line in &lines {
let key = if line.len() > 60 {
&line[line.len() - 60..]
} else {
line
};
if seen.insert(key) {
out.push(line);
}
}
out.join("\n")
}
pub fn compress_top(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 30 {
return cleaned;
}
format!(
"{}\n... [{} more rows] ...",
lines[..30].join("\n"),
lines.len() - 30
)
}
pub fn compress_exec(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
super::super::sys::log_dedup(&cleaned)
}
pub fn compress_config(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let out: Vec<&str> = cleaned
.lines()
.filter(|l| {
!l.contains("certificate-authority-data")
&& !l.contains("client-certificate-data")
&& !l.contains("client-key-data")
})
.collect();
out.join("\n")
}
pub fn compress_kubectl(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("get") {
return compress_get(raw);
}
if sub.starts_with("describe") {
return compress_describe(raw);
}
if sub.starts_with("apply") || sub.starts_with("create") || sub.starts_with("delete") {
return compress_apply(raw);
}
if sub.starts_with("rollout") {
return compress_rollout(raw);
}
if sub.starts_with("logs") {
return compress_logs(raw);
}
if sub.starts_with("events") {
return compress_events(raw);
}
if sub.starts_with("top") {
return compress_top(raw);
}
if sub.starts_with("exec") {
return compress_exec(raw);
}
if sub.starts_with("config") {
return compress_config(raw);
}
compactor::normalise(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describe_strips_annotations() {
let raw = "Name: my-pod\nAnnotations: kubectl.kubernetes.io/last-applied: {}\n another: val\nStatus: Running\n";
let out = compress_describe(raw);
assert!(!out.contains("Annotations:"), "{out}");
assert!(out.contains("Status: Running"));
}
#[test]
fn apply_keeps_resource_summaries() {
let raw = "deployment.apps/foo configured\nservice/bar unchanged\nconfigmap/baz created\n";
let out = compress_apply(raw);
assert!(out.contains("configured"));
assert!(out.contains("unchanged"));
assert!(out.contains("created"));
}
#[test]
fn get_truncates_long_tables() {
let header = "NAME STATUS AGE\n";
let rows: String = (0..35)
.map(|i| format!("pod-{i} Running 1h\n"))
.collect();
let out = compress_get(&format!("{header}{rows}"));
assert!(out.contains("more rows"), "{out}");
}
#[test]
fn events_deduplicates() {
let raw = "Warning BackOff pod/foo Back-off restarting\nWarning BackOff pod/foo Back-off restarting\nNormal Pulled pod/foo Pulled image\n";
let out = compress_events(raw);
assert!(out.contains("Pulled image"));
}
}