#!/usr/bin/env bash
# Neo install script
# Usage:
# curl -fsSL https://raw.githubusercontent.com/owainlewis/neo/main/install.sh | bash
# bash install.sh [--bin-dir
] [--version ] [--no-api-key-check]
set -euo pipefail
# ── Colour helpers ──────────────────────────────────────────────────────────
if [ -t 1 ] && command -v tput &>/dev/null && tput colors &>/dev/null; then
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
RED=$(tput setaf 1); GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3); CYAN=$(tput setaf 6)
else
BOLD=""; DIM=""; RESET=""; RED=""; GREEN=""; YELLOW=""; CYAN=""
fi
info() { printf "%s %s%s\n" "${CYAN}→${RESET}" "$*" "${RESET}"; }
success() { printf "%s %s%s\n" "${GREEN}✓${RESET}" "$*" "${RESET}"; }
warn() { printf "%s %s%s\n" "${YELLOW}⚠${RESET}" "$*" "${RESET}"; }
die() { printf "%s %s%s\n" >&2 "${RED}✗${RESET}" "$*" "${RESET}"; exit 1; }
header() { printf "\n%s%s%s\n\n" "${BOLD}" "$*" "${RESET}"; }
# ── Defaults ────────────────────────────────────────────────────────────────
REPO="owainlewis/neo"
BIN_NAME="neo"
VERSION="" # empty = latest release tag
BIN_DIR="" # empty = auto-detect
CHECK_API_KEY=true
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--bin-dir) BIN_DIR="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--no-api-key-check) CHECK_API_KEY=false; shift ;;
-h|--help)
cat < Directory to install neo into (default: ~/.local/bin)
--version Release tag to install (default: latest)
--no-api-key-check Skip ANTHROPIC_API_KEY reminder
-h, --help Show this help
EOF
exit 0 ;;
*) die "Unknown option: $1" ;;
esac
done
# ── Platform detection ───────────────────────────────────────────────────────
detect_platform() {
local os arch
case "$(uname -s)" in
Linux) os="linux" ;;
Darwin) os="darwin" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
*) die "Unsupported OS: $(uname -s)" ;;
esac
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
armv7l) arch="arm" ;;
*) die "Unsupported architecture: $(uname -m)" ;;
esac
echo "${os}_${arch}"
}
# ── Resolve install directory ─────────────────────────────────────────────────
resolve_bin_dir() {
if [[ -n "$BIN_DIR" ]]; then
echo "$BIN_DIR"
return
fi
# Prefer a writable directory already on PATH
local candidates=("$HOME/.local/bin" "$HOME/bin" "/usr/local/bin")
for dir in "${candidates[@]}"; do
if [[ -d "$dir" && -w "$dir" ]]; then
echo "$dir"
return
fi
done
# Fall back to ~/.local/bin and create it
echo "$HOME/.local/bin"
}
# ── Fetch latest tag from GitHub ──────────────────────────────────────────────
latest_version() {
local tag
if command -v curl &>/dev/null; then
tag=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
elif command -v wget &>/dev/null; then
tag=$(wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
else
die "Neither curl nor wget is available. Install one and retry."
fi
[[ -n "$tag" ]] || die "Could not determine latest release. Use --version to pin one."
echo "$tag"
}
# ── Download helper ───────────────────────────────────────────────────────────
download() {
local url="$1" dest="$2"
if command -v curl &>/dev/null; then
curl -fsSL --progress-bar "$url" -o "$dest"
else
wget -q --show-progress "$url" -O "$dest"
fi
}
# ── Checksum helper ──────────────────────────────────────────────────────────
verify_checksum() {
local file="$1" asset="$2" version="$3" tmp_dir="$4"
local checksums="${tmp_dir}/checksums.txt"
local url="https://github.com/${REPO}/releases/download/${version}/checksums.txt"
if ! download "$url" "$checksums" 2>/dev/null; then
warn "checksums.txt not found; skipping checksum verification"
return 0
fi
local expected
expected=$(awk -v asset="$asset" '$2 == asset { print $1; exit }' "$checksums")
if [[ -z "$expected" ]]; then
warn "No checksum entry for ${asset}; skipping checksum verification"
return 0
fi
local actual
if command -v sha256sum &>/dev/null; then
actual=$(sha256sum "$file" | awk '{print $1}')
elif command -v shasum &>/dev/null; then
actual=$(shasum -a 256 "$file" | awk '{print $1}')
else
warn "No SHA-256 tool found; skipping checksum verification"
return 0
fi
[[ "$actual" == "$expected" ]] || die "Checksum mismatch for ${asset}"
success "Verified checksum"
}
# ── Install via pre-built release binary ─────────────────────────────────────
install_from_release() {
local version="$1" platform="$2" bin_dir="$3"
local ext=""
[[ "$platform" == windows* ]] && ext=".exe"
# Expected GoReleaser asset name pattern: neo__.tar.gz
local asset="neo_${platform}.tar.gz"
local url="https://github.com/${REPO}/releases/download/${version}/${asset}"
local tmp_dir
tmp_dir=$(mktemp -d)
trap "rm -rf '$tmp_dir'" EXIT
info "Downloading ${asset} (${version})…"
if ! download "$url" "${tmp_dir}/${asset}" 2>/dev/null; then
warn "Release asset not found at: $url"
return 1
fi
verify_checksum "${tmp_dir}/${asset}" "$asset" "$version" "$tmp_dir"
command -v tar &>/dev/null || die "tar is required to extract ${asset}"
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
local extracted="${tmp_dir}/${BIN_NAME}${ext}"
if [[ ! -f "$extracted" ]]; then
extracted=$(find "$tmp_dir" -type f -name "${BIN_NAME}${ext}" | head -n 1)
fi
[[ -n "$extracted" && -f "$extracted" ]] || die "Archive did not contain ${BIN_NAME}${ext}"
mkdir -p "$bin_dir"
install -m 0755 "$extracted" "${bin_dir}/${BIN_NAME}${ext}"
success "Installed ${BIN_NAME} → ${bin_dir}/${BIN_NAME}${ext}"
return 0
}
# ── Install via go install (fallback) ────────────────────────────────────────
install_from_go() {
local version="$1" bin_dir="$2"
command -v go &>/dev/null || die "go is not installed. Install Go 1.25+ from https://go.dev/dl/ and retry."
local go_version
go_version=$(go version | awk '{print $3}' | sed 's/go//')
info "Using go install (Go ${go_version})…"
local pkg="github.com/${REPO}/cmd/neo"
local ref="@latest"
[[ "$version" != "latest" ]] && ref="@${version}"
GOBIN="$bin_dir" go install \
-ldflags "-X main.Version=${version}" \
"${pkg}${ref}"
success "Installed ${BIN_NAME} → ${bin_dir}/${BIN_NAME}"
}
# ── PATH nudge ────────────────────────────────────────────────────────────────
check_path() {
local bin_dir="$1"
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$bin_dir"; then
echo ""
warn "${bin_dir} is not on your \$PATH."
echo " Add this line to your shell rc file:"
echo ""
printf " %sexport PATH=\"%s:\$PATH\"%s\n" "${BOLD}" "$bin_dir" "${RESET}"
echo ""
echo " Then reload your shell: source ~/.bashrc (or ~/.zshrc)"
fi
}
# ── API key reminder ──────────────────────────────────────────────────────────
check_api_key() {
if [[ "$CHECK_API_KEY" == false ]]; then return; fi
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo ""
warn "ANTHROPIC_API_KEY is not set."
echo " Get a key at: https://console.anthropic.com/"
echo " Then export it:"
echo ""
printf " %sexport ANTHROPIC_API_KEY=\"sk-ant-...\"%s\n" "${BOLD}" "${RESET}"
else
success "ANTHROPIC_API_KEY is set"
fi
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
header "Installing Neo"
local platform
platform=$(detect_platform)
info "Detected platform: ${platform}"
local bin_dir
bin_dir=$(resolve_bin_dir)
if [[ -z "$VERSION" ]]; then
info "Resolving latest release…"
VERSION=$(latest_version)
fi
info "Version: ${VERSION}"
# Try pre-built binary first, fall back to go install
if ! install_from_release "$VERSION" "$platform" "$bin_dir"; then
warn "Falling back to go install…"
install_from_go "$VERSION" "$bin_dir"
fi
check_path "$bin_dir"
check_api_key
echo ""
success "Done! Run ${BOLD}neo${RESET}${GREEN} to start."
echo ""
}
main "$@"