versioncam 0.1.1
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 +94 -0
- package/LICENSE.md +105 -0
- package/README.md +463 -0
- package/bin/versioncam.js +29 -0
- package/dist/.types-render/render-page/draw.d.ts +28 -0
- package/dist/.types-render/render-page/main.d.ts +24 -0
- package/dist/.types-render/render-page/theme.d.ts +37 -0
- package/dist/app-server.d.ts +59 -0
- package/dist/app-server.js +328 -0
- package/dist/app-server.js.map +1 -0
- package/dist/cli/app.d.ts +13 -0
- package/dist/cli/app.js +21 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/commands/check.d.ts +8 -0
- package/dist/cli/commands/check.js +51 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/doctor.js +130 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/dsl.d.ts +16 -0
- package/dist/cli/commands/dsl.js +22 -0
- package/dist/cli/commands/dsl.js.map +1 -0
- package/dist/cli/commands/frame.d.ts +8 -0
- package/dist/cli/commands/frame.js +72 -0
- package/dist/cli/commands/frame.js.map +1 -0
- package/dist/cli/commands/init.d.ts +23 -0
- package/dist/cli/commands/init.js +109 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +1 -0
- package/dist/cli/commands/inspect.js +32 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/install.d.ts +33 -0
- package/dist/cli/commands/install.js +66 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/login.d.ts +10 -0
- package/dist/cli/commands/login.js +49 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/measure.d.ts +1 -0
- package/dist/cli/commands/measure.js +36 -0
- package/dist/cli/commands/measure.js.map +1 -0
- package/dist/cli/commands/open-app.d.ts +14 -0
- package/dist/cli/commands/open-app.js +45 -0
- package/dist/cli/commands/open-app.js.map +1 -0
- package/dist/cli/commands/preview.d.ts +8 -0
- package/dist/cli/commands/preview.js +55 -0
- package/dist/cli/commands/preview.js.map +1 -0
- package/dist/cli/commands/record.d.ts +10 -0
- package/dist/cli/commands/record.js +86 -0
- package/dist/cli/commands/record.js.map +1 -0
- package/dist/cli/commands/render.d.ts +1 -0
- package/dist/cli/commands/render.js +93 -0
- package/dist/cli/commands/render.js.map +1 -0
- package/dist/cli/commands/review.d.ts +6 -0
- package/dist/cli/commands/review.js +89 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/sheet.d.ts +1 -0
- package/dist/cli/commands/sheet.js +48 -0
- package/dist/cli/commands/sheet.js.map +1 -0
- package/dist/cli/commands/stability.d.ts +14 -0
- package/dist/cli/commands/stability.js +110 -0
- package/dist/cli/commands/stability.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +69 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/usage.d.ts +10 -0
- package/dist/cli/usage.js +46 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/config.d.ts +250 -0
- package/dist/config.js +154 -0
- package/dist/config.js.map +1 -0
- package/dist/core/camera.d.ts +30 -0
- package/dist/core/camera.js +94 -0
- package/dist/core/camera.js.map +1 -0
- package/dist/core/compose.d.ts +38 -0
- package/dist/core/compose.js +81 -0
- package/dist/core/compose.js.map +1 -0
- package/dist/core/cursor.d.ts +38 -0
- package/dist/core/cursor.js +103 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/easing.d.ts +15 -0
- package/dist/core/easing.js +33 -0
- package/dist/core/easing.js.map +1 -0
- package/dist/core/loop.d.ts +30 -0
- package/dist/core/loop.js +96 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/motion-defaults.d.ts +61 -0
- package/dist/core/motion-defaults.js +62 -0
- package/dist/core/motion-defaults.js.map +1 -0
- package/dist/core/rng.d.ts +13 -0
- package/dist/core/rng.js +27 -0
- package/dist/core/rng.js.map +1 -0
- package/dist/core/sse.d.ts +15 -0
- package/dist/core/sse.js +16 -0
- package/dist/core/sse.js.map +1 -0
- package/dist/core/timeline.d.ts +146 -0
- package/dist/core/timeline.js +81 -0
- package/dist/core/timeline.js.map +1 -0
- package/dist/core/timing.d.ts +31 -0
- package/dist/core/timing.js +29 -0
- package/dist/core/timing.js.map +1 -0
- package/dist/core/typing.d.ts +12 -0
- package/dist/core/typing.js +35 -0
- package/dist/core/typing.js.map +1 -0
- package/dist/driver/clip.d.ts +71 -0
- package/dist/driver/clip.js +120 -0
- package/dist/driver/clip.js.map +1 -0
- package/dist/driver/compare.d.ts +34 -0
- package/dist/driver/compare.js +40 -0
- package/dist/driver/compare.js.map +1 -0
- package/dist/driver/gate.d.ts +36 -0
- package/dist/driver/gate.js +27 -0
- package/dist/driver/gate.js.map +1 -0
- package/dist/driver/launch.d.ts +43 -0
- package/dist/driver/launch.js +47 -0
- package/dist/driver/launch.js.map +1 -0
- package/dist/driver/page-hooks.d.ts +72 -0
- package/dist/driver/page-hooks.js +129 -0
- package/dist/driver/page-hooks.js.map +1 -0
- package/dist/driver/reports.d.ts +34 -0
- package/dist/driver/reports.js +42 -0
- package/dist/driver/reports.js.map +1 -0
- package/dist/driver/session.d.ts +285 -0
- package/dist/driver/session.js +773 -0
- package/dist/driver/session.js.map +1 -0
- package/dist/driver/settle.d.ts +41 -0
- package/dist/driver/settle.js +82 -0
- package/dist/driver/settle.js.map +1 -0
- package/dist/env.d.ts +11 -0
- package/dist/env.js +41 -0
- package/dist/env.js.map +1 -0
- package/dist/fixtures.d.ts +13 -0
- package/dist/fixtures.js +13 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/inspect.d.ts +70 -0
- package/dist/inspect/inspect.js +176 -0
- package/dist/inspect/inspect.js.map +1 -0
- package/dist/inspect/measure.d.ts +40 -0
- package/dist/inspect/measure.js +107 -0
- package/dist/inspect/measure.js.map +1 -0
- package/dist/loader.d.ts +28 -0
- package/dist/loader.js +143 -0
- package/dist/loader.js.map +1 -0
- package/dist/page/assets/index-DUom5amc.js +1 -0
- package/dist/page/index.html +18 -0
- package/dist/render/encode.d.ts +40 -0
- package/dist/render/encode.js +183 -0
- package/dist/render/encode.js.map +1 -0
- package/dist/render/ffmpeg.d.ts +13 -0
- package/dist/render/ffmpeg.js +72 -0
- package/dist/render/ffmpeg.js.map +1 -0
- package/dist/render/presentation.d.ts +19 -0
- package/dist/render/presentation.js +27 -0
- package/dist/render/presentation.js.map +1 -0
- package/dist/render/render.d.ts +59 -0
- package/dist/render/render.js +144 -0
- package/dist/render/render.js.map +1 -0
- package/dist/render/sampling.d.ts +47 -0
- package/dist/render/sampling.js +129 -0
- package/dist/render/sampling.js.map +1 -0
- package/dist/render/sequence.d.ts +24 -0
- package/dist/render/sequence.js +105 -0
- package/dist/render/sequence.js.map +1 -0
- package/dist/render/serve.d.ts +35 -0
- package/dist/render/serve.js +124 -0
- package/dist/render/serve.js.map +1 -0
- package/dist/review/review.d.ts +54 -0
- package/dist/review/review.js +229 -0
- package/dist/review/review.js.map +1 -0
- package/dist/scene.d.ts +23 -0
- package/dist/scene.js +2 -0
- package/dist/scene.js.map +1 -0
- package/dsl.md +119 -0
- package/package.json +76 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/README.md +105 -0
- package/plugin/agents/versioncam-reviewer.md +63 -0
- package/plugin/skills/versioncam/SKILL.md +235 -0
- package/plugin/skills/versioncam/authoring.md +226 -0
- package/plugin/skills/versioncam/onboarding.md +199 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What a user of `versioncam` would want to know about each version, newest
|
|
4
|
+
first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and versions follow [semantic versioning](https://semver.org/): while the
|
|
6
|
+
major version is 0, a minor version may change the config or the clip DSL,
|
|
7
|
+
and its entry here says how.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.1.1] — 2026-09-28
|
|
12
|
+
|
|
13
|
+
The first version published by the release workflow rather than by hand.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Where there is no `versioncam.config.ts`, `doctor` and every other command
|
|
18
|
+
say how to get one: `npx versioncam init`, then `/versioncam`, whose first
|
|
19
|
+
run writes it from the repository — or by hand, a default export from
|
|
20
|
+
`defineRecorder()`. They used to offer only the second, which is the long
|
|
21
|
+
way round for anyone using the skill.
|
|
22
|
+
- The skill's plugin manifest links to version.cam, and no longer to a
|
|
23
|
+
repository nobody outside can open.
|
|
24
|
+
|
|
25
|
+
## [0.1.0] — 2026-09-25
|
|
26
|
+
|
|
27
|
+
The first published version. It was built in work packages before it had a
|
|
28
|
+
version number, so this entry is grouped by them, newest first.
|
|
29
|
+
|
|
30
|
+
### Publishing
|
|
31
|
+
|
|
32
|
+
- Licensed under the Functional Source License 1.1 with Apache 2.0 as the
|
|
33
|
+
future licence (`FSL-1.1-ALv2`): use it on your own apps and in your own
|
|
34
|
+
CI, commercial work included; no competing product or service; each
|
|
35
|
+
version is also available under Apache 2.0 two years after its release.
|
|
36
|
+
- `versioncam install` installs the Chromium that this version's Playwright
|
|
37
|
+
expects — `--with-deps` adds its system libraries on Linux CI — and
|
|
38
|
+
`doctor` names it when the browser is missing.
|
|
39
|
+
|
|
40
|
+
### Reproducible on Linux
|
|
41
|
+
|
|
42
|
+
- A draft recording reproduces on Linux. Chromium now repaints whole tiles
|
|
43
|
+
(`--disable-partial-raster`): at a draft's CSS scale, repainting only what
|
|
44
|
+
the page reported as changed had left one pixel of a just-typed letter
|
|
45
|
+
stale in about half of all runs on a CI runner. Full recordings are
|
|
46
|
+
byte-identical with and without it.
|
|
47
|
+
- The page's clock moves only when the recorder moves it — 1/fps a frame,
|
|
48
|
+
16 ms a tick while it waits — instead of running on real time between
|
|
49
|
+
frames, where one slow screenshot had moved it 840 ms and fired an app's
|
|
50
|
+
timer seven frames early. `offCamera` spends no time on the page's clock;
|
|
51
|
+
a sign-in hook runs with the clock ticking; a target that is not yet on
|
|
52
|
+
screen is waited for in ticks, so it arrives on the same tick every run.
|
|
53
|
+
|
|
54
|
+
### Versioncam — 2026-09-24
|
|
55
|
+
|
|
56
|
+
- One name for everything: the package and CLI `versioncam`, the config
|
|
57
|
+
`versioncam.config.ts`, the output directory `.versioncam/`, the skill
|
|
58
|
+
`/versioncam`.
|
|
59
|
+
- `webServer` in the config, Playwright's fields: every command that drives
|
|
60
|
+
the app starts it when nothing answers, and stops everything it started.
|
|
61
|
+
- `versioncam stability` records a clip twice the same way and says whether
|
|
62
|
+
the app reproduces, with the two pictures behind the first frame that
|
|
63
|
+
differs. `frame --at <label>` renders the frame a beat shows; `inspect`
|
|
64
|
+
says where each name came from; `measure` prints what each match says.
|
|
65
|
+
- One skill and one reviewer file, installed by `versioncam init`; the
|
|
66
|
+
skill's first run in a repository writes the config and proves the app
|
|
67
|
+
records the same way twice before it writes a clip.
|
|
68
|
+
|
|
69
|
+
### The authoring loop — 2026-09-22
|
|
70
|
+
|
|
71
|
+
- The agent skill: inspect the app, write a clip, record it, have a fresh
|
|
72
|
+
reviewer judge a contact sheet, fix what it finds. No API key; the model
|
|
73
|
+
is whichever agent session runs it.
|
|
74
|
+
- `versioncam review` — the checks that need no eyes; `sheet --at-marks`,
|
|
75
|
+
sampled at the beats; recordings that describe their beats (`marks`) and
|
|
76
|
+
their waits (`settles.json`); `failure.png` and `failure.json` when a clip
|
|
77
|
+
breaks; `s.drag`; `cursorStart`; review bounds on a clip's length that the
|
|
78
|
+
app sets.
|
|
79
|
+
|
|
80
|
+
### Speed — 2026-09-21
|
|
81
|
+
|
|
82
|
+
- Capture on change: a frame is photographed only when the page can have
|
|
83
|
+
changed, which skips 81–95 % of frames on the example app and leaves the
|
|
84
|
+
recording identical.
|
|
85
|
+
- `--draft`: fifteen frames a second at CSS scale, for looking at while
|
|
86
|
+
writing a clip.
|
|
87
|
+
- `record` prints where the time went.
|
|
88
|
+
|
|
89
|
+
### The package — 2026-09-21
|
|
90
|
+
|
|
91
|
+
- The recorder, extracted from the prototype it grew up in: a config
|
|
92
|
+
boundary (`defineRecorder`), the clip DSL, a renderer with presentation as
|
|
93
|
+
an optional layer, the CLI, and an example app whose CI records its clips
|
|
94
|
+
on every push.
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Peter Bulovec
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# versioncam
|
|
2
|
+
|
|
3
|
+
Record demo clips of a real web app: scripted, deterministic, regenerated on
|
|
4
|
+
deploy.
|
|
5
|
+
|
|
6
|
+
A clip is a script in your repository. It drives the shipped UI through its own
|
|
7
|
+
controls, captures it, composites a cursor and a camera over the result, and
|
|
8
|
+
encodes it. Run it again after every deploy. When the UI changes in a way the
|
|
9
|
+
clip depends on, the clip *fails* — naming the control it could not find and
|
|
10
|
+
the second it gave up — instead of quietly producing a video that is wrong.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Three commands, then a sentence:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i -D versioncam
|
|
18
|
+
npx versioncam install # the Chromium this version records with
|
|
19
|
+
npx versioncam init # installs the authoring skill into .claude/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
/versioncam "Show a user filtering the list down to one shipment"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The first run of the skill sets the repository up: it writes
|
|
27
|
+
`versioncam.config.ts` from what it finds, proves the app starts and records
|
|
28
|
+
the same way twice, and only then writes the clip. See *Authoring*.
|
|
29
|
+
|
|
30
|
+
`npx versioncam doctor` checks the rest — Node 22 or later, Chromium, ffmpeg,
|
|
31
|
+
your config, and whether the app starts. ffmpeg and ffprobe come from the
|
|
32
|
+
system (`brew install ffmpeg`, `apt install ffmpeg`); the optional
|
|
33
|
+
`ffmpeg-static` covers images that have neither, but it does not include
|
|
34
|
+
ffprobe. It is tested on macOS and Linux; Windows is untested.
|
|
35
|
+
|
|
36
|
+
`install` rather than `npx playwright install`, because versioncam pins its
|
|
37
|
+
Playwright exactly — a new Chromium changes every captured pixel — and your
|
|
38
|
+
app may have a Playwright of its own, at another version, whose Chromium is
|
|
39
|
+
the one `npx playwright install` fetches.
|
|
40
|
+
|
|
41
|
+
### In CI
|
|
42
|
+
|
|
43
|
+
The same install, then `check`, which records every clip and fails the job on
|
|
44
|
+
the first one that no longer matches the app:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
- uses: actions/setup-node@v4
|
|
48
|
+
with:
|
|
49
|
+
node-version: 22
|
|
50
|
+
- run: npm ci
|
|
51
|
+
- run: npx versioncam install --with-deps
|
|
52
|
+
- run: npx versioncam check
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`check` records and does not render, so it needs no ffmpeg; a job that also
|
|
56
|
+
runs `render` adds `sudo apt-get install -y ffmpeg`. Two runs on one runner
|
|
57
|
+
image produce the same bytes. To pin everything the pixels depend on — the
|
|
58
|
+
browser build, the fonts, the system libraries — run the job in the Playwright
|
|
59
|
+
image for the version versioncam pins, `mcr.microsoft.com/playwright:v1.59.1-noble`,
|
|
60
|
+
where Chromium and its libraries are already installed.
|
|
61
|
+
|
|
62
|
+
## Configure
|
|
63
|
+
|
|
64
|
+
`versioncam.config.ts`, beside your clips:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { defineRecorder } from "versioncam";
|
|
68
|
+
|
|
69
|
+
export default defineRecorder({
|
|
70
|
+
baseUrl: process.env.DEMOS_BASE_URL ?? "http://localhost:5173",
|
|
71
|
+
viewport: { width: 1440, height: 900 },
|
|
72
|
+
clips: "demos/clips/**/*.clip.ts",
|
|
73
|
+
|
|
74
|
+
// How to start the app when nothing is serving it.
|
|
75
|
+
webServer: { command: "npm run dev" },
|
|
76
|
+
|
|
77
|
+
// Either a saved browser session…
|
|
78
|
+
auth: { storageState: ".versioncam/auth.json" },
|
|
79
|
+
// …or a hook, if signing in needs steps.
|
|
80
|
+
// auth: { login: async (page) => { … } },
|
|
81
|
+
|
|
82
|
+
// What "the page is done" means beyond a quiet DOM and an idle network.
|
|
83
|
+
settle: {
|
|
84
|
+
ready: async (page) => page.evaluate(() => window.__map?.loaded() ?? true),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Everything app-specific goes through this object. If the recorder needs to know
|
|
90
|
+
something about your app that this cannot express, that is a missing field, not
|
|
91
|
+
a reason to fork.
|
|
92
|
+
|
|
93
|
+
**The app starts itself.** `webServer` is Playwright's, field for field:
|
|
94
|
+
`command`, `url` (default `baseUrl`), `cwd` (default the config's directory),
|
|
95
|
+
`timeoutMs` (default two minutes), `reuseExistingServer`, `env`. Every command
|
|
96
|
+
that drives the app — `record`, `check`, `stability`, `inspect`, `measure`,
|
|
97
|
+
`login`, `doctor` — first asks `url` whether anything is serving. Any answer
|
|
98
|
+
counts, a 401 included, and a server that answers is used as it is and left
|
|
99
|
+
running afterwards; with `CI` set it is refused instead, because on a runner a
|
|
100
|
+
server that was up before the job started is not the build under test
|
|
101
|
+
(`reuseExistingServer: true` allows it). Silence means the command starts
|
|
102
|
+
`command` through a shell, with the shell's environment plus `env` — not what
|
|
103
|
+
the recorder loaded from `.env.recording` for itself — waits for an answer,
|
|
104
|
+
and writes what it prints to `.versioncam/webserver.log`. Whatever it started,
|
|
105
|
+
it stops when it finishes — normally, on an error, or on Ctrl-C — by
|
|
106
|
+
signalling the command's whole process group: SIGTERM, five seconds' grace,
|
|
107
|
+
then SIGKILL, and the log says which it took. Something the start command
|
|
108
|
+
hands off to outside that group, such as a container runtime, is out of its
|
|
109
|
+
reach — the command warns when the port still answers after the stop — and so
|
|
110
|
+
is worth starting yourself. So is any stack with a slow cold start: every
|
|
111
|
+
command reuses what is already running.
|
|
112
|
+
|
|
113
|
+
For a stack of several services, point `url` at the one that is ready last,
|
|
114
|
+
not at the page: a dev server answers seconds before the API behind it, and a
|
|
115
|
+
clip that opens then meets a sign-in form with nothing to sign in to.
|
|
116
|
+
|
|
117
|
+
## Write a clip
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { clip } from "versioncam";
|
|
121
|
+
|
|
122
|
+
export default clip("02-find-a-shipment", { title: "Find a shipment" }, async (s) => {
|
|
123
|
+
await s.open("/");
|
|
124
|
+
s.caption("Find a shipment");
|
|
125
|
+
await s.hold(500);
|
|
126
|
+
|
|
127
|
+
await s.camera.zoom(s.byTestId("shipments"), 1.35);
|
|
128
|
+
await s.typeInto(s.byTestId("filter"), "berg");
|
|
129
|
+
await s.settle({ label: "filtered" });
|
|
130
|
+
|
|
131
|
+
await s.highlight(s.byTestId("list-status"), "ring", { for: 1500 });
|
|
132
|
+
await s.hold(1600);
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Every `await` advances the clip's time and captures as it goes. `s.camera.*`,
|
|
137
|
+
`s.highlight` and `s.caption` write to tracks at the current instant and cost
|
|
138
|
+
no time. `s.offCamera(page => …)` runs raw Playwright with nothing captured —
|
|
139
|
+
for setup that should not appear, such as mocking an API.
|
|
140
|
+
|
|
141
|
+
Target elements by role or test id, never by coordinates. Strokes (`s.drag`
|
|
142
|
+
for a straight one, `s.lasso` for a closed loop) are authored in fractions of a
|
|
143
|
+
target's box for the same reason: a clip built from locators survives a layout
|
|
144
|
+
change.
|
|
145
|
+
|
|
146
|
+
`npx versioncam dsl` prints the whole surface, and a test asserts that list and
|
|
147
|
+
the session agree. That is worth more than it sounds:
|
|
148
|
+
this README promised an `s.drag` for months while there was none, and a clip
|
|
149
|
+
that needed one clicked instead — on a d3 brush, where a click sets a selection
|
|
150
|
+
of zero width and so *clears* the filter. It recorded cleanly and showed
|
|
151
|
+
nothing, for months, and no check caught it.
|
|
152
|
+
|
|
153
|
+
**Look before you write.** Clips written from an app's source reach for
|
|
154
|
+
controls that are conditional, renamed, or not rendered at all:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx versioncam inspect /shipments # what is on the page and what it is called
|
|
158
|
+
npx versioncam measure / '.marker' # where the matches are, as fractions
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`inspect` says where each name came from — `[aria-label]`, `[label]`,
|
|
162
|
+
`[placeholder]`, `[text]` or `[name]` — because that decides the locator:
|
|
163
|
+
`byLabel` finds the first two and not a placeholder, which `byPlaceholder`
|
|
164
|
+
finds instead. `measure` prints what each match says, so a beat's `expect`
|
|
165
|
+
can quote the status line it is waiting for rather than guess at it.
|
|
166
|
+
|
|
167
|
+
## Commands
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
versioncam record [id…] drive the app and capture frames
|
|
171
|
+
--draft 15fps at CSS scale: cheap, for writing clips
|
|
172
|
+
versioncam render [id…] composite and encode what has been recorded
|
|
173
|
+
--scene <id> render a designed scene (repeatable)
|
|
174
|
+
--sequence stitch the results into one piece
|
|
175
|
+
versioncam check [id…] record everything; non-zero if any clip breaks
|
|
176
|
+
versioncam stability [id…] record each clip twice the same way; stable or not
|
|
177
|
+
--runs <n> how many times (default 2)
|
|
178
|
+
--full at full quality rather than draft
|
|
179
|
+
versioncam sheet <id> an eight-frame contact sheet
|
|
180
|
+
--at-marks sample the beats instead of even spacing
|
|
181
|
+
versioncam frame <id> <n> one frame at output size, to a png
|
|
182
|
+
--t <seconds> pick the frame by time instead
|
|
183
|
+
--at <label> or by the beat it shows
|
|
184
|
+
versioncam preview <id> open the scrubber in a real browser
|
|
185
|
+
--scene <id> preview a scene instead
|
|
186
|
+
--frame <n> start at this frame
|
|
187
|
+
versioncam review <id> the checks that need no eyes; PASS/FAIL, exit 0/1
|
|
188
|
+
--beats <file> [{ label, expect }] the clip promised
|
|
189
|
+
--min <s> --max <s> duration bounds (default 9–11)
|
|
190
|
+
versioncam inspect <path> what a clip can click, and where each name came from
|
|
191
|
+
--json machine-readable
|
|
192
|
+
versioncam measure <path> <sel> where the matches of a selector are, and what they say
|
|
193
|
+
versioncam dsl the clip DSL, every method, one line each
|
|
194
|
+
versioncam login sign in once and save the browser session
|
|
195
|
+
versioncam doctor check this machine can record, and that the app starts
|
|
196
|
+
versioncam install the Chromium this version of versioncam records with
|
|
197
|
+
--with-deps and its system libraries (Linux CI)
|
|
198
|
+
versioncam init install the authoring skill into .claude/
|
|
199
|
+
--into <dir> into <dir>/versioncam/ instead, for another host
|
|
200
|
+
--force overwrite files that are already there
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`versioncam check` is the CI command. It does not render: a demo that no longer
|
|
204
|
+
matches the app fails while being recorded, at the step that no longer exists,
|
|
205
|
+
and encoding a video of the wreckage helps nobody.
|
|
206
|
+
|
|
207
|
+
## What a recording leaves behind
|
|
208
|
+
|
|
209
|
+
In `.versioncam/recordings/<id>/`: `timeline.json`, the `states/` it indexes, and
|
|
210
|
+
`settles.json` — every wait, what it was still blocked on, and how long it
|
|
211
|
+
really took. A clip that fails also leaves `failure.png`, the page at the
|
|
212
|
+
moment it gave up, and `failure.json` with the message, the frame and the
|
|
213
|
+
authored second.
|
|
214
|
+
|
|
215
|
+
The timeline's `marks` are the clip's beats: one per labelled `settle()`, at
|
|
216
|
+
the first frame that shows what the wait was for. `versioncam sheet --at-marks`
|
|
217
|
+
samples those frames instead of eight even ones, which is the difference
|
|
218
|
+
between a sheet that shows what the clip is about and one that happens to miss
|
|
219
|
+
it.
|
|
220
|
+
|
|
221
|
+
`versioncam review <id> --beats beats.json` is the half of reviewing a clip that
|
|
222
|
+
needs no eyes, and no browser: is it the right length, did a wait give up, does
|
|
223
|
+
every beat in the sheet have a mark, did the screen actually change between one
|
|
224
|
+
beat and the next, and did the clip end on a camera move with no frames left to
|
|
225
|
+
show it. It prints a line per clause and exits 0 or 1. What needs eyes — is
|
|
226
|
+
this legible, is the cursor in the way, does the frame show what the beat
|
|
227
|
+
claimed — is what the contact sheet and `versioncam frame` are for.
|
|
228
|
+
|
|
229
|
+
## How it works
|
|
230
|
+
|
|
231
|
+
**Time is authored, not measured.** The script declares the timeline; the
|
|
232
|
+
driver steps the app through it one output frame at a time, advancing the
|
|
233
|
+
page's fake clock by exactly 1/fps and capturing. Anything slow — a query, a
|
|
234
|
+
tile, a model — happens inside a `settle()` that advances the clock but
|
|
235
|
+
captures nothing, so it never reaches the video.
|
|
236
|
+
|
|
237
|
+
**The cursor is real input with a synthetic glyph.** Headless Chrome draws no
|
|
238
|
+
pointer, so the driver moves the *real* mouse along a planned path — hover and
|
|
239
|
+
press states genuinely fire in the captured frames — and records that path as
|
|
240
|
+
data. The glyph, the press and the ripple are drawn afterwards at output
|
|
241
|
+
resolution, which means the look can change without re-recording.
|
|
242
|
+
|
|
243
|
+
**Motion is modelled.** Minimum-jerk profiles, slight Bézier arcs, Fitts-law
|
|
244
|
+
durations, a dwell before each click, all seeded — so it never looks
|
|
245
|
+
metronomic and never differs between runs.
|
|
246
|
+
|
|
247
|
+
**Rendering needs nothing running.** The page, the recording, the config and
|
|
248
|
+
your scenes are served from an in-memory route table, so `versioncam render` works
|
|
249
|
+
from a recording on disk and a browser.
|
|
250
|
+
|
|
251
|
+
**Output follows the viewport.** Record at 1440×900 and the video is 16:10;
|
|
252
|
+
want 16:9, record at 1600×900. Nothing is letterboxed or cropped. A backdrop,
|
|
253
|
+
device frame and padding are available via `theme.presentation`, off by
|
|
254
|
+
default.
|
|
255
|
+
|
|
256
|
+
## Speed
|
|
257
|
+
|
|
258
|
+
Writing a clip means running it, looking at it, and changing a line — so what
|
|
259
|
+
matters is how long that loop takes.
|
|
260
|
+
|
|
261
|
+
**Most frames are not photographed.** A clip is mostly frames in which nothing
|
|
262
|
+
moved, and the screenshot is by far the most expensive thing a frame does: 25 ms
|
|
263
|
+
against half a millisecond for everything else. So the driver takes one only
|
|
264
|
+
when the page can have changed — the DOM mutated, the element under the cursor
|
|
265
|
+
changed, an animation is running, input was just sent, or `forceCaptureEvery`
|
|
266
|
+
frames have gone by. On the example app that skips 81% to 95% of frames, and
|
|
267
|
+
the recording is identical either way. A test records every clip both ways on
|
|
268
|
+
every push and compares the bytes, because the failure mode is not a crash but
|
|
269
|
+
a video that is quietly wrong.
|
|
270
|
+
|
|
271
|
+
The one thing it cannot see is a surface that redraws itself: a canvas, a WebGL
|
|
272
|
+
view, a video. None of them touches the DOM. `capture.forceCaptureEvery`
|
|
273
|
+
(default 30) is the floor under that; a clip whose subject *is* a canvas should
|
|
274
|
+
set it to 1 and pay full price:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
export default clip("03-the-map", { title: "The map", capture: { forceCaptureEvery: 1 } }, …);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**`--draft` for the loop.** Fifteen frames a second, captured at CSS scale
|
|
281
|
+
instead of device scale, into the same place a real recording goes.
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npx versioncam record --draft 02-find-a-shipment # then render and look
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
It deliberately does not change the page: same viewport, same device pixel
|
|
288
|
+
ratio, same layout, same code. A draft that reduced the DPR could pass while
|
|
289
|
+
the capture it stands in for fails on a media query. It does ignore a clip's
|
|
290
|
+
own `fps`, because the point is to be cheap. Its duration can land up to 1/15 s
|
|
291
|
+
from the full capture's, so a draft is for looking at, not for comparing
|
|
292
|
+
against.
|
|
293
|
+
|
|
294
|
+
`versioncam record` prints where the time went, which is how you tell whether the
|
|
295
|
+
next second would come from the frames or from somewhere else:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
01-create-a-shipment: 425 frames, 63 states, 1 cuts — plays 7.1s · took 6.3s
|
|
299
|
+
open 0.0s · settles 0.5s · frames 5.7s (79 captured, 346 skipped) · write 0.0s
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`plays` is the clip's own length and `took` is what the machine spent making
|
|
303
|
+
it. They move in opposite directions when a clip gets slower to record, which
|
|
304
|
+
is why both are named.
|
|
305
|
+
|
|
306
|
+
## Authoring
|
|
307
|
+
|
|
308
|
+
A clip is a script, and writing one is a loop: look at the page, write, record,
|
|
309
|
+
look at what you got, fix. `versioncam` ships that loop as one agent skill.
|
|
310
|
+
There is no API key and no SDK: the model is whichever agent session runs the
|
|
311
|
+
skill, and it reads contact sheets with the same tool it reads files with.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
npx versioncam init # .claude/skills/versioncam/ and .claude/agents/versioncam-reviewer.md
|
|
315
|
+
# or, for Claude Code without copying anything:
|
|
316
|
+
claude --plugin-dir node_modules/versioncam/plugin
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`init --into <dir>` puts the skill directory somewhere else, for a host that
|
|
320
|
+
reads skills from elsewhere. Then:
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
/versioncam "Find a shipment by filtering the list, and show how many matched"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**The first run** — in a directory with no `versioncam.config.ts` — sets the
|
|
327
|
+
repository up before anything else: how the app starts and where it is served,
|
|
328
|
+
from its scripts and its framework's config; whether it signs in, which it
|
|
329
|
+
asks you about; a config with a `webServer`, so nothing has to be running;
|
|
330
|
+
`doctor` and `inspect` to prove the app starts and settles; and `stability`
|
|
331
|
+
on a three-second clip, so no clip is written against an app that does not
|
|
332
|
+
record the same way twice. It commits nothing.
|
|
333
|
+
|
|
334
|
+
**Every run.** The session inspects the start page and writes the clip itself:
|
|
335
|
+
it reads `versioncam dsl` and your existing clips, writes a beat sheet, writes
|
|
336
|
+
the clip, and records it in draft until `versioncam review` passes, three
|
|
337
|
+
recordings at most. Then a contact sheet sampled *at the beats*, and a fresh
|
|
338
|
+
reviewer every round — `versioncam-reviewer`, which sees the picture, the
|
|
339
|
+
beats and a five-item checklist, and nothing of how the clip was written. A
|
|
340
|
+
reviewer that has read the author's reasoning agrees with it. Its verdict comes
|
|
341
|
+
back as fixes to make, and the round repeats; six rounds at most, then it stops
|
|
342
|
+
and says why. On a host that cannot register the reviewer as an agent, the same
|
|
343
|
+
file is a subagent's prompt, or — with no subagents at all — a checklist the
|
|
344
|
+
session applies to its own sheet, and the verdict says which.
|
|
345
|
+
|
|
346
|
+
**What you get**, under `.versioncam/author/<id>/`: the beat sheet, a
|
|
347
|
+
`summary.json`, the final sheet, and per round what the author found, its
|
|
348
|
+
beats and the reviewer's verdict. The clip itself goes in your clips
|
|
349
|
+
directory, which is the part worth reading and committing.
|
|
350
|
+
|
|
351
|
+
**What it costs.** Six to twelve minutes and one or two rounds for a clip on
|
|
352
|
+
the example app, most of it model latency rather than the recorder. It is not
|
|
353
|
+
cheap and it is not instant; it is, however, unattended.
|
|
354
|
+
|
|
355
|
+
**What it is not.** The skill cannot sign your app in for you — it asks how
|
|
356
|
+
you want that handled, and `versioncam login` is yours to run because it waits
|
|
357
|
+
for you. It does not run `git`. And `versioncam review` only checks what needs
|
|
358
|
+
no eyes: duration, that every beat left a mark, that each beat changed the
|
|
359
|
+
screen, that nothing was written after the last frame. Whether the clip is
|
|
360
|
+
*good* is the reviewer's job, and after that, yours.
|
|
361
|
+
|
|
362
|
+
## Determinism
|
|
363
|
+
|
|
364
|
+
Two runs of a clip on one machine produce the same bytes, draft and full. The
|
|
365
|
+
package's own tests assert it for capture and for rendering on every change,
|
|
366
|
+
on macOS and on Linux, and a golden recording pins the capture path against
|
|
367
|
+
changes nobody meant to make.
|
|
368
|
+
|
|
369
|
+
Across machines the claim is narrower, and worth stating precisely: the
|
|
370
|
+
**timeline** is identical — it is what the driver decided, and nothing in it
|
|
371
|
+
depends on the host. The captured **pixels** are identical only where the
|
|
372
|
+
rendering environment is: macOS and Linux rasterise text differently, so the
|
|
373
|
+
same page in the same state is the same picture and a different file.
|
|
374
|
+
Byte-identity is a property of a fixed environment, which in CI means one
|
|
375
|
+
runner image — the Playwright container for the version versioncam pins is
|
|
376
|
+
the simplest (see *In CI*).
|
|
377
|
+
|
|
378
|
+
If two recordings of your clip on one machine differ, something outside the
|
|
379
|
+
authored clock reached a frame — an unwarmed resource, a real timer, a font
|
|
380
|
+
that had not finished loading. Find the cause; the next section is how.
|
|
381
|
+
|
|
382
|
+
### When a clip will not reproduce
|
|
383
|
+
|
|
384
|
+
The recorder can only be as deterministic as the app it is filming, and an app
|
|
385
|
+
that is *nearly* deterministic is the hard case: it reproduces most runs, so the
|
|
386
|
+
first failure looks like a recorder bug.
|
|
387
|
+
|
|
388
|
+
Before assuming it is one, record the clip twice the same way — which is
|
|
389
|
+
what `stability` does, frame by frame, by what each frame shows:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
npx versioncam stability 02-find-a-shipment # draft; --full for the real thing
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
02-find-a-shipment: run 1 vs run 2 — 268 frames, 264 identical, first difference at frame 131
|
|
397
|
+
.versioncam/stability/02-find-a-shipment/frame-131-run1.png
|
|
398
|
+
.versioncam/stability/02-find-a-shipment/frame-131-run2.png
|
|
399
|
+
unstable — something on this page does not run on authored time
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The two files are the pictures behind the first frame that differs; what
|
|
403
|
+
differs is usually obvious once they are side by side. Each run records beside
|
|
404
|
+
the clip's own recording, under `recordings/__stability/`, never over it. With
|
|
405
|
+
no id it checks every clip, and it exits 0 only when every pair matched —
|
|
406
|
+
which makes it the first thing to run in a repository before writing a clip
|
|
407
|
+
for it, and what the authoring skill does on a first run.
|
|
408
|
+
|
|
409
|
+
If the runs differ, something on the page did not run on authored time. Look
|
|
410
|
+
at the app first, but not only there: of the four causes found so far, the
|
|
411
|
+
recorder was two. What we have found doing this:
|
|
412
|
+
|
|
413
|
+
- An **autosizing text field** that settles a frame later depending on how the
|
|
414
|
+
browser scheduled the layout. The clips that typed diverged; the clips that
|
|
415
|
+
did not, did not.
|
|
416
|
+
- A **spinner**, via the recorder's own fault, since fixed: the screenshot was
|
|
417
|
+
cancelling the animation the driver had just scrubbed. Worth knowing because
|
|
418
|
+
the symptom was six pixels in one frame of three hundred, which reads as
|
|
419
|
+
noise rather than as a bug.
|
|
420
|
+
- A **typed letter's edge**, via the recorder's own fault, since fixed: a
|
|
421
|
+
draft's screenshot has Chromium paint the page again at CSS scale, and it
|
|
422
|
+
repainted only the rectangle the page reported as changed, which at that
|
|
423
|
+
scale could stop a pixel short of a new glyph's antialiased edge. Only on
|
|
424
|
+
Linux, where text has LCD antialiasing: one pixel of one frame, in about half
|
|
425
|
+
of all runs. Side by side the two pictures look identical; subtracting one
|
|
426
|
+
from the other finds it.
|
|
427
|
+
- A **timer firing early**, via the recorder's own fault, since fixed: the
|
|
428
|
+
page's fake clock was installed but left running on real time between
|
|
429
|
+
frames, so a slow screenshot reached the page as time. One stalled frame on
|
|
430
|
+
a CI runner moved the clock 840 ms instead of 67 and the example app's
|
|
431
|
+
shipment appeared seven frames early. The clock is paused now and moves only
|
|
432
|
+
when the driver moves it.
|
|
433
|
+
|
|
434
|
+
If the two recordings match and something else does not, the next question is
|
|
435
|
+
what changed between them — the gate (`capture: { gate: false }` turns it off)
|
|
436
|
+
or the machine.
|
|
437
|
+
|
|
438
|
+
## While recording
|
|
439
|
+
|
|
440
|
+
Do not edit files. A dev server pushes a full page reload to every connected
|
|
441
|
+
client, including the page being recorded. The driver detects it and says so,
|
|
442
|
+
but the run is lost either way.
|
|
443
|
+
|
|
444
|
+
Record one clip at a time against a given app session. The CLI already does;
|
|
445
|
+
the thing to avoid is leaving a browser tab open on the same session.
|
|
446
|
+
|
|
447
|
+
## Help
|
|
448
|
+
|
|
449
|
+
This README is also the documentation at [version.cam](https://version.cam).
|
|
450
|
+
Bugs, questions, and apps it cannot record go to the issue tracker at
|
|
451
|
+
[github.com/petbul/versioncam-home](https://github.com/petbul/versioncam-home/issues)
|
|
452
|
+
— a clip that will not reproduce is easiest to help with alongside the two
|
|
453
|
+
pictures `versioncam stability` saves.
|
|
454
|
+
|
|
455
|
+
## Licence
|
|
456
|
+
|
|
457
|
+
Versioncam is under the Functional Source License, version 1.1, with Apache
|
|
458
|
+
2.0 as its future licence (`FSL-1.1-ALv2`). Use it on your own apps and in your
|
|
459
|
+
own CI, commercial work included, and change it as you need; what the licence
|
|
460
|
+
does not allow is making it available to others in a product or service that
|
|
461
|
+
competes with it. Two years after each version is released, that version is
|
|
462
|
+
also available under Apache 2.0. This is a summary; `LICENSE.md` is the
|
|
463
|
+
licence.
|