mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 12:41:46 +00:00
1008 lines
41 KiB
Bash
Executable File
1008 lines
41 KiB
Bash
Executable File
#!/bin/bash
|
|
# Mole - Application Protection
|
|
# System critical and data-protected application lists
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ -n "${MOLE_APP_PROTECTION_LOADED:-}" ]]; then
|
|
return 0
|
|
fi
|
|
readonly MOLE_APP_PROTECTION_LOADED=1
|
|
|
|
_MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
[[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh"
|
|
|
|
# ============================================================================
|
|
# Application Management
|
|
# ============================================================================
|
|
|
|
# Critical system components protected from uninstallation
|
|
readonly SYSTEM_CRITICAL_BUNDLES=(
|
|
"com.apple.*" # System essentials
|
|
"loginwindow"
|
|
"dock"
|
|
"systempreferences"
|
|
"finder"
|
|
"safari"
|
|
"com.apple.Settings*"
|
|
"com.apple.SystemSettings*"
|
|
"com.apple.controlcenter*"
|
|
"com.apple.backgroundtaskmanagement*"
|
|
"com.apple.loginitems*"
|
|
"com.apple.sharedfilelist*"
|
|
"com.apple.sfl*"
|
|
"backgroundtaskmanagementagent"
|
|
"keychain*"
|
|
"security*"
|
|
"bluetooth*"
|
|
"wifi*"
|
|
"network*"
|
|
"tcc"
|
|
"notification*"
|
|
"accessibility*"
|
|
"universalaccess*"
|
|
"HIToolbox*"
|
|
"textinput*"
|
|
"TextInput*"
|
|
"keyboard*"
|
|
"Keyboard*"
|
|
"inputsource*"
|
|
"InputSource*"
|
|
"keylayout*"
|
|
"KeyLayout*"
|
|
"GlobalPreferences"
|
|
".GlobalPreferences"
|
|
# Input methods (critical for international users)
|
|
"com.tencent.inputmethod.QQInput"
|
|
"com.sogou.inputmethod.*"
|
|
"com.baidu.inputmethod.*"
|
|
"com.apple.inputmethod.*"
|
|
"com.googlecode.rimeime.*"
|
|
"im.rime.*"
|
|
"org.pqrs.Karabiner*"
|
|
"*.inputmethod"
|
|
"*.InputMethod"
|
|
"*IME"
|
|
"com.apple.inputsource*"
|
|
"com.apple.TextInputMenuAgent"
|
|
"com.apple.TextInputSwitcher"
|
|
)
|
|
|
|
# Applications with sensitive data; protected during cleanup but removable
|
|
readonly DATA_PROTECTED_BUNDLES=(
|
|
# ============================================================================
|
|
# System Utilities & Cleanup Tools
|
|
# ============================================================================
|
|
"com.nektony.*" # App Cleaner & Uninstaller
|
|
"com.macpaw.*" # CleanMyMac, CleanMaster
|
|
"com.freemacsoft.AppCleaner" # AppCleaner
|
|
"com.omnigroup.omnidisksweeper" # OmniDiskSweeper
|
|
"com.daisydiskapp.*" # DaisyDisk
|
|
"com.tunabellysoftware.*" # Disk Utility apps
|
|
"com.grandperspectiv.*" # GrandPerspective
|
|
"com.binaryfruit.*" # FusionCast
|
|
|
|
# ============================================================================
|
|
# Password Managers & Security
|
|
# ============================================================================
|
|
"com.1password.*" # 1Password
|
|
"com.agilebits.*" # 1Password legacy
|
|
"com.lastpass.*" # LastPass
|
|
"com.dashlane.*" # Dashlane
|
|
"com.bitwarden.*" # Bitwarden
|
|
"com.keepassx.*" # KeePassXC
|
|
"org.keepassx.*" # KeePassX
|
|
"com.authy.*" # Authy
|
|
"com.yubico.*" # YubiKey Manager
|
|
|
|
# ============================================================================
|
|
# Development Tools - IDEs & Editors
|
|
# ============================================================================
|
|
"com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.)
|
|
"JetBrains*" # JetBrains Application Support folders
|
|
"com.microsoft.VSCode" # Visual Studio Code
|
|
"com.visualstudio.code.*" # VS Code variants
|
|
"com.sublimetext.*" # Sublime Text
|
|
"com.sublimehq.*" # Sublime Merge
|
|
"com.microsoft.VSCodeInsiders" # VS Code Insiders
|
|
"com.apple.dt.Xcode" # Xcode (keep settings)
|
|
"com.coteditor.CotEditor" # CotEditor
|
|
"com.macromates.TextMate" # TextMate
|
|
"com.panic.Nova" # Nova
|
|
"abnerworks.Typora" # Typora (Markdown editor)
|
|
"com.uranusjr.macdown" # MacDown
|
|
|
|
# ============================================================================
|
|
# AI & LLM Tools
|
|
# ============================================================================
|
|
"com.todesktop.*" # Cursor (often uses generic todesktop ID)
|
|
"Cursor" # Cursor App Support
|
|
"com.anthropic.claude*" # Claude
|
|
"Claude" # Claude App Support
|
|
"com.openai.chat*" # ChatGPT
|
|
"ChatGPT" # ChatGPT App Support
|
|
"com.ollama.ollama" # Ollama
|
|
"Ollama" # Ollama App Support
|
|
"com.lmstudio.lmstudio" # LM Studio
|
|
"LM Studio" # LM Studio App Support
|
|
"co.supertool.chatbox" # Chatbox
|
|
"page.jan.jan" # Jan
|
|
"com.huggingface.huggingchat" # HuggingChat
|
|
"Gemini" # Gemini
|
|
"com.perplexity.Perplexity" # Perplexity
|
|
"com.drawthings.DrawThings" # Draw Things
|
|
"com.divamgupta.diffusionbee" # DiffusionBee
|
|
"com.exafunction.windsurf" # Windsurf
|
|
"com.quora.poe.electron" # Poe
|
|
"chat.openai.com.*" # OpenAI web wrappers
|
|
|
|
# ============================================================================
|
|
# Development Tools - Database Clients
|
|
# ============================================================================
|
|
"com.sequelpro.*" # Sequel Pro
|
|
"com.sequel-ace.*" # Sequel Ace
|
|
"com.tinyapp.*" # TablePlus
|
|
"com.dbeaver.*" # DBeaver
|
|
"com.navicat.*" # Navicat
|
|
"com.mongodb.compass" # MongoDB Compass
|
|
"com.redis.RedisInsight" # Redis Insight
|
|
"com.pgadmin.pgadmin4" # pgAdmin
|
|
"com.eggerapps.Sequel-Pro" # Sequel Pro legacy
|
|
"com.valentina-db.Valentina-Studio" # Valentina Studio
|
|
"com.dbvis.DbVisualizer" # DbVisualizer
|
|
|
|
# ============================================================================
|
|
# Development Tools - API & Network
|
|
# ============================================================================
|
|
"com.postmanlabs.mac" # Postman
|
|
"com.konghq.insomnia" # Insomnia
|
|
"com.CharlesProxy.*" # Charles Proxy
|
|
"com.proxyman.*" # Proxyman
|
|
"com.getpaw.*" # Paw
|
|
"com.luckymarmot.Paw" # Paw legacy
|
|
"com.charlesproxy.charles" # Charles
|
|
"com.telerik.Fiddler" # Fiddler
|
|
"com.usebruno.app" # Bruno (API client)
|
|
|
|
# ============================================================================
|
|
# Network Proxy & VPN Tools (pattern-based protection)
|
|
# ============================================================================
|
|
# Clash variants
|
|
"*clash*" # All Clash variants (ClashX, ClashX Pro, Clash Verge, etc)
|
|
"*Clash*" # Capitalized variants
|
|
"com.nssurge.surge-mac" # Surge
|
|
"*surge*" # Surge variants
|
|
"*Surge*" # Surge variants
|
|
"mihomo*" # Mihomo Party and variants
|
|
"*openvpn*" # OpenVPN Connect and variants
|
|
"*OpenVPN*" # OpenVPN capitalized variants
|
|
"net.openvpn.*" # OpenVPN bundle IDs
|
|
|
|
# Proxy Clients (Shadowsocks, V2Ray, etc)
|
|
"*ShadowsocksX-NG*" # ShadowsocksX-NG
|
|
"com.qiuyuzhou.*" # ShadowsocksX-NG bundle
|
|
"*v2ray*" # V2Ray variants
|
|
"*V2Ray*" # V2Ray variants
|
|
"*v2box*" # V2Box
|
|
"*V2Box*" # V2Box
|
|
"*nekoray*" # Nekoray
|
|
"*sing-box*" # Sing-box
|
|
"*OneBox*" # OneBox
|
|
"*hiddify*" # Hiddify
|
|
"*Hiddify*" # Hiddify
|
|
"*loon*" # Loon
|
|
"*Loon*" # Loon
|
|
"*quantumult*" # Quantumult X
|
|
|
|
# Mesh & Corporate VPNs
|
|
"*tailscale*" # Tailscale
|
|
"io.tailscale.*" # Tailscale bundle
|
|
"*zerotier*" # ZeroTier
|
|
"com.zerotier.*" # ZeroTier bundle
|
|
"*1dot1dot1dot1*" # Cloudflare WARP
|
|
"*cloudflare*warp*" # Cloudflare WARP
|
|
|
|
# Commercial VPNs
|
|
"*nordvpn*" # NordVPN
|
|
"*expressvpn*" # ExpressVPN
|
|
"*protonvpn*" # ProtonVPN
|
|
"*surfshark*" # Surfshark
|
|
"*windscribe*" # Windscribe
|
|
"*mullvad*" # Mullvad
|
|
"*privateinternetaccess*" # PIA
|
|
|
|
# Screensaver & Dynamic Wallpaper
|
|
"*Aerial*" # Aerial screensaver (all case variants)
|
|
"*aerial*" # Aerial lowercase
|
|
"*Fliqlo*" # Fliqlo screensaver (all case variants)
|
|
"*fliqlo*" # Fliqlo lowercase
|
|
|
|
# ============================================================================
|
|
# Development Tools - Git & Version Control
|
|
# ============================================================================
|
|
"com.github.GitHubDesktop" # GitHub Desktop
|
|
"com.sublimemerge" # Sublime Merge
|
|
"com.torusknot.SourceTreeNotMAS" # SourceTree
|
|
"com.git-tower.Tower*" # Tower
|
|
"com.gitfox.GitFox" # GitFox
|
|
"com.github.Gitify" # Gitify
|
|
"com.fork.Fork" # Fork
|
|
"com.axosoft.gitkraken" # GitKraken
|
|
|
|
# ============================================================================
|
|
# Development Tools - Terminal & Shell
|
|
# ============================================================================
|
|
"com.googlecode.iterm2" # iTerm2
|
|
"net.kovidgoyal.kitty" # Kitty
|
|
"io.alacritty" # Alacritty
|
|
"com.github.wez.wezterm" # WezTerm
|
|
"com.hyper.Hyper" # Hyper
|
|
"com.mizage.divvy" # Divvy
|
|
"com.fig.Fig" # Fig (terminal assistant)
|
|
"dev.warp.Warp-Stable" # Warp
|
|
"com.termius-dmg" # Termius (SSH client)
|
|
|
|
# ============================================================================
|
|
# Development Tools - Docker & Virtualization
|
|
# ============================================================================
|
|
"com.docker.docker" # Docker Desktop
|
|
"com.getutm.UTM" # UTM
|
|
"com.vmware.fusion" # VMware Fusion
|
|
"com.parallels.desktop.*" # Parallels Desktop
|
|
"org.virtualbox.app.VirtualBox" # VirtualBox
|
|
"com.vagrant.*" # Vagrant
|
|
"com.orbstack.OrbStack" # OrbStack
|
|
|
|
# ============================================================================
|
|
# System Monitoring & Performance
|
|
# ============================================================================
|
|
"com.bjango.istatmenus*" # iStat Menus
|
|
"eu.exelban.Stats" # Stats
|
|
"com.monitorcontrol.*" # MonitorControl
|
|
"com.bresink.system-toolkit.*" # TinkerTool System
|
|
"com.mediaatelier.MenuMeters" # MenuMeters
|
|
"com.activity-indicator.app" # Activity Indicator
|
|
"net.cindori.sensei" # Sensei
|
|
|
|
# ============================================================================
|
|
# Window Management & Productivity
|
|
# ============================================================================
|
|
"com.macitbetter.*" # BetterTouchTool, BetterSnapTool
|
|
"com.hegenberg.*" # BetterTouchTool legacy
|
|
"com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator
|
|
"com.divisiblebyzero.*" # Spectacle
|
|
"com.koingdev.*" # Koingg apps
|
|
"com.if.Amphetamine" # Amphetamine
|
|
"com.lwouis.alt-tab-macos" # AltTab
|
|
"net.matthewpalmer.Vanilla" # Vanilla
|
|
"com.lightheadsw.Caffeine" # Caffeine
|
|
"com.contextual.Contexts" # Contexts
|
|
"com.amethyst.Amethyst" # Amethyst
|
|
"com.knollsoft.Rectangle" # Rectangle
|
|
"com.knollsoft.Hookshot" # Hookshot
|
|
"com.surteesstudios.Bartender" # Bartender
|
|
"com.gaosun.eul" # eul (system monitor)
|
|
"com.pointum.hazeover" # HazeOver
|
|
|
|
# ============================================================================
|
|
# Launcher & Automation
|
|
# ============================================================================
|
|
"com.runningwithcrayons.Alfred" # Alfred
|
|
"com.raycast.macos" # Raycast
|
|
"com.blacktree.Quicksilver" # Quicksilver
|
|
"com.stairways.keyboardmaestro.*" # Keyboard Maestro
|
|
"com.manytricks.Butler" # Butler
|
|
"com.happenapps.Quitter" # Quitter
|
|
"com.pilotmoon.scroll-reverser" # Scroll Reverser
|
|
"org.pqrs.Karabiner-Elements" # Karabiner-Elements
|
|
"com.apple.Automator" # Automator (system, but keep user workflows)
|
|
|
|
# ============================================================================
|
|
# Note-Taking & Documentation
|
|
# ============================================================================
|
|
"com.bear-writer.*" # Bear
|
|
"com.typora.*" # Typora
|
|
"com.ulyssesapp.*" # Ulysses
|
|
"com.literatureandlatte.*" # Scrivener
|
|
"com.dayoneapp.*" # Day One
|
|
"notion.id" # Notion
|
|
"md.obsidian" # Obsidian
|
|
"com.logseq.logseq" # Logseq
|
|
"com.evernote.Evernote" # Evernote
|
|
"com.onenote.mac" # OneNote
|
|
"com.omnigroup.OmniOutliner*" # OmniOutliner
|
|
"net.shinyfrog.bear" # Bear legacy
|
|
"com.goodnotes.GoodNotes" # GoodNotes
|
|
"com.marginnote.MarginNote*" # MarginNote
|
|
"com.roamresearch.*" # Roam Research
|
|
"com.reflect.ReflectApp" # Reflect
|
|
"com.inkdrop.*" # Inkdrop
|
|
|
|
# ============================================================================
|
|
# Design & Creative Tools
|
|
# ============================================================================
|
|
"com.adobe.*" # Adobe Creative Suite
|
|
"com.bohemiancoding.*" # Sketch
|
|
"com.figma.*" # Figma
|
|
"com.framerx.*" # Framer
|
|
"com.zeplin.*" # Zeplin
|
|
"com.invisionapp.*" # InVision
|
|
"com.principle.*" # Principle
|
|
"com.pixelmatorteam.*" # Pixelmator
|
|
"com.affinitydesigner.*" # Affinity Designer
|
|
"com.affinityphoto.*" # Affinity Photo
|
|
"com.affinitypublisher.*" # Affinity Publisher
|
|
"com.linearity.curve" # Linearity Curve
|
|
"com.canva.CanvaDesktop" # Canva
|
|
"com.maxon.cinema4d" # Cinema 4D
|
|
"com.autodesk.*" # Autodesk products
|
|
"com.sketchup.*" # SketchUp
|
|
|
|
# ============================================================================
|
|
# Communication & Collaboration
|
|
# ============================================================================
|
|
"com.tencent.xinWeChat" # WeChat (Chinese users)
|
|
"com.tencent.qq" # QQ
|
|
"com.alibaba.DingTalkMac" # DingTalk
|
|
"com.alibaba.AliLang.osx" # AliLang (retain login/config data)
|
|
"com.alibaba.alilang3.osx.ShipIt" # AliLang updater component
|
|
"com.alibaba.AlilangMgr.QueryNetworkInfo" # AliLang network helper
|
|
"us.zoom.xos" # Zoom
|
|
"com.microsoft.teams*" # Microsoft Teams
|
|
"com.slack.Slack" # Slack
|
|
"com.hnc.Discord" # Discord
|
|
"app.legcord.Legcord" # Legcord
|
|
"org.telegram.desktop" # Telegram
|
|
"ru.keepcoder.Telegram" # Telegram legacy
|
|
"net.whatsapp.WhatsApp" # WhatsApp
|
|
"com.skype.skype" # Skype
|
|
"com.cisco.webexmeetings" # Webex
|
|
"com.ringcentral.RingCentral" # RingCentral
|
|
"com.readdle.smartemail-Mac" # Spark Email
|
|
"com.airmail.*" # Airmail
|
|
"com.postbox-inc.postbox" # Postbox
|
|
"com.tinyspeck.slackmacgap" # Slack legacy
|
|
|
|
# ============================================================================
|
|
# Task Management & Productivity
|
|
# ============================================================================
|
|
"com.omnigroup.OmniFocus*" # OmniFocus
|
|
"com.culturedcode.*" # Things
|
|
"com.todoist.*" # Todoist
|
|
"com.any.do.*" # Any.do
|
|
"com.ticktick.*" # TickTick
|
|
"com.microsoft.to-do" # Microsoft To Do
|
|
"com.trello.trello" # Trello
|
|
"com.asana.nativeapp" # Asana
|
|
"com.clickup.*" # ClickUp
|
|
"com.monday.desktop" # Monday.com
|
|
"com.airtable.airtable" # Airtable
|
|
"com.notion.id" # Notion (also note-taking)
|
|
"com.linear.linear" # Linear
|
|
|
|
# ============================================================================
|
|
# File Transfer & Sync
|
|
# ============================================================================
|
|
"com.panic.transmit*" # Transmit (FTP/SFTP)
|
|
"com.binarynights.ForkLift*" # ForkLift
|
|
"com.noodlesoft.Hazel" # Hazel
|
|
"com.cyberduck.Cyberduck" # Cyberduck
|
|
"io.filezilla.FileZilla" # FileZilla
|
|
"com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents
|
|
"com.synology.*" # Synology apps
|
|
|
|
# ============================================================================
|
|
# Screenshot & Recording
|
|
# ============================================================================
|
|
"com.cleanshot.*" # CleanShot X
|
|
"com.xnipapp.xnip" # Xnip
|
|
"com.reincubate.camo" # Camo
|
|
"com.tunabellysoftware.ScreenFloat" # ScreenFloat
|
|
"net.telestream.screenflow*" # ScreenFlow
|
|
"com.techsmith.snagit*" # Snagit
|
|
"com.techsmith.camtasia*" # Camtasia
|
|
"com.obsidianapp.screenrecorder" # Screen Recorder
|
|
"com.kap.Kap" # Kap
|
|
"com.getkap.*" # Kap legacy
|
|
"com.linebreak.CloudApp" # CloudApp
|
|
"com.droplr.droplr-mac" # Droplr
|
|
|
|
# ============================================================================
|
|
# Media & Entertainment
|
|
# ============================================================================
|
|
"com.spotify.client" # Spotify
|
|
"com.apple.Music" # Apple Music
|
|
"com.apple.podcasts" # Apple Podcasts
|
|
"com.apple.BKAgentService" # Apple Books (Agent)
|
|
"com.apple.iBooksX" # Apple Books
|
|
"com.apple.iBooks" # Apple Books (Legacy)
|
|
"com.apple.FinalCutPro" # Final Cut Pro
|
|
"com.apple.Motion" # Motion
|
|
"com.apple.Compressor" # Compressor
|
|
"com.blackmagic-design.*" # DaVinci Resolve
|
|
"com.colliderli.iina" # IINA
|
|
"org.videolan.vlc" # VLC
|
|
"io.mpv" # MPV
|
|
"com.noodlesoft.Hazel" # Hazel (automation)
|
|
"tv.plex.player.desktop" # Plex
|
|
"com.netease.163music" # NetEase Music
|
|
|
|
# ============================================================================
|
|
# License Management & App Stores
|
|
# ============================================================================
|
|
"com.paddle.Paddle*" # Paddle (license management)
|
|
"com.setapp.DesktopClient" # Setapp
|
|
"com.devmate.*" # DevMate (license framework)
|
|
"org.sparkle-project.Sparkle" # Sparkle (update framework)
|
|
)
|
|
|
|
# Centralized check for critical system components (case-insensitive)
|
|
is_critical_system_component() {
|
|
local token="$1"
|
|
[[ -z "$token" ]] && return 1
|
|
|
|
local lower
|
|
lower=$(echo "$token" | tr '[:upper:]' '[:lower:]')
|
|
|
|
case "$lower" in
|
|
*backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Legacy function - preserved for backward compatibility
|
|
# Use should_protect_from_uninstall() or should_protect_data() instead
|
|
readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}")
|
|
|
|
# Check if bundle ID matches pattern (glob support)
|
|
bundle_matches_pattern() {
|
|
local bundle_id="$1"
|
|
local pattern="$2"
|
|
|
|
[[ -z "$pattern" ]] && return 1
|
|
|
|
# Use bash [[ ]] for glob pattern matching (works with variables in bash 3.2+)
|
|
# shellcheck disable=SC2053 # allow glob pattern matching
|
|
if [[ "$bundle_id" == $pattern ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if application is a protected system component
|
|
should_protect_from_uninstall() {
|
|
local bundle_id="$1"
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Check if application data should be protected during cleanup
|
|
should_protect_data() {
|
|
local bundle_id="$1"
|
|
# Protect both system critical and data protected bundles during cleanup
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Check if a path is protected from deletion
|
|
# Centralized logic to protect system settings, control center, and critical apps
|
|
#
|
|
# Args: $1 - path to check
|
|
# Returns: 0 if protected, 1 if safe to delete
|
|
should_protect_path() {
|
|
local path="$1"
|
|
[[ -z "$path" ]] && return 1
|
|
|
|
local path_lower
|
|
path_lower=$(echo "$path" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# 1. Keyword-based matching for system components
|
|
# Protect System Settings, Preferences, Control Center, and related XPC services
|
|
# Also protect "Settings" (used in macOS Sequoia) and savedState files
|
|
if [[ "$path_lower" =~ systemsettings || "$path_lower" =~ systempreferences || "$path_lower" =~ controlcenter ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Additional check for com.apple.Settings (macOS Sequoia System Settings)
|
|
if [[ "$path_lower" =~ com\.apple\.settings ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# 2. Protect caches critical for system UI rendering
|
|
# These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering
|
|
case "$path" in
|
|
# System Settings and Control Center caches (CRITICAL - prevents blank panel bug)
|
|
*com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*)
|
|
return 0
|
|
;;
|
|
# Finder and Dock (system essential)
|
|
*com.apple.finder.cache* | *com.apple.dock.cache*)
|
|
return 0
|
|
;;
|
|
# System XPC services and sandboxed containers
|
|
*/Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*)
|
|
return 0
|
|
;;
|
|
*/Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*)
|
|
return 0
|
|
;;
|
|
# Shared file lists for System Settings (macOS Sequoia) - Issue #136
|
|
*/com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 3. Extract bundle ID from sandbox paths
|
|
# Matches: .../Library/Containers/bundle.id/...
|
|
# Matches: .../Library/Group Containers/group.id/...
|
|
if [[ "$path" =~ /Library/Containers/([^/]+) ]] || [[ "$path" =~ /Library/Group\ Containers/([^/]+) ]]; then
|
|
local bundle_id="${BASH_REMATCH[1]}"
|
|
if should_protect_data "$bundle_id"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# 4. Check for specific hardcoded critical patterns
|
|
case "$path" in
|
|
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 5. Protect critical preference files and user data
|
|
case "$path" in
|
|
*/Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist)
|
|
return 0
|
|
;;
|
|
# Bluetooth and WiFi configurations
|
|
*/ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*)
|
|
return 0
|
|
;;
|
|
# iCloud Drive - protect user's cloud synced data
|
|
*/Library/Mobile\ Documents* | */Mobile\ Documents*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 6. Match full path against protected patterns
|
|
# This catches things like /Users/tw93/Library/Caches/Claude when pattern is *Claude*
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$path" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# 7. Check if the filename itself matches any protected patterns
|
|
local filename
|
|
filename=$(basename "$path")
|
|
if should_protect_data "$filename"; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if a path is protected by whitelist patterns
|
|
# Args: $1 - path to check
|
|
# Returns: 0 if whitelisted, 1 if not
|
|
is_path_whitelisted() {
|
|
local target_path="$1"
|
|
[[ -z "$target_path" ]] && return 1
|
|
|
|
# Normalize path (remove trailing slash)
|
|
local normalized_target="${target_path%/}"
|
|
|
|
# Empty whitelist means nothing is protected
|
|
[[ ${#WHITELIST_PATTERNS[@]} -eq 0 ]] && return 1
|
|
|
|
for pattern in "${WHITELIST_PATTERNS[@]}"; do
|
|
# Expand tilde in whitelist pattern for comparison
|
|
local expanded_pattern="${pattern/#\~/$HOME}"
|
|
expanded_pattern="${expanded_pattern%/}"
|
|
|
|
# Check for exact match or glob pattern match
|
|
# shellcheck disable=SC2053
|
|
if [[ "$normalized_target" == "$expanded_pattern" ]] ||
|
|
[[ "$normalized_target" == $expanded_pattern ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Locate files associated with an application
|
|
find_app_files() {
|
|
local bundle_id="$1"
|
|
local app_name="$2"
|
|
local -a files_to_clean=()
|
|
|
|
# Normalize app name for matching
|
|
local nospace_name="${app_name// /}"
|
|
local underscore_name="${app_name// /_}"
|
|
|
|
# Standard path patterns for user-level files
|
|
local -a user_patterns=(
|
|
"$HOME/Library/Application Support/$app_name"
|
|
"$HOME/Library/Application Support/$bundle_id"
|
|
"$HOME/Library/Caches/$bundle_id"
|
|
"$HOME/Library/Caches/$app_name"
|
|
"$HOME/Library/Logs/$app_name"
|
|
"$HOME/Library/Logs/$bundle_id"
|
|
"$HOME/Library/Application Support/CrashReporter/$app_name"
|
|
"$HOME/Library/Saved Application State/$bundle_id.savedState"
|
|
"$HOME/Library/Containers/$bundle_id"
|
|
"$HOME/Library/WebKit/$bundle_id"
|
|
"$HOME/Library/WebKit/com.apple.WebKit.WebContent/$bundle_id"
|
|
"$HOME/Library/HTTPStorages/$bundle_id"
|
|
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
|
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
|
"$HOME/Library/Application Scripts/$bundle_id"
|
|
"$HOME/Library/Services/$app_name.workflow"
|
|
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
|
"$HOME/Library/Internet Plug-Ins/$app_name.plugin"
|
|
"$HOME/Library/Audio/Plug-Ins/Components/$app_name.component"
|
|
"$HOME/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
|
"$HOME/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
|
"$HOME/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
|
"$HOME/Library/PreferencePanes/$app_name.prefPane"
|
|
"$HOME/Library/Screen Savers/$app_name.saver"
|
|
"$HOME/Library/Frameworks/$app_name.framework"
|
|
"$HOME/Library/Autosave Information/$bundle_id"
|
|
"$HOME/Library/Contextual Menu Items/$app_name.plugin"
|
|
"$HOME/Library/Spotlight/$app_name.mdimporter"
|
|
"$HOME/Library/ColorPickers/$app_name.colorPicker"
|
|
"$HOME/Library/Workflows/$app_name.workflow"
|
|
"$HOME/.config/$app_name"
|
|
"$HOME/.local/share/$app_name"
|
|
"$HOME/.$app_name"
|
|
"$HOME/.$app_name"rc
|
|
)
|
|
|
|
# Add sanitized name variants if unique enough
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
user_patterns+=(
|
|
"$HOME/Library/Application Support/$nospace_name"
|
|
"$HOME/Library/Caches/$nospace_name"
|
|
"$HOME/Library/Logs/$nospace_name"
|
|
"$HOME/Library/Application Support/$underscore_name"
|
|
)
|
|
fi
|
|
|
|
# Process standard patterns
|
|
for p in "${user_patterns[@]}"; do
|
|
local expanded_path="${p/#\~/$HOME}"
|
|
# Skip if path doesn't exist
|
|
[[ ! -e "$expanded_path" ]] && continue
|
|
|
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
|
# This prevents deletion of entire Library subdirectories when bundle_id is empty
|
|
case "$expanded_path" in
|
|
*/Library/Application\ Support | */Library/Application\ Support/ | \
|
|
*/Library/Caches | */Library/Caches/ | \
|
|
*/Library/Logs | */Library/Logs/ | \
|
|
*/Library/Containers | */Library/Containers/ | \
|
|
*/Library/WebKit | */Library/WebKit/ | \
|
|
*/Library/HTTPStorages | */Library/HTTPStorages/ | \
|
|
*/Library/Application\ Scripts | */Library/Application\ Scripts/ | \
|
|
*/Library/Autosave\ Information | */Library/Autosave\ Information/ | \
|
|
*/Library/Group\ Containers | */Library/Group\ Containers/)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
files_to_clean+=("$expanded_path")
|
|
done
|
|
|
|
# Handle Preferences and ByHost variants (only if bundle_id is valid)
|
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
|
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
|
[[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do
|
|
files_to_clean+=("$pref")
|
|
done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
|
|
|
# Group Containers (special handling)
|
|
if [[ -d ~/Library/Group\ Containers ]]; then
|
|
while IFS= read -r -d '' container; do
|
|
files_to_clean+=("$container")
|
|
done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
fi
|
|
fi
|
|
|
|
# Launch Agents by name (special handling)
|
|
if [[ ${#app_name} -gt 3 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
|
while IFS= read -r -d '' plist; do
|
|
files_to_clean+=("$plist")
|
|
done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
|
fi
|
|
|
|
# Handle specialized toolchains and development environments
|
|
# 1. DevEco-Studio (Huawei)
|
|
if [[ "$app_name" =~ DevEco|deveco ]] || [[ "$bundle_id" =~ huawei.*deveco ]]; then
|
|
for d in ~/DevEcoStudioProjects ~/DevEco-Studio ~/Library/Application\ Support/Huawei ~/Library/Caches/Huawei ~/Library/Logs/Huawei ~/Library/Huawei ~/Huawei ~/HarmonyOS ~/.huawei ~/.ohos; do
|
|
[[ -d "$d" ]] && files_to_clean+=("$d")
|
|
done
|
|
fi
|
|
|
|
# 2. Android Studio (Google)
|
|
if [[ "$app_name" =~ Android.*Studio|android.*studio ]] || [[ "$bundle_id" =~ google.*android.*studio|jetbrains.*android ]]; then
|
|
for d in ~/AndroidStudioProjects ~/Library/Android ~/.android ~/.gradle; do
|
|
[[ -d "$d" ]] && files_to_clean+=("$d")
|
|
done
|
|
[[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null)
|
|
fi
|
|
|
|
# 3. Xcode (Apple)
|
|
if [[ "$app_name" =~ Xcode|xcode ]] || [[ "$bundle_id" =~ apple.*xcode ]]; then
|
|
[[ -d ~/Library/Developer ]] && files_to_clean+=("$HOME/Library/Developer")
|
|
[[ -d ~/.Xcode ]] && files_to_clean+=("$HOME/.Xcode")
|
|
fi
|
|
|
|
# 4. JetBrains (IDE settings)
|
|
if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then
|
|
for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do
|
|
[[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2> /dev/null)
|
|
done
|
|
fi
|
|
|
|
# 5. Unity / Unreal / Godot
|
|
[[ "$app_name" =~ Unity|unity ]] && [[ -d ~/Library/Unity ]] && files_to_clean+=("$HOME/Library/Unity")
|
|
[[ "$app_name" =~ Unreal|unreal ]] && [[ -d ~/Library/Application\ Support/Epic ]] && files_to_clean+=("$HOME/Library/Application Support/Epic")
|
|
[[ "$app_name" =~ Godot|godot ]] && [[ -d ~/Library/Application\ Support/Godot ]] && files_to_clean+=("$HOME/Library/Application Support/Godot")
|
|
|
|
# 6. Tools
|
|
[[ "$bundle_id" =~ microsoft.*vscode ]] && [[ -d ~/.vscode ]] && files_to_clean+=("$HOME/.vscode")
|
|
[[ "$app_name" =~ Docker ]] && [[ -d ~/.docker ]] && files_to_clean+=("$HOME/.docker")
|
|
|
|
# Output results
|
|
if [[ ${#files_to_clean[@]} -gt 0 ]]; then
|
|
printf '%s\n' "${files_to_clean[@]}"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Locate system-level application files
|
|
find_app_system_files() {
|
|
local bundle_id="$1"
|
|
local app_name="$2"
|
|
local -a system_files=()
|
|
|
|
# Sanitized App Name (remove spaces)
|
|
local nospace_name="${app_name// /}"
|
|
|
|
# Standard system path patterns
|
|
local -a system_patterns=(
|
|
"/Library/Application Support/$app_name"
|
|
"/Library/Application Support/$bundle_id"
|
|
"/Library/LaunchAgents/$bundle_id.plist"
|
|
"/Library/LaunchDaemons/$bundle_id.plist"
|
|
"/Library/Preferences/$bundle_id.plist"
|
|
"/Library/Receipts/$bundle_id.bom"
|
|
"/Library/Receipts/$bundle_id.plist"
|
|
"/Library/Frameworks/$app_name.framework"
|
|
"/Library/Internet Plug-Ins/$app_name.plugin"
|
|
"/Library/Audio/Plug-Ins/Components/$app_name.component"
|
|
"/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
|
"/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
|
"/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
|
"/Library/QuickLook/$app_name.qlgenerator"
|
|
"/Library/PreferencePanes/$app_name.prefPane"
|
|
"/Library/Screen Savers/$app_name.saver"
|
|
"/Library/Caches/$bundle_id"
|
|
"/Library/Caches/$app_name"
|
|
)
|
|
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
system_patterns+=(
|
|
"/Library/Application Support/$nospace_name"
|
|
"/Library/Caches/$nospace_name"
|
|
"/Library/Logs/$nospace_name"
|
|
)
|
|
fi
|
|
|
|
# Process patterns
|
|
for p in "${system_patterns[@]}"; do
|
|
[[ ! -e "$p" ]] && continue
|
|
|
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
|
case "$p" in
|
|
/Library/Application\ Support | /Library/Application\ Support/ | \
|
|
/Library/Caches | /Library/Caches/ | \
|
|
/Library/Logs | /Library/Logs/)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
system_files+=("$p")
|
|
done
|
|
|
|
# System LaunchAgents/LaunchDaemons by name
|
|
if [[ ${#app_name} -gt 3 ]]; then
|
|
for base in /Library/LaunchAgents /Library/LaunchDaemons; do
|
|
[[ -d "$base" ]] && while IFS= read -r -d '' plist; do
|
|
system_files+=("$plist")
|
|
done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
|
done
|
|
fi
|
|
|
|
# Privileged Helper Tools and Receipts (special handling)
|
|
# Only search with bundle_id if it's valid (not empty and not "unknown")
|
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
|
[[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do
|
|
system_files+=("$helper")
|
|
done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
|
|
[[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do
|
|
system_files+=("$receipt")
|
|
done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
fi
|
|
|
|
if [[ ${#system_files[@]} -gt 0 ]]; then
|
|
printf '%s\n' "${system_files[@]}"
|
|
fi
|
|
|
|
# Find files from receipts (Deep Scan)
|
|
find_app_receipt_files "$bundle_id"
|
|
}
|
|
|
|
# Locate files using installation receipts (BOM)
|
|
find_app_receipt_files() {
|
|
local bundle_id="$1"
|
|
|
|
# Skip if no bundle ID
|
|
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
|
|
|
local -a receipt_files=()
|
|
local -a bom_files=()
|
|
|
|
# Find receipts matching the bundle ID
|
|
# Usually in /var/db/receipts/
|
|
if [[ -d /private/var/db/receipts ]]; then
|
|
while IFS= read -r -d '' bom; do
|
|
bom_files+=("$bom")
|
|
done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2> /dev/null)
|
|
fi
|
|
|
|
# Process bom files if any found
|
|
if [[ ${#bom_files[@]} -gt 0 ]]; then
|
|
for bom_file in "${bom_files[@]}"; do
|
|
[[ ! -f "$bom_file" ]] && continue
|
|
|
|
# Parse bom file
|
|
# lsbom -f: file paths only
|
|
# -s: suppress output (convert to text)
|
|
local bom_content
|
|
bom_content=$(lsbom -f -s "$bom_file" 2> /dev/null)
|
|
|
|
while IFS= read -r file_path; do
|
|
# Standardize path (remove leading dot)
|
|
local clean_path="${file_path#.}"
|
|
|
|
# Ensure absolute path
|
|
if [[ "$clean_path" != /* ]]; then
|
|
clean_path="/$clean_path"
|
|
fi
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Safety check: restrict removal to trusted paths
|
|
# ------------------------------------------------------------------------
|
|
local is_safe=false
|
|
|
|
# Whitelisted prefixes
|
|
case "$clean_path" in
|
|
/Applications/*) is_safe=true ;;
|
|
/Users/*) is_safe=true ;;
|
|
/usr/local/*) is_safe=true ;;
|
|
/opt/*) is_safe=true ;;
|
|
/Library/*)
|
|
# Filter sub-paths in /Library to avoid system damage
|
|
# Allow safely: Application Support, Caches, Logs, Preferences
|
|
case "$clean_path" in
|
|
/Library/Application\ Support/*) is_safe=true ;;
|
|
/Library/Caches/*) is_safe=true ;;
|
|
/Library/Logs/*) is_safe=true ;;
|
|
/Library/Preferences/*) is_safe=true ;;
|
|
/Library/PrivilegedHelperTools/*) is_safe=true ;;
|
|
/Library/LaunchAgents/*) is_safe=true ;;
|
|
/Library/LaunchDaemons/*) is_safe=true ;;
|
|
/Library/Internet\ Plug-Ins/*) is_safe=true ;;
|
|
/Library/Audio/Plug-Ins/*) is_safe=true ;;
|
|
/Library/Extensions/*) is_safe=false ;; # Default unsafe
|
|
*) is_safe=false ;;
|
|
esac
|
|
;;
|
|
esac
|
|
|
|
# Hard blocks
|
|
case "$clean_path" in
|
|
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/*) is_safe=false ;;
|
|
esac
|
|
|
|
if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then
|
|
# If lsbom lists /Applications, skip to avoid system damage.
|
|
# Extra check: path must be deep enough?
|
|
# If path is just "/Applications", skip.
|
|
if [[ "$clean_path" == "/Applications" || "$clean_path" == "/Library" || "$clean_path" == "/usr/local" ]]; then
|
|
continue
|
|
fi
|
|
|
|
receipt_files+=("$clean_path")
|
|
fi
|
|
|
|
done <<< "$bom_content"
|
|
done
|
|
fi
|
|
if [[ ${#receipt_files[@]} -gt 0 ]]; then
|
|
printf '%s\n' "${receipt_files[@]}"
|
|
fi
|
|
}
|
|
|
|
# Terminate a running application
|
|
force_kill_app() {
|
|
# Gracefully terminates or force-kills an application
|
|
local app_name="$1"
|
|
local app_path="${2:-""}"
|
|
|
|
# Get the executable name from bundle if app_path is provided
|
|
local exec_name=""
|
|
if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then
|
|
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
|
fi
|
|
|
|
# Use executable name for precise matching, fallback to app name
|
|
local match_pattern="${exec_name:-$app_name}"
|
|
|
|
# Check if process is running using exact match only
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
# Try graceful termination first
|
|
pkill -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
|
|
# Check again after graceful kill
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
# Force kill if still running
|
|
pkill -9 -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
|
|
# If still running and sudo is available, try with sudo
|
|
if pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
if sudo -n true 2> /dev/null; then
|
|
sudo pkill -9 -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
fi
|
|
fi
|
|
|
|
# Final check with longer timeout for stubborn processes
|
|
local retries=3
|
|
while [[ $retries -gt 0 ]]; do
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
((retries--))
|
|
done
|
|
|
|
# Still running after all attempts
|
|
pgrep -x "$match_pattern" > /dev/null 2>&1 && return 1 || return 0
|
|
}
|
|
|
|
# Note: calculate_total_size() is defined in lib/core/file_ops.sh
|