lean_ctx/core/patterns/
ansible.rs1use std::collections::HashMap;
2
3pub fn compress(_cmd: &str, output: &str) -> Option<String> {
4 let trimmed = output.trim();
5 if trimmed.is_empty() {
6 return Some("ok".to_string());
7 }
8
9 let has_recap = trimmed.contains("PLAY RECAP");
10 if has_recap {
11 return Some(compress_playbook(trimmed));
12 }
13
14 if trimmed.contains("TASK [") {
15 return Some(compress_tasks(trimmed));
16 }
17
18 Some(compact_lines(trimmed, 15))
19}
20
21fn compress_playbook(output: &str) -> String {
22 let mut recap_lines = Vec::new();
23 let mut in_recap = false;
24
25 for line in output.lines() {
26 if line.contains("PLAY RECAP") {
27 in_recap = true;
28 continue;
29 }
30 if in_recap {
31 let trimmed = line.trim();
32 if !trimmed.is_empty() {
33 recap_lines.push(trimmed.to_string());
34 }
35 }
36 }
37
38 if recap_lines.is_empty() {
39 return compact_lines(output, 15);
40 }
41
42 let mut result = String::from("PLAY RECAP:");
43 for line in &recap_lines {
44 result.push_str(&format!("\n {line}"));
45 }
46 result
47}
48
49fn compress_tasks(output: &str) -> String {
50 let mut tasks: HashMap<String, Vec<String>> = HashMap::new();
51
52 for line in output.lines() {
53 let trimmed = line.trim();
54 if trimmed.starts_with("ok:")
55 || trimmed.starts_with("changed:")
56 || trimmed.starts_with("failed:")
57 || trimmed.starts_with("skipping:")
58 {
59 let status = trimmed.split(':').next().unwrap_or("?").to_string();
60 tasks.entry(status).or_default().push(trimmed.to_string());
61 }
62 }
63
64 if tasks.is_empty() {
65 return compact_lines(output, 15);
66 }
67
68 let mut result = Vec::new();
69 for (status, items) in &tasks {
70 result.push(format!("{status}: {}", items.len()));
71 }
72 result.join(", ")
73}
74
75fn compact_lines(text: &str, max: usize) -> String {
76 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
77 if lines.len() <= max {
78 return lines.join("\n");
79 }
80 format!(
81 "{}\n... ({} more lines)",
82 lines[..max].join("\n"),
83 lines.len() - max
84 )
85}