#!/bin/bash # NeuralInverse Community Edition — Installer # # Usage: curl -fsSL https://cdn.neuralinverse.io/ce/install.sh | bash # # Supports: macOS (arm64 / x64) | Linux (x64 / arm64 / armhf) # - Reads version from cdn.neuralinverse.io/ce/latest.json # - macOS: tries .dmg > .pkg > .zip, removes quarantine, full cache clear, 5-step verify # - Linux: tries .deb/.rpm > .AppImage > .tar.gz, registers URL scheme handler # - Retry logic (up to 2 retries on failure) set -e APP_NAME="NeuralInverse" APP_BIN="neuralinverse" CDN_BASE="https://cdn.neuralinverse.io/ce" SUPPORT_EMAIL="support@neuralinverse.io" TRACK_URL="https://ucaeexhc9h.execute-api.us-east-1.amazonaws.com/" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' echo -e "${BLUE}========================================${NC}" echo -e "${BLUE} NeuralInverse Community Edition${NC}" echo -e "${BLUE}========================================${NC}" echo "" # Fetch version from ce/latest.json echo -e "${BLUE}Fetching latest CE version...${NC}" VERSION=$(curl -fsSL "${CDN_BASE}/latest.json" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])" 2>/dev/null | tr -d '[:space:]') if [ -z "$VERSION" ]; then echo -e "${RED}x${NC} Could not fetch version from ${CDN_BASE}/latest.json" exit 1 fi echo -e "${GREEN}+${NC} Version: ${VERSION}" OS=$(uname -s) # Fire-and-forget download tracking (non-blocking, failure silently ignored) _track() { local platform="$1" curl -sf -X POST "$TRACK_URL" \ -H "Content-Type: application/json" \ -d "{\"version\":\"${VERSION}\",\"platform\":\"${platform}\"}" \ --max-time 4 &>/dev/null & } ############################################################################### # macOS ############################################################################### if [[ "$OS" == "Darwin" ]]; then INSTALL_DIR="/Applications/${APP_NAME}.app" DOWNLOAD_DIR="/tmp/neuralinverse-ce-install" # Detect architecture ARCH=$(uname -m) if [[ "$ARCH" == "arm64" ]]; then ARCH_SUFFIX="arm64" echo -e "${GREEN}+${NC} Detected: Apple Silicon (ARM64)" elif [[ "$ARCH" == "x86_64" ]]; then ARCH_SUFFIX="x64" echo -e "${GREEN}+${NC} Detected: Intel Mac (x64)" else echo -e "${RED}x${NC} Unsupported architecture: $ARCH" exit 1 fi _track "darwin-${ARCH_SUFFIX}" # Try formats in order: .dmg > .pkg > .zip PREFERRED_FORMATS=("dmg" "pkg" "zip") SELECTED_FORMAT="" DOWNLOAD_FILE="" DOWNLOAD_URL="" echo -e "${BLUE}Checking available formats...${NC}" for format in "${PREFERRED_FORMATS[@]}"; do test_file="${APP_NAME}-${VERSION}-darwin-${ARCH_SUFFIX}.${format}" test_url="${CDN_BASE}/${VERSION}/${test_file}" if curl -s -f -I "$test_url" > /dev/null 2>&1; then SELECTED_FORMAT="$format" DOWNLOAD_FILE="$test_file" DOWNLOAD_URL="$test_url" echo -e "${GREEN}+${NC} Found: .${format} format" break fi done if [ -z "$SELECTED_FORMAT" ]; then echo -e "${RED}x${NC} No installation packages found on CDN" echo "Expected: ${CDN_BASE}/${VERSION}/${APP_NAME}-${VERSION}-darwin-${ARCH_SUFFIX}.[dmg|pkg|zip]" exit 1 fi # Cleanup function cleanup() { rm -rf "$DOWNLOAD_DIR" 2>/dev/null || true } # Full cleanup of existing installation + caches + Launch Services check_existing_installation() { if [ -d "$INSTALL_DIR" ]; then echo -e "${YELLOW}!${NC} Existing installation found -- performing full clean reinstall..." pkill -f "$APP_NAME" 2>/dev/null || true sleep 1 sudo rm -rf "$INSTALL_DIR" rm -rf ~/Library/Application\ Support/"$APP_NAME" 2>/dev/null || true rm -rf ~/Library/Caches/"$APP_NAME" 2>/dev/null || true rm -rf ~/Library/Saved\ Application\ State/io.neuralinverse.* 2>/dev/null || true /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ -kill -r -domain local -domain system -domain user 2>/dev/null || true killall Dock 2>/dev/null || true echo -e "${GREEN}+${NC} Existing installation and caches removed" fi } # Download and validate download_app() { echo "" echo -e "${BLUE}[1/5] Downloading ${APP_NAME} (${SELECTED_FORMAT})...${NC}" mkdir -p "$DOWNLOAD_DIR" if curl -L --progress-bar "$DOWNLOAD_URL" -o "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}"; then echo -e "${GREEN}+${NC} Download complete" else echo -e "${RED}x${NC} Download failed" return 1 fi FILE_SIZE=$(stat -f%z "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" 2>/dev/null || echo "0") if [ "$FILE_SIZE" -lt $((1024 * 1024)) ]; then echo -e "${RED}x${NC} Downloaded file is too small (${FILE_SIZE} bytes)" return 1 fi FILE_SIZE_MB=$(echo "scale=1; $FILE_SIZE / 1048576" | bc 2>/dev/null || echo "$((FILE_SIZE / 1048576))") echo -e "${GREEN}+${NC} File size: ${FILE_SIZE_MB} MB" return 0 } # Install from DMG install_dmg() { echo "" echo -e "${BLUE}[2/5] Installing from DMG...${NC}" MOUNT_POINT=$(hdiutil attach "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" -nobrowse 2>&1 | grep "/Volumes/" | sed 's/.*\(\/Volumes\/.*\)/\1/') if [ -z "$MOUNT_POINT" ]; then echo -e "${RED}x${NC} Failed to mount DMG" return 1 fi echo "Copying to Applications folder..." if sudo cp -rp "${MOUNT_POINT}/${APP_NAME}.app" /Applications/; then echo -e "${GREEN}+${NC} Application copied" else echo -e "${RED}x${NC} Failed to copy application" hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null return 1 fi hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null return 0 } # Install from PKG install_pkg() { echo "" echo -e "${BLUE}[2/5] Installing from PKG...${NC}" echo "Running macOS installer..." if sudo installer -pkg "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" -target / 2>&1; then sleep 2 if [ -d "$INSTALL_DIR" ]; then echo -e "${GREEN}+${NC} PKG installation completed" return 0 else echo -e "${YELLOW}!${NC} PKG installer succeeded but app not found, extracting manually..." fi else echo -e "${YELLOW}!${NC} Standard installer failed, extracting manually..." fi EXTRACT_DIR="/private/tmp/ni-extract" sudo rm -rf "$EXTRACT_DIR" 2>/dev/null sudo mkdir -p "$EXTRACT_DIR" if pkgutil --expand "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" "$EXTRACT_DIR" 2>/dev/null; then PAYLOAD=$(find "$EXTRACT_DIR" -name "Payload" -type f | head -1) if [ -n "$PAYLOAD" ]; then PAYLOAD_DIR=$(dirname "$PAYLOAD") cd "$PAYLOAD_DIR" if tar -xzf Payload 2>/dev/null || cpio -i < Payload 2>/dev/null; then APP_BUNDLE=$(find "$EXTRACT_DIR" -name "${APP_NAME}.app" -type d | head -1) if [ -n "$APP_BUNDLE" ]; then sudo cp -rp "$APP_BUNDLE" /Applications/ echo -e "${GREEN}+${NC} Application extracted and installed" sudo rm -rf "$EXTRACT_DIR" 2>/dev/null return 0 fi fi fi fi sudo rm -rf "$EXTRACT_DIR" 2>/dev/null echo -e "${RED}x${NC} Failed to install from PKG" return 1 } # Install from ZIP install_zip() { echo "" echo -e "${BLUE}[2/5] Installing from ZIP...${NC}" EXTRACT_DIR="${DOWNLOAD_DIR}/zip-extract" mkdir -p "$EXTRACT_DIR" if ditto -x -k "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" "$EXTRACT_DIR" 2>/dev/null || \ unzip -q "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" -d "$EXTRACT_DIR"; then APP_BUNDLE=$(find "$EXTRACT_DIR" -name "${APP_NAME}.app" -type d -maxdepth 2 | head -1) if [ -n "$APP_BUNDLE" ]; then sudo cp -rp "$APP_BUNDLE" /Applications/ echo -e "${GREEN}+${NC} Application installed" return 0 else echo -e "${RED}x${NC} Could not find ${APP_NAME}.app in archive" return 1 fi else echo -e "${RED}x${NC} Failed to extract ZIP" return 1 fi } install_app() { case "$SELECTED_FORMAT" in dmg) install_dmg ;; pkg) install_pkg ;; zip) install_zip ;; *) echo -e "${RED}x${NC} Unknown format: $SELECTED_FORMAT"; return 1 ;; esac } # Remove quarantine attributes remove_quarantine() { echo "" echo -e "${BLUE}[3/5] Removing macOS quarantine attributes...${NC}" if [ ! -d "$INSTALL_DIR" ]; then echo -e "${YELLOW}!${NC} Install directory not found" return 1 fi if sudo xattr -rd com.apple.quarantine "$INSTALL_DIR" 2>/dev/null; then echo -e "${GREEN}+${NC} Quarantine attributes removed" else echo -e "${YELLOW}!${NC} Could not remove quarantine (may not be needed)" fi return 0 } # Verify installation verify_installation() { echo "" echo -e "${BLUE}[4/5] Verifying installation...${NC}" if [ ! -d "$INSTALL_DIR" ]; then echo -e "${RED}x${NC} Application directory not found" return 1 fi echo -e "${GREEN}+${NC} Application directory found" if [ ! -f "${INSTALL_DIR}/Contents/Info.plist" ]; then echo -e "${RED}x${NC} Corrupted app bundle (missing Info.plist)" return 1 fi APP_VERSION=$(defaults read "${INSTALL_DIR}/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "unknown") echo -e "${GREEN}+${NC} App version: $APP_VERSION" MAIN_BINARY="${INSTALL_DIR}/Contents/MacOS/${APP_NAME}" if [ ! -f "$MAIN_BINARY" ]; then MAIN_BINARY=$(find "${INSTALL_DIR}/Contents/MacOS/" -type f -perm +111 | head -1) fi if [ -n "$MAIN_BINARY" ] && [ -f "$MAIN_BINARY" ]; then echo -e "${GREEN}+${NC} Application binary found" if [ ! -x "$MAIN_BINARY" ]; then chmod +x "$MAIN_BINARY" fi else echo -e "${RED}x${NC} No executable binary found" return 1 fi echo -e "${GREEN}+${NC} Installation verified" return 0 } post_install() { echo "" echo -e "${BLUE}[5/5] Finalizing...${NC}" echo -e "${GREEN}+${NC} Ready to launch" return 0 } main() { check_existing_installation RETRY_COUNT=0 MAX_RETRIES=2 while [ $RETRY_COUNT -le $MAX_RETRIES ]; do if [ $RETRY_COUNT -gt 0 ]; then echo "" echo -e "${YELLOW}Retry attempt $RETRY_COUNT of $MAX_RETRIES...${NC}" sudo rm -rf "$INSTALL_DIR" 2>/dev/null cleanup fi if download_app && install_app && remove_quarantine && verify_installation && post_install; then cleanup echo "" echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} + NeuralInverse CE ${VERSION} installed${NC}" echo -e "${GREEN}========================================${NC}" echo "" echo "Launch from:" echo " - Applications folder" echo " - Spotlight: Cmd+Space -> ${APP_NAME}" echo " - Terminal: open -a ${APP_NAME}" echo "" exit 0 else RETRY_COUNT=$((RETRY_COUNT + 1)) fi done cleanup echo "" echo -e "${RED}========================================${NC}" echo -e "${RED} x Installation failed${NC}" echo -e "${RED}========================================${NC}" echo "" echo "Troubleshooting:" echo " 1. Check internet: curl -I $DOWNLOAD_URL" echo " 2. Manual install:" echo " curl -L \"$DOWNLOAD_URL\" -o ~/Downloads/${DOWNLOAD_FILE}" echo " sudo xattr -rd com.apple.quarantine ~/Downloads/${DOWNLOAD_FILE}" echo " sudo xattr -rd com.apple.quarantine /Applications/${APP_NAME}.app" echo "" echo "Contact: $SUPPORT_EMAIL" exit 1 } trap cleanup EXIT main fi ############################################################################### # Linux ############################################################################### if [[ "$OS" == "Linux" ]]; then DOWNLOAD_DIR="/tmp/neuralinverse-ce-install" # Detect architecture ARCH=$(uname -m) if [[ "$ARCH" == "x86_64" ]]; then ARCH_SUFFIX="x64" echo -e "${GREEN}+${NC} Detected: x86_64 / AMD64" elif [[ "$ARCH" == "aarch64" ]] || [[ "$ARCH" == "arm64" ]]; then ARCH_SUFFIX="arm64" echo -e "${GREEN}+${NC} Detected: ARM64" elif [[ "$ARCH" == "armv7l" ]] || [[ "$ARCH" == "armv6l" ]] || [[ "$ARCH" =~ ^arm ]]; then ARCH_SUFFIX="armhf" echo -e "${GREEN}+${NC} Detected: ARMhf (32-bit ARM)" else echo -e "${RED}x${NC} Unsupported architecture: $ARCH" exit 1 fi _track "linux-${ARCH_SUFFIX}" # Detect package manager PKG_FORMAT="" INSTALL_CMD="" if command -v apt-get &> /dev/null; then PKG_FORMAT="deb" INSTALL_CMD="apt-get" echo -e "${GREEN}+${NC} Detected: Debian/Ubuntu (apt)" elif command -v dnf &> /dev/null; then PKG_FORMAT="rpm" INSTALL_CMD="dnf" echo -e "${GREEN}+${NC} Detected: Fedora/RHEL (dnf)" elif command -v yum &> /dev/null; then PKG_FORMAT="rpm" INSTALL_CMD="yum" echo -e "${GREEN}+${NC} Detected: CentOS/RHEL (yum)" fi # Build format preference list FORMATS=() if [ -n "$PKG_FORMAT" ]; then FORMATS+=("$PKG_FORMAT") fi if [ "$ARCH_SUFFIX" = "x64" ]; then FORMATS+=("AppImage") fi FORMATS+=("tar.gz") # Find available format on CDN SELECTED_FORMAT="" DOWNLOAD_FILE="" DOWNLOAD_URL="" echo -e "${BLUE}Checking available formats...${NC}" for format in "${FORMATS[@]}"; do test_file="${APP_NAME}-${VERSION}-linux-${ARCH_SUFFIX}.${format}" test_url="${CDN_BASE}/${VERSION}/${test_file}" if curl -s -f -I "$test_url" > /dev/null 2>&1; then SELECTED_FORMAT="$format" DOWNLOAD_FILE="$test_file" DOWNLOAD_URL="$test_url" echo -e "${GREEN}+${NC} Found: .${format}" break fi done if [ -z "$SELECTED_FORMAT" ]; then echo -e "${RED}x${NC} No packages found on CDN for linux-${ARCH_SUFFIX}" exit 1 fi # Check for root (needed for deb/rpm installs) NEED_SUDO=false if [[ "$SELECTED_FORMAT" == "deb" ]] || [[ "$SELECTED_FORMAT" == "rpm" ]]; then NEED_SUDO=true if [[ $EUID -ne 0 ]]; then echo -e "${YELLOW}!${NC} Root privileges required for .${SELECTED_FORMAT} installation" echo "Re-running with sudo..." exec sudo bash "$0" "$@" fi fi # Cleanup cleanup() { rm -rf "$DOWNLOAD_DIR" 2>/dev/null || true } # Check existing check_existing() { if command -v $APP_BIN &> /dev/null; then EXISTING_VERSION=$($APP_BIN --version 2>/dev/null || echo "unknown") echo -e "${YELLOW}!${NC} Existing installation found: $EXISTING_VERSION -- removing and reinstalling..." fi } # Download download_package() { echo "" echo -e "${BLUE}[1/3] Downloading ${APP_NAME} (.${SELECTED_FORMAT})...${NC}" mkdir -p "$DOWNLOAD_DIR" DOWNLOADER="" if command -v curl &> /dev/null; then DOWNLOADER="curl" elif command -v wget &> /dev/null; then DOWNLOADER="wget" else echo -e "${RED}x${NC} Neither curl nor wget found" return 1 fi if [ "$DOWNLOADER" = "curl" ]; then curl -L --progress-bar "$DOWNLOAD_URL" -o "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" else wget --progress=bar:force "$DOWNLOAD_URL" -O "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" fi FILE_SIZE=$(stat -c%s "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" 2>/dev/null || stat -f%z "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" 2>/dev/null || echo "0") if [ "$FILE_SIZE" -lt 1048576 ]; then echo -e "${RED}x${NC} Downloaded file too small (${FILE_SIZE} bytes)" return 1 fi echo -e "${GREEN}+${NC} Download complete ($((FILE_SIZE / 1048576)) MB)" return 0 } # Install install_package() { echo "" echo -e "${BLUE}[2/3] Installing...${NC}" case $SELECTED_FORMAT in deb) if dpkg -i "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}"; then echo -e "${GREEN}+${NC} Package installed" else echo -e "${YELLOW}Fixing dependencies...${NC}" apt-get install -f -y fi ;; rpm) $INSTALL_CMD install -y "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" echo -e "${GREEN}+${NC} Package installed" ;; AppImage) INSTALL_PATH="/usr/local/bin/${APP_BIN}" if [ "$EUID" -ne 0 ]; then INSTALL_PATH="$HOME/.local/bin/${APP_BIN}" mkdir -p "$HOME/.local/bin" fi cp "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" "$INSTALL_PATH" chmod +x "$INSTALL_PATH" echo -e "${GREEN}+${NC} AppImage installed to $INSTALL_PATH" DESKTOP_DIR="/usr/share/applications" if [ "$EUID" -ne 0 ]; then DESKTOP_DIR="$HOME/.local/share/applications" fi mkdir -p "$DESKTOP_DIR" cat > "$DESKTOP_DIR/${APP_BIN}.desktop" << DESKTOP [Desktop Entry] Name=NeuralInverse CE Exec=$INSTALL_PATH %F Icon=${APP_BIN} Type=Application Categories=Development;IDE; DESKTOP cat > "$DESKTOP_DIR/${APP_BIN}-url-handler.desktop" << DESKTOP [Desktop Entry] Name=NeuralInverse CE - URL Handler Exec=$INSTALL_PATH --open-url -- %U Icon=${APP_BIN} Type=Application NoDisplay=true Categories=Development;IDE; MimeType=x-scheme-handler/${APP_BIN}; DESKTOP update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true xdg-mime default "${APP_BIN}-url-handler.desktop" "x-scheme-handler/${APP_BIN}" 2>/dev/null || true kbuildsycoca5 --noincremental 2>/dev/null || true kbuildsycoca6 --noincremental 2>/dev/null || true echo -e "${GREEN}+${NC} URL scheme handler registered" ;; tar.gz) INSTALL_PATH="/opt/${APP_BIN}" if [ "$EUID" -ne 0 ]; then INSTALL_PATH="$HOME/.local/share/${APP_BIN}" fi mkdir -p "$INSTALL_PATH" tar -xzf "${DOWNLOAD_DIR}/${DOWNLOAD_FILE}" -C "$INSTALL_PATH" --strip-components=0 BIN_DIR="/usr/local/bin" if [ "$EUID" -ne 0 ]; then BIN_DIR="$HOME/.local/bin" mkdir -p "$BIN_DIR" fi ln -sf "${INSTALL_PATH}/bin/${APP_BIN}" "${BIN_DIR}/${APP_BIN}" 2>/dev/null || \ ln -sf "${INSTALL_PATH}/${APP_BIN}" "${BIN_DIR}/${APP_BIN}" 2>/dev/null || true echo -e "${GREEN}+${NC} Extracted to $INSTALL_PATH" ;; *) echo -e "${RED}x${NC} Unknown format: $SELECTED_FORMAT" return 1 ;; esac return 0 } # Verify verify_installation() { echo "" echo -e "${BLUE}[3/3] Verifying...${NC}" sleep 1 if command -v $APP_BIN &> /dev/null; then INSTALLED_VERSION=$($APP_BIN --version 2>/dev/null || echo "installed") echo -e "${GREEN}+${NC} Verified: $INSTALLED_VERSION" return 0 fi for path in "/usr/bin/${APP_BIN}" "/usr/local/bin/${APP_BIN}" "$HOME/.local/bin/${APP_BIN}"; do if [ -x "$path" ]; then echo -e "${GREEN}+${NC} Found at: $path" return 0 fi done echo -e "${YELLOW}!${NC} Could not verify via command, checking installation files..." if [ -d "/opt/${APP_BIN}" ] || [ -d "/usr/share/${APP_BIN}" ] || [ -d "$HOME/.local/share/${APP_BIN}" ]; then echo -e "${GREEN}+${NC} Installation directory found" return 0 fi echo -e "${RED}x${NC} Verification failed" return 1 } # Main main() { check_existing RETRY_COUNT=0 MAX_RETRIES=2 while [ $RETRY_COUNT -le $MAX_RETRIES ]; do if [ $RETRY_COUNT -gt 0 ]; then echo "" echo -e "${YELLOW}Retry $RETRY_COUNT of $MAX_RETRIES...${NC}" cleanup fi if download_package && install_package && verify_installation; then cleanup echo "" echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} + NeuralInverse CE ${VERSION} installed${NC}" echo -e "${GREEN}========================================${NC}" echo "" echo "Launch: $APP_BIN" echo "" exit 0 else RETRY_COUNT=$((RETRY_COUNT + 1)) fi done cleanup echo "" echo -e "${RED}========================================${NC}" echo -e "${RED} x Installation failed${NC}" echo -e "${RED}========================================${NC}" echo "" echo "Manual install: curl -L \"$DOWNLOAD_URL\" -o ~/${DOWNLOAD_FILE}" echo "Contact: $SUPPORT_EMAIL" exit 1 } trap cleanup EXIT main fi echo -e "${RED}x${NC} Unsupported OS: $OS" echo "Windows: irm https://cdn.neuralinverse.io/ce/install.ps1 | iex" exit 1