Skip to content

feat: declarative .demo DSL (vhs-for-browsers) #2

Description

@sree-sanak

Why

charmbracelet/vhs (19.5k stars) became a verb because of its .tape DSL — a plain-text format anyone can read and edit. No browser equivalent exists. Every competitor in this space (Screenwright, demosmith-mcp, demo-recorder) requires writing TypeScript/JavaScript shot functions. That's a barrier for the marketing/PM/founder audience who actually need demos most.

A 20-line `.demo` file should produce the same output as 60 lines of imperative JS.

What

A new file format democast accepts as input alongside the existing `democast.config.js`.

Example: `hello.demo`

```

democast 0.2 tape — Minty hello demo

Output demo.mp4
Url http://localhost:3456
Viewport 1280 720
Voice ~/.democast/voices/amy.onnx
Captions burn

Wait 1500ms
Say "Minty. Your network, unified."

Eval showView('today')
Wait 2200ms

Click .daily-focus-name
Wait 2400ms
Say "Open them. Everything they told you, in one place."

ScrollTo #facts-card-slot
Wait 2200ms

Eval showView('ask')
Type #ask-input "investors I should reach out to"
Click .ask-send-btn
Wait 2400ms
Say "Ask your network anything."

Eval showView('network')
Wait 2400ms
Say "Find clusters by company."

Eval showView('today')
Wait 1800ms
Say "All local. Open source."
```

CLI

```
democast record demo.tape # auto-detects .tape | .demo extension
democast all demo.tape
democast init --tape # scaffold a .tape instead of .config.js
```

If the user passes a `.tape` or `.demo` file, parse it. Otherwise fall back to the existing `.config.js` path. Both should remain first-class citizens.

Grammar (start small, expand later)

Verb Args Maps to
`Output` `` `config.output` (or path)
`Url` `` `config.url`
`Viewport` ` [@scale]` `config.viewport`
`Voice` `` `config.narration.voice`
`Captions` `burn` | `soft` | `srt` | `off` `config.narration.captions` (issue #1)
`Wait` `ms` or `s` `page.waitForTimeout`
`Say` `"text"` append a narration line at `current-video-time`
`Click` `` `page.click`
`Type` ` "text"` `page.fill`
`Press` `` `page.keyboard.press`
`Eval` `` `page.evaluate(() => )`
`ScrollTo` `` `page.evaluate(() => el.scrollIntoView())`
`Set` `=` future config knobs (e.g. `Set TypingSpeed=80ms`)
`#` comment ignored

The narration timing for `Say` is computed automatically: each `Say` records a line at the current cumulative wait time so far. The user doesn't have to count milliseconds; they just write the script in chronological order. (This is the actual elegance lift over the JS API.)

How

  1. New module `src/dsl.js` with one exported `parseTape(filepath) → config`. Pure function, no I/O outside reading the file. Fully unit-tested.
  2. `bin/democast.js` checks file extension on the config arg — `.tape`/`.demo` → `parseTape`, `.js`/`.config.js` → existing `require` path.
  3. Add an `init --tape` flag that writes `examples/hello.tape` (also create that example).
  4. README gets a new "Quick start" section right under the install instructions, leading with the .tape format because it's more readable.

Acceptance criteria

  • `democast all examples/hello.tape` produces `out/demo.mp4` end-to-end
  • All grammar verbs in the table above work
  • Comments (`#`) are ignored
  • Args with embedded spaces in quoted strings work (`Say "hello world"`, `Type #q "two words"`)
  • Parser produces a config object identical in shape to what `democast.config.js` exports — `record()` and `narrate()` don't need changes
  • Unit tests for `parseTape` covering: each verb, comment lines, blank lines, indented lines, malformed verbs (clear error message with line number)
  • README leads with the .tape example; the JS API is documented as the "power user" path
  • `democast init --tape` scaffolds a working .tape file
  • An invalid .tape produces an error like `Error at hello.tape:14: unknown verb 'Sayy'` — line + verb both pinpoint the mistake

Out of scope

  • Variables / templating (`$baseUrl`)
  • Loops / conditionals
  • Importing one .tape from another
  • All of these can be added later without breaking the v1 grammar.

Effort estimate

~2-3 hours for an agent. The grammar is small but the parser needs care for quoted strings and helpful errors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readySelf-contained spec; an agent can pick up and ship without follow-up questionsfeatureNew feature or capabilityhighest-leverageBiggest impact for effort

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions