Ctrl K
ComunidadGuías…

bin/purge.sh

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

Archivo original ↗Preguntar a KiBot
#!/bin/bash
# Mole - Purge command.
# Cleans heavy project build artifacts.
# Interactive selection by project.

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 (avoid Perl warnings on non-English systems)
export LC_ALL=C
export LANG=C

# Get script directory and source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/core/common.sh"

# Restores cursor and clears temp files even when set -e aborts (#915).
cleanup() {
    show_cursor 2> /dev/null || true
    cleanup_temp_files
}
trap cleanup EXIT
trap 'trap - EXIT; cleanup; exit 130' INT TERM
source "$SCRIPT_DIR/../lib/core/log.sh"
source "$SCRIPT_DIR/../lib/clean/project.sh"

# Purge ends at safe_remove just like clean, so initialize the invoking user's
# whitelist before project discovery or the interactive selection begins.
load_mole_whitelist || exit 1

# Configuration
CURRENT_SECTION=""

# IMPORTANT: This file overrides start_section / end_section / note_activity
# from lib/core/base.sh by virtue of being sourced after it. The purge variant
# uses a blue ━━━ box header, has no fallback "Nothing to ..." message, and
# writes every note_activity call straight to EXPORT_LIST_FILE (purge always
# wants the export list, not just under DRY_RUN). See the cross-reference in
# lib/core/base.sh and the clean variant in bin/clean.sh before changing any
# of these three.
start_section() {
    local section_name="$1"
    CURRENT_SECTION="$section_name"
    printf '\n'
    echo -e "${BLUE}━━━ ${section_name} ━━━${NC}"
}

end_section() {
    CURRENT_SECTION=""
}

note_activity() {
    if [[ -n "$CURRENT_SECTION" ]]; then
        printf '%s\n' "$CURRENT_SECTION" >> "$EXPORT_LIST_FILE"
    fi
}

