use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static VERSION_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Vercel CLI \d+\.\d+\.\d+[^\n]*\n?").unwrap());
static UPLOAD_PROGRESS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[^\n]*\[=+[^\]]*\][^\n]*\n?").unwrap());
static INTERIM_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^[^\n]*(Queued|Queuing|Initializing|Cloning repo)[^\n]*\n?").unwrap()
});
static SPINNER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s*[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏][^\n]*\n?").unwrap());
pub fn compress_deploy(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = VERSION_RE.replace_all(&cleaned, "");
let s = UPLOAD_PROGRESS_RE.replace_all(&s, "");
let s = INTERIM_RE.replace_all(&s, "");
let s = SPINNER_RE.replace_all(&s, "");
let useful: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty()
&& (t.contains("vercel.app")
|| t.contains(".vercel.app")
|| t.contains("Production:")
|| t.contains("Preview:")
|| t.contains("Inspect:")
|| t.contains("✅")
|| t.contains("✓")
|| t.contains("Deployed")
|| t.contains("deployed")
|| t.contains("Deployment complete")
|| t.contains("Build Completed")
|| t.contains("Duration:")
|| t.contains("error")
|| t.contains("Error")
|| t.contains("Warning")
|| t.starts_with("Error:"))
})
.collect();
if useful.is_empty() {
return compactor::collapse_blanks(&s);
}
useful.join("\n")
}
pub fn compress_build(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = VERSION_RE.replace_all(&cleaned, "");
let useful: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty()
&& (t.contains("error")
|| t.contains("Error")
|| t.contains("warn")
|| t.contains("Built")
|| t.contains("Build Completed")
|| t.contains("Route")
|| t.contains("Page")
|| t.contains("First Load")
|| t.contains("chunks")
|| t.contains("Duration")
|| t.contains("✓")
|| t.contains("λ")
|| t.contains("○"))
})
.collect();
if useful.is_empty() {
return compactor::collapse_blanks(&s);
}
useful.join("\n")
}
pub fn compress_list(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = VERSION_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 30 {
return format!(
"{}\n… [{} more lines] …",
lines[..30].join("\n"),
lines.len() - 30
);
}
lines.join("\n")
}
pub fn compress_logs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 50 {
return format!(
"{}\n… [{} more log lines] …",
lines[..50].join("\n"),
lines.len() - 50
);
}
lines.join("\n")
}
pub fn compress_vercel(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.is_empty() || sub.starts_with("deploy") || sub.starts_with("--prod") {
return compress_deploy(raw);
}
if sub.starts_with("build") {
return compress_build(raw);
}
if sub.starts_with("logs") {
return compress_logs(raw);
}
if sub.starts_with("env")
|| sub.starts_with("domains")
|| sub.starts_with("teams")
|| sub.starts_with("projects")
|| sub.starts_with("ls")
|| sub.starts_with("list")
{
return compress_list(raw);
}
let cleaned = compactor::normalise(raw);
let s = VERSION_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deploy_keeps_preview_url() {
let raw = "Vercel CLI 33.4.0\nDeploying my-app...\nQueued\nBuilding\n✅ Build Completed in /vercel/output [42s]\n⠸ Deploying...\nProduction: https://my-app.vercel.app [copied to clipboard]\n";
let out = compress_deploy(raw);
assert!(out.contains("vercel.app"), "{out}");
assert!(out.contains("Build Completed"), "{out}");
assert!(!out.contains("Vercel CLI 33"), "{out}");
assert!(!out.contains("Queued"), "{out}");
}
#[test]
fn deploy_keeps_error_lines() {
let raw = "Vercel CLI 33.4.0\nQueued\nBuilding\nError: Build failed\n ./src/index.ts(10): error TS2304: Cannot find name 'foo'.\n";
let out = compress_deploy(raw);
assert!(out.contains("Error: Build failed"), "{out}");
assert!(out.contains("TS2304"), "{out}");
assert!(!out.contains("Queued"), "{out}");
}
#[test]
fn deploy_strips_upload_progress() {
let raw = "Uploading [========> ] 85%\nUploading [==========] 100%\nDeployment complete\nhttps://my-app-abc123.vercel.app\n";
let out = compress_deploy(raw);
assert!(!out.contains("========"), "{out}");
assert!(out.contains("vercel.app"), "{out}");
}
#[test]
fn logs_truncates_long_output() {
let lines: Vec<String> = (0..60)
.map(|i| format!("2024-01-01T00:00:{i:02}Z INFO event {i}"))
.collect();
let out = compress_logs(&lines.join("\n"));
assert!(out.contains("more log lines"), "{out}");
}
}