mirror of
https://github.com/tw93/Mole.git
synced 2026-02-11 15:53:59 +00:00
Replace optimize module with pure Bash
This commit is contained in:
7
.github/workflows/shell-quality-checks.yml
vendored
7
.github/workflows/shell-quality-checks.yml
vendored
@@ -27,8 +27,5 @@ jobs:
|
|||||||
- name: Run shellcheck linter and bats tests
|
- name: Run shellcheck linter and bats tests
|
||||||
run: ./scripts/check.sh
|
run: ./scripts/check.sh
|
||||||
|
|
||||||
- name: Build Go disk analyzer
|
- name: Build Universal Binary for disk analyzer
|
||||||
run: mkdir -p bin && go build -o bin/analyze-go ./cmd/analyze
|
run: ./scripts/build-analyze.sh
|
||||||
|
|
||||||
- name: Build Go optimizer
|
|
||||||
run: mkdir -p bin && go build -o bin/optimize-go ./cmd/optimize
|
|
||||||
|
|||||||
BIN
bin/analyze-go
BIN
bin/analyze-go
Binary file not shown.
BIN
bin/optimize-go
BIN
bin/optimize-go
Binary file not shown.
@@ -5,9 +5,7 @@ set -euo pipefail
|
|||||||
# Load common functions
|
# Load common functions
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
source "$SCRIPT_DIR/lib/common.sh"
|
source "$SCRIPT_DIR/lib/common.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/optimize_health.sh"
|
||||||
# Path to optimize-go binary
|
|
||||||
OPTIMIZE_GO="$SCRIPT_DIR/bin/optimize-go"
|
|
||||||
|
|
||||||
# Colors and icons from common.sh
|
# Colors and icons from common.sh
|
||||||
|
|
||||||
@@ -433,15 +431,9 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if optimize-go exists
|
# Collect system health data using pure Bash implementation
|
||||||
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
|
local health_json
|
||||||
if ! health_json=$("$OPTIMIZE_GO" 2> /dev/null); then
|
if ! health_json=$(generate_health_json 2> /dev/null); then
|
||||||
log_error "Failed to collect system health data"
|
log_error "Failed to collect system health data"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,532 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
go.mod
8
go.mod
@@ -4,11 +4,14 @@ go 1.24.0
|
|||||||
|
|
||||||
toolchain go1.24.6
|
toolchain go1.24.6
|
||||||
|
|
||||||
require github.com/charmbracelet/bubbletea v1.3.10
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
golang.org/x/sync v0.18.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
@@ -24,7 +27,6 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.3.8 // indirect
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
288
lib/optimize_health.sh
Executable file
288
lib/optimize_health.sh
Executable file
@@ -0,0 +1,288 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# System Health Check - Pure Bash Implementation
|
||||||
|
# Replaces optimize-go
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get memory info in GB
|
||||||
|
get_memory_info() {
|
||||||
|
local total_bytes used_gb total_gb
|
||||||
|
|
||||||
|
# Total memory
|
||||||
|
total_bytes=$(sysctl -n hw.memsize 2>/dev/null || echo "0")
|
||||||
|
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_bytes / (1024*1024*1024)}")
|
||||||
|
|
||||||
|
# Used memory from vm_stat
|
||||||
|
local vm_output active wired compressed page_size
|
||||||
|
vm_output=$(vm_stat 2>/dev/null || echo "")
|
||||||
|
page_size=4096
|
||||||
|
|
||||||
|
active=$(echo "$vm_output" | awk '/Pages active:/ {print $NF}' | tr -d '.')
|
||||||
|
wired=$(echo "$vm_output" | awk '/Pages wired down:/ {print $NF}' | tr -d '.')
|
||||||
|
compressed=$(echo "$vm_output" | awk '/Pages occupied by compressor:/ {print $NF}' | tr -d '.')
|
||||||
|
|
||||||
|
active=${active:-0}
|
||||||
|
wired=${wired:-0}
|
||||||
|
compressed=${compressed:-0}
|
||||||
|
|
||||||
|
local used_bytes=$(( (active + wired + compressed) * page_size ))
|
||||||
|
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / (1024*1024*1024)}")
|
||||||
|
|
||||||
|
echo "$used_gb $total_gb"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get disk info
|
||||||
|
get_disk_info() {
|
||||||
|
local home="${HOME:-/}"
|
||||||
|
local df_output total_gb used_gb used_percent
|
||||||
|
|
||||||
|
df_output=$(df -k "$home" 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
local total_kb used_kb
|
||||||
|
total_kb=$(echo "$df_output" | awk '{print $2}')
|
||||||
|
used_kb=$(echo "$df_output" | awk '{print $3}')
|
||||||
|
|
||||||
|
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_kb / (1024*1024)}")
|
||||||
|
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_kb / (1024*1024)}")
|
||||||
|
used_percent=$(awk "BEGIN {printf \"%.1f\", ($used_kb / $total_kb) * 100}")
|
||||||
|
|
||||||
|
echo "$used_gb $total_gb $used_percent"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get uptime in days
|
||||||
|
get_uptime_days() {
|
||||||
|
local boot_output boot_time uptime_days
|
||||||
|
|
||||||
|
boot_output=$(sysctl -n kern.boottime 2>/dev/null || echo "")
|
||||||
|
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p')
|
||||||
|
|
||||||
|
if [[ -n "$boot_time" ]]; then
|
||||||
|
local now=$(date +%s)
|
||||||
|
local uptime_sec=$((now - boot_time))
|
||||||
|
uptime_days=$(awk "BEGIN {printf \"%.1f\", $uptime_sec / 86400}")
|
||||||
|
else
|
||||||
|
uptime_days="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$uptime_days"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get directory size in KB
|
||||||
|
dir_size_kb() {
|
||||||
|
local path="$1"
|
||||||
|
[[ ! -e "$path" ]] && echo "0" && return
|
||||||
|
du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format size from KB
|
||||||
|
format_size_kb() {
|
||||||
|
local kb="$1"
|
||||||
|
[[ "$kb" -le 0 ]] && echo "0B" && return
|
||||||
|
|
||||||
|
local mb gb
|
||||||
|
mb=$(awk "BEGIN {printf \"%.1f\", $kb / 1024}")
|
||||||
|
gb=$(awk "BEGIN {printf \"%.2f\", $mb / 1024}")
|
||||||
|
|
||||||
|
if awk "BEGIN {exit !($gb >= 1)}"; then
|
||||||
|
echo "${gb}GB"
|
||||||
|
elif awk "BEGIN {exit !($mb >= 1)}"; then
|
||||||
|
printf "%.0fMB\n" "$mb"
|
||||||
|
else
|
||||||
|
echo "${kb}KB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check startup items count
|
||||||
|
check_startup_items() {
|
||||||
|
local count=0
|
||||||
|
local dirs=(
|
||||||
|
"$HOME/Library/LaunchAgents"
|
||||||
|
"/Library/LaunchAgents"
|
||||||
|
)
|
||||||
|
|
||||||
|
for dir in "${dirs[@]}"; do
|
||||||
|
[[ -d "$dir" ]] && count=$((count + $(ls -1 "$dir" 2>/dev/null | wc -l)))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $count -gt 5 ]]; then
|
||||||
|
local suggested=$((count / 2))
|
||||||
|
[[ $suggested -lt 1 ]] && suggested=1
|
||||||
|
echo "startup_items|Startup Items|${count} items (suggest disable ${suggested})|false"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check cache size
|
||||||
|
check_cache_refresh() {
|
||||||
|
local cache_dir="$HOME/Library/Caches"
|
||||||
|
local size_kb=$(dir_size_kb "$cache_dir")
|
||||||
|
local desc="Refresh Finder previews, Quick Look, and Safari caches"
|
||||||
|
|
||||||
|
if [[ $size_kb -gt 0 ]]; then
|
||||||
|
local size_str=$(format_size_kb "$size_kb")
|
||||||
|
desc="Refresh ${size_str} of Finder/Safari caches"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "cache_refresh|User Cache Refresh|${desc}|true"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Mail downloads
|
||||||
|
check_mail_downloads() {
|
||||||
|
local dirs=(
|
||||||
|
"$HOME/Library/Mail Downloads"
|
||||||
|
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
|
||||||
|
)
|
||||||
|
|
||||||
|
local total_kb=0
|
||||||
|
for dir in "${dirs[@]}"; do
|
||||||
|
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $total_kb -gt 0 ]]; then
|
||||||
|
local size_str=$(format_size_kb "$total_kb")
|
||||||
|
echo "mail_downloads|Mail Downloads|Recover ${size_str} of Mail attachments|true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check saved state
|
||||||
|
check_saved_state() {
|
||||||
|
local state_dir="$HOME/Library/Saved Application State"
|
||||||
|
local size_kb=$(dir_size_kb "$state_dir")
|
||||||
|
|
||||||
|
if [[ $size_kb -gt 0 ]]; then
|
||||||
|
local size_str=$(format_size_kb "$size_kb")
|
||||||
|
echo "saved_state_cleanup|Saved State|Clear ${size_str} of stale saved states|true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check swap files
|
||||||
|
check_swap_cleanup() {
|
||||||
|
local total_kb=0
|
||||||
|
local file
|
||||||
|
|
||||||
|
for file in /private/var/vm/swapfile*; do
|
||||||
|
[[ -f "$file" ]] && total_kb=$((total_kb + $(stat -f%z "$file" 2>/dev/null || echo 0) / 1024))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $total_kb -gt 0 ]]; then
|
||||||
|
local size_str=$(format_size_kb "$total_kb")
|
||||||
|
echo "swap_cleanup|Memory & Swap|Purge swap (${size_str}) & inactive memory|false"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check login items
|
||||||
|
check_login_items() {
|
||||||
|
local items
|
||||||
|
items=$(osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
[[ -z "$items" || "$items" == "missing value" ]] && return
|
||||||
|
|
||||||
|
local count=$(echo "$items" | tr ',' '\n' | grep -v '^[[:space:]]*$' | wc -l | tr -d ' ')
|
||||||
|
[[ $count -gt 0 ]] && echo "login_items|Login Items|Review ${count} login items|true"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check local snapshots
|
||||||
|
check_local_snapshots() {
|
||||||
|
command -v tmutil >/dev/null 2>&1 || return
|
||||||
|
|
||||||
|
local snapshots
|
||||||
|
snapshots=$(tmutil listlocalsnapshots / 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
local count
|
||||||
|
count=$(echo "$snapshots" | grep -c "com.apple.TimeMachine" 2>/dev/null)
|
||||||
|
count=$(echo "$count" | tr -d ' \n')
|
||||||
|
count=${count:-0}
|
||||||
|
[[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]] && echo "local_snapshots|Local Snapshots|${count} APFS local snapshots detected|true"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check developer cleanup
|
||||||
|
check_developer_cleanup() {
|
||||||
|
local dirs=(
|
||||||
|
"$HOME/Library/Developer/Xcode/DerivedData"
|
||||||
|
"$HOME/Library/Developer/Xcode/Archives"
|
||||||
|
"$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
||||||
|
"$HOME/Library/Developer/CoreSimulator/Caches"
|
||||||
|
)
|
||||||
|
|
||||||
|
local total_kb=0
|
||||||
|
for dir in "${dirs[@]}"; do
|
||||||
|
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $total_kb -gt 0 ]]; then
|
||||||
|
local size_str=$(format_size_kb "$total_kb")
|
||||||
|
echo "developer_cleanup|Developer Cleanup|Recover ${size_str} of Xcode/simulator data|false"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate JSON output
|
||||||
|
generate_health_json() {
|
||||||
|
# System info
|
||||||
|
read -r mem_used mem_total <<< "$(get_memory_info)"
|
||||||
|
read -r disk_used disk_total disk_percent <<< "$(get_disk_info)"
|
||||||
|
local uptime=$(get_uptime_days)
|
||||||
|
|
||||||
|
# Start JSON
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"memory_used_gb": $mem_used,
|
||||||
|
"memory_total_gb": $mem_total,
|
||||||
|
"disk_used_gb": $disk_used,
|
||||||
|
"disk_total_gb": $disk_total,
|
||||||
|
"disk_used_percent": $disk_percent,
|
||||||
|
"uptime_days": $uptime,
|
||||||
|
"optimizations": [
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Collect all optimization items
|
||||||
|
local -a items=()
|
||||||
|
|
||||||
|
# Always-on items
|
||||||
|
items+=('system_maintenance|System Maintenance|Rebuild system databases & flush caches|true')
|
||||||
|
items+=('network_services|Network Services|Reset network services|true')
|
||||||
|
items+=('maintenance_scripts|Maintenance Scripts|Run daily/weekly/monthly scripts & rotate logs|true')
|
||||||
|
items+=('radio_refresh|Bluetooth & Wi-Fi Refresh|Reset wireless preference caches|true')
|
||||||
|
items+=('recent_items|Recent Items|Clear recent apps/documents/servers lists|true')
|
||||||
|
items+=('log_cleanup|Diagnostics Cleanup|Purge old diagnostic & crash logs|true')
|
||||||
|
items+=('finder_dock_refresh|Finder & Dock Refresh|Clear Finder/Dock caches and restart|true')
|
||||||
|
items+=('startup_cache|Startup Cache Rebuild|Rebuild kext caches & prelinked kernel|true')
|
||||||
|
|
||||||
|
# Conditional items
|
||||||
|
local item
|
||||||
|
item=$(check_startup_items || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_cache_refresh || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_mail_downloads || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_saved_state || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_swap_cleanup || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_login_items || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_local_snapshots || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
item=$(check_developer_cleanup || true); [[ -n "$item" ]] && items+=("$item")
|
||||||
|
|
||||||
|
# Output items as JSON
|
||||||
|
local first=true
|
||||||
|
for item in "${items[@]}"; do
|
||||||
|
IFS='|' read -r action name desc safe <<< "$item"
|
||||||
|
|
||||||
|
[[ "$first" == "true" ]] && first=false || echo ","
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"category": "system",
|
||||||
|
"name": "$name",
|
||||||
|
"description": "$desc",
|
||||||
|
"action": "$action",
|
||||||
|
"safe": $safe
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
# Close JSON
|
||||||
|
cat << 'EOF'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
generate_health_json
|
||||||
|
fi
|
||||||
33
scripts/build-analyze.sh
Executable file
33
scripts/build-analyze.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build Universal Binary for analyze-go
|
||||||
|
# Supports both Apple Silicon and Intel Macs
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "Building analyze-go for multiple architectures..."
|
||||||
|
|
||||||
|
# Build for arm64 (Apple Silicon)
|
||||||
|
echo " → Building for arm64..."
|
||||||
|
GOARCH=arm64 go build -ldflags="-s -w" -o bin/analyze-go-arm64 cmd/analyze/main.go
|
||||||
|
|
||||||
|
# Build for amd64 (Intel)
|
||||||
|
echo " → Building for amd64..."
|
||||||
|
GOARCH=amd64 go build -ldflags="-s -w" -o bin/analyze-go-amd64 cmd/analyze/main.go
|
||||||
|
|
||||||
|
# Create Universal Binary
|
||||||
|
echo " → Creating Universal Binary..."
|
||||||
|
lipo -create bin/analyze-go-arm64 bin/analyze-go-amd64 -output bin/analyze-go
|
||||||
|
|
||||||
|
# Clean up temporary files
|
||||||
|
rm bin/analyze-go-arm64 bin/analyze-go-amd64
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
echo ""
|
||||||
|
echo "✓ Build complete!"
|
||||||
|
echo ""
|
||||||
|
file bin/analyze-go
|
||||||
|
ls -lh bin/analyze-go | awk '{print "Size:", $5}'
|
||||||
|
echo ""
|
||||||
|
echo "Binary supports: arm64 (Apple Silicon) + x86_64 (Intel)"
|
||||||
Reference in New Issue
Block a user