#!/usr/bin/env bash set -euo pipefail INSTALL_API="https://install.ace.githubnext.com" platform=$(uname -ms) # Windows detection - use MINGW64 bash if [[ ${OS:-} = Windows_NT ]]; then if [[ $platform != MINGW64* ]]; then # TODO: powershell installer echo "Please use Git Bash (MINGW64) to run this installer on Windows" >&2 exit 1 fi fi # Colors (only if terminal supports it) Color_Off='' Red='' Green='' Dim='' if [[ -t 1 ]]; then Color_Off='\033[0m' Red='\033[0;31m' Green='\033[38;2;125;166;139m' Dim='\033[0;2m' fi error() { echo -e " ${Red}error${Color_Off}:" "$@" >&2 exit 1 } info() { echo -e " ${Dim}$@${Color_Off}" } # Check for required tools command -v curl >/dev/null || error "'curl' is required but not installed" command -v unzip >/dev/null || error "'unzip' is required but not installed" # Parse arguments usage() { cat < Install a specific version (e.g., 1.0.0) --no-modify-path Do not modify shell config files Examples: curl -fsSL $INSTALL_API | bash curl -fsSL $INSTALL_API | bash -s -- --version 1.0.0 EOF } requested_version="" no_modify_path=false while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; -v|--version) [[ -n "${2:-}" ]] || error "--version requires a version argument" requested_version="$2" shift 2 ;; --no-modify-path) no_modify_path=true shift ;; *) error "Unknown option: $1" ;; esac done # Detect platform case "$platform" in 'Darwin x86_64') target=darwin-x64 ;; 'Darwin arm64') target=darwin-arm64 ;; 'Linux aarch64' | 'Linux arm64') target=linux-arm64 ;; 'Linux x86_64') target=linux-x64 ;; MINGW64*) target=windows-x64 ;; *) target=linux-x64 ;; esac # Check for musl on Linux case "$target" in 'linux'*) if [[ -f /etc/alpine-release ]]; then target="$target-musl" elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then target="$target-musl" fi ;; esac # Rosetta detection on macOS if [[ $target = darwin-x64 ]]; then if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then target=darwin-arm64 info "Running in Rosetta 2, downloading arm64 binary instead" fi fi # Set up install directory install_dir="${ACE_INSTALL:-$HOME/.ace}" bin_dir="$install_dir/bin" # Display path: use ~ for $HOME, otherwise show full path if [[ $install_dir = $HOME/* ]]; then display_dir="~${install_dir#$HOME}" else display_dir="$install_dir" fi display_bin="$display_dir/bin" if [[ $target = windows-x64 ]]; then exe="$bin_dir/ace.exe" display_exe="$display_bin/ace.exe" else exe="$bin_dir/ace" display_exe="$display_bin/ace" fi mkdir -p "$bin_dir" || error "Failed to create install directory: $bin_dir" if [[ -z "$requested_version" ]]; then # Get latest version (follows 307 redirect to versioned endpoint) release_info=$(curl -sfL "$INSTALL_API/tui/latest") || error "Failed to fetch latest release info" version=$(echo "$release_info" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') [[ -n "$version" ]] || error "Failed to parse version from release info" download_url="$INSTALL_API/tui/latest/$target" else version="${requested_version#v}" # Verify release exists (use GET, not HEAD - server only handles GET) http_code=$(curl -s -o /dev/null -w "%{http_code}" "$INSTALL_API/tui/$version") [[ "$http_code" != "404" ]] || error "Release v$version not found" download_url="$INSTALL_API/tui/$version/$target" fi # Check if already installed if command -v ace >/dev/null 2>&1; then installed=$(ace --version 2>/dev/null || echo "") if [[ "$installed" = "$version" ]]; then echo "" echo -e " ${Green}ace v$version is already installed${Color_Off}" echo "" exit 0 fi fi # Progress bar utilities unbuffered_sed() { if echo | sed -u -e "" >/dev/null 2>&1; then sed -nu "$@" elif echo | sed -l -e "" >/dev/null 2>&1; then sed -nl "$@" else local pad="$(printf "\n%512s" "")" sed -ne "s/$/\\${pad}/" "$@" fi } print_progress() { local bytes="$1" local length="$2" [ "$length" -gt 0 ] || return 0 local width=50 local percent=$(( bytes * 100 / length )) [ "$percent" -gt 100 ] && percent=100 local on=$(( percent * width / 100 )) local off=$(( width - on )) local filled=$(printf "%*s" "$on" "") filled=${filled// /■} local empty=$(printf "%*s" "$off" "") empty=${empty// /・} printf "\r ${Green}%s%s %3d%%${Color_Off}" "$filled" "$empty" "$percent" >&4 } download_with_progress() { local url="$1" local output="$2" if [ -t 2 ]; then exec 4>&2 else exec 4>/dev/null fi local tmp_dir=${TMPDIR:-/tmp} local basename="${tmp_dir}/ace_install_$$" local tracefile="${basename}.trace" rm -f "$tracefile" mkfifo "$tracefile" # Hide cursor printf "\033[?25l" >&4 trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN ( curl --trace-ascii "$tracefile" -s -L -o "$output" "$url" ) & local curl_pid=$! unbuffered_sed \ -e 'y/ACDEGHLNORTV/acdeghlnortv/' \ -e '/^0000: content-length:/p' \ -e '/^<= recv data/p' \ "$tracefile" | \ { local length=0 local bytes=0 while IFS=" " read -r -a line; do [ "${#line[@]}" -lt 2 ] && continue local tag="${line[0]} ${line[1]}" if [ "$tag" = "0000: content-length:" ]; then length="${line[2]}" length=$(echo "$length" | tr -d '\r') bytes=0 elif [ "$tag" = "<= recv" ]; then local size="${line[3]}" bytes=$(( bytes + size )) if [ "$length" -gt 0 ]; then print_progress "$bytes" "$length" fi fi done } wait $curl_pid local ret=$? echo "" >&4 return $ret } info "Installing ace v$version for $target" # Download with progress bar (fallback to curl progress on Windows/non-TTY) if [[ "$target" == windows-* ]] || ! [ -t 2 ] || ! download_with_progress "$download_url" "$exe.zip"; then curl --fail --location --progress-bar --output "$exe.zip" "$download_url" || error "Failed to download ace from $download_url" fi # Extract unzip -oqd "$bin_dir" "$exe.zip" || error "Failed to extract ace" # Move binary (handles nested directory from zip) if [[ -d "$bin_dir/ace-$target" ]]; then if [[ $target = windows-x64 ]]; then mv "$bin_dir/ace-$target/ace.exe" "$exe" else mv "$bin_dir/ace-$target/ace" "$exe" fi rm -rf "$bin_dir/ace-$target" fi chmod +x "$exe" rm -f "$exe.zip" info "ace v$version installed to $display_exe" # Add to PATH (if not already there) if command -v ace >/dev/null; then : # ace already in PATH elif [[ "$no_modify_path" = "true" ]]; then : # user opted out else # Use $HOME in the export so it's portable path_cmd="export PATH=\"\$HOME/.ace/bin:\$PATH\"" # Helper to display config path with ~ for $HOME display_config() { if [[ $1 = $HOME/* ]]; then echo "~${1#$HOME}" else echo "$1" fi } case $(basename "$SHELL") in fish) fish_config=$HOME/.config/fish/config.fish fish_cmd="fish_add_path \$HOME/.ace/bin" if [[ -w $fish_config ]]; then if ! grep -Fq "fish_add_path" "$fish_config" || ! grep -Fq ".ace/bin" "$fish_config"; then { echo -e '\n# GitHub Ace' echo "$fish_cmd" } >> "$fish_config" info "Added $display_bin to \$PATH in $(display_config "$fish_config")" fi else echo " Manually add to $(display_config "$fish_config"):" echo " $fish_cmd" fi ;; zsh) zsh_config=${ZDOTDIR:-$HOME}/.zshrc if [[ -w $zsh_config ]]; then if ! grep -Fq ".ace/bin" "$zsh_config"; then { echo -e '\n# GitHub Ace' echo "$path_cmd" } >> "$zsh_config" info "Added $display_bin to \$PATH in $(display_config "$zsh_config")" fi else echo " Manually add to $(display_config "$zsh_config"):" echo " $path_cmd" fi ;; bash) for bash_config in "$HOME/.bashrc" "$HOME/.bash_profile"; do if [[ -w $bash_config ]]; then if ! grep -Fq ".ace/bin" "$bash_config"; then { echo -e '\n# GitHub Ace' echo "$path_cmd" } >> "$bash_config" info "Added $display_bin to \$PATH in $(display_config "$bash_config")" fi break fi done ;; *) echo " Manually add to your shell config:" echo " $path_cmd" ;; esac fi # GitHub Actions support if [[ "${GITHUB_ACTIONS:-}" = "true" ]]; then echo "$bin_dir" >> "$GITHUB_PATH" info "Added $bin_dir to \$GITHUB_PATH" fi # Success message echo "" echo -e " ${Green} █████╗ ██████╗███████╗${Color_Off}" echo -e " ${Green}██╔══██╗██╔════╝██╔════╝${Color_Off}" echo -e " ${Green}███████║██║ █████╗ ${Color_Off}" echo -e " ${Green}██╔══██║██║ ██╔══╝ ${Color_Off}" echo -e " ${Green}██║ ██║╚██████╗███████╗${Color_Off}" echo -e " ${Green}╚═╝ ╚═╝ ╚═════╝╚══════╝${Color_Off}" echo "" echo -e " ${Dim}To get started:${Color_Off}" echo "" echo -e " ace ${Dim}# Start Ace${Color_Off}" echo -e " ace --help ${Dim}# Display help${Color_Off}" echo ""