Skip to content
Foldcase
On this pagefoldcase test — Showcases as CI

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".

bash
foldcase test              # the current directory
foldcase test src/ui       # a directory
foldcase test button.showcase.ts   # a single file

A *.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:

text
  ✗ src/ui/picker.showcase.ts — Error [ERR_MODULE_NOT_FOUND]: Cannot find module './picker'
  ✓ counter/click-twice

2 total · 1 passed · 1 failed

Extra 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.

bash
foldcase test src/ui --coverage
text
coverage:
  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 collected

Read the limitations before you rely on it:

  • It needs Node on PATH and runs the measurement in a spawned Node subprocess, because Bun exposes no programmatic V8 precise coverage.

  • The collector runs every play twice 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.

bash
foldcase test src/ui --json
foldcase test src/ui --json --coverage    # coverage rides in the same document
json
{
  "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 SerializedErrorname, message, the stack, and code when 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": 0 would 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.

bash
foldcase docs src/ui docs/schemas
foldcase docs                 # output goes to FOLDCASE_DOCS_DIR, default ./foldcase-docs

A 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:

FieldTypeOptional
autosaveAfterDurationno
filter"all" | "open" | "done"no
selectedOption<string>yes
tasksTask[]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:

json
{
  "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_catalog is 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_DIR that cannot be read at launch fails the server itself.

bash
foldcase mcp                                    # point your MCP host's stdio command here
FOLDCASE_SHOWCASE_DIR=src/ui foldcase mcp       # serve a specific directory

foldcase 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