195 lines
5.9 KiB
Bash
Executable File
195 lines
5.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# llmfit installer
|
|
# Usage: curl -fsSL https://raw.githubusercontent.com/AlexsJones/llmfit/main/install.sh | sh
|
|
# curl -fsSL ... | sh -s -- --local # Install to ~/.local/bin (no sudo)
|
|
#
|
|
# Downloads the latest llmfit release from GitHub and installs
|
|
# the binary to /usr/local/bin (or ~/.local/bin with --local or if no sudo).
|
|
# Supports piped execution: sudo prompts read from /dev/tty when stdin is a pipe.
|
|
|
|
set -e
|
|
|
|
REPO="AlexsJones/llmfit"
|
|
BINARY="llmfit"
|
|
LOCAL_INSTALL=""
|
|
|
|
# --- helpers ---
|
|
|
|
info() { printf ' \033[1;34m>\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[1;33m>\033[0m %s\n' "$*"; }
|
|
err() { printf ' \033[1;31m!\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
need() {
|
|
command -v "$1" >/dev/null 2>&1 || err "Required tool '$1' not found. Please install it and try again."
|
|
}
|
|
|
|
# --- parse arguments ---
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--local|-l)
|
|
LOCAL_INSTALL="1"
|
|
;;
|
|
--help|-h)
|
|
echo "Usage: install.sh [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --local, -l Install to ~/.local/bin (no sudo required)"
|
|
echo " --help, -h Show this help message"
|
|
exit 0
|
|
;;
|
|
*)
|
|
warn "Unknown option: $1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
# --- detect platform ---
|
|
|
|
detect_platform() {
|
|
OS="$(uname -s)"
|
|
ARCH="$(uname -m)"
|
|
|
|
case "$OS" in
|
|
Linux) OS="unknown-linux-musl" ;;
|
|
Darwin) OS="apple-darwin" ;;
|
|
*) err "Unsupported OS: $OS" ;;
|
|
esac
|
|
|
|
case "$ARCH" in
|
|
x86_64|amd64) ARCH="x86_64" ;;
|
|
aarch64|arm64) ARCH="aarch64" ;;
|
|
*) err "Unsupported architecture: $ARCH" ;;
|
|
esac
|
|
|
|
PLATFORM="${ARCH}-${OS}"
|
|
}
|
|
|
|
# --- fetch latest release ---
|
|
|
|
fetch_latest_tag() {
|
|
need curl
|
|
need tar
|
|
|
|
# Use the releases redirect instead of the API to avoid GitHub's
|
|
# 60-request/hour rate limit on unauthenticated API calls (403).
|
|
TAG="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
|
| grep -i '^location:' \
|
|
| head -1 \
|
|
| sed 's|.*/tag/||' \
|
|
| tr -d '\r\n')"
|
|
|
|
[ -n "$TAG" ] || err "Could not determine latest release. Check https://github.com/${REPO}/releases"
|
|
}
|
|
|
|
# --- checksum verification ---
|
|
|
|
verify_checksum() {
|
|
CHECKSUM_FILE="${TMPDIR}/${ASSET}.sha256"
|
|
|
|
# Attempt to download the checksum file (-f exits non-zero on HTTP 4xx/5xx)
|
|
if ! curl -fsSL --max-time 10 "${URL}.sha256" -o "$CHECKSUM_FILE" 2>/dev/null; then
|
|
warn "No checksum file found for this release — skipping integrity check"
|
|
return
|
|
fi
|
|
|
|
info "Verifying checksum..."
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
(cd "$TMPDIR" && sha256sum -c "${ASSET}.sha256" --quiet) \
|
|
|| err "Checksum verification failed. The download may be corrupted or tampered with."
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
(cd "$TMPDIR" && shasum -a 256 -q -c "${ASSET}.sha256") \
|
|
|| err "Checksum verification failed. The download may be corrupted or tampered with."
|
|
else
|
|
warn "Neither sha256sum nor shasum available — skipping integrity check"
|
|
fi
|
|
}
|
|
|
|
# --- download and install ---
|
|
|
|
install() {
|
|
ASSET="${BINARY}-${TAG}-${PLATFORM}.tar.gz"
|
|
URL="https://github.com/${REPO}/releases/download/${TAG}/${ASSET}"
|
|
|
|
TMPDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
|
|
info "Downloading ${BINARY} ${TAG} for ${PLATFORM}..."
|
|
curl -fsSL "$URL" -o "${TMPDIR}/${ASSET}" \
|
|
|| err "Download failed. Asset '${ASSET}' may not exist for your platform.\n Check: https://github.com/${REPO}/releases/tag/${TAG}"
|
|
|
|
verify_checksum
|
|
|
|
info "Extracting..."
|
|
tar -xzf "${TMPDIR}/${ASSET}" -C "$TMPDIR"
|
|
|
|
# Find the binary in the extracted contents
|
|
BIN="$(find "$TMPDIR" -name "$BINARY" -type f | head -1)"
|
|
[ -n "$BIN" ] || err "Binary not found in archive. Release asset may have an unexpected layout."
|
|
chmod +x "$BIN"
|
|
|
|
# Determine install directory
|
|
if [ -n "$LOCAL_INSTALL" ]; then
|
|
# User explicitly requested local install
|
|
INSTALL_DIR="${HOME}/.local/bin"
|
|
mkdir -p "$INSTALL_DIR"
|
|
info "Installing to ${INSTALL_DIR} (--local mode)..."
|
|
elif [ -w /usr/local/bin ]; then
|
|
# /usr/local/bin is writable without sudo
|
|
INSTALL_DIR="/usr/local/bin"
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
# sudo is available — use /dev/tty for password prompt when stdin is a pipe
|
|
info "Installing to /usr/local/bin (requires sudo)..."
|
|
if [ -t 0 ]; then
|
|
SUDO_ASKPASS="" sudo mv "$BIN" "/usr/local/bin/${BINARY}"
|
|
elif [ -e /dev/tty ]; then
|
|
SUDO_ASKPASS="" sudo mv "$BIN" "/usr/local/bin/${BINARY}" </dev/tty
|
|
else
|
|
false
|
|
fi
|
|
if [ $? -eq 0 ]; then
|
|
info "Installed ${BINARY} to /usr/local/bin/${BINARY}"
|
|
return
|
|
else
|
|
warn "sudo failed, falling back to ~/.local/bin"
|
|
INSTALL_DIR="${HOME}/.local/bin"
|
|
mkdir -p "$INSTALL_DIR"
|
|
fi
|
|
else
|
|
# No write access and no interactive sudo, use local install
|
|
INSTALL_DIR="${HOME}/.local/bin"
|
|
mkdir -p "$INSTALL_DIR"
|
|
info "Installing to ${INSTALL_DIR} (no sudo available)..."
|
|
fi
|
|
|
|
mv "$BIN" "${INSTALL_DIR}/${BINARY}"
|
|
info "Installed ${BINARY} to ${INSTALL_DIR}/${BINARY}"
|
|
|
|
# Check if install dir is in PATH
|
|
case ":$PATH:" in
|
|
*":${INSTALL_DIR}:"*) ;;
|
|
*)
|
|
warn "Add ${INSTALL_DIR} to your PATH to use '${BINARY}' directly:"
|
|
echo ""
|
|
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
echo ""
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- main ---
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
info "llmfit installer"
|
|
detect_platform
|
|
fetch_latest_tag
|
|
install
|
|
info "Done. Run '${BINARY}' to get started."
|
|
}
|
|
|
|
main "$@"
|