> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inklyai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI reference

> Every interactive-demo command, flag, and default.

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

```sh theme={"dark"}
npx @inkly-org/interactive-demo-cli init my-demos
cd my-demos && npm install
npx interactive-demo dev
```

`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 a `demo.config.json` and an `assets/` folder next to it. The config references media by relative path, such as `assets/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.
* **`__demo` is reserved.** It is the route prefix the dev server uses for the editor and the player files. `assets`, `api`, and `c` are reserved too.
* **Exit codes** are 0 on success and 1 on failure. `capture start` exits 130 if you interrupt it while it is setting up.
* **Help:** `-h` or `--help` works on every command. `interactive-demo help <command>` prints the same text. `-v` or `--version` prints the version.

## init

Scaffolds a new project, or adds a demo to one you already have.

```sh theme={"dark"}
interactive-demo init <name> [--no-starter-demo]
interactive-demo init --demo <slug> [--from <dir|zip>]
interactive-demo init --from <dir|zip>
```

| Flag | Default | What it does |
| - | - | - |
| `--no-starter-demo` | off | Scaffold an empty project with no `getting-started` demo. |
| `--demo <slug>` | — | Inside an existing project: add `demos/<slug>/`. Optional when `--from` is given. |
| `--from <dir\|zip>` | — | Import an existing demo folder, or a `.zip` of one, instead of scaffolding. |

