#!/usr/bin/env bash set -Eeuo pipefail # Release endpoints are centralized here and resolve through ShellMate's latest-release API. FRONTEND_URL="${SHELLMATE_FRONTEND_URL:-https://shellmate.net}" BASE_URL="${SHELLMATE_DOWNLOAD_BASE_URL:-$FRONTEND_URL/api/download/latest}" BIN_DIR="${SHELLMATE_BIN_DIR:-$HOME/.local/bin}" DATA_DIR="${SHELLMATE_INSTALL_DIR:-$HOME/.local/share/shellmate}" TEMP_DIR="" MOUNT_DIR="" MOUNTED=0 info() { printf '\033[1;35mShellMate:\033[0m %s\n' "$*"; } fail() { printf '\033[1;31mShellMate error:\033[0m %s\n' "$*" >&2; exit 1; } cleanup() { if [[ "$MOUNTED" == "1" ]] && [[ -n "$MOUNT_DIR" ]]; then hdiutil detach "$MOUNT_DIR" -quiet || true fi [[ -z "$TEMP_DIR" ]] || rm -rf "$TEMP_DIR" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM command -v curl >/dev/null 2>&1 || fail "curl is required to download ShellMate." OS="$(uname -s)" MACHINE="$(uname -m)" # A shell launched through Rosetta reports x86_64 even on Apple Silicon. if [[ "$OS" == "Darwin" ]] && [[ "$MACHINE" == "x86_64" ]]; then if [[ "$(sysctl -in sysctl.proc_translated 2>/dev/null || true)" == "1" ]] \ || [[ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" == "1" ]]; then MACHINE="arm64" fi fi case "$MACHINE" in x86_64|amd64) ARCH="x64" ;; arm64|aarch64) ARCH="arm64" ;; *) fail "Unsupported CPU architecture: $MACHINE" ;; esac TEMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t shellmate)" mkdir -p "$BIN_DIR" download() { local url="$1" destination="$2" info "Download URL: $url" info "Downloading the latest ShellMate release..." curl -fL --retry 3 --retry-delay 2 --connect-timeout 15 "$url" -o "$destination" \ || fail "Download failed ($url). Check your connection or confirm that a release exists for $OS $ARCH." [[ -s "$destination" ]] || fail "The downloaded release from $url is empty. Installation stopped." } install_macos() { local platform="mac-intel" [[ "$ARCH" == "arm64" ]] && platform="mac-arm" local dmg="$TEMP_DIR/ShellMate.dmg" local staged_app="$TEMP_DIR/ShellMate.app" local app_dir="${SHELLMATE_APP_DIR:-$HOME/Applications}" local target="$app_dir/ShellMate.app" MOUNT_DIR="$TEMP_DIR/mount" command -v hdiutil >/dev/null 2>&1 || fail "hdiutil is required on macOS." command -v ditto >/dev/null 2>&1 || fail "ditto is required on macOS." download "$BASE_URL?platform=$platform" "$dmg" info "Verifying the disk image..." hdiutil verify "$dmg" -quiet || fail "The downloaded disk image failed verification." mkdir -p "$MOUNT_DIR" "$app_dir" info "Opening the disk image..." hdiutil attach "$dmg" -mountpoint "$MOUNT_DIR" -nobrowse -quiet \ || fail "The downloaded disk image could not be opened." MOUNTED=1 local source_app source_app="$(find "$MOUNT_DIR" -maxdepth 2 -type d -name 'ShellMate.app' -print -quit)" [[ -n "$source_app" ]] || fail "ShellMate.app was not found in the downloaded disk image." ditto "$source_app" "$staged_app" [[ -x "$staged_app/Contents/MacOS/ShellMate" ]] || fail "The ShellMate application binary is missing." if command -v lipo >/dev/null 2>&1; then local app_arches app_arches="$(lipo -archs "$staged_app/Contents/MacOS/ShellMate" 2>/dev/null || true)" if [[ "$ARCH" == "arm64" ]] && [[ " $app_arches " != *" arm64 "* ]]; then fail "The downloaded application does not contain the required arm64 binary." fi if [[ "$ARCH" == "x64" ]] && [[ " $app_arches " != *" x86_64 "* ]]; then fail "The downloaded application does not contain the required x86_64 binary." fi fi info "$([[ -d "$target" ]] && printf 'Updating' || printf 'Installing') ShellMate in $app_dir..." if [[ ! -d "$target" ]] && [[ -d "$target.old" ]]; then mv "$target.old" "$target" fi rm -rf "$target.new" mv "$staged_app" "$target.new" if [[ -d "$target" ]]; then rm -rf "$target.old" mv "$target" "$target.old" fi if ! mv "$target.new" "$target"; then [[ ! -d "$target.old" ]] || mv "$target.old" "$target" fail "Could not replace the existing ShellMate application." fi rm -rf "$target.old" rm -f "$BIN_DIR/shellmate" cat > "$BIN_DIR/shellmate" < "$BIN_DIR/shellmate" </dev/null 2>&1; then update-desktop-database "$applications_dir" >/dev/null 2>&1 || true fi } register_deb_desktop_entry() { local applications_dir="$HOME/.local/share/applications" local packaged_desktop desktop_name desktop_target packaged_desktop="$(dpkg-query -L shell-mate 2>/dev/null \ | awk '/\/share\/applications\/[^/]+\.desktop$/ { print; exit }')" [[ -n "$packaged_desktop" && -f "$packaged_desktop" ]] \ || fail "The package installed, but its application-menu entry was not found." mkdir -p "$applications_dir" desktop_name="$(basename -- "$packaged_desktop")" desktop_target="$applications_dir/$desktop_name" # Remove launcher IDs used by older installers. The per-user entry must use # the package's exact filename so GNOME treats it as an override, not a second app. rm -f \ "$applications_dir/shellmate.desktop" \ "$applications_dir/ShellMate.desktop" \ "$applications_dir/shell-mate.desktop" install -m 0644 "$packaged_desktop" "$desktop_target" refresh_linux_desktop_menu info "Registered ShellMate in the Ubuntu applications menu." } install_linux_deb() { local platform="linux-deb" [[ "$ARCH" == "arm64" ]] && platform="linux-deb-arm64" local package="$TEMP_DIR/ShellMate.deb" download "$BASE_URL?platform=$platform" "$package" command -v dpkg-deb >/dev/null 2>&1 || fail "dpkg-deb is required to validate the Debian package." [[ "$(dpkg-deb -f "$package" Package 2>/dev/null || true)" == "shell-mate" ]] \ || fail "The downloaded file is not the expected shell-mate Debian package." local install_command=(apt-get install -y "$package") if [[ "$EUID" != "0" ]]; then command -v sudo >/dev/null 2>&1 || fail "sudo is required to install the DEB package system-wide." info "Debian/Ubuntu requires administrator access to install ShellMate and its system dependencies." install_command=(sudo apt-get install -y "$package") fi info "Installing or updating the native shell-mate Debian package..." "${install_command[@]}" || fail "The Debian package could not be installed." [[ -x /usr/bin/ShellMate ]] || fail "The package installed, but /usr/bin/ShellMate was not found." # Remove an older curl-installed AppImage before registering the DEB launcher. rm -f "$DATA_DIR/ShellMate.AppImage" rmdir "$DATA_DIR" 2>/dev/null || true write_linux_launcher /usr/bin/ShellMate register_deb_desktop_entry info "To uninstall the Debian package later, run: sudo apt remove shell-mate" } install_linux_appimage() { local platform="linux-appimage" [[ "$ARCH" == "arm64" ]] && platform="linux-appimage-arm64" local image="$TEMP_DIR/ShellMate.AppImage" local target="$DATA_DIR/ShellMate.AppImage" download "$BASE_URL?platform=$platform" "$image" chmod 755 "$image" mkdir -p "$DATA_DIR" "$HOME/.local/share/applications" info "$([[ -f "$target" ]] && printf 'Updating' || printf 'Installing') ShellMate in $DATA_DIR..." mv "$image" "$target.new" mv -f "$target.new" "$target" rm -f "$BIN_DIR/shellmate" cat > "$BIN_DIR/shellmate" < "$HOME/.local/share/applications/shellmate.desktop" </dev/null 2>&1; then install_linux_deb else install_linux_appimage fi } case "$OS" in Darwin) install_macos ;; Linux) install_linux ;; *) fail "Unsupported operating system: $OS. Use install.ps1 on Windows." ;; esac case ":$PATH:" in *":$BIN_DIR:"*) ;; *) PROFILE="$HOME/.profile" [[ "$OS" == "Darwin" ]] && PROFILE="$HOME/.zprofile" PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' touch "$PROFILE" grep -Fqx "$PATH_LINE" "$PROFILE" || printf '\n# ShellMate launcher\n%s\n' "$PATH_LINE" >> "$PROFILE" export PATH="$BIN_DIR:$PATH" info "Added $BIN_DIR to PATH in $PROFILE." ;; esac info "ShellMate is ready. Run 'shellmate' to launch it." info "Run this installer again anytime to update. Your settings and user data are untouched."