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 f822db8c50
commit 41663761fb
5 changed files with 102 additions and 5 deletions
+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
+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)