# Reference: Inkly CLI & demo schema

This documents how the Inkly **CLI** works and the **schema** the
`demo.config.json`, the hub index (`inkly.json`), and the
`@inkly/runtime` **viewer** expect.

> **Versioned.** This reference tracks runtime **0.6.2**
> (`@inkly/runtime/schema` + `@inkly/runtime`). The schema is
> version-dependent — the source of truth is the code, not this doc. Read
> `packages/inkly-runtime/src/schema/src` for the config/hub/asset schemas and
> `packages/inkly-runtime/src` (especially `primitives/Stage.tsx`) for how
> the viewer renders them. `demo.config.json#version` is the literal `1`;
> the runtime advertises `MIN_SCHEMA_VERSION = 0.4.0`
> (`packages/inkly-runtime/src/runtime-constants.ts`), and a hub pins an exact
> `@inkly/runtime` build via `inkly.json#runtime`.

A **hub** is a git folder with `inkly.json` at the root and one demo per
folder under `demos/<slug>/`. The source of truth for a demo is its
`demos/<slug>/demo.config.json`. There is **no build step** — the runtime
renders the JSON directly, and `inkly dev` is the local preview.

---

## 1. CLI commands

Install the CLI globally:

```bash
npm i -g @inkly-org/cli
```

Use the installed `inkly` binary for all Inkly commands. Do not run Inkly
through `npx`, especially capture commands, because one-off execution can
resolve Chrome differently and cause browser launch failures.

