tools/serve.py: build, serve and rebuild on change (t9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 01:07:08 -04:00
co-authored by Claude Opus 5.5
parent 259ad89ded
commit 1a3a848bad
7 changed files with 117 additions and 8 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ them. In short:
```sh ```sh
python3 build.py # build dist/index.html from src/ (takes a second) python3 build.py # build dist/index.html from src/ (takes a second)
python3 -m http.server -d dist # play at http://localhost:8000 (or open dist/index.html) python3 tools/serve.py # build, serve at http://localhost:8000, rebuild on src/ changes
python3 test/run_all.py # all headless test suites, in parallel (about a minute) python3 test/run_all.py # all headless test suites, in parallel (about a minute)
python3 test/run_all.py pipes # only suites whose name contains "pipes" python3 test/run_all.py pipes # only suites whose name contains "pipes"
python3 test/run_all.py --browser # also the browser tests (rebuilds first; about 3 more minutes) python3 test/run_all.py --browser # also the browser tests (rebuilds first; about 3 more minutes)
+1 -1
View File
@@ -12,7 +12,7 @@ machines, until the land claim is paid off and beyond.
```sh ```sh
python3 build.py python3 build.py
python3 -m http.server -d dist # then open http://localhost:8000 python3 tools/serve.py # builds, serves at http://localhost:8000 and rebuilds when src/ changes
``` ```
Opening `dist/index.html` directly works too. The game is one self-contained page with no Opening `dist/index.html` directly works too. The game is one self-contained page with no
+1 -1
View File
@@ -6,7 +6,7 @@ code) into one classic script, so they share one global scope and 'use strict' a
src/sim/base.js applies to all of it. No bundler, no dependencies. src/sim/base.js applies to all of it. No bundler, no dependencies.
Writes: Writes:
dist/index.html the full page: open it, or serve dist/ with python -m http.server dist/index.html the full page: open it, or serve with python3 tools/serve.py
dist/deepgrain-foundry.html the same without <!doctype>/<head>/<body>, for hosts (like a dist/deepgrain-foundry.html the same without <!doctype>/<head>/<body>, for hosts (like a
claude.ai artifact) that wrap the page themselves claude.ai artifact) that wrap the page themselves
dist/bundle-lines.json where each source file starts in dist/index.html, so a line number dist/bundle-lines.json where each source file starts in dist/index.html, so a line number
+5 -3
View File
@@ -20,9 +20,10 @@ src/ui/ the browser shell: input, HUD, build menu, notebook, setti
audio, the frame loop audio, the frame loop
test/ headless suites (t_*.js) and their harness; browser tests (play.py, test/ headless suites (t_*.js) and their harness; browser tests (play.py,
t_smooth.py); run_all.py t_smooth.py); run_all.py
tools/ shot.py (screenshots), where.py, map.py, perf.py, playthrough.py, tools/ serve.py (build, serve, rebuild on change), shot.py (screenshots),
use_venv.py (switches browser scripts to .venv), scenes/ (screenshot where.py, map.py, perf.py, playthrough.py, use_venv.py (switches browser
setups), experiments/ (measurements and reports) scripts to .venv), scenes/ (screenshot setups), experiments/
(measurements and reports)
docs/ this map, DESIGN.md, TESTING.md, systems/ (notes per system), docs/ this map, DESIGN.md, TESTING.md, systems/ (notes per system),
history/ (the owner's requests), agent-log.md history/ (the owner's requests), agent-log.md
dist/, shots/ build output and screenshots (not committed) dist/, shots/ build output and screenshots (not committed)
@@ -456,6 +457,7 @@ The frame loop and boot.
- `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md. - `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
- `tools/perf.py`: Times the renderer and the simulation in a headless browser: light, drawWorld (with and without light and glow), the pixel upload and blit, and one simulation tick, in ms. Rebuild first. - `tools/perf.py`: Times the renderer and the simulation in a headless browser: light, drawWorld (with and without light and glow), the pixel upload and blit, and one simulation tick, in ms. Rebuild first.
- `tools/playthrough.py`: Headless browser run: load the page, start a world, play a little, take screenshots. - `tools/playthrough.py`: Headless browser run: load the page, start a world, play a little, take screenshots.
- `tools/serve.py`: Build, serve and rebuild when sources change: python3 tools/serve.py [--port 8000].
- `tools/shot.py`: Screenshot the game in a headless browser, after setting up a scene with JavaScript. - `tools/shot.py`: Screenshot the game in a headless browser, after setting up a scene with JavaScript.
- `tools/use_venv.py`: Let browser scripts run with plain python3 by switching to the repo's .venv. - `tools/use_venv.py`: Let browser scripts run with plain python3 by switching to the repo's .venv.
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from. - `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
+9
View File
@@ -4,6 +4,15 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t9 Script to run the local server for testing (Claude Code, Codex)
- New `tools/serve.py`: builds, serves `dist/` on 127.0.0.1 (port 8000, or the next free one up
to 8010), sends `Cache-Control: no-store`, and rebuilds when `src/` or `build.py` changes; a
failed build is printed and the old page keeps being served.
- `README.md`, `AGENTS.md`, `build.py` docstring, `docs/PROJECT_MAP.md`: point at it.
- Checked by hand: page served with no-store; touching a source file rebuilt it; with the owner's
old server on 8000 it moved to 8001; Ctrl+C stops it. Headless suites and map check pass.
## 2026-10-24: t11 Welcome and first milestone shown again on an older world (Claude Code, Codex) ## 2026-10-24: t11 Welcome and first milestone shown again on an older world (Claude Code, Codex)
- Cause: nothing cleared the discovery cards (`#cards`) when a world was entered, so a new - Cause: nothing cleared the discovery cards (`#cards`) when a world was entered, so a new
+14 -2
View File
@@ -1,11 +1,23 @@
--- ---
id: t9 id: t9
title: Script to run the local server for testing title: Script to run the local server for testing
status: Todo status: Review
created: 2026-10-24 created: 2026-10-24
updated: 2026-10-24T04:41:46Z updated: 2026-10-24T05:06:19Z
order: 3
report: |
(agent) New: python3 tools/serve.py builds the game, serves it at http://localhost:8000 and
rebuilds by itself when anything in src/ changes. Pages aren't cached, so a normal refresh
shows the new build. If 8000 is taken (an old server still running), it uses 8001 and says so.
To check: stop your old server (Ctrl+C), run python3 tools/serve.py, open the address it
prints, change something in src/, refresh. Ctrl+C stops it.
--- ---
## Context ## Context
- i need a script to run the local server for testing. - i need a script to run the local server for testing.
## Subtasks
- [x] Add the server and update the command references and map.
- [x] Check serving, rebuilds, port fallback and shutdown; run the headless suites.
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Build, serve and rebuild when sources change: python3 tools/serve.py [--port 8000]."""
import argparse, errno, http.server, pathlib, subprocess, sys, threading, time
ROOT = pathlib.Path(__file__).resolve().parent.parent
DIST = ROOT / 'dist'
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(DIST), **kwargs)
def end_headers(self):
self.send_header('Cache-Control', 'no-store')
super().end_headers()
def log_message(self, format, *args):
pass
def build(first=False):
result = subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if result.returncode:
print('build failed:\n' + result.stdout, flush=True)
elif first:
print(result.stdout, end='', flush=True)
else:
print(time.strftime('rebuilt %H:%M:%S'), flush=True)
def mtimes():
files = [ROOT / 'build.py', *(ROOT / 'src').rglob('*')]
stamps = {}
for path in files:
try:
if path.is_file(): stamps[path] = path.stat().st_mtime_ns
except FileNotFoundError:
pass
return stamps
def watch(stopped, previous):
while not stopped.wait(0.5):
current = mtimes()
if current != previous:
previous = current
build()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', type=int, default=8000)
args = parser.parse_args()
if not 1 <= args.port <= 65525:
parser.error('--port must be between 1 and 65525 (leaves room for ten retries)')
previous = mtimes()
build(first=True)
if not (DIST / 'index.html').is_file():
sys.exit('Cannot serve: dist/index.html does not exist. Fix the build and try again.')
for port in range(args.port, args.port + 11):
try:
server = http.server.ThreadingHTTPServer(('127.0.0.1', port), Handler)
break
except OSError as error:
if error.errno != errno.EADDRINUSE:
sys.exit(f'Cannot serve on port {port}: {error}')
else:
sys.exit(f'Cannot serve: ports {args.port}-{args.port + 10} are all busy.')
if port != args.port:
print(f'Port {args.port} was busy; using {port}.', flush=True)
print(f'Serving dist/ at http://localhost:{port} (Ctrl+C to stop)', flush=True)
stopped = threading.Event()
threading.Thread(target=watch, args=(stopped, previous), daemon=True).start()
try:
with server:
server.serve_forever()
finally:
stopped.set()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('\nServer stopped.', flush=True)