#!/bin/bash set -e # ============================================================ # D3V-NPMWG Installer for Ubuntu/Debian # xGat3 + WireGuard VPN # https://src.d3v.ac/xtcnet/D3V-Server # ============================================================ INSTALL_DIR="/opt/d3v-npmwg" COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml" CONTAINER_NAME="d3v-npmwg" IMAGE_NAME="src.d3v.ac/xtcnet/d3v-server:latest" FORGEJO_INSTALL_DIR="/opt/forgejo" FORGEJO_COMPOSE_FILE="${FORGEJO_INSTALL_DIR}/docker-compose.yml" FORGEJO_CONTAINER_NAME="forgejo" FORGEJO_IMAGE="codeberg.org/forgejo/forgejo:9" DOCKER_NETWORK="d3v-net" FORGEJO_RUNNER_DIR="/opt/forgejo-runner" FORGEJO_RUNNER_CONTAINER="forgejo-runner" FORGEJO_RUNNER_IMAGE="gitea/act_runner:latest" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # ----------------------------------------------------------- # Helpers # ----------------------------------------------------------- log_step() { echo -e "${CYAN}[*]${NC} $1"; } log_ok() { echo -e "${GREEN}[✓]${NC} $1"; } log_warn() { echo -e "${YELLOW}[!]${NC} $1"; } log_err() { echo -e "${RED}[✗]${NC} $1"; } separator() { echo -e "${GREEN}=================================================================${NC}"; } self_update() { local remote_url="https://src.d3v.ac/xtcnet/D3V-Server/raw/branch/master/install.sh" local tmp tmp=$(mktemp) || return if curl -fsSL -m 10 "$remote_url" -o "$tmp" 2>/dev/null; then if ! cmp -s "$tmp" "$0"; then log_warn "A newer version of install.sh found. Updating..." cp "$tmp" "$0" chmod +x "$0" rm -f "$tmp" log_ok "install.sh updated. Restarting..." exec "$0" "$@" exit 0 fi fi rm -f "$tmp" } require_root() { if [ "$(id -u)" -ne 0 ]; then log_err "This script must be run as root. Use: sudo $0" exit 1 fi } get_compose_cmd() { if docker compose version > /dev/null 2>&1; then echo "docker compose" else log_err "Docker Compose plugin not found. Please install it first." exit 1 fi } detect_public_ip() { local ip="" ip=$(curl -s -m 5 https://ifconfig.me 2>/dev/null) \ || ip=$(curl -s -m 5 https://icanhazip.com 2>/dev/null) \ || ip=$(curl -s -m 5 https://api.ipify.org 2>/dev/null) \ || ip="" echo "$ip" } # ----------------------------------------------------------- # 1. Install system dependencies # ----------------------------------------------------------- install_deps() { separator echo -e "${BOLD} Installing System Dependencies${NC}" separator # --- curl --- log_step "Checking curl..." if command -v curl > /dev/null 2>&1; then log_ok "curl is already installed." else log_step "Installing curl..." apt-get update -qq apt-get install -y curl log_ok "curl installed." fi # --- Docker --- log_step "Checking Docker..." if command -v docker > /dev/null 2>&1; then log_ok "Docker is already installed ($(docker --version))." else log_step "Installing Docker... (this may take 1-2 minutes)" curl -fsSL https://get.docker.com | sh log_ok "Docker installed." fi # --- Ensure Docker daemon is running --- log_step "Starting Docker service..." systemctl enable docker > /dev/null 2>&1 || true systemctl restart docker > /dev/null 2>&1 || true # --- Wait for Docker daemon --- log_step "Waiting for Docker daemon to be ready..." local retries=0 while ! docker info > /dev/null 2>&1; do retries=$((retries + 1)) if [ "$retries" -ge 30 ]; then log_err "Docker daemon did not start after 30 seconds." log_err "Try: systemctl restart docker" exit 1 fi sleep 1 done log_ok "Docker daemon is running." # --- Remove old standalone docker-compose (Python) if present --- if command -v docker-compose > /dev/null 2>&1; then local dc_path dc_path=$(command -v docker-compose) # Check if it's the old Python version if docker-compose version 2>&1 | grep -qi "docker-compose version 1"; then log_warn "Detected legacy docker-compose (Python). Removing it..." apt-get purge -y docker-compose 2>/dev/null || rm -f "$dc_path" 2>/dev/null || true log_ok "Legacy docker-compose removed." fi fi # --- Docker Compose plugin --- log_step "Checking Docker Compose plugin..." if docker compose version > /dev/null 2>&1; then log_ok "Docker Compose plugin is ready ($(docker compose version))." else log_step "Installing Docker Compose plugin..." apt-get update -qq apt-get install -y docker-compose-plugin log_ok "Docker Compose plugin installed." fi echo "" log_ok "All system dependencies are ready." } # ----------------------------------------------------------- # x. Generate docker-compose.yml # ----------------------------------------------------------- generate_docker_compose() { local host="$1" if [ -z "$host" ]; then log_err "generate_docker_compose: host is empty." return 1 fi log_step "Generating docker-compose.yml..." local custom_ports_block="" if [ -f ".custom_ports" ]; then while IFS= read -r port_mapping; do # Ignore empty lines or comments [[ -z "$port_mapping" || "$port_mapping" =~ ^# ]] && continue custom_ports_block+=" - \"${port_mapping}\"\n" done < ".custom_ports" fi # Only include d3v-net if the network already exists (i.e. Forgejo is installed) local network_block="" if docker network ls --format '{{.Name}}' 2>/dev/null | grep -q "^${DOCKER_NETWORK}$"; then network_block=" networks: - d3v-net networks: d3v-net: external: true name: ${DOCKER_NETWORK}" fi cat > "$COMPOSE_FILE" < /dev/null 2>&1; then netfilter-persistent save > /dev/null 2>&1 log_ok "iptables rules saved (netfilter-persistent)." elif command -v iptables-save > /dev/null 2>&1; then mkdir -p /etc/iptables iptables-save > /etc/iptables/rules.v4 # Install iptables-persistent silently so rules reload on boot if ! dpkg -l iptables-persistent > /dev/null 2>&1; then log_step "Installing iptables-persistent for rule persistence..." DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent > /dev/null 2>&1 log_ok "iptables-persistent installed." fi log_ok "iptables rules saved (/etc/iptables/rules.v4)." else log_warn "Could not save iptables rules — install iptables-persistent manually." fi } # ----------------------------------------------------------- # Ensure shared Docker network exists # ----------------------------------------------------------- ensure_docker_network() { if ! docker network ls --format '{{.Name}}' | grep -q "^${DOCKER_NETWORK}$"; then log_step "Creating shared Docker network '${DOCKER_NETWORK}'..." docker network create "${DOCKER_NETWORK}" > /dev/null log_ok "Network '${DOCKER_NETWORK}' created." else log_ok "Network '${DOCKER_NETWORK}' already exists." fi } # ----------------------------------------------------------- # 2. Install D3V-NPMWG # ----------------------------------------------------------- do_install() { require_root if [ -d "$INSTALL_DIR" ]; then log_warn "D3V-NPMWG is already installed at ${INSTALL_DIR}." log_warn "Use the Update option to pull the latest image, or Uninstall first." return fi separator echo -e "${BOLD} D3V-NPMWG Installation${NC}" separator echo "" # --- Dependencies --- install_deps echo "" # --- Detect IP --- log_step "Detecting server public IP..." local detected_ip detected_ip=$(detect_public_ip) local wg_host="" if [ -n "$detected_ip" ]; then log_ok "Detected IP: ${BOLD}${detected_ip}${NC}" read -rp "$(echo -e "${CYAN}[?]${NC} WG_HOST [${detected_ip}]: ")" wg_host wg_host="${wg_host:-$detected_ip}" else log_warn "Could not auto-detect public IP." read -rp "$(echo -e "${CYAN}[?]${NC} Enter server public IP or domain: ")" wg_host fi if [ -z "$wg_host" ]; then log_err "WG_HOST cannot be empty. Aborting." return fi # --- Create directory --- log_step "Creating ${INSTALL_DIR}..." mkdir -p "$INSTALL_DIR" log_ok "Directory created." # --- Ensure shared network --- ensure_docker_network # --- Write docker-compose.yml --- generate_docker_compose "$wg_host" # --- Pull & Start --- log_step "Pulling Docker image (this may take a few minutes)..." local dc dc=$(get_compose_cmd) cd "$INSTALL_DIR" $dc pull log_ok "Image pulled." log_step "Starting containers..." $dc up -d log_ok "Containers started." # --- Verify --- log_step "Waiting for container to become healthy (10s)..." sleep 10 if docker ps --format '{{.Names}}' | grep -q "$CONTAINER_NAME"; then echo "" separator echo -e "${GREEN}${BOLD} D3V-NPMWG INSTALLED SUCCESSFULLY!${NC}" separator echo -e " ${CYAN}Web Admin UI${NC} : ${BOLD}http://${wg_host}:81${NC}" echo -e " ${CYAN}HTTP Proxy${NC} : port 80" echo -e " ${CYAN}HTTPS Proxy${NC} : port 443" echo -e " ${CYAN}WireGuard VPN${NC} : port 51820/udp" echo "" echo -e " Open the Web UI in ~30s and create your admin account." separator else log_err "Container did not start. Check logs:" echo -e " docker logs ${CONTAINER_NAME}" fi } # ----------------------------------------------------------- # 3. Uninstall D3V-NPMWG # ----------------------------------------------------------- do_uninstall() { require_root if [ ! -d "$INSTALL_DIR" ]; then log_warn "D3V-NPMWG is not installed at ${INSTALL_DIR}." return fi log_warn "This will stop and remove D3V-NPMWG and ALL its data." read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm if [[ ! "$confirm" =~ ^[yY]$ ]]; then echo "Cancelled." return fi log_step "Stopping containers..." local dc dc=$(get_compose_cmd) cd "$INSTALL_DIR" && $dc down -v 2>/dev/null || true cd / log_step "Removing ${INSTALL_DIR}..." rm -rf "$INSTALL_DIR" log_ok "D3V-NPMWG uninstalled." } # ----------------------------------------------------------- # 4. Purge — uninstall app + system deps (Docker) # ----------------------------------------------------------- do_purge() { require_root do_uninstall echo "" log_warn "Do you also want to remove Docker and Docker Compose from this system?" read -rp "$(echo -e "${RED}Remove Docker? (y/N): ${NC}")" confirm if [[ ! "$confirm" =~ ^[yY]$ ]]; then echo "Skipped Docker removal." return fi log_step "Removing Docker packages..." apt-get purge -y docker-ce docker-ce-cli containerd.io \ docker-compose-plugin docker-buildx-plugin 2>/dev/null || true apt-get autoremove -y --purge 2>/dev/null || true log_ok "Docker and related packages removed." } # ----------------------------------------------------------- # 5. Reset admin password # ----------------------------------------------------------- do_reset_password() { require_root if ! docker ps --format '{{.Names}}' | grep -q "$CONTAINER_NAME"; then log_err "Container ${CONTAINER_NAME} is not running. Start it first." return fi read -rsp "$(echo -e "${CYAN}[?]${NC} New password: ")" new_pass echo "" if [ -z "$new_pass" ]; then log_err "Password cannot be empty. Cancelled." return fi log_step "Resetting admin password..." local result result=$(docker exec "$CONTAINER_NAME" node -e " import('bcrypt').then(async bcrypt => { const knex = (await import('/app/db.js')).default(); const user = await knex('user').where('is_deleted', 0).orderBy('id', 'asc').first(); if (!user) { console.error('NO_USER'); process.exit(2); } const hash = await bcrypt.hash('${new_pass}', 13); await knex('auth').where('user_id', user.id).update({ secret: hash }); await knex('user').where('id', user.id).update({ is_disabled: 0 }); console.log(user.email); process.exit(0); }).catch(e => { console.error(e.message); process.exit(1); }); " 2>&1) local exit_code=$? if [ $exit_code -eq 0 ]; then log_ok "Password updated successfully." echo -e " Email : ${BOLD}${result}${NC}" else log_err "Failed to reset password: ${result}" fi } # ----------------------------------------------------------- # 6. Update D3V-NPMWG # ----------------------------------------------------------- do_update() { require_root if [ ! -d "$INSTALL_DIR" ]; then log_err "D3V-NPMWG is not installed. Install it first." return fi local dc dc=$(get_compose_cmd) cd "$INSTALL_DIR" || return # Save existing WG_HOST before regenerating template local current_wg_host="" if [ -f "docker-compose.yml" ]; then # Safely extract WG_HOST value ignoring quotes and spaces current_wg_host=$(grep -E 'WG_HOST:' docker-compose.yml | awk -F'"' '{print $2}') fi if [ -z "$current_wg_host" ]; then current_wg_host=$(detect_public_ip) log_warn "Could not extract WG_HOST. Using ${current_wg_host}." fi generate_docker_compose "$current_wg_host" log_step "Pulling latest image..." $dc pull log_ok "Image pulled." log_step "Recreating containers..." $dc up -d log_ok "D3V-NPMWG updated." log_step "Cleaning old images..." docker image prune -f > /dev/null 2>&1 log_ok "Done." } # ----------------------------------------------------------- # 7. Toggle Port 81 (Admin UI) # ----------------------------------------------------------- # x. Custom Stream Ports Manager # ----------------------------------------------------------- do_manage_ports() { require_root echo "" log_step "TCP/UDP Stream Ports Manager" echo "If you created a Stream in Nginx Proxy Manager (e.g., listening on port 10000)," echo "you must expose that port down to the Docker container." echo "" local custom_ports_file=".custom_ports" touch "$custom_ports_file" echo "Current custom exposed ports:" if [ -s "$custom_ports_file" ]; then cat -n "$custom_ports_file" else echo " (None)" fi echo "" read -rp "$(echo -e "${CYAN}[?]${NC} Enter new port mapping (e.g. 10000:10000) or 'clear' to remove all: ")" new_port if [[ "$new_port" == "clear" ]]; then > "$custom_ports_file" log_ok "All custom ports cleared." elif [[ -n "$new_port" ]]; then echo "$new_port" >> "$custom_ports_file" log_ok "Port $new_port added." else log_warn "No changes made." return fi log_step "Regenerating docker-compose.yml and restarting container..." local dc dc=$(get_compose_cmd) local current_wg_host="" if [ -f "docker-compose.yml" ]; then current_wg_host=$(grep -E 'WG_HOST:' docker-compose.yml | awk -F'"' '{print $2}') fi if [ -z "$current_wg_host" ]; then current_wg_host=$(detect_public_ip) fi generate_docker_compose "$current_wg_host" $dc up -d log_ok "Container updated with new port configurations." } # ----------------------------------------------------------- # 7. Toggle Port 81 (Admin UI) # ----------------------------------------------------------- do_toggle_port_81() { require_root echo "" log_warn "This feature uses iptables (DOCKER-USER chain) to block external access to port 81." log_warn "When blocked, you can only access the Admin UI via the WireGuard VPN (http://10.8.0.1:81) or localhost." echo "" read -rp "$(echo -e "${CYAN}[?]${NC} Do you want to (B)lock or (U)nblock external access to port 81? [B/U]: ")" choice if [[ "$choice" =~ ^[bB]$ ]]; then log_step "Blocking external access to port 81..." # Remove existing rule if any to avoid duplicates iptables -D DOCKER-USER -p tcp --dport 81 -j DROP 2>/dev/null || true # Add rule to block port 81 iptables -I DOCKER-USER -p tcp --dport 81 -j DROP log_ok "External access to port 81 is now BLOCKED." save_iptables_rules elif [[ "$choice" =~ ^[uU]$ ]]; then log_step "Unblocking external access to port 81..." iptables -D DOCKER-USER -p tcp --dport 81 -j DROP 2>/dev/null || true log_ok "External access to port 81 is now UNBLOCKED (Public)." save_iptables_rules else log_err "Invalid choice. Cancelled." fi } # ----------------------------------------------------------- # Forgejo — Install / Uninstall / Update # ----------------------------------------------------------- do_forgejo_install() { require_root if [ -d "$FORGEJO_INSTALL_DIR" ]; then log_warn "Forgejo is already installed at ${FORGEJO_INSTALL_DIR}." log_warn "Use the Update option to pull the latest image." return fi separator echo -e "${BOLD} Forgejo Installation${NC}" separator echo "" install_deps echo "" ensure_docker_network # Ensure D3V-NPMWG is on d3v-net (runtime + persistent in compose) if [ -f "$COMPOSE_FILE" ]; then if ! grep -q "d3v-net" "$COMPOSE_FILE"; then log_step "Updating D3V-NPMWG compose to join '${DOCKER_NETWORK}'..." local current_wg_host="" current_wg_host=$(grep -E 'WG_HOST:' "$COMPOSE_FILE" | awk -F'"' '{print $2}') [ -z "$current_wg_host" ] && current_wg_host=$(detect_public_ip) cd "$INSTALL_DIR" generate_docker_compose "$current_wg_host" $(get_compose_cmd) up -d --no-recreate 2>/dev/null || true log_ok "D3V-NPMWG compose updated." fi # Connect running container immediately (no restart needed) if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then docker network connect "${DOCKER_NETWORK}" "${CONTAINER_NAME}" 2>/dev/null || true fi fi log_step "Creating ${FORGEJO_INSTALL_DIR}..." mkdir -p "${FORGEJO_INSTALL_DIR}" log_ok "Directory created." log_step "Generating Forgejo docker-compose.yml..." cat > "$FORGEJO_COMPOSE_FILE" </dev/null || true iptables -I DOCKER-USER -p tcp --dport 3000 -j DROP iptables -D DOCKER-USER -p tcp --dport 2222 -j DROP 2>/dev/null || true iptables -I DOCKER-USER -p tcp --dport 2222 -j DROP log_ok "Ports 3000 and 2222 are now private (HTTPS git via NPM proxy only)." save_iptables_rules echo "" separator echo -e "${GREEN}${BOLD} FORGEJO INSTALLED SUCCESSFULLY!${NC}" separator echo -e " ${CYAN}Git HTTPS${NC}: via NPM proxy after hostname setup below" echo "" separator echo -e "${BOLD} Add Hostname in Nginx Proxy Manager${NC}" separator echo -e " ${YELLOW}Step 1:${NC} Open NPM Admin UI at ${BOLD}http://${server_ip}:81${NC}" echo "" echo -e " ${YELLOW}Step 2:${NC} Go to ${BOLD}Proxy Hosts${NC} → click ${BOLD}Add Proxy Host${NC}" echo "" echo -e " ${YELLOW}Step 3:${NC} ${BOLD}Details${NC} tab:" echo -e " Domain Names : ${CYAN}git.yourdomain.com${NC}" echo -e " Scheme : ${CYAN}http${NC}" echo -e " Forward Hostname : ${CYAN}forgejo${NC} (container name)" echo -e " Forward Port : ${CYAN}3000${NC}" echo -e " [x] Cache Assets [x] Block Common Exploits" echo "" echo -e " ${YELLOW}Step 4:${NC} ${BOLD}SSL${NC} tab → select ${BOLD}Request a new SSL Certificate${NC}" echo -e " [x] Force SSL [x] HTTP/2 Support" echo "" echo -e " ${YELLOW}Step 5:${NC} Click ${BOLD}Save${NC}." echo "" echo -e " ${YELLOW}Step 6:${NC} Open ${BOLD}https://git.yourdomain.com${NC} → complete Forgejo setup" echo -e " Server Domain : ${CYAN}git.yourdomain.com${NC}" echo -e " Base URL : ${CYAN}https://git.yourdomain.com${NC}" echo -e " SSH Port : ${CYAN}2222${NC}" separator else log_err "Forgejo did not start. Check: docker logs ${FORGEJO_CONTAINER_NAME}" fi } do_forgejo_uninstall() { require_root if [ ! -d "$FORGEJO_INSTALL_DIR" ]; then log_warn "Forgejo is not installed at ${FORGEJO_INSTALL_DIR}." return fi log_warn "This will stop and remove Forgejo and ALL its data (repos, users, issues)." read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm if [[ ! "$confirm" =~ ^[yY]$ ]]; then echo "Cancelled." return fi local dc dc=$(get_compose_cmd) cd "$FORGEJO_INSTALL_DIR" && $dc down -v 2>/dev/null || true cd / log_step "Removing ${FORGEJO_INSTALL_DIR}..." rm -rf "$FORGEJO_INSTALL_DIR" log_ok "Forgejo uninstalled." log_step "Unblocking ports 3000 and 2222..." iptables -D DOCKER-USER -p tcp --dport 3000 -j DROP 2>/dev/null || true iptables -D DOCKER-USER -p tcp --dport 2222 -j DROP 2>/dev/null || true log_ok "Ports 3000 and 2222 unblocked." save_iptables_rules } do_forgejo_update() { require_root if [ ! -d "$FORGEJO_INSTALL_DIR" ]; then log_err "Forgejo is not installed. Install it first." return fi local dc dc=$(get_compose_cmd) cd "$FORGEJO_INSTALL_DIR" || return log_step "Pulling latest Forgejo image..." $dc pull log_ok "Image pulled." log_step "Recreating Forgejo container..." $dc up -d log_ok "Forgejo updated." log_step "Cleaning old images..." docker image prune -f > /dev/null 2>&1 log_ok "Done." } do_forgejo_runner_install() { require_root if docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then log_warn "Forgejo Runner is already installed." return fi separator echo -e "${BOLD} Forgejo Runner Installation${NC}" separator echo "" read -rp "$(echo -e "${CYAN}[?]${NC} Forgejo URL [https://src.d3v.ac]: ")" forgejo_url forgejo_url="${forgejo_url:-https://src.d3v.ac}" echo "" echo -e " Get token from: ${BOLD}${forgejo_url}/-/admin/runners${NC}" echo -e " Or: repo ${BOLD}Settings → Actions → Runners → Create new Runner${NC}" echo "" read -rsp "$(echo -e "${CYAN}[?]${NC} Runner registration token: ")" runner_token echo "" if [ -z "$runner_token" ]; then log_err "Token cannot be empty. Cancelled." return fi mkdir -p "$FORGEJO_RUNNER_DIR" # Auto-configure swap based on available RAM local total_ram_mb total_ram_mb=$(awk '/MemTotal/ { printf "%d", $2/1024 }' /proc/meminfo) local swap_size="" if [ "$total_ram_mb" -lt 2048 ]; then swap_size="2G" elif [ "$total_ram_mb" -lt 4096 ]; then swap_size="1G" fi if [ -n "$swap_size" ]; then if [ ! -f /swapfile ]; then log_step "RAM: ${total_ram_mb}MB detected — creating ${swap_size} swapfile..." fallocate -l "$swap_size" /swapfile chmod 600 /swapfile mkswap /swapfile > /dev/null swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab log_ok "Swapfile ${swap_size} created and enabled (persistent across reboots)." else log_ok "Swapfile already exists, skipping." fi else log_ok "RAM: ${total_ram_mb}MB — sufficient, no swap needed." fi # gitea/act_runner auto-registers on first start via env vars, then starts daemon log_step "Starting Forgejo Runner (auto-register on first boot)..." docker run -d \ --name "${FORGEJO_RUNNER_CONTAINER}" \ --restart unless-stopped \ -e GITEA_INSTANCE_URL="${forgejo_url}" \ -e GITEA_RUNNER_REGISTRATION_TOKEN="${runner_token}" \ -e GITEA_RUNNER_NAME="$(hostname)-runner" \ -e GITEA_RUNNER_LABELS="ubuntu-latest:host,ubuntu-22.04:host" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "${FORGEJO_RUNNER_DIR}:/data" \ "${FORGEJO_RUNNER_IMAGE}" log_ok "Forgejo Runner started." echo "" separator echo -e "${GREEN}${BOLD} FORGEJO RUNNER INSTALLED!${NC}" separator echo -e " Runner connected to: ${BOLD}${forgejo_url}${NC}" echo -e " Push a commit to trigger your first workflow." separator } do_forgejo_runner_uninstall() { require_root if ! docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then log_warn "Forgejo Runner is not installed." return fi log_warn "This will stop and remove the Forgejo Runner." read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm if [[ ! "$confirm" =~ ^[yY]$ ]]; then echo "Cancelled." return fi docker stop "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true docker rm "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true rm -rf "$FORGEJO_RUNNER_DIR" log_ok "Forgejo Runner uninstalled." } show_forgejo_menu() { while true; do echo "" separator echo -e "${BOLD} Forgejo Manager${NC}" separator echo " 1) Install Forgejo" echo " 2) Uninstall Forgejo" echo " 3) Update Forgejo" echo " 4) Install Runner" echo " 5) Uninstall Runner" echo " 6) Back" separator read -rp " Select [1-6]: " choice echo "" case "$choice" in 1) do_forgejo_install ;; 2) do_forgejo_uninstall ;; 3) do_forgejo_update ;; 4) do_forgejo_runner_install ;; 5) do_forgejo_runner_uninstall ;; 6) return ;; *) log_err "Invalid option." ;; esac done } # ----------------------------------------------------------- # Interactive menu # ----------------------------------------------------------- show_menu() { while true; do echo "" separator echo -e "${BOLD} D3V-NPMWG Installation Manager${NC}" separator echo " 1) Install D3V-NPMWG" echo " 2) Uninstall D3V-NPMWG" echo " 3) Uninstall D3V-NPMWG + Docker (Purge)" echo " 4) Reset Admin Password" echo " 5) Update D3V-NPMWG" echo " 6) Manage Custom Stream Ports" echo " 7) Toggle Admin Port 81 (Block/Unblock)" echo " 8) Forgejo" echo " 9) Exit" separator read -rp " Select [1-9]: " choice echo "" case "$choice" in 1) do_install ;; 2) do_uninstall ;; 3) do_purge ;; 4) do_reset_password ;; 5) do_update ;; 6) do_manage_ports ;; 7) do_toggle_port_81 ;; 8) show_forgejo_menu ;; 9) echo "Bye!"; exit 0 ;; *) log_err "Invalid option." ;; esac done } show_help() { echo "Usage: $0 [command]" echo "" echo "Commands:" echo " install Install D3V-NPMWG and dependencies" echo " uninstall Remove D3V-NPMWG (keeps Docker)" echo " purge Remove D3V-NPMWG AND Docker" echo " reset Reset web admin password" echo " update Pull latest image and restart" echo " manage-ports Add or remove custom exposed Stream TCP/UDP ports" echo " toggle-port Block or unblock external access to Admin UI (Port 81) using iptables" echo " forgejo Open Forgejo submenu (install/uninstall/update)" echo " help Show this help" echo "" echo "Run without arguments to open the interactive menu." } # ----------------------------------------------------------- # Entry point # ----------------------------------------------------------- self_update "$@" if [ "$#" -eq 0 ]; then show_menu else case "$1" in install) do_install ;; uninstall) do_uninstall ;; purge) do_purge ;; reset) do_reset_password ;; update) do_update ;; manage-ports) do_manage_ports ;; toggle-port) do_toggle_port_81 ;; forgejo) show_forgejo_menu ;; help|-h|--help) show_help ;; *) log_err "Unknown command: $1" show_help exit 1 ;; esac fi