use anyhow::{Context, Result, bail};
use console::style;
use std::process::Command;
const MAX_TAIL_LINES: usize = 40;
pub fn run_command(cmd: &mut Command, label: &str, verbose: bool) -> Result<()> {
if verbose {
let status = cmd
.status()
.with_context(|| format!("failed to start {label}"))?;
if !status.success() {
bail!("{label} failed with status {status}");
}
return Ok(());
}
let output = cmd
.output()
.with_context(|| format!("failed to start {label}"))?;
let stdout = decode_output(&output.stdout);
let stderr = decode_output(&output.stderr);
if !output.status.success() {
let mut message = format!("{label} failed with status {}", output.status);
let rendered = render_failure(&stdout, &stderr);
if !rendered.is_empty() {
message.push_str("\n\n");
message.push_str(&rendered);
}
message.push_str(&format!(
"\n\n{} re-run with {} for full output.",
style("hint:").yellow().bold(),
style("-v").yellow(),
));
bail!(message);
}
Ok(())
}
fn decode_output(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).replace("\r\n", "\n")
}
fn render_failure(stdout: &str, stderr: &str) -> String {
let mut sections = Vec::new();
let stderr_trimmed = stderr.trim_end();
if !stderr_trimmed.is_empty() {
let body = focus_errors(stderr_trimmed);
sections.push(format!("{}\n{}", style("stderr:").red().bold(), body));
}
let stdout_trimmed = stdout.trim_end();
if !stdout_trimmed.is_empty() {
let body = tail_lines(stdout_trimmed, MAX_TAIL_LINES);
sections.push(format!("{}\n{}", style("stdout:").dim().bold(), body));
}
sections.join("\n\n")
}
fn focus_errors(text: &str) -> String {
if let Some(idx) = find_error_marker(text) {
let (skipped_prefix, rest) = text.split_at(idx);
let elided = if skipped_prefix.is_empty() {
String::new()
} else {
format!("{}\n", style("… (earlier output elided)").dim())
};
format!("{elided}{rest}")
} else {
tail_lines(text, MAX_TAIL_LINES)
}
}
fn find_error_marker(text: &str) -> Option<usize> {
let mut search_from = 0;
while let Some(rel) = text[search_from..].find("error") {
let abs = search_from + rel;
let at_line_start = abs == 0 || text.as_bytes()[abs - 1] == b'\n';
let after = &text[abs + "error".len()..];
let looks_like_diagnostic = after.starts_with(':') || after.starts_with('[');
if at_line_start && looks_like_diagnostic {
return Some(abs);
}
search_from = abs + "error".len();
}
None
}
fn tail_lines(text: &str, max: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= max {
return text.to_string();
}
let kept = &lines[lines.len() - max..];
let elided = lines.len() - max;
format!(
"{}\n{}",
style(format!("… ({elided} earlier lines elided)")).dim(),
kept.join("\n"),
)
}