@mineo71/showtime 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-13
4
+
5
+ - `showtime frames` also renders a contact sheet (`frames/sheet.png`) of every step.
6
+ - `showtime lint`: long captions, pauses that should be waits, zoom/spotlight that cannot apply,
7
+ and, with a take, captions too brief to read and overall length.
8
+ - `--json` on every command.
9
+ - `hooks.before` / `hooks.after`: shell commands or HTTP calls around every `check` and `record`,
10
+ so state can be reset between rehearsal and take.
11
+ - `app.mock` (canned responses for matching requests), `app.clock` (the page clock starts at a fixed date),
12
+ `app.dialogs` (accept or dismiss native dialogs; accepted by default).
13
+ - Safe re-takes: a failed `record` leaves the previous take untouched (`take.tmp` keeps the failure).
14
+ - Settling ignores event streams; relative `goto` URLs resolve against `app.url`.
15
+ - Captions never overlap, and move to the top when the action is at the bottom of the screen.
16
+ - `meta.outro` closing card and `meta.logo` on the intro and outro cards.
17
+ - `showtime.config.json` brand file with default `style` and `meta` for every script below it.
18
+ - Takes recorded with 0.2 stay valid.
19
+
3
20
  ## 0.2.1 — 2026-10-13
4
21
 
5
22
  - Releases are automated: `npm version patch|minor|major` dates the changelog, tags and pushes;
package/README.md CHANGED
@@ -26,7 +26,7 @@ No timeline editor. No re-take when a caption changes.
26
26
  <div align="center">
27
27
  <a href="docs/media/demo.mp4"><img src="docs/media/hero.webp" width="860" alt="A demo of a fictional invoicing app, filmed and edited by showTime: the camera zooms on a chart, a cursor fills in a new invoice, a spotlight lands on the total, and the sent invoice appears in the table" /></a>
28
28
  <br />
29
- <sub>Made by <a href="examples/fennel/demo.json">one 44-line script</a> and one command, with no manual editing. <a href="docs/media/demo.mp4">▶ Full-quality MP4</a></sub>
29
+ <sub>Made by <a href="examples/fennel/demo.json">a 39-line script</a>, a <a href="examples/fennel/showtime.config.json">brand file</a> and one command, with no manual editing. <a href="docs/media/demo.mp4">▶ Full-quality MP4</a></sub>
30
30
  </div>
31
31
 
32
32
  ---
@@ -121,7 +121,19 @@ again.
121
121
  <td valign="top">
122
122
  <img src="docs/media/intro.jpg" alt="Intro card with title and subtitle" /><br />
123
123
  <b>🎬 Intro card and window chrome</b><br />
124
- <sub>Title card, a browser frame with your URL, a gradient backdrop, rounded corners and a shadow. Each of them is a <code>style</code> field.</sub>
124
+ <sub>Title card with your logo, a browser frame with your URL, a gradient backdrop, rounded corners and a shadow. Each of them is a <code>style</code> or <code>meta</code> field.</sub>
125
+ </td>
126
+ </tr>
127
+ <tr>
128
+ <td valign="top">
129
+ <img src="docs/media/outro.jpg" alt="Outro card with logo, title and URL" /><br />
130
+ <b>📣 Ends on your call to action</b><br />
131
+ <sub>An outro card with your logo, a headline and the URL to visit. Put it in a brand file once and every demo ends the same way.</sub>
132
+ </td>
133
+ <td valign="top">
134
+ <img src="docs/media/sheet.jpg" alt="Contact sheet of every step" /><br />
135
+ <b>🧪 Reviews its own work</b><br />
136
+ <sub><code>showtime frames</code> renders a contact sheet of every step, and <code>showtime lint</code> flags captions that are too long or too brief to read. The agent fixes them before you look.</sub>
125
137
  </td>
126
138
  </tr>
127
139
  </table>
@@ -132,8 +144,16 @@ And under the hood:
132
144
  When a step fails it prints an ARIA snapshot of the page, so Claude can fix its own selectors.
133
145
  - 🔁 **Stale-take detection.** The take stores a hash of the actions. Edit a caption and `render`
134
146
  reuses the take. Edit a click and `render` tells you to `record` again.
