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.
Files changed (182) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE.md +105 -0
  3. package/README.md +463 -0
  4. package/bin/versioncam.js +29 -0
  5. package/dist/.types-render/render-page/draw.d.ts +28 -0
  6. package/dist/.types-render/render-page/main.d.ts +24 -0
  7. package/dist/.types-render/render-page/theme.d.ts +37 -0
  8. package/dist/app-server.d.ts +59 -0
  9. package/dist/app-server.js +328 -0
  10. package/dist/app-server.js.map +1 -0
  11. package/dist/cli/app.d.ts +13 -0
  12. package/dist/cli/app.js +21 -0
  13. package/dist/cli/app.js.map +1 -0
  14. package/dist/cli/commands/check.d.ts +8 -0
  15. package/dist/cli/commands/check.js +51 -0
  16. package/dist/cli/commands/check.js.map +1 -0
  17. package/dist/cli/commands/doctor.d.ts +8 -0
  18. package/dist/cli/commands/doctor.js +130 -0
  19. package/dist/cli/commands/doctor.js.map +1 -0
  20. package/dist/cli/commands/dsl.d.ts +16 -0
  21. package/dist/cli/commands/dsl.js +22 -0
  22. package/dist/cli/commands/dsl.js.map +1 -0
  23. package/dist/cli/commands/frame.d.ts +8 -0
  24. package/dist/cli/commands/frame.js +72 -0
  25. package/dist/cli/commands/frame.js.map +1 -0
  26. package/dist/cli/commands/init.d.ts +23 -0
  27. package/dist/cli/commands/init.js +109 -0
  28. package/dist/cli/commands/init.js.map +1 -0
  29. package/dist/cli/commands/inspect.d.ts +1 -0
  30. package/dist/cli/commands/inspect.js +32 -0
  31. package/dist/cli/commands/inspect.js.map +1 -0
  32. package/dist/cli/commands/install.d.ts +33 -0
  33. package/dist/cli/commands/install.js +66 -0
  34. package/dist/cli/commands/install.js.map +1 -0
  35. package/dist/cli/commands/login.d.ts +10 -0
  36. package/dist/cli/commands/login.js +49 -0
  37. package/dist/cli/commands/login.js.map +1 -0
  38. package/dist/cli/commands/measure.d.ts +1 -0
  39. package/dist/cli/commands/measure.js +36 -0
  40. package/dist/cli/commands/measure.js.map +1 -0
  41. package/dist/cli/commands/open-app.d.ts +14 -0
  42. package/dist/cli/commands/open-app.js +45 -0
  43. package/dist/cli/commands/open-app.js.map +1 -0
  44. package/dist/cli/commands/preview.d.ts +8 -0
  45. package/dist/cli/commands/preview.js +55 -0
  46. package/dist/cli/commands/preview.js.map +1 -0
  47. package/dist/cli/commands/record.d.ts +10 -0
  48. package/dist/cli/commands/record.js +86 -0
  49. package/dist/cli/commands/record.js.map +1 -0
  50. package/dist/cli/commands/render.d.ts +1 -0
  51. package/dist/cli/commands/render.js +93 -0
  52. package/dist/cli/commands/render.js.map +1 -0
  53. package/dist/cli/commands/review.d.ts +6 -0
  54. package/dist/cli/commands/review.js +89 -0
  55. package/dist/cli/commands/review.js.map +1 -0
  56. package/dist/cli/commands/sheet.d.ts +1 -0
  57. package/dist/cli/commands/sheet.js +48 -0
  58. package/dist/cli/commands/sheet.js.map +1 -0
  59. package/dist/cli/commands/stability.d.ts +14 -0
  60. package/dist/cli/commands/stability.js +110 -0
  61. package/dist/cli/commands/stability.js.map +1 -0
  62. package/dist/cli/main.d.ts +2 -0
  63. package/dist/cli/main.js +69 -0
  64. package/dist/cli/main.js.map +1 -0
  65. package/dist/cli/usage.d.ts +10 -0
  66. package/dist/cli/usage.js +46 -0
  67. package/dist/cli/usage.js.map +1 -0
  68. package/dist/config.d.ts +250 -0
  69. package/dist/config.js +154 -0
  70. package/dist/config.js.map +1 -0
  71. package/dist/core/camera.d.ts +30 -0
  72. package/dist/core/camera.js +94 -0
  73. package/dist/core/camera.js.map +1 -0
  74. package/dist/core/compose.d.ts +38 -0
  75. package/dist/core/compose.js +81 -0
  76. package/dist/core/compose.js.map +1 -0
  77. package/dist/core/cursor.d.ts +38 -0
  78. package/dist/core/cursor.js +103 -0
  79. package/dist/core/cursor.js.map +1 -0
  80. package/dist/core/easing.d.ts +15 -0
  81. package/dist/core/easing.js +33 -0
  82. package/dist/core/easing.js.map +1 -0
  83. package/dist/core/loop.d.ts +30 -0
  84. package/dist/core/loop.js +96 -0
  85. package/dist/core/loop.js.map +1 -0
  86. package/dist/core/motion-defaults.d.ts +61 -0
  87. package/dist/core/motion-defaults.js +62 -0
  88. package/dist/core/motion-defaults.js.map +1 -0
  89. package/dist/core/rng.d.ts +13 -0
  90. package/dist/core/rng.js +27 -0
  91. package/dist/core/rng.js.map +1 -0
  92. package/dist/core/sse.d.ts +15 -0
  93. package/dist/core/sse.js +16 -0
  94. package/dist/core/sse.js.map +1 -0
  95. package/dist/core/timeline.d.ts +146 -0
  96. package/dist/core/timeline.js +81 -0
  97. package/dist/core/timeline.js.map +1 -0
  98. package/dist/core/timing.d.ts +31 -0
  99. package/dist/core/timing.js +29 -0
  100. package/dist/core/timing.js.map +1 -0
  101. package/dist/core/typing.d.ts +12 -0
  102. package/dist/core/typing.js +35 -0
  103. package/dist/core/typing.js.map +1 -0
  104. package/dist/driver/clip.d.ts +71 -0
  105. package/dist/driver/clip.js +120 -0
  106. package/dist/driver/clip.js.map +1 -0
  107. package/dist/driver/compare.d.ts +34 -0
  108. package/dist/driver/compare.js +40 -0
  109. package/dist/driver/compare.js.map +1 -0
  110. package/dist/driver/gate.d.ts +36 -0
  111. package/dist/driver/gate.js +27 -0
  112. package/dist/driver/gate.js.map +1 -0
  113. package/dist/driver/launch.d.ts +43 -0
  114. package/dist/driver/launch.js +47 -0
  115. package/dist/driver/launch.js.map +1 -0
  116. package/dist/driver/page-hooks.d.ts +72 -0
  117. package/dist/driver/page-hooks.js +129 -0
  118. package/dist/driver/page-hooks.js.map +1 -0
  119. package/dist/driver/reports.d.ts +34 -0
  120. package/dist/driver/reports.js +42 -0
  121. package/dist/driver/reports.js.map +1 -0
  122. package/dist/driver/session.d.ts +285 -0
  123. package/dist/driver/session.js +773 -0
  124. package/dist/driver/session.js.map +1 -0
  125. package/dist/driver/settle.d.ts +41 -0
  126. package/dist/driver/settle.js +82 -0
  127. package/dist/driver/settle.js.map +1 -0
  128. package/dist/env.d.ts +11 -0
  129. package/dist/env.js +41 -0
  130. package/dist/env.js.map +1 -0
  131. package/dist/fixtures.d.ts +13 -0
  132. package/dist/fixtures.js +13 -0
  133. package/dist/fixtures.js.map +1 -0
  134. package/dist/index.d.ts +30 -0
  135. package/dist/index.js +16 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/inspect/inspect.d.ts +70 -0
  138. package/dist/inspect/inspect.js +176 -0
  139. package/dist/inspect/inspect.js.map +1 -0
  140. package/dist/inspect/measure.d.ts +40 -0
  141. package/dist/inspect/measure.js +107 -0
  142. package/dist/inspect/measure.js.map +1 -0
  143. package/dist/loader.d.ts +28 -0
  144. package/dist/loader.js +143 -0
  145. package/dist/loader.js.map +1 -0
  146. package/dist/page/assets/index-DUom5amc.js +1 -0
  147. package/dist/page/index.html +18 -0
  148. package/dist/render/encode.d.ts +40 -0
  149. package/dist/render/encode.js +183 -0
  150. package/dist/render/encode.js.map +1 -0
  151. package/dist/render/ffmpeg.d.ts +13 -0
  152. package/dist/render/ffmpeg.js +72 -0
  153. package/dist/render/ffmpeg.js.map +1 -0
  154. package/dist/render/presentation.d.ts +19 -0
  155. package/dist/render/presentation.js +27 -0
  156. package/dist/render/presentation.js.map +1 -0
  157. package/dist/render/render.d.ts +59 -0
  158. package/dist/render/render.js +144 -0
  159. package/dist/render/render.js.map +1 -0
  160. package/dist/render/sampling.d.ts +47 -0
  161. package/dist/render/sampling.js +129 -0
  162. package/dist/render/sampling.js.map +1 -0
  163. package/dist/render/sequence.d.ts +24 -0
  164. package/dist/render/sequence.js +105 -0
  165. package/dist/render/sequence.js.map +1 -0
  166. package/dist/render/serve.d.ts +35 -0
  167. package/dist/render/serve.js +124 -0
  168. package/dist/render/serve.js.map +1 -0
  169. package/dist/review/review.d.ts +54 -0
  170. package/dist/review/review.js +229 -0
  171. package/dist/review/review.js.map +1 -0
  172. package/dist/scene.d.ts +23 -0
  173. package/dist/scene.js +2 -0
  174. package/dist/scene.js.map +1 -0
  175. package/dsl.md +119 -0
  176. package/package.json +76 -0
  177. package/plugin/.claude-plugin/plugin.json +9 -0
  178. package/plugin/README.md +105 -0
  179. package/plugin/agents/versioncam-reviewer.md +63 -0
  180. package/plugin/skills/versioncam/SKILL.md +235 -0
  181. package/plugin/skills/versioncam/authoring.md +226 -0
  182. package/plugin/skills/versioncam/onboarding.md +199 -0
