diff --git a/README.md b/README.md index fc6d63e..7b97b53 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Review [SECURITY.md](SECURITY.md) and [SECURITY_AUDIT.md](SECURITY_AUDIT.md) for ## Tips - Video tutorial: Watch the [Mole tutorial video](https://www.youtube.com/watch?v=UEe9-w4CcQ0), thanks to PAPAYA 電腦教室. -- Safety and logs: `clean`, `uninstall`, `purge`, `installer`, and `remove` are destructive. Review with `--dry-run` first, and add `--debug` when needed. File operations are logged to `~/.config/mole/operations.log`. Disable with `MO_NO_OPLOG=1`. Review [SECURITY.md](SECURITY.md) and [SECURITY_AUDIT.md](SECURITY_AUDIT.md). +- Safety and logs: `clean`, `uninstall`, `purge`, `installer`, and `remove` are destructive. Review with `--dry-run` first, and add `--debug` when needed. File operations are logged to `~/Library/Logs/mole/operations.log`. Disable with `MO_NO_OPLOG=1`. Review [SECURITY.md](SECURITY.md) and [SECURITY_AUDIT.md](SECURITY_AUDIT.md). - Navigation: Mole supports arrow keys and Vim bindings `h/j/k/l`. ## Features in Detail diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md index 3663832..3a40b37 100644 --- a/SECURITY_AUDIT.md +++ b/SECURITY_AUDIT.md @@ -226,7 +226,7 @@ Mole exposes multiple safety controls before and during destructive actions: - interactive high-risk flows require explicit confirmation before deletion - purge marks recent projects conservatively and leaves them unselected by default - analyzer delete uses Finder Trash rather than direct permanent removal -- operation logs are written to `~/.config/mole/operations.log` unless disabled with `MO_NO_OPLOG=1` +- operation logs are written to `~/Library/Logs/mole/operations.log` unless disabled with `MO_NO_OPLOG=1` - timeouts bound external commands so stalled discovery or uninstall operations do not silently hang the entire flow Relevant timeout behavior includes: diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index e8a1163..f6cb487 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -802,6 +802,10 @@ should_protect_path() { */Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist) return 0 ;; + # Protect Mole's own runtime logs so cleanup cannot delete its active log targets. + */Library/Logs/mole | */Library/Logs/mole/ | */Library/Logs/mole/*) + return 0 + ;; # Bluetooth and WiFi configurations */ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*) return 0 diff --git a/lib/core/log.sh b/lib/core/log.sh index 8001402..3092257 100644 --- a/lib/core/log.sh +++ b/lib/core/log.sh @@ -37,6 +37,22 @@ fi # Log Rotation # ============================================================================ +append_log_line() { + local file_path="$1" + local line="${2:-}" + + ensure_user_file "$file_path" + printf '%s\n' "$line" >> "$file_path" 2> /dev/null || true +} + +append_log_lines() { + local file_path="$1" + shift + + ensure_user_file "$file_path" + printf '%s\n' "$@" >> "$file_path" 2> /dev/null || true +} + # Rotate log file if it exceeds maximum size rotate_log_once() { # Skip if already checked this session @@ -81,9 +97,9 @@ log_info() { echo -e "${BLUE}$1${NC}" local timestamp timestamp=$(get_timestamp) - echo "[$timestamp] INFO: $1" >> "$LOG_FILE" 2> /dev/null || true + append_log_line "$LOG_FILE" "[$timestamp] INFO: $1" if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] INFO: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + append_log_line "$DEBUG_LOG_FILE" "[$timestamp] INFO: $1" fi } @@ -92,9 +108,9 @@ log_success() { echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1" local timestamp timestamp=$(get_timestamp) - echo "[$timestamp] SUCCESS: $1" >> "$LOG_FILE" 2> /dev/null || true + append_log_line "$LOG_FILE" "[$timestamp] SUCCESS: $1" if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] SUCCESS: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + append_log_line "$DEBUG_LOG_FILE" "[$timestamp] SUCCESS: $1" fi } @@ -103,9 +119,9 @@ log_warning() { echo -e "${YELLOW}$1${NC}" local timestamp timestamp=$(get_timestamp) - echo "[$timestamp] WARNING: $1" >> "$LOG_FILE" 2> /dev/null || true + append_log_line "$LOG_FILE" "[$timestamp] WARNING: $1" if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] WARNING: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + append_log_line "$DEBUG_LOG_FILE" "[$timestamp] WARNING: $1" fi } @@ -114,9 +130,9 @@ log_error() { echo -e "${YELLOW}${ICON_ERROR}${NC} $1" >&2 local timestamp timestamp=$(get_timestamp) - echo "[$timestamp] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true + append_log_line "$LOG_FILE" "[$timestamp] ERROR: $1" if [[ "${MO_DEBUG:-}" == "1" ]]; then - echo "[$timestamp] ERROR: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + append_log_line "$DEBUG_LOG_FILE" "[$timestamp] ERROR: $1" fi } @@ -126,7 +142,7 @@ debug_log() { echo -e "${GRAY}[DEBUG]${NC} $*" >&2 local timestamp timestamp=$(get_timestamp) - echo "[$timestamp] DEBUG: $*" >> "$DEBUG_LOG_FILE" 2> /dev/null || true + append_log_line "$DEBUG_LOG_FILE" "[$timestamp] DEBUG: $*" fi } @@ -163,7 +179,7 @@ log_operation() { local log_line="[$timestamp] [$command] $action $path" [[ -n "$detail" ]] && log_line+=" ($detail)" - echo "$log_line" >> "$OPERATIONS_LOG_FILE" 2> /dev/null || true + append_log_line "$OPERATIONS_LOG_FILE" "$log_line" } # Log session start marker @@ -175,10 +191,10 @@ log_operation_session_start() { local timestamp timestamp=$(get_timestamp) - { - echo "" - echo "# ========== $command session started at $timestamp ==========" - } >> "$OPERATIONS_LOG_FILE" 2> /dev/null || true + append_log_lines \ + "$OPERATIONS_LOG_FILE" \ + "" \ + "# ========== $command session started at $timestamp ==========" } # shellcheck disable=SC2329 @@ -198,9 +214,9 @@ log_operation_session_end() { size_human="0B" fi - { - echo "# ========== $command session ended at $timestamp, $items items, $size_human ==========" - } >> "$OPERATIONS_LOG_FILE" 2> /dev/null || true + append_log_line \ + "$OPERATIONS_LOG_FILE" \ + "# ========== $command session ended at $timestamp, $items items, $size_human ==========" } # Enhanced debug logging for operations @@ -214,11 +230,18 @@ debug_operation_start() { [[ -n "$operation_desc" ]] && echo -e "${GRAY}[DEBUG] $operation_desc${NC}" >&2 # Also log to file - { - echo "" - echo "=== $operation_name ===" - [[ -n "$operation_desc" ]] && echo "Description: $operation_desc" - } >> "$DEBUG_LOG_FILE" 2> /dev/null || true + if [[ -n "$operation_desc" ]]; then + append_log_lines \ + "$DEBUG_LOG_FILE" \ + "" \ + "=== $operation_name ===" \ + "Description: $operation_desc" + else + append_log_lines \ + "$DEBUG_LOG_FILE" \ + "" \ + "=== $operation_name ===" + fi fi } @@ -232,7 +255,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 + append_log_line "$DEBUG_LOG_FILE" "$detail_type: $detail_value" fi } @@ -252,7 +275,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 + append_log_line "$DEBUG_LOG_FILE" "$action: $msg" fi } diff --git a/tests/clean_user_core.bats b/tests/clean_user_core.bats index ec21cbc..a7ca90a 100644 --- a/tests/clean_user_core.bats +++ b/tests/clean_user_core.bats @@ -80,6 +80,41 @@ EOF [[ "$output" == *"Trash · emptied, 2 items"* ]] } +@test "clean_user_essentials keeps Mole runtime logs while cleaning other user logs" { + mkdir -p "$HOME/Library/Logs/mole" + mkdir -p "$HOME/Library/Logs/OtherApp" + touch "$HOME/Library/Logs/mole/operations.log" + touch "$HOME/Library/Logs/OtherApp/old.log" + + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" +DRY_RUN=false +start_section_spinner() { :; } +stop_section_spinner() { :; } +note_activity() { :; } +is_path_whitelisted() { return 1; } +safe_clean() { + local path="" + for path in "${@:1:$#-1}"; do + if should_protect_path "$path"; then + continue + fi + /bin/rm -rf "$path" + done +} + +clean_user_essentials + +[[ -d "$HOME/Library/Logs/mole" ]] +[[ -f "$HOME/Library/Logs/mole/operations.log" ]] +[[ ! -e "$HOME/Library/Logs/OtherApp/old.log" ]] +EOF + + [ "$status" -eq 0 ] +} + @test "clean_app_caches includes macOS system caches" { run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' set -euo pipefail diff --git a/tests/core_common.bats b/tests/core_common.bats index eb2e8b5..4314d99 100644 --- a/tests/core_common.bats +++ b/tests/core_common.bats @@ -69,6 +69,25 @@ setup() { grep -q "ERROR: $message" "$log_file" } +@test "log_operation recreates operations log if the log directory disappears mid-session" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +rm -rf "$HOME/Library/Logs/mole" +log_operation "clean" "REMOVED" "/tmp/example" "1KB" +EOF + [ "$status" -eq 0 ] + + local oplog="$HOME/Library/Logs/mole/operations.log" + [[ -f "$oplog" ]] + grep -Fq "[clean] REMOVED /tmp/example (1KB)" "$oplog" +} + +@test "should_protect_path protects Mole runtime logs" { + result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; should_protect_path '$HOME/Library/Logs/mole/operations.log' && echo protected || echo not-protected")" + [ "$result" = "protected" ] +} + @test "rotate_log_once only checks log size once per session" { local log_file="$HOME/Library/Logs/mole/mole.log" mkdir -p "$(dirname "$log_file")"