From 22823daae10ed77c80b53848c0f266f12449b092 Mon Sep 17 00:00:00 2001 From: codeGlaze Date: Tue, 17 Feb 2026 07:51:51 +0000 Subject: [PATCH 01/44] breaking: 2026 full-stack modernization Dependency upgrades: Clojure 1.12.4, ClojureScript 1.12.134, Pedestal 0.7.0, Datomic Pro 1.0.7482, React 18.3.1, Reagent 2.0.1, re-frame 1.4.4, PDFBox 3.0.6, Buddy 3.x, figwheel-main 0.2.20, java-time 1.4.2, and supporting library updates. Key changes: - Datomic Free -> Pro (Java 21 compatibility) - Pedestal interceptor wrapping + CSP nonce handling (required by 0.7) - React 18 createRoot migration - clj-time -> java-time - lein-figwheel -> figwheel-main - CI updated to Java 21 - Devcontainer + scripts for Datomic Pro setup 74 tests, 237 assertions, 0 failures. --- .clj-kondo/config.edn | 26 +- .devcontainer/Dockerfile | 77 ++ .devcontainer/devcontainer.json | 45 ++ .devcontainer/post-create.sh | 114 +++ .env.example | 32 + .github/workflows/continuous-integration.yml | 189 ++++- .gitignore | 28 + .lsp/config.edn | 6 + dev.cljs.edn | 13 + dev/user.clj | 59 +- docker-compose-build.yaml | 5 +- docker-compose.yaml | 5 +- menu | 695 ++++++++++++++++ project.clj | 141 ++-- scripts/common.sh | 440 ++++++++++ scripts/create_dummy_user.sh | 28 + scripts/dev-setup.sh | 79 ++ scripts/start.sh | 765 ++++++++++++++++++ scripts/stop.sh | 366 +++++++++ src/clj/orcpub/config.clj | 84 ++ src/clj/orcpub/csp.clj | 45 ++ src/clj/orcpub/dev_init.clj | 28 + src/clj/orcpub/dev_tools.clj | 79 ++ src/clj/orcpub/email.clj | 14 +- src/clj/orcpub/index.clj | 43 +- src/clj/orcpub/pdf.clj | 143 +++- src/clj/orcpub/pedestal.clj | 79 +- src/clj/orcpub/privacy.clj | 2 +- src/clj/orcpub/routes.clj | 108 +-- src/clj/orcpub/security.clj | 16 +- src/clj/orcpub/styles/core.clj | 9 +- src/clj/orcpub/system.clj | 18 +- src/clj/orcpub/time.clj | 177 ++++ src/cljc/orcpub/common.cljc | 33 +- src/cljc/orcpub/components.cljc | 6 +- src/cljc/orcpub/dice.cljc | 2 +- src/cljc/orcpub/dnd/e5/character.cljc | 2 +- src/cljc/orcpub/dnd/e5/classes.cljc | 8 +- src/cljc/orcpub/dnd/e5/display.cljc | 27 +- src/cljc/orcpub/dnd/e5/event_handlers.cljc | 6 +- src/cljc/orcpub/dnd/e5/magic_items.cljc | 8 +- src/cljc/orcpub/dnd/e5/modifiers.cljc | 4 +- src/cljc/orcpub/dnd/e5/monsters.cljc | 4 +- src/cljc/orcpub/dnd/e5/options.cljc | 128 +-- src/cljc/orcpub/dnd/e5/template.cljc | 12 +- src/cljc/orcpub/dnd/e5/template_base.cljc | 12 +- .../orcpub/dnd/e5/templates/ua_options.cljc | 2 +- src/cljc/orcpub/entity.cljc | 50 +- src/cljs/orcpub/character_builder.cljs | 10 +- src/cljs/orcpub/user_agent.cljs | 35 +- test/clj/orcpub/csp_test.clj | 90 +++ .../orcpub/dependencies/integration_test.clj | 148 ++++ test/clj/orcpub/dnd/e5_test.clj | 3 +- test/clj/orcpub/pdf_test.clj | 29 +- test/clj/orcpub/security_test.clj | 62 +- test/cljc/orcpub/dnd/e5/character_test.clj | 7 +- test/cljc/orcpub/dnd/e5/character_test.cljc | 2 +- test/cljc/orcpub/dnd/e5/magic_items_test.clj | 2 +- test/cljc/orcpub/dnd/e5/modifiers_test.clj | 2 +- test/cljc/orcpub/dnd/e5/options_test.clj | 3 +- test/cljc/orcpub/dnd/e5/warlock_test.clj | 304 +++++-- test/cljc/orcpub/entity_test.clj | 3 +- test/cljc/orcpub/template_test.clj | 1 - web/cljs/orcpub/core.cljs | 28 +- 64 files changed, 4418 insertions(+), 573 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/post-create.sh create mode 100644 .env.example create mode 100644 .lsp/config.edn create mode 100644 dev.cljs.edn create mode 100755 menu create mode 100755 scripts/common.sh create mode 100755 scripts/create_dummy_user.sh create mode 100644 scripts/dev-setup.sh create mode 100755 scripts/start.sh create mode 100755 scripts/stop.sh create mode 100644 src/clj/orcpub/config.clj create mode 100644 src/clj/orcpub/csp.clj create mode 100644 src/clj/orcpub/dev_init.clj create mode 100644 src/clj/orcpub/dev_tools.clj create mode 100644 src/clj/orcpub/time.clj create mode 100644 test/clj/orcpub/csp_test.clj create mode 100644 test/clj/orcpub/dependencies/integration_test.clj diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 4463aa8d4..bccb4ebd3 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,11 +1,22 @@ -{:linters {:shadowed-fn-param {:level :off} +{:output {:exclude-files [".*resources/public/js/compiled.*"]} + :linters {:shadowed-fn-param {:level :off} :shadowed-var {:level :off} - :if {:level :off} :unused-namespace {:level :off} :unused-binding {:level :off} + :missing-else-branch {:level :warning} + :clojure-lsp/unused-public-var + {:exclude [user ; REPL utilities + orcpub.styles.core/app]} + ;; garden.selectors vars are generated by macros (defselector, + ;; defpseudoclass, gen-pseudo-class-defs, etc.) at compile time. + ;; clj-kondo can't resolve macro-generated vars statically and + ;; garden doesn't ship a clj-kondo config. + :unresolved-var + {:exclude [garden.selectors]} :unresolved-symbol {:exclude - [(clojure.core.match/match) + [(clojure.test.check.clojure-test/defspec) + (clojure.core.match/match) (cljs.core.match/match) (io.pedestal.interceptor.error/error-dispatch) (orcpub.modifiers/modifier) @@ -29,6 +40,9 @@ (orcpub.dnd.e5.modifiers/reaction) (orcpub.dnd.e5.modifiers/level-val) (orcpub.entity-spec/make-entity) - (orcpub.routes-test/with-conn)]}} - :lint-as {reagent.core/with-let clojure.core/let - hiccup.def/defhtml clojure.core/defn}} + (orcpub.routes-test/with-conn) + (user/with-db)]}} + :lint-as {reagent.core/with-let clojure.core/let + hiccup.def/defhtml clojure.core/defn + user/with-db clojure.core/let + clojure.test.check.clojure-test/defspec clojure.test/deftest}} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..3cdd31069 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,77 @@ +FROM clojure:temurin-21-lein + +# Install useful dev tools (including unzip for Datomic Pro) +RUN apt-get update \ + && apt-get install -y --no-install-recommends rlwrap git curl tmux make git-lfs lsof iproute2 unzip rsync maven \ + libfreetype6 fontconfig fonts-dejavu-core libxrender1 libxext6 libxi6 libxrandr2 \ + && rm -rf /var/lib/apt/lists/* + +# Ensure git-lfs is configured system-wide so LFS files are available in Codespaces +RUN git lfs install --system || true + +# Install Leiningen so `lein` is available for LSP and project tasks +RUN curl -fsSL https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein -o /usr/local/bin/lein \ + && chmod +x /usr/local/bin/lein \ + && /usr/local/bin/lein --version || true + +WORKDIR /workspace + +# Copy project.clj first (needed to know version) +COPY project.clj /workspace/ + +# Copy existing lib/ directory first (preserves pdfbox and other vendor deps) +# Create lib/ directory - COPY will handle copying if lib/ exists in build context +RUN mkdir -p /workspace/lib/ +COPY lib/ /workspace/lib/ + +# Download and install Datomic Pro to lib/ using existing file:lib repository pattern +# Latest version: https://docs.datomic.com/releases-pro.html +ARG DATOMIC_VERSION=1.0.7482 +RUN echo "[DOCKER BUILD] Starting Datomic Pro ${DATOMIC_VERSION} installation..." && \ + mkdir -p lib/com/datomic/datomic-pro/${DATOMIC_VERSION} && \ + echo "[DOCKER BUILD] Downloading Datomic Pro zip..." && \ + curl -L -o /tmp/datomic-pro-${DATOMIC_VERSION}.zip \ + "https://datomic-pro-downloads.s3.amazonaws.com/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.zip" && \ + echo "[DOCKER BUILD] Extracting zip..." && \ + unzip -q /tmp/datomic-pro-${DATOMIC_VERSION}.zip -d /tmp/datomic-extract && \ + echo "[DOCKER BUILD] Listing extracted contents..." && \ + find /tmp/datomic-extract -type f -name "*.jar" | head -5 && \ + # Copy entire distribution into vendor layout so POMs and support files are available + EXTRACTED_DIR=$(find /tmp/datomic-extract -maxdepth 1 -type d ! -name . | head -1) && \ + mkdir -p lib/com/datomic/datomic-pro/${DATOMIC_VERSION} && \ + if [ -n "${EXTRACTED_DIR}" ]; then \ + echo "[DOCKER BUILD] Copying extracted distribution from ${EXTRACTED_DIR} -> lib/com/datomic/datomic-pro/${DATOMIC_VERSION}" && \ + cp -r "${EXTRACTED_DIR}"/* "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/" && \ + # If a peer jar exists in the distribution, also ensure datomic-pro-.jar exists for vendor layout + if [ -f "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/peer-${DATOMIC_VERSION}.jar" ]; then \ + cp "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/peer-${DATOMIC_VERSION}.jar" "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.jar"; \ + fi; \ + else \ + # Fallback to copying a single jar if extraction produced unexpected layout + DATOMIC_POTENTIAL_JAR=$(find /tmp/datomic-extract -type f -name 'peer*.jar' -print -quit || true) && \ + if [ -z "${DATOMIC_POTENTIAL_JAR}" ]; then DATOMIC_POTENTIAL_JAR=$(find /tmp/datomic-extract -type f -name 'datomic-pro-*.jar' -print -quit || true); fi && \ + if [ -z "${DATOMIC_POTENTIAL_JAR}" ]; then echo "[DOCKER BUILD] ERROR: No Datomic JAR found in extracted zip" && find /tmp/datomic-extract -type f | head -20 && exit 1; fi && \ + cp "${DATOMIC_POTENTIAL_JAR}" "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.jar"; \ + fi && \ + echo "[DOCKER BUILD] Verifying vendor path contents..." && \ + ls -lh "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/" && \ + rm -rf /tmp/datomic-pro-${DATOMIC_VERSION}.zip /tmp/datomic-extract && \ + echo "[DOCKER BUILD] โœ… Datomic Pro installed to lib/ (file:lib repository pattern)" + +# Copy the rest of the source (exclude lib/ since we've already set it up) +# Use .dockerignore or copy selectively to avoid overwriting lib/ +COPY . /workspace +# Ensure Datomic Pro JAR is still present after COPY +RUN test -f lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.jar || \ + (echo "[DOCKER BUILD] โš ๏ธ Datomic Pro JAR missing after COPY, re-downloading..." && \ + mkdir -p lib/com/datomic/datomic-pro/${DATOMIC_VERSION} && \ + curl -L -o /tmp/datomic-pro-${DATOMIC_VERSION}.zip \ + "https://datomic-pro-downloads.s3.amazonaws.com/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.zip" && \ + unzip -q /tmp/datomic-pro-${DATOMIC_VERSION}.zip -d /tmp/datomic-extract && \ + DATOMIC_JAR=$(find /tmp/datomic-extract -name "datomic-pro-*.jar" -type f | head -1) && \ + cp "${DATOMIC_JAR}" "lib/com/datomic/datomic-pro/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.jar" && \ + rm -rf /tmp/datomic-pro-${DATOMIC_VERSION}.zip /tmp/datomic-extract && \ + echo "[DOCKER BUILD] โœ… Datomic Pro JAR restored") + +# Default shell to bash for VS Code terminal +SHELL ["/bin/bash", "-c"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d1194a929 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +{ + "name": "ClojureScript Lein-Figwheel Dev", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "features": { + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + } + }, + "forwardPorts": [8890, 3449, 4334], + "portsAttributes": { + "8890": { + "label": "Backend Server", + "onAutoForward": "notify" + }, + "3449": { + "label": "Figwheel", + "onAutoForward": "silent" + }, + "4334": { + "label": "Datomic", + "onAutoForward": "silent" + } + }, + "postCreateCommand": [ + "bash", + "./.devcontainer/post-create.sh" + ], + "containerEnv": { + "POST_CREATE_VERBOSE": "1", + "DATOMIC_VERSION": "1.0.7482", + "DATOMIC_URL": "datomic:dev://localhost:4334/orcpub" + }, + "customizations": { + "vscode": { + "extensions": [ + "betterthantomorrow.calva", + "borkdude.clj-kondo" + ], + "settings": {} + } + } +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 000000000..832fb00a9 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# Datomic Pro install script for devcontainer +# +# 1. Always remove lib/com/datomic/datomic-{type}/ if it exists +# 2. Unzip the Datomic zip (from /lib or /tmp, download if missing) into target dir +# 3. Flatten top-level subdir if present (some zips nest contents) +# 4. Run vendor maven-install from bin/ +# +# This script does NOT cherry-pick, rename, or check for specific files before extraction. +# All contents of the zip are placed in the target directory, overwriting any previous install. +# + +set -euo pipefail + +# Source .env from repo root if present (authoritative config) +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +if [ -f "$REPO_ROOT/.env" ]; then + set -a + # shellcheck disable=SC1090 + . "$REPO_ROOT/.env" + set +a +fi + +# Redirect all output to persistent logs for visibility during Codespace creation +LOG="/tmp/orcpub-post-create.log" +WORKSPACE_LOG="$REPO_ROOT/.devcontainer/post-create.log" +# Ensure workspace log exists and is writable (best-effort) +mkdir -p "$(dirname "$WORKSPACE_LOG")" 2>/dev/null || true +touch "$WORKSPACE_LOG" 2>/dev/null || true +# Tee to both /tmp and workspace log so it's inspectable in Codespaces UI +exec > >(tee -a "$LOG" "$WORKSPACE_LOG") 2>&1 + +# Optional verbose tracing: set POST_CREATE_VERBOSE=1 to enable `set -x` +if [ "${POST_CREATE_VERBOSE:-0}" = "1" ]; then + echo "[POST-CREATE] Verbose tracing enabled" + set -x +fi + +# Timestamp helper +ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } +log() { echo "$(ts) [POST-CREATE] $*"; } + +log "Starting postCreateCommand... (logging to $LOG and $WORKSPACE_LOG)" + +# Configuration with defaults +DATOMIC_TYPE="${DATOMIC_TYPE:-pro}" +RAW_DATOMIC_VERSION="${DATOMIC_VERSION:-1.0.7482}" +# basename in case a path was provided (e.g., /tmp/datomic-pro-1.0.7482.zip) +RAW_DATOMIC_VERSION="$(basename "$RAW_DATOMIC_VERSION")" +# strip leading prefix and trailing .zip if present +DATOMIC_VERSION="${RAW_DATOMIC_VERSION#datomic-${DATOMIC_TYPE}-}" +DATOMIC_VERSION="${DATOMIC_VERSION%.zip}" + +log "Using DATOMIC_TYPE=$DATOMIC_TYPE, DATOMIC_VERSION=$DATOMIC_VERSION" + +# Paths +TARGET_DIR="lib/com/datomic/datomic-${DATOMIC_TYPE}/${DATOMIC_VERSION}" +ZIP_NAME="datomic-${DATOMIC_TYPE}-${DATOMIC_VERSION}.zip" +DOWNLOAD_URL="https://datomic-pro-downloads.s3.amazonaws.com/${DATOMIC_VERSION}/${ZIP_NAME}" + +# Clean and prepare target directory +if [ -d "${TARGET_DIR}" ]; then + log "Removing existing installation at ${TARGET_DIR}" + rm -rf "${TARGET_DIR}" +fi +mkdir -p "${TARGET_DIR}" + +# Find Datomic zip in /lib/ or /tmp/, download if missing +ZIP_PATH="/lib/${ZIP_NAME}" +if [ ! -f "$ZIP_PATH" ]; then + ZIP_PATH="/tmp/${ZIP_NAME}" + if [ ! -f "$ZIP_PATH" ]; then + log "Downloading Datomic from $DOWNLOAD_URL" + curl --fail --location --progress-bar -o "$ZIP_PATH" "$DOWNLOAD_URL" + fi +fi + +# Verify zip integrity (handles corrupt/incomplete downloads) +if ! unzip -t "$ZIP_PATH" >/dev/null 2>&1; then + log "Corrupt or incomplete zip detected, removing and re-downloading..." + rm -f "$ZIP_PATH" + curl --fail --location --progress-bar -o "$ZIP_PATH" "$DOWNLOAD_URL" + + # Verify the re-download + if ! unzip -t "$ZIP_PATH" >/dev/null 2>&1; then + log "ERROR: Re-downloaded zip is still corrupt. Check network connection or URL." + log "URL: $DOWNLOAD_URL" + exit 1 + fi +fi + +# Unzip Datomic distribution into target dir +log "Extracting $ZIP_PATH to $TARGET_DIR" +unzip -q "$ZIP_PATH" -d "${TARGET_DIR}" + +# Flatten if needed (some zips nest contents in a subdirectory) +TOP_SUBDIR=$(find "${TARGET_DIR}" -mindepth 1 -maxdepth 1 -type d -print -quit || true) +if [ -n "${TOP_SUBDIR}" ] && [ -z "$(find "${TARGET_DIR}" -maxdepth 1 -type f -print -quit)" ]; then + log "Flattening nested directory structure" + mv "${TOP_SUBDIR}"/* "${TARGET_DIR}/" + rmdir "${TOP_SUBDIR}" +fi + +# Run vendor maven-install +if [ -x "${TARGET_DIR}/bin/maven-install" ]; then + log "Running maven-install..." + (cd "${TARGET_DIR}" && bash bin/maven-install) +else + log "ERROR: bin/maven-install not found or not executable in ${TARGET_DIR}/bin" + exit 1 +fi + +log "Datomic ${DATOMIC_TYPE} ${DATOMIC_VERSION} installed successfully to ${TARGET_DIR}" diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4514bce15 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Example .env file for OrcPub/Dungeon Master's Vault +# Copy to .env and edit as needed for your environment + +# Datomic configuration +DATOMIC_VERSION=1.0.7482 +DATOMIC_TYPE=pro +DATOMIC_URL=datomic:dev://localhost:4334/orcpub +DATOMIC_PASSWORD=changeme + +# Application secrets +SIGNATURE=changeme +ADMIN_PASSWORD=changeme + +# Web server +PORT=8080 + +# Security: Content Security Policy (strict|permissive|none) +# - strict: Nonce-based CSP with 'strict-dynamic' (default, recommended) +# - permissive: Allows same-origin scripts without nonces (legacy fallback) +# - none: Disables CSP (not recommended for production) +CSP_POLICY=strict + +# Logs directory (defaults to project logs/) +# Example: LOG_DIR=/var/log/orcpub +# Leave unset to use ./logs +LOG_DIR= + +# Email (SMTP) configuration +EMAIL_HOST=smtp.example.com +EMAIL_PORT=587 +EMAIL_USER=your@email.com +EMAIL_PASSWORD=changeme diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7f7451193..823b0b200 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,36 +1,177 @@ -name: Continuous Integration +name: CI -on: +on: + push: + branches: [develop, main] pull_request: - branches: [develop] + branches: [develop, main] workflow_dispatch: +permissions: + contents: read + pull-requests: write + checks: write + jobs: - lint: - name: Run Linter and Tests + test: + name: Test & Lint runs-on: ubuntu-latest + steps: - name: Checkout - uses: actions/checkout@v3 - - name: Prepare java - uses: actions/setup-java@v3 + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 with: - distribution: 'zulu' - java-version: 8.0.292 - - name: Load pdfbox from /lib - run: mkdir ~/.m2/repository/ && mkdir ~/.m2/repository/org/ && cp -rv ./lib/* ~/.m2/repository - - name: ls .m2 - run: ls -la ~/.m2/repository/org/ - - name: Install clojure tools - uses: DeLaGuardo/setup-clojure@11.0 + distribution: 'temurin' + java-version: '8' + + - name: Set up Clojure (Leiningen) + uses: DeLaGuardo/setup-clojure@12.5 + with: + lein: 2.11.2 + + - name: Cache Maven dependencies + uses: actions/cache@v4 with: - # Install just one or all simultaneously - cli: 1.10.1.693 # Clojure CLI based on tools.deps - lein: 2.9.1 # or use 'latest' to always provision latest version of leiningen - boot: 2.8.3 # or use 'latest' to always provision latest version of boot - - name: Get leiningen version - run: lein -v + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Load local pdfbox libs + run: | + mkdir -p ~/.m2/repository/org/ + cp -rv ./lib/* ~/.m2/repository/ + + - name: Install dependencies + run: lein deps + - name: Run linter - run: lein lint + id: lint + run: | + echo "## ๐Ÿ” Lint Results" >> $GITHUB_STEP_SUMMARY + if lein lint 2>&1 | tee lint-output.txt; then + echo "โœ… **Lint passed** - no errors" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ **Lint failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat lint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + - name: Run tests - run: lein test \ No newline at end of file + id: test + run: | + echo "## ๐Ÿงช Test Results" >> $GITHUB_STEP_SUMMARY + if lein test 2>&1 | tee test-output.txt; then + # Extract test summary + SUMMARY=$(grep -E "^Ran [0-9]+ tests" test-output.txt || echo "Tests completed") + echo "โœ… **Tests passed**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ **Tests failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 test-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Build ClojureScript + id: cljs + run: | + echo "## ๐Ÿ“ฆ ClojureScript Build" >> $GITHUB_STEP_SUMMARY + if lein cljsbuild once dev 2>&1 | tee cljs-output.txt; then + echo "โœ… **CLJS build succeeded**" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ **CLJS build failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 cljs-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Post PR comment with results + if: github.event_name == 'pull_request' && always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read outputs + const lintOutput = fs.existsSync('lint-output.txt') ? fs.readFileSync('lint-output.txt', 'utf8') : 'No output'; + const testOutput = fs.existsSync('test-output.txt') ? fs.readFileSync('test-output.txt', 'utf8') : 'No output'; + const cljsOutput = fs.existsSync('cljs-output.txt') ? fs.readFileSync('cljs-output.txt', 'utf8') : 'No output'; + + // Determine status + const lintOk = '${{ steps.lint.outcome }}' === 'success'; + const testOk = '${{ steps.test.outcome }}' === 'success'; + const cljsOk = '${{ steps.cljs.outcome }}' === 'success'; + const allOk = lintOk && testOk && cljsOk; + + // Extract test summary + const testMatch = testOutput.match(/Ran (\d+) tests containing (\d+) assertions/); + const testSummary = testMatch ? `${testMatch[1]} tests, ${testMatch[2]} assertions` : 'Unknown'; + + // Build comment + const status = allOk ? 'โœ… All checks passed' : 'โŒ Some checks failed'; + const body = `## ${status} + +| Check | Status | Details | +|-------|--------|---------| +| ๐Ÿ” Lint | ${lintOk ? 'โœ… Pass' : 'โŒ Fail'} | ${lintOk ? 'No errors' : 'See workflow logs'} | +| ๐Ÿงช Tests | ${testOk ? 'โœ… Pass' : 'โŒ Fail'} | ${testOk ? testSummary : 'See workflow logs'} | +| ๐Ÿ“ฆ CLJS Build | ${cljsOk ? 'โœ… Pass' : 'โŒ Fail'} | ${cljsOk ? 'Compiled successfully' : 'See workflow logs'} | + +
+๐Ÿ“‹ Full test output + +\`\`\` +${testOutput.slice(-2000)} +\`\`\` + +
+ +--- +*Workflow run: [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('All checks passed') || c.body.includes('Some checks failed') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Upload artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ci-failure-logs + path: | + lint-output.txt + test-output.txt + cljs-output.txt + retention-days: 7 diff --git a/.gitignore b/.gitignore index 3e3c55535..933cc6901 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Ignore environment file (secrets/config) +.env .editorconfig .gitattributes /resources/public/css/compiled @@ -7,6 +9,10 @@ figwheel_server.log pom.xml *jar /lib/ +# Vendored snapshot allowed: pdfbox is required locally (vendor snapshot not available upstream) +!lib/org/apache/pdfbox/ +!lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/ +!lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/* /classes/ /out/ /target/ @@ -34,3 +40,25 @@ deploy/homebrew/* # As created by some LSP-protocol tooling, e.g. nvim-lsp .lsp + +# Datomic local files +/.datomic/ + +# Local audit outputs +/audit/ + + +# Cursor IDE local config (contains user-specific paths) +.cursor/ + +# Claude Code local data (conversation history, credentials) +.claude-data/ + +# Ignore all log files in logs/ +logs/ + +*/*.log +/workspaces/.codespaces/.persistedshare/creation.log +.devcontainer/post-create.log + +scripts/analyze/ diff --git a/.lsp/config.edn b/.lsp/config.edn new file mode 100644 index 000000000..b101d323b --- /dev/null +++ b/.lsp/config.edn @@ -0,0 +1,6 @@ +{;; Explicit source-paths override classpath discovery. + ;; Without this, clojure-lsp scans `resources/` (a :resource-path) + ;; and lints compiled CLJS output in resources/public/js/compiled/out/. + :source-paths #{"src/clj" "src/cljc" "src/cljs" "web/cljs" + "test/clj" "test/cljc" "test/cljs" "dev"} + :source-paths-ignore-regex ["resources/public/js/compiled"]} diff --git a/dev.cljs.edn b/dev.cljs.edn new file mode 100644 index 000000000..dc4244ac0 --- /dev/null +++ b/dev.cljs.edn @@ -0,0 +1,13 @@ +;; Figwheel-main build: dev +;; Run with: lein fig:dev +^{:watch-dirs ["web/cljs" "src/cljc" "src/cljs"] + :css-dirs ["resources/public/css"] + :ring-server-options {:port 3449} + :open-url "http://localhost:8890"} +{:main orcpub.core + :output-to "resources/public/js/compiled/orcpub.js" + :output-dir "resources/public/js/compiled/out" + :asset-path "/js/compiled/out" + :optimizations :none + :closure-defines {goog.DEBUG true} + :preloads [devtools.preload]} diff --git a/dev/user.clj b/dev/user.clj index 694b22b19..9417eacc2 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -1,11 +1,21 @@ (ns user (:require [clojure.java.io :as io] [com.stuartsierra.component :as component] - [figwheel-sidecar.repl-api :as f] [datomic.api :as datomic] [orcpub.routes :as r] [orcpub.system :as s] - [orcpub.db.schema :as schema])) + [orcpub.db.schema :as schema] + [orcpub.config :as config])) + +;; Lazy-load figwheel-main only when needed (avoids loading it for server-only REPL) +(def ^:private fig-api + (delay + (try + (require 'figwheel.main.api) + (find-ns 'figwheel.main.api) + (catch Exception e + (println "figwheel.main.api not available:" (.getMessage e)) + nil)))) (alter-var-root #'*print-length* (constantly 50)) @@ -72,11 +82,15 @@ (defn init-database ([] - (init-database :free)) + (init-database nil)) ([mode] - (when-not (contains? #{:free :dev :mem} mode) - (throw (IllegalArgumentException. (str "Unknown db type " mode)))) - (let [db-uri (str "datomic" mode "://localhost:4334/orcpub")] + (let [env-uri (config/datomic-env) + db-uri (if (some-> env-uri not-empty) + env-uri + (let [m (or mode :dev)] + (when-not (contains? #{:free :dev :mem} m) + (throw (IllegalArgumentException. (str "Unknown db type " m)))) + (str "datomic" m "://localhost:4334/orcpub")))] (datomic/create-database db-uri) (let [conn (datomic/connect db-uri)] (datomic/transact conn schema/all-schemas))))) @@ -106,25 +120,22 @@ (defn fig-start "This starts the figwheel server and watch based auto-compiler. + Uses figwheel-main 0.2.20 with dev.cljs.edn build config. Afterwards, call (cljs-repl) to connect." ([] (fig-start "dev")) ([build-id] - ;; this call will only work as long as your :cljsbuild and - ;; :figwheel configurations are at the top level of your project.clj - ;; and are not spread across different lein profiles - - ;; otherwise you can pass a configuration into start-figwheel! manually - (f/start-figwheel! - {:figwheel-options {} - :build-ids [build-id] - :all-builds (get-cljs-build build-id)}))) + (if-let [api @fig-api] + ((ns-resolve api 'start) build-id) + (println "figwheel-main not available. Run 'lein fig:dev' instead.")))) (defn fig-stop "Stop the figwheel server and watch based auto-compiler." [] - (f/stop-figwheel!)) + (if-let [api @fig-api] + ((ns-resolve api 'stop-all)) + (println "figwheel-main not available."))) ;; if you are in an nREPL environment you will need to make sure you ;; have setup piggieback for this to work @@ -132,5 +143,19 @@ "Launch a ClojureScript REPL that is connected to your build and host environment. (NB: Call fig-start first.)" + ([] + (cljs-repl "dev")) + ([build-id] + (if-let [api @fig-api] + ((ns-resolve api 'cljs-repl) build-id) + (println "figwheel-main not available.")))) + +(defn add-test-user + "Creates a test user for development, already marked as verified. Only runs if ORCPUB_ENV=dev." [] - (f/cljs-repl)) + (if (= (System/getenv "ORCPUB_ENV") "dev") + (let [username "test" + email "test@example.com" + password "testpass"] + (r/register {:username username :email email :password password :verified true})) + (println "add-test-user is disabled outside dev environment."))) diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml index 296e97768..d51848440 100644 --- a/docker-compose-build.yaml +++ b/docker-compose-build.yaml @@ -17,8 +17,9 @@ services: EMAIL_ERRORS_TO: '' EMAIL_SSL: 'TRUE' EMAIL_TLS: 'FALSE' - # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below - DATOMIC_URL: datomic:free://datomic:4334/orcpub?password= + # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below + # Use datomic:dev:// for Datomic Pro (required for Java 21 support) + DATOMIC_URL: datomic:dev://datomic:4334/orcpub?password= # The secret used to hash your password in the browser, 20+ characters recommended SIGNATURE: '' depends_on: diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ebaa5048..036512344 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,8 +15,9 @@ services: EMAIL_ERRORS_TO: '' EMAIL_SSL: 'FALSE' EMAIL_TLS: 'FALSE' - # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below - DATOMIC_URL: datomic:free://datomic:4334/orcpub?password= + # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below + # Use datomic:dev:// for Datomic Pro (required for Java 21 support) + DATOMIC_URL: datomic:dev://datomic:4334/orcpub?password= # The secret used to hash your password in the browser, 20+ characters recommended SIGNATURE: '' depends_on: diff --git a/menu b/menu new file mode 100755 index 000000000..2b007eb2d --- /dev/null +++ b/menu @@ -0,0 +1,695 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# menu - OrcPub Development Hub +# ============================================================================= +# A wrapper/hub for starting, stopping, and checking OrcPub services. +# Intended for LOCAL DEVELOPMENT - an alternative to Calva/IDE-based workflows. +# +# Usage: +# ./menu Interactive menu +# ./menu start Start all services (calls start.sh) +# ./menu stop Stop all services (calls stop.sh) +# ./menu status Show service status (calls stop.sh --dry-run) +# ./menu help Show help +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared utilities (colors, logging, port config) +# shellcheck source=scripts/common.sh +source "$SCRIPT_DIR/scripts/common.sh" + +# ----------------------------------------------------------------------------- +# Help +# ----------------------------------------------------------------------------- + +show_help() { + cat << 'EOF' +OrcPub Development Hub + +Usage: + ./menu Interactive menu (recommended) + ./menu start [service] Start services (passthrough to start.sh) + ./menu stop [service] Stop services (passthrough to stop.sh) + ./menu status Show service status + ./menu cleanup Kill duplicate processes & show memory + +Quick CLI examples: + ./menu start datomic Start just Datomic + ./menu start server Start just server + ./menu stop datomic Stop just Datomic + ./menu stop --yes Stop all without prompting + ./menu status Check what's running + +Start options (passed through to start.sh): + datomic|server|figwheel|garden Specific service + init-db Initialize database + --quiet Suppress output + --idempotent Skip if already running + --background Run in background + --tmux Run in tmux session + +Stop options (passed through to stop.sh): + datomic|server|repl|figwheel Specific service + --yes, -y Skip confirmation + --force, -f Use SIGKILL if needed + +Tmux tips: + Attach: tmux attach -t orcpub + Detach: Ctrl+B, then D + Kill: tmux kill-session -t orcpub +EOF +} + +# ----------------------------------------------------------------------------- +# Status Display +# ----------------------------------------------------------------------------- + +show_status_header() { + "$SCRIPT_DIR/scripts/stop.sh" --dry-run 2>/dev/null || true +} + +# ----------------------------------------------------------------------------- +# Start Services Submenu +# ----------------------------------------------------------------------------- + +show_start_submenu() { + echo "" + echo -e "${BOLD}Start Services${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " 1) Datomic only" + echo " 2) Server only" + echo " 3) Figwheel (ClojureScript)" + echo " 4) Garden Auto (CSS watcher)" + echo "" + echo " a) All (foreground)" + echo " b) All (background)" + echo "" + echo " 0) Back" + echo "" +} + +handle_start_submenu() { + local choice + while true; do + show_start_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + 1) + if ! "$SCRIPT_DIR/scripts/start.sh" datomic; then + echo "" + read -r -p "Press Enter to return to menu..." + fi + return + ;; + 2) + if ! "$SCRIPT_DIR/scripts/start.sh" server; then + echo "" + read -r -p "Press Enter to return to menu..." + fi + return + ;; + 3) + if ! "$SCRIPT_DIR/scripts/start.sh" figwheel; then + echo "" + read -r -p "Press Enter to return to menu..." + fi + return + ;; + 4) + if ! "$SCRIPT_DIR/scripts/start.sh" garden; then + echo "" + read -r -p "Press Enter to return to menu..." + fi + return + ;; + a|A) + if ! "$SCRIPT_DIR/scripts/start.sh"; then + echo "" + read -r -p "Press Enter to return to menu..." + fi + return + ;; + b|B) + "$SCRIPT_DIR/scripts/start.sh" --background || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 0|""|$'\e'|$'\x7f') # 0, Enter, Escape, or Backspace + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Stop Services Submenu +# ----------------------------------------------------------------------------- + +show_stop_submenu() { + echo "" + echo -e "${BOLD}Stop Services${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " 1) Stop all" + echo " 2) Datomic only" + echo " 3) Server only" + echo " 4) nREPL only" + echo " 5) Figwheel only" + echo "" + echo " 0) Back" + echo "" +} + +handle_stop_submenu() { + local choice + while true; do + show_stop_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + 1) + "$SCRIPT_DIR/scripts/stop.sh" || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 2) + "$SCRIPT_DIR/scripts/stop.sh" datomic || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 3) + "$SCRIPT_DIR/scripts/stop.sh" server || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 4) + "$SCRIPT_DIR/scripts/stop.sh" repl || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 5) + "$SCRIPT_DIR/scripts/stop.sh" figwheel || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 0|""|$'\e'|$'\x7f') + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Tmux Submenu +# ----------------------------------------------------------------------------- + +show_tmux_submenu() { + local session_status + if tmux has-session -t orcpub 2>/dev/null; then + session_status="${GREEN}(session exists)${NC}" + else + session_status="${YELLOW}(no session)${NC}" + fi + + echo "" + echo -e "${BOLD}Tmux${NC} $session_status" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " 1) Start all in tmux" + echo " 2) Attach to session" + echo " 3) Kill session" + echo "" + echo " 0) Back" + echo "" +} + +handle_tmux_submenu() { + local choice + while true; do + show_tmux_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + 1) + if "$SCRIPT_DIR/scripts/start.sh" --tmux; then + echo "" + echo -e "${CYAN}Services started in tmux session 'orcpub'.${NC}" + echo "" + echo "To attach: tmux attach -t orcpub" + echo "To detach: Ctrl+B, then D" + fi + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 2) + if tmux has-session -t orcpub 2>/dev/null; then + echo "" + echo "Attaching to tmux session 'orcpub'..." + echo "(Detach with Ctrl+B, then D)" + echo "" + sleep 1 + tmux attach -t orcpub || { + echo "" + echo -e "${YELLOW}Session ended or detached.${NC}" + } + else + echo "" + echo -e "${YELLOW}No tmux session 'orcpub' found.${NC}" + echo "Use option 1 to start services in tmux first." + fi + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 3) + if tmux has-session -t orcpub 2>/dev/null; then + if tmux kill-session -t orcpub 2>/dev/null; then + echo "" + echo -e "${GREEN}Killed tmux session 'orcpub'.${NC}" + else + echo "" + echo -e "${YELLOW}Session may have already ended.${NC}" + fi + else + echo "" + echo -e "${YELLOW}No tmux session 'orcpub' to kill.${NC}" + fi + echo "" + read -r -p "Press Enter to continue..." + return + ;; + 0|""|$'\e'|$'\x7f') + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Tail Logs Submenu +# ----------------------------------------------------------------------------- + +show_tail_submenu() { + echo "" + echo -e "${BOLD}Tail Logs${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " 1) Datomic" + echo " 2) Server" + echo " 3) Figwheel" + echo " 4) All (interleaved)" + echo "" + echo " (Press Ctrl+C to stop tailing)" + echo "" + echo " 0) Back" + echo "" +} + +handle_tail_submenu() { + local choice + while true; do + show_tail_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + 1) + local log="$LOG_DIR/datomic.log" + if [[ -f "$log" ]]; then + echo "" + echo -e "${CYAN}Tailing $log (Ctrl+C to stop)${NC}" + echo "" + tail -f "$log" || true + echo "" + else + echo -e "${YELLOW}Log not found: $log${NC}" + fi + read -r -p "Press Enter to continue..." + return + ;; + 2) + local log="$LOG_DIR/server.log" + if [[ -f "$log" ]]; then + echo "" + echo -e "${CYAN}Tailing $log (Ctrl+C to stop)${NC}" + echo "" + tail -f "$log" || true + echo "" + else + echo -e "${YELLOW}Log not found: $log${NC}" + fi + read -r -p "Press Enter to continue..." + return + ;; + 3) + local log="$LOG_DIR/figwheel.log" + if [[ -f "$log" ]]; then + echo "" + echo -e "${CYAN}Tailing $log (Ctrl+C to stop)${NC}" + echo "" + tail -f "$log" || true + echo "" + else + echo -e "${YELLOW}Log not found: $log${NC}" + fi + read -r -p "Press Enter to continue..." + return + ;; + 4) + echo "" + echo -e "${CYAN}Tailing all logs (Ctrl+C to stop)${NC}" + echo "" + # Use tail -f on all existing logs + local logs=() + [[ -f "$LOG_DIR/datomic.log" ]] && logs+=("$LOG_DIR/datomic.log") + [[ -f "$LOG_DIR/server.log" ]] && logs+=("$LOG_DIR/server.log") + [[ -f "$LOG_DIR/figwheel.log" ]] && logs+=("$LOG_DIR/figwheel.log") + if [[ ${#logs[@]} -gt 0 ]]; then + tail -f "${logs[@]}" || true + echo "" + else + echo -e "${YELLOW}No log files found in $LOG_DIR${NC}" + fi + read -r -p "Press Enter to continue..." + return + ;; + 0|""|$'\e'|$'\x7f') + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Open in VS Code Submenu +# ----------------------------------------------------------------------------- + +show_vscode_submenu() { + echo "" + echo -e "${BOLD}Open Log in VS Code${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " 1) Datomic log" + echo " 2) Server log" + echo " 3) Figwheel log" + echo "" + echo " 0) Back" + echo "" +} + +handle_vscode_submenu() { + local choice + while true; do + show_vscode_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + 1) + open_log_in_editor "$LOG_DIR/datomic.log" + return + ;; + 2) + open_log_in_editor "$LOG_DIR/server.log" + return + ;; + 3) + open_log_in_editor "$LOG_DIR/figwheel.log" + return + ;; + 0|""|$'\e'|$'\x7f') + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +open_log_in_editor() { + local log="$1" + + if [[ ! -f "$log" ]]; then + echo "" + echo -e "${YELLOW}Log not found: $log${NC}" + read -r -p "Press Enter to continue..." + return + fi + + if command -v code >/dev/null 2>&1; then + echo "" + echo -e "${GREEN}Opening in VS Code: $log${NC}" + code --goto "$log:1" || { + echo -e "${YELLOW}Failed to open VS Code. Path: $log${NC}" + } + else + echo "" + echo -e "${YELLOW}VS Code CLI not found.${NC}" + echo "Log path: $log" + fi + read -r -p "Press Enter to continue..." +} + +# ----------------------------------------------------------------------------- +# Utilities Submenu +# ----------------------------------------------------------------------------- + +show_utils_submenu() { + echo "" + echo -e "${BOLD}Utilities${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo " l) Tail logs โ†’" + echo " o) Open log in VS Code โ†’" + echo "" + echo " u) Create test user (verified)" + echo " i) Install Datomic" + echo " c) Check prerequisites" + echo " m) Memory cleanup (kill duplicates)" + echo " r) Refresh status" + echo "" + echo " 0) Back" + echo "" +} + +handle_utils_submenu() { + local choice + while true; do + show_utils_submenu + read -r -p "Select: " -n 1 choice + echo "" + + case "$choice" in + l|L) + handle_tail_submenu + ;; + o|O) + handle_vscode_submenu + ;; + u|U) + echo "" + echo -e "${CYAN}Creating test user (test@example.com / testpass)...${NC}" + if "$SCRIPT_DIR/scripts/create_dummy_user.sh" test test@example.com testpass verify; then + echo -e "${GREEN}Test user created successfully${NC}" + else + echo -e "${YELLOW}User creation failed (may already exist)${NC}" + fi + echo "" + read -r -p "Press Enter to continue..." + return + ;; + i|I) + "$SCRIPT_DIR/scripts/start.sh" --install || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + c|C) + "$SCRIPT_DIR/scripts/start.sh" --check || true + echo "" + read -r -p "Press Enter to continue..." + return + ;; + m|M) + echo "" + "$SCRIPT_DIR/scripts/cleanup-duplicates.sh" + echo "" + read -r -p "Press Enter to continue..." + return + ;; + r|R) + return # Just return to refresh main menu + ;; + 0|""|$'\e'|$'\x7f') + return + ;; + *) + echo -e "${YELLOW}Invalid choice${NC}" + sleep 0.5 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Main Menu +# ----------------------------------------------------------------------------- + +show_main_menu() { + echo "" + echo -e "${BOLD}OrcPub Development Hub${NC}" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + echo "" + echo -e "${GREEN}Quick Actions${NC}" + echo " 1) Start Datomic" + echo " 2) Init Database" + echo " 3) Stop all services" + echo " q) Quit" + echo "" + echo " 4) Start Services โ†’" + echo " 5) Stop Services โ†’" + echo " 6) Utilities โ†’" + echo " 7) Tmux โ†’" + echo " 8) Help" + echo "" +} + +interactive_menu() { + local choice + local timeout_sec=120 # Auto-refresh status every 2 minutes + + while true; do + # Show status at top of menu + show_status_header + show_main_menu + + # Read with timeout for auto-refresh, -r to handle backslash + if read -t "$timeout_sec" -r -p "Select: " -n 1 choice; then + echo "" # Newline after single-char input + else + # Timeout reached - just refresh + echo "" + echo -e "${CYAN}[Auto-refresh]${NC}" + continue + fi + + case "$choice" in + 1) + echo "" + if ! "$SCRIPT_DIR/scripts/start.sh" datomic; then + # start.sh failed (port conflict declined, prereq error, etc.) + echo "" + read -r -p "Press Enter to return to menu..." + fi + ;; + 2) + echo "" + "$SCRIPT_DIR/scripts/start.sh" init-db || true + echo "" + read -r -p "Press Enter to continue..." + ;; + 3) + echo "" + "$SCRIPT_DIR/scripts/stop.sh" || true + echo "" + read -r -p "Press Enter to continue..." + ;; + 4) + handle_start_submenu + ;; + 5) + handle_stop_submenu + ;; + 6) + handle_utils_submenu + ;; + 7) + handle_tmux_submenu + ;; + 8) + echo "" + show_help + echo "" + read -r -p "Press Enter to continue..." + ;; + q|Q|$'\e') # q, Q, or Escape + echo "" + echo "Goodbye." + exit 0 + ;; + "") + # Enter pressed - just refresh + ;; + *) + echo "" + echo -e "${YELLOW}Invalid choice: '$choice'${NC}" + sleep 1 + ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- + +main() { + local command="${1:-}" + + case "$command" in + "") + interactive_menu + ;; + start) + shift + "$SCRIPT_DIR/scripts/start.sh" "$@" + ;; + stop) + shift + "$SCRIPT_DIR/scripts/stop.sh" "$@" + ;; + status) + "$SCRIPT_DIR/scripts/stop.sh" --dry-run + ;; + cleanup) + "$SCRIPT_DIR/scripts/cleanup-duplicates.sh" + ;; + help|--help|-h) + show_help + ;; + *) + echo "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/project.clj b/project.clj index c04aeb7ba..48476795a 100644 --- a/project.clj +++ b/project.clj @@ -13,9 +13,6 @@ :min-lein-version "2.7.1" :repositories [["apache" "http://repository.apache.org/snapshots/"] - ["my.datomic.com" {:url "https://my.datomic.com/repo" - :username [:gpg :env] - :password [:gpg :env]}] ; This allows us to seamlessly load jars from local disk. ["local" {:url "file:lib" :checksum :ignore @@ -23,54 +20,76 @@ ] :mirrors {"apache" {:url "https://repository.apache.org/snapshots/"}} - :dependencies [[org.clojure/clojure "1.10.0"] - [org.clojure/test.check "0.9.0"] - [org.clojure/clojurescript "1.10.439"] - [org.clojure/core.async "0.4.490"] - [cljsjs/react "16.6.0-0"] - [cljsjs/react-dom "16.6.0-0"] + :dependencies [[org.clojure/clojure "1.12.4"] + [org.clojure/test.check "1.1.1"] + [org.clojure/clojurescript "1.12.134"] + [org.clojure/core.async "1.8.741"] + ;; React 18 + Reagent 2.0 (Concurrent Mode) + [cljsjs/react "18.3.1-1"] + [cljsjs/react-dom "18.3.1-1"] [cljsjs/filesaverjs "1.3.3-0"] - [com.cognitect/transit-cljs "0.8.256"] + [com.cognitect/transit-cljs "0.8.280"] [cljs-http "0.1.45"] [com.andrewmcveigh/cljs-time "0.5.2"] - [clj-time "0.15.0"] - [clj-http "3.9.1"] + [clojure.java-time "1.4.2"] + [clj-http "3.13.1"] [com.yetanalytics/ring-etag-middleware "0.1.1"] - [org.clojure/test.check "0.9.0"] - - [org.clojure/core.match "0.3.0-alpha5"] - [re-frame "0.10.9"] - [reagent "0.7.0"] - [garden "1.3.2"] - [org.apache.pdfbox/pdfbox "2.1.0-SNAPSHOT"] - [io.pedestal/pedestal.service "0.5.1"] - [io.pedestal/pedestal.route "0.5.1"] - [io.pedestal/pedestal.jetty "0.5.1"] - [org.clojure/data.json "0.2.6"] + + [org.clojure/core.match "1.1.1"] + [re-frame "1.4.4"] + [reagent "2.0.1"] + [garden "1.3.10"] + [org.apache.pdfbox/pdfbox "3.0.6"] + ;; Pedestal 0.7.0 uses Jetty 11, which is compatible with figwheel-main's Ring adapter. + ;; Pedestal 0.7.1+ and 0.8.x use Jetty 12, causing NoClassDefFoundError: ScopedHandler + ;; (removed in Jetty 12). Upgrade blocked until figwheel-main supports Jetty 12. + [io.pedestal/pedestal.service "0.7.0"] + [io.pedestal/pedestal.route "0.7.0"] + [io.pedestal/pedestal.jetty "0.7.0"] + [io.pedestal/pedestal.error "0.7.0"] + [org.clojure/data.json "2.5.0"] [org.slf4j/slf4j-simple "1.7.21"] - [buddy/buddy-auth "1.4.1"] - [buddy/buddy-hashers "1.2.0"] + [buddy/buddy-auth "3.0.323"] + [buddy/buddy-hashers "2.0.167"] [reloaded.repl "0.2.3"] - [bidi "2.0.17"] + [bidi "2.1.6"] + + [com.stuartsierra/component "1.2.0"] + [com.google.guava/guava "32.1.2-jre"] - [com.stuartsierra/component "0.3.2"] - [com.google.guava/guava "21.0"] + [com.fasterxml.jackson.core/jackson-databind "2.15.2"] + [com.fasterxml.jackson.core/jackson-core "2.15.2"] + [com.fasterxml.jackson.core/jackson-annotations "2.15.2"] - [com.fasterxml.jackson.core/jackson-databind "2.11.1"] + ;; Required for Pedestal 0.7.x ring-middlewares + [commons-io/commons-io "2.15.1"] - [hiccup "1.0.5"] - [com.draines/postal "2.0.2"] - [environ "1.1.0"] + [hiccup "2.0.0"] + [com.draines/postal "2.0.5"] + [environ "1.2.0"] [pdfkit-clj "0.1.7"] - [vvvvalvalval/datomock "0.2.0"] - [com.datomic/datomic-free "0.9.5697"] - [funcool/cuerdas "2.2.0"] + ;; datomock fork with Datomic Pro 1.0.6527+ compatibility (new transact signature) + ;; Original vvvvalvalval/datomock 0.2.0 causes AbstractMethodError with Datomic Pro + [org.clojars.favila/datomock "0.2.2-favila1"] + ;; Datomic Pro: Free under Apache 2.0, supports Java 11/17/21, actively maintained. + ;; Exclude slf4j-nop to avoid duplicate SLF4J binding warnings. + ;; Installed to lib/com/datomic/datomic-pro/1.0.7482/ during Docker build/postCreateCommand + ;; Uses existing file:lib repository pattern (same as pdfbox) + ;; Latest version: https://docs.datomic.com/releases-pro.html + ;[com.datomic/datomic-pro "1.0.7482" :exclusions [org.slf4j/slf4j-nop]] + [com.datomic/peer "1.0.7482" :exclusions [org.slf4j/slf4j-nop]] + ;; cuerdas 026.415: Latest release on Clojars... does not match GH release versioning. + [funcool/cuerdas "2026.415"] [camel-snake-kebab "0.4.0"] - [org.webjars/font-awesome "5.13.1"]] - - :plugins [[lein-figwheel "0.5.19"] - [lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]] + [org.webjars/font-awesome "5.13.1"] + ;; See docs/UPGRADE_DEPENDENCIES.md for why this is needed on Java 9+/21 + [javax.servlet/javax.servlet-api "4.0.1"] + ;; figwheel-main for hot-reload dev (compatible with Pedestal 0.7.0's Jetty 11) + [com.bhauman/figwheel-main "0.2.20"] + [com.bhauman/rebel-readline-cljs "0.1.4"] + ] + :plugins [[lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]] [lein-localrepo "0.5.4"] [lein-garden "0.3.0"] [lein-environ "1.1.0"] @@ -84,7 +103,7 @@ :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] - :resource-paths ["resources" "resources/.ebextensions/"] + :resource-paths ["resources" "target" "resources/.ebextensions/"] :uberjar-name "orcpub.jar" @@ -100,7 +119,9 @@ ;; Compress the output? :pretty-print? false}}]} - :prep-tasks [["garden" "once"]] + ;; NOTE: Garden compilation removed from global :prep-tasks for faster REPL startup. + ;; CSS is compiled via: ./menu start garden, lein garden once, or automatically in uberjar build. + ;; The compiled CSS is checked into resources/public/css/compiled/styles.css :cljsbuild {:builds {:dev @@ -123,8 +144,7 @@ :source-map-timestamp true :pretty-print true :closure-defines {goog.DEBUG true} - :optimizations :none - }}}} + :optimizations :none}}}} :figwheel {;; :http-server-root "public" ;; default and assumes "resources" ;; :server-port 3449 ;; default @@ -175,20 +195,21 @@ :uberjar-inclusions [#"^\.ebextensions"] :jar-inclusions [#"^\.ebextensions"] - :aliases {"figwheel-native" ["with-profile" "native-dev" "run" "-m" "user" "--figwheel"] - ;;"figwheel-web" ["figwheel"] + :aliases {"fig:dev" ["trampoline" "run" "-m" "figwheel.main" "--" "--build" "dev" "--repl"] + "fig:build" ["run" "-m" "figwheel.main" "--" "--build-once" "dev"] + "figwheel-native" ["with-profile" "native-dev" "run" "-m" "user" "--figwheel"] "externs" ["do" "clean" ["run" "-m" "externs"]] "rebuild-modules" ["run" "-m" "user" "--rebuild-modules"] - "lint" ["with-profile" "lint" "run" "-m" "clj-kondo.main" "--lint" "src"] + ;; --fail-level error: exit 0 on warnings, exit 1 only on errors + "lint" ["with-profile" "lint" "run" "-m" "clj-kondo.main" "--lint" "src" "--fail-level" "error"] "prod-build" ^{:doc "Recompile code with prod profile."} ["externs" ["with-profile" "prod" "cljsbuild" "once" "main"]]} - :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"] - [figwheel-sidecar "0.5.19"] - [cider/piggieback "0.4.0"] - [org.clojure/test.check "0.9.0"] - [day8.re-frame/re-frame-10x "0.3.7"]] + :profiles {:dev {:dependencies [[binaryage/devtools "1.0.7"] + [cider/piggieback "0.5.3"] + [day8.re-frame/re-frame-10x "1.11.0" :exclusions [zprint rewrite-clj]] + ] :env {:dev-mode "true"} ;; need to add dev source path here to get user.clj loaded :source-paths ["web/cljs" "src/clj" "src/cljc" "src/cljs" "dev"] @@ -199,14 +220,13 @@ :pretty-print true ;; To console.log CLJS data-structures make sure you enable devtools in Chrome ;; https://github.com/binaryage/cljs-devtools - :preloads [devtools.preload day8.re-frame-10x.preload]}}}} + :preloads [devtools.preload]}}}} ;; for CIDER ;; :plugins [[cider/cider-nrepl "0.12.0"]] :repl-options {:init-ns user :nrepl-middleware [cider.piggieback/wrap-cljs-repl]}} - :native-dev {:dependencies [[figwheel-sidecar "0.5.19"] - [com.cemerick/piggieback "0.2.1"] - [org.clojure/test.check "0.9.0"]] + ;; NOTE: :native-dev was for React Native builds (legacy, may be unused) + :native-dev {:dependencies [[cider/piggieback "0.5.3"]] :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] :cljsbuild {:builds [{:id "main" :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] @@ -216,6 +236,8 @@ :output-dir "target" :optimizations :none}}]} :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}} + ;; NOTE: :prod was for React Native builds (legacy, may be unused) + ;; datomic-pro dependency removed - peer is already in main deps :prod {:cljsbuild {:builds [{:id "main" :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/prod"] :compiler {:output-to "main.js" @@ -225,9 +247,8 @@ :externs ["js/externs.js"] :parallel-build true :optimize-constants true - :optimizations :advanced}}]} - :dependencies [[com.datomic/datomic-free "0.9.5697"]]} - :uberjar {:prep-tasks ["clean" "compile" ["cljsbuild" "once" "prod"]] + :optimizations :advanced}}]}} + :uberjar {:prep-tasks ["clean" ["garden" "once"] "compile" ["cljsbuild" "once" "prod"]] :env {:production true} :aot :all :omit-source true @@ -243,6 +264,10 @@ :lint {:dependencies [[clj-kondo "2024.05.22"]] :clj-kondo {:linters {:shadowed-fn-param {:level :off} :shadowed-var {:level :off}}}} + ;; Minimal profile for init-db - no ClojureScript, no Garden + ;; Use: lein with-profile init-db run -m orcpub.dev-init + :init-db {:source-paths ["src/clj" "src/cljc"] + :prep-tasks ^:replace []} ;; Use like: lein with-profile +start-server repl :start-server {:repl-options {:init-ns user :init (start-server)}}}) diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 000000000..d96f3f8b6 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,440 @@ +#!/usr/bin/env bash +# ============================================================================= +# common.sh - Shared utilities for OrcPub external scripts +# ============================================================================= +# Source this file in start.sh, stop.sh, and menu: +# source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +# ============================================================================= + +# Prevent double-sourcing +[[ -n "${_ORCPUB_COMMON_LOADED:-}" ]] && return +_ORCPUB_COMMON_LOADED=1 + +# ----------------------------------------------------------------------------- +# Path Setup +# ----------------------------------------------------------------------------- + +# SCRIPT_DIR should be set by the sourcing script, but provide fallback +COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="${REPO_ROOT:-$(cd "$COMMON_DIR/.." && pwd)}" + +# ----------------------------------------------------------------------------- +# Environment Configuration +# ----------------------------------------------------------------------------- + +# Source .env if present (authoritative config) +if [[ -f "$REPO_ROOT/.env" ]]; then + set -a + # shellcheck disable=SC1091 + . "$REPO_ROOT/.env" + set +a +fi + +# Defaults (used if not set in .env) +DATOMIC_VERSION="${DATOMIC_VERSION:-1.0.7482}" +DATOMIC_TYPE="${DATOMIC_TYPE:-pro}" +JAVA_MIN_VERSION="${JAVA_MIN_VERSION:-11}" +LOG_DIR="${LOG_DIR:-$REPO_ROOT/logs}" + +# Port configuration +DATOMIC_PORT="${DATOMIC_PORT:-4334}" +SERVER_PORT="${SERVER_PORT:-8890}" +NREPL_PORT="${NREPL_PORT:-7888}" +FIGWHEEL_PORT="${FIGWHEEL_PORT:-3449}" +GARDEN_PORT="${GARDEN_PORT:-3000}" + +# Derived paths +DATOMIC_DIR="$REPO_ROOT/lib/com/datomic/datomic-${DATOMIC_TYPE}/${DATOMIC_VERSION}" +DATOMIC_CONFIG="$DATOMIC_DIR/config/working-transactor.properties" +DATOMIC_CONFIG_TEMPLATE="$DATOMIC_DIR/config/samples/dev-transactor-template.properties" + +# Ensure logs directory exists +mkdir -p "$LOG_DIR" 2>/dev/null || true + +# ----------------------------------------------------------------------------- +# Colors +# ----------------------------------------------------------------------------- + +# Disable colors if not a terminal or NO_COLOR is set +if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + BOLD='\033[1m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + CYAN='' + BOLD='' + NC='' +fi + +# ----------------------------------------------------------------------------- +# Exit Codes (standardized across all scripts) +# ----------------------------------------------------------------------------- +# 0 = Success (or already running in idempotent mode) +# 1 = Usage / invalid args +# 2 = Prerequisite / config failure +# 3 = Runtime failure (process crashed, timeout, port conflict) + +EXIT_SUCCESS=0 +EXIT_USAGE=1 +EXIT_PREREQ=2 +EXIT_RUNTIME=3 + +# ----------------------------------------------------------------------------- +# Configurable Timeouts +# ----------------------------------------------------------------------------- + +KILL_WAIT="${KILL_WAIT:-5}" +PORT_WAIT="${PORT_WAIT:-30}" + +# ----------------------------------------------------------------------------- +# Quiet Mode Support +# ----------------------------------------------------------------------------- + +# Global quiet mode flag (set by scripts via --quiet) +QUIET="${QUIET:-false}" + +# ----------------------------------------------------------------------------- +# Interactive Detection +# ----------------------------------------------------------------------------- + +# Check if running interactively (both stdin and stdout are terminals) +is_interactive() { + [[ -t 0 && -t 1 ]] +} + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- + +# log_info and log_warn respect QUIET mode +# log_error ALWAYS outputs (to stderr) - errors should never be silenced +log_info() { + [[ "$QUIET" == "true" ]] && return + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + [[ "$QUIET" == "true" ]] && return + echo -e "${YELLOW}[WARN]${NC} $1" >&2 +} + +log_error() { + # Always output errors to stderr, even in quiet mode + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +# ----------------------------------------------------------------------------- +# Port Utilities +# ----------------------------------------------------------------------------- + +# Check if a port is in use (returns 0 if in use, 1 if free) +port_in_use() { + local port="$1" + if command -v lsof >/dev/null 2>&1; then + lsof -i ":${port}" >/dev/null 2>&1 + elif command -v ss >/dev/null 2>&1; then + ss -tln 2>/dev/null | grep -q ":${port}\b" + elif command -v netstat >/dev/null 2>&1; then + netstat -tln 2>/dev/null | grep -q ":${port}\b" + else + # Fallback: try to connect + timeout 1 bash -c "/dev/null + fi +} + +# Wait for a port to become available (up to timeout seconds) +wait_for_port() { + local port="$1" + local timeout="${2:-30}" + local elapsed=0 + + while [[ $elapsed -lt $timeout ]]; do + if port_in_use "$port"; then + return 0 + fi + sleep 1 + ((elapsed++)) + done + return 1 +} + +# Wait for a port to become available, but fail fast if the process dies +# Usage: wait_for_port_or_die PORT PID [TIMEOUT] +wait_for_port_or_die() { + local port="$1" + local pid="$2" + local timeout="${3:-60}" + local elapsed=0 + + while [[ $elapsed -lt $timeout ]]; do + # Check if process is still alive + if ! kill -0 "$pid" 2>/dev/null; then + log_error "Process $pid died while waiting for port $port" + return 1 + fi + # Check if port is ready + if port_in_use "$port"; then + return 0 + fi + sleep 1 + ((elapsed++)) + done + log_error "Timeout waiting for port $port (process $pid still running)" + return 1 +} + +# Wait for a port to become free (up to timeout seconds) +wait_for_port_free() { + local port="$1" + local timeout="${2:-10}" + local elapsed=0 + + while [[ $elapsed -lt $timeout ]]; do + if ! port_in_use "$port"; then + return 0 + fi + sleep 1 + ((elapsed++)) + done + return 1 +} + +# Find PIDs listening on a port (cross-platform) +find_pids_by_port() { + local port="$1" + local pids="" + + if command -v lsof >/dev/null 2>&1; then + pids=$(lsof -t -i ":${port}" 2>/dev/null || true) + elif command -v ss >/dev/null 2>&1; then + # Cross-platform: use sed instead of grep -oP + pids=$(ss -tlnp 2>/dev/null | grep ":${port}\b" | sed -n 's/.*pid=\([0-9]*\).*/\1/p' || true) + elif command -v netstat >/dev/null 2>&1; then + pids=$(netstat -tlnp 2>/dev/null | grep ":${port}\b" | awk '{print $7}' | cut -d'/' -f1 | grep -E '^[0-9]+$' || true) + fi + + echo "$pids" | tr '\n' ' ' | xargs +} + +# Find PIDs by process name pattern (cross-platform) +find_pids_by_name() { + local pattern="$1" + local pids="" + + if command -v pgrep >/dev/null 2>&1; then + pids=$(pgrep -f "$pattern" 2>/dev/null || true) + else + pids=$(ps aux 2>/dev/null | grep -E "$pattern" | grep -v grep | awk '{print $2}' || true) + fi + + # Filter out our own PID and parent + local self_pid=$$ + local parent_pid=$PPID + local filtered="" + for pid in $pids; do + [[ "$pid" != "$self_pid" && "$pid" != "$parent_pid" ]] && filtered="$filtered $pid" + done + + echo "$filtered" | xargs +} + +# Get process info for display +get_process_info() { + local pid="$1" + [[ -z "$pid" ]] && return + ps -p "$pid" -o pid=,user=,args= 2>/dev/null | head -c 80 || echo "$pid (info unavailable)" +} + +# Get process uptime +get_uptime() { + local pid="$1" + [[ -z "$pid" ]] && echo "-" && return + local etime + etime=$(ps -p "$pid" -o etime= 2>/dev/null | xargs || true) + echo "${etime:-unknown}" +} + +# ----------------------------------------------------------------------------- +# Prerequisite Checks +# ----------------------------------------------------------------------------- + +check_java() { + local java_version + java_version=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+).*/\1/') + + if [[ -z "$java_version" ]]; then + log_error "Java not found. Please install Java $JAVA_MIN_VERSION or higher." + return 1 + fi + + if [[ "$java_version" -lt "$JAVA_MIN_VERSION" ]]; then + log_error "Java $JAVA_MIN_VERSION+ required (found Java $java_version)." + log_info "Use the devcontainer or install a compatible JDK." + return 1 + fi + + log_info "Java $java_version detected (minimum: $JAVA_MIN_VERSION)" + return 0 +} + +check_lein() { + if ! command -v lein >/dev/null 2>&1; then + log_error "Leiningen not found. Please use the devcontainer or install leiningen." + return 1 + fi + return 0 +} + +check_tmux() { + if ! command -v tmux >/dev/null 2>&1; then + log_error "tmux not found. Install tmux or run without --tmux." + return 1 + fi + return 0 +} + +check_datomic_installed() { + if [[ ! -d "$DATOMIC_DIR" ]]; then + log_error "Datomic ${DATOMIC_TYPE} ${DATOMIC_VERSION} not found." + log_error "Expected at: $DATOMIC_DIR" + log_info "Run './start.sh --install' to install Datomic." + return 1 + fi + + if [[ ! -f "$DATOMIC_DIR/bin/transactor" ]]; then + log_error "Datomic transactor not found. Installation may be incomplete." + log_error "Expected at: $DATOMIC_DIR/bin/transactor" + log_info "Run './start.sh --install' to reinstall Datomic." + return 1 + fi + + if [[ ! -x "$DATOMIC_DIR/bin/transactor" ]]; then + log_error "Datomic transactor exists but is not executable." + log_error "Path: $DATOMIC_DIR/bin/transactor" + log_info "Try: chmod +x $DATOMIC_DIR/bin/transactor" + return 1 + fi + + return 0 +} + +# ----------------------------------------------------------------------------- +# Process Management +# ----------------------------------------------------------------------------- + +# Graceful shutdown with SIGKILL fallback +kill_gracefully() { + local pid="$1" + local wait_secs="${2:-$KILL_WAIT}" + + # Try SIGTERM first + kill -TERM "$pid" 2>/dev/null || return 0 + + # Wait for process to exit + for ((i=0; i/dev/null || return 0 + sleep 1 + done + + # Process still running - escalate to SIGKILL + log_warn "Process $pid didn't stop gracefully, sending SIGKILL" + kill -KILL "$pid" 2>/dev/null || true +} + +# Clean up stale PID files +cleanup_stale_pid() { + local name="$1" + local pid_file="$LOG_DIR/${name}.pid" + + if [[ -f "$pid_file" ]]; then + local old_pid + old_pid=$(cat "$pid_file" 2>/dev/null || true) + if [[ -n "$old_pid" ]] && ! kill -0 "$old_pid" 2>/dev/null; then + rm -f "$pid_file" + log_info "Cleaned up stale PID file for $name" + fi + fi +} + +# Find service PIDs using PID file first, then port/pattern fallback +find_service_pids() { + local name="$1" + local port="$2" + local pattern="$3" + local pids="" + + # 1. Check PID file first (most reliable) + local pid_file="$LOG_DIR/${name}.pid" + if [[ -f "$pid_file" ]]; then + local file_pid + file_pid=$(cat "$pid_file" 2>/dev/null || true) + if [[ -n "$file_pid" ]] && kill -0 "$file_pid" 2>/dev/null; then + pids="$file_pid" + fi + fi + + # 2. Fall back to port scan + name pattern + if [[ -z "$pids" ]]; then + pids=$(echo "$(find_pids_by_port "$port") $(find_pids_by_name "$pattern")" | tr ' ' '\n' | sort -u | xargs) + fi + + echo "$pids" +} + +# ----------------------------------------------------------------------------- +# Failure Diagnostics +# ----------------------------------------------------------------------------- + +# Show detailed diagnostics when a service fails to start +show_startup_failure() { + local name="$1" + local log_file="$2" + local port="${3:-}" + + log_error "Service '$name' failed to start. Diagnostics:" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + + if [[ -n "$log_file" && -f "$log_file" ]]; then + echo "Last 30 lines of $log_file:" + tail -30 "$log_file" 2>/dev/null || echo "(could not read log file)" + else + echo "Log file: (not available)" + fi + + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + + if [[ -n "$port" ]]; then + echo "Processes on port $port:" + local pids + pids=$(find_pids_by_port "$port") + if [[ -n "$pids" ]]; then + for pid in $pids; do + ps -p "$pid" -o pid,user,args 2>/dev/null || echo " PID $pid (info unavailable)" + done + else + echo " (none)" + fi + fi + + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" +} + +# ----------------------------------------------------------------------------- +# Datomic Config Helpers +# ----------------------------------------------------------------------------- + +# Parse port from transactor config file +get_datomic_port_from_config() { + local config="$1" + if [[ -f "$config" ]]; then + grep -E '^port=' "$config" 2>/dev/null | cut -d= -f2 || echo "$DATOMIC_PORT" + else + echo "$DATOMIC_PORT" + fi +} diff --git a/scripts/create_dummy_user.sh b/scripts/create_dummy_user.sh new file mode 100755 index 000000000..112fa1f17 --- /dev/null +++ b/scripts/create_dummy_user.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create a user in the database (requires Datomic running) +# Uses :init-db profile for faster startup (skips ClojureScript) + +if [ "$#" -lt 3 ]; then + echo "Usage: $0 [verify]" + echo "Example: $0 testuser test@example.com s3cret verify" + echo "" + echo "Options:" + echo " verify Mark user as verified (can log in immediately)" + exit 1 +fi + +username="$1" +email="$2" +password="$3" +shift 3 + +override="${1:-}" # optional "verify" + +# Use init-db profile for fast startup (no ClojureScript/Garden) +if [ "$override" = "verify" ]; then + lein with-profile init-db run -m orcpub.dev-tools "$username" "$email" "$password" verify +else + lein with-profile init-db run -m orcpub.dev-tools "$username" "$email" "$password" +fi diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh new file mode 100644 index 000000000..fbe009431 --- /dev/null +++ b/scripts/dev-setup.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +# scripts/dev-setup.sh +# Usage: ./scripts/dev-setup.sh [--no-start] [--skip-datomic] [--start] +# +# This script orchestrates initial dev environment setup: +# 1. Start Datomic (if not skipped) +# 2. Run lein deps +# 3. Initialize database +# 4. Optionally start server/figwheel + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +NO_START=false +SKIP_DATOMIC=false +START=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-start) NO_START=true; shift ;; + --skip-datomic) SKIP_DATOMIC=true; shift ;; + --start) START=1; shift ;; + -h|--help) + cat <&2 + } +else + echo "Skipping Datomic startup as requested." +fi + +# Ensure dependencies are downloaded +echo "Running lein deps..." +lein deps + +# Run idempotent DB initialization via dev-init main +echo "Initializing database (idempotent)..." +# Only attempt DB init if Datomic is reachable on the expected port +if timeout 1 bash -c '/dev/null 2>&1; then + if lein run -m orcpub.dev-init; then + echo "DB init succeeded." + else + echo "DB init failed but continuing (non-fatal)." >&2 + fi +else + echo "Datomic not reachable on localhost:4334; skipping DB init (post-create will not fail)." +fi + +if [ "$START" -eq 1 ] && [ "$NO_START" = false ]; then + echo "Starting backend and figwheel in background..." + "$SCRIPT_DIR/start.sh" server --background --quiet || true + "$SCRIPT_DIR/start.sh" figwheel --background --quiet || true + echo "Started server & figwheel (logs in ./logs/)" +else + echo "Setup complete. To start services:" + echo " ./scripts/start.sh server" + echo " ./scripts/start.sh figwheel" + echo "Or use the interactive menu:" + echo " ./menu" +fi + +exit 0 diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 000000000..72c69ed97 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,765 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# start.sh - OrcPub Service Launcher +# ============================================================================= +# Usage: +# ./start.sh Start Datomic + REPL with server (default) +# ./start.sh datomic Start Datomic transactor only +# ./start.sh server Start REPL with server (requires Datomic running) +# ./start.sh figwheel Start Figwheel for ClojureScript hot-reload +# ./start.sh garden Start Garden for CSS auto-compilation +# ./start.sh init-db Initialize the database +# +# Options: +# --install, -i Run Datomic Pro installation (post-create.sh) +# --tmux, -t Run service(s) in tmux session 'orcpub' +# --background, -b Run service(s) in background with nohup +# --quiet, -q Minimal output (for automation) +# --check, -c Validate prerequisites without starting services +# --idempotent, -I Succeed if service is already running +# --help, -h Show this help +# +# Exit Codes: +# 0 - Success (or already running with --idempotent) +# 1 - Usage error (invalid args) +# 2 - Prerequisite failure (missing Java, Datomic, etc.) +# 3 - Runtime failure (port conflict, startup timeout) +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared utilities +# shellcheck source=common.sh +source "$SCRIPT_DIR/common.sh" + +# ----------------------------------------------------------------------------- +# Install +# ----------------------------------------------------------------------------- + +run_install() { + local post_create="$REPO_ROOT/.devcontainer/post-create.sh" + + if [[ ! -x "$post_create" ]]; then + log_error "Install script not found or not executable: $post_create" + exit 1 + fi + + log_info "Running Datomic ${DATOMIC_TYPE} ${DATOMIC_VERSION} installation..." + "$post_create" + log_info "Installation complete." +} + +# ----------------------------------------------------------------------------- +# Port Conflict Detection +# ----------------------------------------------------------------------------- + +# Check port availability, respecting --idempotent and non-interactive mode +# Returns: 0 if available (or idempotent+running), 1 if should abort, 2 if should skip +# Sets IDEMPOTENT_ALREADY_RUNNING=true if service already running in idempotent mode +# Sets SKIP_SERVICE=true if user chose to skip this service +check_port_available() { + local port="$1" + local service="$2" + local idempotent="${3:-false}" + + IDEMPOTENT_ALREADY_RUNNING=false + SKIP_SERVICE=false + + if port_in_use "$port"; then + local pid + pid=$(find_pids_by_port "$port" | awk '{print $1}') + + # Idempotent mode: succeed if already running + if [[ "$idempotent" == "true" ]]; then + log_info "$service already running on port $port (PID: ${pid:-unknown})" + IDEMPOTENT_ALREADY_RUNNING=true + return 0 + fi + + # Non-interactive mode: fail immediately with clear error + if ! is_interactive; then + log_error "Port $port in use (non-interactive mode, exiting)" + log_error "PID: ${pid:-unknown}" + return 1 + fi + + # Interactive mode: offer to stop, skip, or abort (with timeout) + log_warn "Port $port is already in use (PID: ${pid:-unknown})" + if ! read -t 30 -p "$service: [s]kip, s[t]op existing, or [a]bort? [s/t/a] " -n 1 -r; then + echo + log_error "Prompt timed out after 30 seconds" + return 1 + fi + echo + case "$REPLY" in + t|T|y|Y) + log_info "Stopping existing $service..." + "$SCRIPT_DIR/stop.sh" "$service" --yes --quiet + sleep 1 + if port_in_use "$port"; then + log_error "Failed to stop $service on port $port" + return 1 + fi + log_info "Port $port is now available" + return 0 + ;; + s|S|"") + log_info "Skipping $service (already running)" + SKIP_SERVICE=true + return 2 + ;; + *) + log_error "Aborting." + return 1 + ;; + esac + fi + return 0 +} + +# ----------------------------------------------------------------------------- +# Datomic Readiness +# ----------------------------------------------------------------------------- + +wait_for_datomic() { + local timeout="${1:-$PORT_WAIT}" + local log_file="${2:-$LOG_DIR/datomic.log}" + + log_info "Waiting for Datomic to be ready (port $DATOMIC_PORT)..." + + if wait_for_port "$DATOMIC_PORT" "$timeout"; then + log_info "Datomic is ready" + return 0 + else + # Show diagnostics on failure + show_startup_failure "datomic" "$log_file" "$DATOMIC_PORT" + return 1 + fi +} + +prepare_datomic_config() { + if [[ ! -f "$DATOMIC_CONFIG" ]]; then + if [[ -f "$DATOMIC_CONFIG_TEMPLATE" ]]; then + cp "$DATOMIC_CONFIG_TEMPLATE" "$DATOMIC_CONFIG" + # Restrict permissions (config may contain secrets) + chmod 600 "$DATOMIC_CONFIG" + log_info "Created transactor config from template" + else + log_error "Datomic config template not found." + log_error "Expected at: $DATOMIC_CONFIG_TEMPLATE" + return 1 + fi + fi + return 0 +} + +# ----------------------------------------------------------------------------- +# Pre-flight Checks (--check mode) +# ----------------------------------------------------------------------------- + +# Run all prerequisite checks for a target without starting services +# Returns: 0 if all checks pass, 2 (EXIT_PREREQ) otherwise +run_checks() { + local target="$1" + local failed=0 + + log_info "Running pre-flight checks for target: $target" + echo "" + + # Common checks for all targets + echo -n "Java ($JAVA_MIN_VERSION+): " + if check_java 2>/dev/null; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + ((failed++)) + fi + + echo -n "Leiningen: " + if check_lein 2>/dev/null; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + ((failed++)) + fi + + # Target-specific checks + case "$target" in + all|datomic) + echo -n "Datomic installed: " + if check_datomic_installed 2>/dev/null; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + ((failed++)) + fi + + echo -n "Datomic config: " + if [[ -f "$DATOMIC_CONFIG" ]] || [[ -f "$DATOMIC_CONFIG_TEMPLATE" ]]; then + echo -e "${GREEN}OK${NC}" + # Check port consistency if config exists + if [[ -f "$DATOMIC_CONFIG" ]]; then + local config_port + config_port=$(get_datomic_port_from_config "$DATOMIC_CONFIG") + if [[ "$config_port" != "$DATOMIC_PORT" ]]; then + echo -e " ${YELLOW}WARNING: Config port ($config_port) differs from DATOMIC_PORT ($DATOMIC_PORT)${NC}" + fi + fi + else + echo -e "${RED}FAILED${NC} (no config or template)" + ((failed++)) + fi + + echo -n "Datomic port ($DATOMIC_PORT): " + if port_in_use "$DATOMIC_PORT"; then + echo -e "${YELLOW}IN USE${NC} (PID: $(find_pids_by_port "$DATOMIC_PORT" | head -1))" + else + echo -e "${GREEN}AVAILABLE${NC}" + fi + ;; + esac + + case "$target" in + all|server) + echo -n "Server port ($SERVER_PORT): " + if port_in_use "$SERVER_PORT"; then + echo -e "${YELLOW}IN USE${NC} (PID: $(find_pids_by_port "$SERVER_PORT" | head -1))" + else + echo -e "${GREEN}AVAILABLE${NC}" + fi + + if [[ "$target" == "server" ]]; then + echo -n "Datomic reachable: " + if port_in_use "$DATOMIC_PORT"; then + echo -e "${GREEN}YES${NC}" + else + echo -e "${YELLOW}NO${NC} (server requires Datomic)" + fi + fi + ;; + esac + + case "$target" in + figwheel) + echo -n "Figwheel port ($FIGWHEEL_PORT): " + if port_in_use "$FIGWHEEL_PORT"; then + echo -e "${YELLOW}IN USE${NC}" + else + echo -e "${GREEN}AVAILABLE${NC}" + fi + ;; + esac + + echo "" + if [[ $failed -gt 0 ]]; then + log_error "$failed prerequisite check(s) failed" + return $EXIT_PREREQ + else + log_info "All prerequisite checks passed" + return $EXIT_SUCCESS + fi +} + +# ----------------------------------------------------------------------------- +# Tmux Support +# ----------------------------------------------------------------------------- + +TMUX_SESSION="orcpub" + +run_in_tmux() { + local window_name="$1" + shift + local cmd_args=("$@") + + check_tmux || exit $EXIT_PREREQ + + # Build a properly quoted command string to avoid argument splitting + local quoted_cmd + quoted_cmd=$(printf '%q ' "${cmd_args[@]}") + + if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then + # Session exists - add new window + # Use -c for working directory (safer than bash -c string building) + tmux new-window -t "$TMUX_SESSION" -n "$window_name" -c "$REPO_ROOT" + # Set remain-on-exit so window stays open after command finishes + tmux set-option -t "$TMUX_SESSION:$window_name" remain-on-exit on + # Send the command (properly quoted) + tmux send-keys -t "$TMUX_SESSION:$window_name" "$quoted_cmd" C-m + log_info "Started '$window_name' in tmux window (session: $TMUX_SESSION)" + else + # Create new session with this window + tmux new-session -d -s "$TMUX_SESSION" -n "$window_name" -c "$REPO_ROOT" + tmux set-option -t "$TMUX_SESSION:$window_name" remain-on-exit on + tmux send-keys -t "$TMUX_SESSION:$window_name" "$quoted_cmd" C-m + log_info "Created tmux session '$TMUX_SESSION' with window '$window_name'" + fi + + log_info "Attach with: tmux attach -t $TMUX_SESSION" +} + +# ----------------------------------------------------------------------------- +# Background Support +# ----------------------------------------------------------------------------- + +run_in_background() { + local name="$1" + shift + local cmd_args=("$@") + local log_file="$LOG_DIR/${name}.log" + local pid_file="$LOG_DIR/${name}.pid" + + # Clean up any stale PID file + cleanup_stale_pid "$name" + + log_info "Starting $name in background..." + log_info "Log file: $log_file" + + # Run command in background from REPO_ROOT + # Use pushd/popd instead of bash -c for safer execution + ( + cd "$REPO_ROOT" || exit 1 + nohup "${cmd_args[@]}" > "$log_file" 2>&1 & + echo $! > "$pid_file" + ) + + local pid + pid=$(cat "$pid_file" 2>/dev/null || true) + + log_info "$name started (PID: ${pid:-unknown})" + log_info "Tail logs: tail -f $log_file" +} + +# ----------------------------------------------------------------------------- +# Start Targets +# ----------------------------------------------------------------------------- + +start_datomic() { + local idempotent="${1:-false}" + local port_result=0 + + check_datomic_installed || exit $EXIT_PREREQ + + # Check port (0=available, 1=abort, 2=skip) + check_port_available "$DATOMIC_PORT" "datomic" "$idempotent" || port_result=$? + [[ $port_result -eq 1 ]] && exit $EXIT_RUNTIME + + # If already running (idempotent or user chose skip), exit success + if [[ "$IDEMPOTENT_ALREADY_RUNNING" == "true" || "$SKIP_SERVICE" == "true" ]]; then + exit $EXIT_SUCCESS + fi + + prepare_datomic_config || exit $EXIT_PREREQ + + # Clean up stale PID file + cleanup_stale_pid "datomic" + + log_info "Starting Datomic transactor (${DATOMIC_TYPE} ${DATOMIC_VERSION})..." + nohup "$DATOMIC_DIR/bin/transactor" "$DATOMIC_CONFIG" > "$LOG_DIR/datomic.log" 2>&1 & + local datomic_pid=$! + echo "$datomic_pid" > "$LOG_DIR/datomic.pid" + log_info "Datomic transactor started (PID $datomic_pid)" + log_info "Logs: $LOG_DIR/datomic.log" + + # Early verification: ensure process didn't die immediately + sleep 0.5 + if ! kill -0 "$datomic_pid" 2>/dev/null; then + log_error "Datomic process died immediately after starting" + show_startup_failure "datomic" "$LOG_DIR/datomic.log" "$DATOMIC_PORT" + exit $EXIT_RUNTIME + fi + + # Wait for Datomic to be ready + if ! wait_for_datomic "$PORT_WAIT" "$LOG_DIR/datomic.log"; then + log_error "Failed to start Datomic. Check logs: $LOG_DIR/datomic.log" + exit $EXIT_RUNTIME + fi +} + +start_server() { + local idempotent="${1:-false}" + local port_result=0 + + # Check port (0=available, 1=abort, 2=skip) + check_port_available "$SERVER_PORT" "server" "$idempotent" || port_result=$? + [[ $port_result -eq 1 ]] && exit $EXIT_RUNTIME + + # If already running (idempotent or user chose skip), exit success + if [[ "$IDEMPOTENT_ALREADY_RUNNING" == "true" || "$SKIP_SERVICE" == "true" ]]; then + exit $EXIT_SUCCESS + fi + + cd "$REPO_ROOT" + + # Use headless mode if not running interactively (background/nohup) + if [[ -t 0 ]]; then + log_info "Starting REPL with server (profile: +dev,+start-server)..." + lein with-profile +dev,+start-server repl + else + log_info "Starting headless server (profile: +dev,+start-server)..." + lein with-profile +dev,+start-server repl :headless + fi +} + +start_figwheel() { + local idempotent="${1:-false}" + local port_result=0 + + # Check port (0=available, 1=abort, 2=skip) + check_port_available "$FIGWHEEL_PORT" "figwheel" "$idempotent" || port_result=$? + [[ $port_result -eq 1 ]] && exit $EXIT_RUNTIME + + # If already running (idempotent or user chose skip), exit success + if [[ "$IDEMPOTENT_ALREADY_RUNNING" == "true" || "$SKIP_SERVICE" == "true" ]]; then + exit $EXIT_SUCCESS + fi + + # Clean up stale PID file + cleanup_stale_pid "figwheel" + + log_info "Starting Figwheel (ClojureScript hot-reload)..." + cd "$REPO_ROOT" + # Use fig:dev alias (trampoline run -m figwheel.main -- --build dev --repl) + nohup lein fig:dev > "$LOG_DIR/figwheel.log" 2>&1 & + local figwheel_pid=$! + echo "$figwheel_pid" > "$LOG_DIR/figwheel.pid" + log_info "Figwheel started (PID $figwheel_pid)" + log_info "Logs: $LOG_DIR/figwheel.log" + + # Early verification: ensure process didn't die immediately + sleep 0.5 + if ! kill -0 "$figwheel_pid" 2>/dev/null; then + log_error "Figwheel process died immediately after starting" + show_startup_failure "figwheel" "$LOG_DIR/figwheel.log" "$FIGWHEEL_PORT" + exit $EXIT_RUNTIME + fi + + # Wait for Figwheel to be ready (fail fast if process dies) + log_info "Waiting for Figwheel to be ready (port $FIGWHEEL_PORT)..." + if wait_for_port_or_die "$FIGWHEEL_PORT" "$figwheel_pid" "$PORT_WAIT"; then + log_info "Figwheel is ready" + else + show_startup_failure "figwheel" "$LOG_DIR/figwheel.log" "$FIGWHEEL_PORT" + exit $EXIT_RUNTIME + fi +} + +start_garden() { + # Clean up stale PID file + cleanup_stale_pid "garden" + + log_info "Starting Garden (CSS auto-compilation)..." + cd "$REPO_ROOT" + nohup lein garden auto > "$LOG_DIR/garden.log" 2>&1 & + local garden_pid=$! + echo "$garden_pid" > "$LOG_DIR/garden.pid" + log_info "Garden started (PID $garden_pid)" + log_info "Logs: $LOG_DIR/garden.log" + + # Garden has no port to check - verify process survives startup + # Check multiple times to catch delayed failures (e.g., lein project parsing) + local checks=0 + local max_checks=5 + while [[ $checks -lt $max_checks ]]; do + sleep 1 + if ! kill -0 "$garden_pid" 2>/dev/null; then + log_error "Garden process died during startup" + show_startup_failure "garden" "$LOG_DIR/garden.log" "" + exit $EXIT_RUNTIME + fi + ((checks++)) + done + log_info "Garden is running" +} + +init_database() { + log_info "Initializing database..." + + # Check if Datomic is running + if ! port_in_use "$DATOMIC_PORT"; then + log_error "Datomic is not running on port $DATOMIC_PORT" + log_info "Start Datomic first: ./start.sh datomic" + exit $EXIT_PREREQ + fi + + cd "$REPO_ROOT" + # Use init-db profile to skip ClojureScript/Garden compilation + # This only loads src/clj and src/cljc - much faster + if lein with-profile init-db run -m orcpub.dev-init; then + log_info "Database initialized successfully" + else + log_error "Database initialization failed" + exit $EXIT_RUNTIME + fi +} + +start_all() { + local idempotent="${1:-false}" + local started_datomic="false" + local datomic_pid="" + local port_result=0 + + # Cleanup function for signal handling + cleanup_on_exit() { + local exit_code=$? + if [[ "$started_datomic" == "true" && -n "$datomic_pid" ]]; then + log_info "Stopping Datomic (PID $datomic_pid)..." + kill "$datomic_pid" 2>/dev/null || true + # Wait briefly for graceful shutdown + sleep 1 + kill -0 "$datomic_pid" 2>/dev/null && kill -9 "$datomic_pid" 2>/dev/null || true + rm -f "$LOG_DIR/datomic.pid" + log_info "Datomic stopped" + fi + exit "$exit_code" + } + + # Set up trap for cleanup on interrupt/termination + trap cleanup_on_exit INT TERM + + check_datomic_installed || exit $EXIT_PREREQ + + # Check Datomic port (0=available, 1=abort, 2=skip) + port_result=0 + check_port_available "$DATOMIC_PORT" "datomic" "$idempotent" || port_result=$? + if [[ $port_result -eq 1 ]]; then + exit $EXIT_RUNTIME + fi + + # If Datomic already running (idempotent or skipped), skip starting it + if [[ "$IDEMPOTENT_ALREADY_RUNNING" != "true" && "$SKIP_SERVICE" != "true" ]]; then + prepare_datomic_config || exit $EXIT_PREREQ + + # Clean up stale PID + cleanup_stale_pid "datomic" + + log_info "Starting Datomic transactor (background)..." + nohup "$DATOMIC_DIR/bin/transactor" "$DATOMIC_CONFIG" > "$LOG_DIR/datomic.log" 2>&1 & + datomic_pid=$! + started_datomic="true" + echo "$datomic_pid" > "$LOG_DIR/datomic.pid" + log_info "Datomic transactor started (PID $datomic_pid)" + + # Early verification: ensure process didn't die immediately + sleep 0.5 + if ! kill -0 "$datomic_pid" 2>/dev/null; then + log_error "Datomic process died immediately after starting" + show_startup_failure "datomic" "$LOG_DIR/datomic.log" "$DATOMIC_PORT" + started_datomic="false" # Don't try to clean up a dead process + exit $EXIT_RUNTIME + fi + + # Wait for Datomic to be ready (with proper readiness check) + if ! wait_for_datomic "$PORT_WAIT" "$LOG_DIR/datomic.log"; then + log_error "Failed to start Datomic. Check logs: $LOG_DIR/datomic.log" + exit $EXIT_RUNTIME + fi + else + log_info "Datomic already running, skipping startup" + fi + + # Check server port (0=available, 1=abort, 2=skip) + port_result=0 + check_port_available "$SERVER_PORT" "server" "$idempotent" || port_result=$? + if [[ $port_result -eq 1 ]]; then + exit $EXIT_RUNTIME + fi + + if [[ "$IDEMPOTENT_ALREADY_RUNNING" == "true" || "$SKIP_SERVICE" == "true" ]]; then + log_info "Server already running on port $SERVER_PORT" + # Clear trap since we're not managing Datomic lifecycle + trap - INT TERM + exit $EXIT_SUCCESS + fi + + log_info "Starting REPL with server (profile: +dev,+start-server)..." + log_info "Note: Ctrl+C will stop both server and Datomic" + cd "$REPO_ROOT" + lein with-profile +dev,+start-server repl +} + +# ----------------------------------------------------------------------------- +# Help +# ----------------------------------------------------------------------------- + +show_help() { + cat << EOF +OrcPub Service Launcher + +Usage: + ./start.sh [target] [options] + +Targets: + (none) Start Datomic (background) + REPL with server (foreground) + datomic Start Datomic transactor only (foreground) + server Start REPL with server only (foreground, requires Datomic) + figwheel Start Figwheel for ClojureScript hot-reload + garden Start Garden for CSS auto-compilation + init-db Initialize the database (requires Datomic running) + help Show this help + +Options: + --install, -i Install/reinstall Datomic Pro (runs post-create.sh) + --tmux, -t Run in tmux session 'orcpub' (non-blocking) + --background, -b Run in background with nohup (logs to $LOG_DIR/) + --quiet, -q Minimal output (for automation) + --check, -c Validate prerequisites without starting + --idempotent, -I Succeed if service already running + --if-not-running Alias for --idempotent + +Exit Codes: + 0 Success (or already running with --idempotent) + 1 Usage error (invalid args) + 2 Prerequisite failure (missing Java, Datomic not installed, etc.) + 3 Runtime failure (port conflict, startup timeout, process crash) + +Environment Variables (via .env or shell): + DATOMIC_VERSION Datomic version (default: 1.0.7482) + DATOMIC_TYPE Datomic type: pro or dev (default: pro) + JAVA_MIN_VERSION Minimum Java version required (default: 11) + LOG_DIR Directory for log files (default: ./logs) + DATOMIC_PORT Datomic port (default: 4334) + SERVER_PORT Server port (default: 8890) + PORT_WAIT Timeout for port readiness (default: 30) + KILL_WAIT Timeout for graceful shutdown (default: 5) + +Configuration: + Config is loaded from: \$REPO_ROOT/.env + Datomic is expected at: lib/com/datomic/datomic-\${TYPE}/\${VERSION}/ + +Examples: + ./start.sh # Full dev stack: Datomic + server + ./start.sh --install # Install Datomic Pro + ./start.sh datomic # Just Datomic (run in separate terminal) + ./start.sh server # Just REPL+server (after Datomic is running) + ./start.sh init-db # Initialize the database + ./start.sh --tmux # All services in tmux session + ./start.sh datomic --tmux # Datomic in tmux window + ./start.sh datomic -b # Datomic in background + +Automation examples: + ./start.sh --check # Pre-flight: validate all prerequisites + ./start.sh datomic --check # Pre-flight: validate Datomic only + ./start.sh datomic -q --idempotent # Start or succeed if already running + ./start.sh datomic --if-not-running -q # Same as above + +Notes: + - For full development, run in separate terminals: + 1. ./start.sh datomic + 2. ./start.sh server + 3. ./start.sh figwheel (optional) + 4. ./start.sh garden (optional) + - Or use ./start.sh alone for Datomic + server in one terminal + - Or use ./start.sh --tmux to run all in a tmux session + - In non-interactive mode (CI/cron), port conflicts fail immediately +EOF +} + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- + +main() { + local target="" + local do_install="false" + local use_tmux="false" + local use_background="false" + local do_check="false" + local idempotent="false" + local positional=() + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --install|-i) do_install="true"; shift ;; + --tmux|-t) use_tmux="true"; shift ;; + --background|-b) use_background="true"; shift ;; + --quiet|-q) QUIET="true"; export QUIET; shift ;; + --check|-c) do_check="true"; shift ;; + --idempotent|-I|--if-not-running) + idempotent="true"; shift ;; + --help|-h) show_help; exit $EXIT_SUCCESS ;; + -*) log_error "Unknown option: $1"; show_help; exit $EXIT_USAGE ;; + *) positional+=("$1"); shift ;; + esac + done + + target="${positional[0]:-all}" + + # Handle install flag (no prereq checks needed) + if [[ "$do_install" == "true" ]]; then + run_install + exit $EXIT_SUCCESS + fi + + # Handle help (no prereq checks needed) + if [[ "$target" == "help" ]]; then + show_help + exit $EXIT_SUCCESS + fi + + # Handle --check mode (pre-flight validation) + if [[ "$do_check" == "true" ]]; then + run_checks "$target" + exit $? + fi + + # Check prerequisites for runtime targets + check_java || exit $EXIT_PREREQ + check_lein || exit $EXIT_PREREQ + + # If --tmux, delegate to tmux runner + if [[ "$use_tmux" == "true" ]]; then + case "$target" in + all|"") + # Start each service in its own tmux window + run_in_tmux "datomic" "$SCRIPT_DIR/start.sh" datomic + sleep 2 + run_in_tmux "server" "$SCRIPT_DIR/start.sh" server + ;; + datomic) run_in_tmux "datomic" "$SCRIPT_DIR/start.sh" datomic ;; + server) run_in_tmux "server" "$SCRIPT_DIR/start.sh" server ;; + figwheel) run_in_tmux "figwheel" "$SCRIPT_DIR/start.sh" figwheel ;; + garden) run_in_tmux "garden" "$SCRIPT_DIR/start.sh" garden ;; + init-db) run_in_tmux "init-db" "$SCRIPT_DIR/start.sh" init-db ;; + *) log_error "Unknown target: $target"; show_help; exit $EXIT_USAGE ;; + esac + exit $EXIT_SUCCESS + fi + + # If --background, delegate to background runner + if [[ "$use_background" == "true" ]]; then + case "$target" in + all|"") + run_in_background "datomic" "$SCRIPT_DIR/start.sh" datomic + log_info "Waiting for Datomic..." + if wait_for_datomic "$PORT_WAIT" "$LOG_DIR/datomic.log"; then + run_in_background "server" "$SCRIPT_DIR/start.sh" server + else + log_error "Datomic failed to start" + exit $EXIT_RUNTIME + fi + ;; + datomic) run_in_background "datomic" "$SCRIPT_DIR/start.sh" datomic ;; + server) run_in_background "server" "$SCRIPT_DIR/start.sh" server ;; + figwheel) run_in_background "figwheel" "$SCRIPT_DIR/start.sh" figwheel ;; + garden) run_in_background "garden" "$SCRIPT_DIR/start.sh" garden ;; + *) log_error "Cannot run '$target' in background"; exit $EXIT_USAGE ;; + esac + exit $EXIT_SUCCESS + fi + + # Direct execution (foreground) + case "$target" in + all|"") start_all "$idempotent" ;; + datomic) start_datomic "$idempotent" ;; + server) start_server "$idempotent" ;; + figwheel) start_figwheel "$idempotent" ;; + garden) start_garden ;; + init-db) init_database ;; + *) log_error "Unknown target: $target"; show_help; exit $EXIT_USAGE ;; + esac +} + +main "$@" diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 000000000..c5064ea4d --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# stop.sh - OrcPub Process Management (Stop/Kill/Status) +# ============================================================================= +# Usage: +# ./stop.sh Stop all OrcPub services (with confirmation) +# ./stop.sh --dry-run Show status without stopping anything +# ./stop.sh --yes Stop without confirmation +# ./stop.sh --force Use SIGKILL if SIGTERM doesn't work +# ./stop.sh --quiet Minimal output (for scripting) +# ./stop.sh repl Stop nREPL only +# ./stop.sh server Stop server only +# ./stop.sh datomic Stop Datomic only +# ./stop.sh figwheel Stop Figwheel only +# ./stop.sh port Stop process on specific port +# ./stop.sh name Stop processes matching pattern +# +# Exit Codes: +# 0 - Success (processes stopped or none found) +# 1 - Usage error (invalid args) +# 3 - Runtime failure (failed to stop processes) +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared utilities +# shellcheck source=common.sh +source "$SCRIPT_DIR/common.sh" + +# ----------------------------------------------------------------------------- +# Status Display (--dry-run) +# ----------------------------------------------------------------------------- + +show_status() { + local quiet="${1:-false}" + + [[ "$quiet" == "true" ]] && { + # Quiet mode: just exit codes + local running=0 + for port in "$DATOMIC_PORT" "$SERVER_PORT" "$NREPL_PORT"; do + port_in_use "$port" && ((running++)) + done + echo "$running" + return + } + + echo "" + echo -e "${BOLD}OrcPub Service Status${NC}" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + printf "%-16s %-8s %-10s %-10s %s\n" "Service" "Port" "Status" "PID" "Uptime" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + + local services=( + "Datomic:$DATOMIC_PORT:datomic.*transactor" + "Server:$SERVER_PORT:lein.*start-server" + "nREPL:$NREPL_PORT:nrepl" + "Figwheel:$FIGWHEEL_PORT:figwheel" + "Garden:$GARDEN_PORT:garden" + ) + + for entry in "${services[@]}"; do + IFS=':' read -r name port pattern <<< "$entry" + local pid + pid=$(find_pids_by_port "$port" | awk '{print $1}') + + if [[ -n "$pid" ]]; then + local uptime + uptime=$(get_uptime "$pid") + printf "%-16s %-8s ${GREEN}%-10s${NC} %-10s %s\n" "$name" "$port" "running" "$pid" "$uptime" + else + printf "%-16s %-8s ${YELLOW}%-10s${NC} %-10s %s\n" "$name" "$port" "stopped" "-" "-" + fi + done + + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo "" +} + +# ----------------------------------------------------------------------------- +# Kill Functions +# ----------------------------------------------------------------------------- + +confirm_kill() { + local pids="$1" + local description="$2" + local skip_confirm="${3:-false}" + local quiet="${4:-false}" + + if [[ -z "$pids" ]]; then + [[ "$quiet" != "true" ]] && log_warn "No processes found for: $description" + return 1 + fi + + if [[ "$quiet" != "true" ]]; then + echo "" + echo "Found processes to stop ($description):" + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + for pid in $pids; do + echo " $(get_process_info "$pid")" + done + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + fi + + [[ "$skip_confirm" == "true" ]] && return 0 + + # Non-interactive protection: fail fast instead of hanging on read + if ! is_interactive; then + log_error "Cannot prompt for confirmation (non-interactive mode)" + log_error "Use --yes to skip confirmation in scripts/CI" + return 1 + fi + + if ! read -t 30 -p "Stop these processes? [y/N] " -n 1 -r; then + echo + log_error "Prompt timed out after 30 seconds" + return 1 + fi + echo + [[ $REPLY =~ ^[Yy]$ ]] && return 0 + [[ "$quiet" != "true" ]] && log_info "Aborted." + return 1 +} + +kill_pids() { + local pids="$1" + local use_force="${2:-false}" + local quiet="${3:-false}" + local wait_time=3 + + [[ -z "$pids" ]] && return 0 + + [[ "$quiet" != "true" ]] && log_info "Sending SIGTERM to PIDs: $pids" + for pid in $pids; do + kill -TERM "$pid" 2>/dev/null || true + done + + sleep "$wait_time" + + # Check for survivors + local remaining="" + for pid in $pids; do + kill -0 "$pid" 2>/dev/null && remaining="$remaining $pid" + done + remaining=$(echo "$remaining" | xargs) + + if [[ -n "$remaining" ]]; then + if [[ "$use_force" == "true" ]]; then + [[ "$quiet" != "true" ]] && log_warn "Processes still running, sending SIGKILL: $remaining" + for pid in $remaining; do + kill -KILL "$pid" 2>/dev/null || true + done + sleep 1 + else + [[ "$quiet" != "true" ]] && log_warn "Some processes still running: $remaining" + [[ "$quiet" != "true" ]] && log_info "Use --force to send SIGKILL" + return 1 + fi + fi + + [[ "$quiet" != "true" ]] && log_info "All processes terminated successfully" +} + +# ----------------------------------------------------------------------------- +# Stop Targets (PID-first, port-fallback approach) +# ----------------------------------------------------------------------------- + +stop_repl() { + local skip="$1" force="$2" quiet="$3" + local pids result=0 + # Use PID-first lookup from common.sh + pids=$(find_service_pids "nrepl" "$NREPL_PORT" 'nrepl') + if confirm_kill "$pids" "nREPL (port $NREPL_PORT)" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + # Clean up PID file after stopping + rm -f "$LOG_DIR/nrepl.pid" 2>/dev/null || true + fi + return $result +} + +stop_server() { + local skip="$1" force="$2" quiet="$3" + local pids result=0 + pids=$(find_service_pids "server" "$SERVER_PORT" 'lein.*start-server') + if confirm_kill "$pids" "Server (port $SERVER_PORT)" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + rm -f "$LOG_DIR/server.pid" 2>/dev/null || true + fi + return $result +} + +stop_datomic() { + local skip="$1" force="$2" quiet="$3" + local pids result=0 + pids=$(find_service_pids "datomic" "$DATOMIC_PORT" 'datomic.*transactor') + if confirm_kill "$pids" "Datomic (port $DATOMIC_PORT)" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + rm -f "$LOG_DIR/datomic.pid" 2>/dev/null || true + fi + return $result +} + +stop_figwheel() { + local skip="$1" force="$2" quiet="$3" + local pids result=0 + pids=$(find_service_pids "figwheel" "$FIGWHEEL_PORT" 'figwheel') + if confirm_kill "$pids" "Figwheel (port $FIGWHEEL_PORT)" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + rm -f "$LOG_DIR/figwheel.pid" 2>/dev/null || true + fi + return $result +} + +stop_garden() { + local skip="$1" force="$2" quiet="$3" + local pids result=0 + pids=$(find_service_pids "garden" "$GARDEN_PORT" 'garden.*auto') + if confirm_kill "$pids" "Garden" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + rm -f "$LOG_DIR/garden.pid" 2>/dev/null || true + fi + return $result +} + +stop_port() { + local port="$1" skip="$2" force="$3" quiet="$4" + [[ -z "$port" ]] && { log_error "Usage: $0 port "; exit $EXIT_USAGE; } + [[ ! "$port" =~ ^[0-9]+$ ]] && { log_error "Invalid port: $port"; exit $EXIT_USAGE; } + local pids + pids=$(find_pids_by_port "$port") + confirm_kill "$pids" "port $port" "$skip" "$quiet" && kill_pids "$pids" "$force" "$quiet" +} + +stop_name() { + local pattern="$1" skip="$2" force="$3" quiet="$4" + [[ -z "$pattern" ]] && { log_error "Usage: $0 name "; exit $EXIT_USAGE; } + + # Warn about very broad patterns (security/safety measure) + if [[ ${#pattern} -lt 4 ]]; then + log_warn "Pattern '$pattern' is very broad (${#pattern} chars)" + if ! is_interactive; then + log_error "Refusing to use broad pattern in non-interactive mode" + exit $EXIT_USAGE + fi + if ! read -t 30 -p "This may match many processes. Continue? [y/N] " -n 1 -r; then + echo + log_error "Prompt timed out after 30 seconds" + exit $EXIT_RUNTIME + fi + echo + [[ ! $REPLY =~ ^[Yy]$ ]] && { log_info "Aborted."; exit $EXIT_SUCCESS; } + fi + + local pids + pids=$(find_pids_by_name "$pattern") + confirm_kill "$pids" "pattern '$pattern'" "$skip" "$quiet" && kill_pids "$pids" "$force" "$quiet" +} + +stop_all() { + local skip="$1" force="$2" quiet="$3" + local pids="" result=0 + + # Collect PIDs from all services using PID-first approach + for service in datomic server nrepl figwheel garden; do + local service_pids="" + case "$service" in + datomic) service_pids=$(find_service_pids "datomic" "$DATOMIC_PORT" 'datomic.*transactor') ;; + server) service_pids=$(find_service_pids "server" "$SERVER_PORT" 'lein.*start-server') ;; + nrepl) service_pids=$(find_service_pids "nrepl" "$NREPL_PORT" 'nrepl') ;; + figwheel) service_pids=$(find_service_pids "figwheel" "$FIGWHEEL_PORT" 'figwheel') ;; + garden) service_pids=$(find_service_pids "garden" "$GARDEN_PORT" 'garden.*auto') ;; + esac + [[ -n "$service_pids" ]] && pids="$pids $service_pids" + done + + pids=$(echo "$pids" | tr ' ' '\n' | sort -u | xargs) + + if confirm_kill "$pids" "all OrcPub services" "$skip" "$quiet"; then + kill_pids "$pids" "$force" "$quiet" || result=$? + # Clean up all PID files + rm -f "$LOG_DIR"/*.pid 2>/dev/null || true + fi + return $result +} + +# ----------------------------------------------------------------------------- +# Help +# ----------------------------------------------------------------------------- + +show_help() { + cat << 'EOF' +OrcPub Process Management + +Usage: + ./stop.sh [target] [options] + +Targets: + (none) Stop all OrcPub services + repl Stop nREPL processes + server Stop OrcPub server + datomic Stop Datomic transactor + figwheel Stop Figwheel + garden Stop Garden CSS watcher + port Stop process on specific port + name Stop processes matching pattern + +Options: + --dry-run Show status without stopping anything + --yes, -y Skip confirmation prompt + --force, -f Use SIGKILL if SIGTERM doesn't stop the process + --quiet, -q Minimal output (for scripting) + --help, -h Show this help + +Examples: + ./stop.sh # Stop all (interactive) + ./stop.sh --dry-run # Show what's running + ./stop.sh --yes # Stop all without prompting + ./stop.sh repl --yes # Stop nREPL only + ./stop.sh figwheel --yes # Stop Figwheel only + ./stop.sh port 8890 --force # Force kill port 8890 +EOF +} + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- + +main() { + local target="" + local skip_confirm="false" + local use_force="false" + local dry_run="false" + local quiet="false" + local positional=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run|--status) dry_run="true"; shift ;; + --yes|-y) skip_confirm="true"; shift ;; + --force|-f) use_force="true"; shift ;; + --quiet|-q) quiet="true"; QUIET="true"; export QUIET; skip_confirm="true"; shift ;; + --help|-h) show_help; exit $EXIT_SUCCESS ;; + -*) log_error "Unknown option: $1"; show_help; exit $EXIT_USAGE ;; + *) positional+=("$1"); shift ;; + esac + done + + [[ "$dry_run" == "true" ]] && { show_status "$quiet"; exit $EXIT_SUCCESS; } + + target="${positional[0]:-all}" + + case "$target" in + all) stop_all "$skip_confirm" "$use_force" "$quiet" ;; + repl) stop_repl "$skip_confirm" "$use_force" "$quiet" ;; + server) stop_server "$skip_confirm" "$use_force" "$quiet" ;; + datomic) stop_datomic "$skip_confirm" "$use_force" "$quiet" ;; + figwheel) stop_figwheel "$skip_confirm" "$use_force" "$quiet" ;; + garden) stop_garden "$skip_confirm" "$use_force" "$quiet" ;; + port) stop_port "${positional[1]:-}" "$skip_confirm" "$use_force" "$quiet" ;; + name) stop_name "${positional[1]:-}" "$skip_confirm" "$use_force" "$quiet" ;; + *) log_error "Unknown target: $target"; show_help; exit $EXIT_USAGE ;; + esac +} + +main "$@" diff --git a/src/clj/orcpub/config.clj b/src/clj/orcpub/config.clj new file mode 100644 index 000000000..498f418b4 --- /dev/null +++ b/src/clj/orcpub/config.clj @@ -0,0 +1,84 @@ +(ns orcpub.config + (:require [environ.core :refer [env]] + [clojure.string :as str])) + +(def default-datomic-uri "datomic:dev://localhost:4334/orcpub") + +(defn datomic-env + "Return the raw DATOMIC_URL environment value or nil if unset." [] + (or (env :datomic-url) + (some-> (System/getenv "DATOMIC_URL") not-empty))) + +(defn get-datomic-uri + "Return the Datomic URI from the environment or the default. + + Prefers the raw env value (from `datomic-env`), otherwise returns a safe + local development default (datomic:dev://localhost:4334/orcpub)." + [] + (or (datomic-env) + default-datomic-uri)) + +;; Content Security Policy configuration +;; CSP_POLICY environment variable options: +;; - "strict" : Nonce-based CSP with 'strict-dynamic' (default, maximum security) +;; - "permissive" : Allows same-origin scripts without strict-dynamic (legacy fallback) +;; - "none" : Disables CSP entirely (not recommended for production) + +(def permissive-csp-settings + "CSP that allows same-origin scripts without strict-dynamic. + Compatible with traditional