135
- - 🧪 **Self-review.** `showtime frames` renders a still per step so the agent can *look* at its
136
- work before you do.
147
+ - 🧹 **Clean state for every take.** `hooks` run a reset command or call an endpoint before and
148
+ after each rehearsal and take, so a flow that creates data works twice in a row.
149
+ - 🎭 **Deterministic pages.** `app.mock` answers flaky or slow APIs with canned JSON, `app.clock`
150
+ pins the date, and `app.dialogs` answers `confirm()` the way the demo needs.
151
+ - 🎨 **One brand file.** `showtime.config.json` holds your colours, logo and outro; every script
152
+ below it inherits them.
153
+ - 🛟 **Safe re-takes.** A take that fails halfway leaves the last good one untouched.
154
+ - 🤖 **JSON everywhere.** Every command takes `--json`, so agents read results instead of parsing
155
+ prose. Captions also move to the top on their own when the action happens at the bottom of the
156
+ screen.
137
157
  - 📐 **Typed scripts.** Point `"$schema"` at [`docs/script.schema.json`](docs/script.schema.json)
138
158
  for autocomplete and validation in your editor.
139
159
  - 🖥️ **Crisp at 1080p.** The app is captured at 2× device scale, so zoomed shots stay sharp.
@@ -169,7 +189,8 @@ The agent takes it from there:
169
189
  3. **Rehearses** with `showtime check` until every step passes, fixing selectors from the ARIA
170
190
  snapshot.
171
191
  4. **Records and renders** with `showtime record` and `showtime render`.
172
- 5. **Reviews** the stills from `showtime frames`, tightens captions and pacing, and re-renders.
192
+ 5. **Reviews** the contact sheet from `showtime frames` and the warnings from `showtime lint`,
193
+ tightens captions and pacing, and re-renders.
173
194
 
174
195
  The skill ([`skill/SKILL.md`](skill/SKILL.md)) is a plain-Markdown
175
196
  [Agent Skill](https://agentskills.io). Any agent that can run shell commands can follow it, and so
@@ -224,8 +245,9 @@ into and waited on, and everything cinematic is computed afterwards.
224
245
  }
225
246
  ```
226
247
 
227
- `meta.title` adds an intro card. `setup` steps run before the camera starts (cookie banners,
228
- onboarding). Scenes group steps for readability and for naming the stills.
248
+ `meta.title` adds an intro card, `meta.outro` a closing card and `meta.logo` puts your logo on
249
+ both. `setup` steps run before the camera starts (cookie banners, onboarding). Scenes group steps
250
+ for readability and for naming the stills.
229
251
 
230
252
  <details>
231
253
  <summary><b>Actions</b> (<code>do</code>)</summary>
@@ -291,6 +313,52 @@ Targets are Playwright selectors: `role=button[name='Save']`, `text=Save`, `#id`
291
313
  | `colorScheme` | none | `light` or `dark`; also picks the browser-bar theme |
292
314
  | `locale`, `timezoneId` | none | passed to the browser context |
293
315
  | `mask` | `[]` | CSS selectors to blur (plain CSS, not `text=`) |