@@ -0,0 +1,110 @@
1
+ import { copyFileSync, mkdirSync } from "node:fs";
2
+ import { extname, join, relative } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { runClip } from "../../driver/clip.js";
5
+ import { compareRecordings, stateFileAt } from "../../driver/compare.js";
6
+ import { launchBrowser } from "../../driver/launch.js";
7
+ import { loadClips, loadConfig } from "../../loader.js";
8
+ import { drivingApp } from "../app.js";
9
+ /**
10
+ * Does this app record the same way twice?
11
+ *
12
+ * The README's rule — when a clip will not reproduce, record it twice the same
13
+ * way first — as a command, and as the first thing done in a repository before
14
+ * anything is authored for it. A clip is only as deterministic as the app it
15
+ * films: a page that renders the wall-clock time, or animates something the
16
+ * recorder's clock does not drive, gives a different video every run, and
17
+ * every clip written against it will fail CI for reasons nobody wrote.
18
+ *
19
+ * Draft by default, because the question is about the app and not the pixels:
20
+ * a draft is the same page at the same size, a quarter of the frames.
21
+ */
22
+ export async function stability(argv) {
23
+ const { values, positionals } = parseArgs({
24
+ args: argv,
25
+ options: {
26
+ runs: { type: "string", default: "2" },
27
+ // Accepted for saying so explicitly; drafting is the default.
28
+ draft: { type: "boolean" },
29
+ full: { type: "boolean" },
30
+ },
31
+ strict: false,
32
+ allowPositionals: true,
33
+ });
34
+ const runs = Number(values.runs);
35
+ if (!Number.isInteger(runs) || runs < 2) {
36
+ process.stderr.write(`--runs must be a whole number of 2 or more, got ${values.runs}.\n`);
37
+ return 1;
38
+ }
39
+ const draft = values.full !== true;
40
+ const config = await loadConfig(process.cwd());
41
+ const clips = await loadClips(config, positionals);
42
+ if (clips.length === 0) {
43
+ process.stderr.write(positionals.length > 0
44
+ ? `No clip matched ${positionals.join(", ")}.\n`
45
+ : `No clips found. The config looks for ${config.clips.join(", ")} under ${config.root}.\n`);
46
+ return 1;
47
+ }
48
+ return drivingApp(config, () => compareRuns(config, clips, runs, draft));
49
+ }
50
+ async function compareRuns(config, clips, runs, draft) {
51
+ const browser = await launchBrowser();
52
+ let stable = true;
53
+ try {
54
+ for (const clip of clips) {
55
+ const recordings = [];
56
+ for (let run = 1; run <= runs; run += 1) {
57
+ // Each run in its own directory, beside the real recordings and never
58
+ // over one: the clip's own recording is what `render` and `sheet`
59
+ // read, and a stability check must not replace it.
60
+ const into = join(config.recordingsDir, "__stability", `run-${run}`);
61
+ try {
62
+ const { dir, timeline } = await runClip(browser, { ...config, recordingsDir: into }, clip, { draft });
63
+ recordings.push({ dir, timeline });
64
+ }
65
+ catch (error) {
66
+ process.stdout.write(`${clip.id}: run ${run} did not record — ${error.message}\n`);
67
+ return 1;
68
+ }
69
+ }
70
+ for (let run = 1; run < runs; run += 1) {
71
+ const [a, b] = [recordings[run - 1], recordings[run]];
72
+ const result = compareRecordings(a, b);
73
+ const pair = `${clip.id}: run ${run} vs run ${run + 1}`;
74
+ if (result.firstDifference === null) {
75
+ process.stdout.write(`${pair} — ${result.frames} frames, ${result.identical} identical\n`);
76
+ continue;
77
+ }
78
+ stable = false;
79
+ const frame = result.firstDifference;
80
+ process.stdout.write(`${pair} — ${result.frames} frames, ${result.identical} identical, ` +
81
+ `first difference at frame ${frame}\n`);
82
+ // The two pictures behind that frame, side by side on disk: what
83
+ // differs is almost always obvious once seen, and never from a hash.
84
+ const out = join(config.outDir, "stability", clip.id);
85
+ mkdirSync(out, { recursive: true });
86
+ for (const [index, recording] of [
87
+ [run, a],
88
+ [run + 1, b],
89
+ ]) {
90
+ const state = stateFileAt(recording, frame);
91
+ if (!state) {
92
+ process.stdout.write(` run ${index} has no frame ${frame}\n`);
93
+ continue;
94
+ }
95
+ const copy = join(out, `frame-${frame}-run${index}${extname(state)}`);
96
+ copyFileSync(state, copy);
97
+ process.stdout.write(` ${relative(process.cwd(), copy)}\n`);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ finally {
103
+ await browser.close();
104
+ }
105
+ process.stdout.write(stable
106
+ ? "stable\n"
107
+ : "unstable — something on this page does not run on authored time\n");
108
+ return stable ? 0 : 1;
109
+ }
110
+ //# sourceMappingURL=stability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stability.js","sourceRoot":"","sources":["../../../src/cli/commands/stability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { check } from "./commands/check.js";
4
+ import { doctor } from "./commands/doctor.js";
5
+ import { dsl } from "./commands/dsl.js";
6
+ import { frame } from "./commands/frame.js";
7
+ import { init } from "./commands/init.js";
8
+ import { inspect } from "./commands/inspect.js";
9
+ import { install } from "./commands/install.js";
10
+ import { login } from "./commands/login.js";
11
+ import { measure } from "./commands/measure.js";
12
+ import { preview } from "./commands/preview.js";
13
+ import { record } from "./commands/record.js";
14
+ import { render } from "./commands/render.js";
15
+ import { review } from "./commands/review.js";
16
+ import { sheet } from "./commands/sheet.js";
17
+ import { stability } from "./commands/stability.js";
18
+ import { loadEnvFiles } from "../env.js";
19
+ import { USAGE } from "./usage.js";
20
+ const COMMANDS = {
21
+ record,
22
+ render,
23
+ check,
24
+ stability,
25
+ sheet,
26
+ frame,
27
+ preview,
28
+ review,
29
+ inspect,
30
+ measure,
31
+ dsl,
32
+ login,
33
+ doctor,
34
+ install,
35
+ init,
36
+ };
37
+ async function main() {
38
+ const argv = process.argv.slice(2);
39
+ // `--help` is a flag, not a positional, so it never reaches `positionals`.
40
+ // Asking for help is a success; being given nothing is not.
41
+ if (argv.some((a) => a === "--help" || a === "-h" || a === "help")) {
42
+ process.stdout.write(USAGE);
43
+ return 0;
44
+ }
45
+ const { positionals } = parseArgs({
46
+ args: argv,
47
+ strict: false,
48
+ allowPositionals: true,
49
+ });
50
+ const name = positionals[0];
51
+ if (!name) {
52
+ process.stdout.write(USAGE);
53
+ return 1;
54
+ }
55
+ const command = COMMANDS[name];
56
+ if (!command) {
57
+ process.stderr.write(`Unknown command "${name}".\n\n${USAGE}`);
58
+ return 1;
59
+ }
60
+ loadEnvFiles(process.cwd());
61
+ return command(process.argv.slice(3));
62
+ }
63
+ main().then((code) => {
64
+ process.exitCode = code;
65
+ }, (error) => {
66
+ process.stderr.write(`\n${error.message}\n`);
67
+ process.exitCode = 1;
68
+ });
69
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * What `versioncam --help` prints, and the README's *Commands* block,
3
+ * line for line.
4
+ *
5
+ * Its own module so a test can read it without running the CLI: the README
6
+ * is also the website's documentation, and a test holds the two to the same
7
+ * list — the DSL reference learned why, promising an `s.drag` for months that
8
+ * did not exist.
9
+ */
10
+ export declare const USAGE = "versioncam \u2014 record and render demo clips of a real app\n\n versioncam record [id\u2026] drive the app and capture frames\n --draft 15fps at CSS scale: cheap, for writing clips\n versioncam render [id\u2026] composite and encode what has been recorded\n --scene <id> render a designed scene (repeatable)\n --sequence stitch the results into one piece\n versioncam check [id\u2026] record everything; non-zero if any clip breaks\n versioncam stability [id\u2026] record each clip twice the same way; stable or not\n --runs <n> how many times (default 2)\n --full at full quality rather than draft\n versioncam sheet <id> an eight-frame contact sheet\n --at-marks sample the beats instead of even spacing\n versioncam frame <id> <n> one frame at output size, to a png\n --t <seconds> pick the frame by time instead\n --at <label> or by the beat it shows\n versioncam preview <id> open the scrubber in a real browser\n --scene <id> preview a scene instead\n --frame <n> start at this frame\n versioncam review <id> the checks that need no eyes; PASS/FAIL, exit 0/1\n --beats <file> [{ label, expect }] the clip promised\n --min <s> --max <s> duration bounds (default 9\u201311)\n versioncam inspect <path> what a clip can click, and where each name came from\n --json machine-readable\n versioncam measure <path> <sel> where the matches of a selector are, and what they say\n versioncam dsl the clip DSL, every method, one line each\n versioncam login sign in once and save the browser session\n versioncam doctor check this machine can record, and that the app starts\n versioncam install the Chromium this version of versioncam records with\n --with-deps and its system libraries (Linux CI)\n versioncam init install the authoring skill into .claude/\n --into <dir> into <dir>/versioncam/ instead, for another host\n --force overwrite files that are already there\n\nRun from a directory containing versioncam.config.ts.\n";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * What `versioncam --help` prints, and the README's *Commands* block,
3
+ * line for line.
4
+ *
5
+ * Its own module so a test can read it without running the CLI: the README
6
+ * is also the website's documentation, and a test holds the two to the same
7
+ * list — the DSL reference learned why, promising an `s.drag` for months that
8
+ * did not exist.
9
+ */
10
+ export const USAGE = `versioncam — record and render demo clips of a real app
11
+
12
+ versioncam record [id…] drive the app and capture frames
13
+ --draft 15fps at CSS scale: cheap, for writing clips
14
+ versioncam render [id…] composite and encode what has been recorded
15
+ --scene <id> render a designed scene (repeatable)
16
+ --sequence stitch the results into one piece
17
+ versioncam check [id…] record everything; non-zero if any clip breaks
18
+ versioncam stability [id…] record each clip twice the same way; stable or not
19
+ --runs <n> how many times (default 2)
20
+ --full at full quality rather than draft
21
+ versioncam sheet <id> an eight-frame contact sheet
22
+ --at-marks sample the beats instead of even spacing
23
+ versioncam frame <id> <n> one frame at output size, to a png
24
+ --t <seconds> pick the frame by time instead
25
+ --at <label> or by the beat it shows
26
+ versioncam preview <id> open the scrubber in a real browser
27
+ --scene <id> preview a scene instead
28
+ --frame <n> start at this frame
29
+ versioncam review <id> the checks that need no eyes; PASS/FAIL, exit 0/1
30
+ --beats <file> [{ label, expect }] the clip promised
31
+ --min <s> --max <s> duration bounds (default 9–11)
32
+ versioncam inspect <path> what a clip can click, and where each name came from
33
+ --json machine-readable
34
+ versioncam measure <path> <sel> where the matches of a selector are, and what they say
35
+ versioncam dsl the clip DSL, every method, one line each
36
+ versioncam login sign in once and save the browser session
37
+ versioncam doctor check this machine can record, and that the app starts
38
+ versioncam install the Chromium this version of versioncam records with
39
+ --with-deps and its system libraries (Linux CI)
40
+ versioncam init install the authoring skill into .claude/
41
+ --into <dir> into <dir>/versioncam/ instead, for another host
42
+ --force overwrite files that are already there
43
+
44
+ Run from a directory containing versioncam.config.ts.
45
+ `;
46
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/cli/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCpB,CAAC"}
@@ -0,0 +1,250 @@
1
+ import type { Page } from "playwright";
2
+ import { MOTION } from "./core/motion-defaults.js";
3
+ /**
4
+ * The one place an app describes itself to the recorder.
5
+ *
6
+ * Everything app-specific goes through this object. If the recorder ever needs
7
+ * to know something about an app that this cannot express, the boundary is in
8
+ * the wrong place and the fix is a field here, not a special case inside.
9
+ */
10
+ /**
11
+ * `MOTION` is declared `as const` so each constant reads as its own documented
12
+ * value, which also makes its type the literal. Widen it here: an app
13
+ * overriding `zoomDurationMs` should be able to pass 2000, not only 1100.
14
+ */
15
+ export type Motion = {
16
+ -readonly [K in keyof typeof MOTION]: number;
17
+ };
18
+ /**
19
+ * The surround: a backdrop, a device frame, padding, where the caption sits.
20
+ *
21
+ * Absent by default, because the product should record the product. It exists
22
+ * for the case where a clip is going on a landing page and wants to look like
23
+ * a product shot rather than a screen capture.
24
+ */
25
+ export type Presentation = {
26
+ /** Two stops for a diagonal gradient, or one flat colour. */
27
+ backdrop?: [string, string] | string;
28
+ /** Rounded corners, hairline and drop shadow around the app. */
29
+ frame?: boolean;
30
+ /** Inset as a fraction of the smaller canvas dimension. 0 disables it. */
31
+ padding?: number;
32
+ /** "auto" puts it below the app when padded, and over it when not. */
33
+ caption?: "auto" | "below" | "overlay" | "none";
34
+ };
35
+ export type AuthConfig = {
36
+ storageState: string;
37
+ login?: never;
38
+ } | {
39
+ login: (page: Page) => Promise<void>;
40
+ storageState?: never;
41
+ };
42
+ export type SettleConfig = {
43
+ /** How long the page must be quiet before it counts as settled. */
44
+ quietMs?: number;
45
+ /** Real-time cap. Exceeding it warns rather than failing the clip. */
46
+ timeoutMs?: number;
47
+ /** Real milliseconds yielded per tick, so the page can actually do work. */
48
+ yieldMs?: number;
49
+ /**
50
+ * Whatever the generic checks cannot see: a WebGL map that has finished its
51
+ * tiles, a stream that has delivered its first frame.
52
+ */
53
+ ready?: (page: Page) => Promise<boolean>;
54
+ };
55
+ export type CaptureConfig = {
56
+ /**
57
+ * Skip the screenshot when nothing observable changed since the last one.
58
+ * On by default: a clip is mostly frames where nothing moved, and the
59
+ * screenshot is by far the most expensive thing a frame does.
60
+ */
61
+ gate?: boolean;
62
+ /**
63
+ * Capture regardless every N frames, however still the page looks.
64
+ *
65
+ * The gate watches the DOM, the element under the cursor and the running
66
+ * animations, which covers everything a page built from elements can do. It
67
+ * does not cover a canvas, a WebGL view or a video, which redraw themselves
68
+ * without any of those changing. This is the floor under that blind spot:
69
+ * at 60 fps the default catches such a change within half a second. A clip
70
+ * whose subject *is* a canvas should set it to 1 and pay full price.
71
+ */
72
+ forceCaptureEvery?: number;
73
+ };
74
+ export type ReviewConfig = {
75
+ /**
76
+ * How long a clip of this app should run, in seconds.
77
+ *
78
+ * Ten seconds is the shape of a feature clip and the default, but it is a
79
+ * house style rather than a law: an app whose clips are one crisp
80
+ * interaction each should say five, and an app demonstrating a workflow may
81
+ * want twenty. `versioncam review` enforces whatever is here, the authoring
82
+ * loop aims at it, and `--min`/`--max` override it for one run.
83
+ */
84
+ minSeconds?: number;
85
+ maxSeconds?: number;
86
+ };
87
+ export type CursorStart = {
88
+ /** Fractions of the viewport: `{ x: 0.72, y: 0.28 }` is up and to the right. */
89
+ x: number;
90
+ y: number;
91
+ };
92
+ /**
93
+ * How to start the app when nothing is serving it yet.
94
+ *
95
+ * Playwright's `webServer`, field for field where Playwright has one, because
96
+ * everyone who would use this has already learned it. Without it the app has
97
+ * to be running before any command that drives it — which is a thing a person
98
+ * remembers and an agent working in a repository it has never seen does not.
99
+ */
100
+ export type WebServerConfig = {
101
+ /** Run through a shell. Relative paths resolve against `cwd`. */
102
+ command: string;
103
+ /** What to wait for. Defaults to `baseUrl`. */
104
+ url?: string;
105
+ /** Defaults to the config file's directory. */
106
+ cwd?: string;
107
+ /** Default 120 000 — a map app's cold start, not a static page's. */
108
+ timeoutMs?: number;
109
+ /**
110
+ * Use a server that is already answering instead of starting one.
111
+ *
112
+ * Default `true`, and `false` when `process.env.CI` is set: on a developer's
113
+ * machine the server that is up is almost always theirs and the right one,
114
+ * and in CI one that is up before the job started it is something else.
115
+ */
116
+ reuseExistingServer?: boolean;
117
+ /** Merged over `process.env` for the child only. */
118
+ env?: Record<string, string>;
119
+ };
120
+ export type ResolvedWebServer = Required<WebServerConfig>;
121
+ export type RecorderConfig = {
122
+ /** Where the app is served. */
123
+ baseUrl: string;
124
+ /** How to start it when it is not being served. Absent: it must already be. */
125
+ webServer?: WebServerConfig;
126
+ viewport?: {
127
+ width: number;
128
+ height: number;
129
+ };
130
+ /**
131
+ * Device pixel ratio to capture at. 2 means a zoom reads real pixels rather
132
+ * than upscaled ones.
133
+ */
134
+ dpr?: number;
135
+ fps?: number;
136
+ /** Glob(s) for clip files, relative to the config. */
137
+ clips?: string | string[];
138
+ /** Everything the recorder writes. Gitignore it. */
139
+ outDir?: string;
140
+ auth?: AuthConfig;
141
+ settle?: SettleConfig;
142
+ capture?: CaptureConfig;
143
+ review?: ReviewConfig;
144
+ /**
145
+ * Where the cursor rests in the first captured frame, in fractions of the
146
+ * viewport. Default `{ x: 0.72, y: 0.28 }`.
147
+ *
148
+ * There is no good default, which is the point of the field. The original
149
+ * was picked to sit over open map and clear of the toolbars of the first app
150
+ * this recorded, and a reviewer has since failed a clip's opening frame on
151
+ * two different apps for landing on something: a list row in one, a
152
+ * "Select metrics" input in the other. `open()` captures frame 0 before a
153
+ * clip body's first line runs, so
154
+ * no `moveTo` can reach it — an app whose layout puts a control there has to
155
+ * say so here.
156
+ */
157
+ cursorStart?: CursorStart;
158
+ clock?: {
159
+ /** The instant the app's fake clock starts at. */
160
+ time?: string | Date;
161
+ timezoneId?: string;
162
+ locale?: string;
163
+ };
164
+ theme?: {
165
+ /** Click ripple and highlight ring. */
166
+ accent?: string;
167
+ cursorScale?: number;
168
+ presentation?: Presentation;
169
+ };
170
+ /** Output width; the height follows the viewport's aspect. */
171
+ output?: {
172
+ width?: number;
173
+ };
174
+ motion?: Partial<Motion>;
175
+ /** Module whose default export is `Record<string, Scene>`. */
176
+ scenes?: string;
177
+ /** Clip ids, in order, for `versioncam render --sequence`. */
178
+ sequence?: string[];
179
+ };
180
+ export type ResolvedConfig = {
181
+ root: string;
182
+ baseUrl: string;
183
+ webServer: ResolvedWebServer | null;
184
+ viewport: {
185
+ width: number;
186
+ height: number;
187
+ };
188
+ dpr: number;
189
+ fps: number;
190
+ clips: string[];
191
+ outDir: string;
192
+ recordingsDir: string;
193
+ outputDir: string;
194
+ cacheDir: string;
195
+ auth: AuthConfig | null;
196
+ settle: Required<Omit<SettleConfig, "ready">> & {
197
+ ready: ((page: Page) => Promise<boolean>) | null;
198
+ };
199
+ capture: Required<CaptureConfig>;
200
+ review: Required<ReviewConfig>;
201
+ cursorStart: CursorStart;
202
+ clock: {
203
+ time: Date;
204
+ timezoneId: string;
205
+ locale: string;
206
+ };
207
+ theme: {
208
+ accent: string;
209
+ cursorScale: number;
210
+ presentation: Presentation | null;
211
+ };
212
+ output: {
213
+ width: number;
214
+ height: number;
215
+ };
216
+ motion: Motion;
217
+ scenes: string | null;
218
+ sequence: string[] | null;
219
+ };
220
+ /** Typed identity, so a config file gets completion and checking. */
221
+ export declare function defineRecorder(config: RecorderConfig): RecorderConfig;
222
+ /**
223
+ * Output dimensions follow the viewport's aspect.
224
+ *
225
+ * Nothing is letterboxed and nothing is cropped: record at 1440x900 and the
226
+ * video is 16:10; want 16:9, record at 1600x900. Heights are rounded to even
227
+ * because H.264 with yuv420p cannot encode an odd dimension.
228
+ */
229
+ export declare function outputSize(viewport: {
230
+ width: number;
231
+ height: number;
232
+ }, width: number): {
233
+ width: number;
234
+ height: number;
235
+ };
236
+ export declare function resolveConfig(config: RecorderConfig, root: string): ResolvedConfig;
237
+ /** The subset the render page needs, serialised to `/render-config.json`. */
238
+ export type RenderConfig = {
239
+ output: {
240
+ width: number;
241
+ height: number;
242
+ };
243
+ theme: {
244
+ accent: string;
245
+ cursorScale: number;
246
+ presentation: Presentation | null;
247
+ };
248
+ motion: Motion;
249
+ };
250
+ export declare function renderConfigOf(config: ResolvedConfig): RenderConfig;
package/dist/config.js ADDED
@@ -0,0 +1,154 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { MOTION } from "./core/motion-defaults.js";
3
+ /** Typed identity, so a config file gets completion and checking. */
4
+ export function defineRecorder(config) {
5
+ return config;
6
+ }
7
+ function fail(field, why) {
8
+ throw new Error(`versioncam config: \`${field}\` ${why}`);
9
+ }
10
+ function positiveInt(value, field) {
11
+ if (!Number.isInteger(value) || value <= 0) {
12
+ fail(field, `must be a positive integer, got ${value}`);
13
+ }
14
+ return value;
15
+ }
16
+ /**
17
+ * Output dimensions follow the viewport's aspect.
18
+ *
19
+ * Nothing is letterboxed and nothing is cropped: record at 1440x900 and the
20
+ * video is 16:10; want 16:9, record at 1600x900. Heights are rounded to even
21
+ * because H.264 with yuv420p cannot encode an odd dimension.
22
+ */
23
+ export function outputSize(viewport, width) {
24
+ const exact = (width * viewport.height) / viewport.width;
25
+ return { width, height: 2 * Math.round(exact / 2) };
26
+ }
27
+ /**
28
+ * Every field filled, so the code that starts the server never has to know a
29
+ * default.
30
+ *
31
+ * `reuseExistingServer` reads `CI` here, once, rather than at the moment of
32
+ * starting: a config is resolved per command, so this is the same moment, and
33
+ * a resolved config that says what it will do is easier to reason about than
34
+ * one that looks at the environment again later.
35
+ */
36
+ function resolveWebServer(server, baseUrl, root) {
37
+ if (typeof server.command !== "string" || server.command.trim() === "") {
38
+ fail("webServer.command", "must be a non-empty string");
39
+ }
40
+ const url = server.url ?? baseUrl;
41
+ // `URL.canParse("localhost:5174")` is true — a scheme called `localhost` —
42
+ // and that is exactly the string someone types, so ask for the protocol.
43
+ if (!/^https?:$/.test(URL.canParse(url) ? new URL(url).protocol : "")) {
44
+ fail("webServer.url", `must be an http(s) URL, got ${url}`);
45
+ }
46
+ const timeoutMs = positiveInt(server.timeoutMs ?? 120_000, "webServer.timeoutMs");
47
+ return {
48
+ command: server.command,
49
+ url,
50
+ cwd: server.cwd === undefined ? root : resolve(root, server.cwd),
51
+ timeoutMs,
52
+ reuseExistingServer: server.reuseExistingServer ?? !process.env.CI,
53
+ env: server.env ?? {},
54
+ };
55
+ }
56
+ export function resolveConfig(config, root) {
57
+ if (typeof config.baseUrl !== "string" || config.baseUrl.length === 0) {
58
+ fail("baseUrl", "is required");
59
+ }
60
+ const viewport = config.viewport ?? { width: 1440, height: 900 };
61
+ positiveInt(viewport.width, "viewport.width");
62
+ positiveInt(viewport.height, "viewport.height");
63
+ const dpr = config.dpr ?? 2;
64
+ if (!(dpr >= 1 && dpr <= 3))
65
+ fail("dpr", `must be between 1 and 3, got ${dpr}`);
66
+ const fps = config.fps ?? 60;
67
+ if (!Number.isInteger(fps) || fps < 10 || fps > 120) {
68
+ fail("fps", `must be an integer between 10 and 120, got ${fps}`);
69
+ }
70
+ const forceCaptureEvery = config.capture?.forceCaptureEvery ?? 30;
71
+ if (!Number.isInteger(forceCaptureEvery) || forceCaptureEvery < 1) {
72
+ fail("capture.forceCaptureEvery", `must be an integer of 1 or more, got ${forceCaptureEvery}`);
73
+ }
74
+ if (config.auth) {
75
+ const hasState = typeof config.auth.storageState === "string";
76
+ const hasLogin = typeof config.auth.login === "function";
77
+ if (hasState === hasLogin) {
78
+ fail("auth", "must have exactly one of `storageState` or `login`");
79
+ }
80
+ }
81
+ const presentation = config.theme?.presentation;
82
+ if (presentation?.padding !== undefined) {
83
+ const p = presentation.padding;
84
+ if (!(p >= 0 && p <= 0.3)) {
85
+ fail("theme.presentation.padding", `must be between 0 and 0.3, got ${p}`);
86
+ }
87
+ }
88
+ const abs = (p) => (isAbsolute(p) ? p : resolve(root, p));
89
+ const outDir = abs(config.outDir ?? ".versioncam");
90
+ const outputWidth = config.output?.width ?? 1920;
91
+ positiveInt(outputWidth, "output.width");
92
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
93
+ return {
94
+ root,
95
+ baseUrl,
96
+ webServer: config.webServer
97
+ ? resolveWebServer(config.webServer, baseUrl, root)
98
+ : null,
99
+ viewport,
100
+ dpr,
101
+ fps,
102
+ clips: config.clips === undefined
103
+ ? ["demos/**/*.clip.ts"]
104
+ : Array.isArray(config.clips)
105
+ ? config.clips
106
+ : [config.clips],
107
+ outDir,
108
+ recordingsDir: resolve(outDir, "recordings"),
109
+ outputDir: resolve(outDir, "out"),
110
+ cacheDir: resolve(outDir, ".cache"),
111
+ auth: config.auth ?? null,
112
+ settle: {
113
+ quietMs: config.settle?.quietMs ?? 150,
114
+ timeoutMs: config.settle?.timeoutMs ?? 15_000,
115
+ yieldMs: config.settle?.yieldMs ?? 20,
116
+ ready: config.settle?.ready ?? null,
117
+ },
118
+ capture: {
119
+ gate: config.capture?.gate ?? true,
120
+ forceCaptureEvery: forceCaptureEvery,
121
+ },
122
+ review: {
123
+ minSeconds: config.review?.minSeconds ?? 9,
124
+ maxSeconds: config.review?.maxSeconds ?? 11,
125
+ },
126
+ cursorStart: {
127
+ x: config.cursorStart?.x ?? 0.72,
128
+ y: config.cursorStart?.y ?? 0.28,
129
+ },
130
+ clock: {
131
+ time: new Date(config.clock?.time ?? "2026-09-01T09:00:00Z"),
132
+ timezoneId: config.clock?.timezoneId ?? "UTC",
133
+ locale: config.clock?.locale ?? "en-GB",
134
+ },
135
+ theme: {
136
+ // A neutral blue, not anyone's brand. An app says its own accent.
137
+ accent: config.theme?.accent ?? "#2f6fed",
138
+ cursorScale: config.theme?.cursorScale ?? 2.1,
139
+ presentation: presentation ?? null,
140
+ },
141
+ output: outputSize(viewport, outputWidth),
142
+ motion: { ...MOTION, ...config.motion },
143
+ scenes: config.scenes ? abs(config.scenes) : null,
144
+ sequence: config.sequence ?? null,
145
+ };
146
+ }
147
+ export function renderConfigOf(config) {
148
+ return {
149
+ output: config.output,
150
+ theme: config.theme,
151
+ motion: config.motion,
152
+ };
153
+ }
154
+ //# sourceMappingURL=config.js.map