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.get("MemorySize").and_then(|v| v.as_u64()).unwrap_or(0);
133 format!(" {name} ({runtime}, {mem}MB)")
134 })
135 .collect();
136 return format!("{} functions:\n{}", items.len(), items.join("\n"));
137 }
138 }
139 compact_lines(output, 15)
140}
141
142fn compress_cfn(output: &str) -> String {
143 if let Ok(val) = serde_json::from_str::<serde_json::Value>(output) {
144 if let Some(stacks) = val.get("Stacks").and_then(|s| s.as_array()) {
145 let items: Vec<String> = stacks
146 .iter()
147 .map(|s| {
148 let name = s.get("StackName").and_then(|v| v.as_str()).unwrap_or("?");
149 let status = s.get("StackStatus").and_then(|v| v.as_str()).unwrap_or("?");
150 format!(" {name}: {status}")
151 })
152 .collect();
153 return format!("{} stacks:\n{}", items.len(), items.join("\n"));
154 }
155 }
156 compact_lines(output, 10)
157}
158
159fn compress_logs(output: &str) -> String {
160 let lines: Vec<&str> = output.lines().collect();
161 if lines.len() <= 20 {
162 return output.to_string();
163 }
164 let mut deduped: HashMap<String, u32> = HashMap::new();
165 for line in &lines {
166 let key = line
167 .split_whitespace()
168 .skip(2)
169 .collect::<Vec<_>>()
170 .join(" ");
171 if !key.is_empty() {
172 *deduped.entry(key).or_insert(0) += 1;
173 }
174 }
175 let mut sorted: Vec<_> = deduped.into_iter().collect();
176 sorted.sort_by(|a, b| b.1.cmp(&a.1));
177 let top: Vec<String> = sorted
178 .iter()
179 .take(15)
180 .map(|(msg, count)| {
181 if *count > 1 {
182 format!(" ({count}x) {msg}")
183 } else {
184 format!(" {msg}")
185 }
186 })
187 .collect();
188 format!(
189 "{} log entries (deduped to {}):\n{}",
190 lines.len(),
191 top.len(),
192 top.join("\n")
193 )
194}
195
196fn compress_ecs(output: &str) -> String {
197 compact_json_or_text(output, 15)
198}
199
200fn compact_json_or_text(text: &str, max: usize) -> String {
201 if let Ok(val) = serde_json::from_str::<serde_json::Value>(text) {
202 let keys = extract_top_keys(&val);
203 if !keys.is_empty() {
204 return format!("JSON: {{{}}}", keys.join(", "));
205 }
206 }
207 compact_lines(text, max)
208}
209
210fn extract_top_keys(val: &serde_json::Value) -> Vec<String> {
211 match val {
212 serde_json::Value::Object(map) => map
213 .keys()
214 .take(20)
215 .map(|k| {
216 let v = &map[k];
217 match v {
218 serde_json::Value::Array(a) => format!("{k}: [{} items]", a.len()),
219 serde_json::Value::Object(_) => format!("{k}: {{...}}"),
220 _ => format!("{k}: {v}"),
221 }
222 })
223 .collect(),
224 _ => vec![],
225 }
226}
227
228fn compact_lines(text: &str, max: usize) -> String {
229 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
230 if lines.len() <= max {
231 return lines.join("\n");
232 }
233 format!(
234 "{}\n... ({} more lines)",
235 lines[..max].join("\n"),
236 lines.len() - max
237 )
238}