Add pf.sh — interactive deployment and service management script
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73e8f5d202
commit
0a2f0e50a1
344
pf.sh
Executable file
344
pf.sh
Executable file
@ -0,0 +1,344 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# pf.sh — Pivot Forecast management script
|
||||||
|
# Usage: ./pf.sh [deploy|start|stop|restart|status|logs|db-setup|config]
|
||||||
|
# ./pf.sh (interactive menu)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SERVICE_NAME="pf_app"
|
||||||
|
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
ENV_FILE="${APP_DIR}/.env"
|
||||||
|
MIN_NODE_MAJOR=20
|
||||||
|
|
||||||
|
# -- Colors ------------------------------------------------------------------
|
||||||
|
R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m'; NC='\033[0m'
|
||||||
|
bold() { echo -e "\033[1m$*\033[0m"; }
|
||||||
|
info() { echo -e "${B}==>${NC} $*"; }
|
||||||
|
success() { echo -e "${G} ✓${NC} $*"; }
|
||||||
|
warn() { echo -e "${Y} !${NC} $*"; }
|
||||||
|
error() { echo -e "${R} ✗${NC} $*" >&2; }
|
||||||
|
die() { error "$*"; exit 1; }
|
||||||
|
|
||||||
|
# -- Helpers -----------------------------------------------------------------
|
||||||
|
|
||||||
|
require_systemd() {
|
||||||
|
systemctl --version &>/dev/null || die "systemd not found on this system."
|
||||||
|
}
|
||||||
|
|
||||||
|
node_binary() {
|
||||||
|
command -v node 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
check_node() {
|
||||||
|
local node
|
||||||
|
node=$(node_binary)
|
||||||
|
[[ -z "$node" ]] && die "node not found. Install Node.js >= ${MIN_NODE_MAJOR}."
|
||||||
|
local ver
|
||||||
|
ver=$(node --version | sed 's/v//')
|
||||||
|
local major="${ver%%.*}"
|
||||||
|
if (( major < MIN_NODE_MAJOR )); then
|
||||||
|
die "Node.js ${ver} found; requires >= ${MIN_NODE_MAJOR}. Please upgrade."
|
||||||
|
fi
|
||||||
|
success "Node.js ${ver}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
[[ -f "$ENV_FILE" ]] || die ".env not found. Run: ./pf.sh config"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
require_env
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo_if_needed() {
|
||||||
|
# Returns "sudo" if we're not root, empty string if we are
|
||||||
|
[[ "$EUID" -eq 0 ]] && echo "" || echo "sudo"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_installed() {
|
||||||
|
[[ -f "$SERVICE_FILE" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
require_service() {
|
||||||
|
service_installed || die "systemd service not installed. Run: ./pf.sh install-service"
|
||||||
|
}
|
||||||
|
|
||||||
|
db_ping() {
|
||||||
|
load_env
|
||||||
|
local url="${DATABASE_URL:-}"
|
||||||
|
[[ -z "$url" ]] && { warn "DATABASE_URL not set in .env"; return 1; }
|
||||||
|
# Use psql if available for a real connectivity check
|
||||||
|
if command -v psql &>/dev/null; then
|
||||||
|
psql "$url" -c "SELECT 1" &>/dev/null && return 0 || return 1
|
||||||
|
else
|
||||||
|
warn "psql not in PATH — skipping live DB check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Commands ----------------------------------------------------------------
|
||||||
|
|
||||||
|
cmd_deploy() {
|
||||||
|
echo; bold "Deploying Pivot Forecast"
|
||||||
|
echo " App dir: $APP_DIR"
|
||||||
|
echo
|
||||||
|
|
||||||
|
check_node
|
||||||
|
require_env
|
||||||
|
|
||||||
|
info "Pulling latest from git…"
|
||||||
|
git -C "$APP_DIR" pull
|
||||||
|
|
||||||
|
info "Installing server dependencies…"
|
||||||
|
npm --prefix "$APP_DIR" install --omit=dev
|
||||||
|
|
||||||
|
info "Installing UI dependencies…"
|
||||||
|
npm --prefix "$APP_DIR/ui" install
|
||||||
|
|
||||||
|
info "Building UI…"
|
||||||
|
npm --prefix "$APP_DIR/ui" run build
|
||||||
|
|
||||||
|
if service_installed; then
|
||||||
|
info "Restarting service…"
|
||||||
|
cmd_restart
|
||||||
|
else
|
||||||
|
warn "Service not installed — server not started."
|
||||||
|
echo " Run: ./pf.sh install-service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Deploy complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
require_systemd; require_service
|
||||||
|
info "Starting ${SERVICE_NAME}…"
|
||||||
|
$(sudo_if_needed) systemctl start "$SERVICE_NAME"
|
||||||
|
success "Started."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
require_systemd; require_service
|
||||||
|
info "Stopping ${SERVICE_NAME}…"
|
||||||
|
$(sudo_if_needed) systemctl stop "$SERVICE_NAME"
|
||||||
|
success "Stopped."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
require_systemd; require_service
|
||||||
|
info "Restarting ${SERVICE_NAME}…"
|
||||||
|
$(sudo_if_needed) systemctl restart "$SERVICE_NAME"
|
||||||
|
success "Restarted."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
require_systemd
|
||||||
|
echo
|
||||||
|
bold "System service"
|
||||||
|
if service_installed; then
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager -l || true
|
||||||
|
else
|
||||||
|
warn "Service not installed — run: ./pf.sh install-service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
bold "Database"
|
||||||
|
if db_ping; then
|
||||||
|
success "DB reachable"
|
||||||
|
else
|
||||||
|
error "DB not reachable (check DATABASE_URL in .env)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
bold "Git"
|
||||||
|
git -C "$APP_DIR" log -1 --format=" Commit: %h %s (%ar)"
|
||||||
|
local branch
|
||||||
|
branch=$(git -C "$APP_DIR" rev-parse --abbrev-ref HEAD)
|
||||||
|
echo " Branch: $branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
require_systemd; require_service
|
||||||
|
info "Streaming logs (Ctrl-C to exit)…"
|
||||||
|
journalctl -u "$SERVICE_NAME" -f --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_db_setup() {
|
||||||
|
require_env; load_env
|
||||||
|
local url="${DATABASE_URL:-}"
|
||||||
|
[[ -z "$url" ]] && die "DATABASE_URL not set in .env"
|
||||||
|
command -v psql &>/dev/null || die "psql not found — install postgresql-client"
|
||||||
|
|
||||||
|
echo
|
||||||
|
bold "DB Setup — will run: setup_sql/01_schema.sql"
|
||||||
|
warn "This creates the pf schema and tables. Safe to re-run (CREATE IF NOT EXISTS)."
|
||||||
|
read -rp " Continue? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; }
|
||||||
|
|
||||||
|
psql "$url" -f "${APP_DIR}/setup_sql/01_schema.sql"
|
||||||
|
success "Schema applied."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_config() {
|
||||||
|
echo
|
||||||
|
bold "Configure .env"
|
||||||
|
echo " File: $ENV_FILE"
|
||||||
|
echo
|
||||||
|
|
||||||
|
local current_url=""
|
||||||
|
local current_port=""
|
||||||
|
local current_user=""
|
||||||
|
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
current_url=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true)
|
||||||
|
current_port=$(grep -E '^PORT=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true)
|
||||||
|
current_user=$(grep -E '^PF_USER=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -rp " DATABASE_URL [${current_url:-not set}]: " input_url
|
||||||
|
local url="${input_url:-$current_url}"
|
||||||
|
[[ -z "$url" ]] && die "DATABASE_URL is required."
|
||||||
|
|
||||||
|
read -rp " PORT [${current_port:-3010}]: " input_port
|
||||||
|
local port="${input_port:-${current_port:-3010}}"
|
||||||
|
|
||||||
|
read -rp " PF_USER [${current_user:-$USER}]: " input_user
|
||||||
|
local pf_user="${input_user:-${current_user:-$USER}}"
|
||||||
|
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
DATABASE_URL=${url}
|
||||||
|
PORT=${port}
|
||||||
|
PF_USER=${pf_user}
|
||||||
|
EOF
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
success ".env written."
|
||||||
|
|
||||||
|
if db_ping; then
|
||||||
|
success "Database connection verified."
|
||||||
|
else
|
||||||
|
warn "Could not reach the database — double-check DATABASE_URL."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install_service() {
|
||||||
|
require_systemd
|
||||||
|
require_env
|
||||||
|
|
||||||
|
local node_path
|
||||||
|
node_path=$(node_binary)
|
||||||
|
[[ -z "$node_path" ]] && die "node not found — install Node.js first."
|
||||||
|
|
||||||
|
local run_user="$USER"
|
||||||
|
local s
|
||||||
|
s=$(sudo_if_needed)
|
||||||
|
|
||||||
|
echo
|
||||||
|
bold "Install systemd service"
|
||||||
|
echo " Service file : $SERVICE_FILE"
|
||||||
|
echo " Run as user : $run_user"
|
||||||
|
echo " App dir : $APP_DIR"
|
||||||
|
echo " Node binary : $node_path"
|
||||||
|
echo
|
||||||
|
read -rp " Continue? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; }
|
||||||
|
|
||||||
|
$s tee "$SERVICE_FILE" > /dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Pivot Forecast App
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${run_user}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
EnvironmentFile=${ENV_FILE}
|
||||||
|
ExecStart=${node_path} ${APP_DIR}/server.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=${SERVICE_NAME}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
$s systemctl daemon-reload
|
||||||
|
$s systemctl enable "$SERVICE_NAME"
|
||||||
|
success "Service installed and enabled."
|
||||||
|
echo " Start now with: ./pf.sh start"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_uninstall_service() {
|
||||||
|
require_systemd
|
||||||
|
service_installed || { warn "Service not installed."; return; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
warn "This will stop and remove the systemd service (does not touch app files)."
|
||||||
|
read -rp " Continue? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; }
|
||||||
|
|
||||||
|
local s
|
||||||
|
s=$(sudo_if_needed)
|
||||||
|
$s systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
$s systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
$s rm -f "$SERVICE_FILE"
|
||||||
|
$s systemctl daemon-reload
|
||||||
|
success "Service removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Interactive menu --------------------------------------------------------
|
||||||
|
|
||||||
|
interactive_menu() {
|
||||||
|
while true; do
|
||||||
|
echo
|
||||||
|
bold "Pivot Forecast — Management"
|
||||||
|
echo " 1) deploy pull + install + build + restart"
|
||||||
|
echo " 2) start start server"
|
||||||
|
echo " 3) stop stop server"
|
||||||
|
echo " 4) restart restart server"
|
||||||
|
echo " 5) status service + DB + git info"
|
||||||
|
echo " 6) logs tail journald logs"
|
||||||
|
echo " 7) db-setup apply setup_sql/01_schema.sql"
|
||||||
|
echo " 8) config set DATABASE_URL / PORT / PF_USER"
|
||||||
|
echo " 9) install-service create systemd unit file"
|
||||||
|
echo " 10) uninstall-service remove systemd unit file"
|
||||||
|
echo " q) quit"
|
||||||
|
echo
|
||||||
|
read -rp " Choice: " choice
|
||||||
|
case "$choice" in
|
||||||
|
1|deploy) cmd_deploy ;;
|
||||||
|
2|start) cmd_start ;;
|
||||||
|
3|stop) cmd_stop ;;
|
||||||
|
4|restart) cmd_restart ;;
|
||||||
|
5|status) cmd_status ;;
|
||||||
|
6|logs) cmd_logs ;;
|
||||||
|
7|db-setup) cmd_db_setup ;;
|
||||||
|
8|config) cmd_config ;;
|
||||||
|
9|install-service) cmd_install_service ;;
|
||||||
|
10|uninstall-service) cmd_uninstall_service ;;
|
||||||
|
q|Q|quit|exit) echo "Bye."; exit 0 ;;
|
||||||
|
*) warn "Unknown option: $choice" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Dispatch ----------------------------------------------------------------
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
deploy) cmd_deploy ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
restart) cmd_restart ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
logs) cmd_logs ;;
|
||||||
|
db-setup) cmd_db_setup ;;
|
||||||
|
config) cmd_config ;;
|
||||||
|
install-service) cmd_install_service ;;
|
||||||
|
uninstall-service) cmd_uninstall_service ;;
|
||||||
|
"") interactive_menu ;;
|
||||||
|
*) die "Unknown command: $1. Valid: deploy start stop restart status logs db-setup config install-service uninstall-service" ;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user