#!/usr/bin/env bash # Forge installer — https://forge.bhairavi.tech # Usage: curl -fsSL https://forge.bhairavi.tech/install.sh | bash # # Environment variables: # FORGE_VERSION — version to install (default: "latest") # DRY_RUN — set to 1 to print actions without executing them # FORGE_INSTALL_DIR — override install directory (default: ~/.local/bin) set -euo pipefail # --- Constants --- BASE_URL="https://forge.bhairavi.tech/releases" INSTALL_DIR="${FORGE_INSTALL_DIR:-$HOME/.local/bin}" VERSION="${FORGE_VERSION:-latest}" DRY_RUN="${DRY_RUN:-0}" # --- Input validation --- if ! printf '%s' "$VERSION" | grep -qE '^[a-zA-Z0-9._-]+$'; then printf "error: VERSION contains invalid characters: %s\n" "$VERSION" >&2 printf " Only [a-zA-Z0-9._-] are allowed.\n" >&2 exit 1 fi if printf '%s' "$INSTALL_DIR" | grep -qF '..'; then printf "error: INSTALL_DIR contains path traversal (..): %s\n" "$INSTALL_DIR" >&2 exit 1 fi # --- Colors (only when outputting to a terminal) --- if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET='' fi # --- Helpers --- info() { printf "${CYAN}info:${RESET} %s\n" "$1"; } warn() { printf "${YELLOW}warn:${RESET} %s\n" "$1"; } error() { printf "${RED}error:${RESET} %s\n" "$1" >&2; } fatal() { error "$1"; exit 1; } # --- Cleanup trap --- TMPDIR_CREATED="" cleanup() { if [ -n "$TMPDIR_CREATED" ] && [ -d "$TMPDIR_CREATED" ]; then rm -rf "$TMPDIR_CREATED" fi } trap cleanup EXIT INT TERM # --- Detect OS --- detect_os() { local uname_s uname_s="$(uname -s)" case "$uname_s" in Darwin) OS_TAG="apple-darwin" ;; Linux) OS_TAG="unknown-linux-gnu" ;; *) fatal "Unsupported operating system: $uname_s. Forge supports macOS and Linux." ;; esac } # --- Detect architecture --- detect_arch() { local uname_m uname_m="$(uname -m)" case "$uname_m" in arm64 | aarch64) ARCH_TAG="aarch64" ;; x86_64 | amd64) ARCH_TAG="x86_64" ;; *) fatal "Unsupported architecture: $uname_m. Forge supports aarch64 and x86_64." ;; esac } # --- Find a download tool --- detect_downloader() { if command -v curl >/dev/null 2>&1; then DOWNLOADER="curl" elif command -v wget >/dev/null 2>&1; then DOWNLOADER="wget" else fatal "Neither curl nor wget found. Please install one and try again." fi } # --- Download a URL to a file --- download() { local url="$1" local dest="$2" case "$DOWNLOADER" in curl) curl -fsSL --retry 3 --retry-delay 2 -o "$dest" "$url" ;; wget) wget -q --tries=3 -O "$dest" "$url" ;; *) return 1 ;; esac } # --- Ensure a directory is in the user's PATH --- ensure_in_path() { local dir="$1" case ":${PATH}:" in *":${dir}:"*) # Already in PATH return 0 ;; esac info "Adding $dir to PATH..." local shell_rc="" local current_shell current_shell="$(basename "${SHELL:-/bin/sh}")" case "$current_shell" in zsh) shell_rc="$HOME/.zshrc" ;; bash) if [ -f "$HOME/.bashrc" ]; then shell_rc="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then shell_rc="$HOME/.bash_profile" else shell_rc="$HOME/.bashrc" fi ;; *) # Fall back: try .bashrc, then .profile if [ -f "$HOME/.bashrc" ]; then shell_rc="$HOME/.bashrc" else shell_rc="$HOME/.profile" fi ;; esac if [ "$DRY_RUN" = "1" ]; then info "[dry-run] Would append PATH export to $shell_rc" return 0 fi local path_line="export PATH=\"$dir:\$PATH\"" # Avoid duplicate entries if [ -f "$shell_rc" ] && grep -qF "$path_line" "$shell_rc" 2>/dev/null; then info "$dir already referenced in $shell_rc" return 0 fi printf '\n# Added by Forge installer\n%s\n' "$path_line" >> "$shell_rc" info "Added to $shell_rc — restart your shell or run: source $shell_rc" } # --- Main --- main() { printf "\n${BOLD}Forge Installer${RESET}\n" printf "=================\n\n" detect_os detect_arch detect_downloader local archive_name="forge-${VERSION}-${ARCH_TAG}-${OS_TAG}.tar.gz" local download_url="${BASE_URL}/${archive_name}" info "OS: $OS_TAG" info "Architecture: $ARCH_TAG" info "Version: $VERSION" info "Download URL: $download_url" info "Install dir: $INSTALL_DIR" info "Downloader: $DOWNLOADER" if [ "$DRY_RUN" = "1" ]; then printf "\n${YELLOW}[dry-run mode]${RESET} The following actions would be performed:\n\n" info "[dry-run] Download $download_url" info "[dry-run] Extract forge-daemon and forge-next to temp directory" info "[dry-run] Create $INSTALL_DIR if it doesn't exist" info "[dry-run] Move forge-daemon and forge-next to $INSTALL_DIR" ensure_in_path "$INSTALL_DIR" info "[dry-run] Clean up temp directory" printf "\n${GREEN}Dry run complete.${RESET} Set DRY_RUN=0 (or unset it) to install for real.\n\n" return 0 fi # Create temp directory TMPDIR_CREATED="$(mktemp -d)" local archive_path="$TMPDIR_CREATED/$archive_name" # Download info "Downloading $archive_name..." if ! download "$download_url" "$archive_path"; then fatal "Failed to download $download_url. Check your network connection and try again." fi # Verify SHA256 checksum local checksum_url="${download_url}.sha256" local checksum_path="$TMPDIR_CREATED/${archive_name}.sha256" if download "$checksum_url" "$checksum_path" 2>/dev/null; then info "Verifying checksum..." local expected_hash expected_hash="$(awk '{print $1}' "$checksum_path")" local actual_hash if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$archive_path" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual_hash="$(shasum -a 256 "$archive_path" | awk '{print $1}')" else warn "No sha256sum or shasum found — skipping checksum verification" actual_hash="$expected_hash" fi if [ "$actual_hash" != "$expected_hash" ]; then fatal "Checksum mismatch! Expected $expected_hash but got $actual_hash. The download may be corrupted or tampered with." fi info "Checksum verified." else warn "Checksum file not available (${checksum_url}) — skipping verification" fi # Extract (restricted to expected files only, prevents path traversal) info "Extracting..." tar -xzf "$archive_path" -C "$TMPDIR_CREATED" --no-same-owner forge-daemon forge-next 2>/dev/null || \ tar -xzf "$archive_path" -C "$TMPDIR_CREATED" forge-daemon forge-next # Verify expected binaries exist local found_daemon=0 local found_next=0 if [ -f "$TMPDIR_CREATED/forge-daemon" ]; then found_daemon=1 fi if [ -f "$TMPDIR_CREATED/forge-next" ]; then found_next=1 fi if [ "$found_daemon" = "0" ] || [ "$found_next" = "0" ]; then fatal "Archive did not contain expected binaries (forge-daemon, forge-next). The download may be corrupted." fi # Install info "Installing to $INSTALL_DIR..." mkdir -p "$INSTALL_DIR" mv "$TMPDIR_CREATED/forge-daemon" "$INSTALL_DIR/forge-daemon" mv "$TMPDIR_CREATED/forge-next" "$INSTALL_DIR/forge-next" chmod +x "$INSTALL_DIR/forge-daemon" chmod +x "$INSTALL_DIR/forge-next" # Ensure PATH ensure_in_path "$INSTALL_DIR" # Success printf "\n${GREEN}${BOLD}Forge installed successfully!${RESET}\n\n" info "forge-daemon: $INSTALL_DIR/forge-daemon" info "forge-next: $INSTALL_DIR/forge-next" printf "\n${BOLD}Quick start:${RESET}\n" printf " forge-next health # Check daemon is running\n" printf " forge-next doctor # System diagnostics\n" printf " forge-next remember \\ # Store your first memory\n" printf " --type decision \\ \n" printf " --title \"My first note\" \\\n" printf " --content \"Hello, Forge!\"\n" printf "\n" if ! echo "$PATH" | tr ':' '\n' | grep -Fqx "$INSTALL_DIR"; then warn "Restart your shell or run: export PATH=\"$INSTALL_DIR:\$PATH\"" fi } main "$@"