This sandbox ships with no Python/Node/Docker/browser and no sudo, so the bulk of this is setup.sh: bootstrap Python via uv, then get Playwright's Chromium (and tmux, also missing) working by extracting their .deb dependencies non-root instead of apt-get install. driver.py is a small Playwright REPL standing in for chromium-cli, which isn't available here either. Also carves out .claude/skills/ from the blanket .claude/ gitignore -- skills are shared project tooling, not personal/local state.
9.3 KiB
name, description
| name | description |
|---|---|
| run-server | Build, run, and drive the espresso_frame FastAPI server (server/) -- start it against a scratch DB, browser-test its UI, run its pytest suite. Use when asked to start the server, take a screenshot of a frame page, click through the web UI, or verify a server/UI change actually works. |
The espresso_frame server is a FastAPI app (app.main:app) with a
server-rendered Jinja UI. For agent/automated use it's driven by a
Playwright REPL at .claude/skills/run-server/driver.py, run under
tmux -- chromium-cli isn't available in this container, so this
driver replaces it (same command vocabulary: nav/wait-for/click/fill/
screenshot/eval/console-errors).
This container ships with no Python, Node, Docker, or browser, and
no sudo. setup.sh bootstraps everything non-root; it's the bulk of
what makes this skill non-obvious. Run it once per fresh container.
All paths below are relative to server/.
Prerequisites
None to install manually -- setup.sh does it all without root, using
only curl/apt-get download/dpkg-deb -x (never apt-get install,
which needs root). It downloads ~450MB total (Python, Chromium, tmux,
shared libs) on first run.
bash .claude/skills/run-server/setup.sh
Re-running is safe and fast -- every step checks whether it already happened (venv exists? chromium downloaded? libs extracted? tmux present?) before doing any work.
This creates:
.venv/-- Python 3.12 +requirements.txt+playwright, viauv(a static Rust binary that fetches its own Python -- no compiler needed, install viacurl -LsSf https://astral.sh/uv/install.sh | sh)~/.cache/ms-playwright/-- Chromium (fullchrome+ headless-shell)/tmp/run-server-chromium-deps/-- Chromium's + tmux's shared libs and fonts, extracted (not installed) from.debfiles.claude/skills/run-server/env.sh-- thePATH/LD_LIBRARY_PATH/FONTCONFIG_PATH/RUN_SERVER_CHROME_BINexports the driver needs (gitignored -- host-specific/tmppaths, regenerated by setup.sh)
Run (agent path)
1. Start the server against a scratch DB (never the real
deployment's data -- see CLAUDE.md):
bash .claude/skills/run-server/start-server.sh
# -> server PID <pid> up on http://127.0.0.1:8420 (log: /tmp/run-server-scratch/server.log)
Optional args: start-server.sh [scratch-dir] [port] (defaults
/tmp/run-server-scratch / 8420).
2. Drive it, wrapped in tmux so you can send one command at a time
and read the response before sending the next. tmux itself was
extracted the same non-root way as Chromium's libs (see Prerequisites)
and needs LD_LIBRARY_PATH set in this shell too, not just inside
the pane -- source env.sh before the first tmux call:
source .claude/skills/run-server/env.sh
tmux new-session -d -s runserver -x 200 -y 50
tmux send-keys -t runserver \
'source .claude/skills/run-server/env.sh && .venv/bin/python .claude/skills/run-server/driver.py' Enter
timeout 20 bash -c 'until tmux capture-pane -t runserver -p | grep -q "driver>"; do sleep 0.3; done'
# Every fresh scratch DB starts with no users -- bootstrap-admin
# completes first-run /setup and logs in (see Commands table):
tmux send-keys -t runserver 'bootstrap-admin' Enter
timeout 15 bash -c 'until tmux capture-pane -t runserver -p | grep -q "bootstrapped admin"; do sleep 0.3; done'
tmux send-keys -t runserver 'nav /frames/1/config' Enter
tmux send-keys -t runserver 'wait-for #frame-preview-thumb' Enter
tmux send-keys -t runserver 'screenshot before' Enter
tmux capture-pane -t runserver -p
Poll for a specific marker between send-keys and capture-pane
(driver>, bootstrapped admin, screenshot:, ...) rather than a
fixed sleep -- it's faster and fails loudly instead of capturing a
half-rendered screen. Give each poll its own timeout (~15-20s); don't
chain many polling loops inside one shell invocation -- see Gotchas.
Screenshots land in /tmp/run-server-shots/ (override:
SCREENSHOT_DIR). Actually look at them -- a blank or error-page
screenshot is a failure to launch, not success.
Driver commands
| command | what it does |
|---|---|
nav <path-or-url> |
navigate (relative paths resolve against http://127.0.0.1:8420, override with RUN_SERVER_BASE_URL) |
wait-for <selector> |
wait up to 10s for a selector (plain CSS -- see Gotchas for attribute selectors) |
click <selector> |
click an element |
fill <selector> <value> |
fill an input |
press <key> |
keyboard press (e.g. Enter) |
screenshot [name] |
→ /tmp/run-server-shots/<name>.png |
eval <js> |
evaluate JS in the page, prints JSON |
console-errors |
prints all captured console/page errors as a JSON array |
is-open <dialog-selector> |
prints true/false for a <dialog> element's .open -- use this instead of wait-for sel[open] |
bootstrap-admin [user] [pass] |
completes first-run /setup (defaults admin/testpassword123); links frame #1 and logs in |
quit |
closes the browser, exits the driver |
3. Stop the server when done:
bash .claude/skills/run-server/stop-server.sh
tmux kill-session -t runserver
Run (human path)
cd server
DATABASE_URL="sqlite:////tmp/dev.db" CONFIG_PATH="/tmp/dev-config.json" \
.venv/bin/uvicorn app.main:app --reload --port 8420
Open http://localhost:8420 in a real browser. start.sh (the Docker
entrypoint) is not this -- it also launches the Node whiteboard
render-service sidecar, which needs Node (not installed here) and
isn't needed for most UI testing.
Test
.venv/bin/pytest
Uses its own tempfile SQLite per run (tests/conftest.py) -- no setup
needed beyond the venv.
Gotchas
-
chrome-headless-shell(Playwright's default headless target) crashes on basic calls in this container, e.g.page.set_content()returnsTargetClosedError, even after everyldd-reported missing library is resolved. The fullchromebinary (chromium-*/chrome-linux64/chrome)--no-sandboxis stable;driver.pyandsetup.shboth use it, not the headless-shell default.
-
Missing fonts silently break
fill(), not just rendering. Beforefontconfig/libfontconfig1were extracted andfonts.confpointed at the extracted font dir,page.fill()ran with no error but left inputs empty (input_value()returned""), and all text rendered invisible in screenshots. It looks like a scripting bug, not a missing-lib problem -- iffillsilently no-ops, suspect fonts before suspecting the selector or a race. -
No
apt-get install/playwright install-deps(no root) and noapt-get updateinto the real/var/lib/apt/lists(root-owned). Worked around by redirecting apt's state dirs to a scratch, user-writable path (-o Dir::State::Lists=... -o Dir::Cache=...), which makes plainupdateandinstall --download-only --print-uriswork as a non-root user; thendpkg-deb -x <deb> <root>(extract, not install) needs no root either.setup.shdoes this for Chromium's deps and fortmuxitself, which also isn't preinstalled. -
wait-forwith an attribute selector like#frame-preview-dialog[open]is unreliable throughtmux send-keys-- shell/tmux escaping of[/]easily mangles it (seen: a real 10s Playwright timeout from a garbled selector, not a fast failure). Use the app-specificis-open <selector>command instead ofwait-for sel[open]to check a<dialog>'s open state. -
Don't chain many
tmux send-keys+ polling-timeoutloops inside one shell invocation. Each poll can legitimately take up to its own timeout (e.g. 15s) if a selector is wrong; five or six chained in one command can add up past this tool's own command timeout even though each individual step is fine. Send one or two commands per shell call and check the pane before continuing. -
A crashed
chrome-headless-shellprocess can leave a large core dump file (server/core, ~170MB, from the crash described above) if core dumps are enabled. It's not part of the app -- delete it, and use fullchrome(asdriver.pydoes) to avoid triggering it again.
Troubleshooting
error while loading shared libraries: libglib-2.0.so.0(or similar) when launching Chromium directly:LD_LIBRARY_PATHisn't set -- source.claude/skills/run-server/env.shfirst, or run throughdriver.py, which readsRUN_SERVER_CHROME_BINfrom it.fc-listprints nothing after extracting fonts:fonts.conf's<dir>entries still point at the real (unpopulated)/usr/share/fonts.setup.shpatches this withsed; if you extracted packages by hand, do the same.E: Could not open lock file ... Permission deniedfromapt-get: you're missing the-o Dir::State::Lists=... -o Dir::Cache=...overrides -- plainapt-get update/installalways needs root here.tmux: command not found: not preinstalled and no sudo; runsetup.sh, which fetches it the same non-root way as Chromium's libs.tmux: error while loading shared libraries: libutempter.so.0: you sourcedenv.shinside the driver's tmux pane but not in the shell that invokestmuxitself --tmuxwas extracted from the same non-root.debset as Chromium and needsLD_LIBRARY_PATHtoo.source .claude/skills/run-server/env.shbefore the firsttmuxcommand, not just insidesend-keys.