diff --git a/pf.sh b/pf.sh new file mode 100755 index 0000000..746b187 --- /dev/null +++ b/pf.sh @@ -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" < /dev/null </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