mirror of
https://github.com/tw93/Mole.git
synced 2026-02-08 04:29:24 +00:00
chore: restructure windows branch (move windows/ content to root, remove macos files)
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
#!/bin/bash
|
||||
# User GUI Applications Cleanup Module (desktop apps, media, utilities).
|
||||
set -euo pipefail
|
||||
# Xcode and iOS tooling.
|
||||
clean_xcode_tools() {
|
||||
# Skip DerivedData/Archives while Xcode is running.
|
||||
local xcode_running=false
|
||||
if pgrep -x "Xcode" > /dev/null 2>&1; then
|
||||
xcode_running=true
|
||||
fi
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Caches/* "Simulator cache"
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/* "Simulator temp files"
|
||||
safe_clean ~/Library/Caches/com.apple.dt.Xcode/* "Xcode cache"
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ Device\ Logs/* "iOS device logs"
|
||||
safe_clean ~/Library/Developer/Xcode/watchOS\ Device\ Logs/* "watchOS device logs"
|
||||
safe_clean ~/Library/Developer/Xcode/Products/* "Xcode build products"
|
||||
if [[ "$xcode_running" == "false" ]]; then
|
||||
safe_clean ~/Library/Developer/Xcode/DerivedData/* "Xcode derived data"
|
||||
safe_clean ~/Library/Developer/Xcode/Archives/* "Xcode archives"
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Xcode is running, skipping DerivedData and Archives cleanup"
|
||||
fi
|
||||
}
|
||||
# Code editors.
|
||||
clean_code_editors() {
|
||||
safe_clean ~/Library/Application\ Support/Code/logs/* "VS Code logs"
|
||||
safe_clean ~/Library/Application\ Support/Code/Cache/* "VS Code cache"
|
||||
safe_clean ~/Library/Application\ Support/Code/CachedExtensions/* "VS Code extension cache"
|
||||
safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code data cache"
|
||||
safe_clean ~/Library/Caches/com.sublimetext.*/* "Sublime Text cache"
|
||||
}
|
||||
# Communication apps.
|
||||
clean_communication_apps() {
|
||||
safe_clean ~/Library/Application\ Support/discord/Cache/* "Discord cache"
|
||||
safe_clean ~/Library/Application\ Support/legcord/Cache/* "Legcord cache"
|
||||
safe_clean ~/Library/Application\ Support/Slack/Cache/* "Slack cache"
|
||||
safe_clean ~/Library/Caches/us.zoom.xos/* "Zoom cache"
|
||||
safe_clean ~/Library/Caches/com.tencent.xinWeChat/* "WeChat cache"
|
||||
safe_clean ~/Library/Caches/ru.keepcoder.Telegram/* "Telegram cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.teams2/* "Microsoft Teams cache"
|
||||
safe_clean ~/Library/Caches/net.whatsapp.WhatsApp/* "WhatsApp cache"
|
||||
safe_clean ~/Library/Caches/com.skype.skype/* "Skype cache"
|
||||
safe_clean ~/Library/Caches/com.tencent.meeting/* "Tencent Meeting cache"
|
||||
safe_clean ~/Library/Caches/com.tencent.WeWorkMac/* "WeCom cache"
|
||||
safe_clean ~/Library/Caches/com.feishu.*/* "Feishu cache"
|
||||
}
|
||||
# DingTalk.
|
||||
clean_dingtalk() {
|
||||
safe_clean ~/Library/Caches/dd.work.exclusive4aliding/* "DingTalk iDingTalk cache"
|
||||
safe_clean ~/Library/Caches/com.alibaba.AliLang.osx/* "AliLang security component"
|
||||
safe_clean ~/Library/Application\ Support/iDingTalk/log/* "DingTalk logs"
|
||||
safe_clean ~/Library/Application\ Support/iDingTalk/holmeslogs/* "DingTalk holmes logs"
|
||||
}
|
||||
# AI assistants.
|
||||
clean_ai_apps() {
|
||||
safe_clean ~/Library/Caches/com.openai.chat/* "ChatGPT cache"
|
||||
safe_clean ~/Library/Caches/com.anthropic.claudefordesktop/* "Claude desktop cache"
|
||||
safe_clean ~/Library/Logs/Claude/* "Claude logs"
|
||||
}
|
||||
# Design and creative tools.
|
||||
clean_design_tools() {
|
||||
safe_clean ~/Library/Caches/com.bohemiancoding.sketch3/* "Sketch cache"
|
||||
safe_clean ~/Library/Application\ Support/com.bohemiancoding.sketch3/cache/* "Sketch app cache"
|
||||
safe_clean ~/Library/Caches/Adobe/* "Adobe cache"
|
||||
safe_clean ~/Library/Caches/com.adobe.*/* "Adobe app caches"
|
||||
safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache"
|
||||
# Raycast cache is protected (clipboard history, images).
|
||||
}
|
||||
# Video editing tools.
|
||||
clean_video_tools() {
|
||||
safe_clean ~/Library/Caches/net.telestream.screenflow10/* "ScreenFlow cache"
|
||||
safe_clean ~/Library/Caches/com.apple.FinalCut/* "Final Cut Pro cache"
|
||||
safe_clean ~/Library/Caches/com.blackmagic-design.DaVinciResolve/* "DaVinci Resolve cache"
|
||||
safe_clean ~/Library/Caches/com.adobe.PremierePro.*/* "Premiere Pro cache"
|
||||
}
|
||||
# 3D and CAD tools.
|
||||
clean_3d_tools() {
|
||||
safe_clean ~/Library/Caches/org.blenderfoundation.blender/* "Blender cache"
|
||||
safe_clean ~/Library/Caches/com.maxon.cinema4d/* "Cinema 4D cache"
|
||||
safe_clean ~/Library/Caches/com.autodesk.*/* "Autodesk cache"
|
||||
safe_clean ~/Library/Caches/com.sketchup.*/* "SketchUp cache"
|
||||
}
|
||||
# Productivity apps.
|
||||
clean_productivity_apps() {
|
||||
safe_clean ~/Library/Caches/com.tw93.MiaoYan/* "MiaoYan cache"
|
||||
safe_clean ~/Library/Caches/com.klee.desktop/* "Klee cache"
|
||||
safe_clean ~/Library/Caches/klee_desktop/* "Klee desktop cache"
|
||||
safe_clean ~/Library/Caches/com.orabrowser.app/* "Ora browser cache"
|
||||
safe_clean ~/Library/Caches/com.filo.client/* "Filo cache"
|
||||
safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache"
|
||||
safe_clean ~/Library/Application\ Support/Quark/Cache/videoCache/* "Quark video cache"
|
||||
}
|
||||
# Music/media players (protect Spotify offline music).
|
||||
clean_media_players() {
|
||||
local spotify_cache="$HOME/Library/Caches/com.spotify.client"
|
||||
local spotify_data="$HOME/Library/Application Support/Spotify"
|
||||
local has_offline_music=false
|
||||
# Heuristics: offline DB or large cache.
|
||||
if [[ -f "$spotify_data/PersistentCache/Storage/offline.bnk" ]] ||
|
||||
[[ -d "$spotify_data/PersistentCache/Storage" && -n "$(find "$spotify_data/PersistentCache/Storage" -type f -name "*.file" 2> /dev/null | head -1)" ]]; then
|
||||
has_offline_music=true
|
||||
elif [[ -d "$spotify_cache" ]]; then
|
||||
local cache_size_kb
|
||||
cache_size_kb=$(get_path_size_kb "$spotify_cache")
|
||||
if [[ $cache_size_kb -ge 512000 ]]; then
|
||||
has_offline_music=true
|
||||
fi
|
||||
fi
|
||||
if [[ "$has_offline_music" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Spotify cache protected · offline music detected"
|
||||
note_activity
|
||||
else
|
||||
safe_clean ~/Library/Caches/com.spotify.client/* "Spotify cache"
|
||||
fi
|
||||
safe_clean ~/Library/Caches/com.apple.Music "Apple Music cache"
|
||||
safe_clean ~/Library/Caches/com.apple.podcasts "Apple Podcasts cache"
|
||||
safe_clean ~/Library/Caches/com.apple.TV/* "Apple TV cache"
|
||||
safe_clean ~/Library/Caches/tv.plex.player.desktop "Plex cache"
|
||||
safe_clean ~/Library/Caches/com.netease.163music "NetEase Music cache"
|
||||
safe_clean ~/Library/Caches/com.tencent.QQMusic/* "QQ Music cache"
|
||||
safe_clean ~/Library/Caches/com.kugou.mac/* "Kugou Music cache"
|
||||
safe_clean ~/Library/Caches/com.kuwo.mac/* "Kuwo Music cache"
|
||||
}
|
||||
# Video players.
|
||||
clean_video_players() {
|
||||
safe_clean ~/Library/Caches/com.colliderli.iina "IINA cache"
|
||||
safe_clean ~/Library/Caches/org.videolan.vlc "VLC cache"
|
||||
safe_clean ~/Library/Caches/io.mpv "MPV cache"
|
||||
safe_clean ~/Library/Caches/com.iqiyi.player "iQIYI cache"
|
||||
safe_clean ~/Library/Caches/com.tencent.tenvideo "Tencent Video cache"
|
||||
safe_clean ~/Library/Caches/tv.danmaku.bili/* "Bilibili cache"
|
||||
safe_clean ~/Library/Caches/com.douyu.*/* "Douyu cache"
|
||||
safe_clean ~/Library/Caches/com.huya.*/* "Huya cache"
|
||||
}
|
||||
# Download managers.
|
||||
clean_download_managers() {
|
||||
safe_clean ~/Library/Caches/net.xmac.aria2gui "Aria2 cache"
|
||||
safe_clean ~/Library/Caches/org.m0k.transmission "Transmission cache"
|
||||
safe_clean ~/Library/Caches/com.qbittorrent.qBittorrent "qBittorrent cache"
|
||||
safe_clean ~/Library/Caches/com.downie.Downie-* "Downie cache"
|
||||
safe_clean ~/Library/Caches/com.folx.*/* "Folx cache"
|
||||
safe_clean ~/Library/Caches/com.charlessoft.pacifist/* "Pacifist cache"
|
||||
}
|
||||
# Gaming platforms.
|
||||
clean_gaming_platforms() {
|
||||
safe_clean ~/Library/Caches/com.valvesoftware.steam/* "Steam cache"
|
||||
safe_clean ~/Library/Application\ Support/Steam/htmlcache/* "Steam web cache"
|
||||
safe_clean ~/Library/Caches/com.epicgames.EpicGamesLauncher/* "Epic Games cache"
|
||||
safe_clean ~/Library/Caches/com.blizzard.Battle.net/* "Battle.net cache"
|
||||
safe_clean ~/Library/Application\ Support/Battle.net/Cache/* "Battle.net app cache"
|
||||
safe_clean ~/Library/Caches/com.ea.*/* "EA Origin cache"
|
||||
safe_clean ~/Library/Caches/com.gog.galaxy/* "GOG Galaxy cache"
|
||||
safe_clean ~/Library/Caches/com.riotgames.*/* "Riot Games cache"
|
||||
}
|
||||
# Translation/dictionary apps.
|
||||
clean_translation_apps() {
|
||||
safe_clean ~/Library/Caches/com.youdao.YoudaoDict "Youdao Dictionary cache"
|
||||
safe_clean ~/Library/Caches/com.eudic.* "Eudict cache"
|
||||
safe_clean ~/Library/Caches/com.bob-build.Bob "Bob Translation cache"
|
||||
}
|
||||
# Screenshot/recording tools.
|
||||
clean_screenshot_tools() {
|
||||
safe_clean ~/Library/Caches/com.cleanshot.* "CleanShot cache"
|
||||
safe_clean ~/Library/Caches/com.reincubate.camo "Camo cache"
|
||||
safe_clean ~/Library/Caches/com.xnipapp.xnip "Xnip cache"
|
||||
}
|
||||
# Email clients.
|
||||
clean_email_clients() {
|
||||
safe_clean ~/Library/Caches/com.readdle.smartemail-Mac "Spark cache"
|
||||
safe_clean ~/Library/Caches/com.airmail.* "Airmail cache"
|
||||
}
|
||||
# Task management apps.
|
||||
clean_task_apps() {
|
||||
safe_clean ~/Library/Caches/com.todoist.mac.Todoist "Todoist cache"
|
||||
safe_clean ~/Library/Caches/com.any.do.* "Any.do cache"
|
||||
}
|
||||
# Shell/terminal utilities.
|
||||
clean_shell_utils() {
|
||||
safe_clean ~/.zcompdump* "Zsh completion cache"
|
||||
safe_clean ~/.lesshst "less history"
|
||||
safe_clean ~/.viminfo.tmp "Vim temporary files"
|
||||
safe_clean ~/.wget-hsts "wget HSTS cache"
|
||||
}
|
||||
# Input methods and system utilities.
|
||||
clean_system_utils() {
|
||||
safe_clean ~/Library/Caches/com.runjuu.Input-Source-Pro/* "Input Source Pro cache"
|
||||
safe_clean ~/Library/Caches/macos-wakatime.WakaTime/* "WakaTime cache"
|
||||
}
|
||||
# Note-taking apps.
|
||||
clean_note_apps() {
|
||||
safe_clean ~/Library/Caches/notion.id/* "Notion cache"
|
||||
safe_clean ~/Library/Caches/md.obsidian/* "Obsidian cache"
|
||||
safe_clean ~/Library/Caches/com.logseq.*/* "Logseq cache"
|
||||
safe_clean ~/Library/Caches/com.bear-writer.*/* "Bear cache"
|
||||
safe_clean ~/Library/Caches/com.evernote.*/* "Evernote cache"
|
||||
safe_clean ~/Library/Caches/com.yinxiang.*/* "Yinxiang Note cache"
|
||||
}
|
||||
# Launchers and automation tools.
|
||||
clean_launcher_apps() {
|
||||
safe_clean ~/Library/Caches/com.runningwithcrayons.Alfred/* "Alfred cache"
|
||||
safe_clean ~/Library/Caches/cx.c3.theunarchiver/* "The Unarchiver cache"
|
||||
}
|
||||
# Remote desktop tools.
|
||||
clean_remote_desktop() {
|
||||
safe_clean ~/Library/Caches/com.teamviewer.*/* "TeamViewer cache"
|
||||
safe_clean ~/Library/Caches/com.anydesk.*/* "AnyDesk cache"
|
||||
safe_clean ~/Library/Caches/com.todesk.*/* "ToDesk cache"
|
||||
safe_clean ~/Library/Caches/com.sunlogin.*/* "Sunlogin cache"
|
||||
}
|
||||
# Main entry for GUI app cleanup.
|
||||
clean_user_gui_applications() {
|
||||
stop_section_spinner
|
||||
clean_xcode_tools
|
||||
clean_code_editors
|
||||
clean_communication_apps
|
||||
clean_dingtalk
|
||||
clean_ai_apps
|
||||
clean_design_tools
|
||||
clean_video_tools
|
||||
clean_3d_tools
|
||||
clean_productivity_apps
|
||||
clean_media_players
|
||||
clean_video_players
|
||||
clean_download_managers
|
||||
clean_gaming_platforms
|
||||
clean_translation_apps
|
||||
clean_screenshot_tools
|
||||
clean_email_clients
|
||||
clean_task_apps
|
||||
clean_shell_utils
|
||||
clean_system_utils
|
||||
clean_note_apps
|
||||
clean_launcher_apps
|
||||
clean_remote_desktop
|
||||
}
|
||||
442
lib/clean/apps.ps1
Normal file
442
lib/clean/apps.ps1
Normal file
@@ -0,0 +1,442 @@
|
||||
# Mole - Application-Specific Cleanup Module
|
||||
# Cleans leftover data from uninstalled apps and app-specific caches
|
||||
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Prevent multiple sourcing
|
||||
if ((Get-Variable -Name 'MOLE_CLEAN_APPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_APPS_LOADED) { return }
|
||||
$script:MOLE_CLEAN_APPS_LOADED = $true
|
||||
|
||||
# Import dependencies
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir\..\core\base.ps1"
|
||||
. "$scriptDir\..\core\log.ps1"
|
||||
. "$scriptDir\..\core\file_ops.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Orphaned App Data Detection
|
||||
# ============================================================================
|
||||
|
||||
function Get-InstalledPrograms {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get list of installed programs from registry
|
||||
#>
|
||||
|
||||
$programs = @()
|
||||
|
||||
$registryPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
foreach ($path in $registryPaths) {
|
||||
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DisplayName } |
|
||||
Select-Object DisplayName, InstallLocation, Publisher
|
||||
if ($items) {
|
||||
$programs += $items
|
||||
}
|
||||
}
|
||||
|
||||
# Also check UWP apps
|
||||
try {
|
||||
$uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue |
|
||||
Select-Object @{N='DisplayName';E={$_.Name}}, @{N='InstallLocation';E={$_.InstallLocation}}, Publisher
|
||||
if ($uwpApps) {
|
||||
$programs += $uwpApps
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not enumerate UWP apps: $_"
|
||||
}
|
||||
|
||||
return $programs
|
||||
}
|
||||
|
||||
function Find-OrphanedAppData {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Find app data folders for apps that are no longer installed
|
||||
#>
|
||||
param([int]$DaysOld = 60)
|
||||
|
||||
$installedPrograms = Get-InstalledPrograms
|
||||
$installedNames = $installedPrograms | ForEach-Object { $_.DisplayName.ToLower() }
|
||||
|
||||
$orphanedPaths = @()
|
||||
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
|
||||
|
||||
# Check common app data locations
|
||||
$appDataPaths = @(
|
||||
@{ Path = $env:APPDATA; Type = "Roaming" }
|
||||
@{ Path = $env:LOCALAPPDATA; Type = "Local" }
|
||||
)
|
||||
|
||||
foreach ($location in $appDataPaths) {
|
||||
if (-not (Test-Path $location.Path)) { continue }
|
||||
|
||||
$folders = Get-ChildItem -Path $location.Path -Directory -ErrorAction SilentlyContinue
|
||||
|
||||
foreach ($folder in $folders) {
|
||||
# Skip system folders
|
||||
$skipFolders = @('Microsoft', 'Windows', 'Packages', 'Programs', 'Temp', 'Roaming')
|
||||
if ($folder.Name -in $skipFolders) { continue }
|
||||
|
||||
# Skip if recently modified
|
||||
if ($folder.LastWriteTime -gt $cutoffDate) { continue }
|
||||
|
||||
# Check if app is installed using stricter matching
|
||||
# Require exact match or that folder name is a clear prefix/suffix of app name
|
||||
$isInstalled = $false
|
||||
$folderLower = $folder.Name.ToLower()
|
||||
foreach ($name in $installedNames) {
|
||||
# Exact match
|
||||
if ($name -eq $folderLower) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
# Folder is prefix of app name (e.g., "chrome" matches "chrome browser")
|
||||
if ($name.StartsWith($folderLower) -and $folderLower.Length -ge 4) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
# App name is prefix of folder (e.g., "vscode" matches "vscode-data")
|
||||
if ($folderLower.StartsWith($name) -and $name.Length -ge 4) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isInstalled) {
|
||||
$orphanedPaths += @{
|
||||
Path = $folder.FullName
|
||||
Name = $folder.Name
|
||||
Type = $location.Type
|
||||
Size = (Get-PathSize -Path $folder.FullName)
|
||||
LastModified = $folder.LastWriteTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $orphanedPaths
|
||||
}
|
||||
|
||||
function Clear-OrphanedAppData {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean orphaned application data
|
||||
#>
|
||||
param([int]$DaysOld = 60)
|
||||
|
||||
Start-Section "Orphaned app data"
|
||||
|
||||
$orphaned = Find-OrphanedAppData -DaysOld $DaysOld
|
||||
|
||||
if ($orphaned.Count -eq 0) {
|
||||
Write-Info "No orphaned app data found"
|
||||
Stop-Section
|
||||
return
|
||||
}
|
||||
|
||||
# Filter by size (only clean if > 10MB to avoid noise)
|
||||
$significantOrphans = $orphaned | Where-Object { $_.Size -gt 10MB }
|
||||
|
||||
if ($significantOrphans.Count -gt 0) {
|
||||
$totalSize = ($significantOrphans | Measure-Object -Property Size -Sum).Sum
|
||||
$sizeHuman = Format-ByteSize -Bytes $totalSize
|
||||
|
||||
Write-Info "Found $($significantOrphans.Count) orphaned folders ($sizeHuman)"
|
||||
|
||||
foreach ($orphan in $significantOrphans) {
|
||||
$orphanSize = Format-ByteSize -Bytes $orphan.Size
|
||||
Remove-SafeItem -Path $orphan.Path -Description "$($orphan.Name) ($orphanSize)" -Recurse
|
||||
}
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Specific Application Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-OfficeCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Microsoft Office caches and temp files
|
||||
#>
|
||||
|
||||
$officeCachePaths = @(
|
||||
# Office 365 / 2019 / 2021
|
||||
"$env:LOCALAPPDATA\Microsoft\Office\16.0\OfficeFileCache"
|
||||
"$env:LOCALAPPDATA\Microsoft\Office\16.0\Wef"
|
||||
"$env:LOCALAPPDATA\Microsoft\Outlook\RoamCache"
|
||||
"$env:LOCALAPPDATA\Microsoft\Outlook\Offline Address Books"
|
||||
# Older Office versions
|
||||
"$env:LOCALAPPDATA\Microsoft\Office\15.0\OfficeFileCache"
|
||||
# Office temp files
|
||||
"$env:APPDATA\Microsoft\Templates\*.tmp"
|
||||
"$env:APPDATA\Microsoft\Word\*.tmp"
|
||||
"$env:APPDATA\Microsoft\Excel\*.tmp"
|
||||
"$env:APPDATA\Microsoft\PowerPoint\*.tmp"
|
||||
)
|
||||
|
||||
foreach ($path in $officeCachePaths) {
|
||||
if ($path -like "*.tmp") {
|
||||
$parent = Split-Path -Parent $path
|
||||
if (Test-Path $parent) {
|
||||
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -File -ErrorAction SilentlyContinue
|
||||
if ($tmpFiles) {
|
||||
$paths = $tmpFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Office temp files"
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Office $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-OneDriveCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean OneDrive cache
|
||||
#>
|
||||
|
||||
$oneDriveCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Microsoft\OneDrive\logs"
|
||||
"$env:LOCALAPPDATA\Microsoft\OneDrive\setup\logs"
|
||||
)
|
||||
|
||||
foreach ($path in $oneDriveCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Remove-OldFiles -Path $path -DaysOld 7 -Description "OneDrive logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-DropboxCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Dropbox cache
|
||||
#>
|
||||
|
||||
# Dropbox cache is typically in the Dropbox folder itself
|
||||
$dropboxInfoPath = "$env:LOCALAPPDATA\Dropbox\info.json"
|
||||
|
||||
if (Test-Path $dropboxInfoPath) {
|
||||
try {
|
||||
$dropboxInfo = Get-Content $dropboxInfoPath | ConvertFrom-Json
|
||||
$dropboxPath = $dropboxInfo.personal.path
|
||||
|
||||
if ($dropboxPath) {
|
||||
$dropboxCachePath = "$dropboxPath\.dropbox.cache"
|
||||
if (Test-Path $dropboxCachePath) {
|
||||
Clear-DirectoryContents -Path $dropboxCachePath -Description "Dropbox cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not read Dropbox config: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-GoogleDriveCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Google Drive cache
|
||||
#>
|
||||
|
||||
$googleDriveCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Google\DriveFS\Logs"
|
||||
"$env:LOCALAPPDATA\Google\DriveFS\*.tmp"
|
||||
)
|
||||
|
||||
foreach ($path in $googleDriveCachePaths) {
|
||||
if ($path -like "*.tmp") {
|
||||
$parent = Split-Path -Parent $path
|
||||
if (Test-Path $parent) {
|
||||
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -ErrorAction SilentlyContinue
|
||||
if ($tmpFiles) {
|
||||
$paths = $tmpFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Google Drive temp"
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (Test-Path $path) {
|
||||
Remove-OldFiles -Path $path -DaysOld 7 -Description "Google Drive logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-AdobeData {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Adobe application caches and temp files
|
||||
#>
|
||||
|
||||
$adobeCachePaths = @(
|
||||
"$env:APPDATA\Adobe\Common\Media Cache Files"
|
||||
"$env:APPDATA\Adobe\Common\Peak Files"
|
||||
"$env:APPDATA\Adobe\Common\Team Projects Cache"
|
||||
"$env:LOCALAPPDATA\Adobe\*\Cache"
|
||||
"$env:LOCALAPPDATA\Adobe\*\CameraRaw\Cache"
|
||||
"$env:LOCALAPPDATA\Temp\Adobe"
|
||||
)
|
||||
|
||||
foreach ($pattern in $adobeCachePaths) {
|
||||
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
|
||||
foreach ($path in $paths) {
|
||||
if (Test-Path $path.Path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "Adobe cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-AutodeskData {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Autodesk application caches
|
||||
#>
|
||||
|
||||
$autodeskCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Autodesk\*\Cache"
|
||||
"$env:APPDATA\Autodesk\*\cache"
|
||||
)
|
||||
|
||||
foreach ($pattern in $autodeskCachePaths) {
|
||||
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
|
||||
foreach ($path in $paths) {
|
||||
if (Test-Path $path.Path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "Autodesk cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Gaming Platform Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-GamingPlatformCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean gaming platform caches (Steam, Epic, Origin, etc.)
|
||||
#>
|
||||
|
||||
# Steam
|
||||
$steamPaths = @(
|
||||
"${env:ProgramFiles(x86)}\Steam\appcache\httpcache"
|
||||
"${env:ProgramFiles(x86)}\Steam\appcache\librarycache"
|
||||
"${env:ProgramFiles(x86)}\Steam\logs"
|
||||
)
|
||||
foreach ($path in $steamPaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Steam $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Epic Games Launcher
|
||||
$epicPaths = @(
|
||||
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache"
|
||||
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\Logs"
|
||||
)
|
||||
foreach ($path in $epicPaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Epic Games $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# EA App (Origin replacement)
|
||||
$eaPaths = @(
|
||||
"$env:LOCALAPPDATA\Electronic Arts\EA Desktop\cache"
|
||||
"$env:APPDATA\Origin\*\cache"
|
||||
)
|
||||
foreach ($pattern in $eaPaths) {
|
||||
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
|
||||
foreach ($path in $paths) {
|
||||
if (Test-Path $path.Path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "EA/Origin cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# GOG Galaxy
|
||||
$gogPaths = @(
|
||||
"$env:LOCALAPPDATA\GOG.com\Galaxy\webcache"
|
||||
"$env:PROGRAMDATA\GOG.com\Galaxy\logs"
|
||||
)
|
||||
foreach ($path in $gogPaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "GOG Galaxy $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Ubisoft Connect
|
||||
$ubiPaths = @(
|
||||
"$env:LOCALAPPDATA\Ubisoft Game Launcher\cache"
|
||||
"$env:LOCALAPPDATA\Ubisoft Game Launcher\logs"
|
||||
)
|
||||
foreach ($path in $ubiPaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Ubisoft $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Battle.net
|
||||
$battlenetPaths = @(
|
||||
"$env:APPDATA\Battle.net\Cache"
|
||||
"$env:APPDATA\Battle.net\Logs"
|
||||
)
|
||||
foreach ($path in $battlenetPaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Battle.net $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Application Cleanup Function
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-AppCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run all application-specific cleanup tasks
|
||||
#>
|
||||
param([switch]$IncludeOrphaned)
|
||||
|
||||
Start-Section "Applications"
|
||||
|
||||
# Productivity apps
|
||||
Clear-OfficeCache
|
||||
Clear-OneDriveCache
|
||||
Clear-DropboxCache
|
||||
Clear-GoogleDriveCache
|
||||
|
||||
# Creative apps
|
||||
Clear-AdobeData
|
||||
Clear-AutodeskData
|
||||
|
||||
# Gaming platforms
|
||||
Clear-GamingPlatformCaches
|
||||
|
||||
Stop-Section
|
||||
|
||||
# Orphaned app data (separate section)
|
||||
if ($IncludeOrphaned) {
|
||||
Clear-OrphanedAppData -DaysOld 60
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
# Functions: Get-InstalledPrograms, Find-OrphanedAppData, Clear-OfficeCache, etc.
|
||||
@@ -1,313 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Application Data Cleanup Module
|
||||
set -euo pipefail
|
||||
# Args: $1=target_dir, $2=label
|
||||
clean_ds_store_tree() {
|
||||
local target="$1"
|
||||
local label="$2"
|
||||
[[ -d "$target" ]] || return 0
|
||||
local file_count=0
|
||||
local total_bytes=0
|
||||
local spinner_active="false"
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" "
|
||||
start_inline_spinner "Cleaning Finder metadata..."
|
||||
spinner_active="true"
|
||||
fi
|
||||
local -a exclude_paths=(
|
||||
-path "*/Library/Application Support/MobileSync" -prune -o
|
||||
-path "*/Library/Developer" -prune -o
|
||||
-path "*/.Trash" -prune -o
|
||||
-path "*/node_modules" -prune -o
|
||||
-path "*/.git" -prune -o
|
||||
-path "*/Library/Caches" -prune -o
|
||||
)
|
||||
local -a find_cmd=("command" "find" "$target")
|
||||
if [[ "$target" == "$HOME" ]]; then
|
||||
find_cmd+=("-maxdepth" "5")
|
||||
fi
|
||||
find_cmd+=("${exclude_paths[@]}" "-type" "f" "-name" ".DS_Store" "-print0")
|
||||
while IFS= read -r -d '' ds_file; do
|
||||
local size
|
||||
size=$(get_file_size "$ds_file")
|
||||
total_bytes=$((total_bytes + size))
|
||||
((file_count++))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
rm -f "$ds_file" 2> /dev/null || true
|
||||
fi
|
||||
if [[ $file_count -ge $MOLE_MAX_DS_STORE_FILES ]]; then
|
||||
break
|
||||
fi
|
||||
done < <("${find_cmd[@]}" 2> /dev/null || true)
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
if [[ $file_count -gt 0 ]]; then
|
||||
local size_human
|
||||
size_human=$(bytes_to_human "$total_bytes")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} $label ${YELLOW}($file_count files, $size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}($file_count files, $size_human)${NC}"
|
||||
fi
|
||||
local size_kb=$(((total_bytes + 1023) / 1024))
|
||||
((files_cleaned += file_count))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Orphaned app data (60+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Usage: scan_installed_apps "output_file"
|
||||
scan_installed_apps() {
|
||||
local installed_bundles="$1"
|
||||
# Cache installed app scan briefly to speed repeated runs.
|
||||
local cache_file="$HOME/.cache/mole/installed_apps_cache"
|
||||
local cache_age_seconds=300 # 5 minutes
|
||||
if [[ -f "$cache_file" ]]; then
|
||||
local cache_mtime=$(get_file_mtime "$cache_file")
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
local age=$((current_time - cache_mtime))
|
||||
if [[ $age -lt $cache_age_seconds ]]; then
|
||||
debug_log "Using cached app list (age: ${age}s)"
|
||||
if [[ -r "$cache_file" ]] && [[ -s "$cache_file" ]]; then
|
||||
if cat "$cache_file" > "$installed_bundles" 2> /dev/null; then
|
||||
return 0
|
||||
else
|
||||
debug_log "Warning: Failed to read cache, rebuilding"
|
||||
fi
|
||||
else
|
||||
debug_log "Warning: Cache file empty or unreadable, rebuilding"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
debug_log "Scanning installed applications (cache expired or missing)"
|
||||
local -a app_dirs=(
|
||||
"/Applications"
|
||||
"/System/Applications"
|
||||
"$HOME/Applications"
|
||||
# Homebrew Cask locations
|
||||
"/opt/homebrew/Caskroom"
|
||||
"/usr/local/Caskroom"
|
||||
# Setapp applications
|
||||
"$HOME/Library/Application Support/Setapp/Applications"
|
||||
)
|
||||
# Temp dir avoids write contention across parallel scans.
|
||||
local scan_tmp_dir=$(create_temp_dir)
|
||||
local pids=()
|
||||
local dir_idx=0
|
||||
for app_dir in "${app_dirs[@]}"; do
|
||||
[[ -d "$app_dir" ]] || continue
|
||||
(
|
||||
local -a app_paths=()
|
||||
while IFS= read -r app_path; do
|
||||
[[ -n "$app_path" ]] && app_paths+=("$app_path")
|
||||
done < <(find "$app_dir" -name '*.app' -maxdepth 3 -type d 2> /dev/null)
|
||||
local count=0
|
||||
for app_path in "${app_paths[@]:-}"; do
|
||||
local plist_path="$app_path/Contents/Info.plist"
|
||||
[[ ! -f "$plist_path" ]] && continue
|
||||
local bundle_id=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$plist_path" 2> /dev/null || echo "")
|
||||
if [[ -n "$bundle_id" ]]; then
|
||||
echo "$bundle_id"
|
||||
((count++))
|
||||
fi
|
||||
done
|
||||
) > "$scan_tmp_dir/apps_${dir_idx}.txt" &
|
||||
pids+=($!)
|
||||
((dir_idx++))
|
||||
done
|
||||
# Collect running apps and LaunchAgents to avoid false orphan cleanup.
|
||||
(
|
||||
local running_apps=$(run_with_timeout 5 osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "")
|
||||
echo "$running_apps" | tr ',' '\n' | sed -e 's/^ *//;s/ *$//' -e '/^$/d' > "$scan_tmp_dir/running.txt"
|
||||
# Fallback: lsappinfo is more reliable than osascript
|
||||
if command -v lsappinfo > /dev/null 2>&1; then
|
||||
run_with_timeout 3 lsappinfo list 2> /dev/null | grep -o '"CFBundleIdentifier"="[^"]*"' | cut -d'"' -f4 >> "$scan_tmp_dir/running.txt" 2> /dev/null || true
|
||||
fi
|
||||
) &
|
||||
pids+=($!)
|
||||
(
|
||||
run_with_timeout 5 find ~/Library/LaunchAgents /Library/LaunchAgents \
|
||||
-name "*.plist" -type f 2> /dev/null |
|
||||
xargs -I {} basename {} .plist > "$scan_tmp_dir/agents.txt" 2> /dev/null || true
|
||||
) &
|
||||
pids+=($!)
|
||||
debug_log "Waiting for ${#pids[@]} background processes: ${pids[*]}"
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2> /dev/null || true
|
||||
done
|
||||
debug_log "All background processes completed"
|
||||
cat "$scan_tmp_dir"/*.txt >> "$installed_bundles" 2> /dev/null || true
|
||||
safe_remove "$scan_tmp_dir" true
|
||||
sort -u "$installed_bundles" -o "$installed_bundles"
|
||||
ensure_user_dir "$(dirname "$cache_file")"
|
||||
cp "$installed_bundles" "$cache_file" 2> /dev/null || true
|
||||
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
|
||||
debug_log "Scanned $app_count unique applications"
|
||||
}
|
||||
# Sensitive data patterns that should never be treated as orphaned
|
||||
# These patterns protect security-critical application data
|
||||
readonly ORPHAN_NEVER_DELETE_PATTERNS=(
|
||||
"*1password*" "*1Password*"
|
||||
"*keychain*" "*Keychain*"
|
||||
"*bitwarden*" "*Bitwarden*"
|
||||
"*lastpass*" "*LastPass*"
|
||||
"*keepass*" "*KeePass*"
|
||||
"*dashlane*" "*Dashlane*"
|
||||
"*enpass*" "*Enpass*"
|
||||
"*ssh*" "*gpg*" "*gnupg*"
|
||||
"com.apple.keychain*"
|
||||
)
|
||||
|
||||
# Cache file for mdfind results (Bash 3.2 compatible, no associative arrays)
|
||||
ORPHAN_MDFIND_CACHE_FILE=""
|
||||
|
||||
# Usage: is_bundle_orphaned "bundle_id" "directory_path" "installed_bundles_file"
|
||||
is_bundle_orphaned() {
|
||||
local bundle_id="$1"
|
||||
local directory_path="$2"
|
||||
local installed_bundles="$3"
|
||||
|
||||
# 1. Fast path: check protection list (in-memory, instant)
|
||||
if should_protect_data "$bundle_id"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. Fast path: check sensitive data patterns (in-memory, instant)
|
||||
local bundle_lower
|
||||
bundle_lower=$(echo "$bundle_id" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||
for pattern in "${ORPHAN_NEVER_DELETE_PATTERNS[@]}"; do
|
||||
# shellcheck disable=SC2053
|
||||
if [[ "$bundle_lower" == $pattern ]]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. Fast path: check installed bundles file (file read, fast)
|
||||
if grep -Fxq "$bundle_id" "$installed_bundles" 2> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 4. Fast path: hardcoded system components
|
||||
case "$bundle_id" in
|
||||
loginwindow | dock | systempreferences | systemsettings | settings | controlcenter | finder | safari)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 5. Fast path: 60-day modification check (stat call, fast)
|
||||
if [[ -e "$directory_path" ]]; then
|
||||
local last_modified_epoch=$(get_file_mtime "$directory_path")
|
||||
local current_epoch
|
||||
current_epoch=$(get_epoch_seconds)
|
||||
local days_since_modified=$(((current_epoch - last_modified_epoch) / 86400))
|
||||
if [[ $days_since_modified -lt ${ORPHAN_AGE_THRESHOLD:-60} ]]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 6. Slow path: mdfind fallback with file-based caching (Bash 3.2 compatible)
|
||||
# This catches apps installed in non-standard locations
|
||||
if [[ -n "$bundle_id" ]] && [[ "$bundle_id" =~ ^[a-zA-Z0-9._-]+$ ]] && [[ ${#bundle_id} -ge 5 ]]; then
|
||||
# Initialize cache file if needed
|
||||
if [[ -z "$ORPHAN_MDFIND_CACHE_FILE" ]]; then
|
||||
ORPHAN_MDFIND_CACHE_FILE=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX")
|
||||
register_temp_file "$ORPHAN_MDFIND_CACHE_FILE"
|
||||
fi
|
||||
|
||||
# Check cache first (grep is fast for small files)
|
||||
if grep -Fxq "FOUND:$bundle_id" "$ORPHAN_MDFIND_CACHE_FILE" 2> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
if grep -Fxq "NOTFOUND:$bundle_id" "$ORPHAN_MDFIND_CACHE_FILE" 2> /dev/null; then
|
||||
# Already checked, not found - continue to return 0
|
||||
:
|
||||
else
|
||||
# Query mdfind with strict timeout (2 seconds max)
|
||||
local app_exists
|
||||
app_exists=$(run_with_timeout 2 mdfind "kMDItemCFBundleIdentifier == '$bundle_id'" 2> /dev/null | head -1 || echo "")
|
||||
if [[ -n "$app_exists" ]]; then
|
||||
echo "FOUND:$bundle_id" >> "$ORPHAN_MDFIND_CACHE_FILE"
|
||||
return 1
|
||||
else
|
||||
echo "NOTFOUND:$bundle_id" >> "$ORPHAN_MDFIND_CACHE_FILE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# All checks passed - this is an orphan
|
||||
return 0
|
||||
}
|
||||
# Orphaned app data sweep.
|
||||
clean_orphaned_app_data() {
|
||||
if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then
|
||||
stop_section_spinner
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Library folders"
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Scanning installed apps..."
|
||||
local installed_bundles=$(create_temp_file)
|
||||
scan_installed_apps "$installed_bundles"
|
||||
stop_section_spinner
|
||||
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Found $app_count active/installed apps"
|
||||
local orphaned_count=0
|
||||
local total_orphaned_kb=0
|
||||
start_section_spinner "Scanning orphaned app resources..."
|
||||
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps).
|
||||
local -a resource_types=(
|
||||
"$HOME/Library/Caches|Caches|com.*:org.*:net.*:io.*"
|
||||
"$HOME/Library/Logs|Logs|com.*:org.*:net.*:io.*"
|
||||
"$HOME/Library/Saved Application State|States|*.savedState"
|
||||
"$HOME/Library/WebKit|WebKit|com.*:org.*:net.*:io.*"
|
||||
"$HOME/Library/HTTPStorages|HTTP|com.*:org.*:net.*:io.*"
|
||||
"$HOME/Library/Cookies|Cookies|*.binarycookies"
|
||||
)
|
||||
orphaned_count=0
|
||||
for resource_type in "${resource_types[@]}"; do
|
||||
IFS='|' read -r base_path label patterns <<< "$resource_type"
|
||||
if [[ ! -d "$base_path" ]]; then
|
||||
continue
|
||||
fi
|
||||
if ! ls "$base_path" > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
local -a file_patterns=()
|
||||
IFS=':' read -ra pattern_arr <<< "$patterns"
|
||||
for pat in "${pattern_arr[@]}"; do
|
||||
file_patterns+=("$base_path/$pat")
|
||||
done
|
||||
for item_path in "${file_patterns[@]}"; do
|
||||
local iteration_count=0
|
||||
for match in $item_path; do
|
||||
[[ -e "$match" ]] || continue
|
||||
((iteration_count++))
|
||||
if [[ $iteration_count -gt $MOLE_MAX_ORPHAN_ITERATIONS ]]; then
|
||||
break
|
||||
fi
|
||||
local bundle_id=$(basename "$match")
|
||||
bundle_id="${bundle_id%.savedState}"
|
||||
bundle_id="${bundle_id%.binarycookies}"
|
||||
if is_bundle_orphaned "$bundle_id" "$match" "$installed_bundles"; then
|
||||
local size_kb
|
||||
size_kb=$(get_path_size_kb "$match")
|
||||
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
||||
continue
|
||||
fi
|
||||
safe_clean "$match" "Orphaned $label: $bundle_id"
|
||||
((orphaned_count++))
|
||||
((total_orphaned_kb += size_kb))
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
stop_section_spinner
|
||||
if [[ $orphaned_count -gt 0 ]]; then
|
||||
local orphaned_mb=$(echo "$total_orphaned_kb" | awk '{printf "%.1f", $1/1024}')
|
||||
echo " ${GREEN}${ICON_SUCCESS}${NC} Cleaned $orphaned_count items (~${orphaned_mb}MB)"
|
||||
note_activity
|
||||
fi
|
||||
rm -f "$installed_bundles"
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Clean Homebrew caches and remove orphaned dependencies
|
||||
# Env: DRY_RUN
|
||||
# Skips if run within 7 days, runs cleanup/autoremove in parallel with 120s timeout
|
||||
clean_homebrew() {
|
||||
command -v brew > /dev/null 2>&1 || return 0
|
||||
if [[ "${DRY_RUN:-false}" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Homebrew · would cleanup and autoremove"
|
||||
return 0
|
||||
fi
|
||||
# Skip if cleaned recently to avoid repeated heavy operations.
|
||||
local brew_cache_file="${HOME}/.cache/mole/brew_last_cleanup"
|
||||
local cache_valid_days=7
|
||||
local should_skip=false
|
||||
if [[ -f "$brew_cache_file" ]]; then
|
||||
local last_cleanup
|
||||
last_cleanup=$(cat "$brew_cache_file" 2> /dev/null || echo "0")
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
local time_diff=$((current_time - last_cleanup))
|
||||
local days_diff=$((time_diff / 86400))
|
||||
if [[ $days_diff -lt $cache_valid_days ]]; then
|
||||
should_skip=true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew · cleaned ${days_diff}d ago, skipped"
|
||||
fi
|
||||
fi
|
||||
[[ "$should_skip" == "true" ]] && return 0
|
||||
# Skip cleanup if cache is small; still run autoremove.
|
||||
local skip_cleanup=false
|
||||
local brew_cache_size=0
|
||||
if [[ -d ~/Library/Caches/Homebrew ]]; then
|
||||
brew_cache_size=$(run_with_timeout 3 du -sk ~/Library/Caches/Homebrew 2> /dev/null | awk '{print $1}')
|
||||
local du_exit=$?
|
||||
if [[ $du_exit -eq 0 && -n "$brew_cache_size" && "$brew_cache_size" -lt 51200 ]]; then
|
||||
skip_cleanup=true
|
||||
fi
|
||||
fi
|
||||
# Spinner reflects whether cleanup is skipped.
|
||||
if [[ -t 1 ]]; then
|
||||
if [[ "$skip_cleanup" == "true" ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew autoremove (cleanup skipped)..."
|
||||
else
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew cleanup and autoremove..."
|
||||
fi
|
||||
fi
|
||||
# Run cleanup/autoremove in parallel with timeout guard per command.
|
||||
local timeout_seconds=120
|
||||
local brew_tmp_file autoremove_tmp_file
|
||||
local brew_pid autoremove_pid
|
||||
local brew_exit=0
|
||||
local autoremove_exit=0
|
||||
if [[ "$skip_cleanup" == "false" ]]; then
|
||||
brew_tmp_file=$(create_temp_file)
|
||||
run_with_timeout "$timeout_seconds" brew cleanup > "$brew_tmp_file" 2>&1 &
|
||||
brew_pid=$!
|
||||
fi
|
||||
autoremove_tmp_file=$(create_temp_file)
|
||||
run_with_timeout "$timeout_seconds" brew autoremove > "$autoremove_tmp_file" 2>&1 &
|
||||
autoremove_pid=$!
|
||||
|
||||
if [[ -n "$brew_pid" ]]; then
|
||||
wait "$brew_pid" 2> /dev/null || brew_exit=$?
|
||||
fi
|
||||
wait "$autoremove_pid" 2> /dev/null || autoremove_exit=$?
|
||||
|
||||
local brew_success=false
|
||||
if [[ "$skip_cleanup" == "false" && $brew_exit -eq 0 ]]; then
|
||||
brew_success=true
|
||||
fi
|
||||
local autoremove_success=false
|
||||
if [[ $autoremove_exit -eq 0 ]]; then
|
||||
autoremove_success=true
|
||||
fi
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
# Process cleanup output and extract metrics
|
||||
# Summarize cleanup results.
|
||||
if [[ "$skip_cleanup" == "true" ]]; then
|
||||
# Cleanup was skipped due to small cache size
|
||||
local size_mb=$((brew_cache_size / 1024))
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew cleanup · cache ${size_mb}MB, skipped"
|
||||
elif [[ "$brew_success" == "true" && -f "$brew_tmp_file" ]]; then
|
||||
local brew_output
|
||||
brew_output=$(cat "$brew_tmp_file" 2> /dev/null || echo "")
|
||||
local removed_count freed_space
|
||||
removed_count=$(printf '%s\n' "$brew_output" | grep -c "Removing:" 2> /dev/null || true)
|
||||
freed_space=$(printf '%s\n' "$brew_output" | grep -o "[0-9.]*[KMGT]B freed" 2> /dev/null | tail -1 || true)
|
||||
if [[ $removed_count -gt 0 ]] || [[ -n "$freed_space" ]]; then
|
||||
if [[ -n "$freed_space" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew cleanup ${GREEN}($freed_space)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew cleanup (${removed_count} items)"
|
||||
fi
|
||||
fi
|
||||
elif [[ $brew_exit -eq 124 ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Homebrew cleanup timed out · run ${GRAY}brew cleanup${NC} manually"
|
||||
fi
|
||||
# Process autoremove output - only show if packages were removed
|
||||
# Only surface autoremove output when packages were removed.
|
||||
if [[ "$autoremove_success" == "true" && -f "$autoremove_tmp_file" ]]; then
|
||||
local autoremove_output
|
||||
autoremove_output=$(cat "$autoremove_tmp_file" 2> /dev/null || echo "")
|
||||
local removed_packages
|
||||
removed_packages=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" 2> /dev/null || true)
|
||||
if [[ $removed_packages -gt 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed orphaned dependencies (${removed_packages} packages)"
|
||||
fi
|
||||
elif [[ $autoremove_exit -eq 124 ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Autoremove timed out · run ${GRAY}brew autoremove${NC} manually"
|
||||
fi
|
||||
# Update cache timestamp on successful completion or when cleanup was intelligently skipped
|
||||
# This prevents repeated cache size checks within the 7-day window
|
||||
# Update cache timestamp when any work succeeded or was intentionally skipped.
|
||||
if [[ "$skip_cleanup" == "true" ]] || [[ "$brew_success" == "true" ]] || [[ "$autoremove_success" == "true" ]]; then
|
||||
ensure_user_file "$brew_cache_file"
|
||||
get_epoch_seconds > "$brew_cache_file"
|
||||
fi
|
||||
}
|
||||
385
lib/clean/caches.ps1
Normal file
385
lib/clean/caches.ps1
Normal file
@@ -0,0 +1,385 @@
|
||||
# Mole - Cache Cleanup Module
|
||||
# Cleans Windows and application caches
|
||||
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Prevent multiple sourcing
|
||||
if ((Get-Variable -Name 'MOLE_CLEAN_CACHES_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_CACHES_LOADED) { return }
|
||||
$script:MOLE_CLEAN_CACHES_LOADED = $true
|
||||
|
||||
# Import dependencies
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir\..\core\base.ps1"
|
||||
. "$scriptDir\..\core\log.ps1"
|
||||
. "$scriptDir\..\core\file_ops.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Windows System Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-WindowsUpdateCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Update cache (requires admin)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Windows Update cache - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
$wuPath = "$env:WINDIR\SoftwareDistribution\Download"
|
||||
|
||||
if (Test-Path $wuPath) {
|
||||
# Stop Windows Update service first
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Windows Update cache"
|
||||
Set-SectionActivity
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
|
||||
Clear-DirectoryContents -Path $wuPath -Description "Windows Update cache"
|
||||
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not clear Windows Update cache: $_"
|
||||
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-DeliveryOptimizationCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Delivery Optimization cache (requires admin)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Delivery Optimization cache - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
$doPath = "$env:WINDIR\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization"
|
||||
|
||||
if (Test-Path $doPath) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Delivery Optimization cache"
|
||||
Set-SectionActivity
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Stop-Service -Name DoSvc -Force -ErrorAction SilentlyContinue
|
||||
Clear-DirectoryContents -Path "$doPath\Cache" -Description "Delivery Optimization cache"
|
||||
Start-Service -Name DoSvc -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not clear Delivery Optimization cache: $_"
|
||||
Start-Service -Name DoSvc -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-FontCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows font cache (requires admin)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
return
|
||||
}
|
||||
|
||||
$fontCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts\FontCache"
|
||||
|
||||
if (Test-Path $fontCachePath) {
|
||||
Remove-SafeItem -Path $fontCachePath -Description "Font cache"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Browser Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-BrowserCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean browser cache directories
|
||||
#>
|
||||
|
||||
Start-Section "Browser caches"
|
||||
|
||||
# Chrome
|
||||
$chromeCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache"
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Code Cache"
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\GPUCache"
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Service Worker\CacheStorage"
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\ShaderCache"
|
||||
"$env:LOCALAPPDATA\Google\Chrome\User Data\GrShaderCache"
|
||||
)
|
||||
|
||||
foreach ($path in $chromeCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Chrome $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Edge
|
||||
$edgeCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache"
|
||||
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Code Cache"
|
||||
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\GPUCache"
|
||||
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Service Worker\CacheStorage"
|
||||
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\ShaderCache"
|
||||
)
|
||||
|
||||
foreach ($path in $edgeCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Edge $(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Firefox
|
||||
$firefoxProfiles = "$env:APPDATA\Mozilla\Firefox\Profiles"
|
||||
if (Test-Path $firefoxProfiles) {
|
||||
$profiles = Get-ChildItem -Path $firefoxProfiles -Directory -ErrorAction SilentlyContinue
|
||||
foreach ($profile in $profiles) {
|
||||
$firefoxCachePaths = @(
|
||||
"$($profile.FullName)\cache2"
|
||||
"$($profile.FullName)\startupCache"
|
||||
"$($profile.FullName)\shader-cache"
|
||||
)
|
||||
foreach ($path in $firefoxCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Firefox cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Brave
|
||||
$braveCachePath = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache"
|
||||
if (Test-Path $braveCachePath) {
|
||||
Clear-DirectoryContents -Path $braveCachePath -Description "Brave cache"
|
||||
}
|
||||
|
||||
# Opera
|
||||
$operaCachePath = "$env:APPDATA\Opera Software\Opera Stable\Cache"
|
||||
if (Test-Path $operaCachePath) {
|
||||
Clear-DirectoryContents -Path $operaCachePath -Description "Opera cache"
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Application Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-AppCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean common application caches
|
||||
#>
|
||||
|
||||
Start-Section "Application caches"
|
||||
|
||||
# Spotify
|
||||
$spotifyCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Spotify\Data"
|
||||
"$env:LOCALAPPDATA\Spotify\Storage"
|
||||
)
|
||||
foreach ($path in $spotifyCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Spotify cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Discord
|
||||
$discordCachePaths = @(
|
||||
"$env:APPDATA\discord\Cache"
|
||||
"$env:APPDATA\discord\Code Cache"
|
||||
"$env:APPDATA\discord\GPUCache"
|
||||
)
|
||||
foreach ($path in $discordCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Discord cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Slack
|
||||
$slackCachePaths = @(
|
||||
"$env:APPDATA\Slack\Cache"
|
||||
"$env:APPDATA\Slack\Code Cache"
|
||||
"$env:APPDATA\Slack\GPUCache"
|
||||
"$env:APPDATA\Slack\Service Worker\CacheStorage"
|
||||
)
|
||||
foreach ($path in $slackCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Slack cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Teams
|
||||
$teamsCachePaths = @(
|
||||
"$env:APPDATA\Microsoft\Teams\Cache"
|
||||
"$env:APPDATA\Microsoft\Teams\blob_storage"
|
||||
"$env:APPDATA\Microsoft\Teams\databases"
|
||||
"$env:APPDATA\Microsoft\Teams\GPUCache"
|
||||
"$env:APPDATA\Microsoft\Teams\IndexedDB"
|
||||
"$env:APPDATA\Microsoft\Teams\Local Storage"
|
||||
"$env:APPDATA\Microsoft\Teams\tmp"
|
||||
)
|
||||
foreach ($path in $teamsCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Teams cache"
|
||||
}
|
||||
}
|
||||
|
||||
# VS Code
|
||||
$vscodeCachePaths = @(
|
||||
"$env:APPDATA\Code\Cache"
|
||||
"$env:APPDATA\Code\CachedData"
|
||||
"$env:APPDATA\Code\CachedExtensions"
|
||||
"$env:APPDATA\Code\CachedExtensionVSIXs"
|
||||
"$env:APPDATA\Code\Code Cache"
|
||||
"$env:APPDATA\Code\GPUCache"
|
||||
)
|
||||
foreach ($path in $vscodeCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "VS Code cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Zoom
|
||||
$zoomCachePath = "$env:APPDATA\Zoom\data"
|
||||
if (Test-Path $zoomCachePath) {
|
||||
Clear-DirectoryContents -Path $zoomCachePath -Description "Zoom cache"
|
||||
}
|
||||
|
||||
# Adobe Creative Cloud
|
||||
$adobeCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Adobe\*\Cache"
|
||||
"$env:APPDATA\Adobe\Common\Media Cache Files"
|
||||
"$env:APPDATA\Adobe\Common\Peak Files"
|
||||
)
|
||||
foreach ($pattern in $adobeCachePaths) {
|
||||
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
|
||||
foreach ($path in $paths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "Adobe cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Steam (download cache, not games)
|
||||
$steamCachePath = "${env:ProgramFiles(x86)}\Steam\appcache"
|
||||
if (Test-Path $steamCachePath) {
|
||||
Clear-DirectoryContents -Path $steamCachePath -Description "Steam app cache"
|
||||
}
|
||||
|
||||
# Epic Games Launcher
|
||||
$epicCachePath = "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache"
|
||||
if (Test-Path $epicCachePath) {
|
||||
Clear-DirectoryContents -Path $epicCachePath -Description "Epic Games cache"
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Windows Store / UWP App Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-StoreAppCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Store and UWP app caches
|
||||
#>
|
||||
|
||||
# Microsoft Store cache
|
||||
$storeCache = "$env:LOCALAPPDATA\Microsoft\Windows\WCN"
|
||||
if (Test-Path $storeCache) {
|
||||
Clear-DirectoryContents -Path $storeCache -Description "Windows Store cache"
|
||||
}
|
||||
|
||||
# Store app temp files
|
||||
$storeTemp = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_*\LocalCache"
|
||||
$storePaths = Resolve-Path $storeTemp -ErrorAction SilentlyContinue
|
||||
foreach ($path in $storePaths) {
|
||||
if (Test-Path $path.Path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "Store LocalCache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# .NET / Runtime Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-DotNetCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean .NET runtime caches
|
||||
#>
|
||||
|
||||
# .NET temp files
|
||||
$dotnetTemp = "$env:LOCALAPPDATA\Temp\Microsoft.NET"
|
||||
if (Test-Path $dotnetTemp) {
|
||||
Clear-DirectoryContents -Path $dotnetTemp -Description ".NET temp files"
|
||||
}
|
||||
|
||||
# NGen cache (don't touch - managed by Windows)
|
||||
# Assembly cache (don't touch - managed by CLR)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Cache Cleanup Function
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-CacheCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run all cache cleanup tasks
|
||||
#>
|
||||
param(
|
||||
[switch]$IncludeWindowsUpdate,
|
||||
[switch]$IncludeBrowsers,
|
||||
[switch]$IncludeApps
|
||||
)
|
||||
|
||||
Start-Section "System caches"
|
||||
|
||||
# Windows system caches (if admin)
|
||||
if (Test-IsAdmin) {
|
||||
if ($IncludeWindowsUpdate) {
|
||||
Clear-WindowsUpdateCache
|
||||
Clear-DeliveryOptimizationCache
|
||||
}
|
||||
Clear-FontCache
|
||||
}
|
||||
|
||||
Clear-StoreAppCaches
|
||||
Clear-DotNetCaches
|
||||
|
||||
Stop-Section
|
||||
|
||||
# Browser caches
|
||||
if ($IncludeBrowsers) {
|
||||
Clear-BrowserCaches
|
||||
}
|
||||
|
||||
# Application caches
|
||||
if ($IncludeApps) {
|
||||
Clear-AppCaches
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
# Functions: Clear-WindowsUpdateCache, Clear-BrowserCaches, Clear-AppCaches, etc.
|
||||
@@ -1,217 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Cache Cleanup Module
|
||||
set -euo pipefail
|
||||
# Preflight TCC prompts once to avoid mid-run interruptions.
|
||||
check_tcc_permissions() {
|
||||
[[ -t 1 ]] || return 0
|
||||
local permission_flag="$HOME/.cache/mole/permissions_granted"
|
||||
[[ -f "$permission_flag" ]] && return 0
|
||||
local -a tcc_dirs=(
|
||||
"$HOME/Library/Caches"
|
||||
"$HOME/Library/Logs"
|
||||
"$HOME/Library/Application Support"
|
||||
"$HOME/Library/Containers"
|
||||
"$HOME/.cache"
|
||||
)
|
||||
# Quick permission probe (avoid deep scans).
|
||||
local needs_permission_check=false
|
||||
if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then
|
||||
needs_permission_check=true
|
||||
fi
|
||||
if [[ "$needs_permission_check" == "true" ]]; then
|
||||
echo ""
|
||||
echo -e "${BLUE}First-time setup${NC}"
|
||||
echo -e "${GRAY}macOS will request permissions to access Library folders.${NC}"
|
||||
echo -e "${GRAY}You may see ${GREEN}${#tcc_dirs[@]} permission dialogs${NC}${GRAY} - please approve them all.${NC}"
|
||||
echo ""
|
||||
echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to continue: "
|
||||
read -r
|
||||
MOLE_SPINNER_PREFIX="" start_inline_spinner "Requesting permissions..."
|
||||
# Touch each directory to trigger prompts without deep scanning.
|
||||
for dir in "${tcc_dirs[@]}"; do
|
||||
[[ -d "$dir" ]] && command find "$dir" -maxdepth 1 -type d > /dev/null 2>&1
|
||||
done
|
||||
stop_inline_spinner
|
||||
echo ""
|
||||
fi
|
||||
# Mark as granted to avoid repeat prompts.
|
||||
ensure_user_file "$permission_flag"
|
||||
return 0
|
||||
}
|
||||
# Args: $1=browser_name, $2=cache_path
|
||||
# Clean Service Worker cache while protecting critical web editors.
|
||||
clean_service_worker_cache() {
|
||||
local browser_name="$1"
|
||||
local cache_path="$2"
|
||||
[[ ! -d "$cache_path" ]] && return 0
|
||||
local cleaned_size=0
|
||||
local protected_count=0
|
||||
while IFS= read -r cache_dir; do
|
||||
[[ ! -d "$cache_dir" ]] && continue
|
||||
# Extract a best-effort domain name from cache folder.
|
||||
local domain=$(basename "$cache_dir" | grep -oE '[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,}' | head -1 || echo "")
|
||||
local size=$(run_with_timeout 5 get_path_size_kb "$cache_dir")
|
||||
local is_protected=false
|
||||
for protected_domain in "${PROTECTED_SW_DOMAINS[@]}"; do
|
||||
if [[ "$domain" == *"$protected_domain"* ]]; then
|
||||
is_protected=true
|
||||
protected_count=$((protected_count + 1))
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$is_protected" == "false" ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$cache_dir" true || true
|
||||
fi
|
||||
cleaned_size=$((cleaned_size + size))
|
||||
fi
|
||||
done < <(run_with_timeout 10 sh -c "find '$cache_path' -type d -depth 2 2> /dev/null || true")
|
||||
if [[ $cleaned_size -gt 0 ]]; then
|
||||
local spinner_was_running=false
|
||||
if [[ -t 1 && -n "${INLINE_SPINNER_PID:-}" ]]; then
|
||||
stop_inline_spinner
|
||||
spinner_was_running=true
|
||||
fi
|
||||
local cleaned_mb=$((cleaned_size / 1024))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if [[ $protected_count -gt 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $browser_name Service Worker (${cleaned_mb}MB, ${protected_count} protected)"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $browser_name Service Worker (${cleaned_mb}MB)"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} $browser_name Service Worker (would clean ${cleaned_mb}MB, ${protected_count} protected)"
|
||||
fi
|
||||
note_activity
|
||||
if [[ "$spinner_was_running" == "true" ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning browser Service Worker caches..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
# Next.js/Python project caches with tight scan bounds and timeouts.
|
||||
clean_project_caches() {
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
# Fast pre-check before scanning the whole home dir.
|
||||
local has_dev_projects=false
|
||||
local -a common_dev_dirs=(
|
||||
"$HOME/Code"
|
||||
"$HOME/Projects"
|
||||
"$HOME/workspace"
|
||||
"$HOME/github"
|
||||
"$HOME/dev"
|
||||
"$HOME/work"
|
||||
"$HOME/src"
|
||||
"$HOME/repos"
|
||||
"$HOME/Development"
|
||||
"$HOME/www"
|
||||
"$HOME/golang"
|
||||
"$HOME/go"
|
||||
"$HOME/rust"
|
||||
"$HOME/python"
|
||||
"$HOME/ruby"
|
||||
"$HOME/java"
|
||||
"$HOME/dotnet"
|
||||
"$HOME/node"
|
||||
)
|
||||
for dir in "${common_dev_dirs[@]}"; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
has_dev_projects=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Fallback: look for project markers near $HOME.
|
||||
if [[ "$has_dev_projects" == "false" ]]; then
|
||||
local -a project_markers=(
|
||||
"node_modules"
|
||||
".git"
|
||||
"target"
|
||||
"go.mod"
|
||||
"Cargo.toml"
|
||||
"package.json"
|
||||
"pom.xml"
|
||||
"build.gradle"
|
||||
)
|
||||
local spinner_active=false
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" "
|
||||
start_inline_spinner "Detecting dev projects..."
|
||||
spinner_active=true
|
||||
fi
|
||||
for marker in "${project_markers[@]}"; do
|
||||
if run_with_timeout 3 sh -c "find '$HOME' -maxdepth 2 -name '$marker' -not -path '*/Library/*' -not -path '*/.Trash/*' 2>/dev/null | head -1" | grep -q .; then
|
||||
has_dev_projects=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
fi
|
||||
[[ "$has_dev_projects" == "false" ]] && return 0
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" "
|
||||
start_inline_spinner "Searching project caches..."
|
||||
fi
|
||||
local nextjs_tmp_file
|
||||
nextjs_tmp_file=$(create_temp_file)
|
||||
local pycache_tmp_file
|
||||
pycache_tmp_file=$(create_temp_file)
|
||||
local find_timeout=10
|
||||
# Parallel scans (Next.js and __pycache__).
|
||||
(
|
||||
command find "$HOME" -P -mount -type d -name ".next" -maxdepth 3 \
|
||||
-not -path "*/Library/*" \
|
||||
-not -path "*/.Trash/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/.*" \
|
||||
2> /dev/null || true
|
||||
) > "$nextjs_tmp_file" 2>&1 &
|
||||
local next_pid=$!
|
||||
(
|
||||
command find "$HOME" -P -mount -type d -name "__pycache__" -maxdepth 3 \
|
||||
-not -path "*/Library/*" \
|
||||
-not -path "*/.Trash/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/.*" \
|
||||
2> /dev/null || true
|
||||
) > "$pycache_tmp_file" 2>&1 &
|
||||
local py_pid=$!
|
||||
local elapsed=0
|
||||
local check_interval=0.2 # Check every 200ms instead of 1s for smoother experience
|
||||
while [[ $(echo "$elapsed < $find_timeout" | awk '{print ($1 < $2)}') -eq 1 ]]; do
|
||||
if ! kill -0 $next_pid 2> /dev/null && ! kill -0 $py_pid 2> /dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep $check_interval
|
||||
elapsed=$(echo "$elapsed + $check_interval" | awk '{print $1 + $2}')
|
||||
done
|
||||
# Kill stuck scans after timeout.
|
||||
for pid in $next_pid $py_pid; do
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
kill -TERM "$pid" 2> /dev/null || true
|
||||
local grace_period=0
|
||||
while [[ $grace_period -lt 20 ]]; do
|
||||
if ! kill -0 "$pid" 2> /dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
((grace_period++))
|
||||
done
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
kill -KILL "$pid" 2> /dev/null || true
|
||||
fi
|
||||
wait "$pid" 2> /dev/null || true
|
||||
else
|
||||
wait "$pid" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
while IFS= read -r next_dir; do
|
||||
[[ -d "$next_dir/cache" ]] && safe_clean "$next_dir/cache"/* "Next.js build cache" || true
|
||||
done < "$nextjs_tmp_file"
|
||||
while IFS= read -r pycache; do
|
||||
[[ -d "$pycache" ]] && safe_clean "$pycache"/* "Python bytecode cache" || true
|
||||
done < "$pycache_tmp_file"
|
||||
}
|
||||
537
lib/clean/dev.ps1
Normal file
537
lib/clean/dev.ps1
Normal file
@@ -0,0 +1,537 @@
|
||||
# Mole - Developer Tools Cleanup Module
|
||||
# Cleans development tool caches and build artifacts
|
||||
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Prevent multiple sourcing
|
||||
if ((Get-Variable -Name 'MOLE_CLEAN_DEV_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_DEV_LOADED) { return }
|
||||
$script:MOLE_CLEAN_DEV_LOADED = $true
|
||||
|
||||
# Import dependencies
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir\..\core\base.ps1"
|
||||
. "$scriptDir\..\core\log.ps1"
|
||||
. "$scriptDir\..\core\file_ops.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Node.js / JavaScript Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-NpmCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean npm, pnpm, yarn, and bun caches
|
||||
#>
|
||||
|
||||
# npm cache
|
||||
if (Get-Command npm -ErrorAction SilentlyContinue) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "npm cache"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$null = npm cache clean --force 2>&1
|
||||
Write-Success "npm cache"
|
||||
Set-SectionActivity
|
||||
}
|
||||
catch {
|
||||
Write-Debug "npm cache clean failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# npm cache directory (fallback)
|
||||
$npmCachePath = "$env:APPDATA\npm-cache"
|
||||
if (Test-Path $npmCachePath) {
|
||||
Clear-DirectoryContents -Path $npmCachePath -Description "npm cache directory"
|
||||
}
|
||||
|
||||
# pnpm store
|
||||
$pnpmStorePath = "$env:LOCALAPPDATA\pnpm\store"
|
||||
if (Test-Path $pnpmStorePath) {
|
||||
if (Get-Command pnpm -ErrorAction SilentlyContinue) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "pnpm store"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$null = pnpm store prune 2>&1
|
||||
Write-Success "pnpm store pruned"
|
||||
Set-SectionActivity
|
||||
}
|
||||
catch {
|
||||
Write-Debug "pnpm store prune failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Yarn cache
|
||||
$yarnCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Yarn\Cache"
|
||||
"$env:USERPROFILE\.yarn\cache"
|
||||
)
|
||||
foreach ($path in $yarnCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
Clear-DirectoryContents -Path $path -Description "Yarn cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Bun cache
|
||||
$bunCachePath = "$env:USERPROFILE\.bun\install\cache"
|
||||
if (Test-Path $bunCachePath) {
|
||||
Clear-DirectoryContents -Path $bunCachePath -Description "Bun cache"
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-NodeBuildCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Node.js build-related caches
|
||||
#>
|
||||
|
||||
# node-gyp
|
||||
$nodeGypPath = "$env:LOCALAPPDATA\node-gyp\Cache"
|
||||
if (Test-Path $nodeGypPath) {
|
||||
Clear-DirectoryContents -Path $nodeGypPath -Description "node-gyp cache"
|
||||
}
|
||||
|
||||
# Electron cache
|
||||
$electronCachePath = "$env:LOCALAPPDATA\electron\Cache"
|
||||
if (Test-Path $electronCachePath) {
|
||||
Clear-DirectoryContents -Path $electronCachePath -Description "Electron cache"
|
||||
}
|
||||
|
||||
# TypeScript cache
|
||||
$tsCachePath = "$env:LOCALAPPDATA\TypeScript"
|
||||
if (Test-Path $tsCachePath) {
|
||||
Clear-DirectoryContents -Path $tsCachePath -Description "TypeScript cache"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Python Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-PythonCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Python and pip caches
|
||||
#>
|
||||
|
||||
# pip cache
|
||||
if (Get-Command pip -ErrorAction SilentlyContinue) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "pip cache"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$null = pip cache purge 2>&1
|
||||
Write-Success "pip cache"
|
||||
Set-SectionActivity
|
||||
}
|
||||
catch {
|
||||
Write-Debug "pip cache purge failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# pip cache directory
|
||||
$pipCachePath = "$env:LOCALAPPDATA\pip\Cache"
|
||||
if (Test-Path $pipCachePath) {
|
||||
Clear-DirectoryContents -Path $pipCachePath -Description "pip cache directory"
|
||||
}
|
||||
|
||||
# Python bytecode caches (__pycache__)
|
||||
# Note: These are typically in project directories, cleaned by purge command
|
||||
|
||||
# pyenv cache
|
||||
$pyenvCachePath = "$env:USERPROFILE\.pyenv\cache"
|
||||
if (Test-Path $pyenvCachePath) {
|
||||
Clear-DirectoryContents -Path $pyenvCachePath -Description "pyenv cache"
|
||||
}
|
||||
|
||||
# Poetry cache
|
||||
$poetryCachePath = "$env:LOCALAPPDATA\pypoetry\Cache"
|
||||
if (Test-Path $poetryCachePath) {
|
||||
Clear-DirectoryContents -Path $poetryCachePath -Description "Poetry cache"
|
||||
}
|
||||
|
||||
# conda packages
|
||||
$condaCachePaths = @(
|
||||
"$env:USERPROFILE\.conda\pkgs"
|
||||
"$env:USERPROFILE\anaconda3\pkgs"
|
||||
"$env:USERPROFILE\miniconda3\pkgs"
|
||||
)
|
||||
foreach ($path in $condaCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
# Only clean index and temp files, not actual packages
|
||||
$tempFiles = Get-ChildItem -Path $path -Filter "*.tmp" -ErrorAction SilentlyContinue
|
||||
if ($tempFiles) {
|
||||
$paths = $tempFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Conda temp files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Jupyter runtime
|
||||
$jupyterRuntimePath = "$env:APPDATA\jupyter\runtime"
|
||||
if (Test-Path $jupyterRuntimePath) {
|
||||
Clear-DirectoryContents -Path $jupyterRuntimePath -Description "Jupyter runtime"
|
||||
}
|
||||
|
||||
# pytest cache
|
||||
$pytestCachePath = "$env:USERPROFILE\.pytest_cache"
|
||||
if (Test-Path $pytestCachePath) {
|
||||
Remove-SafeItem -Path $pytestCachePath -Description "pytest cache" -Recurse
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# .NET / C# Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-DotNetDevCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean .NET development caches
|
||||
#>
|
||||
|
||||
# NuGet cache
|
||||
$nugetCachePath = "$env:USERPROFILE\.nuget\packages"
|
||||
# Don't clean packages by default - they're needed for builds
|
||||
# Only clean http-cache and temp
|
||||
|
||||
$nugetHttpCache = "$env:LOCALAPPDATA\NuGet\v3-cache"
|
||||
if (Test-Path $nugetHttpCache) {
|
||||
Clear-DirectoryContents -Path $nugetHttpCache -Description "NuGet HTTP cache"
|
||||
}
|
||||
|
||||
$nugetTempPath = "$env:LOCALAPPDATA\NuGet\plugins-cache"
|
||||
if (Test-Path $nugetTempPath) {
|
||||
Clear-DirectoryContents -Path $nugetTempPath -Description "NuGet plugins cache"
|
||||
}
|
||||
|
||||
# MSBuild temp files
|
||||
$msbuildTemp = "$env:LOCALAPPDATA\Microsoft\MSBuild"
|
||||
if (Test-Path $msbuildTemp) {
|
||||
$tempDirs = Get-ChildItem -Path $msbuildTemp -Directory -Filter "*temp*" -ErrorAction SilentlyContinue
|
||||
foreach ($dir in $tempDirs) {
|
||||
Clear-DirectoryContents -Path $dir.FullName -Description "MSBuild temp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Go Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-GoCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Go build and module caches
|
||||
#>
|
||||
|
||||
if (Get-Command go -ErrorAction SilentlyContinue) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Go cache"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$null = go clean -cache 2>&1
|
||||
Write-Success "Go build cache"
|
||||
Set-SectionActivity
|
||||
}
|
||||
catch {
|
||||
Write-Debug "go clean -cache failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Go module cache
|
||||
$goModCachePath = "$env:GOPATH\pkg\mod\cache"
|
||||
if (-not $env:GOPATH) {
|
||||
$goModCachePath = "$env:USERPROFILE\go\pkg\mod\cache"
|
||||
}
|
||||
if (Test-Path $goModCachePath) {
|
||||
Clear-DirectoryContents -Path $goModCachePath -Description "Go module cache"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Rust Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-RustCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Rust/Cargo caches
|
||||
#>
|
||||
|
||||
# Cargo registry cache
|
||||
$cargoRegistryCache = "$env:USERPROFILE\.cargo\registry\cache"
|
||||
if (Test-Path $cargoRegistryCache) {
|
||||
Clear-DirectoryContents -Path $cargoRegistryCache -Description "Cargo registry cache"
|
||||
}
|
||||
|
||||
# Cargo git cache
|
||||
$cargoGitCache = "$env:USERPROFILE\.cargo\git\checkouts"
|
||||
if (Test-Path $cargoGitCache) {
|
||||
Clear-DirectoryContents -Path $cargoGitCache -Description "Cargo git cache"
|
||||
}
|
||||
|
||||
# Rustup downloads
|
||||
$rustupDownloads = "$env:USERPROFILE\.rustup\downloads"
|
||||
if (Test-Path $rustupDownloads) {
|
||||
Clear-DirectoryContents -Path $rustupDownloads -Description "Rustup downloads"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Java / JVM Ecosystem
|
||||
# ============================================================================
|
||||
|
||||
function Clear-JvmCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean JVM ecosystem caches (Gradle, Maven, etc.)
|
||||
#>
|
||||
|
||||
# Gradle caches
|
||||
$gradleCachePaths = @(
|
||||
"$env:USERPROFILE\.gradle\caches"
|
||||
"$env:USERPROFILE\.gradle\daemon"
|
||||
"$env:USERPROFILE\.gradle\wrapper\dists"
|
||||
)
|
||||
foreach ($path in $gradleCachePaths) {
|
||||
if (Test-Path $path) {
|
||||
# Only clean temp and old daemon logs
|
||||
$tempFiles = Get-ChildItem -Path $path -Recurse -Filter "*.lock" -ErrorAction SilentlyContinue
|
||||
if ($tempFiles) {
|
||||
$paths = $tempFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Gradle lock files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Maven repository (only clean temp files)
|
||||
$mavenRepoPath = "$env:USERPROFILE\.m2\repository"
|
||||
if (Test-Path $mavenRepoPath) {
|
||||
$tempFiles = Get-ChildItem -Path $mavenRepoPath -Recurse -Filter "*.lastUpdated" -ErrorAction SilentlyContinue
|
||||
if ($tempFiles) {
|
||||
$paths = $tempFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Maven update markers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Docker / Containers
|
||||
# ============================================================================
|
||||
|
||||
function Clear-DockerCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Docker build caches and unused data
|
||||
#>
|
||||
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Check if Docker daemon is running
|
||||
$dockerRunning = $false
|
||||
try {
|
||||
$null = docker info 2>&1
|
||||
$dockerRunning = $true
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Docker daemon not running"
|
||||
}
|
||||
|
||||
if ($dockerRunning) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Docker build cache"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$null = docker builder prune -af 2>&1
|
||||
Write-Success "Docker build cache"
|
||||
Set-SectionActivity
|
||||
}
|
||||
catch {
|
||||
Write-Debug "docker builder prune failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Docker Desktop cache (Windows)
|
||||
$dockerDesktopCache = "$env:LOCALAPPDATA\Docker\wsl\data"
|
||||
# Note: Don't clean this - it's the WSL2 virtual disk
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Cloud CLI Tools
|
||||
# ============================================================================
|
||||
|
||||
function Clear-CloudCliCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean cloud CLI tool caches (AWS, Azure, GCP)
|
||||
#>
|
||||
|
||||
# AWS CLI cache
|
||||
$awsCachePath = "$env:USERPROFILE\.aws\cli\cache"
|
||||
if (Test-Path $awsCachePath) {
|
||||
Clear-DirectoryContents -Path $awsCachePath -Description "AWS CLI cache"
|
||||
}
|
||||
|
||||
# Azure CLI logs
|
||||
$azureLogsPath = "$env:USERPROFILE\.azure\logs"
|
||||
if (Test-Path $azureLogsPath) {
|
||||
Clear-DirectoryContents -Path $azureLogsPath -Description "Azure CLI logs"
|
||||
}
|
||||
|
||||
# Google Cloud logs
|
||||
$gcloudLogsPath = "$env:APPDATA\gcloud\logs"
|
||||
if (Test-Path $gcloudLogsPath) {
|
||||
Clear-DirectoryContents -Path $gcloudLogsPath -Description "gcloud logs"
|
||||
}
|
||||
|
||||
# Kubernetes cache
|
||||
$kubeCachePath = "$env:USERPROFILE\.kube\cache"
|
||||
if (Test-Path $kubeCachePath) {
|
||||
Clear-DirectoryContents -Path $kubeCachePath -Description "Kubernetes cache"
|
||||
}
|
||||
|
||||
# Terraform plugin cache
|
||||
$terraformCachePath = "$env:APPDATA\terraform.d\plugin-cache"
|
||||
if (Test-Path $terraformCachePath) {
|
||||
Clear-DirectoryContents -Path $terraformCachePath -Description "Terraform plugin cache"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# IDE Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-IdeCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean IDE caches (VS, VSCode, JetBrains, etc.)
|
||||
#>
|
||||
|
||||
# Visual Studio cache
|
||||
$vsCachePaths = @(
|
||||
"$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ComponentModelCache"
|
||||
"$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ImageCache"
|
||||
)
|
||||
foreach ($pattern in $vsCachePaths) {
|
||||
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
|
||||
foreach ($path in $paths) {
|
||||
if (Test-Path $path.Path) {
|
||||
Clear-DirectoryContents -Path $path.Path -Description "Visual Studio cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# JetBrains IDEs caches
|
||||
$jetbrainsBasePaths = @(
|
||||
"$env:LOCALAPPDATA\JetBrains"
|
||||
"$env:APPDATA\JetBrains"
|
||||
)
|
||||
foreach ($basePath in $jetbrainsBasePaths) {
|
||||
if (Test-Path $basePath) {
|
||||
$ideFolders = Get-ChildItem -Path $basePath -Directory -ErrorAction SilentlyContinue
|
||||
foreach ($ideFolder in $ideFolders) {
|
||||
$cacheFolders = @("caches", "index", "tmp")
|
||||
foreach ($cacheFolder in $cacheFolders) {
|
||||
$cachePath = Join-Path $ideFolder.FullName $cacheFolder
|
||||
if (Test-Path $cachePath) {
|
||||
Clear-DirectoryContents -Path $cachePath -Description "$($ideFolder.Name) $cacheFolder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Git Caches
|
||||
# ============================================================================
|
||||
|
||||
function Clear-GitCaches {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Git temporary files and lock files
|
||||
#>
|
||||
|
||||
# Git config locks (stale)
|
||||
$gitConfigLock = "$env:USERPROFILE\.gitconfig.lock"
|
||||
if (Test-Path $gitConfigLock) {
|
||||
Remove-SafeItem -Path $gitConfigLock -Description "Git config lock"
|
||||
}
|
||||
|
||||
# GitHub CLI cache
|
||||
$ghCachePath = "$env:APPDATA\GitHub CLI"
|
||||
if (Test-Path $ghCachePath) {
|
||||
$cacheFiles = Get-ChildItem -Path $ghCachePath -Filter "*.json" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) }
|
||||
if ($cacheFiles) {
|
||||
$paths = $cacheFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "GitHub CLI cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Developer Tools Cleanup Function
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-DevToolsCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run all developer tools cleanup tasks
|
||||
#>
|
||||
|
||||
Start-Section "Developer tools"
|
||||
|
||||
# JavaScript ecosystem
|
||||
Clear-NpmCache
|
||||
Clear-NodeBuildCaches
|
||||
|
||||
# Python ecosystem
|
||||
Clear-PythonCaches
|
||||
|
||||
# .NET ecosystem
|
||||
Clear-DotNetDevCaches
|
||||
|
||||
# Go ecosystem
|
||||
Clear-GoCaches
|
||||
|
||||
# Rust ecosystem
|
||||
Clear-RustCaches
|
||||
|
||||
# JVM ecosystem
|
||||
Clear-JvmCaches
|
||||
|
||||
# Containers
|
||||
Clear-DockerCaches
|
||||
|
||||
# Cloud CLI tools
|
||||
Clear-CloudCliCaches
|
||||
|
||||
# IDEs
|
||||
Clear-IdeCaches
|
||||
|
||||
# Git
|
||||
Clear-GitCaches
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
# Functions: Clear-NpmCache, Clear-PythonCaches, Clear-DockerCaches, etc.
|
||||
296
lib/clean/dev.sh
296
lib/clean/dev.sh
@@ -1,296 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Developer Tools Cleanup Module
|
||||
set -euo pipefail
|
||||
# Tool cache helper (respects DRY_RUN).
|
||||
clean_tool_cache() {
|
||||
local description="$1"
|
||||
shift
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if "$@" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $description"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} $description · would clean"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# npm/pnpm/yarn/bun caches.
|
||||
clean_dev_npm() {
|
||||
if command -v npm > /dev/null 2>&1; then
|
||||
clean_tool_cache "npm cache" npm cache clean --force
|
||||
note_activity
|
||||
fi
|
||||
# Clean pnpm store cache
|
||||
local pnpm_default_store=~/Library/pnpm/store
|
||||
# Check if pnpm is actually usable (not just Corepack shim)
|
||||
if command -v pnpm > /dev/null 2>&1 && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 pnpm --version > /dev/null 2>&1; then
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 clean_tool_cache "pnpm cache" pnpm store prune
|
||||
local pnpm_store_path
|
||||
start_section_spinner "Checking store path..."
|
||||
pnpm_store_path=$(COREPACK_ENABLE_DOWNLOAD_PROMPT=0 run_with_timeout 2 pnpm store path 2> /dev/null) || pnpm_store_path=""
|
||||
stop_section_spinner
|
||||
if [[ -n "$pnpm_store_path" && "$pnpm_store_path" != "$pnpm_default_store" ]]; then
|
||||
safe_clean "$pnpm_default_store"/* "Orphaned pnpm store"
|
||||
fi
|
||||
else
|
||||
# pnpm not installed or not usable, just clean the default store directory
|
||||
safe_clean "$pnpm_default_store"/* "pnpm store"
|
||||
fi
|
||||
note_activity
|
||||
safe_clean ~/.tnpm/_cacache/* "tnpm cache directory"
|
||||
safe_clean ~/.tnpm/_logs/* "tnpm logs"
|
||||
safe_clean ~/.yarn/cache/* "Yarn cache"
|
||||
safe_clean ~/.bun/install/cache/* "Bun cache"
|
||||
}
|
||||
# Python/pip ecosystem caches.
|
||||
clean_dev_python() {
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
clean_tool_cache "pip cache" bash -c 'pip3 cache purge >/dev/null 2>&1 || true'
|
||||
note_activity
|
||||
fi
|
||||
safe_clean ~/.pyenv/cache/* "pyenv cache"
|
||||
safe_clean ~/.cache/poetry/* "Poetry cache"
|
||||
safe_clean ~/.cache/uv/* "uv cache"
|
||||
safe_clean ~/.cache/ruff/* "Ruff cache"
|
||||
safe_clean ~/.cache/mypy/* "MyPy cache"
|
||||
safe_clean ~/.pytest_cache/* "Pytest cache"
|
||||
safe_clean ~/.jupyter/runtime/* "Jupyter runtime cache"
|
||||
safe_clean ~/.cache/huggingface/* "Hugging Face cache"
|
||||
safe_clean ~/.cache/torch/* "PyTorch cache"
|
||||
safe_clean ~/.cache/tensorflow/* "TensorFlow cache"
|
||||
safe_clean ~/.conda/pkgs/* "Conda packages cache"
|
||||
safe_clean ~/anaconda3/pkgs/* "Anaconda packages cache"
|
||||
safe_clean ~/.cache/wandb/* "Weights & Biases cache"
|
||||
}
|
||||
# Go build/module caches.
|
||||
clean_dev_go() {
|
||||
if command -v go > /dev/null 2>&1; then
|
||||
clean_tool_cache "Go cache" bash -c 'go clean -modcache >/dev/null 2>&1 || true; go clean -cache >/dev/null 2>&1 || true'
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Rust/cargo caches.
|
||||
clean_dev_rust() {
|
||||
safe_clean ~/.cargo/registry/cache/* "Rust cargo cache"
|
||||
safe_clean ~/.cargo/git/* "Cargo git cache"
|
||||
safe_clean ~/.rustup/downloads/* "Rust downloads cache"
|
||||
}
|
||||
# Docker caches (guarded by daemon check).
|
||||
clean_dev_docker() {
|
||||
if command -v docker > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
start_section_spinner "Checking Docker daemon..."
|
||||
local docker_running=false
|
||||
if run_with_timeout 3 docker info > /dev/null 2>&1; then
|
||||
docker_running=true
|
||||
fi
|
||||
stop_section_spinner
|
||||
if [[ "$docker_running" == "true" ]]; then
|
||||
clean_tool_cache "Docker build cache" docker builder prune -af
|
||||
else
|
||||
debug_log "Docker daemon not running, skipping Docker cache cleanup"
|
||||
fi
|
||||
else
|
||||
note_activity
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Docker build cache · would clean"
|
||||
fi
|
||||
fi
|
||||
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
||||
}
|
||||
# Nix garbage collection.
|
||||
clean_dev_nix() {
|
||||
if command -v nix-collect-garbage > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "Nix garbage collection" nix-collect-garbage --delete-older-than 30d
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Nix garbage collection · would clean"
|
||||
fi
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Cloud CLI caches.
|
||||
clean_dev_cloud() {
|
||||
safe_clean ~/.kube/cache/* "Kubernetes cache"
|
||||
safe_clean ~/.local/share/containers/storage/tmp/* "Container storage temp"
|
||||
safe_clean ~/.aws/cli/cache/* "AWS CLI cache"
|
||||
safe_clean ~/.config/gcloud/logs/* "Google Cloud logs"
|
||||
safe_clean ~/.azure/logs/* "Azure CLI logs"
|
||||
}
|
||||
# Frontend build caches.
|
||||
clean_dev_frontend() {
|
||||
safe_clean ~/.cache/typescript/* "TypeScript cache"
|
||||
safe_clean ~/.cache/electron/* "Electron cache"
|
||||
safe_clean ~/.cache/node-gyp/* "node-gyp cache"
|
||||
safe_clean ~/.node-gyp/* "node-gyp build cache"
|
||||
safe_clean ~/.turbo/cache/* "Turbo cache"
|
||||
safe_clean ~/.vite/cache/* "Vite cache"
|
||||
safe_clean ~/.cache/vite/* "Vite global cache"
|
||||
safe_clean ~/.cache/webpack/* "Webpack cache"
|
||||
safe_clean ~/.parcel-cache/* "Parcel cache"
|
||||
safe_clean ~/.cache/eslint/* "ESLint cache"
|
||||
safe_clean ~/.cache/prettier/* "Prettier cache"
|
||||
}
|
||||
# Mobile dev caches (can be large).
|
||||
# Check for multiple Android NDK versions.
|
||||
check_android_ndk() {
|
||||
local ndk_dir="$HOME/Library/Android/sdk/ndk"
|
||||
if [[ -d "$ndk_dir" ]]; then
|
||||
local count
|
||||
count=$(find "$ndk_dir" -mindepth 1 -maxdepth 1 -type d 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$count" -gt 1 ]]; then
|
||||
note_activity
|
||||
echo -e " Found ${GREEN}${count}${NC} Android NDK versions"
|
||||
echo -e " You can delete unused versions manually: ${ndk_dir}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
clean_dev_mobile() {
|
||||
check_android_ndk
|
||||
|
||||
if command -v xcrun > /dev/null 2>&1; then
|
||||
debug_log "Checking for unavailable Xcode simulators"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
clean_tool_cache "Xcode unavailable simulators" xcrun simctl delete unavailable
|
||||
else
|
||||
start_section_spinner "Checking unavailable simulators..."
|
||||
if xcrun simctl delete unavailable > /dev/null 2>&1; then
|
||||
stop_section_spinner
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Xcode unavailable simulators"
|
||||
else
|
||||
stop_section_spinner
|
||||
fi
|
||||
fi
|
||||
note_activity
|
||||
fi
|
||||
# DeviceSupport caches/logs (preserve core support files).
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "iOS device symbol cache"
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*.log "iOS device support logs"
|
||||
safe_clean ~/Library/Developer/Xcode/watchOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "watchOS device symbol cache"
|
||||
safe_clean ~/Library/Developer/Xcode/tvOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "tvOS device symbol cache"
|
||||
# Simulator runtime caches.
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Profiles/Runtimes/*/Contents/Resources/RuntimeRoot/System/Library/Caches/* "Simulator runtime cache"
|
||||
safe_clean ~/Library/Caches/Google/AndroidStudio*/* "Android Studio cache"
|
||||
safe_clean ~/Library/Caches/CocoaPods/* "CocoaPods cache"
|
||||
safe_clean ~/.cache/flutter/* "Flutter cache"
|
||||
safe_clean ~/.android/build-cache/* "Android build cache"
|
||||
safe_clean ~/.android/cache/* "Android SDK cache"
|
||||
safe_clean ~/Library/Developer/Xcode/UserData/IB\ Support/* "Xcode Interface Builder cache"
|
||||
safe_clean ~/.cache/swift-package-manager/* "Swift package manager cache"
|
||||
}
|
||||
# JVM ecosystem caches.
|
||||
clean_dev_jvm() {
|
||||
safe_clean ~/.gradle/caches/* "Gradle caches"
|
||||
safe_clean ~/.gradle/daemon/* "Gradle daemon logs"
|
||||
safe_clean ~/.sbt/* "SBT cache"
|
||||
safe_clean ~/.ivy2/cache/* "Ivy cache"
|
||||
}
|
||||
# Other language tool caches.
|
||||
clean_dev_other_langs() {
|
||||
safe_clean ~/.bundle/cache/* "Ruby Bundler cache"
|
||||
safe_clean ~/.composer/cache/* "PHP Composer cache"
|
||||
safe_clean ~/.nuget/packages/* "NuGet packages cache"
|
||||
safe_clean ~/.pub-cache/* "Dart Pub cache"
|
||||
safe_clean ~/.cache/bazel/* "Bazel cache"
|
||||
safe_clean ~/.cache/zig/* "Zig cache"
|
||||
safe_clean ~/Library/Caches/deno/* "Deno cache"
|
||||
}
|
||||
# CI/CD and DevOps caches.
|
||||
clean_dev_cicd() {
|
||||
safe_clean ~/.cache/terraform/* "Terraform cache"
|
||||
safe_clean ~/.grafana/cache/* "Grafana cache"
|
||||
safe_clean ~/.prometheus/data/wal/* "Prometheus WAL cache"
|
||||
safe_clean ~/.jenkins/workspace/*/target/* "Jenkins workspace cache"
|
||||
safe_clean ~/.cache/gitlab-runner/* "GitLab Runner cache"
|
||||
safe_clean ~/.github/cache/* "GitHub Actions cache"
|
||||
safe_clean ~/.circleci/cache/* "CircleCI cache"
|
||||
safe_clean ~/.sonar/* "SonarQube cache"
|
||||
}
|
||||
# Database tool caches.
|
||||
clean_dev_database() {
|
||||
safe_clean ~/Library/Caches/com.sequel-ace.sequel-ace/* "Sequel Ace cache"
|
||||
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
|
||||
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager cache"
|
||||
safe_clean ~/Library/Caches/com.navicat.* "Navicat cache"
|
||||
safe_clean ~/Library/Caches/com.dbeaver.* "DBeaver cache"
|
||||
safe_clean ~/Library/Caches/com.redis.RedisInsight "Redis Insight cache"
|
||||
}
|
||||
# API/debugging tool caches.
|
||||
clean_dev_api_tools() {
|
||||
safe_clean ~/Library/Caches/com.postmanlabs.mac/* "Postman cache"
|
||||
safe_clean ~/Library/Caches/com.konghq.insomnia/* "Insomnia cache"
|
||||
safe_clean ~/Library/Caches/com.tinyapp.TablePlus/* "TablePlus cache"
|
||||
safe_clean ~/Library/Caches/com.getpaw.Paw/* "Paw API cache"
|
||||
safe_clean ~/Library/Caches/com.charlesproxy.charles/* "Charles Proxy cache"
|
||||
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
|
||||
}
|
||||
# Misc dev tool caches.
|
||||
clean_dev_misc() {
|
||||
safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache"
|
||||
safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache"
|
||||
safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache"
|
||||
safe_clean ~/Library/Caches/com.github.GitHubDesktop/* "GitHub Desktop cache"
|
||||
safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports"
|
||||
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
|
||||
safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data"
|
||||
}
|
||||
# Shell and VCS leftovers.
|
||||
clean_dev_shell() {
|
||||
safe_clean ~/.gitconfig.lock "Git config lock"
|
||||
safe_clean ~/.gitconfig.bak* "Git config backup"
|
||||
safe_clean ~/.oh-my-zsh/cache/* "Oh My Zsh cache"
|
||||
safe_clean ~/.config/fish/fish_history.bak* "Fish shell backup"
|
||||
safe_clean ~/.bash_history.bak* "Bash history backup"
|
||||
safe_clean ~/.zsh_history.bak* "Zsh history backup"
|
||||
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
|
||||
}
|
||||
# Network tool caches.
|
||||
clean_dev_network() {
|
||||
safe_clean ~/.cache/curl/* "curl cache"
|
||||
safe_clean ~/.cache/wget/* "wget cache"
|
||||
safe_clean ~/Library/Caches/curl/* "macOS curl cache"
|
||||
safe_clean ~/Library/Caches/wget/* "macOS wget cache"
|
||||
}
|
||||
# Orphaned SQLite temp files (-shm/-wal). Disabled due to low ROI.
|
||||
clean_sqlite_temp_files() {
|
||||
return 0
|
||||
}
|
||||
# Main developer tools cleanup sequence.
|
||||
clean_developer_tools() {
|
||||
stop_section_spinner
|
||||
clean_sqlite_temp_files
|
||||
clean_dev_npm
|
||||
clean_dev_python
|
||||
clean_dev_go
|
||||
clean_dev_rust
|
||||
clean_dev_docker
|
||||
clean_dev_cloud
|
||||
clean_dev_nix
|
||||
clean_dev_shell
|
||||
clean_dev_frontend
|
||||
clean_project_caches
|
||||
clean_dev_mobile
|
||||
clean_dev_jvm
|
||||
clean_dev_other_langs
|
||||
clean_dev_cicd
|
||||
clean_dev_database
|
||||
clean_dev_api_tools
|
||||
clean_dev_network
|
||||
clean_dev_misc
|
||||
safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache"
|
||||
# Clean Homebrew locks without repeated sudo prompts.
|
||||
local brew_lock_dirs=(
|
||||
"/opt/homebrew/var/homebrew/locks"
|
||||
"/usr/local/var/homebrew/locks"
|
||||
)
|
||||
for lock_dir in "${brew_lock_dirs[@]}"; do
|
||||
if [[ -d "$lock_dir" && -w "$lock_dir" ]]; then
|
||||
safe_clean "$lock_dir"/* "Homebrew lock files"
|
||||
elif [[ -d "$lock_dir" ]]; then
|
||||
if find "$lock_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
debug_log "Skipping read-only Homebrew locks in $lock_dir"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
clean_homebrew
|
||||
}
|
||||
@@ -1,925 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Project Purge Module (mo purge).
|
||||
# Removes heavy project build artifacts and dependencies.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CORE_LIB_DIR="$(cd "$PROJECT_LIB_DIR/../core" && pwd)"
|
||||
if ! command -v ensure_user_dir > /dev/null 2>&1; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$CORE_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
# Targets to look for (heavy build artifacts).
|
||||
readonly PURGE_TARGETS=(
|
||||
"node_modules"
|
||||
"target" # Rust, Maven
|
||||
"build" # Gradle, various
|
||||
"dist" # JS builds
|
||||
"venv" # Python
|
||||
".venv" # Python
|
||||
".pytest_cache" # Python (pytest)
|
||||
".mypy_cache" # Python (mypy)
|
||||
".tox" # Python (tox virtualenvs)
|
||||
".nox" # Python (nox virtualenvs)
|
||||
".ruff_cache" # Python (ruff)
|
||||
".gradle" # Gradle local
|
||||
"__pycache__" # Python
|
||||
".next" # Next.js
|
||||
".nuxt" # Nuxt.js
|
||||
".output" # Nuxt.js
|
||||
"vendor" # PHP Composer
|
||||
"bin" # .NET build output (guarded; see is_protected_purge_artifact)
|
||||
"obj" # C# / Unity
|
||||
".turbo" # Turborepo cache
|
||||
".parcel-cache" # Parcel bundler
|
||||
".dart_tool" # Flutter/Dart build cache
|
||||
".zig-cache" # Zig
|
||||
"zig-out" # Zig
|
||||
".angular" # Angular
|
||||
".svelte-kit" # SvelteKit
|
||||
".astro" # Astro
|
||||
"coverage" # Code coverage reports
|
||||
)
|
||||
# Minimum age in days before considering for cleanup.
|
||||
readonly MIN_AGE_DAYS=7
|
||||
# Scan depth defaults (relative to search root).
|
||||
readonly PURGE_MIN_DEPTH_DEFAULT=2
|
||||
readonly PURGE_MAX_DEPTH_DEFAULT=8
|
||||
# Search paths (default, can be overridden via config file).
|
||||
readonly DEFAULT_PURGE_SEARCH_PATHS=(
|
||||
"$HOME/www"
|
||||
"$HOME/dev"
|
||||
"$HOME/Projects"
|
||||
"$HOME/GitHub"
|
||||
"$HOME/Code"
|
||||
"$HOME/Workspace"
|
||||
"$HOME/Repos"
|
||||
"$HOME/Development"
|
||||
)
|
||||
|
||||
# Config file for custom purge paths.
|
||||
readonly PURGE_CONFIG_FILE="$HOME/.config/mole/purge_paths"
|
||||
|
||||
# Resolved search paths.
|
||||
PURGE_SEARCH_PATHS=()
|
||||
|
||||
# Project indicators for container detection.
|
||||
readonly PROJECT_INDICATORS=(
|
||||
"package.json"
|
||||
"Cargo.toml"
|
||||
"go.mod"
|
||||
"pyproject.toml"
|
||||
"requirements.txt"
|
||||
"pom.xml"
|
||||
"build.gradle"
|
||||
"Gemfile"
|
||||
"composer.json"
|
||||
"pubspec.yaml"
|
||||
"Makefile"
|
||||
"build.zig"
|
||||
"build.zig.zon"
|
||||
".git"
|
||||
)
|
||||
|
||||
# Check if a directory contains projects (directly or in subdirectories).
|
||||
is_project_container() {
|
||||
local dir="$1"
|
||||
local max_depth="${2:-2}"
|
||||
|
||||
# Skip hidden/system directories.
|
||||
local basename
|
||||
basename=$(basename "$dir")
|
||||
[[ "$basename" == .* ]] && return 1
|
||||
[[ "$basename" == "Library" ]] && return 1
|
||||
[[ "$basename" == "Applications" ]] && return 1
|
||||
[[ "$basename" == "Movies" ]] && return 1
|
||||
[[ "$basename" == "Music" ]] && return 1
|
||||
[[ "$basename" == "Pictures" ]] && return 1
|
||||
[[ "$basename" == "Public" ]] && return 1
|
||||
|
||||
# Single find expression for indicators.
|
||||
local -a find_args=("$dir" "-maxdepth" "$max_depth" "(")
|
||||
local first=true
|
||||
for indicator in "${PROJECT_INDICATORS[@]}"; do
|
||||
if [[ "$first" == "true" ]]; then
|
||||
first=false
|
||||
else
|
||||
find_args+=("-o")
|
||||
fi
|
||||
find_args+=("-name" "$indicator")
|
||||
done
|
||||
find_args+=(")" "-print" "-quit")
|
||||
|
||||
if find "${find_args[@]}" 2> /dev/null | grep -q .; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Discover project directories in $HOME.
|
||||
discover_project_dirs() {
|
||||
local -a discovered=()
|
||||
|
||||
for path in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
|
||||
if [[ -d "$path" ]]; then
|
||||
discovered+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Scan $HOME for other containers (depth 1).
|
||||
local dir
|
||||
for dir in "$HOME"/*/; do
|
||||
[[ ! -d "$dir" ]] && continue
|
||||
dir="${dir%/}" # Remove trailing slash
|
||||
|
||||
local already_found=false
|
||||
for existing in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
|
||||
if [[ "$dir" == "$existing" ]]; then
|
||||
already_found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ "$already_found" == "true" ]] && continue
|
||||
|
||||
if is_project_container "$dir" 2; then
|
||||
discovered+=("$dir")
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' "${discovered[@]}" | sort -u
|
||||
}
|
||||
|
||||
# Save discovered paths to config.
|
||||
save_discovered_paths() {
|
||||
local -a paths=("$@")
|
||||
|
||||
ensure_user_dir "$(dirname "$PURGE_CONFIG_FILE")"
|
||||
|
||||
cat > "$PURGE_CONFIG_FILE" << 'EOF'
|
||||
# Mole Purge Paths - Auto-discovered project directories
|
||||
# Edit this file to customize, or run: mo purge --paths
|
||||
# Add one path per line (supports ~ for home directory)
|
||||
EOF
|
||||
|
||||
printf '\n' >> "$PURGE_CONFIG_FILE"
|
||||
for path in "${paths[@]}"; do
|
||||
# Convert $HOME to ~ for portability
|
||||
path="${path/#$HOME/~}"
|
||||
echo "$path" >> "$PURGE_CONFIG_FILE"
|
||||
done
|
||||
}
|
||||
|
||||
# Load purge paths from config or auto-discover
|
||||
load_purge_config() {
|
||||
PURGE_SEARCH_PATHS=()
|
||||
|
||||
if [[ -f "$PURGE_CONFIG_FILE" ]]; then
|
||||
while IFS= read -r line; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
[[ -z "$line" || "$line" =~ ^# ]] && continue
|
||||
|
||||
line="${line/#\~/$HOME}"
|
||||
|
||||
PURGE_SEARCH_PATHS+=("$line")
|
||||
done < "$PURGE_CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [[ ${#PURGE_SEARCH_PATHS[@]} -eq 0 ]]; then
|
||||
if [[ -t 1 ]] && [[ -z "${_PURGE_DISCOVERY_SILENT:-}" ]]; then
|
||||
echo -e "${GRAY}First run: discovering project directories...${NC}" >&2
|
||||
fi
|
||||
|
||||
local -a discovered=()
|
||||
while IFS= read -r path; do
|
||||
[[ -n "$path" ]] && discovered+=("$path")
|
||||
done < <(discover_project_dirs)
|
||||
|
||||
if [[ ${#discovered[@]} -gt 0 ]]; then
|
||||
PURGE_SEARCH_PATHS=("${discovered[@]}")
|
||||
save_discovered_paths "${discovered[@]}"
|
||||
|
||||
if [[ -t 1 ]] && [[ -z "${_PURGE_DISCOVERY_SILENT:-}" ]]; then
|
||||
echo -e "${GRAY}Found ${#discovered[@]} project directories, saved to config${NC}" >&2
|
||||
fi
|
||||
else
|
||||
PURGE_SEARCH_PATHS=("${DEFAULT_PURGE_SEARCH_PATHS[@]}")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize paths on script load.
|
||||
load_purge_config
|
||||
|
||||
# Args: $1 - path to check
|
||||
# Safe cleanup requires the path be inside a project directory.
|
||||
is_safe_project_artifact() {
|
||||
local path="$1"
|
||||
local search_path="$2"
|
||||
if [[ "$path" != /* ]]; then
|
||||
return 1
|
||||
fi
|
||||
# Must not be a direct child of the search root.
|
||||
local relative_path="${path#"$search_path"/}"
|
||||
local depth=$(echo "$relative_path" | LC_ALL=C tr -cd '/' | wc -c)
|
||||
if [[ $depth -lt 1 ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Detect if directory is a Rails project root
|
||||
is_rails_project_root() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/config/application.rb" ]] || return 1
|
||||
[[ -f "$dir/Gemfile" ]] || return 1
|
||||
[[ -f "$dir/bin/rails" || -f "$dir/config/environment.rb" ]]
|
||||
}
|
||||
|
||||
# Detect if directory is a Go project root
|
||||
is_go_project_root() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/go.mod" ]]
|
||||
}
|
||||
|
||||
# Detect if directory is a PHP Composer project root
|
||||
is_php_project_root() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/composer.json" ]]
|
||||
}
|
||||
|
||||
# Decide whether a "bin" directory is a .NET directory
|
||||
is_dotnet_bin_dir() {
|
||||
local path="$1"
|
||||
[[ "$(basename "$path")" == "bin" ]] || return 1
|
||||
|
||||
# Check if parent directory has a .csproj/.fsproj/.vbproj file
|
||||
local parent_dir
|
||||
parent_dir="$(dirname "$path")"
|
||||
find "$parent_dir" -maxdepth 1 \( -name "*.csproj" -o -name "*.fsproj" -o -name "*.vbproj" \) 2> /dev/null | grep -q . || return 1
|
||||
|
||||
# Check if bin directory contains Debug/ or Release/ subdirectories
|
||||
[[ -d "$path/Debug" || -d "$path/Release" ]] || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if a vendor directory should be protected from purge
|
||||
# Expects path to be a vendor directory (basename == vendor)
|
||||
# Strategy: Only clean PHP Composer vendor, protect all others
|
||||
is_protected_vendor_dir() {
|
||||
local path="$1"
|
||||
local base
|
||||
base=$(basename "$path")
|
||||
[[ "$base" == "vendor" ]] || return 1
|
||||
local parent_dir
|
||||
parent_dir=$(dirname "$path")
|
||||
|
||||
# PHP Composer vendor can be safely regenerated with 'composer install'
|
||||
# Do NOT protect it (return 1 = not protected = can be cleaned)
|
||||
if is_php_project_root "$parent_dir"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Rails vendor (importmap dependencies) - should be protected
|
||||
if is_rails_project_root "$parent_dir"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Go vendor (optional vendoring) - protect to avoid accidental deletion
|
||||
if is_go_project_root "$parent_dir"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Unknown vendor type - protect by default (conservative approach)
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if an artifact should be protected from purge
|
||||
is_protected_purge_artifact() {
|
||||
local path="$1"
|
||||
local base
|
||||
base=$(basename "$path")
|
||||
|
||||
case "$base" in
|
||||
bin)
|
||||
# Only allow purging bin/ when we can detect .NET context.
|
||||
if is_dotnet_bin_dir "$path"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
vendor)
|
||||
is_protected_vendor_dir "$path"
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Scan purge targets using fd (fast) or pruned find.
|
||||
scan_purge_targets() {
|
||||
local search_path="$1"
|
||||
local output_file="$2"
|
||||
local min_depth="$PURGE_MIN_DEPTH_DEFAULT"
|
||||
local max_depth="$PURGE_MAX_DEPTH_DEFAULT"
|
||||
if [[ ! "$min_depth" =~ ^[0-9]+$ ]]; then
|
||||
min_depth="$PURGE_MIN_DEPTH_DEFAULT"
|
||||
fi
|
||||
if [[ ! "$max_depth" =~ ^[0-9]+$ ]]; then
|
||||
max_depth="$PURGE_MAX_DEPTH_DEFAULT"
|
||||
fi
|
||||
if [[ "$max_depth" -lt "$min_depth" ]]; then
|
||||
max_depth="$min_depth"
|
||||
fi
|
||||
if [[ ! -d "$search_path" ]]; then
|
||||
return
|
||||
fi
|
||||
if command -v fd > /dev/null 2>&1; then
|
||||
# Escape regex special characters in target names for fd patterns
|
||||
local escaped_targets=()
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
escaped_targets+=("$(printf '%s' "$target" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g')")
|
||||
done
|
||||
local pattern="($(
|
||||
IFS='|'
|
||||
echo "${escaped_targets[*]}"
|
||||
))"
|
||||
local fd_args=(
|
||||
"--absolute-path"
|
||||
"--hidden"
|
||||
"--no-ignore"
|
||||
"--type" "d"
|
||||
"--min-depth" "$min_depth"
|
||||
"--max-depth" "$max_depth"
|
||||
"--threads" "4"
|
||||
"--exclude" ".git"
|
||||
"--exclude" "Library"
|
||||
"--exclude" ".Trash"
|
||||
"--exclude" "Applications"
|
||||
)
|
||||
fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null | while IFS= read -r item; do
|
||||
if is_safe_project_artifact "$item" "$search_path"; then
|
||||
echo "$item"
|
||||
fi
|
||||
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
|
||||
else
|
||||
# Pruned find avoids descending into heavy directories.
|
||||
local prune_args=()
|
||||
local prune_dirs=(".git" "Library" ".Trash" "Applications")
|
||||
for dir in "${prune_dirs[@]}"; do
|
||||
prune_args+=("-name" "$dir" "-prune" "-o")
|
||||
done
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
prune_args+=("-name" "$target" "-print" "-prune" "-o")
|
||||
done
|
||||
local find_expr=()
|
||||
for dir in "${prune_dirs[@]}"; do
|
||||
find_expr+=("-name" "$dir" "-prune" "-o")
|
||||
done
|
||||
local i=0
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
find_expr+=("-name" "$target" "-print" "-prune")
|
||||
if [[ $i -lt $((${#PURGE_TARGETS[@]} - 1)) ]]; then
|
||||
find_expr+=("-o")
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
command find "$search_path" -mindepth "$min_depth" -maxdepth "$max_depth" -type d \
|
||||
\( "${find_expr[@]}" \) 2> /dev/null | while IFS= read -r item; do
|
||||
if is_safe_project_artifact "$item" "$search_path"; then
|
||||
echo "$item"
|
||||
fi
|
||||
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
|
||||
fi
|
||||
}
|
||||
# Filter out nested artifacts (e.g. node_modules inside node_modules).
|
||||
filter_nested_artifacts() {
|
||||
while IFS= read -r item; do
|
||||
local parent_dir=$(dirname "$item")
|
||||
local is_nested=false
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
if [[ "$parent_dir" == *"/$target/"* || "$parent_dir" == *"/$target" ]]; then
|
||||
is_nested=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$is_nested" == "false" ]]; then
|
||||
echo "$item"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
filter_protected_artifacts() {
|
||||
while IFS= read -r item; do
|
||||
if ! is_protected_purge_artifact "$item"; then
|
||||
echo "$item"
|
||||
fi
|
||||
done
|
||||
}
|
||||
# Args: $1 - path
|
||||
# Check if a path was modified recently (safety check).
|
||||
is_recently_modified() {
|
||||
local path="$1"
|
||||
local age_days=$MIN_AGE_DAYS
|
||||
if [[ ! -e "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local mod_time
|
||||
mod_time=$(get_file_mtime "$path")
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
local age_seconds=$((current_time - mod_time))
|
||||
local age_in_days=$((age_seconds / 86400))
|
||||
if [[ $age_in_days -lt $age_days ]]; then
|
||||
return 0 # Recently modified
|
||||
else
|
||||
return 1 # Old enough to clean
|
||||
fi
|
||||
}
|
||||
# Args: $1 - path
|
||||
# Get directory size in KB.
|
||||
get_dir_size_kb() {
|
||||
local path="$1"
|
||||
if [[ -d "$path" ]]; then
|
||||
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
# Purge category selector.
|
||||
select_purge_categories() {
|
||||
local -a categories=("$@")
|
||||
local total_items=${#categories[@]}
|
||||
local clear_line=$'\r\033[2K'
|
||||
if [[ $total_items -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Calculate items per page based on terminal height.
|
||||
_get_items_per_page() {
|
||||
local term_height=24
|
||||
if [[ -t 0 ]] || [[ -t 2 ]]; then
|
||||
term_height=$(stty size < /dev/tty 2> /dev/null | awk '{print $1}')
|
||||
fi
|
||||
if [[ -z "$term_height" || $term_height -le 0 ]]; then
|
||||
if command -v tput > /dev/null 2>&1; then
|
||||
term_height=$(tput lines 2> /dev/null || echo "24")
|
||||
else
|
||||
term_height=24
|
||||
fi
|
||||
fi
|
||||
local reserved=6
|
||||
local available=$((term_height - reserved))
|
||||
if [[ $available -lt 3 ]]; then
|
||||
echo 3
|
||||
elif [[ $available -gt 50 ]]; then
|
||||
echo 50
|
||||
else
|
||||
echo "$available"
|
||||
fi
|
||||
}
|
||||
|
||||
local items_per_page=$(_get_items_per_page)
|
||||
local cursor_pos=0
|
||||
local top_index=0
|
||||
|
||||
# Initialize selection (all selected by default, except recent ones)
|
||||
local -a selected=()
|
||||
IFS=',' read -r -a recent_flags <<< "${PURGE_RECENT_CATEGORIES:-}"
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
# Default unselected if category has recent items
|
||||
if [[ ${recent_flags[i]:-false} == "true" ]]; then
|
||||
selected[i]=false
|
||||
else
|
||||
selected[i]=true
|
||||
fi
|
||||
done
|
||||
local original_stty=""
|
||||
if [[ -t 0 ]] && command -v stty > /dev/null 2>&1; then
|
||||
original_stty=$(stty -g 2> /dev/null || echo "")
|
||||
fi
|
||||
# Terminal control functions
|
||||
restore_terminal() {
|
||||
trap - EXIT INT TERM
|
||||
show_cursor
|
||||
if [[ -n "${original_stty:-}" ]]; then
|
||||
stty "${original_stty}" 2> /dev/null || stty sane 2> /dev/null || true
|
||||
fi
|
||||
}
|
||||
# shellcheck disable=SC2329
|
||||
handle_interrupt() {
|
||||
restore_terminal
|
||||
exit 130
|
||||
}
|
||||
draw_menu() {
|
||||
# Recalculate items_per_page dynamically to handle window resize
|
||||
items_per_page=$(_get_items_per_page)
|
||||
|
||||
# Clamp pagination state to avoid cursor drifting out of view
|
||||
local max_top_index=0
|
||||
if [[ $total_items -gt $items_per_page ]]; then
|
||||
max_top_index=$((total_items - items_per_page))
|
||||
fi
|
||||
if [[ $top_index -gt $max_top_index ]]; then
|
||||
top_index=$max_top_index
|
||||
fi
|
||||
if [[ $top_index -lt 0 ]]; then
|
||||
top_index=0
|
||||
fi
|
||||
|
||||
local visible_count=$((total_items - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
if [[ $cursor_pos -gt $((visible_count - 1)) ]]; then
|
||||
cursor_pos=$((visible_count - 1))
|
||||
fi
|
||||
if [[ $cursor_pos -lt 0 ]]; then
|
||||
cursor_pos=0
|
||||
fi
|
||||
|
||||
printf "\033[H"
|
||||
# Calculate total size of selected items for header
|
||||
local selected_size=0
|
||||
local selected_count=0
|
||||
IFS=',' read -r -a sizes <<< "${PURGE_CATEGORY_SIZES:-}"
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
selected_size=$((selected_size + ${sizes[i]:-0}))
|
||||
((selected_count++))
|
||||
fi
|
||||
done
|
||||
local selected_gb
|
||||
selected_gb=$(printf "%.1f" "$(echo "scale=2; $selected_size/1024/1024" | bc)")
|
||||
|
||||
# Show position indicator if scrolling is needed
|
||||
local scroll_indicator=""
|
||||
if [[ $total_items -gt $items_per_page ]]; then
|
||||
local current_pos=$((top_index + cursor_pos + 1))
|
||||
scroll_indicator=" ${GRAY}[${current_pos}/${total_items}]${NC}"
|
||||
fi
|
||||
|
||||
printf "%s\n" "$clear_line"
|
||||
printf "%s${PURPLE_BOLD}Select Categories to Clean${NC}%s ${GRAY}- ${selected_gb}GB ($selected_count selected)${NC}\n" "$clear_line" "$scroll_indicator"
|
||||
printf "%s\n" "$clear_line"
|
||||
|
||||
IFS=',' read -r -a recent_flags <<< "${PURGE_RECENT_CATEGORIES:-}"
|
||||
|
||||
# Calculate visible range
|
||||
local end_index=$((top_index + visible_count))
|
||||
|
||||
# Draw only visible items
|
||||
for ((i = top_index; i < end_index; i++)); do
|
||||
local checkbox="$ICON_EMPTY"
|
||||
[[ ${selected[i]} == true ]] && checkbox="$ICON_SOLID"
|
||||
local recent_marker=""
|
||||
[[ ${recent_flags[i]:-false} == "true" ]] && recent_marker=" ${GRAY}| Recent${NC}"
|
||||
local rel_pos=$((i - top_index))
|
||||
if [[ $rel_pos -eq $cursor_pos ]]; then
|
||||
printf "%s${CYAN}${ICON_ARROW} %s %s%s${NC}\n" "$clear_line" "$checkbox" "${categories[i]}" "$recent_marker"
|
||||
else
|
||||
printf "%s %s %s%s\n" "$clear_line" "$checkbox" "${categories[i]}" "$recent_marker"
|
||||
fi
|
||||
done
|
||||
|
||||
# Fill empty slots to clear previous content
|
||||
local items_shown=$visible_count
|
||||
for ((i = items_shown; i < items_per_page; i++)); do
|
||||
printf "%s\n" "$clear_line"
|
||||
done
|
||||
|
||||
printf "%s\n" "$clear_line"
|
||||
|
||||
printf "%s${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN} | Space Select | Enter Confirm | A All | I Invert | Q Quit${NC}\n" "$clear_line"
|
||||
}
|
||||
trap restore_terminal EXIT
|
||||
trap handle_interrupt INT TERM
|
||||
# Preserve interrupt character for Ctrl-C
|
||||
stty -echo -icanon intr ^C 2> /dev/null || true
|
||||
hide_cursor
|
||||
if [[ -t 1 ]]; then
|
||||
clear_screen
|
||||
fi
|
||||
# Main loop
|
||||
while true; do
|
||||
draw_menu
|
||||
# Read key
|
||||
IFS= read -r -s -n1 key || key=""
|
||||
case "$key" in
|
||||
$'\x1b')
|
||||
# Arrow keys or ESC
|
||||
# Read next 2 chars with timeout (bash 3.2 needs integer)
|
||||
IFS= read -r -s -n1 -t 1 key2 || key2=""
|
||||
if [[ "$key2" == "[" ]]; then
|
||||
IFS= read -r -s -n1 -t 1 key3 || key3=""
|
||||
case "$key3" in
|
||||
A) # Up arrow
|
||||
if [[ $cursor_pos -gt 0 ]]; then
|
||||
((cursor_pos--))
|
||||
elif [[ $top_index -gt 0 ]]; then
|
||||
((top_index--))
|
||||
fi
|
||||
;;
|
||||
B) # Down arrow
|
||||
local absolute_index=$((top_index + cursor_pos))
|
||||
local last_index=$((total_items - 1))
|
||||
if [[ $absolute_index -lt $last_index ]]; then
|
||||
local visible_count=$((total_items - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then
|
||||
((cursor_pos++))
|
||||
elif [[ $((top_index + visible_count)) -lt $total_items ]]; then
|
||||
((top_index++))
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# ESC alone (no following chars)
|
||||
restore_terminal
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
" ") # Space - toggle current item
|
||||
local idx=$((top_index + cursor_pos))
|
||||
if [[ ${selected[idx]} == true ]]; then
|
||||
selected[idx]=false
|
||||
else
|
||||
selected[idx]=true
|
||||
fi
|
||||
;;
|
||||
"a" | "A") # Select all
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
selected[i]=true
|
||||
done
|
||||
;;
|
||||
"i" | "I") # Invert selection
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
selected[i]=false
|
||||
else
|
||||
selected[i]=true
|
||||
fi
|
||||
done
|
||||
;;
|
||||
"q" | "Q" | $'\x03') # Quit or Ctrl-C
|
||||
restore_terminal
|
||||
return 1
|
||||
;;
|
||||
"" | $'\n' | $'\r') # Enter - confirm
|
||||
# Build result
|
||||
PURGE_SELECTION_RESULT=""
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
[[ -n "$PURGE_SELECTION_RESULT" ]] && PURGE_SELECTION_RESULT+=","
|
||||
PURGE_SELECTION_RESULT+="$i"
|
||||
fi
|
||||
done
|
||||
restore_terminal
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
# Main cleanup function - scans and prompts user to select artifacts to clean
|
||||
clean_project_artifacts() {
|
||||
local -a all_found_items=()
|
||||
local -a safe_to_clean=()
|
||||
local -a recently_modified=()
|
||||
# Set up cleanup on interrupt
|
||||
# Note: Declared without 'local' so cleanup_scan trap can access them
|
||||
scan_pids=()
|
||||
scan_temps=()
|
||||
# shellcheck disable=SC2329
|
||||
cleanup_scan() {
|
||||
# Kill all background scans
|
||||
for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do
|
||||
kill "$pid" 2> /dev/null || true
|
||||
done
|
||||
# Clean up temp files
|
||||
for temp in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
||||
rm -f "$temp" 2> /dev/null || true
|
||||
done
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
echo ""
|
||||
exit 130
|
||||
}
|
||||
trap cleanup_scan INT TERM
|
||||
# Start parallel scanning of all paths at once
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Scanning projects..."
|
||||
fi
|
||||
# Launch all scans in parallel
|
||||
for path in "${PURGE_SEARCH_PATHS[@]}"; do
|
||||
if [[ -d "$path" ]]; then
|
||||
local scan_output
|
||||
scan_output=$(mktemp)
|
||||
scan_temps+=("$scan_output")
|
||||
# Launch scan in background for true parallelism
|
||||
scan_purge_targets "$path" "$scan_output" &
|
||||
local scan_pid=$!
|
||||
scan_pids+=("$scan_pid")
|
||||
fi
|
||||
done
|
||||
# Wait for all scans to complete
|
||||
for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do
|
||||
wait "$pid" 2> /dev/null || true
|
||||
done
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
# Collect all results
|
||||
for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
||||
if [[ -f "$scan_output" ]]; then
|
||||
while IFS= read -r item; do
|
||||
if [[ -n "$item" ]]; then
|
||||
all_found_items+=("$item")
|
||||
fi
|
||||
done < "$scan_output"
|
||||
rm -f "$scan_output"
|
||||
fi
|
||||
done
|
||||
# Clean up trap
|
||||
trap - INT TERM
|
||||
if [[ ${#all_found_items[@]} -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Great! No old project artifacts to clean"
|
||||
printf '\n'
|
||||
return 2 # Special code: nothing to clean
|
||||
fi
|
||||
# Mark recently modified items (for default selection state)
|
||||
for item in "${all_found_items[@]}"; do
|
||||
if is_recently_modified "$item"; then
|
||||
recently_modified+=("$item")
|
||||
fi
|
||||
# Add all items to safe_to_clean, let user choose
|
||||
safe_to_clean+=("$item")
|
||||
done
|
||||
# Build menu options - one per artifact
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Calculating sizes..."
|
||||
fi
|
||||
local -a menu_options=()
|
||||
local -a item_paths=()
|
||||
local -a item_sizes=()
|
||||
local -a item_recent_flags=()
|
||||
# Helper to get project name from path
|
||||
# For ~/www/pake/src-tauri/target -> returns "pake"
|
||||
# For ~/work/code/MyProject/node_modules -> returns "MyProject"
|
||||
# Strategy: Find the nearest ancestor directory containing a project indicator file
|
||||
get_project_name() {
|
||||
local path="$1"
|
||||
local artifact_name
|
||||
artifact_name=$(basename "$path")
|
||||
|
||||
# Start from the parent of the artifact and walk up
|
||||
local current_dir
|
||||
current_dir=$(dirname "$path")
|
||||
|
||||
while [[ "$current_dir" != "/" && "$current_dir" != "$HOME" && -n "$current_dir" ]]; do
|
||||
# Check if current directory contains any project indicator
|
||||
for indicator in "${PROJECT_INDICATORS[@]}"; do
|
||||
if [[ -e "$current_dir/$indicator" ]]; then
|
||||
# Found a project root, return its name
|
||||
basename "$current_dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Move up one level
|
||||
current_dir=$(dirname "$current_dir")
|
||||
done
|
||||
|
||||
# Fallback: try the old logic (first directory under search root)
|
||||
local search_roots=()
|
||||
if [[ ${#PURGE_SEARCH_PATHS[@]} -gt 0 ]]; then
|
||||
search_roots=("${PURGE_SEARCH_PATHS[@]}")
|
||||
else
|
||||
search_roots=("$HOME/www" "$HOME/dev" "$HOME/Projects")
|
||||
fi
|
||||
for root in "${search_roots[@]}"; do
|
||||
root="${root%/}"
|
||||
if [[ -n "$root" && "$path" == "$root/"* ]]; then
|
||||
local relative_path="${path#"$root"/}"
|
||||
echo "$relative_path" | cut -d'/' -f1
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Final fallback: use grandparent directory
|
||||
dirname "$(dirname "$path")" | xargs basename
|
||||
}
|
||||
# Format display with alignment (like app_selector)
|
||||
format_purge_display() {
|
||||
local project_name="$1"
|
||||
local artifact_type="$2"
|
||||
local size_str="$3"
|
||||
# Terminal width for alignment
|
||||
local terminal_width=$(tput cols 2> /dev/null || echo 80)
|
||||
local fixed_width=28 # Reserve for type and size
|
||||
local available_width=$((terminal_width - fixed_width))
|
||||
# Bounds: 24-35 chars for project name
|
||||
[[ $available_width -lt 24 ]] && available_width=24
|
||||
[[ $available_width -gt 35 ]] && available_width=35
|
||||
# Truncate project name if needed
|
||||
local truncated_name=$(truncate_by_display_width "$project_name" "$available_width")
|
||||
local current_width=$(get_display_width "$truncated_name")
|
||||
local char_count=${#truncated_name}
|
||||
local padding=$((available_width - current_width))
|
||||
local printf_width=$((char_count + padding))
|
||||
# Format: "project_name size | artifact_type"
|
||||
printf "%-*s %9s | %-13s" "$printf_width" "$truncated_name" "$size_str" "$artifact_type"
|
||||
}
|
||||
# Build menu options - one line per artifact
|
||||
for item in "${safe_to_clean[@]}"; do
|
||||
local project_name=$(get_project_name "$item")
|
||||
local artifact_type=$(basename "$item")
|
||||
local size_kb=$(get_dir_size_kb "$item")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
# Check if recent
|
||||
local is_recent=false
|
||||
for recent_item in "${recently_modified[@]+"${recently_modified[@]}"}"; do
|
||||
if [[ "$item" == "$recent_item" ]]; then
|
||||
is_recent=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
menu_options+=("$(format_purge_display "$project_name" "$artifact_type" "$size_human")")
|
||||
item_paths+=("$item")
|
||||
item_sizes+=("$size_kb")
|
||||
item_recent_flags+=("$is_recent")
|
||||
done
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
# Set global vars for selector
|
||||
export PURGE_CATEGORY_SIZES=$(
|
||||
IFS=,
|
||||
echo "${item_sizes[*]}"
|
||||
)
|
||||
export PURGE_RECENT_CATEGORIES=$(
|
||||
IFS=,
|
||||
echo "${item_recent_flags[*]}"
|
||||
)
|
||||
# Interactive selection (only if terminal is available)
|
||||
PURGE_SELECTION_RESULT=""
|
||||
if [[ -t 0 ]]; then
|
||||
if ! select_purge_categories "${menu_options[@]}"; then
|
||||
unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Non-interactive: select all non-recent items
|
||||
for ((i = 0; i < ${#menu_options[@]}; i++)); do
|
||||
if [[ ${item_recent_flags[i]} != "true" ]]; then
|
||||
[[ -n "$PURGE_SELECTION_RESULT" ]] && PURGE_SELECTION_RESULT+=","
|
||||
PURGE_SELECTION_RESULT+="$i"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -z "$PURGE_SELECTION_RESULT" ]]; then
|
||||
echo ""
|
||||
echo -e "${GRAY}No items selected${NC}"
|
||||
printf '\n'
|
||||
unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT
|
||||
return 0
|
||||
fi
|
||||
# Clean selected items
|
||||
echo ""
|
||||
IFS=',' read -r -a selected_indices <<< "$PURGE_SELECTION_RESULT"
|
||||
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
||||
local cleaned_count=0
|
||||
for idx in "${selected_indices[@]}"; do
|
||||
local item_path="${item_paths[idx]}"
|
||||
local artifact_type=$(basename "$item_path")
|
||||
local project_name=$(get_project_name "$item_path")
|
||||
local size_kb="${item_sizes[idx]}"
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
# Safety checks
|
||||
if [[ -z "$item_path" || "$item_path" == "/" || "$item_path" == "$HOME" || "$item_path" != "$HOME/"* ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Cleaning $project_name/$artifact_type..."
|
||||
fi
|
||||
if [[ -e "$item_path" ]]; then
|
||||
safe_remove "$item_path" true
|
||||
if [[ ! -e "$item_path" ]]; then
|
||||
local current_total=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
||||
echo "$((current_total + size_kb))" > "$stats_dir/purge_stats"
|
||||
((cleaned_count++))
|
||||
fi
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_name - $artifact_type ${GREEN}($size_human)${NC}"
|
||||
fi
|
||||
done
|
||||
# Update count
|
||||
echo "$cleaned_count" > "$stats_dir/purge_count"
|
||||
unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT
|
||||
}
|
||||
423
lib/clean/system.ps1
Normal file
423
lib/clean/system.ps1
Normal file
@@ -0,0 +1,423 @@
|
||||
# Mole - System Cleanup Module
|
||||
# Cleans Windows system files that require administrator access
|
||||
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Prevent multiple sourcing
|
||||
if ((Get-Variable -Name 'MOLE_CLEAN_SYSTEM_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_SYSTEM_LOADED) { return }
|
||||
$script:MOLE_CLEAN_SYSTEM_LOADED = $true
|
||||
|
||||
# Import dependencies
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir\..\core\base.ps1"
|
||||
. "$scriptDir\..\core\log.ps1"
|
||||
. "$scriptDir\..\core\file_ops.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# System Temp Files
|
||||
# ============================================================================
|
||||
|
||||
function Clear-SystemTempFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean system-level temporary files (requires admin)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping system temp cleanup - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
# Windows Temp folder
|
||||
$winTemp = "$env:WINDIR\Temp"
|
||||
if (Test-Path $winTemp) {
|
||||
Remove-OldFiles -Path $winTemp -DaysOld 7 -Description "Windows temp files"
|
||||
}
|
||||
|
||||
# System temp (different from Windows temp)
|
||||
$systemTemp = "$env:SYSTEMROOT\Temp"
|
||||
if ((Test-Path $systemTemp) -and ($systemTemp -ne $winTemp)) {
|
||||
Remove-OldFiles -Path $systemTemp -DaysOld 7 -Description "System temp files"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Windows Logs
|
||||
# ============================================================================
|
||||
|
||||
function Clear-WindowsLogs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows log files (requires admin)
|
||||
#>
|
||||
param([int]$DaysOld = 7)
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Windows logs cleanup - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
# Windows Logs directory
|
||||
$logPaths = @(
|
||||
"$env:WINDIR\Logs\CBS"
|
||||
"$env:WINDIR\Logs\DISM"
|
||||
"$env:WINDIR\Logs\DPX"
|
||||
"$env:WINDIR\Logs\WindowsUpdate"
|
||||
"$env:WINDIR\Logs\SIH"
|
||||
"$env:WINDIR\Logs\waasmedia"
|
||||
"$env:WINDIR\Debug"
|
||||
"$env:WINDIR\Panther"
|
||||
"$env:PROGRAMDATA\Microsoft\Windows\WER\ReportQueue"
|
||||
"$env:PROGRAMDATA\Microsoft\Windows\WER\ReportArchive"
|
||||
)
|
||||
|
||||
foreach ($path in $logPaths) {
|
||||
if (Test-Path $path) {
|
||||
Remove-OldFiles -Path $path -DaysOld $DaysOld -Description "$(Split-Path -Leaf $path) logs"
|
||||
}
|
||||
}
|
||||
|
||||
# Setup logs (*.log files in Windows directory)
|
||||
$setupLogs = Get-ChildItem -Path "$env:WINDIR\*.log" -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) }
|
||||
if ($setupLogs) {
|
||||
$paths = $setupLogs | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Windows setup logs"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Windows Update Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-WindowsUpdateFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Update download cache (requires admin)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Windows Update cleanup - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
# Stop Windows Update service
|
||||
$wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
|
||||
$wasRunning = $wuService.Status -eq 'Running'
|
||||
|
||||
if ($wasRunning) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Windows Update cache (service would be restarted)"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Stop-Service -Name wuauserv -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not stop Windows Update service: $_"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
# Clean download cache
|
||||
$wuDownloadPath = "$env:WINDIR\SoftwareDistribution\Download"
|
||||
if (Test-Path $wuDownloadPath) {
|
||||
Clear-DirectoryContents -Path $wuDownloadPath -Description "Windows Update download cache"
|
||||
}
|
||||
|
||||
# Clean DataStore (old update history - be careful!)
|
||||
# Only clean temp files, not the actual database
|
||||
$wuDataStore = "$env:WINDIR\SoftwareDistribution\DataStore\Logs"
|
||||
if (Test-Path $wuDataStore) {
|
||||
Clear-DirectoryContents -Path $wuDataStore -Description "Windows Update logs"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
# Always restart service if it was running, even if cleanup failed
|
||||
if ($wasRunning) {
|
||||
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Installer Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-InstallerCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Installer cache (orphaned patches)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Windows Installer patch cache
|
||||
# WARNING: Be very careful here - only clean truly orphaned files
|
||||
$installerPath = "$env:WINDIR\Installer"
|
||||
|
||||
# Only clean .tmp files and very old .msp files that are likely orphaned
|
||||
if (Test-Path $installerPath) {
|
||||
$tmpFiles = Get-ChildItem -Path $installerPath -Filter "*.tmp" -File -ErrorAction SilentlyContinue
|
||||
if ($tmpFiles) {
|
||||
$paths = $tmpFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Installer temp files"
|
||||
}
|
||||
}
|
||||
|
||||
# Installer logs in temp
|
||||
$installerLogs = Get-ChildItem -Path $env:TEMP -Filter "MSI*.LOG" -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) }
|
||||
if ($installerLogs) {
|
||||
$paths = $installerLogs | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Old MSI logs"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Component Store Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-ComponentStoreCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run Windows Component Store cleanup (DISM)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping component store cleanup - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Component Store cleanup (DISM)"
|
||||
Set-SectionActivity
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Info "Running Component Store cleanup (this may take a while)..."
|
||||
|
||||
# Run DISM cleanup
|
||||
$result = Start-Process -FilePath "dism.exe" `
|
||||
-ArgumentList "/Online", "/Cleanup-Image", "/StartComponentCleanup" `
|
||||
-Wait -PassThru -NoNewWindow -ErrorAction Stop
|
||||
|
||||
if ($result.ExitCode -eq 0) {
|
||||
Write-Success "Component Store cleanup"
|
||||
Set-SectionActivity
|
||||
}
|
||||
else {
|
||||
Write-Debug "DISM returned exit code: $($result.ExitCode)"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Component Store cleanup failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Memory Dump Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-MemoryDumps {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows memory dumps
|
||||
#>
|
||||
|
||||
$dumpPaths = @(
|
||||
"$env:WINDIR\MEMORY.DMP"
|
||||
"$env:WINDIR\Minidump"
|
||||
"$env:LOCALAPPDATA\CrashDumps"
|
||||
)
|
||||
|
||||
foreach ($path in $dumpPaths) {
|
||||
if (Test-Path $path -PathType Leaf) {
|
||||
# Single file (MEMORY.DMP)
|
||||
Remove-SafeItem -Path $path -Description "Memory dump"
|
||||
}
|
||||
elseif (Test-Path $path -PathType Container) {
|
||||
# Directory (Minidump, CrashDumps)
|
||||
Clear-DirectoryContents -Path $path -Description "$(Split-Path -Leaf $path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Font Cache
|
||||
# ============================================================================
|
||||
|
||||
function Clear-SystemFontCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clear Windows font cache (requires admin and may need restart)
|
||||
#>
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
return
|
||||
}
|
||||
|
||||
$fontCacheService = Get-Service -Name "FontCache" -ErrorAction SilentlyContinue
|
||||
|
||||
if ($fontCacheService) {
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "System font cache"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Stop font cache service
|
||||
Stop-Service -Name "FontCache" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Clear font cache files
|
||||
$fontCachePath = "$env:WINDIR\ServiceProfiles\LocalService\AppData\Local\FontCache"
|
||||
if (Test-Path $fontCachePath) {
|
||||
Clear-DirectoryContents -Path $fontCachePath -Description "System font cache"
|
||||
}
|
||||
|
||||
# Restart font cache service
|
||||
Start-Service -Name "FontCache" -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Font cache cleanup failed: $_"
|
||||
Start-Service -Name "FontCache" -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Disk Cleanup Tool Integration
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-DiskCleanupTool {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run Windows built-in Disk Cleanup tool with predefined settings
|
||||
#>
|
||||
param([switch]$Full)
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Disk Cleanup tool - requires admin for full cleanup"
|
||||
}
|
||||
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Windows Disk Cleanup tool"
|
||||
return
|
||||
}
|
||||
|
||||
# Set up registry keys for automated cleanup
|
||||
$cleanupKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
|
||||
|
||||
$cleanupItems = @(
|
||||
"Active Setup Temp Folders"
|
||||
"Downloaded Program Files"
|
||||
"Internet Cache Files"
|
||||
"Old ChkDsk Files"
|
||||
"Recycle Bin"
|
||||
"Setup Log Files"
|
||||
"System error memory dump files"
|
||||
"System error minidump files"
|
||||
"Temporary Files"
|
||||
"Temporary Setup Files"
|
||||
"Thumbnail Cache"
|
||||
"Windows Error Reporting Archive Files"
|
||||
"Windows Error Reporting Queue Files"
|
||||
"Windows Error Reporting System Archive Files"
|
||||
"Windows Error Reporting System Queue Files"
|
||||
)
|
||||
|
||||
if ($Full -and (Test-IsAdmin)) {
|
||||
$cleanupItems += @(
|
||||
"Previous Installations"
|
||||
"Temporary Windows installation files"
|
||||
"Update Cleanup"
|
||||
"Windows Defender"
|
||||
"Windows Upgrade Log Files"
|
||||
)
|
||||
}
|
||||
|
||||
# Enable cleanup items in registry
|
||||
foreach ($item in $cleanupItems) {
|
||||
$itemPath = Join-Path $cleanupKey $item
|
||||
if (Test-Path $itemPath) {
|
||||
Set-ItemProperty -Path $itemPath -Name "StateFlags0100" -Value 2 -Type DWord -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
# Run disk cleanup
|
||||
$process = Start-Process -FilePath "cleanmgr.exe" `
|
||||
-ArgumentList "/sagerun:100" `
|
||||
-Wait -PassThru -NoNewWindow -ErrorAction Stop
|
||||
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Success "Windows Disk Cleanup"
|
||||
Set-SectionActivity
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Disk Cleanup failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main System Cleanup Function
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-SystemCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run all system-level cleanup tasks (requires admin for full effect)
|
||||
#>
|
||||
param(
|
||||
[switch]$IncludeComponentStore,
|
||||
[switch]$IncludeDiskCleanup
|
||||
)
|
||||
|
||||
Start-Section "System cleanup"
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-MoleWarning "Running without admin - some cleanup tasks will be skipped"
|
||||
}
|
||||
|
||||
# System temp files
|
||||
Clear-SystemTempFiles
|
||||
|
||||
# Windows logs
|
||||
Clear-WindowsLogs -DaysOld 7
|
||||
|
||||
# Windows Update cache
|
||||
Clear-WindowsUpdateFiles
|
||||
|
||||
# Installer cache
|
||||
Clear-InstallerCache
|
||||
|
||||
# Memory dumps
|
||||
Clear-MemoryDumps
|
||||
|
||||
# Font cache
|
||||
Clear-SystemFontCache
|
||||
|
||||
# Optional: Component Store (can take a long time)
|
||||
if ($IncludeComponentStore) {
|
||||
Invoke-ComponentStoreCleanup
|
||||
}
|
||||
|
||||
# Optional: Windows Disk Cleanup tool
|
||||
if ($IncludeDiskCleanup) {
|
||||
Invoke-DiskCleanupTool -Full
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
# Functions: Clear-SystemTempFiles, Clear-WindowsLogs, Invoke-SystemCleanup, etc.
|
||||
@@ -1,339 +0,0 @@
|
||||
#!/bin/bash
|
||||
# System-Level Cleanup Module (requires sudo).
|
||||
set -euo pipefail
|
||||
# System caches, logs, and temp files.
|
||||
clean_deep_system() {
|
||||
stop_section_spinner
|
||||
local cache_cleaned=0
|
||||
safe_sudo_find_delete "/Library/Caches" "*.cache" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
safe_sudo_find_delete "/Library/Caches" "*.tmp" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
safe_sudo_find_delete "/Library/Caches" "*.log" "$MOLE_LOG_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
[[ $cache_cleaned -eq 1 ]] && log_success "System caches"
|
||||
local tmp_cleaned=0
|
||||
safe_sudo_find_delete "/private/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
safe_sudo_find_delete "/private/var/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
[[ $tmp_cleaned -eq 1 ]] && log_success "System temp files"
|
||||
safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*" "$MOLE_CRASH_REPORT_AGE_DAYS" "f" || true
|
||||
log_success "System crash reports"
|
||||
safe_sudo_find_delete "/private/var/log" "*.log" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/log" "*.gz" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "System logs"
|
||||
if [[ -d "/Library/Updates" && ! -L "/Library/Updates" ]]; then
|
||||
if ! is_sip_enabled; then
|
||||
local updates_cleaned=0
|
||||
while IFS= read -r -d '' item; do
|
||||
if [[ -z "$item" ]] || [[ ! "$item" =~ ^/Library/Updates/[^/]+$ ]]; then
|
||||
debug_log "Skipping malformed path: $item"
|
||||
continue
|
||||
fi
|
||||
local item_flags
|
||||
item_flags=$($STAT_BSD -f%Sf "$item" 2> /dev/null || echo "")
|
||||
if [[ "$item_flags" == *"restricted"* ]]; then
|
||||
continue
|
||||
fi
|
||||
if safe_sudo_remove "$item"; then
|
||||
((updates_cleaned++))
|
||||
fi
|
||||
done < <(find /Library/Updates -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
[[ $updates_cleaned -gt 0 ]] && log_success "System library updates"
|
||||
fi
|
||||
fi
|
||||
if [[ -d "/macOS Install Data" ]]; then
|
||||
local mtime=$(get_file_mtime "/macOS Install Data")
|
||||
local age_days=$((($(get_epoch_seconds) - mtime) / 86400))
|
||||
debug_log "Found macOS Install Data (age: ${age_days} days)"
|
||||
if [[ $age_days -ge 30 ]]; then
|
||||
local size_kb=$(get_path_size_kb "/macOS Install Data")
|
||||
if [[ -n "$size_kb" && "$size_kb" -gt 0 ]]; then
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
debug_log "Cleaning macOS Install Data: $size_human (${age_days} days old)"
|
||||
if safe_sudo_remove "/macOS Install Data"; then
|
||||
log_success "macOS Install Data ($size_human)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
debug_log "Keeping macOS Install Data (only ${age_days} days old, needs 30+)"
|
||||
fi
|
||||
fi
|
||||
start_section_spinner "Scanning system caches..."
|
||||
local code_sign_cleaned=0
|
||||
local found_count=0
|
||||
local last_update_time
|
||||
last_update_time=$(get_epoch_seconds)
|
||||
local update_interval=2
|
||||
while IFS= read -r -d '' cache_dir; do
|
||||
if safe_remove "$cache_dir" true; then
|
||||
((code_sign_cleaned++))
|
||||
fi
|
||||
((found_count++))
|
||||
|
||||
# Optimize: only check time every 50 files
|
||||
if ((found_count % 50 == 0)); then
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
if [[ $((current_time - last_update_time)) -ge $update_interval ]]; then
|
||||
start_section_spinner "Scanning system caches... ($found_count found)"
|
||||
last_update_time=$current_time
|
||||
fi
|
||||
fi
|
||||
done < <(run_with_timeout 5 command find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
|
||||
stop_section_spinner
|
||||
[[ $code_sign_cleaned -gt 0 ]] && log_success "Browser code signature caches ($code_sign_cleaned items)"
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/db/DiagnosticPipeline" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "System diagnostic logs"
|
||||
safe_sudo_find_delete "/private/var/db/powerlog" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "Power logs"
|
||||
safe_sudo_find_delete "/private/var/db/reportmemoryexception/MemoryLimitViolations" "*" "30" "f" || true
|
||||
log_success "Memory exception reports"
|
||||
start_section_spinner "Cleaning diagnostic trace logs..."
|
||||
local diag_logs_cleaned=0
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*.tracev3" "30" "f" && diag_logs_cleaned=1 || true
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*.tracev3" "30" "f" && diag_logs_cleaned=1 || true
|
||||
stop_section_spinner
|
||||
[[ $diag_logs_cleaned -eq 1 ]] && log_success "System diagnostic trace logs"
|
||||
}
|
||||
# Incomplete Time Machine backups.
|
||||
clean_time_machine_failed_backups() {
|
||||
local tm_cleaned=0
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Checking Time Machine configuration..."
|
||||
local spinner_active=true
|
||||
local tm_info
|
||||
tm_info=$(run_with_timeout 2 tmutil destinationinfo 2>&1 || echo "failed")
|
||||
if [[ "$tm_info" == *"No destinations configured"* || "$tm_info" == "failed" ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -d "/Volumes" ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
if tmutil status 2> /dev/null | grep -q "Running = 1"; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
echo -e " ${YELLOW}!${NC} Time Machine backup in progress, skipping cleanup"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Checking backup volumes..."
|
||||
fi
|
||||
# Fast pre-scan for backup volumes to avoid slow tmutil checks.
|
||||
local -a backup_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" ]] || continue
|
||||
[[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue
|
||||
[[ -L "$volume" ]] && continue
|
||||
if [[ -d "$volume/Backups.backupdb" ]] || [[ -d "$volume/.MobileBackups" ]]; then
|
||||
backup_volumes+=("$volume")
|
||||
fi
|
||||
done
|
||||
if [[ ${#backup_volumes[@]} -eq 0 ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Scanning backup volumes..."
|
||||
fi
|
||||
for volume in "${backup_volumes[@]}"; do
|
||||
local fs_type
|
||||
fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "unknown")
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
|
||||
esac
|
||||
local backupdb_dir="$volume/Backups.backupdb"
|
||||
if [[ -d "$backupdb_dir" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
# Only delete old incomplete backups (safety window).
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
if [[ $hours_old -lt $MOLE_TM_BACKUP_SAFE_HOURS ]]; then
|
||||
continue
|
||||
fi
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
fi
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete backup: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}!${NC} tmutil not available, skipping: $backup_name"
|
||||
continue
|
||||
fi
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete backup: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
note_activity
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Could not delete: $backup_name · try manually with sudo"
|
||||
fi
|
||||
done < <(run_with_timeout 15 find "$backupdb_dir" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
# APFS bundles.
|
||||
for bundle in "$volume"/*.backupbundle "$volume"/*.sparsebundle; do
|
||||
[[ -e "$bundle" ]] || continue
|
||||
[[ -d "$bundle" ]] || continue
|
||||
local bundle_name=$(basename "$bundle")
|
||||
local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
|
||||
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
if [[ $hours_old -lt $MOLE_TM_BACKUP_SAFE_HOURS ]]; then
|
||||
continue
|
||||
fi
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
fi
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
note_activity
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Could not delete from bundle: $backup_name"
|
||||
fi
|
||||
done < <(run_with_timeout 15 find "$mounted_path" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
if [[ $tm_cleaned -eq 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
fi
|
||||
}
|
||||
# Local APFS snapshots (keep the most recent).
|
||||
clean_local_snapshots() {
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Checking local snapshots..."
|
||||
local snapshot_list
|
||||
snapshot_list=$(tmutil listlocalsnapshots / 2> /dev/null)
|
||||
stop_section_spinner
|
||||
[[ -z "$snapshot_list" ]] && return 0
|
||||
local cleaned_count=0
|
||||
local total_cleaned_size=0 # Estimation not possible without thin
|
||||
local newest_ts=0
|
||||
local newest_name=""
|
||||
local -a snapshots=()
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then
|
||||
local snap_name="${BASH_REMATCH[0]}"
|
||||
snapshots+=("$snap_name")
|
||||
local date_str="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]:0:2}:${BASH_REMATCH[4]:2:2}:${BASH_REMATCH[4]:4:2}"
|
||||
local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2> /dev/null || echo "0")
|
||||
[[ "$snap_ts" == "0" ]] && continue
|
||||
if [[ "$snap_ts" -gt "$newest_ts" ]]; then
|
||||
newest_ts="$snap_ts"
|
||||
newest_name="$snap_name"
|
||||
fi
|
||||
fi
|
||||
done <<< "$snapshot_list"
|
||||
|
||||
[[ ${#snapshots[@]} -eq 0 ]] && return 0
|
||||
[[ -z "$newest_name" ]] && return 0
|
||||
|
||||
local deletable_count=$((${#snapshots[@]} - 1))
|
||||
[[ $deletable_count -le 0 ]] && return 0
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo -e " ${YELLOW}!${NC} ${#snapshots[@]} local snapshot(s) found, skipping non-interactive mode"
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} ${GRAY}Tip: Snapshots may cause Disk Utility to show different 'Available' values${NC}"
|
||||
return 0
|
||||
fi
|
||||
echo -e " ${YELLOW}!${NC} Time Machine local snapshots found"
|
||||
echo -e " ${GRAY}macOS can recreate them if needed.${NC}"
|
||||
echo -e " ${GRAY}The most recent snapshot will be kept.${NC}"
|
||||
echo -ne " ${PURPLE}${ICON_ARROW}${NC} Remove all local snapshots except the most recent one? ${GREEN}Enter${NC} continue, ${GRAY}Space${NC} skip: "
|
||||
local choice
|
||||
if type read_key > /dev/null 2>&1; then
|
||||
choice=$(read_key)
|
||||
else
|
||||
IFS= read -r -s -n 1 choice || choice=""
|
||||
if [[ -z "$choice" || "$choice" == $'\n' || "$choice" == $'\r' ]]; then
|
||||
choice="ENTER"
|
||||
fi
|
||||
fi
|
||||
if [[ "$choice" == "ENTER" ]]; then
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
else
|
||||
echo -e " ${GRAY}Skipped${NC}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local snap_name
|
||||
for snap_name in "${snapshots[@]}"; do
|
||||
if [[ "$snap_name" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then
|
||||
if [[ "${BASH_REMATCH[0]}" != "$newest_name" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Local snapshot: $snap_name ${YELLOW}dry-run${NC}"
|
||||
((cleaned_count++))
|
||||
note_activity
|
||||
else
|
||||
if sudo tmutil deletelocalsnapshots "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}-${BASH_REMATCH[4]}" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed snapshot: $snap_name"
|
||||
((cleaned_count++))
|
||||
note_activity
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Failed to remove: $snap_name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ $cleaned_count -gt 0 && "$DRY_RUN" != "true" ]]; then
|
||||
log_success "Cleaned $cleaned_count local snapshots, kept latest"
|
||||
fi
|
||||
}
|
||||
352
lib/clean/user.ps1
Normal file
352
lib/clean/user.ps1
Normal file
@@ -0,0 +1,352 @@
|
||||
# Mole - User Cleanup Module
|
||||
# Cleans user-level temporary files, caches, and downloads
|
||||
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Prevent multiple sourcing
|
||||
if ((Get-Variable -Name 'MOLE_CLEAN_USER_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_USER_LOADED) { return }
|
||||
$script:MOLE_CLEAN_USER_LOADED = $true
|
||||
|
||||
# Import dependencies
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir\..\core\base.ps1"
|
||||
. "$scriptDir\..\core\log.ps1"
|
||||
. "$scriptDir\..\core\file_ops.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Windows Temp Files Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-UserTempFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean user temporary files
|
||||
#>
|
||||
param([int]$DaysOld = 7)
|
||||
|
||||
Start-Section "User temp files"
|
||||
|
||||
# User temp directory
|
||||
$userTemp = $env:TEMP
|
||||
if (Test-Path $userTemp) {
|
||||
Remove-OldFiles -Path $userTemp -DaysOld $DaysOld -Description "User temp files"
|
||||
}
|
||||
|
||||
# Windows Temp (if accessible)
|
||||
$winTemp = "$env:WINDIR\Temp"
|
||||
if ((Test-Path $winTemp) -and (Test-IsAdmin)) {
|
||||
Remove-OldFiles -Path $winTemp -DaysOld $DaysOld -Description "Windows temp files"
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Downloads Folder Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-OldDownloads {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean old files from Downloads folder (with user confirmation pattern)
|
||||
#>
|
||||
param([int]$DaysOld = 30)
|
||||
|
||||
$downloadsPath = [Environment]::GetFolderPath('UserProfile') + '\Downloads'
|
||||
|
||||
if (-not (Test-Path $downloadsPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Find old installers and archives
|
||||
$patterns = @('*.exe', '*.msi', '*.zip', '*.7z', '*.rar', '*.tar.gz', '*.iso')
|
||||
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
|
||||
|
||||
$oldFiles = @()
|
||||
foreach ($pattern in $patterns) {
|
||||
$files = Get-ChildItem -Path $downloadsPath -Filter $pattern -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt $cutoffDate }
|
||||
if ($files) {
|
||||
$oldFiles += $files
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldFiles.Count -gt 0) {
|
||||
$paths = $oldFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Old downloads (>${DaysOld}d)"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Recycle Bin Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-RecycleBin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Empty the Recycle Bin
|
||||
#>
|
||||
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Recycle Bin (would empty)"
|
||||
Set-SectionActivity
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Use Shell.Application COM object
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$recycleBin = $shell.Namespace(0xA) # Recycle Bin
|
||||
$items = $recycleBin.Items()
|
||||
|
||||
if ($items.Count -gt 0) {
|
||||
# Calculate size
|
||||
$totalSize = 0
|
||||
foreach ($item in $items) {
|
||||
$totalSize += $item.Size
|
||||
}
|
||||
|
||||
# Clear using Clear-RecycleBin cmdlet (Windows 10+)
|
||||
Clear-RecycleBin -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$sizeHuman = Format-ByteSize -Bytes $totalSize
|
||||
Write-Success "Recycle Bin $($script:Colors.Green)($sizeHuman)$($script:Colors.NC)"
|
||||
Set-SectionActivity
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not clear Recycle Bin: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Recent Files Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-RecentFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean old recent file shortcuts
|
||||
#>
|
||||
param([int]$DaysOld = 30)
|
||||
|
||||
$recentPath = "$env:APPDATA\Microsoft\Windows\Recent"
|
||||
|
||||
if (Test-Path $recentPath) {
|
||||
Remove-OldFiles -Path $recentPath -DaysOld $DaysOld -Filter "*.lnk" -Description "Old recent shortcuts"
|
||||
}
|
||||
|
||||
# AutomaticDestinations (jump lists)
|
||||
$autoDestPath = "$recentPath\AutomaticDestinations"
|
||||
if (Test-Path $autoDestPath) {
|
||||
Remove-OldFiles -Path $autoDestPath -DaysOld $DaysOld -Description "Old jump list entries"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Thumbnail Cache Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-ThumbnailCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows thumbnail cache
|
||||
#>
|
||||
|
||||
$thumbCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"
|
||||
|
||||
if (-not (Test-Path $thumbCachePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Thumbnail cache files (thumbcache_*.db)
|
||||
$thumbFiles = Get-ChildItem -Path $thumbCachePath -Filter "thumbcache_*.db" -File -ErrorAction SilentlyContinue
|
||||
|
||||
if ($thumbFiles) {
|
||||
$paths = $thumbFiles | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Thumbnail cache"
|
||||
}
|
||||
|
||||
# Icon cache
|
||||
$iconCache = "$env:LOCALAPPDATA\IconCache.db"
|
||||
if (Test-Path $iconCache) {
|
||||
Remove-SafeItem -Path $iconCache -Description "Icon cache"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Windows Error Reports Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-ErrorReports {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Error Reporting files
|
||||
#>
|
||||
param([int]$DaysOld = 7)
|
||||
|
||||
$werPaths = @(
|
||||
"$env:LOCALAPPDATA\Microsoft\Windows\WER"
|
||||
"$env:LOCALAPPDATA\CrashDumps"
|
||||
"$env:USERPROFILE\AppData\Local\Microsoft\Windows\WER"
|
||||
)
|
||||
|
||||
foreach ($path in $werPaths) {
|
||||
if (Test-Path $path) {
|
||||
$items = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if ($items) {
|
||||
$paths = $items | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Error reports"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Memory dumps
|
||||
$dumpPaths = @(
|
||||
"$env:LOCALAPPDATA\CrashDumps"
|
||||
"$env:USERPROFILE\*.dmp"
|
||||
)
|
||||
|
||||
foreach ($path in $dumpPaths) {
|
||||
$dumps = Get-ChildItem -Path $path -Filter "*.dmp" -ErrorAction SilentlyContinue
|
||||
if ($dumps) {
|
||||
$paths = $dumps | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Memory dumps"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Windows Prefetch Cleanup (requires admin)
|
||||
# ============================================================================
|
||||
|
||||
function Clear-Prefetch {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean Windows Prefetch files (requires admin)
|
||||
#>
|
||||
param([int]$DaysOld = 14)
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Debug "Skipping Prefetch cleanup - requires admin"
|
||||
return
|
||||
}
|
||||
|
||||
$prefetchPath = "$env:WINDIR\Prefetch"
|
||||
|
||||
if (Test-Path $prefetchPath) {
|
||||
Remove-OldFiles -Path $prefetchPath -DaysOld $DaysOld -Description "Prefetch files"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Log Files Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-UserLogs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean old log files from common locations
|
||||
#>
|
||||
param([int]$DaysOld = 7)
|
||||
|
||||
$logLocations = @(
|
||||
"$env:LOCALAPPDATA\Temp\*.log"
|
||||
"$env:APPDATA\*.log"
|
||||
"$env:USERPROFILE\*.log"
|
||||
)
|
||||
|
||||
foreach ($location in $logLocations) {
|
||||
$parent = Split-Path -Parent $location
|
||||
$filter = Split-Path -Leaf $location
|
||||
|
||||
if (Test-Path $parent) {
|
||||
$logs = Get-ChildItem -Path $parent -Filter $filter -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) }
|
||||
|
||||
if ($logs) {
|
||||
$paths = $logs | ForEach-Object { $_.FullName }
|
||||
Remove-SafeItems -Paths $paths -Description "Old log files"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Clipboard History Cleanup
|
||||
# ============================================================================
|
||||
|
||||
function Clear-ClipboardHistory {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clear Windows clipboard history
|
||||
#>
|
||||
|
||||
if (Test-DryRunMode) {
|
||||
Write-DryRun "Clipboard history (would clear)"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Load Windows Forms assembly for clipboard access
|
||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
||||
|
||||
# Clear current clipboard
|
||||
[System.Windows.Forms.Clipboard]::Clear()
|
||||
|
||||
# Clear clipboard history (Windows 10 1809+)
|
||||
$clipboardPath = "$env:LOCALAPPDATA\Microsoft\Windows\Clipboard"
|
||||
if (Test-Path $clipboardPath) {
|
||||
Clear-DirectoryContents -Path $clipboardPath -Description "Clipboard history"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Debug "Could not clear clipboard: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main User Cleanup Function
|
||||
# ============================================================================
|
||||
|
||||
function Invoke-UserCleanup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run all user-level cleanup tasks
|
||||
#>
|
||||
param(
|
||||
[int]$TempDaysOld = 7,
|
||||
[int]$DownloadsDaysOld = 30,
|
||||
[int]$LogDaysOld = 7,
|
||||
[switch]$IncludeDownloads,
|
||||
[switch]$IncludeRecycleBin
|
||||
)
|
||||
|
||||
Start-Section "User essentials"
|
||||
|
||||
# Always clean these
|
||||
Clear-UserTempFiles -DaysOld $TempDaysOld
|
||||
Clear-RecentFiles -DaysOld 30
|
||||
Clear-ThumbnailCache
|
||||
Clear-ErrorReports -DaysOld 7
|
||||
Clear-UserLogs -DaysOld $LogDaysOld
|
||||
Clear-Prefetch -DaysOld 14
|
||||
|
||||
# Optional: Downloads cleanup
|
||||
if ($IncludeDownloads) {
|
||||
Clear-OldDownloads -DaysOld $DownloadsDaysOld
|
||||
}
|
||||
|
||||
# Optional: Recycle Bin
|
||||
if ($IncludeRecycleBin) {
|
||||
Clear-RecycleBin
|
||||
}
|
||||
|
||||
Stop-Section
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
# Functions: Clear-UserTempFiles, Clear-OldDownloads, Clear-RecycleBin, etc.
|
||||
@@ -1,695 +0,0 @@
|
||||
#!/bin/bash
|
||||
# User Data Cleanup Module
|
||||
set -euo pipefail
|
||||
clean_user_essentials() {
|
||||
start_section_spinner "Scanning caches..."
|
||||
safe_clean ~/Library/Caches/* "User app cache"
|
||||
stop_section_spinner
|
||||
start_section_spinner "Scanning empty items..."
|
||||
clean_empty_library_items
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Logs/* "User app logs"
|
||||
if is_path_whitelisted "$HOME/.Trash"; then
|
||||
note_activity
|
||||
echo -e " ${GREEN}${ICON_EMPTY}${NC} Trash · whitelist protected"
|
||||
else
|
||||
safe_clean ~/.Trash/* "Trash"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_empty_library_items() {
|
||||
if [[ ! -d "$HOME/Library" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 1. Clean top-level empty directories in Library
|
||||
local -a empty_dirs=()
|
||||
while IFS= read -r -d '' dir; do
|
||||
[[ -d "$dir" ]] && empty_dirs+=("$dir")
|
||||
done < <(find "$HOME/Library" -mindepth 1 -maxdepth 1 -type d -empty -print0 2> /dev/null)
|
||||
|
||||
if [[ ${#empty_dirs[@]} -gt 0 ]]; then
|
||||
safe_clean "${empty_dirs[@]}" "Empty Library folders"
|
||||
fi
|
||||
|
||||
# 2. Clean empty subdirectories in Application Support and other key locations
|
||||
# Iteratively remove empty directories until no more are found
|
||||
local -a key_locations=(
|
||||
"$HOME/Library/Application Support"
|
||||
"$HOME/Library/Caches"
|
||||
)
|
||||
|
||||
for location in "${key_locations[@]}"; do
|
||||
[[ -d "$location" ]] || continue
|
||||
|
||||
# Limit passes to keep cleanup fast; 3 iterations handle most nested scenarios.
|
||||
local max_iterations=3
|
||||
local iteration=0
|
||||
|
||||
while [[ $iteration -lt $max_iterations ]]; do
|
||||
local -a nested_empty_dirs=()
|
||||
# Find empty directories
|
||||
while IFS= read -r -d '' dir; do
|
||||
# Skip if whitelisted
|
||||
if is_path_whitelisted "$dir"; then
|
||||
continue
|
||||
fi
|
||||
# Skip protected system components
|
||||
local dir_name=$(basename "$dir")
|
||||
if is_critical_system_component "$dir_name"; then
|
||||
continue
|
||||
fi
|
||||
[[ -d "$dir" ]] && nested_empty_dirs+=("$dir")
|
||||
done < <(find "$location" -mindepth 1 -type d -empty -print0 2> /dev/null)
|
||||
|
||||
# If no empty dirs found, we're done with this location
|
||||
if [[ ${#nested_empty_dirs[@]} -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
local location_name=$(basename "$location")
|
||||
safe_clean "${nested_empty_dirs[@]}" "Empty $location_name subdirs"
|
||||
|
||||
((iteration++))
|
||||
done
|
||||
done
|
||||
|
||||
# Empty file cleanup is skipped to avoid removing app sentinel files.
|
||||
}
|
||||
|
||||
# Remove old Google Chrome versions while keeping Current.
|
||||
clean_chrome_old_versions() {
|
||||
local -a app_paths=(
|
||||
"/Applications/Google Chrome.app"
|
||||
"$HOME/Applications/Google Chrome.app"
|
||||
)
|
||||
|
||||
# Use -f to match Chrome Helper processes as well
|
||||
if pgrep -f "Google Chrome" > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Google Chrome running · old versions cleanup skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cleaned_count=0
|
||||
local total_size=0
|
||||
local cleaned_any=false
|
||||
|
||||
for app_path in "${app_paths[@]}"; do
|
||||
[[ -d "$app_path" ]] || continue
|
||||
|
||||
local versions_dir="$app_path/Contents/Frameworks/Google Chrome Framework.framework/Versions"
|
||||
[[ -d "$versions_dir" ]] || continue
|
||||
|
||||
local current_link="$versions_dir/Current"
|
||||
[[ -L "$current_link" ]] || continue
|
||||
|
||||
local current_version
|
||||
current_version=$(readlink "$current_link" 2> /dev/null || true)
|
||||
current_version="${current_version##*/}"
|
||||
[[ -n "$current_version" ]] || continue
|
||||
|
||||
local -a old_versions=()
|
||||
local dir name
|
||||
for dir in "$versions_dir"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name=$(basename "$dir")
|
||||
[[ "$name" == "Current" ]] && continue
|
||||
[[ "$name" == "$current_version" ]] && continue
|
||||
if is_path_whitelisted "$dir"; then
|
||||
continue
|
||||
fi
|
||||
old_versions+=("$dir")
|
||||
done
|
||||
|
||||
if [[ ${#old_versions[@]} -eq 0 ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for dir in "${old_versions[@]}"; do
|
||||
local size_kb
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if has_sudo_session; then
|
||||
safe_sudo_remove "$dir" > /dev/null 2>&1 || true
|
||||
else
|
||||
safe_remove "$dir" true > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ "$cleaned_any" == "true" ]]; then
|
||||
local size_human
|
||||
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Chrome old versions ${YELLOW}(${cleaned_count} dirs, $size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Chrome old versions ${GREEN}(${cleaned_count} dirs, $size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove old Microsoft Edge versions while keeping Current.
|
||||
clean_edge_old_versions() {
|
||||
local -a app_paths=(
|
||||
"/Applications/Microsoft Edge.app"
|
||||
"$HOME/Applications/Microsoft Edge.app"
|
||||
)
|
||||
|
||||
# Use -f to match Edge Helper processes as well
|
||||
if pgrep -f "Microsoft Edge" > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · old versions cleanup skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cleaned_count=0
|
||||
local total_size=0
|
||||
local cleaned_any=false
|
||||
|
||||
for app_path in "${app_paths[@]}"; do
|
||||
[[ -d "$app_path" ]] || continue
|
||||
|
||||
local versions_dir="$app_path/Contents/Frameworks/Microsoft Edge Framework.framework/Versions"
|
||||
[[ -d "$versions_dir" ]] || continue
|
||||
|
||||
local current_link="$versions_dir/Current"
|
||||
[[ -L "$current_link" ]] || continue
|
||||
|
||||
local current_version
|
||||
current_version=$(readlink "$current_link" 2> /dev/null || true)
|
||||
current_version="${current_version##*/}"
|
||||
[[ -n "$current_version" ]] || continue
|
||||
|
||||
local -a old_versions=()
|
||||
local dir name
|
||||
for dir in "$versions_dir"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name=$(basename "$dir")
|
||||
[[ "$name" == "Current" ]] && continue
|
||||
[[ "$name" == "$current_version" ]] && continue
|
||||
if is_path_whitelisted "$dir"; then
|
||||
continue
|
||||
fi
|
||||
old_versions+=("$dir")
|
||||
done
|
||||
|
||||
if [[ ${#old_versions[@]} -eq 0 ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for dir in "${old_versions[@]}"; do
|
||||
local size_kb
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if has_sudo_session; then
|
||||
safe_sudo_remove "$dir" > /dev/null 2>&1 || true
|
||||
else
|
||||
safe_remove "$dir" true > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ "$cleaned_any" == "true" ]]; then
|
||||
local size_human
|
||||
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Edge old versions ${YELLOW}(${cleaned_count} dirs, $size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Edge old versions ${GREEN}(${cleaned_count} dirs, $size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove old Microsoft EdgeUpdater versions while keeping latest.
|
||||
clean_edge_updater_old_versions() {
|
||||
local updater_dir="$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable"
|
||||
[[ -d "$updater_dir" ]] || return 0
|
||||
|
||||
if pgrep -f "Microsoft Edge" > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · updater cleanup skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a version_dirs=()
|
||||
local dir
|
||||
for dir in "$updater_dir"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
version_dirs+=("$dir")
|
||||
done
|
||||
|
||||
if [[ ${#version_dirs[@]} -lt 2 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local latest_version
|
||||
latest_version=$(printf '%s\n' "${version_dirs[@]##*/}" | sort -V | tail -n 1)
|
||||
[[ -n "$latest_version" ]] || return 0
|
||||
|
||||
local cleaned_count=0
|
||||
local total_size=0
|
||||
local cleaned_any=false
|
||||
|
||||
for dir in "${version_dirs[@]}"; do
|
||||
local name
|
||||
name=$(basename "$dir")
|
||||
[[ "$name" == "$latest_version" ]] && continue
|
||||
if is_path_whitelisted "$dir"; then
|
||||
continue
|
||||
fi
|
||||
local size_kb
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$dir" true > /dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$cleaned_any" == "true" ]]; then
|
||||
local size_human
|
||||
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Edge updater old versions ${YELLOW}(${cleaned_count} dirs, $size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Edge updater old versions ${GREEN}(${cleaned_count} dirs, $size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
|
||||
scan_external_volumes() {
|
||||
[[ -d "/Volumes" ]] || return 0
|
||||
local -a candidate_volumes=()
|
||||
local -a network_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" && -w "$volume" && ! -L "$volume" ]] || continue
|
||||
[[ "$volume" == "/" || "$volume" == "/Volumes/Macintosh HD" ]] && continue
|
||||
local protocol=""
|
||||
protocol=$(run_with_timeout 1 command diskutil info "$volume" 2> /dev/null | grep -i "Protocol:" | awk '{print $2}' || echo "")
|
||||
case "$protocol" in
|
||||
SMB | NFS | AFP | CIFS | WebDAV)
|
||||
network_volumes+=("$volume")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
local fs_type=""
|
||||
fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "")
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav)
|
||||
network_volumes+=("$volume")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
candidate_volumes+=("$volume")
|
||||
done
|
||||
local volume_count=${#candidate_volumes[@]}
|
||||
local network_count=${#network_volumes[@]}
|
||||
if [[ $volume_count -eq 0 ]]; then
|
||||
if [[ $network_count -gt 0 ]]; then
|
||||
echo -e " ${GRAY}${ICON_LIST}${NC} External volumes (${network_count} network volume(s) skipped)"
|
||||
note_activity
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Scanning $volume_count external volume(s)..."
|
||||
for volume in "${candidate_volumes[@]}"; do
|
||||
[[ -d "$volume" && -r "$volume" ]] || continue
|
||||
local volume_trash="$volume/.Trashes"
|
||||
if [[ -d "$volume_trash" && "$DRY_RUN" != "true" ]] && ! is_path_whitelisted "$volume_trash"; then
|
||||
while IFS= read -r -d '' item; do
|
||||
safe_remove "$item" true || true
|
||||
done < <(command find "$volume_trash" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
fi
|
||||
if [[ "$PROTECT_FINDER_METADATA" != "true" ]]; then
|
||||
clean_ds_store_tree "$volume" "$(basename "$volume") volume (.DS_Store)"
|
||||
fi
|
||||
done
|
||||
stop_section_spinner
|
||||
}
|
||||
# Finder metadata (.DS_Store).
|
||||
clean_finder_metadata() {
|
||||
stop_section_spinner
|
||||
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
|
||||
note_activity
|
||||
echo -e " ${GREEN}${ICON_EMPTY}${NC} Finder metadata · whitelist protected"
|
||||
return
|
||||
fi
|
||||
clean_ds_store_tree "$HOME" "Home directory (.DS_Store)"
|
||||
}
|
||||
# macOS system caches and user-level leftovers.
|
||||
clean_macos_system_caches() {
|
||||
stop_section_spinner
|
||||
# safe_clean already checks protected paths.
|
||||
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states" || true
|
||||
safe_clean ~/Library/Caches/com.apple.photoanalysisd "Photo analysis cache" || true
|
||||
safe_clean ~/Library/Caches/com.apple.akd "Apple ID cache" || true
|
||||
safe_clean ~/Library/Caches/com.apple.WebKit.Networking/* "WebKit network cache" || true
|
||||
safe_clean ~/Library/DiagnosticReports/* "Diagnostic reports" || true
|
||||
safe_clean ~/Library/Caches/com.apple.QuickLook.thumbnailcache "QuickLook thumbnails" || true
|
||||
safe_clean ~/Library/Caches/Quick\ Look/* "QuickLook cache" || true
|
||||
safe_clean ~/Library/Caches/com.apple.iconservices* "Icon services cache" || true
|
||||
safe_clean ~/Downloads/*.download "Safari incomplete downloads" || true
|
||||
safe_clean ~/Downloads/*.crdownload "Chrome incomplete downloads" || true
|
||||
safe_clean ~/Downloads/*.part "Partial incomplete downloads" || true
|
||||
safe_clean ~/Library/Autosave\ Information/* "Autosave information" || true
|
||||
safe_clean ~/Library/IdentityCaches/* "Identity caches" || true
|
||||
safe_clean ~/Library/Suggestions/* "Siri suggestions cache" || true
|
||||
safe_clean ~/Library/Calendars/Calendar\ Cache "Calendar cache" || true
|
||||
safe_clean ~/Library/Application\ Support/AddressBook/Sources/*/Photos.cache "Address Book photo cache" || true
|
||||
}
|
||||
clean_recent_items() {
|
||||
stop_section_spinner
|
||||
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
|
||||
local -a recent_lists=(
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentApplications.sfl2"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentDocuments.sfl2"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentServers.sfl2"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentHosts.sfl2"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentApplications.sfl"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentDocuments.sfl"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentServers.sfl"
|
||||
"$shared_dir/com.apple.LSSharedFileList.RecentHosts.sfl"
|
||||
)
|
||||
if [[ -d "$shared_dir" ]]; then
|
||||
for sfl_file in "${recent_lists[@]}"; do
|
||||
[[ -e "$sfl_file" ]] && safe_clean "$sfl_file" "Recent items list" || true
|
||||
done
|
||||
fi
|
||||
safe_clean ~/Library/Preferences/com.apple.recentitems.plist "Recent items preferences" || true
|
||||
}
|
||||
clean_mail_downloads() {
|
||||
stop_section_spinner
|
||||
local mail_age_days=${MOLE_MAIL_AGE_DAYS:-}
|
||||
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
|
||||
mail_age_days=30
|
||||
fi
|
||||
local -a mail_dirs=(
|
||||
"$HOME/Library/Mail Downloads"
|
||||
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
|
||||
)
|
||||
local count=0
|
||||
local cleaned_kb=0
|
||||
for target_path in "${mail_dirs[@]}"; do
|
||||
if [[ -d "$target_path" ]]; then
|
||||
local dir_size_kb=0
|
||||
dir_size_kb=$(get_path_size_kb "$target_path")
|
||||
if ! [[ "$dir_size_kb" =~ ^[0-9]+$ ]]; then
|
||||
dir_size_kb=0
|
||||
fi
|
||||
local min_kb="${MOLE_MAIL_DOWNLOADS_MIN_KB:-}"
|
||||
if ! [[ "$min_kb" =~ ^[0-9]+$ ]]; then
|
||||
min_kb=5120
|
||||
fi
|
||||
if [[ "$dir_size_kb" -lt "$min_kb" ]]; then
|
||||
continue
|
||||
fi
|
||||
while IFS= read -r -d '' file_path; do
|
||||
if [[ -f "$file_path" ]]; then
|
||||
local file_size_kb=$(get_path_size_kb "$file_path")
|
||||
if safe_remove "$file_path" true; then
|
||||
((count++))
|
||||
((cleaned_kb += file_size_kb))
|
||||
fi
|
||||
fi
|
||||
done < <(command find "$target_path" -type f -mtime +"$mail_age_days" -print0 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
if [[ $count -gt 0 ]]; then
|
||||
local cleaned_mb=$(echo "$cleaned_kb" | awk '{printf "%.1f", $1/1024}' || echo "0.0")
|
||||
echo " ${GREEN}${ICON_SUCCESS}${NC} Cleaned $count mail attachments (~${cleaned_mb}MB)"
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Sandboxed app caches.
|
||||
clean_sandboxed_app_caches() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Containers/com.apple.wallpaper.agent/Data/Library/Caches/* "Wallpaper agent cache"
|
||||
safe_clean ~/Library/Containers/com.apple.mediaanalysisd/Data/Library/Caches/* "Media analysis cache"
|
||||
safe_clean ~/Library/Containers/com.apple.AppStore/Data/Library/Caches/* "App Store cache"
|
||||
safe_clean ~/Library/Containers/com.apple.configurator.xpc.InternetService/Data/tmp/* "Apple Configurator temp files"
|
||||
local containers_dir="$HOME/Library/Containers"
|
||||
[[ ! -d "$containers_dir" ]] && return 0
|
||||
start_section_spinner "Scanning sandboxed apps..."
|
||||
local total_size=0
|
||||
local cleaned_count=0
|
||||
local found_any=false
|
||||
# Use nullglob to avoid literal globs.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
for container_dir in "$containers_dir"/*; do
|
||||
process_container_cache "$container_dir"
|
||||
done
|
||||
eval "$_ng_state"
|
||||
stop_section_spinner
|
||||
if [[ "$found_any" == "true" ]]; then
|
||||
local size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Sandboxed app caches ${YELLOW}($size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches ${GREEN}($size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Process a single container cache directory.
|
||||
process_container_cache() {
|
||||
local container_dir="$1"
|
||||
[[ -d "$container_dir" ]] || return 0
|
||||
local bundle_id=$(basename "$container_dir")
|
||||
if is_critical_system_component "$bundle_id"; then
|
||||
return 0
|
||||
fi
|
||||
if should_protect_data "$bundle_id" || should_protect_data "$(echo "$bundle_id" | LC_ALL=C tr '[:upper:]' '[:lower:]')"; then
|
||||
return 0
|
||||
fi
|
||||
local cache_dir="$container_dir/Data/Library/Caches"
|
||||
[[ -d "$cache_dir" ]] || return 0
|
||||
# Fast non-empty check.
|
||||
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size=$(get_path_size_kb "$cache_dir")
|
||||
((total_size += size))
|
||||
found_any=true
|
||||
((cleaned_count++))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
# Clean contents safely with local nullglob.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
for item in "$cache_dir"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true || true
|
||||
done
|
||||
eval "$_ng_state"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
# Browser caches (Safari/Chrome/Edge/Firefox).
|
||||
clean_browsers() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
||||
# Chrome/Chromium.
|
||||
safe_clean ~/Library/Caches/Google/Chrome/* "Chrome cache"
|
||||
safe_clean ~/Library/Application\ Support/Google/Chrome/*/Application\ Cache/* "Chrome app cache"
|
||||
safe_clean ~/Library/Application\ Support/Google/Chrome/*/GPUCache/* "Chrome GPU cache"
|
||||
safe_clean ~/Library/Caches/Chromium/* "Chromium cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.edgemac/* "Edge cache"
|
||||
safe_clean ~/Library/Caches/company.thebrowser.Browser/* "Arc cache"
|
||||
safe_clean ~/Library/Caches/company.thebrowser.dia/* "Dia cache"
|
||||
safe_clean ~/Library/Caches/BraveSoftware/Brave-Browser/* "Brave cache"
|
||||
local firefox_running=false
|
||||
if pgrep -x "Firefox" > /dev/null 2>&1; then
|
||||
firefox_running=true
|
||||
fi
|
||||
if [[ "$firefox_running" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Firefox is running · cache cleanup skipped"
|
||||
else
|
||||
safe_clean ~/Library/Caches/Firefox/* "Firefox cache"
|
||||
fi
|
||||
safe_clean ~/Library/Caches/com.operasoftware.Opera/* "Opera cache"
|
||||
safe_clean ~/Library/Caches/com.vivaldi.Vivaldi/* "Vivaldi cache"
|
||||
safe_clean ~/Library/Caches/Comet/* "Comet cache"
|
||||
safe_clean ~/Library/Caches/com.kagi.kagimacOS/* "Orion cache"
|
||||
safe_clean ~/Library/Caches/zen/* "Zen cache"
|
||||
if [[ "$firefox_running" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Firefox is running · profile cache cleanup skipped"
|
||||
else
|
||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||
fi
|
||||
clean_chrome_old_versions
|
||||
clean_edge_old_versions
|
||||
clean_edge_updater_old_versions
|
||||
}
|
||||
# Cloud storage caches.
|
||||
clean_cloud_storage() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache"
|
||||
safe_clean ~/Library/Caches/com.getdropbox.dropbox "Dropbox cache"
|
||||
safe_clean ~/Library/Caches/com.google.GoogleDrive "Google Drive cache"
|
||||
safe_clean ~/Library/Caches/com.baidu.netdisk "Baidu Netdisk cache"
|
||||
safe_clean ~/Library/Caches/com.alibaba.teambitiondisk "Alibaba Cloud cache"
|
||||
safe_clean ~/Library/Caches/com.box.desktop "Box cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.OneDrive "OneDrive cache"
|
||||
}
|
||||
# Office app caches.
|
||||
clean_office_applications() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.Excel "Microsoft Excel cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.Powerpoint "Microsoft PowerPoint cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.Outlook/* "Microsoft Outlook cache"
|
||||
safe_clean ~/Library/Caches/com.apple.iWork.* "Apple iWork cache"
|
||||
safe_clean ~/Library/Caches/com.kingsoft.wpsoffice.mac "WPS Office cache"
|
||||
safe_clean ~/Library/Caches/org.mozilla.thunderbird/* "Thunderbird cache"
|
||||
safe_clean ~/Library/Caches/com.apple.mail/* "Apple Mail cache"
|
||||
}
|
||||
# Virtualization caches.
|
||||
clean_virtualization_tools() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache"
|
||||
safe_clean ~/Library/Caches/com.parallels.* "Parallels cache"
|
||||
safe_clean ~/VirtualBox\ VMs/.cache "VirtualBox cache"
|
||||
safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files"
|
||||
}
|
||||
# Application Support logs/caches.
|
||||
clean_application_support_logs() {
|
||||
stop_section_spinner
|
||||
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
|
||||
note_activity
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Application Support"
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Scanning Application Support..."
|
||||
local total_size=0
|
||||
local cleaned_count=0
|
||||
local found_any=false
|
||||
# Enable nullglob for safe globbing.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
for app_dir in ~/Library/Application\ Support/*; do
|
||||
[[ -d "$app_dir" ]] || continue
|
||||
local app_name=$(basename "$app_dir")
|
||||
local app_name_lower=$(echo "$app_name" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||
local is_protected=false
|
||||
if should_protect_data "$app_name"; then
|
||||
is_protected=true
|
||||
elif should_protect_data "$app_name_lower"; then
|
||||
is_protected=true
|
||||
fi
|
||||
if [[ "$is_protected" == "true" ]]; then
|
||||
continue
|
||||
fi
|
||||
if is_critical_system_component "$app_name"; then
|
||||
continue
|
||||
fi
|
||||
local -a start_candidates=("$app_dir/log" "$app_dir/logs" "$app_dir/activitylog" "$app_dir/Cache/Cache_Data" "$app_dir/Crashpad/completed")
|
||||
for candidate in "${start_candidates[@]}"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size=$(get_path_size_kb "$candidate")
|
||||
((total_size += size))
|
||||
((cleaned_count++))
|
||||
found_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
for item in "$candidate"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Group Containers logs (explicit allowlist).
|
||||
local known_group_containers=(
|
||||
"group.com.apple.contentdelivery"
|
||||
)
|
||||
for container in "${known_group_containers[@]}"; do
|
||||
local container_path="$HOME/Library/Group Containers/$container"
|
||||
local -a gc_candidates=("$container_path/Logs" "$container_path/Library/Logs")
|
||||
for candidate in "${gc_candidates[@]}"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size=$(get_path_size_kb "$candidate")
|
||||
((total_size += size))
|
||||
((cleaned_count++))
|
||||
found_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
for item in "$candidate"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
eval "$_ng_state"
|
||||
stop_section_spinner
|
||||
if [[ "$found_any" == "true" ]]; then
|
||||
local size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Application Support logs/caches ${YELLOW}($size_human dry)${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches ${GREEN}($size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# iOS device backup info.
|
||||
check_ios_device_backups() {
|
||||
local backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||
# Simplified check without find to avoid hanging.
|
||||
if [[ -d "$backup_dir" ]]; then
|
||||
local backup_kb=$(get_path_size_kb "$backup_dir")
|
||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
|
||||
local backup_human=$(command du -sh "$backup_dir" 2> /dev/null | awk '{print $1}')
|
||||
if [[ -n "$backup_human" ]]; then
|
||||
note_activity
|
||||
echo -e " Found ${GREEN}${backup_human}${NC} iOS backups"
|
||||
echo -e " You can delete them manually: ${backup_dir}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# Apple Silicon specific caches (IS_M_SERIES).
|
||||
clean_apple_silicon_caches() {
|
||||
if [[ "${IS_M_SERIES:-false}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
start_section "Apple Silicon updates"
|
||||
safe_clean /Library/Apple/usr/share/rosetta/rosetta_update_bundle "Rosetta 2 cache"
|
||||
safe_clean ~/Library/Caches/com.apple.rosetta.update "Rosetta 2 user cache"
|
||||
safe_clean ~/Library/Caches/com.apple.amp.mediasevicesd "Apple Silicon media service cache"
|
||||
end_section
|
||||
}
|
||||
Reference in New Issue
Block a user