Skip to main content

null_e/cleaners/
cloud.rs

1//! Cloud CLI cleanup module
2//!
3//! Handles cleanup of cloud provider CLI caches:
4//! - AWS CLI
5//! - Google Cloud SDK (gcloud)
6//! - Azure CLI
7//! - Kubernetes (kubectl)
8//! - Terraform
9//! - Pulumi
10
11use super::{calculate_dir_size, CleanableItem, SafetyLevel};
12use crate::error::Result;
13use std::path::PathBuf;
14
15/// Cloud CLI cleaner
16pub struct CloudCliCleaner {
17    home: PathBuf,
18}
19
20impl CloudCliCleaner {
21    /// Create a new cloud CLI cleaner
22    pub fn new() -> Option<Self> {
23        let home = dirs::home_dir()?;
24        Some(Self { home })
25    }
26
27    /// Detect all cloud CLI cleanable items
28    pub fn detect(&self) -> Result<Vec<CleanableItem>> {
29        let mut items = Vec::new();
30
31        // AWS
32        items.extend(self.detect_aws()?);
33
34        // Google Cloud
35        items.extend(self.detect_gcloud()?);
36
37        // Azure
38        items.extend(self.detect_azure()?);
39
40        // Kubernetes
41        items.extend(self.detect_kubernetes()?);
42
43        // Terraform
44        items.extend(self.detect_terraform()?);
45
46        // Pulumi
47        items.extend(self.detect_pulumi()?);
48
49        // Helm
50        items.extend(self.detect_helm()?);
51
52        Ok(items)
53    }
54
55    /// Detect AWS CLI caches
56    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        // SAM CLI cache
92        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    /// Detect Google Cloud SDK caches
116    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        // Main gcloud directory (just report size, don't suggest deleting config)
152        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                // Only if very large, suggest looking at it
157                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    /// Detect Azure CLI caches
177    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    /// Detect Kubernetes caches
216    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        // Minikube
251        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        // Kind
272        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    /// Detect Terraform caches
296    fn detect_terraform(&self) -> Result<Vec<CleanableItem>> {
297        let mut items = Vec::new();
298
299        // Terraform plugin cache
300        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        // OpenTofu
321        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    /// Detect Pulumi caches
345    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        // Pulumi plugins
354        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    /// Detect Helm caches
378    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}