1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 17:59:44 +00:00
Files
Mole/install.sh
Tw93 3e50a103f6 chore: clean bot commits and improve documentation
- Merged all bot commits (github-actions, Bot) into Tw93 account
     - Fixed M/U/T key bindings in main menu
     - Updated help menu and README command order
     - Improved install script output
     - Updated GitHub Actions to use Tw93 identity
2025-12-10 14:12:43 +08:00

641 lines
20 KiB
Bash
Executable File

#!/bin/bash
# Mole Installation Script
set -euo pipefail
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Simple spinner
_SPINNER_PID=""
start_line_spinner() {
local msg="$1"
[[ ! -t 1 ]] && {
echo -e "${BLUE}|${NC} $msg"
return
}
local chars="${MO_SPINNER_CHARS:-|/-\\}"
[[ -z "$chars" ]] && chars='|/-\\'
local i=0
(while true; do
c="${chars:$((i % ${#chars})):1}"
printf "\r${BLUE}%s${NC} %s" "$c" "$msg"
((i++))
sleep 0.12
done) &
_SPINNER_PID=$!
}
stop_line_spinner() { if [[ -n "$_SPINNER_PID" ]]; then
kill "$_SPINNER_PID" 2> /dev/null || true
wait "$_SPINNER_PID" 2> /dev/null || true
_SPINNER_PID=""
printf "\r\033[K"
fi; }
# Verbosity (0 = quiet, 1 = verbose)
VERBOSE=1
# Icons (duplicated from lib/core/common.sh - necessary as install.sh runs standalone)
readonly ICON_SUCCESS="✓"
readonly ICON_ADMIN="●"
readonly ICON_CONFIRM="◎"
readonly ICON_ERROR="☻"
# Logging functions
log_info() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${BLUE}$1${NC}"; }
log_success() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${GREEN}${ICON_SUCCESS}${NC} $1"; }
log_warning() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${YELLOW}$1${NC}"; }
log_error() { echo -e "${RED}${ICON_ERROR}${NC} $1"; }
log_admin() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${BLUE}${ICON_ADMIN}${NC} $1"; }
log_confirm() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${BLUE}${ICON_CONFIRM}${NC} $1"; }
# Default installation directory
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="$HOME/.config/mole"
SOURCE_DIR=""
# Default action (install|update)
ACTION="install"
show_help() {
cat << 'EOF'
Mole Installation Script
========================
USAGE:
./install.sh [OPTIONS]
OPTIONS:
--prefix PATH Install to custom directory (default: /usr/local/bin)
--config PATH Config directory (default: ~/.config/mole)
--update Update Mole to the latest version
--uninstall Uninstall mole
--help, -h Show this help
EXAMPLES:
./install.sh # Install to /usr/local/bin
./install.sh --prefix ~/.local/bin # Install to custom directory
./install.sh --update # Update Mole in place
./install.sh --uninstall # Uninstall mole
The installer will:
1. Copy mole binary and scripts to the install directory
2. Set up config directory with all modules
3. Make the mole command available system-wide
EOF
echo ""
}
# Resolve the directory containing source files (supports curl | bash)
resolve_source_dir() {
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" && -f "$SOURCE_DIR/mole" ]]; then
return 0
fi
# 1) If script is on disk, use its directory (only when mole executable present)
if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$script_dir/mole" ]]; then
SOURCE_DIR="$script_dir"
return 0
fi
fi
# 2) If CLEAN_SOURCE_DIR env is provided, honor it
if [[ -n "${CLEAN_SOURCE_DIR:-}" && -d "$CLEAN_SOURCE_DIR" && -f "$CLEAN_SOURCE_DIR/mole" ]]; then
SOURCE_DIR="$CLEAN_SOURCE_DIR"
return 0
fi
# 3) Fallback: fetch repository to a temp directory (works for curl | bash)
local tmp
tmp="$(mktemp -d)"
# Expand tmp now so trap doesn't depend on local scope
trap "rm -rf '$tmp'" EXIT
start_line_spinner "Fetching Mole source..."
if command -v curl > /dev/null 2>&1; then
if curl -fsSL -o "$tmp/mole.tar.gz" "https://github.com/tw93/mole/archive/refs/heads/main.tar.gz"; then
stop_line_spinner
tar -xzf "$tmp/mole.tar.gz" -C "$tmp"
# Extracted folder name: mole-main
if [[ -d "$tmp/mole-main" ]]; then
SOURCE_DIR="$tmp/mole-main"
return 0
fi
fi
fi
stop_line_spinner
start_line_spinner "Cloning Mole source..."
if command -v git > /dev/null 2>&1; then
if git clone --depth=1 https://github.com/tw93/mole.git "$tmp/mole" > /dev/null 2>&1; then
stop_line_spinner
SOURCE_DIR="$tmp/mole"
return 0
fi
fi
stop_line_spinner
log_error "Failed to fetch source files. Ensure curl or git is available."
exit 1
}
get_source_version() {
local source_mole="$SOURCE_DIR/mole"
if [[ -f "$source_mole" ]]; then
sed -n 's/^VERSION="\(.*\)"$/\1/p' "$source_mole" | head -n1
fi
}
get_installed_version() {
local binary="$INSTALL_DIR/mole"
if [[ -x "$binary" ]]; then
# Try running the binary first (preferred method)
local version
version=$("$binary" --version 2> /dev/null | awk 'NF {print $NF; exit}')
if [[ -n "$version" ]]; then
echo "$version"
else
# Fallback: parse VERSION from file (in case binary is broken)
sed -n 's/^VERSION="\(.*\)"$/\1/p' "$binary" | head -n1
fi
fi
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--prefix)
INSTALL_DIR="$2"
shift 2
;;
--config)
CONFIG_DIR="$2"
shift 2
;;
--update)
ACTION="update"
shift 1
;;
--uninstall)
uninstall_mole
exit 0
;;
--verbose | -v)
VERBOSE=1
shift 1
;;
--help | -h)
show_help
exit 0
;;
*)
log_error "Unknown option: $1"
show_help
exit 1
;;
esac
done
}
# Check system requirements
check_requirements() {
# Check if running on macOS
if [[ "$OSTYPE" != "darwin"* ]]; then
log_error "This tool is designed for macOS only"
exit 1
fi
# Check if already installed via Homebrew
if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then
if [[ "$ACTION" == "update" ]]; then
return 0
fi
echo -e "${YELLOW}Mole is installed via Homebrew${NC}"
echo ""
echo "Choose one:"
echo -e " 1. Update via Homebrew: ${GREEN}brew upgrade mole${NC}"
echo -e " 2. Switch to manual: ${GREEN}brew uninstall mole${NC} then re-run this"
echo ""
exit 1
fi
# Check if install directory exists and is writable
if [[ ! -d "$(dirname "$INSTALL_DIR")" ]]; then
log_error "Parent directory $(dirname "$INSTALL_DIR") does not exist"
exit 1
fi
}
# Create installation directories
create_directories() {
# Create install directory if it doesn't exist
if [[ ! -d "$INSTALL_DIR" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$(dirname "$INSTALL_DIR")" ]]; then
sudo mkdir -p "$INSTALL_DIR"
else
mkdir -p "$INSTALL_DIR"
fi
fi
# Create config directory
mkdir -p "$CONFIG_DIR"
mkdir -p "$CONFIG_DIR/bin"
mkdir -p "$CONFIG_DIR/lib"
}
# Install files
install_files() {
resolve_source_dir
local source_dir_abs
local install_dir_abs
local config_dir_abs
source_dir_abs="$(cd "$SOURCE_DIR" && pwd)"
install_dir_abs="$(cd "$INSTALL_DIR" && pwd)"
config_dir_abs="$(cd "$CONFIG_DIR" && pwd)"
# Copy main executable when destination differs
if [[ -f "$SOURCE_DIR/mole" ]]; then
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
log_admin "Admin access required for /usr/local/bin"
sudo cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
sudo chmod +x "$INSTALL_DIR/mole"
else
cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
chmod +x "$INSTALL_DIR/mole"
fi
log_success "Installed mole to $INSTALL_DIR"
fi
else
log_error "mole executable not found in ${SOURCE_DIR:-unknown}"
exit 1
fi
# Install mo alias for Mole if available
if [[ -f "$SOURCE_DIR/mo" ]]; then
if [[ "$source_dir_abs" == "$install_dir_abs" ]]; then
log_success "mo alias already present"
else
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
sudo cp "$SOURCE_DIR/mo" "$INSTALL_DIR/mo"
sudo chmod +x "$INSTALL_DIR/mo"
else
cp "$SOURCE_DIR/mo" "$INSTALL_DIR/mo"
chmod +x "$INSTALL_DIR/mo"
fi
log_success "Installed mo alias"
fi
fi
# Copy configuration and modules
if [[ -d "$SOURCE_DIR/bin" ]]; then
local source_bin_abs="$(cd "$SOURCE_DIR/bin" && pwd)"
local config_bin_abs="$(cd "$CONFIG_DIR/bin" && pwd)"
if [[ "$source_bin_abs" == "$config_bin_abs" ]]; then
log_success "Modules already synced"
else
cp -r "$SOURCE_DIR/bin"/* "$CONFIG_DIR/bin/"
chmod +x "$CONFIG_DIR/bin"/*
log_success "Installed modules"
fi
fi
if [[ -d "$SOURCE_DIR/lib" ]]; then
local source_lib_abs="$(cd "$SOURCE_DIR/lib" && pwd)"
local config_lib_abs="$(cd "$CONFIG_DIR/lib" && pwd)"
if [[ "$source_lib_abs" == "$config_lib_abs" ]]; then
log_success "Libraries already synced"
else
cp -r "$SOURCE_DIR/lib"/* "$CONFIG_DIR/lib/"
log_success "Installed libraries"
fi
fi
# Copy other files if they exist and directories differ
if [[ "$config_dir_abs" != "$source_dir_abs" ]]; then
for file in README.md LICENSE install.sh; do
if [[ -f "$SOURCE_DIR/$file" ]]; then
cp -f "$SOURCE_DIR/$file" "$CONFIG_DIR/"
fi
done
fi
if [[ -f "$CONFIG_DIR/install.sh" ]]; then
chmod +x "$CONFIG_DIR/install.sh"
fi
# Update the mole script to use the config directory when installed elsewhere
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
sudo sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole"
else
sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole"
fi
fi
}
# Verify installation
verify_installation() {
if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/core/common.sh" ]]; then
# Test if mole command works
if "$INSTALL_DIR/mole" --help > /dev/null 2>&1; then
return 0
else
log_warning "Mole command installed but may not be working properly"
fi
else
log_error "Installation verification failed"
exit 1
fi
}
# Add to PATH if needed
setup_path() {
# Check if install directory is in PATH
if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
return
fi
# Only suggest PATH setup for custom directories
if [[ "$INSTALL_DIR" != "/usr/local/bin" ]]; then
log_warning "$INSTALL_DIR is not in your PATH"
echo ""
echo "To use mole from anywhere, add this line to your shell profile:"
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
echo ""
echo "For example, add it to ~/.zshrc or ~/.bash_profile"
fi
}
print_usage_summary() {
local action="$1"
local new_version="$2"
local previous_version="${3:-}"
if [[ ${VERBOSE} -ne 1 ]]; then
return
fi
echo ""
local message="Mole ${action} successfully"
if [[ "$action" == "updated" && -n "$previous_version" && -n "$new_version" && "$previous_version" != "$new_version" ]]; then
message+=" (${previous_version} -> ${new_version})"
elif [[ -n "$new_version" ]]; then
message+=" (version ${new_version})"
fi
log_confirm "$message"
echo ""
echo "Usage:"
if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
echo " mo # Interactive menu"
echo " mo clean # Deep cleanup"
echo " mo uninstall # Remove apps + leftovers"
echo " mo optimize # Check and maintain system"
echo " mo analyze # Explore disk usage"
echo " mo status # Monitor system health"
echo " mo touchid # Configure Touch ID for sudo"
echo " mo update # Update to latest version"
echo " mo --help # Show all commands"
else
echo " $INSTALL_DIR/mo # Interactive menu"
echo " $INSTALL_DIR/mo clean # Deep cleanup"
echo " $INSTALL_DIR/mo uninstall # Remove apps + leftovers"
echo " $INSTALL_DIR/mo optimize # Check and maintain system"
echo " $INSTALL_DIR/mo analyze # Explore disk usage"
echo " $INSTALL_DIR/mo status # Monitor system health"
echo " $INSTALL_DIR/mo touchid # Configure Touch ID for sudo"
echo " $INSTALL_DIR/mo update # Update to latest version"
echo " $INSTALL_DIR/mo --help # Show all commands"
fi
echo ""
}
# Uninstall function
uninstall_mole() {
log_confirm "Uninstalling Mole"
echo ""
# Remove executable
if [[ -f "$INSTALL_DIR/mole" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
log_admin "Admin access required"
sudo rm -f "$INSTALL_DIR/mole"
else
rm -f "$INSTALL_DIR/mole"
fi
log_success "Removed mole executable"
fi
if [[ -f "$INSTALL_DIR/mo" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
sudo rm -f "$INSTALL_DIR/mo"
else
rm -f "$INSTALL_DIR/mo"
fi
log_success "Removed mo alias"
fi
# SAFETY CHECK: Verify config directory is safe to remove
# Only allow removal of mole-specific directories
local is_safe=0
# Additional safety: never delete system critical paths (check first)
case "$CONFIG_DIR" in
/ | /usr | /usr/local | /usr/local/bin | /usr/local/lib | /usr/local/share | \
/Library | /System | /bin | /sbin | /etc | /var | /opt | "$HOME" | "$HOME/Library" | \
/usr/local/lib/* | /usr/local/share/* | /Library/* | /System/*)
is_safe=0
;;
*)
# Safe patterns: must be in user's home and end with 'mole'
if [[ "$CONFIG_DIR" == "$HOME/.config/mole" ]] ||
[[ "$CONFIG_DIR" == "$HOME"/.*/mole ]]; then
is_safe=1
fi
;;
esac
# Ask before removing config directory
if [[ -d "$CONFIG_DIR" ]]; then
if [[ $is_safe -eq 0 ]]; then
log_warning "Config directory $CONFIG_DIR is not safe to auto-remove"
log_warning "Skipping automatic removal for safety"
echo ""
echo "Please manually review and remove mole-specific files from:"
echo " $CONFIG_DIR"
else
echo ""
read -p "Remove configuration directory $CONFIG_DIR? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$CONFIG_DIR"
log_success "Removed configuration"
else
log_success "Configuration preserved"
fi
fi
fi
echo ""
log_confirm "Mole uninstalled successfully"
}
# Main installation function
perform_install() {
resolve_source_dir
local source_version
source_version="$(get_source_version || true)"
check_requirements
create_directories
install_files
verify_installation
setup_path
local installed_version
installed_version="$(get_installed_version || true)"
if [[ -z "$installed_version" ]]; then
installed_version="$source_version"
fi
print_usage_summary "installed" "$installed_version"
}
perform_update() {
check_requirements
if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then
# Try to use shared function if available (when running from installed Mole)
resolve_source_dir 2> /dev/null || true
if [[ -f "$SOURCE_DIR/lib/core/common.sh" ]]; then
# shellcheck disable=SC1090,SC1091
source "$SOURCE_DIR/lib/core/common.sh"
update_via_homebrew "$VERSION"
else
# Fallback: inline implementation
if [[ -t 1 ]]; then
start_line_spinner "Updating Homebrew..."
else
echo "Updating Homebrew..."
fi
brew update 2>&1 | grep -Ev "^(==>|Already up-to-date)" || true
if [[ -t 1 ]]; then
stop_line_spinner
fi
if [[ -t 1 ]]; then
start_line_spinner "Upgrading Mole..."
else
echo "Upgrading Mole..."
fi
local upgrade_output
upgrade_output=$(brew upgrade mole 2>&1) || true
if [[ -t 1 ]]; then
stop_line_spinner
fi
if echo "$upgrade_output" | grep -q "already installed"; then
local current_version
current_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}')
echo -e "${GREEN}${NC} Already on latest version (${current_version:-$VERSION})"
elif echo "$upgrade_output" | grep -q "Error:"; then
log_error "Homebrew upgrade failed"
echo "$upgrade_output" | grep "Error:" >&2
exit 1
else
echo "$upgrade_output" | grep -Ev "^(==>|Updating Homebrew|Warning:)" || true
local new_version
new_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}')
echo -e "${GREEN}${NC} Updated to latest version (${new_version:-$VERSION})"
fi
rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message"
fi
exit 0
fi
local installed_version
installed_version="$(get_installed_version || true)"
if [[ -z "$installed_version" ]]; then
log_warning "Mole is not currently installed in $INSTALL_DIR. Running fresh installation."
perform_install
return
fi
resolve_source_dir
local target_version
target_version="$(get_source_version || true)"
if [[ -z "$target_version" ]]; then
log_error "Unable to determine the latest Mole version."
exit 1
fi
if [[ "$installed_version" == "$target_version" ]]; then
echo -e "${GREEN}${NC} Already on latest version ($installed_version)"
exit 0
fi
# Update with minimal output (suppress info/success, show errors only)
local old_verbose=$VERBOSE
VERBOSE=0
create_directories || {
VERBOSE=$old_verbose
log_error "Failed to create directories"
exit 1
}
install_files || {
VERBOSE=$old_verbose
log_error "Failed to install files"
exit 1
}
verify_installation || {
VERBOSE=$old_verbose
log_error "Failed to verify installation"
exit 1
}
setup_path
VERBOSE=$old_verbose
local updated_version
updated_version="$(get_installed_version || true)"
if [[ -z "$updated_version" ]]; then
updated_version="$target_version"
fi
echo -e "${GREEN}${NC} Updated to latest version ($updated_version)"
}
# Run requested action
parse_args "$@"
case "$ACTION" in
update)
perform_update
;;
*)
perform_install
;;
esac