screencast-axi 0.1.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/LICENSE +21 -0
- package/README.md +555 -0
- package/dist/bin/screencast-axi.d.ts +2 -0
- package/dist/bin/screencast-axi.js +16 -0
- package/dist/bin/screencast-axi.js.map +1 -0
- package/dist/src/auth/strategies.d.ts +54 -0
- package/dist/src/auth/strategies.js +137 -0
- package/dist/src/auth/strategies.js.map +1 -0
- package/dist/src/auth/types.d.ts +65 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/auth/types.js.map +1 -0
- package/dist/src/browser.d.ts +100 -0
- package/dist/src/browser.js +168 -0
- package/dist/src/browser.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +160 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/auth.d.ts +4 -0
- package/dist/src/commands/auth.js +207 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/check.d.ts +12 -0
- package/dist/src/commands/check.js +103 -0
- package/dist/src/commands/check.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +15 -0
- package/dist/src/commands/doctor.js +131 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/guide.d.ts +6 -0
- package/dist/src/commands/guide.js +257 -0
- package/dist/src/commands/guide.js.map +1 -0
- package/dist/src/commands/home.d.ts +12 -0
- package/dist/src/commands/home.js +78 -0
- package/dist/src/commands/home.js.map +1 -0
- package/dist/src/commands/list.d.ts +6 -0
- package/dist/src/commands/list.js +128 -0
- package/dist/src/commands/list.js.map +1 -0
- package/dist/src/commands/record.d.ts +7 -0
- package/dist/src/commands/record.js +266 -0
- package/dist/src/commands/record.js.map +1 -0
- package/dist/src/commands/scaffold.d.ts +4 -0
- package/dist/src/commands/scaffold.js +122 -0
- package/dist/src/commands/scaffold.js.map +1 -0
- package/dist/src/commands/setup.d.ts +14 -0
- package/dist/src/commands/setup.js +137 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config.d.ts +126 -0
- package/dist/src/config.js +275 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/director.d.ts +217 -0
- package/dist/src/director.js +415 -0
- package/dist/src/director.js.map +1 -0
- package/dist/src/duration.d.ts +33 -0
- package/dist/src/duration.js +68 -0
- package/dist/src/duration.js.map +1 -0
- package/dist/src/encode.d.ts +67 -0
- package/dist/src/encode.js +225 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/errors.d.ts +19 -0
- package/dist/src/errors.js +27 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/flags.d.ts +39 -0
- package/dist/src/flags.js +130 -0
- package/dist/src/flags.js.map +1 -0
- package/dist/src/forensics.d.ts +51 -0
- package/dist/src/forensics.js +107 -0
- package/dist/src/forensics.js.map +1 -0
- package/dist/src/hooks.d.ts +9 -0
- package/dist/src/hooks.js +20 -0
- package/dist/src/hooks.js.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inventory.d.ts +45 -0
- package/dist/src/inventory.js +89 -0
- package/dist/src/inventory.js.map +1 -0
- package/dist/src/manifest.d.ts +89 -0
- package/dist/src/manifest.js +137 -0
- package/dist/src/manifest.js.map +1 -0
- package/dist/src/output.d.ts +8 -0
- package/dist/src/output.js +2 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/overlay.d.ts +105 -0
- package/dist/src/overlay.js +302 -0
- package/dist/src/overlay.js.map +1 -0
- package/dist/src/reference.d.ts +1 -0
- package/dist/src/reference.js +108 -0
- package/dist/src/reference.js.map +1 -0
- package/dist/src/run.d.ts +82 -0
- package/dist/src/run.js +321 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/skill.d.ts +12 -0
- package/dist/src/skill.js +69 -0
- package/dist/src/skill.js.map +1 -0
- package/dist/src/toolchain.d.ts +62 -0
- package/dist/src/toolchain.js +127 -0
- package/dist/src/toolchain.js.map +1 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +18 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +10 -0
- package/dist/src/version.js +11 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +93 -0
- package/skills/screencast-axi/SKILL.md +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valentyn Morenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
# screencast-axi
|
|
2
|
+
|
|
3
|
+
Record a scripted browser workflow as a watchable clip: a drawn cursor, burnt-in captions, and
|
|
4
|
+
mp4 + webm + a poster out the other end. Built to [AXI](https://github.com/kunchenguid/axi)
|
|
5
|
+
conventions, so an agent can drive it as comfortably as a person can.
|
|
6
|
+
|
|
7
|
+
<picture>
|
|
8
|
+
<source srcset="docs/demo.anim.webp" type="image/webp">
|
|
9
|
+
<img src="docs/demo.gif" alt="Three acts: a prompt asking for a clip, the scenario that gets written, and the recording it produces in a real app." width="640">
|
|
10
|
+
</picture>
|
|
11
|
+
|
|
12
|
+
<sup>The ask, the scenario that came back, and the take it produced - itself recorded by this tool,
|
|
13
|
+
from [`demo/scenarios/demo.ts`](demo/scenarios/demo.ts). Re-record it with `pnpm demo`.</sup>
|
|
14
|
+
|
|
15
|
+
> **Status: 0.x and not yet published.** The recorder works end to end; the command surface is
|
|
16
|
+
> still growing. See [Roadmap](#roadmap).
|
|
17
|
+
|
|
18
|
+
## What the tool does
|
|
19
|
+
|
|
20
|
+
The clips below were produced by this repo - the scenarios are in
|
|
21
|
+
[`demo/scenarios/`](demo/scenarios/), and `pnpm demo:usecases` re-records them.
|
|
22
|
+
|
|
23
|
+
### Point it at any site and describe the workflow
|
|
24
|
+
|
|
25
|
+
There is no integration to write: no plugin for the site, no fixtures, no test IDs added to the
|
|
26
|
+
page. A scenario names what to do, and the recorder does it.
|
|
27
|
+
|
|
28
|
+
> _"Record a clip of searching Wikipedia for Ada Lovelace, opening the article, and jumping to the
|
|
29
|
+
> Work section. Around ten seconds."_
|
|
30
|
+
|
|
31
|
+
<picture>
|
|
32
|
+
<source srcset="docs/usecase-anysite.anim.webp" type="image/webp">
|
|
33
|
+
<img src="docs/usecase-anysite.gif" alt="Searching Wikipedia, watching live suggestions arrive, opening the article and jumping to a section." width="480">
|
|
34
|
+
</picture>
|
|
35
|
+
|
|
36
|
+
<sup>The whole scenario is [50 lines](demo/scenarios/anysite.ts), most of it narration and
|
|
37
|
+
comments.</sup>
|
|
38
|
+
|
|
39
|
+
### Records a sign-in, or skips past one
|
|
40
|
+
|
|
41
|
+
> _"Sign in to our staging dashboard and record a 15 second clip of creating an invoice. I will
|
|
42
|
+
> do the login myself."_
|
|
43
|
+
|
|
44
|
+
A login is an ordinary workflow as far as the recorder is concerned: a field, a field, a button.
|
|
45
|
+
It is worth filming, because it is the part of a product most demos skip.
|
|
46
|
+
|
|
47
|
+
<picture>
|
|
48
|
+
<source srcset="docs/usecase-login.anim.webp" type="image/webp">
|
|
49
|
+
<img src="docs/usecase-login.gif" alt="A real login form being filled in and submitted, landing on the page behind it." width="460">
|
|
50
|
+
</picture>
|
|
51
|
+
|
|
52
|
+
For a real app you usually want the opposite - the clip should open already through the door
|
|
53
|
+
rather than spend its first seconds on a form. So signing in becomes a one-time human step, and
|
|
54
|
+
this is what that looks like from your side:
|
|
55
|
+
|
|
56
|
+
> **A real Chrome window opens on your screen, at the site you are recording.**
|
|
57
|
+
> Sign in there however that site wants: password, SSO, a magic link, two-factor, a passkey.
|
|
58
|
+
> Then **close the window** - closing it is the signal that you are done.
|
|
59
|
+
> Nothing is ever typed into the terminal, and no credential passes through this package. The
|
|
60
|
+
> session is saved into a Chrome profile on your machine, and every later take reuses it.
|
|
61
|
+
|
|
62
|
+
An agent can open that window for you rather than making you retype a command, but it cannot sign
|
|
63
|
+
in for you and will not try: without `--interactive` it stops and says a person is needed, and it
|
|
64
|
+
refuses outright where no window could appear, such as a headless CI box. The wait is bounded, so
|
|
65
|
+
it can never hang.
|
|
66
|
+
|
|
67
|
+
For a login you can script, an `AuthStrategy` object in the config does the same job in typed code
|
|
68
|
+
([worked example](demo/auth/form-login.ts)). Either way it runs before recording starts, so none
|
|
69
|
+
of it lands in the clip.
|
|
70
|
+
|
|
71
|
+
### Renders at phone and tablet viewports
|
|
72
|
+
|
|
73
|
+
> _"Record our onboarding on an iPhone, portrait, about fifteen seconds - I want it for the app
|
|
74
|
+
> store listing."_
|
|
75
|
+
|
|
76
|
+
A device preset does more than set a width. Its `isMobile` and `hasTouch` flags decide whether the
|
|
77
|
+
site's own `@media (hover: none)` and touch rules apply at all, and it carries the right user
|
|
78
|
+
agent - so this is the mobile layout the site actually serves, not a desktop squeezed narrow.
|
|
79
|
+
Playwright ships 140+ presets; `--viewport 390x844` covers the rest.
|
|
80
|
+
|
|
81
|
+
<picture>
|
|
82
|
+
<source srcset="docs/usecase-mobile.anim.webp" type="image/webp">
|
|
83
|
+
<img src="docs/usecase-mobile.gif" alt="The same workflow recorded in a portrait phone viewport." width="240">
|
|
84
|
+
</picture>
|
|
85
|
+
|
|
86
|
+
<sup>The video is captured at the CSS viewport - 390x664 here - because Playwright composites the
|
|
87
|
+
page into the video canvas without scaling up. `deviceScaleFactor` still changes how the page
|
|
88
|
+
renders and which images it picks, but not the resolution of the recording.</sup>
|
|
89
|
+
|
|
90
|
+
### Shows you what it is doing
|
|
91
|
+
|
|
92
|
+
A scenario is arbitrary code driving a real browser, often one signed into your own account, so
|
|
93
|
+
"is this safe to run" deserves a better answer than "read the TypeScript".
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
screencast-axi rehearse <id> --headed
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`--headed` runs it in a real window instead of hidden, so you can watch the whole thing. It does
|
|
100
|
+
not change the output - the clip is identical either way - so it costs nothing but the window.
|
|
101
|
+
|
|
102
|
+
Watching answers the question once, though, and only while you sit there. So a rehearsal also
|
|
103
|
+
prints what it did, in a form you can read before running, diff after an edit, and keep:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
$ screencast-axi rehearse usecase-login
|
|
107
|
+
|
|
108
|
+
rehearsed: usecase-login
|
|
109
|
+
duration_s: 10.8
|
|
110
|
+
pace: 1
|
|
111
|
+
viewport: 900x540
|
|
112
|
+
steps[4]: A real login form,"Username, then password",Submitted for real,And the page behind it
|
|
113
|
+
hosts[1]: the-internet.herokuapp.com
|
|
114
|
+
performed[10]:
|
|
115
|
+
- at_s: 0.2
|
|
116
|
+
did: goto
|
|
117
|
+
target: "https://the-internet.herokuapp.com/login"
|
|
118
|
+
- at_s: 1.8
|
|
119
|
+
did: waitFor
|
|
120
|
+
target: #username
|
|
121
|
+
- at_s: 1.8
|
|
122
|
+
did: step
|
|
123
|
+
detail: A real login form
|
|
124
|
+
- at_s: 2.9
|
|
125
|
+
did: step
|
|
126
|
+
detail: "Username, then password"
|
|
127
|
+
- at_s: 2.9
|
|
128
|
+
did: type
|
|
129
|
+
target: #username
|
|
130
|
+
detail: tomsmith
|
|
131
|
+
- at_s: 4.3
|
|
132
|
+
did: type
|
|
133
|
+
target: #password
|
|
134
|
+
detail: •••••••• (20 chars)
|
|
135
|
+
- at_s: 7.1
|
|
136
|
+
did: step
|
|
137
|
+
detail: Submitted for real
|
|
138
|
+
- at_s: 7.1
|
|
139
|
+
did: click
|
|
140
|
+
target: "button[type=submit]"
|
|
141
|
+
- at_s: 8.2
|
|
142
|
+
did: waitFor
|
|
143
|
+
target: h2
|
|
144
|
+
- at_s: 8.8
|
|
145
|
+
did: step
|
|
146
|
+
detail: And the page behind it
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Note the password: a field the log would otherwise leak records its shape and nothing else.
|
|
150
|
+
|
|
151
|
+
`hosts` is the short answer to where it went, taken from the pages the browser actually reached -
|
|
152
|
+
so a redirect or a navigation buried in `setup()` shows up too. A list of one host reads very
|
|
153
|
+
differently from a list of nine.
|
|
154
|
+
|
|
155
|
+
### Aims a clip at a length
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
screencast-axi record tour --duration 30s
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
One measuring pass, then it solves for the pace that lands near the target. A take is
|
|
162
|
+
`fixed + pace x scalable` - the site's own waits do not get slower because the recorder does - so
|
|
163
|
+
the solve uses the measured split rather than assuming everything scales. Pace is clamped to a
|
|
164
|
+
watchable range, and a target outside it is reported rather than obeyed.
|
|
165
|
+
|
|
166
|
+
### Emits what the destination needs
|
|
167
|
+
|
|
168
|
+
mp4 and webm every time, plus a poster frame. `--gif` and `--webp` add looping images for the
|
|
169
|
+
places a `<video>` does not render - a README, an npm page, an email. Every image on this page is
|
|
170
|
+
one of them. See [Output formats](#output-formats) for what each costs.
|
|
171
|
+
|
|
172
|
+
Drag-and-drop is handled too, including the HTML5 protocol that Chromium will not synthesise from
|
|
173
|
+
mouse events - the interaction most likely to look like a teleport if a recorder cuts corners.
|
|
174
|
+
|
|
175
|
+
## Why a script, not a screen recorder
|
|
176
|
+
|
|
177
|
+
A screencast is a performance with a script, not a recording of work being done. What makes one
|
|
178
|
+
watchable is a set of constants - a settle before each click, an eased pointer glide, a consistent
|
|
179
|
+
hold on each caption - and constants only help if you can run the same thing again and get the
|
|
180
|
+
same thing back. A scenario file survives a product change, gets reviewed in a pull request, and
|
|
181
|
+
can be re-cut at a different length without re-deciding anything.
|
|
182
|
+
|
|
183
|
+
## Quick start
|
|
184
|
+
|
|
185
|
+
No config needed. Point it at any site:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
npx -y screencast-axi scaffold product-tour --url https://example.com
|
|
189
|
+
# fill in the run() body
|
|
190
|
+
npx -y screencast-axi rehearse ./scenarios/product-tour.ts
|
|
191
|
+
npx -y screencast-axi record ./scenarios/product-tour.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`rehearse` runs the scenario without encoding, so a stale selector surfaces in seconds rather than
|
|
195
|
+
a minute - and the failure comes back with the URL it reached, a screenshot, and what each part of
|
|
196
|
+
the selector actually matched.
|
|
197
|
+
|
|
198
|
+
A scenario is TypeScript:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { defineScenario } from "screencast-axi";
|
|
202
|
+
|
|
203
|
+
export default defineScenario({
|
|
204
|
+
id: "product-tour",
|
|
205
|
+
title: "A three-stop tour",
|
|
206
|
+
description: "The pages that matter, in order.",
|
|
207
|
+
steps: ["Where the work lives", "How it gets organised", "And what comes out"],
|
|
208
|
+
|
|
209
|
+
async run(d) {
|
|
210
|
+
await d.tour([
|
|
211
|
+
{ path: "/", step: 0, scroll: 0.6 },
|
|
212
|
+
{ path: "/features", step: 1, scroll: 0.5 },
|
|
213
|
+
{ path: "/pricing", step: 2 },
|
|
214
|
+
]);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Each line in `steps` goes on screen once, in order. The same array becomes the burnt-in caption
|
|
220
|
+
and the manifest's step list, so the written workflow cannot drift from the recorded one - a take
|
|
221
|
+
that skips a line fails.
|
|
222
|
+
|
|
223
|
+
## Install
|
|
224
|
+
|
|
225
|
+
The skill is installed from GitHub; the CLI is pulled on demand, so there is nothing global:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
npx skills add Valzon/screencast-axi --skill screencast-axi -g
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Prerequisites
|
|
232
|
+
|
|
233
|
+
| Piece | Required | How it is found |
|
|
234
|
+
| ----------- | -------- | -------------------------------------------------------------------- |
|
|
235
|
+
| Node | >= 20 | |
|
|
236
|
+
| Chromium | yes | Playwright downloads it |
|
|
237
|
+
| `ffmpeg` | yes | `$SCREENCAST_FFMPEG`, then an installed `ffmpeg-static`, then `PATH` |
|
|
238
|
+
| WebP poster | optional | ffmpeg's `libwebp`, else `cwebp`, else a PNG poster |
|
|
239
|
+
|
|
240
|
+
ffmpeg is not bundled: it is 80MB+ per platform, and _which_ build you have matters - many builds
|
|
241
|
+
(Homebrew's among them) ship without `libwebp`, which is why the poster has a fallback chain
|
|
242
|
+
rather than one hard requirement. If you would rather not install it system-wide,
|
|
243
|
+
`pnpm add -D ffmpeg-static` and the cascade finds it.
|
|
244
|
+
|
|
245
|
+
**Platforms:** macOS and Linux are tested, including in CI. Windows is intended to work but is
|
|
246
|
+
**unverified** - the known risks are argument quoting in spawned ffmpeg filter strings, path
|
|
247
|
+
separators inside those arguments, and Chrome's lock on a persistent profile directory.
|
|
248
|
+
|
|
249
|
+
## Recording a page behind a login
|
|
250
|
+
|
|
251
|
+
The strategy that needs no code for the site you are recording is `profileAuth()`. A person runs
|
|
252
|
+
this once:
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
npx -y screencast-axi auth login --interactive
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A browser opens, they sign in however that site wants - OAuth, SSO, a magic link, two-factor - and
|
|
259
|
+
close the window. The session lives in a persistent Chrome profile and every take reuses it. No
|
|
260
|
+
credential is handled by this package.
|
|
261
|
+
|
|
262
|
+
`auth login` never reads stdin, so an agent can open the window on the user's screen rather than
|
|
263
|
+
making them retype a command. It cannot hang: the wait is bounded, and it refuses up front where
|
|
264
|
+
no window could appear.
|
|
265
|
+
|
|
266
|
+
Also shipped: `storageStateAuth({ path })` for a portable session file, and
|
|
267
|
+
`basicAuth({ username, password })` for staging environments. Anything else is an `AuthStrategy`
|
|
268
|
+
object written in the config - typed and debuggable rather than a shelled-out script.
|
|
269
|
+
|
|
270
|
+
> A scenario that creates or drags something **writes to whatever it is pointed at**. Prefer
|
|
271
|
+
> read-only scenarios against production, or point at staging.
|
|
272
|
+
|
|
273
|
+
## Seeing what a scenario does
|
|
274
|
+
|
|
275
|
+
A scenario is arbitrary code driving a real browser, often one signed into your own account, so
|
|
276
|
+
"what will this actually do" deserves a better answer than "read the TypeScript". Two:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
screencast-axi rehearse <id> # prints every action it took
|
|
280
|
+
screencast-axi rehearse <id> --headed # and shows you it happening
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
A rehearsal prints `performed` - every goto, click, typed value, drag and scroll, in order, with
|
|
284
|
+
timings - and `hosts`, the short answer to where the script went. `--headed` opens a real window
|
|
285
|
+
and does not change the output; the clip is identical either way.
|
|
286
|
+
|
|
287
|
+
## Output formats
|
|
288
|
+
|
|
289
|
+
Every take produces an mp4, a webm and a poster. Looping images are opt-in with `--gif` and
|
|
290
|
+
`--webp`, for the places a `<video>` does not render - a README, an npm page, an email.
|
|
291
|
+
|
|
292
|
+
Measured on a real 16.7s app screencast, all at 800px and 15fps:
|
|
293
|
+
|
|
294
|
+
| Format | Size | vs mp4 |
|
|
295
|
+
| ------------- | -------- | ------ |
|
|
296
|
+
| mp4 (h264) | 182 KB | 1x |
|
|
297
|
+
| animated WebP | 944 KB | 5.2x |
|
|
298
|
+
| GIF | 2,034 KB | 11.2x |
|
|
299
|
+
|
|
300
|
+
Prefer WebP where it renders: same content, roughly half the bytes, full colour rather than a
|
|
301
|
+
256-entry palette. Quality is not the deciding factor for flat app UI - 192 colours plus dithering
|
|
302
|
+
keeps small text legible - but a GIF has no controls, no seeking, no poster frame, cannot be
|
|
303
|
+
paused, and ignores `prefers-reduced-motion`. It is an export, not a storage format.
|
|
304
|
+
|
|
305
|
+
## Configuration
|
|
306
|
+
|
|
307
|
+
Optional. `screencast.config.ts` at the repo root, found by walking up from the working directory:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { defineConfig, profileAuth } from "screencast-axi";
|
|
311
|
+
|
|
312
|
+
export default defineConfig({
|
|
313
|
+
baseUrl: "http://localhost:3000",
|
|
314
|
+
scenarios: ["scenarios/*.ts"],
|
|
315
|
+
outDir: "public/demos",
|
|
316
|
+
viewport: { width: 1440, height: 900 },
|
|
317
|
+
|
|
318
|
+
browser: { profileDir: ".screencast/profile" },
|
|
319
|
+
auth: profileAuth({ signedInSelector: "[data-testid=user-menu]" }),
|
|
320
|
+
|
|
321
|
+
overlay: {
|
|
322
|
+
accent: "#4f46e5",
|
|
323
|
+
// Page chrome that should not end up in the footage.
|
|
324
|
+
hideSelectors: ["#cookie-banner", "nextjs-portal"],
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Relative paths resolve against the config file, never the shell's working directory, so a command
|
|
330
|
+
means the same thing from anywhere in a repo.
|
|
331
|
+
|
|
332
|
+
## Mobile, and aiming at a length
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
screencast-axi record tour --device "iPhone 13" --orientation portrait --duration 20s
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Device presets come from Playwright's registry, so a phone clip is captured at its real device
|
|
339
|
+
pixels rather than its CSS viewport - the difference between readable UI text and a smear.
|
|
340
|
+
|
|
341
|
+
`--duration` runs one measuring pass, then solves for the pace that lands near the target. A take
|
|
342
|
+
is `fixed + pace x scalable`: the site's own waits do not get slower because the recorder does, so
|
|
343
|
+
the solve uses the measured split rather than assuming everything scales.
|
|
344
|
+
|
|
345
|
+
## Reading the manifest
|
|
346
|
+
|
|
347
|
+
Every take writes `manifest.json` beside the clips. A site can read it at build time without
|
|
348
|
+
pulling in Playwright or a browser:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { readManifest, clipFilesFor } from "screencast-axi/manifest";
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
That entry point has no runtime dependencies at all.
|
|
355
|
+
|
|
356
|
+
## Command reference
|
|
357
|
+
|
|
358
|
+
<!-- reference:start -->
|
|
359
|
+
|
|
360
|
+
Every command takes `--help`. Flags always come after the command:
|
|
361
|
+
`screencast-axi <command> [args] [flags]`. An unknown flag is a usage error rather than
|
|
362
|
+
something quietly ignored.
|
|
363
|
+
|
|
364
|
+
#### `init`
|
|
365
|
+
|
|
366
|
+
Write a config, a scenarios directory and a gitignore entry.
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
screencast-axi init
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
| Flag | What it does |
|
|
373
|
+
| ------------- | --------------------------------- |
|
|
374
|
+
| `--out <dir>` | Where clips should be written. |
|
|
375
|
+
| `--url <url>` | Base URL for the site you record. |
|
|
376
|
+
| `--force` | Overwrite an existing config. |
|
|
377
|
+
|
|
378
|
+
#### `scaffold`
|
|
379
|
+
|
|
380
|
+
Write a scenario skeleton so the boilerplate is never what goes wrong.
|
|
381
|
+
|
|
382
|
+
```sh
|
|
383
|
+
screencast-axi scaffold <id>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
| Flag | What it does |
|
|
387
|
+
| ----------------- | ----------------------------------------- |
|
|
388
|
+
| `--url <url>` | Base URL the scenario opens. |
|
|
389
|
+
| `--title <text>` | Human title for the clip. |
|
|
390
|
+
| `--dir <path>` | Where to write it. |
|
|
391
|
+
| `--tour <value>` | Write an n-stop page walkthrough instead. |
|
|
392
|
+
| `--device <name>` | Playwright device preset. |
|
|
393
|
+
|
|
394
|
+
#### `rehearse`
|
|
395
|
+
|
|
396
|
+
Run a scenario without recording, and print every action it took.
|
|
397
|
+
|
|
398
|
+
```sh
|
|
399
|
+
screencast-axi rehearse <id|path...>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
| Flag | What it does |
|
|
403
|
+
| ------------------- | ----------------------------------------- |
|
|
404
|
+
| `--config <path>` | Path to a config file. |
|
|
405
|
+
| `--base-url <url>` | Override the scenario's base URL. |
|
|
406
|
+
| `--pace <value>` | Speed multiplier; lower is faster. |
|
|
407
|
+
| `--device <name>` | Playwright device preset. |
|
|
408
|
+
| `--viewport <WxH>` | Explicit size, e.g. 390x844. |
|
|
409
|
+
| `--orientation <o>` | portrait or landscape. |
|
|
410
|
+
| `--headed` | Watch it happen in a real browser window. |
|
|
411
|
+
| `--auth <name>` | Named auth strategy from the config. |
|
|
412
|
+
| `--no-auth` | Record signed out. |
|
|
413
|
+
|
|
414
|
+
#### `record`
|
|
415
|
+
|
|
416
|
+
Run a scenario for real and encode the clip.
|
|
417
|
+
|
|
418
|
+
```sh
|
|
419
|
+
screencast-axi record <id|path...>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
| Flag | What it does |
|
|
423
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
424
|
+
| `--config <path>` | Path to a config file. |
|
|
425
|
+
| `--base-url <url>` | Override the scenario's base URL. |
|
|
426
|
+
| `--pace <value>` | Speed multiplier; lower is faster. |
|
|
427
|
+
| `--device <name>` | Playwright device preset. |
|
|
428
|
+
| `--viewport <WxH>` | Explicit size, e.g. 390x844. |
|
|
429
|
+
| `--orientation <o>` | portrait or landscape. |
|
|
430
|
+
| `--headed` | Watch it happen in a real browser window. |
|
|
431
|
+
| `--auth <name>` | Named auth strategy from the config. |
|
|
432
|
+
| `--no-auth` | Record signed out. |
|
|
433
|
+
| `--duration <30s>` | Aim for this length, e.g. 30s (measures first, then solves for pace). |
|
|
434
|
+
| `--out <dir>` | Output directory. |
|
|
435
|
+
| `--all` | Record every scenario the config lists. |
|
|
436
|
+
| `--full` | Include the full action log. |
|
|
437
|
+
| `--gif` | Also emit a looping GIF. |
|
|
438
|
+
| `--webp` | Also emit a looping WebP (half a GIF's size). |
|
|
439
|
+
| `--loop-width <value>` | Width of the looping formats. |
|
|
440
|
+
| `--loop-fps <value>` | Frame rate of the looping formats. |
|
|
441
|
+
| `--keep-raw` | Keep the raw capture for inspection. |
|
|
442
|
+
|
|
443
|
+
#### `list`
|
|
444
|
+
|
|
445
|
+
Every scenario, with what needs re-shooting.
|
|
446
|
+
|
|
447
|
+
```sh
|
|
448
|
+
screencast-axi list
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
| Flag | What it does |
|
|
452
|
+
| ----------------- | ----------------------------------------- |
|
|
453
|
+
| `--config <path>` | Path to a config file. |
|
|
454
|
+
| `--full` | Every field, not the four-column summary. |
|
|
455
|
+
| `--stale` | Only what needs re-shooting. |
|
|
456
|
+
| `--tag <name>` | Filter by scenario tag. Repeatable. |
|
|
457
|
+
|
|
458
|
+
#### `show`
|
|
459
|
+
|
|
460
|
+
One clip in full: files, sizes, narration, when it was shot.
|
|
461
|
+
|
|
462
|
+
```sh
|
|
463
|
+
screencast-axi show <id>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
| Flag | What it does |
|
|
467
|
+
| ----------------- | ------------------------------ |
|
|
468
|
+
| `--config <path>` | Path to a config file. |
|
|
469
|
+
| `--full` | Include untruncated step text. |
|
|
470
|
+
|
|
471
|
+
#### `check`
|
|
472
|
+
|
|
473
|
+
Cross-reference the manifest, the scenarios and the files on disk.
|
|
474
|
+
|
|
475
|
+
```sh
|
|
476
|
+
screencast-axi check
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
| Flag | What it does |
|
|
480
|
+
| ----------------- | --------------------------------------------------------------- |
|
|
481
|
+
| `--config <path>` | Path to a config file. |
|
|
482
|
+
| `--fix-orphans` | Delete manifest entries and media with no scenario behind them. |
|
|
483
|
+
|
|
484
|
+
#### `doctor`
|
|
485
|
+
|
|
486
|
+
Check everything a recording needs, in one pass.
|
|
487
|
+
|
|
488
|
+
```sh
|
|
489
|
+
screencast-axi doctor
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| Flag | What it does |
|
|
493
|
+
| ----------------- | ---------------------- |
|
|
494
|
+
| `--config <path>` | Path to a config file. |
|
|
495
|
+
|
|
496
|
+
#### `setup`
|
|
497
|
+
|
|
498
|
+
Install the browser; name anything you must install yourself.
|
|
499
|
+
|
|
500
|
+
```sh
|
|
501
|
+
screencast-axi setup
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
| Flag | What it does |
|
|
505
|
+
| ----------------- | ----------------------------------- |
|
|
506
|
+
| `--browsers-only` | Install Chromium and stop. |
|
|
507
|
+
| `--scope <s>` | For `setup hooks`: user or project. |
|
|
508
|
+
|
|
509
|
+
#### `auth`
|
|
510
|
+
|
|
511
|
+
Sign in by hand once, or check the saved session still works.
|
|
512
|
+
|
|
513
|
+
```sh
|
|
514
|
+
screencast-axi auth login|check [name]
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
| Flag | What it does |
|
|
518
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
519
|
+
| `--config <path>` | Path to a config file. |
|
|
520
|
+
| `--base-url <url>` | Site to sign in to. |
|
|
521
|
+
| `--interactive` | Required for `login`: opens a real browser. |
|
|
522
|
+
| `--save-state <path>` | Write a Playwright storage state here as well. |
|
|
523
|
+
| `--headed` | Show the browser for `check`. |
|
|
524
|
+
| `--wait <5m>` | How long `login` waits for the window to close (default 5m). |
|
|
525
|
+
|
|
526
|
+
#### `guide`
|
|
527
|
+
|
|
528
|
+
Topic-sized guidance, pulled one topic at a time.
|
|
529
|
+
|
|
530
|
+
```sh
|
|
531
|
+
screencast-axi guide [topic]
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
_No flags._
|
|
535
|
+
<!-- reference:end -->
|
|
536
|
+
|
|
537
|
+
## Roadmap
|
|
538
|
+
|
|
539
|
+
Publishing to npm. The command surface is complete; `setup hooks` is declared but not implemented
|
|
540
|
+
and says so rather than pretending.
|
|
541
|
+
|
|
542
|
+
## Contributing
|
|
543
|
+
|
|
544
|
+
```sh
|
|
545
|
+
pnpm install
|
|
546
|
+
pnpm build && pnpm test && pnpm typecheck && pnpm format
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`skills/screencast-axi/SKILL.md` is generated from `src/skill.ts` - edit the generator, run
|
|
550
|
+
`pnpm build:skill`, and commit the result. CI fails if the two drift, and the generator throws if
|
|
551
|
+
the stub grows past its character cap.
|
|
552
|
+
|
|
553
|
+
## License
|
|
554
|
+
|
|
555
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { tryFastPath } from "axi-sdk-js/fast-path";
|
|
3
|
+
import { VERSION } from "../src/version.js";
|
|
4
|
+
// `screencast-axi --help | head` closes the pipe early, and an unhandled EPIPE
|
|
5
|
+
// turns that into a stack trace. Piping output somewhere that stops reading is
|
|
6
|
+
// ordinary shell use, not an error worth reporting.
|
|
7
|
+
process.stdout.on("error", (error) => {
|
|
8
|
+
if (error.code === "EPIPE")
|
|
9
|
+
process.exit(0);
|
|
10
|
+
throw error;
|
|
11
|
+
});
|
|
12
|
+
if (!tryFastPath(process.argv.slice(2), { version: VERSION })) {
|
|
13
|
+
const { main } = await import("../src/cli.js");
|
|
14
|
+
await main();
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=screencast-axi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screencast-axi.js","sourceRoot":"","sources":["../../bin/screencast-axi.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,+EAA+E;AAC/E,+EAA+E;AAC/E,oDAAoD;AACpD,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;IAC1D,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,KAAK,CAAC;AACd,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;IAC/C,MAAM,IAAI,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AuthStrategy } from "./types.js";
|
|
2
|
+
/** Signed-out. The default, and correct for most public surfaces. */
|
|
3
|
+
export declare function noAuth(): AuthStrategy;
|
|
4
|
+
export interface ProfileAuthOptions {
|
|
5
|
+
/** Where to land after signing in, so `auth check` can prove it worked. */
|
|
6
|
+
readonly landingPath?: string;
|
|
7
|
+
/** Something only a signed-in page renders. The strongest check available. */
|
|
8
|
+
readonly signedInSelector?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Cookie the app sets once its own session is live.
|
|
11
|
+
*
|
|
12
|
+
* `name=value` checks the value too. Presence alone is a weak test: plenty
|
|
13
|
+
* of sites set the same cookie to a signed-out value (GitHub sends
|
|
14
|
+
* `logged_in=no`), so a name-only check quietly passes when it should not.
|
|
15
|
+
*/
|
|
16
|
+
readonly signedInCookie?: string;
|
|
17
|
+
readonly label?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sign in once by hand; every take afterwards reuses the session.
|
|
21
|
+
*
|
|
22
|
+
* This is the strategy that makes the tool work on a site nobody wrote a
|
|
23
|
+
* strategy for. The browser opens, a person signs in however that site wants -
|
|
24
|
+
* OAuth, SSO, a magic link, two-factor - and closes the window. The session
|
|
25
|
+
* lives in the persistent Chrome profile, so no code here has to know anything
|
|
26
|
+
* about how that site authenticates, and no credential is ever handled by this
|
|
27
|
+
* package.
|
|
28
|
+
*
|
|
29
|
+
* It needs `browser.profileDir` set, since a persistent profile is the thing
|
|
30
|
+
* doing the remembering.
|
|
31
|
+
*/
|
|
32
|
+
export declare function profileAuth(options?: ProfileAuthOptions): AuthStrategy;
|
|
33
|
+
export interface StorageStateOptions {
|
|
34
|
+
/** Playwright storage-state JSON. Relative paths anchor to the config. */
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly landingPath?: string;
|
|
37
|
+
readonly signedInSelector?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A saved Playwright session file.
|
|
41
|
+
*
|
|
42
|
+
* Portable and CI-friendly in a way a Chrome profile is not: it is one JSON
|
|
43
|
+
* file that can be produced once and committed to a secret store. Needs an
|
|
44
|
+
* isolated context, so it cannot be combined with `browser.profileDir`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function storageStateAuth(options: StorageStateOptions): AuthStrategy;
|
|
47
|
+
export interface BasicAuthOptions {
|
|
48
|
+
readonly username: string;
|
|
49
|
+
readonly password: string;
|
|
50
|
+
}
|
|
51
|
+
/** HTTP basic auth, which is how most staging environments are gated. */
|
|
52
|
+
export declare function basicAuth(options: BasicAuthOptions): AuthStrategy;
|
|
53
|
+
/** Splits `name=value` into its parts; a bare name means "any value". */
|
|
54
|
+
export declare function splitCookie(spec: string): [name: string, value: string | undefined];
|