#!/bin/bash
set -e

REPO="nebulity/latdx-cli"
BINARY_NAME="latdx"
INSTALL_DIR="${LATDX_INSTALL_DIR:-$HOME/.local/bin}"
DOCS_URL="https://docs.latdx.com"

# Whether a CI service is active, per the broad CI env convention. Any
# non-empty value counts except the explicit opt-outs (0/false/off/no),
# matching the CLI's isCiEnvironment so the installer and `latdx` agree.
is_ci() {
    case "$(printf '%s' "${CI:-}" | tr '[:upper:]' '[:lower:]')" in
        "" | 0 | false | off | no) return 1 ;;
        *) return 0 ;;
    esac
}

# Style. Color is emitted when NO_COLOR is unset and either stdout is a
# terminal or the install runs under CI, whose log viewers render ANSI on
# the non-TTY `curl | bash` pipe. A plain redirect to a file outside CI
# stays escape-free. The mint accent (#0DF293) is the brand's single accent.
if [ -z "${NO_COLOR+x}" ] && { [ -t 1 ] || is_ci; }; then
    MINT=$'\033[38;2;13;242;147m'
    AMBER=$'\033[38;2;227;179;65m'
    RED=$'\033[38;2;240;98;106m'
    BOLD=$'\033[1m'
    DIM=$'\033[2m'
    RESET=$'\033[0m'
else
    MINT=''; AMBER=''; RED=''; BOLD=''; DIM=''; RESET=''
fi
CHECK="${MINT}✓${RESET}"
WARN="${AMBER}!${RESET}"
CROSS="${RED}✗${RESET}"

# A completed step row: green check, left-aligned label, value.
step() {
    printf "  %s %-10s %s\n" "$CHECK" "$1" "$2"
}

# A warning step row: amber "!", same columns as step.
warn_step() {
    printf "  %s %-10s %s\n" "$WARN" "$1" "$2"
}

# A "get started" command row: mint command, dim description.
cmd_row() {
    printf "    %s%-28s%s%s%s%s\n" "$MINT" "$1" "$RESET" "$DIM" "$2" "$RESET"
}

