mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(deploy): add one-click Docker/Podman Compose installer for Linux and macOS - Add install.sh with interactive wizard, Podman/Docker runtime detection, port conflict check, health verification, and systemd user unit creation - Add update.sh for image pull and restart with health check - Add uninstall.sh with interactive user data backup before removal - Unify CLI output styling with step/ok/warn/error/info helpers - Add install-guide.md documentation - Add install.test.ts integration test suite * feat(deploy): add one-click Docker/Podman Compose installer - interactive setup wizard with port, image, CORS, memory prompts - automatic Docker/Podman detection with install guidance - systemd user unit for Linux, health check polling - update.sh (pull + restart + prune) and uninstall.sh (backup + cleanup) - node:test integration suite and install-guide.md * style(deploy): improve POSIX sh compatibility and systemd unit handling - unify shell shebangs to #!/usr/bin/env bash - add pipefail option for better error handling - fix systemd unit for Podman: remove After/Requires when no service - correct documentation to match actual uninstall behavior * fix(deploy): address review feedback for installer scripts - remove curl | sh path, document clone-first only - isolate tests via docker-compose.override.yml with unique names - support both --image <ref> and --image=<ref> in update.sh - add running container detection before install * docs(install): remove demo scripts and add MCP note
233 lines
7.9 KiB
Bash
Executable file
233 lines
7.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# Open Design — Uninstaller
|
||
# Stops and removes the Docker Compose deployment
|
||
#
|
||
# Usage: ./uninstall.sh [--keep-data] [--non-interactive]
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
|
||
ENV_FILE="${DEPLOY_DIR}/.env"
|
||
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.yml"
|
||
OVERRIDE_FILE="${DEPLOY_DIR}/docker-compose.override.yml"
|
||
|
||
COMPOSE_FILES=(-f "$COMPOSE_FILE")
|
||
if [ -f "$OVERRIDE_FILE" ]; then
|
||
COMPOSE_FILES+=(-f "$OVERRIDE_FILE")
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Detect container runtime
|
||
# ---------------------------------------------------------------------------
|
||
COMPOSE_CMD=""
|
||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||
COMPOSE_CMD="docker compose"
|
||
elif command -v podman >/dev/null 2>&1 && podman compose version >/dev/null 2>&1; then
|
||
COMPOSE_CMD="podman compose"
|
||
elif command -v podman >/dev/null 2>&1 && command -v podman-compose >/dev/null 2>&1; then
|
||
COMPOSE_CMD="podman-compose"
|
||
elif command -v docker >/dev/null 2>&1 && command -v docker-compose >/dev/null 2>&1; then
|
||
COMPOSE_CMD="docker-compose"
|
||
else
|
||
error "No container runtime found. Install Docker or Podman."
|
||
exit 1
|
||
fi
|
||
|
||
RUNTIME="${COMPOSE_CMD%% *}"
|
||
|
||
NON_INTERACTIVE=0
|
||
KEEP_DATA=0
|
||
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--non-interactive) NON_INTERACTIVE=1 ;;
|
||
--keep-data) KEEP_DATA=1 ;;
|
||
--help|-h)
|
||
echo "Usage: uninstall.sh [options]"
|
||
echo " --keep-data Preserve the open_design_data volume"
|
||
echo " --non-interactive Skip confirmation prompts"
|
||
exit 0
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Banner
|
||
# ---------------------------------------------------------------------------
|
||
printf "\n"
|
||
printf "${BOLD} ┌──────────────────────────────────────┐${RESET}\n"
|
||
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
||
printf "${BOLD} │${RESET} ${CYAN}◈${RESET} ${BOLD}Open Design${RESET} ${BOLD}│${RESET}\n"
|
||
printf "${BOLD} │${RESET} ${DIM}Uninstaller${RESET} ${BOLD}│${RESET}\n"
|
||
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
||
printf "${BOLD} └──────────────────────────────────────┘${RESET}\n"
|
||
printf "\n"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Find data volume (Compose prepends project name)
|
||
# ---------------------------------------------------------------------------
|
||
CONTAINER_NAME="${COMPOSE_CONTAINER_NAME:-open-design}"
|
||
VOLUME_BASE="${COMPOSE_VOLUME_NAME:-open_design_data}"
|
||
PROJECT_NAME="${COMPOSE_PROJECT_NAME:-open-design}"
|
||
|
||
DATA_VOLUME=""
|
||
for _vol in "${PROJECT_NAME}_${VOLUME_BASE}" "${VOLUME_BASE}"; do
|
||
if $RUNTIME volume inspect "$_vol" >/dev/null 2>&1; then
|
||
DATA_VOLUME="$_vol"
|
||
break
|
||
fi
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Backup user data
|
||
# ---------------------------------------------------------------------------
|
||
BACKUP_DIR=""
|
||
|
||
_do_backup() {
|
||
_dest="$1"
|
||
mkdir -p "$_dest"
|
||
|
||
step "Backing up user data to ${_dest}..."
|
||
|
||
# Try container cp first (works if container exists, running or stopped)
|
||
if $RUNTIME inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||
$RUNTIME cp "$CONTAINER_NAME":/app/.od/. "$_dest/" 2>/dev/null
|
||
fi
|
||
|
||
# If container cp didn't work or container doesn't exist, use temp container
|
||
if [ ! -f "${_dest}/app.sqlite" ] && [ -n "$DATA_VOLUME" ]; then
|
||
_image="$($RUNTIME images -q | head -1)"
|
||
if [ -n "$_image" ]; then
|
||
$RUNTIME run --rm \
|
||
-v "${DATA_VOLUME}:/source:ro" \
|
||
-v "${_dest}:/backup" \
|
||
--user root \
|
||
"$_image" \
|
||
sh -c "cp -r /source/. /backup/" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
if [ -f "${_dest}/app.sqlite" ] || [ -d "${_dest}/projects" ]; then
|
||
ok "Backup saved to ${_dest}"
|
||
ok "Contents: app database, projects, artifacts, media config"
|
||
else
|
||
warn "No user data found to back up."
|
||
rm -rf "$_dest"
|
||
BACKUP_DIR=""
|
||
fi
|
||
}
|
||
|
||
if [ "$KEEP_DATA" = "0" ] && [ -n "$DATA_VOLUME" ]; then
|
||
if [ "$NON_INTERACTIVE" = "0" ]; then
|
||
_default_backup="${HOME}/open-design-backup-$(date +%Y%m%d%H%M%S)"
|
||
prompt_text "Backup destination" "$_default_backup"
|
||
BACKUP_DIR="$PROMPT_RESULT"
|
||
_do_backup "$BACKUP_DIR"
|
||
else
|
||
BACKUP_DIR="${HOME}/open-design-backup-$(date +%Y%m%d%H%M%S)"
|
||
_do_backup "$BACKUP_DIR"
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Confirm destructive action
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$NON_INTERACTIVE" = "0" ]; then
|
||
printf "\n"
|
||
warn "Everything will now be removed: containers, data volume, and config."
|
||
printf " Continue? [y/N]: "
|
||
read -r _confirm
|
||
case "$_confirm" in
|
||
[Yy]*) ;;
|
||
*) step "Cancelled."; exit 0 ;;
|
||
esac
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Stop and remove containers
|
||
# ---------------------------------------------------------------------------
|
||
if $COMPOSE_CMD "${COMPOSE_FILES[@]}" ps -q 2>/dev/null | grep -q .; then
|
||
step "Stopping containers..."
|
||
$COMPOSE_CMD "${COMPOSE_FILES[@]}" down
|
||
ok "Containers stopped."
|
||
else
|
||
# Try removing stopped container directly
|
||
if $RUNTIME inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||
step "Removing stopped container..."
|
||
$RUNTIME rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||
else
|
||
step "No containers found."
|
||
fi
|
||
fi
|
||
|
||
# Remove data volume (unless --keep-data)
|
||
if [ "$KEEP_DATA" = "0" ] && [ -n "$DATA_VOLUME" ]; then
|
||
step "Removing data volume ${DATA_VOLUME}..."
|
||
$RUNTIME volume rm "$DATA_VOLUME" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
# Remove systemd unit (Linux)
|
||
SYSTEMD_UNIT="${HOME}/.config/systemd/user/open-design.service"
|
||
if [ -f "$SYSTEMD_UNIT" ]; then
|
||
step "Removing systemd unit..."
|
||
systemctl --user disable --now open-design 2>/dev/null || true
|
||
rm -f "$SYSTEMD_UNIT"
|
||
systemctl --user daemon-reload
|
||
ok "systemd unit removed."
|
||
fi
|
||
|
||
# Remove .env
|
||
if [ -f "$ENV_FILE" ]; then
|
||
step "Removing ${ENV_FILE}..."
|
||
rm -f "$ENV_FILE"
|
||
fi
|
||
|
||
printf "\n"
|
||
printf "${BOLD}${GREEN} ── Uninstall Complete ────────────────────────────${RESET}\n"
|
||
printf "\n"
|
||
if [ "$KEEP_DATA" = "1" ]; then
|
||
info "Data volume was preserved."
|
||
step "Remove it manually: $RUNTIME volume rm $DATA_VOLUME"
|
||
elif [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
|
||
info "Your data was backed up to: ${BACKUP_DIR}"
|
||
step "To restore, copy contents into a new deployment's data volume."
|
||
fi
|
||
printf "\n"
|