mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 18:34:46 +00:00
Added system optimization features
This commit is contained in:
3
.github/workflows/shell-quality-checks.yml
vendored
3
.github/workflows/shell-quality-checks.yml
vendored
@@ -29,3 +29,6 @@ jobs:
|
||||
|
||||
- name: Build Go disk analyzer
|
||||
run: mkdir -p bin && go build -o bin/analyze-go ./cmd/analyze
|
||||
|
||||
- name: Build Go optimizer
|
||||
run: mkdir -p bin && go build -o bin/optimize-go ./cmd/optimize
|
||||
|
||||
23
README.md
23
README.md
@@ -23,6 +23,7 @@
|
||||
- **Thorough Uninstall** - Scans 22+ locations to remove app leftovers, not just the .app file
|
||||
- **Interactive Disk Analyzer** - Navigate folders with arrow keys, find and delete large files quickly
|
||||
- **Fast & Lightweight** - Terminal-based with arrow-key navigation, pagination, and Touch ID support
|
||||
- **System Optimization** - Rebuilds caches, resets services, and trims swap/network cruft with one run
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -46,6 +47,7 @@ mo clean # System cleanup
|
||||
mo clean --dry-run # Preview mode
|
||||
mo clean --whitelist # Manage protected caches
|
||||
mo uninstall # Uninstall apps
|
||||
mo optimize # System optimization
|
||||
mo analyze # Disk analyzer
|
||||
|
||||
mo touchid # Configure Touch ID for sudo
|
||||
@@ -104,6 +106,27 @@ Space freed: 95.50GB | Free space now: 223.5GB
|
||||
====================================================================
|
||||
```
|
||||
|
||||
### System Optimization
|
||||
|
||||
```bash
|
||||
$ mo optimize
|
||||
|
||||
System: 5/32 GB RAM | 333/460 GB Disk (72%) | Uptime 6d
|
||||
|
||||
▶ System Maintenance - Rebuild system databases & flush caches
|
||||
▶ Network Services - Reset network services
|
||||
▶ Finder & Dock Refresh - Clear Finder/Dock caches and restart
|
||||
▶ Diagnostics Cleanup - Purge old diagnostic & crash logs
|
||||
▶ Mail Downloads - Recover Mail attachment space
|
||||
▶ Memory & Swap - Purge swapfiles, restart dynamic pager
|
||||
|
||||
====================================================================
|
||||
System optimization completed
|
||||
Automations: 8 sections optimized end-to-end.
|
||||
Highlights: caches refreshed, services restarted, startup assets rebuilt.
|
||||
====================================================================
|
||||
```
|
||||
|
||||
### Smart App Uninstaller
|
||||
|
||||
```bash
|
||||
|
||||
36
bin/clean.sh
36
bin/clean.sh
@@ -378,7 +378,31 @@ clean_ds_store_tree() {
|
||||
|
||||
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
|
||||
|
||||
# Build exclusion paths for find (skip common slow/large directories)
|
||||
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
|
||||
)
|
||||
|
||||
# Limit depth for HOME to avoid slow scans
|
||||
local max_depth=""
|
||||
if [[ "$target" == "$HOME" ]]; then
|
||||
max_depth="-maxdepth 5"
|
||||
fi
|
||||
|
||||
# Find .DS_Store files with exclusions and depth limit
|
||||
while IFS= read -r -d '' ds_file; do
|
||||
local size
|
||||
size=$(stat -f%z "$ds_file" 2> /dev/null || echo 0)
|
||||
@@ -387,7 +411,17 @@ clean_ds_store_tree() {
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
rm -f "$ds_file" 2> /dev/null || true
|
||||
fi
|
||||
done < <(find "$target" -type f -name '.DS_Store' -print0 2> /dev/null)
|
||||
|
||||
# Stop after 500 files to avoid hanging
|
||||
if [[ $file_count -ge 500 ]]; then
|
||||
break
|
||||
fi
|
||||
done < <(find "$target" $max_depth "${exclude_paths[@]}" -type f -name '.DS_Store' -print0 2> /dev/null)
|
||||
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
echo -ne "\r\033[K"
|
||||
fi
|
||||
|
||||
if [[ $file_count -gt 0 ]]; then
|
||||
local size_human
|
||||
|
||||
BIN
bin/optimize-go
Executable file
BIN
bin/optimize-go
Executable file
Binary file not shown.
550
bin/optimize.sh
Executable file
550
bin/optimize.sh
Executable file
@@ -0,0 +1,550 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Load common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
# Path to optimize-go binary
|
||||
OPTIMIZE_GO="$SCRIPT_DIR/bin/optimize-go"
|
||||
|
||||
# Colors and icons from common.sh
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${PURPLE}Optimize Your Mac${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
show_system_health() {
|
||||
local health_json="$1"
|
||||
|
||||
# Parse system health using jq
|
||||
local mem_used=$(echo "$health_json" | jq -r '.memory_used_gb')
|
||||
local mem_total=$(echo "$health_json" | jq -r '.memory_total_gb')
|
||||
local disk_used=$(echo "$health_json" | jq -r '.disk_used_gb')
|
||||
local disk_total=$(echo "$health_json" | jq -r '.disk_total_gb')
|
||||
local disk_percent=$(echo "$health_json" | jq -r '.disk_used_percent')
|
||||
local uptime=$(echo "$health_json" | jq -r '.uptime_days')
|
||||
|
||||
# Compact one-line format
|
||||
printf "System: %.0f/%.0f GB RAM | %.0f/%.0f GB Disk (%.0f%%) | Uptime %.0fd\n" \
|
||||
"$mem_used" "$mem_total" "$disk_used" "$disk_total" "$disk_percent" "$uptime"
|
||||
echo ""
|
||||
}
|
||||
|
||||
parse_optimizations() {
|
||||
local health_json="$1"
|
||||
|
||||
# Extract optimizations array
|
||||
echo "$health_json" | jq -c '.optimizations[]' 2> /dev/null
|
||||
}
|
||||
|
||||
announce_action() {
|
||||
local name="$1"
|
||||
local desc="$2"
|
||||
local kind="$3"
|
||||
|
||||
local badge=""
|
||||
if [[ "$kind" == "confirm" ]]; then
|
||||
badge="${YELLOW}[Confirm]${NC} "
|
||||
fi
|
||||
|
||||
local line="${BLUE}${ICON_ARROW}${NC} ${badge}${name}"
|
||||
if [[ -n "$desc" ]]; then
|
||||
line+=" ${GRAY}- ${desc}${NC}"
|
||||
fi
|
||||
|
||||
if ${first_heading:-true}; then
|
||||
first_heading=false
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -e "$line"
|
||||
}
|
||||
|
||||
touchid_configured() {
|
||||
local pam_file="/etc/pam.d/sudo"
|
||||
[[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2> /dev/null
|
||||
}
|
||||
|
||||
touchid_supported() {
|
||||
if command -v bioutil > /dev/null 2>&1; then
|
||||
bioutil -r 2> /dev/null | grep -q "Touch ID" && return 0
|
||||
fi
|
||||
[[ "$(uname -m)" == "arm64" ]]
|
||||
}
|
||||
|
||||
cleanup_path() {
|
||||
local raw_path="$1"
|
||||
local label="$2"
|
||||
|
||||
local expanded_path="${raw_path/#\~/$HOME}"
|
||||
if [[ ! -e "$expanded_path" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label"
|
||||
return
|
||||
fi
|
||||
|
||||
local size_kb
|
||||
size_kb=$(du -sk "$expanded_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
local size_display=""
|
||||
if [[ "$size_kb" =~ ^[0-9]+$ && "$size_kb" -gt 0 ]]; then
|
||||
size_display=$(bytes_to_human "$((size_kb * 1024))")
|
||||
fi
|
||||
|
||||
if rm -rf "$expanded_path"; then
|
||||
if [[ -n "$size_display" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label"
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $label"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_directory() {
|
||||
local raw_path="$1"
|
||||
local expanded_path="${raw_path/#\~/$HOME}"
|
||||
mkdir -p "$expanded_path" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
count_local_snapshots() {
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
|
||||
local output
|
||||
output=$(tmutil listlocalsnapshots / 2> /dev/null || true)
|
||||
if [[ -z "$output" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$output" | grep -c "com.apple.TimeMachine." | tr -d ' '
|
||||
}
|
||||
|
||||
execute_optimization() {
|
||||
local action="$1"
|
||||
local path="$2"
|
||||
|
||||
case "$action" in
|
||||
system_maintenance)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..."
|
||||
timeout 10 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user > /dev/null 2>&1 || true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} LaunchServices database rebuilt"
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Flushing DNS cache..."
|
||||
if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} DNS cache flushed"
|
||||
else
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Failed to flush DNS cache"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Purging memory cache..."
|
||||
if sudo purge 2> /dev/null; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Memory cache purged"
|
||||
else
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Failed to purge memory"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding font cache..."
|
||||
sudo atsutil databases -remove > /dev/null 2>&1
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Font cache rebuilt"
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding Spotlight index..."
|
||||
sudo mdutil -E / > /dev/null 2>&1 || true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight index rebuilt"
|
||||
;;
|
||||
|
||||
startup_items)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Opening Launch Agents directory..."
|
||||
open ~/Library/LaunchAgents
|
||||
open /Library/LaunchAgents
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Please review and disable unnecessary startup items"
|
||||
echo -e "${GRAY} Tip: Move unwanted .plist files to trash${NC}"
|
||||
;;
|
||||
|
||||
network_services)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting network services..."
|
||||
if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Network services reset"
|
||||
else
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Failed to reset network services"
|
||||
fi
|
||||
;;
|
||||
|
||||
cache_refresh)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Quick Look cache..."
|
||||
qlmanage -r cache > /dev/null 2>&1 || true
|
||||
qlmanage -r > /dev/null 2>&1 || true
|
||||
|
||||
local -a cache_targets=(
|
||||
"$HOME/Library/Caches/com.apple.QuickLook.thumbnailcache|Quick Look thumbnails"
|
||||
"$HOME/Library/Caches/com.apple.iconservices.store|Icon Services store"
|
||||
"$HOME/Library/Caches/com.apple.iconservices|Icon Services cache"
|
||||
"$HOME/Library/Caches/com.apple.Safari/WebKitCache|Safari WebKit cache"
|
||||
"$HOME/Library/Caches/com.apple.Safari/Favicon|Safari favicon cache"
|
||||
)
|
||||
|
||||
for target in "${cache_targets[@]}"; do
|
||||
IFS='|' read -r target_path label <<< "$target"
|
||||
cleanup_path "$target_path" "$label"
|
||||
done
|
||||
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Finder and Safari caches refreshed"
|
||||
;;
|
||||
|
||||
maintenance_scripts)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Running macOS periodic scripts..."
|
||||
local periodic_cmd="/usr/sbin/periodic"
|
||||
if [[ -x "$periodic_cmd" ]]; then
|
||||
local periodic_output=""
|
||||
if periodic_output=$(sudo "$periodic_cmd" daily weekly monthly 2>&1); then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Daily/weekly/monthly scripts completed"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} periodic scripts reported an issue"
|
||||
printf '%s\n' "$periodic_output" | sed 's/^/ /'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rotating system logs..."
|
||||
if sudo newsyslog > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Log rotation complete"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} newsyslog reported an issue"
|
||||
fi
|
||||
|
||||
if [[ -x "/usr/libexec/repair_packages" ]]; then
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Repairing base system permissions..."
|
||||
if sudo /usr/libexec/repair_packages --repair --standard-pkgs --volume / > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Base system permission repair complete"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} repair_packages reported an issue"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
log_cleanup)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing diagnostic & crash logs..."
|
||||
local -a user_logs=(
|
||||
"$HOME/Library/Logs/DiagnosticReports"
|
||||
"$HOME/Library/Logs/CrashReporter"
|
||||
"$HOME/Library/Logs/corecaptured"
|
||||
)
|
||||
for target in "${user_logs[@]}"; do
|
||||
cleanup_path "$target" "$(basename "$target")"
|
||||
done
|
||||
|
||||
if [[ -d "/Library/Logs/DiagnosticReports" ]]; then
|
||||
sudo find /Library/Logs/DiagnosticReports -type f -name "*.crash" -delete 2> /dev/null || true
|
||||
sudo find /Library/Logs/DiagnosticReports -type f -name "*.panic" -delete 2> /dev/null || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} System diagnostic logs cleared"
|
||||
else
|
||||
echo -e " ${GRAY}-${NC} No system diagnostic logs found"
|
||||
fi
|
||||
;;
|
||||
|
||||
recent_items)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..."
|
||||
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
|
||||
if [[ -d "$shared_dir" ]]; then
|
||||
local removed
|
||||
removed=$(find "$shared_dir" -name "*.sfl2" -type f -print -delete 2> /dev/null | wc -l | tr -d ' ')
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Reset $removed shared file lists"
|
||||
else
|
||||
echo -e " ${GRAY}-${NC} Recent item caches already clean"
|
||||
fi
|
||||
|
||||
rm -f "$HOME/Library/Preferences/com.apple.recentitems.plist" 2> /dev/null || true
|
||||
defaults delete NSGlobalDomain NSRecentDocumentsLimit 2> /dev/null || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Finder/Apple menu recent items cleared"
|
||||
;;
|
||||
|
||||
radio_refresh)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Bluetooth preferences..."
|
||||
rm -f "$HOME/Library/Preferences/com.apple.Bluetooth.plist" 2> /dev/null || true
|
||||
sudo rm -f /Library/Preferences/com.apple.Bluetooth.plist 2> /dev/null || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Bluetooth caches refreshed"
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Wi-Fi configuration..."
|
||||
local sysconfig="/Library/Preferences/SystemConfiguration"
|
||||
if [[ -d "$sysconfig" ]]; then
|
||||
sudo cp "$sysconfig"/com.apple.airport.preferences.plist "$sysconfig"/com.apple.airport.preferences.plist.bak 2> /dev/null || true
|
||||
sudo rm -f "$sysconfig"/com.apple.airport.preferences.plist "$sysconfig"/NetworkInterfaces.plist "$sysconfig"/preferences.plist 2> /dev/null || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Wi-Fi preferences reset"
|
||||
else
|
||||
echo -e " ${GRAY}-${NC} SystemConfiguration directory missing"
|
||||
fi
|
||||
|
||||
sudo ifconfig awdl0 down 2> /dev/null || true
|
||||
sudo ifconfig awdl0 up 2> /dev/null || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Wireless services refreshed"
|
||||
;;
|
||||
|
||||
mail_downloads)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing Mail attachment downloads..."
|
||||
local -a mail_dirs=(
|
||||
"$HOME/Library/Mail Downloads|Mail Downloads"
|
||||
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads|Mail Container Downloads"
|
||||
)
|
||||
for target in "${mail_dirs[@]}"; do
|
||||
IFS='|' read -r target_path label <<< "$target"
|
||||
cleanup_path "$target_path" "$label"
|
||||
ensure_directory "$target_path"
|
||||
done
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Mail downloads cleared"
|
||||
;;
|
||||
|
||||
saved_state_cleanup)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Purging saved application states..."
|
||||
local state_dir="$HOME/Library/Saved Application State"
|
||||
cleanup_path "$state_dir" "Saved Application State"
|
||||
ensure_directory "$state_dir"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Saved states cleared"
|
||||
;;
|
||||
|
||||
finder_dock_refresh)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Finder & Dock caches..."
|
||||
local -a interface_targets=(
|
||||
"$HOME/Library/Caches/com.apple.finder|Finder cache"
|
||||
"$HOME/Library/Caches/com.apple.dock.iconcache|Dock icon cache"
|
||||
)
|
||||
for target in "${interface_targets[@]}"; do
|
||||
IFS='|' read -r target_path label <<< "$target"
|
||||
cleanup_path "$target_path" "$label"
|
||||
done
|
||||
killall Finder > /dev/null 2>&1 || true
|
||||
killall Dock > /dev/null 2>&1 || true
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Finder & Dock relaunched"
|
||||
;;
|
||||
|
||||
swap_cleanup)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Flushing memory caches..."
|
||||
if sudo purge > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Inactive memory purged"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} purge command failed"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Stopping dynamic pager and removing swapfiles..."
|
||||
if sudo launchctl unload /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1; then
|
||||
sudo rm -f /private/var/vm/swapfile* > /dev/null 2>&1 || true
|
||||
sudo touch /private/var/vm/swapfile0 > /dev/null 2>&1 || true
|
||||
sudo chmod 600 /private/var/vm/swapfile0 > /dev/null 2>&1 || true
|
||||
sudo launchctl load /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1 || true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Swap cache rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Could not unload dynamic_pager"
|
||||
fi
|
||||
;;
|
||||
|
||||
login_items)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Listing login items..."
|
||||
osascript -e 'tell application "System Events" to get the name of every login item' 2> /dev/null | sed 's/, /\n • /g; s/^/ • /'
|
||||
echo -e "${GRAY}Use System Settings → General → Login Items to disable entries you don't need.${NC}"
|
||||
;;
|
||||
|
||||
startup_cache)
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding kext caches..."
|
||||
if sudo kextcache -i / > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Kernel/kext caches rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} kextcache reported an issue"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing system prelinked kernel caches..."
|
||||
sudo rm -rf /System/Library/PrelinkedKernels/* > /dev/null 2>&1 || true
|
||||
sudo kextcache -system-prelinked-kernel > /dev/null 2>&1 || true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed"
|
||||
;;
|
||||
|
||||
local_snapshots)
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}!${NC} tmutil not available on this system"
|
||||
return
|
||||
fi
|
||||
|
||||
local before after
|
||||
before=$(count_local_snapshots)
|
||||
if [[ "$before" -eq 0 ]]; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} No local snapshots to thin"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Thinning $before APFS local snapshots..."
|
||||
if sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
|
||||
after=$(count_local_snapshots)
|
||||
local removed=$((before - after))
|
||||
if [[ "$removed" -lt 0 ]]; then
|
||||
removed=0
|
||||
fi
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $removed snapshots (remaining: $after)"
|
||||
else
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Failed to thin local snapshots"
|
||||
fi
|
||||
;;
|
||||
|
||||
developer_cleanup)
|
||||
local -a dev_targets=(
|
||||
"$HOME/Library/Developer/Xcode/DerivedData|Xcode DerivedData"
|
||||
"$HOME/Library/Developer/Xcode/Archives|Build archives"
|
||||
"$HOME/Library/Developer/Xcode/iOS DeviceSupport|iOS Device support files"
|
||||
"$HOME/Library/Developer/CoreSimulator/Caches|CoreSimulator caches"
|
||||
)
|
||||
|
||||
for target in "${dev_targets[@]}"; do
|
||||
IFS='|' read -r target_path label <<< "$target"
|
||||
cleanup_path "$target_path" "$label"
|
||||
done
|
||||
|
||||
if command -v xcrun > /dev/null 2>&1; then
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Removing unavailable simulator runtimes..."
|
||||
if xcrun simctl delete unavailable > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Unavailable simulators removed"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Could not prune simulator runtimes"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Developer caches cleaned"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Unknown action: $action"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main() {
|
||||
print_header
|
||||
|
||||
# Check dependencies
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
log_error "jq is required but not installed. Install with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v bc > /dev/null 2>&1; then
|
||||
log_error "bc is required but not installed. Install with: brew install bc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if optimize-go exists
|
||||
if [[ ! -x "$OPTIMIZE_GO" ]]; then
|
||||
log_error "optimize-go binary not found. Please run: go build -o bin/optimize-go cmd/optimize/main.go"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Collect system health data (silent)
|
||||
local health_json
|
||||
if ! health_json=$("$OPTIMIZE_GO" 2> /dev/null); then
|
||||
log_error "Failed to collect system health data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show system health
|
||||
show_system_health "$health_json"
|
||||
|
||||
# Parse and display optimizations
|
||||
local -a safe_items=()
|
||||
local -a confirm_items=()
|
||||
|
||||
while IFS= read -r opt_json; do
|
||||
[[ -z "$opt_json" ]] && continue
|
||||
|
||||
local name=$(echo "$opt_json" | jq -r '.name')
|
||||
local desc=$(echo "$opt_json" | jq -r '.description')
|
||||
local action=$(echo "$opt_json" | jq -r '.action')
|
||||
local path=$(echo "$opt_json" | jq -r '.path // ""')
|
||||
local safe=$(echo "$opt_json" | jq -r '.safe')
|
||||
|
||||
local item="${name}|${desc}|${action}|${path}"
|
||||
|
||||
if [[ "$safe" == "true" ]]; then
|
||||
safe_items+=("$item")
|
||||
else
|
||||
confirm_items+=("$item")
|
||||
fi
|
||||
done < <(parse_optimizations "$health_json")
|
||||
|
||||
# Simple confirmation
|
||||
echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to optimize, ${GRAY}ESC${NC} to cancel: "
|
||||
|
||||
IFS= read -r -s -n1 key || key=""
|
||||
case "$key" in
|
||||
$'\e' | q | Q)
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "${GRAY}Cancelled${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
"" | $'\n' | $'\r')
|
||||
printf "\r\033[K"
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "${GRAY}Cancelled${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Execute all optimizations
|
||||
local first_heading=true
|
||||
|
||||
# Run safe optimizations
|
||||
if [[ ${#safe_items[@]} -gt 0 ]]; then
|
||||
for item in "${safe_items[@]}"; do
|
||||
IFS='|' read -r name desc action path <<< "$item"
|
||||
announce_action "$name" "$desc" "safe"
|
||||
execute_optimization "$action" "$path"
|
||||
done
|
||||
fi
|
||||
|
||||
# Run confirm items
|
||||
if [[ ${#confirm_items[@]} -gt 0 ]]; then
|
||||
for item in "${confirm_items[@]}"; do
|
||||
IFS='|' read -r name desc action path <<< "$item"
|
||||
announce_action "$name" "$desc" "confirm"
|
||||
execute_optimization "$action" "$path"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local summary_title="System optimization completed"
|
||||
local -a summary_details=()
|
||||
|
||||
local safe_count=${#safe_items[@]}
|
||||
local confirm_count=${#confirm_items[@]}
|
||||
if (( safe_count > 0 )); then
|
||||
summary_details+=("Automations: ${GREEN}${safe_count}${NC} sections optimized end-to-end.")
|
||||
else
|
||||
summary_details+=("Automations: No automated changes were necessary.")
|
||||
fi
|
||||
|
||||
if (( confirm_count > 0 )); then
|
||||
summary_details+=("Follow-ups: ${YELLOW}${confirm_count}${NC} manual checks suggested (see log).")
|
||||
fi
|
||||
|
||||
summary_details+=("Highlights: caches refreshed, services restarted, startup assets rebuilt.")
|
||||
summary_details+=("Result: system responsiveness should feel lighter.")
|
||||
|
||||
local show_touchid_tip="false"
|
||||
if touchid_supported && ! touchid_configured; then
|
||||
show_touchid_tip="true"
|
||||
fi
|
||||
|
||||
if [[ "$show_touchid_tip" == "true" ]]; then
|
||||
echo -e "Tip: run 'mo touchid' to approve sudo via Touch ID."
|
||||
fi
|
||||
print_summary_block "success" "$summary_title" "${summary_details[@]}"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
532
cmd/optimize/main.go
Normal file
532
cmd/optimize/main.go
Normal file
@@ -0,0 +1,532 @@
|
||||
// Mole System Optimizer
|
||||
// System optimization and maintenance
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OptimizationItem struct {
|
||||
Category string `json:"category"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Action string `json:"action"`
|
||||
Safe bool `json:"safe"`
|
||||
}
|
||||
|
||||
type SystemHealth struct {
|
||||
MemoryUsedGB float64 `json:"memory_used_gb"`
|
||||
MemoryTotalGB float64 `json:"memory_total_gb"`
|
||||
DiskUsedGB float64 `json:"disk_used_gb"`
|
||||
DiskTotalGB float64 `json:"disk_total_gb"`
|
||||
DiskUsedPercent float64 `json:"disk_used_percent"`
|
||||
UptimeDays float64 `json:"uptime_days"`
|
||||
Optimizations []OptimizationItem `json:"optimizations"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
health := collectSystemHealth()
|
||||
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(health); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func collectSystemHealth() SystemHealth {
|
||||
health := SystemHealth{
|
||||
Optimizations: []OptimizationItem{},
|
||||
}
|
||||
|
||||
// Collect system info
|
||||
health.MemoryUsedGB, health.MemoryTotalGB = getMemoryInfo()
|
||||
health.DiskUsedGB, health.DiskTotalGB, health.DiskUsedPercent = getDiskInfo()
|
||||
health.UptimeDays = getUptimeDays()
|
||||
|
||||
// System optimizations (always show)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "system",
|
||||
Name: "System Maintenance",
|
||||
Description: "Rebuild system databases & flush caches",
|
||||
Action: "system_maintenance",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Startup items (conditional)
|
||||
if item := checkStartupItems(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
// Network services (always show)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "network",
|
||||
Name: "Network Services",
|
||||
Description: "Reset network services",
|
||||
Action: "network_services",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Cache refresh (always available)
|
||||
if item := buildCacheRefreshItem(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
// macOS maintenance scripts (always available)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "maintenance",
|
||||
Name: "Maintenance Scripts",
|
||||
Description: "Run daily/weekly/monthly scripts & rotate logs",
|
||||
Action: "maintenance_scripts",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Wireless preferences refresh (always available)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "network",
|
||||
Name: "Bluetooth & Wi-Fi Refresh",
|
||||
Description: "Reset wireless preference caches",
|
||||
Action: "radio_refresh",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Recent items cleanup (always available)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "privacy",
|
||||
Name: "Recent Items",
|
||||
Description: "Clear recent apps/documents/servers lists",
|
||||
Action: "recent_items",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Diagnostic log cleanup (always available)
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "system",
|
||||
Name: "Diagnostics Cleanup",
|
||||
Description: "Purge old diagnostic & crash logs",
|
||||
Action: "log_cleanup",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
if item := buildMailDownloadsItem(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
if item := buildSavedStateItem(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "interface",
|
||||
Name: "Finder & Dock Refresh",
|
||||
Description: "Clear Finder/Dock caches and restart",
|
||||
Action: "finder_dock_refresh",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
if item := buildSwapCleanupItem(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
if item := buildLoginItemsItem(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
health.Optimizations = append(health.Optimizations, OptimizationItem{
|
||||
Category: "system",
|
||||
Name: "Startup Cache Rebuild",
|
||||
Description: "Rebuild kext caches & prelinked kernel",
|
||||
Action: "startup_cache",
|
||||
Safe: true,
|
||||
})
|
||||
|
||||
// Local snapshot thinning (conditional)
|
||||
if item := checkLocalSnapshots(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
// Developer-focused cleanup (conditional)
|
||||
if item := checkDeveloperCleanup(); item != nil {
|
||||
health.Optimizations = append(health.Optimizations, *item)
|
||||
}
|
||||
|
||||
return health
|
||||
}
|
||||
|
||||
func getMemoryInfo() (float64, float64) {
|
||||
cmd := exec.Command("sysctl", "-n", "hw.memsize")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
totalBytes, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
totalGB := float64(totalBytes) / (1024 * 1024 * 1024)
|
||||
|
||||
// Get used memory via vm_stat
|
||||
cmd = exec.Command("vm_stat")
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
return 0, totalGB
|
||||
}
|
||||
|
||||
var pageSize int64 = 4096
|
||||
var active, wired, compressed int64
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Pages active:") {
|
||||
active = parseVMStatLine(line)
|
||||
} else if strings.Contains(line, "Pages wired down:") {
|
||||
wired = parseVMStatLine(line)
|
||||
} else if strings.Contains(line, "Pages occupied by compressor:") {
|
||||
compressed = parseVMStatLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
usedBytes := (active + wired + compressed) * pageSize
|
||||
usedGB := float64(usedBytes) / (1024 * 1024 * 1024)
|
||||
|
||||
return usedGB, totalGB
|
||||
}
|
||||
|
||||
func parseVMStatLine(line string) int64 {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return 0
|
||||
}
|
||||
numStr := strings.TrimSuffix(fields[len(fields)-1], ".")
|
||||
num, _ := strconv.ParseInt(numStr, 10, 64)
|
||||
return num
|
||||
}
|
||||
|
||||
func getUptimeDays() float64 {
|
||||
cmd := exec.Command("sysctl", "-n", "kern.boottime")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
line := string(output)
|
||||
if idx := strings.Index(line, "sec = "); idx != -1 {
|
||||
secStr := line[idx+6:]
|
||||
if endIdx := strings.Index(secStr, ","); endIdx != -1 {
|
||||
secStr = secStr[:endIdx]
|
||||
if bootTime, err := strconv.ParseInt(strings.TrimSpace(secStr), 10, 64); err == nil {
|
||||
uptime := time.Now().Unix() - bootTime
|
||||
return float64(uptime) / (24 * 3600)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getDiskInfo() (float64, float64, float64) {
|
||||
var stat syscall.Statfs_t
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "/"
|
||||
}
|
||||
|
||||
if err := syscall.Statfs(home, &stat); err != nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
totalBytes := stat.Blocks * uint64(stat.Bsize)
|
||||
freeBytes := stat.Bfree * uint64(stat.Bsize)
|
||||
usedBytes := totalBytes - freeBytes
|
||||
|
||||
totalGB := float64(totalBytes) / (1024 * 1024 * 1024)
|
||||
usedGB := float64(usedBytes) / (1024 * 1024 * 1024)
|
||||
usedPercent := (float64(usedBytes) / float64(totalBytes)) * 100
|
||||
|
||||
return usedGB, totalGB, usedPercent
|
||||
}
|
||||
|
||||
func checkStartupItems() *OptimizationItem {
|
||||
launchAgentsCount := 0
|
||||
agentsDirs := []string{
|
||||
filepath.Join(os.Getenv("HOME"), "Library/LaunchAgents"),
|
||||
"/Library/LaunchAgents",
|
||||
}
|
||||
|
||||
for _, dir := range agentsDirs {
|
||||
if entries, err := os.ReadDir(dir); err == nil {
|
||||
launchAgentsCount += len(entries)
|
||||
}
|
||||
}
|
||||
|
||||
if launchAgentsCount > 5 {
|
||||
suggested := launchAgentsCount / 2
|
||||
if suggested < 1 {
|
||||
suggested = 1
|
||||
}
|
||||
return &OptimizationItem{
|
||||
Category: "startup",
|
||||
Name: "Startup Items",
|
||||
Description: fmt.Sprintf("%d items (suggest disable %d)", launchAgentsCount, suggested),
|
||||
Action: "startup_items",
|
||||
Safe: false,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCacheRefreshItem() *OptimizationItem {
|
||||
desc := "Refresh Finder previews, Quick Look, and Safari caches"
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
cacheDir := filepath.Join(home, "Library", "Caches")
|
||||
if sizeKB := dirSizeKB(cacheDir); sizeKB > 0 {
|
||||
desc = fmt.Sprintf("Refresh %s of Finder/Safari caches", formatSizeFromKB(sizeKB))
|
||||
}
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "cache",
|
||||
Name: "User Cache Refresh",
|
||||
Description: desc,
|
||||
Action: "cache_refresh",
|
||||
Safe: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildMailDownloadsItem() *OptimizationItem {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
filepath.Join(home, "Library", "Mail Downloads"),
|
||||
filepath.Join(home, "Library", "Containers", "com.apple.mail", "Data", "Library", "Mail Downloads"),
|
||||
}
|
||||
|
||||
var totalKB int64
|
||||
for _, dir := range dirs {
|
||||
totalKB += dirSizeKB(dir)
|
||||
}
|
||||
|
||||
if totalKB == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "applications",
|
||||
Name: "Mail Downloads",
|
||||
Description: fmt.Sprintf("Recover %s of Mail attachments", formatSizeFromKB(totalKB)),
|
||||
Action: "mail_downloads",
|
||||
Safe: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildSavedStateItem() *OptimizationItem {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateDir := filepath.Join(home, "Library", "Saved Application State")
|
||||
sizeKB := dirSizeKB(stateDir)
|
||||
if sizeKB == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "system",
|
||||
Name: "Saved State",
|
||||
Description: fmt.Sprintf("Clear %s of stale saved states", formatSizeFromKB(sizeKB)),
|
||||
Action: "saved_state_cleanup",
|
||||
Safe: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildSwapCleanupItem() *OptimizationItem {
|
||||
swapGlob := "/private/var/vm/swapfile*"
|
||||
matches, err := filepath.Glob(swapGlob)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var totalKB int64
|
||||
for _, file := range matches {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
totalKB += info.Size() / 1024
|
||||
}
|
||||
|
||||
if totalKB == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "memory",
|
||||
Name: "Memory & Swap",
|
||||
Description: fmt.Sprintf("Purge swap (%s) & inactive memory", formatSizeFromKB(totalKB)),
|
||||
Action: "swap_cleanup",
|
||||
Safe: false,
|
||||
}
|
||||
}
|
||||
|
||||
func buildLoginItemsItem() *OptimizationItem {
|
||||
items := listLoginItems()
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "startup",
|
||||
Name: "Login Items",
|
||||
Description: fmt.Sprintf("Review %d login items", len(items)),
|
||||
Action: "login_items",
|
||||
Safe: true,
|
||||
}
|
||||
}
|
||||
|
||||
func listLoginItems() []string {
|
||||
cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to get the name of every login item")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(string(output))
|
||||
if line == "" || line == "missing value" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ", ")
|
||||
var items []string
|
||||
for _, part := range parts {
|
||||
name := strings.TrimSpace(part)
|
||||
name = strings.Trim(name, "\"")
|
||||
if name != "" {
|
||||
items = append(items, name)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func checkLocalSnapshots() *OptimizationItem {
|
||||
if _, err := exec.LookPath("tmutil"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("tmutil", "listlocalsnapshots", "/")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
count := 0
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "com.apple.TimeMachine.") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "storage",
|
||||
Name: "Local Snapshots",
|
||||
Description: fmt.Sprintf("%d APFS local snapshots detected", count),
|
||||
Action: "local_snapshots",
|
||||
Safe: true,
|
||||
}
|
||||
}
|
||||
|
||||
func checkDeveloperCleanup() *OptimizationItem {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
|
||||
filepath.Join(home, "Library", "Developer", "Xcode", "Archives"),
|
||||
filepath.Join(home, "Library", "Developer", "Xcode", "iOS DeviceSupport"),
|
||||
filepath.Join(home, "Library", "Developer", "CoreSimulator", "Caches"),
|
||||
}
|
||||
|
||||
var totalKB int64
|
||||
for _, dir := range dirs {
|
||||
totalKB += dirSizeKB(dir)
|
||||
}
|
||||
|
||||
if totalKB == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &OptimizationItem{
|
||||
Category: "developer",
|
||||
Name: "Developer Cleanup",
|
||||
Description: fmt.Sprintf("Recover %s of Xcode/simulator data", formatSizeFromKB(totalKB)),
|
||||
Action: "developer_cleanup",
|
||||
Safe: false,
|
||||
}
|
||||
}
|
||||
|
||||
func dirSizeKB(path string) int64 {
|
||||
if path == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd := exec.Command("du", "-sk", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(output))
|
||||
if len(fields) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func formatSizeFromKB(kb int64) string {
|
||||
if kb <= 0 {
|
||||
return "0B"
|
||||
}
|
||||
|
||||
mb := float64(kb) / 1024
|
||||
gb := mb / 1024
|
||||
|
||||
switch {
|
||||
case gb >= 1:
|
||||
return fmt.Sprintf("%.1fGB", gb)
|
||||
case mb >= 1:
|
||||
return fmt.Sprintf("%.0fMB", mb)
|
||||
default:
|
||||
return fmt.Sprintf("%dKB", kb)
|
||||
}
|
||||
}
|
||||
29
mole
29
mole
@@ -132,6 +132,7 @@ show_help() {
|
||||
echo
|
||||
printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC"
|
||||
printf " %s%-28s%s %s\n" "$GREEN" "mo" "$NC" "Interactive main menu"
|
||||
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "System health check & optimization"
|
||||
printf " %s%-28s%s %s\n" "$GREEN" "mo clean" "$NC" "Deeper system cleanup"
|
||||
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup (no deletions)"
|
||||
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches"
|
||||
@@ -454,9 +455,10 @@ show_main_menu() {
|
||||
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Mac - System health & tuning" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Help & Information - Usage guide and tips" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
|
||||
printf '\r\033[2K%s\n' "$(show_menu_option 6 "Exit - Close Mole" "$([[ $selected -eq 6 ]] && echo true || echo false)")"
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
printf '\r\033[2K\n'
|
||||
@@ -521,7 +523,7 @@ interactive_main_menu() {
|
||||
|
||||
case "$key" in
|
||||
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
||||
"DOWN") ((current_option < 5)) && ((current_option++)) ;;
|
||||
"DOWN") ((current_option < 6)) && ((current_option++)) ;;
|
||||
"ENTER" | "$current_option")
|
||||
show_cursor
|
||||
case $current_option in
|
||||
@@ -529,30 +531,32 @@ interactive_main_menu() {
|
||||
exec "$SCRIPT_DIR/bin/clean.sh"
|
||||
;;
|
||||
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
||||
3) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
||||
4)
|
||||
3) exec "$SCRIPT_DIR/bin/optimize.sh" ;;
|
||||
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
||||
5)
|
||||
clear
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
5) cleanup_and_exit ;;
|
||||
6) cleanup_and_exit ;;
|
||||
esac
|
||||
;;
|
||||
"QUIT") cleanup_and_exit ;;
|
||||
[1-5])
|
||||
[1-6])
|
||||
show_cursor
|
||||
case $key in
|
||||
1)
|
||||
exec "$SCRIPT_DIR/bin/clean.sh"
|
||||
;;
|
||||
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
||||
3) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
||||
4)
|
||||
3) exec "$SCRIPT_DIR/bin/optimize.sh" ;;
|
||||
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
||||
5)
|
||||
clear
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
5) cleanup_and_exit ;;
|
||||
6) cleanup_and_exit ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@@ -561,6 +565,9 @@ interactive_main_menu() {
|
||||
|
||||
main() {
|
||||
case "${1:-""}" in
|
||||
"optimize")
|
||||
exec "$SCRIPT_DIR/bin/optimize.sh"
|
||||
;;
|
||||
"clean")
|
||||
exec "$SCRIPT_DIR/bin/clean.sh" "${@:2}"
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user