mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
update, uninstall and cleanup-apps silently dropped any flag they did not know, so 'serverkit update --help' ran a real update. --help now prints usage and an unknown flag fails before anything runs.
1742 lines
59 KiB
Bash
1742 lines
59 KiB
Bash
#!/bin/bash
|
|
#
|
|
# ServerKit CLI - Management tool for ServerKit
|
|
#
|
|
# Architecture:
|
|
# - Backend: systemd service (runs directly on host)
|
|
# - Frontend: static bundle served by host nginx (or, on the legacy
|
|
# layout, the serverkit-frontend container — see frontend_mode)
|
|
#
|
|
# Usage: serverkit <command> [options]
|
|
#
|
|
|
|
set -e
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Truecolor ServerKit identity (degrades to plain text when not supported)
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
|
|
ESC=$'\033'
|
|
RST_TC="${ESC}[0m"; BLD_TC="${ESC}[1m"
|
|
paint() { printf '%s[38;2;%d;%d;%dm' "$ESC" "$1" "$2" "$3"; }
|
|
else
|
|
RST_TC=''; BLD_TC=''
|
|
paint() { :; }
|
|
fi
|
|
|
|
V1="$(paint 196 181 253)"; V2="$(paint 167 139 250)"; V3="$(paint 139 92 246)"
|
|
V4="$(paint 124 58 237)"; V5="$(paint 109 40 217)"
|
|
PAPER_TC="$(paint 237 233 254)"; ASH_TC="$(paint 165 160 190)"; FOG_TC="$(paint 113 108 140)"
|
|
HUE_OK_TC="$(paint 52 211 153)"; HUE_WARN_TC="$(paint 250 204 21)"; HUE_ERR_TC="$(paint 248 113 113)"; HUE_LINK_TC="$(paint 103 232 249)"
|
|
|
|
# Configuration
|
|
INSTALL_DIR="${SERVERKIT_DIR:-/opt/serverkit}"
|
|
VENV_DIR="${SERVERKIT_VENV_DIR:-$INSTALL_DIR/venv}"
|
|
NGINX_DIR="${SERVERKIT_NGINX_DIR:-/etc/nginx}"
|
|
BACKEND_SERVICE="serverkit"
|
|
FRONTEND_CONTAINER="serverkit-frontend"
|
|
|
|
# How the SPA is served on this box. install.sh has host nginx serve the
|
|
# prebuilt bundle straight from frontend/dist; the `serverkit-frontend`
|
|
# container only exists under the opt-in `legacy-frontend` compose profile.
|
|
# Read the answer off the nginx vhost instead of assuming either layout —
|
|
# a plain `docker compose up -d` on a host install does not start that
|
|
# container, it builds the whole all-in-one panel image (issue #127).
|
|
#
|
|
# static nginx `root .../frontend/dist` (install.sh, the default)
|
|
# container nginx proxies `location /` to a local port (legacy profile)
|
|
# unknown no serverkit vhost found
|
|
serverkit_vhost() {
|
|
local conf
|
|
for conf in "$NGINX_DIR/sites-enabled/serverkit.conf" \
|
|
"$NGINX_DIR/sites-available/serverkit.conf" \
|
|
"$NGINX_DIR/conf.d/serverkit.conf"; do
|
|
if [ -f "$conf" ]; then
|
|
echo "$conf"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# The directory nginx serves the SPA from (static mode only).
|
|
frontend_static_root() {
|
|
local conf
|
|
conf="$(serverkit_vhost)" || return 1
|
|
sed -n 's/^[[:space:]]*root[[:space:]]\{1,\}\([^;]*\);.*/\1/p' "$conf" \
|
|
| grep 'frontend/dist' | head -1
|
|
}
|
|
|
|
frontend_mode() {
|
|
local conf
|
|
if conf="$(serverkit_vhost)"; then
|
|
if grep -Eq '^[[:space:]]*root[[:space:]]+[^;]*frontend/dist' "$conf"; then
|
|
echo static
|
|
elif grep -Eq 'proxy_pass[[:space:]]+http://127\.0\.0\.1:[0-9]+' "$conf"; then
|
|
echo container
|
|
else
|
|
echo unknown
|
|
fi
|
|
elif [ -f "$INSTALL_DIR/frontend/dist/index.html" ]; then
|
|
# No vhost yet but the bundle is on disk: install.sh layout.
|
|
echo static
|
|
else
|
|
echo unknown
|
|
fi
|
|
}
|
|
|
|
# The compose invocation that actually starts the legacy frontend container.
|
|
compose_frontend() {
|
|
docker compose --profile legacy-frontend "$@" frontend
|
|
}
|
|
|
|
# Helper functions
|
|
print_header() {
|
|
local ver
|
|
ver=$(get_local_version 2>/dev/null || echo "unknown")
|
|
printf '\n'
|
|
printf ' %s%s▖▌▌%s %s%sServerKit%s %sv%s%s %s•%s %s%s%s\n' \
|
|
"${BLD_TC}" "${V2}" "${RST_TC}" "${BLD_TC}" "${PAPER_TC}" "${RST_TC}" "${FOG_TC}" "$ver" "${RST_TC}" \
|
|
"${HUE_OK_TC}" "${RST_TC}" "${ASH_TC}" "Stable" "${RST_TC}"
|
|
printf ' %s%s▌▖▌%s %sSelf-hosted infrastructure, made simple.%s\n' \
|
|
"${BLD_TC}" "${V3}" "${RST_TC}" "${ASH_TC}" "${RST_TC}"
|
|
printf ' %s%s▌▌▖%s %sWeb apps · Databases · Docker · Email · DNS · Security%s\n' \
|
|
"${BLD_TC}" "${V4}" "${RST_TC}" "${FOG_TC}" "${RST_TC}"
|
|
printf ' %s%s▘▘▘%s %sPython + React, one command · serverkit.ai%s\n' \
|
|
"${BLD_TC}" "${V5}" "${RST_TC}" "${FOG_TC}" "${RST_TC}"
|
|
printf '\n'
|
|
}
|
|
|
|
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
|
print_error() { echo -e "${RED}✗ $1${NC}"; }
|
|
print_warning() { echo -e "${YELLOW}! $1${NC}"; }
|
|
print_info() { echo -e "${BLUE}→ $1${NC}"; }
|
|
|
|
check_root() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
print_error "This command requires root privileges. Run with sudo."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_installed() {
|
|
if [ ! -d "$INSTALL_DIR" ]; then
|
|
print_error "ServerKit is not installed in $INSTALL_DIR"
|
|
print_info "Run the install script first"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_venv() {
|
|
# If the expected virtual environment is missing, try to locate an existing
|
|
# one from legacy/alternative layouts before giving up.
|
|
if [ ! -f "$VENV_DIR/bin/activate" ] || [ ! -x "$VENV_DIR/bin/python" ]; then
|
|
print_warning "Virtual environment missing at $VENV_DIR"
|
|
local candidate found_venv=""
|
|
for candidate in "$INSTALL_DIR/venv" "$INSTALL_DIR/backend/venv" \
|
|
"$INSTALL_DIR/backend/.venv" "$INSTALL_DIR/.venv"; do
|
|
if [ -f "$candidate/bin/activate" ] && [ -x "$candidate/bin/python" ]; then
|
|
found_venv="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
if [ -n "$found_venv" ] && [ "$found_venv" != "$VENV_DIR" ]; then
|
|
print_info "Using virtual environment at $found_venv"
|
|
VENV_DIR="$found_venv"
|
|
else
|
|
print_error "No usable Python virtual environment found. Run serverkit update to rebuild it."
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
run_cli() {
|
|
# Run CLI commands directly on host using the virtual environment
|
|
require_venv
|
|
cd "$INSTALL_DIR/backend"
|
|
source "$VENV_DIR/bin/activate"
|
|
python cli.py "$@"
|
|
}
|
|
|
|
# Version management
|
|
VERSION_FILE="$INSTALL_DIR/VERSION"
|
|
REMOTE_VERSION_URL="https://raw.githubusercontent.com/jhd3197/serverkit/main/VERSION"
|
|
|
|
get_local_version() {
|
|
if [ -f "$VERSION_FILE" ]; then
|
|
cat "$VERSION_FILE" | tr -d '\n\r '
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
get_remote_version() {
|
|
curl -sf "$REMOTE_VERSION_URL" 2>/dev/null | tr -d '\n\r ' || echo "unknown"
|
|
}
|
|
|
|
compare_versions() {
|
|
# Returns: 0 if equal, 1 if v1 > v2, 2 if v1 < v2
|
|
local v1="$1"
|
|
local v2="$2"
|
|
|
|
if [ "$v1" = "$v2" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Unparsable versions used to compare as "equal" (every [ -gt ] probe below
|
|
# failed silently under 2>/dev/null), reporting "up to date" on a box with
|
|
# a missing or corrupt VERSION file. Treat them as "older" instead so
|
|
# check-update points the operator at `serverkit update`, which re-validates.
|
|
local vre='^[0-9]+\.[0-9]+(\.[0-9]+)?$'
|
|
if ! [[ "$v1" =~ $vre ]] || ! [[ "$v2" =~ $vre ]]; then
|
|
return 2
|
|
fi
|
|
|
|
# Compare version components
|
|
local v1_major=$(echo "$v1" | cut -d. -f1)
|
|
local v1_minor=$(echo "$v1" | cut -d. -f2)
|
|
local v1_patch=$(echo "$v1" | cut -d. -f3)
|
|
|
|
local v2_major=$(echo "$v2" | cut -d. -f1)
|
|
local v2_minor=$(echo "$v2" | cut -d. -f2)
|
|
local v2_patch=$(echo "$v2" | cut -d. -f3)
|
|
|
|
# X.Y versions carry no patch component — compare it as 0.
|
|
v1_patch="${v1_patch:-0}"
|
|
v2_patch="${v2_patch:-0}"
|
|
|
|
if [ "$v1_major" -gt "$v2_major" ] 2>/dev/null; then return 1; fi
|
|
if [ "$v1_major" -lt "$v2_major" ] 2>/dev/null; then return 2; fi
|
|
if [ "$v1_minor" -gt "$v2_minor" ] 2>/dev/null; then return 1; fi
|
|
if [ "$v1_minor" -lt "$v2_minor" ] 2>/dev/null; then return 2; fi
|
|
if [ "$v1_patch" -gt "$v2_patch" ] 2>/dev/null; then return 1; fi
|
|
if [ "$v1_patch" -lt "$v2_patch" ] 2>/dev/null; then return 2; fi
|
|
|
|
return 0
|
|
}
|
|
|
|
cmd_version() {
|
|
local version=$(get_local_version)
|
|
echo "ServerKit version $version"
|
|
}
|
|
|
|
cmd_check_update() {
|
|
print_info "Checking for updates..."
|
|
|
|
local local_version=$(get_local_version)
|
|
local remote_version=$(get_remote_version)
|
|
|
|
echo ""
|
|
echo " Installed version: $local_version"
|
|
echo " Latest version: $remote_version"
|
|
echo ""
|
|
|
|
if [ "$remote_version" = "unknown" ]; then
|
|
print_warning "Could not check remote version (network error?)"
|
|
return 1
|
|
fi
|
|
|
|
set +e # Temporarily disable exit on error
|
|
compare_versions "$local_version" "$remote_version"
|
|
local result=$?
|
|
set -e # Re-enable exit on error
|
|
|
|
if [ $result -eq 2 ]; then
|
|
print_success "Update available! Run 'sudo serverkit update' to upgrade."
|
|
return 0
|
|
elif [ $result -eq 0 ]; then
|
|
print_success "You are running the latest version."
|
|
return 0
|
|
else
|
|
print_info "You are running a newer version than released."
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Commands
|
|
cmd_help() {
|
|
print_header
|
|
echo ""
|
|
echo "Usage: serverkit <command> [options]"
|
|
echo ""
|
|
echo "Service Commands:"
|
|
echo " start Start all services"
|
|
echo " stop Stop all services"
|
|
echo " restart Restart all services"
|
|
echo " status Show service status"
|
|
echo " logs [service] View logs (backend, frontend, or all)"
|
|
echo " update [options] Update ServerKit"
|
|
echo " --branch <name> Switch to specific branch (e.g., dev)"
|
|
echo " --release [vX.Y.Z] Update from release tarball"
|
|
echo " --force Force update even if already latest"
|
|
echo " --dry-run Show what would change without applying"
|
|
echo " branch Show current branch"
|
|
echo " uninstall Uninstall ServerKit (preserves data by default)"
|
|
echo " --purge / --delete-volumes also delete volumes, DB, data"
|
|
echo " --keep-data also keep /etc/serverkit"
|
|
echo " --yes skip the confirmation prompt"
|
|
echo " --dry-run show actions without changing anything"
|
|
echo ""
|
|
echo "User Management:"
|
|
echo " create-admin Create a new admin user"
|
|
echo " reset-password Reset a user's password"
|
|
echo " setup-code Show the code that creating the first admin requires"
|
|
echo " unlock-user Unlock a locked user account"
|
|
echo " list-users List all users"
|
|
echo " make-admin Promote user to admin"
|
|
echo " deactivate-user Deactivate a user account"
|
|
echo " activate-user Activate a user account"
|
|
echo ""
|
|
echo "Database Commands:"
|
|
echo " init-db Initialize the database"
|
|
echo " migrate-db Apply database migrations (alias: db-migrate [--no-backup])"
|
|
echo " db-status Show migration status (--json)"
|
|
echo " db-history List all migration revisions"
|
|
echo " backup-db Backup the database"
|
|
echo " restore-db Restore database from backup"
|
|
echo ""
|
|
echo "Cleanup Commands:"
|
|
echo " disk Show what is using disk space and reclaim it (interactive)"
|
|
echo " --safe free every item marked safe, no prompt"
|
|
echo " --all free every item, no prompt"
|
|
echo " --only <keys> comma-separated items, e.g. telemetry"
|
|
echo " --days <n> telemetry retention window (default 7)"
|
|
echo " --keep <n> upgrade snapshots to keep (default 1)"
|
|
echo " --older-than <n> only offer snapshots at least n days old"
|
|
echo " --dry-run report what would be freed, change nothing"
|
|
echo " --allow-restart permit a panel restart if VACUUM is blocked"
|
|
echo " cleanup-apps Delete all apps and containers"
|
|
echo " factory-reset Complete reset (delete everything)"
|
|
echo ""
|
|
echo "App Management:"
|
|
echo " create-app Show how to create apps (via web UI)"
|
|
echo " deploy-template Deploy a template to local or remote server"
|
|
echo " list-servers List deployment target servers"
|
|
echo " deployment-status Show deployment job status and logs"
|
|
echo " list-apps [--all] List deployed apps (--all shows Docker containers)"
|
|
echo " start-app Start an app"
|
|
echo " stop-app Stop an app"
|
|
echo " app-logs View app logs"
|
|
echo ""
|
|
echo "Site Management:"
|
|
echo " setup-proxy Setup host nginx as reverse proxy (run once)"
|
|
echo " add-site Add a new site/domain"
|
|
echo " list-sites List all configured sites"
|
|
echo " remove-site Remove a site"
|
|
echo ""
|
|
echo "Version Commands:"
|
|
echo " version Show current version"
|
|
echo " check-update Check if updates are available"
|
|
echo ""
|
|
echo "Diagnostics:"
|
|
echo " doctor Check for common issues"
|
|
echo " doctor --fix Auto-fix detected issues"
|
|
echo ""
|
|
echo "Panel Commands (talk to the running panel API; most support --json):"
|
|
echo " panel-status Panel health and version"
|
|
echo " panel-doctor Panel-side diagnostics ([--repair] [--yes])"
|
|
echo " repair Repair a single drift finding (repair <type> <id>)"
|
|
echo " services list | restart <name> — monitored system services"
|
|
echo " apps list — applications known to the panel"
|
|
echo " manifest plan | diff | apply — a project's serverkit.yaml"
|
|
echo " login-url Mint a one-time panel login link"
|
|
echo " support-bundle Build a scrubbed diagnostic zip"
|
|
echo " connect Pair with ServerKit Cloud (connect [status|doctor|disconnect])"
|
|
echo ""
|
|
echo "Utility Commands:"
|
|
echo " generate-keys Generate secure keys for .env"
|
|
echo " config Edit configuration"
|
|
echo " completion Print bash tab-completion (serverkit completion > /etc/bash_completion.d/serverkit)"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " sudo serverkit start"
|
|
echo " sudo serverkit create-admin"
|
|
echo " sudo serverkit add-site test.mysite.com 3000"
|
|
echo " serverkit logs backend"
|
|
echo ""
|
|
}
|
|
|
|
cmd_completion() {
|
|
# Print a bash tab-completion script for `serverkit`.
|
|
# Install: serverkit completion > /etc/bash_completion.d/serverkit
|
|
# Ad-hoc: eval "$(serverkit completion)"
|
|
# The word lists are static on purpose (no venv/Python needed to complete);
|
|
# scripts/test/test_cli.sh guards them against drifting from the dispatch.
|
|
cat <<'COMPLETION_EOF'
|
|
# bash completion for the serverkit CLI (generated by `serverkit completion`)
|
|
_serverkit() {
|
|
local cur commands
|
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
commands="help start stop restart status logs update branch version check-update uninstall create-admin reset-password setup-code unlock-user list-users make-admin deactivate-user activate-user init-db migrate-db db-migrate db-status db-history backup-db restore-db disk cleanup-apps factory-reset generate-keys config setup-proxy create-app deploy-template list-servers deployment-status list-apps start-app stop-app app-logs add-site list-sites remove-site doctor manifest services apps repair login-url support-bundle connect panel-status panel-doctor completion"
|
|
|
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
return
|
|
fi
|
|
|
|
case "${COMP_WORDS[1]}" in
|
|
manifest) COMPREPLY=( $(compgen -W "plan diff apply --project --file --json --yes" -- "$cur") ) ;;
|
|
services) COMPREPLY=( $(compgen -W "list restart --json" -- "$cur") ) ;;
|
|
apps) COMPREPLY=( $(compgen -W "list --json" -- "$cur") ) ;;
|
|
logs) COMPREPLY=( $(compgen -W "backend frontend all" -- "$cur") ) ;;
|
|
update) COMPREPLY=( $(compgen -W "--branch --release --force --dry-run" -- "$cur") ) ;;
|
|
uninstall) COMPREPLY=( $(compgen -W "--purge --delete-volumes --keep-data --yes --dry-run" -- "$cur") ) ;;
|
|
doctor) COMPREPLY=( $(compgen -W "--fix" -- "$cur") ) ;;
|
|
panel-doctor) COMPREPLY=( $(compgen -W "--repair --yes --json" -- "$cur") ) ;;
|
|
panel-status|list-users|list-servers|db-status) COMPREPLY=( $(compgen -W "--json" -- "$cur") ) ;;
|
|
disk) COMPREPLY=( $(compgen -W "--safe --all --only --days --keep --older-than --dry-run --allow-restart --yes --json" -- "$cur") ) ;;
|
|
cleanup-apps) COMPREPLY=( $(compgen -W "--delete-volumes --keep-db" -- "$cur") ) ;;
|
|
list-apps) COMPREPLY=( $(compgen -W "--all --json" -- "$cur") ) ;;
|
|
deployment-status) COMPREPLY=( $(compgen -W "--logs --json" -- "$cur") ) ;;
|
|
db-migrate|migrate-db) COMPREPLY=( $(compgen -W "--no-backup" -- "$cur") ) ;;
|
|
login-url) COMPREPLY=( $(compgen -W "--ttl --ip --user" -- "$cur") ) ;;
|
|
support-bundle) COMPREPLY=( $(compgen -W "--out --passphrase" -- "$cur") ) ;;
|
|
connect) COMPREPLY=( $(compgen -W "status doctor disconnect --cloud --json" -- "$cur") ) ;;
|
|
deploy-template) COMPREPLY=( $(compgen -W "--name --target --var --wait" -- "$cur") ) ;;
|
|
*) COMPREPLY=() ;;
|
|
esac
|
|
}
|
|
complete -F _serverkit serverkit
|
|
COMPLETION_EOF
|
|
}
|
|
|
|
cmd_start() {
|
|
check_root
|
|
check_installed
|
|
print_info "Starting ServerKit..."
|
|
|
|
# Start backend (systemd) — a partial install / non-systemd box must get a
|
|
# readable warning, not a raw set -e abort.
|
|
systemctl start $BACKEND_SERVICE || print_warning "Could not start backend service '$BACKEND_SERVICE' (is systemd available?)"
|
|
|
|
# Frontend: static files need nginx up; only the legacy layout has a container.
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend up -d || print_warning "Could not start frontend container (is Docker running?)"
|
|
else
|
|
systemctl start nginx || print_warning "Could not start nginx (it serves the frontend)"
|
|
fi
|
|
|
|
sleep 3
|
|
print_success "ServerKit started"
|
|
cmd_status_brief
|
|
}
|
|
|
|
cmd_stop() {
|
|
check_root
|
|
check_installed
|
|
print_info "Stopping ServerKit..."
|
|
|
|
# Stop backend (systemd)
|
|
systemctl stop $BACKEND_SERVICE || true
|
|
|
|
# Legacy frontend container only. nginx stays up on a host install: it
|
|
# also fronts every managed site, not just the panel.
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend stop || true
|
|
fi
|
|
|
|
print_success "ServerKit stopped"
|
|
}
|
|
|
|
cmd_restart() {
|
|
check_root
|
|
check_installed
|
|
print_info "Restarting ServerKit..."
|
|
|
|
# Restart backend (systemd) — mirror cmd_stop's guards: a failing service
|
|
# manager is reported, never a raw abort.
|
|
systemctl restart $BACKEND_SERVICE || print_warning "Could not restart backend service '$BACKEND_SERVICE' (is systemd available?)"
|
|
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend restart || print_warning "Could not restart frontend container (is Docker running?)"
|
|
else
|
|
systemctl reload nginx || print_warning "Could not reload nginx (it serves the frontend)"
|
|
fi
|
|
|
|
sleep 3
|
|
print_success "ServerKit restarted"
|
|
cmd_status_brief
|
|
}
|
|
|
|
cmd_status_brief() {
|
|
# Quick status check
|
|
echo ""
|
|
|
|
# Backend status
|
|
if systemctl is-active --quiet $BACKEND_SERVICE; then
|
|
print_success "Backend: running"
|
|
else
|
|
print_error "Backend: stopped"
|
|
fi
|
|
|
|
# Frontend status
|
|
case "$(frontend_mode)" in
|
|
container)
|
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
|
|
print_success "Frontend: running (container)"
|
|
else
|
|
print_error "Frontend: stopped (container)"
|
|
fi
|
|
;;
|
|
*)
|
|
if systemctl is-active --quiet nginx 2>/dev/null; then
|
|
print_success "Frontend: served by nginx"
|
|
else
|
|
print_error "Frontend: nginx is not running"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# API health
|
|
if curl -s http://127.0.0.1:5000/api/v1/system/health > /dev/null 2>&1; then
|
|
print_success "API: healthy"
|
|
else
|
|
print_warning "API: not responding"
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
check_installed
|
|
print_header
|
|
echo ""
|
|
|
|
echo "Backend Service (systemd):"
|
|
echo "─────────────────────────────"
|
|
systemctl status $BACKEND_SERVICE --no-pager -l 2>/dev/null || print_warning "Backend service not found"
|
|
|
|
echo ""
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
echo "Frontend Container (Docker):"
|
|
echo "─────────────────────────────"
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend ps 2>/dev/null || print_warning "Frontend container not found"
|
|
else
|
|
echo "Frontend (static, served by nginx):"
|
|
echo "─────────────────────────────"
|
|
echo " root: $(frontend_static_root 2>/dev/null || echo "$INSTALL_DIR/frontend/dist")"
|
|
systemctl status nginx --no-pager -l 2>/dev/null | head -5 || print_warning "nginx service not found"
|
|
fi
|
|
|
|
echo ""
|
|
cmd_status_brief
|
|
}
|
|
|
|
cmd_logs() {
|
|
check_installed
|
|
service="${1:-all}"
|
|
|
|
case "$service" in
|
|
backend)
|
|
if command -v journalctl >/dev/null 2>&1; then
|
|
journalctl -u $BACKEND_SERVICE -f || print_warning "Could not read the backend journal (is journald running?)"
|
|
else
|
|
print_warning "journalctl not found — cannot stream backend logs on this system"
|
|
fi
|
|
;;
|
|
frontend)
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend logs -f || print_warning "Could not read frontend container logs (is Docker running?)"
|
|
else
|
|
print_info "The frontend is static files served by nginx; following nginx logs..."
|
|
tail -f /var/log/nginx/access.log /var/log/nginx/error.log 2>/dev/null \
|
|
|| print_warning "Could not read /var/log/nginx/*.log"
|
|
fi
|
|
;;
|
|
all|*)
|
|
print_info "Showing backend logs (Ctrl+C to switch to frontend)..."
|
|
echo ""
|
|
# Show both in split view isn't easy in bash, so show backend first.
|
|
# A journal-less box must still fall through to the Docker logs
|
|
# below instead of dying here.
|
|
if command -v journalctl >/dev/null 2>&1; then
|
|
journalctl -u $BACKEND_SERVICE -n 50 --no-pager || print_warning "Could not read the backend journal"
|
|
else
|
|
print_warning "journalctl not found — skipping backend logs"
|
|
fi
|
|
echo ""
|
|
if [ "$(frontend_mode)" = container ]; then
|
|
print_info "Showing frontend logs..."
|
|
echo ""
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend logs --tail=50 || print_warning "Could not read frontend container logs (is Docker running?)"
|
|
else
|
|
print_info "Showing nginx logs (the frontend is static files served by nginx)..."
|
|
echo ""
|
|
tail -n 50 /var/log/nginx/error.log 2>/dev/null || print_warning "Could not read /var/log/nginx/error.log"
|
|
fi
|
|
echo ""
|
|
print_info "For live logs, use: serverkit logs backend OR serverkit logs frontend"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_branch() {
|
|
check_installed
|
|
cd "$INSTALL_DIR"
|
|
|
|
local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
local current_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
|
|
echo ""
|
|
echo " Current branch: $current_branch"
|
|
echo " Current commit: $current_commit"
|
|
echo " Version: $(get_local_version)"
|
|
echo ""
|
|
|
|
# Show available remote branches
|
|
print_info "Available branches:"
|
|
git branch -r 2>/dev/null | grep -v HEAD | sed 's/origin\// /' | head -10
|
|
echo ""
|
|
}
|
|
|
|
update_usage() {
|
|
echo "Usage: serverkit update [options]"
|
|
echo " --branch <name> Switch to specific branch (e.g., dev)"
|
|
echo " --release [vX.Y.Z] Update from release tarball"
|
|
echo " --force Force update even if already latest"
|
|
echo " --dry-run Show what would change without applying"
|
|
}
|
|
|
|
cmd_update() {
|
|
local update_args=()
|
|
|
|
# Parse arguments and forward them to the canonical update script.
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--force|-f)
|
|
update_args+=("--force")
|
|
shift
|
|
;;
|
|
--branch|-b)
|
|
# A missing value used to make `shift 2` fail and exit the CLI
|
|
# silently under set -e. Guard it like --release below.
|
|
if [[ -n "${2:-}" ]] && [[ ! "$2" =~ ^- ]]; then
|
|
update_args+=("--branch" "$2")
|
|
shift 2
|
|
else
|
|
print_error "--branch requires a branch name (e.g. --branch dev)"
|
|
exit 1
|
|
fi
|
|
;;
|
|
--release|-r)
|
|
update_args+=("--release")
|
|
if [[ -n "${2:-}" ]] && [[ ! "$2" =~ ^- ]]; then
|
|
update_args+=("$2")
|
|
shift 2
|
|
else
|
|
shift
|
|
fi
|
|
;;
|
|
--dry-run|-n)
|
|
update_args+=("--dry-run")
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
update_usage
|
|
return 0
|
|
;;
|
|
*)
|
|
# An unknown flag used to be dropped and the update ran
|
|
# anyway — `update --help` updated the box.
|
|
print_error "Unknown option for update: $1"
|
|
update_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
check_root
|
|
check_installed
|
|
print_header
|
|
echo ""
|
|
|
|
# -f, not -x: scripts are tracked 0644 in git and invoked via `bash`, so
|
|
# requiring the executable bit wrongly fails every update with
|
|
# "Updater not found" on a perfectly good install.
|
|
if [ ! -f "$INSTALL_DIR/scripts/update.sh" ]; then
|
|
print_error "Updater not found at $INSTALL_DIR/scripts/update.sh"
|
|
exit 1
|
|
fi
|
|
|
|
SERVERKIT_DIR="$INSTALL_DIR" SERVERKIT_VENV_DIR="$VENV_DIR" \
|
|
bash "$INSTALL_DIR/scripts/update.sh" "${update_args[@]}"
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
check_root
|
|
|
|
# Flags mirror uninstall.sh so both entry points accept the same options.
|
|
local purge=0 keep_data=0 assume_yes=0 dry_run=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--purge|--delete-volumes) purge=1; shift ;;
|
|
--keep-data) keep_data=1; shift ;;
|
|
--yes|-y) assume_yes=1; shift ;;
|
|
--dry-run|-n) dry_run=1; shift ;;
|
|
--help|-h)
|
|
echo "Usage: serverkit uninstall [--purge|--delete-volumes] [--keep-data] [--yes] [--dry-run]"
|
|
return 0 ;;
|
|
*)
|
|
print_error "Unknown option for uninstall: $1"
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
print_header
|
|
echo ""
|
|
if [ "$purge" = "1" ]; then
|
|
print_warning "This will remove ServerKit AND delete all data (volumes, database, backups)."
|
|
else
|
|
print_warning "This will remove ServerKit. User data (volumes, /var/lib, backups) is preserved."
|
|
echo " Use --purge to also delete data, or --keep-data to also keep /etc/serverkit."
|
|
fi
|
|
[ "$dry_run" = "1" ] && print_warning "DRY RUN — nothing will actually be changed."
|
|
echo ""
|
|
|
|
if [ "$assume_yes" != "1" ]; then
|
|
# EOF (no tty / piped input) must not abort under set -e; an empty
|
|
# REPLY falls through to the safe "No" path below.
|
|
read -p "Are you sure? (y/N) " -n 1 -r || true
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
print_warning "Uninstall cancelled"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local lib="$INSTALL_DIR/scripts/lib/uninstall.sh"
|
|
if [ ! -f "$lib" ]; then
|
|
# Remnants-only box: the install tree (and the shared lib) is gone but
|
|
# traces remain. Don't duplicate the teardown here — point at the
|
|
# standalone uninstaller, which carries its own inline fallback.
|
|
print_error "Canonical uninstall routine not found at $lib"
|
|
echo ""
|
|
if [ -f "$INSTALL_DIR/uninstall.sh" ]; then
|
|
print_info "Run the standalone uninstaller instead:"
|
|
echo " sudo bash $INSTALL_DIR/uninstall.sh"
|
|
else
|
|
print_info "Fetch and run the standalone uninstaller instead:"
|
|
echo " curl -fsSL https://raw.githubusercontent.com/jhd3197/serverkit/main/uninstall.sh | sudo bash"
|
|
echo " (pass flags with: ... | sudo bash -s -- --purge --yes)"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# Source the shared routine into memory *before* it starts deleting the
|
|
# install tree (which is where the file lives), then step out of that tree.
|
|
# shellcheck source=/dev/null
|
|
source "$lib"
|
|
cd /tmp 2>/dev/null || cd / || true
|
|
|
|
export SERVERKIT_DIR="$INSTALL_DIR"
|
|
export SERVERKIT_PURGE="$purge"
|
|
export SERVERKIT_KEEP_DATA="$keep_data"
|
|
export SERVERKIT_UNINSTALL_DRY_RUN="$dry_run"
|
|
serverkit_uninstall_core
|
|
|
|
print_success "ServerKit uninstalled"
|
|
echo ""
|
|
echo "Note: System packages (Docker, Node.js, nginx, Python) were NOT removed."
|
|
echo "Remove them manually if no longer needed."
|
|
echo ""
|
|
echo "To reinstall, run:"
|
|
echo " cd ~ && curl -fsSL https://serverkit.ai/install.sh | sudo bash"
|
|
}
|
|
|
|
cmd_create_admin() {
|
|
check_installed
|
|
print_info "Creating admin user..."
|
|
run_cli create-admin "$@"
|
|
}
|
|
|
|
cmd_reset_password() {
|
|
check_installed
|
|
print_info "Resetting password..."
|
|
run_cli reset-password "$@"
|
|
}
|
|
|
|
cmd_setup_code() {
|
|
check_installed
|
|
run_cli setup-code
|
|
}
|
|
|
|
cmd_unlock_user() {
|
|
check_installed
|
|
run_cli unlock-user "$@"
|
|
}
|
|
|
|
cmd_list_users() {
|
|
check_installed
|
|
run_cli list-users "$@"
|
|
}
|
|
|
|
cmd_make_admin() {
|
|
check_installed
|
|
run_cli make-admin "$@"
|
|
}
|
|
|
|
cmd_deactivate_user() {
|
|
check_installed
|
|
run_cli deactivate-user "$@"
|
|
}
|
|
|
|
cmd_activate_user() {
|
|
check_installed
|
|
run_cli activate-user "$@"
|
|
}
|
|
|
|
cmd_init_db() {
|
|
check_installed
|
|
run_cli init-db
|
|
}
|
|
|
|
cmd_migrate_db() {
|
|
check_installed
|
|
# The Click verb is `db-migrate`; `migrate-db` is the legacy wrapper name.
|
|
run_cli db-migrate
|
|
}
|
|
|
|
cmd_panel_cli() {
|
|
# Forward a command verbatim to the Python admin CLI (backend/cli.py) so
|
|
# every Click verb is reachable as `serverkit <verb> …` without a wrapper
|
|
# function per command (that pattern is how migrate-db silently broke).
|
|
check_installed
|
|
run_cli "$@"
|
|
}
|
|
|
|
cmd_cleanup_apps() {
|
|
check_root
|
|
check_installed
|
|
|
|
DELETE_VOLUMES=""
|
|
KEEP_DB=""
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--delete-volumes|-v)
|
|
DELETE_VOLUMES="--delete-volumes"
|
|
shift
|
|
;;
|
|
--keep-db|-k)
|
|
KEEP_DB="--keep-db"
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
run_cli cleanup-apps --help
|
|
return 0
|
|
;;
|
|
*)
|
|
print_error "Unknown option for cleanup-apps: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
run_cli cleanup-apps $DELETE_VOLUMES $KEEP_DB
|
|
}
|
|
|
|
cmd_disk() {
|
|
# Reclaim disk space. Root because the reclaimers touch /var/backups,
|
|
# /tmp staging, the journal and the package caches.
|
|
check_root
|
|
check_installed
|
|
run_cli disk "$@"
|
|
}
|
|
|
|
cmd_factory_reset() {
|
|
check_root
|
|
check_installed
|
|
run_cli factory-reset
|
|
}
|
|
|
|
cmd_backup_db() {
|
|
check_installed
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
BACKUP_FILE="$BACKUP_DIR/serverkit-$(date +%Y%m%d-%H%M%S).db"
|
|
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
if [ -f "$DB_FILE" ]; then
|
|
cp "$DB_FILE" "$BACKUP_FILE"
|
|
print_success "Database backed up to $BACKUP_FILE"
|
|
else
|
|
print_error "Database file not found: $DB_FILE"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_restore_db() {
|
|
check_root
|
|
check_installed
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
|
|
|
|
if [ -z "$1" ]; then
|
|
echo "Available backups:"
|
|
ls -la "$BACKUP_DIR"/*.db 2>/dev/null || echo "No backups found"
|
|
echo ""
|
|
read -p "Enter backup file path: " BACKUP_FILE || true
|
|
else
|
|
BACKUP_FILE="$1"
|
|
fi
|
|
|
|
if [ ! -f "$BACKUP_FILE" ]; then
|
|
print_error "Backup file not found: $BACKUP_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
print_warning "This will overwrite the current database!"
|
|
read -p "Continue? (y/N) " -n 1 -r || true
|
|
echo
|
|
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
# Stop backend before restore
|
|
systemctl stop $BACKEND_SERVICE
|
|
|
|
cp "$BACKUP_FILE" "$DB_FILE"
|
|
|
|
# Restart backend
|
|
systemctl start $BACKEND_SERVICE
|
|
|
|
print_success "Database restored"
|
|
fi
|
|
}
|
|
|
|
cmd_generate_keys() {
|
|
print_header
|
|
echo ""
|
|
echo "Add these to your .env file:"
|
|
echo ""
|
|
SECRET_KEY=$(openssl rand -hex 32)
|
|
JWT_SECRET_KEY=$(openssl rand -hex 32)
|
|
SERVERKIT_ENCRYPTION_KEY=$(python3 -c 'import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')
|
|
echo "SECRET_KEY=$SECRET_KEY"
|
|
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY"
|
|
echo "SERVERKIT_ENCRYPTION_KEY=$SERVERKIT_ENCRYPTION_KEY"
|
|
echo ""
|
|
}
|
|
|
|
cmd_config() {
|
|
check_installed
|
|
${EDITOR:-nano} "$INSTALL_DIR/.env"
|
|
print_info "Restart ServerKit to apply changes: sudo serverkit restart"
|
|
}
|
|
|
|
cmd_setup_proxy() {
|
|
check_root
|
|
check_installed
|
|
print_info "Setting up host nginx as reverse proxy..."
|
|
|
|
if [ -f "$INSTALL_DIR/scripts/setup-nginx-proxy.sh" ]; then
|
|
chmod +x "$INSTALL_DIR/scripts/setup-nginx-proxy.sh"
|
|
"$INSTALL_DIR/scripts/setup-nginx-proxy.sh"
|
|
else
|
|
print_error "Setup script not found. Run 'serverkit update' first."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_create_app() {
|
|
print_header
|
|
echo ""
|
|
echo "To create a new app, use the ServerKit web interface:"
|
|
echo ""
|
|
echo " 1. Open ServerKit in your browser"
|
|
echo " 2. Go to Templates in the sidebar"
|
|
echo " 3. Choose from 60+ available templates:"
|
|
echo " - PHP Application (custom PHP projects)"
|
|
echo " - Python Application (Flask, FastAPI, Django)"
|
|
echo " - Node.js Application (Express, Next.js)"
|
|
echo " - WordPress, Ghost, Nextcloud, and many more"
|
|
echo " 4. Click Install and configure your app"
|
|
echo ""
|
|
echo "The web interface provides:"
|
|
echo " ✓ One-click installation"
|
|
echo " ✓ Automatic port assignment"
|
|
echo " ✓ Configuration UI"
|
|
echo " ✓ Update management"
|
|
echo ""
|
|
}
|
|
|
|
cmd_deploy_template() {
|
|
check_installed
|
|
shift
|
|
run_cli deploy-template "$@"
|
|
}
|
|
|
|
cmd_list_servers() {
|
|
check_installed
|
|
run_cli list-servers "$@"
|
|
}
|
|
|
|
cmd_deployment_status() {
|
|
check_installed
|
|
shift
|
|
run_cli deployment-status "$@"
|
|
}
|
|
|
|
cmd_list_apps() {
|
|
check_installed
|
|
# Forward flags verbatim (--all, --json, …); -a stays as the legacy alias.
|
|
local arg args=()
|
|
for arg in "$@"; do
|
|
[ "$arg" = "-a" ] && arg="--all"
|
|
args+=("$arg")
|
|
done
|
|
run_cli list-apps "${args[@]}"
|
|
}
|
|
|
|
cmd_start_app() {
|
|
check_root
|
|
NAME="$1"
|
|
|
|
if [ -z "$NAME" ]; then
|
|
echo "Usage: serverkit start-app <name>"
|
|
cmd_list_apps
|
|
exit 1
|
|
fi
|
|
|
|
APP_DIR="/var/www/$NAME"
|
|
if [ ! -d "$APP_DIR" ]; then
|
|
print_error "App not found: $NAME"
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Starting $NAME..."
|
|
cd "$APP_DIR"
|
|
docker compose up -d
|
|
print_success "$NAME started"
|
|
}
|
|
|
|
cmd_stop_app() {
|
|
check_root
|
|
NAME="$1"
|
|
|
|
if [ -z "$NAME" ]; then
|
|
echo "Usage: serverkit stop-app <name>"
|
|
cmd_list_apps
|
|
exit 1
|
|
fi
|
|
|
|
APP_DIR="/var/www/$NAME"
|
|
if [ ! -d "$APP_DIR" ]; then
|
|
print_error "App not found: $NAME"
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Stopping $NAME..."
|
|
cd "$APP_DIR"
|
|
docker compose down
|
|
print_success "$NAME stopped"
|
|
}
|
|
|
|
cmd_app_logs() {
|
|
NAME="$1"
|
|
|
|
if [ -z "$NAME" ]; then
|
|
echo "Usage: serverkit app-logs <name>"
|
|
cmd_list_apps
|
|
exit 1
|
|
fi
|
|
|
|
APP_DIR="/var/www/$NAME"
|
|
if [ ! -d "$APP_DIR" ]; then
|
|
print_error "App not found: $NAME"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$APP_DIR"
|
|
docker compose logs -f
|
|
}
|
|
|
|
# Resolve the nginx vhost layout: Debian-style sites-available/sites-enabled
|
|
# when present, otherwise the conf.d layout used by the RHEL family. Sets
|
|
# SITE_FILE (where the vhost lives) and SITE_LINK (the sites-enabled symlink;
|
|
# empty in conf.d layouts, where the vhost file itself is live).
|
|
resolve_site_paths() {
|
|
local domain="$1"
|
|
if [ -d "$NGINX_DIR/sites-available" ]; then
|
|
SITE_FILE="$NGINX_DIR/sites-available/${domain}.conf"
|
|
SITE_LINK="$NGINX_DIR/sites-enabled/${domain}.conf"
|
|
else
|
|
SITE_FILE="$NGINX_DIR/conf.d/${domain}.conf"
|
|
SITE_LINK=""
|
|
fi
|
|
}
|
|
|
|
# Reload nginx if we can; on non-systemd or stopped-nginx boxes this is an
|
|
# observation, not a fatal error — the config is already on disk either way.
|
|
reload_nginx_or_warn() {
|
|
if systemctl reload nginx 2>/dev/null || nginx -s reload 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
print_warning "Could not reload nginx — apply the change manually: systemctl reload nginx (or nginx -s reload)"
|
|
}
|
|
|
|
cmd_add_site() {
|
|
check_root
|
|
|
|
DOMAIN="$1"
|
|
PORT="${2:-3000}"
|
|
|
|
if [ -z "$DOMAIN" ]; then
|
|
echo "Usage: serverkit add-site <domain> [port]"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " serverkit add-site test.builditdesign.com"
|
|
echo " serverkit add-site myapp.builditdesign.com 3000"
|
|
exit 1
|
|
fi
|
|
|
|
resolve_site_paths "$DOMAIN"
|
|
|
|
if [ -f "$SITE_FILE" ]; then
|
|
print_warning "Site config already exists: $SITE_FILE"
|
|
read -p "Overwrite? (y/N) " -n 1 -r || true
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
print_info "Creating site config for $DOMAIN..."
|
|
mkdir -p "$(dirname "$SITE_FILE")"
|
|
|
|
cat > "$SITE_FILE" << EOF
|
|
# Site: $DOMAIN
|
|
# Created: $(date)
|
|
|
|
server {
|
|
listen 80;
|
|
server_name $DOMAIN;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:$PORT;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header Upgrade \$http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
}
|
|
}
|
|
EOF
|
|
|
|
# Enable the site (Debian layout only — conf.d configs are live as written)
|
|
if [ -n "$SITE_LINK" ]; then
|
|
mkdir -p "$(dirname "$SITE_LINK")"
|
|
ln -sf "$SITE_FILE" "$SITE_LINK"
|
|
fi
|
|
|
|
# Test and reload nginx. A stopped nginx (or a non-systemd box) must not
|
|
# raw-abort here — the site file is already written and valid.
|
|
if nginx -t 2>/dev/null; then
|
|
reload_nginx_or_warn
|
|
print_success "Site added: $DOMAIN → localhost:$PORT"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Add DNS record in Cloudflare:"
|
|
echo " Type: A, Name: ${DOMAIN%%.*}, Content: $(curl -s ifconfig.me)"
|
|
echo " 2. Start your app on port $PORT"
|
|
echo ""
|
|
else
|
|
print_error "Nginx config error. Check: $SITE_FILE"
|
|
if [ -n "$SITE_LINK" ]; then
|
|
rm -f "$SITE_LINK"
|
|
elif mv -f "$SITE_FILE" "${SITE_FILE}.disabled" 2>/dev/null; then
|
|
# conf.d layouts have no enabled/available split — park the broken
|
|
# file so it can't poison an unrelated nginx reload.
|
|
print_info "Broken config parked at: ${SITE_FILE}.disabled"
|
|
else
|
|
print_warning "Remove or fix $SITE_FILE before reloading nginx"
|
|
fi
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_list_sites() {
|
|
print_header
|
|
echo ""
|
|
echo "Configured Sites:"
|
|
echo "─────────────────────────────────────────"
|
|
|
|
# Debian layout lists sites-enabled; conf.d layouts (RHEL family) list conf.d.
|
|
local sites_dir="$NGINX_DIR/sites-enabled"
|
|
if [ ! -d "$sites_dir" ]; then
|
|
sites_dir="$NGINX_DIR/conf.d"
|
|
fi
|
|
|
|
if [ -d "$sites_dir" ]; then
|
|
for site in "$sites_dir"/*.conf; do
|
|
if [ -f "$site" ]; then
|
|
# `|| true`: a vhost without server_name/proxy_pass is data,
|
|
# not an error — never abort the listing under set -e/pipefail.
|
|
DOMAIN=$(grep -m1 "server_name" "$site" | awk '{print $2}' | tr -d ';' || true)
|
|
# Portable port extraction (GNU-only `grep -oP` broke busybox)
|
|
PORT=$(grep -m1 "proxy_pass" "$site" | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
|
|
if [ -z "$PORT" ]; then
|
|
PORT="static"
|
|
fi
|
|
ENABLED="✓"
|
|
printf " %s %-35s → port %s\n" "$ENABLED" "$DOMAIN" "$PORT"
|
|
fi
|
|
done
|
|
else
|
|
echo " No sites configured. Run 'serverkit setup-proxy' first."
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
cmd_doctor() {
|
|
check_installed
|
|
print_header
|
|
echo ""
|
|
print_info "Running diagnostics..."
|
|
echo ""
|
|
|
|
ISSUES=0
|
|
FIXED=0
|
|
FIX_MODE="${1:-}" # --fix to auto-fix
|
|
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
check_root
|
|
fi
|
|
|
|
doctor_issue() {
|
|
ISSUES=$((ISSUES + 1))
|
|
print_error "$1"
|
|
}
|
|
|
|
doctor_ok() {
|
|
print_success "$1"
|
|
}
|
|
|
|
doctor_fixed() {
|
|
FIXED=$((FIXED + 1))
|
|
print_success "FIXED: $1"
|
|
}
|
|
|
|
doctor_skip() {
|
|
print_warning "SKIP: $1 (run with --fix to auto-fix)"
|
|
}
|
|
|
|
# For issues doctor can diagnose but must not fix on its own (a missing
|
|
# release bundle, a layout it cannot tell apart): say what to run instead
|
|
# of promising an auto-fix that does not exist.
|
|
doctor_hint() {
|
|
echo " Try: $1"
|
|
}
|
|
|
|
# ── 1. Backend service ──────────────────────────────────────
|
|
echo "Backend"
|
|
echo "───────────────────────────────────────"
|
|
|
|
if systemctl is-active --quiet $BACKEND_SERVICE 2>/dev/null; then
|
|
doctor_ok "systemd service is running"
|
|
else
|
|
doctor_issue "systemd service is not running"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
systemctl start $BACKEND_SERVICE 2>/dev/null && doctor_fixed "Started backend service" || print_error " Could not start service"
|
|
else
|
|
doctor_skip "systemctl start $BACKEND_SERVICE"
|
|
fi
|
|
fi
|
|
|
|
if curl -sf http://127.0.0.1:5000/api/v1/system/health > /dev/null 2>&1; then
|
|
doctor_ok "API responding on port 5000"
|
|
else
|
|
if systemctl is-active --quiet $BACKEND_SERVICE 2>/dev/null; then
|
|
doctor_issue "Service running but API not responding on port 5000"
|
|
echo " Check logs: journalctl -u serverkit -n 30"
|
|
else
|
|
doctor_issue "API not responding (service is down)"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── 2. Frontend ─────────────────────────────────────────────
|
|
# Two layouts exist (see frontend_mode). Check the one this box actually
|
|
# uses; never "fix" a static install by starting containers.
|
|
FRONTEND_RUNNING=false
|
|
FRONTEND_PORT=""
|
|
FRONTEND_MODE="$(frontend_mode)"
|
|
|
|
echo "Frontend"
|
|
echo "───────────────────────────────────────"
|
|
|
|
case "$FRONTEND_MODE" in
|
|
static)
|
|
STATIC_ROOT="$(frontend_static_root 2>/dev/null || true)"
|
|
[ -n "$STATIC_ROOT" ] || STATIC_ROOT="$INSTALL_DIR/frontend/dist"
|
|
if [ -f "$STATIC_ROOT/index.html" ]; then
|
|
doctor_ok "Static bundle present, served by nginx from $STATIC_ROOT"
|
|
else
|
|
doctor_issue "Frontend bundle missing: $STATIC_ROOT/index.html"
|
|
echo " The host install serves a prebuilt bundle through nginx; there is no container to start."
|
|
doctor_hint "serverkit update (reinstalls the release, bundle included)"
|
|
fi
|
|
# A leftover legacy container is harmless but wastes a port and confuses
|
|
# port-mismatch checks; point at it, do not touch it.
|
|
if command -v docker >/dev/null 2>&1 \
|
|
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
|
|
print_warning "Container '$FRONTEND_CONTAINER' is running but nginx serves the frontend from disk; it is unused"
|
|
doctor_hint "cd $INSTALL_DIR && docker compose --profile legacy-frontend stop frontend"
|
|
fi
|
|
;;
|
|
container)
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
doctor_issue "nginx proxies the frontend to a container but Docker is not installed or not in PATH"
|
|
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
|
|
FRONTEND_RUNNING=true
|
|
# Get the actual host port the container is mapped to
|
|
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
|
|
doctor_ok "Container running (port $FRONTEND_PORT)"
|
|
else
|
|
doctor_issue "Container '$FRONTEND_CONTAINER' is not running"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
cd "$INSTALL_DIR"
|
|
compose_frontend up -d 2>/dev/null && {
|
|
sleep 3
|
|
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
|
|
FRONTEND_RUNNING=true
|
|
doctor_fixed "Started frontend container (port $FRONTEND_PORT)"
|
|
} || print_error " Could not start container"
|
|
else
|
|
doctor_skip "docker compose --profile legacy-frontend up -d frontend"
|
|
fi
|
|
fi
|
|
|
|
if [ "$FRONTEND_RUNNING" = true ] && [ -n "$FRONTEND_PORT" ]; then
|
|
if curl -sf "http://127.0.0.1:$FRONTEND_PORT" > /dev/null 2>&1; then
|
|
doctor_ok "Frontend responding on port $FRONTEND_PORT"
|
|
else
|
|
doctor_issue "Container running but not responding on port $FRONTEND_PORT"
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
doctor_issue "Cannot tell how the frontend is served: no serverkit vhost under $NGINX_DIR and no $INSTALL_DIR/frontend/dist"
|
|
doctor_hint "serverkit update (reinstalls the release and its nginx config)"
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
|
|
# ── 3. Nginx ────────────────────────────────────────────────
|
|
echo "Nginx"
|
|
echo "───────────────────────────────────────"
|
|
|
|
if systemctl is-active --quiet nginx 2>/dev/null; then
|
|
doctor_ok "nginx is running"
|
|
else
|
|
doctor_issue "nginx is not running"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
systemctl start nginx 2>/dev/null && doctor_fixed "Started nginx" || print_error " Could not start nginx"
|
|
else
|
|
doctor_skip "systemctl start nginx"
|
|
fi
|
|
fi
|
|
|
|
if nginx -t 2>/dev/null; then
|
|
doctor_ok "nginx config syntax OK"
|
|
else
|
|
doctor_issue "nginx config has errors"
|
|
echo " Run: nginx -t"
|
|
fi
|
|
|
|
# Scan ALL enabled nginx configs for competing server_name _
|
|
# Only serverkit.conf should use server_name _ (the catch-all panel block)
|
|
for SITE_CONF in "$NGINX_DIR/sites-enabled"/*; do
|
|
[ -f "$SITE_CONF" ] || continue
|
|
SITE_NAME=$(basename "$SITE_CONF")
|
|
# Skip all ServerKit-managed configs
|
|
case "$SITE_NAME" in serverkit-*|serverkit.conf) continue ;; esac
|
|
|
|
if grep -Eq 'server_name[[:space:]]+_;' "$SITE_CONF" 2>/dev/null; then
|
|
doctor_issue "'$SITE_NAME' uses server_name _ (conflicts with panel, can hijack all traffic)"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
rm -f "$NGINX_DIR/sites-enabled/$SITE_NAME"
|
|
rm -f "$NGINX_DIR/sites-available/$SITE_NAME"
|
|
doctor_fixed "Removed conflicting '$SITE_NAME'"
|
|
else
|
|
doctor_skip "Remove conflicting '$SITE_NAME' from sites-enabled"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Ensure serverkit-locations directory exists
|
|
if [ ! -d "$NGINX_DIR/serverkit-locations" ]; then
|
|
print_warning "Missing $NGINX_DIR/serverkit-locations directory"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
mkdir -p "$NGINX_DIR/serverkit-locations"
|
|
doctor_fixed "Created $NGINX_DIR/serverkit-locations"
|
|
else
|
|
doctor_skip "mkdir -p $NGINX_DIR/serverkit-locations"
|
|
fi
|
|
fi
|
|
|
|
# Check for port mismatch between nginx and frontend container
|
|
NGINX_CONF="$NGINX_DIR/sites-available/serverkit.conf"
|
|
if [ -f "$NGINX_CONF" ]; then
|
|
NGINX_FRONTEND_PORT=$(grep -m1 'proxy_pass.*127.0.0.1' "$NGINX_CONF" | head -1 | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
|
|
|
|
if [ -n "$FRONTEND_PORT" ] && [ -n "$NGINX_FRONTEND_PORT" ]; then
|
|
if [ "$NGINX_FRONTEND_PORT" = "$FRONTEND_PORT" ]; then
|
|
doctor_ok "nginx proxy port ($NGINX_FRONTEND_PORT) matches frontend container ($FRONTEND_PORT)"
|
|
else
|
|
doctor_issue "PORT MISMATCH: nginx proxies to :$NGINX_FRONTEND_PORT but frontend is on :$FRONTEND_PORT"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
sed -i "s|proxy_pass http://127.0.0.1:${NGINX_FRONTEND_PORT}|proxy_pass http://127.0.0.1:${FRONTEND_PORT}|" "$NGINX_CONF"
|
|
if nginx -t 2>/dev/null; then
|
|
nginx -s reload 2>/dev/null
|
|
doctor_fixed "Updated nginx to proxy to port $FRONTEND_PORT"
|
|
else
|
|
print_error " nginx config broken after edit — reverting"
|
|
sed -i "s|proxy_pass http://127.0.0.1:${FRONTEND_PORT}|proxy_pass http://127.0.0.1:${NGINX_FRONTEND_PORT}|" "$NGINX_CONF"
|
|
fi
|
|
else
|
|
doctor_skip "Update nginx proxy_pass from :$NGINX_FRONTEND_PORT to :$FRONTEND_PORT"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check API proxy port
|
|
NGINX_API_PORT=$(grep -A1 'location /api' "$NGINX_CONF" | grep proxy_pass | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' | head -1 || true)
|
|
if [ -n "$NGINX_API_PORT" ]; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${NGINX_API_PORT}.*gunicorn\|:${NGINX_API_PORT}.*python"; then
|
|
doctor_ok "nginx API proxy port ($NGINX_API_PORT) has backend listening"
|
|
else
|
|
doctor_issue "nginx proxies API to :$NGINX_API_PORT but nothing is listening there"
|
|
fi
|
|
fi
|
|
else
|
|
doctor_issue "nginx config not found: $NGINX_CONF"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── 4. Docker ───────────────────────────────────────────────
|
|
echo "Docker"
|
|
echo "───────────────────────────────────────"
|
|
|
|
if command -v docker &>/dev/null; then
|
|
doctor_ok "Docker installed"
|
|
else
|
|
doctor_issue "Docker not found"
|
|
fi
|
|
|
|
if docker info &>/dev/null; then
|
|
doctor_ok "Docker daemon running"
|
|
else
|
|
doctor_issue "Docker daemon not running"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
systemctl start docker 2>/dev/null && doctor_fixed "Started Docker daemon" || print_error " Could not start Docker"
|
|
else
|
|
doctor_skip "systemctl start docker"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── 5. Disk space ───────────────────────────────────────────
|
|
echo "System"
|
|
echo "───────────────────────────────────────"
|
|
|
|
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
|
|
if [ "$DISK_USAGE" -lt 90 ]; then
|
|
doctor_ok "Disk usage: ${DISK_USAGE}%"
|
|
elif [ "$DISK_USAGE" -lt 95 ]; then
|
|
print_warning "Disk usage high: ${DISK_USAGE}%"
|
|
else
|
|
doctor_issue "Disk almost full: ${DISK_USAGE}%"
|
|
fi
|
|
|
|
RAM_AVAIL=$(free -m | awk '/Mem:/ {print $7}')
|
|
if [ "$RAM_AVAIL" -gt 100 ]; then
|
|
doctor_ok "Available RAM: ${RAM_AVAIL}MB"
|
|
else
|
|
doctor_issue "Low RAM: ${RAM_AVAIL}MB available"
|
|
fi
|
|
|
|
# Check swap
|
|
SWAP_TOTAL=$(free -m | awk '/^Swap:/ {print $2}')
|
|
if [ "$SWAP_TOTAL" -gt 0 ]; then
|
|
doctor_ok "Swap: ${SWAP_TOTAL}MB"
|
|
else
|
|
print_warning "No swap configured (recommended for low-RAM servers)"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── 6. Database ─────────────────────────────────────────────
|
|
echo "Database"
|
|
echo "───────────────────────────────────────"
|
|
|
|
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
|
|
if [ -f "$DB_FILE" ]; then
|
|
DB_SIZE=$(du -h "$DB_FILE" | cut -f1)
|
|
doctor_ok "SQLite database exists ($DB_SIZE)"
|
|
else
|
|
doctor_issue "Database file not found: $DB_FILE"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── 7. Connectivity ─────────────────────────────────────────
|
|
echo "Connectivity"
|
|
echo "───────────────────────────────────────"
|
|
|
|
if curl -sf http://127.0.0.1 > /dev/null 2>&1; then
|
|
doctor_ok "localhost:80 responding"
|
|
else
|
|
doctor_issue "localhost:80 not responding (nginx → frontend broken)"
|
|
fi
|
|
|
|
if curl -sf http://127.0.0.1/api/v1/system/health > /dev/null 2>&1; then
|
|
doctor_ok "localhost:80/api proxying to backend"
|
|
else
|
|
doctor_issue "localhost:80/api not reaching backend"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ── Summary ─────────────────────────────────────────────────
|
|
echo "───────────────────────────────────────"
|
|
if [ "$ISSUES" -eq 0 ]; then
|
|
print_success "All checks passed!"
|
|
else
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
echo -e "${YELLOW}Found $ISSUES issue(s), fixed $FIXED${NC}"
|
|
REMAINING=$((ISSUES - FIXED))
|
|
if [ "$REMAINING" -gt 0 ]; then
|
|
echo "$REMAINING issue(s) need manual attention"
|
|
fi
|
|
else
|
|
echo -e "${RED}Found $ISSUES issue(s)${NC}"
|
|
echo "Run 'sudo serverkit doctor --fix' to auto-fix"
|
|
fi
|
|
fi
|
|
echo ""
|
|
|
|
# Meaningful exit code so doctor can gate scripts: non-zero while issues
|
|
# remain (with --fix, only the ones that could not be auto-fixed count).
|
|
local remaining="$ISSUES"
|
|
if [ "$FIX_MODE" = "--fix" ]; then
|
|
remaining=$((ISSUES - FIXED))
|
|
fi
|
|
if [ "$remaining" -gt 0 ]; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
cmd_remove_site() {
|
|
check_root
|
|
|
|
DOMAIN="$1"
|
|
|
|
if [ -z "$DOMAIN" ]; then
|
|
echo "Usage: serverkit remove-site <domain>"
|
|
cmd_list_sites
|
|
exit 1
|
|
fi
|
|
|
|
resolve_site_paths "$DOMAIN"
|
|
|
|
if [ ! -f "$SITE_FILE" ]; then
|
|
print_error "Site not found: $DOMAIN"
|
|
exit 1
|
|
fi
|
|
|
|
print_warning "This will remove the site config for $DOMAIN"
|
|
read -p "Continue? (y/N) " -n 1 -r || true
|
|
echo
|
|
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
if [ -n "$SITE_LINK" ]; then
|
|
rm -f "$SITE_LINK"
|
|
fi
|
|
rm -f "$SITE_FILE"
|
|
reload_nginx_or_warn
|
|
print_success "Site removed: $DOMAIN"
|
|
fi
|
|
}
|
|
|
|
# Main
|
|
#
|
|
# Sourcing this file (e.g. from scripts/test/test_cli.sh) exposes every
|
|
# function above for unit testing without dispatching a command — the same
|
|
# pattern as scripts/update.sh. Only direct execution runs the dispatch below.
|
|
[ "${BASH_SOURCE[0]}" = "${0}" ] || return 0
|
|
|
|
case "${1:-help}" in
|
|
help|--help|-h)
|
|
cmd_help
|
|
;;
|
|
start)
|
|
cmd_start
|
|
;;
|
|
stop)
|
|
cmd_stop
|
|
;;
|
|
restart)
|
|
cmd_restart
|
|
;;
|
|
status)
|
|
cmd_status
|
|
;;
|
|
logs)
|
|
cmd_logs "$2"
|
|
;;
|
|
update)
|
|
shift
|
|
cmd_update "$@"
|
|
;;
|
|
branch)
|
|
cmd_branch
|
|
;;
|
|
version|--version|-v)
|
|
cmd_version
|
|
;;
|
|
check-update)
|
|
cmd_check_update
|
|
;;
|
|
uninstall)
|
|
shift
|
|
cmd_uninstall "$@"
|
|
;;
|
|
create-admin)
|
|
shift
|
|
cmd_create_admin "$@"
|
|
;;
|
|
reset-password)
|
|
shift
|
|
cmd_reset_password "$@"
|
|
;;
|
|
setup-code)
|
|
cmd_setup_code
|
|
;;
|
|
unlock-user)
|
|
shift
|
|
cmd_unlock_user "$@"
|
|
;;
|
|
list-users)
|
|
shift
|
|
cmd_list_users "$@"
|
|
;;
|
|
make-admin)
|
|
shift
|
|
cmd_make_admin "$@"
|
|
;;
|
|
deactivate-user)
|
|
shift
|
|
cmd_deactivate_user "$@"
|
|
;;
|
|
activate-user)
|
|
shift
|
|
cmd_activate_user "$@"
|
|
;;
|
|
init-db)
|
|
cmd_init_db
|
|
;;
|
|
migrate-db)
|
|
cmd_migrate_db
|
|
;;
|
|
manifest|services|apps|repair|login-url|support-bundle|connect|db-migrate|db-status|db-history)
|
|
cmd_panel_cli "$@"
|
|
;;
|
|
panel-status)
|
|
shift
|
|
cmd_panel_cli status "$@"
|
|
;;
|
|
panel-doctor)
|
|
shift
|
|
cmd_panel_cli doctor "$@"
|
|
;;
|
|
completion)
|
|
cmd_completion
|
|
;;
|
|
disk)
|
|
shift
|
|
cmd_disk "$@"
|
|
;;
|
|
cleanup-apps)
|
|
shift
|
|
cmd_cleanup_apps "$@"
|
|
;;
|
|
factory-reset)
|
|
cmd_factory_reset
|
|
;;
|
|
backup-db)
|
|
cmd_backup_db
|
|
;;
|
|
restore-db)
|
|
cmd_restore_db "$2"
|
|
;;
|
|
generate-keys)
|
|
cmd_generate_keys
|
|
;;
|
|
config)
|
|
cmd_config
|
|
;;
|
|
setup-proxy)
|
|
cmd_setup_proxy
|
|
;;
|
|
create-app)
|
|
cmd_create_app "$2" "$3" "$4"
|
|
;;
|
|
deploy-template)
|
|
cmd_deploy_template "$@"
|
|
;;
|
|
list-servers)
|
|
shift
|
|
cmd_list_servers "$@"
|
|
;;
|
|
deployment-status)
|
|
cmd_deployment_status "$@"
|
|
;;
|
|
list-apps)
|
|
shift
|
|
cmd_list_apps "$@"
|
|
;;
|
|
start-app)
|
|
cmd_start_app "$2"
|
|
;;
|
|
stop-app)
|
|
cmd_stop_app "$2"
|
|
;;
|
|
app-logs)
|
|
cmd_app_logs "$2"
|
|
;;
|
|
add-site)
|
|
cmd_add_site "$2" "$3"
|
|
;;
|
|
list-sites)
|
|
cmd_list_sites
|
|
;;
|
|
remove-site)
|
|
cmd_remove_site "$2"
|
|
;;
|
|
doctor)
|
|
cmd_doctor "$2"
|
|
;;
|
|
*)
|
|
print_error "Unknown command: $1"
|
|
echo "Run 'serverkit help' for usage"
|
|
exit 1
|
|
;;
|
|
esac
|