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,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A committed launcher for the CLI.
4
+ *
5
+ * `bin` cannot point straight at `dist/cli/main.js`: npm creates a bin symlink
6
+ * only when its target already exists, and on a fresh checkout the package is
7
+ * installed before it is built. The link is then silently absent and
8
+ * `versioncam` is not a command — which is exactly the shape the failure took
9
+ * in CI.
10
+ *
11
+ * This file always exists, so the link is always made, and it defers to the
12
+ * build at run time.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const entry = join(dirname(fileURLToPath(import.meta.url)), "..", "dist", "cli", "main.js");
19
+
20
+ if (!existsSync(entry)) {
21
+ process.stderr.write(
22
+ "versioncam has not been built: no dist/cli/main.js.\n" +
23
+ "Reinstall it — a published version always includes the build — or,\n" +
24
+ "in a checkout of its source, run `npm run build`.\n",
25
+ );
26
+ process.exit(1);
27
+ }
28
+
29
+ await import(entry);
@@ -0,0 +1,28 @@
1
+ import type { Timeline } from "../src/core/timeline";
2
+ import type { RenderConfig } from "../src/config";
3
+ export type Canvas2D = CanvasRenderingContext2D;
4
+ export type Frames = {
5
+ /** Decoded state images, indexed the same way `timeline.states` is. */
6
+ images: (ImageBitmap | HTMLImageElement)[];
7
+ };
8
+ /**
9
+ * Which state to show at `frame`, and what to crossfade it with.
10
+ *
11
+ * Only a settle produces a crossfade. Everywhere else a change between
12
+ * consecutive frames is a real one-frame change and must stay crisp —
13
+ * blurring those would smear the cursor's own motion.
14
+ */
15
+ export declare function stateForFrame(timeline: Timeline, frame: number, crossfadeFrames: number): {
16
+ current: number;
17
+ previous: number | null;
18
+ alpha: number;
19
+ };
20
+ /**
21
+ * Draw one output frame. A pure function of `frame` — nothing here reads the
22
+ * wall clock, which is what lets the renderer be scrubbed, re-run and
23
+ * byte-compared.
24
+ */
25
+ export declare function drawRecordingFrame(ctx: Canvas2D, timeline: Timeline, frames: Frames, frame: number, canvas: {
26
+ width: number;
27
+ height: number;
28
+ }, config: RenderConfig): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The render page.
3
+ *
4
+ * Two jobs, one code path: a scrubber for tuning motion by eye, and a
5
+ * `window.__render.setFrame(n)` the encoder drives headlessly. They must be
6
+ * the same path — a preview that renders differently from the export is worse
7
+ * than no preview.
8
+ *
9
+ * Everything it needs arrives over routes the renderer installs, not from a
10
+ * web server: the recording, the config, and the app's scene bundle if it has
11
+ * one. Rendering therefore needs nothing running except this page.
12
+ */
13
+ type RenderApi = {
14
+ ready: Promise<void>;
15
+ setFrame: (frame: number) => Promise<void>;
16
+ durationFrames: number;
17
+ fps: number;
18
+ };
19
+ declare global {
20
+ interface Window {
21
+ __render?: RenderApi;
22
+ }
23
+ }
24
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The fixed half of the clip's look.
3
+ *
4
+ * What an app chooses — its accent, whether there is a backdrop and a device
5
+ * frame at all — comes from the config. What is here is the craft that should
6
+ * not vary between apps: corner radii, shadow weights, the cursor's geometry,
7
+ * the caption's type. An app that wants these different is asking for a
8
+ * different renderer, not a different config.
9
+ */
10
+ export declare const CHROME: {
11
+ /** Device frame, used only when presentation asks for one. */
12
+ readonly radius: 16;
13
+ readonly hairline: "rgba(255, 255, 255, 0.16)";
14
+ readonly shadow: "rgba(0, 0, 0, 0.45)";
15
+ readonly shadowBlur: 60;
16
+ readonly shadowOffsetY: 18;
17
+ readonly captionFont: "600 30px \"Helvetica Neue\", Helvetica, Arial, \"Liberation Sans\", sans-serif";
18
+ readonly captionColor: "rgba(255, 255, 255, 0.94)";
19
+ readonly captionShadow: "rgba(0, 0, 0, 0.5)";
20
+ /** The band a caption sits on when it overlays the app rather than the padding. */
21
+ readonly captionScrim: "rgba(0, 0, 0, 0.45)";
22
+ readonly captionScrimHeight: 86;
23
+ readonly cursorFill: "#ffffff";
24
+ readonly cursorStroke: "rgba(0, 0, 0, 0.55)";
25
+ readonly cursorShadow: "rgba(0, 0, 0, 0.35)";
26
+ readonly ringWidth: 3;
27
+ readonly ringRadius: 10;
28
+ readonly spotlightDim: 0.55;
29
+ readonly spotlightTint: "6, 2, 16";
30
+ };
31
+ /**
32
+ * `#rrggbb` plus an alpha, for the ring's glow.
33
+ *
34
+ * Falls back to the colour untouched if it is not a hex triple, so a config
35
+ * can use `rgb()` or a named colour and only lose the glow.
36
+ */
37
+ export declare function withAlpha(color: string, alpha: number): string;
@@ -0,0 +1,59 @@
1
+ import type { ResolvedConfig } from "./config.js";
2
+ /**
3
+ * Starting the app a clip records — and, the half that matters, stopping it.
4
+ *
5
+ * Every command that drives the app runs inside `withApp`, so `record`,
6
+ * `check`, `inspect` and the rest work from a cold shell, and an agent in a
7
+ * repository it has never seen does not have to be told how to start the dev
8
+ * server, or remember to stop it. The semantics are Playwright's `webServer`
9
+ * wherever Playwright has an opinion: poll first, reuse what answers unless
10
+ * told not to, start the command otherwise, wait for it to answer.
11
+ *
12
+ * The one place this goes further is the stopping. The command runs as the
13
+ * leader of its own process group, and every way out of the command that
14
+ * started it — a normal return, a thrown error, Ctrl-C, a SIGTERM from
15
+ * whatever ran us — signals the whole group, not just the shell at its top.
16
+ * A recorder that leaves dev servers behind after a failed clip is the kind
17
+ * of thing that makes people stop using a tool.
18
+ */
19
+ export type AppHandle = {
20
+ /** True when this process started the app; false when it found it up. */
21
+ started: boolean;
22
+ /** One line for a person: what was found or done, and the HTTP status. */
23
+ detail: string;
24
+ /** Stop what was started. Resolves once the whole process group is gone. */
25
+ stop(): Promise<void>;
26
+ };
27
+ export type Probe = {
28
+ up: true;
29
+ status: number;
30
+ } | {
31
+ up: false;
32
+ reason: string;
33
+ };
34
+ /**
35
+ * One request, and any HTTP answer at all counts as up.
36
+ *
37
+ * Including a 401 and a redirect, which is how an app behind a sign-in answers,
38
+ * and it records perfectly well once `auth` is configured. Only silence means
39
+ * nothing is there. `redirect: "manual"`, so the answer is the app's own and
40
+ * not wherever a redirect happens to land.
41
+ */
42
+ export declare function probe(url: string, timeoutMs?: number): Promise<Probe>;
43
+ /**
44
+ * Make sure the app answers, starting it if the config says how.
45
+ *
46
+ * With no `webServer` in the config this does nothing, and every command
47
+ * behaves as it always has: the app is the person's to run. Otherwise it
48
+ * polls first, because a server that is already up — a developer's own `npm
49
+ * run dev` — is the common case and restarting it would be rude. Up and not
50
+ * reusable is a refusal rather than a restart: whatever is serving that port
51
+ * is not ours to replace.
52
+ */
53
+ export declare function ensureApp(config: ResolvedConfig): Promise<AppHandle>;
54
+ /**
55
+ * `ensureApp`, run `fn`, and stop whatever was started, however `fn` ends.
56
+ *
57
+ * A server that was already up is left exactly as it was found.
58
+ */
59
+ export declare function withApp<T>(config: ResolvedConfig, fn: (app: AppHandle) => Promise<T>): Promise<T>;
@@ -0,0 +1,328 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createWriteStream, mkdirSync } from "node:fs";
3
+ import { constants } from "node:os";
4
+ import { join } from "node:path";
5
+ import { inheritedEnv } from "./env.js";
6
+ /** How much of a failed start's output an error message carries. */
7
+ const TAIL_LINES = 40;
8
+ /**
9
+ * Between SIGTERM and SIGKILL. A dev server that has not exited five seconds
10
+ * after being asked is not going to, and one that is still flushing a database
11
+ * to disk needs more than the instant a SIGKILL gives it.
12
+ */
13
+ const GRACE_MS = 5_000;
14
+ /**
15
+ * One request, and any HTTP answer at all counts as up.
16
+ *
17
+ * Including a 401 and a redirect, which is how an app behind a sign-in answers,
18
+ * and it records perfectly well once `auth` is configured. Only silence means
19
+ * nothing is there. `redirect: "manual"`, so the answer is the app's own and
20
+ * not wherever a redirect happens to land.
21
+ */
22
+ export async function probe(url, timeoutMs = 5_000) {
23
+ try {
24
+ const response = await fetch(url, {
25
+ redirect: "manual",
26
+ signal: AbortSignal.timeout(timeoutMs),
27
+ });
28
+ // The body is irrelevant; cancelling it hands the socket back.
29
+ await response.body?.cancel();
30
+ return { up: true, status: response.status };
31
+ }
32
+ catch (error) {
33
+ return { up: false, reason: whyNot(error, timeoutMs) };
34
+ }
35
+ }
36
+ /**
37
+ * The part of a failed fetch worth printing.
38
+ *
39
+ * `fetch` says "fetch failed" about everything; what went wrong is on its
40
+ * `cause` — `ECONNREFUSED`, `ENOTFOUND` — and that is the word a person
41
+ * recognises.
42
+ */
43
+ function whyNot(error, timeoutMs) {
44
+ const e = error;
45
+ if (e.name === "TimeoutError")
46
+ return `no answer within ${seconds(timeoutMs)}`;
47
+ return e.cause?.code || e.cause?.message || e.message || String(error);
48
+ }
49
+ function seconds(ms) {
50
+ return `${(ms / 1000).toFixed(1)} s`;
51
+ }
52
+ /** The port a URL is served on, for a message a person can act on. */
53
+ function portOf(url) {
54
+ const u = new URL(url);
55
+ return u.port || (u.protocol === "https:" ? "443" : "80");
56
+ }
57
+ /**
58
+ * Make sure the app answers, starting it if the config says how.
59
+ *
60
+ * With no `webServer` in the config this does nothing, and every command
61
+ * behaves as it always has: the app is the person's to run. Otherwise it
62
+ * polls first, because a server that is already up — a developer's own `npm
63
+ * run dev` — is the common case and restarting it would be rude. Up and not
64
+ * reusable is a refusal rather than a restart: whatever is serving that port
65
+ * is not ours to replace.
66
+ */
67
+ export async function ensureApp(config) {
68
+ const server = config.webServer;
69
+ if (!server) {
70
+ return {
71
+ started: false,
72
+ detail: "no webServer in the config",
73
+ stop: async () => { },
74
+ };
75
+ }
76
+ const found = await probe(server.url);
77
+ if (found.up) {
78
+ if (!server.reuseExistingServer) {
79
+ throw new Error(`Something is already serving ${server.url} (port ${portOf(server.url)}), ` +
80
+ `and webServer.reuseExistingServer is false` +
81
+ (process.env.CI ? " — the default when CI is set" : "") +
82
+ ". It is not ours to replace: stop it, or allow reusing it.");
83
+ }
84
+ return {
85
+ started: false,
86
+ detail: `already serving at ${server.url} — HTTP ${found.status}`,
87
+ stop: async () => { },
88
+ };
89
+ }
90
+ return start(server, config.outDir);
91
+ }
92
+ /**
93
+ * `ensureApp`, run `fn`, and stop whatever was started, however `fn` ends.
94
+ *
95
+ * A server that was already up is left exactly as it was found.
96
+ */
97
+ export async function withApp(config, fn) {
98
+ const app = await ensureApp(config);
99
+ try {
100
+ return await fn(app);
101
+ }
102
+ finally {
103
+ await app.stop();
104
+ }
105
+ }
106
+ /** A line reader that keeps only the last `TAIL_LINES` lines it was fed. */
107
+ function tailOf(stream, tail) {
108
+ let partial = "";
109
+ stream.setEncoding("utf8");
110
+ stream.on("data", (chunk) => {
111
+ const lines = (partial + chunk).split("\n");
112
+ partial = lines.pop() ?? "";
113
+ tail.push(...lines);
114
+ if (tail.length > TAIL_LINES)
115
+ tail.splice(0, tail.length - TAIL_LINES);
116
+ });
117
+ stream.on("end", () => {
118
+ if (partial)
119
+ tail.push(partial);
120
+ });
121
+ }
122
+ async function start(server, outDir) {
123
+ mkdirSync(outDir, { recursive: true });
124
+ const logFile = join(outDir, "webserver.log");
125
+ const log = createWriteStream(logFile, { flags: "a" });
126
+ log.write(`\n--- ${new Date().toISOString()} ${server.command} (in ${server.cwd})\n`);
127
+ const began = Date.now();
128
+ // `detached` makes the shell the leader of a new process group, so one
129
+ // signal to the group reaches everything the command started — `npm run
130
+ // dev`'s Vite, `make start`'s three services — and not only the shell. It
131
+ // also takes the group out of the terminal's, so a Ctrl-C meant for us does
132
+ // not reach the server before we have decided what to do about it.
133
+ //
134
+ // The environment is the shell's, not this process's: what the recorder
135
+ // loaded from `.env.recording` for its own sign-in stays with the recorder.
136
+ const child = spawn(server.command, {
137
+ shell: true,
138
+ cwd: server.cwd,
139
+ env: { ...inheritedEnv(), ...server.env },
140
+ detached: true,
141
+ stdio: ["ignore", "pipe", "pipe"],
142
+ });
143
+ // Listened to for the life of the child, not once: a second "error" with no
144
+ // listener would take this whole process down with it.
145
+ const spawnError = new Promise((resolve) => child.on("error", resolve));
146
+ if (child.pid === undefined) {
147
+ // Spawning itself failed — a `cwd` that does not exist, most likely — and
148
+ // the reason arrives as an event, not a return value.
149
+ const error = await spawnError;
150
+ log.end();
151
+ throw new Error(`webServer could not start "${server.command}" in ${server.cwd}: ${error.message}`);
152
+ }
153
+ const pid = child.pid;
154
+ const tail = [];
155
+ child.stdout.pipe(log, { end: false });
156
+ child.stderr.pipe(log, { end: false });
157
+ tailOf(child.stdout, tail);
158
+ tailOf(child.stderr, tail);
159
+ // Asserted rather than annotated, or TypeScript narrows it to `null` for
160
+ // good: it cannot see the assignment inside the callback below.
161
+ let exit = null;
162
+ const exited = new Promise((resolve) => child.once("exit", (code, signal) => {
163
+ exit = { code, signal };
164
+ resolve();
165
+ }));
166
+ // Every way out. A signal handler sends SIGTERM to the group *first*, and
167
+ // synchronously, because whatever else is listening may exit the process
168
+ // before an async stop gets anywhere: Playwright's own SIGINT handler closes
169
+ // its browsers and calls `process.exit(130)`. The `exit` handler is the last
170
+ // resort for the same reason — it cannot wait, but it can still ask.
171
+ let stopped = false;
172
+ const onSignal = (signal) => {
173
+ signalGroup(pid, "SIGTERM");
174
+ void stop().finally(() => process.exit(128 + (constants.signals[signal] ?? 0)));
175
+ };
176
+ const onExit = () => {
177
+ if (!stopped)
178
+ signalGroup(pid, "SIGTERM");
179
+ };
180
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
181
+ for (const signal of signals)
182
+ process.on(signal, onSignal);
183
+ process.on("exit", onExit);
184
+ let stopping = null;
185
+ const stop = () => (stopping ??= (async () => {
186
+ const outcome = await stopGroup(pid, exited);
187
+ stopped = true;
188
+ for (const signal of signals)
189
+ process.off(signal, onSignal);
190
+ process.off("exit", onExit);
191
+ // A descendant that left the group — something that daemonises — can
192
+ // still hold the pipes open, and an open pipe keeps this process alive
193
+ // after its work is done. Whatever it is, it is no longer ours to read.
194
+ child.stdout.destroy();
195
+ child.stderr.destroy();
196
+ child.unref();
197
+ log.write(`--- ${outcome}\n`);
198
+ // Whether the group is gone is a question about processes. Whether the
199
+ // app is gone is a question about the port, and it is the one the next
200
+ // command will trip over — a container, say, that the start command
201
+ // handed the work to and that no signal to the group reaches.
202
+ const after = await probe(server.url, 1_000);
203
+ if (after.up) {
204
+ const warning = `webServer: stopped "${server.command}", but ${server.url} still ` +
205
+ "answers: something it started is outside its process group and still running.";
206
+ log.write(`--- ${warning}\n`);
207
+ process.stderr.write(`${warning}\n`);
208
+ }
209
+ // Flushed before this resolves, so whoever reads the log next — a person
210
+ // after a failed start, a test — reads all of it.
211
+ await new Promise((resolve) => log.end(resolve));
212
+ })());
213
+ const fail = async (why) => {
214
+ await stop();
215
+ // The exit can land a moment before the last of the output does.
216
+ await within(new Promise((resolve) => child.once("close", resolve)), 250);
217
+ const output = tail.length
218
+ ? `\nIts last ${tail.length} lines of output (all of it is in ${logFile}):\n` +
219
+ tail.map((line) => ` ${line}`).join("\n")
220
+ : `\nIt printed nothing (${logFile}).`;
221
+ throw new Error(`${why}${output}`);
222
+ };
223
+ const deadline = began + server.timeoutMs;
224
+ // Playwright's cadence, short at first because most dev servers answer
225
+ // within a second, then every half second so a slow one is not hammered.
226
+ const delays = [100, 250, 500];
227
+ for (let attempt = 0;; attempt += 1) {
228
+ if (exit) {
229
+ const { code, signal } = exit;
230
+ return fail(`webServer "${server.command}" exited ` +
231
+ (signal ? `on ${signal}` : `with code ${code}`) +
232
+ ` before ${server.url} answered.`);
233
+ }
234
+ const remaining = deadline - Date.now();
235
+ if (remaining <= 0) {
236
+ return fail(`webServer timed out: "${server.command}" did not answer at ${server.url} ` +
237
+ `within ${seconds(server.timeoutMs)}.`);
238
+ }
239
+ const answer = await probe(server.url, Math.min(5_000, remaining));
240
+ if (answer.up) {
241
+ return {
242
+ started: true,
243
+ detail: `started with "${server.command}", HTTP ${answer.status} ` +
244
+ `after ${seconds(Date.now() - began)}`,
245
+ stop,
246
+ };
247
+ }
248
+ await within(exited, Math.min(delays[Math.min(attempt, delays.length - 1)], remaining));
249
+ }
250
+ }
251
+ function sleep(ms) {
252
+ return new Promise((resolve) => setTimeout(resolve, ms));
253
+ }
254
+ /**
255
+ * `promise`, or `ms`, whichever comes first — and the timer cleared when the
256
+ * promise wins. A bare `Promise.race` with a sleep leaves the losing timer
257
+ * armed, and an armed timer holds the process open: every command would sit
258
+ * out the rest of a five-second grace period after it had finished.
259
+ */
260
+ function within(promise, ms) {
261
+ return new Promise((resolve) => {
262
+ const timer = setTimeout(resolve, ms);
263
+ void promise.then(() => {
264
+ clearTimeout(timer);
265
+ resolve();
266
+ });
267
+ });
268
+ }
269
+ /**
270
+ * Signal every process in the group. False when nothing in it can be.
271
+ *
272
+ * "Nothing" has two spellings. ESRCH is the group being gone. EPERM, on macOS,
273
+ * is the group still existing with only zombies in it — processes that have
274
+ * exited and are waiting for launchd to reap them — because the kernel skips
275
+ * zombies and then reports that it found no process it was allowed to signal.
276
+ * Neither holds a port. Anything else is reported rather than thrown: this
277
+ * runs in `finally` blocks and signal handlers, where throwing would bury the
278
+ * error that got us there.
279
+ */
280
+ function signalGroup(pid, signal) {
281
+ try {
282
+ process.kill(-pid, signal);
283
+ return true;
284
+ }
285
+ catch (error) {
286
+ const code = error.code;
287
+ if (code !== "ESRCH" && code !== "EPERM") {
288
+ process.stderr.write(`webServer: could not signal process group ${pid} (${code}).\n`);
289
+ }
290
+ return false;
291
+ }
292
+ }
293
+ /**
294
+ * SIGTERM, up to `GRACE_MS` for the group to go, then SIGKILL. Returns what
295
+ * happened, for the log: whether a server needed killing is exactly what a
296
+ * database left in a bad state makes you want to know.
297
+ *
298
+ * "Gone" is the group, not the shell at its top: the shell exits at once and
299
+ * its children — the actual server — are what hold the port. Waiting for the
300
+ * shell's own exit too lets Node reap it, since an exited-but-unreaped child
301
+ * is still a member of the group as far as `kill(-pid, 0)` is concerned.
302
+ */
303
+ async function stopGroup(pid, exited) {
304
+ const began = Date.now();
305
+ if (!signalGroup(pid, "SIGTERM"))
306
+ return "stopped: it had already exited";
307
+ if (await groupGone(pid, exited, GRACE_MS)) {
308
+ return `stopped: SIGTERM, all of it gone after ${seconds(Date.now() - began)}`;
309
+ }
310
+ signalGroup(pid, "SIGKILL");
311
+ if (await groupGone(pid, exited, 2_000)) {
312
+ return `stopped: still running ${seconds(GRACE_MS)} after SIGTERM, so SIGKILL`;
313
+ }
314
+ const warning = `webServer: process group ${pid} is still there after SIGKILL.`;
315
+ process.stderr.write(`${warning}\n`);
316
+ return warning;
317
+ }
318
+ async function groupGone(pid, exited, withinMs) {
319
+ const until = Date.now() + withinMs;
320
+ await within(exited, withinMs);
321
+ while (Date.now() < until) {
322
+ if (!signalGroup(pid, 0))
323
+ return true;
324
+ await sleep(50);
325
+ }
326
+ return !signalGroup(pid, 0);
327
+ }
328
+ //# sourceMappingURL=app-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-server.js","sourceRoot":"","sources":["../src/app-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { ResolvedConfig } from "../config.js";
2
+ /**
3
+ * `withApp`, for a command: the same, plus one line when it had to start the
4
+ * app itself.
5
+ *
6
+ * On stderr, because stdout belongs to the command — `inspect --json` is
7
+ * parsed, and the first line `sheet` prints is a path things read. And only
8
+ * when something was started: that is worth knowing, since it is why the
9
+ * command took a few seconds longer and why nothing is listening once it has
10
+ * finished, while a server found already running is the normal case and not
11
+ * worth a line in every call an authoring loop makes.
12
+ */
13
+ export declare function drivingApp<T>(config: ResolvedConfig, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,21 @@
1
+ import { withApp } from "../app-server.js";
2
+ /**
3
+ * `withApp`, for a command: the same, plus one line when it had to start the
4
+ * app itself.
5
+ *
6
+ * On stderr, because stdout belongs to the command — `inspect --json` is
7
+ * parsed, and the first line `sheet` prints is a path things read. And only
8
+ * when something was started: that is worth knowing, since it is why the
9
+ * command took a few seconds longer and why nothing is listening once it has
10
+ * finished, while a server found already running is the normal case and not
11
+ * worth a line in every call an authoring loop makes.
12
+ */
13
+ export function drivingApp(config, fn) {
14
+ return withApp(config, async (app) => {
15
+ if (app.started) {
16
+ process.stderr.write(`app: ${app.detail}; it stops when this command does\n`);
17
+ }
18
+ return fn();
19
+ });
20
+ }
21
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/cli/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAG3C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CACxB,MAAsB,EACtB,EAAoB;IAEpB,OAAO,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACnC,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,QAAQ,GAAG,CAAC,MAAM,qCAAqC,CACxD,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Record every clip and report which still work.
3
+ *
4
+ * This is the command a CI job runs. It does not render — a demo that no
5
+ * longer matches the app fails while being recorded, at the step that no
6
+ * longer exists, and encoding a video of the wreckage helps nobody.
7
+ */
8
+ export declare function check(argv: string[]): Promise<number>;
@@ -0,0 +1,51 @@
1
+ import { parseArgs } from "node:util";
2
+ import { launchBrowser } from "../../driver/launch.js";
3
+ import { runClip } from "../../driver/clip.js";
4
+ import { loadClips, loadConfig } from "../../loader.js";
5
+ import { drivingApp } from "../app.js";
6
+ /**
7
+ * Record every clip and report which still work.
8
+ *
9
+ * This is the command a CI job runs. It does not render — a demo that no
10
+ * longer matches the app fails while being recorded, at the step that no
11
+ * longer exists, and encoding a video of the wreckage helps nobody.
12
+ */
13
+ export async function check(argv) {
14
+ const { positionals } = parseArgs({
15
+ args: argv,
16
+ strict: false,
17
+ allowPositionals: true,
18
+ });
19
+ const config = await loadConfig(process.cwd());
20
+ const clips = await loadClips(config, positionals);
21
+ if (clips.length === 0) {
22
+ process.stderr.write("No clips to check.\n");
23
+ return 1;
24
+ }
25
+ return drivingApp(config, () => checkAll(config, clips));
26
+ }
27
+ async function checkAll(config, clips) {
28
+ const browser = await launchBrowser();
29
+ const failures = [];
30
+ try {
31
+ for (const clip of clips) {
32
+ try {
33
+ await runClip(browser, config, clip);
34
+ process.stdout.write(`PASS ${clip.id}\n`);
35
+ }
36
+ catch (error) {
37
+ failures.push({ id: clip.id, error: error });
38
+ process.stdout.write(`FAIL ${clip.id}\n`);
39
+ }
40
+ }
41
+ }
42
+ finally {
43
+ await browser.close();
44
+ }
45
+ process.stdout.write(`\n${clips.length - failures.length}/${clips.length} clips still match the app.\n`);
46
+ for (const { id, error } of failures) {
47
+ process.stderr.write(`\n${id}\n ${error.message}\n`);
48
+ }
49
+ return failures.length > 0 ? 1 : 0;
50
+ }
51
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/cli/commands/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAG/C,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAc;IACxC,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QAChC,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;KACvB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,WAAuB,CAAC,CAAC;IAE/D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC7C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,MAAsB,EACtB,KAAuB;IAEvB,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAmC,EAAE,CAAC;IAEpD,IAAI,CAAC;QACH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;gBACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;YAC7C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,KAAc,EAAE,CAAC,CAAC;gBACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,KAAK,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,+BAA+B,CACnF,CAAC;IACF,KAAK,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,QAAQ,EAAE,CAAC;QACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Can this machine record and encode?
3
+ *
4
+ * Reports every check rather than stopping at the first failure, because the
5
+ * usual case on a new machine is that two or three things are missing at once
6
+ * and being told about them one run at a time is tedious.
7
+ */
8
+ export declare function doctor(): Promise<number>;