enhance(skill): update logseq-repl

This commit is contained in:
rcmerci
2026-04-28 21:40:32 +08:00
parent eca0ab1a36
commit d140cdbf1e
13 changed files with 982 additions and 2015 deletions

View File

@@ -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 `<repo>/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: `<repo>/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 <name>`
`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:
- `<repo>/tmp/logseq-repl/shared-shadow-watch.log`
- `<repo>/tmp/logseq-repl/desktop-electron.log`
- `<repo>/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: `<repo>/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 <build>` 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 <name>`
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 <name>`; 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 <name>
```
#### `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 <name>
```
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 <name>`
5. verify runtime counts if attach fails
6. attach to the matching build or helper
7. run `cleanup-repl.sh` when finished

View File

@@ -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 <path> 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."

View File

@@ -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 <path> 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."

View File

@@ -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

View File

@@ -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] [-- <extra db-worker-node args>]
Options:
--repo <name> Graph repo name passed to db-worker-node (default: demo)
--repo-root <path> 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<timeout_seconds; i++)); do
if [[ -f "$file" ]] && grep -q "$pattern" "$file"; then
return 0
fi
sleep 1
done
return 1
}
wait_for_shadow_build_ready() {
local file="$1"
local timeout_seconds="$2"
local i
for ((i=0; i<timeout_seconds; i++)); do
if [[ -f "$file" ]] && grep -q "\[:db-worker-node\] Build completed\." "$file"; then
return 0
fi
if [[ -f "$file" ]] && grep -q "\[:db-worker-node\] Build failure\." "$file"; then
echo "Error: shadow-cljs reported build failure. Check $file" >&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

View File

@@ -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 <path> 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<timeout_seconds; second++)); do
all_found=1
for pattern in "$@"; do
if [[ ! -f "$file" ]] || ! grep -Fq "$pattern" "$file"; then
all_found=0
break
fi
done
if [[ "$all_found" -eq 1 ]]; then
return 0
fi
sleep 1
done
return 1
}
wait_for_any_pattern() {
local file="$1"
local timeout_seconds="$2"
shift 2
local second pattern
for ((second=0; second<timeout_seconds; second++)); do
if [[ -f "$file" ]]; then
for pattern in "$@"; do
if grep -Fq "$pattern" "$file"; then
return 0
fi
done
fi
sleep 1
done
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)"
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

View File

@@ -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())

View File

@@ -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" "$@"

View File

@@ -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 <path> 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."

View File

@@ -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."

View File

@@ -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" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == "-o" && "\${2:-}" == "pgid=" && "\${3:-}" == "-p" ]]; then
echo " $fake_pgid"
exit 0
fi
exec /bin/ps "\$@"
EOF
bash_env_file="$TEST_ROOT/bash-env"
cat > "$bash_env_file" <<EOF
kill() {
echo "kill \$*" >> "$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."

View File

@@ -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

View File

@@ -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" <<EOF
#!/usr/bin/env bash
exec "$(command -v python3)" "\$@"
EOF
chmod +x "$BIN_DIR/pnpm" "$BIN_DIR/node" "$BIN_DIR/lsof" "$BIN_DIR/python3"
export PATH="$BIN_DIR:$ORIGINAL_PATH"
export FAKE_CMD_LOG="$CMD_LOG"
export FAKE_APP_RUNTIME_COUNT="${FAKE_APP_RUNTIME_COUNT:-1}"
export FAKE_ELECTRON_RUNTIME_COUNT="${FAKE_ELECTRON_RUNTIME_COUNT:-1}"
export FAKE_DB_RUNTIME_COUNT="${FAKE_DB_RUNTIME_COUNT:-1}"
}
cleanup_fake_env() {
if [[ -n "${REPO_ROOT:-}" ]]; then
local pid_file pid
for pid_file in "$REPO_ROOT"/tmp/logseq-repl/*.pid; do
[[ -e "$pid_file" ]] || continue
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_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."