lean_ctx/core/patterns/
terraform.rs1macro_rules! static_regex {
2 ($pattern:expr) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10fn plan_summary_re() -> &'static regex::Regex {
11 static_regex!(r"Plan:\s*(\d+)\s+to add,\s*(\d+)\s+to change,\s*(\d+)\s+to destroy")
12}
13
14fn apply_summary_re() -> &'static regex::Regex {
15 static_regex!(
16 r"Apply complete!\s*Resources:\s*(\d+)\s+added,\s*(\d+)\s+changed,\s*(\d+)\s+destroyed"
17 )
18}
19
20fn installed_provider_re() -> &'static regex::Regex {
21 static_regex!(r"-\s*Installed\s+([^\s]+)\s+v([0-9][^\s]*)")
22}
23
24fn provider_version_re() -> &'static regex::Regex {
25 static_regex!(r"\*\s*provider\[([^\]]+)\]\s+([0-9][^\s]*)")
26}
27
28fn is_provider_init_noise(line: &str) -> bool {
29 let t = line.trim_start();
30 let tl = t.to_ascii_lowercase();
31 tl.contains("initializing provider plugins")
32 || tl.contains("initializing the backend")
33 || tl.contains("finding ")
34 && (tl.contains("versions matching") || tl.contains("version of"))
35 || tl.starts_with("- finding ")
36 || tl.starts_with("- installing ")
37 || tl.contains("terraform init") && tl.contains("upgrade")
38 || tl.starts_with("╷")
39 || tl.starts_with("╵")
40 || tl.starts_with("│")
41}
42
43pub fn compress(command: &str, output: &str) -> Option<String> {
44 let c = command.trim();
45 let prefix = if c == "terraform" || c.starts_with("terraform ") {
46 "terraform"
47 } else if c == "tofu" || c.starts_with("tofu ") {
48 "tofu"
49 } else {
50 return None;
51 };
52 let sub = c.strip_prefix(prefix).map_or("", str::trim_start);
53 let sub_cmd = sub.split_whitespace().next().unwrap_or("");
54
55 match sub_cmd {
56 "plan" => Some(compress_plan(output)),
57 "apply" => Some(compress_apply(output)),
58 "init" => Some(compress_init(output)),
59 "validate" => Some(compress_validate(output)),
60 _ => Some(compress_generic(output)),
61 }
62}
63
64fn compress_plan(output: &str) -> String {
65 let mut kept = Vec::new();
66
67 for line in output.lines() {
68 if is_provider_init_noise(line) {
69 continue;
70 }
71 let tl = line.trim_start();
72 if tl.starts_with("- Installed ") || tl.starts_with("- Installing ") {
73 continue;
74 }
75
76 if let Some(caps) = plan_summary_re().captures(line) {
77 let add = caps.get(1).map_or("0", |m| m.as_str());
78 let chg = caps.get(2).map_or("0", |m| m.as_str());
79 let des = caps.get(3).map_or("0", |m| m.as_str());
80 kept.push(format!("+ {add} added, ~ {chg} changed, - {des} destroyed"));
81 continue;
82 }
83
84 let l = line.to_ascii_lowercase();
85 if l.contains("no changes.") || l.contains("infrastructure matches the configuration") {
86 kept.push("No changes.".to_string());
87 continue;
88 }
89
90 let is_diag = tl.contains('╷')
91 || tl.contains('│')
92 || tl.contains('╵')
93 || l.contains("error:")
94 || (l.contains("error ")
95 && (l.contains("terraform") || l.contains("plan") || l.contains("provider")))
96 || l.contains("warning:")
97 || l.contains("warning ");
98 if is_diag {
99 kept.push(line.trim().to_string());
100 }
101 }
102
103 if kept.is_empty() {
104 "terraform plan (no summary parsed)".to_string()
105 } else {
106 kept.join("\n")
107 }
108}
109
110fn compress_apply(output: &str) -> String {
111 let mut results = Vec::new();
112 let mut errors = Vec::new();
113
114 for line in output.lines() {
115 if is_provider_init_noise(line) {
116 continue;
117 }
118 let tl = line.trim();
119 if tl.is_empty() {
120 continue;
121 }
122
123 if let Some(caps) = apply_summary_re().captures(line) {
124 let a = caps.get(1).map_or("0", |m| m.as_str());
125 let c = caps.get(2).map_or("0", |m| m.as_str());
126 let d = caps.get(3).map_or("0", |m| m.as_str());
127 results.push(format!(
128 "Apply complete: +{a} added, ~{c} changed, -{d} destroyed"
129 ));
130 continue;
131 }
132
133 let ll = tl.to_ascii_lowercase();
134 if ll.contains("error")
135 && (ll.contains("apply") || ll.contains("terraform") || tl.contains('╷'))
136 {
137 errors.push(tl.to_string());
138 } else if ll.starts_with("creation complete")
139 || ll.starts_with("modification complete")
140 || ll.starts_with("destruction complete")
141 || ll.starts_with("destroy complete")
142 {
143 results.push(tl.to_string());
144 }
145 }
146
147 let mut out = Vec::new();
148 if !results.is_empty() {
149 out.push(results.join("\n"));
150 }
151 if !errors.is_empty() {
152 out.push(format!("errors:\n{}", errors.join("\n")));
153 }
154 if out.is_empty() {
155 "terraform apply (no summary parsed)".to_string()
156 } else {
157 out.join("\n\n")
158 }
159}
160
161fn compress_init(output: &str) -> String {
162 let mut providers: Vec<String> = Vec::new();
163 let mut success = false;
164
165 for line in output.lines() {
166 let tl = line.trim();
167 if tl.is_empty() {
168 continue;
169 }
170 let ll = tl.to_ascii_lowercase();
171 if ll.contains("terraform has been successfully initialized")
172 || ll.contains("initialization complete")
173 {
174 success = true;
175 }
176 if let Some(caps) = installed_provider_re().captures(tl) {
177 let name = caps.get(1).map_or("?", |m| m.as_str());
178 let ver = caps.get(2).map_or("?", |m| m.as_str());
179 providers.push(format!("{name} v{ver}"));
180 continue;
181 }
182 if let Some(caps) = provider_version_re().captures(tl) {
183 let reg = caps.get(1).map_or("?", |m| m.as_str());
184 let ver = caps.get(2).map_or("?", |m| m.as_str());
185 providers.push(format!("{reg} {ver}"));
186 }
187 }
188
189 let status = if success {
190 "Terraform initialized"
191 } else {
192 "terraform init"
193 };
194
195 if providers.is_empty() {
196 status.to_string()
197 } else {
198 format!("{status}\n{}", providers.join(", "))
199 }
200}
201
202fn compress_validate(output: &str) -> String {
203 let mut errs = Vec::new();
204 for line in output.lines() {
205 let tl = line.trim();
206 if tl.is_empty() {
207 continue;
208 }
209 let ll = tl.to_ascii_lowercase();
210 if ll.contains("success!") && ll.contains("configuration is valid") {
211 return "Success".to_string();
212 }
213 if ll.contains("error") || tl.starts_with('╷') || tl.starts_with('│') {
214 errs.push(tl.to_string());
215 }
216 }
217 if errs.is_empty() {
218 "Success".to_string()
219 } else {
220 errs.join("\n")
221 }
222}
223
224fn compress_generic(output: &str) -> String {
225 let mut lines: Vec<String> = output
226 .lines()
227 .filter(|l| !is_provider_init_noise(l))
228 .map(|l| l.trim().to_string())
229 .filter(|l| !l.is_empty())
230 .collect();
231 if lines.len() > 40 {
232 let n = lines.len();
233 lines = lines.split_off(n - 25);
234 format!("... (truncated)\n{}", lines.join("\n"))
235 } else {
236 lines.join("\n")
237 }
238}