package cmd import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" "github.com/zemenawi/zutils/pkg/colors" "github.com/zemenawi/zutils/pkg/formatter" ) type JunkItem struct { Path string Size int64 Type string SafeToClean bool } type JunkStats struct { Items []JunkItem TotalSize int64 Cleanable int64 NonCleanable int64 } var junkPatterns = []struct { Pattern string Type string SafeToClean bool }{ {".cache", "Cache Directory", true}, {"__pycache__", "Python Bytecode", true}, {".pytest_cache", "Python Test Cache", true}, {".mypy_cache", "Mypy Cache", true}, {"node_modules/.cache", "Node Module Cache", true}, {".npm", "NPM Cache", true}, {".yarn", "Yarn Cache", true}, {".pnpm-store", "PNPM Store", true}, {".next", "Next.js Cache", true}, {".nuxt", "Nuxt Cache", true}, {".output", "Nuxt Output", true}, {".solid", "SolidJS Cache", true}, {".svelte-kit", "SvelteKit Cache", true}, {"dist", "Build Output", true}, {"build", "Build Output", true}, {".gradle/caches", "Gradle Cache", true}, {".cargo/registry/cache", "Cargo Registry Cache", true}, {".composer/cache", "Composer Cache", true}, {".gem/cache", "Ruby Gem Cache", true}, {".sass-cache", "Sass Cache", true}, {".eslintcache", "ESLint Cache", true}, {".webpack", "Webpack Cache", true}, {".parcel-cache", "Parcel Cache", true}, {"thumbs.db", "Windows Thumbnail Cache", true}, {".ds_store", "macOS metadata", true}, {".spotlight", "macOS Spotlight", true}, {".trash", "Trash", true}, {"trash-1000", "Trash", true}, {".cache/thumbnails", "Thumbnail Cache", true}, {".-runtime-version", "Runtime Version", false}, } var userHome string func init() { userHome, _ = os.UserHomeDir() } func JunksCommand(args []string) error { if len(args) < 1 { return listJunks() } switch args[0] { case "clean": return cleanJunks(args[1:]) default: return fmt.Errorf("unknown subcommand: %s\n\nUsage:\n z junks List junk files and space\n z junks clean Clean junk files", args[0]) } } func listJunks() error { fmt.Printf("%s%s🔍 Scanning for junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset) stats, err := scanForJunks() if err != nil { return fmt.Errorf("error scanning: %w", err) } if len(stats.Items) == 0 { fmt.Printf("%s✨ No junk files found! Your system is clean.%s\n", colors.Green, colors.Reset) return nil } PrintBoxHeader("SYSTEM JUNK ANALYSIS", colors.Red) fmt.Printf("%s%s💾 Total Junk Size:%s %s\n", colors.Red, colors.Bold, colors.Reset, formatter.FormatSize(stats.TotalSize)) fmt.Printf("%s%s✅ Cleanable:%s %s\n", colors.Green, colors.Bold, colors.Reset, formatter.FormatSize(stats.Cleanable)) fmt.Printf("%s%s⚠️ Non-cleanable:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, formatter.FormatSize(stats.NonCleanable)) fmt.Println() byType := groupByType(stats.Items) type typeGroup struct { junkType string items []JunkItem size int64 } var groups []typeGroup for junkType, items := range byType { var size int64 for _, item := range items { size += item.Size } groups = append(groups, typeGroup{junkType: junkType, items: items, size: size}) } sort.Slice(groups, func(i, j int) bool { return groups[i].size > groups[j].size }) PrintSectionHeader("JUNK BY TYPE") for _, group := range groups { safe := "" if len(group.items) > 0 && group.items[0].SafeToClean { safe = colors.Green + " [cleanable]" + colors.Reset } else { safe = colors.Yellow + " [manual cleanup]" + colors.Reset } fmt.Printf("%s%s📁 %s%s%s (%d items, %s)\n", colors.Red, colors.Bold, group.junkType, safe, colors.Reset, len(group.items), formatter.FormatSize(group.size)) for _, item := range group.items { rel, _ := filepath.Rel(userHome, item.Path) if strings.HasPrefix(item.Path, "/") && !strings.HasPrefix(item.Path, userHome) { rel = item.Path } fmt.Printf(" %s\n", rel) } fmt.Println() } fmt.Printf("%s%s💡 Tip:%s Run %s z junks clean %s to remove cleanable junk files.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset) return nil } func cleanJunks(args []string) error { force := false for _, arg := range args { if arg == "--force" || arg == "-f" { force = true } } stats, err := scanForJunks() if err != nil { return fmt.Errorf("error scanning: %w", err) } cleanableItems := filterCleanable(stats.Items) if len(cleanableItems) == 0 { fmt.Printf("%s✨ No cleanable junk files found!%s\n", colors.Green, colors.Reset) return nil } var totalCleanable int64 for _, item := range cleanableItems { totalCleanable += item.Size } PrintBoxHeader("CLEAN JUNK FILES", colors.Red) fmt.Printf("%s%s🧹 Found %d cleanable items (%s)%s\n\n", colors.Yellow, colors.Bold, len(cleanableItems), formatter.FormatSize(totalCleanable), colors.Reset) if !force { fmt.Printf("%s%s⚠️ This will permanently delete:%s\n", colors.Red, colors.Bold, colors.Reset) byType := groupByType(cleanableItems) for junkType, items := range byType { var size int64 for _, item := range items { size += item.Size } fmt.Printf(" • %s (%d items, %s)\n", junkType, len(items), formatter.FormatSize(size)) } fmt.Println() fmt.Printf("%s%sConfirmation required.%s Run with %s--force%s to proceed.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset) fmt.Printf("%s%sExample:%s z junks clean --force\n", colors.Cyan, colors.Bold, colors.Reset) return nil } fmt.Printf("%s%s🗑️ Cleaning junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset) var cleaned int64 var failed int for _, item := range cleanableItems { if err := os.RemoveAll(item.Path); err != nil { failed++ fmt.Printf(" %s✖%s Failed: %s\n", colors.Red, colors.Reset, item.Path) } else { cleaned += item.Size rel, _ := filepath.Rel(userHome, item.Path) if !strings.HasPrefix(item.Path, userHome) { rel = item.Path } fmt.Printf(" %s✓%s Removed: %s (%s)\n", colors.Green, colors.Reset, rel, formatter.FormatSize(item.Size)) } } fmt.Println() PrintSectionHeader("CLEANUP SUMMARY") fmt.Printf("%s%s✅ Removed:%s %d items (%s)\n", colors.Green, colors.Bold, colors.Reset, len(cleanableItems)-failed, formatter.FormatSize(cleaned)) if failed > 0 { fmt.Printf("%s%s⚠️ Failed:%s %d items\n", colors.Red, colors.Bold, colors.Reset, failed) } fmt.Printf("%s%s💾 Space reclaimed:%s %s\n", colors.Cyan, colors.Bold, colors.Reset, formatter.FormatSize(cleaned)) return nil } func scanForJunks() (*JunkStats, error) { stats := &JunkStats{ Items: make([]JunkItem, 0), } searchRoots := getJunkSearchRoots() typeResult := scanDirs(searchRoots, junkPatterns, stats) stats.TotalSize = typeResult.TotalSize stats.Cleanable = typeResult.Cleanable stats.NonCleanable = typeResult.NonCleanable return stats, nil } func getJunkSearchRoots() []string { var roots []string stdCache := os.Getenv("XDG_CACHE_HOME") if stdCache != "" { roots = append(roots, stdCache) } else { roots = append(roots, filepath.Join(userHome, ".cache")) } if appCache := filepath.Join(userHome, ".npm"); dirExists(appCache) { roots = append(roots, appCache) } if appCache := filepath.Join(userHome, ".yarn"); dirExists(appCache) { roots = append(roots, appCache) } if appCache := filepath.Join(userHome, ".pnpm-store"); dirExists(appCache) { roots = append(roots, appCache) } if appCache := filepath.Join(userHome, ".gradle"); dirExists(appCache) { roots = append(roots, appCache) } if appCache := filepath.Join(userHome, ".cargo/registry/cache"); dirExists(appCache) { roots = append(roots, appCache) } if appCache := filepath.Join(userHome, ".cargo/registry/index"); dirExists(appCache) { roots = append(roots, appCache) } tempDir := os.TempDir() if tempDir != "" && dirExists(tempDir) { roots = append(roots, tempDir) } return roots } func dirExists(path string) bool { info, err := os.Stat(path) return err == nil && info.IsDir() } func scanDirs(roots []string, patterns []struct { Pattern string Type string SafeToClean bool }, stats *JunkStats) *JunkStats { for _, root := range roots { if _, err := os.Stat(root); err != nil { continue } filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } baseName := filepath.Base(path) for _, jp := range patterns { matched := false if jp.Pattern == baseName || baseName == jp.Pattern { matched = true } else if strings.Contains(path, jp.Pattern) { matched = true } else if matched, _ = regexp.MatchString("^"+jp.Pattern+"$", baseName); matched { } if matched { size := getDirSize(path) stats.Items = append(stats.Items, JunkItem{ Path: path, Size: size, Type: jp.Type, SafeToClean: jp.SafeToClean, }) if info.IsDir() { if jp.SafeToClean { stats.Cleanable += size } else { stats.NonCleanable += size } stats.TotalSize += size return filepath.SkipDir } else { if jp.SafeToClean { stats.Cleanable += size } else { stats.NonCleanable += size } stats.TotalSize += size } break } } return nil }) } return stats } func getDirSize(path string) int64 { var size int64 info, err := os.Stat(path) if err != nil { return 0 } if !info.IsDir() { return info.Size() } filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error { if err != nil { return nil } if fileInfo.Mode().IsRegular() { size += fileInfo.Size() } return nil }) return size } func groupByType(items []JunkItem) map[string][]JunkItem { result := make(map[string][]JunkItem) for _, item := range items { result[item.Type] = append(result[item.Type], item) } return result } func filterCleanable(items []JunkItem) []JunkItem { var cleanable []JunkItem for _, item := range items { if item.SafeToClean { cleanable = append(cleanable, item) } } return cleanable }