Files
rose-ash/shared/dev_watcher.py
giles a643b3532d
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m43s
Phase 5 cleanup: remove legacy HTML components, fix nav-tree fragment
- Remove old raw! layout components (~app-head, ~app-layout, ~oob-response,
  ~header-row, ~menu-row, ~oob-header, ~header-child) from layout.sexp
- Convert nav-tree fragment from Jinja HTML to sexp source, fixing the
  "Unexpected character: ." parse error caused by HTML leaking into sexp
- Add _as_sexp() helper to safely coerce HTML fragments to ~rich-text
- Fix federation/sexp/search.sexpr extra closing paren
- Remove dead _html() wrappers from blog and account sexp_components
- Remove stale render import from cart sexp_components
- Add dev_watcher.py to auto-reload on .sexp/.sexpr/.js/.css changes
- Add test_parse_all.py to parse-check all 59 sexpr/sexp files
- Fix test assertions for sx- attribute prefix (was hx-)
- Add sexp.js version logging for cache debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:12:03 +00:00

70 lines
2.0 KiB
Python

"""Watch non-Python files and trigger Hypercorn reload.
Hypercorn --reload only watches .py files. This script watches .sexp,
.sexpr, .js, and .css files and touches a sentinel .py file when they
change, causing Hypercorn to restart.
Usage (from entrypoint.sh, before exec hypercorn):
python3 -m shared.dev_watcher &
"""
import os
import time
import sys
WATCH_EXTENSIONS = {".sexp", ".sexpr", ".js", ".css"}
SENTINEL = os.path.join(os.path.dirname(__file__), "_reload_sentinel.py")
POLL_INTERVAL = 1.5 # seconds
def _collect_mtimes(roots):
mtimes = {}
for root in roots:
for dirpath, _dirs, files in os.walk(root):
for fn in files:
ext = os.path.splitext(fn)[1]
if ext in WATCH_EXTENSIONS:
path = os.path.join(dirpath, fn)
try:
mtimes[path] = os.path.getmtime(path)
except OSError:
pass
return mtimes
def main():
# Watch /app/shared and /app/<service>/sexp plus static dirs
roots = []
for entry in os.listdir("/app"):
full = os.path.join("/app", entry)
if os.path.isdir(full):
roots.append(full)
if not roots:
roots = ["/app"]
# Ensure sentinel exists
if not os.path.exists(SENTINEL):
with open(SENTINEL, "w") as f:
f.write("# reload sentinel\n")
prev = _collect_mtimes(roots)
while True:
time.sleep(POLL_INTERVAL)
curr = _collect_mtimes(roots)
changed = []
for path, mtime in curr.items():
if path not in prev or prev[path] != mtime:
changed.append(path)
if changed:
names = ", ".join(os.path.basename(p) for p in changed[:3])
if len(changed) > 3:
names += f" (+{len(changed) - 3} more)"
print(f"[dev_watcher] Changed: {names} — triggering reload",
flush=True)
os.utime(SENTINEL, None)
prev = curr
if __name__ == "__main__":
main()