1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

🎨 Updates for better use and support

This commit is contained in:
Tw93
2025-09-30 13:48:28 +08:00
parent ced0221540
commit 59f4c22a32
6 changed files with 297 additions and 102 deletions

View File

@@ -130,26 +130,74 @@ safe_clean() {
local total_size_bytes=0 local total_size_bytes=0
local total_count=0 local total_count=0
# Optimized: skip size calculation for empty checks, just try to delete
# Size calculation is the slowest part - do it in parallel
local -a existing_paths=()
for path in "${targets[@]}"; do for path in "${targets[@]}"; do
local size_bytes=0 [[ -e "$path" ]] && existing_paths+=("$path")
local count=0
if [[ -e "$path" ]]; then
size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then
continue
fi
rm -rf "$path" 2>/dev/null || true
((total_size_bytes += size_bytes))
((total_count += count))
removed_any=1
fi
done done
if [[ ${#existing_paths[@]} -eq 0 ]]; then
LAST_CLEAN_RESULT=0
return 0
fi
# Fast parallel processing for multiple targets
if [[ ${#existing_paths[@]} -gt 3 ]]; then
local temp_dir=$(mktemp -d)
# Launch parallel du jobs (bash 3.2 compatible)
local -a pids=()
for path in "${existing_paths[@]}"; do
(
local size=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
echo "$size $count" > "$temp_dir/$(echo -n "$path" | shasum -a 256 | cut -d' ' -f1)"
) &
pids+=($!)
# Limit to 15 parallel jobs (bash 3.2 compatible)
if (( ${#pids[@]} >= 15 )); then
wait "${pids[0]}" 2>/dev/null || true
pids=("${pids[@]:1}")
fi
done
# Wait for remaining jobs
for pid in "${pids[@]}"; do
wait "$pid" 2>/dev/null || true
done
# Collect results and delete
for path in "${existing_paths[@]}"; do
local hash=$(echo -n "$path" | shasum -a 256 | cut -d' ' -f1)
if [[ -f "$temp_dir/$hash" ]]; then
read -r size count < "$temp_dir/$hash"
if [[ "$count" -gt 0 && "$size" -gt 0 ]]; then
rm -rf "$path" 2>/dev/null || true
((total_size_bytes += size))
((total_count += count))
removed_any=1
fi
fi
done
rm -rf "$temp_dir"
else
# Serial processing for few targets (faster than parallel overhead)
for path in "${existing_paths[@]}"; do
local size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
if [[ "$count" -gt 0 && "$size_bytes" -gt 0 ]]; then
rm -rf "$path" 2>/dev/null || true
((total_size_bytes += size_bytes))
((total_count += count))
removed_any=1
fi
done
fi
# Only show output if something was actually cleaned # Only show output if something was actually cleaned
if [[ $removed_any -eq 1 ]]; then if [[ $removed_any -eq 1 ]]; then
local size_human local size_human
@@ -178,7 +226,7 @@ safe_clean() {
} }
start_cleanup() { start_cleanup() {
echo "Removing app caches, browser data, developer tools, and temporary files..." echo "Mole will remove app caches, browser data, developer tools, and temporary files."
echo "" echo ""
# Check if we're in an interactive terminal # Check if we're in an interactive terminal
@@ -191,13 +239,33 @@ start_cleanup() {
else else
# Non-interactive mode - skip password prompt # Non-interactive mode - skip password prompt
password="" password=""
log_info "Running in non-interactive mode, skipping system-level cleanup." echo ""
echo -e "${BLUE}${NC} Running in non-interactive mode"
echo " • System-level cleanup will be skipped (requires password)"
echo " • User-level cleanup will proceed automatically"
echo ""
fi fi
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
SYSTEM_CLEAN=true SYSTEM_CLEAN=true
# Start sudo keepalive with shorter intervals for reliability # Start sudo keepalive with error handling and shorter intervals
while true; do sudo -n true; sleep 30; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & (
local retry_count=0
while true; do
if ! sudo -n true 2>/dev/null; then
((retry_count++))
if [[ $retry_count -ge 3 ]]; then
log_warning "Sudo keepalive failed, system-level cleanup may be interrupted" >&2
exit 1
fi
sleep 5
continue
fi
retry_count=0
sleep 30
kill -0 "$$" 2>/dev/null || exit
done
) 2>/dev/null &
SUDO_KEEPALIVE_PID=$! SUDO_KEEPALIVE_PID=$!
log_info "Starting comprehensive cleanup with admin privileges..." log_info "Starting comprehensive cleanup with admin privileges..."
else else

View File

@@ -90,6 +90,30 @@ get_app_last_used() {
# Scan applications and collect information # Scan applications and collect information
scan_applications() { scan_applications() {
# Cache configuration
local cache_dir="$HOME/.cache/mole"
local cache_file="$cache_dir/app_scan_cache"
local cache_meta="$cache_dir/app_scan_meta"
local cache_ttl=3600 # 1 hour cache validity
mkdir -p "$cache_dir" 2>/dev/null
# Quick count of current apps
local current_app_count=$(find /Applications -name "*.app" -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')
# Check if cache is valid
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
# Cache is valid if: age < TTL AND app count matches
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
echo "Using cached app list (${cache_age}s old, $current_app_count apps) ✓" >&2
echo "$cache_file"
return 0
fi
fi
local temp_file=$(mktemp) local temp_file=$(mktemp)
echo -n "Scanning... " >&2 echo -n "Scanning... " >&2
@@ -160,24 +184,9 @@ scan_applications() {
# Select the first (best) candidate # Select the first (best) candidate
display_name="${candidates[0]:-$app_name}" display_name="${candidates[0]:-$app_name}"
# Brand name mapping for better user recognition (post-process) # Apply brand name mapping from common.sh
case "$display_name" in display_name="$(get_brand_name "$display_name")"
"qiyimac"|"爱奇艺") display_name="iQiyi" ;;
"wechat"|"微信") display_name="WeChat" ;;
"QQ"|"QQ") display_name="QQ" ;;
"VooV Meeting"|"腾讯会议") display_name="VooV Meeting" ;;
"dingtalk"|"钉钉") display_name="DingTalk" ;;
"NeteaseMusic"|"网易云音乐") display_name="NetEase Music" ;;
"BaiduNetdisk"|"百度网盘") display_name="Baidu NetDisk" ;;
"alipay"|"支付宝") display_name="Alipay" ;;
"taobao"|"淘宝") display_name="Taobao" ;;
"futunn"|"富途牛牛") display_name="Futu NiuNiu" ;;
"tencent lemon"|"Tencent Lemon Cleaner") display_name="Tencent Lemon" ;;
"keynote"|"Keynote") display_name="Keynote" ;;
"pages"|"Pages") display_name="Pages" ;;
"numbers"|"Numbers") display_name="Numbers" ;;
esac
fi fi
# Skip protected system apps early # Skip protected system apps early
@@ -189,20 +198,21 @@ scan_applications() {
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}") app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null) done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
# Second pass: process each app with accurate size calculation # Second pass: process each app with parallel size calculation
local app_count=0 local app_count=0
local total_apps=${#app_data_tuples[@]} local total_apps=${#app_data_tuples[@]}
local max_parallel=10 # Process 10 apps in parallel
local pids=()
# Process app metadata extraction function
process_app_metadata() {
local app_data_tuple="$1"
local output_file="$2"
local current_epoch="$3"
for app_data_tuple in "${app_data_tuples[@]}"; do
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple" IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
# Show progress every few items # Parallel size calculation
((app_count++))
if (( app_count % 5 == 0 )) || [[ $app_count -eq $total_apps ]]; then
echo -ne "\rScanning... $app_count/$total_apps" >&2
fi
# Accurate size calculation - this is what takes time but user wants it
local app_size="N/A" local app_size="N/A"
if [[ -d "$app_path" ]]; then if [[ -d "$app_path" ]]; then
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A") app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
@@ -216,7 +226,6 @@ scan_applications() {
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
# Convert macOS date format to epoch
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0") last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0")
if [[ $last_used_epoch -gt 0 ]]; then if [[ $last_used_epoch -gt 0 ]]; then
@@ -230,29 +239,17 @@ scan_applications() {
last_used="${days_ago} days ago" last_used="${days_ago} days ago"
elif [[ $days_ago -lt 30 ]]; then elif [[ $days_ago -lt 30 ]]; then
local weeks_ago=$(( days_ago / 7 )) local weeks_ago=$(( days_ago / 7 ))
if [[ $weeks_ago -eq 1 ]]; then [[ $weeks_ago -eq 1 ]] && last_used="1 week ago" || last_used="${weeks_ago} weeks ago"
last_used="1 week ago"
else
last_used="${weeks_ago} weeks ago"
fi
elif [[ $days_ago -lt 365 ]]; then elif [[ $days_ago -lt 365 ]]; then
local months_ago=$(( days_ago / 30 )) local months_ago=$(( days_ago / 30 ))
if [[ $months_ago -eq 1 ]]; then [[ $months_ago -eq 1 ]] && last_used="1 month ago" || last_used="${months_ago} months ago"
last_used="1 month ago"
else
last_used="${months_ago} months ago"
fi
else else
local years_ago=$(( days_ago / 365 )) local years_ago=$(( days_ago / 365 ))
if [[ $years_ago -eq 1 ]]; then [[ $years_ago -eq 1 ]] && last_used="1 year ago" || last_used="${years_ago} years ago"
last_used="1 year ago"
else
last_used="${years_ago} years ago"
fi
fi fi
fi fi
else else
# Fallback to file modification time if no usage metadata # Fallback to file modification time
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0") last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
if [[ $last_used_epoch -gt 0 ]]; then if [[ $last_used_epoch -gt 0 ]]; then
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
@@ -267,8 +264,33 @@ scan_applications() {
fi fi
fi fi
# Format: epoch|app_path|display_name|bundle_id|size|last_used_display # Write to output file atomically
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file" echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$output_file"
}
export -f process_app_metadata
# Process apps in parallel batches
for app_data_tuple in "${app_data_tuples[@]}"; do
((app_count++))
# Launch background process
process_app_metadata "$app_data_tuple" "$temp_file" "$current_epoch" &
pids+=($!)
# Update progress
echo -ne "\rScanning... $app_count/$total_apps" >&2
# Wait if we've hit max parallel limit
if (( ${#pids[@]} >= max_parallel )); then
wait "${pids[0]}" 2>/dev/null
pids=("${pids[@]:1}") # Remove first pid
fi
done
# Wait for remaining background processes
for pid in "${pids[@]}"; do
wait "$pid" 2>/dev/null
done done
echo -e "\rFound $app_count applications ✓" >&2 echo -e "\rFound $app_count applications ✓" >&2
@@ -279,9 +301,13 @@ scan_applications() {
return 1 return 1
fi fi
# Sort by last used (oldest first) and return the temp file path # Sort by last used (oldest first) and cache the result
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted"
rm -f "$temp_file" rm -f "$temp_file"
# Update cache with app count metadata
cp "${temp_file}.sorted" "$cache_file" 2>/dev/null || true
echo "$current_app_count" > "$cache_meta" 2>/dev/null || true
echo "${temp_file}.sorted" echo "${temp_file}.sorted"
} }
@@ -479,7 +505,7 @@ main() {
fi fi
echo "" echo ""
# 直接执行批量卸载,确认已在批量卸载函数中处理 # Execute batch uninstallation, confirmation handled in batch_uninstall_applications
batch_uninstall_applications batch_uninstall_applications
# Cleanup # Cleanup

View File

@@ -40,6 +40,8 @@ select_apps_for_uninstall() {
echo "" echo ""
echo "🗑️ App Uninstaller" echo "🗑️ App Uninstaller"
echo "" echo ""
echo "Mole will uninstall selected apps and clean all their related files."
echo ""
echo "Found ${#apps_data[@]} apps. Select apps to remove:" echo "Found ${#apps_data[@]} apps. Select apps to remove:"
echo "" echo ""

View File

@@ -18,7 +18,8 @@ batch_uninstall_applications() {
local total_estimated_size=0 local total_estimated_size=0
local -a app_details=() local -a app_details=()
echo "📋 Analyzing selected applications..." echo ""
echo -e "${BLUE}📋 Analyzing selected applications...${NC}"
for selected_app in "${selected_apps[@]}"; do for selected_app in "${selected_apps[@]}"; do
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
@@ -51,22 +52,24 @@ batch_uninstall_applications() {
# Show summary and get batch confirmation # Show summary and get batch confirmation
echo "" echo ""
echo "Will remove ${#selected_apps[@]} applications, free $size_display" echo -e "${YELLOW}📦 Will remove ${BLUE}${#selected_apps[@]}${YELLOW} applications, free ${GREEN}$size_display${NC}"
if [[ ${#running_apps[@]} -gt 0 ]]; then if [[ ${#running_apps[@]} -gt 0 ]]; then
echo "Running apps will be force-quit: ${running_apps[*]}" echo -e "${YELLOW}⚠️ Running apps will be force-quit: ${RED}${running_apps[*]}${NC}"
fi fi
echo "" echo ""
read -p "Press ENTER to confirm, or any other key to cancel: " -r echo -e -n "${BLUE}Press ENTER to confirm, or any other key to cancel:${NC} "
read -r
if [[ -n "$REPLY" ]]; then if [[ -n "$REPLY" ]]; then
log_info "Uninstallation cancelled by user" log_info "Uninstallation cancelled by user"
return 0 return 0
fi fi
echo "⚡ Starting uninstallation in 3 seconds... (Press Ctrl+C to abort)" echo -e "${PURPLE}⚡ Starting uninstallation in 3 seconds...${NC} ${YELLOW}(Press Ctrl+C to abort)${NC}"
sleep 1 && echo "⚡ 2..." sleep 1 && echo -e "${PURPLE}${BLUE}2${PURPLE}...${NC}"
sleep 1 && echo "⚡ 1..." sleep 1 && echo -e "${PURPLE}${BLUE}1${PURPLE}...${NC}"
sleep 1 sleep 1
echo -e "${GREEN}✨ Let's go!${NC}"
# Force quit running apps first (batch) # Force quit running apps first (batch)
if [[ ${#running_apps[@]} -gt 0 ]]; then if [[ ${#running_apps[@]} -gt 0 ]]; then
@@ -92,7 +95,7 @@ batch_uninstall_applications() {
# Decode the related files list # Decode the related files list
local related_files=$(echo "$encoded_files" | base64 -d) local related_files=$(echo "$encoded_files" | base64 -d)
echo "🗑️ Uninstalling: $app_name" echo -e "${YELLOW}🗑️ Uninstalling: ${BLUE}$app_name${NC}"
# Remove the application # Remove the application
if rm -rf "$app_path" 2>/dev/null; then if rm -rf "$app_path" 2>/dev/null; then
@@ -127,24 +130,24 @@ batch_uninstall_applications() {
echo "" echo ""
echo "====================================================================" echo "===================================================================="
echo "🎉 UNINSTALLATION COMPLETE!" echo "🎉 UNINSTALLATION COMPLETE!"
if [[ $success_count -gt 0 ]]; then if [[ $success_count -gt 0 ]]; then
if [[ $total_size_freed -gt 1048576 ]]; then if [[ $total_size_freed -gt 1048576 ]]; then
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fGB", $1/1024/1024}') local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}')
elif [[ $total_size_freed -gt 1024 ]]; then elif [[ $total_size_freed -gt 1024 ]]; then
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}') local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
else else
local freed_display="${total_size_freed}KB" local freed_display="${total_size_freed}KB"
fi fi
echo "🗑️ Apps uninstalled: $success_count | Space freed: $freed_display" echo "🗑️ Apps uninstalled: $success_count | Space freed: ${GREEN}${freed_display}${NC}"
else else
echo "🗑️ No applications were uninstalled" echo "🗑️ No applications were uninstalled"
fi fi
if [[ $failed_count -gt 0 ]]; then if [[ $failed_count -gt 0 ]]; then
echo "⚠️ Failed to uninstall: $failed_count" echo -e "${RED}⚠️ Failed to uninstall: $failed_count${NC}"
fi fi
echo "====================================================================" echo "===================================================================="
if [[ $failed_count -gt 0 ]]; then if [[ $failed_count -gt 0 ]]; then
log_warning "$failed_count applications failed to uninstall" log_warning "$failed_count applications failed to uninstall"

View File

@@ -29,7 +29,7 @@ log_info() {
log_success() { log_success() {
rotate_log rotate_log
echo -e "${GREEN}$1${NC}" echo -e " ${GREEN}${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true
} }
@@ -116,7 +116,8 @@ read_key() {
*) echo "OTHER" ;; *) echo "OTHER" ;;
esac esac
else else
echo "OTHER" # ESC pressed alone - treat as quit
echo "QUIT"
fi fi
;; ;;
*) echo "OTHER" ;; *) echo "OTHER" ;;
@@ -253,15 +254,22 @@ readonly PRESERVED_BUNDLE_PATTERNS=(
".GlobalPreferences" ".GlobalPreferences"
# Input methods (critical for international users) # Input methods (critical for international users)
"com.tencent.inputmethod.*" # Specific input method bundles
"com.sogou.*" "com.tencent.inputmethod.QQInput"
"com.baidu.*" "com.sogou.inputmethod.*"
"*.inputmethod.*" "com.baidu.inputmethod.*"
"*input*" "com.apple.inputmethod.*"
"*inputmethod*" "com.googlecode.rimeime.*"
"*InputMethod*" "im.rime.*"
"*ime*" "org.pqrs.Karabiner*"
"*IME*" # Generic patterns (more conservative)
"*.inputmethod"
"*.InputMethod"
"*IME"
# Keep system input services safe
"com.apple.inputsource*"
"com.apple.TextInputMenuAgent"
"com.apple.TextInputSwitcher"
# Cleanup and system tools (avoid infinite loops and preserve licenses) # Cleanup and system tools (avoid infinite loops and preserve licenses)
"com.nektony.*" # App Cleaner & Uninstaller "com.nektony.*" # App Cleaner & Uninstaller
@@ -382,3 +390,27 @@ calculate_total_size() {
echo "$total_kb" echo "$total_kb"
} }
# Get normalized brand name (bash 3.2 compatible using case statement)
get_brand_name() {
local name="$1"
# Brand name mapping for better user recognition
case "$name" in
"qiyimac"|"爱奇艺") echo "iQiyi" ;;
"wechat"|"微信") echo "WeChat" ;;
"QQ") echo "QQ" ;;
"VooV Meeting"|"腾讯会议") echo "VooV Meeting" ;;
"dingtalk"|"钉钉") echo "DingTalk" ;;
"NeteaseMusic"|"网易云音乐") echo "NetEase Music" ;;
"BaiduNetdisk"|"百度网盘") echo "Baidu NetDisk" ;;
"alipay"|"支付宝") echo "Alipay" ;;
"taobao"|"淘宝") echo "Taobao" ;;
"futunn"|"富途牛牛") echo "Futu NiuNiu" ;;
"tencent lemon"|"Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
"keynote"|"Keynote") echo "Keynote" ;;
"pages"|"Pages") echo "Pages" ;;
"numbers"|"Numbers") echo "Numbers" ;;
*) echo "$name" ;; # Return original if no mapping found
esac
}

80
mole
View File

@@ -21,11 +21,72 @@ source "$SCRIPT_DIR/lib/common.sh"
# Version info # Version info
VERSION="1.1.0" VERSION="1.1.0"
MOLE_TAGLINE="Dig deep like a mole to clean your Mac." MOLE_TAGLINE="can dig deep to clean your Mac."
# Check for updates (non-blocking, cached)
check_for_updates() {
local cache_dir="$HOME/.cache/mole"
local version_cache="$cache_dir/version_check"
local check_interval=86400 # Check once per day (24 hours)
mkdir -p "$cache_dir" 2>/dev/null
# Check if we should run version check (based on cache age)
if [[ -f "$version_cache" ]]; then
local cache_age=$(($(date +%s) - $(stat -f%m "$version_cache" 2>/dev/null || echo 0)))
if [[ $cache_age -lt $check_interval ]]; then
# Cache is still fresh, show cached message if exists
if [[ -s "$version_cache" ]]; then
cat "$version_cache"
fi
return 0
fi
fi
# Run version check in background (non-blocking)
(
local latest_version=""
local timeout=3 # 3 second timeout for version check
# Try to fetch latest version from GitHub with timeout
if command -v curl >/dev/null 2>&1; then
latest_version=$(curl -fsSL --connect-timeout 2 --max-time $timeout \
"https://api.github.com/repos/tw93/mole/releases/latest" 2>/dev/null | \
grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^[Vv]//')
elif command -v wget >/dev/null 2>&1; then
latest_version=$(wget -qO- --timeout=$timeout --tries=1 \
"https://api.github.com/repos/tw93/mole/releases/latest" 2>/dev/null | \
grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^[Vv]//')
fi
# Compare versions if fetch succeeded
if [[ -n "$latest_version" && "$latest_version" != "$VERSION" ]]; then
# Version mismatch - cache the update message
local msg="${YELLOW}📢 New version available: ${GREEN}${latest_version}${YELLOW} (current: ${VERSION})${NC}\n Run ${GREEN}mole update${YELLOW} to upgrade${NC}"
echo -e "$msg" > "$version_cache"
echo -e "$msg"
else
# Up to date or check failed - clear cache
echo "" > "$version_cache"
fi
# Touch cache file to update timestamp
touch "$version_cache" 2>/dev/null
) &
# Don't wait for background check
disown 2>/dev/null || true
}
show_brand_banner() { show_brand_banner() {
printf '%b🦡 Mole — %s%b\n' \ cat << EOF
"$GREEN" "$MOLE_TAGLINE" "$NC" ${GREEN} __ __ _ ${NC}
${GREEN}| \/ | ___ | | ___ ${NC}
${GREEN}| |\/| |/ _ \| |/ _ \\${NC}
${GREEN}| | | | (_) | | __/${NC} ${BLUE}https://github.com/tw93/mole${NC}
${GREEN}|_| |_|\___/|_|\___|${NC} ${GREEN}${MOLE_TAGLINE}${NC}
EOF
} }
show_version() { show_version() {
@@ -58,17 +119,17 @@ update_mole() {
local tmp_installer local tmp_installer
tmp_installer="$(mktemp)" || { log_error "Failed to create temp file"; exit 1; } tmp_installer="$(mktemp)" || { log_error "Failed to create temp file"; exit 1; }
# Download installer # Download installer with timeout
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
if ! curl -fsSL "$installer_url" -o "$tmp_installer"; then if ! curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer"; then
rm -f "$tmp_installer" rm -f "$tmp_installer"
log_error "Failed to download installer" log_error "Failed to download installer (network timeout or error)"
exit 1 exit 1
fi fi
elif command -v wget >/dev/null 2>&1; then elif command -v wget >/dev/null 2>&1; then
if ! wget -qO "$tmp_installer" "$installer_url"; then if ! wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url"; then
rm -f "$tmp_installer" rm -f "$tmp_installer"
log_error "Failed to download installer" log_error "Failed to download installer (network timeout or error)"
exit 1 exit 1
fi fi
else else
@@ -183,6 +244,9 @@ interactive_main_menu() {
} }
main() { main() {
# Check for updates (non-blocking, won't delay startup)
check_for_updates
case "${1:-""}" in case "${1:-""}" in
"clean") "clean")
exec "$SCRIPT_DIR/bin/clean.sh" exec "$SCRIPT_DIR/bin/clean.sh"