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,36 @@
1
+ import { parseArgs } from "node:util";
2
+ import { launchBrowser } from "../../driver/launch.js";
3
+ import { loadConfig } from "../../loader.js";
4
+ import { formatMeasurement, measureSelector } from "../../inspect/measure.js";
5
+ import { drivingApp } from "../app.js";
6
+ import { openAppPage } from "./open-app.js";
7
+ export async function measure(argv) {
8
+ const { positionals, values } = parseArgs({
9
+ args: argv,
10
+ options: { json: { type: "boolean" } },
11
+ strict: false,
12
+ allowPositionals: true,
13
+ });
14
+ const [path, selector] = positionals;
15
+ if (!path || !selector) {
16
+ process.stderr.write("Usage: versioncam measure <path> <selector>\n");
17
+ return 1;
18
+ }
19
+ const config = await loadConfig(process.cwd());
20
+ return drivingApp(config, async () => {
21
+ const browser = await launchBrowser();
22
+ try {
23
+ const { page, context } = await openAppPage(browser, config, path);
24
+ const measurement = await measureSelector(page, selector);
25
+ process.stdout.write(values.json
26
+ ? `${JSON.stringify(measurement, null, 2)}\n`
27
+ : `${formatMeasurement(measurement)}\n`);
28
+ await context.close();
29
+ return measurement.count > 0 ? 0 : 1;
30
+ }
31
+ finally {
32
+ await browser.close();
33
+ }
34
+ });
35
+ }
36
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.js","sourceRoot":"","sources":["../../../src/cli/commands/measure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAc;IAC1C,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACxC,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;QACtC,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;KACvB,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,WAAuB,CAAC;IACjD,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACtE,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,OAAO,UAAU,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;QACnC,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACnE,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,MAAM,CAAC,IAAI;gBACT,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI;gBAC7C,CAAC,CAAC,GAAG,iBAAiB,CAAC,WAAW,CAAC,IAAI,CAC1C,CAAC;YACF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,WAAW,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Browser, BrowserContext, Page } from "playwright";
2
+ import type { ResolvedConfig } from "../../config.js";
3
+ /**
4
+ * Open the app the way a clip would, minus the capture.
5
+ *
6
+ * The inspection commands have to see the page in the state a clip would find
7
+ * it in — signed in, settled — or their inventory describes a loading screen.
8
+ * The freeze script is deliberately not installed: a person looking at the
9
+ * page wants it behaving normally.
10
+ */
11
+ export declare function openAppPage(browser: Browser, config: ResolvedConfig, path: string): Promise<{
12
+ page: Page;
13
+ context: BrowserContext;
14
+ }>;
@@ -0,0 +1,45 @@
1
+ import { MUTATION_COUNTER_SCRIPT } from "../../driver/page-hooks.js";
2
+ import { settlePage } from "../../driver/settle.js";
3
+ /**
4
+ * Open the app the way a clip would, minus the capture.
5
+ *
6
+ * The inspection commands have to see the page in the state a clip would find
7
+ * it in — signed in, settled — or their inventory describes a loading screen.
8
+ * The freeze script is deliberately not installed: a person looking at the
9
+ * page wants it behaving normally.
10
+ */
11
+ export async function openAppPage(browser, config, path) {
12
+ const context = await browser.newContext({
13
+ viewport: config.viewport,
14
+ deviceScaleFactor: config.dpr,
15
+ timezoneId: config.clock.timezoneId,
16
+ locale: config.clock.locale,
17
+ storageState: config.auth?.storageState,
18
+ });
19
+ const page = await context.newPage();
20
+ await page.addInitScript(MUTATION_COUNTER_SCRIPT);
21
+ let inFlight = 0;
22
+ page.on("request", () => (inFlight += 1));
23
+ page.on("requestfinished", () => (inFlight = Math.max(0, inFlight - 1)));
24
+ page.on("requestfailed", () => (inFlight = Math.max(0, inFlight - 1)));
25
+ const url = path.startsWith("http") ? path : `${config.baseUrl}${path}`;
26
+ await page.goto(url, { waitUntil: "domcontentloaded" });
27
+ if (config.auth?.login)
28
+ await config.auth.login(page);
29
+ await settlePage(page, () => inFlight, {
30
+ label: "inspect",
31
+ quietMs: config.settle.quietMs,
32
+ // A cold open, not a mid-clip wait, so it gets a cold open's budget — the
33
+ // same ninety seconds `open()` allows. The general `settle.timeoutMs` is
34
+ // fifteen, which a map app exceeds every single time: `inspect` then warned
35
+ // "timed out, still waiting on hook" on every call and returned complete,
36
+ // correct data anyway. An agent using it second-guessed that data for
37
+ // several calls before trusting it, which is a fair reading of a message
38
+ // that says a thing failed when it did not.
39
+ timeoutMs: 90_000,
40
+ yieldMs: config.settle.yieldMs,
41
+ hook: config.settle.ready ? () => config.settle.ready(page) : undefined,
42
+ });
43
+ return { page, context };
44
+ }
45
+ //# sourceMappingURL=open-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-app.js","sourceRoot":"","sources":["../../../src/cli/commands/open-app.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,MAAsB,EACtB,IAAY;IAEZ,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,iBAAiB,EAAE,MAAM,CAAC,GAAG;QAC7B,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU;QACnC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;QAC3B,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,YAAY;KACxC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,IAAI,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAElD,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvE,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;IACxE,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAExD,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK;QAAE,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEtD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,EAAE;QACrC,KAAK,EAAE,SAAS;QAChB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;QAC9B,0EAA0E;QAC1E,yEAAyE;QACzE,4EAA4E;QAC5E,0EAA0E;QAC1E,sEAAsE;QACtE,yEAAyE;QACzE,4CAA4C;QAC5C,SAAS,EAAE,MAAM;QACjB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;QAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;KACzE,CAAC,CAAC;IAEH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Open the scrubber, in a browser a person can see.
3
+ *
4
+ * Tuning a clip's easing and framing is done by eye, frame by frame, and this
5
+ * is the only way to do it — against exactly the code path the encoder uses,
6
+ * so what is tuned is what ships.
7
+ */
8
+ export declare function preview(argv: string[]): Promise<number>;
@@ -0,0 +1,55 @@
1
+ import { parseArgs } from "node:util";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { launchBrowser } from "../../driver/launch.js";
5
+ import { loadConfig } from "../../loader.js";
6
+ import { renderConfigOf } from "../../config.js";
7
+ import { bundleScenes } from "../../loader.js";
8
+ import { distPageDir, installRenderRoutes, RENDER_ORIGIN, } from "../../render/serve.js";
9
+ /**
10
+ * Open the scrubber, in a browser a person can see.
11
+ *
12
+ * Tuning a clip's easing and framing is done by eye, frame by frame, and this
13
+ * is the only way to do it — against exactly the code path the encoder uses,
14
+ * so what is tuned is what ships.
15
+ */
16
+ export async function preview(argv) {
17
+ const { positionals, values } = parseArgs({
18
+ args: argv,
19
+ options: { scene: { type: "string" }, frame: { type: "string" } },
20
+ strict: false,
21
+ allowPositionals: true,
22
+ });
23
+ const config = await loadConfig(process.cwd());
24
+ const sceneId = values.scene;
25
+ const id = sceneId ?? positionals[0];
26
+ if (!id) {
27
+ process.stderr.write("Usage: versioncam preview <id> | --scene <id>\n");
28
+ return 1;
29
+ }
30
+ const dir = join(config.recordingsDir, id);
31
+ if (!sceneId && !existsSync(join(dir, "timeline.json"))) {
32
+ process.stderr.write(`No recording for "${id}".\n`);
33
+ return 1;
34
+ }
35
+ const browser = await launchBrowser({ headless: false });
36
+ const context = await browser.newContext({
37
+ viewport: { width: 1400, height: 1000 },
38
+ deviceScaleFactor: 1,
39
+ });
40
+ const page = await context.newPage();
41
+ await installRenderRoutes(page, {
42
+ distDir: distPageDir(),
43
+ renderConfig: renderConfigOf(config),
44
+ recordingDir: sceneId ? undefined : dir,
45
+ scenesBundle: (await bundleScenes(config)) ?? undefined,
46
+ });
47
+ const query = sceneId ? `scene=${encodeURIComponent(sceneId)}` : "recording";
48
+ const frame = values.frame ? `&frame=${values.frame}` : "";
49
+ await page.goto(`${RENDER_ORIGIN}/?${query}${frame}`);
50
+ process.stdout.write("Scrubber open. Close the window to finish.\n");
51
+ await page.waitForEvent("close", { timeout: 0 });
52
+ await browser.close();
53
+ return 0;
54
+ }
55
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.js","sourceRoot":"","sources":["../../../src/cli/commands/preview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Record every clip the config points at, serially.
3
+ *
4
+ * Serial by necessity rather than by caution: clips drive one app, and apps
5
+ * commonly hold per-session server state that two concurrent recordings
6
+ * corrupt. A failure does not stop the run — the other clips are still worth
7
+ * having, and a report of which ones broke is more useful than the first
8
+ * error alone.
9
+ */
10
+ export declare function record(argv: string[]): Promise<number>;
@@ -0,0 +1,86 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { launchBrowser } from "../../driver/launch.js";
5
+ import { runClip } from "../../driver/clip.js";
6
+ import { formatTiming } from "../../core/timing.js";
7
+ import { loadClips, loadConfig } from "../../loader.js";
8
+ import { drivingApp } from "../app.js";
9
+ /**
10
+ * Record every clip the config points at, serially.
11
+ *
12
+ * Serial by necessity rather than by caution: clips drive one app, and apps
13
+ * commonly hold per-session server state that two concurrent recordings
14
+ * corrupt. A failure does not stop the run — the other clips are still worth
15
+ * having, and a report of which ones broke is more useful than the first
16
+ * error alone.
17
+ */
18
+ export async function record(argv) {
19
+ const { values, positionals } = parseArgs({
20
+ args: argv,
21
+ options: { draft: { type: "boolean", default: false } },
22
+ strict: false,
23
+ allowPositionals: true,
24
+ });
25
+ const draft = values.draft === true;
26
+ const config = await loadConfig(process.cwd());
27
+ const clips = await loadClips(config, positionals);
28
+ if (clips.length === 0) {
29
+ process.stderr.write(positionals.length > 0
30
+ ? `No clip matched ${positionals.join(", ")}.\n`
31
+ : `No clips found. The config looks for ${config.clips.join(", ")} under ${config.root}.\n`);
32
+ return 1;
33
+ }
34
+ return drivingApp(config, () => recordAll(config, clips, draft));
35
+ }
36
+ async function recordAll(config, clips, draft) {
37
+ const browser = await launchBrowser();
38
+ const failures = [];
39
+ try {
40
+ for (const clip of clips) {
41
+ const started = Date.now();
42
+ try {
43
+ const { timeline, cost } = await runClip(browser, config, clip, {
44
+ draft,
45
+ });
46
+ // Both seconds are named. The line used to print "(7.5s) … — 2.6s" and
47
+ // leave the reader to work out that the first is the clip and the
48
+ // second is the machine; they move in opposite directions when a clip
49
+ // gets slower to record, which makes guessing worse than useless.
50
+ const plays = (timeline.durationFrames / timeline.fps).toFixed(1);
51
+ const took = ((Date.now() - started) / 1000).toFixed(1);
52
+ process.stdout.write(` ${clip.id}${draft ? " (draft)" : ""}: ${timeline.durationFrames} frames, ` +
53
+ `${timeline.states.length} states, ${timeline.cuts.length} cuts ` +
54
+ `— plays ${plays}s · took ${took}s\n` +
55
+ ` ${formatTiming(cost)}\n`);
56
+ }
57
+ catch (error) {
58
+ failures.push({ id: clip.id, error: error });
59
+ process.stdout.write(` ${clip.id}: FAILED\n`);
60
+ }
61
+ }
62
+ }
63
+ finally {
64
+ await browser.close();
65
+ }
66
+ if (failures.length > 0) {
67
+ process.stderr.write("\n");
68
+ for (const { id, error } of failures) {
69
+ // The message says what broke; the picture says why. An authoring agent
70
+ // spent two of its three attempts reasoning about plausible causes —
71
+ // timing races, mount races, drag granularity — for a failure that was
72
+ // obvious in one look at the screenshot: its drag had landed on the
73
+ // chart's axis labels instead of the brush. It had been told to read the
74
+ // file and still treated it as a secondary check, because nothing at the
75
+ // moment of failure said the file was there. Now it does.
76
+ const shot = join(config.recordingsDir, id, "failure.png");
77
+ const picture = existsSync(shot)
78
+ ? `\n the page when it gave up: ${shot}`
79
+ : "";
80
+ process.stderr.write(`${id}\n ${error.message}${picture}\n\n`);
81
+ }
82
+ return 1;
83
+ }
84
+ return 0;
85
+ }
86
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../../../src/cli/commands/record.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function render(argv: string[]): Promise<number>;
@@ -0,0 +1,93 @@
1
+ import { parseArgs } from "node:util";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { launchBrowser } from "../../driver/launch.js";
5
+ import { loadConfig, loadSceneIds } from "../../loader.js";
6
+ import { renderRecording, renderScene } from "../../render/render.js";
7
+ import { buildSequence } from "../../render/sequence.js";
8
+ /** Recordings on disk, newest config first, filtered by substring. */
9
+ function recordedIds(recordingsDir, only) {
10
+ if (!existsSync(recordingsDir))
11
+ return [];
12
+ return readdirSync(recordingsDir, { withFileTypes: true })
13
+ .filter((e) => e.isDirectory())
14
+ .map((e) => e.name)
15
+ .filter((id) => existsSync(join(recordingsDir, id, "timeline.json")))
16
+ .filter((id) => only.length === 0 || only.some((n) => id.includes(n)))
17
+ .sort();
18
+ }
19
+ export async function render(argv) {
20
+ const { positionals, values } = parseArgs({
21
+ args: argv,
22
+ options: {
23
+ scene: { type: "string", multiple: true },
24
+ sequence: { type: "boolean" },
25
+ },
26
+ strict: false,
27
+ allowPositionals: true,
28
+ });
29
+ const config = await loadConfig(process.cwd());
30
+ const only = positionals;
31
+ const wantedScenes = values.scene ?? [];
32
+ // With no `--scene`, every scene the app defines is rendered too: a config
33
+ // that names a scenes module means those scenes are part of the deliverable.
34
+ const sceneIds = wantedScenes.length > 0 ? wantedScenes : await loadSceneIds(config);
35
+ const recordings = recordedIds(config.recordingsDir, only);
36
+ if (recordings.length === 0 && sceneIds.length === 0) {
37
+ process.stderr.write(`Nothing to render. Run \`versioncam record\` first — recordings live in ${config.recordingsDir}.\n`);
38
+ return 1;
39
+ }
40
+ const browser = await launchBrowser();
41
+ const rendered = [];
42
+ const failures = [];
43
+ try {
44
+ for (const id of recordings) {
45
+ try {
46
+ const out = await renderRecording(browser, config, id);
47
+ rendered.push(id);
48
+ process.stdout.write(` ${id}: ${out.durationFrames} frames at ${out.fps}fps ` +
49
+ `(${(out.durationFrames / out.fps).toFixed(1)}s)\n`);
50
+ }
51
+ catch (error) {
52
+ failures.push({ id, error: error });
53
+ }
54
+ }
55
+ for (const id of sceneIds) {
56
+ if (only.length > 0 && !only.some((n) => id.includes(n)))
57
+ continue;
58
+ try {
59
+ const out = await renderScene(browser, config, id);
60
+ rendered.push(id);
61
+ process.stdout.write(` ${id} (scene): ${out.durationFrames} frames at ${out.fps}fps\n`);
62
+ }
63
+ catch (error) {
64
+ failures.push({ id, error: error });
65
+ }
66
+ }
67
+ }
68
+ finally {
69
+ await browser.close();
70
+ }
71
+ if (values.sequence && rendered.length >= 2) {
72
+ // The config's order when it states one, since a sequence is an argument
73
+ // and not an accident of how the directory happens to sort.
74
+ const order = config.sequence ?? rendered;
75
+ const result = await buildSequence(order.filter((id) => rendered.includes(id)), config.outputDir);
76
+ process.stdout.write(` sequence: ${result.used.length} clips, ${result.total.toFixed(1)}s\n`);
77
+ if (result.missing.length > 0) {
78
+ process.stdout.write(` (not rendered: ${result.missing.join(", ")})\n`);
79
+ }
80
+ }
81
+ else if (values.sequence) {
82
+ process.stderr.write(" sequence needs at least two rendered clips.\n");
83
+ }
84
+ if (failures.length > 0) {
85
+ process.stderr.write("\n");
86
+ for (const { id, error } of failures) {
87
+ process.stderr.write(`${id}\n ${error.message}\n\n`);
88
+ }
89
+ return 1;
90
+ }
91
+ return 0;
92
+ }
93
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/cli/commands/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Check a recording against what it was supposed to be — without a model, and
3
+ * without a browser. Everything here is in the files the recording left
4
+ * behind, which is what makes it something CI can run.
5
+ */
6
+ export declare function review(argv: string[]): Promise<number>;
@@ -0,0 +1,89 @@
1
+ import { parseArgs } from "node:util";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { loadConfig } from "../../loader.js";
5
+ import { formatReview, reviewRecording } from "../../review/review.js";
6
+ const USAGE = "Usage: versioncam review <id> [--beats <file>] [--min <s>] [--max <s>]\n";
7
+ /** The beat sheet, checked as it is read: a bad one is an authoring mistake. */
8
+ function readBeats(file) {
9
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
10
+ if (!Array.isArray(parsed)) {
11
+ throw new Error(`${file} must be an array of { label, expect }.`);
12
+ }
13
+ return parsed.map((entry, index) => {
14
+ const beat = entry;
15
+ if (typeof beat?.label !== "string" || beat.label.length === 0) {
16
+ throw new Error(`${file}: beat ${index} has no \`label\`.`);
17
+ }
18
+ return { label: beat.label, expect: beat.expect };
19
+ });
20
+ }
21
+ function bound(value, flag) {
22
+ if (value === undefined)
23
+ return undefined;
24
+ const seconds = Number(value);
25
+ if (!Number.isFinite(seconds)) {
26
+ throw new Error(`--${flag} must be a number of seconds, got "${value}".`);
27
+ }
28
+ return seconds;
29
+ }
30
+ /**
31
+ * Check a recording against what it was supposed to be — without a model, and
32
+ * without a browser. Everything here is in the files the recording left
33
+ * behind, which is what makes it something CI can run.
34
+ */
35
+ export async function review(argv) {
36
+ const { positionals, values } = parseArgs({
37
+ args: argv,
38
+ options: {
39
+ beats: { type: "string" },
40
+ min: { type: "string" },
41
+ max: { type: "string" },
42
+ },
43
+ strict: false,
44
+ allowPositionals: true,
45
+ });
46
+ const id = positionals[0];
47
+ if (!id) {
48
+ process.stderr.write(USAGE);
49
+ return 1;
50
+ }
51
+ const config = await loadConfig(process.cwd());
52
+ const dir = join(config.recordingsDir, id);
53
+ const timelineFile = join(dir, "timeline.json");
54
+ if (!existsSync(timelineFile)) {
55
+ process.stderr.write(`No recording for "${id}". Run \`versioncam record ${id}\` first.\n`);
56
+ return 1;
57
+ }
58
+ const recorded = JSON.parse(readFileSync(timelineFile, "utf8"));
59
+ const settlesFile = join(dir, "settles.json");
60
+ const settles = existsSync(settlesFile)
61
+ ? JSON.parse(readFileSync(settlesFile, "utf8"))
62
+ : [];
63
+ let beats = [];
64
+ if (typeof values.beats === "string") {
65
+ const file = resolve(process.cwd(), values.beats);
66
+ if (!existsSync(file)) {
67
+ process.stderr.write(`No beat sheet at ${file}.\n`);
68
+ return 1;
69
+ }
70
+ beats = readBeats(file);
71
+ }
72
+ const result = reviewRecording({
73
+ // A recording made before marks existed has none, and then the beat and
74
+ // progress clauses have nothing to work with — which they say.
75
+ timeline: { ...recorded, marks: recorded.marks ?? [] },
76
+ settles,
77
+ beats,
78
+ // The flags override the config, which overrides the default. A clip's
79
+ // right length is a house style, not a law: an app whose clips are one
80
+ // crisp interaction each says so in `review` and is not argued with.
81
+ bounds: {
82
+ minSeconds: bound(values.min, "min") ?? config.review.minSeconds,
83
+ maxSeconds: bound(values.max, "max") ?? config.review.maxSeconds,
84
+ },
85
+ });
86
+ process.stdout.write(`${formatReview(id, result)}\n`);
87
+ return result.pass ? 0 : 1;
88
+ }
89
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../../src/cli/commands/review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function sheet(argv: string[]): Promise<number>;
@@ -0,0 +1,48 @@
1
+ import { parseArgs } from "node:util";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { launchBrowser } from "../../driver/launch.js";
5
+ import { loadConfig } from "../../loader.js";
6
+ import { contactSheet, readTimeline } from "../../render/render.js";
7
+ import { markedFrames } from "../../render/sampling.js";
8
+ export async function sheet(argv) {
9
+ const { positionals, values } = parseArgs({
10
+ args: argv,
11
+ options: { "at-marks": { type: "boolean", default: false } },
12
+ strict: false,
13
+ allowPositionals: true,
14
+ });
15
+ const id = positionals[0];
16
+ if (!id) {
17
+ process.stderr.write("Usage: versioncam sheet <id> [--at-marks]\n");
18
+ return 1;
19
+ }
20
+ const config = await loadConfig(process.cwd());
21
+ const dir = join(config.recordingsDir, id);
22
+ if (!existsSync(join(dir, "timeline.json"))) {
23
+ process.stderr.write(`No recording for "${id}".\n`);
24
+ return 1;
25
+ }
26
+ let frames;
27
+ if (values["at-marks"] === true) {
28
+ const timeline = await readTimeline(config, id);
29
+ // A recording made before marks existed has none, and then this is the
30
+ // first frame and the last one — which is honest: there are no beats to
31
+ // aim at, and saying so beats inventing eight.
32
+ frames = markedFrames(timeline.durationFrames, timeline.marks ?? [], undefined, config.motion.crossfadeFrames, timeline.cuts);
33
+ }
34
+ const browser = await launchBrowser();
35
+ try {
36
+ const file = await contactSheet(browser, config, { kind: "recording", id, dir }, { frames });
37
+ // The path first, on its own line: everything that calls this reads it.
38
+ process.stdout.write(`${file}\n`);
39
+ if (frames) {
40
+ process.stdout.write(`frames ${frames.join(" · ")}\n`);
41
+ }
42
+ return 0;
43
+ }
44
+ finally {
45
+ await browser.close();
46
+ }
47
+ }
48
+ //# sourceMappingURL=sheet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sheet.js","sourceRoot":"","sources":["../../../src/cli/commands/sheet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAc;IACxC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACxC,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;KACvB,CAAC,CAAC;IACH,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAuB,CAAC;IAChD,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACpE,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAC3C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,MAA4B,CAAC;IACjC,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChD,uEAAuE;QACvE,wEAAwE;QACxE,+CAA+C;QAC/C,MAAM,GAAG,YAAY,CACnB,QAAQ,CAAC,cAAc,EACvB,QAAQ,CAAC,KAAK,IAAI,EAAE,EACpB,SAAS,EACT,MAAM,CAAC,MAAM,CAAC,eAAe,EAC7B,QAAQ,CAAC,IAAI,CACd,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAC7B,OAAO,EACP,MAAM,EACN,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG,EAAE,EAC9B,EAAE,MAAM,EAAE,CACX,CAAC;QACF,wEAAwE;QACxE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Does this app record the same way twice?
3
+ *
4
+ * The README's rule — when a clip will not reproduce, record it twice the same
5
+ * way first — as a command, and as the first thing done in a repository before
6
+ * anything is authored for it. A clip is only as deterministic as the app it
7
+ * films: a page that renders the wall-clock time, or animates something the
8
+ * recorder's clock does not drive, gives a different video every run, and
9
+ * every clip written against it will fail CI for reasons nobody wrote.
10
+ *
11
+ * Draft by default, because the question is about the app and not the pixels:
12
+ * a draft is the same page at the same size, a quarter of the frames.
13
+ */
14
+ export declare function stability(argv: string[]): Promise<number>;