Ctrl K
ComunidadGuías…

bin/optimize.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash
# Mole - Optimize command.
# Runs system maintenance tasks.
# Supports dry-run where applicable.

set -euo pipefail

# User state and installed tools must never run with inherited root privileges.
# Individual maintenance operations request administrator access themselves.
if [[ "$EUID" -eq 0 ]]; then
    printf '%s\n' 'Run Mole without sudo; it requests administrator access when needed.' >&2
    exit 1
fi

# Fix locale issues.
export LC_ALL=C
export LANG=C

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$SCRIPT_DIR/lib/core/common.sh"

# Clean temp files on exit.
trap cleanup_temp_files EXIT INT TERM
source "$SCRIPT_DIR/lib/core/sudo.sh"
source "$SCRIPT_DIR/lib/optimize/diagnostics.sh"
source "$SCRIPT_DIR/lib/optimize/maintenance.sh"
source "$SCRIPT_DIR/lib/optimize/catalog.sh"
source "$SCRIPT_DIR/lib/optimize/tasks.sh"
source "$SCRIPT_DIR/lib/check/health_json.sh"
source "$SCRIPT_DIR/lib/manage/whitelist.sh"

print_header() {
    printf '\n'
    echo -e "${PURPLE_BOLD}Optimize${NC}"
}

