#!/bin/sh # Moltis installer script # https://www.moltis.org/ # # Usage: # curl -fsSL https://www.moltis.org/install.sh | sh # # Or with options: # curl -fsSL https://www.moltis.org/install.sh | sh -s -- --no-homebrew # curl -fsSL https://www.moltis.org/install.sh | sh -s -- --method=binary # curl -fsSL https://www.moltis.org/install.sh | sh -s -- --version=0.1.3 set -e GITHUB_REPO="moltis-org/moltis" HOMEBREW_TAP="moltis-org/tap" BINARY_NAME="moltis" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" # Default options USE_HOMEBREW=true PREFERRED_METHOD="" VERSION="" # Colors (disabled if not a terminal) if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' fi info() { printf "${BLUE}==>${NC} ${BOLD}%s${NC}\n" "$1" } success() { printf "${GREEN}==>${NC} ${BOLD}%s${NC}\n" "$1" } warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1" >&2 } error() { printf "${RED}Error:${NC} %s\n" "$1" >&2 exit 1 } # Parse arguments while [ $# -gt 0 ]; do case "$1" in --no-homebrew) USE_HOMEBREW=false ;; --method=*) PREFERRED_METHOD="${1#*=}" ;; --version=*) VERSION="${1#*=}" ;; -h|--help) cat </dev/null 2>&1 } get_latest_version() { # Extract tag_name, stripping optional leading "v" prefix. if command_exists curl; then curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"tag_name": *"v?([^"]+)".*/\1/' elif command_exists wget; then wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"tag_name": *"v?([^"]+)".*/\1/' else error "Neither curl nor wget found. Please install one of them." fi } # Return the GitHub release tag for a given version. # Date-based versions (YYYYMMDD.NN) are bare tags; semver gets a "v" prefix. release_tag() { v="$1" case "$v" in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].*) echo "$v" ;; *) echo "v$v" ;; esac } download() { url="$1" dest="$2" if command_exists curl; then curl -fsSL "$url" -o "$dest" elif command_exists wget; then wget -q "$url" -O "$dest" else error "Neither curl nor wget found. Please install one of them." fi } verify_checksum() { file="$1" expected_sha256="$2" if command_exists sha256sum; then actual=$(sha256sum "$file" | cut -d' ' -f1) elif command_exists shasum; then actual=$(shasum -a 256 "$file" | cut -d' ' -f1) else warn "Cannot verify checksum (sha256sum/shasum not found)" return 0 fi if [ "$actual" != "$expected_sha256" ]; then error "Checksum verification failed!\nExpected: $expected_sha256\nActual: $actual" fi } ensure_install_dir() { if [ ! -d "$INSTALL_DIR" ]; then mkdir -p "$INSTALL_DIR" fi } install_shared_assets() { source_dir="$1" if [ ! -d "$source_dir" ]; then return 0 fi share_dir="$HOME/.moltis/share" mkdir -p "$share_dir" cp -R "$source_dir"/. "$share_dir"/ info "Installed shared assets to $share_dir" } add_to_path_instructions() { shell_name=$(basename "$SHELL") case "$shell_name" in bash) rc_file="$HOME/.bashrc" ;; zsh) rc_file="$HOME/.zshrc" ;; fish) rc_file="$HOME/.config/fish/config.fish" ;; *) rc_file="$HOME/.profile" ;; esac # Check if already in PATH case ":$PATH:" in *":$INSTALL_DIR:"*) return ;; esac printf "\n" warn "$INSTALL_DIR is not in your PATH." printf "Add it by running:\n\n" if [ "$shell_name" = "fish" ]; then printf " ${BOLD}fish_add_path %s${NC}\n\n" "$INSTALL_DIR" else printf " ${BOLD}echo 'export PATH=\"%s:\$PATH\"' >> %s${NC}\n\n" "$INSTALL_DIR" "$rc_file" fi printf "Then restart your shell or run:\n" printf " ${BOLD}source %s${NC}\n" "$rc_file" } # Installation methods install_homebrew() { info "Installing via Homebrew..." if ! command_exists brew; then error "Homebrew not found. Install it from https://brew.sh/" fi brew tap "$HOMEBREW_TAP" 2>/dev/null || true brew install moltis success "Moltis installed via Homebrew" } install_binary() { os="$1" arch="$2" version="$3" # Determine target triple case "$os" in macos) target="${arch}-apple-darwin" ;; linux) target="${arch}-unknown-linux-gnu" ;; *) error "Unsupported OS for binary installation: $os" ;; esac tag=$(release_tag "$version") tarball="${BINARY_NAME}-${version}-${target}.tar.gz" url="https://github.com/${GITHUB_REPO}/releases/download/${tag}/${tarball}" checksum_url="${url}.sha256" info "Downloading ${BINARY_NAME} v${version} for ${target}..." tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT download "$url" "$tmpdir/$tarball" || error "Failed to download $tarball. Check if a release exists for your platform." # Verify checksum if download "$checksum_url" "$tmpdir/checksum.sha256" 2>/dev/null; then expected_sha=$(cut -d' ' -f1 "$tmpdir/checksum.sha256") verify_checksum "$tmpdir/$tarball" "$expected_sha" info "Checksum verified" else warn "Could not download checksum file, skipping verification" fi # Extract and install tar -xzf "$tmpdir/$tarball" -C "$tmpdir" ensure_install_dir mv "$tmpdir/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" chmod +x "$INSTALL_DIR/$BINARY_NAME" if [ -d "$tmpdir/share/moltis" ]; then install_shared_assets "$tmpdir/share/moltis" elif [ -d "$tmpdir/share/web" ] && [ -d "$tmpdir/share/wasm" ]; then install_shared_assets "$tmpdir/share" fi success "Moltis installed to $INSTALL_DIR/$BINARY_NAME" add_to_path_instructions } install_deb() { arch="$1" version="$2" case "$arch" in x86_64) deb_arch="amd64" ;; aarch64) deb_arch="arm64" ;; *) error "Unsupported architecture for .deb: $arch" ;; esac # Package naming: moltis_VERSION_ARCH.deb (cargo-deb with --deb-version, no revision) tag=$(release_tag "$version") deb_file="moltis_${version}_${deb_arch}.deb" url="https://github.com/${GITHUB_REPO}/releases/download/${tag}/${deb_file}" info "Downloading ${deb_file}..." tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT download "$url" "$tmpdir/$deb_file" || error "Failed to download $deb_file" info "Installing .deb package (requires sudo)..." sudo dpkg -i "$tmpdir/$deb_file" || sudo apt-get install -f -y success "Moltis installed via .deb package" } install_rpm() { arch="$1" version="$2" case "$arch" in x86_64) rpm_arch="x86_64" ;; aarch64) rpm_arch="aarch64" ;; *) error "Unsupported architecture for .rpm: $arch" ;; esac # Package naming: moltis-VERSION-1.ARCH.rpm tag=$(release_tag "$version") rpm_file="moltis-${version}-1.${rpm_arch}.rpm" url="https://github.com/${GITHUB_REPO}/releases/download/${tag}/${rpm_file}" info "Downloading ${rpm_file}..." tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT download "$url" "$tmpdir/$rpm_file" || error "Failed to download $rpm_file" info "Installing .rpm package (requires sudo)..." if command_exists dnf; then sudo dnf install -y "$tmpdir/$rpm_file" elif command_exists yum; then sudo yum install -y "$tmpdir/$rpm_file" else sudo rpm -i "$tmpdir/$rpm_file" fi success "Moltis installed via .rpm package" } install_arch() { arch="$1" version="$2" tag=$(release_tag "$version") pkg_file="moltis-${version}-1-${arch}.pkg.tar.zst" url="https://github.com/${GITHUB_REPO}/releases/download/${tag}/${pkg_file}" info "Downloading ${pkg_file}..." tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT download "$url" "$tmpdir/$pkg_file" || error "Failed to download $pkg_file" info "Installing Arch package (requires sudo)..." sudo pacman -U --noconfirm "$tmpdir/$pkg_file" success "Moltis installed via Arch package" } install_appimage() { os="$1" arch="$2" version="$3" if [ "$os" != "linux" ]; then error "AppImage installation is only supported on Linux" fi case "$arch" in x86_64|aarch64) ;; *) error "Unsupported architecture for AppImage: $arch" ;; esac tag=$(release_tag "$version") appimage_file="moltis-${version}-${arch}.AppImage" url="https://github.com/${GITHUB_REPO}/releases/download/${tag}/${appimage_file}" checksum_url="${url}.sha256" info "Downloading ${appimage_file}..." tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT download "$url" "$tmpdir/$appimage_file" || error "Failed to download $appimage_file" if download "$checksum_url" "$tmpdir/checksum.sha256" 2>/dev/null; then expected_sha=$(cut -d' ' -f1 "$tmpdir/checksum.sha256") verify_checksum "$tmpdir/$appimage_file" "$expected_sha" info "Checksum verified" else warn "Could not download checksum file, skipping verification" fi ensure_install_dir mv "$tmpdir/$appimage_file" "$INSTALL_DIR/$BINARY_NAME" chmod +x "$INSTALL_DIR/$BINARY_NAME" success "Moltis AppImage installed to $INSTALL_DIR/$BINARY_NAME" add_to_path_instructions } install_snap() { info "Installing via Snap..." if ! command_exists snap; then error "Snap not found. Install it first: https://snapcraft.io/docs/installing-snapd" fi sudo snap install moltis success "Moltis installed via Snap" } detect_proxmox() { command_exists pveversion && [ -d /etc/pve ] } install_proxmox() { info "Proxmox VE detected — installing Moltis as an LXC container..." if ! detect_proxmox; then error "This does not appear to be a Proxmox VE host." fi PROXMOX_REPO_URL="https://raw.githubusercontent.com/moltis-org/ProxmoxVED/feat/moltis" PROXMOX_SCRIPT_URL="$PROXMOX_REPO_URL/ct/moltis.sh" tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT proxmox_script="$tmpdir/moltis-proxmox.sh" patched_script="$tmpdir/moltis-proxmox-patched.sh" download "$PROXMOX_SCRIPT_URL" "$proxmox_script" || error "Failed to download Proxmox helper" # Patch the fetched helper at launch time for Moltis-specific fixes that live # in remote community-scripts files: keep /usr/bin/update on the Moltis fork, # make the Docker prompt safe when lxc-attach has no interactive stdin, and # keep optional CA certificate display failures from removing the container. awk -v repo_url="$PROXMOX_REPO_URL" ' { if ($0 ~ /^source <\(curl -fsSL / && !curl_patched) { print "curl() {" print " case \"${*: -1}\" in" print " */install/moltis-install.sh)" print " command curl \"$@\" | sed '\''s|^[[:space:]]*read -r -p \".*Docker for sandbox support.*\" prompt$|if [[ -t 0 ]]; then &; else prompt=\"${MOLTIS_INSTALL_DOCKER:-no}\"; fi|'\''" print " ;;" print " *) command curl \"$@\" ;;" print " esac" print "}" curl_patched = 1 } if ($0 == "CA_CERT=$(pct exec \"$CTID\" -- cat /etc/moltis/certs/ca.pem 2>/dev/null)" && !ca_cert_patched) { sub(/\)$/, " || true)") ca_cert_patched = 1 } print if ($0 == "description" && !patched) { q = sprintf("%c", 39) print "pct exec \"$CTID\" -- bash -c \"cat > /usr/bin/update <<" q "MOLTIS_UPDATE_EOF" q print "bash -c \\\"\\$(curl -fsSL " repo_url "/ct/moltis.sh)\\\"" print "MOLTIS_UPDATE_EOF" print "chmod +x /usr/bin/update\" || true" patched = 1 } } ' "$proxmox_script" >"$patched_script" grep -q 'curl()' "$patched_script" || error "Failed to apply Docker prompt patch to Proxmox helper" grep -q 'MOLTIS_UPDATE_EOF' "$patched_script" || error "Failed to apply update URL patch to Proxmox helper" grep -q 'cat /etc/moltis/certs/ca.pem 2>/dev/null || true' "$patched_script" || warn "Could not apply optional CA certificate patch to Proxmox helper" info "Launching Proxmox VE helper script..." bash "$patched_script" } install_from_source() { warn "Building from source. This may take several minutes..." if ! command_exists cargo; then info "Rust not found. Installing via rustup..." if command_exists curl; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y else wget -qO- https://sh.rustup.rs | sh -s -- -y fi # shellcheck disable=SC1091 . "$HOME/.cargo/env" fi if ! command_exists git; then error "Git is required to build from source. Please install it first." fi version="$1" tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT info "Cloning repository..." tag=$(release_tag "$version") git clone --depth 1 --branch "$tag" "https://github.com/${GITHUB_REPO}.git" "$tmpdir/moltis" cd "$tmpdir/moltis" info "Building WASM tool components..." rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release info "Building release binary..." cargo build --release ensure_install_dir cp "target/release/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" chmod +x "$INSTALL_DIR/$BINARY_NAME" SHARE_STAGING="$tmpdir/moltis-share" mkdir -p "$SHARE_STAGING/web" "$SHARE_STAGING/wasm" cp -R "crates/web/src/assets/." "$SHARE_STAGING/web/" cp "target/wasm32-wasip2/release/moltis_wasm_calc.wasm" "$SHARE_STAGING/wasm/" cp "target/wasm32-wasip2/release/moltis_wasm_web_fetch.wasm" "$SHARE_STAGING/wasm/" cp "target/wasm32-wasip2/release/moltis_wasm_web_search.wasm" "$SHARE_STAGING/wasm/" install_shared_assets "$SHARE_STAGING" success "Moltis built and installed to $INSTALL_DIR/$BINARY_NAME" add_to_path_instructions } # Main installation logic main() { printf "\n" printf " ${BOLD}Moltis Installer${NC}\n" printf " Personal AI gateway - one binary, multiple LLM providers\n" printf "\n" OS=$(detect_os) ARCH=$(detect_arch) info "Detected: $OS ($ARCH)" if [ "$OS" = "windows" ]; then error "Windows is not supported by this installer. Please download the binary manually from:\nhttps://github.com/${GITHUB_REPO}/releases" fi if [ "$OS" = "unknown" ]; then error "Unsupported operating system: $(uname -s)" fi # Get version if [ -z "$VERSION" ]; then info "Fetching latest version..." VERSION=$(get_latest_version) if [ -z "$VERSION" ]; then error "Failed to determine latest version" fi fi info "Version: $VERSION" # Determine installation method if [ -n "$PREFERRED_METHOD" ]; then case "$PREFERRED_METHOD" in homebrew) install_homebrew ;; binary) install_binary "$OS" "$ARCH" "$VERSION" ;; deb) install_deb "$ARCH" "$VERSION" ;; rpm) install_rpm "$ARCH" "$VERSION" ;; arch) install_arch "$ARCH" "$VERSION" ;; appimage) install_appimage "$OS" "$ARCH" "$VERSION" ;; snap) install_snap ;; proxmox) install_proxmox return ;; source) install_from_source "$VERSION" ;; *) error "Unknown installation method: $PREFERRED_METHOD" ;; esac elif [ "$OS" = "linux" ] && detect_proxmox; then info "Proxmox VE host detected" printf " Install as an LXC container (recommended) or directly on this host?\n" printf " ${BOLD}1)${NC} LXC container (isolated, easy to manage)\n" printf " ${BOLD}2)${NC} Install directly on this host\n" printf "\n" printf " Choice [1]: " read -r choice /dev/null || choice="1" choice="${choice:-1}" if [ "$choice" = "1" ]; then install_proxmox return fi # Fall through to normal Linux install DISTRO=$(detect_linux_distro) case "$DISTRO" in ubuntu|debian|linuxmint|pop|elementary|zorin) install_deb "$ARCH" "$VERSION" ;; fedora|rhel|centos|rocky|alma|ol) install_rpm "$ARCH" "$VERSION" ;; *) install_binary "$OS" "$ARCH" "$VERSION" ;; esac elif [ "$OS" = "macos" ]; then # macOS: prefer Homebrew, fall back to binary if [ "$USE_HOMEBREW" = true ] && command_exists brew; then install_homebrew else install_binary "$OS" "$ARCH" "$VERSION" fi elif [ "$OS" = "linux" ]; then # Linux: detect distro and use appropriate package manager DISTRO=$(detect_linux_distro) case "$DISTRO" in ubuntu|debian|linuxmint|pop|elementary|zorin) if command_exists apt-get; then install_deb "$ARCH" "$VERSION" else install_binary "$OS" "$ARCH" "$VERSION" fi ;; fedora|rhel|centos|rocky|alma|ol) if command_exists dnf || command_exists yum; then install_rpm "$ARCH" "$VERSION" else install_binary "$OS" "$ARCH" "$VERSION" fi ;; arch|manjaro|endeavouros|garuda) if command_exists pacman; then install_arch "$ARCH" "$VERSION" else install_binary "$OS" "$ARCH" "$VERSION" fi ;; *) # Unknown distro: try binary, offer source as fallback if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "aarch64" ]; then install_binary "$OS" "$ARCH" "$VERSION" else warn "No pre-built binary available for $ARCH architecture." info "Falling back to building from source..." install_from_source "$VERSION" fi ;; esac fi # Verify installation if command_exists "$BINARY_NAME"; then installed_version=$("$BINARY_NAME" --version 2>/dev/null | head -1 || echo "unknown") printf "\n" success "Installation complete!" printf " ${BOLD}%s${NC}\n" "$installed_version" printf "\n" printf "Get started:\n" printf " ${BOLD}moltis${NC} # Start the gateway\n" printf " ${BOLD}moltis --help${NC} # Show help\n" printf "\n" printf "Documentation: ${BLUE}https://www.moltis.org/${NC}\n" elif [ -x "$INSTALL_DIR/$BINARY_NAME" ]; then printf "\n" success "Installation complete!" printf "\n" add_to_path_instructions fi } main