316
+ | `mock` | `[]` | `[{ url, method?, status?, json? \| body?, headers?, delayMs? }]`: answer matching requests (`url` is a glob like `**/api/stats*`; the first match wins) |
317
+ | `clock` | none | start the page clock at this ISO date-time; it then runs normally |
318
+ | `dialogs` | `accept` | answer `alert`/`confirm`/`prompt` with `accept` or `dismiss` |
319
+
320
+ </details>
321
+
322
+ <details>
323
+ <summary><b>Cards</b> (<code>meta</code>)</summary>
324
+
325
+ | field | |
326
+ |------------|-|
327
+ | `title`, `subtitle` | intro card |
328
+ | `logo` | SVG, PNG, JPEG or WebP (≤ 2 MB), relative to the file that names it; shown on the intro and outro |
329
+ | `outro` | `{ title, subtitle?, url? }`: closing card after the video |
330
+
331
+ </details>
332
+
333
+ <details>
334
+ <summary><b>Hooks</b> (<code>hooks</code>)</summary>
335
+
336
+ ```json
337
+ "hooks": {
338
+ "before": [{ "run": "npm run db:reset" }],
339
+ "after": [{ "request": { "url": "http://localhost:3000/api/test/reset", "method": "POST" } }]
340
+ }
341
+ ```
342
+
343
+ `before` hooks run at the start of every `check` and `record` (so `make` resets between the
344
+ rehearsal and the take); `after` hooks run when they end, even after a failure, and a failing
345
+ `after` hook is a warning that never costs you the take. `run` is a shell
346
+ command in the script's folder (120 s timeout); `request` is an HTTP call that must answer 2xx.
347
+
348
+ </details>
349
+
350
+ <details>
351
+ <summary><b>Brand file</b> (<code>showtime.config.json</code>)</summary>
352
+
353
+ Put it next to your scripts or in any folder above them; the nearest one applies. Its `style` and
354
+ `meta` are defaults, and a script's own fields win key by key.
355
+
356
+ ```json
357
+ {
358
+ "style": { "background": "linear-gradient(135deg, #0f172a, #312e81)", "accent": "#6366f1", "address": "app.acme.com" },
359
+ "meta": { "logo": "brand/logo.svg", "outro": { "title": "Try Acme free", "url": "acme.com" } }
360
+ }
361
+ ```
294
362
 
295
363
  </details>
296
364
 
@@ -303,10 +371,12 @@ Targets are Playwright selectors: `role=button[name='Save']`, `text=Save`, `#id`
303
371
  | `showtime check <script> [--json] [--headed]` | rehearse; a screenshot per step; the first failure with an ARIA snapshot |
304
372
  | `showtime record <script> [--headed] [--keep-frames]` | record a take into `.showtime/<name>/take` |
305
373
  | `showtime render <script> [-o out.mp4]` | render from the last take, without re-recording |
306
- | `showtime frames <script>` | QA stills: the intro plus one per step |
374
+ | `showtime frames <script>` | QA stills (intro, one per step, outro) and a contact sheet of them |
375
+ | `showtime lint <script>` | warnings about captions, pauses and length; timing checks need a take |
307
376
  | `showtime schema` | print the script's JSON Schema |
308
377
 
309
- The video lands next to the script (`demo/pitch.json` → `demo/pitch.mp4`). Working files live in
378
+ Every command takes `--json` and then prints exactly one JSON document. The video lands next to
379
+ the script (`demo/pitch.json` → `demo/pitch.mp4`). Working files live in
310
380
  `.showtime/<name>/`, which is worth adding to your `.gitignore`.
311
381
 
312
382
  ## Tips for a great demo
@@ -317,7 +387,9 @@ The video lands next to the script (`demo/pitch.json` → `demo/pitch.mp4`). Wor
317
387
  `spotlight` on the one number that sells it.
318
388
  - **Never `pause` for loading.** Use `wait` with a selector and auto-speed compresses the spinner.
319
389
  - **Start clean.** Use `setup` for cookie banners, `app.storageState` to skip the login, `app.mask`
320
- for anything personal, and seed data you can reset between `check` and `record`.
390
+ for anything personal, and `hooks` to reset the data between `check` and `record`.
391
+ - **Pin the world.** Mock slow or random APIs with `app.mock` and pin the date with `app.clock`,
392
+ so every take looks the same.
321
393
 
322
394
  ## FAQ
323
395
 
@@ -18,6 +18,31 @@
18
18
  },
19
19
  "subtitle": {
20
20
  "type": "string"
21
+ },
22
+ "logo": {
23
+ "description": "SVG/PNG/JPEG/WebP shown on the intro and outro cards",
24
+ "type": "string",
25
+ "minLength": 1
26
+ },
27
+ "outro": {
28
+ "description": "Closing card after the video",
29
+ "type": "object",
30
+ "properties": {
31
+ "title": {
32
+ "type": "string",
33
+ "minLength": 1
34
+ },
35
+ "subtitle": {
36
+ "type": "string"
37
+ },
38
+ "url": {
39
+ "type": "string"
40
+ }
41
+ },
42
+ "required": [
43
+ "title"
44
+ ],
45
+ "additionalProperties": false
21
46
  }