# Draw a mint-bordered box sized to its longest line. First line is bold,
# the rest are dim. Pass plain text (no color codes) so the width math holds.
box() {
    local max=0 i=0 rule="" l first=1
    for l in "$@"; do [ ${#l} -gt "$max" ] && max=${#l}; done
    while [ "$i" -lt $((max + 4)) ]; do rule="${rule}─"; i=$((i + 1)); done
    printf "  %s╭%s╮%s\n" "$MINT" "$rule" "$RESET"
    for l in "$@"; do
        if [ "$first" = 1 ]; then
            printf "  %s│%s  %s%-*s%s  %s│%s\n" "$MINT" "$RESET" "$BOLD" "$max" "$l" "$RESET" "$MINT" "$RESET"
            first=0
        else
            printf "  %s│%s  %s%-*s%s  %s│%s\n" "$MINT" "$RESET" "$DIM" "$max" "$l" "$RESET" "$MINT" "$RESET"
        fi
    done
    printf "  %s╰%s╯%s\n" "$MINT" "$rule" "$RESET"
}

# Output mode. The CLI self-update (`latdx upgrade`) exports
# LATDX_INSTALL_MODE=upgrade so the header reads "upgrade" and the first-run
# onboarding footer is omitted (the user already has a working install); the
# default curl-pipe install keeps the full onboarding tail.
INSTALL_MODE="${LATDX_INSTALL_MODE:-install}"
if [ "$INSTALL_MODE" = "upgrade" ]; then
    ACTION_LABEL="upgrade"
else
    ACTION_LABEL="installer"
fi

LATDX_BASE_URL="${LATDX_BASE_URL:-}"

# Release channel for an unpinned install. "stable" resolves the newest
# non-prerelease; "latest" also accepts prereleases (nightly builds). A
# pinned VERSION argument bypasses the channel entirely.
CHANNEL="${LATDX_CHANNEL:-stable}"
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "latest" ]; then
    echo "Error: invalid LATDX_CHANNEL '${CHANNEL}' (expected: stable, latest)" >&2
    exit 1
fi

# Parse command line arguments
VERSION="$1"  # Optional: specific version (e.g., 0.15.2)

if [[ -n "$VERSION" ]] && [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "Usage: $0 [VERSION]" >&2
    echo "  VERSION: semantic version (e.g., 0.15.2). Defaults to the latest stable release." >&2
    exit 1
fi

# Check for curl or wget
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 "Error: curl or wget is required" >&2
    exit 1
fi

download() {
    local url="$1" output="$2"
    if [ "$DOWNLOADER" = "curl" ]; then
        if [ -n "$output" ]; then curl -fsSL -o "$output" "$url"
        else curl -fsSL "$url"; fi
    else
        if [ -n "$output" ]; then wget -q -O "$output" "$url"
        else wget -q -O - "$url"; fi
    fi
}

# GET a GitHub REST API URL to stdout. Anonymous calls are limited to 60 per
# hour per IP, a budget shared by every job behind a CI runner or NAT egress
# address; GITHUB_TOKEN, when set, authenticates the call instead. The token
# never appears in the process list: curl reads the header from a config on
# stdin, and wget from a private (mode 600) startup file that WGETRC points at
# for that one call. The startup file repeats the caller's own wgetrc first, so
# its settings (a proxy, say) still apply, and is removed when the call ends.
github_api() {
    local url="$1"
    if [ "$DOWNLOADER" = "curl" ]; then
        if [ -n "${GITHUB_TOKEN:-}" ]; then
            printf 'header = "Authorization: Bearer %s"\n' "$GITHUB_TOKEN" | curl -fsSL -K - "$url"
        else
            curl -fsSL "$url"
        fi
    elif [ -n "${GITHUB_TOKEN:-}" ]; then
        (
            user_wgetrc="${WGETRC:-$HOME/.wgetrc}"
            umask 077
            wgetrc="$(mktemp)"
            trap 'rm -f "$wgetrc"' EXIT
            trap 'exit 1' HUP INT TERM
            {
                if [ -f "$user_wgetrc" ] && [ -r "$user_wgetrc" ]; then cat "$user_wgetrc"; echo; fi
                printf 'header = Authorization: Bearer %s\n' "$GITHUB_TOKEN"
            } > "$wgetrc"
            WGETRC="$wgetrc" wget -q -O - "$url"
        )
    else
        wget -q -O - "$url"
    fi
}

# Seconds the releases/latest redirect probe may take before the install falls
# back to the GitHub API. Matches install.ps1.
REDIRECT_PROBE_TIMEOUT=15

# Print the URL that https://github.com/<repo>/releases/latest redirects to,
# which is the tag page of the newest non-prerelease. This is a plain web
# redirect, not the REST API, so the API rate limit does not apply to it.
latest_release_url() {
    local url="https://github.com/${REPO}/releases/latest"
    if [ "$DOWNLOADER" = "curl" ]; then
        curl -fsSLI --max-time "$REDIRECT_PROBE_TIMEOUT" -o /dev/null -w '%{url_effective}' "$url"
    else
        wget -S --spider --timeout="$REDIRECT_PROBE_TIMEOUT" --tries=1 "$url" 2>&1 \
            | awk 'tolower($1) == "location:" { loc = $2 } END { gsub(/\r/, "", loc); print loc }'
    fi
}

# A non-fatal probe used to detect whether a release asset exists, following the
# GitHub -> S3 redirect to its final hop. Returns 0 when the asset is present and
# sets ASSET_SIZE to its byte size (from the final Content-Length, empty if the
# server omits it); returns non-zero otherwise. Capturing the size here lets the
# download reuse it for the progress bar instead of spending a second HEAD.
ASSET_SIZE=""
asset_exists() {
    local url="$1" headers
    ASSET_SIZE=""
    if [ "$DOWNLOADER" = "curl" ]; then
        headers="$(curl -sfIL "$url" 2>/dev/null)" || return 1
    else
        headers="$(wget -S --spider "$url" 2>&1)" || return 1
    fi
    ASSET_SIZE="$(printf '%s\n' "$headers" \
        | awk 'BEGIN{IGNORECASE=1} /content-length:/{v=$2} END{gsub(/\r/,"",v);print v}')"
    return 0
}

# Width of the mint download bar, in cells.
BAR_WIDTH=24

# Bar glyphs mirror @latdx/core's renderProgressBar (▰ filled, ▱ empty) so the
# installer's download bar and `latdx test run`'s progress bar share one look.
BAR_FULL="▰"
BAR_EMPTY="▱"

# Redraw the in-place download bar for a whole-percent value: mint fill on a dim
# track, aligned to the same columns as the check rows so it collapses cleanly
# into the "download" step once complete.
draw_bar() {
    local pct="$1" filled i bar="" track=""
    filled=$(( pct * BAR_WIDTH / 100 ))
    i=0; while [ "$i" -lt "$filled" ]; do bar="${bar}${BAR_FULL}"; i=$((i + 1)); done
    i="$filled"; while [ "$i" -lt "$BAR_WIDTH" ]; do track="${track}${BAR_EMPTY}"; i=$((i + 1)); done
    printf "\r  %s↓%s %-10s %s%s%s%s%s%s %3d%%" \
        "$MINT" "$RESET" "download" "$MINT" "$bar" "$RESET" "$DIM" "$track" "$RESET" "$pct"
}

# Download the release asset behind an on-brand mint progress bar when stderr is
# a terminal (the common `curl | bash` case). The bar polls the output file's
# size against the asset's Content-Length (resolved earlier by asset_exists);
# when the size is unknown it shows a live byte counter instead. Non-interactive
# installs download silently.
download_asset() {
    local url="$1" output="$2" total="$3"
    if [ ! -t 2 ]; then
        download "$url" "$output"
        return
    fi

    # Create the file up front so the size poll never races curl opening it; a
    # failed `< "$output"` input redirection prints past `2>/dev/null`.
    : > "$output"
    download "$url" "$output" &
    local pid=$! got pct
    if [ -n "$total" ] && [ "$total" -gt 0 ] 2>/dev/null; then
        while kill -0 "$pid" 2>/dev/null; do
            got="$(wc -c < "$output" 2>/dev/null || echo 0)"
            pct=$(( got * 100 / total ))
            if [ "$pct" -gt 100 ]; then pct=100; fi
            draw_bar "$pct"
            sleep 0.1
        done
        draw_bar 100
    else
        while kill -0 "$pid" 2>/dev/null; do
            got="$(wc -c < "$output" 2>/dev/null || echo 0)"
            printf "\r  %s↓%s %-10s %s KB" "$MINT" "$RESET" "download" "$(( got / 1024 ))"
            sleep 0.1
        done
    fi

    local rc=0
    wait "$pid" || rc=$?
    printf "\r\033[K"
    return "$rc"
}

# Detect OS
case "$(uname -s)" in
    Darwin) os="darwin" ;;
    Linux)  os="linux" ;;
    MINGW*|MSYS*|CYGWIN*) os="win" ;;
    *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac

