The four commands
Finds every .showcase.ts under a path, runs each play, and prints a pass/fail line per Showcase plus a rolled-up count. The exit code is 1 if anything failed...
foldcase test — Showcases as CI
Finds every *.showcase.ts under a path, runs each play, and prints a pass/fail line
per Showcase plus a rolled-up count. The exit code is 1 if anything failed, and also 1 if
nothing was found — an empty run must never read as "everything passed".
foldcase test # the current directory
foldcase test src/ui # a directory
foldcase test button.showcase.ts # a single fileA *.showcase.ts that will not import — a bad path, a missing dependency, a
type imported as a value — is reported as a
failed entry for that file, and the rest of the run continues:
✗ src/ui/picker.showcase.ts — Error [ERR_MODULE_NOT_FOUND]: Cannot find module './picker'
✓ counter/click-twice
2 total · 1 passed · 1 failedExtra positionals and unknown flags are refused, not ignored: foldcase test a.ts b.ts
and foldcase test src --covrage both name what was not understood and exit 1, because
running half of what you asked for behind a green exit would be a lie — and for an agent,
a typo that silently no-ops is the worst failure mode.
--coverage
Adds a V8 line and function coverage summary of the code each play actually executed —
per Showcase and in aggregate.
foldcase test src/ui --coveragecoverage:
src/ui/Button.showcase.ts lines 42/48 (88%) fns 6/7 (86%)
not measured:
src/ui/Picker.showcase.ts — Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import …
1 file(s) · lines 42/48 (88%) · fns 6/7 (86%) · 1 not measured
by showcase:
✓ button/default lines 30/48 (63%)
✓ button/disabled lines 24/48 (50%)
? picker/open no coverage collectedRead the limitations before you rely on it:
It needs Node on
PATHand runs the measurement in a spawned Node subprocess, because Bun exposes no programmatic V8 precise coverage.The collector runs every
playtwice more — an aggregate pass and a per-Showcase pass — on top of the suite run. Keep plays pure and fast.The subprocess resolves modules the way Node does, which is stricter than Bun. A file Bun imports happily can fail there — a directory import is the common one. Such a file is listed under
not measured:with the reason, and counted on the summary line, so a truncated measurement never reads as a whole one.It is additive: it never changes the run's pass/fail exit code, and a collection failure is a warning rather than an error.
--json
Prints the run as one JSON document instead of the summary, for a script or an agent that has to act on it rather than read it.
foldcase test src/ui --json
foldcase test src/ui --json --coverage # coverage rides in the same document{
"suite": {
"total": 2, "passed": 1, "failed": 1,
"reports": [
{ "id": "counter/click-twice", "status": "passed",
"file": "/abs/src/ui/counter.showcase.ts" },
{ "id": "button/disabled", "status": "failed",
"file": "/abs/src/ui/button.showcase.ts",
"error": { "name": "AssertionError", "message": "expected true, got false" } }
]
}
}Every report names its file. An id alone does not say what to open, and the loader — not the author — holds that fact, so it is carried in the report rather than declared on the
Showcase.A failure is a full
SerializedError—name,message, thestack, andcodewhen the runtime set one — not the two-field sketch above.stdout is the document and nothing else. Logs and warnings go to stderr.
The exit codes do not move: 1 on any failure, 1 when nothing was discovered — and a run that discovered nothing prints no document at all, because
"failed": 0would read as a clean run.
foldcase docs — Model and Message tables
Introspects each component's message and model Schemas and writes one Markdown file
per component.
foldcase docs src/ui docs/schemas
foldcase docs # output goes to FOLDCASE_DOCS_DIR, default ./foldcase-docsA component is the set of Showcases sharing an id namespace — everything before the
last /. Five Showcases under ui/picker/* produce one ui-picker.md,
button/starts-unclicked and button/counts-one-click are the button component, and an
id with no / is its own. This is the same notion of a component foldcase_run_catalog
filters on with an id_prefix.
Each table is read from the first Showcase, in id order, that declares that Schema, so a namespace holding both plain logic Showcases and schema-carrying ones still documents. A component whose Showcases declare neither Schema has nothing to table and gets no file.
The Message table is Message | Field | Type | Optional, one row per tag and payload
field; the Model table is Field | Type | Optional. Types describe the value your Model
holds, not the JSON it serializes to — from
examples/counter:
| Field | Type | Optional |
|---|---|---|
autosaveAfter | Duration | no |
filter | "all" | "open" | "done" | no |
selected | Option<string> | yes |
tasks | Task[] | no |
A Schema.DurationFromMillis field reads Duration, not number; Schema.Option(T)
reads Option<T> with Optional yes; a named class resolves to its definition name; a
| inside a type is escaped so the table survives it. Output is sorted, so regenerating
gives a clean diff.
A Showcase may declare dispatches — the Message tags its play sends. Once any Showcase
of a component declares them, the page adds a Not showcased: line naming the union tags
no play dispatches: the next Showcase to write. A declared tag the union does not carry
is a lie in the catalog — it is named on stderr and the command exits 1. A component
where no Showcase declares stays silent, because unknown must never read as covered.
A file that will not load is named on stderr and the command exits non-zero; the documents it could write are still written. With every file loaded, the exit is 0 — including when nothing was written because no Showcase declares a Schema.
--json works here too, and says what was written and what would not load:
{
"docs": [{ "component": "counter", "path": "/abs/docs/schemas/counter.md" }],
"failures": [{ "_tag": "foldcase/ShowcaseModuleError",
"path": "/abs/src/ui/picker.showcase.ts",
"reason": "Cannot find module './picker'" }],
"gaps": [{ "component": "counter",
"undispatched": ["ClickedReset"], "unknown": [] }]
}--check
Compares instead of writing — nothing is created, nothing is touched. A document that
would change or is missing is listed with its reason, and the exit is 1 on any drift, any
load failure, or any unknown dispatch; 0 when everything is current. In --json the same
appears as stale: [{ component, path, reason: "missing" | "changed" }]. Put it in CI
beside foldcase test, so the tables cannot drift from the catalog.
foldcase mcp — the catalog server
The wiring and the six tools are above. Server semantics worth knowing:
The whole toolkit is registered before the server reads a byte of stdin, so a host that discovers its tools once at startup gets all six from its first
tools/list.The server serves one catalog at a time, and
foldcase_load_catalogis the only thing that moves it. A directory parameter on every verb would make an id mean nothing on its own, so the directory is state, and every listing and load report says which one is being served. A load that fails leaves the last good catalog in place.The run tools spawn a fresh subprocess of the server's own runtime and load from disk, so they always run current code. The child reuses the same single loader, and a spawn failure is folded into the report as a failed entry, never a silent pass.
A showcase file that will not load is logged to stderr and the rest of the catalog is served anyway. Only a
FOLDCASE_SHOWCASE_DIRthat cannot be read at launch fails the server itself.
foldcase mcp # point your MCP host's stdio command here
FOLDCASE_SHOWCASE_DIR=src/ui foldcase mcp # serve a specific directoryfoldcase init — wire a consumer repo
Writes the .mcp.json server entry and the AGENTS.md section shown in
Wire it into your agent, idempotently, and reports one line
per artifact — foldcase init: .mcp.json created|updated|kept. --json prints the same
outcome as one InitDocument. An .mcp.json that will not parse is refused and left
untouched.
Last updated Aug 4, 2026