22
47
  },
23
48
  "additionalProperties": false
@@ -83,6 +108,71 @@
83
108
  "type": "string",
84
109
  "minLength": 1
85
110
  }
111
+ },
112
+ "mock": {
113
+ "default": [],
114
+ "description": "Answer matching network requests with canned responses; the first match wins",
115
+ "type": "array",
116
+ "items": {
117
+ "type": "object",
118
+ "properties": {
119
+ "url": {
120
+ "type": "string",
121
+ "minLength": 1,
122
+ "description": "Playwright URL glob of the requests to answer, e.g. **/api/reports*"
123
+ },
124
+ "method": {
125
+ "description": "Only answer this HTTP method; others go to the network",
126
+ "type": "string",
127
+ "minLength": 1
128
+ },
129
+ "status": {
130
+ "default": 200,
131
+ "type": "integer",
132
+ "minimum": 100,
133
+ "maximum": 599
134
+ },
135
+ "json": {
136
+ "description": "Response body, sent as application/json"
137
+ },
138
+ "body": {
139
+ "description": "Raw response body",
140
+ "type": "string"
141
+ },
142
+ "headers": {
143
+ "type": "object",
144
+ "propertyNames": {
145
+ "type": "string"
146
+ },
147
+ "additionalProperties": {
148
+ "type": "string"
149
+ }
150
+ },
151
+ "delayMs": {
152
+ "description": "Wait this long before answering",
153
+ "type": "integer",
154
+ "minimum": 0,
155
+ "maximum": 30000
156
+ }
157
+ },
158
+ "required": [
159
+ "url"
160
+ ],
161
+ "additionalProperties": false
162
+ }
163
+ },
164
+ "clock": {
165
+ "description": "Start the page clock at this ISO date-time; it then runs normally",
166
+ "type": "string"
167
+ },
168
+ "dialogs": {
169
+ "default": "accept",
170
+ "description": "How to answer alert/confirm/prompt dialogs",
171
+ "type": "string",
172
+ "enum": [
173
+ "accept",
174
+ "dismiss"
175
+ ]
86
176
  }
87
177
  },
88
178
  "required": [
@@ -90,6 +180,133 @@
90
180
  ],
91
181
  "additionalProperties": false
92
182
  },
