mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 20:50:06 +00:00
Display commit hash in parentheses after "Nightly" channel label to help users identify exact version when testing nightly builds.
960 lines
31 KiB
Bash
Executable File
960 lines
31 KiB
Bash
Executable File
#!/bin/bash
|
|
# Mole - Main CLI entrypoint.
|
|
# Routes subcommands and interactive menu.
|
|
# Handles update/remove flows.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/lib/core/common.sh"
|
|
source "$SCRIPT_DIR/lib/core/commands.sh"
|
|
|
|
trap cleanup_temp_files EXIT INT TERM
|
|
|
|
# Version and update helpers
|
|
VERSION="1.28.1"
|
|
MOLE_TAGLINE="Deep clean and optimize your Mac."
|
|
|
|
is_touchid_configured() {
|
|
local pam_sudo_file="/etc/pam.d/sudo"
|
|
[[ -f "$pam_sudo_file" ]] && grep -q "pam_tid.so" "$pam_sudo_file" 2> /dev/null
|
|
}
|
|
|
|
get_latest_version() {
|
|
curl -fsSL --connect-timeout 2 --max-time 3 -H "Cache-Control: no-cache" \
|
|
"https://raw.githubusercontent.com/tw93/mole/main/mole" 2> /dev/null |
|
|
grep '^VERSION=' | head -1 | sed 's/VERSION="\(.*\)"/\1/'
|
|
}
|
|
|
|
get_latest_version_from_github() {
|
|
local version
|
|
version=$(curl -fsSL --connect-timeout 2 --max-time 3 \
|
|
"https://api.github.com/repos/tw93/mole/releases/latest" 2> /dev/null |
|
|
grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
|
version="${version#v}"
|
|
version="${version#V}"
|
|
echo "$version"
|
|
}
|
|
|
|
# Install detection (Homebrew vs manual).
|
|
# Uses variable capture + string matching to avoid SIGPIPE under pipefail.
|
|
is_homebrew_install() {
|
|
local mole_path link_target brew_list="" has_brew=false
|
|
mole_path=$(command -v mole 2> /dev/null) || return 1
|
|
|
|
# Cache brew list once if brew is available
|
|
if command -v brew > /dev/null 2>&1; then
|
|
has_brew=true
|
|
brew_list=$(brew list --formula 2> /dev/null) || true
|
|
fi
|
|
|
|
# Helper to check if mole is in brew list
|
|
_mole_in_brew_list() {
|
|
[[ -n "$brew_list" ]] && [[ $'\n'"$brew_list"$'\n' == *$'\n'"mole"$'\n'* ]]
|
|
}
|
|
|
|
if [[ -L "$mole_path" ]]; then
|
|
link_target=$(readlink "$mole_path" 2> /dev/null) || true
|
|
if [[ "$link_target" == *"Cellar/mole"* ]]; then
|
|
$has_brew && _mole_in_brew_list && return 0
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ -f "$mole_path" ]]; then
|
|
case "$mole_path" in
|
|
/opt/homebrew/bin/mole | /usr/local/bin/mole)
|
|
if [[ -d /opt/homebrew/Cellar/mole ]] || [[ -d /usr/local/Cellar/mole ]]; then
|
|
if $has_brew; then
|
|
_mole_in_brew_list && return 0
|
|
else
|
|
return 0 # Cellar exists, probably Homebrew install
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if $has_brew; then
|
|
local brew_prefix
|
|
brew_prefix=$(brew --prefix 2> /dev/null)
|
|
if [[ -n "$brew_prefix" && "$mole_path" == "$brew_prefix/bin/mole" && -d "$brew_prefix/Cellar/mole" ]]; then
|
|
_mole_in_brew_list && return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
get_install_channel() {
|
|
local channel_file="$SCRIPT_DIR/install_channel"
|
|
local channel="stable"
|
|
if [[ -f "$channel_file" ]]; then
|
|
channel=$(sed -n 's/^CHANNEL=\(.*\)$/\1/p' "$channel_file" | head -1)
|
|
fi
|
|
case "$channel" in
|
|
nightly | dev | stable) printf '%s\n' "$channel" ;;
|
|
*) printf 'stable\n' ;;
|
|
esac
|
|
}
|
|
|
|
get_install_commit() {
|
|
local channel_file="$SCRIPT_DIR/install_channel"
|
|
if [[ -f "$channel_file" ]]; then
|
|
sed -n 's/^COMMIT_HASH=\(.*\)$/\1/p' "$channel_file" | head -1
|
|
fi
|
|
}
|
|
|
|
# Background update notice
|
|
check_for_updates() {
|
|
local msg_cache="$HOME/.cache/mole/update_message"
|
|
ensure_user_dir "$(dirname "$msg_cache")"
|
|
ensure_user_file "$msg_cache"
|
|
|
|
(
|
|
(
|
|
local latest
|
|
|
|
latest=$(get_latest_version_from_github)
|
|
if [[ -z "$latest" ]]; then
|
|
latest=$(get_latest_version)
|
|
fi
|
|
|
|
if [[ -n "$latest" && "$VERSION" != "$latest" && "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -1)" == "$VERSION" ]]; then
|
|
printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" > "$msg_cache"
|
|
else
|
|
echo -n > "$msg_cache"
|
|
fi
|
|
) > /dev/null 2>&1 < /dev/null &
|
|
)
|
|
}
|
|
|
|
show_update_notification() {
|
|
local msg_cache="$HOME/.cache/mole/update_message"
|
|
if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then
|
|
cat "$msg_cache"
|
|
echo
|
|
fi
|
|
}
|
|
|
|
# UI helpers
|
|
show_brand_banner() {
|
|
cat << EOF
|
|
${GREEN} __ __ _ ${NC}
|
|
${GREEN}| \/ | ___ | | ___ ${NC}
|
|
${GREEN}| |\/| |/ _ \| |/ _ \\${NC}
|
|
${GREEN}| | | | (_) | | __/${NC} ${BLUE}https://github.com/tw93/mole${NC}
|
|
${GREEN}|_| |_|\___/|_|\___|${NC} ${GREEN}${MOLE_TAGLINE}${NC}
|
|
|
|
EOF
|
|
}
|
|
|
|
animate_mole_intro() {
|
|
if [[ ! -t 1 ]]; then
|
|
return
|
|
fi
|
|
|
|
clear_screen
|
|
printf '\n'
|
|
hide_cursor
|
|
|
|
local -a mole_lines=()
|
|
|
|
while IFS= read -r line; do
|
|
mole_lines+=("$line")
|
|
done << 'EOF'
|
|
/\_/\
|
|
____/ o o \
|
|
/~____ =o= /
|
|
(______)__m_m)
|
|
/ \
|
|
__/ /\ \__
|
|
/__/ \__\_
|
|
EOF
|
|
|
|
local idx
|
|
local body_cutoff=4
|
|
local body_color="${PURPLE}"
|
|
local ground_color="${GREEN}"
|
|
|
|
for idx in "${!mole_lines[@]}"; do
|
|
if ((idx < body_cutoff)); then
|
|
printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}"
|
|
else
|
|
printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}"
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
printf '\n'
|
|
sleep 0.5
|
|
|
|
printf '\033[2J\033[H'
|
|
show_cursor
|
|
}
|
|
|
|
show_version() {
|
|
local os_ver
|
|
if command -v sw_vers > /dev/null; then
|
|
os_ver=$(sw_vers -productVersion)
|
|
else
|
|
os_ver="Unknown"
|
|
fi
|
|
|
|
local arch
|
|
arch=$(uname -m)
|
|
|
|
local kernel
|
|
kernel=$(uname -r)
|
|
|
|
local sip_status
|
|
if command -v csrutil > /dev/null; then
|
|
sip_status=$(csrutil status 2> /dev/null | grep -o "enabled\|disabled" || echo "Unknown")
|
|
sip_status="$(LC_ALL=C tr '[:lower:]' '[:upper:]' <<< "${sip_status:0:1}")${sip_status:1}"
|
|
else
|
|
sip_status="Unknown"
|
|
fi
|
|
|
|
local disk_free
|
|
disk_free=$(df -h / 2> /dev/null | awk 'NR==2 {print $4}' || echo "Unknown")
|
|
|
|
local install_method="Manual"
|
|
if is_homebrew_install; then
|
|
install_method="Homebrew"
|
|
fi
|
|
|
|
local channel
|
|
channel=$(get_install_channel)
|
|
|
|
printf '\nMole version %s\n' "$VERSION"
|
|
if [[ "$channel" == "nightly" ]]; then
|
|
local commit
|
|
commit=$(get_install_commit)
|
|
if [[ -n "$commit" ]]; then
|
|
printf 'Channel: Nightly (%s)\n' "$commit"
|
|
else
|
|
printf 'Channel: Nightly\n'
|
|
fi
|
|
fi
|
|
printf 'macOS: %s\n' "$os_ver"
|
|
printf 'Architecture: %s\n' "$arch"
|
|
printf 'Kernel: %s\n' "$kernel"
|
|
printf 'SIP: %s\n' "$sip_status"
|
|
printf 'Disk Free: %s\n' "$disk_free"
|
|
printf 'Install: %s\n' "$install_method"
|
|
printf 'Shell: %s\n\n' "${SHELL:-Unknown}"
|
|
}
|
|
|
|
show_help() {
|
|
show_brand_banner
|
|
echo
|
|
printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo" "$NC" "Main menu"
|
|
for entry in "${MOLE_COMMANDS[@]}"; do
|
|
local name="${entry%%:*}"
|
|
local desc="${entry#*:}"
|
|
local display="mo $name"
|
|
[[ "$name" == "help" ]] && display="mo --help"
|
|
[[ "$name" == "version" ]] && display="mo --version"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "$display" "$NC" "$desc"
|
|
done
|
|
echo
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches"
|
|
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --dry-run" "$NC" "Preview optimization"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall --dry-run" "$NC" "Preview app uninstall"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --dry-run" "$NC" "Preview project purge"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo installer --dry-run" "$NC" "Preview installer cleanup"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo touchid enable --dry-run" "$NC" "Preview Touch ID setup"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo completion --dry-run" "$NC" "Preview shell completion edits"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze /Volumes" "$NC" "Analyze external drives only"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo update --force" "$NC" "Force reinstall latest stable version"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo update --nightly" "$NC" "Install latest unreleased main branch build"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "mo remove --dry-run" "$NC" "Preview Mole removal"
|
|
echo
|
|
printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC"
|
|
printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs"
|
|
echo
|
|
}
|
|
|
|
# Update flow (Homebrew or installer).
|
|
update_mole() {
|
|
local force_update="${1:-false}"
|
|
local nightly_update="${2:-false}"
|
|
local update_interrupted=false
|
|
local sudo_keepalive_pid=""
|
|
|
|
# Cleanup function for sudo keepalive
|
|
_update_cleanup() {
|
|
[[ -n "$sudo_keepalive_pid" ]] && _stop_sudo_keepalive "$sudo_keepalive_pid" || true
|
|
}
|
|
trap '_update_cleanup; update_interrupted=true; echo ""; exit 130' INT TERM
|
|
|
|
if is_homebrew_install; then
|
|
if [[ "$nightly_update" == "true" ]]; then
|
|
log_error "Nightly update is only available for script installations"
|
|
echo -e "${ICON_REVIEW} Homebrew installs follow stable releases."
|
|
echo -e "${ICON_REVIEW} Reinstall via script to use: ${GRAY}mo update --nightly${NC}"
|
|
exit 1
|
|
fi
|
|
update_via_homebrew "$VERSION"
|
|
exit 0
|
|
fi
|
|
|
|
local latest=""
|
|
local download_label="Downloading latest version..."
|
|
local install_label="Installing update..."
|
|
local final_success_label="latest version"
|
|
|
|
if [[ "$nightly_update" == "true" ]]; then
|
|
latest="main"
|
|
download_label="Downloading nightly installer..."
|
|
install_label="Installing nightly update..."
|
|
final_success_label="nightly build (main)"
|
|
else
|
|
latest=$(get_latest_version_from_github)
|
|
[[ -z "$latest" ]] && latest=$(get_latest_version)
|
|
|
|
if [[ -z "$latest" ]]; then
|
|
log_error "Unable to check for updates. Check network connection."
|
|
echo -e "${ICON_REVIEW} Check if you can access GitHub, https://github.com"
|
|
echo -e "${ICON_REVIEW} Try again with: ${GRAY}mo update${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$VERSION" == "$latest" && "$force_update" != "true" ]]; then
|
|
echo ""
|
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Already on latest version, ${VERSION}"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then
|
|
start_inline_spinner "$download_label"
|
|
else
|
|
echo "${download_label%...}"
|
|
fi
|
|
|
|
local installer_url="https://raw.githubusercontent.com/tw93/mole/main/install.sh"
|
|
local tmp_installer
|
|
tmp_installer="$(mktemp_file)" || {
|
|
log_error "Update failed"
|
|
exit 1
|
|
}
|
|
|
|
local download_error=""
|
|
if command -v curl > /dev/null 2>&1; then
|
|
download_error=$(curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer" 2>&1) || {
|
|
local curl_exit=$?
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
log_error "Update failed, curl error: $curl_exit"
|
|
|
|
case $curl_exit in
|
|
6) echo -e "${ICON_REVIEW} Could not resolve host. Check DNS or network connection." ;;
|
|
7) echo -e "${ICON_REVIEW} Failed to connect. Check network or proxy settings." ;;
|
|
22) echo -e "${ICON_REVIEW} HTTP 404 Not Found. The installer may have moved." ;;
|
|
28) echo -e "${ICON_REVIEW} Connection timed out. Try again or check firewall." ;;
|
|
*) echo -e "${ICON_REVIEW} Check network connection and try again." ;;
|
|
esac
|
|
echo -e "${ICON_REVIEW} URL: $installer_url"
|
|
exit 1
|
|
}
|
|
elif command -v wget > /dev/null 2>&1; then
|
|
download_error=$(wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url" 2>&1) || {
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
log_error "Update failed, wget error"
|
|
echo -e "${ICON_REVIEW} Check network connection and try again."
|
|
echo -e "${ICON_REVIEW} URL: $installer_url"
|
|
exit 1
|
|
}
|
|
else
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
log_error "curl or wget required"
|
|
echo -e "${ICON_REVIEW} Install curl with: ${GRAY}brew install curl${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
chmod +x "$tmp_installer"
|
|
|
|
local mole_path
|
|
mole_path="$(command -v mole 2> /dev/null || echo "$0")"
|
|
local install_dir
|
|
install_dir="$(cd "$(dirname "$mole_path")" && pwd)"
|
|
|
|
local requires_sudo="false"
|
|
if [[ ! -w "$install_dir" ]]; then
|
|
requires_sudo="true"
|
|
elif [[ -e "$install_dir/mole" && ! -w "$install_dir/mole" ]]; then
|
|
requires_sudo="true"
|
|
fi
|
|
|
|
if [[ "$requires_sudo" == "true" ]]; then
|
|
if ! request_sudo_access "Mole update requires admin access"; then
|
|
log_error "Update aborted, admin access denied"
|
|
rm -f "$tmp_installer"
|
|
exit 1
|
|
fi
|
|
# Start sudo keepalive to prevent cache expiration during install
|
|
sudo_keepalive_pid=$(_start_sudo_keepalive)
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then
|
|
start_inline_spinner "$install_label"
|
|
else
|
|
echo "${install_label%...}"
|
|
fi
|
|
|
|
process_install_output() {
|
|
local output="$1"
|
|
local fallback_version="$2"
|
|
local success_label="$3"
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
|
|
local filtered_output
|
|
filtered_output=$(printf '%s\n' "$output" | sed '/^$/d')
|
|
if [[ -n "$filtered_output" ]]; then
|
|
printf '\n%s\n' "$filtered_output"
|
|
fi
|
|
|
|
if ! printf '%s\n' "$output" | grep -Eq "Updated to latest version|Already on latest version"; then
|
|
local new_version
|
|
new_version=$(printf '%s\n' "$output" | sed -n 's/.*-> \([^[:space:]]\{1,\}\).*/\1/p' | head -1)
|
|
if [[ -z "$new_version" ]]; then
|
|
new_version=$(printf '%s\n' "$output" | sed -n 's/.*version[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*/\1/p' | head -1)
|
|
fi
|
|
if [[ -z "$new_version" ]]; then
|
|
new_version=$("$mole_path" --version 2> /dev/null | awk 'NR==1 && NF {print $NF}' || echo "")
|
|
fi
|
|
if [[ -z "$new_version" ]]; then
|
|
new_version="$fallback_version"
|
|
fi
|
|
printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to ${success_label}, ${new_version:-unknown}"
|
|
else
|
|
printf '\n'
|
|
fi
|
|
}
|
|
|
|
local install_output
|
|
local update_tag="V${latest#V}"
|
|
local config_dir="${MOLE_CONFIG_DIR:-$SCRIPT_DIR}"
|
|
if [[ ! -f "$config_dir/lib/core/common.sh" ]]; then
|
|
config_dir="$HOME/.config/mole"
|
|
fi
|
|
|
|
if [[ "$nightly_update" == "true" ]]; then
|
|
if install_output=$(MOLE_VERSION="main" "$tmp_installer" --prefix "$install_dir" --config "$config_dir" 2>&1); then
|
|
process_install_output "$install_output" "$latest" "$final_success_label"
|
|
else
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
_update_cleanup
|
|
log_error "Nightly update failed"
|
|
echo "$install_output" | tail -10 >&2 # Show last 10 lines of error
|
|
exit 1
|
|
fi
|
|
elif [[ "$force_update" == "true" ]]; then
|
|
if install_output=$(MOLE_VERSION="$update_tag" "$tmp_installer" --prefix "$install_dir" --config "$config_dir" 2>&1); then
|
|
process_install_output "$install_output" "$latest" "$final_success_label"
|
|
else
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
_update_cleanup
|
|
log_error "Update failed"
|
|
echo "$install_output" | tail -10 >&2 # Show last 10 lines of error
|
|
exit 1
|
|
fi
|
|
else
|
|
if install_output=$(MOLE_VERSION="$update_tag" "$tmp_installer" --prefix "$install_dir" --config "$config_dir" --update 2>&1); then
|
|
process_install_output "$install_output" "$latest" "$final_success_label"
|
|
else
|
|
if install_output=$(MOLE_VERSION="$update_tag" "$tmp_installer" --prefix "$install_dir" --config "$config_dir" 2>&1); then
|
|
process_install_output "$install_output" "$latest" "$final_success_label"
|
|
else
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
rm -f "$tmp_installer"
|
|
_update_cleanup
|
|
log_error "Update failed"
|
|
echo "$install_output" | tail -10 >&2 # Show last 10 lines of error
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
rm -f "$tmp_installer"
|
|
rm -f "$HOME/.cache/mole/update_message"
|
|
|
|
# Cleanup and reset trap
|
|
_update_cleanup
|
|
trap - INT TERM
|
|
}
|
|
|
|
# Remove flow (Homebrew + manual + config/cache).
|
|
remove_mole() {
|
|
local dry_run_mode="${1:-false}"
|
|
|
|
if [[ -t 1 ]]; then
|
|
start_inline_spinner "Detecting Mole installations..."
|
|
else
|
|
echo "Detecting installations..."
|
|
fi
|
|
|
|
local is_homebrew=false
|
|
local brew_cmd=""
|
|
local brew_has_mole="false"
|
|
local -a manual_installs=()
|
|
local -a alias_installs=()
|
|
|
|
if command -v brew > /dev/null 2>&1; then
|
|
brew_cmd="brew"
|
|
elif [[ -x "/opt/homebrew/bin/brew" ]]; then
|
|
brew_cmd="/opt/homebrew/bin/brew"
|
|
elif [[ -x "/usr/local/bin/brew" ]]; then
|
|
brew_cmd="/usr/local/bin/brew"
|
|
fi
|
|
|
|
if [[ -n "$brew_cmd" ]]; then
|
|
if "$brew_cmd" list --formula 2> /dev/null | grep -q "^mole$"; then
|
|
brew_has_mole="true"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$brew_has_mole" == "true" ]] || is_homebrew_install; then
|
|
is_homebrew=true
|
|
fi
|
|
|
|
local found_mole
|
|
found_mole=$(command -v mole 2> /dev/null || true)
|
|
if [[ -n "$found_mole" && -f "$found_mole" ]]; then
|
|
if [[ ! -L "$found_mole" ]] || ! readlink "$found_mole" | grep -q "Cellar/mole"; then
|
|
manual_installs+=("$found_mole")
|
|
fi
|
|
fi
|
|
|
|
local -a fallback_paths=(
|
|
"/usr/local/bin/mole"
|
|
"$HOME/.local/bin/mole"
|
|
"/opt/local/bin/mole"
|
|
)
|
|
|
|
for path in "${fallback_paths[@]}"; do
|
|
if [[ -f "$path" && "$path" != "$found_mole" ]]; then
|
|
if [[ ! -L "$path" ]] || ! readlink "$path" | grep -q "Cellar/mole"; then
|
|
manual_installs+=("$path")
|
|
fi
|
|
fi
|
|
done
|
|
|
|
local found_mo
|
|
found_mo=$(command -v mo 2> /dev/null || true)
|
|
if [[ -n "$found_mo" && -f "$found_mo" ]]; then
|
|
alias_installs+=("$found_mo")
|
|
fi
|
|
|
|
local -a alias_fallback=(
|
|
"/usr/local/bin/mo"
|
|
"$HOME/.local/bin/mo"
|
|
"/opt/local/bin/mo"
|
|
)
|
|
|
|
for alias in "${alias_fallback[@]}"; do
|
|
if [[ -f "$alias" && "$alias" != "$found_mo" ]]; then
|
|
alias_installs+=("$alias")
|
|
fi
|
|
done
|
|
|
|
if [[ -t 1 ]]; then
|
|
stop_inline_spinner
|
|
fi
|
|
|
|
printf '\n'
|
|
|
|
local manual_count=${#manual_installs[@]}
|
|
local alias_count=${#alias_installs[@]}
|
|
if [[ "$is_homebrew" == "false" && ${manual_count:-0} -eq 0 && ${alias_count:-0} -eq 0 ]]; then
|
|
printf '%s\n\n' "${YELLOW}No Mole installation detected${NC}"
|
|
exit 0
|
|
fi
|
|
|
|
# Dry-run mode: show preview and exit without confirmation
|
|
if [[ "$dry_run_mode" == "true" ]]; then
|
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, no files will be removed"
|
|
echo ""
|
|
echo -e "${YELLOW}Remove Mole${NC}, would delete the following:"
|
|
if [[ "$is_homebrew" == "true" ]]; then
|
|
echo -e " ${GRAY}${ICON_LIST} Would run: brew uninstall --force mole${NC}"
|
|
fi
|
|
if [[ ${manual_count:-0} -gt 0 ]]; then
|
|
for install in "${manual_installs[@]}"; do
|
|
[[ -f "$install" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: ${install}${NC}"
|
|
done
|
|
fi
|
|
if [[ ${alias_count:-0} -gt 0 ]]; then
|
|
for alias in "${alias_installs[@]}"; do
|
|
[[ -f "$alias" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: ${alias}${NC}"
|
|
done
|
|
fi
|
|
[[ -d "$HOME/.cache/mole" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: $HOME/.cache/mole${NC}"
|
|
[[ -d "$HOME/.config/mole" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: $HOME/.config/mole${NC}"
|
|
|
|
printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Dry run complete, no changes made"
|
|
exit 0
|
|
fi
|
|
|
|
echo -e "${YELLOW}Remove Mole${NC}, will delete the following:"
|
|
if [[ "$is_homebrew" == "true" ]]; then
|
|
echo " ${ICON_LIST} Mole via Homebrew"
|
|
fi
|
|
for install in ${manual_installs[@]+"${manual_installs[@]}"} ${alias_installs[@]+"${alias_installs[@]}"}; do
|
|
echo " ${ICON_LIST} $install"
|
|
done
|
|
echo " ${ICON_LIST} ~/.config/mole"
|
|
echo " ${ICON_LIST} ~/.cache/mole"
|
|
echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to confirm, ${GRAY}ESC${NC} to cancel: "
|
|
|
|
IFS= read -r -s -n1 key || key=""
|
|
drain_pending_input # Clean up any escape sequence remnants
|
|
case "$key" in
|
|
$'\e')
|
|
exit 0
|
|
;;
|
|
"" | $'\n' | $'\r')
|
|
printf "\r\033[K" # Clear the prompt line
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
local has_error=false
|
|
if [[ "$is_homebrew" == "true" ]]; then
|
|
if [[ -z "$brew_cmd" ]]; then
|
|
log_error "Homebrew command not found. Please ensure Homebrew is installed and in your PATH."
|
|
log_warning "Manual step: brew uninstall --force mole"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Attempting to uninstall Mole via Homebrew..."
|
|
local brew_uninstall_output
|
|
if ! brew_uninstall_output=$("$brew_cmd" uninstall --force mole 2>&1); then
|
|
has_error=true
|
|
log_error "Homebrew uninstallation failed:"
|
|
printf "%s\n" "$brew_uninstall_output" | sed "s/^/${RED} | ${NC}/" >&2
|
|
log_warning "Manual step: ${YELLOW}brew uninstall --force mole${NC}"
|
|
echo "" # Add a blank line for readability
|
|
else
|
|
log_success "Mole uninstalled via Homebrew."
|
|
fi
|
|
fi
|
|
if [[ ${manual_count:-0} -gt 0 ]]; then
|
|
for install in "${manual_installs[@]}"; do
|
|
if [[ -f "$install" ]]; then
|
|
if [[ ! -w "$(dirname "$install")" ]]; then
|
|
if ! sudo rm -f "$install" 2> /dev/null; then
|
|
has_error=true
|
|
fi
|
|
else
|
|
if ! rm -f "$install" 2> /dev/null; then
|
|
has_error=true
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
if [[ ${alias_count:-0} -gt 0 ]]; then
|
|
for alias in "${alias_installs[@]}"; do
|
|
if [[ -f "$alias" ]]; then
|
|
if [[ ! -w "$(dirname "$alias")" ]]; then
|
|
if ! sudo rm -f "$alias" 2> /dev/null; then
|
|
has_error=true
|
|
fi
|
|
else
|
|
if ! rm -f "$alias" 2> /dev/null; then
|
|
has_error=true
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
if [[ -d "$HOME/.cache/mole" ]]; then
|
|
rm -rf "$HOME/.cache/mole" 2> /dev/null || true
|
|
fi
|
|
if [[ -d "$HOME/.config/mole" ]]; then
|
|
rm -rf "$HOME/.config/mole" 2> /dev/null || true
|
|
fi
|
|
|
|
local final_message
|
|
if [[ "$has_error" == "true" ]]; then
|
|
final_message="${YELLOW}${ICON_ERROR} Mole uninstalled with some errors, thank you for using Mole!${NC}"
|
|
else
|
|
final_message="${GREEN}${ICON_SUCCESS} Mole uninstalled successfully, thank you for using Mole!${NC}"
|
|
fi
|
|
printf '\n%s\n\n' "$final_message"
|
|
|
|
exit 0
|
|
}
|
|
|
|
# Menu UI
|
|
show_main_menu() {
|
|
local selected="${1:-1}"
|
|
local _full_draw="${2:-true}" # Kept for compatibility (unused)
|
|
local banner="${MAIN_MENU_BANNER:-}"
|
|
local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}"
|
|
|
|
if [[ -z "$banner" ]]; then
|
|
banner="$(show_brand_banner)"
|
|
MAIN_MENU_BANNER="$banner"
|
|
fi
|
|
|
|
printf '\033[H'
|
|
|
|
local line=""
|
|
printf '\r\033[2K\n'
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
printf '\r\033[2K%s\n' "$line"
|
|
done <<< "$banner"
|
|
|
|
if [[ -n "$update_message" ]]; then
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
printf '\r\033[2K%s\n' "$line"
|
|
done <<< "$update_message"
|
|
fi
|
|
|
|
printf '\r\033[2K\n'
|
|
|
|
printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Free up disk space" "$([[ $selected -eq 1 ]] && echo true || echo false)")"
|
|
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Remove apps completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
|
|
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Check and maintain system" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
|
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Explore disk usage" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
|
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Status Monitor system health" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
|
|
|
|
if [[ -t 0 ]]; then
|
|
printf '\r\033[2K\n'
|
|
local controls="${GRAY}↑↓ | Enter | M More | "
|
|
if ! is_touchid_configured; then
|
|
controls="${controls}T TouchID"
|
|
else
|
|
controls="${controls}U Update"
|
|
fi
|
|
controls="${controls} | Q Quit${NC}"
|
|
printf '\r\033[2K%s\n' "$controls"
|
|
printf '\r\033[2K\n'
|
|
fi
|
|
|
|
printf '\033[J'
|
|
}
|
|
|
|
interactive_main_menu() {
|
|
if [[ -t 1 ]]; then
|
|
local tty_name
|
|
tty_name=$(tty 2> /dev/null || echo "")
|
|
if [[ -n "$tty_name" ]]; then
|
|
local flag_file
|
|
local cache_dir="$HOME/.cache/mole"
|
|
ensure_user_dir "$cache_dir"
|
|
flag_file="$cache_dir/intro_$(echo "$tty_name" | LC_ALL=C tr -c '[:alnum:]_' '_')"
|
|
if [[ ! -f "$flag_file" ]]; then
|
|
animate_mole_intro
|
|
ensure_user_file "$flag_file"
|
|
fi
|
|
fi
|
|
fi
|
|
local current_option=1
|
|
local first_draw=true
|
|
local brand_banner=""
|
|
local msg_cache="$HOME/.cache/mole/update_message"
|
|
local update_message=""
|
|
|
|
brand_banner="$(show_brand_banner)"
|
|
MAIN_MENU_BANNER="$brand_banner"
|
|
|
|
if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then
|
|
update_message="$(cat "$msg_cache" 2> /dev/null || echo "")"
|
|
fi
|
|
MAIN_MENU_UPDATE_MESSAGE="$update_message"
|
|
|
|
cleanup_and_exit() {
|
|
show_cursor
|
|
exit 0
|
|
}
|
|
|
|
trap cleanup_and_exit INT
|
|
hide_cursor
|
|
|
|
while true; do
|
|
show_main_menu $current_option "$first_draw"
|
|
if [[ "$first_draw" == "true" ]]; then
|
|
first_draw=false
|
|
fi
|
|
|
|
local key
|
|
if ! key=$(read_key); then
|
|
continue
|
|
fi
|
|
|
|
case "$key" in
|
|
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
|
"DOWN") ((current_option < 5)) && ((current_option++)) ;;
|
|
"ENTER")
|
|
show_cursor
|
|
case $current_option in
|
|
1) exec "$SCRIPT_DIR/bin/clean.sh" ;;
|
|
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
|
3) exec "$SCRIPT_DIR/bin/optimize.sh" ;;
|
|
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
|
5) exec "$SCRIPT_DIR/bin/status.sh" ;;
|
|
esac
|
|
;;
|
|
"CHAR:1")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/clean.sh"
|
|
;;
|
|
"CHAR:2")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/uninstall.sh"
|
|
;;
|
|
"CHAR:3")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/optimize.sh"
|
|
;;
|
|
"CHAR:4")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/analyze.sh"
|
|
;;
|
|
"CHAR:5")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/status.sh"
|
|
;;
|
|
"MORE")
|
|
show_cursor
|
|
clear
|
|
show_help
|
|
exit 0
|
|
;;
|
|
"VERSION")
|
|
show_cursor
|
|
clear
|
|
show_version
|
|
exit 0
|
|
;;
|
|
"TOUCHID")
|
|
show_cursor
|
|
exec "$SCRIPT_DIR/bin/touchid.sh"
|
|
;;
|
|
"UPDATE")
|
|
show_cursor
|
|
clear
|
|
update_mole
|
|
exit 0
|
|
;;
|
|
"QUIT") cleanup_and_exit ;;
|
|
esac
|
|
|
|
drain_pending_input
|
|
done
|
|
}
|
|
|
|
# CLI dispatch
|
|
main() {
|
|
local -a args=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--debug)
|
|
export MO_DEBUG=1
|
|
;;
|
|
*)
|
|
args+=("$arg")
|
|
;;
|
|
esac
|
|
done
|
|
|
|
case "${args[0]:-""}" in
|
|
"optimize")
|
|
exec "$SCRIPT_DIR/bin/optimize.sh" "${args[@]:1}"
|
|
;;
|
|
"clean")
|
|
exec "$SCRIPT_DIR/bin/clean.sh" "${args[@]:1}"
|
|
;;
|
|
"uninstall")
|
|
exec "$SCRIPT_DIR/bin/uninstall.sh" "${args[@]:1}"
|
|
;;
|
|
"analyze")
|
|
exec "$SCRIPT_DIR/bin/analyze.sh" "${args[@]:1}"
|
|
;;
|
|
"status")
|
|
exec "$SCRIPT_DIR/bin/status.sh" "${args[@]:1}"
|
|
;;
|
|
"purge")
|
|
exec "$SCRIPT_DIR/bin/purge.sh" "${args[@]:1}"
|
|
;;
|
|
"installer")
|
|
exec "$SCRIPT_DIR/bin/installer.sh" "${args[@]:1}"
|
|
;;
|
|
"touchid")
|
|
exec "$SCRIPT_DIR/bin/touchid.sh" "${args[@]:1}"
|
|
;;
|
|
"completion")
|
|
exec "$SCRIPT_DIR/bin/completion.sh" "${args[@]:1}"
|
|
;;
|
|
"update")
|
|
local force_update=false
|
|
local nightly_update=false
|
|
for arg in "${args[@]:1}"; do
|
|
case "$arg" in
|
|
--force | -f) force_update=true ;;
|
|
--nightly) nightly_update=true ;;
|
|
*)
|
|
echo "Unknown update option: $arg"
|
|
echo "Use 'mole update [--force] [--nightly]' for supported options."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
update_mole "$force_update" "$nightly_update"
|
|
exit 0
|
|
;;
|
|
"remove")
|
|
local dry_run_remove=false
|
|
for arg in "${args[@]:1}"; do
|
|
case "$arg" in
|
|
"--dry-run" | "-n") dry_run_remove=true ;;
|
|
*)
|
|
echo "Unknown remove option: $arg"
|
|
echo "Use 'mole remove [--dry-run]' for supported options."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
remove_mole "$dry_run_remove"
|
|
;;
|
|
"help" | "--help" | "-h")
|
|
show_help
|
|
exit 0
|
|
;;
|
|
"version" | "--version" | "-V")
|
|
show_version
|
|
exit 0
|
|
;;
|
|
"")
|
|
check_for_updates
|
|
interactive_main_menu
|
|
;;
|
|
*)
|
|
echo "Unknown command: ${args[0]}"
|
|
echo "Use 'mole --help' for usage information."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|