#!/usr/bin/env bash # ©AngelaMos | 2026 # install.sh # # One-shot installer for nadezhda. Takes a fresh machine to `nadezhda` runnable # by its bare name, with zero further steps, whether run from a clone or piped # from a domain via curl. Prefers a prebuilt release binary (no Go needed); # falls back to building from source (auto-installs the Go toolchain if absent). set -euo pipefail # ============================================================================ # CONFIG # ============================================================================ REPO_OWNER="CarterPerez-dev" REPO_NAME="nadezhda" BINARY="nadezhda" TAGLINE="Security news + CVE intelligence in your terminal. Keyless by default." REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}.git" INSTALL_DIR="${NADEZHDA_INSTALL_DIR:-$HOME/.local/bin}" DEFAULT_BRANCH="main" GO_MIN="1.21" PREBUILT=1 # ============================================================================ # Colors # ============================================================================ if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m' YELLOW=$'\033[33m'; CYAN=$'\033[36m'; RESET=$'\033[0m' else BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; CYAN=""; RESET="" fi info() { printf '%s\n' " ${CYAN}+${RESET} $*" >&2; } ok() { printf '%s\n' " ${GREEN}+${RESET} $*" >&2; } warn() { printf '%s\n' " ${YELLOW}!${RESET} $*" >&2; } die() { printf '%s\n' " ${RED}x $*${RESET}" >&2; exit 1; } header(){ printf '\n%s\n\n' "${BOLD}${CYAN}--- $* ---${RESET}" >&2; } have() { command -v "$1" >/dev/null 2>&1; } trap 'printf "%s\n" "${RED}x install failed${RESET}" >&2' ERR TMP_DIR="" cleanup() { [ -n "$TMP_DIR" ] && rm -rf "$TMP_DIR"; return 0; } trap cleanup EXIT banner() { printf '%s' "${CYAN}${BOLD}" >&2 cat >&2 <<'ART' ╭───────────────────────╮ │ n a d e z h d a │ ╰───────────────────────╯ ART printf '%s\n' "${RESET}" >&2 printf '%s\n' " ${DIM}${TAGLINE}${RESET}" >&2 } # ============================================================================ # Privilege + package-manager fan # ============================================================================ SUDO="" if [ "$(id -u)" -ne 0 ]; then if have sudo; then SUDO="sudo"; fi fi pkg_install() { if have apt-get; then $SUDO apt-get update -y || warn "apt update had errors; continuing" $SUDO apt-get install -y --no-install-recommends "$@" elif have dnf; then $SUDO dnf install -y "$@" elif have pacman; then $SUDO pacman -S --needed --noconfirm "$@" elif have zypper; then $SUDO zypper install -y "$@" elif have apk; then $SUDO apk add "$@" elif have brew; then brew install "$@" else die "no known package manager. Install manually: $*"; fi } download() { if have curl; then curl -fsSL "$1" -o "$2" || return 1 elif have wget; then wget -qO "$2" "$1" || return 1 else die "need curl or wget"; fi } # ============================================================================ # Args # ============================================================================ usage() { cat >&2 </dev/null || warn "pull failed; using existing clone" else info "cloning ${REPO_URL}" git clone --depth 1 --branch "$DEFAULT_BRANCH" --quiet "$REPO_URL" "$cache" \ || die "clone failed from ${REPO_URL}" fi printf '%s\n' "$cache" } # ============================================================================ # Toolchain (Go) + build from source # ============================================================================ install_go() { info "installing a current Go toolchain" local latest tgz latest="$(download "https://go.dev/VERSION?m=text" /dev/stdout 2>/dev/null | head -n1)" || latest="" case "$latest" in go*) ;; *) latest="go1.25.5" ;; esac tgz="${latest}.${OS}-${ARCH}.tar.gz" TMP_DIR="${TMP_DIR:-$(mktemp -d)}" download "https://go.dev/dl/${tgz}" "$TMP_DIR/go.tgz" || die "failed to download ${tgz} from go.dev/dl" rm -rf "$HOME/.local/go" mkdir -p "$HOME/.local" tar -C "$HOME/.local" -xzf "$TMP_DIR/go.tgz" || die "failed to extract Go" export PATH="$HOME/.local/go/bin:$PATH" export GOTOOLCHAIN=auto have go || die "Go toolchain install failed" ok "go $(go env GOVERSION 2>/dev/null | sed 's/^go//') at ~/.local/go" } need_toolchain() { local cur if have go; then cur="$(go env GOVERSION 2>/dev/null | sed 's/^go//')" if [ -n "$cur" ] && [ "$(printf '%s\n%s\n' "$GO_MIN" "$cur" | sort -V | head -n1)" = "$GO_MIN" ]; then export GOTOOLCHAIN=auto ok "go $cur (auto-toolchain fetches the go.mod-pinned version if newer)" return fi warn "go ${cur:-unknown} predates toolchain auto-download; installing a current Go" fi install_go } build_from_source() { info "building ${BINARY} (compiles the pure-Go SQLite driver; give it a minute)" mkdir -p "$INSTALL_DIR" GOBIN="$INSTALL_DIR" go install ./cmd/nadezhda || die "go install failed" ok "installed ${BINARY} -> ${INSTALL_DIR}/${BINARY}" } try_prebuilt() { [ "$PREBUILT" = "1" ] || return 1 local ver archive url ver="$(download "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" /dev/stdout 2>/dev/null \ | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" || true [ -n "$ver" ] || return 1 archive="${BINARY}_${ver#v}_${OS}_${ARCH}.tar.gz" url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${ver}/${archive}" TMP_DIR="$(mktemp -d)" download "$url" "$TMP_DIR/a.tgz" || { warn "no prebuilt for ${OS}/${ARCH}; will build from source"; return 1; } tar -xzf "$TMP_DIR/a.tgz" -C "$TMP_DIR" || return 1 mkdir -p "$INSTALL_DIR"; install -m 0755 "$TMP_DIR/$BINARY" "$INSTALL_DIR/$BINARY" ok "installed prebuilt ${ver} -> ${INSTALL_DIR}/${BINARY}" } # ============================================================================ # PATH wiring # ============================================================================ wire_path() { case ":$PATH:" in *":$INSTALL_DIR:"*) ok "$INSTALL_DIR already on PATH"; return ;; esac local shell rc="" shell="$(basename "${SHELL:-bash}")" case "$shell" in zsh) rc="$HOME/.zshrc" ;; fish) mkdir -p "$HOME/.config/fish/conf.d" echo "fish_add_path $INSTALL_DIR" > "$HOME/.config/fish/conf.d/${BINARY}.fish" ok "added to fish conf.d" ;; bash) rc="$HOME/.bashrc"; [ -f "$rc" ] || rc="$HOME/.bash_profile" ;; *) rc="$HOME/.profile" ;; esac if [ -n "$rc" ] && ! grep -q "$INSTALL_DIR" "$rc" 2>/dev/null; then printf '\nexport PATH="%s:$PATH"\n' "$INSTALL_DIR" >> "$rc" ok "added $INSTALL_DIR to PATH in $rc" fi export PATH="$INSTALL_DIR:$PATH" } # ============================================================================ # Main # ============================================================================ main() { banner have "$BINARY" && info "existing install at $(command -v "$BINARY") — updating" REPO="" if ! try_prebuilt; then header "Building from source" REPO="$(resolve_repo)"; cd "$REPO" need_toolchain build_from_source fi wire_path header "Verify" if have "$BINARY"; then ok "$BINARY -> $(command -v "$BINARY")" "$BINARY" version 2>/dev/null || true else warn "installed to $INSTALL_DIR but not yet on PATH — open a new shell" fi printf '\n%s\n\n' " ${GREEN}${BOLD}${BINARY} is ready.${RESET}" >&2 if have just && [ -n "$REPO" ] && [ -f "${REPO}/justfile" ]; then printf '%s\n' " ${DIM}dev commands:${RESET} just" >&2 fi cat >&2 <