2026-05-22 15:04:16 +09:00
|
|
|
|
#!/usr/bin/env bash
|
2026-08-18 07:09:45 +00:00
|
|
|
|
# OpenDesign — One-Click Installer
|
2026-05-22 15:04:16 +09:00
|
|
|
|
# Docker Compose wrapper for Linux and macOS
|
|
|
|
|
|
#
|
|
|
|
|
|
# Usage:
|
|
|
|
|
|
# Clone this repo, then run:
|
|
|
|
|
|
# ./install.sh [--non-interactive] [--port 7456] [--image <ref>] [--skip-docker-install] [--no-systemd]
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Configuration
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
DEFAULT_PORT=7456
|
2026-06-17 12:25:18 +08:00
|
|
|
|
DEFAULT_IMAGE="ghcr.io/nexu-io/od:latest"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
DEFAULT_MEM_LIMIT="384m"
|
|
|
|
|
|
HEALTH_TIMEOUT=60
|
|
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
|
|
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.yml"
|
|
|
|
|
|
OVERRIDE_FILE="${DEPLOY_DIR}/docker-compose.override.yml"
|
2026-07-03 05:05:22 +02:00
|
|
|
|
LINUX_OVERRIDE_FILE="${DEPLOY_DIR}/docker-compose.linux.yml"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
|
|
|
|
|
|
# Build the -f argument list: always include the base file,
|
2026-07-03 05:05:22 +02:00
|
|
|
|
# add the Linux host-network override, and add docker-compose.override.yml if present
|
|
|
|
|
|
# (used by tests for isolation).
|
2026-05-22 15:04:16 +09:00
|
|
|
|
COMPOSE_FILES=(-f "$COMPOSE_FILE")
|
2026-07-03 05:05:22 +02:00
|
|
|
|
if [ "$(uname -s)" = "Linux" ] && [ -f "$LINUX_OVERRIDE_FILE" ]; then
|
|
|
|
|
|
COMPOSE_FILES+=(-f "$LINUX_OVERRIDE_FILE")
|
|
|
|
|
|
fi
|
2026-05-22 15:04:16 +09:00
|
|
|
|
if [ -f "$OVERRIDE_FILE" ]; then
|
|
|
|
|
|
COMPOSE_FILES+=(-f "$OVERRIDE_FILE")
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ ! -f "$COMPOSE_FILE" ]; then
|
|
|
|
|
|
echo "WARN: docker-compose.yml not found at ${COMPOSE_FILE}" >&2
|
|
|
|
|
|
echo " Clone the repository and run this script from deploy/scripts/." >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
ENV_FILE="${DEPLOY_DIR}/.env"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Colors & formatting
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET=""
|
|
|
|
|
|
if [ -t 1 ]; then
|
|
|
|
|
|
BOLD="\033[1m" DIM="\033[2m" RED="\033[31m" GREEN="\033[32m"
|
|
|
|
|
|
YELLOW="\033[33m" CYAN="\033[36m" RESET="\033[0m"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
step() { printf " ${DIM}▸${RESET} %s\n" "$1"; }
|
|
|
|
|
|
ok() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
|
|
|
|
|
warn() { printf " ${YELLOW}!${RESET} %s\n" "$1" >&2; }
|
|
|
|
|
|
error() { printf " ${RED}✗${RESET} %s\n" "$1" >&2; }
|
|
|
|
|
|
info() { printf " ${CYAN}›${RESET} %s\n" "$1"; }
|
|
|
|
|
|
|
|
|
|
|
|
prompt_text() {
|
|
|
|
|
|
_prompt="$1" _default="$2"
|
|
|
|
|
|
printf "%s [%s]: " "$_prompt" "$_default" >&2
|
|
|
|
|
|
read -r _val
|
|
|
|
|
|
PROMPT_RESULT="${_val:-$_default}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prompt_confirm() {
|
|
|
|
|
|
_question="$1" _default="$2"
|
|
|
|
|
|
if [ "$NON_INTERACTIVE" = "1" ]; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
_yn_default="y"
|
|
|
|
|
|
if [ "$_default" = "0" ]; then _yn_default="n"; fi
|
|
|
|
|
|
printf "%s [%s]: " "$_question" "$_yn_default" >&2
|
|
|
|
|
|
read -r _yn
|
|
|
|
|
|
case "$_yn" in
|
|
|
|
|
|
[Yy]*) return 0 ;;
|
|
|
|
|
|
[Nn]*) return 1 ;;
|
|
|
|
|
|
*) [ "$_default" = "1" ] && return 0; return 1 ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Argument parsing
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
NON_INTERACTIVE=0
|
|
|
|
|
|
OPT_PORT=""
|
|
|
|
|
|
OPT_IMAGE=""
|
|
|
|
|
|
OPT_SKIP_DOCKER=0
|
|
|
|
|
|
OPT_NO_SYSTEMD=0
|
|
|
|
|
|
|
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
--non-interactive) NON_INTERACTIVE=1 ;;
|
|
|
|
|
|
--port) shift; OPT_PORT="$1" ;;
|
|
|
|
|
|
--port=*) OPT_PORT="${1#--port=}" ;;
|
|
|
|
|
|
--image) shift; OPT_IMAGE="$1" ;;
|
|
|
|
|
|
--image=*) OPT_IMAGE="${1#--image=}" ;;
|
|
|
|
|
|
--skip-docker-install) OPT_SKIP_DOCKER=1 ;;
|
|
|
|
|
|
--no-systemd) OPT_NO_SYSTEMD=1 ;;
|
|
|
|
|
|
--help|-h)
|
|
|
|
|
|
echo "Usage: install.sh [options]"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "Options:"
|
|
|
|
|
|
echo " --non-interactive Use defaults for all prompts"
|
|
|
|
|
|
echo " --port <n> Host port (default: ${DEFAULT_PORT})"
|
|
|
|
|
|
echo " --image <ref> Docker image reference"
|
|
|
|
|
|
echo " --skip-docker-install Do not attempt to install Docker"
|
|
|
|
|
|
echo " --no-systemd Skip systemd unit creation"
|
|
|
|
|
|
echo " --help Show this help"
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
warn "Unknown argument: $1 (ignored)"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
shift
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Banner
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf "${BOLD} ┌──────────────────────────────────────┐${RESET}\n"
|
|
|
|
|
|
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
2026-08-18 07:09:45 +00:00
|
|
|
|
printf "${BOLD} │${RESET} ${CYAN}◈${RESET} ${BOLD}OpenDesign${RESET} ${BOLD}│${RESET}\n"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
printf "${BOLD} │${RESET} ${DIM}One-Click Installer${RESET} ${BOLD}│${RESET}\n"
|
|
|
|
|
|
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
|
|
|
|
|
printf "${BOLD} └──────────────────────────────────────┘${RESET}\n"
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 1. OS detection
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
OS="$(uname -s)"
|
|
|
|
|
|
ARCH="$(uname -m)"
|
|
|
|
|
|
|
|
|
|
|
|
case "$OS" in
|
|
|
|
|
|
Linux)
|
|
|
|
|
|
if [ -f /etc/os-release ]; then
|
|
|
|
|
|
# shellcheck disable=SC1091
|
|
|
|
|
|
. /etc/os-release
|
|
|
|
|
|
DISTRO="$ID"
|
|
|
|
|
|
DISTRO_VERSION="$VERSION_ID"
|
|
|
|
|
|
else
|
|
|
|
|
|
DISTRO="unknown"
|
|
|
|
|
|
DISTRO_VERSION=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
step "OS: Linux ${DISTRO} ${DISTRO_VERSION} (${ARCH})"
|
|
|
|
|
|
;;
|
|
|
|
|
|
Darwin)
|
|
|
|
|
|
step "OS: macOS $(sw_vers -productVersion) (${ARCH})"
|
|
|
|
|
|
DISTRO="macos"
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
error "Unsupported OS: ${OS}. This script supports Linux and macOS."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 2. Container runtime detection (Docker or Podman)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
CONTAINER_CMD=""
|
|
|
|
|
|
CONTAINER_RUNTIME=""
|
|
|
|
|
|
|
|
|
|
|
|
detect_container_runtime() {
|
|
|
|
|
|
if command -v docker >/dev/null 2>&1; then
|
|
|
|
|
|
CONTAINER_CMD="docker"
|
|
|
|
|
|
CONTAINER_RUNTIME="docker"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if command -v podman >/dev/null 2>&1; then
|
|
|
|
|
|
CONTAINER_CMD="podman"
|
|
|
|
|
|
CONTAINER_RUNTIME="podman"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detect_compose() {
|
|
|
|
|
|
if $CONTAINER_CMD compose version >/dev/null 2>&1; then
|
|
|
|
|
|
COMPOSE_CMD="${CONTAINER_CMD} compose"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "podman" ] && command -v podman-compose >/dev/null 2>&1; then
|
|
|
|
|
|
COMPOSE_CMD="podman-compose"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "docker" ] && command -v docker-compose >/dev/null 2>&1; then
|
|
|
|
|
|
COMPOSE_CMD="docker-compose"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check_runtime_running() {
|
|
|
|
|
|
$CONTAINER_CMD info >/dev/null 2>&1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ! detect_container_runtime; then
|
|
|
|
|
|
if [ "$OPT_SKIP_DOCKER" = "1" ]; then
|
|
|
|
|
|
error "No container runtime found (Docker or Podman) and --skip-docker-install was set."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
warn "No container runtime found (Docker or Podman)."
|
|
|
|
|
|
|
|
|
|
|
|
case "$DISTRO" in
|
|
|
|
|
|
ubuntu|debian)
|
|
|
|
|
|
install_cmd="sudo apt-get update && sudo apt-get install -y docker.io docker-compose-plugin"
|
|
|
|
|
|
step "Install Docker with: ${install_cmd}"
|
|
|
|
|
|
step "Or Podman: sudo apt-get install -y podman podman-compose"
|
|
|
|
|
|
;;
|
|
|
|
|
|
fedora)
|
|
|
|
|
|
install_cmd="sudo dnf install -y docker docker-compose-plugin"
|
|
|
|
|
|
step "Install Docker with: ${install_cmd}"
|
|
|
|
|
|
step "Or Podman: sudo dnf install -y podman podman-compose"
|
|
|
|
|
|
;;
|
|
|
|
|
|
centos|rhel|rocky|alma)
|
|
|
|
|
|
install_cmd="sudo yum install -y docker docker-compose-plugin"
|
|
|
|
|
|
step "Install Docker with: ${install_cmd}"
|
|
|
|
|
|
step "Or Podman: sudo yum install -y podman podman-compose"
|
|
|
|
|
|
;;
|
|
|
|
|
|
macos)
|
|
|
|
|
|
install_cmd="brew install --cask docker"
|
|
|
|
|
|
step "Install Docker with: ${install_cmd}"
|
|
|
|
|
|
step "Or Podman: brew install podman && podman machine init && podman machine start"
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
error "Cannot auto-detect install method for: ${DISTRO}"
|
|
|
|
|
|
step "Install Docker: https://docs.docker.com/get-docker/"
|
|
|
|
|
|
step "Or Podman: https://podman.io/getting-started/installation"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
|
|
|
|
|
if prompt_confirm "Install Docker now?" 0; then
|
|
|
|
|
|
step "Running: ${install_cmd}"
|
|
|
|
|
|
eval "$install_cmd"
|
|
|
|
|
|
if ! detect_container_runtime; then
|
|
|
|
|
|
error "Installation did not succeed. Install manually and re-run."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
error "A container runtime is required. Install Docker or Podman and re-run."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
error "A container runtime is required in non-interactive mode. Install Docker or Podman first."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if ! detect_compose; then
|
|
|
|
|
|
error "Container Compose is not available."
|
|
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
|
|
|
|
|
|
case "$OS" in
|
|
|
|
|
|
Darwin) step "Install with: brew install podman-compose" ;;
|
|
|
|
|
|
*) step "Install with: sudo apt-get install podman-compose (or equivalent for your distro)" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
else
|
|
|
|
|
|
step "Install the compose plugin: https://docs.docker.com/compose/install/"
|
|
|
|
|
|
fi
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-07-03 05:05:22 +02:00
|
|
|
|
# The Linux host-network override uses the !reset YAML tag (Docker Compose
|
|
|
|
|
|
# v2.17+). Reject legacy docker-compose v1 and podman-compose early so users
|
|
|
|
|
|
# get a clear message instead of a YAML parse error mid-install.
|
|
|
|
|
|
if [ "$OS" = "Linux" ] && [ -f "$LINUX_OVERRIDE_FILE" ]; then
|
|
|
|
|
|
if [ "$COMPOSE_CMD" != "docker compose" ]; then
|
|
|
|
|
|
error "The Linux host-network override requires 'docker compose' v2."
|
|
|
|
|
|
error "Found: ${COMPOSE_CMD}"
|
|
|
|
|
|
step "Install the Docker Compose plugin: https://docs.docker.com/compose/install/"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
_compose_ver="$($COMPOSE_CMD version --short 2>/dev/null || echo "0.0.0")"
|
|
|
|
|
|
_compose_major="$(echo "$_compose_ver" | cut -d. -f1)"
|
|
|
|
|
|
_compose_minor="$(echo "$_compose_ver" | cut -d. -f2)"
|
|
|
|
|
|
if [ "$_compose_major" -lt 2 ] || { [ "$_compose_major" -eq 2 ] && [ "$_compose_minor" -lt 17 ]; }; then
|
|
|
|
|
|
error "Docker Compose v2.17 or later required for the Linux override (found v${_compose_ver})."
|
|
|
|
|
|
step "Upgrade: https://docs.docker.com/compose/install/"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-05-22 15:04:16 +09:00
|
|
|
|
if ! check_runtime_running; then
|
|
|
|
|
|
warn "${CONTAINER_RUNTIME} is not running."
|
|
|
|
|
|
case "$DISTRO" in
|
|
|
|
|
|
ubuntu|debian|fedora|centos|rhel|rocky|alma)
|
|
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
|
|
|
|
|
|
step "Start with: podman machine init && podman machine start"
|
|
|
|
|
|
else
|
|
|
|
|
|
step "Start with: sudo systemctl start docker"
|
|
|
|
|
|
fi
|
|
|
|
|
|
;;
|
|
|
|
|
|
macos)
|
|
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
|
|
|
|
|
|
step "Start with: podman machine start"
|
|
|
|
|
|
else
|
|
|
|
|
|
step "Open Docker Desktop from your Applications folder."
|
|
|
|
|
|
fi
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
|
|
|
|
|
if prompt_confirm "Retry after starting ${CONTAINER_RUNTIME}?" 1; then
|
|
|
|
|
|
if ! check_runtime_running; then
|
|
|
|
|
|
error "${CONTAINER_RUNTIME} is still not running. Start it and re-run."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
RUNTIME_VERSION="$(${CONTAINER_CMD} --version 2>/dev/null || echo 'unknown')"
|
|
|
|
|
|
COMPOSE_VERSION="$(${COMPOSE_CMD} version 2>/dev/null || echo 'unknown')"
|
|
|
|
|
|
step "Runtime: ${CONTAINER_RUNTIME} ${RUNTIME_VERSION}"
|
|
|
|
|
|
step "Compose: ${COMPOSE_VERSION}"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-08-18 07:09:45 +00:00
|
|
|
|
# 2b. Check if OpenDesign is already running
|
2026-05-22 15:04:16 +09:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
if $CONTAINER_CMD ps --filter "name=open-design" --format '{{.Names}}' 2>/dev/null | grep -q 'open-design'; then
|
|
|
|
|
|
STATUS="$($CONTAINER_CMD inspect --format '{{.State.Status}}' open-design 2>/dev/null || echo 'unknown')"
|
|
|
|
|
|
IMAGE="$($CONTAINER_CMD inspect --format '{{.Config.Image}}' open-design 2>/dev/null || echo 'unknown')"
|
|
|
|
|
|
PORTS="$($CONTAINER_CMD port open-design 2>/dev/null || echo 'unknown')"
|
|
|
|
|
|
|
2026-08-18 07:09:45 +00:00
|
|
|
|
error "OpenDesign is already running."
|
2026-05-22 15:04:16 +09:00
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf " Container: open-design\n"
|
|
|
|
|
|
printf " Status: %s\n" "$STATUS"
|
|
|
|
|
|
printf " Image: %s\n" "$IMAGE"
|
|
|
|
|
|
printf " Ports: %s\n" "$PORTS"
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
step "To update: ${SCRIPT_DIR}/update.sh"
|
|
|
|
|
|
step "To remove: ${SCRIPT_DIR}/uninstall.sh"
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 3. Port conflict detection
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
PORT="${OPT_PORT:-$DEFAULT_PORT}"
|
|
|
|
|
|
|
|
|
|
|
|
check_port_in_use() {
|
|
|
|
|
|
if command -v ss >/dev/null 2>&1; then
|
|
|
|
|
|
ss -tlnp 2>/dev/null | grep -q ":${1} " && return 0
|
|
|
|
|
|
elif command -v lsof >/dev/null 2>&1; then
|
|
|
|
|
|
lsof -i :"$1" 2>/dev/null | grep -q LISTEN && return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if check_port_in_use "$PORT"; then
|
|
|
|
|
|
warn "Port ${PORT} is already in use."
|
|
|
|
|
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
|
|
|
|
|
_suggest=$((PORT + 1))
|
|
|
|
|
|
prompt_text "Enter a different port" "$_suggest"
|
|
|
|
|
|
PORT="$PROMPT_RESULT"
|
|
|
|
|
|
if check_port_in_use "$PORT"; then
|
|
|
|
|
|
error "Port ${PORT} is also in use. Please pick a free port."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
error "Port ${PORT} is occupied. Use --port to specify a different one."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 4. Interactive prompts
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
IMAGE="${OPT_IMAGE:-$DEFAULT_IMAGE}"
|
|
|
|
|
|
ALLOWED_ORIGINS=""
|
|
|
|
|
|
MEM_LIMIT="$DEFAULT_MEM_LIMIT"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
prompt_text "Docker image" "$DEFAULT_IMAGE"
|
|
|
|
|
|
IMAGE="$PROMPT_RESULT"
|
|
|
|
|
|
|
|
|
|
|
|
prompt_text "Port" "$PORT"
|
|
|
|
|
|
PORT="$PROMPT_RESULT"
|
|
|
|
|
|
|
|
|
|
|
|
prompt_text "Allowed origins (CORS, comma-separated, or empty)" ""
|
|
|
|
|
|
ALLOWED_ORIGINS="$PROMPT_RESULT"
|
|
|
|
|
|
|
|
|
|
|
|
prompt_text "Memory limit" "$DEFAULT_MEM_LIMIT"
|
|
|
|
|
|
MEM_LIMIT="$PROMPT_RESULT"
|
|
|
|
|
|
|
|
|
|
|
|
if check_port_in_use "$PORT"; then
|
|
|
|
|
|
error "Port ${PORT} is already in use. Aborting."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 5. Generate .env
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
if [ -f "$ENV_FILE" ]; then
|
|
|
|
|
|
BACKUP="${ENV_FILE}.$(date +%Y%m%d%H%M%S).bak"
|
|
|
|
|
|
step "Existing .env found. Backing up to ${BACKUP}"
|
|
|
|
|
|
cp "$ENV_FILE" "$BACKUP"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-05-26 14:15:48 +08:00
|
|
|
|
GENERATED_TOKEN=$(openssl rand -hex 32 2>/dev/null || od -vAn -N32 -tx1 /dev/urandom | tr -d ' \n' 2>/dev/null)
|
|
|
|
|
|
|
2026-05-22 15:04:16 +09:00
|
|
|
|
cat > "$ENV_FILE" << ENVFILE
|
|
|
|
|
|
# Generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
|
|
OPEN_DESIGN_IMAGE=${IMAGE}
|
|
|
|
|
|
OPEN_DESIGN_PORT=${PORT}
|
|
|
|
|
|
OPEN_DESIGN_ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
|
|
|
|
|
|
OPEN_DESIGN_MEM_LIMIT=${MEM_LIMIT}
|
|
|
|
|
|
NODE_OPTIONS=--max-old-space-size=192
|
2026-05-26 14:15:48 +08:00
|
|
|
|
OD_API_TOKEN=${GENERATED_TOKEN}
|
2026-05-22 15:04:16 +09:00
|
|
|
|
ENVFILE
|
|
|
|
|
|
|
|
|
|
|
|
ok "Written ${ENV_FILE}"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 6. Pull and start
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
step "Pulling image: ${IMAGE}"
|
|
|
|
|
|
$COMPOSE_CMD "${COMPOSE_FILES[@]}" pull
|
|
|
|
|
|
|
2026-08-18 07:09:45 +00:00
|
|
|
|
step "Starting OpenDesign..."
|
2026-05-22 15:04:16 +09:00
|
|
|
|
$COMPOSE_CMD "${COMPOSE_FILES[@]}" up -d --no-build
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 7. Health check
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
step "Waiting for health check (up to ${HEALTH_TIMEOUT}s)..."
|
|
|
|
|
|
|
|
|
|
|
|
HEALTH_URL="http://127.0.0.1:${PORT}/api/health"
|
|
|
|
|
|
HEALTH_OK=0
|
|
|
|
|
|
ELAPSED=0
|
|
|
|
|
|
|
|
|
|
|
|
while [ "$ELAPSED" -lt "$HEALTH_TIMEOUT" ]; do
|
|
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
|
|
|
|
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo '000')"
|
|
|
|
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
|
|
|
|
HTTP_CODE="$(wget -q -O /dev/null --server-response "$HEALTH_URL" 2>&1 | grep 'HTTP/' | tail -1 | awk '{print $2}')"
|
|
|
|
|
|
else
|
|
|
|
|
|
HTTP_CODE="000"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$HTTP_CODE" = "200" ]; then
|
|
|
|
|
|
HEALTH_OK=1
|
|
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
sleep 2
|
|
|
|
|
|
ELAPSED=$((ELAPSED + 2))
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$HEALTH_OK" = "1" ]; then
|
|
|
|
|
|
ok "Daemon is healthy (${HTTP_CODE} OK)"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "Health check did not pass within ${HEALTH_TIMEOUT}s."
|
|
|
|
|
|
step "Check status: ${COMPOSE_CMD} \"${COMPOSE_FILES[@]}\" logs"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 8. systemd unit (Linux only)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
if [ "$OS" = "Linux" ] && [ "$OPT_NO_SYSTEMD" = "0" ]; then
|
|
|
|
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
|
|
|
|
SYSTEMD_DIR="${HOME}/.config/systemd/user"
|
|
|
|
|
|
SYSTEMD_UNIT="${SYSTEMD_DIR}/open-design.service"
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "$SYSTEMD_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
CONTAINER_BIN="$(command -v "$CONTAINER_CMD")"
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
echo "[Unit]"
|
2026-08-18 07:09:45 +00:00
|
|
|
|
echo "Description=OpenDesign daemon (${CONTAINER_RUNTIME} Compose)"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
if [ "$CONTAINER_RUNTIME" = "docker" ]; then
|
|
|
|
|
|
echo "After=${CONTAINER_RUNTIME}.service"
|
|
|
|
|
|
echo "Requires=${CONTAINER_RUNTIME}.service"
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "[Service]"
|
|
|
|
|
|
echo "Type=oneshot"
|
|
|
|
|
|
echo "RemainAfterExit=yes"
|
|
|
|
|
|
echo "WorkingDirectory=${DEPLOY_DIR}"
|
2026-07-03 05:05:22 +02:00
|
|
|
|
# Build the compose -f list to match what the installer used at install
|
|
|
|
|
|
# time, so systemd restarts use the same file set (including the Linux
|
|
|
|
|
|
# host-network override when present).
|
|
|
|
|
|
_COMPOSE_ARGS="-f ${COMPOSE_FILE}"
|
|
|
|
|
|
if [ -f "$LINUX_OVERRIDE_FILE" ]; then
|
|
|
|
|
|
_COMPOSE_ARGS="${_COMPOSE_ARGS} -f ${LINUX_OVERRIDE_FILE}"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
fi
|
2026-07-03 05:05:22 +02:00
|
|
|
|
if [ -f "$OVERRIDE_FILE" ]; then
|
|
|
|
|
|
_COMPOSE_ARGS="${_COMPOSE_ARGS} -f ${OVERRIDE_FILE}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo "ExecStart=${CONTAINER_BIN} compose ${_COMPOSE_ARGS} up -d --no-build"
|
|
|
|
|
|
echo "ExecStop=${CONTAINER_BIN} compose ${_COMPOSE_ARGS} down"
|
2026-05-22 15:04:16 +09:00
|
|
|
|
echo "TimeoutStartSec=120"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "[Install]"
|
|
|
|
|
|
echo "WantedBy=default.target"
|
|
|
|
|
|
} > "$SYSTEMD_UNIT"
|
|
|
|
|
|
|
|
|
|
|
|
systemctl --user daemon-reload
|
|
|
|
|
|
systemctl --user enable open-design 2>/dev/null || true
|
|
|
|
|
|
ok "systemd unit installed: open-design.service"
|
|
|
|
|
|
step "Manage with: systemctl --user start|stop|status open-design"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "systemd not found. Skipping service installation."
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 9. Summary
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf "${BOLD}${GREEN} ── Installation Complete ──────────────────────────${RESET}\n"
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf " URL: http://127.0.0.1:%s\n" "$PORT"
|
|
|
|
|
|
printf " Image: %s\n" "$IMAGE"
|
|
|
|
|
|
printf " Data volume: open_design_data\n"
|
|
|
|
|
|
printf " Config: %s\n" "$ENV_FILE"
|
|
|
|
|
|
if [ "$OS" = "Linux" ] && [ -f "${HOME}/.config/systemd/user/open-design.service" ]; then
|
|
|
|
|
|
printf " Service: systemd (open-design.service)\n"
|
|
|
|
|
|
fi
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf " Next steps:\n"
|
|
|
|
|
|
printf " Update: %s/update.sh\n" "$SCRIPT_DIR"
|
|
|
|
|
|
printf " Uninstall: %s/uninstall.sh\n" "$SCRIPT_DIR"
|
|
|
|
|
|
printf " Logs: %s logs -f\n" "$COMPOSE_CMD"
|
|
|
|
|
|
printf "\n"
|
|
|
|
|
|
printf " Open http://127.0.0.1:%s in your browser\n\n" "$PORT"
|