1use crate::ansi::strip_ansi;
4
5pub fn compress(cmd: &str, output: &str) -> Option<String> {
6 let trimmed = output.trim();
7 if trimmed.is_empty() {
8 return Some("ok".to_string());
9 }
10
11 if cmd.contains("test") {
12 return Some(compress_test(trimmed));
13 }
14 if cmd.contains("install") || cmd.contains("add") || cmd.contains("remove") {
15 return Some(compress_install(trimmed));
16 }
17 if cmd.contains("build") || cmd.contains("run") {
18 return Some(compress_build(trimmed));
19 }
20
21 Some(compact_lines(trimmed, 15))
22}
23
24fn compress_test(output: &str) -> String {
25 let mut passed = 0u32;
26 let mut failed = 0u32;
27 let mut skipped = 0u32;
28 let mut failures = Vec::new();
29 let mut time = String::new();
30
31 for line in output.lines() {
32 let trimmed = line.trim();
33 let plain = strip_ansi(trimmed);
34 if plain.contains("pass") && (plain.contains("tests") || plain.contains("test")) {
35 for word in plain.split_whitespace() {
36 if let Ok(n) = word.parse::<u32>() {
37 passed = n;
38 break;
39 }
40 }
41 }
42 if plain.contains("fail") && !plain.starts_with("FAIL") {
43 for word in plain.split_whitespace() {
44 if let Ok(n) = word.parse::<u32>() {
45 failed = n;
46 break;
47 }
48 }
49 }
50 if plain.contains("skip") {
51 for word in plain.split_whitespace() {
52 if let Ok(n) = word.parse::<u32>() {
53 skipped = n;
54 break;
55 }
56 }
57 }
58 if plain.starts_with("FAIL") || plain.starts_with("✗") || plain.starts_with("×") {
59 failures.push(plain.clone());
60 }
61 if (plain.contains("Ran") || plain.contains("Done"))
62 && (plain.contains("ms") || plain.contains('s'))
63 {
64 time.clone_from(&plain);
65 }
66 }
67
68 if passed == 0 && failed == 0 {
69 return compact_lines(output, 10);
70 }
71
72 let mut result = format!("bun test: {passed} passed");
73 if failed > 0 {
74 result.push_str(&format!(", {failed} failed"));
75 }
76 if skipped > 0 {
77 result.push_str(&format!(", {skipped} skipped"));
78 }
79 if !time.is_empty() {
80 result.push_str(&format!(" ({time})"));
81 }
82 for f in failures.iter().take(5) {
83 result.push_str(&format!("\n {f}"));
84 }
85 result
86}
87
88fn compress_install(output: &str) -> String {
89 let mut installed = 0u32;
90 let mut removed = 0u32;
91 let mut time = String::new();
92
93 for line in output.lines() {
94 let plain = strip_ansi(line.trim());
95 if plain.contains("installed") || plain.starts_with('+') {
96 installed += 1;
97 }
98 if plain.contains("removed") || plain.starts_with('-') {
99 removed += 1;
100 }
101 if plain.contains("done") && (plain.contains("ms") || plain.contains('s')) {
102 time.clone_from(&plain);
103 }
104 }
105
106 let mut parts = Vec::new();
107 if installed > 0 {
108 parts.push(format!("{installed} installed"));
109 }
110 if removed > 0 {
111 parts.push(format!("{removed} removed"));
112 }
113 if !time.is_empty() {
114 parts.push(time);
115 }
116
117 if parts.is_empty() {
118 return compact_lines(output, 5);
119 }
120 format!("bun: {}", parts.join(", "))
121}
122
123fn compress_build(output: &str) -> String {
124 let lines: Vec<&str> = output.lines().collect();
125 let errors: Vec<&&str> = lines
126 .iter()
127 .filter(|l| l.contains("error") || l.contains("Error"))
128 .collect();
129 if !errors.is_empty() {
130 let mut result = format!("{} errors:", errors.len());
131 for e in errors.iter().take(10) {
132 result.push_str(&format!("\n {}", e.trim()));
133 }
134 return result;
135 }
136 compact_lines(output, 10)
137}
138
139fn compact_lines(text: &str, max: usize) -> String {
140 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
141 if lines.len() <= max {
142 return lines.join("\n");
143 }
144 format!(
145 "{}\n{}",
146 lines[..max].join("\n"),
147 super::elision_marker(lines.len() - max)
148 )
149}