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