Skip to main content

lean_ctx/core/patterns/
aws.rs

1use 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}