# Extract a simple numeric value from JSON by key without a jq dependency.
json_get_value() {
    local json="$1"
    local key="$2"
    local value
    value=$(echo "$json" | grep -o "\"${key}\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//')
    echo "${value:-0}"
}

# Validate JSON has expected structure (basic check).
json_validate() {
    local json="$1"
    # Check for required keys
    [[ "$json" == *'"memory_used_gb"'* ]] &&
        [[ "$json" == *'"optimizations"'* ]] &&
        [[ "$json" == *'{'* ]] && [[ "$json" == *'}'* ]]
}

show_optimization_summary() {
    local total
    total=$(optimize_outcome_total)
    if ((total == 0)); then
        return
    fi

    local summary_title
    local -a summary_details=()
    local applied unchanged skipped unavailable attention failed
    applied=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_APPLIED")
    unchanged=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_UNCHANGED")
    skipped=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_SKIPPED")
    unavailable=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_UNAVAILABLE")
    attention=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_ATTENTION")
    failed=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_FAILED")

    local -a outcome_parts=()
    [[ $unchanged -gt 0 ]] && outcome_parts+=("$unchanged unchanged")
    [[ $skipped -gt 0 ]] && outcome_parts+=("$skipped skipped")
    [[ $unavailable -gt 0 ]] && outcome_parts+=("$unavailable unavailable")
    [[ $attention -gt 0 ]] && outcome_parts+=("$attention need attention")
    [[ $failed -gt 0 ]] && outcome_parts+=("$failed failed")

    local outcome_line=""
    if [[ ${#outcome_parts[@]} -gt 0 ]]; then
        outcome_line="${outcome_parts[0]}"
        local index
        for ((index = 1; index < ${#outcome_parts[@]}; index++)); do
            outcome_line+=" | ${outcome_parts[$index]}"
        done
    fi

    if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
        summary_title="Dry Run Complete, No Changes Made"
        summary_details+=("Would apply ${YELLOW}${applied}${NC} optimizations")
        [[ -n "$outcome_line" ]] && summary_details+=("$outcome_line")
        summary_details+=("Run without ${YELLOW}--dry-run${NC} to apply these changes")
    else
        summary_title="Optimization Complete"

        local cache_kb="${OPTIMIZE_CACHE_CLEANED_KB:-0}"
        local db_count="${OPTIMIZE_DATABASES_COUNT:-0}"
        local config_count="${OPTIMIZE_CONFIGS_REPAIRED:-0}"

        local key_stat=""
        if [[ "$cache_kb" =~ ^[0-9]+$ ]] && [[ "$cache_kb" -gt 0 ]]; then
            local cache_human
            cache_human=$(bytes_to_human "$((cache_kb * 1024))")
            key_stat="${cache_human} cache cleaned"
        elif [[ "$db_count" =~ ^[0-9]+$ ]] && [[ "$db_count" -gt 0 ]]; then
            key_stat="${db_count} databases optimized"
        elif [[ "$config_count" =~ ^[0-9]+$ ]] && [[ "$config_count" -gt 0 ]]; then
            key_stat="${config_count} configs repaired"
        fi

        if [[ -n "$key_stat" ]]; then
            summary_details+=("Applied ${GREEN}${applied}${NC} optimizations, ${key_stat}")
        else
            summary_details+=("Applied ${GREEN}${applied}${NC} optimizations")
        fi

        [[ -n "$outcome_line" ]] && summary_details+=("$outcome_line")
        if [[ $attention -gt 0 || $failed -gt 0 ]]; then
            summary_details+=("Review the warnings above")
        else
            summary_details+=("Optimization pass complete")
        fi
    fi

    print_summary_block "$summary_title" "${summary_details[@]}"
}

show_system_health() {
    local health_json="$1"

    local mem_used=$(json_get_value "$health_json" "memory_used_gb")
    local mem_total=$(json_get_value "$health_json" "memory_total_gb")
    local disk_used=$(json_get_value "$health_json" "disk_used_gb")
    local disk_total=$(json_get_value "$health_json" "disk_total_gb")
    local disk_percent=$(json_get_value "$health_json" "disk_used_percent")
    local uptime=$(json_get_value "$health_json" "uptime_days")

    mem_used=${mem_used:-0}
    mem_total=${mem_total:-0}
    disk_used=${disk_used:-0}
    disk_total=${disk_total:-0}
    disk_percent=${disk_percent:-0}
    uptime=${uptime:-0}

    # printf parses float arguments with the locale's decimal separator, so
    # comma-decimal locales reject dot values like "5.70" (#1220). Round in
    # C-locale awk and print plain strings to avoid float parsing entirely.
    local rounded
    rounded=$(LC_ALL=C awk -v mu="$mem_used" -v mt="$mem_total" -v du="$disk_used" -v dt="$disk_total" -v ut="$uptime" \
        'BEGIN { printf "%.0f %.0f %.0f %.0f %.0f", mu, mt, du, dt, ut }' 2> /dev/null || echo "0 0 0 0 0")
    read -r mem_used mem_total disk_used disk_total uptime <<< "$rounded"

    printf "${ICON_ADMIN} System  %s/%s GB RAM | %s/%s GB Disk | Uptime %sd\n" \
        "$mem_used" "$mem_total" "$disk_used" "$disk_total" "$uptime"
}

announce_action() {
    local name="$1"

    if [[ "${FIRST_ACTION:-true}" == "true" ]]; then
        export FIRST_ACTION=false
    else
        echo ""
    fi
    echo -e "${BLUE}${ICON_ARROW} ${name}${NC}"
}

cleanup_all() {
    local exit_status="${1:-0}"
    stop_inline_spinner 2> /dev/null || true
    stop_sudo_session
    cleanup_temp_files
    # Log session end
    local applied=0
    local failed=0
    if declare -F optimize_outcome_count > /dev/null; then
        applied=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_APPLIED")
        failed=$(optimize_outcome_count "$MOLE_OPTIMIZE_OUTCOME_FAILED")
        local failed_action
        while IFS= read -r failed_action; do
            [[ -n "$failed_action" ]] || continue
            log_operation "optimize" "TASK_FAILED" "$failed_action" "task outcome"
        done < <(optimize_failed_actions)
        # Applied tasks changed system state (SQLite, defaults, Spotlight,
        # DNS); a preview applied nothing, so it records none.
        if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
            local applied_action
            while IFS= read -r applied_action; do
                [[ -n "$applied_action" ]] || continue
                log_operation "optimize" "REBUILT" "$applied_action" "task outcome"
            done < <(optimize_actions_with_outcome "$MOLE_OPTIMIZE_OUTCOME_APPLIED")
        fi
    fi
    if [[ "$exit_status" -ne 0 && "$failed" -eq 0 ]]; then
        local failure_action="session"
        [[ "$exit_status" -eq 130 ]] && failure_action="interrupted"
        log_operation "optimize" "TASK_FAILED" "$failure_action" "exit status $exit_status"
    fi
    log_operation_session_end "optimize" "$applied" "0"
}

handle_interrupt() {
    trap - EXIT
    cleanup_all 130
    exit 130
}

main() {
    # Set current command for operation logging
    export MOLE_CURRENT_COMMAND="optimize"

    local health_json
    for arg in "$@"; do
        case "$arg" in
            "--help" | "-h")
                show_optimize_help
                exit 0
                ;;
            "--debug")
                export MO_DEBUG=1
                ;;
            "--dry-run" | "-n")
                export MOLE_DRY_RUN=1
                ;;
            "--whitelist")
                manage_whitelist "optimize"
                exit 0
                ;;
            *)
                echo "Unknown optimize option: $arg" >&2
                echo "Use 'mo optimize --help' for supported options." >&2
                exit 1
                ;;
        esac
    done

    log_operation_session_start "optimize"

    trap 'cleanup_all "$?"' EXIT
    trap handle_interrupt INT TERM

    if [[ -t 1 ]]; then
        clear_screen
    fi
    print_header

    # Dry-run indicator.
    if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
        echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No files will be modified\n"
    fi

    if ! command -v bc > /dev/null 2>&1; then
        echo -e "${YELLOW}${ICON_ERROR}${NC} Missing dependency: bc"
        echo -e "${GRAY}Install with: ${GREEN}brew install bc${NC}"
        exit 1
    fi

    if [[ -t 1 ]]; then
        start_inline_spinner "Collecting system info..."
    fi

    if ! health_json=$(generate_health_json 2> /dev/null); then
        if [[ -t 1 ]]; then
            stop_inline_spinner
        fi
        echo ""
        log_error "Failed to collect system health data"
        exit 1
    fi

    if ! json_validate "$health_json"; then
        if [[ -t 1 ]]; then
            stop_inline_spinner
        fi
        echo ""
        log_error "Invalid system health data format"
        echo -e "${GRAY}${ICON_REVIEW}${NC} Check if awk, sysctl, and df commands are available"
        exit 1
    fi

    if [[ -t 1 ]]; then
        stop_inline_spinner
    fi

    load_whitelist "optimize" || exit 1
    if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then
        local count=${#CURRENT_WHITELIST_PATTERNS[@]}
        if [[ $count -le 3 ]]; then
            local patterns_list=$(
                IFS=', '
                echo "${CURRENT_WHITELIST_PATTERNS[*]}"
            )
            echo -e "${ICON_ADMIN} Active Whitelist: ${patterns_list}"
        fi
    fi

    show_system_health "$health_json"

    run_optimize_diagnostics

    echo ""
    # Track sudo availability so individual tasks can skip cleanly when admin
    # access was denied. Without this, every sudo task re-prompts for the
    # password and half-runs after a refusal. Default true in dry-run so the
    # task list still expands fully for inspection.
    export MOLE_OPTIMIZE_SUDO_AVAILABLE="false"
    if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
        MOLE_OPTIMIZE_SUDO_AVAILABLE="true"
    elif ensure_sudo_session "System optimization requires admin access"; then
        MOLE_OPTIMIZE_SUDO_AVAILABLE="true"
    else
        opt_msg "Skipping sudo-required optimizations: admin access not granted"
    fi

    export FIRST_ACTION=true
    optimize_outcomes_reset
    local index action health_name
    for ((index = 0; index < ${#MOLE_OPTIMIZE_ACTIONS[@]}; index++)); do
        action=${MOLE_OPTIMIZE_ACTIONS[$index]}
        health_name=${MOLE_OPTIMIZE_HEALTH_NAMES[$index]}
        announce_action "$health_name"
        execute_optimization "$action"
    done

    if [[ "$(optimize_outcome_total)" -ne ${#MOLE_OPTIMIZE_ACTIONS[@]} ]]; then
        log_error "Optimize task outcomes are incomplete"
        return 1
    fi

    show_optimization_summary

    printf '\n'
    optimize_outcomes_succeeded
}

main "$@"