#!/usr/bin/env bash
set -euo pipefail
BOLD="\033[1m"
DIM="\033[2m"
CYAN="\033[36m"
GREEN="\033[32m"
YELLOW="\033[33m"
RED="\033[31m"
RESET="\033[0m"
info() { echo -e "${CYAN}▸${RESET} $1"; }
ok() { echo -e "${GREEN}✓${RESET} $1"; }
warn() { echo -e "${YELLOW}!${RESET} $1"; }
fail() { echo -e "${RED}✗${RESET} $1"; exit 1; }
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
fail "$1 is required but not installed."
fi
}
maybe_sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
DOCKER_GROUP_CHANGED=false
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
*) fail "Unsupported OS: $OS. Use install.ps1 for Windows." ;;
esac
PKG=""
if [ "$PLATFORM" = "linux" ]; then
if command -v apt-get >/dev/null 2>&1; then
PKG="apt"
elif command -v dnf >/dev/null 2>&1; then
PKG="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG="yum"
elif command -v pacman >/dev/null 2>&1; then
PKG="pacman"
elif command -v zypper >/dev/null 2>&1; then
PKG="zypper"
fi
fi
echo ""
echo -e "${BOLD}LearnHouse Installer${RESET}"
echo -e "${DIM}Platform: $PLATFORM ($ARCH)${RESET}"
echo ""
if [ "$PLATFORM" = "linux" ] && ! command -v curl >/dev/null 2>&1; then
info "Installing curl..."
case "$PKG" in
apt) maybe_sudo apt-get update -qq && maybe_sudo apt-get install -y -qq curl ;;
dnf) maybe_sudo dnf install -y -q curl ;;
yum) maybe_sudo yum install -y -q curl ;;
pacman) maybe_sudo pacman -Sy --noconfirm curl ;;
zypper) maybe_sudo zypper install -y curl ;;
*) fail "curl is required. Install it manually and re-run." ;;
esac
fi
if command -v docker >/dev/null 2>&1; then
DOCKER_VERSION=$(docker --version 2>/dev/null | head -1)
ok "Docker already installed: $DOCKER_VERSION"
else
info "Docker not found. Installing..."
if [ "$PLATFORM" = "macos" ]; then
if ! command -v brew >/dev/null 2>&1; then
fail "Homebrew is required to install Docker on macOS. Install it first: https://brew.sh"
fi
info "Installing Docker via Homebrew..."
brew install --cask docker
echo ""
warn "Docker Desktop has been installed but needs to be started manually."
warn "Open Docker Desktop from Applications, then re-run this script."
echo ""
if ! docker info >/dev/null 2>&1; then
warn "Docker daemon is not running. Start Docker Desktop first."
echo -e "${DIM} Open Docker from Applications or run: open -a Docker${RESET}"
echo ""
echo -e "${DIM} After Docker is running, re-run:${RESET}"
echo -e "${DIM} npx learnhouse${RESET}"
exit 0
fi
elif [ "$PLATFORM" = "linux" ]; then
need_cmd curl
info "Installing Docker via official install script..."
curl -fsSL https://get.docker.com | maybe_sudo sh
if command -v systemctl >/dev/null 2>&1; then
maybe_sudo systemctl enable docker
maybe_sudo systemctl start docker
ok "Docker daemon enabled and started"
fi
if [ "$(id -u)" -ne 0 ]; then
maybe_sudo usermod -aG docker "$USER"
DOCKER_GROUP_CHANGED=true
ok "Added $USER to the docker group"
fi
ok "Docker installed"
fi
fi
if docker info >/dev/null 2>&1; then
ok "Docker daemon is running"
elif maybe_sudo docker info >/dev/null 2>&1; then
ok "Docker daemon is running (requires sudo until next login)"
else
warn "Docker is installed but the daemon is not running."
if [ "$PLATFORM" = "macos" ]; then
echo -e "${DIM} Start Docker Desktop from Applications${RESET}"
else
echo -e "${DIM} Start with: sudo systemctl start docker${RESET}"
fi
fi
if docker compose version >/dev/null 2>&1 || maybe_sudo docker compose version >/dev/null 2>&1; then
ok "Docker Compose v2 available"
else
if [ "$PLATFORM" = "linux" ]; then
info "Installing docker-compose-plugin..."
case "$PKG" in
apt) maybe_sudo apt-get update -qq && maybe_sudo apt-get install -y -qq docker-compose-plugin ;;
dnf) maybe_sudo dnf install -y docker-compose-plugin ;;
yum) maybe_sudo yum install -y docker-compose-plugin ;;
pacman) maybe_sudo pacman -Sy --noconfirm docker-compose ;;
zypper) maybe_sudo zypper install -y docker-compose ;;
*) warn "Could not auto-install Docker Compose. See: https://docs.docker.com/compose/install/linux/" ;;
esac
if docker compose version >/dev/null 2>&1 || maybe_sudo docker compose version >/dev/null 2>&1; then
ok "Docker Compose v2 installed"
else
warn "Docker Compose v2 could not be installed. See: https://docs.docker.com/compose/install/linux/"
fi
else
warn "Docker Compose v2 not found. It's included with Docker Desktop."
fi
fi
NEED_NODE=false
if command -v node >/dev/null 2>&1; then
NODE_VERSION=$(node --version 2>/dev/null)
NODE_MAJOR=$(echo "$NODE_VERSION" | sed 's/v//' | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 18 ]; then
ok "Node.js already installed: $NODE_VERSION"
else
warn "Node.js $NODE_VERSION is too old (need >=18). Installing newer version..."
NEED_NODE=true
fi
else
info "Node.js not found. Installing..."
NEED_NODE=true
fi
if [ "$NEED_NODE" = "true" ]; then
if [ "$PLATFORM" = "macos" ]; then
if command -v brew >/dev/null 2>&1; then
info "Installing Node.js 20 via Homebrew..."
brew install node@20
brew link --overwrite node@20 2>/dev/null || true
ok "Node.js installed"
else
fail "Homebrew is required to install Node.js on macOS. Install it first: https://brew.sh"
fi
elif [ "$PLATFORM" = "linux" ]; then
need_cmd curl
case "$ARCH" in
x86_64) NODE_ARCH="x64" ;;
aarch64) NODE_ARCH="arm64" ;;
armv7l) NODE_ARCH="armv7l" ;;
*) NODE_ARCH="$ARCH" ;;
esac
NODE_INSTALL_VERSION="20"
INSTALLED_VIA_PKG=false
if [ "$PKG" = "apt" ]; then
APT_NODE_VER=$(apt-cache show nodejs 2>/dev/null | grep -m1 '^Version:' | sed 's/[^0-9]*//' | cut -d. -f1 || echo "0")
if [ "$APT_NODE_VER" -ge 18 ] 2>/dev/null; then
info "Installing Node.js via apt (v${APT_NODE_VER})..."
maybe_sudo apt-get update -qq
maybe_sudo apt-get install -y -qq nodejs npm
INSTALLED_VIA_PKG=true
fi
elif [ "$PKG" = "dnf" ]; then
DNF_NODE_VER=$(dnf info nodejs 2>/dev/null | grep -m1 'Version' | awk '{print $3}' | cut -d. -f1 || echo "0")
if [ "$DNF_NODE_VER" -ge 18 ] 2>/dev/null; then
info "Installing Node.js via dnf (v${DNF_NODE_VER})..."
maybe_sudo dnf install -y nodejs npm
INSTALLED_VIA_PKG=true
fi
elif [ "$PKG" = "pacman" ]; then
info "Installing Node.js via pacman..."
maybe_sudo pacman -Sy --noconfirm nodejs npm
INSTALLED_VIA_PKG=true
elif [ "$PKG" = "zypper" ]; then
info "Installing Node.js via zypper..."
maybe_sudo zypper install -y nodejs npm
INSTALLED_VIA_PKG=true
fi
if [ "$INSTALLED_VIA_PKG" = "false" ]; then
info "Installing Node.js ${NODE_INSTALL_VERSION} via official binary..."
NODE_FULL_VER=$(curl -fsSL "https://nodejs.org/dist/latest-v${NODE_INSTALL_VERSION}.x/" | grep -oP 'node-v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -z "$NODE_FULL_VER" ]; then
fail "Could not determine latest Node.js ${NODE_INSTALL_VERSION} version. Install manually: https://nodejs.org"
fi
NODE_TARBALL="node-v${NODE_FULL_VER}-linux-${NODE_ARCH}.tar.xz"
NODE_URL="https://nodejs.org/dist/v${NODE_FULL_VER}/${NODE_TARBALL}"
info "Downloading ${NODE_TARBALL}..."
TMPDIR=$(mktemp -d)
curl -fsSL "$NODE_URL" -o "$TMPDIR/$NODE_TARBALL"
maybe_sudo tar -xJf "$TMPDIR/$NODE_TARBALL" -C /usr/local --strip-components=1
rm -rf "$TMPDIR"
fi
if command -v node >/dev/null 2>&1; then
ok "Node.js installed: $(node --version)"
else
fail "Node.js installation failed. Install manually: https://nodejs.org"
fi
fi
fi
if command -v npx >/dev/null 2>&1; then
ok "npx available"
else
if [ -x /usr/local/bin/npx ]; then
export PATH="/usr/local/bin:$PATH"
ok "npx available"
else
fail "npx not found. It should come with Node.js. Try reinstalling Node.js."
fi
fi
echo ""
echo -e "${GREEN}${BOLD}All dependencies are ready!${RESET}"
echo ""
echo -e "${CYAN}Launching LearnHouse setup...${RESET}"
echo ""
if [ ! -t 0 ]; then
LAUNCH_CMD="npx learnhouse@latest setup </dev/tty"
else
LAUNCH_CMD="npx learnhouse@latest setup"
fi
if [ "${DOCKER_GROUP_CHANGED:-false}" = "true" ]; then
exec sg docker -c "$LAUNCH_CMD"
else
eval "exec $LAUNCH_CMD"
fi