Child of #386, M2. The mechanism; M3 seeds it.
What to build
A local source in src/demos.ts. DemoEntry.source gains "local", with:
path?: string — relative to the directory of the catalog file it came from (record that directory on the entry when loading), or absolute. Optional: an entry with no path and a setup is a generator entry, whose setup renders the estate into the empty target.
inPlace?: boolean — serve path where it is (no copy, no demoTargetDir). Default false: copy to behold-demos/<name> with the bundled filter (node_modules excluded). inPlace with no path is invalid; inPlace with setup is allowed (fountain-ops's just up boots a cluster and writes nothing the repo does not already gitignore) but the entry must say so in its description.
- Validation in
loadDemoRegistry's filter, same posture as today: a malformed entry drops, neighbours survive.
missingRequirements: a local entry whose resolved path does not exist reports it as missing (../choudoufu (not checked out)), so --list, /api/demos's satisfiable and the panel button all read it. fetchesFromNetwork is false for local.
loadDemo: copy or generate or use in place per the above; npm install and setup as today. setup runs with cwd = target and two extra env vars: BEHOLD_WORKBENCH_DIR (the catalog file's directory, so a script can reach ../choudoufu) and BEHOLD_DEMO_NAME.
A second catalog file. loadDemoRegistry(pkgRoot) reads demos.json and then workbench.json beside it, and BEHOLD_WORKBENCH (a file path) after that; each entry carries catalog: "demos" | "workbench". A workbench name that collides with a bundled one is dropped with a stderr line. workbench.json is NOT added to package.json files — it is for a checkout. behold demo --list prints the bundled block, then a workbench (this checkout) block. GET /api/demos carries catalog per entry.
CHOUDOUFU_BIN. choudoufuBinary() in src/choudoufu-member.ts returns process.env.CHOUDOUFU_BIN || "choudoufu"; every spawn of choudoufu (choudoufu-member.ts, choudoufu-live.ts, choudoufu-moves.ts, server.ts, doctor.ts — grep "choudoufu" as a spawn argv) goes through it. missingRequirements treats the requirement choudoufu as satisfied when CHOUDOUFU_BIN names an existing file. The doctor's choudoufu line prints which binary answered.
A recipe. just example name="terralith-4" → npm run dev -- demo {{name}}.
Definition of done
src/demos.test.ts: a local entry validates (path, no path + setup, inPlace); an in-place entry's serveDirs are the path (and dirs under it); a generator entry's target is created empty and setup runs in it; a missing path is a missing requirement; the second catalog merges, collisions drop, BEHOLD_WORKBENCH is honoured; catalog is carried.
src/demos-route.test.ts: /api/demos shows catalog and satisfiable for a local entry.
- A test that
CHOUDOUFU_BIN reaches the spawn argv (choudoufuSpawnEnv has a sibling choudoufuBinary).
just check green. AGENTS.md gets the catalog rules under a new "The workbench catalog" heading (short; the epic's decisions 1–4).
Child of #386, M2. The mechanism; M3 seeds it.
What to build
A
localsource insrc/demos.ts.DemoEntry.sourcegains"local", with:path?: string— relative to the directory of the catalog file it came from (record that directory on the entry when loading), or absolute. Optional: an entry with nopathand asetupis a generator entry, whose setup renders the estate into the empty target.inPlace?: boolean— servepathwhere it is (no copy, nodemoTargetDir). Default false: copy tobehold-demos/<name>with the bundled filter (node_modules excluded).inPlacewith nopathis invalid;inPlacewithsetupis allowed (fountain-ops'sjust upboots a cluster and writes nothing the repo does not already gitignore) but the entry must say so in its description.loadDemoRegistry's filter, same posture as today: a malformed entry drops, neighbours survive.missingRequirements: alocalentry whose resolvedpathdoes not exist reports it as missing (../choudoufu (not checked out)), so--list,/api/demos'ssatisfiableand the panel button all read it.fetchesFromNetworkis false for local.loadDemo: copy or generate or use in place per the above;npm installandsetupas today.setupruns with cwd = target and two extra env vars:BEHOLD_WORKBENCH_DIR(the catalog file's directory, so a script can reach../choudoufu) andBEHOLD_DEMO_NAME.A second catalog file.
loadDemoRegistry(pkgRoot)readsdemos.jsonand thenworkbench.jsonbeside it, andBEHOLD_WORKBENCH(a file path) after that; each entry carriescatalog: "demos" | "workbench". A workbench name that collides with a bundled one is dropped with a stderr line.workbench.jsonis NOT added to package.jsonfiles— it is for a checkout.behold demo --listprints the bundled block, then aworkbench (this checkout)block.GET /api/demoscarriescatalogper entry.CHOUDOUFU_BIN.choudoufuBinary()in src/choudoufu-member.ts returnsprocess.env.CHOUDOUFU_BIN || "choudoufu"; every spawn of choudoufu (choudoufu-member.ts, choudoufu-live.ts, choudoufu-moves.ts, server.ts, doctor.ts — grep"choudoufu"as a spawn argv) goes through it.missingRequirementstreats the requirementchoudoufuas satisfied whenCHOUDOUFU_BINnames an existing file. The doctor's choudoufu line prints which binary answered.A recipe.
just example name="terralith-4"→npm run dev -- demo {{name}}.Definition of done
src/demos.test.ts: a local entry validates (path, no path + setup, inPlace); an in-place entry's serveDirs are the path (anddirsunder it); a generator entry's target is created empty and setup runs in it; a missing path is a missing requirement; the second catalog merges, collisions drop,BEHOLD_WORKBENCHis honoured;catalogis carried.src/demos-route.test.ts:/api/demosshowscatalogandsatisfiablefor a local entry.CHOUDOUFU_BINreaches the spawn argv (choudoufuSpawnEnvhas a siblingchoudoufuBinary).just checkgreen. AGENTS.md gets the catalog rules under a new "The workbench catalog" heading (short; the epic's decisions 1–4).