diff --git a/.github/workflows/shell-quality-checks.yml b/.github/workflows/shell-quality-checks.yml index 7d1fb98..5eed9d9 100644 --- a/.github/workflows/shell-quality-checks.yml +++ b/.github/workflows/shell-quality-checks.yml @@ -27,8 +27,5 @@ jobs: - name: Run shellcheck linter and bats tests run: ./scripts/check.sh - - 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 + - name: Build Universal Binary for disk analyzer + run: ./scripts/build-analyze.sh diff --git a/bin/analyze-go b/bin/analyze-go index 1ec942e..30c3d7a 100755 Binary files a/bin/analyze-go and b/bin/analyze-go differ diff --git a/bin/optimize-go b/bin/optimize-go deleted file mode 100755 index 7d10726..0000000 Binary files a/bin/optimize-go and /dev/null differ diff --git a/bin/optimize.sh b/bin/optimize.sh index 3e1d16d..63744d1 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -5,9 +5,7 @@ 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" +source "$SCRIPT_DIR/lib/optimize_health.sh" # Colors and icons from common.sh @@ -433,15 +431,9 @@ main() { 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) + # Collect system health data using pure Bash implementation 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" exit 1 fi diff --git a/cmd/optimize/main.go b/cmd/optimize/main.go deleted file mode 100644 index 99a05c1..0000000 --- a/cmd/optimize/main.go +++ /dev/null @@ -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) - } -} diff --git a/go.mod b/go.mod index 15e5930..3e2b2c7 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.24.0 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 ( 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/lipgloss v1.1.0 // 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/rivo/uniseg v0.4.7 // 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/text v0.3.8 // indirect ) diff --git a/lib/optimize_health.sh b/lib/optimize_health.sh new file mode 100755 index 0000000..bf26548 --- /dev/null +++ b/lib/optimize_health.sh @@ -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 diff --git a/scripts/build-analyze.sh b/scripts/build-analyze.sh new file mode 100755 index 0000000..c1b191e --- /dev/null +++ b/scripts/build-analyze.sh @@ -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)"