zutils/cmd/junks.go
selamanapps e96d60ae91 feat: add permission interpreter, junks, ports, usages, network commands
New Commands:
- z network: Show IP addresses, public IP, DNS servers, active connections
- z ports: List listening ports with process/PID info
- z usages: All processes sorted by resource usage with filtering
- z junks: Find and clean junk files (caches, temporary files)

Enhancements:
- Permission interpreter in z info file - shows human-readable permissions
  e.g. -rw-rw-r-- → "Owner can read, write; Group can read, write; Other can read"
- z usages now supports filtering: z usages chrome -m -n=50
- Summary section shows total CPU/memory when filtering by name
- Added help entries for all new commands
2026-05-02 01:53:36 +03:00

383 lines
No EOL
10 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}