| Command | What it does |
| --- | --- |
| `inkly init <name> [--layout <sidebar\|tabs>]` | Scaffold a new hub folder: `inkly.json`, `README.md`, `CLAUDE.md`, starter `public/` assets, and a `demos/getting-started/` demo. Refuses to overwrite. |
| `inkly add <name> [--collection <name>]` | Add a new demo to the current hub. Walks up to `inkly.json`, writes `demos/<slug>/demo.config.json`, appends the slug to a collection. `inkly demo <slug>` is an alias. |
| `inkly dev [<path>] [--port <n>]` | Boot the local Vite preview server (default <http://localhost:3000>). Serves the hub index and every demo at `/<slug>`; accepts a hub root or a bare demo folder. Watches `inkly.json`, every `demo.config.json`/`assets.json`, and live-reloads. **This is the render — no separate build.** |
| `inkly validate [--json] [--strict]` | Validate `inkly.json`, every `demo.config.json`/`assets.json`, collection references, theme ids, chapter `stepIds`, and `asset:<id>` references. Use in CI. |
| `inkly lock [--check] [--local <path>] [--json]` | Generate or verify `inkly.lock` (pins the resolved runtime bytes). `--check` fails if out of date; `--local` sources the manifest from a local runtime build. |
| `inkly capture <start\|stop\|cancel\|status\|undo\|nav\|profiles\|ask-user-to-log-in>` | **Agent-facing.** Capture an image/video walkthrough; you drive the page via `capture nav` (see the `agent-capture` skill). `ask-user-to-log-in --url <login-url>` opens a non-automated window so the user can sign in by hand (incl. OAuth), derives a reusable profile name from the URL host, and returns it as `profile.name`; pass `--profile <name>` only when you need a custom name. |
| `inkly capture-html <start\|stop\|cancel\|status\|undo\|nav\|profiles\|ask-user-to-log-in>` | **Agent-facing.** Capture a self-contained HTML walkthrough (see the `agent-html-capture` skill). |
| `inkly snapshot <demo-path> [--demo <slug>] [--json]` | Publish a standalone `/p/<id>` snapshot of one demo to the hosted platform. `<demo-path>` may be a demo inside a hub OR a bare exported demo folder (a `capture stop` export with no `inkly.json` above it) — in that case it wraps the folder in a throwaway hub, so you can publish a fresh capture without assembling a hub first. |
| `inkly login [--no-open] [--token <t>]` / `inkly logout` / `inkly status [--json]` | Connect/disconnect the hosted app and show auth + hub status. |
| `inkly sync [--demo <slug>] [--dry-run] [--json]` | Upload local capture blobs to the platform CDN and update `assets.json` with CDN metadata. |
| `inkly skills install` | Install/refresh the bundled Inkly CLI agent skill into your installed agents. |
| `inkly doctor [--json]` | Local diagnostics: CLI/Node version, hub root, login, schema validation, capture cache. |
| `inkly version` / `inkly update [--dry-run]` | Print / update the installed CLI. |
| `inkly help [command]` | Show CLI help. |

Typical session:

```bash
inkly init acme-tour --layout sidebar
cd acme-tour
inkly add onboarding --collection "Main"
# …edit demos/onboarding/demo.config.json…
inkly dev   # → http://localhost:3000/onboarding
```

---

## 2. Hub index — `inkly.json`

The hub index lists the hub's demos and its branding/runtime. Shape
(`InklyJsonSchema`, `packages/inkly-runtime/src/schema/src/hub.ts`):

```json
{
  "$schema": "https://www.runtime.inklyai.dev/schema/inkly.json",
  "name": "Acme product tours",          // REQUIRED. Hub title.
  "runtime": "0.6.2",                  // REQUIRED. EXACT semver of @inkly/runtime to load.
  "runtimeRange": "^0.6.2",            // optional. Declared compat window.
  "layout": "tabs",                       // optional. 'tabs' (default) | 'sidebar'.
  "theme": "inkly",                       // optional. Preset id; platform defaults to 'inkly'.
  "tokens": { "primary": "#4f46e5", "font": "Inter, system-ui", "radius": "12px" },
  "brand": {
    "logo": "asset:logo",                 // asset id, repo path, or https URL
    "name": "Acme",
    "logoHref": "https://acme.com",
    "favicon": "public/favicon.svg",      // hub-only; demos cannot override favicon
    "cta": { "label": "Start free", "href": "https://acme.com/signup" },
    "secondaryCta": { "label": "Docs", "href": "https://acme.com/docs" }
  },
  "title": "Ship demos *faster.*",        // hero title; *…* renders an accent-color italic span
  "subtitle": "Interactive product tours.",
  "collections": [
    {
      "name": "Main",
      "description": "Core flows.",
      "icon": "folder",                   // optional Lucide icon (kebab-case)
      "demos": ["onboarding", "product-tour"]   // demo slugs under demos/
    }
  ]
}
```

- `runtime` must be an **exact** version; the platform serves that exact
  `@inkly/runtime` bundle. `runtimeRange` only declares compatibility.
- The dev server discovers demos by scanning `demos/<slug>/demo.config.json`;
  the slug is the folder path under `demos/`.
- Slugs are kebab-case (lowercase letters/digits/hyphens, start and end
  alphanumeric). **Reserved** (cannot be a demo folder): `__inkly`,
  `assets`, `api`, `c`, and anything starting with `_` or `.`.

---

## 3. `demo.config.json` — top-level

```json
{
  "id": "onboarding-x7k2",          // REQUIRED. 12-char url-safe id ([A-Za-z0-9_-]{12}).
  "version": 1,                      // REQUIRED. Literal 1.
  "title": "Onboarding tour",        // optional. Player header.
  "subtitle": "Get set up in 2 min", // optional. Hub-index description.
  "runtime": "0.6.2",              // optional. Per-demo runtime override (exact semver).
  "visibility": "public",            // optional. 'public' (default) | 'private'. 'inkly dev' ignores it.
  "background": { "type": "color", "from": "#0b1020", "to": "#1e293b" },
  "theme": { "preset": "inkly", "tokens": {}, "brand": {} },
  "chrome": { "showHubBreadcrumb": true, "controls": "full", "autoplay": false },
  "aspectRatio": { "width": 16, "height": 10 },
  "chapters": [ { "id": "ch1", "title": "Setup", "stepIds": ["s1", "s2"] } ],
  "steps": [ /* one or more — see §4 */ ]
}
```

- `id` is an opaque 12-char id (minted by the capture/CLI tooling), not the
  slug. `background` is the full-page canvas: `{type:'none'}`,
  `{type:'color', color}` or `{type:'color', from, to}` (gradient), or
  `{type:'image', src}`.
- `chrome.controls` is `'full' | 'minimal' | 'hidden'` (successor to the
  legacy `hideControls` boolean); `autoplay:false` (default) parks at each
  step until the viewer advances.
- `chapters[].stepIds` and any cover-CTA `step`/`chapter` targets must
  reference real ids — parse fails otherwise.

---

## 4. Steps

Two **kinds**: `content` (a media background + annotations — the workhorse)
and `cover` (a splash/outro holding exactly one widget). A content step's
`background.type` is `image`, `video`, or `html`.

Common content-step fields: `kind`, `id`, `label?`, `duration?` (ms),
`advance: { trigger: 'auto' | 'click' }`, `background`, `annotations[]`,
`transform?` (zoom/pan), `script?`/`voiceover?`/`captions?`.

### 4a. Image / video / html backgrounds

```json
{ "kind": "content", "id": "dashboard",
  "background": { "type": "image", "src": "asset:dash", "naturalWidth": 2880, "naturalHeight": 1800,
                  "objectFit": "cover", "objectPosition": "center top" } }
```
```json
{ "kind": "content", "id": "flow",
  "background": { "type": "video", "src": "asset:clip", "posterSrc": "asset:poster",
                  "naturalWidth": 1920, "naturalHeight": 1080, "autoplay": true, "muted": true } }
```
```json
{ "kind": "content", "id": "settings",
  "background": { "type": "html", "path": "snapshots/snap-001/index.html",
                  "naturalWidth": 1440, "naturalHeight": 900, "zoom": 1,
                  "scroll": { "x": 0, "y": 0, "maxX": 0, "maxY": 1200 },
                  "sourceUrl": "https://app.example.com/settings" } }
```

- `naturalWidth`/`naturalHeight` are **required** on image and video — they
  drive the player aspect ratio. `src` accepts `asset:<id>`, a repo-relative
  path, or an absolute `https://`/`data:` URL.
- HTML `path` is workspace-relative and produced by capture — do **not**
  hand-write it. The viewer renders it in a sandboxed iframe and restores
  `scroll`. Messages can target a captured element by `alphaId` (§5).
- Use `video` only when motion is the point; otherwise an image step is
  lighter. Use `html` for text-heavy UI the viewer should read.

### 4b. Cover step

Holds exactly one widget — `headline`, `form`, `embed`, or `custom` —
and defaults to `advance.trigger: 'click'`.

```json
{ "kind": "cover", "id": "intro",
  "widgets": [ { "type": "headline", "id": "h",
    "title": "Your demo in **60 seconds**.",
    "description": "Click *Get started*.",
    "cta": { "label": "Get started", "action": { "type": "next" } } } ] }
```

CTA `action`: `{type:'next'}`, `{type:'prev'}`, `{type:'restart'}`,
`{type:'step', stepId}`, `{type:'chapter', chapterId}`,
`{type:'url', href, target?}`.

---

## 5. Annotations (content steps only)

`step.annotations[]` — all coordinates are **normalized `[0,1]`** over the
background's natural size (`0.5,0.5` = center):

- **`message`** — `variant: 'pointer' | 'callout' | 'area'` at `(x,y)`
  (`area` also needs `w,h`). `text` is inline markdown; `anchor`
  (`top|right|bottom|left|auto`) picks the card edge; `advancesStep`
  (default true). On HTML steps, set
  `target: { type: 'html-element', alphaId: '<id>' }` to pin to the captured
  element (`alphaId` = its `[data-inkly-id]`) instead of `x/y`.
- **`blur`** — `{ x, y, w, h, intensity }` masks a rectangle (PII / unreleased UI).
- **`text`** — `{ x, y, text, fontSize, color }` floats a free label.

A content step's `transform: { zoom, x, y }` zooms into a focal point
(`x,y` normalized 0..1, `zoom >= 1`).

---

## 6. Assets — `asset:<id>` and `assets.json`

`asset:<id>` URIs resolve through the per-demo `assets.json` next to
`demo.config.json`:

```json
{
  "version": 1,
  "assets": [
    { "id": "dash", "sha256": "<64-hex>", "kind": "image", "contentType": "image/png",
      "size": 1234567, "viewport": { "w": 2880, "h": 1800 },
      "publicUrl": "/onboarding-x7k2/<file>.png" }
  ]
}
```

- The runtime resolves `asset:<id>` by looking up the id in the manifest and
  using its delivered URL (`publicUrl`/CDN). Absolute `http(s)://`,
  `data:`, `blob:` URLs pass through unchanged; repo-relative paths resolve
  against the demo folder.
- Capture writes the bytes to `public/` (local) and/or the CDN (after
  `inkly sync`) plus the manifest entry. `assets.json#screens` is a
  capture-time audit log the runtime ignores.
- Treat capture-written assets as compiled output — reference them, don't
  hand-edit them.

---

## 7. What the viewer (`@inkly/runtime`) expects

The viewer renders `demo.config.json` directly — read
`packages/inkly-runtime/src/primitives/Stage.tsx` for specifics:

- COORDINATES are normalized 0..1 over the background natural size, mapped to
  the rendered stage. Pointers, annotations, blur regions, and the zoom focal
  point all use this space.
- ZOOM/PAN: `transform.zoom > 1` zooms an image step toward `transform.(x,y)`;
  the viewer holds 1x for the first entrance frame, then eases to the authored
  zoom.
- VIDEO: autoplays muted on entry; annotations and zoom are held until the clip
  ends; the poster shows while loading.
- HTML steps render in a sandboxed iframe (scripts stripped/limited at capture),
  restore `scroll`, block in-page navigation, and resolve element-targeted
  messages by querying `[data-inkly-id]` for a live rect each frame.
- ASSET resolution maps `asset:<id>` → manifest `publicUrl` (or a host
  resolver); the local dev server serves `/<slug>/<file>` from the demo's
  `public/`.
- CHAPTERS drive the outline/nav; the player emits events (`ready`,
  `step_view`, `complete`, `cta_click`, `form_submit`).
- VERSIONING: the viewer assumes schema `version: 1` and advertises
  `MIN_SCHEMA_VERSION = 0.4.0`. The hub's `inkly.json#runtime`
  (or `demo.config.json#runtime`) selects which `@inkly/runtime` build
  renders. If you target a different runtime, re-check the schema against that
  version's source.

---

## 8. Rules of the road

- `inkly.json` and every `demo.config.json` must stay schema-valid; the dev
  server and `inkly validate` print errors verbatim — fix them at the source.
- Slugs are kebab-case and not reserved; demo `id` is the 12-char opaque id.
- Image/video backgrounds **require** `naturalWidth`/`naturalHeight`.
- Don't hand-edit `snapshots/` paths or capture-written `assets.json`
  entries — treat them as compiled output.
- This reference is pinned to runtime 0.6.2; if the runtime moves,
  re-read `packages/inkly-runtime/src/schema` and `packages/inkly-runtime`.

