1use super::{calculate_dir_size, CleanableItem, SafetyLevel};
12use crate::error::Result;
13use std::path::PathBuf;
14
15pub struct CloudCliCleaner {
17 home: PathBuf,
18}
19
20impl CloudCliCleaner {
21 pub fn new() -> Option<Self> {
23 let home = dirs::home_dir()?;
24 Some(Self { home })
25 }
26
27 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
29 let mut items = Vec::new();
30
31 items.extend(self.detect_aws()?);
33
34 items.extend(self.detect_gcloud()?);
36
37 items.extend(self.detect_azure()?);
39
40 items.extend(self.detect_kubernetes()?);
42
43 items.extend(self.detect_terraform()?);
45
46 items.extend(self.detect_pulumi()?);
48
49 items.extend(self.detect_helm()?);
51
52 Ok(items)
53 }
54
55 fn detect_aws(&self) -> Result<Vec<CleanableItem>> {
57 let mut items = Vec::new();
58
59 let aws_paths = [
60 (".aws/cli/cache", "AWS CLI Cache", SafetyLevel::Safe),
61 (".aws/sso/cache", "AWS SSO Cache", SafetyLevel::Safe),
62 (".aws/boto/cache", "AWS Boto Cache", SafetyLevel::Safe),
63 ];
64
65 for (rel_path, name, safety) in aws_paths {
66 let path = self.home.join(rel_path);
67 if !path.exists() {
68 continue;
69 }
70
71 let (size, file_count) = calculate_dir_size(&path)?;
72 if size < 5_000_000 {
73 continue;
74 }
75
76 items.push(CleanableItem {
77 name: name.to_string(),
78 category: "Cloud CLI".to_string(),
79 subcategory: "AWS".to_string(),
80 icon: "âī¸",
81 path,
82 size,
83 file_count: Some(file_count),
84 last_modified: None,
85 description: "AWS CLI credential and API response cache. Safe to delete.",
86 safe_to_delete: safety,
87 clean_command: None,
88 });
89 }
90
91 let sam_cache = self.home.join(".aws-sam/cache");
93 if sam_cache.exists() {
94 let (size, file_count) = calculate_dir_size(&sam_cache)?;
95 if size > 50_000_000 {
96 items.push(CleanableItem {
97 name: "AWS SAM Cache".to_string(),
98 category: "Cloud CLI".to_string(),
99 subcategory: "AWS".to_string(),
100 icon: "âī¸",
101 path: sam_cache,
102 size,
103 file_count: Some(file_count),
104 last_modified: None,
105 description: "AWS SAM build cache. Will be rebuilt on next sam build.",
106 safe_to_delete: SafetyLevel::SafeWithCost,
107 clean_command: None,
108 });
109 }
110 }
111
112 Ok(items)
113 }
114
115 fn detect_gcloud(&self) -> Result<Vec<CleanableItem>> {
117 let mut items = Vec::new();
118
119 let gcloud_paths = [
120 (".config/gcloud/logs", "gcloud Logs", SafetyLevel::Safe),
121 (".config/gcloud/cache", "gcloud Cache", SafetyLevel::Safe),
122 (".config/gcloud/application_default_credentials_cache", "gcloud ADC Cache", SafetyLevel::Safe),
123 ];
124
125 for (rel_path, name, safety) in gcloud_paths {
126 let path = self.home.join(rel_path);
127 if !path.exists() {
128 continue;
129 }
130
131 let (size, file_count) = calculate_dir_size(&path)?;
132 if size < 10_000_000 {
133 continue;
134 }
135
136 items.push(CleanableItem {
137 name: name.to_string(),
138 category: "Cloud CLI".to_string(),
139 subcategory: "Google Cloud".to_string(),
140 icon: "đŠī¸",
141 path,
142 size,
143 file_count: Some(file_count),
144 last_modified: None,
145 description: "Google Cloud SDK cache and logs. Safe to delete.",
146 safe_to_delete: safety,
147 clean_command: None,
148 });
149 }
150
151 let gcloud_dir = self.home.join(".config/gcloud");
153 if gcloud_dir.exists() {
154 let (size, _) = calculate_dir_size(&gcloud_dir)?;
155 if size > 500_000_000 {
156 items.push(CleanableItem {
158 name: "gcloud Directory".to_string(),
159 category: "Cloud CLI".to_string(),
160 subcategory: "Google Cloud".to_string(),
161 icon: "đŠī¸",
162 path: gcloud_dir,
163 size,
164 file_count: None,
165 last_modified: None,
166 description: "Google Cloud SDK directory. Contains config - clean subdirs only.",
167 safe_to_delete: SafetyLevel::Caution,
168 clean_command: Some("gcloud components cleanup --unused".to_string()),
169 });
170 }
171 }
172
173 Ok(items)
174 }
175
176 fn detect_azure(&self) -> Result<Vec<CleanableItem>> {
178 let mut items = Vec::new();
179
180 let azure_paths = [
181 (".azure/logs", "Azure CLI Logs", SafetyLevel::Safe),
182 (".azure/cliextensions", "Azure CLI Extensions Cache", SafetyLevel::SafeWithCost),
183 (".azure/commands", "Azure CLI Commands Cache", SafetyLevel::Safe),
184 ];
185
186 for (rel_path, name, safety) in azure_paths {
187 let path = self.home.join(rel_path);
188 if !path.exists() {
189 continue;
190 }
191
192 let (size, file_count) = calculate_dir_size(&path)?;
193 if size < 10_000_000 {
194 continue;
195 }
196
197 items.push(CleanableItem {
198 name: name.to_string(),
199 category: "Cloud CLI".to_string(),
200 subcategory: "Azure".to_string(),
201 icon: "đˇ",
202 path,
203 size,
204 file_count: Some(file_count),
205 last_modified: None,
206 description: "Azure CLI cache and logs.",
207 safe_to_delete: safety,
208 clean_command: Some("az cache purge".to_string()),
209 });
210 }
211
212 Ok(items)
213 }
214
215 fn detect_kubernetes(&self) -> Result<Vec<CleanableItem>> {
217 let mut items = Vec::new();
218
219 let kube_paths = [
220 (".kube/cache", "kubectl Cache", SafetyLevel::Safe),
221 (".kube/http-cache", "kubectl HTTP Cache", SafetyLevel::Safe),
222 ];
223
224 for (rel_path, name, safety) in kube_paths {
225 let path = self.home.join(rel_path);
226 if !path.exists() {
227 continue;
228 }
229
230 let (size, file_count) = calculate_dir_size(&path)?;
231 if size < 10_000_000 {
232 continue;
233 }
234
235 items.push(CleanableItem {
236 name: name.to_string(),
237 category: "Cloud CLI".to_string(),
238 subcategory: "Kubernetes".to_string(),
239 icon: "â¸ī¸",
240 path,
241 size,
242 file_count: Some(file_count),
243 last_modified: None,
244 description: "Kubernetes API discovery cache. Safe to delete.",
245 safe_to_delete: safety,
246 clean_command: None,
247 });
248 }
249
250 let minikube_cache = self.home.join(".minikube/cache");
252 if minikube_cache.exists() {
253 let (size, file_count) = calculate_dir_size(&minikube_cache)?;
254 if size > 500_000_000 {
255 items.push(CleanableItem {
256 name: "Minikube Cache".to_string(),
257 category: "Cloud CLI".to_string(),
258 subcategory: "Kubernetes".to_string(),
259 icon: "â¸ī¸",
260 path: minikube_cache,
261 size,
262 file_count: Some(file_count),
263 last_modified: None,
264 description: "Minikube ISO and preload images. Can be re-downloaded.",
265 safe_to_delete: SafetyLevel::SafeWithCost,
266 clean_command: Some("minikube delete --purge".to_string()),
267 });
268 }
269 }
270
271 let kind_cache = self.home.join(".kind");
273 if kind_cache.exists() {
274 let (size, file_count) = calculate_dir_size(&kind_cache)?;
275 if size > 100_000_000 {
276 items.push(CleanableItem {
277 name: "Kind Cache".to_string(),
278 category: "Cloud CLI".to_string(),
279 subcategory: "Kubernetes".to_string(),
280 icon: "â¸ī¸",
281 path: kind_cache,
282 size,
283 file_count: Some(file_count),
284 last_modified: None,
285 description: "Kind (Kubernetes in Docker) cache.",
286 safe_to_delete: SafetyLevel::SafeWithCost,
287 clean_command: None,
288 });
289 }
290 }
291
292 Ok(items)
293 }
294
295 fn detect_terraform(&self) -> Result<Vec<CleanableItem>> {
297 let mut items = Vec::new();
298
299 let tf_plugin_cache = self.home.join(".terraform.d/plugin-cache");
301 if tf_plugin_cache.exists() {
302 let (size, file_count) = calculate_dir_size(&tf_plugin_cache)?;
303 if size > 100_000_000 {
304 items.push(CleanableItem {
305 name: "Terraform Plugin Cache".to_string(),
306 category: "Cloud CLI".to_string(),
307 subcategory: "Terraform".to_string(),
308 icon: "đī¸",
309 path: tf_plugin_cache,
310 size,
311 file_count: Some(file_count),
312 last_modified: None,
313 description: "Terraform provider plugins cache. Will be re-downloaded.",
314 safe_to_delete: SafetyLevel::SafeWithCost,
315 clean_command: None,
316 });
317 }
318 }
319
320 let tofu_cache = self.home.join(".terraform.d");
322 if tofu_cache.exists() {
323 let (size, file_count) = calculate_dir_size(&tofu_cache)?;
324 if size > 200_000_000 {
325 items.push(CleanableItem {
326 name: "Terraform/OpenTofu Data".to_string(),
327 category: "Cloud CLI".to_string(),
328 subcategory: "Terraform".to_string(),
329 icon: "đī¸",
330 path: tofu_cache,
331 size,
332 file_count: Some(file_count),
333 last_modified: None,
334 description: "Terraform/OpenTofu plugins and credentials cache.",
335 safe_to_delete: SafetyLevel::Caution,
336 clean_command: None,
337 });
338 }
339 }
340
341 Ok(items)
342 }
343
344 fn detect_pulumi(&self) -> Result<Vec<CleanableItem>> {
346 let mut items = Vec::new();
347
348 let pulumi_dir = self.home.join(".pulumi");
349 if !pulumi_dir.exists() {
350 return Ok(items);
351 }
352
353 let plugins = pulumi_dir.join("plugins");
355 if plugins.exists() {
356 let (size, file_count) = calculate_dir_size(&plugins)?;
357 if size > 500_000_000 {
358 items.push(CleanableItem {
359 name: "Pulumi Plugins".to_string(),
360 category: "Cloud CLI".to_string(),
361 subcategory: "Pulumi".to_string(),
362 icon: "đĢ",
363 path: plugins,
364 size,
365 file_count: Some(file_count),
366 last_modified: None,
367 description: "Pulumi provider plugins. Can be re-downloaded.",
368 safe_to_delete: SafetyLevel::SafeWithCost,
369 clean_command: Some("pulumi plugin rm --all".to_string()),
370 });
371 }
372 }
373
374 Ok(items)
375 }
376
377 fn detect_helm(&self) -> Result<Vec<CleanableItem>> {
379 let mut items = Vec::new();
380
381 let helm_paths = [
382 (".cache/helm", "Helm Cache"),
383 ("Library/Caches/helm", "Helm Cache (macOS)"),
384 ];
385
386 for (rel_path, name) in helm_paths {
387 let path = self.home.join(rel_path);
388 if !path.exists() {
389 continue;
390 }
391
392 let (size, file_count) = calculate_dir_size(&path)?;
393 if size < 50_000_000 {
394 continue;
395 }
396
397 items.push(CleanableItem {
398 name: name.to_string(),
399 category: "Cloud CLI".to_string(),
400 subcategory: "Helm".to_string(),
401 icon: "âĩ",
402 path,
403 size,
404 file_count: Some(file_count),
405 last_modified: None,
406 description: "Helm chart cache. Will be re-downloaded.",
407 safe_to_delete: SafetyLevel::SafeWithCost,
408 clean_command: None,
409 });
410 }
411
412 Ok(items)
413 }
414}
415
416impl Default for CloudCliCleaner {
417 fn default() -> Self {
418 Self::new().expect("CloudCliCleaner requires home directory")
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn test_cloud_cli_cleaner_creation() {
428 let cleaner = CloudCliCleaner::new();
429 assert!(cleaner.is_some());
430 }
431
432 #[test]
433 fn test_cloud_cli_detection() {
434 if let Some(cleaner) = CloudCliCleaner::new() {
435 let items = cleaner.detect().unwrap();
436 println!("Found {} cloud CLI items", items.len());
437 for item in &items {
438 println!(" {} {} ({} bytes)", item.icon, item.name, item.size);
439 }
440 }
441 }
442}