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