diff --git a/README.md b/README.md index 9b7f37e..f816754 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ mo purge --paths # Configure project scan directories - **Safety**: Built with strict protections. See [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`. - **Be Careful**: Although safe by design, file deletion is permanent. Please review operations carefully. - **Debug Mode**: Use `--debug` for detailed logs (e.g., `mo clean --debug`). Combine with `--dry-run` for comprehensive preview including risk levels and file details. +- **Operation Log**: File operations are logged to `~/.config/mole/operations.log` for troubleshooting. Disable with `MO_NO_OPLOG=1`. - **Navigation**: Supports arrow keys and Vim bindings (`h/j/k/l`). - **Status Shortcuts**: In `mo status`, press `k` to toggle cat visibility and save preference, `q` to quit. - **Configuration**: Run `mo touchid` for Touch ID sudo, `mo completion` for shell tab completion, `mo clean --whitelist` to manage protected paths. diff --git a/bin/clean.sh b/bin/clean.sh index 4320106..1a182fe 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -365,6 +365,7 @@ safe_clean() { if should_protect_path "$path"; then skip=true ((skipped_count++)) + log_operation "clean" "SKIPPED" "$path" "protected" fi [[ "$skip" == "true" ]] && continue @@ -372,6 +373,7 @@ safe_clean() { if is_path_whitelisted "$path"; then skip=true ((skipped_count++)) + log_operation "clean" "SKIPPED" "$path" "whitelist" fi [[ "$skip" == "true" ]] && continue [[ -e "$path" ]] && existing_paths+=("$path") @@ -699,6 +701,10 @@ safe_clean() { } start_cleanup() { + # Set current command for operation logging + export MOLE_CURRENT_COMMAND="clean" + log_operation_session_start "clean" + if [[ -t 1 ]]; then printf '\033[2J\033[H' fi @@ -1065,6 +1071,9 @@ perform_cleanup() { set -e fi + # Log session end with summary + log_operation_session_end "clean" "$files_cleaned" "$total_size_cleaned" + print_summary_block "$summary_heading" "${summary_details[@]}" printf '\n' } diff --git a/bin/optimize.sh b/bin/optimize.sh index 9fe3864..3a5e0f1 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -139,12 +139,12 @@ show_optimization_summary() { show_system_health() { local health_json="$1" - local mem_used=$(echo "$health_json" | jq -r '.memory_used_gb // 0' 2> /dev/null || echo "0") - local mem_total=$(echo "$health_json" | jq -r '.memory_total_gb // 0' 2> /dev/null || echo "0") - local disk_used=$(echo "$health_json" | jq -r '.disk_used_gb // 0' 2> /dev/null || echo "0") - local disk_total=$(echo "$health_json" | jq -r '.disk_total_gb // 0' 2> /dev/null || echo "0") - local disk_percent=$(echo "$health_json" | jq -r '.disk_used_percent // 0' 2> /dev/null || echo "0") - local uptime=$(echo "$health_json" | jq -r '.uptime_days // 0' 2> /dev/null || echo "0") + local mem_used=$(echo "$health_json" | jq -r '.memory_used_gb // 0' 2>/dev/null || echo "0") + local mem_total=$(echo "$health_json" | jq -r '.memory_total_gb // 0' 2>/dev/null || echo "0") + local disk_used=$(echo "$health_json" | jq -r '.disk_used_gb // 0' 2>/dev/null || echo "0") + local disk_total=$(echo "$health_json" | jq -r '.disk_total_gb // 0' 2>/dev/null || echo "0") + local disk_percent=$(echo "$health_json" | jq -r '.disk_used_percent // 0' 2>/dev/null || echo "0") + local uptime=$(echo "$health_json" | jq -r '.uptime_days // 0' 2>/dev/null || echo "0") mem_used=${mem_used:-0} mem_total=${mem_total:-0} @@ -159,7 +159,7 @@ show_system_health() { parse_optimizations() { local health_json="$1" - echo "$health_json" | jq -c '.optimizations[]' 2> /dev/null + echo "$health_json" | jq -c '.optimizations[]' 2>/dev/null } announce_action() { @@ -177,12 +177,12 @@ announce_action() { touchid_configured() { local pam_file="/etc/pam.d/sudo" - [[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2> /dev/null + [[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2>/dev/null } touchid_supported() { - if command -v bioutil > /dev/null 2>&1; then - if bioutil -r 2> /dev/null | grep -qi "Touch ID"; then + if command -v bioutil >/dev/null 2>&1; then + if bioutil -r 2>/dev/null | grep -qi "Touch ID"; then return 0 fi fi @@ -272,7 +272,7 @@ ask_for_security_fixes() { echo "" echo -e "${BLUE}SECURITY FIXES${NC}" for entry in "${SECURITY_FIXES[@]}"; do - IFS='|' read -r _ label <<< "$entry" + IFS='|' read -r _ label <<<"$entry" echo -e " ${ICON_LIST} $label" done echo "" @@ -299,7 +299,7 @@ ask_for_security_fixes() { } apply_firewall_fix() { - if sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on > /dev/null 2>&1; then + if sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on >/dev/null 2>&1; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Firewall enabled" FIREWALL_DISABLED=false return 0 @@ -309,7 +309,7 @@ apply_firewall_fix() { } apply_gatekeeper_fix() { - if sudo spctl --master-enable 2> /dev/null; then + if sudo spctl --master-enable 2>/dev/null; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Gatekeeper enabled" GATEKEEPER_DISABLED=false return 0 @@ -333,17 +333,17 @@ perform_security_fixes() { local applied=0 for entry in "${SECURITY_FIXES[@]}"; do - IFS='|' read -r action _ <<< "$entry" + IFS='|' read -r action _ <<<"$entry" case "$action" in - firewall) - apply_firewall_fix && ((applied++)) - ;; - gatekeeper) - apply_gatekeeper_fix && ((applied++)) - ;; - touchid) - apply_touchid_fix && ((applied++)) - ;; + firewall) + apply_firewall_fix && ((applied++)) + ;; + gatekeeper) + apply_gatekeeper_fix && ((applied++)) + ;; + touchid) + apply_touchid_fix && ((applied++)) + ;; esac done @@ -354,9 +354,11 @@ perform_security_fixes() { } cleanup_all() { - stop_inline_spinner 2> /dev/null || true + stop_inline_spinner 2>/dev/null || true stop_sudo_session cleanup_temp_files + # Log session end + log_operation_session_end "optimize" "${OPTIMIZE_SAFE_COUNT:-0}" "0" } handle_interrupt() { @@ -365,22 +367,27 @@ handle_interrupt() { } main() { + # Set current command for operation logging + export MOLE_CURRENT_COMMAND="optimize" + local health_json for arg in "$@"; do case "$arg" in - "--debug") - export MO_DEBUG=1 - ;; - "--dry-run") - export MOLE_DRY_RUN=1 - ;; - "--whitelist") - manage_whitelist "optimize" - exit 0 - ;; + "--debug") + export MO_DEBUG=1 + ;; + "--dry-run") + export MOLE_DRY_RUN=1 + ;; + "--whitelist") + manage_whitelist "optimize" + exit 0 + ;; esac done + log_operation_session_start "optimize" + trap cleanup_all EXIT trap handle_interrupt INT TERM @@ -394,13 +401,13 @@ main() { echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No files will be modified\n" fi - if ! command -v jq > /dev/null 2>&1; then + if ! command -v jq >/dev/null 2>&1; then echo -e "${YELLOW}${ICON_ERROR}${NC} Missing dependency: jq" echo -e "${GRAY}Install with: ${GREEN}brew install jq${NC}" exit 1 fi - if ! command -v bc > /dev/null 2>&1; then + 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 @@ -410,7 +417,7 @@ main() { start_inline_spinner "Collecting system info..." fi - if ! health_json=$(generate_health_json 2> /dev/null); then + if ! health_json=$(generate_health_json 2>/dev/null); then if [[ -t 1 ]]; then stop_inline_spinner fi @@ -419,7 +426,7 @@ main() { exit 1 fi - if ! echo "$health_json" | jq empty 2> /dev/null; then + if ! echo "$health_json" | jq empty 2>/dev/null; then if [[ -t 1 ]]; then stop_inline_spinner fi @@ -451,7 +458,7 @@ main() { local -a confirm_items=() local opts_file opts_file=$(mktemp_file) - parse_optimizations "$health_json" > "$opts_file" + parse_optimizations "$health_json" >"$opts_file" while IFS= read -r opt_json; do [[ -z "$opt_json" ]] && continue @@ -469,7 +476,7 @@ main() { else confirm_items+=("$item") fi - done < "$opts_file" + done <"$opts_file" echo "" if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then @@ -479,7 +486,7 @@ main() { export FIRST_ACTION=true if [[ ${#safe_items[@]} -gt 0 ]]; then for item in "${safe_items[@]}"; do - IFS='|' read -r name desc action path <<< "$item" + IFS='|' read -r name desc action path <<<"$item" announce_action "$name" "$desc" "safe" execute_optimization "$action" "$path" done @@ -487,7 +494,7 @@ main() { if [[ ${#confirm_items[@]} -gt 0 ]]; then for item in "${confirm_items[@]}"; do - IFS='|' read -r name desc action path <<< "$item" + IFS='|' read -r name desc action path <<<"$item" announce_action "$name" "$desc" "confirm" execute_optimization "$action" "$path" done diff --git a/bin/purge.sh b/bin/purge.sh index eb3af0b..ce67c57 100755 --- a/bin/purge.sh +++ b/bin/purge.sh @@ -42,6 +42,10 @@ note_activity() { # 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' @@ -214,6 +218,9 @@ perform_purge() { summary_details+=("Free space now: $(get_free_space)") 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' } diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 13b20cc..cc415fc 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -374,6 +374,8 @@ cleanup() { wait "$sudo_keepalive_pid" 2> /dev/null || true sudo_keepalive_pid="" fi + # Log session end + log_operation_session_end "uninstall" "${files_cleaned:-0}" "${total_size_cleaned:-0}" show_cursor exit "${1:-0}" } @@ -381,6 +383,10 @@ cleanup() { trap cleanup EXIT INT TERM main() { + # Set current command for operation logging + export MOLE_CURRENT_COMMAND="uninstall" + log_operation_session_start "uninstall" + local force_rescan=false # Global flags for arg in "$@"; do diff --git a/lib/core/file_ops.sh b/lib/core/file_ops.sh index ccff245..a083433 100644 --- a/lib/core/file_ops.sh +++ b/lib/core/file_ops.sh @@ -42,7 +42,7 @@ validate_path_for_deletion() { # Check symlink target if path is a symbolic link if [[ -L "$path" ]]; then local link_target - link_target=$(readlink "$path" 2> /dev/null) || { + link_target=$(readlink "$path" 2>/dev/null) || { log_error "Cannot read symlink: $path" return 1 } @@ -52,16 +52,16 @@ validate_path_for_deletion() { if [[ "$link_target" != /* ]]; then local link_dir link_dir=$(dirname "$path") - resolved_target=$(cd "$link_dir" 2> /dev/null && cd "$(dirname "$link_target")" 2> /dev/null && pwd)/$(basename "$link_target") || resolved_target="" + resolved_target=$(cd "$link_dir" 2>/dev/null && cd "$(dirname "$link_target")" 2>/dev/null && pwd)/$(basename "$link_target") || resolved_target="" fi # Validate resolved target against protected paths if [[ -n "$resolved_target" ]]; then case "$resolved_target" in - /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) - log_error "Symlink points to protected system path: $path -> $resolved_target" - return 1 - ;; + /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) + log_error "Symlink points to protected system path: $path -> $resolved_target" + return 1 + ;; esac fi fi @@ -88,48 +88,48 @@ validate_path_for_deletion() { # Allow deletion of coresymbolicationd cache (safe system cache that can be rebuilt) case "$path" in - /System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*) - return 0 - ;; + /System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*) + return 0 + ;; esac # Allow known safe paths under /private case "$path" in - /private/tmp | /private/tmp/* | \ - /private/var/tmp | /private/var/tmp/* | \ - /private/var/log | /private/var/log/* | \ - /private/var/folders | /private/var/folders/* | \ - /private/var/db/diagnostics | /private/var/db/diagnostics/* | \ - /private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \ - /private/var/db/powerlog | /private/var/db/powerlog/* | \ - /private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/* | \ - /private/var/db/receipts/*.bom | /private/var/db/receipts/*.plist) - return 0 - ;; + /private/tmp | /private/tmp/* | \ + /private/var/tmp | /private/var/tmp/* | \ + /private/var/log | /private/var/log/* | \ + /private/var/folders | /private/var/folders/* | \ + /private/var/db/diagnostics | /private/var/db/diagnostics/* | \ + /private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \ + /private/var/db/powerlog | /private/var/db/powerlog/* | \ + /private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/* | \ + /private/var/db/receipts/*.bom | /private/var/db/receipts/*.plist) + return 0 + ;; esac # Check path isn't critical system directory case "$path" in - / | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions) - log_error "Path validation failed: critical system directory: $path" - return 1 - ;; - /private) - log_error "Path validation failed: critical system directory: $path" - return 1 - ;; - /etc | /etc/* | /private/etc | /private/etc/*) - log_error "Path validation failed: /etc contains critical system files: $path" - return 1 - ;; - /var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*) - log_error "Path validation failed: /var/db contains system databases: $path" - return 1 - ;; + / | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions) + log_error "Path validation failed: critical system directory: $path" + return 1 + ;; + /private) + log_error "Path validation failed: critical system directory: $path" + return 1 + ;; + /etc | /etc/* | /private/etc | /private/etc/*) + log_error "Path validation failed: /etc contains critical system files: $path" + return 1 + ;; + /var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*) + log_error "Path validation failed: /var/db contains system databases: $path" + return 1 + ;; esac # Check if path is protected (keychains, system settings, etc) - if declare -f should_protect_path > /dev/null 2>&1; then + if declare -f should_protect_path >/dev/null 2>&1; then if should_protect_path "$path"; then if [[ "${MO_DEBUG:-0}" == "1" ]]; then log_warning "Path validation: protected path skipped: $path" @@ -172,16 +172,16 @@ safe_remove() { if [[ -e "$path" ]]; then local size_kb - size_kb=$(get_path_size_kb "$path" 2> /dev/null || echo "0") + size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0") if [[ "$size_kb" -gt 0 ]]; then file_size=$(bytes_to_human "$((size_kb * 1024))") fi if [[ -f "$path" || -d "$path" ]] && ! [[ -L "$path" ]]; then local mod_time - mod_time=$(stat -f%m "$path" 2> /dev/null || echo "0") + mod_time=$(stat -f%m "$path" 2>/dev/null || echo "0") local now - now=$(date +%s 2> /dev/null || echo "0") + now=$(date +%s 2>/dev/null || echo "0") if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then file_age=$(((now - mod_time) / 86400)) fi @@ -197,6 +197,18 @@ safe_remove() { debug_log "Removing: $path" + # Calculate size before deletion for logging + local size_kb=0 + local size_human="" + if oplog_enabled; then + if [[ -e "$path" ]]; then + size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0") + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$size_kb" -gt 0 ]]; then + size_human=$(bytes_to_human "$((size_kb * 1024))" 2>/dev/null || echo "${size_kb}KB") + fi + fi + fi + # Perform the deletion # Use || to capture the exit code so set -e won't abort on rm failures local error_msg @@ -204,6 +216,8 @@ safe_remove() { error_msg=$(rm -rf "$path" 2>&1) || rm_exit=$? # safe_remove if [[ $rm_exit -eq 0 ]]; then + # Log successful removal + log_operation "${MOLE_CURRENT_COMMAND:-clean}" "REMOVED" "$path" "$size_human" return 0 else # Check if it's a permission error @@ -212,8 +226,10 @@ safe_remove() { MOLE_PERMISSION_DENIED_COUNT=$((MOLE_PERMISSION_DENIED_COUNT + 1)) export MOLE_PERMISSION_DENIED_COUNT debug_log "Permission denied: $path, may need Full Disk Access" + log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "permission denied" else [[ "$silent" != "true" ]] && log_error "Failed to remove: $path" + log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "error" fi return 1 fi @@ -249,18 +265,18 @@ safe_sudo_remove() { local file_size="" local file_age="" - if sudo test -e "$path" 2> /dev/null; then + if sudo test -e "$path" 2>/dev/null; then local size_kb - size_kb=$(sudo du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0") + size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then file_size=$(bytes_to_human "$((size_kb * 1024))") fi - if sudo test -f "$path" 2> /dev/null || sudo test -d "$path" 2> /dev/null; then + if sudo test -f "$path" 2>/dev/null || sudo test -d "$path" 2>/dev/null; then local mod_time - mod_time=$(sudo stat -f%m "$path" 2> /dev/null || echo "0") + mod_time=$(sudo stat -f%m "$path" 2>/dev/null || echo "0") local now - now=$(date +%s 2> /dev/null || echo "0") + now=$(date +%s 2>/dev/null || echo "0") if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then file_age=$(((now - mod_time) / 86400)) fi @@ -276,11 +292,25 @@ safe_sudo_remove() { debug_log "Removing, sudo: $path" + # Calculate size before deletion for logging + local size_kb=0 + local size_human="" + if oplog_enabled; then + if sudo test -e "$path" 2>/dev/null; then + size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$size_kb" -gt 0 ]]; then + size_human=$(bytes_to_human "$((size_kb * 1024))" 2>/dev/null || echo "${size_kb}KB") + fi + fi + fi + # Perform the deletion - if sudo rm -rf "$path" 2> /dev/null; then # SAFE: safe_sudo_remove implementation + if sudo rm -rf "$path" 2>/dev/null; then # SAFE: safe_sudo_remove implementation + log_operation "${MOLE_CURRENT_COMMAND:-clean}" "REMOVED" "$path" "$size_human" return 0 else log_error "Failed to remove, sudo: $path" + log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "sudo error" return 1 fi } @@ -326,7 +356,7 @@ safe_find_delete() { continue fi safe_remove "$match" true || true - done < <(command find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) + done < <(command find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true) return 0 } @@ -339,12 +369,12 @@ safe_sudo_find_delete() { local type_filter="${4:-f}" # Validate base directory (use sudo for permission-restricted dirs) - if ! sudo test -d "$base_dir" 2> /dev/null; then + if ! sudo test -d "$base_dir" 2>/dev/null; then debug_log "Directory does not exist, skipping: $base_dir" return 0 fi - if sudo test -L "$base_dir" 2> /dev/null; then + if sudo test -L "$base_dir" 2>/dev/null; then log_error "Refusing to search symlinked directory: $base_dir" return 1 fi @@ -368,7 +398,7 @@ safe_sudo_find_delete() { continue fi safe_sudo_remove "$match" || true - done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) + done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true) return 0 } @@ -388,7 +418,7 @@ get_path_size_kb() { # Use || echo 0 to ensure failure in du (e.g. permission error) doesn't exit script under set -e # Pipefail would normally cause the pipeline to fail if du fails, but || handle catches it. local size - size=$(command du -sk "$path" 2> /dev/null | awk 'NR==1 {print $1; exit}' || true) + size=$(command du -sk "$path" 2>/dev/null | awk 'NR==1 {print $1; exit}' || true) # Ensure size is a valid number (fix for non-numeric du output) if [[ "$size" =~ ^[0-9]+$ ]]; then @@ -409,7 +439,7 @@ calculate_total_size() { size_kb=$(get_path_size_kb "$file") ((total_kb += size_kb)) fi - done <<< "$files" + done <<<"$files" echo "$total_kb" } diff --git a/lib/core/log.sh b/lib/core/log.sh index 8d7f085..2268d84 100644 --- a/lib/core/log.sh +++ b/lib/core/log.sh @@ -23,10 +23,15 @@ fi readonly LOG_FILE="${HOME}/.config/mole/mole.log" readonly DEBUG_LOG_FILE="${HOME}/.config/mole/mole_debug_session.log" -readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB +readonly OPERATIONS_LOG_FILE="${HOME}/.config/mole/operations.log" +readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB +readonly OPLOG_MAX_SIZE_DEFAULT=5242880 # 5MB # Ensure log directory and file exist with correct ownership ensure_user_file "$LOG_FILE" +if [[ "${MO_NO_OPLOG:-}" != "1" ]]; then + ensure_user_file "$OPERATIONS_LOG_FILE" +fi # ============================================================================ # Log Rotation @@ -40,9 +45,18 @@ rotate_log_once() { local max_size="$LOG_MAX_SIZE_DEFAULT" if [[ -f "$LOG_FILE" ]] && [[ $(get_file_size "$LOG_FILE") -gt "$max_size" ]]; then - mv "$LOG_FILE" "${LOG_FILE}.old" 2> /dev/null || true + mv "$LOG_FILE" "${LOG_FILE}.old" 2>/dev/null || true ensure_user_file "$LOG_FILE" fi + + # Rotate operations log (5MB limit) + if [[ "${MO_NO_OPLOG:-}" != "1" ]]; then + local oplog_max_size="$OPLOG_MAX_SIZE_DEFAULT" + if [[ -f "$OPERATIONS_LOG_FILE" ]] && [[ $(get_file_size "$OPERATIONS_LOG_FILE") -gt "$oplog_max_size" ]]; then + mv "$OPERATIONS_LOG_FILE" "${OPERATIONS_LOG_FILE}.old" 2>/dev/null || true + ensure_user_file "$OPERATIONS_LOG_FILE" + fi + fi } # ============================================================================ @@ -53,9 +67,9 @@ rotate_log_once() { log_info() { echo -e "${BLUE}$1${NC}" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] INFO: $1" >> "$LOG_FILE" 2> /dev/null || true + echo "[$timestamp] INFO: $1" >>"$LOG_FILE" 2>/dev/null || true if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] INFO: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "[$timestamp] INFO: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -63,9 +77,9 @@ log_info() { log_success() { echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] SUCCESS: $1" >> "$LOG_FILE" 2> /dev/null || true + echo "[$timestamp] SUCCESS: $1" >>"$LOG_FILE" 2>/dev/null || true if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] SUCCESS: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "[$timestamp] SUCCESS: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -73,9 +87,9 @@ log_success() { log_warning() { echo -e "${YELLOW}$1${NC}" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] WARNING: $1" >> "$LOG_FILE" 2> /dev/null || true + echo "[$timestamp] WARNING: $1" >>"$LOG_FILE" 2>/dev/null || true if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] WARNING: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "[$timestamp] WARNING: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -83,9 +97,9 @@ log_warning() { log_error() { echo -e "${YELLOW}${ICON_ERROR}${NC} $1" >&2 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true + echo "[$timestamp] ERROR: $1" >>"$LOG_FILE" 2>/dev/null || true if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] ERROR: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "[$timestamp] ERROR: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -93,10 +107,84 @@ log_error() { debug_log() { if [[ "${MO_DEBUG:-}" == "1" ]]; then echo -e "${GRAY}[DEBUG]${NC} $*" >&2 - echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } +# ============================================================================ +# Operation Logging (Enabled by default) +# ============================================================================ +# Records all file operations for user troubleshooting +# Disable with MO_NO_OPLOG=1 + +oplog_enabled() { + [[ "${MO_NO_OPLOG:-}" != "1" ]] +} + +# Log an operation to the operations log file +# Usage: log_operation [detail] +# Example: log_operation "clean" "REMOVED" "/path/to/file" "15.2MB" +# Example: log_operation "clean" "SKIPPED" "/path/to/file" "whitelist" +# Example: log_operation "uninstall" "REMOVED" "/Applications/App.app" "150MB" +log_operation() { + # Allow disabling via environment variable + oplog_enabled || return 0 + + local command="${1:-unknown}" # clean/uninstall/optimize/purge + local action="${2:-UNKNOWN}" # REMOVED/SKIPPED/FAILED/REBUILT + local path="${3:-}" + local detail="${4:-}" + + # Skip if no path provided + [[ -z "$path" ]] && return 0 + + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + local log_line="[$timestamp] [$command] $action $path" + [[ -n "$detail" ]] && log_line+=" ($detail)" + + echo "$log_line" >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true +} + +# Log session start marker +# Usage: log_operation_session_start +log_operation_session_start() { + oplog_enabled || return 0 + + local command="${1:-mole}" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + { + echo "" + echo "# ========== $command session started at $timestamp ==========" + } >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true +} + +# Log session end with summary +# Usage: log_operation_session_end +log_operation_session_end() { + oplog_enabled || return 0 + + local command="${1:-mole}" + local items="${2:-0}" + local size="${3:-0}" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + local size_human="" + if [[ "$size" =~ ^[0-9]+$ ]] && [[ "$size" -gt 0 ]]; then + size_human=$(bytes_to_human "$((size * 1024))" 2>/dev/null || echo "${size}KB") + else + size_human="0B" + fi + + { + echo "# ========== $command session ended at $timestamp, $items items, $size_human ==========" + } >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true +} + # Enhanced debug logging for operations debug_operation_start() { local operation_name="$1" @@ -112,7 +200,7 @@ debug_operation_start() { echo "" echo "=== $operation_name ===" [[ -n "$operation_desc" ]] && echo "Description: $operation_desc" - } >> "$DEBUG_LOG_FILE" 2> /dev/null || true + } >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -126,7 +214,7 @@ debug_operation_detail() { echo -e "${GRAY}[DEBUG] $detail_type: $detail_value${NC}" >&2 # Also log to file - echo "$detail_type: $detail_value" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "$detail_type: $detail_value" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -146,7 +234,7 @@ debug_file_action() { echo -e "${GRAY}[DEBUG] $action: $msg${NC}" >&2 # Also log to file - echo "$action: $msg" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "$action: $msg" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -158,16 +246,16 @@ debug_risk_level() { if [[ "${MO_DEBUG:-}" == "1" ]]; then local color="$GRAY" case "$risk_level" in - LOW) color="$GREEN" ;; - MEDIUM) color="$YELLOW" ;; - HIGH) color="$RED" ;; + LOW) color="$GREEN" ;; + MEDIUM) color="$YELLOW" ;; + HIGH) color="$RED" ;; esac # Output to stderr with color echo -e "${GRAY}[DEBUG] Risk Level: ${color}${risk_level}${GRAY}, $reason${NC}" >&2 # Also log to file - echo "Risk Level: $risk_level, $reason" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + echo "Risk Level: $risk_level, $reason" >>"$DEBUG_LOG_FILE" 2>/dev/null || true fi } @@ -179,7 +267,7 @@ log_system_info() { # Reset debug log file for this new session ensure_user_file "$DEBUG_LOG_FILE" - if ! : > "$DEBUG_LOG_FILE" 2> /dev/null; then + if ! : >"$DEBUG_LOG_FILE" 2>/dev/null; then echo -e "${YELLOW}${ICON_WARNING}${NC} Debug log not writable: $DEBUG_LOG_FILE" >&2 fi @@ -192,19 +280,19 @@ log_system_info() { echo "Hostname: $(hostname)" echo "Architecture: $(uname -m)" echo "Kernel: $(uname -r)" - if command -v sw_vers > /dev/null; then + if command -v sw_vers >/dev/null; then echo "macOS: $(sw_vers -productVersion), $(sw_vers -buildVersion)" fi echo "Shell: ${SHELL:-unknown}, ${TERM:-unknown}" # Check sudo status non-interactively - if sudo -n true 2> /dev/null; then + if sudo -n true 2>/dev/null; then echo "Sudo Access: Active" else echo "Sudo Access: Required" fi echo "----------------------------------------------------------------------" - } >> "$DEBUG_LOG_FILE" 2> /dev/null || true + } >>"$DEBUG_LOG_FILE" 2>/dev/null || true # Notification to stderr echo -e "${GRAY}[DEBUG] Debug logging enabled. Session log: $DEBUG_LOG_FILE${NC}" >&2 @@ -216,7 +304,7 @@ log_system_info() { # Run command silently (ignore errors) run_silent() { - "$@" > /dev/null 2>&1 || true + "$@" >/dev/null 2>&1 || true } # Run command with error logging @@ -224,12 +312,12 @@ run_logged() { local cmd="$1" # Log to main file, and also to debug file if enabled if [[ "${MO_DEBUG:-}" == "1" ]]; then - if ! "$@" 2>&1 | tee -a "$LOG_FILE" | tee -a "$DEBUG_LOG_FILE" > /dev/null; then + if ! "$@" 2>&1 | tee -a "$LOG_FILE" | tee -a "$DEBUG_LOG_FILE" >/dev/null; then log_warning "Command failed: $cmd" return 1 fi else - if ! "$@" 2>&1 | tee -a "$LOG_FILE" > /dev/null; then + if ! "$@" 2>&1 | tee -a "$LOG_FILE" >/dev/null; then log_warning "Command failed: $cmd" return 1 fi