diff --git a/.agents/skills/logseq-repl/SKILL.md b/.agents/skills/logseq-repl/SKILL.md index a4dfb2a1d0..d224197aaf 100644 --- a/.agents/skills/logseq-repl/SKILL.md +++ b/.agents/skills/logseq-repl/SKILL.md @@ -1,33 +1,120 @@ --- name: logseq-repl -description: Start and coordinate Logseq development REPL workflows for the Desktop renderer `:app`, Electron main-process `:electron`, and `:db-worker-node` runtimes while sharing a single `yarn watch` process. +description: Start and coordinate Logseq development REPL workflows for the Desktop renderer `:app`, Electron main-process `:electron`, and `:db-worker-node` runtimes through one unified workflow. --- -# Logseq REPL workflows +# Logseq REPL Workflow Use this skill when the user needs a Logseq development REPL for: - Desktop renderer `:app` - Electron main process `:electron` - `:db-worker-node` -- or any combination of them +- any combination of those runtimes -This skill keeps both workflows coordinated through `/tmp/logseq-repl/` so they do not start multiple different REPL types at the same time with competing `shadow-cljs` servers. +The workflow uses one shared state directory: `/tmp/logseq-repl/`. -## Required preflight cleanup +## Scripts -Before starting or attaching any REPL, run both cleanup scripts: +Start everything: ```bash -.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh -.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh +./scripts/start-repl.sh --repo demo ``` -They stop only skill-managed Desktop, `db-worker-node`, and shared `shadow-cljs` processes. They do **not** guarantee a clean environment if another manually started or externally managed Logseq/shadow-cljs session is still running. +Clean up everything: -## Required post-cleanup port audit +```bash +./scripts/cleanup-repl.sh +``` -Immediately after cleanup, verify the standard ports: +Verify all REPL targets after startup: + +```bash +./scripts/verify-repls.sh +``` + +`start-repl.sh` starts: + +1. shared `pnpm watch` +2. Desktop dev app via `pnpm dev-electron-app` +3. `db-worker-node` via `node ./static/db-worker-node.js --repo ` + +`start-repl.sh` is a small shell wrapper around `start-repl.py`. The Python script verifies that `:app`, `:electron`, and `:db-worker-node` runtimes are live, runs `verify-repls.sh` to connect to each target REPL and print a small result, then prints attach commands. It does not attach an interactive REPL by itself and exits after verification. + +`cleanup-repl.sh` stops all workflow-managed processes without requiring a target selection. It also removes legacy state files from older split workflows and attempts to stop repo-owned listeners on the standard REPL ports. + +`verify-repls.sh` connects to `app`, `electron`, and `db-worker-node` with `pnpm exec shadow-cljs cljs-repl`, evaluates one target-specific expression in each REPL, and prints the output. + +## Standard Flow + +Before starting or attaching: + +```bash +./scripts/cleanup-repl.sh +``` + +Then start all runtimes: + +```bash +./scripts/start-repl.sh --repo demo +``` + +Attach only to the target you need: + +```bash +pnpm exec shadow-cljs cljs-repl app +pnpm exec shadow-cljs cljs-repl electron +pnpm exec shadow-cljs cljs-repl db-worker-node +``` + +## Runtime Selection + +- Need DOM, UI, renderer state, or page rendering? Use `:app`. +- Need Electron main process APIs, menus, window lifecycle, or main-process filesystem behavior? Use `:electron`. +- Need Node worker behavior or db-worker-node code paths? Use `:db-worker-node`. + +Runtime reminders: + +- `:app` = Electron renderer +- `:electron` = Electron main process +- `:db-worker` = browser worker +- `:db-worker-node` = Node worker + +## Readiness Model + +Keep these separate: + +1. watch alive: a `shadow-cljs` server or `pnpm watch` process exists +2. build ready: the target build completed successfully +3. runtime attached: a live JS runtime is connected for `:app`, `:electron`, or `:db-worker-node` + +If runtime count is `0`, do not attach yet. Fix runtime startup first. + +Check runtime counts: + +```bash +pnpm exec shadow-cljs clj-eval "(do (require '[shadow.cljs.devtools.api :as api]) (println {:app (count (api/repl-runtimes :app)) :electron (count (api/repl-runtimes :electron)) :db-worker-node (count (api/repl-runtimes :db-worker-node))}))" +``` + +Interpretation: + +- `:app > 0` means a Desktop renderer runtime is attached +- `:electron > 0` means an Electron main-process runtime is attached +- `:db-worker-node > 0` means a worker-node runtime is attached +- `0` means not ready, even if watch/build logs look healthy + +## Logs + +Look here first: + +- `/tmp/logseq-repl/shared-shadow-watch.log` +- `/tmp/logseq-repl/desktop-electron.log` +- `/tmp/logseq-repl/db-worker-node.log` + +## Port Audit + +After cleanup, verify standard ports if startup still reports conflicts: ```bash lsof -nP -iTCP:8701 -sTCP:LISTEN @@ -40,127 +127,15 @@ lsof -nP -iTCP:9631 -sTCP:LISTEN Interpretation: - no listeners: clean enough to continue -- listeners after cleanup: external conflict first -- listeners only after startup: expected if owned by the workflow you just started +- listeners after cleanup: resolve the external conflict first +- listeners only after startup: expected if owned by the workflow -If listeners remain right after cleanup, stop and resolve that conflict before trusting later startup results. - -## Readiness model: watch vs build vs runtime - -Keep these separate: - -1. **watch alive** — a `shadow-cljs` server or `yarn watch` process exists -2. **build ready** — the target build completed successfully -3. **runtime attached** — a live JS runtime is connected for `:app`, `:electron`, or `:db-worker-node` - -Common failure mode: - -- `yarn watch` is alive -- logs say `Build completed` -- runtime count is still `0` - -If runtime count is `0`, do **not** attach yet. Fix runtime startup first. - -## Cheat sheet - -### Pick the right runtime - -- Need DOM, UI, renderer state, page rendering? Use **Desktop app `:app` REPL**. -- Need Node worker behavior or db-worker-node code paths? Use **db-worker-node REPL**. -- Need Electron main process APIs, menus, window lifecycle, or main-process filesystem code? Use **Electron main-process `:electron` REPL**. - -Runtime reminders: - -- `:app` = Electron renderer -- `:electron` = Electron main process -- `:db-worker` = browser worker -- `:db-worker-node` = Node worker - -### Fast paths +## Non-Interactive Verification Examples Desktop `:app`: ```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl -npx shadow-cljs cljs-repl app -``` - -Electron `:electron`: - -```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl -npx shadow-cljs cljs-repl electron -``` - -`db-worker-node`: - -```bash -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl -npx shadow-cljs cljs-repl db-worker-node -``` - -Multiple runtimes: - -```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl -npx shadow-cljs cljs-repl app -npx shadow-cljs cljs-repl electron -npx shadow-cljs cljs-repl db-worker-node -``` - -### Preflight for Desktop `:app` - -- close browser dev app instances before starting Desktop `:app` -- keep the Desktop window as the only renderer runtime you want to attach to - -Why: the start script expects exactly one live `:app` runtime. - -### Shared watch and logs - -Both workflows share one `yarn watch` process. - -Look here first: - -- real shared watch log: `/tmp/logseq-repl/shared-shadow-watch.log` - -Workflow-local files may exist but may stay mostly empty when shared watch is reused. - -### Editor attach helpers - -```clojure -(shadow.user/cljs-repl) -(shadow.user/electron-repl) -(shadow.user/worker-node-repl) -``` - -### Runtime-count health checks - -Before attaching, verify that the intended runtime count is non-zero. - -Check all runtimes at once: - -```bash -npx shadow-cljs clj-eval "(do (require '[shadow.cljs.devtools.api :as api]) (println {:app (count (api/repl-runtimes :app)) :electron (count (api/repl-runtimes :electron)) :db-worker-node (count (api/repl-runtimes :db-worker-node))}))" -``` - -Interpretation: - -- `:app > 0` means a Desktop renderer runtime is really attached -- `:electron > 0` means the Electron main-process runtime is really attached -- `:db-worker-node > 0` means the worker-node runtime is really attached -- `0` means **not ready yet**, even if watch/build logs look healthy - -Do not run `npx shadow-cljs cljs-repl ` until the intended runtime count is non-zero. - -### Non-interactive verification examples - -Prefer heredocs over complex `printf` quoting. - -Desktop `:app`: - -```bash -cat <<'EOF' | npx shadow-cljs cljs-repl app +cat <<'EOF' | pnpm exec shadow-cljs cljs-repl app (prn {:runtime :app :document? (some? js/document) :title (.-title js/document)}) :cljs/quit EOF @@ -169,7 +144,7 @@ EOF Electron `:electron`: ```bash -cat <<'EOF' | npx shadow-cljs cljs-repl electron +cat <<'EOF' | pnpm exec shadow-cljs cljs-repl electron (prn {:runtime :electron :process? (some? js/process) :type (.-type js/process)}) :cljs/quit EOF @@ -178,360 +153,45 @@ EOF `db-worker-node`: ```bash -cat <<'EOF' | npx shadow-cljs cljs-repl db-worker-node +cat <<'EOF' | pnpm exec shadow-cljs cljs-repl db-worker-node (prn {:runtime :db-worker-node :process? (some? js/process) :platform (.-platform js/process)}) :cljs/quit EOF ``` -### Cleanup both workflows - -```bash -.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh -.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh -``` - -Expected behavior: - -- cleaning one workflow keeps shared watch alive if the other is still active -- cleaning the last active workflow stops shared watch - ---- - -## Details and troubleshooting - -### Desktop app `:app` REPL - -Scripts: - -- `scripts/start-desktop-app-repl.sh` -- `scripts/cleanup-desktop-app-repl.sh` - -Standard start: - -```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh -``` - -Start without attaching: - -```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl -``` - -What it does: - -1. starts or reuses `yarn watch` -2. waits for `:app` and `:electron` builds plus nREPL -3. starts or reuses `yarn dev-electron-app` -4. verifies one live `:app` runtime -5. runs a DOM smoke test -6. attaches `npx shadow-cljs cljs-repl app` unless `--no-repl` is used - -### Electron main-process `:electron` REPL - -Use the same Desktop startup script because the Electron main process comes from the same `yarn dev-electron-app` session and shared `yarn watch`. - -Standard start without attaching: - -```bash -.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl -``` - -Then attach `:electron`: - -```bash -npx shadow-cljs cljs-repl electron -``` - -What it does: - -1. starts or reuses `yarn watch` -2. waits for `:app` and `:electron` builds plus nREPL -3. starts or reuses `yarn dev-electron-app` -4. verifies the Desktop app is alive via the renderer smoke test -5. lets you attach `npx shadow-cljs cljs-repl electron` - -Notes: - -- there is no separate Electron main-process start script today; reuse `start-desktop-app-repl.sh` -- `:electron` and `:app` can both be attached against the same Desktop dev app session -- **watch ready is not enough**; `:electron` runtime count must be non-zero before attaching -- if `npx shadow-cljs cljs-repl electron` says `No available JS runtime`, first inspect runtime counts; do not keep retrying blindly - -### db-worker-node REPL - -Scripts: - -- `scripts/start-db-worker-node-repl.sh` -- `scripts/cleanup-db-worker-node-repl.sh` - -Standard start: - -```bash -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo -``` - -Start without attaching: - -```bash -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl -``` - -Pass extra runtime args: - -```bash -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo -- --create-empty-db -``` - -What it does: - -1. starts or reuses shared `yarn watch` -2. waits for the `:db-worker-node` build -3. starts or reuses `node ./static/db-worker-node.js --repo ` -4. runs a REPL connectivity check -5. attaches `npx shadow-cljs cljs-repl db-worker-node` unless `--no-repl` is used - -### Run multiple runtimes together - -Use one terminal or editor session per runtime. - -Examples: - -- `:app` + `:electron`: start Desktop once with `--no-repl`, then attach each runtime separately -- `:app` + `:db-worker-node`: start both workflows with `--no-repl`, then attach separately -- `:electron` + `:db-worker-node`: start Desktop once plus db-worker-node once, then attach separately -- all three: one shared Desktop/watch workflow plus one db-worker-node workflow - -Shared `yarn watch` is expected here. Do not rely on one interactive REPL session to cover multiple runtimes for you. - -### Editor attach - -If the start scripts already launched `yarn watch`, nREPL should be on `localhost:8701`. - -#### Desktop `:app` - -Direct shadow-cljs/editor attach: - -- Host: `localhost` -- Port: `8701` -- Build: `:app` - -CLJ-first attach: - -```clojure -(shadow.cljs.devtools.api/repl :app) -``` - -Helper: +## Editor Attach Helpers ```clojure (shadow.user/cljs-repl) -``` - -#### Electron `:electron` - -Direct shadow-cljs/editor attach: - -- Host: `localhost` -- Port: `8701` -- Build: `:electron` - -CLJ-first attach: - -```clojure -(shadow.cljs.devtools.api/repl :electron) -``` - -Helper: - -```clojure (shadow.user/electron-repl) -``` - -`shadow.user/electron-repl` ensures `:electron` is watched, then attaches to the Electron main-process runtime. - -#### `db-worker-node` - -Direct shadow-cljs/editor attach: - -- Host: `localhost` -- Port: `8701` -- Build: `:db-worker-node` - -CLJ-first attach: - -```clojure -(shadow.cljs.devtools.api/repl :db-worker-node) -``` - -Helper: - -```clojure (shadow.user/worker-node-repl) ``` -`shadow.user/worker-node-repl` picks the first available `:db-worker-node` runtime. +## Troubleshooting -### Troubleshooting +Failure triage order: -#### Failure triage order +1. inspect `tmp/logseq-repl/shared-shadow-watch.log` +2. inspect `tmp/logseq-repl/desktop-electron.log` +3. inspect `tmp/logseq-repl/db-worker-node.log` +4. inspect standard port listeners with `lsof` +5. inspect runtime counts with `shadow.cljs.devtools.api/repl-runtimes` -When startup or attach fails, inspect evidence in this order before retrying: +Common cases: -1. `tmp/logseq-repl/shared-shadow-watch.log` -2. `tmp/desktop-app-repl/desktop-electron.log` or `tmp/db-worker-node-repl/*` -3. port listeners via `lsof` -4. runtime counts via `shadow.cljs.devtools.api/repl-runtimes` +- `No available JS runtime`: the build may be ready, but the runtime has not connected. Check runtime counts before retrying attach. +- multiple `:app` runtimes: close browser dev app instances so only the Desktop renderer remains. +- ports already in use after cleanup: another Logseq/shadow-cljs dev session is still running. +- `db-worker-node` repo mismatch: rerun `start-repl.sh --repo `; it restarts the worker runtime for the requested repo. -Do not jump from a failed attach straight to another attach attempt. +## Recommended Response Pattern -#### `start-desktop-app-repl.sh` timed out or said watch is not ready - -Possible causes: - -- external port conflict after cleanup -- watch alive but expected ports or log patterns differ from the script assumptions -- builds completed but runtime count is still `0` - -Triage: - -1. read `tmp/logseq-repl/shared-shadow-watch.log` -2. rerun the port audit -3. check runtime counts -4. if runtime counts are all `0`, debug app startup next instead of attaching - -#### Desktop `:app`: `No available JS runtime` - -Cause: the Desktop renderer is not connected yet, or the Desktop window is not open. - -Fix: - -1. run `scripts/start-desktop-app-repl.sh --no-repl` -2. confirm the Desktop window is open -3. confirm `:app` runtime count is non-zero -4. only then attach from the script or editor - -#### Desktop startup: ports already in use (`8701`, `9630`, `3001`, `3002`) - -Cause: another Logseq/shadow-cljs dev session is already running, often one that was not started by these skill scripts. - -How to confirm: - -```bash -lsof -nP -iTCP:8701 -sTCP:LISTEN -lsof -nP -iTCP:9630 -sTCP:LISTEN -lsof -nP -iTCP:3001 -sTCP:LISTEN -lsof -nP -iTCP:3002 -sTCP:LISTEN -``` - -If those listeners are not the PIDs tracked under `tmp/logseq-repl/`, `tmp/desktop-app-repl/`, or `tmp/db-worker-node-repl/`, the cleanup scripts will not stop them. - -Fix: - -1. stop the conflicting manually started dev session -2. rerun both cleanup scripts -3. rerun the port audit -4. retry `start-desktop-app-repl.sh` only after ports are clean - -#### Desktop `:app`: wrong runtime attached - -Cause: browser and Desktop renderer runtimes are both alive. - -Fix: close the browser dev app and retry. - -#### Desktop `:app`: connected to `:electron` by mistake - -Symptom: DOM/browser globals are missing. - -Fix: reconnect to `:app`, not `:electron`. - -#### Electron `:electron`: `No available JS runtime` - -Cause: the Electron main process is not connected yet, or `yarn dev-electron-app` has not finished starting. - -Fix: - -1. run `scripts/start-desktop-app-repl.sh --no-repl` -2. confirm the Desktop dev app is still open -3. confirm `:electron` runtime count is non-zero -4. only then run `npx shadow-cljs cljs-repl electron` - -If build logs look healthy but `:electron` runtime count stays `0`, debug the Desktop app startup instead of retrying the attach command. - -#### Electron desktop app exits quickly - -Symptoms may include: - -- `yarn dev-electron-app` exits almost immediately -- `gulp electron` reports incomplete async completion -- no `:app` or `:electron` runtime ever appears - -Triage: - -1. inspect `tmp/desktop-app-repl/desktop-electron.log` -2. inspect `tmp/logseq-repl/shared-shadow-watch.log` -3. inspect port conflicts - -Do not attempt `cljs-repl electron` until runtime count is non-zero. - -#### Electron `:electron`: browser globals are missing - -Expected: `js/process` exists, but `js/document` usually does not. - -Fix: if you need DOM/browser APIs, reconnect to `:app` instead. - -#### `db-worker-node`: `shadow-cljs already running in project` - -Cause: stale or conflicting shadow/db-worker-node processes. - -Fix: - -```bash -.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh -``` - -Then confirm `:db-worker-node` runtime count is non-zero before attaching. - -#### `db-worker-node`: `repo is required` - -Fix: - -```bash -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo -``` - -#### `db-worker-node`: `No available JS runtime` - -Fix: - -```bash -.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh -.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo -``` - -Then confirm runtime count before attaching. - -#### `db-worker-node`: runtime/module startup errors - -Rebuild runtime artifacts first: - -```bash -yarn db-worker-node:release:bundle -``` - -## Recommended response pattern - -When helping a user connect to one or more REPLs: +When helping a user connect to a REPL: 1. identify whether they need `:app`, `:electron`, `:db-worker-node`, or a combination -2. run both cleanup scripts -3. run the post-cleanup port audit; if standard ports are still occupied, treat that as an external conflict first -4. if they need Desktop `:app`, close browser dev app instances first -5. start the needed workflow: `start-desktop-app-repl.sh` for `:app`/`:electron`, `start-db-worker-node-repl.sh` for `:db-worker-node` -6. if they need multiple runtimes, start each workflow with `--no-repl` -7. verify runtime counts before attaching; do not assume watch/build readiness implies runtime readiness -8. attach from the matching build or helper: `shadow.user/cljs-repl`, `shadow.user/electron-repl`, or `shadow.user/worker-node-repl` -9. if something fails, inspect `tmp/logseq-repl/shared-shadow-watch.log` first, then use port checks, runtime counts, and app logs to triage instead of repeatedly retrying attach commands -10. run the matching cleanup script when finished +2. run `cleanup-repl.sh` +3. if standard ports remain occupied, resolve that conflict first +4. run `start-repl.sh --repo ` +5. verify runtime counts if attach fails +6. attach to the matching build or helper +7. run `cleanup-repl.sh` when finished diff --git a/.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh b/.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh deleted file mode 100755 index d3e7cab063..0000000000 --- a/.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" -FORCE_KILL=0 - -usage() { - cat <<'EOF' -Stop processes started by start-db-worker-node-repl.sh. - -Usage: - cleanup-db-worker-node-repl.sh [options] - -Options: - --repo-root Logseq repository root (default: auto-detect from script location) - --force Use SIGKILL if process does not stop gracefully - -h, --help Show this help -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --repo-root) - shift - REPO_ROOT="${1:?missing value for --repo-root}" - ;; - --force) - FORCE_KILL=1 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage - exit 1 - ;; - esac - shift -done - -LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" -DESKTOP_LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" -SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" -SHADOW_PID_FILE="$LOG_DIR/shadow-db-worker-node.pid" -DB_PID_FILE="$LOG_DIR/db-worker-node.pid" -DB_REPO_FILE="$LOG_DIR/db-worker-node.repo" -DESKTOP_SHADOW_PID_FILE="$DESKTOP_LOG_DIR/shadow-watch.pid" -SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" - -is_running_pid() { - local pid="$1" - [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null -} - -read_pid() { - local file="$1" - if [[ -f "$file" ]]; then - tr -d '[:space:]' < "$file" - fi -} - -stop_by_pid_file() { - local pid_file="$1" - local label="$2" - - local pid - pid="$(read_pid "$pid_file" || true)" - - if [[ -z "${pid:-}" ]]; then - echo "$label: no pid file, nothing to stop" - return 0 - fi - - if ! is_running_pid "$pid"; then - echo "$label: process already stopped (pid=$pid)" - rm -f "$pid_file" - return 0 - fi - - echo "$label: stopping pid=$pid" - kill "$pid" 2>/dev/null || true - - local i - for ((i=0; i<10; i++)); do - if ! is_running_pid "$pid"; then - echo "$label: stopped" - rm -f "$pid_file" - return 0 - fi - sleep 1 - done - - if [[ "$FORCE_KILL" -eq 1 ]]; then - echo "$label: force killing pid=$pid" - kill -9 "$pid" 2>/dev/null || true - sleep 1 - fi - - if is_running_pid "$pid"; then - echo "$label: failed to stop pid=$pid" >&2 - return 1 - fi - - echo "$label: stopped" - rm -f "$pid_file" -} - -stop_shadow_watch() { - local own_pid shared_pid other_pid - own_pid="$(read_pid "$SHADOW_PID_FILE" || true)" - shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" - other_pid="$(read_pid "$DESKTOP_SHADOW_PID_FILE" || true)" - - if [[ -n "${own_pid:-}" && -n "${other_pid:-}" && "$own_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then - echo "shadow-cljs watch: shared with other workflows, leaving it running" - rm -f "$SHADOW_PID_FILE" - return 0 - fi - - if [[ -n "${shared_pid:-}" && -n "${other_pid:-}" && "$shared_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then - echo "shadow-cljs watch: shared with other workflows, leaving it running" - rm -f "$SHADOW_PID_FILE" - return 0 - fi - - if [[ -f "$SHARED_SHADOW_PID_FILE" ]]; then - stop_by_pid_file "$SHARED_SHADOW_PID_FILE" "shadow-cljs watch" - rm -f "$SHADOW_PID_FILE" - else - stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch" - fi -} - -if [[ ! -d "$LOG_DIR" && ! -d "$SHARED_LOG_DIR" ]]; then - echo "State directory not found: $LOG_DIR" - echo "Nothing to clean up." - exit 0 -fi - -stop_by_pid_file "$DB_PID_FILE" "db-worker-node" -rm -f "$DB_REPO_FILE" -stop_shadow_watch - -echo "Cleanup done." diff --git a/.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh b/.agents/skills/logseq-repl/scripts/cleanup-repl.sh similarity index 57% rename from .agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh rename to .agents/skills/logseq-repl/scripts/cleanup-repl.sh index 4f56e22029..2d3b05f7f9 100755 --- a/.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh +++ b/.agents/skills/logseq-repl/scripts/cleanup-repl.sh @@ -8,14 +8,14 @@ FORCE_KILL=0 usage() { cat <<'EOF' -Stop processes started by start-desktop-app-repl.sh. +Stop all processes started by start-repl.sh. Usage: - cleanup-desktop-app-repl.sh [options] + cleanup-repl.sh [options] Options: --repo-root Logseq repository root (default: auto-detect from script location) - --force Use SIGKILL if process does not stop gracefully + --force Use SIGKILL if a process does not stop gracefully -h, --help Show this help EOF } @@ -42,13 +42,9 @@ while [[ $# -gt 0 ]]; do shift done -LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" -DB_WORKER_LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" -SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" -SHADOW_PID_FILE="$LOG_DIR/shadow-watch.pid" -DESKTOP_PID_FILE="$LOG_DIR/desktop-electron.pid" -DB_WORKER_SHADOW_PID_FILE="$DB_WORKER_LOG_DIR/shadow-db-worker-node.pid" -SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" +LOG_DIR="$REPO_ROOT/tmp/logseq-repl" +LEGACY_DESKTOP_LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" +LEGACY_DB_LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" is_running_pid() { local pid="$1" @@ -94,7 +90,7 @@ stop_by_pid_file() { pid="$(read_pid "$pid_file" || true)" if [[ -z "${pid:-}" ]]; then - echo "$label: no pid file, nothing to stop" + rm -f "$pid_file" return 0 fi @@ -132,39 +128,47 @@ stop_by_pid_file() { rm -f "$pid_file" } -stop_shadow_watch() { - local own_pid shared_pid other_pid - own_pid="$(read_pid "$SHADOW_PID_FILE" || true)" - shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" - other_pid="$(read_pid "$DB_WORKER_SHADOW_PID_FILE" || true)" - - if [[ -n "${own_pid:-}" && -n "${other_pid:-}" && "$own_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then - echo "shadow-cljs watch: shared with other workflows, leaving it running" - rm -f "$SHADOW_PID_FILE" - return 0 - fi - - if [[ -n "${shared_pid:-}" && -n "${other_pid:-}" && "$shared_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then - echo "shadow-cljs watch: shared with other workflows, leaving it running" - rm -f "$SHADOW_PID_FILE" - return 0 - fi - - if [[ -f "$SHARED_SHADOW_PID_FILE" ]]; then - stop_by_pid_file "$SHARED_SHADOW_PID_FILE" "shadow-cljs watch" - rm -f "$SHADOW_PID_FILE" - else - stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch" - fi +repo_owns_pid() { + local pid="$1" + local cwd + cwd="$(lsof -nP -a -p "$pid" -d cwd 2>/dev/null | awk 'NR > 1 {print $NF; exit}' || true)" + [[ "$cwd" == "$REPO_ROOT" ]] } -if [[ ! -d "$LOG_DIR" && ! -d "$SHARED_LOG_DIR" ]]; then +stop_repo_port_listener() { + local port="$1" + local pid + + while read -r pid; do + [[ -n "${pid:-}" ]] || continue + if is_running_pid "$pid" && repo_owns_pid "$pid"; then + echo "port $port listener: stopping pid=$pid" + signal_process_group TERM "$pid" + fi + done < <(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true) +} + +if [[ ! -d "$LOG_DIR" && ! -d "$LEGACY_DESKTOP_LOG_DIR" && ! -d "$LEGACY_DB_LOG_DIR" ]]; then echo "State directory not found: $LOG_DIR" echo "Nothing to clean up." exit 0 fi -stop_by_pid_file "$DESKTOP_PID_FILE" "desktop-electron" -stop_shadow_watch +stop_by_pid_file "$LOG_DIR/db-worker-node.pid" "db-worker-node" +stop_by_pid_file "$LOG_DIR/desktop-electron.pid" "desktop-electron" +stop_by_pid_file "$LOG_DIR/shared-shadow-watch.pid" "shadow-cljs watch" + +stop_by_pid_file "$LEGACY_DB_LOG_DIR/db-worker-node.pid" "legacy db-worker-node" +stop_by_pid_file "$LEGACY_DB_LOG_DIR/shadow-db-worker-node.pid" "legacy shadow-cljs watch" +stop_by_pid_file "$LEGACY_DESKTOP_LOG_DIR/desktop-electron.pid" "legacy desktop-electron" +stop_by_pid_file "$LEGACY_DESKTOP_LOG_DIR/shadow-watch.pid" "legacy shadow-cljs watch" + +rm -f "$LOG_DIR/db-worker-node.repo" "$LEGACY_DB_LOG_DIR/db-worker-node.repo" + +for port in 8701 3001 3002 9630 9631; do + stop_repo_port_listener "$port" +done + +rm -f "$LOG_DIR"/*.pid "$LEGACY_DB_LOG_DIR"/*.pid "$LEGACY_DESKTOP_LOG_DIR"/*.pid 2>/dev/null || true echo "Cleanup done." diff --git a/.agents/skills/logseq-repl/scripts/common.sh b/.agents/skills/logseq-repl/scripts/common.sh index 27a4ec40b8..c8598c57b3 100644 --- a/.agents/skills/logseq-repl/scripts/common.sh +++ b/.agents/skills/logseq-repl/scripts/common.sh @@ -51,7 +51,7 @@ logseq_repl_runtime_count() { local output pushd "$repo_root" >/dev/null - if ! output="$(npx shadow-cljs clj-eval "(do (require '[shadow.cljs.devtools.api :as api]) (println (count (api/repl-runtimes :$build_name))))" 2>&1)"; then + if ! output="$(pnpm exec shadow-cljs clj-eval "(do (require '[shadow.cljs.devtools.api :as api]) (println (count (api/repl-runtimes :$build_name))))" 2>&1)"; then popd >/dev/null echo "Error: failed to inspect :$build_name runtimes." >&2 echo "--- clj-eval output ---" >&2 diff --git a/.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh b/.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh deleted file mode 100755 index cc5ac0a5ef..0000000000 --- a/.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" -DB_REPO="${DB_REPO:-demo}" -ATTACH_REPL=1 -EXTRA_NODE_ARGS=() - -# shellcheck source=common.sh -source "$SCRIPT_DIR/common.sh" - -usage() { - cat <<'EOF' -Start or reuse db-worker-node REPL workflow. - -Usage: - start-db-worker-node-repl.sh [options] [-- ] - -Options: - --repo Graph repo name passed to db-worker-node (default: demo) - --repo-root Logseq repository root (default: auto-detect from script location) - --no-repl Do not attach `shadow-cljs cljs-repl` after startup - --repl Force attach REPL (default behavior) - -h, --help Show this help - -Examples: - ./start-db-worker-node-repl.sh - ./start-db-worker-node-repl.sh --repo demo --no-repl - ./start-db-worker-node-repl.sh --repo demo -- --create-empty-db -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --repo) - shift - DB_REPO="${1:?missing value for --repo}" - ;; - --repo-root) - shift - REPO_ROOT="${1:?missing value for --repo-root}" - ;; - --no-repl) - ATTACH_REPL=0 - ;; - --repl) - ATTACH_REPL=1 - ;; - -h|--help) - usage - exit 0 - ;; - --) - shift - EXTRA_NODE_ARGS+=("$@") - break - ;; - *) - EXTRA_NODE_ARGS+=("$1") - ;; - esac - shift -done - -if [[ ! -d "$REPO_ROOT" ]]; then - echo "Error: repo root not found: $REPO_ROOT" >&2 - exit 1 -fi - -if ! command -v npx >/dev/null 2>&1; then - echo "Error: npx not found in PATH" >&2 - exit 1 -fi - -if ! command -v yarn >/dev/null 2>&1; then - echo "Error: yarn not found in PATH" >&2 - exit 1 -fi - -if ! command -v node >/dev/null 2>&1; then - echo "Error: node not found in PATH" >&2 - exit 1 -fi - -LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" -SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" -mkdir -p "$LOG_DIR" -mkdir -p "$SHARED_LOG_DIR" - -if ! logseq_repl_require_clean_standard_ports; then - exit 1 -fi - -SHADOW_PID_FILE="$LOG_DIR/shadow-db-worker-node.pid" -DB_PID_FILE="$LOG_DIR/db-worker-node.pid" -DB_REPO_FILE="$LOG_DIR/db-worker-node.repo" -SHADOW_LOG="$LOG_DIR/shadow-db-worker-node.log" -DB_LOG="$LOG_DIR/db-worker-node.log" -SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" -SHARED_SHADOW_LOG="$SHARED_LOG_DIR/shared-shadow-watch.log" - -wait_for_log_pattern() { - local file="$1" - local pattern="$2" - local timeout_seconds="$3" - - local i - for ((i=0; i&2 - return 1 - fi - - sleep 1 - done - - echo "Warning: did not observe db-worker-node build completion within timeout." >&2 - return 1 -} - -start_background_command() { - local log_file="$1" - shift - - pushd "$REPO_ROOT" >/dev/null - if command -v setsid >/dev/null 2>&1; then - nohup setsid "$@" > "$log_file" 2>&1 & - else - nohup "$@" > "$log_file" 2>&1 & - fi - local pid=$! - popd >/dev/null - - echo "$pid" -} - -ensure_shadow_watch() { - local existing_pid shared_pid - existing_pid="$(logseq_repl_read_pid "$SHADOW_PID_FILE" || true)" - - if [[ -n "${existing_pid:-}" ]] && logseq_repl_is_running_pid "$existing_pid"; then - echo "Reusing shared shadow-cljs watch (pid=$existing_pid)" - return 0 - fi - - shared_pid="$(logseq_repl_read_pid "$SHARED_SHADOW_PID_FILE" || true)" - if [[ -n "${shared_pid:-}" ]] && logseq_repl_is_running_pid "$shared_pid"; then - echo "$shared_pid" > "$SHADOW_PID_FILE" - : > "$SHADOW_LOG" - echo "Reusing shared shadow-cljs watch (pid=$shared_pid)" - if wait_for_shadow_build_ready "$SHARED_SHADOW_LOG" 120; then - echo "shadow-cljs db-worker-node build is ready" - else - echo "Error: shadow-cljs build is not ready. Check $SHARED_SHADOW_LOG" >&2 - exit 1 - fi - return 0 - fi - - echo "Starting shared shadow-cljs watch via yarn watch ..." - local shadow_pid - shadow_pid="$(start_background_command "$SHARED_SHADOW_LOG" yarn watch)" - - echo "$shadow_pid" > "$SHARED_SHADOW_PID_FILE" - echo "$shadow_pid" > "$SHADOW_PID_FILE" - : > "$SHADOW_LOG" - sleep 1 - - if ! logseq_repl_is_running_pid "$shadow_pid"; then - echo "Error: yarn watch exited early. Check $SHARED_SHADOW_LOG" >&2 - exit 1 - fi - - if wait_for_log_pattern "$SHARED_SHADOW_LOG" "watching build :db-worker-node\\|\\[:db-worker-node\\] Build completed\\." 45; then - echo "shadow-cljs watch is ready (pid=$shadow_pid)" - else - echo "Warning: did not observe watch-ready log within timeout. Continuing anyway." >&2 - fi - - if wait_for_shadow_build_ready "$SHARED_SHADOW_LOG" 120; then - echo "shadow-cljs db-worker-node build is ready" - else - echo "Error: shadow-cljs build is not ready. Check $SHARED_SHADOW_LOG" >&2 - exit 1 - fi -} - -ensure_db_worker_node() { - local existing_pid existing_repo - existing_pid="$(logseq_repl_read_pid "$DB_PID_FILE" || true)" - existing_repo="$(logseq_repl_read_pid "$DB_REPO_FILE" || true)" - - if [[ -n "${existing_pid:-}" ]] && logseq_repl_is_running_pid "$existing_pid"; then - if [[ "$existing_repo" == "$DB_REPO" ]]; then - echo "Reusing db-worker-node runtime (pid=$existing_pid, repo=$DB_REPO)" - return 0 - fi - - echo "Stopping existing db-worker-node (pid=$existing_pid) due to repo mismatch" - kill "$existing_pid" 2>/dev/null || true - sleep 1 - fi - - echo "Starting db-worker-node (repo=$DB_REPO) ..." - pushd "$REPO_ROOT" >/dev/null - local node_cmd=(node ./static/db-worker-node.js --repo "$DB_REPO") - if (( ${#EXTRA_NODE_ARGS[@]} > 0 )); then - node_cmd+=("${EXTRA_NODE_ARGS[@]}") - fi - nohup "${node_cmd[@]}" > "$DB_LOG" 2>&1 & - local db_pid=$! - popd >/dev/null - - echo "$db_pid" > "$DB_PID_FILE" - echo "$DB_REPO" > "$DB_REPO_FILE" - sleep 1 - - if ! logseq_repl_is_running_pid "$db_pid"; then - echo "Error: db-worker-node exited early. Check $DB_LOG" >&2 - exit 1 - fi - - echo "db-worker-node is running (pid=$db_pid, repo=$DB_REPO)" -} - -ensure_worker_node_runtime() { - local runtime_count second - - for ((second=0; second<60; second++)); do - runtime_count="$(logseq_repl_runtime_count "$REPO_ROOT" db-worker-node)" - - if [[ "$runtime_count" != "0" ]]; then - echo "Detected live :db-worker-node runtime count: $runtime_count" - return 0 - fi - - sleep 1 - done - - echo "Error: expected a live :db-worker-node runtime, but runtime count stayed 0." >&2 - echo "Check $DB_LOG and $SHARED_SHADOW_LOG before retrying." >&2 - exit 1 -} - -verify_repl_connectivity() { - echo "Verifying CLJS REPL connectivity ..." - - local repl_output - pushd "$REPO_ROOT" >/dev/null - if repl_output="$(printf '(+ 1 2)\n:cljs/quit\n' | npx shadow-cljs cljs-repl db-worker-node 2>&1)"; then - popd >/dev/null - else - local repl_status=$? - popd >/dev/null - echo "Error: failed to run REPL connectivity check (exit=$repl_status)." >&2 - echo "--- REPL output ---" >&2 - echo "$repl_output" >&2 - echo "-------------------" >&2 - exit 1 - fi - - if [[ "$repl_output" != *"shadow-cljs - connected to server"* ]]; then - echo "Error: REPL check did not report successful server connection." >&2 - echo "--- REPL output ---" >&2 - echo "$repl_output" >&2 - echo "-------------------" >&2 - exit 1 - fi - - if [[ "$repl_output" != *$'cljs.user=> 3'* ]]; then - echo "Error: REPL check did not produce expected evaluation result." >&2 - echo "--- REPL output ---" >&2 - echo "$repl_output" >&2 - echo "-------------------" >&2 - exit 1 - fi - - echo "REPL connectivity check passed" -} - -ensure_shadow_watch -ensure_db_worker_node -ensure_worker_node_runtime -verify_repl_connectivity - -echo - -echo "Logs:" -echo " shared shadow-cljs: $SHARED_SHADOW_LOG" -echo " db-worker-node: $DB_LOG" -echo " workflow state file: $SHADOW_LOG" -echo "PID files:" -echo " $SHADOW_PID_FILE" -echo " $DB_PID_FILE" -echo - -if [[ "$ATTACH_REPL" -eq 1 ]]; then - echo "Attaching CLJS REPL: npx shadow-cljs cljs-repl db-worker-node" - pushd "$REPO_ROOT" >/dev/null - npx shadow-cljs cljs-repl db-worker-node - popd >/dev/null -else - echo "Startup complete. REPL attach skipped (--no-repl)." -fi diff --git a/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh b/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh deleted file mode 100755 index e2d266b0af..0000000000 --- a/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" -ATTACH_REPL=1 - -# shellcheck source=common.sh -source "$SCRIPT_DIR/common.sh" - -usage() { - cat <<'EOF' -Start or reuse the Desktop app `:app` REPL workflow. - -Usage: - start-desktop-app-repl.sh [options] - -Options: - --repo-root Logseq repository root (default: auto-detect from script location) - --no-repl Do not attach `shadow-cljs cljs-repl app` after startup - --repl Force attach REPL (default behavior) - -h, --help Show this help -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --repo-root) - shift - REPO_ROOT="${1:?missing value for --repo-root}" - ;; - --no-repl) - ATTACH_REPL=0 - ;; - --repl) - ATTACH_REPL=1 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage - exit 1 - ;; - esac - shift -done - -if [[ ! -d "$REPO_ROOT" ]]; then - echo "Error: repo root not found: $REPO_ROOT" >&2 - exit 1 -fi - -if ! command -v yarn >/dev/null 2>&1; then - echo "Error: yarn not found in PATH" >&2 - exit 1 -fi - -if ! command -v npx >/dev/null 2>&1; then - echo "Error: npx not found in PATH" >&2 - exit 1 -fi - -LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" -SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" -mkdir -p "$LOG_DIR" -mkdir -p "$SHARED_LOG_DIR" - -if ! logseq_repl_require_clean_standard_ports; then - exit 1 -fi - -SHADOW_PID_FILE="$LOG_DIR/shadow-watch.pid" -DESKTOP_PID_FILE="$LOG_DIR/desktop-electron.pid" -SHADOW_LOG="$LOG_DIR/shadow-watch.log" -DESKTOP_LOG="$LOG_DIR/desktop-electron.log" -SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" -SHARED_SHADOW_LOG="$SHARED_LOG_DIR/shared-shadow-watch.log" - -wait_for_all_patterns() { - local file="$1" - local timeout_seconds="$2" - shift 2 - - local second pattern all_found - for ((second=0; second/dev/null - if command -v setsid >/dev/null 2>&1; then - nohup setsid "$@" > "$log_file" 2>&1 & - else - nohup "$@" > "$log_file" 2>&1 & - fi - local pid=$! - popd >/dev/null - - echo "$pid" -} - -ensure_shadow_watch() { - local existing_pid shared_pid - existing_pid="$(logseq_repl_read_pid "$SHADOW_PID_FILE" || true)" - - if [[ -n "${existing_pid:-}" ]] && logseq_repl_is_running_pid "$existing_pid"; then - echo "Reusing shared shadow-cljs watch (pid=$existing_pid)" - return 0 - fi - - shared_pid="$(logseq_repl_read_pid "$SHARED_SHADOW_PID_FILE" || true)" - if [[ -n "${shared_pid:-}" ]] && logseq_repl_is_running_pid "$shared_pid"; then - echo "$shared_pid" > "$SHADOW_PID_FILE" - : > "$SHADOW_LOG" - echo "Reusing shared shadow-cljs watch (pid=$shared_pid)" - return 0 - fi - - echo "Starting shadow-cljs watch via yarn watch ..." - local shadow_pid - shadow_pid="$(start_background_command "$SHARED_SHADOW_LOG" yarn watch)" - echo "$shadow_pid" > "$SHARED_SHADOW_PID_FILE" - echo "$shadow_pid" > "$SHADOW_PID_FILE" - : > "$SHADOW_LOG" - sleep 1 - - if ! logseq_repl_is_running_pid "$shadow_pid"; then - echo "Error: yarn watch exited early. Check $SHARED_SHADOW_LOG" >&2 - exit 1 - fi - - if wait_for_all_patterns "$SHARED_SHADOW_LOG" 180 \ - "[:electron] Build completed." \ - "[:app] Build completed."; then - echo "shadow-cljs watch builds are ready (pid=$shadow_pid)" - else - echo "Error: yarn watch did not finish the :app/:electron builds in time. Check $SHARED_SHADOW_LOG" >&2 - exit 1 - fi -} - -ensure_desktop_app() { - local existing_pid - existing_pid="$(read_pid "$DESKTOP_PID_FILE" || true)" - - if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then - echo "Reusing Desktop dev app (pid=$existing_pid)" - return 0 - fi - - echo "Starting Desktop dev app via yarn dev-electron-app ..." - local desktop_pid - desktop_pid="$(start_background_command "$DESKTOP_LOG" yarn dev-electron-app)" - echo "$desktop_pid" > "$DESKTOP_PID_FILE" - sleep 1 - - if ! is_running_pid "$desktop_pid"; then - echo "Error: yarn dev-electron-app exited early. Check $DESKTOP_LOG" >&2 - exit 1 - fi - - if wait_for_any_pattern "$DESKTOP_LOG" 120 "shadow-cljs - #" "Logseq App("; then - echo "Desktop dev app is running (pid=$desktop_pid)" - else - echo "Error: Desktop dev app did not report startup in time. Check $DESKTOP_LOG" >&2 - exit 1 - fi -} - -ensure_single_app_runtime() { - local runtime_count second - - for ((second=0; second<60; second++)); do - runtime_count="$(logseq_repl_runtime_count "$REPO_ROOT" app)" - - if [[ "$runtime_count" == "1" ]]; then - echo "Detected exactly one live :app runtime" - return 0 - fi - - if [[ "$runtime_count" != "0" ]]; then - break - fi - - sleep 1 - done - - if [[ "$runtime_count" == "0" ]]; then - echo "Error: Expected exactly one live :app runtime, found 0 after waiting for the Desktop renderer to connect." >&2 - echo "Check $DESKTOP_LOG and $SHARED_SHADOW_LOG, make sure the Desktop window is fully open, then retry." >&2 - exit 1 - fi - - echo "Error: Expected exactly one live :app runtime, found $runtime_count." >&2 - echo "Close the browser dev app so only the Desktop renderer remains, then retry." >&2 - exit 1 -} - -verify_renderer_smoke_test() { - echo "Verifying Desktop renderer smoke test ..." - - local smoke_output - pushd "$REPO_ROOT" >/dev/null - if ! smoke_output="$(printf '(some? js/document)\n(.-title js/document)\n:cljs/quit\n' | npx shadow-cljs cljs-repl app 2>&1)"; then - popd >/dev/null - echo "Error: smoke test REPL command failed." >&2 - echo "--- smoke output ---" >&2 - echo "$smoke_output" >&2 - echo "--------------------" >&2 - exit 1 - fi - popd >/dev/null - - if [[ "$smoke_output" != *"shadow-cljs - connected to server"* ]]; then - echo "Error: smoke test did not connect to shadow-cljs." >&2 - echo "--- smoke output ---" >&2 - echo "$smoke_output" >&2 - echo "--------------------" >&2 - exit 1 - fi - - if [[ "$smoke_output" != *$'cljs.user=> true'* ]]; then - echo "Error: smoke test did not confirm js/document." >&2 - echo "--- smoke output ---" >&2 - echo "$smoke_output" >&2 - echo "--------------------" >&2 - exit 1 - fi - - echo "Desktop renderer smoke test passed" -} - -ensure_shadow_watch -ensure_desktop_app -ensure_single_app_runtime -verify_renderer_smoke_test - -echo - -echo "Logs:" -echo " shared shadow-cljs: $SHARED_SHADOW_LOG" -echo " desktop-app: $DESKTOP_LOG" -echo " workflow state file: $SHADOW_LOG" -echo "PID files:" -echo " $SHADOW_PID_FILE" -echo " $DESKTOP_PID_FILE" -echo - -if [[ "$ATTACH_REPL" -eq 1 ]]; then - echo "Attaching CLJS REPL: npx shadow-cljs cljs-repl app" - pushd "$REPO_ROOT" >/dev/null - npx shadow-cljs cljs-repl app - popd >/dev/null -else - echo "Startup complete. REPL attach skipped (--no-repl)." -fi diff --git a/.agents/skills/logseq-repl/scripts/start-repl.py b/.agents/skills/logseq-repl/scripts/start-repl.py new file mode 100755 index 0000000000..347c037b0a --- /dev/null +++ b/.agents/skills/logseq-repl/scripts/start-repl.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import signal +import subprocess +import sys +import time +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_REPO_ROOT = SCRIPT_DIR.parents[3] +STANDARD_PORTS = (8701, 3001, 3002, 9630, 9631) +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(line_buffering=True) + sys.stderr.reconfigure(line_buffering=True) + + +def parse_args(): + parser = argparse.ArgumentParser( + prog="start-repl.sh", + description="Start the unified Logseq REPL workflow.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "This starts shared pnpm watch, the Desktop dev app, and the\n" + "db-worker-node runtime, verifies the REPL targets, and exits." + ), + ) + parser.add_argument("--repo", default=os.environ.get("DB_REPO", "demo"), + help="Graph repo name passed to db-worker-node (default: demo)") + parser.add_argument("--repo-root", default=os.environ.get("REPO_ROOT", str(DEFAULT_REPO_ROOT)), + help="Logseq repository root (default: auto-detect from script location)") + parser.add_argument("extra_node_args", nargs=argparse.REMAINDER, + help="Arguments after -- are passed to db-worker-node") + args = parser.parse_args() + if args.extra_node_args and args.extra_node_args[0] == "--": + args.extra_node_args = args.extra_node_args[1:] + return args + + +def require_command(name): + if subprocess.run(["/usr/bin/env", "sh", "-c", f"command -v {name} >/dev/null 2>&1"]).returncode != 0: + raise SystemExit(f"Error: {name} not found in PATH") + + +def read_pid(path): + try: + text = path.read_text().strip() + except FileNotFoundError: + return None + return int(text) if re.fullmatch(r"\d+", text) else None + + +def is_running(pid): + if not pid: + return False + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +def write_pid(path, pid): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(f"{pid}\n") + + +def wait_for_patterns(path, timeout, patterns, all_required=True): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if path.exists(): + text = path.read_text(errors="replace") + if all_required and all(pattern in text for pattern in patterns): + return True + if not all_required and any(pattern in text for pattern in patterns): + return True + time.sleep(1) + return False + + +def process_group_kwargs(): + if os.name == "nt": + return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} + return {"start_new_session": True} + + +def start_process(repo_root, log_path, command): + log_path.parent.mkdir(parents=True, exist_ok=True) + log_file = log_path.open("wb") + return subprocess.Popen( + command, + cwd=repo_root, + stdin=subprocess.DEVNULL, + stdout=log_file, + stderr=subprocess.STDOUT, + **process_group_kwargs(), + ) + + +def port_listener_pid(port): + try: + output = subprocess.check_output( + ["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"], + text=True, + stderr=subprocess.DEVNULL, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return None + for line in output.splitlines(): + if line.strip().isdigit(): + return int(line.strip()) + return None + + +def has_managed_processes(paths): + return any(is_running(read_pid(path)) for path in paths) + + +def require_clean_ports(): + had_conflict = False + for port in STANDARD_PORTS: + pid = port_listener_pid(port) + if pid: + had_conflict = True + print(f"Port {port} is already listening (pid={pid})", file=sys.stderr) + if had_conflict: + print("Error: standard Logseq REPL ports are still occupied after cleanup.", file=sys.stderr) + print("Resolve the external conflict first, then retry.", file=sys.stderr) + return False + return True + + +def ensure_shadow_watch(repo_root, shadow_pid_file, shadow_log): + pid = read_pid(shadow_pid_file) + if is_running(pid): + print(f"Reusing shared shadow-cljs watch (pid={pid})") + return pid + + print("Starting shared shadow-cljs watch via pnpm watch ...") + process = start_process(repo_root, shadow_log, ["pnpm", "watch"]) + write_pid(shadow_pid_file, process.pid) + time.sleep(1) + + if process.poll() is not None: + raise SystemExit(f"Error: pnpm watch exited early. Check {shadow_log}") + + if not wait_for_patterns(shadow_log, 180, [ + "[:electron] Build completed.", + "[:app] Build completed.", + "[:db-worker-node] Build completed.", + ]): + raise SystemExit(f"Error: pnpm watch did not finish the expected builds in time. Check {shadow_log}") + + listener_pid = port_listener_pid(8701) + if listener_pid and is_running(listener_pid): + write_pid(shadow_pid_file, listener_pid) + pid = listener_pid + else: + pid = process.pid + + print("shadow-cljs watch builds are ready") + return pid + + +def ensure_desktop_app(repo_root, desktop_pid_file, desktop_log): + pid = read_pid(desktop_pid_file) + if is_running(pid): + print(f"Reusing Desktop dev app (pid={pid})") + return pid + + print("Starting Desktop dev app via pnpm dev-electron-app ...") + process = start_process(repo_root, desktop_log, ["pnpm", "dev-electron-app"]) + write_pid(desktop_pid_file, process.pid) + time.sleep(1) + + if process.poll() is not None: + raise SystemExit(f"Error: pnpm dev-electron-app exited early. Check {desktop_log}") + + if not wait_for_patterns(desktop_log, 120, ["shadow-cljs - #", "Logseq App("], all_required=False): + raise SystemExit(f"Error: Desktop dev app did not report startup in time. Check {desktop_log}") + + print(f"Desktop dev app is running (pid={process.pid})") + return process.pid + + +def ensure_db_worker_node(repo_root, db_pid_file, db_repo_file, db_log, repo, extra_args): + pid = read_pid(db_pid_file) + existing_repo = db_repo_file.read_text().strip() if db_repo_file.exists() else None + + if is_running(pid): + if existing_repo == repo: + print(f"Reusing db-worker-node runtime (pid={pid}, repo={repo})") + return pid + print(f"Stopping existing db-worker-node (pid={pid}) due to repo mismatch") + terminate_pid(pid) + time.sleep(1) + + print(f"Starting db-worker-node (repo={repo}) ...") + process = start_process( + repo_root, + db_log, + ["node", "./static/db-worker-node.js", "--repo", repo, *extra_args], + ) + write_pid(db_pid_file, process.pid) + db_repo_file.write_text(f"{repo}\n") + time.sleep(1) + + if process.poll() is not None: + raise SystemExit(f"Error: db-worker-node exited early. Check {db_log}") + + print(f"db-worker-node is running (pid={process.pid}, repo={repo})") + return process.pid + + +def runtime_count(repo_root, build_name): + form = f"(do (require '[shadow.cljs.devtools.api :as api]) (println (count (api/repl-runtimes :{build_name}))))" + proc = subprocess.run( + ["pnpm", "exec", "shadow-cljs", "clj-eval", form], + cwd=repo_root, + text=True, + capture_output=True, + ) + if proc.returncode != 0: + raise SystemExit( + f"Error: failed to inspect :{build_name} runtimes.\n" + f"--- clj-eval output ---\n{proc.stdout}{proc.stderr}\n-----------------------" + ) + matches = re.findall(r"^(\d+)$", proc.stdout + proc.stderr, flags=re.MULTILINE) + if not matches: + raise SystemExit( + f"Error: could not parse :{build_name} runtime count.\n" + f"--- clj-eval output ---\n{proc.stdout}{proc.stderr}\n-----------------------" + ) + return int(matches[-1]) + + +def wait_for_runtime_count(repo_root, build_name, expected, timeout): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + count = runtime_count(repo_root, build_name) + if expected == "exactly-one": + if count == 1: + print(f"Detected exactly one live :{build_name} runtime") + return + if count != 0: + raise SystemExit(f"Error: Expected exactly one live :{build_name} runtime, found {count}.") + elif expected == "nonzero" and count != 0: + print(f"Detected live :{build_name} runtime count: {count}") + return + time.sleep(1) + + if expected == "exactly-one": + raise SystemExit(f"Error: Expected exactly one live :{build_name} runtime, found 0 after waiting.") + raise SystemExit(f"Error: expected a live :{build_name} runtime, but runtime count stayed 0.") + + +def verify_repls(repo_root): + subprocess.run([str(SCRIPT_DIR / "verify-repls.sh"), "--repo-root", str(repo_root)], check=True) + + +def print_summary(shadow_log, desktop_log, db_log, shadow_pid_file, desktop_pid_file, db_pid_file): + print() + print("Logs:") + print(f" shared shadow-cljs: {shadow_log}") + print(f" desktop-app: {desktop_log}") + print(f" db-worker-node: {db_log}") + print("PID files:") + print(f" {shadow_pid_file}") + print(f" {desktop_pid_file}") + print(f" {db_pid_file}") + print() + print("Attach commands:") + print(" pnpm exec shadow-cljs cljs-repl app") + print(" pnpm exec shadow-cljs cljs-repl electron") + print(" pnpm exec shadow-cljs cljs-repl db-worker-node") + print() + print("Startup complete. Attach to the needed REPL manually.") + + +def terminate_pid(pid): + if not pid: + return + try: + if os.name == "nt": + os.kill(pid, signal.CTRL_BREAK_EVENT) + else: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + + +def main(): + args = parse_args() + repo_root = Path(args.repo_root).resolve() + if not repo_root.is_dir(): + raise SystemExit(f"Error: repo root not found: {repo_root}") + + require_command("pnpm") + require_command("node") + + log_dir = repo_root / "tmp" / "logseq-repl" + legacy_desktop_dir = repo_root / "tmp" / "desktop-app-repl" + legacy_db_dir = repo_root / "tmp" / "db-worker-node-repl" + log_dir.mkdir(parents=True, exist_ok=True) + + shadow_pid_file = log_dir / "shared-shadow-watch.pid" + desktop_pid_file = log_dir / "desktop-electron.pid" + db_pid_file = log_dir / "db-worker-node.pid" + db_repo_file = log_dir / "db-worker-node.repo" + shadow_log = log_dir / "shared-shadow-watch.log" + desktop_log = log_dir / "desktop-electron.log" + db_log = log_dir / "db-worker-node.log" + + managed_pid_files = [ + shadow_pid_file, + desktop_pid_file, + db_pid_file, + legacy_desktop_dir / "shadow-watch.pid", + legacy_desktop_dir / "desktop-electron.pid", + legacy_db_dir / "shadow-db-worker-node.pid", + legacy_db_dir / "db-worker-node.pid", + ] + + if not has_managed_processes(managed_pid_files) and not require_clean_ports(): + return 1 + + ensure_shadow_watch(repo_root, shadow_pid_file, shadow_log) + ensure_desktop_app(repo_root, desktop_pid_file, desktop_log) + ensure_db_worker_node(repo_root, db_pid_file, db_repo_file, db_log, args.repo, args.extra_node_args) + wait_for_runtime_count(repo_root, "app", "exactly-one", 60) + wait_for_runtime_count(repo_root, "electron", "nonzero", 60) + wait_for_runtime_count(repo_root, "db-worker-node", "nonzero", 60) + verify_repls(repo_root) + print_summary(shadow_log, desktop_log, db_log, shadow_pid_file, desktop_pid_file, db_pid_file) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/logseq-repl/scripts/start-repl.sh b/.agents/skills/logseq-repl/scripts/start-repl.sh new file mode 100755 index 0000000000..6047d810a0 --- /dev/null +++ b/.agents/skills/logseq-repl/scripts/start-repl.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec python3 "$SCRIPT_DIR/start-repl.py" "$@" diff --git a/.agents/skills/logseq-repl/scripts/verify-repls.sh b/.agents/skills/logseq-repl/scripts/verify-repls.sh new file mode 100755 index 0000000000..4f84abf51a --- /dev/null +++ b/.agents/skills/logseq-repl/scripts/verify-repls.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" + +usage() { + cat <<'EOF' +Verify that all Logseq CLJS REPL targets are usable. + +Usage: + verify-repls.sh [options] + +Options: + --repo-root Logseq repository root (default: auto-detect from script location) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-root) + shift + REPO_ROOT="${1:?missing value for --repo-root}" + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +if [[ ! -d "$REPO_ROOT" ]]; then + echo "Error: repo root not found: $REPO_ROOT" >&2 + exit 1 +fi + +if ! command -v pnpm >/dev/null 2>&1; then + echo "Error: pnpm not found in PATH" >&2 + exit 1 +fi + +verify_target() { + local target="$1" + local form="$2" + + echo "Checking :$target ..." + + local repl_output + pushd "$REPO_ROOT" >/dev/null + if ! repl_output="$(printf '%s\n' "$form" | pnpm exec shadow-cljs cljs-repl "$target" 2>&1)"; then + popd >/dev/null + echo "Error: REPL verification failed for :$target." >&2 + echo "--- :$target output ---" >&2 + echo "$repl_output" >&2 + echo "-----------------------" >&2 + return 1 + fi + popd >/dev/null + + if [[ "$repl_output" != *"shadow-cljs - connected to server"* ]]; then + echo "Error: REPL verification did not connect for :$target." >&2 + echo "--- :$target output ---" >&2 + echo "$repl_output" >&2 + echo "-----------------------" >&2 + return 1 + fi + + echo "--- :$target result ---" + echo "$repl_output" + echo "-----------------------" + echo "REPL verification passed for :$target" +} + +echo "Verifying CLJS REPL targets ..." +verify_target app "(prn {:runtime :app :document? (some? js/document)})" +verify_target electron "(prn {:runtime :electron :process? (some? js/process) :type (some-> js/process .-type)})" +verify_target db-worker-node "(prn {:runtime :db-worker-node :process? (some? js/process) :platform (some-> js/process .-platform)})" +echo "All CLJS REPL targets verified." diff --git a/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh b/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh deleted file mode 100644 index 845d48bb69..0000000000 --- a/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" -SKILLS_ROOT="$(cd "$SKILL_DIR/.." && pwd)" -START_SCRIPT="$SKILL_DIR/scripts/start-db-worker-node-repl.sh" -CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-db-worker-node-repl.sh" -DESKTOP_START_SCRIPT="$SKILL_DIR/scripts/start-desktop-app-repl.sh" -DESKTOP_CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-desktop-app-repl.sh" -SKILL_FILE="$SKILL_DIR/SKILL.md" -CURRENT_SKILL_NAME="$(basename "$SKILL_DIR")" -ORIGINAL_PATH="$PATH" - -# shellcheck source=./test-lib.sh -source "$TEST_DIR/test-lib.sh" - -PASS_COUNT=0 -FAIL_COUNT=0 - -create_fake_env() { - TEST_ROOT="$(mktemp -d)" - REPO_ROOT="$TEST_ROOT/repo" - BIN_DIR="$TEST_ROOT/bin" - CMD_LOG="$TEST_ROOT/commands.log" - - mkdir -p "$REPO_ROOT/static" "$BIN_DIR" - : > "$CMD_LOG" - - cat > "$BIN_DIR/yarn" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -echo "yarn $*" >> "$FAKE_CMD_LOG" - -case "${1:-}" in - watch) - echo "shadow-cljs - nREPL server started on port 8701" - echo "[:electron] Build completed." - echo "[:app] Build completed." - echo "[:db-worker-node] Build completed." - while true; do sleep 1; done - ;; - dev-electron-app) - echo "17:12:00.841 › Logseq App(2.0.1) Starting..." - echo "shadow-cljs - #6 ready!" - while true; do sleep 1; done - ;; - *) - echo "Unexpected yarn command: $*" >&2 - exit 1 - ;; - esac -EOF - - cat > "$BIN_DIR/npx" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -input="" -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" ]] && [[ -p /dev/stdin ]]; then - input="$(cat)" -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "clj-eval" ]]; then - echo "npx-clj-eval $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "${FAKE_APP_RUNTIME_COUNT:-1}" - exit 0 -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "app" ]]; then - if [[ "$input" == *"(.-title js/document)"* ]]; then - echo "npx-app-smoke $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=> true" - echo 'cljs.user=> "Logseq"' - echo "cljs.user=>" - exit 0 - fi - - echo "npx-app-attach $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=>" - exit 0 -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "db-worker-node" ]]; then - if [[ "$input" == *"(+ 1 2)"* ]]; then - echo "npx-db-smoke $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=> 3" - echo "cljs.user=>" - exit 0 - fi - - echo "npx-db-attach $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=>" - exit 0 -fi - -echo "Unexpected npx command: $*" >&2 -exit 1 -EOF - - cat > "$BIN_DIR/node" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -echo "node $*" >> "$FAKE_CMD_LOG" -while true; do sleep 1; done -EOF - - cat > "$BIN_DIR/setsid" <<'EOF' -#!/usr/bin/env bash -exec "$@" -EOF - - chmod +x "$BIN_DIR/yarn" "$BIN_DIR/npx" "$BIN_DIR/node" "$BIN_DIR/setsid" - - export PATH="$BIN_DIR:$ORIGINAL_PATH" - export FAKE_CMD_LOG="$CMD_LOG" - export FAKE_APP_RUNTIME_COUNT="${FAKE_APP_RUNTIME_COUNT:-1}" -} - -cleanup_fake_env() { - if [[ -n "${REPO_ROOT:-}" ]]; then - local pid_file - for pid_file in \ - "$REPO_ROOT"/tmp/db-worker-node-repl/*.pid \ - "$REPO_ROOT"/tmp/desktop-app-repl/*.pid \ - "$REPO_ROOT"/tmp/logseq-repl/*.pid; do - [[ -e "$pid_file" ]] || continue - local pid - pid="$(tr -d '[:space:]' < "$pid_file")" - if [[ "$pid" =~ ^[0-9]+$ ]]; then - kill -9 "$pid" 2>/dev/null || true - fi - done - fi - - PATH="$ORIGINAL_PATH" - unset FAKE_CMD_LOG FAKE_APP_RUNTIME_COUNT || true - - if [[ -n "${TEST_ROOT:-}" && -d "$TEST_ROOT" ]]; then - rm -rf "$TEST_ROOT" - fi -} - -scripts_exist_and_skill_is_renamed_test() { - assert_equals "logseq-repl" "$CURRENT_SKILL_NAME" - assert_file_exists "$START_SCRIPT" - assert_file_exists "$CLEANUP_SCRIPT" - assert_file_exists "$DESKTOP_START_SCRIPT" - assert_file_exists "$DESKTOP_CLEANUP_SCRIPT" -} - -start_no_repl_launches_shared_shadow_and_runtime_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" - assert_contains "shared shadow-cljs: $REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" "$TEST_ROOT/output.log" - assert_contains "db-worker-node: $REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.log" "$TEST_ROOT/output.log" - assert_contains "yarn watch" "$CMD_LOG" - assert_contains "node ./static/db-worker-node.js --repo demo" "$CMD_LOG" - assert_contains "npx-db-smoke shadow-cljs cljs-repl db-worker-node" "$CMD_LOG" - - assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" - assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid" - assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" - assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" -} - -start_reuses_existing_db_worker_runtime_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/first.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/second.log" 2>&1 - - local watch_count node_count - watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" - node_count="$(grep -c '^node ./static/db-worker-node.js --repo demo$' "$CMD_LOG")" - - assert_equals "1" "$watch_count" - assert_equals "1" "$node_count" - assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/second.log" - assert_contains "Reusing db-worker-node runtime" "$TEST_ROOT/second.log" -} - -desktop_and_db_worker_share_single_shadow_watch_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 - - local watch_count desktop_shadow_pid db_shadow_pid - watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" - desktop_shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid")" - db_shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid")" - - assert_equals "1" "$watch_count" - assert_equals "$desktop_shadow_pid" "$db_shadow_pid" - assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/db.log" -} - -cleanup_db_worker_keeps_shared_watch_for_desktop_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 - - local watch_pid runtime_pid - watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" - runtime_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid")" - - bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-db.log" 2>&1 - - if ! kill -0 "$watch_pid" 2>/dev/null; then - fail "expected shared watch to keep running while desktop workflow is active" - fi - - if kill -0 "$runtime_pid" 2>/dev/null; then - fail "expected db-worker-node runtime to stop during cleanup" - fi - - assert_contains "shadow-cljs watch: shared with other workflows, leaving it running" "$TEST_ROOT/cleanup-db.log" - assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" - assert_not_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" -} - -cleanup_desktop_keeps_shared_watch_for_db_worker_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 - - local watch_pid electron_pid - watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" - electron_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid")" - - bash "$DESKTOP_CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-desktop.log" 2>&1 - - if ! kill -0 "$watch_pid" 2>/dev/null; then - fail "expected shared watch to keep running while db-worker workflow is active" - fi - - if kill -0 "$electron_pid" 2>/dev/null; then - fail "expected desktop electron process to stop during cleanup" - fi - - assert_contains "shadow-cljs watch: shared with other workflows, leaving it running" "$TEST_ROOT/cleanup-desktop.log" - assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" - assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" -} - -shared_watch_stops_after_last_cleanup_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 - - local watch_pid - watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" - - bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-db.log" 2>&1 - bash "$DESKTOP_CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-desktop.log" 2>&1 - - if kill -0 "$watch_pid" 2>/dev/null; then - fail "expected shared watch to stop after the last workflow cleaned up" - fi - - assert_not_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" -} - -start_attaches_repl_by_default_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo < /dev/null > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Attaching CLJS REPL: npx shadow-cljs cljs-repl db-worker-node" "$TEST_ROOT/output.log" - assert_contains "npx-db-attach shadow-cljs cljs-repl db-worker-node" "$CMD_LOG" -} - -help_and_docs_are_portable_test() { - local temp_dir start_help_file cleanup_help_file path_pattern - temp_dir="$(mktemp -d)" - start_help_file="$temp_dir/start-help.txt" - cleanup_help_file="$temp_dir/cleanup-help.txt" - path_pattern="$(portable_path_pattern)" - - bash "$START_SCRIPT" --help > "$start_help_file" - bash "$CLEANUP_SCRIPT" --help > "$cleanup_help_file" - - assert_not_matches "$path_pattern" "$start_help_file" - assert_not_matches "$path_pattern" "$cleanup_help_file" - assert_not_matches "$path_pattern" "$SKILL_FILE" - assert_contains "name: logseq-repl" "$SKILL_FILE" - assert_contains 'Desktop app `:app` REPL' "$SKILL_FILE" - assert_contains 'Electron main-process `:electron` REPL' "$SKILL_FILE" - assert_contains "db-worker-node REPL" "$SKILL_FILE" - assert_contains "multiple different REPL types at the same time" "$SKILL_FILE" - assert_contains "tmp/logseq-repl" "$SKILL_FILE" - assert_contains 'Run multiple runtimes together' "$SKILL_FILE" - assert_contains "shared-shadow-watch.log" "$SKILL_FILE" - assert_contains "shadow.user/electron-repl" "$SKILL_FILE" - assert_contains "shadow.user/worker-node-repl" "$SKILL_FILE" - assert_contains "Non-interactive verification examples" "$SKILL_FILE" - assert_contains "Cleanup both workflows" "$SKILL_FILE" - - rm -rf "$temp_dir" -} - -obsolete_skill_directories_removed_test() { - assert_not_exists "$SKILLS_ROOT/desktop-app-repl" - assert_not_exists "$SKILLS_ROOT/db-worker-node-repl" -} - -run_test "scripts exist and skill is renamed" scripts_exist_and_skill_is_renamed_test -run_test "start --no-repl launches shared shadow and runtime" start_no_repl_launches_shared_shadow_and_runtime_test -run_test "start reuses existing db-worker runtime" start_reuses_existing_db_worker_runtime_test -run_test "desktop and db-worker share a single shadow watch" desktop_and_db_worker_share_single_shadow_watch_test -run_test "cleanup db-worker keeps shared watch for desktop" cleanup_db_worker_keeps_shared_watch_for_desktop_test -run_test "cleanup desktop keeps shared watch for db-worker" cleanup_desktop_keeps_shared_watch_for_db_worker_test -run_test "shared watch stops after last cleanup" shared_watch_stops_after_last_cleanup_test -run_test "start attaches repl by default" start_attaches_repl_by_default_test -run_test "help and docs are portable" help_and_docs_are_portable_test -run_test "obsolete skill directories removed" obsolete_skill_directories_removed_test - -echo -if [[ "$FAIL_COUNT" -gt 0 ]]; then - echo "$FAIL_COUNT test(s) failed; $PASS_COUNT passed." >&2 - exit 1 -fi - -echo "All $PASS_COUNT test(s) passed." diff --git a/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh b/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh deleted file mode 100644 index 86bbc277ef..0000000000 --- a/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh +++ /dev/null @@ -1,395 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" -SKILLS_ROOT="$(cd "$SKILL_DIR/.." && pwd)" -START_SCRIPT="$SKILL_DIR/scripts/start-desktop-app-repl.sh" -CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-desktop-app-repl.sh" -SKILL_FILE="$SKILL_DIR/SKILL.md" -CURRENT_SKILL_NAME="$(basename "$SKILL_DIR")" -ORIGINAL_PATH="$PATH" - -# shellcheck source=./test-lib.sh -source "$TEST_DIR/test-lib.sh" - -PASS_COUNT=0 -FAIL_COUNT=0 - -create_fake_env() { - TEST_ROOT="$(mktemp -d)" - REPO_ROOT="$TEST_ROOT/repo" - BIN_DIR="$TEST_ROOT/bin" - CMD_LOG="$TEST_ROOT/commands.log" - - mkdir -p "$REPO_ROOT/static" "$BIN_DIR" - : > "$CMD_LOG" - - cat > "$BIN_DIR/yarn" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -echo "yarn $*" >> "$FAKE_CMD_LOG" - -case "${1:-}" in - watch) - echo "shadow-cljs - nREPL server started on port 8701" - echo "[:electron] Build completed." - echo "[:app] Build completed." - while true; do sleep 1; done - ;; - dev-electron-app) - echo "17:12:00.841 › Logseq App(2.0.1) Starting..." - echo "shadow-cljs - #6 ready!" - while true; do sleep 1; done - ;; - *) - echo "Unexpected yarn command: $*" >&2 - exit 1 - ;; - esac -EOF - - cat > "$BIN_DIR/npx" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -input="" -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && ( "${3:-}" == "app" || "${3:-}" == "electron" ) ]] && [[ -p /dev/stdin ]]; then - input="$(cat)" -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "clj-eval" ]]; then - echo "npx-clj-eval $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "${FAKE_APP_RUNTIME_COUNT:-1}" - exit 0 -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "app" ]]; then - if [[ "$input" == *"(.-title js/document)"* ]]; then - echo "npx-smoke $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - if [[ "${FAKE_SMOKE_TEST_FAIL:-0}" == "1" ]]; then - echo "cljs.user=> false" - echo "cljs.user=> \"Wrong\"" - echo "cljs.user=>" - exit 0 - fi - echo "cljs.user=> true" - echo "cljs.user=> \"${FAKE_SMOKE_TEST_TITLE:-Logseq}\"" - echo "cljs.user=>" - exit 0 - fi - - echo "npx-attach $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=>" - exit 0 -fi - -if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "electron" ]]; then - if [[ "$input" == *":runtime :electron"* ]]; then - echo "npx-electron-smoke $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=> {:runtime :electron, :process? true, :type \"browser\"}" - echo "cljs.user=>" - exit 0 - fi - - echo "npx-electron-attach $*" >> "$FAKE_CMD_LOG" - echo "shadow-cljs - connected to server" - echo "cljs.user=>" - exit 0 -fi - -echo "Unexpected npx command: $*" >&2 -exit 1 -EOF - - cat > "$BIN_DIR/setsid" <<'EOF' -#!/usr/bin/env bash -exec "$@" -EOF - - chmod +x "$BIN_DIR/yarn" "$BIN_DIR/npx" "$BIN_DIR/setsid" - - export PATH="$BIN_DIR:$ORIGINAL_PATH" - export FAKE_CMD_LOG="$CMD_LOG" - export FAKE_APP_RUNTIME_COUNT="${FAKE_APP_RUNTIME_COUNT:-1}" - export FAKE_SMOKE_TEST_FAIL="${FAKE_SMOKE_TEST_FAIL:-0}" - export FAKE_SMOKE_TEST_TITLE="${FAKE_SMOKE_TEST_TITLE:-Logseq}" -} - -link_system_tool() { - local name="$1" - local target - target="$(command -v "$name")" - ln -s "$target" "$SYSTEM_BIN_DIR/$name" -} - -create_fake_env_without_setsid() { - create_fake_env - - SYSTEM_BIN_DIR="$TEST_ROOT/system-bin" - mkdir -p "$SYSTEM_BIN_DIR" - rm -f "$BIN_DIR/setsid" - - local tool - for tool in bash awk cat dirname grep mkdir nohup ps rm sleep tr; do - link_system_tool "$tool" - done - - export PATH="$BIN_DIR:$SYSTEM_BIN_DIR" -} - -cleanup_fake_env() { - if [[ -n "${REPO_ROOT:-}" && -d "$REPO_ROOT/tmp/desktop-app-repl" ]]; then - local pid_file - for pid_file in "$REPO_ROOT"/tmp/desktop-app-repl/*.pid; do - [[ -e "$pid_file" ]] || continue - local pid - pid="$(tr -d '[:space:]' < "$pid_file")" - if [[ "$pid" =~ ^[0-9]+$ ]]; then - kill -9 "$pid" 2>/dev/null || true - fi - done - fi - - PATH="$ORIGINAL_PATH" - unset FAKE_CMD_LOG FAKE_APP_RUNTIME_COUNT FAKE_SMOKE_TEST_FAIL FAKE_SMOKE_TEST_TITLE || true - - if [[ -n "${TEST_ROOT:-}" && -d "$TEST_ROOT" ]]; then - rm -rf "$TEST_ROOT" - fi -} - -scripts_exist_and_skill_is_renamed_test() { - assert_equals "logseq-repl" "$CURRENT_SKILL_NAME" - assert_file_exists "$START_SCRIPT" - assert_file_exists "$CLEANUP_SCRIPT" -} - -start_no_repl_launches_watch_and_electron_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" - assert_contains "shared shadow-cljs: $REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" "$TEST_ROOT/output.log" - assert_contains "desktop-app: $REPO_ROOT/tmp/desktop-app-repl/desktop-electron.log" "$TEST_ROOT/output.log" - assert_contains "yarn watch" "$CMD_LOG" - assert_contains "yarn dev-electron-app" "$CMD_LOG" - assert_contains "npx-clj-eval shadow-cljs clj-eval" "$CMD_LOG" - assert_contains "npx-smoke shadow-cljs cljs-repl app" "$CMD_LOG" - - assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" - assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" - assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.log" - assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.log" - assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" - assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" -} - -start_reuses_running_processes_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/first.log" 2>&1 - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/second.log" 2>&1 - - local watch_count - watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" - local electron_count - electron_count="$(grep -c '^yarn dev-electron-app$' "$CMD_LOG")" - - assert_equals "1" "$watch_count" - assert_equals "1" "$electron_count" - assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/second.log" - assert_contains "Reusing Desktop dev app" "$TEST_ROOT/second.log" -} - -start_fails_when_multiple_app_runtimes_exist_test() { - create_fake_env - trap cleanup_fake_env RETURN - export FAKE_APP_RUNTIME_COUNT=2 - - if bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1; then - fail "expected start script to fail when multiple :app runtimes exist" - fi - - assert_contains "Expected exactly one live :app runtime" "$TEST_ROOT/output.log" - assert_contains "Close the browser dev app" "$TEST_ROOT/output.log" -} - -start_attaches_repl_by_default_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" < /dev/null > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Attaching CLJS REPL: npx shadow-cljs cljs-repl app" "$TEST_ROOT/output.log" - assert_contains "npx-attach shadow-cljs cljs-repl app" "$CMD_LOG" -} - -electron_attach_works_after_desktop_start_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/start.log" 2>&1 - printf '(prn {:runtime :electron :process? (some? js/process) :type (.-type js/process)})\n:cljs/quit\n' | npx shadow-cljs cljs-repl electron > "$TEST_ROOT/electron.log" 2>&1 - - assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/start.log" - assert_contains "shadow-cljs - connected to server" "$TEST_ROOT/electron.log" - assert_contains ":runtime :electron" "$TEST_ROOT/electron.log" - assert_contains "npx-electron-smoke shadow-cljs cljs-repl electron" "$CMD_LOG" -} - -start_works_without_setsid_test() { - create_fake_env_without_setsid - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" - assert_contains "yarn watch" "$CMD_LOG" - assert_contains "yarn dev-electron-app" "$CMD_LOG" -} - -start_accepts_non_logseq_window_title_test() { - create_fake_env - trap cleanup_fake_env RETURN - export FAKE_SMOKE_TEST_TITLE="My Graph" - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 - - assert_contains "Desktop renderer smoke test passed" "$TEST_ROOT/output.log" - assert_contains "npx-smoke shadow-cljs cljs-repl app" "$CMD_LOG" -} - -cleanup_stops_tracked_processes_test() { - create_fake_env - trap cleanup_fake_env RETURN - - bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/start.log" 2>&1 - - local shadow_pid - shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid")" - local electron_pid - electron_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid")" - - bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup.log" 2>&1 - - if kill -0 "$shadow_pid" 2>/dev/null; then - fail "expected shadow watch pid to be stopped" - fi - - if kill -0 "$electron_pid" 2>/dev/null; then - fail "expected desktop electron pid to be stopped" - fi - - assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" - assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" - assert_not_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" - assert_contains "Cleanup done." "$TEST_ROOT/cleanup.log" -} - -cleanup_does_not_signal_own_process_group_test() { - create_fake_env - trap cleanup_fake_env RETURN - - local bash_env_file fake_pid fake_pgid - fake_pid=424242 - fake_pgid=777777 - mkdir -p "$REPO_ROOT/tmp/desktop-app-repl" - echo "$fake_pid" > "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" - - cat > "$BIN_DIR/ps" < "$bash_env_file" <> "$CMD_LOG" -return 0 -} -EOF - - chmod +x "$BIN_DIR/ps" - - BASH_ENV="$bash_env_file" bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-own-pgid.log" 2>&1 || true - - if grep -Eq '^kill -TERM -[0-9]+' "$CMD_LOG"; then - fail "expected cleanup not to signal its own process group" - fi -} - -help_and_docs_are_portable_test() { - local temp_dir start_help_file cleanup_help_file path_pattern - temp_dir="$(mktemp -d)" - start_help_file="$temp_dir/start-help.txt" - cleanup_help_file="$temp_dir/cleanup-help.txt" - path_pattern="$(portable_path_pattern)" - - bash "$START_SCRIPT" --help > "$start_help_file" - bash "$CLEANUP_SCRIPT" --help > "$cleanup_help_file" - - assert_not_matches "$path_pattern" "$start_help_file" - assert_not_matches "$path_pattern" "$cleanup_help_file" - assert_not_matches "$path_pattern" "$SKILL_FILE" - assert_contains "name: logseq-repl" "$SKILL_FILE" - assert_contains 'Desktop app `:app` REPL' "$SKILL_FILE" - assert_contains "start-desktop-app-repl.sh" "$SKILL_FILE" - assert_contains "start-db-worker-node-repl.sh" "$SKILL_FILE" - assert_contains "tmp/logseq-repl" "$SKILL_FILE" - assert_contains 'Run multiple runtimes together' "$SKILL_FILE" - assert_contains "close browser dev app instances" "$SKILL_FILE" - assert_contains "shared-shadow-watch.log" "$SKILL_FILE" - assert_contains "shadow.user/electron-repl" "$SKILL_FILE" - assert_contains "shadow.user/worker-node-repl" "$SKILL_FILE" - assert_contains "Non-interactive verification examples" "$SKILL_FILE" - assert_contains "printf '(prn {:runtime :app" "$SKILL_FILE" - assert_contains "printf '(prn {:runtime :electron" "$SKILL_FILE" - assert_contains "printf '(prn {:runtime :db-worker-node" "$SKILL_FILE" - assert_contains 'Electron main-process `:electron` REPL' "$SKILL_FILE" - assert_contains "Cleanup both workflows" "$SKILL_FILE" - assert_not_contains_text "name: repl-workflows" "$SKILL_FILE" - - rm -rf "$temp_dir" -} - -obsolete_skill_directories_removed_test() { - assert_not_exists "$SKILLS_ROOT/desktop-app-repl" - assert_not_exists "$SKILLS_ROOT/db-worker-node-repl" -} - -run_test "scripts exist and skill is renamed" scripts_exist_and_skill_is_renamed_test -run_test "start --no-repl launches watch and electron" start_no_repl_launches_watch_and_electron_test -run_test "start reuses running processes" start_reuses_running_processes_test -run_test "start fails when multiple app runtimes exist" start_fails_when_multiple_app_runtimes_exist_test -run_test "start attaches repl by default" start_attaches_repl_by_default_test -run_test "electron attach works after desktop start" electron_attach_works_after_desktop_start_test -run_test "start works without setsid" start_works_without_setsid_test -run_test "start accepts non-Logseq window title" start_accepts_non_logseq_window_title_test -run_test "cleanup stops tracked processes" cleanup_stops_tracked_processes_test -run_test "cleanup does not signal its own process group" cleanup_does_not_signal_own_process_group_test -run_test "help and docs are portable" help_and_docs_are_portable_test -run_test "obsolete skill directories removed" obsolete_skill_directories_removed_test - -echo -if [[ "$FAIL_COUNT" -gt 0 ]]; then - echo "$FAIL_COUNT test(s) failed; $PASS_COUNT passed." >&2 - exit 1 -fi - -echo "All $PASS_COUNT test(s) passed." diff --git a/.agents/skills/logseq-repl/tests/test-lib.sh b/.agents/skills/logseq-repl/tests/test-lib.sh index 0334f54688..27be12e615 100644 --- a/.agents/skills/logseq-repl/tests/test-lib.sh +++ b/.agents/skills/logseq-repl/tests/test-lib.sh @@ -67,7 +67,13 @@ run_test() { local name="$1" local fn="$2" - if (set -e; "$fn"); then + local status + set +e + (set -e; "$fn") + status=$? + set -e + + if [[ "$status" -eq 0 ]]; then echo "PASS: $name" PASS_COUNT=$((PASS_COUNT + 1)) else diff --git a/.agents/skills/logseq-repl/tests/test-logseq-repl.sh b/.agents/skills/logseq-repl/tests/test-logseq-repl.sh new file mode 100755 index 0000000000..eff46e8a89 --- /dev/null +++ b/.agents/skills/logseq-repl/tests/test-logseq-repl.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" +START_SCRIPT="$SKILL_DIR/scripts/start-repl.sh" +START_PY_SCRIPT="$SKILL_DIR/scripts/start-repl.py" +CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-repl.sh" +VERIFY_SCRIPT="$SKILL_DIR/scripts/verify-repls.sh" +SKILL_FILE="$SKILL_DIR/SKILL.md" +ORIGINAL_PATH="$PATH" + +# shellcheck source=./test-lib.sh +source "$TEST_DIR/test-lib.sh" + +PASS_COUNT=0 +FAIL_COUNT=0 + +create_fake_env() { + TEST_ROOT="$(mktemp -d)" + REPO_ROOT="$TEST_ROOT/repo" + BIN_DIR="$TEST_ROOT/bin" + CMD_LOG="$TEST_ROOT/commands.log" + + mkdir -p "$REPO_ROOT/static" "$BIN_DIR" + : > "$CMD_LOG" + + cat > "$BIN_DIR/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "pnpm $*" >> "$FAKE_CMD_LOG" + +case "${1:-}" in + watch) + echo "shadow-cljs - nREPL server started on port 8701" + echo "[:electron] Build completed." + echo "[:app] Build completed." + echo "[:db-worker-node] Build completed." + while true; do sleep 1; done + ;; + dev-electron-app) + echo "17:12:00.841 Logseq App(2.0.1) Starting..." + echo "shadow-cljs - #6 ready!" + while true; do sleep 1; done + ;; + exec) + shift + if [[ "${1:-}" != "shadow-cljs" ]]; then + echo "Unexpected pnpm exec command: $*" >&2 + exit 1 + fi + shift + + input="" + if [[ "${1:-}" == "cljs-repl" ]] && [[ -p /dev/stdin ]]; then + input="$(cat)" + fi + + if [[ "${1:-}" == "clj-eval" ]]; then + echo "pnpm-exec-clj-eval shadow-cljs $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + case "$*" in + *"repl-runtimes :app"*) echo "${FAKE_APP_RUNTIME_COUNT:-1}" ;; + *"repl-runtimes :electron"*) echo "${FAKE_ELECTRON_RUNTIME_COUNT:-1}" ;; + *"repl-runtimes :db-worker-node"*) echo "${FAKE_DB_RUNTIME_COUNT:-1}" ;; + *) echo "0" ;; + esac + exit 0 + fi + + if [[ "${1:-}" == "cljs-repl" ]]; then + echo "pnpm-exec-repl ${2:-missing}" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + + if [[ "$input" == *":cljs/quit"* ]]; then + echo "verification must not send :cljs/quit" >&2 + exit 1 + fi + + case "${2:-}" in + app) + if [[ "$input" != *":runtime :app"* ]]; then + echo "unexpected app verification form" >&2 + exit 1 + fi + echo 'cljs.user=> {:runtime :app, :document? true}' + ;; + electron) + if [[ "$input" != *":runtime :electron"* ]]; then + echo "unexpected electron verification form" >&2 + exit 1 + fi + echo 'cljs.user=> {:runtime :electron, :process? true}' + ;; + db-worker-node) + if [[ "$input" != *":runtime :db-worker-node"* ]]; then + echo "unexpected db-worker-node verification form" >&2 + exit 1 + fi + echo 'cljs.user=> {:runtime :db-worker-node, :process? true}' + ;; + *) + echo "Unexpected cljs-repl target: ${2:-}" >&2 + exit 1 + ;; + esac + + echo "cljs.user=>" + exit 0 + fi + + echo "Unexpected shadow-cljs command: $*" >&2 + exit 1 + ;; + *) + echo "Unexpected pnpm command: $*" >&2 + exit 1 + ;; +esac +EOF + + cat > "$BIN_DIR/node" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "node $*" >> "$FAKE_CMD_LOG" +echo "shadow-cljs - #6 ready!" +while true; do sleep 1; done +EOF + + cat > "$BIN_DIR/lsof" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +exit 1 +EOF + + cat > "$BIN_DIR/python3" </dev/null || true + fi + done + fi + + PATH="$ORIGINAL_PATH" + unset FAKE_CMD_LOG FAKE_APP_RUNTIME_COUNT FAKE_ELECTRON_RUNTIME_COUNT FAKE_DB_RUNTIME_COUNT || true + + if [[ -n "${TEST_ROOT:-}" && -d "$TEST_ROOT" ]]; then + rm -rf "$TEST_ROOT" + fi +} + +scripts_exist_test() { + assert_file_exists "$START_SCRIPT" + assert_file_exists "$START_PY_SCRIPT" + assert_file_exists "$CLEANUP_SCRIPT" + assert_file_exists "$VERIFY_SCRIPT" +} + +start_launches_all_repl_processes_without_attaching_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo > "$TEST_ROOT/start.log" 2>&1 + + assert_contains "Verifying CLJS REPL targets ..." "$TEST_ROOT/start.log" + assert_contains "REPL verification passed for :app" "$TEST_ROOT/start.log" + assert_contains "REPL verification passed for :electron" "$TEST_ROOT/start.log" + assert_contains "REPL verification passed for :db-worker-node" "$TEST_ROOT/start.log" + assert_contains "Startup complete. Attach to the needed REPL manually." "$TEST_ROOT/start.log" + assert_contains "pnpm exec shadow-cljs cljs-repl app" "$TEST_ROOT/start.log" + assert_contains "pnpm exec shadow-cljs cljs-repl electron" "$TEST_ROOT/start.log" + assert_contains "pnpm exec shadow-cljs cljs-repl db-worker-node" "$TEST_ROOT/start.log" + assert_contains "pnpm watch" "$CMD_LOG" + assert_contains "pnpm dev-electron-app" "$CMD_LOG" + assert_contains "node ./static/db-worker-node.js --repo demo" "$CMD_LOG" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/desktop-electron.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/db-worker-node.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/desktop-electron.log" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/db-worker-node.log" +} + +verify_script_checks_all_targets_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$VERIFY_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/verify.log" 2>&1 + + assert_contains "Verifying CLJS REPL targets ..." "$TEST_ROOT/verify.log" + assert_contains "REPL verification passed for :app" "$TEST_ROOT/verify.log" + assert_contains "REPL verification passed for :electron" "$TEST_ROOT/verify.log" + assert_contains "REPL verification passed for :db-worker-node" "$TEST_ROOT/verify.log" + assert_contains "All CLJS REPL targets verified." "$TEST_ROOT/verify.log" + assert_contains "pnpm-exec-repl app" "$CMD_LOG" + assert_contains "pnpm-exec-repl electron" "$CMD_LOG" + assert_contains "pnpm-exec-repl db-worker-node" "$CMD_LOG" +} + +verify_script_fails_when_target_repl_fails_test() { + create_fake_env + trap cleanup_fake_env RETURN + + cat > "$BIN_DIR/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "pnpm $*" >> "$FAKE_CMD_LOG" + +if [[ "${1:-}" == "exec" && "${2:-}" == "shadow-cljs" && "${3:-}" == "cljs-repl" && "${4:-}" == "electron" ]]; then + echo "No available JS runtime" >&2 + exit 1 +fi + +echo "shadow-cljs - connected to server" +echo "cljs.user=> {:ok true}" +EOF + chmod +x "$BIN_DIR/pnpm" + + if bash "$VERIFY_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/verify.log" 2>&1; then + fail "expected verify script to fail when one target REPL fails" + fi + + assert_contains "Error: REPL verification failed for :electron." "$TEST_ROOT/verify.log" + assert_contains "No available JS runtime" "$TEST_ROOT/verify.log" +} + +start_reuses_all_running_processes_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo > "$TEST_ROOT/first.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo > "$TEST_ROOT/second.log" 2>&1 + + assert_equals "1" "$(grep -c '^pnpm watch$' "$CMD_LOG")" + assert_equals "1" "$(grep -c '^pnpm dev-electron-app$' "$CMD_LOG")" + assert_equals "1" "$(grep -c '^node ./static/db-worker-node.js --repo demo$' "$CMD_LOG")" + assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/second.log" + assert_contains "Reusing Desktop dev app" "$TEST_ROOT/second.log" + assert_contains "Reusing db-worker-node runtime" "$TEST_ROOT/second.log" +} + +start_fails_when_app_runtime_is_ambiguous_test() { + create_fake_env + trap cleanup_fake_env RETURN + export FAKE_APP_RUNTIME_COUNT=2 + + if bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo > "$TEST_ROOT/start.log" 2>&1; then + fail "expected start script to fail when more than one :app runtime exists" + fi + + assert_contains "Expected exactly one live :app runtime" "$TEST_ROOT/start.log" +} + +cleanup_stops_all_repl_processes_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo > "$TEST_ROOT/start.log" 2>&1 + + local watch_pid desktop_pid db_pid + watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" + desktop_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/desktop-electron.pid")" + db_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/db-worker-node.pid")" + + bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup.log" 2>&1 + + if kill -0 "$watch_pid" 2>/dev/null; then + fail "expected shared watch to stop" + fi + + if kill -0 "$desktop_pid" 2>/dev/null; then + fail "expected Desktop dev app to stop" + fi + + if kill -0 "$db_pid" 2>/dev/null; then + fail "expected db-worker-node to stop" + fi + + assert_not_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" + assert_not_exists "$REPO_ROOT/tmp/logseq-repl/desktop-electron.pid" + assert_not_exists "$REPO_ROOT/tmp/logseq-repl/db-worker-node.pid" + assert_contains "Cleanup done." "$TEST_ROOT/cleanup.log" +} + +cleanup_removes_legacy_state_files_test() { + create_fake_env + trap cleanup_fake_env RETURN + + mkdir -p "$REPO_ROOT/tmp/desktop-app-repl" "$REPO_ROOT/tmp/db-worker-node-repl" "$REPO_ROOT/tmp/logseq-repl" + sleep 30 & + local legacy_desktop_pid=$! + sleep 30 & + local legacy_db_pid=$! + echo "$legacy_desktop_pid" > "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" + echo "$legacy_db_pid" > "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid" + echo "$legacy_db_pid" > "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" + + bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup.log" 2>&1 + + if kill -0 "$legacy_desktop_pid" 2>/dev/null; then + fail "expected legacy desktop pid to stop" + fi + + if kill -0 "$legacy_db_pid" 2>/dev/null; then + fail "expected legacy db-worker pid to stop" + fi + + assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" + assert_not_exists "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid" + assert_not_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" +} + +help_and_docs_describe_unified_scripts_test() { + local temp_dir start_help cleanup_help + temp_dir="$(mktemp -d)" + start_help="$temp_dir/start-help.txt" + cleanup_help="$temp_dir/cleanup-help.txt" + + bash "$START_SCRIPT" --help > "$start_help" + bash "$CLEANUP_SCRIPT" --help > "$cleanup_help" + + assert_contains "start-repl.sh" "$start_help" + assert_contains "start-repl.py" "$SKILL_FILE" + assert_contains "cleanup-repl.sh" "$cleanup_help" + assert_contains "start-repl.sh" "$SKILL_FILE" + assert_contains "cleanup-repl.sh" "$SKILL_FILE" + assert_contains "verify-repls.sh" "$SKILL_FILE" + assert_not_contains_text "start-desktop-app-repl.sh" "$SKILL_FILE" + assert_not_contains_text "start-db-worker-node-repl.sh" "$SKILL_FILE" + assert_not_contains_text "cleanup-desktop-app-repl.sh" "$SKILL_FILE" + assert_not_contains_text "cleanup-db-worker-node-repl.sh" "$SKILL_FILE" + + rm -rf "$temp_dir" +} + +run_test "scripts exist" scripts_exist_test +run_test "start launches all REPL processes without attaching" start_launches_all_repl_processes_without_attaching_test +run_test "verify script checks all targets" verify_script_checks_all_targets_test +run_test "verify script fails when target REPL fails" verify_script_fails_when_target_repl_fails_test +run_test "start reuses all running processes" start_reuses_all_running_processes_test +run_test "start fails when app runtime is ambiguous" start_fails_when_app_runtime_is_ambiguous_test +run_test "cleanup stops all REPL processes" cleanup_stops_all_repl_processes_test +run_test "cleanup removes legacy state files" cleanup_removes_legacy_state_files_test +run_test "help and docs describe unified scripts" help_and_docs_describe_unified_scripts_test + +echo +if [[ "$FAIL_COUNT" -gt 0 ]]; then + echo "$FAIL_COUNT test(s) failed; $PASS_COUNT passed." >&2 + exit 1 +fi + +echo "All $PASS_COUNT test(s) passed."