#!/usr/bin/env bash set -euo pipefail umask 077 LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "${LOCAL}/../.." && pwd)" STATE="${LOCAL}/.state" PYTHON="${ROOT}/backend/.venv/bin/python" export ENV_FILE="${ROOT}/backend/.env" export NO_ENTERPRISE=1 compose() { docker --context colima-bytedesk compose --env-file "${LOCAL}/.env" -f "${LOCAL}/compose.yaml" "$@" } start_process() { local name="$1" directory="$2" pid shift 2 if [[ -f "${STATE}/${name}.pid" ]] && kill -0 "$(cat "${STATE}/${name}.pid")" 2>/dev/null; then printf '%s is already running.\n' "${name}" return fi pid="$("${PYTHON}" - "${STATE}/${name}.log" "${directory}" "$@" <<'PY' import subprocess import sys with open(sys.argv[1], "a") as log: process = subprocess.Popen(sys.argv[3:], cwd=sys.argv[2], stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True) print(process.pid) PY )" printf '%s\n' "${pid}" > "${STATE}/${name}.pid" } stop_process() { local name="$1" pid command if [[ ! -f "${STATE}/${name}.pid" ]]; then return; fi pid="$(cat "${STATE}/${name}.pid")" if kill -0 "${pid}" 2>/dev/null; then command="$(ps -p "${pid}" -o command=)" if [[ "${command}" != *"${ROOT}"* ]]; then printf 'Refusing to stop an unrelated process at PID %s.\n' "${pid}" >&2 return 1 fi kill "${pid}" for attempt in {1..40}; do if ! kill -0 "${pid}" 2>/dev/null; then break; fi sleep 0.25 done fi rm -f "${STATE}/${name}.pid" } build_frontend() { mkdir -p "${STATE}/assets" cd "${ROOT}/frontend" node_modules/.bin/vite build --outDir "${STATE}/dashboard" --emptyOutDir node_modules/.bin/vite build --config vite.widget.config.ts --outDir "${STATE}/assets" --emptyOutDir if [[ ! -f "${STATE}/assets/widget.css" ]]; then cp "${STATE}"/assets/unused/*.css "${STATE}/assets/widget.css" fi node_modules/.bin/esbuild src/webclient/replypier.js --bundle --minify --format=iife \ --target=es2015 --loader:.svg=text --define:process.env.NODE_ENV='"production"' \ --define:__REPLYPIER_API_URL__='"http://127.0.0.1:8018/api/v1"' \ --outfile="${STATE}/dashboard/webclient/replypier.min.js" cat > "${STATE}/dashboard/config.js" <<'JS' self.APP_CONFIG = { API_URL: "http://127.0.0.1:8018/api/v1", WS_URL: "ws://127.0.0.1:8018", WIDGET_URL: "http://127.0.0.1:8018", NODE_ENV: "production" }; JS if [[ -f "${STATE}/test-environment.json" ]]; then "${PYTHON}" - "${LOCAL}" <<'PY' import json import sys from pathlib import Path local = Path(sys.argv[1]) state = local / ".state" environment = json.loads((state / "test-environment.json").read_text()) template = (local / "demo.template.html").read_text() (state / "dashboard/local-demo.html").write_text(template.replace("__WIDGET_ID__", environment["widget_id"])) PY fi } case "${1:-status}" in init) python3 "${LOCAL}/init_env.py" ;; setup) python3 "${LOCAL}/init_env.py" if [[ ! -x "${PYTHON}" ]]; then uv venv --python 3.12 "${ROOT}/backend/.venv" fi uv pip install --python "${PYTHON}" -r "${LOCAL}/requirements.lock.txt" (cd "${ROOT}/frontend" && npm ci --no-audit --no-fund) build_frontend ;; build) build_frontend ;; up) if [[ ! -f "${STATE}/dashboard/index.html" ]]; then printf 'Run %s setup first.\n' "$0" >&2 exit 1 fi if ! colima --profile bytedesk status >/dev/null 2>&1; then colima --profile bytedesk start --activate=false --ssh-config=false fi compose up -d --wait --wait-timeout 180 compose exec -T db psql -U replypier -d replypier -c 'CREATE EXTENSION IF NOT EXISTS vector;' (cd "${ROOT}/backend" && "${PYTHON}" -m alembic upgrade head) start_process backend "${ROOT}/backend" "${PYTHON}" -m uvicorn app.main:app --host 127.0.0.1 --port 8018 start_process knowledge "${ROOT}/backend" "${PYTHON}" -m app.workers.run_knowledge_processor start_process frontend "${ROOT}/frontend" "$(command -v node)" "${ROOT}/frontend/node_modules/vite/bin/vite.js" preview --outDir "${STATE}/dashboard" --host 127.0.0.1 --port 3018 --strictPort for attempt in {1..90}; do if curl --silent --fail http://127.0.0.1:8018/health >/dev/null && curl --silent --fail http://127.0.0.1:3018/ >/dev/null; then printf '\nReplyPier: http://127.0.0.1:3018/\n' exit 0 fi sleep 2 done printf 'Startup did not become healthy; inspect %s/*.log\n' "${STATE}" >&2 exit 1 ;; stop) stop_process frontend stop_process knowledge stop_process backend compose stop ;; restart) stop_process frontend stop_process knowledge stop_process backend exec "$0" up ;; status) compose ps for name in backend knowledge frontend; do if [[ -f "${STATE}/${name}.pid" ]]; then ps -p "$(cat "${STATE}/${name}.pid")" -o pid=,etime=,command= || true fi done curl --silent --show-error --fail http://127.0.0.1:8018/health printf '\n' ;; logs) tail -n 70 "${STATE}/${2:-backend}.log" ;; *) printf 'Usage: %s {init|setup|build|up|stop|restart|status|logs [backend|knowledge|frontend]}\n' "$0" >&2 exit 2 ;; esac