# Keep the most specific tail of a long purge path visible on the live scan line.
compact_purge_scan_path() {
    local path="$1"
    local max_path_len="${2:-0}"

    if ! [[ "$max_path_len" =~ ^[0-9]+$ ]] || [[ "$max_path_len" -lt 4 ]]; then
        max_path_len=4
    fi

    if [[ ${#path} -le $max_path_len ]]; then
        echo "$path"
        return
    fi

    local suffix_len=$((max_path_len - 3))
    local suffix="${path: -$suffix_len}"
    local path_tail=""
    local remainder="$path"

    while [[ "$remainder" == */* ]]; do
        local segment="/${remainder##*/}"
        remainder="${remainder%/*}"

        if [[ -z "$path_tail" ]]; then
            if [[ ${#segment} -le $suffix_len ]]; then
                path_tail="$segment"
            else
                break
            fi
            continue
        fi

        if [[ $((${#segment} + ${#path_tail})) -le $suffix_len ]]; then
            path_tail="${segment}${path_tail}"
        else
            break
        fi
    done

    if [[ -n "$path_tail" ]]; then
        echo "...${path_tail}"
        return
    fi

    echo "...$suffix"
}

# Main purge function
start_purge() {
    # Set current command for operation logging
    export MOLE_CURRENT_COMMAND="purge"
    log_operation_session_start "purge"

    # Clear screen for better UX
    if [[ -t 1 ]]; then
        printf '\033[2J\033[H'
    fi

    # Initialize stats file in user cache directory
    local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
    ensure_user_dir "$stats_dir"
    ensure_user_file "$stats_dir/purge_stats"
    ensure_user_file "$stats_dir/purge_count"
    ensure_user_file "$stats_dir/purge_scanning"
    echo "0" > "$stats_dir/purge_stats"
    echo "0" > "$stats_dir/purge_count"
    echo "" > "$stats_dir/purge_scanning"
}

# Perform the purge
perform_purge() {
    local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
    local monitor_pid=""

    # Cleanup function - use flag to prevent duplicate execution
    _cleanup_done=false
    cleanup_monitor() {
        # Prevent multiple cleanup executions from trap conflicts
        [[ "$_cleanup_done" == "true" ]] && return
        _cleanup_done=true

        # Remove scanning file to stop monitor
        rm -f "$stats_dir/purge_scanning" 2> /dev/null || true

        if [[ -n "$monitor_pid" ]]; then
            kill "$monitor_pid" 2> /dev/null || true
            wait "$monitor_pid" 2> /dev/null || true
        fi
        if [[ -t 1 ]]; then
            printf '\r\033[2K\n' > /dev/tty 2> /dev/null || true
        fi
    }

    # Ensure Ctrl-C/TERM always stops spinner(s) and exits immediately.
    handle_interrupt() {
        cleanup_monitor
        stop_inline_spinner 2> /dev/null || true
        show_cursor 2> /dev/null || true
        printf '\n' >&2
        exit 130
    }

    # Set up trap for cleanup + abort
    trap handle_interrupt INT TERM

    # Show scanning with spinner below the title line
    if [[ -t 1 ]]; then
        # Print title ONCE with newline; spinner occupies the line below
        printf '%s\n' "${PURPLE_BOLD}Purge Project Artifacts${NC}"

        # Capture terminal width in parent (most reliable before forking)
        local _parent_cols=80
        local _stty_out
        if _stty_out=$(stty size < /dev/tty 2> /dev/null); then
            _parent_cols="${_stty_out##* }" # "rows cols" -> take cols
        else
            _parent_cols=$(tput cols 2> /dev/null || echo 80)
        fi
        [[ "$_parent_cols" =~ ^[0-9]+$ && $_parent_cols -gt 0 ]] || _parent_cols=80

        # Start background monitor: writes directly to /dev/tty to avoid stdout state issues
        (
            mo_load_spinner_frames
            local spinner_idx=0
            local last_path=""
            # Use parent-captured width; never refresh inside the loop (avoids unreliable tput in bg)
            local term_cols="$_parent_cols"
            # Visible prefix "| Scanning " = 11 chars; reserve 25 total for safety margin
            local max_path_len=$((term_cols - 25))
            ((max_path_len < 5)) && max_path_len=5

            # Set up trap to exit cleanly (erase the spinner line via /dev/tty)
            trap 'printf "\r\033[2K" >/dev/tty 2>/dev/null; exit 0' INT TERM

            local _parent_pid=$$
            while [[ -f "$stats_dir/purge_scanning" ]]; do
                # Exit if parent process died (prevents orphaned spinner)
                if ! kill -0 "$_parent_pid" 2> /dev/null; then
                    break
                fi
                local current_path
                current_path=$(cat "$stats_dir/purge_scanning" 2> /dev/null || echo "")

                if [[ -n "$current_path" ]]; then
                    local display_path="${current_path/#$HOME/~}"
                    display_path=$(compact_purge_scan_path "$display_path" "$max_path_len")
                    last_path="$display_path"
                fi

                local spin_char="${MO_SPINNER_FRAMES[$spinner_idx]}"
                spinner_idx=$(((spinner_idx + 1) % ${#MO_SPINNER_FRAMES[@]}))

                # Write directly to /dev/tty: \033[2K clears entire current line, \r goes to start
                if [[ -n "$last_path" ]]; then
                    printf '\r\033[2K%s %sScanning %s%s' \
                        "${BLUE}${spin_char}${NC}" \
                        "${GRAY}" "$last_path" "${NC}" > /dev/tty 2> /dev/null
                else
                    printf '\r\033[2K%s %sScanning...%s' \
                        "${BLUE}${spin_char}${NC}" \
                        "${GRAY}" "${NC}" > /dev/tty 2> /dev/null
                fi

                sleep 0.05
            done
            printf '\r\033[2K' > /dev/tty 2> /dev/null
            exit 0
        ) &
        monitor_pid=$!
    else
        echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}"
    fi

    clean_project_artifacts
    local purge_outcome="${PURGE_RUN_OUTCOME:-scan_failed}"

    # Clean up
    trap - INT TERM
    cleanup_monitor

    case "$purge_outcome" in
        no_candidates | cancelled) return 0 ;;
        scan_failed) return 1 ;;
        completed | incomplete) ;;
        *)
            log_error "Unknown purge outcome: $purge_outcome"
            return 1
            ;;
    esac

    # Final summary (matching clean.sh format)
    echo ""

    local summary_heading="Purge complete"
    local -a summary_details=()
    local total_size_cleaned=0
    local total_items_cleaned=0

    if [[ -f "$stats_dir/purge_stats" ]]; then
        total_size_cleaned=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
        rm -f "$stats_dir/purge_stats"
    fi

    if [[ -f "$stats_dir/purge_count" ]]; then
        total_items_cleaned=$(cat "$stats_dir/purge_count" 2> /dev/null || echo "0")
        rm -f "$stats_dir/purge_count"
    fi

    if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
        summary_heading="Dry run complete - no changes made"
    fi
    if [[ "$purge_outcome" == "incomplete" ]]; then
        summary_heading="Purge incomplete"
        [[ "${MOLE_DRY_RUN:-0}" == "1" ]] && summary_heading="Dry run incomplete - no changes made"
    fi

    if [[ $total_items_cleaned -gt 0 ]]; then
        local freed_size_human
        freed_size_human=$(bytes_to_human_kb "$total_size_cleaned")

        local summary_line="Estimated space freed: ${GREEN}${freed_size_human}${NC}"
        if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
            summary_line="Would free approximately: ${GREEN}${freed_size_human}${NC}"
        fi
        if [[ ${PURGE_UNKNOWN_SIZE_COUNT:-0} -gt 0 ]]; then
            summary_line+=" + ${PURGE_UNKNOWN_SIZE_COUNT} unmeasured"
        fi
        [[ $total_items_cleaned -gt 0 ]] && summary_line+=" | Items: $total_items_cleaned"
        summary_line+=" | Free: $(get_free_space)"
        summary_details+=("$summary_line")
    else
        summary_details+=("No artifacts were removed.")
        summary_details+=("Free space: $(get_free_space)")
    fi
    if [[ "$purge_outcome" == "incomplete" ]]; then
        summary_details+=("Some artifacts were skipped or could not be processed.")
    fi

    # Log session end
    log_operation_session_end "purge" "${total_items_cleaned:-0}" "${total_size_cleaned:-0}"

    print_summary_block "$summary_heading" "${summary_details[@]}"
    printf '\n'
    [[ "$purge_outcome" == "completed" ]]
}

# Show help message
show_help() {
    echo -e "${PURPLE_BOLD}Mole Purge${NC}, Clean old project build artifacts"
    echo ""
    echo -e "${YELLOW}Usage:${NC} mo purge [options]"
    echo ""
    echo -e "${YELLOW}Options:${NC}"
    echo "  --paths         Edit custom scan directories"
    echo "  --dry-run       Preview purge actions without making changes"
    echo "  --include-empty Show zero-size project artifact directories"
    echo "  --yes           Confirm unattended cleanup of eligible artifacts"
    echo "  --debug         Enable debug logging"
    echo "  -h, --help      Show this help message"
    echo ""
    echo -e "${YELLOW}Default Paths:${NC}"
    for path in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
        echo "  * $path"
    done
}

# Main entry point
main() {
    # Parse arguments
    for arg in "$@"; do
        case "$arg" in
            "--paths")
                source "$SCRIPT_DIR/../lib/manage/purge_paths.sh"
                manage_purge_paths
                exit 0
                ;;
            "--help" | "-h")
                show_help
                exit 0
                ;;
            "--debug")
                export MO_DEBUG=1
                ;;
            "--dry-run" | "-n")
                export MOLE_DRY_RUN=1
                ;;
            "--yes")
                export MOLE_PURGE_YES=1
                ;;
            "--include-empty")
                export MOLE_PURGE_INCLUDE_EMPTY=1
                ;;
            *)
                echo "Unknown option: $arg" >&2
                echo "Use 'mo purge --help' for usage information" >&2
                exit 1
                ;;
        esac
    done

    start_purge
    if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
        echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No project artifacts will be removed"
        printf '\n'
    fi
    hide_cursor
    perform_purge
}

if [[ "${MOLE_SKIP_MAIN:-0}" == "1" ]]; then
    if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then
        return 0
    else
        exit 0
    fi
fi

main "$@"