mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 15:00:07 +00:00
Display commit hash in parentheses after "Nightly" channel label to help users identify exact version when testing nightly builds.
881 lines
26 KiB
Bash
Executable File
881 lines
26 KiB
Bash
Executable File
#!/bin/bash
|
|
# Mole - Installer for manual installs.
|
|
# Fetches source/binaries and installs to prefix.
|
|
# Supports update and edge installs.
|
|
|
|
set -euo pipefail
|
|
|
|
GREEN='\033[0;32m'
|
|
BLUE='\033[0;34m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m'
|
|
|
|
_SPINNER_PID=""
|
|
start_line_spinner() {
|
|
local msg="$1"
|
|
[[ ! -t 1 ]] && {
|
|
echo -e "${BLUE}|${NC} $msg"
|
|
return
|
|
}
|
|
local 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; }
|
|
|
|
VERBOSE=1
|
|
|
|
# Icons duplicated from lib/core/common.sh (install.sh runs standalone).
|
|
# Avoid readonly to prevent conflicts when sourcing common.sh later.
|
|
ICON_SUCCESS="✓"
|
|
ICON_ADMIN="●"
|
|
ICON_CONFIRM="◎"
|
|
ICON_ERROR="☻"
|
|
|
|
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 "${YELLOW}${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"; }
|
|
|
|
safe_rm() {
|
|
local target="${1:-}"
|
|
local tmp_root
|
|
|
|
if [[ -z "$target" ]]; then
|
|
log_error "safe_rm: empty path"
|
|
return 1
|
|
fi
|
|
if [[ ! -e "$target" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
tmp_root="${TMPDIR:-/tmp}"
|
|
case "$target" in
|
|
"$tmp_root" | /tmp)
|
|
log_error "safe_rm: refusing to remove temp root: $target"
|
|
return 1
|
|
;;
|
|
"$tmp_root"/* | /tmp/*) ;;
|
|
*)
|
|
log_error "safe_rm: refusing to remove non-temp path: $target"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
if [[ -d "$target" ]]; then
|
|
find "$target" -depth \( -type f -o -type l \) -exec rm -f {} + 2> /dev/null || true
|
|
find "$target" -depth -type d -exec rmdir {} + 2> /dev/null || true
|
|
else
|
|
rm -f "$target" 2> /dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Install defaults
|
|
INSTALL_DIR="/usr/local/bin"
|
|
CONFIG_DIR="$HOME/.config/mole"
|
|
SOURCE_DIR=""
|
|
|
|
ACTION="install"
|
|
|
|
# Resolve source dir (local checkout, env override, or download).
|
|
needs_sudo() {
|
|
if [[ -e "$INSTALL_DIR" ]]; then
|
|
[[ ! -w "$INSTALL_DIR" ]]
|
|
return
|
|
fi
|
|
|
|
local parent_dir
|
|
parent_dir="$(dirname "$INSTALL_DIR")"
|
|
[[ ! -w "$parent_dir" ]]
|
|
}
|
|
|
|
maybe_sudo() {
|
|
if needs_sudo; then
|
|
sudo "$@"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
resolve_source_dir() {
|
|
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" && -f "$SOURCE_DIR/mole" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
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
|
|
|
|
if [[ -n "${CLEAN_SOURCE_DIR:-}" && -d "$CLEAN_SOURCE_DIR" && -f "$CLEAN_SOURCE_DIR/mole" ]]; then
|
|
SOURCE_DIR="$CLEAN_SOURCE_DIR"
|
|
return 0
|
|
fi
|
|
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
|
|
# Safe cleanup function for temporary directory
|
|
cleanup_tmp() {
|
|
stop_line_spinner 2> /dev/null || true
|
|
if [[ -z "${tmp:-}" ]]; then
|
|
return 0
|
|
fi
|
|
safe_rm "$tmp"
|
|
}
|
|
trap cleanup_tmp EXIT
|
|
|
|
local branch="${MOLE_VERSION:-}"
|
|
if [[ -z "$branch" ]]; then
|
|
branch="$(get_latest_release_tag || true)"
|
|
fi
|
|
if [[ -z "$branch" ]]; then
|
|
branch="$(get_latest_release_tag_from_git || true)"
|
|
fi
|
|
if [[ -z "$branch" ]]; then
|
|
branch="main"
|
|
fi
|
|
if [[ "$branch" != "main" && "$branch" != "dev" ]]; then
|
|
branch="$(normalize_release_tag "$branch")"
|
|
fi
|
|
local url="https://github.com/tw93/mole/archive/refs/heads/main.tar.gz"
|
|
|
|
if [[ "$branch" == "dev" ]]; then
|
|
url="https://github.com/tw93/mole/archive/refs/heads/dev.tar.gz"
|
|
elif [[ "$branch" != "main" ]]; then
|
|
url="https://github.com/tw93/mole/archive/refs/tags/${branch}.tar.gz"
|
|
fi
|
|
|
|
start_line_spinner "Fetching Mole source, ${branch}..."
|
|
if command -v curl > /dev/null 2>&1; then
|
|
if curl -fsSL --connect-timeout 10 --max-time 60 -o "$tmp/mole.tar.gz" "$url" 2> /dev/null; then
|
|
if tar -xzf "$tmp/mole.tar.gz" -C "$tmp" 2> /dev/null; then
|
|
stop_line_spinner
|
|
|
|
local extracted_dir
|
|
extracted_dir=$(find "$tmp" -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
|
|
|
if [[ -n "$extracted_dir" && -f "$extracted_dir/mole" ]]; then
|
|
SOURCE_DIR="$extracted_dir"
|
|
return 0
|
|
fi
|
|
fi
|
|
else
|
|
stop_line_spinner
|
|
# Only exit early for version tags (not for main/dev branches)
|
|
if [[ "$branch" != "main" && "$branch" != "dev" ]]; then
|
|
log_error "Failed to fetch version ${branch}. Check if tag exists."
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
stop_line_spinner
|
|
|
|
start_line_spinner "Cloning Mole source..."
|
|
if command -v git > /dev/null 2>&1; then
|
|
local git_args=("--depth=1")
|
|
if [[ "$branch" != "main" ]]; then
|
|
git_args+=("--branch" "$branch")
|
|
fi
|
|
|
|
if git clone "${git_args[@]}" 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
|
|
}
|
|
|
|
# Version helpers
|
|
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_source_commit_hash() {
|
|
# Try to get from local git repo first
|
|
if [[ -d "$SOURCE_DIR/.git" ]]; then
|
|
git -C "$SOURCE_DIR" rev-parse --short HEAD 2> /dev/null && return
|
|
fi
|
|
# Fallback to GitHub API
|
|
curl -fsSL --connect-timeout 3 \
|
|
"https://api.github.com/repos/tw93/mole/commits/main" 2> /dev/null |
|
|
sed -n 's/.*"sha"[[:space:]]*:[[:space:]]*"\([a-f0-9]\{7\}\).*/\1/p' | head -1
|
|
}
|
|
|
|
get_latest_release_tag() {
|
|
local tag
|
|
if ! command -v curl > /dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
tag=$(curl -fsSL --connect-timeout 2 --max-time 3 \
|
|
"https://api.github.com/repos/tw93/mole/releases/latest" 2> /dev/null |
|
|
sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
|
if [[ -z "$tag" ]]; then
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$tag"
|
|
}
|
|
|
|
get_latest_release_tag_from_git() {
|
|
if ! command -v git > /dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
git ls-remote --tags --refs https://github.com/tw93/mole.git 2> /dev/null |
|
|
awk -F/ '{print $NF}' |
|
|
grep -E '^V[0-9]' |
|
|
sort -V |
|
|
tail -n 1
|
|
}
|
|
|
|
normalize_release_tag() {
|
|
local tag="$1"
|
|
while [[ "$tag" =~ ^[vV] ]]; do
|
|
tag="${tag#v}"
|
|
tag="${tag#V}"
|
|
done
|
|
if [[ -n "$tag" ]]; then
|
|
printf 'V%s\n' "$tag"
|
|
fi
|
|
}
|
|
|
|
get_installed_version() {
|
|
local binary="$INSTALL_DIR/mole"
|
|
if [[ -x "$binary" ]]; then
|
|
local version
|
|
version=$("$binary" --version 2> /dev/null | awk '/Mole version/ {print $NF; exit}')
|
|
if [[ -n "$version" ]]; then
|
|
echo "$version"
|
|
else
|
|
sed -n 's/^VERSION="\(.*\)"$/\1/p' "$binary" | head -n1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
resolve_install_channel() {
|
|
case "${MOLE_VERSION:-}" in
|
|
main | latest)
|
|
printf 'nightly\n'
|
|
return 0
|
|
;;
|
|
dev)
|
|
printf 'dev\n'
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
if [[ "${MOLE_EDGE_INSTALL:-}" == "true" ]]; then
|
|
printf 'nightly\n'
|
|
return 0
|
|
fi
|
|
|
|
printf 'stable\n'
|
|
}
|
|
|
|
write_install_channel_metadata() {
|
|
local channel="$1"
|
|
local commit_hash="${2:-}"
|
|
local metadata_file="$CONFIG_DIR/install_channel"
|
|
|
|
local tmp_file
|
|
tmp_file=$(mktemp "${CONFIG_DIR}/install_channel.XXXXXX") || return 1
|
|
{
|
|
printf 'CHANNEL=%s\n' "$channel"
|
|
[[ -n "$commit_hash" ]] && printf 'COMMIT_HASH=%s\n' "$commit_hash"
|
|
} > "$tmp_file" || {
|
|
rm -f "$tmp_file" 2> /dev/null || true
|
|
return 1
|
|
}
|
|
|
|
mv -f "$tmp_file" "$metadata_file" || {
|
|
rm -f "$tmp_file" 2> /dev/null || true
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# CLI parsing (supports main/latest and version tokens).
|
|
parse_args() {
|
|
local -a args=("$@")
|
|
local version_token=""
|
|
local i skip_next=false
|
|
for i in "${!args[@]}"; do
|
|
local token="${args[$i]}"
|
|
[[ -z "$token" ]] && continue
|
|
# Skip values for options that take arguments
|
|
if [[ "$skip_next" == "true" ]]; then
|
|
skip_next=false
|
|
continue
|
|
fi
|
|
if [[ "$token" == "--prefix" || "$token" == "--config" ]]; then
|
|
skip_next=true
|
|
continue
|
|
fi
|
|
if [[ "$token" == -* ]]; then
|
|
continue
|
|
fi
|
|
if [[ -n "$version_token" ]]; then
|
|
log_error "Unexpected argument: $token"
|
|
exit 1
|
|
fi
|
|
case "$token" in
|
|
latest | main)
|
|
export MOLE_VERSION="main"
|
|
export MOLE_EDGE_INSTALL="true"
|
|
version_token="$token"
|
|
unset 'args[$i]'
|
|
;;
|
|
dev)
|
|
export MOLE_VERSION="dev"
|
|
export MOLE_EDGE_INSTALL="true"
|
|
version_token="$token"
|
|
unset 'args[$i]'
|
|
;;
|
|
[0-9]* | V[0-9]* | v[0-9]*)
|
|
export MOLE_VERSION="$token"
|
|
version_token="$token"
|
|
unset 'args[$i]'
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $token"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
if [[ ${#args[@]} -gt 0 ]]; then
|
|
set -- ${args[@]+"${args[@]}"}
|
|
else
|
|
set --
|
|
fi
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--prefix)
|
|
if [[ -z "${2:-}" ]]; then
|
|
log_error "Missing value for --prefix"
|
|
exit 1
|
|
fi
|
|
INSTALL_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--config)
|
|
if [[ -z "${2:-}" ]]; then
|
|
log_error "Missing value for --config"
|
|
exit 1
|
|
fi
|
|
CONFIG_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--update)
|
|
ACTION="update"
|
|
shift 1
|
|
;;
|
|
--verbose | -v)
|
|
VERBOSE=1
|
|
shift 1
|
|
;;
|
|
--help | -h)
|
|
log_error "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Environment checks and directory setup
|
|
check_requirements() {
|
|
if [[ "$OSTYPE" != "darwin"* ]]; then
|
|
log_error "This tool is designed for macOS only"
|
|
exit 1
|
|
fi
|
|
|
|
if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then
|
|
local mole_path
|
|
mole_path=$(command -v mole 2> /dev/null || true)
|
|
local is_homebrew_binary=false
|
|
|
|
if [[ -n "$mole_path" && -L "$mole_path" ]]; then
|
|
if readlink "$mole_path" | grep -q "Cellar/mole"; then
|
|
is_homebrew_binary=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$is_homebrew_binary" == "true" ]]; 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 --force mole${NC} then re-run this"
|
|
echo ""
|
|
exit 1
|
|
else
|
|
log_warning "Cleaning up stale Homebrew installation..."
|
|
brew uninstall --force mole > /dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -d "$(dirname "$INSTALL_DIR")" ]]; then
|
|
log_error "Parent directory $(dirname "$INSTALL_DIR") does not exist"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_directories() {
|
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
maybe_sudo mkdir -p "$INSTALL_DIR"
|
|
fi
|
|
|
|
if ! mkdir -p "$CONFIG_DIR" "$CONFIG_DIR/bin" "$CONFIG_DIR/lib"; then
|
|
log_error "Failed to create config directory: $CONFIG_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
}
|
|
|
|
# Binary install helpers
|
|
build_binary_from_source() {
|
|
local binary_name="$1"
|
|
local target_path="$2"
|
|
local cmd_dir=""
|
|
|
|
case "$binary_name" in
|
|
analyze)
|
|
cmd_dir="cmd/analyze"
|
|
;;
|
|
status)
|
|
cmd_dir="cmd/status"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
if ! command -v go > /dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -d "$SOURCE_DIR/$cmd_dir" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then
|
|
start_line_spinner "Building ${binary_name} from source..."
|
|
else
|
|
echo "Building ${binary_name} from source..."
|
|
fi
|
|
|
|
if (cd "$SOURCE_DIR" && go build -ldflags="-s -w" -o "$target_path" "./$cmd_dir" > /dev/null 2>&1); then
|
|
if [[ -t 1 ]]; then stop_line_spinner; fi
|
|
chmod +x "$target_path"
|
|
log_success "Built ${binary_name} from source"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then stop_line_spinner; fi
|
|
log_warning "Failed to build ${binary_name} from source"
|
|
return 1
|
|
}
|
|
|
|
download_binary() {
|
|
local binary_name="$1"
|
|
local target_path="$CONFIG_DIR/bin/${binary_name}-go"
|
|
local arch
|
|
arch=$(uname -m)
|
|
local arch_suffix="amd64"
|
|
if [[ "$arch" == "arm64" ]]; then
|
|
arch_suffix="arm64"
|
|
fi
|
|
|
|
if [[ -f "$SOURCE_DIR/bin/${binary_name}-go" ]]; then
|
|
cp "$SOURCE_DIR/bin/${binary_name}-go" "$target_path"
|
|
chmod +x "$target_path"
|
|
log_success "Installed local ${binary_name} binary"
|
|
return 0
|
|
elif [[ -f "$SOURCE_DIR/bin/${binary_name}-darwin-${arch_suffix}" ]]; then
|
|
cp "$SOURCE_DIR/bin/${binary_name}-darwin-${arch_suffix}" "$target_path"
|
|
chmod +x "$target_path"
|
|
log_success "Installed local ${binary_name} binary"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "${MOLE_EDGE_INSTALL:-}" == "true" ]]; then
|
|
if build_binary_from_source "$binary_name" "$target_path"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local version
|
|
version=$(get_source_version)
|
|
if [[ -z "$version" ]]; then
|
|
log_warning "Could not determine version for ${binary_name}, trying local build"
|
|
if build_binary_from_source "$binary_name" "$target_path"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
local url="https://github.com/tw93/mole/releases/download/V${version}/${binary_name}-darwin-${arch_suffix}"
|
|
|
|
# Skip preflight network checks to avoid false negatives.
|
|
|
|
if [[ -t 1 ]]; then
|
|
start_line_spinner "Downloading ${binary_name}..."
|
|
else
|
|
echo "Downloading ${binary_name}..."
|
|
fi
|
|
|
|
if curl -fsSL --connect-timeout 10 --max-time 60 -o "$target_path" "$url"; then
|
|
if [[ -t 1 ]]; then stop_line_spinner; fi
|
|
chmod +x "$target_path"
|
|
xattr -c "$target_path" 2> /dev/null || true
|
|
log_success "Downloaded ${binary_name} binary"
|
|
else
|
|
if [[ -t 1 ]]; then stop_line_spinner; fi
|
|
log_warning "Could not download ${binary_name} binary, v${version}, trying local build"
|
|
if build_binary_from_source "$binary_name" "$target_path"; then
|
|
return 0
|
|
fi
|
|
log_error "Failed to install ${binary_name} binary"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# File installation (bin/lib/scripts + go helpers).
|
|
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)"
|
|
|
|
if [[ -f "$SOURCE_DIR/mole" ]]; then
|
|
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
|
|
if needs_sudo; then
|
|
log_admin "Admin access required for /usr/local/bin"
|
|
fi
|
|
|
|
# Atomic update: copy to temporary name first, then move
|
|
maybe_sudo cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole.new"
|
|
maybe_sudo chmod +x "$INSTALL_DIR/mole.new"
|
|
maybe_sudo mv -f "$INSTALL_DIR/mole.new" "$INSTALL_DIR/mole"
|
|
|
|
log_success "Installed mole to $INSTALL_DIR"
|
|
fi
|
|
else
|
|
log_error "mole executable not found in ${SOURCE_DIR:-unknown}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -f "$SOURCE_DIR/mo" ]]; then
|
|
if [[ "$source_dir_abs" == "$install_dir_abs" ]]; then
|
|
log_success "mo alias already present"
|
|
else
|
|
maybe_sudo cp "$SOURCE_DIR/mo" "$INSTALL_DIR/mo.new"
|
|
maybe_sudo chmod +x "$INSTALL_DIR/mo.new"
|
|
maybe_sudo mv -f "$INSTALL_DIR/mo.new" "$INSTALL_DIR/mo"
|
|
log_success "Installed mo alias"
|
|
fi
|
|
fi
|
|
|
|
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
|
|
local -a bin_files=("$SOURCE_DIR/bin"/*)
|
|
if [[ ${#bin_files[@]} -gt 0 ]]; then
|
|
cp -r "${bin_files[@]}" "$CONFIG_DIR/bin/"
|
|
for file in "$CONFIG_DIR/bin/"*; do
|
|
[[ -e "$file" ]] && chmod +x "$file"
|
|
done
|
|
log_success "Installed modules"
|
|
fi
|
|
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
|
|
local -a lib_files=("$SOURCE_DIR/lib"/*)
|
|
if [[ ${#lib_files[@]} -gt 0 ]]; then
|
|
cp -r "${lib_files[@]}" "$CONFIG_DIR/lib/"
|
|
log_success "Installed libraries"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
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
|
|
|
|
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
|
|
maybe_sudo sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole"
|
|
fi
|
|
|
|
if ! download_binary "analyze"; then
|
|
exit 1
|
|
fi
|
|
if ! download_binary "status"; then
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Verification and PATH hint
|
|
verify_installation() {
|
|
|
|
if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/core/common.sh" ]]; then
|
|
|
|
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
|
|
}
|
|
|
|
setup_path() {
|
|
if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
|
|
return
|
|
fi
|
|
|
|
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 ""
|
|
}
|
|
|
|
# Main install/update flows
|
|
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
|
|
|
|
local install_channel commit_hash=""
|
|
install_channel="$(resolve_install_channel)"
|
|
if [[ "$install_channel" == "nightly" ]]; then
|
|
commit_hash=$(get_source_commit_hash)
|
|
fi
|
|
if ! write_install_channel_metadata "$install_channel" "$commit_hash"; then
|
|
log_warning "Could not write install channel metadata"
|
|
fi
|
|
|
|
# Edge installs get a suffix to make the version explicit.
|
|
if [[ "${MOLE_EDGE_INSTALL:-}" == "true" ]]; then
|
|
installed_version="${installed_version}-edge"
|
|
echo ""
|
|
local branch_name="${MOLE_VERSION:-main}"
|
|
log_warning "Edge version installed on ${branch_name} branch"
|
|
log_info "This is a testing version; use 'mo update' to switch to stable"
|
|
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
|
|
resolve_source_dir 2> /dev/null || true
|
|
local current_version
|
|
current_version=$(get_installed_version || echo "unknown")
|
|
if [[ -f "$SOURCE_DIR/lib/core/common.sh" ]]; then
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$SOURCE_DIR/lib/core/common.sh"
|
|
update_via_homebrew "$current_version"
|
|
else
|
|
log_error "Cannot update Homebrew-managed Mole without full installation"
|
|
echo ""
|
|
echo "Please update via Homebrew:"
|
|
echo -e " ${GREEN}brew upgrade mole${NC}"
|
|
exit 1
|
|
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}${ICON_SUCCESS}${NC} Already on latest version, $installed_version"
|
|
exit 0
|
|
fi
|
|
|
|
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
|
|
|
|
local install_channel commit_hash=""
|
|
install_channel="$(resolve_install_channel)"
|
|
if [[ "$install_channel" == "nightly" ]]; then
|
|
commit_hash=$(get_source_commit_hash)
|
|
fi
|
|
if ! write_install_channel_metadata "$install_channel" "$commit_hash"; then
|
|
log_warning "Could not write install channel metadata"
|
|
fi
|
|
|
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version, $updated_version"
|
|
}
|
|
|
|
parse_args "$@"
|
|
|
|
case "$ACTION" in
|
|
update)
|
|
perform_update
|
|
;;
|
|
*)
|
|
perform_install
|
|
;;
|
|
esac
|