1use std::collections::HashMap;
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("s3 ls") || cmd.contains("s3 cp") || cmd.contains("s3 sync") {
12 return Some(compress_s3(cmd, trimmed));
13 }
14 if cmd.contains("ec2 describe-instances") {
15 return Some(compress_ec2_instances(trimmed));
16 }
17 if cmd.contains("lambda list-functions") {
18 return Some(compress_lambda_list(trimmed));
19 }
20 if cmd.contains("cloudformation describe-stacks") || cmd.contains("cfn ") {
21 return Some(compress_cfn(trimmed));
22 }
23 if cmd.contains("sts get-caller-identity") {
24 return Some(trimmed.to_string());
25 }
26 if cmd.contains("logs") {
27 return Some(compress_logs(trimmed));
28 }
29 if cmd.contains("ecs list") || cmd.contains("ecs describe") {
30 return Some(compress_ecs(trimmed));
31 }
32
33 Some(compact_json_or_text(trimmed, 15))
34}
35
36fn compress_s3(cmd: &str, output: &str) -> String {
37 if cmd.contains("s3 ls") {
38 let entries: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
39 if entries.len() <= 20 {
40 return entries.join("\n");
41 }
42 let dirs: Vec<&&str> = entries.iter().filter(|l| l.contains("PRE ")).collect();
43 let files: Vec<&&str> = entries.iter().filter(|l| !l.contains("PRE ")).collect();
44 return format!(
45 "{} dirs, {} files\n{}",
46 dirs.len(),
47 files.len(),
48 entries
49 .iter()
50 .take(15)
51 .copied()
52 .collect::<Vec<_>>()
53 .join("\n")
54 );
55 }
56
57 let mut uploaded = 0u32;
58 let mut copied = 0u32;
59 for line in output.lines() {
60 if line.contains("upload:") {
61 uploaded += 1;
62 }
63 if line.contains("copy:") {
64 copied += 1;
65 }
66 }
67 if uploaded + copied == 0 {
68 return compact_lines(output, 10);
69 }
70 let mut result = String::new();
71 if uploaded > 0 {
72 result.push_str(&format!("{uploaded} uploaded"));
73 }
74 if copied > 0 {
75 if !result.is_empty() {
76 result.push_str(", ");
77 }
78 result.push_str(&format!("{copied} copied"));
79 }
80 result
81}
82
83fn compress_ec2_instances(output: &str) -> String {
84 if let Ok(val) = serde_json::from_str::<serde_json::Value>(output) {
85 let reservations = val.get("Reservations").and_then(|r| r.as_array());
86 if let Some(res) = reservations {
87 let mut instances = Vec::new();
88 for r in res {
89 if let Some(insts) = r.get("Instances").and_then(|i| i.as_array()) {
90 for inst in insts {
91 let id = inst
92 .get("InstanceId")
93 .and_then(|v| v.as_str())
94 .unwrap_or("?");
95 let state = inst
96 .get("State")
97 .and_then(|s| s.get("Name"))
98 .and_then(|n| n.as_str())
99 .unwrap_or("?");
100 let itype = inst
101 .get("InstanceType")
102 .and_then(|v| v.as_str())
103 .unwrap_or("?");
104 let name = inst
105 .get("Tags")
106 .and_then(|t| t.as_array())
107 .and_then(|tags| {
108 tags.iter()
109 .find(|t| t.get("Key").and_then(|k| k.as_str()) == Some("Name"))
110 })
111 .and_then(|t| t.get("Value").and_then(|v| v.as_str()))
112 .unwrap_or("-");
113 instances.push(format!(" {id} {state} {itype} \"{name}\""));
114 }
115 }
116 }
117 return format!("{} instances:\n{}", instances.len(), instances.join("\n"));
118 }
119 }
120 compact_lines(output, 15)
121}
122
123fn compress_lambda_list(output: &str) -> String {
124 if let Ok(val) = serde_json::from_str::<serde_json::Value>(output)
125 && let Some(fns) = val.get("Functions").and_then(|f| f.as_array())
126 {
127 let items: Vec<String> = fns
128 .iter()
129 .map(|f| {
130 let name = f
131 .get("FunctionName")
132 .and_then(|v| v.as_str())
133 .unwrap_or("?");
134 let runtime = f.get("Runtime").and_then(|v| v.as_str()).unwrap_or("?");
135 let mem = f
136 .get("MemorySize")
137 .and_then(serde_json::Value::as_u64)
138 .unwrap_or(0);
139 format!(" {name} ({runtime}, {mem}MB)")
140 })
141 .collect();
142 return format!("{} functions:\n{}", items.len(), items.join("\n"));
143 }
144 compact_lines(output, 15)
145}
146
147fn compress_cfn(output: &str) -> String {
148 if let Ok(val) = serde_json::from_str::<serde_json::Value>(output)
149 && let Some(stacks) = val.get("Stacks").and_then(|s| s.as_array())
150 {
151 let items: Vec<String> = stacks
152 .iter()
153 .map(|s| {
154 let name = s.get("StackName").and_then(|v| v.as_str()).unwrap_or("?");
155 let status = s.get("StackStatus").and_then(|v| v.as_str()).unwrap_or("?");
156 format!(" {name}: {status}")
157 })
158 .collect();
159 return format!("{} stacks:\n{}", items.len(), items.join("\n"));
160 }
161 compact_lines(output, 10)
162}
163
164fn compress_logs(output: &str) -> String {
165 let lines: Vec<&str> = output.lines().collect();
166 if lines.len() <= 20 {
167 return output.to_string();
168 }
169 let mut deduped: HashMap<String, u32> = HashMap::new();
170 for line in &lines {
171 let key = line
172 .split_whitespace()
173 .skip(2)
174 .collect::<Vec<_>>()
175 .join(" ");
176 if !key.is_empty() {
177 *deduped.entry(key).or_insert(0) += 1;
178 }
179 }
180 let mut sorted: Vec<_> = deduped.into_iter().collect();
181 sorted.sort_by_key(|x| std::cmp::Reverse(x.1));
182 let top: Vec<String> = sorted
183 .iter()
184 .take(15)
185 .map(|(msg, count)| {
186 if *count > 1 {
187 format!(" ({count}x) {msg}")
188 } else {
189 format!(" {msg}")
190 }
191 })
192 .collect();
193 format!(
194 "{} log entries (deduped to {}):\n{}",
195 lines.len(),
196 top.len(),
197 top.join("\n")
198 )
199}
200
201fn compress_ecs(output: &str) -> String {
202 compact_json_or_text(output, 15)
203}
204
205fn compact_json_or_text(text: &str, max: usize) -> String {
206 if let Ok(val) = serde_json::from_str::<serde_json::Value>(text) {
207 let keys = extract_top_keys(&val);
208 if !keys.is_empty() {
209 return format!("JSON: {{{}}}", keys.join(", "));
210 }
211 }
212 compact_lines(text, max)
213}
214
215fn extract_top_keys(val: &serde_json::Value) -> Vec<String> {
216 match val {
217 serde_json::Value::Object(map) => map
218 .keys()
219 .take(20)
220 .map(|k| {
221 let v = &map[k];
222 match v {
223 serde_json::Value::Array(a) => format!("{k}: [{} items]", a.len()),
224 serde_json::Value::Object(_) => format!("{k}: {{...}}"),
225 _ => format!("{k}: {v}"),
226 }
227 })
228 .collect(),
229 _ => vec![],
230 }
231}
232
233fn compact_lines(text: &str, max: usize) -> String {
234 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
235 if lines.len() <= max {
236 return lines.join("\n");
237 }
238 format!(
239 "{}\n{}",
240 lines[..max].join("\n"),
241 super::elision_marker(lines.len() - max)
242 )
243}