retake-demos 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/server.js CHANGED
@@ -33,11 +33,12 @@ import { draftManifest, loadDotenv, pickProvider, proposeEdits, providerStatus,
33
33
  import { applyEdits, receiptsFor } from "../edits.js";
34
34
  import { dryRun } from "../dryrun.js";
35
35
  import { startOperator, getSession, addPending, answerPending, markDone, stopSession } from "../operator/run.js";
36
- import { gifskiBin, makeGif } from "../render.js";
36
+ import { ffmpegBin, gifskiBin, makeGif } from "../render.js";
37
37
  import { captureHash } from "../record.js";
38
38
  import { digest } from "../digest.js";
39
39
  import { startOffer, startApp, listeningPorts } from "../appserver.js";
40
40
  import { PKG_ROOT, PROJECT_ROOT, VERSION, entry } from "../paths.js";
41
+ import { SECRET_NAME, missingSecrets, writeEnvFile } from "../env.js";
41
42
  const ROOT = PROJECT_ROOT;
42
43
  const DEMOS = path.join(ROOT, "demos");
43
44
  /** Where takes land. Default outputs/ in the project; changeable in Settings
@@ -117,7 +118,7 @@ const readBody = (req) => new Promise((ok) => {
117
118
  req.on("end", () => ok(b));
118
119
  });
119
120
  const safeName = (s) => /^[a-z0-9-]+$/.test(s);
120
- const MIME = { ".mp4": "video/mp4", ".gif": "image/gif", ".png": "image/png", ".md": "text/markdown", ".json": "application/json", ".html": "text/html", ".yaml": "text/yaml" };
121
+ const MIME = { ".mp4": "video/mp4", ".gif": "image/gif", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".md": "text/markdown", ".json": "application/json", ".html": "text/html", ".yaml": "text/yaml" };
121
122
  /** A short name for "the app at this URL": the last path segment if there
122
123
  is one (demo.playwright.dev/todomvc → todomvc), else the host without
123
124
  www and TLD (www.saucedemo.com → saucedemo). localhost keeps its port —
@@ -326,25 +327,7 @@ function startRun(name, mode = {}) {
326
327
  return run;
327
328
  }
328
329
  /** Read/write the .env in the project root — only the keys we own. */
329
- function writeEnv(set) {
330
- const file = path.join(ROOT, ".env");
331
- const lines = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n") : [];
332
- for (const [k, v] of Object.entries(set)) {
333
- if (v === undefined)
334
- continue;
335
- const i = lines.findIndex((l) => l.startsWith(k + "="));
336
- const line = `${k}=${v}`;
337
- if (v === "") {
338
- if (i >= 0)
339
- lines.splice(i, 1);
340
- }
341
- else if (i >= 0)
342
- lines[i] = line;
343
- else
344
- lines.push(line);
345
- }
346
- fs.writeFileSync(file, lines.filter((l, i, a) => l !== "" || i < a.length - 1).join("\n").replace(/\n*$/, "\n"));
347
- }
330
+ function writeEnv(set) { writeEnvFile(ROOT, set); }
348
331
  function envSummary() {
349
332
  return { localUrl: process.env.RETAKE_LOCAL_URL ?? "", localModel: process.env.RETAKE_LOCAL_MODEL ?? "", groq: !!process.env.GROQ_API_KEY, mistral: !!process.env.MISTRAL_API_KEY };
350
333
  }
@@ -405,6 +388,8 @@ function withStaleness(a) {
405
388
  return { ...a, active: false, stale: true };
406
389
  return a;
407
390
  }
391
+ const secretRequests = new Map();
392
+ const openSecretRequests = () => [...secretRequests.values()].filter((r) => !r.filled).map(({ id, names, why }) => ({ id, names, why }));
408
393
  function noteActivity(b) {
409
394
  if (!activity.active && !b.done) {
410
395
  activity.active = true;
@@ -448,9 +433,11 @@ export function serve(port) {
448
433
  res.writeHead(200, { "content-type": "text/html" });
449
434
  return res.end(fs.readFileSync(f));
450
435
  }
451
- const msite = /^\/landing\/media\/([A-Za-z0-9._-]+)$/.exec(p);
436
+ // Both prefixes: the page is served at /landing here and from a site
437
+ // root when deployed, so its asset paths are relative either way.
438
+ const msite = /^(?:\/landing)?\/(media|logos)\/([A-Za-z0-9._-]+)$/.exec(p);
452
439
  if (msite && (req.method === "GET" || req.method === "HEAD")) {
453
- const f = path.join(PKG_ROOT, "site", "media", msite[1]);
440
+ const f = path.join(PKG_ROOT, "site", msite[1], msite[2]);
454
441
  if (!fs.existsSync(f))
455
442
  return json(res, 404, { error: "not found" });
456
443
  res.writeHead(200, { "content-type": MIME[path.extname(f)] ?? "application/octet-stream", "content-length": fs.statSync(f).size });
@@ -536,6 +523,61 @@ export function serve(port) {
536
523
  const seen = new Set();
537
524
  return json(res, 200, items.filter((x) => !seen.has(x.name) && seen.add(x.name)));
538
525
  }
526
+ // ---------- the poster: any frame, a card, or their own file ----------
527
+ // thumbnail.png IS the poster. Every source here just writes it, so the
528
+ // choice costs no re-render and never touches the video.
529
+ const mcov = /^\/api\/cover\/([a-z0-9-]+)$/.exec(p);
530
+ if (mcov && req.method === "POST") {
531
+ const name = mcov[1];
532
+ const dir = path.join(outRoot(), name);
533
+ const target = path.join(dir, "thumbnail.png");
534
+ if (!fs.existsSync(dir))
535
+ return json(res, 404, { error: `no take for ${name}` });
536
+ const b = JSON.parse(await readBody(req));
537
+ if (b.auto) {
538
+ fs.rmSync(path.join(dir, ".poster"), { force: true });
539
+ return json(res, 200, { ok: true, poster: "auto — the next render takes it from the thumbnail scene" });
540
+ }
541
+ try {
542
+ if (b.still) {
543
+ // A scene still, by its file name under stills/.
544
+ const src = path.join(dir, "stills", path.basename(b.still));
545
+ if (!fs.existsSync(src))
546
+ return json(res, 404, { error: `no still ${b.still}` });
547
+ fs.copyFileSync(src, target);
548
+ }
549
+ else if (b.file) {
550
+ // A candidate already in the take (cover.png, cover-titled.png).
551
+ const src = path.join(dir, path.basename(b.file));
552
+ if (!fs.existsSync(src))
553
+ return json(res, 404, { error: `no file ${b.file}` });
554
+ fs.copyFileSync(src, target);
555
+ }
556
+ else if (typeof b.at === "number") {
557
+ // Any moment of the finished video — usually the best frame is
558
+ // nowhere near a scene marker.
559
+ const mp4 = path.join(dir, "demo.mp4");
560
+ if (!fs.existsSync(mp4))
561
+ return json(res, 404, { error: "no demo.mp4" });
562
+ execFileSync(ffmpegBin(), ["-y", "-loglevel", "error", "-ss", Math.max(0, b.at).toFixed(2), "-i", mp4, "-frames:v", "1", target]);
563
+ }
564
+ else if (b.dataUrl) {
565
+ // Their own image.
566
+ const m2 = /^data:image\/(png|jpe?g|webp);base64,(.+)$/.exec(b.dataUrl);
567
+ if (!m2)
568
+ return json(res, 400, { error: "expected a png, jpeg or webp data URL" });
569
+ fs.writeFileSync(target, Buffer.from(m2[2], "base64"));
570
+ }
571
+ else
572
+ return json(res, 400, { error: "give a still, a file, an `at` time, or a dataUrl" });
573
+ }
574
+ catch (e) {
575
+ return json(res, 500, { error: e.message });
576
+ }
577
+ // The choice survives re-renders; "auto" hands it back to the scene default.
578
+ fs.writeFileSync(path.join(dir, ".poster"), JSON.stringify({ ...b, dataUrl: b.dataUrl ? "(uploaded)" : undefined, at: b.at }));
579
+ return json(res, 200, { ok: true, poster: "thumbnail.png" });
580
+ }
539
581
  if (p === "/api/trash/clear" && req.method === "POST") {
540
582
  const trash = path.join(ROOT, ".trash");
541
583
  let n = 0;
@@ -720,9 +762,50 @@ export function serve(port) {
720
762
  }
721
763
  if (p === "/api/activity" && req.method === "GET")
722
764
  return json(res, 200, withStaleness(activity));
765
+ // ---------- secrets: asked for by name, typed into the window, written here ----------
766
+ if (p === "/api/secrets/request" && req.method === "POST") {
767
+ const b = JSON.parse(await readBody(req));
768
+ const names = (b.names ?? []).filter((n) => SECRET_NAME.test(n));
769
+ if (!names.length)
770
+ return json(res, 400, { error: "names must be like APP_PASSWORD" });
771
+ // The same ask twice is one form, not two stacked ones.
772
+ const dup = [...secretRequests.values()].find((x) => !x.filled && x.names.join() === names.join());
773
+ if (dup)
774
+ return json(res, 200, { id: dup.id });
775
+ const r = { id: "s" + Date.now().toString(36), names, why: String(b.why ?? "").slice(0, 300), filled: false, at: Date.now() };
776
+ secretRequests.set(r.id, r);
777
+ for (const w of activityWatchers)
778
+ w({ type: "secrets", data: { id: r.id, names: r.names, why: r.why } });
779
+ return json(res, 200, { id: r.id });
780
+ }
781
+ if (p === "/api/secrets/pending" && req.method === "GET")
782
+ return json(res, 200, openSecretRequests());
783
+ if (p === "/api/secrets/fill" && req.method === "POST") {
784
+ const b = JSON.parse(await readBody(req));
785
+ const r = secretRequests.get(b.id);
786
+ if (!r)
787
+ return json(res, 404, { error: "no such request" });
788
+ const values = {};
789
+ for (const [k, v] of Object.entries(b.values ?? {}))
790
+ if (r.names.includes(k) && typeof v === "string" && v.trim())
791
+ values[k] = v;
792
+ if (!Object.keys(values).length)
793
+ return json(res, 400, { error: "nothing to save" });
794
+ writeEnvFile(ROOT, values);
795
+ if (!missingSecrets(ROOT, r.names).length)
796
+ r.filled = true;
797
+ for (const w of activityWatchers)
798
+ w({ type: "secrets-done", data: { id: r.id, filled: r.filled } });
799
+ return json(res, 200, { ok: true, filled: r.filled, set: Object.keys(values) });
800
+ }
801
+ const msec = /^\/api\/secrets\/(s[a-z0-9]+)$/.exec(p);
802
+ if (msec && req.method === "GET") {
803
+ const r = secretRequests.get(msec[1]);
804
+ return r ? json(res, 200, { filled: r.filled, missing: missingSecrets(ROOT, r.names) }) : json(res, 404, { error: "no such request" });
805
+ }
723
806
  if (p === "/api/activity/stream" && req.method === "GET") {
724
807
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
725
- res.write(`event: state\ndata: ${JSON.stringify({ ...activity, boot: BOOT })}\n\n`);
808
+ res.write(`event: state\ndata: ${JSON.stringify({ ...activity, boot: BOOT, secrets: openSecretRequests() })}\n\n`);
726
809
  const send = (ev) => res.write(`event: ${ev.type}\ndata: ${JSON.stringify(ev.data)}\n\n`);
727
810
  activityWatchers.add(send);
728
811
  const beat = setInterval(() => res.write(": ping\n\n"), 25_000);
@@ -859,7 +942,7 @@ export function serve(port) {
859
942
  return json(res, 200, { provider, selected, models: [{ id: selected, name: selected || "Configured default" }] });
860
943
  }
861
944
  if (p === "/api/settings" && req.method === "GET")
862
- return json(res, 200, { ...providerStatus(), model: process.env.RETAKE_MODEL ?? "", selection: modelSelection(), gifski: !!gifskiBin(), env: envSummary(), outDir: outRoot(), outDefault: OUT_DEFAULT, version: VERSION });
945
+ return json(res, 200, { envPath: path.join(ROOT, ".env"), ...providerStatus(), model: process.env.RETAKE_MODEL ?? "", selection: modelSelection(), gifski: !!gifskiBin(), env: envSummary(), outDir: outRoot(), outDefault: OUT_DEFAULT, version: VERSION });
863
946
  const mopen = /^\/api\/open\/([a-z0-9-]+)$/.exec(p);
864
947
  if (mopen && req.method === "POST") {
865
948
  const dir = path.join(outRoot(), mopen[1]);
@@ -1023,7 +1106,7 @@ export function serve(port) {
1023
1106
  if (b.camera === "static")
1024
1107
  node.set("camera", "static");
1025
1108
  else if (b.camera === "auto")
1026
- node.delete("camera");
1109
+ node.set("camera", "auto");
1027
1110
  else if (typeof b.camera === "object")
1028
1111
  node.set("camera", b.camera);
1029
1112
  }
package/dist/voice.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Voiceover: the captions read aloud. The captions ARE the script — each
3
+ * scene's line is synthesized once and placed at the scene's start, so the
4
+ * narration is honest to the take by construction.
5
+ *
6
+ * Engine: edge-tts (Microsoft's neural voices — free, network). Found as a
7
+ * CLI (`edge-tts`, pipx/pip) or module (`python3 -m edge_tts`). Retake does
8
+ * not bundle a voice engine; the error says how to install one.
9
+ */
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { execFileSync, spawnSync } from "node:child_process";
13
+ export const DEFAULT_VOICE = "en-US-JennyNeural";
14
+ function engine() {
15
+ const home = process.env.HOME ?? "";
16
+ for (const c of ["edge-tts", path.join(home, ".local", "bin", "edge-tts")]) {
17
+ if (spawnSync(c, ["--help"], { stdio: "ignore" }).status === 0)
18
+ return { cmd: c, args: [] };
19
+ }
20
+ if (spawnSync("python3", ["-m", "edge_tts", "--help"], { stdio: "ignore" }).status === 0)
21
+ return { cmd: "python3", args: ["-m", "edge_tts"] };
22
+ throw new Error("voiceover needs edge-tts — install it once with `pipx install edge-tts` (or `pip install edge-tts`), then re-render");
23
+ }
24
+ export function synthesize(text, voice, out) {
25
+ const e = engine();
26
+ execFileSync(e.cmd, [...e.args, "--voice", voice, "--text", text, "--write-media", out], { stdio: "pipe", timeout: 60_000 });
27
+ if (!fs.existsSync(out) || fs.statSync(out).size < 200)
28
+ throw new Error(`voiceover: nothing came back for “${text.slice(0, 40)}…” — is the network up? (edge-tts is a network service)`);
29
+ }
30
+ export function audioSeconds(ffmpeg, file) {
31
+ const out = String(spawnSync(ffmpeg, ["-hide_banner", "-i", file], { encoding: "utf8" }).stderr);
32
+ const m = /Duration:\s*(\d+):(\d+):(\d+\.?\d*)/.exec(out);
33
+ if (!m)
34
+ throw new Error(`could not read duration of ${file}`);
35
+ return Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]);
36
+ }
package/llms.txt ADDED
@@ -0,0 +1,11 @@
1
+ # Retake
2
+
3
+ > Demo-as-code for coding agents: a YAML manifest in, a product walkthrough video out. Rerun the demo instead of re-recording it.
4
+
5
+ An agent that was handed this repository should read README.md first — its "If you are a coding agent" section is the setup sequence — then AGENTS.md (the CLI loop, manifests, logins, what trips agents up) and skill/SKILL.md (the method Claude Code loads as a skill).
6
+
7
+ - README.md: https://raw.githubusercontent.com/glebbogachev00/retake/main/README.md
8
+ - AGENTS.md: https://raw.githubusercontent.com/glebbogachev00/retake/main/AGENTS.md
9
+ - skill/SKILL.md: https://raw.githubusercontent.com/glebbogachev00/retake/main/skill/SKILL.md
10
+ - demos/example.yaml: https://raw.githubusercontent.com/glebbogachev00/retake/main/demos/example.yaml
11
+ - npm: https://www.npmjs.com/package/retake-demos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retake-demos",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Demo-as-code for coding agents: a YAML manifest in, a product walkthrough video out. Rerun the demo instead of re-recording it.",
5
5
  "keywords": [
6
6
  "demo",
@@ -30,17 +30,22 @@
30
30
  "files": [
31
31
  "dist",
32
32
  "skill",
33
- "site/index.html",
34
33
  "assets/logo.svg",
35
34
  "assets/icon-192.png",
36
35
  "assets/icon-512.png",
37
36
  "demos/example.yaml",
38
37
  ".env.example",
39
38
  "README.md",
40
- "LICENSE"
39
+ "LICENSE",
40
+ "scripts/patch-testreel.mjs",
41
+ "AGENTS.md",
42
+ "llms.txt",
43
+ "site/index.html",
44
+ "site/logos"
41
45
  ],
42
46
  "scripts": {
43
47
  "build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/ui && cp src/ui/*.html dist/ui/ && chmod +x dist/cli.js",
48
+ "postinstall": "node scripts/patch-testreel.mjs",
44
49
  "prepare": "npm run build",
45
50
  "prepublishOnly": "npm run typecheck && npm test",
46
51
  "retake": "tsx src/cli.ts",
@@ -52,7 +57,7 @@
52
57
  "@modelcontextprotocol/sdk": "^1.30.0",
53
58
  "commander": "^14.0.3",
54
59
  "playwright": "^1.58.2",
55
- "testreel": "^0.2.0",
60
+ "testreel": "0.2.0",
56
61
  "yaml": "^2.8.3",
57
62
  "zod": "^4.4.3"
58
63
  },
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lift testreel's cursor cap.
4
+ *
5
+ * testreel draws the cursor overlay with ffmpeg expressions it builds as one
6
+ * nested `if(lt(t,…),…,if(lt(t,…),…,…))` per keyframe. ffmpeg's expression
7
+ * parser stops at 98 nesting levels, so past ~45 cursor moves the overlay
8
+ * silently drops out of the video. Upstream (0.2.0) has not changed this.
9
+ *
10
+ * The same piecewise functions can be written flat — a sum of windowed
11
+ * segments, `gte(t,a)*lt(t,b)*value` — with no nesting and the same result
12
+ * frame for frame. This script rewrites the four builders Retake relies on
13
+ * (x/y position, show/hide, fade, cursor style) in testreel's dist files.
14
+ *
15
+ * It runs on `postinstall`, is idempotent, and refuses to touch anything if
16
+ * the functions are not byte-identical to testreel 0.2.0 — a newer testreel
17
+ * means re-checking, not guessing. (testreel is pinned to an exact version
18
+ * in package.json for the same reason.)
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { createRequire } from "node:module";
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const MARK = "/*retake:flat-cursor*/";
26
+
27
+ // The originals, exactly as shipped in testreel 0.2.0 (dist/index.js).
28
+ const ORIGINALS = {
29
+ Qe: 'function Qe(e,t){if(e.length===0)return"0";if(e.length===1)return String(e[0].value);let o=String(e[e.length-1].value);for(let r=e.length-1;r>=1;r--){let n=e[r-1],i=e[r],s=i.transitionMs/1e3,a=i.time,u=a+s,l=`${n.value}+(${i.value}-${n.value})*(t-${a.toFixed(4)})/${s.toFixed(4)}`;o=`if(lt(t,${a.toFixed(4)}),${n.value},if(lt(t,${u.toFixed(4)}),${l},${o}))`}return o}',
30
+ _t: 'function _t(e){let t=e.filter(n=>n.type==="hide"||n.type==="show");if(t.length===0)return"1";let r=t[t.length-1].type==="hide"?"0":"1";for(let n=t.length-2;n>=0;n--){let i=t[n].type==="hide"?"0":"1";r=`if(lt(t,${t[n+1].time.toFixed(4)}),${i},${r})`}return r=`if(lt(t,${t[0].time.toFixed(4)}),1,${r})`,r}',
31
+ zt: 'function zt(e,t,o="T"){let r=e.filter(a=>a.type==="hide"||a.type==="show").sort((a,u)=>a.time-u.time);if(r.length===0)return"1";let n=Math.max(.001,t/1e3),s=r[r.length-1].type==="hide"?"0":"1";for(let a=r.length-1;a>=0;a--){let u=r[a],l=a===0?1:r[a-1].type==="hide"?0:1,p=u.type==="hide"?0:1,w=u.time,d=u.time+n;if(l===p){s=`if(lt(${o},${w.toFixed(4)}),${l},${s})`;continue}let f=p-l,h=`${l}+(${f})*(${o}-${w.toFixed(4)})/${n.toFixed(4)}`;s=`if(lt(${o},${w.toFixed(4)}),${l},if(lt(${o},${d.toFixed(4)}),${h},${s}))`}return s}',
32
+ Mt: 'function Mt(e,t,o){let r=e.filter(a=>a.type==="move"&&a.cursorStyle!==void 0);if(r.length===0)return o===t?"1":"0";let n=[];n.push({time:0,active:o===t});for(let a of r)n.push({time:a.time,active:(a.cursorStyle??o)===t});let i=[n[0]];for(let a=1;a<n.length;a++)n[a].active!==i[i.length-1].active&&i.push(n[a]);if(i.length===1)return i[0].active?"1":"0";let s=i[i.length-1].active?"1":"0";for(let a=i.length-2;a>=0;a--){let u=i[a].active?"1":"0";s=`if(lt(t,${i[a+1].time.toFixed(4)}),${u},${s})`}return s}',
33
+ };
34
+
35
+ // Flat replacements. `__flat(v, segs)` sums windowed segments; a segment is
36
+ // {f, t, e}: from time f (null = -∞) to time t (null = +∞), expression e.
37
+ // Segments are contiguous and non-overlapping, so the sum IS the function.
38
+ // Where the originals let a running transition finish before the next
39
+ // keyframe takes over (nested ifs, outermost wins), these do the same.
40
+ const FLAT = {
41
+ helper: `${MARK}function __flat(v,a){a=a.filter(s=>s.f==null||s.t==null||s.t>s.f);if(a.length===0)return"0";if(a.length===1)return"("+a[0].e+")";return a.map(s=>{let g=s.f==null?"":"gte("+v+","+s.f.toFixed(4)+")*",l=s.t==null?"":"lt("+v+","+s.t.toFixed(4)+")*";return g+l+"("+s.e+")"}).join("+")}`,
42
+ Qe: 'function Qe(e,t){if(e.length===0)return"0";if(e.length===1)return String(e[0].value);let a=[{f:null,t:e[1].time,e:String(e[0].value)}],cur=e[1].time;for(let r=1;r<e.length;r++){let n=e[r-1],i=e[r],s=i.transitionMs/1e3,st=Math.max(i.time,cur),u=i.time+s,nx=r+1<e.length?e[r+1].time:null;if(u>st){a.push({f:st,t:u,e:`${n.value}+(${i.value}-${n.value})*(t-${i.time.toFixed(4)})/${s.toFixed(4)}`});cur=u}if(nx==null||nx>cur){a.push({f:cur,t:nx,e:String(i.value)});if(nx!=null)cur=nx}}return __flat("t",a)}',
43
+ _t: 'function _t(e){let t=e.filter(n=>n.type==="hide"||n.type==="show");if(t.length===0)return"1";let a=[{f:null,t:t[0].time,e:"1"}];for(let n=0;n<t.length;n++)a.push({f:t[n].time,t:n+1<t.length?t[n+1].time:null,e:t[n].type==="hide"?"0":"1"});return __flat("t",a)}',
44
+ zt: 'function zt(e,t,o="T"){let r=e.filter(a=>a.type==="hide"||a.type==="show").sort((a,u)=>a.time-u.time);if(r.length===0)return"1";let n=Math.max(.001,t/1e3),a=[{f:null,t:r[0].time,e:"1"}],cur=r[0].time;for(let i=0;i<r.length;i++){let u=r[i],l=i===0?1:r[i-1].type==="hide"?0:1,p=u.type==="hide"?0:1,w=u.time,nx=i+1<r.length?r[i+1].time:null;if(l!==p){let st=Math.max(w,cur),d=w+n;if(d>st){a.push({f:st,t:d,e:`${l}+(${p-l})*(${o}-${w.toFixed(4)})/${n.toFixed(4)}`});cur=d}}if(nx==null||nx>cur){a.push({f:cur,t:nx,e:String(p)});if(nx!=null)cur=nx}}return __flat(o,a)}',
45
+ Mt: 'function Mt(e,t,o){let r=e.filter(a=>a.type==="move"&&a.cursorStyle!==void 0);if(r.length===0)return o===t?"1":"0";let n=[];n.push({time:0,active:o===t});for(let a of r)n.push({time:a.time,active:(a.cursorStyle??o)===t});let i=[n[0]];for(let a=1;a<n.length;a++)n[a].active!==i[i.length-1].active&&i.push(n[a]);if(i.length===1)return i[0].active?"1":"0";let s=[];for(let a=0;a<i.length;a++)s.push({f:a===0?null:i[a].time,t:a+1<i.length?i[a+1].time:null,e:i[a].active?"1":"0"});return __flat("t",s)}',
46
+ };
47
+
48
+ function patchFile(file) {
49
+ let src = fs.readFileSync(file, "utf8");
50
+ if (src.includes(MARK)) return "already patched";
51
+ for (const [name, original] of Object.entries(ORIGINALS)) {
52
+ const n = src.split(original).length - 1;
53
+ if (n !== 1) throw new Error(`${path.basename(file)}: expected exactly one copy of ${name} as shipped in testreel 0.2.0, found ${n} — testreel changed; re-check scripts/patch-testreel.mjs before trusting the cursor past ~45 moves`);
54
+ }
55
+ // Prepend the helper to the first builder so it is defined once, in scope.
56
+ src = src.replace(ORIGINALS.Qe, FLAT.helper + FLAT.Qe);
57
+ for (const name of ["_t", "zt", "Mt"]) src = src.replace(ORIGINALS[name], FLAT[name]);
58
+ fs.writeFileSync(file, src);
59
+ return "patched";
60
+ }
61
+
62
+ let dir;
63
+ try {
64
+ // testreel's exports map hides package.json; walk up from the resolved entry.
65
+ let d = path.dirname(require.resolve("testreel"));
66
+ while (!fs.existsSync(path.join(d, "package.json"))) d = path.dirname(d);
67
+ dir = d;
68
+ } catch { console.log("patch-testreel: testreel not installed yet — nothing to do"); process.exit(0); }
69
+ const version = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version;
70
+ if (version !== "0.2.0") throw new Error(`patch-testreel: written for testreel 0.2.0, found ${version}`);
71
+
72
+ // Retake imports the ESM build; the CJS build has different minified names
73
+ // and is left alone.
74
+ console.log(`patch-testreel: dist/index.js — ${patchFile(path.join(dir, "dist", "index.js"))}`);