#!/bin/sh set -eu # Ato CLI installer # - Primary: prebuilt ato binary from GitHub Releases API # - Also installs nacelle from ATO_RELEASE_BASE_URL for Tier2 execution # - Fallback: cargo install from ato-cli repository ATO_RELEASE_REPO="${ATO_RELEASE_REPO:-Koh0920/ato-cli}" ATO_RELEASE_VERSION="${ATO_RELEASE_VERSION:-latest}" ATO_GITHUB_API_BASE_URL="${ATO_GITHUB_API_BASE_URL:-https://api.github.com}" ATO_GITHUB_RELEASE_BASE_URL="${ATO_GITHUB_RELEASE_BASE_URL:-https://github.com}" ATO_RELEASE_BASE_URL="${ATO_RELEASE_BASE_URL:-https://dl.ato.run}" ATO_INSTALL_DIR_INPUT="${ATO_INSTALL_DIR:-}" ATO_INSTALL_DIR="$HOME/.local/bin" ATO_SKIP_CARGO_FALLBACK="${ATO_SKIP_CARGO_FALLBACK:-0}" ATO_SKIP_NACELLE_INSTALL="${ATO_SKIP_NACELLE_INSTALL:-0}" ATO_SKIP_EDITOR_SETUP="${ATO_SKIP_EDITOR_SETUP:-0}" PROGRAM_NAME="ato" PROGRAM_ARCHIVE_PREFIX="ato-cli" NACELLE_NAME="nacelle" RESOLVED_RELEASE_TAG="" RESOLVED_RELEASE_VERSION="" info() { printf '%s\n' "$*" >&2 } warn() { printf 'Warning: %s\n' "$*" >&2 } fail() { printf 'Error: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1" } resolve_install_dir() { if [ -n "$ATO_INSTALL_DIR_INPUT" ]; then ATO_INSTALL_DIR="$ATO_INSTALL_DIR_INPUT" return 0 fi existing_ato="$(command -v "$PROGRAM_NAME" 2>/dev/null || true)" if [ -n "$existing_ato" ]; then case "$existing_ato" in "$HOME"/*) existing_dir="$(dirname "$existing_ato")" if [ -w "$existing_dir" ]; then ATO_INSTALL_DIR="$existing_dir" info "==> Existing ato detected at $existing_ato (updating in place)" fi ;; esac fi } resolve_editor_profile_path() { shell_name="$(basename "${SHELL:-}")" case "$shell_name" in zsh) if [ -n "${ZDOTDIR:-}" ]; then printf '%s\n' "${ZDOTDIR%/}/.zshrc" else printf '%s\n' "$HOME/.zshrc" fi ;; bash) printf '%s\n' "$HOME/.bashrc" ;; *) printf '%s\n' "$HOME/.profile" ;; esac } detect_default_editor_command() { if [ "$os_raw" = "Darwin" ] && command -v open >/dev/null 2>&1; then printf '%s\n' 'open -W -t' return 0 fi for candidate in sensible-editor editor nano vim vi; do if command -v "$candidate" >/dev/null 2>&1; then printf '%s\n' "$candidate" return 0 fi done return 1 } configure_default_editor() { marker='# Added by ato installer: default editor' if [ "$ATO_SKIP_EDITOR_SETUP" = "1" ]; then info '==> Skipping default EDITOR setup (ATO_SKIP_EDITOR_SETUP=1)' return 0 fi if [ -n "${VISUAL:-}" ] || [ -n "${EDITOR:-}" ]; then info '==> Keeping existing VISUAL/EDITOR environment' return 0 fi editor_command="$(detect_default_editor_command || true)" if [ -z "$editor_command" ]; then warn 'No default editor candidate found; leaving VISUAL/EDITOR unchanged.' return 0 fi profile_path="$(resolve_editor_profile_path)" profile_dir="$(dirname "$profile_path")" mkdir -p "$profile_dir" if [ -f "$profile_path" ] && grep -Fq "$marker" "$profile_path"; then info "==> Default EDITOR already configured in $profile_path" return 0 fi { printf '\n%s\n' "$marker" printf 'if [ -z "${VISUAL:-}" ] && [ -z "${EDITOR:-}" ]; then\n' printf " export EDITOR='%s'\n" "$editor_command" printf 'fi\n' } >> "$profile_path" info "==> Added default EDITOR=$editor_command to $profile_path" info "==> Open a new shell or run: export EDITOR='$editor_command'" } detect_target() { os_raw="$(uname -s 2>/dev/null || true)" arch_raw="$(uname -m 2>/dev/null || true)" case "$os_raw" in Darwin) os="apple-darwin" nacelle_os="darwin" ;; Linux) os="unknown-linux-gnu" nacelle_os="linux" ;; *) fail "Unsupported OS: ${os_raw:-unknown}. Supported: macOS, Linux." ;; esac case "$arch_raw" in x86_64|amd64) arch="x86_64" nacelle_arch="x64" ;; arm64|aarch64) arch="aarch64" nacelle_arch="arm64" ;; *) fail "Unsupported architecture: ${arch_raw:-unknown}. Supported: x86_64, aarch64." ;; esac TARGET_TRIPLE="${arch}-${os}" NACELLE_TARGET="${nacelle_os}-${nacelle_arch}" } normalize_release_tag() { case "$1" in latest) printf 'latest' ;; v*) printf '%s' "$1" ;; *) printf 'v%s' "$1" ;; esac } github_api_get() { curl -fsSL \ --retry 2 \ --connect-timeout 10 \ --max-time 60 \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "$1" \ -o "$2" } resolve_release_metadata() { metadata_path="$1" if [ "$ATO_RELEASE_VERSION" = "latest" ]; then release_endpoint="${ATO_GITHUB_API_BASE_URL%/}/repos/${ATO_RELEASE_REPO}/releases/latest" else release_tag="$(normalize_release_tag "$ATO_RELEASE_VERSION")" release_endpoint="${ATO_GITHUB_API_BASE_URL%/}/repos/${ATO_RELEASE_REPO}/releases/tags/${release_tag}" fi if ! github_api_get "$release_endpoint" "$metadata_path"; then return 1 fi RESOLVED_RELEASE_TAG="$(awk -F '"' '/"tag_name"[[:space:]]*:/ { print $4; exit }' "$metadata_path")" [ -n "$RESOLVED_RELEASE_TAG" ] || fail "GitHub release metadata did not contain tag_name" RESOLVED_RELEASE_VERSION="${RESOLVED_RELEASE_TAG#v}" } download_release_archive() { archive_path="$1" shift release_base="${ATO_GITHUB_RELEASE_BASE_URL%/}/${ATO_RELEASE_REPO}/releases/download/${RESOLVED_RELEASE_TAG}" for archive_name in "$@"; do [ -n "$archive_name" ] || continue archive_url="${release_base}/${archive_name}" if curl -fL --retry 2 --connect-timeout 10 --max-time 120 "$archive_url" -o "$archive_path"; then printf '%s' "$archive_name" return 0 fi done return 1 } install_nacelle() { need_cmd curl latest_url="${ATO_RELEASE_BASE_URL%/}/nacelle/latest.txt" latest_path="$TMP_DIR/nacelle-latest.txt" nacelle_tmp="$TMP_DIR/${NACELLE_NAME}" tried_urls="" info "==> Downloading ${NACELLE_NAME} version metadata" if ! curl -fL --retry 2 --connect-timeout 10 --max-time 60 "$latest_url" -o "$latest_path"; then warn "Failed to fetch nacelle version metadata: $latest_url" warn "ato CLI was installed, but nacelle could not be installed automatically." warn "Runtime commands that require nacelle may not work until the nacelle release bucket is fixed." return 1 fi nacelle_version="$(tr -d '\r\n' < "$latest_path")" if [ -z "$nacelle_version" ]; then warn "nacelle latest.txt was empty: $latest_url" warn "ato CLI was installed, but nacelle could not be installed automatically." return 1 fi nacelle_candidates() { case "$os_raw:$arch_raw" in Darwin:arm64|Darwin:aarch64) printf '%s\n' \ "${NACELLE_NAME}-macos-universal" \ "${NACELLE_NAME}-darwin-arm64" \ "${NACELLE_NAME}-darwin-aarch64" \ "${NACELLE_NAME}-macos-arm64" \ "${NACELLE_NAME}-macos-aarch64" \ "${NACELLE_NAME}-${nacelle_version}-${NACELLE_TARGET}" ;; Darwin:x86_64|Darwin:amd64) printf '%s\n' \ "${NACELLE_NAME}-macos-universal" \ "${NACELLE_NAME}-darwin-x64" \ "${NACELLE_NAME}-darwin-x86_64" \ "${NACELLE_NAME}-macos-x64" \ "${NACELLE_NAME}-macos-x86_64" \ "${NACELLE_NAME}-${nacelle_version}-${NACELLE_TARGET}" ;; Linux:arm64|Linux:aarch64) printf '%s\n' \ "${NACELLE_NAME}-linux-aarch64" \ "${NACELLE_NAME}-linux-arm64" \ "${NACELLE_NAME}-${nacelle_version}-${NACELLE_TARGET}" ;; Linux:x86_64|Linux:amd64) printf '%s\n' \ "${NACELLE_NAME}-linux-x86_64" \ "${NACELLE_NAME}-linux-x64" \ "${NACELLE_NAME}-${nacelle_version}-${NACELLE_TARGET}" ;; *) printf '%s\n' "${NACELLE_NAME}-${nacelle_version}-${NACELLE_TARGET}" ;; esac } info "==> Downloading ${NACELLE_NAME} for ${NACELLE_TARGET}" rm -f "$nacelle_tmp" for nacelle_binary in $(nacelle_candidates); do nacelle_url="${ATO_RELEASE_BASE_URL%/}/nacelle/${nacelle_version}/${nacelle_binary}" tried_urls="${tried_urls}${nacelle_url}\n" if curl -fL --retry 2 --connect-timeout 10 --max-time 120 "$nacelle_url" -o "$nacelle_tmp"; then mkdir -p "$ATO_INSTALL_DIR" cp "$nacelle_tmp" "$ATO_INSTALL_DIR/$NACELLE_NAME" chmod 0755 "$ATO_INSTALL_DIR/$NACELLE_NAME" return 0 fi done warn "Failed to download nacelle for ${NACELLE_TARGET} (version ${nacelle_version})." warn "Tried these URLs:" printf '%b' "$tried_urls" >&2 warn "ato CLI was installed, but nacelle could not be installed automatically." warn "Runtime commands that require nacelle may not work until nacelle artifacts are published to ${ATO_RELEASE_BASE_URL%/}/nacelle/${nacelle_version}/" return 1 } install_from_archive() { need_cmd curl need_cmd tar release_metadata_path="$TMP_DIR/ato-release.json" extract_dir="$TMP_DIR/extract" info "==> Resolving ${PROGRAM_NAME} release metadata from GitHub" if ! resolve_release_metadata "$release_metadata_path"; then return 1 fi archive_path="$TMP_DIR/${PROGRAM_NAME}-archive" archive_name="$(download_release_archive \ "$archive_path" \ "${PROGRAM_ARCHIVE_PREFIX}-${RESOLVED_RELEASE_VERSION}-${TARGET_TRIPLE}.tar.xz" \ "${PROGRAM_ARCHIVE_PREFIX}-${TARGET_TRIPLE}.tar.xz" \ "${PROGRAM_ARCHIVE_PREFIX}-${RESOLVED_RELEASE_VERSION}-${TARGET_TRIPLE}.tar.gz" \ "${PROGRAM_ARCHIVE_PREFIX}-${TARGET_TRIPLE}.tar.gz")" || return 1 info "==> Downloading ${PROGRAM_NAME} ${RESOLVED_RELEASE_TAG} for ${TARGET_TRIPLE}" mkdir -p "$extract_dir" case "$archive_name" in *.tar.xz) tar -xJf "$archive_path" -C "$extract_dir" || return 1 ;; *.tar.gz) tar -xzf "$archive_path" -C "$extract_dir" || return 1 ;; *) return 1 ;; esac binary_path="$extract_dir/$PROGRAM_NAME" archive_dir="${archive_name%.tar.xz}" archive_dir="${archive_dir%.tar.gz}" if [ ! -f "$binary_path" ] && [ -f "$extract_dir/$archive_dir/$PROGRAM_NAME" ]; then binary_path="$extract_dir/$archive_dir/$PROGRAM_NAME" fi if [ ! -f "$binary_path" ]; then return 1 fi mkdir -p "$ATO_INSTALL_DIR" cp "$binary_path" "$ATO_INSTALL_DIR/$PROGRAM_NAME" chmod 0755 "$ATO_INSTALL_DIR/$PROGRAM_NAME" return 0 } install_via_cargo() { need_cmd cargo mkdir -p "$ATO_INSTALL_DIR" cargo_root="$TMP_DIR/cargo-root" mkdir -p "$cargo_root" info "==> Falling back to cargo install (ato-cli)" set -- \ install \ --git "https://github.com/${ATO_RELEASE_REPO}.git" \ ato-cli \ --bin ato \ --locked \ --force \ --root "$cargo_root" if [ -n "$RESOLVED_RELEASE_TAG" ]; then set -- "$@" --tag "$RESOLVED_RELEASE_TAG" elif [ "$ATO_RELEASE_VERSION" != "latest" ]; then set -- "$@" --tag "$(normalize_release_tag "$ATO_RELEASE_VERSION")" fi cargo "$@" [ -f "$cargo_root/bin/ato" ] || fail "cargo install finished, but ato binary was not found." cp "$cargo_root/bin/ato" "$ATO_INSTALL_DIR/$PROGRAM_NAME" chmod 0755 "$ATO_INSTALL_DIR/$PROGRAM_NAME" } print_post_install() { info "==> Installed: $ATO_INSTALL_DIR/$PROGRAM_NAME" case ":$PATH:" in *":$ATO_INSTALL_DIR:"*) info "==> Try: $PROGRAM_NAME --version" ;; *) info "==> Add this directory to PATH:" info " export PATH=\"$ATO_INSTALL_DIR:\$PATH\"" info "==> Then run: $PROGRAM_NAME --version" ;; esac } need_cmd uname need_cmd mktemp TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t ato-install)" trap 'rm -rf "$TMP_DIR"' EXIT INT HUP TERM detect_target resolve_install_dir if install_from_archive; then info "==> Installed from GitHub release" else info "==> Binary download failed for ${TARGET_TRIPLE}" if [ "$ATO_SKIP_CARGO_FALLBACK" = "1" ]; then fail "Set ATO_SKIP_CARGO_FALLBACK=0 (or unset it) to allow cargo fallback." fi install_via_cargo info "==> Installed via cargo fallback" fi if [ "$ATO_SKIP_NACELLE_INSTALL" = "1" ]; then info "==> Skipping nacelle install (ATO_SKIP_NACELLE_INSTALL=1)" else if install_nacelle; then info "==> Installed nacelle from release bucket (${ATO_RELEASE_BASE_URL})" else warn "Continuing without nacelle. Set ATO_SKIP_NACELLE_INSTALL=1 to suppress this warning." fi fi configure_default_editor print_post_install