# Detect architecture
case "$(uname -m)" in
    x86_64|amd64) arch="x64" ;;
    arm64|aarch64) arch="arm64" ;;
    *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

# Rosetta 2 detection: prefer native arm64 on Apple Silicon
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
    if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
        arch="arm64"
    fi
fi

echo ""
printf "  %s%slatdx%s %s%s%s\n\n" "$MINT" "$BOLD" "$RESET" "$DIM" "$ACTION_LABEL" "$RESET"
step "platform" "${os}-${arch}"

# Resolve version, tracking whether the caller pinned it. The LATdx stable
# channel is GitHub's newest non-prerelease, the release that
# github.com/<repo>/releases/latest redirects to; the REST API's
# /releases/latest endpoint answers the same question and is only a fallback.
# Latest-channel nightly builds publish as prereleases, which both exclude, so
# the latest channel lists all releases through the API instead and picks the
# highest version including prereleases.
if [ -z "$VERSION" ]; then
    pinned=0
    if [ "$CHANNEL" = "latest" ]; then
        # LATdx prerelease tags publish as plain X.Y.Z releases marked
        # prerelease on GitHub; a hyphenated SemVer tail is accepted for
        # compatibility only. At an equal core version the plain release
        # outranks its prereleases (SemVer order), matching install.ps1 and
        # the CLI self-updater's compareReleaseVersions. Drafts are skipped:
        # a listing authenticated by a token with push access includes them.
        # Each release carries exactly one top-level tag_name and draft field,
        # so the Nth of each belong to the same release in any field order.
        versions=$(github_api "https://api.github.com/repos/${REPO}/releases?per_page=100" | awk '
            /^[ \t]*"tag_name": "/ { tags[++n_tags] = $0 }
            /^[ \t]*"draft": / { drafts[++n_drafts] = ($0 ~ /true/) }
            END {
                for (i = 1; i <= n_tags; i++) {
                    tag = tags[i]
                    if (!drafts[i] && sub(/.*"tag_name": "cli-v/, "", tag)) { sub(/".*/, "", tag); print tag }
                }
            }' | sort -V)
        VERSION=$(printf '%s\n' "$versions" | tail -1)
        core="${VERSION%%-*}"
        if [ "$VERSION" != "$core" ] && printf '%s\n' "$versions" | grep -qx "$core"; then
            VERSION="$core"
        fi
    else
        release_url="$(latest_release_url 2>/dev/null)" || release_url=""
        case "$release_url" in
            */releases/tag/cli-v*) VERSION="${release_url##*/releases/tag/cli-v}" ;;
        esac
        if [ -z "$VERSION" ]; then
            VERSION=$(github_api "https://api.github.com/repos/${REPO}/releases/latest" | \
                grep '"tag_name"' | head -1 | sed 's/.*"tag_name": "cli-v\([^"]*\)".*/\1/')
        fi
    fi
    if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
        echo "Error: could not determine the ${CHANNEL} version from github.com/${REPO}." >&2
        echo "  Pin one instead (... | bash -s 1.2.3), or set GITHUB_TOKEN if GitHub's anonymous" >&2
        echo "  API rate limit (60 requests per hour per IP) is exhausted." >&2
        exit 1
    fi
