#!/bin/bash # appicons installer # Usage: curl -fsSL https://raw.githubusercontent.com/guillempuche/appicons/main/scripts/install.sh | bash set -euo pipefail REPO="guillempuche/appicons" INSTALL_DIR="$HOME/.appicons" MIN_BUN_VERSION="1.0.0" # Colors for output (disabled if not a terminal) if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi info() { echo -e "${GREEN}==>${NC} $1"; } warn() { echo -e "${YELLOW}Warning:${NC} $1"; } error() { echo -e "${RED}Error:${NC} $1" >&2; exit 1; } # Check for required commands check_requirements() { local missing=() command -v curl &>/dev/null || missing+=("curl") command -v tar &>/dev/null || missing+=("tar") if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required commands: ${missing[*]}" fi } # Detect OS and architecture detect_platform() { local os arch case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" # Check for musl libc (Alpine Linux) - not supported due to native modules if ldd --version 2>&1 | grep -qi musl; then error "Alpine Linux (musl libc) is not supported due to native module dependencies" fi ;; MINGW*|MSYS*|CYGWIN*) error "Windows detected. Run in PowerShell: irm https://raw.githubusercontent.com/guillempuche/appicons/main/scripts/install.ps1 | iex" ;; *) error "Unsupported operating system: $(uname -s)" ;; esac case "$(uname -m)" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; *) error "Unsupported architecture: $(uname -m)" ;; esac # Check if this platform has a release local platform="${os}-${arch}" if [[ "$platform" == "linux-arm64" ]]; then warn "Linux ARM64 support is experimental" fi echo "$platform" } # Compare semver versions (returns 0 if $1 >= $2) version_gte() { local v1="$1" v2="$2" # Use sort -V if available, otherwise simple string compare if printf '%s\n%s' "$v2" "$v1" | sort -V 2>/dev/null | head -n1 | grep -qx "$v2"; then return 0 fi # Fallback: assume version is sufficient if sort -V fails return 0 } # Check if Bun is installed with minimum version ensure_bun() { if command -v bun &>/dev/null; then local version version=$(bun --version 2>/dev/null || echo "0.0.0") if version_gte "$version" "$MIN_BUN_VERSION"; then info "Bun is already installed: $version" return 0 else warn "Bun version $version is below minimum $MIN_BUN_VERSION, upgrading..." fi fi info "Installing Bun runtime..." if ! curl -fsSL https://bun.sh/install | bash; then error "Failed to install Bun. Please install manually: https://bun.sh" fi # Source the updated PATH export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" if ! command -v bun &>/dev/null; then error "Failed to install Bun. Please install manually: https://bun.sh" fi info "Bun installed successfully: $(bun --version)" } # Get latest release version from GitHub get_latest_version() { local response version http_code # Use a temp file to capture both body and http code local tmp_file tmp_file=$(mktemp) trap "rm -f '$tmp_file'" RETURN http_code=$(curl -fsSL -w "%{http_code}" -o "$tmp_file" \ "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null) || true if [[ "$http_code" == "403" ]]; then error "GitHub API rate limit exceeded. Try again later or set GITHUB_TOKEN environment variable" elif [[ "$http_code" != "200" ]]; then error "Failed to fetch latest version from GitHub (HTTP $http_code)" fi version=$(grep '"tag_name"' "$tmp_file" | sed -E 's/.*"([^"]+)".*/\1/' | head -1) if [[ -z "$version" ]]; then error "Failed to parse version from GitHub response" fi echo "$version" } # Download and extract release archive download_release() { local platform="$1" local version="$2" local archive_name="appicons-${platform}.tar.gz" local download_url="https://github.com/${REPO}/releases/download/${version}/${archive_name}" info "Downloading appicons ${version} for ${platform}..." # Create temp directory local tmp_dir tmp_dir=$(mktemp -d) trap "rm -rf '$tmp_dir'" EXIT # Download archive if ! curl -fsSL "$download_url" -o "$tmp_dir/$archive_name"; then error "Failed to download release. Check if ${platform} is supported: $download_url" fi # Verify archive is valid if ! tar -tzf "$tmp_dir/$archive_name" &>/dev/null; then error "Downloaded archive is corrupted" fi # Preserve user data (history) across updates local history_backup="" if [[ -f "$INSTALL_DIR/history.json" ]]; then history_backup=$(mktemp) cp "$INSTALL_DIR/history.json" "$history_backup" info "Backed up history.json" elif [[ -f "$HOME/.appicons_history_backup.json" ]]; then # Restore from uninstall backup if available history_backup="$HOME/.appicons_history_backup.json" info "Found history backup from previous uninstall" fi # Remove existing installation if [[ -d "$INSTALL_DIR" ]]; then info "Removing existing installation..." rm -rf "$INSTALL_DIR" fi # Create install directory and extract mkdir -p "$INSTALL_DIR" tar -xzf "$tmp_dir/$archive_name" -C "$INSTALL_DIR" # Restore user data if [[ -n "$history_backup" ]] && [[ -f "$history_backup" ]]; then cp "$history_backup" "$INSTALL_DIR/history.json" # Clean up temp backup (but not if it's the uninstall backup path itself, # which we handle separately) if [[ "$history_backup" == "$HOME/.appicons_history_backup.json" ]]; then rm -f "$HOME/.appicons_history_backup.json" else rm -f "$history_backup" fi info "Restored history.json" fi # Verify extraction if [[ ! -f "$INSTALL_DIR/appicons.js" ]]; then error "Installation failed: appicons.js not found after extraction" fi info "Extracted to $INSTALL_DIR" } # Create wrapper script that runs with Bun create_wrapper() { local wrapper_path="$INSTALL_DIR/appicons" cat > "$wrapper_path" << 'WRAPPER' #!/bin/bash # appicons wrapper - runs the JS bundle with Bun set -euo pipefail # Find Bun (prefer system bun, then user installation) find_bun() { if command -v bun &>/dev/null; then command -v bun elif [[ -x "$HOME/.bun/bin/bun" ]]; then echo "$HOME/.bun/bin/bun" else echo "Error: Bun is not installed. Please run: curl -fsSL https://bun.sh/install | bash" >&2 exit 1 fi } BUN_PATH="$(find_bun)" # Get the directory where this script is located (resolving symlinks) # This handles: direct execution, symlinks, nested symlinks, relative symlinks resolve_script_dir() { local source="${BASH_SOURCE[0]}" local dir # Resolve symlinks while [[ -L "$source" ]]; do dir="$(cd -P "$(dirname "$source")" && pwd)" source="$(readlink "$source")" # Handle relative symlinks [[ "$source" != /* ]] && source="$dir/$source" done cd -P "$(dirname "$source")" && pwd } SCRIPT_DIR="$(resolve_script_dir)" # Verify the JS file exists if [[ ! -f "$SCRIPT_DIR/appicons.js" ]]; then echo "Error: appicons.js not found in $SCRIPT_DIR" >&2 echo "Please reinstall: curl -fsSL https://raw.githubusercontent.com/guillempuche/appicons/main/scripts/install.sh | bash" >&2 exit 1 fi # Run the CLI with Bun exec "$BUN_PATH" "$SCRIPT_DIR/appicons.js" "$@" WRAPPER chmod +x "$wrapper_path" info "Created wrapper script" } # Determine best bin directory get_bin_dir() { # Prefer XDG_BIN_HOME if set if [[ -n "${XDG_BIN_HOME:-}" ]] && [[ -d "$XDG_BIN_HOME" ]]; then echo "$XDG_BIN_HOME" return fi # Prefer ~/.local/bin (create if needed, avoids sudo) if [[ -d "$HOME/.local/bin" ]] || mkdir -p "$HOME/.local/bin" 2>/dev/null; then echo "$HOME/.local/bin" return fi # Fall back to /usr/local/bin (requires sudo) echo "/usr/local/bin" } # Create symlink in bin directory create_symlink() { local wrapper_path="$INSTALL_DIR/appicons" local bin_dir bin_dir=$(get_bin_dir) local symlink_path="$bin_dir/appicons" # Create bin directory if needed if [[ ! -d "$bin_dir" ]]; then if [[ "$bin_dir" == "$HOME"* ]]; then mkdir -p "$bin_dir" elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then # sudo available without password warn "$bin_dir does not exist, creating it..." sudo mkdir -p "$bin_dir" else error "$bin_dir does not exist and cannot create without sudo. Please run: sudo mkdir -p $bin_dir" fi fi # Remove existing symlink/file if [[ -L "$symlink_path" ]] || [[ -f "$symlink_path" ]]; then info "Removing existing symlink..." if [[ -w "$bin_dir" ]]; then rm -f "$symlink_path" elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then sudo rm -f "$symlink_path" else error "Cannot remove $symlink_path without sudo. Please run: sudo rm -f $symlink_path" fi fi # Create new symlink if [[ -w "$bin_dir" ]]; then ln -s "$wrapper_path" "$symlink_path" elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then info "Creating symlink (using sudo)..." sudo ln -s "$wrapper_path" "$symlink_path" else error "Cannot create symlink in $bin_dir without sudo. Please run: sudo ln -s $wrapper_path $symlink_path" fi info "Created symlink: $symlink_path -> $wrapper_path" # Check if bin_dir is in PATH if [[ ":$PATH:" != *":$bin_dir:"* ]]; then warn "$bin_dir is not in your PATH" echo "" echo " Add this to your ~/.bashrc or ~/.profile:" echo "" echo " export PATH=\"$bin_dir:\$PATH\"" echo "" echo " Then run: source ~/.bashrc" fi } # Verify installation verify_installation() { # Refresh PATH to include new symlink location local bin_dir bin_dir=$(get_bin_dir) export PATH="$bin_dir:$PATH" if ! command -v appicons &>/dev/null; then return 1 fi info "Verifying installation..." if appicons --version 2>/dev/null; then return 0 else return 1 fi } main() { echo "" echo " appicons installer" echo " Generate app icons, splash screens, and adaptive icons" echo "" check_requirements local platform version platform=$(detect_platform) info "Detected platform: $platform" ensure_bun version=$(get_latest_version) info "Latest version: $version" download_release "$platform" "$version" create_wrapper create_symlink echo "" if verify_installation; then echo "" info "Installation complete!" echo "" echo " Run 'appicons' to launch the interactive TUI" echo " Run 'appicons --help' for CLI options" echo "" else echo "" warn "Installation complete, but verification failed." echo " Try restarting your terminal or running:" echo " source ~/.bashrc # or ~/.zshrc" echo "" fi } main "$@"