use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static DOWNLOADING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Downloading provider[^\n]*\n?").unwrap());
static INSTALLING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Installing provider[^\n]*\n?").unwrap());
static SPINNER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s*[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏][^\n]*\n?").unwrap());
static TYPE_HEADER_BLANK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s{4,}Type\s+Name\s+Status\s*$").unwrap());
static PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^Do you want to perform this (update|destroy|preview)\?[^\n]*\n?").unwrap()
});
pub fn compress_preview(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = DOWNLOADING_RE.replace_all(&cleaned, "");
let s = INSTALLING_RE.replace_all(&s, "");
let s = SPINNER_RE.replace_all(&s, "");
let s = PROMPT_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_up(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = DOWNLOADING_RE.replace_all(&cleaned, "");
let s = INSTALLING_RE.replace_all(&s, "");
let s = SPINNER_RE.replace_all(&s, "");
let s = PROMPT_RE.replace_all(&s, "");
let s = TYPE_HEADER_BLANK_RE.replace_all(&s, "");
let out: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim();
!t.ends_with("updating...")
&& !t.ends_with("creating...")
&& !t.ends_with("deleting...")
})
.collect();
compactor::collapse_blanks(&out.join("\n"))
}
pub fn compress_destroy(raw: &str) -> String {
compress_up(raw)
}
pub fn compress_stack(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 30 {
return format!(
"{}\n… [{} more stacks] …",
lines[..30].join("\n"),
lines.len() - 30
);
}
lines.join("\n")
}
pub fn compress_logs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
super::super::sys::log_dedup(&cleaned)
}
pub fn compress_pulumi(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("preview") || sub.starts_with("pre") {
return compress_preview(raw);
}
if sub.starts_with("up") || sub.starts_with("update") {
return compress_up(raw);
}
if sub.starts_with("destroy") || sub.starts_with("down") {
return compress_destroy(raw);
}
if sub.starts_with("stack") {
return compress_stack(raw);
}
if sub.starts_with("logs") {
return compress_logs(raw);
}
compactor::normalise(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preview_strips_downloading_provider() {
let raw = "Downloading provider: registry.pulumi.com/pulumi/aws@6.0.0\nInstalling provider pulumi-aws\nPreviewing update (dev):\n\n Type Name Plan\n + pulumi:pulumi:Stack myapp-dev create\n\nResources:\n + 3 to create\n";
let out = compress_preview(raw);
assert!(!out.contains("Downloading provider"), "{out}");
assert!(!out.contains("Installing provider"), "{out}");
assert!(out.contains("Resources:"), "{out}");
assert!(out.contains("3 to create"), "{out}");
}
#[test]
fn up_strips_updating_relay_lines() {
let raw = "Updating (dev):\n\n Type Name Status\n + pulumi:pulumi:Stack myapp-dev updating...\n + aws:s3:Bucket my-bucket creating...\n + aws:s3:Bucket my-bucket created\n\nOutputs:\n bucketName: \"my-bucket-abc\"\n\nResources:\n + 2 created\n\nDuration: 8s\n";
let out = compress_up(raw);
assert!(!out.contains("updating..."), "{out}");
assert!(!out.contains("creating..."), "{out}");
assert!(out.contains("created"), "{out}");
assert!(out.contains("Outputs:"), "{out}");
assert!(out.contains("Duration:"), "{out}");
}
#[test]
fn up_strips_interactive_prompt() {
let raw = "Previewing update (dev):\n\nResources:\n + 1 to create\n\nDo you want to perform this update? yes\nUpdating (dev):\n";
let out = compress_up(raw);
assert!(
!out.contains("Do you want to perform this update?"),
"{out}"
);
assert!(out.contains("Resources:"), "{out}");
}
#[test]
fn destroy_keeps_deleted_summary() {
let raw = "Destroying (dev):\n\n Type Name Status\n - pulumi:pulumi:Stack myapp-dev deleting...\n - aws:s3:Bucket my-bucket deleted\n\nResources:\n - 2 deleted\n\nDuration: 4s\n";
let out = compress_destroy(raw);
assert!(!out.contains("deleting..."), "{out}");
assert!(out.contains("- 2 deleted"), "{out}");
assert!(out.contains("Duration:"), "{out}");
}
#[test]
fn stack_truncates_large_list() {
let rows: Vec<String> = (0..35)
.map(|i| format!("stack-{i} aws https://app.pulumi.com/org/proj/stack-{i}"))
.collect();
let out = compress_stack(&rows.join("\n"));
assert!(out.contains("more stacks"), "{out}");
}
}