#!/usr/bin/env python3 """REPL driver for the espresso_frame server's web UI. Playwright-based since chromium-cli isn't available in this container. Reads one command per line from stdin, prints a result line -- built for tmux send-keys/capture-pane use by an agent. Vocabulary mirrors chromium-cli where it overlaps (nav/wait-for/click/fill/screenshot/ eval/console-errors). Requires setup.sh to have run first (Python venv + Playwright Chromium + the non-root shared-lib/font extraction). Run via: server/.claude/skills/run-server/env.sh sourced, then .venv/bin/python server/.claude/skills/run-server/driver.py See SKILL.md for the full agent-path invocation (tmux wrapping etc). """ import glob import json import os import sys from playwright.sync_api import sync_playwright SHOT_DIR = os.environ.get("SCREENSHOT_DIR", "/tmp/run-server-shots") os.makedirs(SHOT_DIR, exist_ok=True) BASE = os.environ.get("RUN_SERVER_BASE_URL", "http://127.0.0.1:8420") def find_chrome() -> str: override = os.environ.get("RUN_SERVER_CHROME_BIN") if override and os.path.exists(override): return override matches = glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome")) if not matches: sys.exit("chrome binary not found -- run setup.sh first") return matches[0] pw = sync_playwright().start() # The FULL `chrome` binary, not chrome-headless-shell (Playwright's # default headless target): chrome-headless-shell crashed on basic # calls like set_content() in this container even once every # ldd-reported missing lib was resolved. Full chrome + --no-sandbox is # stable here. browser = pw.chromium.launch(executable_path=find_chrome(), args=["--no-sandbox"]) page = browser.new_page(viewport={"width": 1280, "height": 900}) console_errors: list[str] = [] page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None) page.on("pageerror", lambda exc: console_errors.append(str(exc))) def cmd_nav(arg): url = arg if arg.startswith("http") else BASE + arg page.goto(url) print("nav ->", page.url) def cmd_wait_for(arg): page.wait_for_selector(arg, timeout=10000) print("found:", arg) def cmd_click(arg): page.click(arg) print("clicked:", arg) def cmd_fill(arg): sel, _, value = arg.partition(" ") page.fill(sel, value) print("filled:", sel, "=", value) def cmd_press(arg): page.keyboard.press(arg) print("pressed:", arg) def cmd_screenshot(arg): name = arg or f"ss-{len(os.listdir(SHOT_DIR))}" path = os.path.join(SHOT_DIR, name + ".png") page.screenshot(path=path) print("screenshot:", path) def cmd_eval(arg): try: print(json.dumps(page.evaluate(arg))) except Exception as e: print("ERROR:", e) def cmd_console_errors(_arg): print(json.dumps(console_errors)) def cmd_is_open(arg): """App-specific: print whether a element is open (true/false).""" print(json.dumps(page.eval_on_selector(arg, "el => el.open"))) def cmd_bootstrap_admin(arg): """App-specific: complete first-run /setup (username/password args, default admin/testpassword123). Every scratch DB starts with no users, and /setup is the only way in -- it also auto-links the pre-existing frame #1 (created by migrations) to the new admin, so /frames/1/... is reachable right after this.""" parts = arg.split() username = parts[0] if len(parts) > 0 else "admin" password = parts[1] if len(parts) > 1 else "testpassword123" page.goto(BASE + "/setup") page.fill("input[name=username]", username) page.fill("input[name=password]", password) page.click("button[type=submit]") page.wait_for_load_state("networkidle") print("bootstrapped admin, now at:", page.url) def cmd_quit(_arg): browser.close() pw.stop() sys.exit(0) COMMANDS = { "nav": cmd_nav, "wait-for": cmd_wait_for, "click": cmd_click, "fill": cmd_fill, "press": cmd_press, "screenshot": cmd_screenshot, "eval": cmd_eval, "console-errors": cmd_console_errors, "is-open": cmd_is_open, "bootstrap-admin": cmd_bootstrap_admin, "quit": cmd_quit, } print("run-server driver -- commands:", ", ".join(COMMANDS), flush=True) print("driver>", end=" ", flush=True) for line in sys.stdin: line = line.strip() if not line: print("driver>", end=" ", flush=True) continue cmd, _, rest = line.partition(" ") fn = COMMANDS.get(cmd) if fn is None: print("unknown command:", cmd, "-- try one of:", ", ".join(COMMANDS)) else: try: fn(rest) except Exception as e: print("ERROR:", e) print("driver>", end=" ", flush=True)