else
    pinned=1
fi

# Detect a prior install so the output can show an upgrade delta.
if [ "$os" = "win" ]; then
    target_bin="${INSTALL_DIR}/${BINARY_NAME}.exe"
else
    target_bin="${INSTALL_DIR}/${BINARY_NAME}"
fi
prior_version=""
if [ -x "$target_bin" ]; then
    # LATDX_AUTO_UPDATE=0 keeps this probe from triggering the CLI's self-update.
    prior_version="$(LATDX_AUTO_UPDATE=0 "$target_bin" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
fi

if [ -n "$prior_version" ] && [ "$prior_version" != "$VERSION" ]; then
    release_note="${DIM}(was v${prior_version})${RESET}"
elif [ "$pinned" = 1 ]; then
    release_note="${DIM}(pinned)${RESET}"
else
    release_note="${DIM}(${CHANNEL})${RESET}"
fi
step "release" "v${VERSION} ${release_note}"

RELEASE_TAG="cli-v${VERSION}"
BASE_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}"

# Create temp directory
TMPDIR_INSTALL="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_INSTALL"' EXIT

# Resolve asset name. Newer releases (>= the one that switched packaging)
# ship a bundle that contains the binary plus a sibling jars/ directory:
#
#     latdx-darwin-arm64.tar.gz   ->  latdx + jars/apex-ls.jar + jars/apex-ls-bridge.jar
#     latdx-win-x64.zip           ->  latdx.exe + jars/...
#
# Older releases shipped a bare binary (`latdx-darwin-arm64`, `latdx-win-x64.exe`)
# with no JARs. Probe the bundle first, fall back to the bare binary so this
# script keeps working against older release tags.
if [ "$os" = "win" ]; then
    bundle_asset="${BINARY_NAME}-${os}-${arch}.zip"
    legacy_asset="${BINARY_NAME}-${os}-${arch}.exe"
else
    bundle_asset="${BINARY_NAME}-${os}-${arch}.tar.gz"
    legacy_asset="${BINARY_NAME}-${os}-${arch}"
fi

if asset_exists "${BASE_URL}/${bundle_asset}"; then
    asset_name="$bundle_asset"
    asset_kind="bundle"
elif asset_exists "${BASE_URL}/${legacy_asset}"; then
    asset_name="$legacy_asset"
    asset_kind="legacy"
    echo "Note: release ${RELEASE_TAG} ships a legacy bare-binary asset; test caching will require the LATDX_APEX_LS_JAR / LATDX_APEX_LS_BRIDGE_JAR env vars." >&2
else
    echo "Error: neither ${bundle_asset} nor ${legacy_asset} found in release ${RELEASE_TAG}" >&2
    exit 1
fi

# Download asset and checksums
download_asset "${BASE_URL}/${asset_name}" "${TMPDIR_INSTALL}/${asset_name}" "$ASSET_SIZE"
download "${BASE_URL}/SHA256SUMS" "${TMPDIR_INSTALL}/SHA256SUMS"
step "download" "${asset_name}"

