Skip to main content

lean_ctx_patterns/
aws.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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}