Skip to content

Instantly share code, notes, and snippets.

@jcayzac
Last active July 3, 2026 09:13
Show Gist options
  • Select an option

  • Save jcayzac/164831f6e59c0ada7385d3b5e9b15fb9 to your computer and use it in GitHub Desktop.

Select an option

Save jcayzac/164831f6e59c0ada7385d3b5e9b15fb9 to your computer and use it in GitHub Desktop.
Render a Bikeshed spec, diff it against a git baseline, and serve the result.
#!/usr/bin/env -S uv run --script
# SPDX-License-Identifier: MIT AND Apache-2.0
#
# Copyright (c) 2026 Julien Cayzac
#
# ─ MIT License ────────────────────────────────────────────────────────────────
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# ─ Apache License ─────────────────────────────────────────────────────────────
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "bikeshed==7.1.1",
# "gitpython==3.1.50",
# "lxml==6.1.0",
# "lxml-stubs==0.5.1",
# "rich==15.0.0",
# "typer==0.26.8",
# ]
# ///
"""
Render a Bikeshed spec, diff it against a git baseline, serve the result.
The new document is always the output skeleton — its <head>, inlined styles,
and all structural containers are preserved intact. Children of each element
are aligned against the baseline by stable Bikeshed anchors (id > href >
descendant anchor > structural key), using difflib LCS. Matched pairs are
recursed into; unmatched new nodes are marked inserted; unmatched old nodes
are spliced back in as deleted; matched text-only leaves get word-level diffs.
Nothing is written to disk unless -o is given.
"""
from __future__ import annotations
import copy
import dataclasses
import difflib
import hashlib
import http.server
import io
import re
import socketserver
import sys
import threading
import time
import traceback
from pathlib import Path
from typing import TYPE_CHECKING
import typer
import lxml.etree as ET
from lxml import html as lhtml
from rich.console import Console
from rich.markup import escape as e
if TYPE_CHECKING:
from lxml.html import HtmlElement
# ── Console output ────────────────────────────────────────────────────────────
@dataclasses.dataclass
class _Console:
"""stdout/stderr console pair with four semantic output helpers.
Call configure(color) once at startup. All helpers are thread-safe.
Bikeshed uses its own ANSI system (not Rich), controlled by
messages.state.printMode. configure() sets that too so Bikeshed
output respects --color as well.
"""
stdout: Console = dataclasses.field(
default_factory=lambda: Console(highlight=False)
)
stderr: Console = dataclasses.field(
default_factory=lambda: Console(highlight=False, stderr=True)
)
def configure(self, color: str) -> None:
"""Apply the requested color mode: 'always', 'never', or 'auto'."""
from bikeshed import messages as bk
bk.state = bk.state.replace(
printMode="plain" if color == "never" else "console"
)
match color:
case "always":
self.stdout = Console(highlight=False, force_terminal=True)
self.stderr = Console(highlight=False, force_terminal=True, stderr=True)
case "never":
self.stdout = Console(highlight=False, no_color=True)
self.stderr = Console(highlight=False, no_color=True, stderr=True)
case _:
self.stdout = Console(highlight=False)
self.stderr = Console(highlight=False, stderr=True)
def step(self, msg: str) -> None:
self.stdout.print(f"[dim]·[/] {msg}")
def done(self, msg: str) -> None:
self.stdout.print(f"[green]✓[/] {msg}")
def warn(self, msg: str) -> None:
self.stderr.print(f"[yellow]⚠[/] {msg}")
def fail(self, msg: str) -> None:
self.stderr.print(f"[red]✗[/] {msg}")
console = _Console()
# ── Constants ─────────────────────────────────────────────────────────────────
_DEFAULT_PORT = 9991
_POLL_INTERVAL = 0.5 # seconds
REPO_DIR = Path.cwd()
# ── Shared state ──────────────────────────────────────────────────────────────
@dataclasses.dataclass
class _State:
# Baseline body: loaded once from git and reused on every rebuild.
baseline: HtmlElement | None = None
# Latest rendered diff, served on GET / and updated after every rebuild.
diff: bytes | None = None
# Version token embedded in the served page and polled by the reload script.
build_id: str = "0"
_state = _State()
_state_lock = threading.Lock()
# ── Diff engine ───────────────────────────────────────────────────────────────
# Classes whose text is volatile (section numbers, self-link glyphs) and must
# be excluded from identity keys so renumbering doesn't flag every heading.
_VOLATILE_CLASSES: frozenset[str] = frozenset({"secno", "self-link"})
# Bikeshed appends a document-wide occurrence counter to cross-reference IDs
# (e.g. "ref-for-USVString⑦"). Inserting content shifts all later counters,
# so strip the suffix before using an id/href as an identity key.
_CIRCLED_DIGITS = "⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
_RE_COUNTER = re.compile("[" + re.escape(_CIRCLED_DIGITS) + "]+$")
_RE_WHITESPACE = re.compile(r"\s+")
_RE_WORD = re.compile(r"\S+|\s+")
# Bikeshed always places the identifying anchor (section id, TOC href) near the
# front of a container. Capping the DFS avoids scanning large subtrees for the
# rare container with no anchor.
_ANCHOR_SCAN_LIMIT = 40
def _children(el: HtmlElement) -> list[HtmlElement]:
"""Element children only — comments and PIs have non-string tags."""
return [c for c in el if isinstance(c.tag, str)]
def _stable(value: str) -> str:
"""Strip a Bikeshed occurrence counter from an id or href."""
return _RE_COUNTER.sub("", value)
def _collect_text(el: HtmlElement, out: list[str], *, is_root: bool) -> None:
"""Depth-first text collection, skipping volatile-class subtrees."""
if set((el.get("class") or "").split()) & _VOLATILE_CLASSES:
if not is_root and el.tail:
out.append(el.tail)
return
if el.text:
out.append(el.text)
for child in el:
if isinstance(child.tag, str):
_collect_text(child, out, is_root=False)
elif child.tail:
out.append(child.tail)
if not is_root and el.tail:
out.append(el.tail)
def _norm_text(el: HtmlElement) -> str:
"""Collapse whitespace in el's text, excluding volatile-class subtrees."""
parts: list[str] = []
_collect_text(el, parts, is_root=True)
return _RE_WHITESPACE.sub(" ", "".join(parts)).strip()
def _stable_classes(el: HtmlElement) -> tuple[str, ...]:
return tuple(
c for c in (el.get("class") or "").split() if c not in _VOLATILE_CLASSES
)
def _defining_anchor(el: HtmlElement) -> tuple[str, str] | None:
"""Return the first stable (kind, value) anchor in a bounded DFS."""
queue = _children(el)
i = 0
while i < len(queue) and i < _ANCHOR_SCAN_LIMIT:
node = queue[i]
i += 1
if eid := node.get("id"):
return ("id", _stable(eid))
if node.tag == "a" and (href := node.get("href")):
return ("href", _stable(href))
queue.extend(_children(node))
return None
def _node_key(el: HtmlElement) -> tuple:
"""Stable identity key for aligning a node across old and new documents.
Priority: own id > own href (for <a>) > descendant anchor > structural
signature. Only true text leaves fall back to a content hash — hashing a
container's text would misalign it whenever any inner content changes.
"""
if eid := el.get("id"):
return ("id", _stable(eid))
if el.tag == "a" and (href := el.get("href")):
return ("href", _stable(href), _norm_text(el))
if _children(el):
anchor = _defining_anchor(el)
return ("anchor", *anchor) if anchor else ("box", el.tag, _stable_classes(el))
return ("text", el.tag, hashlib.md5(_norm_text(el).encode()).hexdigest())
def _add_class(el: HtmlElement, cls: str) -> None:
existing = el.get("class")
el.set("class", f"{existing} {cls}" if existing else cls)
def _splice_deletion(
old_node: HtmlElement, new_parent: HtmlElement, before: HtmlElement | None
) -> None:
"""Insert a deep-copy of old_node into new_parent, marked as deleted."""
clone = copy.deepcopy(old_node)
clone.tail = "\n"
_add_class(clone, "diff-del")
if before is not None:
before.addprevious(clone)
else:
new_parent.append(clone)
def _is_matching_text_leaf(a: HtmlElement, b: HtmlElement) -> bool:
"""True when a and b are same-tag leaves — eligible for word-level diff."""
return a.tag == b.tag and not _children(a) and not _children(b)
def _apply_word_diff(old_el: HtmlElement, new_el: HtmlElement) -> None:
"""Replace new_el's content with inline word-level ins/del spans."""
old_words = _RE_WORD.findall(old_el.text or "")
new_words = _RE_WORD.findall(new_el.text or "")
new_el.text = ""
for child in list(new_el):
new_el.remove(child)
def append_text(text: str) -> None:
if len(new_el):
new_el[-1].tail = (new_el[-1].tail or "") + text
else:
new_el.text = (new_el.text or "") + text
def append_span(css_class: str, words: list[str]) -> None:
if text := "".join(words):
ET.SubElement(new_el, "span", {"class": css_class}).text = text
for op, i1, i2, j1, j2 in difflib.SequenceMatcher(
None, old_words, new_words, autojunk=False
).get_opcodes():
match op:
case "equal":
append_text("".join(new_words[j1:j2]))
case "insert":
append_span("diff-ins-word", new_words[j1:j2])
case "delete":
append_span("diff-del-word", old_words[i1:i2])
case "replace":
append_span("diff-del-word", old_words[i1:i2])
append_span("diff-ins-word", new_words[j1:j2])
def _diff_element(old_el: HtmlElement, new_el: HtmlElement) -> None:
"""Recursively annotate new_el with differences relative to old_el."""
old_kids = _children(old_el)
new_kids = _children(new_el)
# Leaf node: compare text directly; word-diff if it changed.
if not old_kids and not new_kids:
if old_el.text != new_el.text:
_apply_word_diff(old_el, new_el)
return
for op, i1, i2, j1, j2 in difflib.SequenceMatcher(
None,
[_node_key(c) for c in old_kids],
[_node_key(c) for c in new_kids],
autojunk=False,
).get_opcodes():
match op:
case "equal":
for old, new in zip(old_kids[i1:i2], new_kids[j1:j2]):
_diff_element(old, new)
case "insert":
for node in new_kids[j1:j2]:
_add_class(node, "diff-ins")
case "delete":
anchor = new_kids[j1] if j1 < len(new_kids) else None
for node in old_kids[i1:i2]:
_splice_deletion(node, new_el, anchor)
case "replace":
# Single matched text-leaf pair: word diff is more informative
# than marking the whole node as deleted and re-inserted.
if i2 - i1 == j2 - j1 == 1 and _is_matching_text_leaf(
old_kids[i1], new_kids[j1]
):
_apply_word_diff(old_kids[i1], new_kids[j1])
else:
anchor = new_kids[j1] if j1 < len(new_kids) else None
for node in new_kids[j1:j2]:
_add_class(node, "diff-ins")
for node in old_kids[i1:i2]:
_splice_deletion(node, new_el, anchor)
# ── Injected assets ───────────────────────────────────────────────────────────
# Both bg and fg are pinned together for each theme so contrast is guaranteed
# regardless of whatever the page's own stylesheet does.
# fixup.js (loaded by the W3C stylesheet) sets body.darkmode when dark is
# active — this responds to both the OS preference and the page's own toggle.
_DIFF_CSS = """
:root {
--diff-ins-bg: #d4fcbc; --diff-ins-fg: #08350a;
--diff-del-bg: #ffc9d1; --diff-del-fg: #4a0912;
--legend-bg: #fff; --legend-fg: #111; --legend-border: #ccc;
}
body.darkmode {
--diff-ins-bg: #0f3d16; --diff-ins-fg: #c6f7c9;
--diff-del-bg: #4a1420; --diff-del-fg: #ffd0d8;
--legend-bg: #1b1b1b; --legend-fg: #eee; --legend-border: #555;
}
.diff-ins, .diff-ins *:not(.diff-del, .diff-del *) {
background-color: var(--diff-ins-bg) !important;
color: var(--diff-ins-fg) !important;
}
.diff-del, .diff-del * {
background-color: var(--diff-del-bg) !important;
color: var(--diff-del-fg) !important;
text-decoration: line-through !important;
}
.diff-ins-word {
background-color: var(--diff-ins-bg) !important;
color: var(--diff-ins-fg) !important;
}
.diff-del-word {
background-color: var(--diff-del-bg) !important;
color: var(--diff-del-fg) !important;
text-decoration: line-through !important;
}
.diff-ins a, .diff-ins code, .diff-ins-word a, .diff-ins-word code {
color: var(--diff-ins-fg) !important;
}
.diff-del a, .diff-del code, .diff-del-word a, .diff-del-word code {
color: var(--diff-del-fg) !important;
}
.diff-ins img, .diff-ins-word img { outline: 2px solid #1a7f37; }
.diff-del img, .diff-del-word img { outline: 2px solid #cf222e; }
#diff-legend {
position: fixed; top: 8px; right: 8px; z-index: 99999;
font: 12px/1.4 system-ui, sans-serif;
background: var(--legend-bg); color: var(--legend-fg);
border: 1px solid var(--legend-border);
border-radius: 6px; padding: 6px 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.3);
}
#diff-legend b {
display: inline-block; width: 10px; height: 10px;
border-radius: 2px; vertical-align: middle; margin-right: 4px;
}
"""
_LEGEND_HTML = (
'<div id="diff-legend">'
'<span><b style="background:var(--diff-ins-bg)"></b>added</span>&nbsp;&nbsp;'
'<span><b style="background:var(--diff-del-bg)"></b>removed</span>'
"</div>"
)
# Preserves scroll position across live reloads via sessionStorage.
# The build id is embedded at render time and polled against /version;
# a mismatch saves scroll position and reloads.
_RELOAD_JS = """
(function () {
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
const BUILD_ID = "__BUILD_ID__";
const KEY = 'specdiff:scroll';
const save = () => { try { sessionStorage.setItem(KEY, window.scrollY); } catch (_) {} };
window.addEventListener('scroll', save, { passive: true });
window.addEventListener('beforeunload', save);
const restore = () => {
const y = parseInt(sessionStorage.getItem(KEY), 10);
if (!isNaN(y)) {
requestAnimationFrame(() => window.scrollTo(0, y));
setTimeout(() => window.scrollTo(0, y), 60);
}
};
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', restore);
else restore();
const poll = () =>
fetch('/version', { cache: 'no-store' })
.then(r => r.text())
.then(v => { if (v.trim() && v.trim() !== BUILD_ID) { save(); location.reload(); } else setTimeout(poll, 1000); })
.catch(() => setTimeout(poll, 1500));
setTimeout(poll, 1000);
})();
"""
# ── Rendering ─────────────────────────────────────────────────────────────────
def _run_bikeshed(doc, label: str) -> str | None:
"""Preprocess and serialize an already-constructed Bikeshed Spec."""
if not doc.valid:
console.fail(f"bikeshed could not initialize [bold]{e(label)}[/]")
return None
doc.preprocess()
return doc.serialize() or None
def _render_file(path: Path) -> str | None:
"""Render a .bs file from disk via Bikeshed; return HTML or None."""
console.step(f"bikeshed: rendering [bold]{e(path.name)}[/]…")
try:
from bikeshed import Spec
return _run_bikeshed(Spec(str(path)), path.name)
except Exception:
console.fail(traceback.format_exc())
return None
def _render_bytes(content: bytes, label: str) -> str | None:
"""Render raw .bs bytes via Bikeshed stdin; return HTML or None.
Diagnostics are silenced — the baseline's warnings are not actionable
from the working tree.
"""
console.step(f"bikeshed: rendering [bold]{e(label)}[/]…")
try:
from bikeshed import Spec
from bikeshed.messages import messagesSilent
sys.stdin = io.TextIOWrapper(io.BytesIO(content), encoding="utf-8")
with messagesSilent():
return _run_bikeshed(Spec("-"), label)
except Exception:
console.fail(traceback.format_exc())
return None
finally:
sys.stdin = sys.__stdin__
def _load_baseline(ref: str, path: str) -> HtmlElement | None:
"""Fetch path@ref from git and render it; cache the result in _state."""
if _state.baseline is not None:
return _state.baseline
try:
import git
blob = git.Repo(REPO_DIR).commit(ref).tree / path
content = blob.data_stream.read()
except Exception as ex:
console.fail(
f"git: cannot read [bold]{e(path)}[/] at [bold]{e(ref)}[/]: {e(str(ex))}"
)
return None
html = _render_bytes(content, f"{path}@{ref}")
if html is None:
return None
_state.baseline = lhtml.fromstring(html).find(".//body")
return _state.baseline
# ── Build ─────────────────────────────────────────────────────────────────────
def _annotate(
new_html: str, baseline_ref: str, baseline_path: str
) -> lhtml.HtmlElement | None:
"""Diff new_html against the baseline and return the annotated document root."""
baseline = _load_baseline(baseline_ref, baseline_path)
if baseline is None:
return None
new_root = lhtml.fromstring(new_html)
new_body = new_root.find(".//body")
if new_body is None:
console.fail("new document has no <body>")
return None
console.step("diffing…")
# Deep-copy baseline so the cache stays pristine across repeated builds.
_diff_element(copy.deepcopy(baseline), new_body)
head = new_root.find(".//head")
assert head is not None # every Bikeshed document has a <head>
ET.SubElement(head, "style", {"id": "diff-css"}).text = _DIFF_CSS
new_body.insert(0, lhtml.fragment_fromstring(_LEGEND_HTML))
return new_root
def _serialize(root: lhtml.HtmlElement) -> bytes:
return lhtml.tostring(root, encoding="utf-8", doctype="<!DOCTYPE html>")
def _rebuild(
baseline_ref: str, baseline_path: str, src: Path, output: Path | None
) -> None:
"""Render src, diff against baseline, then publish in memory or write to disk."""
new_html = _render_file(src)
if new_html is None:
return
root = _annotate(new_html, baseline_ref, baseline_path)
if root is None:
return
if output is None:
build_id = str(time.time_ns())
body = root.find(".//body")
ET.SubElement(body, "script", {"id": "diff-js"}).text = _RELOAD_JS.replace(
"__BUILD_ID__", build_id
)
diff = _serialize(root)
with _state_lock:
_state.diff = diff
_state.build_id = build_id
console.done(f"diff ready ({len(diff):,} bytes)")
else:
diff = _serialize(root)
output.write_bytes(diff)
url = output.resolve().as_uri()
console.done(
f"[blue][link={url}]{e(str(output))}[/link][/blue] ({len(diff):,} bytes)"
)
def _watch(src: Path, baseline_ref: str, baseline_path: str) -> None:
"""Poll src for mtime changes and trigger a rebuild on each change."""
last_mtime = src.stat().st_mtime
while True:
time.sleep(_POLL_INTERVAL)
try:
mtime = src.stat().st_mtime
except FileNotFoundError:
console.fail(f"[bold]{e(src.name)}[/] disappeared")
return
if mtime == last_mtime:
continue
last_mtime = mtime
console.step(f"[bold]{e(src.name)}[/] changed")
try:
_rebuild(baseline_ref, baseline_path, src, output=None)
except Exception:
console.fail(traceback.format_exc())
# ── HTTP server ───────────────────────────────────────────────────────────────
_BUILDING = "building…".encode()
_MIME: dict[str, str] = {
".css": "text/css",
".js": "text/javascript",
".svg": "image/svg+xml",
".png": "image/png",
}
class _Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, format: str, *args: object) -> None:
pass # suppress per-request console noise
def _respond(self, body: bytes, content_type: str, status: int = 200) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def do_GET(self) -> None:
path = self.path.split("?", 1)[0]
if path == "/version":
with _state_lock:
self._respond(_state.build_id.encode(), "text/plain; charset=utf-8")
return
if path in ("/", "/diff.html"):
with _state_lock:
body = _state.diff
if body is None:
self._respond(_BUILDING, "text/plain; charset=utf-8", 503)
else:
self._respond(body, "text/html; charset=utf-8")
return
# Serve static assets (FileAPI.css, dfn.js, …) from the repo root.
candidate = (REPO_DIR / path.lstrip("/")).resolve()
if candidate.is_file() and candidate.is_relative_to(REPO_DIR):
self._respond(
candidate.read_bytes(),
_MIME.get(candidate.suffix, "application/octet-stream"),
)
else:
self._respond(b"not found", "text/plain", 404)
do_HEAD = do_GET
class _Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
allow_reuse_address = True
# ── CLI ───────────────────────────────────────────────────────────────────────
app = typer.Typer(
add_completion=False,
context_settings={"help_option_names": ["-h", "--help"]},
)
@app.command()
def main(
spec_path: str = typer.Argument(
default="index.bs",
metavar="<path_to_spec.bs>",
help=(
"Path to the .bs file to watch, relative to the repo root. "
"If the single argument given does not end in .bs, it is treated as the base ref."
),
),
base_ref: str = typer.Argument(
default="main",
metavar="<base_ref>",
help="Git ref for the baseline: branch, tag, full or truncated SHA1, or remote ref (e.g. origin/main).",
),
output: Path | None = typer.Option(
None,
"-o",
"--output",
metavar="<output.html>",
help="Write a static diff file and exit (no watching, no live reload).",
),
port: int = typer.Option(
_DEFAULT_PORT,
"-p",
"--port",
metavar="<num>",
help="Port to serve on.",
),
bind: str = typer.Option(
"localhost",
"-b",
"--bind",
metavar="<address>",
help="Address to listen on (hostname, IPv4, or IPv6).",
),
color: str = typer.Option(
"auto",
"--color",
metavar="<always|never|auto>",
help="Color output: always, never, or auto (default).",
),
) -> None:
"""Render a Bikeshed spec, diff it against a git baseline, and serve the result."""
console.configure(color)
# Single non-.bs argument: treat it as the ref, not the path.
if not spec_path.endswith(".bs"):
spec_path, base_ref = "index.bs", spec_path
src = Path(spec_path) if Path(spec_path).is_absolute() else REPO_DIR / spec_path
if not src.exists():
console.fail(f"[bold]{e(str(src))}[/] not found")
raise typer.Exit(1)
console.step(f"[bold]{e(src.name)}[/] vs [bold]{e(spec_path)}@{e(base_ref)}[/]")
try:
_rebuild(base_ref, spec_path, src, output)
except Exception:
console.fail(traceback.format_exc())
if output is not None:
return
threading.Thread(
target=_watch, args=(src, base_ref, spec_path), daemon=True
).start()
server = _Server((bind, port), _Handler)
console.done(
f"serving at [blue][link=http://{bind}:{port}/]http://{bind}:{port}/[/link][/blue]"
)
try:
server.serve_forever()
except KeyboardInterrupt:
console.step("stopping.")
server.shutdown()
if __name__ == "__main__":
# Strip `--` separators that orchestrators (uv run, shell wrappers) may inject.
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a != "--"]
app()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment