diff --git a/applications/icons/hyperfy.png b/applications/icons/hyperfy.png new file mode 100644 index 0000000000..b40133348b Binary files /dev/null and b/applications/icons/hyperfy.png differ diff --git a/bin/omarchy-install-hyperfy b/bin/omarchy-install-hyperfy new file mode 100644 index 0000000000..da81bae405 --- /dev/null +++ b/bin/omarchy-install-hyperfy @@ -0,0 +1,365 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Constants +APP_NAME="Hyperfy" +APP_SLUG="hyperfy" +DEFAULT_PORT="3000" +CONTAINER_NAME_DEFAULT="hyperfy-web" + +DATA_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/omarchy" +GAMES_ROOT="${DATA_ROOT}/games" +APP_ROOT="${GAMES_ROOT}/${APP_SLUG}" +CONFIG_DIR="${APP_ROOT}/config" +STACK_DIR="${APP_ROOT}/compose" +ENV_FILE="${STACK_DIR}/.env" +COMPOSE_FILE="${STACK_DIR}/docker-compose.yml" + +# Helpers +need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1"; exit 1; }; } +die() { echo "Error: $*" >&2; exit 1; } +header() { gum style --border thick --margin "1" --padding "1 2" --border-foreground 212 "⚡ $*"; } +info() { gum style --foreground 244 "$*"; } + +is_port_free() { + local p="$1" + if command -v ss >/dev/null 2>&1; then + ! ss -tulpn 2>/dev/null | grep -qE "[:.]${p}\b" + else + ! lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -qE "[:.]${p}\b" + fi +} + +wait_http_ready() { + local url="$1" timeout="${2:-30}" elapsed=0 + until curl -fsS "$url" >/dev/null 2>&1; do + sleep 1 + elapsed=$((elapsed+1)) + if [ "$elapsed" -ge "$timeout" ]; then return 1; fi + done +} + +ensure_docker_running() { + docker info >/dev/null 2>&1 || die "Docker daemon not running. Start it (e.g., 'sudo systemctl start docker') and retry." +} + +# Preflight +header "Install ${APP_NAME}" +need gum git docker curl +ensure_docker_running + +# Docker image selection +echo +info "Choose Docker image:" +echo "This determines which version of Hyperfy to run. 'dev' is the latest development version with newest features." +IMAGE_CHOICE="$(gum choose --header 'Select Hyperfy Docker image:' 'DEV' 'MAIN' 'CUSTOM')" + +case "$IMAGE_CHOICE" in + "DEV") + HYPERFY_IMAGE="ghcr.io/hyperfy-xyz/hyperfy:dev" + ;; + "MAIN") + HYPERFY_IMAGE="ghcr.io/hyperfy-xyz/hyperfy:main" + ;; + "CUSTOM") + echo "Custom Image Configuration:" + echo "Enter the full Docker image path (e.g., ghcr.io/hyperfy-xyz/hyperfy:latest)" + HYPERFY_IMAGE="$(gum input --prompt 'Docker image: ')" + ;; +esac + +info "Using Docker image: ${HYPERFY_IMAGE}" + +# Basic configuration +echo +echo "Port Configuration:" +echo "This is the port where Hyperfy will be accessible (e.g., http://localhost:3000)" +PORT_INPUT="$(gum input --prompt 'Port to expose (default 3000): ' --value "${DEFAULT_PORT}")" +HYPERFY_PORT="${PORT_INPUT:-$DEFAULT_PORT}" + +if ! is_port_free "$HYPERFY_PORT"; then + die "Port ${HYPERFY_PORT} is in use. Choose a free port and re-run." +fi + +echo +echo "Container Configuration:" +echo "This is the name of the Docker container (used for management)" +CONTAINER_NAME="$(gum input --prompt 'Container name: ' --value "${CONTAINER_NAME_DEFAULT}")" +[ -n "$CONTAINER_NAME" ] || CONTAINER_NAME="${CONTAINER_NAME_DEFAULT}" + +# Hyperfy configuration +echo +info "Configure Hyperfy Environment:" + +echo "World Configuration:" +echo "This is the name of the world folder where your Hyperfy world data will be stored" +WORLD_NAME="$(gum input --prompt 'World folder name: ' --value 'world')" + +echo +echo "Admin Configuration:" +echo "Set an admin code to restrict admin access, or leave empty to make everyone an admin" +ADMIN_CODE="$(gum input --prompt 'Admin code (leave empty for everyone admin): ')" + +echo +echo "Save Configuration:" +echo "How often the world saves automatically (in seconds). Set to 0 to disable auto-saving" +SAVE_INTERVAL="$(gum input --prompt 'Save interval (seconds, 0 to disable): ' --value '60')" + +echo +echo "Player Physics:" +echo "Whether players can physically collide with each other in the world" +PLAYER_COLLISION="$(gum choose --header 'Should players collide with other players?' 'true' 'false')" + +echo +echo "Upload Limits:" +echo "Maximum file size for model uploads (in MB). Larger files will be rejected" +MAX_UPLOAD_SIZE="$(gum input --prompt 'Max upload size for models (MB): ' --value '12')" + +echo +echo "Asset Storage:" +echo "How to store uploaded assets (models, textures, etc.). Local stores on disk, S3 uses cloud storage" +ASSETS_TYPE="$(gum choose --header 'Asset storage type:' 'local' 's3')" +if [[ "$ASSETS_TYPE" == "s3" ]]; then + echo "S3 Configuration:" + echo "S3 bucket URI where assets will be stored (e.g., s3://my-bucket/hyperfy-assets)" + ASSETS_S3_URI="$(gum input --prompt 'S3 URI (s3://bucket/path): ')" +else + ASSETS_S3_URI="" +fi + +echo +echo "Database Configuration:" +echo "Local uses SQLite (simple), PostgreSQL is for production or shared databases" +DB_TYPE="$(gum choose --header 'Database type:' 'local' 'postgres')" +if [[ "$DB_TYPE" == "postgres" ]]; then + echo "PostgreSQL Configuration:" + echo "Connection string for your PostgreSQL database" + DB_URI="$(gum input --prompt 'Postgres URI (postgres://user:pass@host:port/db): ')" + echo "Optional database schema (leave empty for default)" + DB_SCHEMA="$(gum input --prompt 'Database schema (optional): ')" +else + DB_URI="local" + DB_SCHEMA="" +fi + +echo +echo "System Settings:" +echo "Whether to clean up unused assets when starting (frees disk space but takes time)" +CLEAN_ON_START="$(gum choose --header 'Clean unused assets on startup?' 'true' 'false')" + +# Optional AI configuration +echo +info "AI Configuration (optional):" +echo "AI integration allows Hyperfy to use AI models for content generation and assistance" +CONFIGURE_AI="$(gum choose --header 'Configure AI integration?' 'Skip' 'Configure')" +if [[ "$CONFIGURE_AI" == "Configure" ]]; then + AI_PROVIDER="$(gum choose --header 'AI Provider:' 'anthropic' 'openai' 'xai' 'google')" + case "$AI_PROVIDER" in + "anthropic") + AI_MODEL="$(gum choose --header 'Anthropic Model:' 'claude-sonnet-4-20250514' 'claude-opus-4-1-20250805')" + AI_EFFORT="medium" + ;; + "openai") + AI_MODEL="$(gum choose --header 'OpenAI Model:' 'gpt-5' 'gpt-5-mini' 'gpt-5-nano')" + AI_EFFORT="$(gum choose --header 'AI Effort:' 'minimal' 'low' 'medium' 'high')" + ;; + "xai") + AI_MODEL="grok-4-0709" + AI_EFFORT="medium" + ;; + "google") + AI_MODEL="$(gum choose --header 'Google Model:' 'gemini-2.5-pro' 'gemini-2.5-flash')" + AI_EFFORT="medium" + ;; + esac + AI_API_KEY="$(gum input --prompt 'AI API Key: ' --password)" +else + AI_PROVIDER="anthropic" + AI_MODEL="claude-sonnet-4-20250514" + AI_EFFORT="medium" + AI_API_KEY="" +fi + +# Optional LiveKit configuration +echo +info "LiveKit Voice Chat (optional):" +echo "LiveKit enables real-time voice chat between players in your Hyperfy world" +CONFIGURE_LIVEKIT="$(gum choose --header 'Configure LiveKit voice chat?' 'Skip' 'Configure')" +if [[ "$CONFIGURE_LIVEKIT" == "Configure" ]]; then + LIVEKIT_WS_URL="$(gum input --prompt 'LiveKit WebSocket URL: ')" + LIVEKIT_API_KEY="$(gum input --prompt 'LiveKit API Key: ')" + LIVEKIT_API_SECRET="$(gum input --prompt 'LiveKit API Secret: ' --password)" +else + LIVEKIT_WS_URL="" + LIVEKIT_API_KEY="" + LIVEKIT_API_SECRET="" +fi + +gum confirm "Proceed installing ${APP_NAME} with Docker to: ${APP_ROOT} and expose http://localhost:${HYPERFY_PORT} ?" || exit 0 + +# Create configuration +mkdir -p "${APP_ROOT}" "${STACK_DIR}" "${CONFIG_DIR}" + +# Generate Docker Compose file +cat > "${COMPOSE_FILE}" < "${ENV_FILE}" < "${CONFIG_DIR}/.env" < in chat) +# If left blank, everyone is an admin! +ADMIN_CODE=${ADMIN_CODE} + +# How often the world saves (seconds) +# Can be set to 0 to disable saving +SAVE_INTERVAL=${SAVE_INTERVAL} + +# Whether players should collide with other players +PUBLIC_PLAYER_COLLISION=${PLAYER_COLLISION} + +# The maximum upload file size for models etc (mb) +PUBLIC_MAX_UPLOAD_SIZE=${MAX_UPLOAD_SIZE} + +# The public web socket url the client connects to +PUBLIC_WS_URL=ws://localhost:${HYPERFY_PORT}/ws + +# The public url used by clients to access api (eg upload assets) +PUBLIC_API_URL=http://localhost:${HYPERFY_PORT}/api + +# How assets are stored, fetched and uploaded (local or s3) +ASSETS=${ASSETS_TYPE} +ASSETS_BASE_URL=http://localhost:${HYPERFY_PORT}/assets +ASSETS_S3_URI=${ASSETS_S3_URI} + +# By default world data is stored in a local sqlite database in the world folder +# Optionally set this to a postgres uri to store remotely, eg 'postgres://username:password@host:port/database' +DB_URI=${DB_URI} +DB_SCHEMA=${DB_SCHEMA} + +# Whether the server should do a cleanup of unused blueprints and assets before launching +CLEAN=${CLEAN_ON_START} + +# LiveKit (voice chat) +LIVEKIT_WS_URL=${LIVEKIT_WS_URL} +LIVEKIT_API_KEY=${LIVEKIT_API_KEY} +LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET} + +## +# AI +# -- +# AI_PROVIDER: openai, anthropic, xai, google +# AI_MODEL: claude-opus-4-1-20250805, claude-sonnet-4-20250514, gpt-5, gpt-5-mini, gpt-5-nano, grok-4-0709, gemini-2.5-pro, gemini-2.5-flash +# AI_EFFORT: minimal, low, medium, high (OpenAI only) +# AI_API_KEY: The api key for the selected provider +## +AI_PROVIDER=${AI_PROVIDER} +AI_MODEL=${AI_MODEL} +AI_EFFORT=${AI_EFFORT} +AI_API_KEY=${AI_API_KEY} +EOF + +# Create webapp launcher +APP_DISPLAY_NAME="${APP_NAME}" +echo +info "Creating webapp launcher..." + +# Create Hyperfy icon +ICON_DIR="$HOME/.local/share/applications/icons" +mkdir -p "${ICON_DIR}" + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +HYPERFY_ICON="${SCRIPT_DIR}/hyperfy.png" + +if [ -f "${HYPERFY_ICON}" ]; then + info "Using provided Hyperfy icon" + cp -f "${HYPERFY_ICON}" "${ICON_DIR}/Hyperfy.png" +else + info "Hyperfy icon not found at ${HYPERFY_ICON}, creating default icon" + convert -size 64x64 xc:transparent -fill "#FF6B35" -draw "polygon 32,8 48,24 32,40 16,24" "${ICON_DIR}/Hyperfy.png" 2>/dev/null || { + echo "Creating fallback icon..." + touch "${ICON_DIR}/Hyperfy.png" + } +fi + +# Create desktop webapp entry +if command -v omarchy-webapp-install >/dev/null 2>&1; then + ICON_FILENAME="${APP_NAME}.png" + gum spin --title "Installing as webapp..." -- \ + omarchy-webapp-install "${APP_DISPLAY_NAME}" "http://localhost:${HYPERFY_PORT}" "${ICON_FILENAME}" "omarchy-launch-hyperfy" +else + echo "omarchy-webapp-install not found, creating basic desktop entry" + DESKTOP_FILE="$HOME/.local/share/applications/${APP_DISPLAY_NAME}.desktop" + cat >"$DESKTOP_FILE" </dev/null 2>&1; then break; fi + sleep 1 +done + +# Launch as webapp using omarchy-launch-webapp if available +if command -v omarchy-launch-webapp >/dev/null 2>&1; then + omarchy-launch-webapp "$URL" +else + xdg-open "$URL" +fi diff --git a/bin/omarchy-menu b/bin/omarchy-menu index 6b989ca901..2c7b999fb1 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -301,10 +301,17 @@ show_install_ai_menu() { } show_install_gaming_menu() { - case $(menu "Install" " Steam\n RetroArch [AUR]\n󰍳 Minecraft") in + case $(menu "Install" " Steam\n RetroArch [AUR]\n󰍳 Minecraft\n Hyperfy") in *Steam*) present_terminal omarchy-install-steam ;; *RetroArch*) aur_install_and_launch "RetroArch" "retroarch retroarch-assets libretro libretro-fbneo" "com.libretro.RetroArch.desktop" ;; *Minecraft*) aur_install_and_launch "Minecraft [AUR]" "minecraft-launcher" "minecraft-launcher" ;; + *Hyperfy*) + if [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/omarchy/games/hyperfy/compose/docker-compose.yml" ]; then + omarchy-launch-hyperfy + else + present_terminal omarchy-install-hyperfy + fi + ;; *) show_install_menu ;; esac } diff --git a/bin/omarchy-webapp-remove b/bin/omarchy-webapp-remove index ca4daefc96..2f9de66833 100755 --- a/bin/omarchy-webapp-remove +++ b/bin/omarchy-webapp-remove @@ -6,7 +6,7 @@ DESKTOP_DIR="$HOME/.local/share/applications/" if [ "$#" -eq 0 ]; then # Find all web apps while IFS= read -r -d '' file; do - if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then + if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\|omarchy-launch-hyperfy\).*' "$file"; then WEB_APPS+=("$(basename "${file%.desktop}")") fi done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0) @@ -35,6 +35,37 @@ if [[ ${#APP_NAMES[@]} -eq 0 ]]; then fi for APP_NAME in "${APP_NAMES[@]}"; do + # Check if this is Hyperfy and needs Docker cleanup + if [[ "$APP_NAME" == "Hyperfy" ]]; then + echo "Removing Hyperfy and cleaning up Docker containers..." + STACK_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/omarchy/games/hyperfy/compose" + COMPOSE_FILE="${STACK_DIR}/docker-compose.yml" + ENV_FILE="${STACK_DIR}/.env" + + if [ -f "$COMPOSE_FILE" ] && [ -f "$ENV_FILE" ]; then + echo "Stopping and removing Docker containers..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" down -v 2>/dev/null || true + echo "Removing Hyperfy data directory..." + rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/omarchy/games/hyperfy" + fi + + # Also remove any standalone Hyperfy containers + echo "Removing any remaining Hyperfy containers..." + docker ps -a --filter "name=hyperfy" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true + + # Remove Hyperfy Docker images + echo "Removing Hyperfy Docker images..." + docker images --filter "reference=*hyperfy*" --format "{{.Repository}}:{{.Tag}}" | xargs -r docker rmi -f 2>/dev/null || true + + # Also remove the specific Hyperfy image that gets downloaded + echo "Removing Hyperfy Docker image..." + docker rmi ghcr.io/hyperfy-xyz/hyperfy:dev 2>/dev/null || true + + # Remove any Hyperfy volumes + echo "Removing Hyperfy volumes..." + docker volume ls --filter "name=hyperfy" --format "{{.Name}}" | xargs -r docker volume rm 2>/dev/null || true + fi + rm -f "$DESKTOP_DIR/$APP_NAME.desktop" rm -f "$ICON_DIR/$APP_NAME.png" echo "Removed $APP_NAME"