demolab-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- demolab_cli/CHANGELOG.md +445 -0
- demolab_cli/VERSION +1 -0
- demolab_cli/__init__.py +10 -0
- demolab_cli/_paths.py +63 -0
- demolab_cli/build.py +284 -0
- demolab_cli/cli.py +414 -0
- demolab_cli/deploy/deploy.yml +61 -0
- demolab_cli/deploy/preview.yml +56 -0
- demolab_cli/devserver.py +380 -0
- demolab_cli/guides/AUTORESEARCH-RULES.md +149 -0
- demolab_cli/guides/GLOSSARY.md +55 -0
- demolab_cli/guides/HOUSESTYLE.md +108 -0
- demolab_cli/guides/RULES.md +142 -0
- demolab_cli/guides/SLIDES.md +145 -0
- demolab_cli/guides/STRUCTURE.md +78 -0
- demolab_cli/guides/SUPPORT.md +36 -0
- demolab_cli/overlay.py +47 -0
- demolab_cli/runbooks/AUTORESEARCH.md +67 -0
- demolab_cli/runbooks/DOCTOR.md +116 -0
- demolab_cli/runbooks/EMBED-DOCS.md +39 -0
- demolab_cli/runbooks/FROM-JUPYTER.md +84 -0
- demolab_cli/runbooks/FROM-PAPER.md +48 -0
- demolab_cli/runbooks/GETTING-STARTED.md +281 -0
- demolab_cli/runbooks/GROUND-CLAIMS.md +166 -0
- demolab_cli/runbooks/LINT.md +176 -0
- demolab_cli/runbooks/MIGRATE-CODE.md +49 -0
- demolab_cli/runbooks/MIGRATE-STACK.md +50 -0
- demolab_cli/runbooks/NEXT.md +52 -0
- demolab_cli/runbooks/NIGHT-SHIFT.md +68 -0
- demolab_cli/runbooks/PLAN.md +64 -0
- demolab_cli/runbooks/RED-TEAM.md +72 -0
- demolab_cli/runbooks/STEELMAN.md +44 -0
- demolab_cli/runbooks/TOUR.md +42 -0
- demolab_cli/runbooks/UPDATE.md +84 -0
- demolab_cli/scaffold/demo/artifacts/data/ar018/swe-bench.svg +1 -0
- demolab_cli/scaffold/demo/artifacts/data/exp000/lif.svg +1103 -0
- demolab_cli/scaffold/demo/artifacts/data/exp000/net.png +0 -0
- demolab_cli/scaffold/demo/artifacts/data/exp000/numbers.json +38 -0
- demolab_cli/scaffold/demo/artifacts/data/exp000/run.sh +3 -0
- demolab_cli/scaffold/demo/artifacts/data/exp001/eif.svg +1122 -0
- demolab_cli/scaffold/demo/artifacts/data/exp001/enet.png +0 -0
- demolab_cli/scaffold/demo/artifacts/data/exp001/numbers.json +40 -0
- demolab_cli/scaffold/demo/artifacts/data/exp001/run.sh +3 -0
- demolab_cli/scaffold/demo/artifacts/data/exp002/cartpole.mp4 +0 -0
- demolab_cli/scaffold/demo/artifacts/data/exp002/numbers.json +18 -0
- demolab_cli/scaffold/demo/artifacts/data/exp002/run.sh +3 -0
- demolab_cli/scaffold/demo/artifacts/data/exp003/double_pendulum.mp4 +0 -0
- demolab_cli/scaffold/demo/artifacts/data/exp003/numbers.json +21 -0
- demolab_cli/scaffold/demo/artifacts/data/exp003/run.sh +3 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar000.pdf +546 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar003.pdf +773 -1
- demolab_cli/scaffold/demo/artifacts/pdfs/ar004.pdf +2846 -2
- demolab_cli/scaffold/demo/artifacts/pdfs/ar005.pdf +4216 -5
- demolab_cli/scaffold/demo/artifacts/pdfs/ar006.pdf +1200 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar010.pdf +718 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar011.pdf +530 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar012.pdf +634 -1
- demolab_cli/scaffold/demo/artifacts/pdfs/ar013.pdf +575 -1
- demolab_cli/scaffold/demo/artifacts/pdfs/ar014.pdf +0 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar015.pdf +0 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar016.pdf +520 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/ar017.pdf +875 -1
- demolab_cli/scaffold/demo/artifacts/pdfs/ar018.pdf +871 -1
- demolab_cli/scaffold/demo/artifacts/pdfs/book.pdf +10343 -6
- demolab_cli/scaffold/demo/artifacts/pdfs/exp000.pdf +2036 -4
- demolab_cli/scaffold/demo/artifacts/pdfs/exp001.pdf +2226 -2
- demolab_cli/scaffold/demo/artifacts/pdfs/exp002.pdf +683 -0
- demolab_cli/scaffold/demo/artifacts/pdfs/exp003.pdf +767 -0
- demolab_cli/scaffold/demo/demolab.yaml +26 -0
- demolab_cli/scaffold/demo/experiments/exp000.py +106 -0
- demolab_cli/scaffold/demo/experiments/exp001.py +106 -0
- demolab_cli/scaffold/demo/experiments/exp002.py +74 -0
- demolab_cli/scaffold/demo/experiments/exp003.py +73 -0
- demolab_cli/scaffold/demo/experiments/playground.py +157 -0
- demolab_cli/scaffold/demo/site/CNAME +1 -0
- demolab_cli/scaffold/demo/site/landing.typ +38 -0
- demolab_cli/scaffold/demo/tools/mujoco/test_mujoco.py +42 -0
- demolab_cli/scaffold/demo/tools/mujoco/tool.py +393 -0
- demolab_cli/scaffold/demo/tools/neuron/test_neuron.py +106 -0
- demolab_cli/scaffold/demo/tools/neuron/tool.py +526 -0
- demolab_cli/scaffold/demo/writings/ar000.typ +86 -0
- demolab_cli/scaffold/demo/writings/ar003.typ +82 -0
- demolab_cli/scaffold/demo/writings/ar004.slide.typ +418 -0
- demolab_cli/scaffold/demo/writings/ar005.slide.typ +294 -0
- demolab_cli/scaffold/demo/writings/ar006.typ +148 -0
- demolab_cli/scaffold/demo/writings/ar010.typ +54 -0
- demolab_cli/scaffold/demo/writings/ar011.typ +32 -0
- demolab_cli/scaffold/demo/writings/ar012.typ +52 -0
- demolab_cli/scaffold/demo/writings/ar013.typ +47 -0
- demolab_cli/scaffold/demo/writings/ar014.typ +56 -0
- demolab_cli/scaffold/demo/writings/ar015.typ +33 -0
- demolab_cli/scaffold/demo/writings/ar016.typ +29 -0
- demolab_cli/scaffold/demo/writings/ar017.typ +98 -0
- demolab_cli/scaffold/demo/writings/ar018.typ +128 -0
- demolab_cli/scaffold/demo/writings/exp000.typ +77 -0
- demolab_cli/scaffold/demo/writings/exp001.typ +81 -0
- demolab_cli/scaffold/demo/writings/exp002.typ +47 -0
- demolab_cli/scaffold/demo/writings/exp003.typ +61 -0
- demolab_cli/scaffold/demo-manifest.json +54 -0
- demolab_cli/scaffold/root/AGENTS.md +54 -0
- demolab_cli/scaffold/root/CLAUDE.md +18 -0
- demolab_cli/scaffold/root/README.md +28 -0
- demolab_cli/scaffold/root/github/workflows/tests.yml +21 -0
- demolab_cli/scaffold/root/gitignore +30 -0
- demolab_cli/scaffold/root/pyproject.toml +32 -0
- demolab_cli/scaffold/skeleton/HOUSESTYLE.local.md +25 -0
- demolab_cli/scaffold/skeleton/artifacts/data/.gitkeep +0 -0
- demolab_cli/scaffold/skeleton/artifacts/pdfs/.gitkeep +0 -0
- demolab_cli/scaffold/skeleton/demolab.yaml +40 -0
- demolab_cli/scaffold/skeleton/experiments/helpers/__init__.py +1 -0
- demolab_cli/scaffold/skeleton/experiments/helpers/provenance.py +78 -0
- demolab_cli/scaffold/skeleton/experiments/helpers/style.py +39 -0
- demolab_cli/scaffold/skeleton/experiments/helpers/test_provenance.py +37 -0
- demolab_cli/scaffold/skeleton/tools/.gitkeep +0 -0
- demolab_cli/scaffold/skeleton/writings/.gitkeep +0 -0
- demolab_cli/scaffold/starters/monte-carlo-pi/README.md +41 -0
- demolab_cli/scaffold/starters/monte-carlo-pi/exp000.py +110 -0
- demolab_cli/scaffold/starters/monte-carlo-pi/exp000.typ +63 -0
- demolab_cli/slides.py +42 -0
- demolab_cli/typ/cite-popover.js +38 -0
- demolab_cli/typ/favicon.svg +8 -0
- demolab_cli/typ/lib.typ +518 -0
- demolab_cli/typ/main.typ +92 -0
- demolab_cli/typ/style.css +263 -0
- demolab_cli-1.0.0.dist-info/METADATA +101 -0
- demolab_cli-1.0.0.dist-info/RECORD +129 -0
- demolab_cli-1.0.0.dist-info/WHEEL +4 -0
- demolab_cli-1.0.0.dist-info/entry_points.txt +3 -0
- demolab_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
demolab_cli/build.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Discover writings/*.typ, write a JSON manifest, and compile all three targets.
|
|
2
|
+
|
|
3
|
+
build.py does only what Typst can't: it globs the filesystem (Typst has no directory
|
|
4
|
+
listing) and orchestrates the compiler. It stages the engine's Typst surface into the lab
|
|
5
|
+
(.demolab/ + temp/bundle/main.typ — typst --root confines reads to the lab tree, and the
|
|
6
|
+
engine lives in site-packages), writes the discovered id/asset lists to
|
|
7
|
+
temp/bundle/index.json, and compiles; the static typ/main.typ reads that manifest and does
|
|
8
|
+
the rest (imports, documents, assets) in plain Typst — there is no generated source.
|
|
9
|
+
|
|
10
|
+
One `typst compile --format bundle --features bundle,html temp/bundle/main.typ` emits,
|
|
11
|
+
into artifacts/site/:
|
|
12
|
+
index.html — homepage index of experiments + articles
|
|
13
|
+
<id>.html — per-entry web page (figures inline, video plays)
|
|
14
|
+
<id>.mp4 — video assets
|
|
15
|
+
pdfs/<id>.pdf — per-entry individual PDF
|
|
16
|
+
pdfs/book.pdf — every entry concatenated into one PDF (book mode)
|
|
17
|
+
|
|
18
|
+
The site (artifacts/site/) is a self-contained build output (gitignored, deployed to
|
|
19
|
+
Pages). The PDFs are also mirrored to the committed artifacts/pdfs/ as shareable
|
|
20
|
+
deliverables.
|
|
21
|
+
|
|
22
|
+
Each writings/<id>.typ exposes `#let meta = (...)` and `#let body = [...]`.
|
|
23
|
+
Entries not yet in that convention are skipped (incremental migration).
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import shutil
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from demolab_cli import _paths
|
|
36
|
+
|
|
37
|
+
# The lab being built. Normally found by walking up from the cwd for demolab.yaml;
|
|
38
|
+
# DEMOLAB_ROOT overrides it (the test fixtures and `demolab dev --demo` build materialised
|
|
39
|
+
# labs in scratch dirs). Falls back to the cwd so an empty dir still gets the friendly
|
|
40
|
+
# empty-state build rather than an import-time error.
|
|
41
|
+
ROOT = Path(os.environ.get("DEMOLAB_ROOT") or _paths.find_lab_root() or Path.cwd()).resolve()
|
|
42
|
+
WRITINGS = ROOT / "writings"
|
|
43
|
+
BUILD = ROOT / "temp" / "bundle" # scratch: staged main.typ + manifest + deck PDFs
|
|
44
|
+
MAIN = BUILD / "main.typ" # staged copy of the packaged typ/main.typ
|
|
45
|
+
MANIFEST = BUILD / "index.json" # scratch: id/asset lists main.typ reads
|
|
46
|
+
DECKS = BUILD / "decks" # scratch: compiled deck PDFs, embedded as assets
|
|
47
|
+
SITE = ROOT / "artifacts" / "site" # bundle output (HTML + mp4 + pdfs/), gitignored
|
|
48
|
+
PDFS = ROOT / "artifacts" / "pdfs" # committed copy of the PDFs (shareable)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _find_typst() -> str:
|
|
52
|
+
"""The typst CLI — needs --features bundle,html (experimental). A lab-local install wins
|
|
53
|
+
(.tools/bin — the no-package-manager fallback for locked-down machines), then PATH;
|
|
54
|
+
shutil.which resolves typst.exe on Windows, where a bare name can fail."""
|
|
55
|
+
for name in ("typst.exe", "typst"):
|
|
56
|
+
local = ROOT / ".tools" / "bin" / name
|
|
57
|
+
if local.exists():
|
|
58
|
+
return str(local)
|
|
59
|
+
return shutil.which("typst") or "typst"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
TYPST = _find_typst()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def stage() -> None:
|
|
66
|
+
"""Materialise everything Typst reads from inside the lab: .demolab/ (lib + web assets,
|
|
67
|
+
version-stamped) and the bundle root main.typ (copied fresh every build — it's tiny, and
|
|
68
|
+
a stale copy after an engine upgrade would be a subtle bug)."""
|
|
69
|
+
_paths.stage(ROOT)
|
|
70
|
+
BUILD.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
shutil.copy2(_paths.TYP / "main.typ", MAIN)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def discover():
|
|
75
|
+
"""Entry ids (exp*/ar*) that follow the meta+body convention, sorted.
|
|
76
|
+
|
|
77
|
+
Match real top-level definitions (`#let meta` / `#let body` at line start), not
|
|
78
|
+
prose or comments that merely mention them. Slide decks (`*.slide.typ`) are a
|
|
79
|
+
separate category — see discover_decks — so they're skipped here."""
|
|
80
|
+
ids = []
|
|
81
|
+
for p in sorted(WRITINGS.glob("*.typ")):
|
|
82
|
+
if p.name.endswith(".slide.typ"):
|
|
83
|
+
continue
|
|
84
|
+
lines = p.read_text().splitlines()
|
|
85
|
+
has_meta = any(ln.startswith("#let meta") for ln in lines)
|
|
86
|
+
has_body = any(ln.startswith("#let body") for ln in lines)
|
|
87
|
+
if has_meta and has_body:
|
|
88
|
+
ids.append(p.stem)
|
|
89
|
+
return ids
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def discover_decks():
|
|
93
|
+
"""Deck ids from `writings/<id>.slide.typ` — standalone touying slide decks, sorted.
|
|
94
|
+
|
|
95
|
+
Touying decks are paged-only (they don't survive HTML export, see the deck header
|
|
96
|
+
comment), so they aren't bundle entries. Instead they're compiled to standalone PDFs
|
|
97
|
+
and linked from the homepage. Each deck declares `#let meta` (title/date) but no
|
|
98
|
+
`#let body`; the meta is imported to label the link."""
|
|
99
|
+
return [p.name.removesuffix(".slide.typ") for p in sorted(WRITINGS.glob("*.slide.typ"))]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_manifest(ids: list[str], deck_ids: list[str], broken: dict | None = None) -> None:
|
|
103
|
+
"""Write temp/bundle/index.json — the id/asset lists the staged main.typ reads.
|
|
104
|
+
|
|
105
|
+
This is the only place per-entry knowledge is assembled, and it's pure data (no Typst
|
|
106
|
+
source): the entry ids + kind, each entry's mp4 filenames (globbed here because Typst
|
|
107
|
+
can't list a directory), and the deck ids. An entry in `broken` (id -> error text) carries an
|
|
108
|
+
`error` field and loads no assets — main.typ renders it as a stub instead of importing it."""
|
|
109
|
+
broken = broken or {}
|
|
110
|
+
entries = []
|
|
111
|
+
for i in ids:
|
|
112
|
+
entry = {
|
|
113
|
+
"id": i,
|
|
114
|
+
"kind": "experiment" if i.startswith("exp") else "article",
|
|
115
|
+
"videos": [] if i in broken else [v.name for v in sorted((ROOT / "artifacts" / "data" / i).glob("*.mp4"))],
|
|
116
|
+
}
|
|
117
|
+
if i in broken:
|
|
118
|
+
entry["error"] = broken[i]
|
|
119
|
+
entries.append(entry)
|
|
120
|
+
# Signal whether the optional root demolab.yaml / landing.typ exist — Typst can't stat
|
|
121
|
+
# files, so main.typ only reads them (branding / the custom landing page) when these
|
|
122
|
+
# flags say they're there.
|
|
123
|
+
manifest = {
|
|
124
|
+
"entries": entries,
|
|
125
|
+
"decks": [{"id": d} for d in deck_ids],
|
|
126
|
+
"has_brand_config": (ROOT / "demolab.yaml").exists(),
|
|
127
|
+
"has_landing": (ROOT / "landing.typ").exists(),
|
|
128
|
+
}
|
|
129
|
+
MANIFEST.write_text(json.dumps(manifest, indent=2) + "\n")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compile_decks(deck_ids: list[str]) -> list[str]:
|
|
133
|
+
"""Compile each standalone deck to a scratch PDF (temp/bundle/decks/<id>.pdf); return the ones
|
|
134
|
+
that built. A deck that fails to compile is skipped with a warning rather than failing the whole
|
|
135
|
+
build (main.typ only embeds decks that produced a PDF).
|
|
136
|
+
|
|
137
|
+
main.typ embeds these as bundle assets at pdfs/<id>.pdf. Must run before the bundle compile so
|
|
138
|
+
the asset `read(...)` finds the files. The dev server (devserver.py) reruns this on every
|
|
139
|
+
change, so deck edits and new decks live-reload like any entry."""
|
|
140
|
+
DECKS.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
good = []
|
|
142
|
+
for d in deck_ids:
|
|
143
|
+
proc = subprocess.run(
|
|
144
|
+
[TYPST, "compile", "--root", str(ROOT),
|
|
145
|
+
str(WRITINGS / f"{d}.slide.typ"), str(DECKS / f"{d}.pdf")],
|
|
146
|
+
capture_output=True, text=True,
|
|
147
|
+
)
|
|
148
|
+
if proc.returncode == 0:
|
|
149
|
+
good.append(d)
|
|
150
|
+
else:
|
|
151
|
+
print(f" ⚠ deck {d} failed to build — skipping it: "
|
|
152
|
+
+ _error_excerpt(proc.stdout + proc.stderr).splitlines()[0], flush=True)
|
|
153
|
+
return good
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _entry_from_error(err: str, candidates: set) -> str | None:
|
|
157
|
+
"""Which entry did a bundle-compile error come from? Parsed from a `/writings/<id>.typ` mention
|
|
158
|
+
in the message (only ids we can still drop)."""
|
|
159
|
+
for m in re.finditer(r"/writings/([A-Za-z0-9_-]+)\.typ", err):
|
|
160
|
+
if m.group(1) in candidates:
|
|
161
|
+
return m.group(1)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _error_excerpt(err: str, lines: int = 8) -> str:
|
|
166
|
+
"""The first `error:` block from Typst's output, for the stub page and the warning."""
|
|
167
|
+
rows = err.splitlines()
|
|
168
|
+
for i, row in enumerate(rows):
|
|
169
|
+
if row.lstrip().startswith("error:"):
|
|
170
|
+
return "\n".join(rows[i:i + lines]).strip()
|
|
171
|
+
return err.strip() or "build failed"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def compile_bundle(ids: list[str], deck_ids: list[str]) -> dict:
|
|
175
|
+
"""Compile the whole bundle. If an entry fails (a missing figure, a Typst error), flag it and
|
|
176
|
+
retry, so it renders as a stub page instead of taking the rest of the site down with it. Returns
|
|
177
|
+
the {id: error} map of entries that were stubbed."""
|
|
178
|
+
broken: dict = {}
|
|
179
|
+
while True:
|
|
180
|
+
write_manifest(ids, deck_ids, broken=broken)
|
|
181
|
+
proc = subprocess.run(
|
|
182
|
+
[TYPST, "compile", "--format", "bundle", "--features", "bundle,html",
|
|
183
|
+
"--root", str(ROOT), str(MAIN), str(SITE) + "/"],
|
|
184
|
+
capture_output=True, text=True,
|
|
185
|
+
)
|
|
186
|
+
if proc.returncode == 0:
|
|
187
|
+
return broken
|
|
188
|
+
err = proc.stdout + proc.stderr
|
|
189
|
+
bad = _entry_from_error(err, set(ids) - broken.keys())
|
|
190
|
+
if bad is None:
|
|
191
|
+
# Not attributable to one entry (an engine, asset, or deck error): surface the real
|
|
192
|
+
# failure rather than looping.
|
|
193
|
+
sys.stderr.write(err)
|
|
194
|
+
raise subprocess.CalledProcessError(proc.returncode, proc.args, proc.stdout, proc.stderr)
|
|
195
|
+
broken[bad] = _error_excerpt(err)
|
|
196
|
+
print(f" ⚠ {bad} failed to build — stubbing it, keeping the rest: "
|
|
197
|
+
+ broken[bad].splitlines()[0], flush=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _warn_if_content_misplaced(ids: list[str]) -> None:
|
|
201
|
+
"""Tripwire for the classic agent slip: creating experiments/ or writings/ one level ABOVE
|
|
202
|
+
the lab — a patch applied from the workspace root instead of the lab root. build.py globs
|
|
203
|
+
from the lab root (found by its demolab.yaml marker, not the bare cwd), so misplaced files are
|
|
204
|
+
simply invisible: the build quietly renders the empty-state homepage and gives no signal that
|
|
205
|
+
the work landed in the wrong place. So when the root has no content, peek at the parent dir; if
|
|
206
|
+
the stray dirs are sitting there with real files, say so loudly and point at the fix.
|
|
207
|
+
|
|
208
|
+
A warning, not an error: an empty scaffold is a valid state, and a nested/monorepo layout could
|
|
209
|
+
put an unrelated experiments/ in the parent — we don't want a false positive to block a build.
|
|
210
|
+
ASCII only: this output is often captured by an agent harness on Windows (CP1252), where a
|
|
211
|
+
stray non-ASCII byte would crash the pipe (see devserver.py)."""
|
|
212
|
+
# A populated lab has writings (ids) or a top-level experiment runner. helpers/*.py in a bare
|
|
213
|
+
# scaffold live in a subdir, so a non-recursive experiments/*.py glob stays empty until there's
|
|
214
|
+
# a real runner — no false "has content" on a fresh scaffold.
|
|
215
|
+
if ids or any((ROOT / "experiments").glob("*.py")):
|
|
216
|
+
return
|
|
217
|
+
stray = [d for d, pat in ((ROOT.parent / "writings", "*.typ"),
|
|
218
|
+
(ROOT.parent / "experiments", "*.py"))
|
|
219
|
+
if d.is_dir() and any(d.glob(pat))]
|
|
220
|
+
if not stray:
|
|
221
|
+
return
|
|
222
|
+
print("WARNING: this lab has no content at its root, but found populated content OUTSIDE the",
|
|
223
|
+
file=sys.stderr)
|
|
224
|
+
print(f" lab root ({ROOT}) — the build cannot see these:", file=sys.stderr)
|
|
225
|
+
for d in stray:
|
|
226
|
+
print(f" {d}", file=sys.stderr)
|
|
227
|
+
print(" This usually means files were created from the workspace root instead of the",
|
|
228
|
+
file=sys.stderr)
|
|
229
|
+
print(f" lab root. Move them into {ROOT} (writings/, experiments/) and rebuild.",
|
|
230
|
+
file=sys.stderr)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main() -> None:
|
|
234
|
+
# --generate-only writes the manifest + deck PDFs without compiling the bundle: a hand
|
|
235
|
+
# tool for inspecting what the compiler will see. (Dev serving is devserver.py, which runs
|
|
236
|
+
# a full build on each change; it doesn't use this flag.)
|
|
237
|
+
generate_only = "--generate-only" in sys.argv
|
|
238
|
+
# --skip-decks reuses the deck PDFs already in temp/bundle/decks/ instead of recompiling
|
|
239
|
+
# them. The dev server passes it when a change touched no deck source or data asset, so a
|
|
240
|
+
# prose/CSS/lib edit doesn't pay for deck compilation it can't have affected. Safe only when
|
|
241
|
+
# those PDFs exist (a full build ran first); a bare `demolab build` never skips.
|
|
242
|
+
skip_decks = "--skip-decks" in sys.argv
|
|
243
|
+
stage()
|
|
244
|
+
ids = discover()
|
|
245
|
+
_warn_if_content_misplaced(ids)
|
|
246
|
+
deck_ids = discover_decks()
|
|
247
|
+
# Zero writings is a valid state (a freshly `demolab scaffold`-ed repo): main.typ renders
|
|
248
|
+
# a friendly empty-state homepage, so we build rather than error.
|
|
249
|
+
# Compile decks first so their PDFs exist for the asset embeds in main.typ (skip reuses the
|
|
250
|
+
# PDFs already on disk). Either way, only decks that actually have a PDF are referenced.
|
|
251
|
+
if skip_decks:
|
|
252
|
+
good_decks = [d for d in deck_ids if (DECKS / f"{d}.pdf").exists()]
|
|
253
|
+
else:
|
|
254
|
+
good_decks = compile_decks(deck_ids)
|
|
255
|
+
write_manifest(ids, good_decks)
|
|
256
|
+
SITE.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
if generate_only:
|
|
258
|
+
print(f"wrote manifest for {len(ids)} entries: {', '.join(ids)}"
|
|
259
|
+
+ (f" + {len(good_decks)} decks: {', '.join(good_decks)}" if good_decks else ""))
|
|
260
|
+
return
|
|
261
|
+
# One bad entry (a missing figure, a Typst error) becomes a stub page instead of failing the
|
|
262
|
+
# whole site — compile_bundle flags it and retries.
|
|
263
|
+
broken = compile_bundle(ids, good_decks)
|
|
264
|
+
good = [i for i in ids if i not in broken]
|
|
265
|
+
# mirror the compiled PDFs (entries, book, and decks) to the committed artifacts/pdfs/
|
|
266
|
+
PDFS.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
for pdf in sorted((SITE / "pdfs").glob("*.pdf")):
|
|
268
|
+
shutil.copy(pdf, PDFS / pdf.name)
|
|
269
|
+
# The verbose detail (which ids built / stubbed, where the PDFs mirror) goes on its own line;
|
|
270
|
+
# the CONCISE summary is printed LAST, because the dev-server watch loop echoes only build.py's
|
|
271
|
+
# final stdout line on each rebuild. So a `demolab dev` session shows a terse one-liner, while a
|
|
272
|
+
# one-shot `demolab build` still prints the full id list above it.
|
|
273
|
+
print(f" entries: {', '.join(good)}"
|
|
274
|
+
+ (f" · decks: {', '.join(good_decks)}" if good_decks else "")
|
|
275
|
+
+ (f" · ⚠ stubbed: {', '.join(sorted(broken))}" if broken else "")
|
|
276
|
+
+ f" · pdfs -> {PDFS.relative_to(ROOT)}/")
|
|
277
|
+
summary = f"built {len(good)} entries" + (f" + {len(good_decks)} decks" if good_decks else "")
|
|
278
|
+
if broken:
|
|
279
|
+
summary += f", {len(broken)} stubbed"
|
|
280
|
+
print(f"{summary} -> {SITE.relative_to(ROOT)}/", flush=True)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
sys.exit(main())
|