pro-visu 0.3.1 → 0.4.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/cli/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { cac } from "cac";
5
5
 
6
6
  // src/version.ts
7
- var TOOL_VERSION = true ? "0.3.1" : "0.0.0-dev";
7
+ var TOOL_VERSION = true ? "0.4.0" : "0.0.0-dev";
8
8
 
9
9
  // src/cli/update-check.ts
10
10
  import updateNotifier from "update-notifier";
@@ -163,17 +163,88 @@ async function ensureChromium(opts) {
163
163
  }
164
164
 
165
165
  // src/media/ensure-ffmpeg.ts
166
- import path5 from "path";
167
166
  import { existsSync as existsSync2 } from "fs";
168
- import { rm as rm3 } from "fs/promises";
169
167
  import { spawn as spawn3 } from "child_process";
170
- import { createRequire as createRequire2 } from "module";
171
168
 
172
169
  // src/media/ffmpeg.ts
173
- import path4 from "path";
170
+ import path5 from "path";
174
171
  import { spawn as spawn2 } from "child_process";
175
- import { rm as rm2, writeFile as writeFile2 } from "fs/promises";
176
- import ffmpegStatic from "ffmpeg-static";
172
+ import { rm as rm3, writeFile as writeFile2 } from "fs/promises";
173
+
174
+ // src/media/ffmpeg-binary.ts
175
+ import os from "os";
176
+ import path4 from "path";
177
+ import https from "https";
178
+ import { createWriteStream } from "fs";
179
+ import { mkdir as mkdir2, rename, rm as rm2, chmod } from "fs/promises";
180
+ import { createGunzip } from "zlib";
181
+ import { pipeline } from "stream/promises";
182
+ var FFMPEG_RELEASE = process.env.FFMPEG_BINARY_RELEASE || "b6.1.1";
183
+ var BINARIES_URL = process.env.FFMPEG_BINARIES_URL || "https://github.com/eugeneware/ffmpeg-static/releases/download";
184
+ var SUPPORTED = {
185
+ darwin: ["x64", "arm64"],
186
+ linux: ["x64", "ia32", "arm64", "arm"],
187
+ win32: ["x64", "ia32"],
188
+ freebsd: ["x64"]
189
+ };
190
+ function ffmpegIsSupported() {
191
+ return (SUPPORTED[process.platform] ?? []).includes(process.arch);
192
+ }
193
+ function ffmpegCacheDir() {
194
+ const base = process.env.PROVISU_FFMPEG_DIR || path4.join(os.homedir(), ".cache", "pro-visu", "ffmpeg");
195
+ return path4.join(base, FFMPEG_RELEASE);
196
+ }
197
+ function ffmpegCachedBinary() {
198
+ return path4.join(ffmpegCacheDir(), process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg");
199
+ }
200
+ function ffmpegBinaryPath() {
201
+ return process.env.FFMPEG_BIN || ffmpegCachedBinary();
202
+ }
203
+ function ffmpegDownloadUrl() {
204
+ if (!ffmpegIsSupported()) return null;
205
+ return `${BINARIES_URL}/${FFMPEG_RELEASE}/ffmpeg-${process.platform}-${process.arch}.gz`;
206
+ }
207
+ function getFollowing(url, redirects = 5) {
208
+ return new Promise((resolve, reject) => {
209
+ const req = https.get(url, { headers: { "User-Agent": "pro-visu" } }, (res) => {
210
+ const status = res.statusCode ?? 0;
211
+ if (status >= 300 && status < 400 && res.headers.location) {
212
+ res.resume();
213
+ if (redirects <= 0) return reject(new Error("Too many redirects fetching ffmpeg."));
214
+ resolve(getFollowing(new URL(res.headers.location, url).toString(), redirects - 1));
215
+ return;
216
+ }
217
+ if (status !== 200) {
218
+ res.resume();
219
+ reject(new Error(`ffmpeg download failed: HTTP ${status} for ${url}`));
220
+ return;
221
+ }
222
+ resolve(res);
223
+ });
224
+ req.on("error", reject);
225
+ });
226
+ }
227
+ async function downloadFfmpeg() {
228
+ const url = ffmpegDownloadUrl();
229
+ if (!url) {
230
+ throw new Error(
231
+ `No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
232
+ );
233
+ }
234
+ const dest = ffmpegCachedBinary();
235
+ const tmp = `${dest}.download`;
236
+ await mkdir2(path4.dirname(dest), { recursive: true });
237
+ await rm2(tmp, { force: true });
238
+ const res = await getFollowing(url);
239
+ await pipeline(res, createGunzip(), createWriteStream(tmp));
240
+ await chmod(tmp, 493).catch(() => {
241
+ });
242
+ await rm2(dest, { force: true });
243
+ await rename(tmp, dest);
244
+ return dest;
245
+ }
246
+
247
+ // src/media/ffmpeg.ts
177
248
  var SCALE_COLOR = "out_color_matrix=bt709:out_range=tv";
178
249
  var COLOR_TAGS = [
179
250
  "-colorspace",
@@ -186,11 +257,7 @@ var COLOR_TAGS = [
186
257
  "tv"
187
258
  ];
188
259
  function ffmpegPath() {
189
- const p = ffmpegStatic;
190
- if (!p) {
191
- throw new Error("ffmpeg-static did not provide a binary for this platform.");
192
- }
193
- return p;
260
+ return ffmpegBinaryPath();
194
261
  }
195
262
  function buildTranscodeArgs(args) {
196
263
  const seek = args.startOffsetSeconds && args.startOffsetSeconds > 0 ? ["-ss", args.startOffsetSeconds.toFixed(3)] : [];
@@ -332,7 +399,7 @@ function buildConcatArgs(listFile, outPath) {
332
399
  ];
333
400
  }
334
401
  async function concatMp4(segments, outPath, logger, signal) {
335
- await ensureDir(path4.dirname(outPath));
402
+ await ensureDir(path5.dirname(outPath));
336
403
  const listFile = `${outPath}.concat.txt`;
337
404
  const list = segments.map((s) => `file '${s.replace(/\\/g, "/")}'`).join("\n");
338
405
  await writeFile2(listFile, `${list}
@@ -340,7 +407,7 @@ async function concatMp4(segments, outPath, logger, signal) {
340
407
  try {
341
408
  await runFfmpeg(buildConcatArgs(listFile, outPath), logger, signal);
342
409
  } finally {
343
- await rm2(listFile, { force: true });
410
+ await rm3(listFile, { force: true });
344
411
  }
345
412
  }
346
413
  async function runFfmpeg(argv, logger, signal) {
@@ -362,7 +429,7 @@ ${stderr.slice(-2e3)}`));
362
429
  });
363
430
  }
364
431
  async function transcodeToMp4(args) {
365
- await ensureDir(path4.dirname(args.outputPath));
432
+ await ensureDir(path5.dirname(args.outputPath));
366
433
  await runFfmpeg(buildTranscodeArgs(args), args.logger, args.signal);
367
434
  }
368
435
  function aspectTarget(aspect) {
@@ -467,7 +534,6 @@ function buildStillSegmentArgs(a) {
467
534
  }
468
535
 
469
536
  // src/media/ensure-ffmpeg.ts
470
- var require3 = createRequire2(import.meta.url);
471
537
  async function ffmpegWorks() {
472
538
  let bin;
473
539
  try {
@@ -488,42 +554,26 @@ async function ffmpegWorks() {
488
554
  child.on("close", (code) => resolve(code === 0));
489
555
  });
490
556
  }
491
- function resolveInstallScript() {
492
- try {
493
- return require3.resolve("ffmpeg-static/install.js");
494
- } catch {
495
- try {
496
- const pkg = require3.resolve("ffmpeg-static/package.json");
497
- return path5.join(path5.dirname(pkg), "install.js");
498
- } catch {
499
- return null;
500
- }
501
- }
502
- }
503
557
  async function ensureFfmpeg(opts) {
504
558
  if (await ffmpegWorks()) return true;
505
559
  if (opts.checkOnly) return false;
506
- const installScript = resolveInstallScript();
507
- if (!installScript) {
508
- opts.logger.error("ffmpeg-static is not installed; cannot fetch an ffmpeg binary.");
560
+ if (process.env.FFMPEG_BIN) {
561
+ opts.logger.error(`FFMPEG_BIN is set to "${process.env.FFMPEG_BIN}" but that binary won't run.`);
562
+ return false;
563
+ }
564
+ if (!ffmpegIsSupported()) {
565
+ opts.logger.error(
566
+ `No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
567
+ );
509
568
  return false;
510
569
  }
511
570
  opts.logger.info("Fetching ffmpeg (one-time, ~80 MB)\u2026");
512
571
  try {
513
- await rm3(ffmpegPath(), { force: true });
514
- } catch {
572
+ await downloadFfmpeg();
573
+ } catch (err) {
574
+ opts.logger.error(`ffmpeg download failed: ${err.message}`);
575
+ return false;
515
576
  }
516
- await new Promise((resolve, reject) => {
517
- const child = spawn3(process.execPath, [installScript], {
518
- cwd: path5.dirname(installScript),
519
- stdio: "inherit"
520
- });
521
- child.on("error", reject);
522
- child.on(
523
- "close",
524
- (code) => code === 0 ? resolve() : reject(new Error(`ffmpeg download failed (exit code ${code}).`))
525
- );
526
- });
527
577
  if (!await ffmpegWorks()) {
528
578
  opts.logger.error("ffmpeg was downloaded but still won't run on this platform.");
529
579
  return false;
@@ -578,6 +628,16 @@ var serverSettingsSchema = z.object({
578
628
  /** If a server is already reachable at the URL, use it as-is (don't start or stop one). */
579
629
  reuseExisting: z.boolean().default(true).describe("If a server is already reachable at the URL, use it as-is (don't start or stop one). Default true.")
580
630
  }).strict();
631
+ var captureSettingsSchema = z.object({
632
+ /** Query params appended to every URL-based asset (e.g. `{ capture: "1" }` → `?capture=1`). */
633
+ query: z.record(z.string(), z.string()).optional().describe('Query params appended to every URL-based asset, e.g. { capture: "1" }.'),
634
+ /** Cookies set on every capture context before navigation, scoped to the asset's origin. */
635
+ cookies: z.array(z.object({ name: z.string().min(1), value: z.string() }).strict()).optional().describe("Cookies set on every capture context before navigation (scoped to the asset's origin)."),
636
+ /** localStorage entries seeded before the page's own scripts run. */
637
+ localStorage: z.record(z.string(), z.string()).optional().describe("localStorage entries seeded (per origin) before the page's own scripts run."),
638
+ /** JS source run in every page before its own scripts — e.g. set a global capture flag. */
639
+ initScript: z.string().optional().describe("JS run in every page before its own scripts (e.g. `window.__PV_CAPTURE__ = true`).")
640
+ }).strict();
581
641
  var settingsSchema = z.object({
582
642
  /** Output directory, relative to the repo root. */
583
643
  outDir: z.string().min(1).default("pro-visu").describe('Output directory for generated assets, relative to the repo root (default "pro-visu").'),
@@ -599,6 +659,8 @@ var settingsSchema = z.object({
599
659
  defaults: z.record(z.string(), z.record(z.string(), z.unknown())).default({}).describe("Per-generator option defaults, keyed by generator id, merged underneath each asset's own options."),
600
660
  /** Optional managed dev/prod server lifecycle (build → start → wait → … → stop). */
601
661
  server: serverSettingsSchema.optional().describe("Build \u2192 start \u2192 wait \u2192 capture \u2192 stop a server automatically."),
662
+ /** "Capture mode" toggles (query/cookies/localStorage/init script) applied to every URL capture. */
663
+ capture: captureSettingsSchema.optional().describe("Capture-mode toggles applied to every URL-based asset (e.g. disable animations / hide the cookie banner)."),
602
664
  /** Render quality. "draft" lowers fps/scale and speeds the encoder for fast iteration. */
603
665
  quality: z.enum(["draft", "final"]).default("final").describe('Render quality; "draft" lowers fps/scale and speeds the encoder for fast iteration (default "final").'),
604
666
  /** Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). */
@@ -943,6 +1005,38 @@ var scrollReelOptionsSchema = z2.object({
943
1005
  import path6 from "path";
944
1006
  import { mkdtemp } from "fs/promises";
945
1007
 
1008
+ // src/pipeline/capture.ts
1009
+ function withCaptureQuery(url, capture) {
1010
+ if (!url || !capture?.query) return url;
1011
+ try {
1012
+ const u = new URL(url);
1013
+ for (const [k, v] of Object.entries(capture.query)) u.searchParams.set(k, v);
1014
+ return u.toString();
1015
+ } catch {
1016
+ return url;
1017
+ }
1018
+ }
1019
+ async function applyCapture(context, capture, url) {
1020
+ if (!capture) return;
1021
+ if (capture.cookies?.length && url) {
1022
+ try {
1023
+ const origin = new URL(url).origin;
1024
+ await context.addCookies(
1025
+ capture.cookies.map((c) => ({ name: c.name, value: c.value, url: origin }))
1026
+ );
1027
+ } catch {
1028
+ }
1029
+ }
1030
+ const scripts = [];
1031
+ if (capture.localStorage) {
1032
+ scripts.push(
1033
+ `try{var e=${JSON.stringify(capture.localStorage)};for(var k in e)localStorage.setItem(k,e[k]);}catch(_){}`
1034
+ );
1035
+ }
1036
+ if (capture.initScript) scripts.push(capture.initScript);
1037
+ if (scripts.length) await context.addInitScript(scripts.join("\n"));
1038
+ }
1039
+
946
1040
  // src/generators/scroll-reel/scroll.ts
947
1041
  var EASINGS = {
948
1042
  linear: (t) => t,
@@ -957,16 +1051,21 @@ async function prepareScroll(args) {
957
1051
  const target = findScrollTarget(document, getComputedStyle);
958
1052
  forceInstant(target);
959
1053
  const sleep2 = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
1054
+ const withCap = (p, ms) => Promise.race([p ?? Promise.resolve(), sleep2(ms)]);
960
1055
  scrollTargetTo(target, maxScrollOf(target));
961
1056
  await sleep2(Math.max(0, args.settleMs));
962
1057
  try {
963
- await (document.fonts?.ready ?? Promise.resolve());
1058
+ await withCap(document.fonts?.ready ?? null, 5e3);
964
1059
  } catch {
965
1060
  }
966
1061
  try {
967
1062
  const imgs = Array.from(document.images ?? []);
968
- await Promise.all(imgs.map((im) => im.decode ? im.decode().catch(() => {
969
- }) : null));
1063
+ await withCap(
1064
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1065
+ Promise.all(imgs.map((im) => im.decode ? im.decode().catch(() => {
1066
+ }) : null)),
1067
+ 4e3
1068
+ );
970
1069
  } catch {
971
1070
  }
972
1071
  scrollTargetTo(target, 0);
@@ -1410,6 +1509,7 @@ async function captureScrollWebm(args) {
1410
1509
  size: { width: options.width, height: options.height }
1411
1510
  }
1412
1511
  });
1512
+ await applyCapture(context, args.capture, url);
1413
1513
  const page = await context.newPage();
1414
1514
  const video = page.video();
1415
1515
  const recStart = Date.now();
@@ -1444,10 +1544,10 @@ async function captureScrollWebm(args) {
1444
1544
  }
1445
1545
 
1446
1546
  // src/media/frame-capture.ts
1447
- import os from "os";
1547
+ import os2 from "os";
1448
1548
  import path7 from "path";
1449
1549
  function autoWorkers() {
1450
- const cores = os.cpus()?.length ?? 2;
1550
+ const cores = os2.cpus()?.length ?? 2;
1451
1551
  return Math.max(1, Math.min(6, Math.floor(cores / 2)));
1452
1552
  }
1453
1553
  function planFrames(totalFrames, workers) {
@@ -2019,6 +2119,7 @@ async function captureScrollFrames(a) {
2019
2119
  signal: a.signal,
2020
2120
  prepare: async (page, { logger }) => {
2021
2121
  if (a.colorScheme) await page.emulateMedia({ colorScheme: a.colorScheme });
2122
+ await applyCapture(page.context(), a.capture, a.url);
2022
2123
  await installNetworkHygiene(page, options);
2023
2124
  await installPreNav(page, options);
2024
2125
  logger.debug(`navigating to ${a.url} (waitUntil=${options.waitUntil})`);
@@ -2483,6 +2584,7 @@ async function captureInteractionWebm(args) {
2483
2584
  deviceScaleFactor: options.deviceScaleFactor,
2484
2585
  recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
2485
2586
  });
2587
+ await applyCapture(context, args.capture, url);
2486
2588
  const page = await context.newPage();
2487
2589
  const video = page.video();
2488
2590
  const actions = options.actions ?? [];
@@ -2545,6 +2647,7 @@ async function captureFocusWebm(args) {
2545
2647
  deviceScaleFactor: options.deviceScaleFactor,
2546
2648
  recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
2547
2649
  });
2650
+ await applyCapture(context, args.capture, url);
2548
2651
  const page = await context.newPage();
2549
2652
  const video = page.video();
2550
2653
  const actions = focus.actions ?? [];
@@ -2639,6 +2742,7 @@ async function run(ctx, options) {
2639
2742
  ctx.logger.info(`recording ${url} (focus: ${options.focus.selector})`);
2640
2743
  const { webmPath, leadSeconds, durationSeconds, cropBox } = await captureFocusWebm({
2641
2744
  browser: ctx.browser,
2745
+ capture: ctx.capture,
2642
2746
  url,
2643
2747
  options,
2644
2748
  colorScheme: scheme,
@@ -2692,6 +2796,7 @@ async function run(ctx, options) {
2692
2796
  ctx.logger.info(`recording ${url} (interaction, ${options.actions.length} action(s))`);
2693
2797
  const { webmPath, leadSeconds, durationSeconds } = await captureInteractionWebm({
2694
2798
  browser: ctx.browser,
2799
+ capture: ctx.capture,
2695
2800
  url,
2696
2801
  options,
2697
2802
  colorScheme: scheme,
@@ -2755,6 +2860,7 @@ async function run(ctx, options) {
2755
2860
  );
2756
2861
  await captureScrollFrames({
2757
2862
  browser: ctx.browser,
2863
+ capture: ctx.capture,
2758
2864
  url: routeUrl,
2759
2865
  options: routeOpts,
2760
2866
  outPath: segPath,
@@ -2810,6 +2916,7 @@ async function run(ctx, options) {
2810
2916
  ctx.logger.info(`recording ${url} (realtime)`);
2811
2917
  const { webmPath, leadSeconds } = await captureScrollWebm({
2812
2918
  browser: ctx.browser,
2919
+ capture: ctx.capture,
2813
2920
  url,
2814
2921
  options,
2815
2922
  tmpDir: ctx.tmpDir,
@@ -2873,6 +2980,7 @@ async function run(ctx, options) {
2873
2980
  ctx.logger.info(`recording ${url}${label} (frame-stepped, ${workers} worker(s))`);
2874
2981
  await captureScrollFrames({
2875
2982
  browser: ctx.browser,
2983
+ capture: ctx.capture,
2876
2984
  url,
2877
2985
  options: vopts,
2878
2986
  outPath: captureMp4,
@@ -2999,6 +3107,7 @@ async function captureBreakpoint(args, bp) {
2999
3107
  viewport: { width: bp.width, height: bp.height },
3000
3108
  deviceScaleFactor: bp.deviceScaleFactor ?? options.deviceScaleFactor
3001
3109
  });
3110
+ await applyCapture(context, args.capture, url);
3002
3111
  const page = await context.newPage();
3003
3112
  try {
3004
3113
  logger.debug(`[${bp.name}] navigating to ${url}`);
@@ -3069,7 +3178,8 @@ async function run2(ctx, options) {
3069
3178
  browser: ctx.browser,
3070
3179
  url,
3071
3180
  options,
3072
- logger: ctx.logger
3181
+ logger: ctx.logger,
3182
+ capture: ctx.capture
3073
3183
  });
3074
3184
  const records = [];
3075
3185
  for (const shot of shots) {
@@ -3449,7 +3559,7 @@ var wallOptionsSchema = z6.object({
3449
3559
  // src/generators/scene/index.ts
3450
3560
  import path13 from "path";
3451
3561
  import { fileURLToPath } from "url";
3452
- import { copyFile as copyFile2, rename, stat as stat4 } from "fs/promises";
3562
+ import { copyFile as copyFile2, rename as rename2, stat as stat4 } from "fs/promises";
3453
3563
 
3454
3564
  // src/scene/serve.ts
3455
3565
  import http from "http";
@@ -3774,7 +3884,7 @@ async function renderScene(ctx, options, generatorId = SCENE_ID) {
3774
3884
  });
3775
3885
  }
3776
3886
  try {
3777
- await rename(composedTmp, outPath);
3887
+ await rename2(composedTmp, outPath);
3778
3888
  } catch {
3779
3889
  await copyFile2(composedTmp, outPath);
3780
3890
  }
@@ -4916,7 +5026,7 @@ function killTreeByPid(pid) {
4916
5026
  // src/server/manage-server.ts
4917
5027
  import path20 from "path";
4918
5028
  import http2 from "http";
4919
- import https from "https";
5029
+ import https2 from "https";
4920
5030
  import { spawn as spawn6 } from "child_process";
4921
5031
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
4922
5032
  function resolveServerUrl(server) {
@@ -4924,7 +5034,7 @@ function resolveServerUrl(server) {
4924
5034
  }
4925
5035
  function probe(url) {
4926
5036
  return new Promise((resolve) => {
4927
- const lib = url.startsWith("https:") ? https : http2;
5037
+ const lib = url.startsWith("https:") ? https2 : http2;
4928
5038
  const req = lib.get(url, (res) => {
4929
5039
  res.resume();
4930
5040
  resolve((res.statusCode ?? 0) > 0);
@@ -5085,7 +5195,7 @@ async function startManagedServer(server, baseCwd, logger, tasks = {}, signal) {
5085
5195
  }
5086
5196
 
5087
5197
  // src/pipeline/runner.ts
5088
- import os2 from "os";
5198
+ import os3 from "os";
5089
5199
  import path23 from "path";
5090
5200
  import { existsSync as existsSync7 } from "fs";
5091
5201
  import { mkdtemp as mkdtemp4 } from "fs/promises";
@@ -5109,7 +5219,10 @@ async function createContext(args) {
5109
5219
  await ensureDir(genDir);
5110
5220
  return {
5111
5221
  browser: args.browser,
5112
- target: args.target,
5222
+ // Fold the capture query params into the target URL so every generator's `requireUrl(ctx)`
5223
+ // captures in capture mode without each one re-applying it.
5224
+ target: { ...args.target, url: withCaptureQuery(args.target.url, args.capture) },
5225
+ capture: args.capture,
5113
5226
  resolvedInputs: args.resolvedInputs,
5114
5227
  outDir: args.outDir,
5115
5228
  resolveOutPath: (filename) => path21.join(genDir, filename),
@@ -5196,7 +5309,7 @@ function computeCacheKey(parts) {
5196
5309
  // src/manifest/manifest.ts
5197
5310
  import path22 from "path";
5198
5311
  import { existsSync as existsSync6 } from "fs";
5199
- import { readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile8 } from "fs/promises";
5312
+ import { readFile as readFile7, rename as rename3, rm as rm5, writeFile as writeFile8 } from "fs/promises";
5200
5313
 
5201
5314
  // src/manifest/schema.ts
5202
5315
  import { z as z10 } from "zod";
@@ -5263,7 +5376,7 @@ async function writeManifest(outDir, manifest) {
5263
5376
  const maxAttempts = 5;
5264
5377
  for (let attempt = 1; ; attempt++) {
5265
5378
  try {
5266
- await rename2(tmp, file);
5379
+ await rename3(tmp, file);
5267
5380
  return;
5268
5381
  } catch (err) {
5269
5382
  const code = err.code;
@@ -5324,7 +5437,7 @@ async function runPipeline(opts) {
5324
5437
  if (specs.length === 0) return [];
5325
5438
  await ensureDir(opts.outDir);
5326
5439
  const manifest = await ManifestStore.load(opts.outDir);
5327
- const tmpRoot = await mkdtemp4(path23.join(os2.tmpdir(), "pro-visu-"));
5440
+ const tmpRoot = await mkdtemp4(path23.join(os3.tmpdir(), "pro-visu-"));
5328
5441
  const browser = await launchBrowser(opts.config.settings.browser);
5329
5442
  opts.onResources?.({ tmpDir: tmpRoot });
5330
5443
  const concurrency = Math.max(1, opts.concurrency ?? opts.config.settings.concurrency);
@@ -5376,7 +5489,9 @@ async function runPipeline(opts) {
5376
5489
  inputs: inputHashes,
5377
5490
  files: fileHashes,
5378
5491
  quality,
5379
- toolVersion: opts.toolVersion
5492
+ toolVersion: opts.toolVersion,
5493
+ // Capture-mode toggles change the rendered output, so a change must bust the cache.
5494
+ capture: opts.config.settings.capture
5380
5495
  });
5381
5496
  if (cacheEnabled) {
5382
5497
  const existing = manifest.find(spec.name);
@@ -5405,6 +5520,7 @@ async function runPipeline(opts) {
5405
5520
  toolVersion: opts.toolVersion,
5406
5521
  quality,
5407
5522
  manifest,
5523
+ capture: opts.config.settings.capture,
5408
5524
  onProgress: reporter ? (v) => reporter.progress(spec.name, v) : void 0,
5409
5525
  signal: opts.signal
5410
5526
  });