#!/usr/bin/env bash set -euo pipefail # AdaL CLI Installer # Usage: curl -fsSL https://adal.sylph.ai/install.sh | bash # latest stable # curl -fsSL https://adal.sylph.ai/install.sh | bash -s -- --version beta # latest beta # curl -fsSL https://adal.sylph.ai/install.sh | bash -s -- --version 1.0.2-beta.1 # specific version # # Installs to ~/.adal/versions// with symlink at ~/.adal/bin/adal # Supports: macOS (arm64/x64), Linux (x64/arm64), Windows (x64 via Git Bash) APP="adal" RELEASES_URL="https://d35qg8ac0yw4p7.cloudfront.net/cli" TRACK_URL="${ADAL_TRACK_URL:-https://adal.sylph.ai/api/installs/track}" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MUTED='\033[0;2m' NC='\033[0m' # ─── Parse arguments ────────────────────────────────────────────────────────── usage() { cat < Install a specific version (e.g., 0.6.0-beta.1) --local-tarball Install from a local native tarball (skip download) --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.) -h, --help Show this help message EOF } requested_version="" no_modify_path=false local_tarball="" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; -v|--version) if [[ -n "${2:-}" ]]; then requested_version="$2"; shift 2 else echo -e "${RED}Error: --version requires a version argument${NC}" >&2; exit 1 fi ;; --no-modify-path) no_modify_path=true; shift ;; --local-tarball) if [[ -n "${2:-}" ]]; then local_tarball="$2"; shift 2 else echo -e "${RED}Error: --local-tarball requires a path argument${NC}" >&2; exit 1 fi ;; *) echo -e "${YELLOW}Warning: Unknown option '$1'${NC}" >&2; shift ;; esac done # ─── Platform detection ─────────────────────────────────────────────────────── detect_platform() { local raw_os raw_os=$(uname -s) case "$raw_os" in Darwin*) os="darwin" ;; Linux*) os="linux" ;; MINGW*|MSYS*|CYGWIN*) os="win32" ;; *) echo -e "${RED}Unsupported operating system: $raw_os${NC}" >&2 echo -e "${MUTED}AdaL supports macOS, Linux, and Windows (via Git Bash).${NC}" >&2 exit 1 ;; esac local raw_arch raw_arch=$(uname -m) case "$raw_arch" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; *) echo -e "${RED}Unsupported architecture: $raw_arch${NC}" >&2 exit 1 ;; esac # Detect Rosetta 2: if running x64 under emulation on ARM Mac, use arm64 if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then arch="arm64" echo -e "${MUTED}Detected Rosetta 2 — using native arm64 binary${NC}" fi fi # Detect musl (Alpine Linux) is_musl=false if [ "$os" = "linux" ]; then if [ -f /etc/alpine-release ]; then is_musl=true elif command -v ldd >/dev/null 2>&1; then if ldd --version 2>&1 | grep -qi musl; then is_musl=true fi fi fi PLATFORM="${os}-${arch}" # On musl-libc Linux (Alpine and similar), pick the musl-native tarball # instead of the glibc one — glibc binaries (including the bundled Bun # runtime) cannot load when /lib64/ld-linux-x86-64.so.2 is absent. if [ "$is_musl" = true ] && [ "$os" = "linux" ]; then PLATFORM="${PLATFORM}-musl" echo -e "${MUTED}Detected musl libc (Alpine and similar) — using ${PLATFORM} tarball${NC}" fi # Validate platform is supported. # NOTE: linux-arm64-musl is intentionally NOT in this list — no CI matrix # entry builds that tarball yet, so accepting it here would yield a # confusing 404 at download time. Re-add once the release pipeline # publishes it. case "$PLATFORM" in darwin-arm64|darwin-x64|linux-x64|linux-x64-musl|linux-arm64|win32-x64) ;; *) echo -e "${RED}Unsupported platform: $PLATFORM${NC}" >&2 echo -e "${MUTED}Supported: macOS (arm64/x64), Linux (x64 glibc/musl, arm64 glibc), Windows (x64)${NC}" >&2 exit 1 ;; esac } # ─── musl runtime deps ──────────────────────────────────────────────────────── # Bun in the musl tarball is dynamically linked against libstdc++ and libgcc_s # (Bun's C++ runtime + GCC's unwinder). Slim Alpine base images (including # python:3.11-alpine, alpine:3.x, and most SWE-bench Alpine variants) don't # ship these by default — exec'ing adal then prints a flood of "Error # loading shared library libstdc++.so.6 / libgcc_s.so.1" messages. Install # them via apk before extracting the tarball. ensure_musl_runtime_deps() { [ "$is_musl" = true ] || return 0 [ "$os" = "linux" ] || return 0 # Skip if both libs are already present. if ldconfig -p 2>/dev/null | grep -q libstdc++.so.6 && \ ldconfig -p 2>/dev/null | grep -q libgcc_s.so.1; then return 0 fi if [ -f /usr/lib/libstdc++.so.6 ] && [ -f /usr/lib/libgcc_s.so.1 ]; then return 0 fi if ! command -v apk >/dev/null 2>&1; then echo -e "${YELLOW}musl detected but apk is not available; cannot auto-install libstdc++/libgcc.${NC}" >&2 echo -e "${MUTED}Please install them manually before running adal.${NC}" >&2 return 0 fi echo -e "${MUTED}Installing libstdc++ and libgcc (required by bundled Bun runtime)...${NC}" if [ "$(id -u 2>/dev/null)" = "0" ]; then apk add --no-cache libstdc++ libgcc >/dev/null 2>&1 || \ echo -e "${YELLOW}apk add libstdc++ libgcc failed; adal may not start.${NC}" >&2 elif command -v sudo >/dev/null 2>&1; then sudo apk add --no-cache libstdc++ libgcc >/dev/null 2>&1 || \ echo -e "${YELLOW}sudo apk add libstdc++ libgcc failed; adal may not start.${NC}" >&2 else echo -e "${YELLOW}Not running as root and sudo not available — please run: apk add libstdc++ libgcc${NC}" >&2 fi } # ─── Download helpers ───────────────────────────────────────────────────────── DOWNLOADER="" detect_downloader() { if command -v curl >/dev/null 2>&1; then DOWNLOADER="curl" elif command -v wget >/dev/null 2>&1; then DOWNLOADER="wget" else echo -e "${RED}Either curl or wget is required but neither is installed${NC}" >&2 exit 1 fi } download_file() { local url="$1" local output="${2:-}" if [ "$DOWNLOADER" = "curl" ]; then if [ -n "$output" ]; then curl -fsSL -o "$output" "$url" else curl -fsSL "$url" fi elif [ "$DOWNLOADER" = "wget" ]; then if [ -n "$output" ]; then wget -q -O "$output" "$url" else wget -q -O - "$url" fi fi } download_with_progress() { local url="$1" local output="$2" local expected_size="$3" # optional, in bytes # Start download in background if [ "$DOWNLOADER" = "curl" ]; then curl -fsSL -o "$output" "$url" & elif [ "$DOWNLOADER" = "wget" ]; then wget -q -O "$output" "$url" & fi local dl_pid=$! # Show progress if we know the expected size if [ -n "$expected_size" ] && [ "$expected_size" -gt 0 ] 2>/dev/null; then local last_pct=-1 while kill -0 "$dl_pid" 2>/dev/null; do if [ -f "$output" ]; then local current_size if [[ "$(uname -s)" == "Darwin" ]]; then current_size=$(stat -f%z "$output" 2>/dev/null || echo 0) else current_size=$(stat -c%s "$output" 2>/dev/null || echo 0) fi local pct=$((current_size * 100 / expected_size)) [ "$pct" -gt 100 ] && pct=100 if [ "$pct" -ne "$last_pct" ]; then printf "\r${MUTED}🌸 Installing ${NC}${APP} ${MUTED}v${VERSION} (%d%%)${NC}" "$pct" >&2 last_pct=$pct fi fi sleep 0.5 done printf "\r${MUTED}🌸 Installing ${NC}${APP} ${MUTED}v${VERSION} (100%%)${NC}\n" >&2 fi wait "$dl_pid" return $? } # ─── Checksum verification ──────────────────────────────────────────────────── verify_checksum() { local file="$1" local expected="$2" local actual if command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$file" | cut -d' ' -f1) elif command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$file" | cut -d' ' -f1) else echo -e "${YELLOW}Warning: Cannot verify checksum (no shasum or sha256sum)${NC}" >&2 return 0 fi if [ "$actual" != "$expected" ]; then echo -e "${RED}Checksum verification failed!${NC}" >&2 echo -e "${MUTED} Expected: $expected${NC}" >&2 echo -e "${MUTED} Actual: $actual${NC}" >&2 return 1 fi } # ─── JSON parsing (no jq dependency) ────────────────────────────────────────── # Extract a string value from JSON: json_get '{"key":"val"}' "key" → val json_get() { local json="$1" local key="$2" echo "$json" | sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1 } # Extract a number value from JSON json_get_num() { local json="$1" local key="$2" echo "$json" | sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p" | head -1 } # Extract a platform block from manifest JSON json_get_platform() { local json="$1" local platform="$2" # Extract everything between "platform": { ... } echo "$json" | tr -d '\n' | sed -n "s/.*\"$platform\"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p" } # ─── Version resolution ────────────────────────────────────────────────────── resolve_version() { if [ -n "$requested_version" ]; then # Strip leading 'v' if present requested_version="${requested_version#v}" VERSION="$requested_version" # Support channel names (e.g., "beta") — resolve to actual version number if echo "$VERSION" | grep -qE '^[a-z]+$'; then echo -e "${MUTED}Resolving channel '$VERSION'...${NC}" local resolved resolved=$(download_file "$RELEASES_URL/$VERSION" 2>/dev/null | tr -d '[:space:]') if [ -z "$resolved" ]; then echo -e "${RED}Error: Channel '${VERSION}' not found${NC}" >&2 exit 1 fi VERSION="$resolved" else # Verify the version exists on S3 by checking the manifest local http_status if [ "$DOWNLOADER" = "curl" ]; then http_status=$(curl -sI -o /dev/null -w "%{http_code}" "$RELEASES_URL/manifests/manifest-${VERSION}.json") else http_status=$(wget --spider -S "$RELEASES_URL/manifests/manifest-${VERSION}.json" 2>&1 | grep "HTTP/" | tail -1 | awk '{print $2}') fi if [ "$http_status" = "404" ] || [ "$http_status" = "403" ]; then echo -e "${RED}Error: Version ${VERSION} not found${NC}" >&2 exit 1 fi fi else # Fetch latest version from S3 channel pointer file echo -e "${MUTED}Checking for latest version...${NC}" VERSION=$(download_file "$RELEASES_URL/latest" 2>/dev/null | tr -d '[:space:]') if [ -z "$VERSION" ]; then echo -e "${RED}Failed to determine latest version${NC}" >&2 exit 1 fi fi } # ─── Check existing installation ───────────────────────────────────────────── INSTALL_BASE="$HOME/.adal" VERSIONS_DIR="$INSTALL_BASE/versions" BIN_DIR="$INSTALL_BASE/bin" check_existing() { if [ -d "$VERSIONS_DIR/$VERSION" ]; then echo -e "${MUTED}Version $VERSION is already installed${NC}" # Still update symlinks and PATH in case they're broken or missing. setup_symlinks cleanup_old_versions setup_path setup_github_actions_path detect_npm_install print_success exit 0 fi } # ─── Local tarball install ───────────────────────────────────────────────────── install_from_local_tarball() { local tarball="$1" if [ ! -f "$tarball" ]; then echo -e "${RED}Error: Local tarball not found: $tarball${NC}" >&2 exit 1 fi # Extract version from tarball filename: adal--.tar.gz local basename_tar basename_tar=$(basename "$tarball") # Strip extension (.tar.gz or .zip) local stem="${basename_tar%.tar.gz}" stem="${stem%.zip}" # Extract version: adal-- → strip "adal-" prefix, then strip "-" suffix # Platform is the last 2 or 3 dash-separated segments (e.g., linux-x64 or linux-x64-musl) VERSION=$(echo "$stem" | sed 's/^adal-//; s/-\(darwin\|linux\|win32\)-.*$//') if [ -z "$VERSION" ]; then echo -e "${RED}Error: Cannot determine version from tarball filename: $basename_tar${NC}" >&2 echo -e "${MUTED}Expected format: adal--.tar.gz${NC}" >&2 exit 1 fi printf "\n${MUTED}🌸 Installing ${NC}${APP} ${MUTED}v${VERSION} (from local tarball)${NC}\n" # Extract mkdir -p "$VERSIONS_DIR" tar -xzf "$tarball" -C "$VERSIONS_DIR" # The tarball extracts to adal-/ — rename to just / if [ -d "$VERSIONS_DIR/adal-$VERSION" ]; then rm -rf "$VERSIONS_DIR/$VERSION" mv "$VERSIONS_DIR/adal-$VERSION" "$VERSIONS_DIR/$VERSION" fi # Verify extraction if [ ! -f "$VERSIONS_DIR/$VERSION/adal-cli.js" ]; then echo -e "${RED}Error: Extraction failed — adal-cli.js not found${NC}" >&2 exit 1 fi } # ─── Main install logic ────────────────────────────────────────────────────── install_version() { printf "\n${MUTED}🌸 Installing ${NC}${APP} ${MUTED}v${VERSION}${NC}" local archive_ext="tar.gz" if [ "$os" = "win32" ]; then archive_ext="zip" fi local filename="adal-${VERSION}-${PLATFORM}.${archive_ext}" local download_url="$RELEASES_URL/${VERSION}/${filename}" # Download manifest for checksum # Fetch manifest silently local manifest_url="$RELEASES_URL/manifests/manifest-${VERSION}.json" local manifest_json manifest_json=$(download_file "$manifest_url" 2>/dev/null || echo "") local expected_checksum="" local expected_size="" if [ -n "$manifest_json" ]; then local platform_block platform_block=$(json_get_platform "$manifest_json" "$PLATFORM") if [ -n "$platform_block" ]; then expected_checksum=$(json_get "$platform_block" "checksum" 2>/dev/null || echo "") expected_size=$(json_get_num "$platform_block" "size" 2>/dev/null || echo "") fi fi # Download tarball local tmp_dir tmp_dir=$(mktemp -d) local archive_path="$tmp_dir/$filename" # Download with single-line progress percentage if ! download_with_progress "$download_url" "$archive_path" "$expected_size"; then echo -e "${RED}Download failed${NC}" >&2 rm -rf "$tmp_dir" exit 1 fi # Verify checksum if [ -n "$expected_checksum" ]; then # Verify checksum silently if ! verify_checksum "$archive_path" "$expected_checksum"; then rm -rf "$tmp_dir" exit 1 fi # Checksum verified silently else echo -e " ${YELLOW}⚠ No checksum available (manifest not found)${NC}" fi # Extract # Extract silently mkdir -p "$VERSIONS_DIR" if [ "$os" = "linux" ]; then tar -xzf "$archive_path" -C "$VERSIONS_DIR" else # macOS and Windows (Git Bash) if command -v tar >/dev/null 2>&1 && [ "$archive_ext" = "tar.gz" ]; then tar -xzf "$archive_path" -C "$VERSIONS_DIR" elif command -v unzip >/dev/null 2>&1; then unzip -q "$archive_path" -d "$VERSIONS_DIR" else echo -e "${RED}Error: No extraction tool available (need tar or unzip)${NC}" >&2 rm -rf "$tmp_dir" exit 1 fi fi # The tarball extracts to adal-/ — rename to just / if [ -d "$VERSIONS_DIR/adal-$VERSION" ]; then # Remove existing version dir if somehow there rm -rf "$VERSIONS_DIR/$VERSION" mv "$VERSIONS_DIR/adal-$VERSION" "$VERSIONS_DIR/$VERSION" fi # Verify extraction if [ ! -f "$VERSIONS_DIR/$VERSION/adal-cli.js" ]; then echo -e "${RED}Error: Extraction failed — adal-cli.js not found${NC}" >&2 rm -rf "$tmp_dir" exit 1 fi # Cleanup rm -rf "$tmp_dir" # Extracted silently # Track after successful extraction — guarantees the row represents a real, # finished install/upgrade, not a failed/aborted attempt. track_install } # ─── Symlink setup ──────────────────────────────────────────────────────────── setup_symlinks() { mkdir -p "$BIN_DIR" # Update 'current' symlink local current_link="$VERSIONS_DIR/current" rm -f "$current_link" ln -s "$VERSION" "$current_link" # Create bin/adal → ../versions/current/adal local bin_target="$BIN_DIR/adal" rm -f "$bin_target" if [ "$os" = "win32" ]; then # On Windows/Git Bash, create a shell wrapper instead of symlink cat > "$bin_target" << 'WINWRAP' #!/usr/bin/env sh SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" exec "$SCRIPT_DIR/../versions/current/adal.cmd" "$@" WINWRAP chmod +x "$bin_target" else ln -s "../versions/current/adal" "$bin_target" fi setup_immediate_path_shim # Symlinks created silently } path_contains_dir() { local dir="$1" [[ ":$PATH:" == *":$dir:"* ]] } create_adal_shim() { local target_dir="$1" local shim_path="$target_dir/adal" local expected_target="$VERSIONS_DIR/current/adal" [ -d "$target_dir" ] || return 1 [ -w "$target_dir" ] || return 1 if [ -L "$shim_path" ] && [ "$(readlink "$shim_path" 2>/dev/null || true)" = "$expected_target" ]; then return 2 # Already correct; no user-visible work happened. fi # Avoid clobbering unrelated real files. Symlinks are safe to refresh because # previous npm/native installs commonly expose commands as symlinks. if [ -e "$shim_path" ] && [ ! -L "$shim_path" ]; then return 1 fi rm -f "$shim_path" ln -s "$expected_target" "$shim_path" return 0 } setup_immediate_path_shim() { [ "$os" != "win32" ] || return 0 # Best-effort same-terminal support for `curl | bash` followed by `adal`. # The installer cannot change the parent shell's PATH, so only use selected # directories that are already in PATH and writable without sudo. local candidates=( "$HOME/.local/bin" "/opt/homebrew/bin" "/usr/local/bin" ) local dir for dir in "${candidates[@]}"; do if path_contains_dir "$dir"; then if create_adal_shim "$dir"; then echo -e " ${GREEN}✅ Made adal available in current PATH via $dir${NC}" return 0 elif [ "$?" -eq 2 ]; then return 0 fi fi done # Still create the user-local shim for future shells that include ~/.local/bin. mkdir -p "$HOME/.local/bin" create_adal_shim "$HOME/.local/bin" || true } # ─── Clean old versions ────────────────────────────────────────────────────── cleanup_old_versions() { local keep=1 local versions=() # List version directories (exclude 'current' symlink and the just-installed version) for dir in "$VERSIONS_DIR"/*/; do [ -d "$dir" ] || continue local name name=$(basename "$dir") [ "$name" = "current" ] && continue [ "$name" = "$VERSION" ] && continue versions+=("$name") done # Remove all old versions (the current $VERSION is already excluded above) if [ ${#versions[@]} -gt 0 ]; then for ver in "${versions[@]}"; do rm -rf "$VERSIONS_DIR/$ver" done fi } # ─── PATH setup ────────────────────────────────────────────────────────────── add_to_path() { local config_file="$1" local command="$2" local legacy_command="${command//\$HOME/$HOME}" if grep -Fxq "$command" "$config_file" 2>/dev/null || \ grep -Fxq "$legacy_command" "$config_file" 2>/dev/null; then return 0 # Already present fi if [ -w "$config_file" ]; then echo "" >> "$config_file" echo "$command" >> "$config_file" echo -e " ${GREEN}✅ Added AdaL to PATH in $config_file${NC}" else echo -e " ${YELLOW}Manually add to $config_file:${NC}" echo -e " $command" fi } setup_path() { if [ "$no_modify_path" = true ]; then return 0 fi # Check if already in PATH if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then return 0 fi local current_shell current_shell=$(basename "${SHELL:-/bin/sh}") local config_files="" local default_config_file="" case "$current_shell" in fish) config_files="$HOME/.config/fish/config.fish" default_config_file="$HOME/.config/fish/config.fish" ;; zsh) config_files="${ZDOTDIR:-$HOME}/.zshrc" default_config_file="${ZDOTDIR:-$HOME}/.zshrc" ;; bash) config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" default_config_file="$HOME/.bashrc" ;; ash|sh) config_files="$HOME/.profile" default_config_file="$HOME/.profile" ;; *) config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" default_config_file="$HOME/.profile" ;; esac # Find the first existing config file. local config_file="" for file in $config_files; do if [ -f "$file" ]; then config_file="$file" break fi done # Fresh machines may not have a shell config yet. Create the default one so # future terminals can find AdaL without requiring manual PATH setup. if [ -z "$config_file" ]; then config_file="$default_config_file" mkdir -p "$(dirname "$config_file")" touch "$config_file" fi case "$current_shell" in fish) add_to_path "$config_file" "fish_add_path \$HOME/.adal/bin" ;; *) add_to_path "$config_file" "export PATH=\"\$HOME/.adal/bin:\$PATH\"" ;; esac } setup_github_actions_path() { if [ -n "${GITHUB_ACTIONS:-}" ] && [ "${GITHUB_ACTIONS}" = "true" ] && [ -n "${GITHUB_PATH:-}" ]; then if ! grep -qx "$BIN_DIR" "$GITHUB_PATH" 2>/dev/null; then echo "$BIN_DIR" >> "$GITHUB_PATH" echo -e " ${GREEN}✅ Added to \$GITHUB_PATH${NC}" fi fi } # ─── Migration detection ───────────────────────────────────────────────────── detect_npm_install() { if command -v adal >/dev/null 2>&1; then local existing_path existing_path=$(which adal 2>/dev/null || command -v adal 2>/dev/null || true) if [ -n "$existing_path" ] && [[ "$existing_path" != *".adal/bin"* ]]; then # Silently note the npm install exists — don't suggest uninstalling # because that can break 'adal' command if ~/.adal/bin isn't in PATH yet. # Both coexist safely; native takes priority after terminal restart. : fi fi } print_success() { echo -e "" echo -e "${GREEN}🌸 AdaL CLI v${VERSION} installed!${NC}" echo -e "" echo -e "${MUTED}Location: ${NC}$BIN_DIR/adal" echo -e "" echo -e "${MUTED}Next: Run ${NC}adal${MUTED} in your working directory${NC}" echo -e "" } # ─── Anonymous install tracking ────────────────────────────────────────────── # Fire-and-forget POST so we can count actual installs/upgrades. # Only called from install_version(), so same-version no-op reinstalls don't # fire (check_existing() exits earlier on that path). Honours ADAL_NO_TRACK=1. # # event_type: # install — first install on this machine (no other versions under $VERSIONS_DIR) # upgrade — some other version was already installed track_install() { [ "${ADAL_NO_TRACK:-0}" = "1" ] && return 0 [ -z "$TRACK_URL" ] && return 0 # Two channels: "stable" = clean semver (X.Y.Z exactly), "beta" = anything else. # Any version with a `-suffix` (-beta, -alpha, -rc, -preview, -dev, etc.) is "beta" # so dev/internal builds can't masquerade as stable in the dashboard. local channel="stable" case "$VERSION" in *-*) channel="beta" ;; esac # install vs upgrade — does any prior version dir exist? local event_type="install" if [ -d "$VERSIONS_DIR" ]; then for dir in "$VERSIONS_DIR"/*/; do [ -d "$dir" ] || continue local name name=$(basename "$dir") [ "$name" = "current" ] && continue [ "$name" = "$VERSION" ] && continue event_type="upgrade" break done fi local body="{\"platform\":\"cli\",\"channel\":\"$channel\",\"event_type\":\"$event_type\",\"version\":\"$VERSION\"}" ( if [ "$DOWNLOADER" = "curl" ]; then curl -fsS -m 2 -X POST -H 'content-type: application/json' \ -d "$body" "$TRACK_URL" >/dev/null 2>&1 || true elif [ "$DOWNLOADER" = "wget" ]; then wget -q -T 2 --header='content-type: application/json' \ --post-data="$body" -O /dev/null "$TRACK_URL" >/dev/null 2>&1 || true fi ) & disown 2>/dev/null || true } # ─── Main ───────────────────────────────────────────────────────────────────── main() { detect_platform ensure_musl_runtime_deps if [ -n "$local_tarball" ]; then # Local tarball mode: skip download, version resolution, and checksum install_from_local_tarball "$local_tarball" else detect_downloader resolve_version check_existing install_version fi setup_symlinks cleanup_old_versions setup_path setup_github_actions_path detect_npm_install print_success } main