2026-03-05 22:10:33 -05:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
2026-03-05 23:47:46 -05:00
|
|
|
# Sovereign Agent Stack — VPS Deployment Script v7
|
2026-03-05 22:10:33 -05:00
|
|
|
#
|
2026-03-05 23:47:46 -05:00
|
|
|
# Paperclip only. No fluff.
|
2026-03-05 23:34:20 -05:00
|
|
|
# - Paperclip in local_trusted mode (127.0.0.1:3100)
|
2026-03-05 23:47:46 -05:00
|
|
|
# - Nginx reverse proxy on port 80
|
|
|
|
|
# - Cookie-based auth gate — login once, 7-day session
|
|
|
|
|
# - PostgreSQL backend
|
2026-03-05 22:10:33 -05:00
|
|
|
#
|
|
|
|
|
# Usage:
|
2026-03-05 23:47:46 -05:00
|
|
|
# curl -O https://raw.githubusercontent.com/AlexanderWhitestone/Timmy-time-dashboard/main/setup_timmy.sh
|
|
|
|
|
# chmod +x setup_timmy.sh
|
|
|
|
|
# ./setup_timmy.sh install
|
|
|
|
|
# ./setup_timmy.sh start
|
2026-03-05 22:10:33 -05:00
|
|
|
#
|
2026-03-05 23:47:46 -05:00
|
|
|
# Dashboard: http://YOUR_DOMAIN (behind auth)
|
2026-03-05 22:10:33 -05:00
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
# --- Configuration ---
|
|
|
|
|
PROJECT_DIR="${PROJECT_DIR:-$HOME/sovereign-stack}"
|
|
|
|
|
PAPERCLIP_DIR="$PROJECT_DIR/paperclip"
|
|
|
|
|
LOG_DIR="$PROJECT_DIR/logs"
|
|
|
|
|
PID_DIR="$PROJECT_DIR/pids"
|
|
|
|
|
|
2026-03-05 23:34:20 -05:00
|
|
|
DOMAIN="${DOMAIN:-$(curl -s ifconfig.me)}"
|
|
|
|
|
|
2026-03-05 22:10:33 -05:00
|
|
|
DB_USER="paperclip"
|
|
|
|
|
DB_PASS="paperclip"
|
|
|
|
|
DB_NAME="paperclip"
|
2026-03-05 23:34:20 -05:00
|
|
|
DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
|
|
|
|
|
|
|
|
|
|
AUTH_USER="${AUTH_USER:-Rockachopa}"
|
|
|
|
|
AUTH_PASS="${AUTH_PASS:-Iamrockachopathegend}"
|
|
|
|
|
|
|
|
|
|
SECRETS_FILE="$PROJECT_DIR/.secrets"
|
2026-03-05 22:10:33 -05:00
|
|
|
|
|
|
|
|
# --- Colors ---
|
2026-03-05 23:47:46 -05:00
|
|
|
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m'
|
|
|
|
|
|
2026-03-05 22:10:33 -05:00
|
|
|
banner() { echo -e "\n${CYAN}═══════════════════════════════════════════════${NC}\n${BOLD}$1${NC}\n${CYAN}═══════════════════════════════════════════════${NC}\n"; }
|
|
|
|
|
step() { echo -e "${GREEN}▸${NC} $1"; }
|
|
|
|
|
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
2026-03-05 23:34:20 -05:00
|
|
|
fail() { echo -e "${RED}✘${NC} $1"; exit 1; }
|
2026-03-05 22:10:33 -05:00
|
|
|
info() { echo -e "${BOLD}$1${NC}"; }
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Secrets ---
|
2026-03-05 23:34:20 -05:00
|
|
|
load_or_create_secrets() {
|
|
|
|
|
if [ -f "$SECRETS_FILE" ]; then
|
|
|
|
|
source "$SECRETS_FILE"
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Loaded secrets"
|
2026-03-05 23:34:20 -05:00
|
|
|
else
|
|
|
|
|
BETTER_AUTH_SECRET="sovereign-$(openssl rand -hex 16)"
|
|
|
|
|
PAPERCLIP_AGENT_JWT_SECRET="agent-$(openssl rand -hex 16)"
|
|
|
|
|
cat > "$SECRETS_FILE" <<SECRETS
|
|
|
|
|
BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET"
|
|
|
|
|
PAPERCLIP_AGENT_JWT_SECRET="$PAPERCLIP_AGENT_JWT_SECRET"
|
|
|
|
|
SECRETS
|
|
|
|
|
chmod 600 "$SECRETS_FILE"
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Generated secrets"
|
2026-03-05 23:34:20 -05:00
|
|
|
fi
|
|
|
|
|
}
|
2026-03-05 22:10:33 -05:00
|
|
|
|
2026-03-05 23:34:20 -05:00
|
|
|
# --- Preflight ---
|
2026-03-05 22:10:33 -05:00
|
|
|
check_preflight() {
|
2026-03-05 23:47:46 -05:00
|
|
|
banner "Preflight"
|
2026-03-05 22:10:33 -05:00
|
|
|
|
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Installing dependencies..."
|
2026-03-05 23:34:20 -05:00
|
|
|
sudo apt-get update -y > /dev/null 2>&1
|
2026-03-05 23:47:46 -05:00
|
|
|
sudo apt-get install -y curl git postgresql postgresql-contrib build-essential nginx > /dev/null 2>&1
|
2026-03-05 22:10:33 -05:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
|
|
|
step "Installing Node.js 20..."
|
2026-03-05 23:34:20 -05:00
|
|
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - > /dev/null 2>&1
|
|
|
|
|
sudo apt-get install -y nodejs > /dev/null 2>&1
|
2026-03-05 22:10:33 -05:00
|
|
|
fi
|
2026-03-05 23:34:20 -05:00
|
|
|
step "Node $(node -v)"
|
2026-03-05 22:10:33 -05:00
|
|
|
|
|
|
|
|
if ! command -v pnpm >/dev/null 2>&1; then
|
|
|
|
|
step "Installing pnpm..."
|
2026-03-05 23:34:20 -05:00
|
|
|
sudo npm install -g pnpm > /dev/null 2>&1
|
2026-03-05 22:10:33 -05:00
|
|
|
fi
|
2026-03-05 23:34:20 -05:00
|
|
|
step "pnpm $(pnpm -v)"
|
2026-03-05 22:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:34:20 -05:00
|
|
|
# --- Database ---
|
2026-03-05 22:10:33 -05:00
|
|
|
setup_database() {
|
2026-03-05 23:47:46 -05:00
|
|
|
banner "Database"
|
2026-03-05 23:34:20 -05:00
|
|
|
sudo systemctl start postgresql || sudo service postgresql start || true
|
|
|
|
|
|
2026-03-05 22:10:33 -05:00
|
|
|
sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || \
|
|
|
|
|
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS' SUPERUSER;"
|
2026-03-05 23:34:20 -05:00
|
|
|
|
2026-03-05 22:10:33 -05:00
|
|
|
sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME" || \
|
|
|
|
|
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
2026-03-05 23:34:20 -05:00
|
|
|
|
|
|
|
|
step "Database ready"
|
2026-03-05 22:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Auth Gate ---
|
2026-03-05 23:34:20 -05:00
|
|
|
install_auth_gate() {
|
|
|
|
|
step "Installing auth gate..."
|
|
|
|
|
|
|
|
|
|
cat > "$PROJECT_DIR/auth-gate.py" <<'AUTHGATE'
|
|
|
|
|
#!/usr/bin/env python3
|
2026-03-05 23:47:46 -05:00
|
|
|
"""Cookie-based auth gate. Login once, 7-day session."""
|
2026-03-05 23:34:20 -05:00
|
|
|
import hashlib, hmac, http.server, time, base64, os
|
|
|
|
|
|
|
|
|
|
SECRET = os.environ.get("AUTH_GATE_SECRET", "sovereign-timmy-gate-2026")
|
|
|
|
|
USER = os.environ.get("AUTH_USER", "admin")
|
|
|
|
|
PASS = os.environ.get("AUTH_PASS", "changeme")
|
|
|
|
|
COOKIE_NAME = "sovereign_gate"
|
|
|
|
|
COOKIE_MAX_AGE = 86400 * 7
|
|
|
|
|
|
|
|
|
|
def make_token(ts):
|
2026-03-05 23:47:46 -05:00
|
|
|
return hmac.new(SECRET.encode(), f"{USER}:{ts}".encode(), hashlib.sha256).hexdigest()[:32]
|
2026-03-05 23:34:20 -05:00
|
|
|
|
|
|
|
|
def verify_token(token):
|
|
|
|
|
try:
|
|
|
|
|
parts = token.split(".")
|
2026-03-05 23:47:46 -05:00
|
|
|
if len(parts) != 2: return False
|
2026-03-05 23:34:20 -05:00
|
|
|
ts, sig = int(parts[0]), parts[1]
|
2026-03-05 23:47:46 -05:00
|
|
|
if time.time() - ts > COOKIE_MAX_AGE: return False
|
2026-03-05 23:34:20 -05:00
|
|
|
return sig == make_token(ts)
|
2026-03-05 23:47:46 -05:00
|
|
|
except: return False
|
2026-03-05 23:34:20 -05:00
|
|
|
|
|
|
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
|
|
def log_message(self, *a): pass
|
|
|
|
|
def do_GET(self):
|
2026-03-05 23:47:46 -05:00
|
|
|
for c in self.headers.get("Cookie", "").split(";"):
|
2026-03-05 23:34:20 -05:00
|
|
|
c = c.strip()
|
2026-03-05 23:47:46 -05:00
|
|
|
if c.startswith(f"{COOKIE_NAME}=") and verify_token(c[len(COOKIE_NAME)+1:]):
|
|
|
|
|
self.send_response(200); self.end_headers(); return
|
2026-03-05 23:34:20 -05:00
|
|
|
auth = self.headers.get("Authorization", "")
|
|
|
|
|
if auth.startswith("Basic "):
|
|
|
|
|
try:
|
2026-03-05 23:47:46 -05:00
|
|
|
u, p = base64.b64decode(auth[6:]).decode().split(":", 1)
|
2026-03-05 23:34:20 -05:00
|
|
|
if u == USER and p == PASS:
|
|
|
|
|
ts = int(time.time())
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
self.send_header("Set-Cookie",
|
2026-03-05 23:47:46 -05:00
|
|
|
f"{COOKIE_NAME}={ts}.{make_token(ts)}; Path=/; Max-Age={COOKIE_MAX_AGE}; HttpOnly; SameSite=Lax")
|
|
|
|
|
self.end_headers(); return
|
|
|
|
|
except: pass
|
2026-03-05 23:34:20 -05:00
|
|
|
self.send_response(401)
|
|
|
|
|
self.send_header("WWW-Authenticate", 'Basic realm="Sovereign Stack"')
|
|
|
|
|
self.end_headers()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
s = http.server.HTTPServer(("127.0.0.1", 9876), Handler)
|
2026-03-05 23:47:46 -05:00
|
|
|
print("Auth gate on 127.0.0.1:9876"); s.serve_forever()
|
2026-03-05 23:34:20 -05:00
|
|
|
AUTHGATE
|
|
|
|
|
chmod +x "$PROJECT_DIR/auth-gate.py"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Nginx ---
|
2026-03-05 23:34:20 -05:00
|
|
|
install_nginx() {
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Configuring nginx..."
|
2026-03-05 23:34:20 -05:00
|
|
|
|
|
|
|
|
cat > /etc/nginx/sites-available/paperclip <<NGINX
|
|
|
|
|
server {
|
|
|
|
|
listen 80;
|
|
|
|
|
server_name $DOMAIN;
|
|
|
|
|
|
|
|
|
|
location = /_auth {
|
|
|
|
|
internal;
|
|
|
|
|
proxy_pass http://127.0.0.1:9876;
|
|
|
|
|
proxy_pass_request_body off;
|
|
|
|
|
proxy_set_header Content-Length "";
|
|
|
|
|
proxy_set_header X-Original-URI \$request_uri;
|
|
|
|
|
proxy_set_header Cookie \$http_cookie;
|
|
|
|
|
proxy_set_header Authorization \$http_authorization;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
auth_request /_auth;
|
|
|
|
|
auth_request_set \$auth_cookie \$upstream_http_set_cookie;
|
|
|
|
|
add_header Set-Cookie \$auth_cookie;
|
|
|
|
|
|
|
|
|
|
proxy_pass http://127.0.0.1:3100;
|
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
|
proxy_set_header Upgrade \$http_upgrade;
|
|
|
|
|
proxy_set_header Connection 'upgrade';
|
|
|
|
|
proxy_set_header Host localhost;
|
|
|
|
|
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 X-Forwarded-Host \$host;
|
|
|
|
|
proxy_cache_bypass \$http_upgrade;
|
|
|
|
|
proxy_read_timeout 86400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error_page 401 = @login;
|
|
|
|
|
location @login {
|
|
|
|
|
proxy_pass http://127.0.0.1:9876;
|
|
|
|
|
proxy_set_header Authorization \$http_authorization;
|
|
|
|
|
proxy_set_header Cookie \$http_cookie;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
NGINX
|
|
|
|
|
|
|
|
|
|
ln -sf /etc/nginx/sites-available/paperclip /etc/nginx/sites-enabled/paperclip
|
|
|
|
|
rm -f /etc/nginx/sites-enabled/default
|
2026-03-05 23:47:46 -05:00
|
|
|
nginx -t || fail "Nginx config failed"
|
2026-03-05 23:34:20 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Install ---
|
|
|
|
|
install_stack() {
|
|
|
|
|
banner "Installing Paperclip"
|
|
|
|
|
mkdir -p "$PROJECT_DIR" "$LOG_DIR" "$PID_DIR"
|
|
|
|
|
|
|
|
|
|
if [ ! -d "$PAPERCLIP_DIR" ]; then
|
|
|
|
|
step "Cloning Paperclip..."
|
|
|
|
|
git clone --depth 1 https://github.com/paperclipai/paperclip.git "$PAPERCLIP_DIR"
|
|
|
|
|
fi
|
|
|
|
|
cd "$PAPERCLIP_DIR"
|
|
|
|
|
step "Installing dependencies..."
|
|
|
|
|
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
|
|
|
|
cd - > /dev/null
|
|
|
|
|
|
|
|
|
|
install_auth_gate
|
|
|
|
|
install_nginx
|
|
|
|
|
|
|
|
|
|
banner "Installed"
|
|
|
|
|
info "Run: ./setup_timmy.sh start"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- Cleanup ---
|
2026-03-05 23:34:20 -05:00
|
|
|
kill_zombies() {
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Cleaning stale processes..."
|
2026-03-05 23:34:20 -05:00
|
|
|
for port in $(seq 3100 3110); do
|
|
|
|
|
pid=$(lsof -ti :$port 2>/dev/null || true)
|
2026-03-05 23:47:46 -05:00
|
|
|
[ -n "$pid" ] && kill -9 $pid 2>/dev/null && step "Killed port $port" || true
|
2026-03-05 23:34:20 -05:00
|
|
|
done
|
|
|
|
|
pid=$(lsof -ti :9876 2>/dev/null || true)
|
2026-03-05 23:47:46 -05:00
|
|
|
[ -n "$pid" ] && kill -9 $pid 2>/dev/null || true
|
2026-03-05 23:34:20 -05:00
|
|
|
sleep 1
|
2026-03-05 22:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Start ---
|
2026-03-05 22:10:33 -05:00
|
|
|
start_services() {
|
2026-03-05 23:47:46 -05:00
|
|
|
banner "Starting"
|
2026-03-05 23:34:20 -05:00
|
|
|
load_or_create_secrets
|
|
|
|
|
kill_zombies
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# Stop Docker Caddy if conflicting on port 80
|
2026-03-05 23:34:20 -05:00
|
|
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q timmy-caddy; then
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Stopping Docker Caddy (port 80)..."
|
2026-03-05 23:34:20 -05:00
|
|
|
docker stop timmy-caddy > /dev/null 2>&1 || true
|
2026-03-05 22:10:33 -05:00
|
|
|
fi
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Auth gate..."
|
2026-03-05 23:34:20 -05:00
|
|
|
AUTH_USER="$AUTH_USER" AUTH_PASS="$AUTH_PASS" \
|
|
|
|
|
nohup python3 "$PROJECT_DIR/auth-gate.py" > "$LOG_DIR/auth-gate.log" 2>&1 &
|
|
|
|
|
echo $! > "$PID_DIR/auth-gate.pid"
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Nginx..."
|
2026-03-05 23:34:20 -05:00
|
|
|
systemctl restart nginx
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Paperclip (local_trusted → 127.0.0.1:3100)..."
|
2026-03-05 23:34:20 -05:00
|
|
|
cd "$PAPERCLIP_DIR"
|
|
|
|
|
HOST=127.0.0.1 \
|
|
|
|
|
DATABASE_URL="$DATABASE_URL" \
|
|
|
|
|
PAPERCLIP_DEPLOYMENT_MODE=local_trusted \
|
|
|
|
|
BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
|
|
|
|
|
PAPERCLIP_AGENT_JWT_SECRET="$PAPERCLIP_AGENT_JWT_SECRET" \
|
|
|
|
|
BETTER_AUTH_URL="http://$DOMAIN" \
|
|
|
|
|
nohup pnpm dev > "$LOG_DIR/paperclip.log" 2>&1 &
|
|
|
|
|
echo $! > "$PID_DIR/paperclip.pid"
|
|
|
|
|
cd - > /dev/null
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
step "Waiting for Paperclip..."
|
2026-03-05 23:34:20 -05:00
|
|
|
for i in $(seq 1 30); do
|
2026-03-05 22:10:33 -05:00
|
|
|
if grep -q "Server listening on" "$LOG_DIR/paperclip.log" 2>/dev/null; then
|
2026-03-05 23:34:20 -05:00
|
|
|
echo ""
|
2026-03-05 23:47:46 -05:00
|
|
|
info "══════════════════════════════════════"
|
|
|
|
|
info " Dashboard: http://$DOMAIN"
|
|
|
|
|
info " Auth: $AUTH_USER / ********"
|
|
|
|
|
info "══════════════════════════════════════"
|
2026-03-05 22:10:33 -05:00
|
|
|
return
|
2026-03-05 23:47:46 -05:00
|
|
|
fi
|
|
|
|
|
printf "."; sleep 2
|
2026-03-05 22:10:33 -05:00
|
|
|
done
|
2026-03-05 23:47:46 -05:00
|
|
|
echo ""; warn "Slow start. Check: tail -f $LOG_DIR/paperclip.log"
|
2026-03-05 22:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- Stop ---
|
2026-03-05 22:10:33 -05:00
|
|
|
stop_services() {
|
2026-03-05 23:47:46 -05:00
|
|
|
banner "Stopping"
|
|
|
|
|
for service in paperclip auth-gate; do
|
2026-03-05 22:10:33 -05:00
|
|
|
pid_file="$PID_DIR/$service.pid"
|
|
|
|
|
if [ -f "$pid_file" ]; then
|
|
|
|
|
pid=$(cat "$pid_file")
|
2026-03-05 23:47:46 -05:00
|
|
|
ps -p "$pid" > /dev/null 2>&1 && kill "$pid" 2>/dev/null && step "Stopped $service" || true
|
2026-03-05 23:34:20 -05:00
|
|
|
rm -f "$pid_file"
|
2026-03-05 22:10:33 -05:00
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 23:34:20 -05:00
|
|
|
# --- Status ---
|
|
|
|
|
show_status() {
|
2026-03-05 23:47:46 -05:00
|
|
|
banner "Status"
|
|
|
|
|
for s in paperclip auth-gate; do
|
|
|
|
|
pf="$PID_DIR/$s.pid"
|
|
|
|
|
if [ -f "$pf" ] && ps -p $(cat "$pf") > /dev/null 2>&1; then
|
|
|
|
|
echo -e " ${GREEN}●${NC} $s"
|
2026-03-05 23:34:20 -05:00
|
|
|
else
|
|
|
|
|
echo -e " ${RED}○${NC} $s"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
echo ""
|
|
|
|
|
for port in 80 3100 9876; do
|
2026-03-05 23:47:46 -05:00
|
|
|
if lsof -ti :$port > /dev/null 2>&1; then
|
|
|
|
|
echo -e " ${GREEN}●${NC} :$port"
|
2026-03-05 23:34:20 -05:00
|
|
|
else
|
2026-03-05 23:47:46 -05:00
|
|
|
echo -e " ${RED}○${NC} :$port"
|
2026-03-05 23:34:20 -05:00
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
echo ""
|
2026-03-05 23:47:46 -05:00
|
|
|
systemctl is-active --quiet nginx && echo -e " ${GREEN}●${NC} nginx" || echo -e " ${RED}○${NC} nginx"
|
2026-03-05 23:34:20 -05:00
|
|
|
}
|
2026-03-05 22:10:33 -05:00
|
|
|
|
2026-03-05 23:47:46 -05:00
|
|
|
# --- CLI ---
|
2026-03-05 22:10:33 -05:00
|
|
|
case "${1:-}" in
|
2026-03-05 23:47:46 -05:00
|
|
|
install) check_preflight; setup_database; install_stack ;;
|
|
|
|
|
start) start_services ;;
|
|
|
|
|
stop) stop_services ;;
|
|
|
|
|
restart) stop_services; sleep 2; start_services ;;
|
|
|
|
|
status) show_status ;;
|
|
|
|
|
logs) tail -f "$LOG_DIR"/*.log ;;
|
|
|
|
|
*) echo "Usage: $0 {install|start|stop|restart|status|logs}"; exit 1 ;;
|
2026-03-05 22:10:33 -05:00
|
|
|
esac
|