# Verify checksum
expected=$(grep " ${asset_name}\$" "${TMPDIR_INSTALL}/SHA256SUMS" | awk '{print $1}')
if [ -z "$expected" ]; then
    # Older SHA256SUMS used the bare-binary name without an extension; retry
    # with a looser match to stay compatible.
    expected=$(grep "${asset_name}" "${TMPDIR_INSTALL}/SHA256SUMS" | awk '{print $1}' | head -1)
fi
if [ -z "$expected" ]; then
    echo "Error: ${asset_name} not found in SHA256SUMS" >&2
    exit 1
fi

if [ "$os" = "darwin" ]; then
    actual=$(shasum -a 256 "${TMPDIR_INSTALL}/${asset_name}" | awk '{print $1}')
else
    actual=$(sha256sum "${TMPDIR_INSTALL}/${asset_name}" | awk '{print $1}')
fi

if [ "$actual" != "$expected" ]; then
    printf "  %s %-10s mismatch\n" "$CROSS" "checksum" >&2
    printf "\n  %sStopped: the download didn't match its hash. Nothing was installed.%s\n" "$RED" "$RESET" >&2
    printf "    %sexpected %s%s\n" "$DIM" "$expected" "$RESET" >&2
    printf "    %sactual   %s%s\n" "$DIM" "$actual" "$RESET" >&2
    printf "  %sRetry the install, or report it: %s/installation/%s\n" "$DIM" "$DOCS_URL" "$RESET" >&2
    exit 1
fi

step "checksum" "verified"

# Optional: verify minisign signature
if command -v minisign >/dev/null 2>&1; then
    if download "${BASE_URL}/SHA256SUMS.minisig" "${TMPDIR_INSTALL}/SHA256SUMS.minisig" 2>/dev/null; then
        PUBKEY="RWRg/erd72b5rVMgbjzeb+02CkOVWtkN+kCduGAEUKES/2QkXYgaPZ0Q"
        if minisign -Vm "${TMPDIR_INSTALL}/SHA256SUMS" -P "$PUBKEY" -q 2>/dev/null; then
            step "signature" "verified"
        else
            echo "Warning: signature verification failed" >&2
        fi
    fi
fi

