feat(skill): add logseq-repl skill

This commit is contained in:
rcmerci
2026-04-26 23:05:15 +08:00
parent 342b7b9123
commit b2676eb405
11 changed files with 1678 additions and 183 deletions

View File

@@ -1,158 +0,0 @@
---
name: db-worker-node-repl
description: Start `db-worker-node` in development and attach a `shadow-cljs` CLJS REPL (or editor nREPL session) to the `:db-worker-node` build for interactive debugging.
---
# db-worker-node REPL
## When to use
Use this skill when the user asks how to:
- start `db-worker-node` locally,
- start `shadow-cljs` REPL/nREPL,
- connect a CLJS REPL to the `:db-worker-node` runtime.
## Repo context
Commands below assume repo root:
- `/Users/rcmerci/gh-repos/logseq`
## Automation scripts (recommended)
This skill now includes two scripts under `scripts/`:
- `scripts/start-db-worker-node-repl.sh`
- `scripts/cleanup-db-worker-node-repl.sh`
Location:
- `/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/`
### Start script
`start-db-worker-node-repl.sh` automates the workflow:
1. starts/reuses `npx shadow-cljs watch db-worker-node`,
2. starts/reuses `node ./static/db-worker-node.js --repo <name>`,
3. attaches `npx shadow-cljs cljs-repl db-worker-node` (unless `--no-repl`).
Basic usage:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh
```
Common options:
```bash
# Start with a specific repo and do not attach REPL
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl
# Pass extra args to db-worker-node
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo demo -- --create-empty-db
# Override repo root
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo-root /path/to/logseq
```
Environment overrides:
- `REPO_ROOT` (default: `/Users/rcmerci/gh-repos/logseq`)
- `DB_REPO` (default: `demo`)
State/log files created under:
- `<repo>/tmp/db-worker-node-repl/shadow-db-worker-node.log`
- `<repo>/tmp/db-worker-node-repl/db-worker-node.log`
- `<repo>/tmp/db-worker-node-repl/shadow-db-worker-node.pid`
- `<repo>/tmp/db-worker-node-repl/db-worker-node.pid`
### Cleanup script
`cleanup-db-worker-node-repl.sh` stops the processes tracked by PID files.
Basic usage:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh
```
Options:
```bash
# Force kill if graceful stop times out
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh --force
# Override repo root
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh --repo-root /path/to/logseq
```
## Editor nREPL attach flow (Calva/CIDER/Cursive)
If your editor connects to nREPL directly:
- Host: `localhost`
- Port: `8701`
- Build: `:db-worker-node`
For a CLJ-first session (for example CIDER), after connecting to nREPL run:
```clojure
(shadow.cljs.devtools.api/repl :db-worker-node)
```
## If `yarn watch` is already running
Still prefer the start script as the single entry point:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh
```
If state becomes inconsistent, run cleanup first, then start again.
## Troubleshooting
### `shadow-cljs already running in project`
Cause: stale or conflicting shadow/db-worker-node processes.
Fix:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh
```
### `repo is required`
Cause: runtime was started without `--repo`.
Fix:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo <name>
```
### `No available JS runtime`
Cause: shadow REPL is up but no live `:db-worker-node` runtime is attached.
Fix:
```bash
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh
/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo <name>
```
### Runtime/module startup errors in node process
If startup fails with missing module/bundled runtime errors, rebuild runtime artifacts first:
```bash
yarn db-worker-node:release:bundle
```
Then run the start script again.

View File

@@ -32,7 +32,7 @@ Useful tools:
### `db-worker-node`
Before controlling or attaching to `db-worker-node`, load repo-local `db-worker-node-repl`.
Before controlling or attaching to `db-worker-node`, load repo-local `logseq-repl`.
Useful tools:

View File

@@ -0,0 +1,293 @@
---
name: logseq-repl
description: Start and coordinate Logseq development REPL workflows for the Desktop renderer `:app` runtime and the `:db-worker-node` runtime while sharing a single `yarn watch` process.
---
# Logseq REPL workflows
Use this skill when the user needs a Logseq development REPL for:
- Desktop renderer `:app`
- `:db-worker-node`
- or both at once
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.
## 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? You probably need `:electron`, not this workflow.
Runtime reminders:
- `:app` = Electron renderer
- `:electron` = Electron main process
- `:db-worker` = browser worker
- `:db-worker-node` = Node worker
### Fast paths
Desktop `:app`:
```bash
.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl
npx shadow-cljs cljs-repl app
```
`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
```
Both 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 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/worker-node-repl)
```
### Non-interactive verification examples
Desktop `:app`:
```bash
printf '(prn {:runtime :app :document? (some? js/document) :title (.-title js/document)})\n:cljs/quit\n' | npx shadow-cljs cljs-repl app
```
`db-worker-node`:
```bash
printf '(prn {:runtime :db-worker-node :process? (some? js/process) :platform (.-platform js/process)})\n:cljs/quit\n' | npx shadow-cljs cljs-repl db-worker-node
```
### 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
### 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 both `:app` and `:db-worker-node` together
Use one terminal or editor session per runtime.
Shared `yarn watch` is expected here. Do not rely on one interactive REPL session to cover both 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:
```clojure
(shadow.user/cljs-repl)
```
#### `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
#### 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. re-run the script without `--no-repl`, or attach from your editor
#### 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`.
#### `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
```
#### `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>
```
#### `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 both REPLs:
1. identify whether they need `:app`, `:db-worker-node`, or both
2. if they need Desktop `:app`, close browser dev app instances first
3. run the matching start script under `logseq-repl/scripts/`
4. if they need both runtimes, start both with `--no-repl`
5. attach from the matching build or helper
6. point troubleshooting at `tmp/logseq-repl/shared-shadow-watch.log`
7. run the matching cleanup script when finished

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-/Users/rcmerci/gh-repos/logseq}"
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() {
@@ -12,7 +14,7 @@ Usage:
cleanup-db-worker-node-repl.sh [options]
Options:
--repo-root <path> Logseq repository root (default: /Users/rcmerci/gh-repos/logseq)
--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
@@ -41,8 +43,13 @@ while [[ $# -gt 0 ]]; do
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"
@@ -102,13 +109,40 @@ stop_by_pid_file() {
rm -f "$pid_file"
}
if [[ ! -d "$LOG_DIR" ]]; then
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"
stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch"
rm -f "$DB_REPO_FILE"
stop_shadow_watch
echo "Cleanup done."

View File

@@ -0,0 +1,170 @@
#!/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-desktop-app-repl.sh.
Usage:
cleanup-desktop-app-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/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"
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
}
process_group_id() {
local pid="$1"
ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]'
}
current_process_group_id() {
process_group_id "$$"
}
signal_process_group() {
local signal="$1"
local pid="$2"
local current_pgid pgid
pgid="$(process_group_id "$pid" || true)"
current_pgid="$(current_process_group_id || true)"
if [[ -n "${pgid:-}" && "$pgid" =~ ^[0-9]+$ && "$pgid" != "${current_pgid:-}" ]]; then
kill "-$signal" "-$pgid" 2>/dev/null || true
fi
kill "-$signal" "$pid" 2>/dev/null || true
}
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"
signal_process_group TERM "$pid"
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"
signal_process_group KILL "$pid"
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 "$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
}
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 "$DESKTOP_PID_FILE" "desktop-electron"
stop_shadow_watch
echo "Cleanup done."

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-/Users/rcmerci/gh-repos/logseq}"
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=()
@@ -15,7 +17,7 @@ Usage:
Options:
--repo <name> Graph repo name passed to db-worker-node (default: demo)
--repo-root <path> Logseq repository root (default: /Users/rcmerci/gh-repos/logseq)
--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
@@ -69,18 +71,28 @@ if ! command -v npx >/dev/null 2>&1; then
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"
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"
is_running_pid() {
local pid="$1"
@@ -131,51 +143,80 @@ wait_for_shadow_build_ready() {
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
local existing_pid shared_pid
existing_pid="$(read_pid "$SHADOW_PID_FILE" || true)"
if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then
echo "Reusing shadow-cljs watch (pid=$existing_pid)"
echo "Reusing shared shadow-cljs watch (pid=$existing_pid)"
return 0
fi
echo "Starting shadow-cljs watch db-worker-node ..."
pushd "$REPO_ROOT" >/dev/null
nohup npx shadow-cljs watch db-worker-node > "$SHADOW_LOG" 2>&1 &
local shadow_pid=$!
popd >/dev/null
shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)"
if [[ -n "${shared_pid:-}" ]] && 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 ! is_running_pid "$shadow_pid"; then
echo "Error: shadow-cljs exited early. Check $SHADOW_LOG" >&2
echo "Error: yarn watch exited early. Check $SHARED_SHADOW_LOG" >&2
exit 1
fi
if wait_for_log_pattern "$SHADOW_LOG" "watching build :db-worker-node" 45; then
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 "$SHADOW_LOG" 120; then
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 $SHADOW_LOG" >&2
echo "Error: shadow-cljs build is not ready. Check $SHARED_SHADOW_LOG" >&2
exit 1
fi
}
ensure_db_worker_node() {
local existing_pid
local existing_pid existing_repo
existing_pid="$(read_pid "$DB_PID_FILE" || true)"
existing_repo="$(read_pid "$DB_REPO_FILE" || true)"
if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then
local existing_cmd
existing_cmd="$(ps -p "$existing_pid" -o command= || true)"
if [[ "$existing_cmd" == *"--repo $DB_REPO"* ]]; then
if [[ "$existing_repo" == "$DB_REPO" ]]; then
echo "Reusing db-worker-node runtime (pid=$existing_pid, repo=$DB_REPO)"
return 0
fi
@@ -196,6 +237,7 @@ ensure_db_worker_node() {
popd >/dev/null
echo "$db_pid" > "$DB_PID_FILE"
echo "$DB_REPO" > "$DB_REPO_FILE"
sleep 1
if ! is_running_pid "$db_pid"; then
@@ -247,9 +289,11 @@ ensure_db_worker_node
verify_repl_connectivity
echo
echo "Logs:"
echo " shadow-cljs: $SHADOW_LOG"
echo " db-worker: $DB_LOG"
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"

View File

@@ -0,0 +1,329 @@
#!/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
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"
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"
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
}
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="$(read_pid "$SHADOW_PID_FILE" || true)"
if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then
echo "Reusing shared shadow-cljs watch (pid=$existing_pid)"
return 0
fi
shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)"
if [[ -n "${shared_pid:-}" ]] && 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 ! 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 \
"shadow-cljs - nREPL server started on port 8701" \
"[:electron] Build completed." \
"[:app] Build completed."; then
echo "shadow-cljs watch is ready (pid=$shadow_pid)"
else
echo "Error: yarn watch did not become ready 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
}
get_app_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 :app))))" 2>&1)"; then
popd >/dev/null
echo "Error: failed to inspect :app runtimes." >&2
echo "--- clj-eval output ---" >&2
echo "$output" >&2
echo "-----------------------" >&2
exit 1
fi
popd >/dev/null
local runtime_count
runtime_count="$(printf '%s\n' "$output" | awk '/^[0-9]+$/{n=$0} END{if (n != "") print n; else exit 1}')" || {
echo "Error: could not parse :app runtime count." >&2
echo "--- clj-eval output ---" >&2
echo "$output" >&2
echo "-----------------------" >&2
exit 1
}
printf '%s\n' "$runtime_count"
}
ensure_single_app_runtime() {
local runtime_count second
for ((second=0; second<60; second++)); do
runtime_count="$(get_app_runtime_count)"
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 "Make sure the Desktop dev app 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,343 @@
#!/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 "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 both `:app` and `:db-worker-node` together' "$SKILL_FILE"
assert_contains "shared-shadow-watch.log" "$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

@@ -0,0 +1,363 @@
#!/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" ]] && [[ -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
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"
}
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 both `:app` and `:db-worker-node` together' "$SKILL_FILE"
assert_contains "close browser dev app instances" "$SKILL_FILE"
assert_contains "shared-shadow-watch.log" "$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 :db-worker-node" "$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 "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

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
fail() {
echo "FAIL: $*" >&2
return 1
}
assert_file_exists() {
local path="$1"
[[ -e "$path" ]] || fail "expected file to exist: $path"
}
assert_not_exists() {
local path="$1"
[[ ! -e "$path" ]] || fail "expected path to be absent: $path"
}
assert_contains() {
local needle="$1"
local haystack_file="$2"
if ! grep -Fq "$needle" "$haystack_file"; then
echo "Expected to find: $needle" >&2
echo "--- file: $haystack_file ---" >&2
cat "$haystack_file" >&2
echo "----------------------------" >&2
return 1
fi
}
assert_not_contains_text() {
local needle="$1"
local file="$2"
if grep -Fq "$needle" "$file"; then
echo "Did not expect to find: $needle" >&2
echo "--- file: $file ---" >&2
cat "$file" >&2
echo "--------------------" >&2
return 1
fi
}
assert_not_matches() {
local pattern="$1"
local file="$2"
if grep -Eq "$pattern" "$file"; then
echo "Did not expect regex match: $pattern" >&2
echo "--- file: $file ---" >&2
cat "$file" >&2
echo "--------------------" >&2
return 1
fi
}
assert_equals() {
local expected="$1"
local actual="$2"
[[ "$expected" == "$actual" ]] || fail "expected '$expected' but got '$actual'"
}
portable_path_pattern() {
local slash='/'
local windows_drive='[A-Za-z]:\\[^[:space:]]+'
printf '%s' "${slash}Users${slash}[^[:space:]]+|${slash}home${slash}[^[:space:]]+|${windows_drive}"
}
run_test() {
local name="$1"
local fn="$2"
if (set -e; "$fn"); then
echo "PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo "FAIL: $name" >&2
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}

View File

@@ -12,5 +12,5 @@
- `--profile` - performance check
- Logseq-cli skill - explore cli self in agent
- db-worker-node.log - logs for db-worker-node process
- db-worker-node-repl skill - Directly validate some db worker node code in the REPL
- logseq-repl skill - Directly validate some db worker node code in the REPL