interactive-demo scaffolds a project, records a demo from a live web app, previews it with the editor, and validates it. Then publish puts it online and prints a link, or build writes static files you host yourself.
Only login, publish, and embed talk to a server. Everything else works offline.
init writes a package.json with the CLI as a devDependency. Inside a project you can run npx interactive-demo <command>, or the npm run dev, npm run validate, and npm run build scripts it sets up.
Most commands look for the project root: the nearest parent directory with an interactive-demo.json in it. dev is the exception. Point it at a bare demo folder and it serves that folder alone.
Requirements: Node.js 20 or newer. capture also needs Chrome, and ffmpeg on PATH for video steps.
Conventions
- Demos live at
demos/<slug>/, each with ademo.config.jsonand anassets/folder next to it. The config references media by relative path, such asassets/screen-001.png. There is no manifest to keep in sync. - Slugs are kebab-case: lowercase letters, digits, and hyphens, starting and ending with a letter or digit.
__demois reserved. It is the route prefix the dev server uses for the editor and the player files.assets,api, andcare reserved too.- Exit codes are 0 on success and 1 on failure.
capture startexits 130 if you interrupt it while it is setting up. - Help:
-hor--helpworks on every command.interactive-demo help <command>prints the same text.-vor--versionprints the version.
init
Scaffolds a new project, or adds a demo to one you already have.init <name> creates <name>/ and writes README.md, .gitignore, package.json, interactive-demo.json, and demos/getting-started/: a one-step placeholder demo and the image it runs on. Replace it with your first capture. Pass --no-starter-demo to leave it out.
The generated README.md and the printed next steps lead with the three ways to capture (capture start <url>, asking your agent, or the Chrome extension and init --from <zip>), then npm run dev, publish, and build.
There is one theme, so there is no --theme flag. An old --theme is ignored with a warning.
init --demo <slug> writes the same one-step placeholder as demos/<slug>/demo.config.json and demos/<slug>/assets/placeholder.png, mints the demo’s permanent id, and prints it. If the project file keeps a demos list, the new slug is appended to it.
init --from copies an existing demo folder in wholesale: the config and everything beside it, skipping node_modules/ and .git/. The source must hold a schema-valid demo.config.json. Its id is kept if it is a valid 12-character id, and re-minted if not.
The source may also be a .zip of such a folder, such as the one the Chrome extension downloads. It is unpacked to a temporary directory and imported the same way, so there is no unzip step. A zip that wraps the demo in a <slug>/ folder and one that holds demo.config.json at its root both work.
Without --demo, the slug is taken from the source name: onboarding.zip becomes demos/onboarding/. That name has to be a valid kebab-case slug. If it is not, as with Onboarding Flow.zip, pass --demo to choose one.
Common failures
Common failures
- The target directory already exists.
initrefuses rather than merging into it. Pick another name or remove it. - The name is not a valid slug, or is one of the reserved words. The error names the rule it broke.
--demoor--fromoutside a project. Runinit <name>first, orcdinto the project.--demo <slug>wheredemos/<slug>already exists.- A slug taken from a
--fromsource name that is not kebab-case or is reserved. Pass--demo <slug>. - A
--fromsource with no schema-validdemo.config.json.
dev
Starts the local preview server and the editor. Edits made in the editor are written straight to the files in your repo. Edits you make in a text editor hot-reload the page.<path> is a project root, or a bare demo folder: a directory with a demo.config.json and no project file above it. It defaults to the current directory. A bare folder is wrapped in an in-memory project, so an exported capture previews with no setup.
The server binds to 127.0.0.1 only. It is not reachable from other machines on your network.
On startup it prints the URL, the project name, the demo count, up to ten demo URLs, and the editor URL.
dev writes to disk in one case. A demo.config.json with no id, or with an id that another demo already uses, gets one minted and saved. The line [dev] healed missing id: or [dev] re-minted duplicate id: tells you which file changed. Commit the result, because the published URL is keyed on that id.
A demo whose config fails to parse does not take the server down. It stays routable, / and /<slug>/ explain the problem, and fixing the file reloads it.
Common failures
Common failures
- No
interactive-demo.jsonin the directory or any parent, and nodemo.config.jsonin the directory you pointed at. - A demo folder named with a reserved slug, or one that is not kebab-case.
devrefuses to start and names the folder. Player bundle not found(HTTP 503 onplayer.js). The runtime package is not resolvable. Runnpm install.- The file watcher only reacts to
demo.config.jsonandinteractive-demo.json. Dropping a new file intoassets/does not trigger a reload, but the file is served as soon as something asks for it.
capture
Records a click-through of a live web app as a demo.start opens the URL in Chrome and arms a recorder. Every click records one step. stop writes the demo folder. The capture guide explains how it behaves.
A normal session:
Without
--profile, capture login names the profile after the URL’s host and prints it: https://app.example.com/login gets app-example-com. capture login cannot be combined with --connect-to-browser, because an attached browser owns its own profile.
If ffmpeg is missing, start prints a warning to stderr, reports videoDisabledReason in its JSON, and records every step as a still.
Common failures
Common failures
No Chrome binary found.Pass--browser /path/to/chromeor setCHROME_PATH.capture startneeds anhttp(s)URL.- The recorder fails to arm within 15 seconds.
startfails and prints the tail of the listener and Chrome logs. stopwith no steps captured exits 1 and cleans up. Each step comes from a click on the page. The initial page load is not a step.stopoutside a project. Runinitfirst, or pass--out <dir>.- Two sessions running and no
--session <id>. The error lists the ids.
validate
Checks the project and every demo in it: schema, media paths, and slugs. Run it in CI beforebuild.
Plain output is one
ERROR or WARNING line per issue, then a summary line. Warnings print on a passing run too. It exits 1 if there are errors, or under --strict if there are warnings.
Errors
- A
brand.logothat does not exist, or that escapes the project root. - A demo folder whose slug is invalid or reserved.
- A media path with no file behind it, or one that escapes the demo folder.
- An
asset:pointer. That form is from before media-by-path and nothing resolves it. Use a path underassets/, or an absolute URL.
- A demo config with no id, or an id that is not a 12-character URL-safe one.
devandpublishwrite one. - Two demos sharing one id, usually a hand-copied folder. Run
devto re-mint. - The project’s
demoslist naming a demo that is not there. - A theme preset:
themein the project file ortheme.presetin a demo. Presets were removed and both are ignored. Delete them.
build
Writes one self-contained static folder per demo. Deploy it to any static host. See Host it yourself.
Per demo, under
<out>/<slug>/:
<out>/embed.js, the pop-up loader, is written once at the output root. The command prints what it built plus, for the first demo, the inline iframe and pop-up snippets with a placeholder host.
Common failures
Common failures
- Not inside a project.
Refusing to empty <dir>. The output folder has filesbuilddid not write. Delete it, pick another--out, or pass--force.Refusing to build into <dir>.--outpoints at the project folder.Player bundle not found. The runtime package is not resolvable next to the CLI. Runnpm install.- A media path with no file behind it is not caught here.
assets/is copied as-is and the page 404s at runtime. Runvalidatefirst.
embed
Prints the embed snippet for a demo’s published URL. To embed a folder you built and host yourself, use the snippetsbuild prints. See Sharing and embedding.
<path> is a demo folder (demos/intro) or a slug. You can leave it out when the project has exactly one demo.
Inline mode prints the iframe wrapper sized from the demo’s own aspect ratio and header height, so the player never letterboxes. Popup mode prints the loader and a button for HTML, React, Next.js, Vue, and Svelte.
You must be logged in, because the snippet points at a published URL. If the demo has never been published, embed publishes it first and says so.
login
Logs in to the hosting service, sopublish and embed have a token to use. No other command needs it.
With no
--token, the CLI starts a callback server on 127.0.0.1, prints the login URL, and opens it. Finish in the browser and the CLI exchanges the result for an API token. It gives up after two minutes.
Credentials go to ~/.interactive-demo/credentials.json, written owner-only (mode 0600). logout deletes the file.
--status prints the credentials path, the project root, the API origin, whether a token is configured, and whether the server still accepts it (ok, failed, or skipped).
INTERACTIVE_DEMO_API_BASE overrides the origin in every mode, including --local.
Common failures
Common failures
Timed out waiting for browser login.Nothing came back within two minutes. Run it again, or use--no-openand open the URL yourself.- With
--token, a token the server rejects is still saved, and the command says(token saved without online verification). Check it with--status.
publish
Puts a demo on the hosting service and prints its URL. See Publish.
What it does, in order:
- Hashes every file the config references and uploads each one.
- Rewrites those relative paths to the returned URLs in a frozen copy of the config.
- Sends that copy.
demo.config.json first, because the URL is keyed on it and the next publish has to find the same id. Commit it.
By default, publishing again replaces the existing version, so an embed pointing at it picks up the new one. --new mints a separate URL and leaves the old one serving the old demo. When that would orphan an existing URL, the command warns.
On success it prints the URL and the same sized iframe snippet embed prints. --list prints one line per demo with its URL or (not published).
Common failures
Common failures
Not logged in.Runinteractive-demo loginfirst.- A config that references media with no local file. The command names every missing file and refuses.
- More than one demo in the project and no selector. Pass a path or
--demo <slug>. The error lists the slugs. - A project-relative
brand.logo.publishdoes not upload project files, so it drops the logo from the published page and warns on stderr. Use an absolutehttps://URL ininteractive-demo.json.
version
Prints the CLI version. (local build) suffix means the CLI is running from a source checkout rather than an installed package.

