lean_ctx/core/patterns/
just.rs1use std::collections::HashMap;
2
3pub fn compress(command: &str, output: &str) -> Option<String> {
4 if command.contains("--list") || command.contains("-l") {
5 return Some(compress_list(output));
6 }
7 if command.contains("--summary") {
8 return Some(compress_summary(output));
9 }
10 if command.contains("--evaluate") {
11 return Some(compress_evaluate(output));
12 }
13 Some(compress_run(output))
14}
15
16fn compress_list(output: &str) -> String {
17 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
18
19 let header = lines.first().filter(|l| l.contains("Available recipes"));
20
21 let recipes: Vec<&str> = lines
22 .iter()
23 .filter(|l| {
24 let t = l.trim();
25 !t.is_empty() && !t.starts_with("Available") && !t.starts_with("Wrote:")
26 })
27 .copied()
28 .collect();
29
30 let mut result = if let Some(h) = header {
31 format!("{}\n", h.trim())
32 } else {
33 format!("{} recipes:\n", recipes.len())
34 };
35
36 for r in recipes.iter().take(30) {
37 result.push_str(&format!(" {}\n", r.trim()));
38 }
39 if recipes.len() > 30 {
40 result.push_str(&format!(" ... +{} more\n", recipes.len() - 30));
41 }
42
43 result.trim_end().to_string()
44}
45
46fn compress_summary(output: &str) -> String {
47 let recipes: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
48 format!("{} recipes: {}", recipes.len(), recipes.join(", "))
49}
50
51fn compress_evaluate(output: &str) -> String {
52 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
53 if lines.len() <= 10 {
54 return lines.join("\n");
55 }
56 format!(
57 "{}\n... ({} more lines)",
58 lines[..10].join("\n"),
59 lines.len() - 10
60 )
61}
62
63fn compress_run(output: &str) -> String {
64 let lines: Vec<&str> = output.lines().collect();
65 let mut kept = Vec::new();
66 let mut echo_dedup: HashMap<String, u32> = HashMap::new();
67 let mut last_error: Option<String> = None;
68
69 for line in &lines {
70 let trimmed = line.trim();
71
72 if trimmed.starts_with("===>") || trimmed.starts_with("==>") {
73 kept.push(trimmed.to_string());
74 continue;
75 }
76
77 if trimmed.starts_with("error") || trimmed.contains("Error:") || trimmed.contains("FAILED")
78 {
79 last_error = Some(trimmed.to_string());
80 kept.push(trimmed.to_string());
81 continue;
82 }
83
84 if trimmed.starts_with("warning") || trimmed.contains("Warning:") {
85 kept.push(trimmed.to_string());
86 continue;
87 }
88
89 let echo_key = if trimmed.len() > 80 {
90 trimmed[..80].to_string()
91 } else {
92 trimmed.to_string()
93 };
94 *echo_dedup.entry(echo_key).or_insert(0) += 1;
95 }
96
97 let dedup_count = echo_dedup.values().filter(|&&v| v > 1).count();
98 let total_lines = lines.len();
99
100 if kept.is_empty() && total_lines <= 20 {
101 return output.to_string();
102 }
103
104 if kept.is_empty() {
105 let shown = lines
106 .iter()
107 .take(15)
108 .copied()
109 .collect::<Vec<_>>()
110 .join("\n");
111 if total_lines > 15 {
112 return format!("{shown}\n... ({} more lines)", total_lines - 15);
113 }
114 return shown;
115 }
116
117 let mut result = kept.join("\n");
118 if dedup_count > 0 {
119 result.push_str(&format!(
120 "\n({dedup_count} repeated line groups suppressed)"
121 ));
122 }
123 if let Some(err) = last_error {
124 if !result.contains(&err) {
125 result.push_str(&format!("\nlast error: {err}"));
126 }
127 }
128 result
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn compresses_list() {
137 let output = "Available recipes:\n build\n test\n lint\n deploy\n clean\n";
138 let result = compress("just --list", output).unwrap();
139 assert!(result.contains("Available recipes"), "should keep header");
140 assert!(result.contains("build"), "should list recipes");
141 }
142
143 #[test]
144 fn compresses_summary() {
145 let output = "build\ntest\nlint\ndeploy\n";
146 let result = compress("just --summary", output).unwrap();
147 assert!(result.contains("4 recipes"), "should count recipes");
148 }
149
150 #[test]
151 fn compresses_run_with_errors() {
152 let output =
153 "===> Building project\nCompiling step 1\nCompiling step 2\nerror: build failed\n";
154 let result = compress("just build", output).unwrap();
155 assert!(result.contains("===> Building"), "should keep headers");
156 assert!(result.contains("error: build failed"), "should keep errors");
157 }
158
159 #[test]
160 fn short_output_passthrough() {
161 let output = "done\n";
162 let result = compress("just clean", output).unwrap();
163 assert_eq!(result.trim(), "done");
164 }
165}