# Java preflight. Bundles carry the analysis engine that powers test caching,
# which needs a Java 11+ runtime on PATH. Warn early instead of letting the
# first cached test run fail later; never block the install over it.
java_ok=1
if [ "$asset_kind" = "bundle" ]; then
    if command -v java >/dev/null 2>&1; then
        java_line="$(java -version 2>&1 | head -1)"
        java_version="$(printf '%s' "$java_line" | sed -nE 's/.*version "([^"]+)".*/\1/p')"
        java_major="$(printf '%s' "$java_version" | sed -nE 's/^([0-9]+).*/\1/p')"
        # Java 8 and earlier report 1.x; map 1.8 -> 8 to compare against 11.
        if [ "$java_major" = "1" ]; then
            java_major="$(printf '%s' "$java_version" | sed -nE 's/^1\.([0-9]+).*/\1/p')"
        fi
        if [ -n "$java_major" ] && [ "$java_major" -ge 11 ] 2>/dev/null; then
            step "java" "${java_version:-detected}"
        else
            warn_step "java" "${java_version:-found}, but test caching needs 11+"
            java_ok=0
        fi
    else
        warn_step "java" "not found on PATH"
        java_ok=0
    fi
fi

# Install. For bundle assets we extract the archive so the binary lands next
# to its sibling jars/ directory; the CLI auto-discovers JARs via
# <execDir>/jars/. For legacy assets we copy the binary directly.
mkdir -p "$INSTALL_DIR"
if [ "$asset_kind" = "bundle" ]; then
    # Remove any prior jars/ from a previous install so stale JARs don't
    # mix with the new bundle's contents.
    rm -rf "${INSTALL_DIR}/jars"
    if [ "$os" = "win" ]; then
        if command -v unzip >/dev/null 2>&1; then
            unzip -oq "${TMPDIR_INSTALL}/${asset_name}" -d "$INSTALL_DIR"
        else
            echo "Error: unzip is required to extract ${asset_name}" >&2
            exit 1
        fi
        installed_bin="${INSTALL_DIR}/${BINARY_NAME}.exe"
    else
        tar -xzf "${TMPDIR_INSTALL}/${asset_name}" -C "$INSTALL_DIR"
        installed_bin="${INSTALL_DIR}/${BINARY_NAME}"
    fi
    chmod +x "$installed_bin"
else
    if [ "$os" = "win" ]; then
        installed_bin="${INSTALL_DIR}/${BINARY_NAME}.exe"
    else
        installed_bin="${INSTALL_DIR}/${BINARY_NAME}"
    fi
    cp "${TMPDIR_INSTALL}/${asset_name}" "$installed_bin"
    chmod +x "$installed_bin"
fi

# Self-check: prove the freshly installed binary actually runs. LATDX_AUTO_UPDATE=0
# keeps this probe from triggering a self-update right after install.
verified_version="$(LATDX_AUTO_UPDATE=0 "$installed_bin" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [ -n "$verified_version" ]; then
    step "verify" "${BINARY_NAME} --version → ${verified_version}"
else
    warn_step "verify" "installed, but ${BINARY_NAME} --version did not run"
fi

if [ -n "$LATDX_BASE_URL" ]; then
    if LATDX_AUTO_UPDATE=0 "$installed_bin" auth use-url "$LATDX_BASE_URL" >/dev/null 2>&1; then
        step "site" "$LATDX_BASE_URL"
    else
        warn_step "site" "could not persist $LATDX_BASE_URL"
    fi
fi

# Persist a non-default channel so `latdx upgrade` and the background
# auto-update keep tracking the same release stream this install came from.
if [ "$CHANNEL" = "latest" ]; then
    if LATDX_AUTO_UPDATE=0 "$installed_bin" config set channel latest >/dev/null 2>&1; then
        step "channel" "latest"
    else
        warn_step "channel" "could not persist latest channel"
    fi
fi

# Boxed closer. Abbreviate $HOME to ~ for the path line.
display_path="$installed_bin"
case "$display_path" in "$HOME"/*) display_path="~${display_path#"$HOME"}" ;; esac

echo ""
if [ -n "$prior_version" ] && [ "$prior_version" != "$VERSION" ]; then
    box "Updated to latdx v${VERSION}" "$display_path"
else
    box "latdx v${VERSION} installed" "$display_path"
fi

if [ "$java_ok" = 0 ]; then
    echo ""
    printf "  %s!%s %stest caching needs a Java 11+ runtime on PATH:%s\n" "$AMBER" "$RESET" "$DIM" "$RESET"
    printf "      %smacOS%s  brew install openjdk@17\n" "$DIM" "$RESET"
    printf "      %slinux%s  apt install default-jre\n" "$DIM" "$RESET"
fi

# Check if INSTALL_DIR is in PATH. Shown before the next steps because the
# latdx command below won't resolve until the install dir is on PATH.
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
    echo ""
    printf "  %sAdd %s to your PATH:%s\n" "$DIM" "$INSTALL_DIR" "$RESET"
    shell_name="$(basename "$SHELL")"
    case "$shell_name" in
        zsh)  rc="~/.zshrc" ;;
        bash) rc="~/.bashrc" ;;
        fish) printf "    fish_add_path %s\n" "$INSTALL_DIR"; rc="" ;;
        *)    rc="your shell config" ;;
    esac
    if [ -n "$rc" ]; then
        printf "    echo 'export PATH=\"%s:\$PATH\"' >> %s\n" "$INSTALL_DIR" "$rc"
        printf "    source %s\n" "$rc"
    fi
fi

# First-run onboarding tail. Skipped on upgrade, where the box already
# confirms the new version and the user is past first-run setup.
if [ "$INSTALL_MODE" != "upgrade" ]; then
    echo ""
    printf "  %sGet started%s\n" "$DIM" "$RESET"
    cmd_row "latdx auth login" "sign in (optional for a first run)"
    cmd_row "latdx test run -o my-org" "run + cache your Apex tests"
    cmd_row "latdx --help" "all commands"

    echo ""
    printf "  %sUse with an AI agent%s\n" "$DIM" "$RESET"
    printf "    %sclaude mcp add latdx -- latdx mcp serve%s\n" "$MINT" "$RESET"

    echo ""
    printf "  %sDocs · %s%s\n" "$DIM" "$DOCS_URL" "$RESET"
fi
