#!/bin/sh set -eu REPO_OWNER="${LUNARGATE_REPO_OWNER:-lunargate-ai}" REPO_NAME="${LUNARGATE_REPO_NAME:-gateway}" VERSION="${LUNARGATE_VERSION:-}" log() { printf '%s\n' "$*" } fail() { printf 'Error: %s\n' "$*" >&2 exit 1 } usage() { cat <<'EOF' Usage: install.sh [install|update|uninstall|help] Commands: install Install LunarGate (default) update Download and install the latest LunarGate binary uninstall Remove the installed LunarGate binary help Show this help message Environment: INSTALL_PATH Directory where the lunargate binary should be installed Examples: curl -fsSL https://get.lunargate.ai/install.sh | sh curl -fsSL https://get.lunargate.ai/install.sh | INSTALL_PATH="$HOME/.local/bin" sh EOF } is_writable_or_creatable_dir() { dir="$1" while [ ! -e "$dir" ]; do next_dir="$(dirname "$dir")" [ "$next_dir" != "$dir" ] || break dir="$next_dir" done [ -d "$dir" ] && [ -w "$dir" ] } path_contains_dir() { case ":${PATH:-}:" in *:"$1":*) return 0 ;; *) return 1 ;; esac } resolve_install_dir() { if [ -n "${INSTALL_PATH:-}" ]; then printf '%s' "$INSTALL_PATH" return 0 fi default_dir="/usr/local/bin" if is_writable_or_creatable_dir "$default_dir"; then printf '%s' "$default_dir" return 0 fi if [ -n "${HOME:-}" ]; then fallback_dir="${HOME}/.local/bin" if is_writable_or_creatable_dir "$fallback_dir"; then log "/usr/local/bin is not writable; falling back to ${fallback_dir}." >&2 printf '%s' "$fallback_dir" return 0 fi fi printf '%s' "$default_dir" } resolve_existing_binary_path() { if [ -n "${INSTALL_PATH:-}" ]; then printf '%s/lunargate' "$INSTALL_PATH" return 0 fi existing_path="$(command -v lunargate 2>/dev/null || true)" case "$existing_path" in */lunargate) printf '%s' "$existing_path" return 0 ;; esac if [ -f "/usr/local/bin/lunargate" ]; then printf '%s' "/usr/local/bin/lunargate" return 0 fi if [ -n "${HOME:-}" ] && [ -f "${HOME}/.local/bin/lunargate" ]; then printf '%s' "${HOME}/.local/bin/lunargate" return 0 fi fail "could not find an installed lunargate binary. Set INSTALL_PATH=/path/to/bin to uninstall explicitly" } detect_os() { case "$(uname -s)" in Linux) printf 'linux' ;; Darwin) printf 'darwin' ;; *) fail "unsupported operating system: $(uname -s)" ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) printf 'amd64' ;; arm64|aarch64) printf 'arm64' ;; *) fail "unsupported architecture: $(uname -m)" ;; esac } require_cmd() { command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" } release_api_url() { if [ -n "$VERSION" ]; then printf 'https://api.github.com/repos/%s/%s/releases/tags/%s' "$REPO_OWNER" "$REPO_NAME" "$VERSION" else printf 'https://api.github.com/repos/%s/%s/releases/latest' "$REPO_OWNER" "$REPO_NAME" fi } fetch_release_json() { url="$(release_api_url)" if json="$(curl -fsSL --retry 3 --connect-timeout 15 "$url")"; then printf '%s' "$json" return 0 fi if [ -n "$VERSION" ]; then case "$VERSION" in v*) ;; *) fallback_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/v${VERSION}" json="$(curl -fsSL --retry 3 --connect-timeout 15 "$fallback_url")" || fail "failed to fetch release metadata for ${VERSION}" printf '%s' "$json" return 0 ;; esac fi fail "failed to fetch release metadata" } parse_tag() { printf '%s' "$1" | tr ',' '\n' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 } find_asset_url() { json="$1" pattern="$2" printf '%s' "$json" | tr ',' '\n' | sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | grep "$pattern" | head -n 1 } verify_checksum() { checksum_file="$1" archive_file="$2" if command -v sha256sum >/dev/null 2>&1; then (cd "$(dirname "$archive_file")" && sha256sum -c "$checksum_file" --ignore-missing) >/dev/null 2>&1 || fail "checksum verification failed" return 0 fi if command -v shasum >/dev/null 2>&1; then expected="$(grep " $(basename "$archive_file")$" "$checksum_file" | awk '{print $1}')" [ -n "$expected" ] || fail "checksum entry not found for $(basename "$archive_file")" actual="$(shasum -a 256 "$archive_file" | awk '{print $1}')" [ "$expected" = "$actual" ] || fail "checksum verification failed" return 0 fi log "Skipping checksum verification because neither sha256sum nor shasum is available." } install_file() { source_path="$1" target_path="$2" target_dir="$(dirname "$target_path")" if is_writable_or_creatable_dir "$target_dir"; then mkdir -p "$target_dir" install "$source_path" "$target_path" return 0 fi command -v sudo >/dev/null 2>&1 || fail "need write access to $target_dir, set INSTALL_PATH, or have sudo installed" sudo mkdir -p "$target_dir" sudo install "$source_path" "$target_path" } remove_file() { target_path="$1" target_dir="$(dirname "$target_path")" [ -e "$target_path" ] || fail "binary not found at ${target_path}" if [ -w "$target_dir" ]; then rm -f "$target_path" return 0 fi command -v sudo >/dev/null 2>&1 || fail "need write access to $target_dir, set INSTALL_PATH, or have sudo installed" sudo rm -f "$target_path" } install_release() { action_label="$1" require_cmd curl require_cmd tar require_cmd grep require_cmd sed require_cmd install require_cmd mktemp os="$(detect_os)" arch="$(detect_arch)" install_dir="$(resolve_install_dir)" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT INT TERM log "${action_label} LunarGate release for ${os}/${arch}..." release_json="$(fetch_release_json)" tag="$(parse_tag "$release_json")" [ -n "$tag" ] || fail "could not determine release tag" archive_url="$(find_asset_url "$release_json" "/gateway_.*_${os}_${arch}\\.tar\\.gz$")" [ -n "$archive_url" ] || fail "release asset for ${os}/${arch} not found" checksums_url="$(find_asset_url "$release_json" "/checksums\\.txt$")" [ -n "$checksums_url" ] || fail "checksums.txt asset not found" archive_path="${tmp_dir}/$(basename "$archive_url")" checksums_path="${tmp_dir}/checksums.txt" log "Downloading ${tag}..." curl -fsSL --retry 3 --connect-timeout 15 "$archive_url" -o "$archive_path" curl -fsSL --retry 3 --connect-timeout 15 "$checksums_url" -o "$checksums_path" verify_checksum "$checksums_path" "$archive_path" tar -xzf "$archive_path" -C "$tmp_dir" [ -f "${tmp_dir}/lunargate" ] || fail "release archive does not contain lunargate binary" log "Installing binary to ${install_dir}/lunargate..." install_file "${tmp_dir}/lunargate" "${install_dir}/lunargate" log "LunarGate ${tag} installed successfully." if ! path_contains_dir "$install_dir"; then log "Add ${install_dir} to your PATH to run 'lunargate' directly." fi log "Run: lunargate --help" } uninstall_release() { binary_path="$(resolve_existing_binary_path)" log "Removing LunarGate binary from ${binary_path}..." remove_file "$binary_path" log "LunarGate uninstalled successfully." } main() { action="${1:-install}" case "$action" in install) install_release "Resolving" ;; update) install_release "Updating" ;; uninstall) uninstall_release ;; help|-h|--help) usage ;; *) usage >&2 fail "unsupported command: ${action}" ;; esac } main "$@"