2025-12-08 15:33:52 +08:00
#!/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 "
# ============================================================================
2025-12-18 17:35:54 +08:00
# Application Management
2025-12-08 15:33:52 +08:00
# ============================================================================
2025-12-18 17:35:54 +08:00
# Critical system components protected from uninstallation
2025-12-08 15:33:52 +08:00
readonly SYSTEM_CRITICAL_BUNDLES = (
"com.apple.*" # System essentials
"loginwindow"
"dock"
"systempreferences"
"finder"
"safari"
2025-12-18 09:34:38 +08:00
"com.apple.Settings*"
"com.apple.SystemSettings*"
"com.apple.controlcenter*"
2025-12-12 20:17:47 +08:00
"com.apple.backgroundtaskmanagement*"
"com.apple.loginitems*"
"com.apple.sharedfilelist*"
"com.apple.sfl*"
"backgroundtaskmanagementagent"
2025-12-08 15:33:52 +08:00
"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"
)
2025-12-18 17:35:54 +08:00
# Applications with sensitive data; protected during cleanup but removable
2025-12-08 15:33:52 +08:00
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
2025-12-14 08:53:19 +08:00
# ============================================================================
# AI & LLM Tools
# ============================================================================
2025-12-14 00:53:52 +00:00
"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
2025-12-14 08:53:19 +08:00
"com.huggingface.huggingchat" # HuggingChat
2025-12-14 00:53:52 +00:00
"Gemini" # Gemini
"com.perplexity.Perplexity" # Perplexity
"com.drawthings.DrawThings" # Draw Things
2025-12-14 08:53:19 +08:00
"com.divamgupta.diffusionbee" # DiffusionBee
2025-12-14 00:53:52 +00:00
"com.exafunction.windsurf" # Windsurf
"com.quora.poe.electron" # Poe
"chat.openai.com.*" # OpenAI web wrappers
2025-12-14 08:53:19 +08:00
2025-12-08 15:33:52 +08:00
# ============================================================================
# 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)
2025-12-18 10:42:13 +08:00
# ============================================================================
2025-12-18 17:35:54 +08:00
# Network Proxy & VPN Tools (pattern-based protection)
2025-12-18 10:42:13 +08:00
# ============================================================================
# Clash variants
2025-12-08 15:33:52 +08:00
"*clash*" # All Clash variants (ClashX, ClashX Pro, Clash Verge, etc)
"*Clash*" # Capitalized variants
"com.nssurge.surge-mac" # Surge
2025-12-18 10:42:13 +08:00
"*surge*" # Surge variants
"*Surge*" # Surge variants
2025-12-08 15:33:52 +08:00
"mihomo*" # Mihomo Party and variants
"*openvpn*" # OpenVPN Connect and variants
"*OpenVPN*" # OpenVPN capitalized variants
2025-12-18 10:42:13 +08:00
"net.openvpn.*" # OpenVPN bundle IDs
2025-12-13 12:16:25 +08:00
# Proxy Clients (Shadowsocks, V2Ray, etc)
2025-12-13 04:16:55 +00:00
"*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
2025-12-13 12:16:25 +08:00
# Mesh & Corporate VPNs
2025-12-13 04:16:55 +00:00
"*tailscale*" # Tailscale
"io.tailscale.*" # Tailscale bundle
"*zerotier*" # ZeroTier
"com.zerotier.*" # ZeroTier bundle
"*1dot1dot1dot1*" # Cloudflare WARP
"*cloudflare*warp*" # Cloudflare WARP
2025-12-13 12:16:25 +08:00
# Commercial VPNs
2025-12-13 04:16:55 +00:00
"*nordvpn*" # NordVPN
"*expressvpn*" # ExpressVPN
"*protonvpn*" # ProtonVPN
"*surfshark*" # Surfshark
"*windscribe*" # Windscribe
"*mullvad*" # Mullvad
2025-12-13 12:16:25 +08:00
"*privateinternetaccess*" # PIA
2025-12-18 10:42:13 +08:00
# Screensaver & Dynamic Wallpaper
"*Aerial*" # Aerial screensaver (all case variants)
"*aerial*" # Aerial lowercase
"*Fliqlo*" # Fliqlo screensaver (all case variants)
"*fliqlo*" # Fliqlo lowercase
2025-12-08 15:33:52 +08:00
# ============================================================================
# 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
2025-12-26 11:58:37 +00:00
"app.legcord.Legcord" # Legcord
2025-12-08 15:33:52 +08:00
"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
# ============================================================================
2025-12-13 13:14:08 +00:00
"com.spotify.client" # Spotify
"com.apple.Music" # Apple Music
"com.apple.podcasts" # Apple Podcasts
2025-12-13 21:13:28 +08:00
"com.apple.BKAgentService" # Apple Books (Agent)
2025-12-13 13:14:08 +00:00
"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
2025-12-08 15:33:52 +08:00
# ============================================================================
# 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)
)
2025-12-22 11:24:04 +08:00
# 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
2025-12-22 03:24:39 +00:00
*backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*)
2025-12-22 11:24:04 +08:00
return 0
; ;
*)
return 1
; ;
esac
}
2025-12-08 15:33:52 +08:00
# 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 [@] } " )
2025-12-18 17:35:54 +08:00
# Check if bundle ID matches pattern (glob support)
2025-12-08 15:33:52 +08:00
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
}
2025-12-18 17:35:54 +08:00
# Check if application is a protected system component
2025-12-08 15:33:52 +08:00
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
}
2025-12-18 17:35:54 +08:00
# Check if application data should be protected during cleanup
2025-12-08 15:33:52 +08:00
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
}
2025-12-18 17:35:54 +08:00
# Check if a path is protected from deletion
2025-12-18 10:42:13 +08:00
# 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:]' )
2025-12-18 17:35:54 +08:00
# 1. Keyword-based matching for system components
2025-12-18 10:42:13 +08:00
# Protect System Settings, Preferences, Control Center, and related XPC services
2025-12-18 15:20:26 +08:00
# Also protect "Settings" (used in macOS Sequoia) and savedState files
2025-12-18 10:42:13 +08:00
if [ [ " $path_lower " = ~ systemsettings || " $path_lower " = ~ systempreferences || " $path_lower " = ~ controlcenter ] ] ; then
return 0
fi
2025-12-18 15:20:26 +08:00
# Additional check for com.apple.Settings (macOS Sequoia System Settings)
if [ [ " $path_lower " = ~ com\. apple\. settings ] ] ; then
return 0
fi
2025-12-29 00:29:42 +08:00
# Protect Notes cache (search index issues)
if [ [ " $path_lower " = ~ com\. apple\. notes ] ] ; then
return 0
fi
2025-12-18 17:35:54 +08:00
# 2. Protect caches critical for system UI rendering
2025-12-18 10:42:13 +08:00
# 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
; ;
2025-12-18 17:19:18 +08:00
# 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
; ;
2025-12-18 10:42:13 +08:00
esac
2025-12-18 17:35:54 +08:00
# 3. Extract bundle ID from sandbox paths
2025-12-18 10:42:13 +08:00
# 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
2025-12-18 17:02:04 +08:00
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
2025-12-18 10:42:13 +08:00
return 0
; ;
esac
2025-12-28 21:30:39 +08:00
# 5. Protect critical preference files and user data
2025-12-18 17:02:04 +08:00
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
; ;
2025-12-28 21:30:39 +08:00
# iCloud Drive - protect user's cloud synced data
*/Library/Mobile\ Documents* | */Mobile\ Documents*)
return 0
; ;
2025-12-18 17:02:04 +08:00
esac
2025-12-18 17:35:54 +08:00
# 6. Match full path against protected patterns
2025-12-18 10:42:13 +08:00
# 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
2025-12-18 17:02:04 +08:00
# 7. Check if the filename itself matches any protected patterns
2025-12-18 10:42:13 +08:00
local filename
filename = $( basename " $path " )
if should_protect_data " $filename " ; then
return 0
fi
return 1
}
2025-12-28 09:21:04 +08:00
# 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 " ] ] ||
2025-12-28 01:40:26 +00:00
[ [ " $normalized_target " = = $expanded_pattern ] ] ; then
2025-12-28 09:21:04 +08:00
return 0
fi
done
return 1
}
2025-12-18 17:35:54 +08:00
# Locate files associated with an application
2025-12-08 15:33:52 +08:00
find_app_files( ) {
local bundle_id = " $1 "
local app_name = " $2 "
local -a files_to_clean = ( )
2025-12-18 17:35:54 +08:00
# Normalize app name for matching
2025-12-18 17:02:04 +08:00
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
2025-12-11 15:57:30 +08:00
if [ [ ${# app_name } -gt 3 && " $app_name " = ~ [ [ :space:] ] ] ] ; then
2025-12-18 17:02:04 +08:00
user_patterns += (
" $HOME /Library/Application Support/ $nospace_name "
" $HOME /Library/Caches/ $nospace_name "
" $HOME /Library/Logs/ $nospace_name "
" $HOME /Library/Application Support/ $underscore_name "
)
2025-12-11 15:57:30 +08:00
fi
2025-12-18 17:02:04 +08:00
# Process standard patterns
for p in " ${ user_patterns [@] } " ; do
local expanded_path = " ${ p /# \~ / $HOME } "
2025-12-25 11:45:52 +08:00
# Skip if path doesn't exist
[ [ ! -e " $expanded_path " ] ] && continue
2025-12-25 03:50:34 +00:00
2025-12-25 11:45:52 +08:00
# 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/ | \
2025-12-25 03:50:34 +00:00
*/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/)
2025-12-25 11:45:52 +08:00
continue
; ;
esac
2025-12-25 03:50:34 +00:00
2025-12-25 11:45:52 +08:00
files_to_clean += ( " $expanded_path " )
2025-12-18 17:02:04 +08:00
done
2025-12-08 15:33:52 +08:00
2025-12-25 11:45:52 +08:00
# 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
2025-12-18 17:02:04 +08:00
fi
2025-12-08 15:33:52 +08:00
2025-12-18 17:02:04 +08:00
# Launch Agents by name (special handling)
if [ [ ${# app_name } -gt 3 ] ] && [ [ -d ~/Library/LaunchAgents ] ] ; then
2025-12-11 15:57:30 +08:00
while IFS = read -r -d '' plist; do
files_to_clean += ( " $plist " )
2025-12-18 17:02:04 +08:00
done < <( command find ~/Library/LaunchAgents -maxdepth 1 \( -name " * $app_name *.plist " \) -print0 2> /dev/null)
2025-12-11 15:57:30 +08:00
fi
2025-12-08 15:33:52 +08:00
2025-12-18 17:35:54 +08:00
# Handle specialized toolchains and development environments
2025-12-18 17:02:04 +08:00
# 1. DevEco-Studio (Huawei)
2025-12-08 16:51:54 +08:00
if [ [ " $app_name " = ~ DevEco| deveco ] ] || [ [ " $bundle_id " = ~ huawei.*deveco ] ] ; then
2025-12-18 17:02:04 +08:00
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
2025-12-08 16:51:54 +08:00
fi
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# 2. Android Studio (Google)
2025-12-08 16:51:54 +08:00
if [ [ " $app_name " = ~ Android.*Studio| android.*studio ] ] || [ [ " $bundle_id " = ~ google.*android.*studio| jetbrains.*android ] ] ; then
2025-12-18 17:02:04 +08:00
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)
2025-12-08 16:51:54 +08:00
fi
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# 3. Xcode (Apple)
2025-12-08 16:51:54 +08:00
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
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# 4. JetBrains (IDE settings)
2025-12-08 16:51:54 +08:00
if [ [ " $bundle_id " = ~ jetbrains ] ] || [ [ " $app_name " = ~ IntelliJ| PyCharm| WebStorm| GoLand| RubyMine| PhpStorm| CLion| DataGrip| Rider ] ] ; then
2025-12-18 17:02:04 +08:00
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
2025-12-08 16:51:54 +08:00
fi
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# 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 " )
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# 6. Tools
[ [ " $bundle_id " = ~ microsoft.*vscode ] ] && [ [ -d ~/.vscode ] ] && files_to_clean += ( " $HOME /.vscode " )
[ [ " $app_name " = ~ Docker ] ] && [ [ -d ~/.docker ] ] && files_to_clean += ( " $HOME /.docker " )
2025-12-08 18:59:52 +08:00
2025-12-18 17:02:04 +08:00
# Output results
2025-12-25 11:45:52 +08:00
if [ [ ${# files_to_clean [@] } -gt 0 ] ] ; then
printf '%s\n' " ${ files_to_clean [@] } "
fi
return 0
2025-12-08 15:33:52 +08:00
}
2025-12-18 17:35:54 +08:00
# Locate system-level application files
2025-12-08 15:33:52 +08:00
find_app_system_files( ) {
local bundle_id = " $1 "
local app_name = " $2 "
local -a system_files = ( )
2025-12-11 15:57:30 +08:00
# Sanitized App Name (remove spaces)
2025-12-18 17:02:04 +08:00
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 "
)
2025-12-11 15:57:30 +08:00
if [ [ ${# app_name } -gt 3 && " $app_name " = ~ [ [ :space:] ] ] ] ; then
2025-12-18 17:02:04 +08:00
system_patterns += (
" /Library/Application Support/ $nospace_name "
" /Library/Caches/ $nospace_name "
" /Library/Logs/ $nospace_name "
)
2025-12-11 15:57:30 +08:00
fi
2025-12-08 15:33:52 +08:00
2025-12-18 17:02:04 +08:00
# Process patterns
for p in " ${ system_patterns [@] } " ; do
2025-12-25 11:45:52 +08:00
[ [ ! -e " $p " ] ] && continue
2025-12-25 03:50:34 +00:00
2025-12-25 11:45:52 +08:00
# 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/ | \
2025-12-25 03:50:34 +00:00
/Library/Caches | /Library/Caches/ | \
/Library/Logs | /Library/Logs/)
2025-12-25 11:45:52 +08:00
continue
; ;
esac
2025-12-25 03:50:34 +00:00
2025-12-25 11:45:52 +08:00
system_files += ( " $p " )
2025-12-18 17:02:04 +08:00
done
2025-12-08 15:33:52 +08:00
2025-12-18 17:02:04 +08:00
# System LaunchAgents/LaunchDaemons by name
2025-12-11 15:57:30 +08:00
if [ [ ${# app_name } -gt 3 ] ] ; then
2025-12-18 17:02:04 +08:00
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
2025-12-11 15:57:30 +08:00
fi
2025-12-08 15:33:52 +08:00
2025-12-18 17:02:04 +08:00
# Privileged Helper Tools and Receipts (special handling)
2025-12-25 11:45:52 +08:00
# 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
2025-12-08 15:33:52 +08:00
2025-12-25 11:45:52 +08:00
if [ [ ${# system_files [@] } -gt 0 ] ] ; then
printf '%s\n' " ${ system_files [@] } "
fi
2025-12-12 14:10:36 +08:00
# Find files from receipts (Deep Scan)
find_app_receipt_files " $bundle_id "
}
2025-12-18 17:35:54 +08:00
# Locate files using installation receipts (BOM)
2025-12-12 14:10:36 +08:00
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 " )
2025-12-18 17:02:04 +08:00
done < <( find /private/var/db/receipts -maxdepth 1 -name " ${ bundle_id } *.bom " -print0 2> /dev/null)
2025-12-12 14:10:36 +08:00
fi
2025-12-12 14:33:07 +08:00
# 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 #. } "
2025-12-18 17:35:54 +08:00
# Ensure absolute path
2025-12-12 14:33:07 +08:00
if [ [ " $clean_path " != /* ] ] ; then
clean_path = " / $clean_path "
fi
2025-12-12 14:10:36 +08:00
2025-12-12 14:33:07 +08:00
# ------------------------------------------------------------------------
2025-12-18 17:35:54 +08:00
# Safety check: restrict removal to trusted paths
2025-12-12 14:33:07 +08:00
# ------------------------------------------------------------------------
local is_safe = false
2025-12-12 14:10:36 +08:00
2025-12-12 14:33:07 +08:00
# Whitelisted prefixes
case " $clean_path " in
/Applications/*) is_safe = true ; ;
2025-12-12 14:36:17 +08:00
/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
2025-12-18 17:35:54 +08:00
# If lsbom lists /Applications, skip to avoid system damage.
2025-12-12 14:36:17 +08:00
# 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 " )
2025-12-12 14:10:36 +08:00
fi
2025-12-12 14:36:17 +08:00
done <<< " $bom_content "
done
2025-12-12 14:33:07 +08:00
fi
2025-12-12 14:10:36 +08:00
if [ [ ${# receipt_files [@] } -gt 0 ] ] ; then
printf '%s\n' " ${ receipt_files [@] } "
fi
2025-12-08 15:33:52 +08:00
}
2025-12-18 17:35:54 +08:00
# Terminate a running application
2025-12-08 15:33:52 +08:00
force_kill_app( ) {
2025-12-18 17:35:54 +08:00
# Gracefully terminates or force-kills an application
2025-12-08 15:33:52 +08:00
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
}
2025-12-25 11:24:12 +08:00
# Note: calculate_total_size() is defined in lib/core/file_ops.sh