**`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](/open-source/capture#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.

```sh theme={"dark"}
interactive-demo init acme-demos
cd acme-demos
interactive-demo init --demo billing
interactive-demo init --demo onboarding --from ~/captures/onboarding
interactive-demo init --from ~/Downloads/acme-3f91b2.zip
```

<Accordion title="Common failures">
  * The target directory already exists. `init` refuses 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.
  * `--demo` or `--from` outside a project. Run `init <name>` first, or `cd` into the project.
  * `--demo <slug>` where `demos/<slug>` already exists.
  * A slug taken from a `--from` source name that is not kebab-case or is reserved. Pass `--demo <slug>`.
  * A `--from` source with no schema-valid `demo.config.json`.
</Accordion>

## dev

Starts the local preview server and the [editor](/open-source/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.

```sh theme={"dark"}
interactive-demo dev [<path>] [--port <n>]
```

| Flag | Default | What it does |
| - | - | - |
| `--port <n>`, `-p <n>` | `3000` | Preferred port. If it is taken, the next free port is used. Must be 1–65535. |

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

| Route | What it is |
| - | - |
| `/` | An index of every demo in the project, with an **Edit** link each. |
| `/<slug>/` | The demo page, the same page `build` writes. |
| `/__demo/editor/` | The editor. `/__demo/editor/#/<slug>` opens one demo. |
| `/<slug>/assets/…` | The demo's media, served from `demos/<slug>/assets/`. |
| `/<slug>/player.js`, `player.css`, `player-fonts.css` | The player, from the installed runtime package. |
| `/<slug>/fonts/…`, `/<slug>/backgrounds/…` | The font files and cover backdrop `player-fonts.css` points at. |
| `/<slug>/brand/…` | The project's logo, when `brand.logo` is a project file. |
| `/__demo/player.js`, `/__demo/player.css`, `/__demo/player-fonts.css`, `/__demo/fonts/…`, `/__demo/backgrounds/…` | The same files at a fixed path, for the editor. |
| `/__demo/demos`, `/__demo/demo/<slug>` | JSON: the demo list, and one demo's config. |
| `/__demo/editor/demos/<slug>/files`, `…/assets`, `…/embed` | The editor's read and write API: the demo's files, its `assets/` folder, and the Share dialog's snippets. |
| `/__demo/editor/capabilities` | Optional [host features](/open-source/editor#host-capabilities) for the editor. `dev` answers `{}`. |

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.

<Accordion title="Common failures">
  * No `interactive-demo.json` in the directory or any parent, and no `demo.config.json` in the directory you pointed at.
  * A demo folder named with a reserved slug, or one that is not kebab-case. `dev` refuses to start and names the folder.
  * `Player bundle not found` (HTTP 503 on `player.js`). The runtime package is not resolvable. Run `npm install`.
  * The file watcher only reacts to `demo.config.json` and `interactive-demo.json`. Dropping a new file into `assets/` does not trigger a reload, but the file is served as soon as something asks for it.
</Accordion>

## 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](/open-source/capture) explains how it behaves.

```sh theme={"dark"}
interactive-demo capture start <url> [--name <name>] [options]
interactive-demo capture stop [--session <id>] [--out <dir>]
interactive-demo capture cancel [--session <id>]
interactive-demo capture status [--session <id>]
interactive-demo capture undo [--session <id>]
interactive-demo capture profiles
interactive-demo capture login <url> [--profile <name>]
```

Every subcommand prints JSON, so a script or an agent can drive it as easily as a person.

| Flag | Default | What it does |
| - | - | - |
| `--name <name>` | the page host | Demo title, and the basis for the folder slug. |
| `--session <id>` | the only running session | Which session to act on. Required once two are running. |
| `--out <dir>` | the project's `demos/` | With `stop`: write the demo at `<dir>/<slug>` instead. |
| `--browser <path>` | an installed Chrome, or `CHROME_PATH` | Chrome or Chromium binary. |
| `--connect-to-browser <url>` | — | Attach to an already-running Chrome DevTools endpoint instead of launching one. |
| `--width <n>` | `1440` | Viewport width. Must be at least 320. |
| `--height <n>` | `900` | Viewport height. Must be at least 240. |
| `--window-size <w>x<h>` | — | Shortcut for `--width` and `--height`. |
| `--timeout <ms>` | `120000` | Page load timeout. Must be at least 1000. |
| `--headed` | on | Visible Chrome window. Wins over `--headless` if both are passed. |
| `--headless` | off | Headless Chrome. Only useful when something else drives the page. |
| `--profile <name>` | — | Reuse a persistent Chrome profile, so a login survives between captures. |
| `--keep-profile` | off | Keep the temporary profile after `stop` or `cancel`. |
| `--no-video` | video on | Still images only. Never build video steps. |
| `--no-zoom` | zoom on | Do not zoom screenshot steps in on the clicked point. |
| `--compress-images` | off | Re-encode screenshots to WebP. |

A normal session:

```sh theme={"dark"}
interactive-demo capture start https://app.example.com --name "Onboarding"
# click through the product in the Chrome window that opens
interactive-demo capture status    # how many steps so far, and their labels
interactive-demo capture undo      # drop the last one
interactive-demo capture stop      # writes demos/onboarding/
```

| Subcommand | What it does |
| - | - |
| `start` | Prints the session id, the browser it launched, the tab it opened, and whether video is on. Returns as soon as the recorder has armed. Recording continues in a detached process. |
| `stop` | Writes `demos/<slug>/` (or `<out>/<slug>/`), tears the session down, and prints the demo folder, the step count, and one label per step. |
| `cancel` | Tears the session down without writing anything. |
| `undo` | Drops the most recent step from a live session. |
| `status` | Reports the recorder state and the steps so far without touching the browser. `browserAlive: false` means the window has gone and further clicks record nothing. |
| `profiles` | Lists persistent profiles, with whether each one has cookies yet. |
| `login` | Opens an ordinary Chrome window on a persistent profile, for sign-ins that reject an automated browser. |

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.

<Accordion title="Common failures">
  * `No Chrome binary found.` Pass `--browser /path/to/chrome` or set `CHROME_PATH`.
  * `capture start` needs an `http(s)` URL.
  * The recorder fails to arm within 15 seconds. `start` fails and prints the tail of the listener and Chrome logs.
  * `stop` with 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.
  * `stop` outside a project. Run `init` first, or pass `--out <dir>`.
  * Two sessions running and no `--session <id>`. The error lists the ids.
</Accordion>

## validate

Checks the project and every demo in it: schema, media paths, and slugs. Run it in CI before `build`.

```sh theme={"dark"}
interactive-demo validate [--json] [--strict]
```

| Flag | Default | What it does |
| - | - | - |
| `--json` | off | Print the result as JSON (`ok`, `projectRoot`, `errors`, `warnings`, `issues`). |
| `--strict` | off | Treat warnings as failures. |

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.logo` that 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 under `assets/`, or an absolute URL.

**Warnings**

* A demo config with no id, or an id that is not a 12-character URL-safe one. `dev` and `publish` write one.
* Two demos sharing one id, usually a hand-copied folder. Run `dev` to re-mint.
* The project's `demos` list naming a demo that is not there.
* A theme preset: `theme` in the project file or `theme.preset` in 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](/open-source/publish-and-self-host#host-it-yourself).

```sh theme={"dark"}
interactive-demo build [--out <dir>] [--force]
```

| Flag | Default | What it does |
| - | - | - |
| `--out <dir>` | `dist` | Output folder, relative to the project root. |
| `--force` | off | Empty and reuse a non-empty output folder that `build` did not create. |

Per demo, under `<out>/<slug>/`:

```text theme={"dark"}
index.html          the page: stylesheet, the #demo-config script tag, #root, player.js
player.js           the player, React bundled in
player.css
player-fonts.css    plus fonts/ and backgrounds/
assets/…            the demo's media, copied from demos/<slug>/assets/
brand/…             the project's logo, if brand.logo is a project file
```

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

<Warning>
  `build` empties the output folder first, so it guards which folder it will empty. It writes a `.interactive-demo-build` marker into its output. On the next run it empties a folder that is missing, empty, or carries that marker. Any other non-empty folder is refused unless you pass `--force`. The project folder, or a folder containing it, is always refused.
</Warning>

<Accordion title="Common failures">
  * Not inside a project.
  * `Refusing to empty <dir>`. The output folder has files `build` did not write. Delete it, pick another `--out`, or pass `--force`.
  * `Refusing to build into <dir>`. `--out` points at the project folder.
  * `Player bundle not found`. The runtime package is not resolvable next to the CLI. Run `npm install`.
  * A media path with no file behind it is not caught here. `assets/` is copied as-is and the page 404s at runtime. Run `validate` first.
</Accordion>

## embed

Prints the embed snippet for a demo's **published** URL. To embed a folder you built and host yourself, use the snippets `build` prints. See [Sharing and embedding](/open-source/embedding).

```sh theme={"dark"}
interactive-demo embed [<path>|--demo <slug>] [--mode inline|popup] [--label <text>] [--json]
```

| Flag | Default | What it does |
| - | - | - |
| `--demo <slug>` | — | Select the demo by slug. |
| `--mode <mode>` | `inline` | `inline` prints a sized iframe. `popup` prints the loader plus a trigger button. |
| `--label <text>` | `Try the demo` | Button text in popup mode. |
| `--json` | off | Print the snippets as JSON. |

`<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, so `publish` and `embed` have a token to use. No other command needs it.

```sh theme={"dark"}
interactive-demo login [--token <token>] [--no-open] [--local] [--status] [--json]
interactive-demo logout
```

| Flag | Default | What it does |
| - | - | - |
| `--token <token>` | — | Save an API token directly and skip the browser. `INTERACTIVE_DEMO_API_TOKEN` does the same. |
| `--no-open` | browser opens | Print the login URL instead of opening a browser. |
| `--status` | — | Show where the credentials live and whether the token still works. |
| `--json` | off | With `--status`: print it as JSON. |
| `--local` | off | Point at a local development build of the hosting service (`http://localhost:3000`). |

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

<Accordion title="Common failures">
  * `Timed out waiting for browser login.` Nothing came back within two minutes. Run it again, or use `--no-open` and 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`.
</Accordion>

## publish

Puts a demo on the hosting service and prints its URL. See [Publish](/open-source/publish-and-self-host#publish).

```sh theme={"dark"}
interactive-demo publish [<path>|--demo <slug>] [--new] [--json]
interactive-demo publish --list [--json]
```

| Flag | Default | What it does |
| - | - | - |
| `--demo <slug>` | — | Select the demo by slug. |
| `--new` | off | Mint a new URL instead of updating the demo's existing one in place. |
| `--list` | — | Show the published URL of every demo in the project. |
| `--json` | off | Print machine-readable JSON. |

What it does, in order:

1. Hashes every file the config references and uploads each one.
2. Rewrites those relative paths to the returned URLs in a frozen copy of the config.
3. Sends that copy.

Nothing on disk changes. The one exception is a demo with no id: the minted id is written to `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)`.

<Accordion title="Common failures">
  * `Not logged in.` Run `interactive-demo login` first.
  * 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`. `publish` does not upload project files, so it drops the logo from the published page and warns on stderr. Use an absolute `https://` URL in `interactive-demo.json`.
</Accordion>

## version

Prints the CLI version.

```sh theme={"dark"}
interactive-demo version
interactive-demo --version
```

A ` (local build)` suffix means the CLI is running from a source checkout rather than an installed package.

## Environment variables

| Variable | Used by | What it does |
| - | - | - |
| `CHROME_PATH` | `capture` | Chrome or Chromium binary, when it is not found automatically. |
| `INTERACTIVE_DEMO_CAPTURE_HOME` | `capture` | Where sessions, frames, and profiles live. Default `~/.interactive-demo/capture`. |
| `INTERACTIVE_DEMO_CAPTURE_BROWSER_URL` | `capture` | Default for `--connect-to-browser`. |
| `INTERACTIVE_DEMO_API_TOKEN` | `login` | Same as `login --token`. |
| `INTERACTIVE_DEMO_API_BASE` | `login`, `publish`, `embed` | Override the hosting origin. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.