183
+ "hooks": {
184
+ "default": {},
185
+ "type": "object",
186
+ "properties": {
187
+ "before": {
188
+ "default": [],
189
+ "description": "Run before every check and record, e.g. to reset demo data",
190
+ "type": "array",
191
+ "items": {
192
+ "anyOf": [
193
+ {
194
+ "type": "object",
195
+ "properties": {
196
+ "run": {
197
+ "type": "string",
198
+ "minLength": 1,
199
+ "description": "Shell command, run in the script folder"
200
+ }
201
+ },
202
+ "required": [
203
+ "run"
204
+ ],
205
+ "additionalProperties": false
206
+ },
207
+ {
208
+ "type": "object",
209
+ "properties": {
210
+ "request": {
211
+ "type": "object",
212
+ "properties": {
213
+ "url": {
214
+ "type": "string",
215
+ "minLength": 1
216
+ },
217
+ "method": {
218
+ "default": "POST",
219
+ "type": "string",
220
+ "minLength": 1
221
+ },
222
+ "json": {},
223
+ "headers": {
224
+ "type": "object",
225
+ "propertyNames": {
226
+ "type": "string"
227
+ },
228
+ "additionalProperties": {
229
+ "type": "string"
230
+ }
231
+ }
232
+ },
233
+ "required": [
234
+ "url"
235
+ ],
236
+ "additionalProperties": false
237
+ }
238
+ },
239
+ "required": [
240
+ "request"
241
+ ],
242
+ "additionalProperties": false
243
+ }
244
+ ]
245
+ }
246
+ },
247
+ "after": {
248
+ "default": [],
249
+ "description": "Run after every check and record, even when it failed",
250
+ "type": "array",
251
+ "items": {
252
+ "anyOf": [
253
+ {
254
+ "type": "object",
255
+ "properties": {
256
+ "run": {
257
+ "type": "string",
258
+ "minLength": 1,
259
+ "description": "Shell command, run in the script folder"
260
+ }
261
+ },
262
+ "required": [
263
+ "run"
264
+ ],
265
+ "additionalProperties": false
266
+ },
267
+ {
268
+ "type": "object",
269
+ "properties": {
270
+ "request": {
271
+ "type": "object",
272
+ "properties": {
273
+ "url": {
274
+ "type": "string",
275
+ "minLength": 1
276
+ },
277
+ "method": {
278
+ "default": "POST",
279
+ "type": "string",
280
+ "minLength": 1
281
+ },
282
+ "json": {},
283
+ "headers": {
284
+ "type": "object",
285
+ "propertyNames": {
286
+ "type": "string"
287
+ },
288
+ "additionalProperties": {
289
+ "type": "string"
290
+ }
291
+ }
292
+ },
293
+ "required": [
294
+ "url"
295
+ ],
296
+ "additionalProperties": false
297
+ }
298
+ },
299
+ "required": [
300
+ "request"
301
+ ],
302
+ "additionalProperties": false
303
+ }
304
+ ]
305
+ }
306
+ }
307
+ },
308
+ "additionalProperties": false
309
+ },
93
310
  "setup": {
94
311
  "default": [],
95
312
  "type": "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mineo71/showtime",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Product demos, directed by AI: Claude writes a JSON script, showTime records your web app and renders a polished MP4",
5
5
  "keywords": [
6
6
  "demo",
package/skill/SKILL.md CHANGED
@@ -33,14 +33,19 @@ showtime setup # downloads the browsers it records with
33
33
  4. **Rehearse:** `showtime check demo/pitch.json`. On failure read the error, the ARIA snapshot it
34
34
  prints and `.showtime/<name>/check/failure.png`; fix the selector or add a `wait`, repeat until
35
35
  green.
36
- 5. **Record:** `showtime record demo/pitch.json` (runs in real time).
37
- 6. **Render:** `showtime render demo/pitch.json` → `demo/pitch.mp4`.
38
- 7. **QA:** `showtime frames demo/pitch.json`, then Read the PNGs it lists. Check that zoom lands on
39
- the right control, captions are legible and do not hide the action, and pacing feels right.
40
- 8. **Iterate:** edits to `show`, `style` or `meta` only need `render`. Edits to actions (`do`,
36
+ 5. **Lint:** `showtime lint demo/pitch.json` and fix every warning (long captions, pauses that
37
+ should be waits, zoom or spotlight on steps without a target).
38
+ 6. **Record:** `showtime record demo/pitch.json` (runs in real time).
39
+ 7. **Render:** `showtime render demo/pitch.json` → `demo/pitch.mp4`.
40
+ 8. **QA:** `showtime frames demo/pitch.json`, then Read the contact sheet it prints first
41
+ (`sheet.png`: every step in one image); open single stills only to look closer. Check that zoom
42
+ lands on the right control, captions are legible, and pacing feels right. Run `showtime lint`
43
+ again: with a take it also flags captions on screen too briefly to read.
44
+ 9. **Iterate:** edits to `show`, `style` or `meta` only need `render`. Edits to actions (`do`,
41
45
  targets, text, `hold`, `app`) need `record` again — `render` refuses a stale take.
42
46
 
43
- `showtime make <script>` runs check → record → render in one go.
47
+ `showtime make <script>` runs check → record → render in one go. Every command takes `--json`
48
+ and then prints exactly one JSON document on stdout.
44
49
 
45
50
  ## Long-running commands
46
51
 
@@ -48,14 +53,22 @@ showtime setup # downloads the browsers it records with
48
53
  Remotion. They routinely take several minutes. In Claude Code run them with `timeout: 600000`, or
49
54
  with `run_in_background: true` and wait for the completion notification. In Codex and other
50
55
  agents, give the command a timeout of at least 10 minutes.
51
- Never let a `record` be killed halfway: it replaces the previous take.
56
+ A `record` that fails or is stopped halfway leaves the previous take in place.
52
57
 
53
58
  ## Side effects
54
59
 
55
60
  `check` performs every action for real (it is a full run, just fast), and `record` repeats them.
56
61
  Flows that create data ("New project", sign-up, checkout) can collide on the second run
57
- ("name already exists"). Use a resettable demo account or seed data, make names unique, or undo
58
- the change in `setup`; after a green `check`, reset the data before `record`.
62
+ ("name already exists"). Add `hooks` that reset the data: `before` hooks run at the start of every
63
+ `check` and `record`, `after` hooks when they end.
64
+
65
+ ```
66
+ "hooks": { "before": [{ "run": "npm run db:reset" }] }
67
+ ```
68
+
69
+ A hook is `{ "run": "<shell command, run in the script folder>" }` or
70
+ `{ "request": { "url": "…", "method": "POST", "json": {…} } }`. If the project has no reset command,
71
+ ask the user for one rather than guessing.
59
72
 
60
73
  ## Example
61
74
 
@@ -97,7 +110,14 @@ the change in `setup`; after a green `check`, reset the data before `record`.
97
110
  (address-bar text), `padding`, `radius`, `shadow`, `font`, `accent`, `autoZoom`, `autoSpeed`, `maxZoom`.
98
111
  - **`app`:** `url`, `viewport`, `deviceScaleFactor`, `storageState` (Playwright storage-state JSON,
99
112
  path relative to the script), `colorScheme`, `locale`, `timezoneId`, `mask` (CSS selectors to blur —
100
- plain CSS only, not `text=` / `role=`).
113
+ plain CSS only, not `text=` / `role=`), `mock` (`[{ url: "**/api/x*", json | body, status?,
114
+ method?, delayMs? }]`), `clock` (ISO date-time the page clock starts at; first matching mock wins), `dialogs` (`accept` | `dismiss`).
115
+ Relative `goto` URLs resolve against `app.url`.
116
+ - **`meta`:** `title`, `subtitle` (intro card), `logo` (SVG/PNG/JPEG/WebP), `outro`
117
+ (`{ title, subtitle?, url? }`, closing card).
118
+ - **`hooks`:** `before` / `after` lists of `{ run }` or `{ request }`.
119
+ - **Brand file:** `showtime.config.json` in the script's folder or above holds default `style` and
120
+ `meta` (logo, outro) for every script; the script's own fields win.
101
121
  - Output lands next to the script; working files in `.showtime/<script-name>/`.
102
122
 
103
123
  ## Direction tips
@@ -109,4 +129,8 @@ the change in `setup`; after a green `check`, reset the data before `record`.
109
129
  - Never `pause` to wait for loading — use `wait {for}`; auto-speed compresses the loader.
110
130
  - Use `setup` for cookie banners and onboarding, `app.storageState` to start logged in, `app.mask`
111
131
  to blur personal data.
132
+ - Mock APIs that are slow, random or need real accounts (`app.mock`), and pin the date
133
+ (`app.clock`) so every take shows the same numbers and timestamps.
134
+ - End pitch demos with `meta.outro`: the product name and the URL to visit. Put brand colours, logo
135
+ and outro in `showtime.config.json` when a project has several demos.
112
136
  - Keep the default 1440×900 viewport at `deviceScaleFactor` 2 for crisp 16:9 zooms.
package/src/cli/format.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import type { Issue } from '../lint';
2
3
  import type { CheckReport } from '../runner/check';
3
4
 
4
5
  const ARIA_LINES = 80;
@@ -17,3 +18,12 @@ export function formatCheck(report: CheckReport): string {
17
18
  ...aria.map((line) => ` ${line}`),
18
19
  ].join('\n');
19
20
  }
21
+
22
+ export function formatLint(issues: Issue[], timing: boolean): string {
23
+ const lines = issues.map((i) => `${i.level === 'warn' ? '⚠' : 'ℹ'} ${i.where} ${i.message} [${i.rule}]`);
24
+ const warns = issues.filter((i) => i.level === 'warn').length;
25
+ const notes = issues.length - warns;
26
+ lines.push(issues.length ? `${warns} warning${warns === 1 ? '' : 's'}, ${notes} note${notes === 1 ? '' : 's'}` : '✓ no issues');
27
+ if (!timing) lines.push('(record a take to also check timing)');
28
+ return lines.join('\n');
29
+ }