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