use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static CACHE_HASH_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\b(executing|output)\s+[0-9a-f]{8,}\b").unwrap());
static EMPTY_RELAY_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[a-zA-Z0-9_\-./]+:[a-zA-Z0-9_\-]+:\s*$").unwrap());
static SPINNER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s*[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*[^\n]*\n?").unwrap());
static NEXT_NOISE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^[a-zA-Z0-9_\-./]+:[a-zA-Z0-9_\-]+: Creating an optimized[^\n]*\n?").unwrap()
});
const NOISE_PREFIXES: &[&str] = &[
"• Remote caching",
"• Daemon",
"• Running target",
">>> Finished", ];
const NOISE_CONTAINS: &[&str] = &["No cached artifacts found", "No cache entry", "Full Turbo"];
pub fn compress_turbo(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = SPINNER_RE.replace_all(&cleaned, "");
let s = NEXT_NOISE_RE.replace_all(&s, "");
let s = EMPTY_RELAY_RE.replace_all(&s, "");
let mut out: Vec<String> = Vec::new();
let mut in_summary = false;
for line in s.lines() {
let t = line.trim();
if t.starts_with("Tasks:") || t.starts_with("Cached:") || t.starts_with("Time:") {
in_summary = true;
}
if in_summary {
out.push(line.to_string());
continue;
}
if NOISE_PREFIXES.iter().any(|p| t.starts_with(p)) {
continue;
}
if NOISE_CONTAINS.iter().any(|p| t.contains(p)) {
continue;
}
let line_out = CACHE_HASH_RE.replace_all(line, "$1 …").to_string();
out.push(line_out);
}
compactor::collapse_blanks(&out.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_remote_caching_noise() {
let raw = "• Packages in scope: app, docs\n• Running build in 2 packages\n• Remote caching disabled\n\napp:build: cache miss, executing abcdef123456\napp:build: ✓ Compiled successfully\n\nTasks: 2 successful, 2 total\n";
let out = compress_turbo(raw);
assert!(!out.contains("Remote caching"), "{out}");
assert!(out.contains("Packages in scope"), "{out}");
assert!(out.contains("Tasks:"), "{out}");
}
#[test]
fn strips_cache_hash_suffix() {
let raw = "app:build: cache miss, executing abc123def456\ndocs:build: cache hit, replaying output 789abcdef0\n";
let out = compress_turbo(raw);
assert!(!out.contains("abc123def456"), "{out}");
assert!(!out.contains("789abcdef0"), "{out}");
assert!(out.contains("cache miss"), "{out}");
assert!(out.contains("cache hit"), "{out}");
}
#[test]
fn strips_empty_relay_lines() {
let raw = "app:build: \napp:build: \napp:build: > tsc\napp:build: \n";
let out = compress_turbo(raw);
let empty_relays = out
.lines()
.filter(|l| l.trim_end().ends_with(':') || l.trim().is_empty())
.count();
assert!(empty_relays <= 1, "too many empty relay lines: {out}");
}
#[test]
fn keeps_error_output() {
let raw = "app:build: cache miss, executing abc123\napp:build: error TS2304: Cannot find name 'foo'.\napp:build: src/index.ts(10,5): error TS2304\n\nTasks: 0 successful, 1 failed\n";
let out = compress_turbo(raw);
assert!(out.contains("error TS2304"), "{out}");
assert!(out.contains("Tasks:"), "{out}");
}
#[test]
fn keeps_summary_block() {
let raw = "app:build: ✓ done\n\nTasks: 3 successful, 3 total\nCached: 1 cached, 3 total\n Time: 5.432s >>> FULL TURBO\n";
let out = compress_turbo(raw);
assert!(out.contains("Tasks: 3 successful"), "{out}");
assert!(out.contains("Cached:"), "{out}");
assert!(out.contains("Time:"), "{out}");
}
}