hyperframes 0.2.3 → 0.2.5

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.js CHANGED
@@ -54,7 +54,7 @@ var VERSION;
54
54
  var init_version = __esm({
55
55
  "src/version.ts"() {
56
56
  "use strict";
57
- VERSION = true ? "0.2.3" : "0.0.0-dev";
57
+ VERSION = true ? "0.2.5" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -2731,7 +2731,7 @@ import { get as httpsGet } from "https";
2731
2731
  import { pipeline } from "stream/promises";
2732
2732
  function downloadFile(url, dest) {
2733
2733
  const tmp = `${dest}.tmp`;
2734
- return new Promise((resolve27, reject) => {
2734
+ return new Promise((resolve28, reject) => {
2735
2735
  const follow = (u) => {
2736
2736
  httpsGet(u, (res) => {
2737
2737
  if (res.statusCode === 301 || res.statusCode === 302) {
@@ -2748,7 +2748,7 @@ function downloadFile(url, dest) {
2748
2748
  const file = createWriteStream(tmp);
2749
2749
  pipeline(res, file).then(() => {
2750
2750
  renameSync(tmp, dest);
2751
- resolve27();
2751
+ resolve28();
2752
2752
  }).catch((err) => {
2753
2753
  try {
2754
2754
  unlinkSync(tmp);
@@ -3401,13 +3401,13 @@ function hasNpx() {
3401
3401
  }
3402
3402
  }
3403
3403
  function runSkillsAdd(repo) {
3404
- return new Promise((resolve27, reject) => {
3404
+ return new Promise((resolve28, reject) => {
3405
3405
  const child = spawn("npx", ["skills", "add", repo, "--all"], {
3406
3406
  stdio: "inherit",
3407
3407
  timeout: 12e4
3408
3408
  });
3409
3409
  child.on("close", (code, signal) => {
3410
- if (code === 0) resolve27();
3410
+ if (code === 0) resolve28();
3411
3411
  else if (signal === "SIGINT" || code === 130) process.exit(0);
3412
3412
  else reject(new Error(`npx skills add exited with code ${code}`));
3413
3413
  });
@@ -3554,7 +3554,8 @@ var init_utils = __esm({
3554
3554
 
3555
3555
  // ../core/src/lint/context.ts
3556
3556
  function buildLintContext(html, options = {}) {
3557
- let source = html || "";
3557
+ const rawSource = html || "";
3558
+ let source = rawSource;
3558
3559
  const templateMatch = source.match(/<template[^>]*>([\s\S]*)<\/template>/i);
3559
3560
  if (templateMatch?.[1]) source = templateMatch[1];
3560
3561
  const tags = extractOpenTags(source);
@@ -3565,6 +3566,7 @@ function buildLintContext(html, options = {}) {
3565
3566
  const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id");
3566
3567
  return {
3567
3568
  source,
3569
+ rawSource,
3568
3570
  tags,
3569
3571
  styles,
3570
3572
  scripts,
@@ -4575,8 +4577,30 @@ ${right.raw}`)
4575
4577
  findings.push({
4576
4578
  code: "gsap_infinite_repeat",
4577
4579
  severity: "error",
4578
- message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.ceil(duration / cycleDuration) - 1`.",
4579
- fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.ceil(totalDuration / singleCycleDuration) - 1`.",
4580
+ message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.floor(duration / cycleDuration) - 1`.",
4581
+ fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.floor(totalDuration / singleCycleDuration) - 1`. Use Math.floor (not Math.ceil) to ensure the animation fits within the total duration.",
4582
+ snippet: truncateSnippet(snippet)
4583
+ });
4584
+ }
4585
+ }
4586
+ return findings;
4587
+ },
4588
+ // gsap_repeat_ceil_overshoot
4589
+ ({ scripts }) => {
4590
+ const findings = [];
4591
+ for (const script of scripts) {
4592
+ const content = script.content;
4593
+ const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
4594
+ let match;
4595
+ while ((match = pattern.exec(content)) !== null) {
4596
+ const contextStart = Math.max(0, match.index - 40);
4597
+ const contextEnd = Math.min(content.length, match.index + match[0].length + 40);
4598
+ const snippet = content.slice(contextStart, contextEnd).trim();
4599
+ findings.push({
4600
+ code: "gsap_repeat_ceil_overshoot",
4601
+ severity: "warning",
4602
+ message: "GSAP repeat calculation uses `Math.ceil` which can overshoot the composition duration. For example, Math.ceil(10.5 / 2) - 1 = 5 repeats \u2192 6 cycles \xD7 2s = 12s, exceeding 10.5s.",
4603
+ fixHint: "Use `Math.floor` instead of `Math.ceil` to ensure the animation fits within the duration: `repeat: Math.floor(totalDuration / cycleDuration) - 1`. Math.floor(10.5 / 2) - 1 = 4 repeats \u2192 5 cycles \xD7 2s = 10s \u2713",
4580
4604
  snippet: truncateSnippet(snippet)
4581
4605
  });
4582
4606
  }
@@ -4971,6 +4995,76 @@ var init_composition = __esm({
4971
4995
  }
4972
4996
  return findings;
4973
4997
  },
4998
+ // root_composition_missing_data_start
4999
+ ({ rootTag }) => {
5000
+ const findings = [];
5001
+ if (!rootTag) return findings;
5002
+ const compId = readAttr(rootTag.raw, "data-composition-id");
5003
+ if (!compId) return findings;
5004
+ const hasStart = readAttr(rootTag.raw, "data-start") !== null;
5005
+ if (!hasStart) {
5006
+ findings.push({
5007
+ code: "root_composition_missing_data_start",
5008
+ severity: "warning",
5009
+ message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
5010
+ fixHint: 'Add data-start="0" to the root composition element.',
5011
+ snippet: truncateSnippet(rootTag.raw)
5012
+ });
5013
+ }
5014
+ return findings;
5015
+ },
5016
+ // root_composition_missing_data_duration
5017
+ ({ rootTag }) => {
5018
+ const findings = [];
5019
+ if (!rootTag) return findings;
5020
+ const compId = readAttr(rootTag.raw, "data-composition-id");
5021
+ if (!compId) return findings;
5022
+ const hasDuration = readAttr(rootTag.raw, "data-duration") !== null;
5023
+ if (!hasDuration) {
5024
+ findings.push({
5025
+ code: "root_composition_missing_data_duration",
5026
+ severity: "warning",
5027
+ message: `Root composition "${compId}" is missing data-duration. Without an explicit duration, the runtime may infer Infinity for compositions with repeating animations, causing playback issues.`,
5028
+ fixHint: 'Add data-duration="X" to the root composition element, where X is the total duration in seconds.',
5029
+ snippet: truncateSnippet(rootTag.raw)
5030
+ });
5031
+ }
5032
+ return findings;
5033
+ },
5034
+ // standalone_composition_wrapped_in_template
5035
+ ({ rawSource, options }) => {
5036
+ const findings = [];
5037
+ if (options.isSubComposition) return findings;
5038
+ const trimmed = rawSource.trimStart().toLowerCase();
5039
+ if (trimmed.startsWith("<template")) {
5040
+ findings.push({
5041
+ code: "standalone_composition_wrapped_in_template",
5042
+ severity: "warning",
5043
+ message: "Root index.html is wrapped in a <template> tag. Only sub-compositions loaded via data-composition-src should use <template> wrappers. The runtime cannot play a standalone composition inside a template.",
5044
+ fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
5045
+ });
5046
+ }
5047
+ return findings;
5048
+ },
5049
+ // root_composition_missing_html_wrapper
5050
+ ({ rawSource, rootTag, options }) => {
5051
+ const findings = [];
5052
+ if (options.isSubComposition) return findings;
5053
+ const trimmed = rawSource.trimStart().toLowerCase();
5054
+ if (trimmed.startsWith("<template")) return findings;
5055
+ const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
5056
+ const hasComposition = rawSource.includes("data-composition-id");
5057
+ if (hasComposition && !hasDoctype) {
5058
+ findings.push({
5059
+ code: "root_composition_missing_html_wrapper",
5060
+ severity: "error",
5061
+ message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment \u2014 browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
5062
+ fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
5063
+ snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
5064
+ });
5065
+ }
5066
+ return findings;
5067
+ },
4974
5068
  // requestanimationframe_in_composition
4975
5069
  ({ scripts }) => {
4976
5070
  const findings = [];
@@ -5192,7 +5286,7 @@ function lintProject(project) {
5192
5286
  const filePath = join6(compositionsDir, file);
5193
5287
  const html = readFileSync6(filePath, "utf-8");
5194
5288
  allHtmlSources.push(html);
5195
- const result = lintHyperframeHtml(html, { filePath });
5289
+ const result = lintHyperframeHtml(html, { filePath, isSubComposition: true });
5196
5290
  results.push({ file: `compositions/${file}`, result });
5197
5291
  totalErrors += result.errorCount;
5198
5292
  totalWarnings += result.warningCount;
@@ -5328,6 +5422,238 @@ var init_lintFormat = __esm({
5328
5422
  }
5329
5423
  });
5330
5424
 
5425
+ // src/server/portUtils.ts
5426
+ import net from "net";
5427
+ import http from "http";
5428
+ import { execFile } from "child_process";
5429
+ import { promisify } from "util";
5430
+ import { resolve as resolve2 } from "path";
5431
+ function isPortAvailableOnHost(port, host) {
5432
+ return new Promise((resolve28) => {
5433
+ const server = net.createServer();
5434
+ server.unref();
5435
+ server.on("error", (err) => {
5436
+ resolve28(err.code !== "EADDRINUSE");
5437
+ });
5438
+ server.listen({ port, host }, () => {
5439
+ server.close(() => {
5440
+ resolve28(true);
5441
+ });
5442
+ });
5443
+ });
5444
+ }
5445
+ async function testPortOnAllHosts(port) {
5446
+ const hosts = ["127.0.0.1", "0.0.0.0", "::1", "::"];
5447
+ const results = await Promise.all(hosts.map((h2) => isPortAvailableOnHost(port, h2)));
5448
+ return results.every(Boolean);
5449
+ }
5450
+ function detectHyperframesServer(port, normalizedProjectDir) {
5451
+ return new Promise((resolveResult) => {
5452
+ const req = http.get(
5453
+ {
5454
+ hostname: "127.0.0.1",
5455
+ port,
5456
+ path: "/__hyperframes_config",
5457
+ timeout: PROBE_TIMEOUT_MS
5458
+ },
5459
+ (res) => {
5460
+ if (res.statusCode !== 200) {
5461
+ res.resume();
5462
+ return resolveResult({ type: "not-hyperframes" });
5463
+ }
5464
+ let data = "";
5465
+ let bytes = 0;
5466
+ res.on("data", (chunk) => {
5467
+ bytes += typeof chunk === "string" ? chunk.length : chunk.byteLength;
5468
+ if (bytes > PROBE_MAX_BYTES) {
5469
+ req.destroy();
5470
+ return resolveResult({ type: "not-hyperframes" });
5471
+ }
5472
+ data += chunk;
5473
+ });
5474
+ res.on("error", () => {
5475
+ resolveResult({ type: "not-hyperframes" });
5476
+ });
5477
+ res.on("end", () => {
5478
+ try {
5479
+ const json = JSON.parse(data);
5480
+ if (json.isHyperframes !== true) {
5481
+ return resolveResult({ type: "not-hyperframes" });
5482
+ }
5483
+ const normalize = (p) => resolve2(p).replace(/\\/g, "/").toLowerCase();
5484
+ if (normalize(json.projectDir) === normalizedProjectDir) {
5485
+ return resolveResult({ type: "match" });
5486
+ }
5487
+ return resolveResult({ type: "mismatch", projectName: json.projectName });
5488
+ } catch {
5489
+ resolveResult({ type: "not-hyperframes" });
5490
+ }
5491
+ });
5492
+ }
5493
+ );
5494
+ req.on("error", () => {
5495
+ resolveResult({ type: "not-hyperframes" });
5496
+ });
5497
+ req.on("timeout", () => {
5498
+ req.destroy();
5499
+ resolveResult({ type: "not-hyperframes" });
5500
+ });
5501
+ });
5502
+ }
5503
+ async function getProcessOnPort(port) {
5504
+ if (process.platform === "win32") return null;
5505
+ try {
5506
+ const { stdout: stdout2 } = await execFileAsync("lsof", [`-ti:${port}`, "-sTCP:LISTEN"], {
5507
+ timeout: 2e3
5508
+ });
5509
+ const pid = stdout2.trim().split("\n")[0]?.trim();
5510
+ return pid || null;
5511
+ } catch {
5512
+ return null;
5513
+ }
5514
+ }
5515
+ function probePort(port) {
5516
+ return new Promise((resolveResult) => {
5517
+ const req = http.get(
5518
+ { hostname: "127.0.0.1", port, path: "/__hyperframes_config", timeout: PROBE_TIMEOUT_MS },
5519
+ (res) => {
5520
+ if (res.statusCode !== 200) {
5521
+ res.resume();
5522
+ return resolveResult(null);
5523
+ }
5524
+ let data = "";
5525
+ let bytes = 0;
5526
+ res.on("data", (chunk) => {
5527
+ bytes += typeof chunk === "string" ? chunk.length : chunk.byteLength;
5528
+ if (bytes > PROBE_MAX_BYTES) {
5529
+ req.destroy();
5530
+ return resolveResult(null);
5531
+ }
5532
+ data += chunk;
5533
+ });
5534
+ res.on("error", () => resolveResult(null));
5535
+ res.on("end", () => {
5536
+ try {
5537
+ const json = JSON.parse(data);
5538
+ resolveResult(json.isHyperframes === true ? json : null);
5539
+ } catch {
5540
+ resolveResult(null);
5541
+ }
5542
+ });
5543
+ }
5544
+ );
5545
+ req.on("error", () => resolveResult(null));
5546
+ req.on("timeout", () => {
5547
+ req.destroy();
5548
+ resolveResult(null);
5549
+ });
5550
+ });
5551
+ }
5552
+ async function scanActiveServers(startPort = 3002) {
5553
+ const endPort = startPort + MAX_PORT_SCAN - 1;
5554
+ const servers = [];
5555
+ const batchSize = 20;
5556
+ for (let batchStart = startPort; batchStart <= endPort; batchStart += batchSize) {
5557
+ const batchEnd = Math.min(batchStart + batchSize - 1, endPort);
5558
+ const ports = Array.from({ length: batchEnd - batchStart + 1 }, (_2, i) => batchStart + i);
5559
+ const results = await Promise.all(
5560
+ ports.map(async (port) => {
5561
+ const config = await probePort(port);
5562
+ if (!config) return null;
5563
+ const pid = await getProcessOnPort(port);
5564
+ return {
5565
+ port,
5566
+ projectName: config.projectName,
5567
+ projectDir: config.projectDir,
5568
+ version: config.version,
5569
+ pid
5570
+ };
5571
+ })
5572
+ );
5573
+ for (const r of results) {
5574
+ if (r) servers.push(r);
5575
+ }
5576
+ }
5577
+ return servers;
5578
+ }
5579
+ async function killActiveServers(startPort = 3002) {
5580
+ const servers = await scanActiveServers(startPort);
5581
+ let killed = 0;
5582
+ for (const server of servers) {
5583
+ if (server.pid) {
5584
+ try {
5585
+ process.kill(parseInt(server.pid, 10), "SIGTERM");
5586
+ killed++;
5587
+ } catch {
5588
+ }
5589
+ }
5590
+ }
5591
+ return killed;
5592
+ }
5593
+ async function findPortAndServe(fetch3, startPort, projectDir, forceNew) {
5594
+ const { createAdaptorServer } = await import("@hono/node-server");
5595
+ const normalizedDir = resolve2(projectDir).replace(/\\/g, "/").toLowerCase();
5596
+ const endPort = startPort + MAX_PORT_SCAN - 1;
5597
+ let server = null;
5598
+ for (let port = startPort; port <= endPort; port++) {
5599
+ const available = await testPortOnAllHosts(port);
5600
+ if (available) {
5601
+ if (!server) server = createAdaptorServer({ fetch: fetch3 });
5602
+ try {
5603
+ await new Promise((resolveListener, rejectListener) => {
5604
+ const onError = (err) => {
5605
+ server.removeListener("listening", onListening);
5606
+ rejectListener(err);
5607
+ };
5608
+ const onListening = () => {
5609
+ server.removeListener("error", onError);
5610
+ resolveListener();
5611
+ };
5612
+ server.once("error", onError);
5613
+ server.once("listening", onListening);
5614
+ server.listen(port);
5615
+ });
5616
+ return { type: "started", server, port };
5617
+ } catch (err) {
5618
+ if (err.code === "EADDRINUSE") {
5619
+ continue;
5620
+ }
5621
+ throw err;
5622
+ }
5623
+ }
5624
+ if (!forceNew) {
5625
+ const detection = await detectHyperframesServer(port, normalizedDir);
5626
+ if (detection.type === "match") {
5627
+ return { type: "already-running", port };
5628
+ }
5629
+ if (detection.type === "mismatch") {
5630
+ console.log(
5631
+ ` ${c.dim(`Port ${port} in use by HyperFrames project "${detection.projectName}" \u2014 skipping`)}`
5632
+ );
5633
+ continue;
5634
+ }
5635
+ }
5636
+ const pid = await getProcessOnPort(port);
5637
+ if (pid) {
5638
+ console.log(` ${c.dim(`Port ${port} in use by PID ${pid} \u2014 skipping`)}`);
5639
+ }
5640
+ }
5641
+ throw new Error(
5642
+ `Ports ${startPort}\u2013${endPort} are all in use. Use --port to specify a different starting port.`
5643
+ );
5644
+ }
5645
+ var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES;
5646
+ var init_portUtils = __esm({
5647
+ "src/server/portUtils.ts"() {
5648
+ "use strict";
5649
+ init_colors();
5650
+ execFileAsync = promisify(execFile);
5651
+ MAX_PORT_SCAN = 100;
5652
+ PROBE_TIMEOUT_MS = 300;
5653
+ PROBE_MAX_BYTES = 4096;
5654
+ }
5655
+ });
5656
+
5331
5657
  // src/server/fileWatcher.ts
5332
5658
  import { watch } from "fs";
5333
5659
  function createProjectWatcher(projectDir) {
@@ -5372,11 +5698,11 @@ var init_fileWatcher = __esm({
5372
5698
  });
5373
5699
 
5374
5700
  // ../core/src/studio-api/helpers/safePath.ts
5375
- import { resolve as resolve2, sep, join as join7 } from "path";
5701
+ import { resolve as resolve3, sep, join as join7 } from "path";
5376
5702
  import { readdirSync as readdirSync3 } from "fs";
5377
5703
  function isSafePath(base, resolved) {
5378
- const norm = resolve2(base) + sep;
5379
- return resolved.startsWith(norm) || resolved === resolve2(base);
5704
+ const norm = resolve3(base) + sep;
5705
+ return resolved.startsWith(norm) || resolved === resolve3(base);
5380
5706
  }
5381
5707
  function walkDir(dir, prefix = "") {
5382
5708
  const files = [];
@@ -5441,7 +5767,7 @@ import {
5441
5767
  renameSync as renameSync2,
5442
5768
  readdirSync as readdirSync4
5443
5769
  } from "fs";
5444
- import { resolve as resolve3, dirname, join as join8 } from "path";
5770
+ import { resolve as resolve4, dirname, join as join8 } from "path";
5445
5771
  async function resolveProjectFile(c2, adapter2, opts) {
5446
5772
  const id = c2.req.param("id");
5447
5773
  const project = await adapter2.resolveProject(id);
@@ -5452,7 +5778,7 @@ async function resolveProjectFile(c2, adapter2, opts) {
5452
5778
  if (filePath.includes("\0")) {
5453
5779
  return { error: c2.json({ error: "forbidden" }, 403) };
5454
5780
  }
5455
- const absPath = resolve3(project.dir, filePath);
5781
+ const absPath = resolve4(project.dir, filePath);
5456
5782
  if (!isSafePath(project.dir, absPath)) {
5457
5783
  return { error: c2.json({ error: "forbidden" }, 403) };
5458
5784
  }
@@ -5472,7 +5798,7 @@ function generateCopyPath(projectDir, originalPath) {
5472
5798
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
5473
5799
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
5474
5800
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
5475
- while (existsSync7(resolve3(projectDir, candidate))) {
5801
+ while (existsSync7(resolve4(projectDir, candidate))) {
5476
5802
  num++;
5477
5803
  candidate = `${cleanBase} (copy ${num})${ext}`;
5478
5804
  }
@@ -5553,7 +5879,7 @@ function registerFileRoutes(api, adapter2) {
5553
5879
  if (!body.newPath || body.newPath.includes("\0")) {
5554
5880
  return c2.json({ error: "newPath required" }, 400);
5555
5881
  }
5556
- const newAbs = resolve3(res.project.dir, body.newPath);
5882
+ const newAbs = resolve4(res.project.dir, body.newPath);
5557
5883
  if (!isSafePath(res.project.dir, newAbs)) {
5558
5884
  return c2.json({ error: "forbidden" }, 403);
5559
5885
  }
@@ -5572,12 +5898,12 @@ function registerFileRoutes(api, adapter2) {
5572
5898
  if (!body.path || body.path.includes("\0")) {
5573
5899
  return c2.json({ error: "path required" }, 400);
5574
5900
  }
5575
- const srcAbs = resolve3(project.dir, body.path);
5901
+ const srcAbs = resolve4(project.dir, body.path);
5576
5902
  if (!isSafePath(project.dir, srcAbs) || !existsSync7(srcAbs)) {
5577
5903
  return c2.json({ error: "not found" }, 404);
5578
5904
  }
5579
5905
  const copyPath = generateCopyPath(project.dir, body.path);
5580
- const destAbs = resolve3(project.dir, copyPath);
5906
+ const destAbs = resolve4(project.dir, copyPath);
5581
5907
  if (!isSafePath(project.dir, destAbs)) {
5582
5908
  return c2.json({ error: "forbidden" }, 403);
5583
5909
  }
@@ -5596,7 +5922,7 @@ function registerFileRoutes(api, adapter2) {
5596
5922
  const project = await adapter2.resolveProject(c2.req.param("id"));
5597
5923
  if (!project) return c2.json({ error: "not found" }, 404);
5598
5924
  const subDir = c2.req.query("dir") ?? "";
5599
- const targetDir = subDir ? resolve3(project.dir, subDir) : project.dir;
5925
+ const targetDir = subDir ? resolve4(project.dir, subDir) : project.dir;
5600
5926
  if (!isSafePath(project.dir, targetDir)) return c2.json({ error: "forbidden" }, 403);
5601
5927
  if (subDir && !existsSync7(targetDir)) mkdirSync5(targetDir, { recursive: true });
5602
5928
  const formData = await c2.req.formData();
@@ -5610,7 +5936,7 @@ function registerFileRoutes(api, adapter2) {
5610
5936
  skipped.push(name);
5611
5937
  continue;
5612
5938
  }
5613
- const destPath = resolve3(targetDir, name);
5939
+ const destPath = resolve4(targetDir, name);
5614
5940
  if (!isSafePath(project.dir, destPath)) continue;
5615
5941
  let finalPath = destPath;
5616
5942
  let finalName = name;
@@ -5619,13 +5945,13 @@ function registerFileRoutes(api, adapter2) {
5619
5945
  const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
5620
5946
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
5621
5947
  let n = 2;
5622
- while (n < 1e4 && existsSync7(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
5948
+ while (n < 1e4 && existsSync7(resolve4(targetDir, `${base} (${n})${ext}`))) n++;
5623
5949
  if (n >= 1e4) {
5624
5950
  skipped.push(name);
5625
5951
  continue;
5626
5952
  }
5627
5953
  finalName = `${base} (${n})${ext}`;
5628
- finalPath = resolve3(targetDir, finalName);
5954
+ finalPath = resolve4(targetDir, finalName);
5629
5955
  }
5630
5956
  const buffer = Buffer.from(await value.arrayBuffer());
5631
5957
  writeFileSync4(finalPath, buffer);
@@ -9535,8 +9861,8 @@ var init_custom_element_registry = __esm({
9535
9861
  } : (element) => element.localName === localName;
9536
9862
  registry.set(localName, { Class, check });
9537
9863
  if (waiting.has(localName)) {
9538
- for (const resolve27 of waiting.get(localName))
9539
- resolve27(Class);
9864
+ for (const resolve28 of waiting.get(localName))
9865
+ resolve28(Class);
9540
9866
  waiting.delete(localName);
9541
9867
  }
9542
9868
  ownerDocument.querySelectorAll(
@@ -9576,13 +9902,13 @@ var init_custom_element_registry = __esm({
9576
9902
  */
9577
9903
  whenDefined(localName) {
9578
9904
  const { registry, waiting } = this;
9579
- return new Promise((resolve27) => {
9905
+ return new Promise((resolve28) => {
9580
9906
  if (registry.has(localName))
9581
- resolve27(registry.get(localName).Class);
9907
+ resolve28(registry.get(localName).Class);
9582
9908
  else {
9583
9909
  if (!waiting.has(localName))
9584
9910
  waiting.set(localName, []);
9585
- waiting.get(localName).push(resolve27);
9911
+ waiting.get(localName).push(resolve28);
9586
9912
  }
9587
9913
  });
9588
9914
  }
@@ -18283,7 +18609,7 @@ var init_esm10 = __esm({
18283
18609
  });
18284
18610
 
18285
18611
  // ../core/src/compiler/rewriteSubCompPaths.ts
18286
- import { join as join9, resolve as resolve4, dirname as dirname2 } from "path";
18612
+ import { join as join9, resolve as resolve5, dirname as dirname2 } from "path";
18287
18613
  function isAbsoluteOrSpecial(val) {
18288
18614
  return !val || val.startsWith("http://") || val.startsWith("https://") || val.startsWith("//") || val.startsWith("data:") || val.startsWith("#");
18289
18615
  }
@@ -18296,7 +18622,7 @@ function rewriteAssetPath(compSrcPath, relativePath) {
18296
18622
  const compDir = dirname2(compSrcPath);
18297
18623
  if (!compDir || compDir === ".") return relativePath;
18298
18624
  const resolved = join9(compDir, relativePath);
18299
- const normalized = resolve4("/", resolved).slice(1);
18625
+ const normalized = resolve5("/", resolved).slice(1);
18300
18626
  return normalized;
18301
18627
  }
18302
18628
  function rewriteAssetPaths(elements, compSrcPath, getAttr2, setAttr) {
@@ -18308,7 +18634,7 @@ function rewriteAssetPaths(elements, compSrcPath, getAttr2, setAttr) {
18308
18634
  if (isAbsoluteOrSpecial(val)) continue;
18309
18635
  if (!needsRewrite(val)) continue;
18310
18636
  const rewritten = join9(compDir, val);
18311
- const normalized = resolve4("/", rewritten).slice(1);
18637
+ const normalized = resolve5("/", rewritten).slice(1);
18312
18638
  if (normalized !== val) {
18313
18639
  setAttr(el, attr, normalized);
18314
18640
  }
@@ -18397,7 +18723,7 @@ var init_subComposition = __esm({
18397
18723
 
18398
18724
  // ../core/src/studio-api/routes/preview.ts
18399
18725
  import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
18400
- import { resolve as resolve5 } from "path";
18726
+ import { resolve as resolve6 } from "path";
18401
18727
  function registerPreviewRoutes(api, adapter2) {
18402
18728
  api.get("/projects/:id/preview", async (c2) => {
18403
18729
  const project = await adapter2.resolveProject(c2.req.param("id"));
@@ -18405,7 +18731,7 @@ function registerPreviewRoutes(api, adapter2) {
18405
18731
  try {
18406
18732
  let bundled = await adapter2.bundle(project.dir);
18407
18733
  if (!bundled) {
18408
- const indexPath = resolve5(project.dir, "index.html");
18734
+ const indexPath = resolve6(project.dir, "index.html");
18409
18735
  if (!existsSync9(indexPath)) return c2.text("not found", 404);
18410
18736
  bundled = readFileSync9(indexPath, "utf-8");
18411
18737
  }
@@ -18421,7 +18747,7 @@ ${runtimeTag}`;
18421
18747
  }
18422
18748
  return c2.html(bundled);
18423
18749
  } catch {
18424
- const file = resolve5(project.dir, "index.html");
18750
+ const file = resolve6(project.dir, "index.html");
18425
18751
  if (existsSync9(file)) return c2.html(readFileSync9(file, "utf-8"));
18426
18752
  return c2.text("not found", 404);
18427
18753
  }
@@ -18432,7 +18758,7 @@ ${runtimeTag}`;
18432
18758
  const compPath = decodeURIComponent(
18433
18759
  c2.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
18434
18760
  );
18435
- const compFile = resolve5(project.dir, compPath);
18761
+ const compFile = resolve6(project.dir, compPath);
18436
18762
  if (!isSafePath(project.dir, compFile) || !existsSync9(compFile) || !statSync2(compFile).isFile()) {
18437
18763
  return c2.text("not found", 404);
18438
18764
  }
@@ -18447,7 +18773,7 @@ ${runtimeTag}`;
18447
18773
  const subPath = decodeURIComponent(
18448
18774
  c2.req.path.replace(`/projects/${project.id}/preview/`, "").split("?")[0] ?? ""
18449
18775
  );
18450
- const file = resolve5(project.dir, subPath);
18776
+ const file = resolve6(project.dir, subPath);
18451
18777
  if (!isSafePath(project.dir, file) || !existsSync9(file) || !statSync2(file).isFile()) {
18452
18778
  return c2.text("not found", 404);
18453
18779
  }
@@ -19823,7 +20149,7 @@ var init_timingCompiler = __esm({
19823
20149
 
19824
20150
  // ../core/src/inline-scripts/hyperframesRuntime.engine.ts
19825
20151
  import { buildSync } from "esbuild";
19826
- import { dirname as dirname3, resolve as resolve6 } from "path";
20152
+ import { dirname as dirname3, resolve as resolve7 } from "path";
19827
20153
  import { fileURLToPath } from "url";
19828
20154
  var init_hyperframesRuntime_engine = __esm({
19829
20155
  "../core/src/inline-scripts/hyperframesRuntime.engine.ts"() {
@@ -19896,17 +20222,25 @@ var init_gsap2 = __esm({
19896
20222
  }
19897
20223
  });
19898
20224
 
19899
- // ../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/bidi.js
20225
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/generated/bidi-data.js
20226
+ var init_bidi_data = __esm({
20227
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/generated/bidi-data.js"() {
20228
+ "use strict";
20229
+ }
20230
+ });
20231
+
20232
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/bidi.js
19900
20233
  var init_bidi = __esm({
19901
- "../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/bidi.js"() {
20234
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/bidi.js"() {
19902
20235
  "use strict";
20236
+ init_bidi_data();
19903
20237
  }
19904
20238
  });
19905
20239
 
19906
- // ../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/analysis.js
20240
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/analysis.js
19907
20241
  var arabicScriptRe, combiningMarkRe, decimalDigitRe;
19908
20242
  var init_analysis = __esm({
19909
- "../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/analysis.js"() {
20243
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/analysis.js"() {
19910
20244
  "use strict";
19911
20245
  arabicScriptRe = new RegExp("\\p{Script=Arabic}", "u");
19912
20246
  combiningMarkRe = new RegExp("\\p{M}", "u");
@@ -19914,27 +20248,27 @@ var init_analysis = __esm({
19914
20248
  }
19915
20249
  });
19916
20250
 
19917
- // ../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/measurement.js
20251
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/measurement.js
19918
20252
  var emojiPresentationRe;
19919
20253
  var init_measurement = __esm({
19920
- "../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/measurement.js"() {
20254
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/measurement.js"() {
19921
20255
  "use strict";
19922
20256
  init_analysis();
19923
20257
  emojiPresentationRe = new RegExp("\\p{Emoji_Presentation}", "u");
19924
20258
  }
19925
20259
  });
19926
20260
 
19927
- // ../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/line-break.js
20261
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/line-break.js
19928
20262
  var init_line_break = __esm({
19929
- "../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/line-break.js"() {
20263
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/line-break.js"() {
19930
20264
  "use strict";
19931
20265
  init_measurement();
19932
20266
  }
19933
20267
  });
19934
20268
 
19935
- // ../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/layout.js
20269
+ // ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/layout.js
19936
20270
  var init_layout = __esm({
19937
- "../../node_modules/.bun/@chenglou+pretext@0.0.3/node_modules/@chenglou/pretext/dist/layout.js"() {
20271
+ "../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/layout.js"() {
19938
20272
  "use strict";
19939
20273
  init_bidi();
19940
20274
  init_analysis();
@@ -19990,18 +20324,35 @@ async function getCdpSession(page) {
19990
20324
  }
19991
20325
  return client;
19992
20326
  }
20327
+ async function sendBeginFrame(client, params) {
20328
+ for (let attempt = 0; ; attempt++) {
20329
+ try {
20330
+ return await client.send("HeadlessExperimental.beginFrame", params);
20331
+ } catch (err) {
20332
+ const msg = err instanceof Error ? err.message : String(err);
20333
+ const isPending = msg.includes("Another frame is pending");
20334
+ if (isPending && attempt < PENDING_FRAME_RETRIES) {
20335
+ await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
20336
+ continue;
20337
+ }
20338
+ if (isPending) {
20339
+ throw new Error(
20340
+ `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
20341
+ );
20342
+ }
20343
+ throw err;
20344
+ }
20345
+ }
20346
+ }
19993
20347
  async function beginFrameCapture(page, options, frameTimeTicks, interval) {
19994
20348
  const client = await getCdpSession(page);
19995
- const format = options.format === "png" ? "png" : "jpeg";
19996
- const result = await client.send("HeadlessExperimental.beginFrame", {
19997
- frameTimeTicks,
19998
- interval,
19999
- screenshot: {
20000
- format,
20001
- quality: format === "jpeg" ? options.quality ?? 80 : void 0,
20002
- optimizeForSpeed: true
20003
- }
20004
- });
20349
+ const isPng = options.format === "png";
20350
+ const screenshot = {
20351
+ format: isPng ? "png" : "jpeg",
20352
+ quality: isPng ? void 0 : options.quality ?? 80,
20353
+ optimizeForSpeed: true
20354
+ };
20355
+ const result = await sendBeginFrame(client, { frameTimeTicks, interval, screenshot });
20005
20356
  let buffer;
20006
20357
  if (result.screenshotData) {
20007
20358
  buffer = Buffer.from(result.screenshotData, "base64");
@@ -20011,16 +20362,12 @@ async function beginFrameCapture(page, options, frameTimeTicks, interval) {
20011
20362
  if (cached2) {
20012
20363
  buffer = cached2;
20013
20364
  } else {
20014
- const retry = await client.send("HeadlessExperimental.beginFrame", {
20365
+ const fallback = await sendBeginFrame(client, {
20015
20366
  frameTimeTicks: frameTimeTicks + 1e-3,
20016
20367
  interval,
20017
- screenshot: {
20018
- format,
20019
- quality: format === "jpeg" ? options.quality ?? 80 : void 0,
20020
- optimizeForSpeed: true
20021
- }
20368
+ screenshot
20022
20369
  });
20023
- buffer = retry.screenshotData ? Buffer.from(retry.screenshotData, "base64") : Buffer.alloc(0);
20370
+ buffer = fallback.screenshotData ? Buffer.from(fallback.screenshotData, "base64") : Buffer.alloc(0);
20024
20371
  if (buffer.length > 0) lastFrameCache.set(page, buffer);
20025
20372
  }
20026
20373
  }
@@ -20134,13 +20481,14 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
20134
20481
  }
20135
20482
  }, activeVideoIds);
20136
20483
  }
20137
- var cdpSessionCache, lastFrameCache;
20484
+ var cdpSessionCache, lastFrameCache, PENDING_FRAME_RETRIES;
20138
20485
  var init_screenshotService = __esm({
20139
20486
  "../engine/src/services/screenshotService.ts"() {
20140
20487
  "use strict";
20141
20488
  init_src();
20142
20489
  cdpSessionCache = /* @__PURE__ */ new WeakMap();
20143
20490
  lastFrameCache = /* @__PURE__ */ new WeakMap();
20491
+ PENDING_FRAME_RETRIES = 5;
20144
20492
  }
20145
20493
  });
20146
20494
 
@@ -20472,7 +20820,7 @@ var init_frameCapture = __esm({
20472
20820
  // ../engine/src/utils/gpuEncoder.ts
20473
20821
  import { spawn as spawn2 } from "child_process";
20474
20822
  async function detectGpuEncoder() {
20475
- return new Promise((resolve27) => {
20823
+ return new Promise((resolve28) => {
20476
20824
  const ffmpeg = spawn2("ffmpeg", ["-encoders"], {
20477
20825
  stdio: ["pipe", "pipe", "pipe"]
20478
20826
  });
@@ -20481,13 +20829,13 @@ async function detectGpuEncoder() {
20481
20829
  stdout2 += data.toString();
20482
20830
  });
20483
20831
  ffmpeg.on("close", () => {
20484
- if (stdout2.includes("h264_nvenc")) resolve27("nvenc");
20485
- else if (stdout2.includes("h264_videotoolbox")) resolve27("videotoolbox");
20486
- else if (stdout2.includes("h264_vaapi")) resolve27("vaapi");
20487
- else if (stdout2.includes("h264_qsv")) resolve27("qsv");
20488
- else resolve27(null);
20832
+ if (stdout2.includes("h264_nvenc")) resolve28("nvenc");
20833
+ else if (stdout2.includes("h264_videotoolbox")) resolve28("videotoolbox");
20834
+ else if (stdout2.includes("h264_vaapi")) resolve28("vaapi");
20835
+ else if (stdout2.includes("h264_qsv")) resolve28("qsv");
20836
+ else resolve28(null);
20489
20837
  });
20490
- ffmpeg.on("error", () => resolve27(null));
20838
+ ffmpeg.on("error", () => resolve28(null));
20491
20839
  });
20492
20840
  }
20493
20841
  async function getCachedGpuEncoder() {
@@ -20526,7 +20874,7 @@ async function runFfmpeg(args, opts) {
20526
20874
  const signal = opts?.signal;
20527
20875
  const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
20528
20876
  const onStderr = opts?.onStderr;
20529
- return new Promise((resolve27) => {
20877
+ return new Promise((resolve28) => {
20530
20878
  const ffmpeg = spawn3("ffmpeg", args);
20531
20879
  let stderr = "";
20532
20880
  const onAbort = () => {
@@ -20552,7 +20900,7 @@ async function runFfmpeg(args, opts) {
20552
20900
  ffmpeg.on("close", (code) => {
20553
20901
  clearTimeout(timer);
20554
20902
  if (signal) signal.removeEventListener("abort", onAbort);
20555
- resolve27({
20903
+ resolve28({
20556
20904
  success: !signal?.aborted && code === 0,
20557
20905
  exitCode: code,
20558
20906
  stderr,
@@ -20562,7 +20910,7 @@ async function runFfmpeg(args, opts) {
20562
20910
  ffmpeg.on("error", (err) => {
20563
20911
  clearTimeout(timer);
20564
20912
  if (signal) signal.removeEventListener("abort", onAbort);
20565
- resolve27({
20913
+ resolve28({
20566
20914
  success: false,
20567
20915
  exitCode: null,
20568
20916
  stderr: err.message,
@@ -20721,7 +21069,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20721
21069
  const inputPath = join17(framesDir, framePattern);
20722
21070
  const inputArgs = ["-framerate", String(options.fps), "-i", inputPath];
20723
21071
  const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder);
20724
- return new Promise((resolve27) => {
21072
+ return new Promise((resolve28) => {
20725
21073
  const ffmpeg = spawn4("ffmpeg", args);
20726
21074
  let stderr = "";
20727
21075
  const onAbort = () => {
@@ -20746,7 +21094,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20746
21094
  if (signal) signal.removeEventListener("abort", onAbort);
20747
21095
  const durationMs = Date.now() - startTime;
20748
21096
  if (signal?.aborted) {
20749
- resolve27({
21097
+ resolve28({
20750
21098
  success: false,
20751
21099
  outputPath,
20752
21100
  durationMs,
@@ -20757,7 +21105,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20757
21105
  return;
20758
21106
  }
20759
21107
  if (code !== 0) {
20760
- resolve27({
21108
+ resolve28({
20761
21109
  success: false,
20762
21110
  outputPath,
20763
21111
  durationMs,
@@ -20768,12 +21116,12 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20768
21116
  return;
20769
21117
  }
20770
21118
  const fileSize = existsSync15(outputPath) ? statSync4(outputPath).size : 0;
20771
- resolve27({ success: true, outputPath, durationMs, framesEncoded: frameCount, fileSize });
21119
+ resolve28({ success: true, outputPath, durationMs, framesEncoded: frameCount, fileSize });
20772
21120
  });
20773
21121
  ffmpeg.on("error", (err) => {
20774
21122
  clearTimeout(timer);
20775
21123
  if (signal) signal.removeEventListener("abort", onAbort);
20776
- resolve27({
21124
+ resolve28({
20777
21125
  success: false,
20778
21126
  outputPath,
20779
21127
  durationMs: Date.now() - startTime,
@@ -20831,18 +21179,18 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
20831
21179
  let gpuEncoder = null;
20832
21180
  if (options.useGpu) gpuEncoder = await getCachedGpuEncoder();
20833
21181
  const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder);
20834
- const chunkResult = await new Promise((resolve27) => {
21182
+ const chunkResult = await new Promise((resolve28) => {
20835
21183
  const ffmpeg = spawn4("ffmpeg", args);
20836
21184
  let stderr = "";
20837
21185
  ffmpeg.stderr.on("data", (d) => {
20838
21186
  stderr += d.toString();
20839
21187
  });
20840
21188
  ffmpeg.on("close", (code) => {
20841
- if (code === 0) resolve27({ success: true });
20842
- else resolve27({ success: false, error: `Chunk ${i} encode failed: ${stderr.slice(-400)}` });
21189
+ if (code === 0) resolve28({ success: true });
21190
+ else resolve28({ success: false, error: `Chunk ${i} encode failed: ${stderr.slice(-400)}` });
20843
21191
  });
20844
21192
  ffmpeg.on("error", (err) => {
20845
- resolve27({ success: false, error: `Chunk ${i} encode error: ${err.message}` });
21193
+ resolve28({ success: false, error: `Chunk ${i} encode error: ${err.message}` });
20846
21194
  });
20847
21195
  });
20848
21196
  if (!chunkResult.success) {
@@ -20872,18 +21220,18 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
20872
21220
  "-y",
20873
21221
  outputPath
20874
21222
  ];
20875
- const concatResult = await new Promise((resolve27) => {
21223
+ const concatResult = await new Promise((resolve28) => {
20876
21224
  const ffmpeg = spawn4("ffmpeg", concatArgs);
20877
21225
  let stderr = "";
20878
21226
  ffmpeg.stderr.on("data", (d) => {
20879
21227
  stderr += d.toString();
20880
21228
  });
20881
21229
  ffmpeg.on("close", (code) => {
20882
- if (code === 0) resolve27({ success: true });
20883
- else resolve27({ success: false, error: `Chunk concat failed: ${stderr.slice(-400)}` });
21230
+ if (code === 0) resolve28({ success: true });
21231
+ else resolve28({ success: false, error: `Chunk concat failed: ${stderr.slice(-400)}` });
20884
21232
  });
20885
21233
  ffmpeg.on("error", (err) => {
20886
- resolve27({ success: false, error: `Chunk concat error: ${err.message}` });
21234
+ resolve28({ success: false, error: `Chunk concat error: ${err.message}` });
20887
21235
  });
20888
21236
  });
20889
21237
  if (!concatResult.success) {
@@ -20991,16 +21339,16 @@ function createFrameReorderBuffer(startFrame, endFrame) {
20991
21339
  }
20992
21340
  };
20993
21341
  return {
20994
- waitForFrame: (frame) => new Promise((resolve27) => {
20995
- waiters.push({ frame, resolve: resolve27 });
21342
+ waitForFrame: (frame) => new Promise((resolve28) => {
21343
+ waiters.push({ frame, resolve: resolve28 });
20996
21344
  resolveWaiters();
20997
21345
  }),
20998
21346
  advanceTo: (frame) => {
20999
21347
  nextFrame = frame;
21000
21348
  resolveWaiters();
21001
21349
  },
21002
- waitForAllDone: () => new Promise((resolve27) => {
21003
- waiters.push({ frame: endFrame, resolve: resolve27 });
21350
+ waitForAllDone: () => new Promise((resolve28) => {
21351
+ waiters.push({ frame: endFrame, resolve: resolve28 });
21004
21352
  resolveWaiters();
21005
21353
  })
21006
21354
  };
@@ -21129,7 +21477,7 @@ async function spawnStreamingEncoder(outputPath, options, signal, config) {
21129
21477
  let stderr = "";
21130
21478
  let exitCode = null;
21131
21479
  let exitPromiseResolve = null;
21132
- const exitPromise = new Promise((resolve27) => exitPromiseResolve = resolve27);
21480
+ const exitPromise = new Promise((resolve28) => exitPromiseResolve = resolve28);
21133
21481
  ffmpeg.stderr?.on("data", (data) => {
21134
21482
  stderr += data.toString();
21135
21483
  });
@@ -21173,8 +21521,8 @@ Process error: ${err.message}`;
21173
21521
  clearTimeout(timer);
21174
21522
  if (signal) signal.removeEventListener("abort", onAbort);
21175
21523
  if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
21176
- await new Promise((resolve27) => {
21177
- ffmpeg.stdin.end(() => resolve27());
21524
+ await new Promise((resolve28) => {
21525
+ ffmpeg.stdin.end(() => resolve28());
21178
21526
  });
21179
21527
  }
21180
21528
  await exitPromise;
@@ -21213,7 +21561,7 @@ var init_streamingEncoder = __esm({
21213
21561
  // ../engine/src/utils/ffprobe.ts
21214
21562
  import { spawn as spawn6 } from "child_process";
21215
21563
  function runFfprobe(args) {
21216
- return new Promise((resolve27, reject) => {
21564
+ return new Promise((resolve28, reject) => {
21217
21565
  const proc = spawn6("ffprobe", args);
21218
21566
  let stdout2 = "";
21219
21567
  let stderr = "";
@@ -21227,7 +21575,7 @@ function runFfprobe(args) {
21227
21575
  if (code !== 0) {
21228
21576
  reject(new Error(`[FFmpeg] ffprobe exited with code ${code}: ${stderr}`));
21229
21577
  } else {
21230
- resolve27(stdout2);
21578
+ resolve28(stdout2);
21231
21579
  }
21232
21580
  });
21233
21581
  proc.on("error", (err) => {
@@ -21524,7 +21872,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21524
21872
  ];
21525
21873
  if (format === "png") args.push("-compression_level", "6");
21526
21874
  args.push("-y", outputPattern);
21527
- return new Promise((resolve27, reject) => {
21875
+ return new Promise((resolve28, reject) => {
21528
21876
  const ffmpeg = spawn7("ffmpeg", args);
21529
21877
  let stderr = "";
21530
21878
  const onAbort = () => {
@@ -21559,7 +21907,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21559
21907
  files.forEach((file, index) => {
21560
21908
  framePaths.set(index, join19(videoOutputDir, file));
21561
21909
  });
21562
- resolve27({
21910
+ resolve28({
21563
21911
  videoId,
21564
21912
  srcPath: videoPath,
21565
21913
  outputDir: videoOutputDir,
@@ -21581,7 +21929,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21581
21929
  });
21582
21930
  });
21583
21931
  }
21584
- async function extractAllVideoFrames(videos, baseDir, options, signal, config) {
21932
+ async function extractAllVideoFrames(videos, baseDir, options, signal, config, compiledDir) {
21585
21933
  const startTime = Date.now();
21586
21934
  const extracted = [];
21587
21935
  const errors = [];
@@ -21594,7 +21942,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config) {
21594
21942
  try {
21595
21943
  let videoPath = video.src;
21596
21944
  if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
21597
- videoPath = join19(baseDir, videoPath);
21945
+ const fromCompiled = compiledDir ? join19(compiledDir, videoPath) : null;
21946
+ videoPath = fromCompiled && existsSync18(fromCompiled) ? fromCompiled : join19(baseDir, videoPath);
21598
21947
  }
21599
21948
  if (isHttpUrl(videoPath)) {
21600
21949
  const downloadDir = join19(options.outputDir, "_downloads");
@@ -22072,7 +22421,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
22072
22421
  tracksProcessed: tracks.length
22073
22422
  };
22074
22423
  }
22075
- async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config) {
22424
+ async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config, compiledDir) {
22076
22425
  const startMs = Date.now();
22077
22426
  const tracks = [];
22078
22427
  const errors = [];
@@ -22086,7 +22435,8 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
22086
22435
  try {
22087
22436
  let srcPath = element.src;
22088
22437
  if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
22089
- srcPath = join20(baseDir, srcPath);
22438
+ const fromCompiled = compiledDir ? join20(compiledDir, srcPath) : null;
22439
+ srcPath = fromCompiled && existsSync19(fromCompiled) ? fromCompiled : join20(baseDir, srcPath);
22090
22440
  }
22091
22441
  if (isHttpUrl(srcPath)) {
22092
22442
  try {
@@ -22099,7 +22449,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
22099
22449
  }
22100
22450
  }
22101
22451
  if (!existsSync19(srcPath)) {
22102
- errors.push(`Source not found: ${element.id}`);
22452
+ errors.push(`Source not found: ${element.id} (${element.src})`);
22103
22453
  return;
22104
22454
  }
22105
22455
  if (element.end - element.start <= 0) {
@@ -22460,11 +22810,11 @@ function createFileServer(options) {
22460
22810
  headers: { "Content-Type": contentType }
22461
22811
  });
22462
22812
  });
22463
- return new Promise((resolve27) => {
22813
+ return new Promise((resolve28) => {
22464
22814
  const server = serve({ fetch: app.fetch, port }, (info) => {
22465
22815
  const actualPort = info.port;
22466
22816
  const url = `http://localhost:${actualPort}`;
22467
- resolve27({
22817
+ resolve28({
22468
22818
  url,
22469
22819
  port: actualPort,
22470
22820
  close: () => server.close()
@@ -22578,9 +22928,9 @@ var init_src2 = __esm({
22578
22928
  });
22579
22929
 
22580
22930
  // ../core/src/compiler/htmlCompiler.ts
22581
- import { resolve as resolve7 } from "path";
22931
+ import { resolve as resolve8 } from "path";
22582
22932
  function resolveMediaSrc(src, projectDir) {
22583
- return src.startsWith("http://") || src.startsWith("https://") ? src : resolve7(projectDir, src);
22933
+ return src.startsWith("http://") || src.startsWith("https://") ? src : resolve8(projectDir, src);
22584
22934
  }
22585
22935
  async function compileHtml(rawHtml, projectDir, probeMediaDuration) {
22586
22936
  const { html: staticCompiled, unresolved } = compileTimingAttrs(rawHtml);
@@ -22659,7 +23009,7 @@ var init_staticGuard = __esm({
22659
23009
 
22660
23010
  // ../core/src/compiler/htmlBundler.ts
22661
23011
  import { readFileSync as readFileSync14, existsSync as existsSync22 } from "fs";
22662
- import { join as join23, resolve as resolve8, isAbsolute, sep as sep2 } from "path";
23012
+ import { join as join23, resolve as resolve9, isAbsolute, sep as sep2 } from "path";
22663
23013
  import { transformSync } from "esbuild";
22664
23014
  function parseHTMLContent(html) {
22665
23015
  const trimmed = html.trimStart().toLowerCase();
@@ -22669,9 +23019,9 @@ function parseHTMLContent(html) {
22669
23019
  return parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`).document;
22670
23020
  }
22671
23021
  function safePath(projectDir, relativePath) {
22672
- const resolved = resolve8(projectDir, relativePath);
22673
- const normalizedBase = resolve8(projectDir) + sep2;
22674
- if (!resolved.startsWith(normalizedBase) && resolved !== resolve8(projectDir)) return null;
23022
+ const resolved = resolve9(projectDir, relativePath);
23023
+ const normalizedBase = resolve9(projectDir) + sep2;
23024
+ if (!resolved.startsWith(normalizedBase) && resolved !== resolve9(projectDir)) return null;
22675
23025
  return resolved;
22676
23026
  }
22677
23027
  function stripEmbeddedRuntimeScripts2(html) {
@@ -23206,7 +23556,7 @@ var init_compiler = __esm({
23206
23556
  // ../producer/src/services/hyperframeRuntimeLoader.ts
23207
23557
  import { createHash as createHash2 } from "crypto";
23208
23558
  import { existsSync as existsSync23, readFileSync as readFileSync15 } from "fs";
23209
- import { dirname as dirname7, resolve as resolve9 } from "path";
23559
+ import { dirname as dirname7, resolve as resolve10 } from "path";
23210
23560
  import { fileURLToPath as fileURLToPath2 } from "url";
23211
23561
  function resolveHyperframeManifestPath() {
23212
23562
  if (process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH) {
@@ -23242,7 +23592,7 @@ function resolveVerifiedHyperframeRuntime() {
23242
23592
  `[HyperframeRuntimeLoader] Invalid manifest at ${manifestPath}; missing iife artifact or sha256.`
23243
23593
  );
23244
23594
  }
23245
- const runtimePath = resolve9(dirname7(manifestPath), runtimeFileName);
23595
+ const runtimePath = resolve10(dirname7(manifestPath), runtimeFileName);
23246
23596
  if (!existsSync23(runtimePath)) {
23247
23597
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
23248
23598
  }
@@ -23266,18 +23616,18 @@ var init_hyperframeRuntimeLoader = __esm({
23266
23616
  "../producer/src/services/hyperframeRuntimeLoader.ts"() {
23267
23617
  "use strict";
23268
23618
  PRODUCER_DIR = dirname7(fileURLToPath2(import.meta.url));
23269
- SIBLING_MANIFEST_PATH = resolve9(PRODUCER_DIR, "hyperframe.manifest.json");
23270
- MODULE_RELATIVE_MANIFEST_PATH = resolve9(
23619
+ SIBLING_MANIFEST_PATH = resolve10(PRODUCER_DIR, "hyperframe.manifest.json");
23620
+ MODULE_RELATIVE_MANIFEST_PATH = resolve10(
23271
23621
  PRODUCER_DIR,
23272
23622
  "../../../core/dist/hyperframe.manifest.json"
23273
23623
  );
23274
23624
  CWD_RELATIVE_MANIFEST_PATHS = [
23275
23625
  // When bundled to a single file (dist/public-server.js), the manifest
23276
23626
  // is copied as a sibling by build.mjs
23277
- resolve9(PRODUCER_DIR, "hyperframe.manifest.json"),
23278
- resolve9(process.cwd(), "packages/core/dist/hyperframe.manifest.json"),
23279
- resolve9(process.cwd(), "../core/dist/hyperframe.manifest.json"),
23280
- resolve9(process.cwd(), "core/dist/hyperframe.manifest.json")
23627
+ resolve10(PRODUCER_DIR, "hyperframe.manifest.json"),
23628
+ resolve10(process.cwd(), "packages/core/dist/hyperframe.manifest.json"),
23629
+ resolve10(process.cwd(), "../core/dist/hyperframe.manifest.json"),
23630
+ resolve10(process.cwd(), "core/dist/hyperframe.manifest.json")
23281
23631
  ];
23282
23632
  }
23283
23633
  });
@@ -23380,10 +23730,10 @@ function createFileServer2(options) {
23380
23730
  headers: { "Content-Type": contentType }
23381
23731
  });
23382
23732
  });
23383
- return new Promise((resolve27) => {
23733
+ return new Promise((resolve28) => {
23384
23734
  const connections = /* @__PURE__ */ new Set();
23385
23735
  const server = serve2({ fetch: app.fetch, port }, (info) => {
23386
- resolve27({
23736
+ resolve28({
23387
23737
  url: `http://localhost:${info.port}`,
23388
23738
  port: info.port,
23389
23739
  close: () => {
@@ -23906,7 +24256,7 @@ var init_deterministicFonts = __esm({
23906
24256
 
23907
24257
  // ../producer/src/services/htmlCompiler.ts
23908
24258
  import { readFileSync as readFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync15 } from "fs";
23909
- import { join as join25, dirname as dirname8, resolve as resolve10 } from "path";
24259
+ import { join as join25, dirname as dirname8, resolve as resolve11 } from "path";
23910
24260
  import postcss from "postcss";
23911
24261
  function dedupeElementsById(elements) {
23912
24262
  const deduped = /* @__PURE__ */ new Map();
@@ -23991,7 +24341,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
23991
24341
  const elEnd = elEndRaw ? parseFloat(elEndRaw) : Infinity;
23992
24342
  const absoluteStart = parentOffset + elStart;
23993
24343
  const absoluteEnd = Math.min(parentEnd, isFinite(elEnd) ? parentOffset + elEnd : Infinity);
23994
- const filePath = resolve10(projectDir, srcPath);
24344
+ const filePath = resolve11(projectDir, srcPath);
23995
24345
  if (visited.has(filePath)) {
23996
24346
  continue;
23997
24347
  }
@@ -24191,7 +24541,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
24191
24541
  if (!srcPath) continue;
24192
24542
  let compHtml = subCompositions.get(srcPath) || null;
24193
24543
  if (!compHtml) {
24194
- const filePath = resolve10(projectDir, srcPath);
24544
+ const filePath = resolve11(projectDir, srcPath);
24195
24545
  if (existsSync25(filePath)) {
24196
24546
  compHtml = readFileSync17(filePath, "utf-8");
24197
24547
  }
@@ -24404,7 +24754,7 @@ ${safeText}
24404
24754
  return result;
24405
24755
  }
24406
24756
  function collectExternalAssets(html, projectDir) {
24407
- const absProjectDir = resolve10(projectDir);
24757
+ const absProjectDir = resolve11(projectDir);
24408
24758
  const externalAssets = /* @__PURE__ */ new Map();
24409
24759
  const CSS_URL_RE2 = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
24410
24760
  function processPath(rawPath) {
@@ -24412,7 +24762,7 @@ function collectExternalAssets(html, projectDir) {
24412
24762
  if (!trimmed || trimmed.startsWith("/") || trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("//") || trimmed.startsWith("data:") || trimmed.startsWith("#")) {
24413
24763
  return null;
24414
24764
  }
24415
- const absPath = resolve10(absProjectDir, trimmed);
24765
+ const absPath = resolve11(absProjectDir, trimmed);
24416
24766
  if (absPath.startsWith(absProjectDir + "/") || absPath === absProjectDir) {
24417
24767
  return null;
24418
24768
  }
@@ -24489,7 +24839,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
24489
24839
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
24490
24840
  for (const video of videos) {
24491
24841
  if (isHttpUrl(video.src)) continue;
24492
- const videoPath = resolve10(projectDir, video.src);
24842
+ const videoPath = resolve11(projectDir, video.src);
24493
24843
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
24494
24844
  Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
24495
24845
  if (analysis.isProblematic) {
@@ -24682,7 +25032,7 @@ import {
24682
25032
  copyFileSync as copyFileSync2,
24683
25033
  appendFileSync
24684
25034
  } from "fs";
24685
- import { join as join26, dirname as dirname9, resolve as resolve11 } from "path";
25035
+ import { join as join26, dirname as dirname9, resolve as resolve12 } from "path";
24686
25036
  import { randomUUID as randomUUID2 } from "crypto";
24687
25037
  import { freemem as freemem2 } from "os";
24688
25038
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -24747,7 +25097,7 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
24747
25097
  writeFileSync8(outPath, html, "utf-8");
24748
25098
  }
24749
25099
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
24750
- const outPath = resolve11(join26(compileDir, relativePath));
25100
+ const outPath = resolve12(join26(compileDir, relativePath));
24751
25101
  if (!outPath.startsWith(compileDir + "/")) {
24752
25102
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
24753
25103
  continue;
@@ -24820,7 +25170,7 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
24820
25170
  }
24821
25171
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
24822
25172
  const moduleDir = dirname9(fileURLToPath3(import.meta.url));
24823
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve11(process.env.PRODUCER_RENDERS_DIR, "..") : resolve11(moduleDir, "../..");
25173
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve12(process.env.PRODUCER_RENDERS_DIR, "..") : resolve12(moduleDir, "../..");
24824
25174
  const debugDir = join26(producerRoot, ".debug");
24825
25175
  const workDir = job.config.debug ? join26(debugDir, job.id) : join26(dirname9(outputPath), `work-${job.id}`);
24826
25176
  const pipelineStart = Date.now();
@@ -25097,12 +25447,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25097
25447
  const stage2Start = Date.now();
25098
25448
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
25099
25449
  let frameLookup = null;
25450
+ const compiledDir = join26(workDir, "compiled");
25100
25451
  if (composition.videos.length > 0) {
25101
25452
  const extractionResult = await extractAllVideoFrames(
25102
25453
  composition.videos,
25103
25454
  projectDir,
25104
25455
  { fps: job.config.fps, outputDir: join26(workDir, "video-frames") },
25105
- abortSignal
25456
+ abortSignal,
25457
+ void 0,
25458
+ compiledDir
25106
25459
  );
25107
25460
  assertNotAborted();
25108
25461
  if (extractionResult.extracted.length > 0) {
@@ -25142,7 +25495,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25142
25495
  join26(workDir, "audio-work"),
25143
25496
  audioOutputPath,
25144
25497
  job.duration,
25145
- abortSignal
25498
+ abortSignal,
25499
+ void 0,
25500
+ compiledDir
25146
25501
  );
25147
25502
  assertNotAborted();
25148
25503
  hasAudio = audioResult.success;
@@ -25589,7 +25944,7 @@ var init_config3 = __esm({
25589
25944
 
25590
25945
  // ../producer/src/services/hyperframeLint.ts
25591
25946
  import { existsSync as existsSync27, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
25592
- import { resolve as resolve12, join as join27 } from "path";
25947
+ import { resolve as resolve13, join as join27 } from "path";
25593
25948
  function isStringRecord(value) {
25594
25949
  if (!value || typeof value !== "object" || Array.isArray(value)) {
25595
25950
  return false;
@@ -25616,7 +25971,7 @@ function pickEntryFile(files, preferredEntryFile) {
25616
25971
  return null;
25617
25972
  }
25618
25973
  function readProjectEntryFile(projectDir, preferredEntryFile) {
25619
- const absProjectDir = resolve12(projectDir);
25974
+ const absProjectDir = resolve13(projectDir);
25620
25975
  if (!existsSync27(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
25621
25976
  return { error: `Project directory not found: ${absProjectDir}` };
25622
25977
  }
@@ -25624,7 +25979,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
25624
25979
  (value) => typeof value === "string" && value.trim().length > 0
25625
25980
  );
25626
25981
  for (const entryFile of entryCandidates) {
25627
- const absoluteEntryPath = resolve12(absProjectDir, entryFile);
25982
+ const absoluteEntryPath = resolve13(absProjectDir, entryFile);
25628
25983
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
25629
25984
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
25630
25985
  }
@@ -25684,19 +26039,59 @@ var init_hyperframeLint = __esm({
25684
26039
  });
25685
26040
 
25686
26041
  // ../producer/src/utils/paths.ts
25687
- import { resolve as resolve13, basename, join as join28 } from "path";
26042
+ import { resolve as resolve14, basename, join as join28 } from "path";
25688
26043
  function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
25689
- const absoluteProjectDir = resolve13(projectDir);
26044
+ const absoluteProjectDir = resolve14(projectDir);
25690
26045
  const projectName = basename(absoluteProjectDir);
25691
26046
  const resolvedOutputPath = outputPath ?? join28(rendersDir, `${projectName}.mp4`);
25692
- const absoluteOutputPath = resolve13(resolvedOutputPath);
26047
+ const absoluteOutputPath = resolve14(resolvedOutputPath);
25693
26048
  return { absoluteProjectDir, absoluteOutputPath };
25694
26049
  }
25695
26050
  var DEFAULT_RENDERS_DIR;
25696
26051
  var init_paths = __esm({
25697
26052
  "../producer/src/utils/paths.ts"() {
25698
26053
  "use strict";
25699
- DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve13(new URL(import.meta.url).pathname, "../../..", "renders");
26054
+ DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve14(new URL(import.meta.url).pathname, "../../..", "renders");
26055
+ }
26056
+ });
26057
+
26058
+ // ../producer/src/utils/semaphore.ts
26059
+ var Semaphore;
26060
+ var init_semaphore = __esm({
26061
+ "../producer/src/utils/semaphore.ts"() {
26062
+ "use strict";
26063
+ Semaphore = class {
26064
+ constructor(maxConcurrent) {
26065
+ this.maxConcurrent = maxConcurrent;
26066
+ }
26067
+ queue = [];
26068
+ active = 0;
26069
+ async acquire() {
26070
+ if (this.active < this.maxConcurrent) {
26071
+ this.active++;
26072
+ return () => this.release();
26073
+ }
26074
+ return new Promise((resolve28) => {
26075
+ this.queue.push(() => {
26076
+ this.active++;
26077
+ resolve28(() => this.release());
26078
+ });
26079
+ });
26080
+ }
26081
+ release() {
26082
+ this.active--;
26083
+ const next = this.queue.shift();
26084
+ if (next) next();
26085
+ }
26086
+ /** Current number of active slots. */
26087
+ get activeCount() {
26088
+ return this.active;
26089
+ }
26090
+ /** Number of waiters in the queue. */
26091
+ get waitingCount() {
26092
+ return this.queue.length;
26093
+ }
26094
+ };
25700
26095
  }
25701
26096
  });
25702
26097
 
@@ -25710,7 +26105,7 @@ import {
25710
26105
  rmSync as rmSync7,
25711
26106
  createReadStream
25712
26107
  } from "fs";
25713
- import { resolve as resolve14, dirname as dirname10, join as join29 } from "path";
26108
+ import { resolve as resolve15, dirname as dirname10, join as join29 } from "path";
25714
26109
  import { tmpdir as tmpdir2 } from "os";
25715
26110
  import { parseArgs as parseArgs2 } from "util";
25716
26111
  import crypto from "crypto";
@@ -25732,12 +26127,12 @@ async function prepareRenderBody(body) {
25732
26127
  const options = parseRenderOptions(body);
25733
26128
  const projectDir = typeof body.projectDir === "string" ? body.projectDir : void 0;
25734
26129
  if (projectDir) {
25735
- const absProjectDir = resolve14(projectDir);
26130
+ const absProjectDir = resolve15(projectDir);
25736
26131
  if (!existsSync28(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
25737
26132
  return { error: `Project directory not found: ${absProjectDir}` };
25738
26133
  }
25739
26134
  const entry = options.entryFile || "index.html";
25740
- if (!existsSync28(resolve14(absProjectDir, entry))) {
26135
+ if (!existsSync28(resolve15(absProjectDir, entry))) {
25741
26136
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
25742
26137
  }
25743
26138
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -25778,7 +26173,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
25778
26173
  try {
25779
26174
  return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
25780
26175
  } catch (error) {
25781
- const fallbackPath = resolve14(rendersDir, `producer-fallback-${Date.now()}.mp4`);
26176
+ const fallbackPath = resolve15(rendersDir, `producer-fallback-${Date.now()}.mp4`);
25782
26177
  log.warn("Failed to resolve output path, using fallback", {
25783
26178
  fallback: fallbackPath,
25784
26179
  error: error instanceof Error ? error.message : String(error)
@@ -25829,6 +26224,8 @@ function createRenderHandlers(options = {}) {
25829
26224
  const rendersDir = options.rendersDir ?? process.env.PRODUCER_RENDERS_DIR ?? "/tmp";
25830
26225
  const artifactTtlMs = options.artifactTtlMs ?? Number(process.env.PRODUCER_OUTPUT_ARTIFACT_TTL_MS || 15 * 60 * 1e3);
25831
26226
  const store = createArtifactStore(artifactTtlMs);
26227
+ const maxConcurrentRenders = options.maxConcurrentRenders ?? Number(process.env.PRODUCER_MAX_CONCURRENT_RENDERS || 2);
26228
+ const renderSemaphore = new Semaphore(maxConcurrentRenders);
25832
26229
  const startTime = Date.now();
25833
26230
  const health = (c2) => c2.json({
25834
26231
  status: "ok",
@@ -25885,6 +26282,7 @@ function createRenderHandlers(options = {}) {
25885
26282
  );
25886
26283
  const outputDir = dirname10(absoluteOutputPath);
25887
26284
  if (!existsSync28(outputDir)) mkdirSync17(outputDir, { recursive: true });
26285
+ const release2 = await renderSemaphore.acquire();
25888
26286
  log.info("render started", {
25889
26287
  requestId,
25890
26288
  projectDir: input.projectDir,
@@ -25952,6 +26350,7 @@ function createRenderHandlers(options = {}) {
25952
26350
  500
25953
26351
  );
25954
26352
  } finally {
26353
+ release2();
25955
26354
  cleanupTempDir(cleanupProjectDir, log);
25956
26355
  }
25957
26356
  };
@@ -26008,6 +26407,16 @@ function createRenderHandlers(options = {}) {
26008
26407
  const abortController = new AbortController();
26009
26408
  const onRequestAbort = () => abortController.abort(new RenderCancelledError("request_aborted"));
26010
26409
  c2.req.raw.signal.addEventListener("abort", onRequestAbort, { once: true });
26410
+ if (renderSemaphore.activeCount >= maxConcurrentRenders) {
26411
+ await stream.writeSSE({
26412
+ data: JSON.stringify({
26413
+ type: "queued",
26414
+ requestId,
26415
+ position: renderSemaphore.waitingCount
26416
+ })
26417
+ });
26418
+ }
26419
+ const release2 = await renderSemaphore.acquire();
26011
26420
  try {
26012
26421
  await executeRenderJob(
26013
26422
  job,
@@ -26075,6 +26484,7 @@ function createRenderHandlers(options = {}) {
26075
26484
  })
26076
26485
  });
26077
26486
  } finally {
26487
+ release2();
26078
26488
  c2.req.raw.signal.removeEventListener("abort", onRequestAbort);
26079
26489
  cleanupTempDir(cleanupProjectDir, log);
26080
26490
  }
@@ -26099,7 +26509,12 @@ function createRenderHandlers(options = {}) {
26099
26509
  }
26100
26510
  });
26101
26511
  };
26102
- return { render: render2, renderStream, lint, health, outputs };
26512
+ const queue = (c2) => c2.json({
26513
+ maxConcurrentRenders,
26514
+ activeRenders: renderSemaphore.activeCount,
26515
+ queuedRenders: renderSemaphore.waitingCount
26516
+ });
26517
+ return { render: render2, renderStream, lint, health, outputs, queue };
26103
26518
  }
26104
26519
  function createProducerApp(options = {}) {
26105
26520
  const app = new Hono4();
@@ -26107,6 +26522,7 @@ function createProducerApp(options = {}) {
26107
26522
  app.get("/health", handlers.health);
26108
26523
  app.post("/render", handlers.render);
26109
26524
  app.post("/render/stream", handlers.renderStream);
26525
+ app.get("/render/queue", handlers.queue);
26110
26526
  app.post("/lint", handlers.lint);
26111
26527
  app.get("/outputs/:token", handlers.outputs);
26112
26528
  return app;
@@ -26144,7 +26560,8 @@ var init_server = __esm({
26144
26560
  init_hyperframeLint();
26145
26561
  init_paths();
26146
26562
  init_logger();
26147
- entryScript = process.argv[1] ? resolve14(process.argv[1]) : "";
26563
+ init_semaphore();
26564
+ entryScript = process.argv[1] ? resolve15(process.argv[1]) : "";
26148
26565
  isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
26149
26566
  if (isPublicServerEntry) {
26150
26567
  const { values } = parseArgs2({
@@ -26217,18 +26634,18 @@ __export(studioServer_exports, {
26217
26634
  import { Hono as Hono5 } from "hono";
26218
26635
  import { streamSSE as streamSSE3 } from "hono/streaming";
26219
26636
  import { existsSync as existsSync29, readFileSync as readFileSync20, writeFileSync as writeFileSync10, statSync as statSync10 } from "fs";
26220
- import { resolve as resolve15, join as join30, basename as basename2 } from "path";
26637
+ import { resolve as resolve16, join as join30, basename as basename2 } from "path";
26221
26638
  function resolveDistDir() {
26222
- const builtPath = resolve15(__dirname, "studio");
26223
- if (existsSync29(resolve15(builtPath, "index.html"))) return builtPath;
26224
- const devPath = resolve15(__dirname, "..", "..", "..", "studio", "dist");
26225
- if (existsSync29(resolve15(devPath, "index.html"))) return devPath;
26639
+ const builtPath = resolve16(__dirname, "studio");
26640
+ if (existsSync29(resolve16(builtPath, "index.html"))) return builtPath;
26641
+ const devPath = resolve16(__dirname, "..", "..", "..", "studio", "dist");
26642
+ if (existsSync29(resolve16(devPath, "index.html"))) return devPath;
26226
26643
  return builtPath;
26227
26644
  }
26228
26645
  function resolveRuntimePath() {
26229
- const builtPath = resolve15(__dirname, "hyperframe-runtime.js");
26646
+ const builtPath = resolve16(__dirname, "hyperframe-runtime.js");
26230
26647
  if (existsSync29(builtPath)) return builtPath;
26231
- const devPath = resolve15(
26648
+ const devPath = resolve16(
26232
26649
  __dirname,
26233
26650
  "..",
26234
26651
  "..",
@@ -26379,6 +26796,14 @@ function createStudioServer(options) {
26379
26796
  }
26380
26797
  };
26381
26798
  const app = new Hono5();
26799
+ app.get("/__hyperframes_config", (c2) => {
26800
+ return c2.json({
26801
+ isHyperframes: true,
26802
+ projectName: projectId,
26803
+ projectDir,
26804
+ version: VERSION
26805
+ });
26806
+ });
26382
26807
  app.get("/api/runtime.js", (c2) => {
26383
26808
  if (!existsSync29(runtimePath)) return c2.text("runtime not built", 404);
26384
26809
  return c2.body(readFileSync20(runtimePath, "utf-8"), 200, {
@@ -26412,7 +26837,7 @@ function createStudioServer(options) {
26412
26837
  return api.fetch(forwardReq);
26413
26838
  });
26414
26839
  app.get("/assets/*", (c2) => {
26415
- const filePath = resolve15(studioDir, c2.req.path.slice(1));
26840
+ const filePath = resolve16(studioDir, c2.req.path.slice(1));
26416
26841
  if (!existsSync29(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
26417
26842
  const content = readFileSync20(filePath);
26418
26843
  return new Response(content, {
@@ -26420,7 +26845,7 @@ function createStudioServer(options) {
26420
26845
  });
26421
26846
  });
26422
26847
  app.get("/icons/*", (c2) => {
26423
- const filePath = resolve15(studioDir, c2.req.path.slice(1));
26848
+ const filePath = resolve16(studioDir, c2.req.path.slice(1));
26424
26849
  if (!existsSync29(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
26425
26850
  const content = readFileSync20(filePath);
26426
26851
  return new Response(content, {
@@ -26428,7 +26853,7 @@ function createStudioServer(options) {
26428
26853
  });
26429
26854
  });
26430
26855
  app.get("*", (c2) => {
26431
- const indexPath = resolve15(studioDir, "index.html");
26856
+ const indexPath = resolve16(studioDir, "index.html");
26432
26857
  if (!existsSync29(indexPath)) {
26433
26858
  return c2.text("Studio not found. Rebuild with: pnpm run build", 500);
26434
26859
  }
@@ -26441,6 +26866,7 @@ var init_studioServer = __esm({
26441
26866
  "src/server/studioServer.ts"() {
26442
26867
  "use strict";
26443
26868
  init_fileWatcher();
26869
+ init_version();
26444
26870
  init_studio_api();
26445
26871
  _thumbnailBrowser = null;
26446
26872
  _thumbnailBrowserInitializing = null;
@@ -26455,45 +26881,12 @@ __export(preview_exports, {
26455
26881
  });
26456
26882
  import { spawn as spawn8 } from "child_process";
26457
26883
  import { existsSync as existsSync30, lstatSync, symlinkSync, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync18 } from "fs";
26458
- import { resolve as resolve16, dirname as dirname11, basename as basename3, join as join31 } from "path";
26884
+ import { resolve as resolve17, dirname as dirname11, basename as basename3, join as join31 } from "path";
26459
26885
  import { fileURLToPath as fileURLToPath4 } from "url";
26460
26886
  import { createRequire } from "module";
26461
- async function serveWithPortFallback(fetch3, startPort, maxAttempts = 10) {
26462
- const { createAdaptorServer } = await import("@hono/node-server");
26463
- const server = createAdaptorServer({ fetch: fetch3 });
26464
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
26465
- const port = startPort + attempt;
26466
- try {
26467
- await new Promise((resolveListener, rejectListener) => {
26468
- const onError = (err) => {
26469
- server.removeListener("listening", onListening);
26470
- rejectListener(err);
26471
- };
26472
- const onListening = () => {
26473
- server.removeListener("error", onError);
26474
- resolveListener();
26475
- };
26476
- server.once("error", onError);
26477
- server.once("listening", onListening);
26478
- server.listen(port);
26479
- });
26480
- return { server, port };
26481
- } catch (err) {
26482
- const code = err.code;
26483
- if (code === "EADDRINUSE") {
26484
- continue;
26485
- }
26486
- throw err;
26487
- }
26488
- }
26489
- const lastPort = startPort + maxAttempts - 1;
26490
- throw new Error(
26491
- `Ports ${startPort}\u2013${lastPort} are all in use. Use --port to specify a different port.`
26492
- );
26493
- }
26494
26887
  async function runDevMode(dir, projectName) {
26495
26888
  const thisFile = fileURLToPath4(import.meta.url);
26496
- const repoRoot = resolve16(dirname11(thisFile), "..", "..", "..", "..");
26889
+ const repoRoot = resolve17(dirname11(thisFile), "..", "..", "..", "..");
26497
26890
  const projectsDir = join31(repoRoot, "packages", "studio", "data", "projects");
26498
26891
  const pName = projectName ?? basename3(dir);
26499
26892
  const symlinkPath = join31(projectsDir, pName);
@@ -26505,7 +26898,7 @@ async function runDevMode(dir, projectName) {
26505
26898
  const stat = lstatSync(symlinkPath);
26506
26899
  if (stat.isSymbolicLink()) {
26507
26900
  const target = readlinkSync(symlinkPath);
26508
- if (resolve16(target) !== resolve16(dir)) {
26901
+ if (resolve17(target) !== resolve17(dir)) {
26509
26902
  unlinkSync5(symlinkPath);
26510
26903
  }
26511
26904
  }
@@ -26559,8 +26952,8 @@ async function runDevMode(dir, projectName) {
26559
26952
  }
26560
26953
  });
26561
26954
  }
26562
- return new Promise((resolve27) => {
26563
- child.on("close", () => resolve27());
26955
+ return new Promise((resolve28) => {
26956
+ child.on("close", () => resolve28());
26564
26957
  });
26565
26958
  }
26566
26959
  function hasLocalStudio(dir) {
@@ -26582,7 +26975,7 @@ async function runLocalStudioMode(dir, projectName) {
26582
26975
  let createdSymlink = false;
26583
26976
  if (dir !== symlinkPath) {
26584
26977
  if (existsSync30(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
26585
- if (resolve16(readlinkSync(symlinkPath)) !== resolve16(dir)) {
26978
+ if (resolve17(readlinkSync(symlinkPath)) !== resolve17(dir)) {
26586
26979
  unlinkSync5(symlinkPath);
26587
26980
  }
26588
26981
  }
@@ -26630,20 +27023,20 @@ async function runLocalStudioMode(dir, projectName) {
26630
27023
  }
26631
27024
  });
26632
27025
  }
26633
- return new Promise((resolve27) => {
26634
- child.on("close", () => resolve27());
27026
+ return new Promise((resolve28) => {
27027
+ child.on("close", () => resolve28());
26635
27028
  });
26636
27029
  }
26637
- async function runEmbeddedMode(dir, startPort, projectName) {
27030
+ async function runEmbeddedMode(dir, startPort, projectName, forceNew = false) {
26638
27031
  const { createStudioServer: createStudioServer2 } = await Promise.resolve().then(() => (init_studioServer(), studioServer_exports));
26639
27032
  const pName = projectName ?? basename3(dir);
26640
27033
  const { app } = createStudioServer2({ projectDir: dir, projectName: pName });
26641
27034
  Wt2(c.bold("hyperframes preview"));
26642
27035
  const s = be();
26643
27036
  s.start("Starting studio...");
26644
- let actualPort;
27037
+ let result;
26645
27038
  try {
26646
- ({ port: actualPort } = await serveWithPortFallback(app.fetch, startPort));
27039
+ result = await findPortAndServe(app.fetch, startPort, dir, forceNew);
26647
27040
  } catch (err) {
26648
27041
  s.stop(c.error("Failed to start studio"));
26649
27042
  console.error();
@@ -26652,11 +27045,26 @@ async function runEmbeddedMode(dir, startPort, projectName) {
26652
27045
  process.exitCode = 1;
26653
27046
  return;
26654
27047
  }
26655
- const url = `http://localhost:${actualPort}`;
27048
+ if (result.type === "already-running") {
27049
+ const url2 = `http://localhost:${result.port}`;
27050
+ s.stop(c.success("Already running"));
27051
+ console.log();
27052
+ console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
27053
+ console.log(` ${c.dim("Studio")} ${c.accent(url2)}`);
27054
+ console.log();
27055
+ console.log(
27056
+ ` ${c.dim("Reusing existing server. Use --force-new to start a fresh instance.")}`
27057
+ );
27058
+ console.log();
27059
+ import("open").then((mod) => mod.default(`${url2}#project/${pName}`)).catch(() => {
27060
+ });
27061
+ return;
27062
+ }
27063
+ const url = `http://localhost:${result.port}`;
26656
27064
  s.stop(c.success("Studio running"));
26657
27065
  console.log();
26658
- if (actualPort !== startPort) {
26659
- console.log(` ${c.warn(`Port ${startPort} is in use, using ${actualPort} instead`)}`);
27066
+ if (result.port !== startPort) {
27067
+ console.log(` ${c.warn(`Port ${startPort} is in use, using ${result.port} instead`)}`);
26660
27068
  console.log();
26661
27069
  }
26662
27070
  console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
@@ -26682,21 +27090,72 @@ var init_preview2 = __esm({
26682
27090
  init_env();
26683
27091
  init_lintProject();
26684
27092
  init_lintFormat();
27093
+ init_portUtils();
26685
27094
  examples = [
26686
27095
  ["Preview the current project", "hyperframes preview"],
26687
27096
  ["Preview a specific project directory", "hyperframes preview ./my-video"],
26688
- ["Use a custom port", "hyperframes preview --port 8080"]
27097
+ ["Use a custom port", "hyperframes preview --port 8080"],
27098
+ ["Force a new server even if one is already running", "hyperframes preview --force-new"],
27099
+ ["List all active preview servers", "hyperframes preview --list"],
27100
+ ["Kill all active preview servers", "hyperframes preview --kill-all"]
26689
27101
  ];
26690
27102
  preview_default = defineCommand({
26691
27103
  meta: { name: "preview", description: "Start the studio for previewing compositions" },
26692
27104
  args: {
26693
27105
  dir: { type: "positional", description: "Project directory", required: false },
26694
- port: { type: "string", description: "Port to run the preview server on", default: "3002" }
27106
+ port: { type: "string", description: "Port to run the preview server on", default: "3002" },
27107
+ "force-new": {
27108
+ type: "boolean",
27109
+ description: "Start a new server even if one is already running for this project",
27110
+ default: false
27111
+ },
27112
+ list: {
27113
+ type: "boolean",
27114
+ description: "List all active preview servers and exit",
27115
+ default: false
27116
+ },
27117
+ "kill-all": {
27118
+ type: "boolean",
27119
+ description: "Kill all active preview servers and exit",
27120
+ default: false
27121
+ }
26695
27122
  },
26696
27123
  async run({ args }) {
26697
- const rawArg = args.dir;
26698
- const dir = resolve16(rawArg ?? ".");
26699
27124
  const startPort = parseInt(args.port ?? "3002", 10);
27125
+ if (args.list) {
27126
+ const servers = await scanActiveServers(startPort);
27127
+ if (servers.length === 0) {
27128
+ console.log("\n No active preview servers found.\n");
27129
+ return;
27130
+ }
27131
+ console.log(`
27132
+ ${c.bold("Active preview servers:")}
27133
+ `);
27134
+ for (const s of servers) {
27135
+ const pidStr = s.pid ? c.dim(` (PID ${s.pid})`) : "";
27136
+ console.log(
27137
+ ` ${c.accent(`Port ${s.port}`)} ${s.projectName} ${c.dim(s.projectDir)}${pidStr}`
27138
+ );
27139
+ }
27140
+ console.log(`
27141
+ ${servers.length} server${servers.length === 1 ? "" : "s"} running.
27142
+ `);
27143
+ return;
27144
+ }
27145
+ if (args["kill-all"]) {
27146
+ const servers = await scanActiveServers(startPort);
27147
+ if (servers.length === 0) {
27148
+ console.log("\n No active preview servers to kill.\n");
27149
+ return;
27150
+ }
27151
+ const killed = await killActiveServers(startPort);
27152
+ console.log(`
27153
+ Killed ${killed} preview server${killed === 1 ? "" : "s"}.
27154
+ `);
27155
+ return;
27156
+ }
27157
+ const rawArg = args.dir;
27158
+ const dir = resolve17(rawArg ?? ".");
26700
27159
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
26701
27160
  const projectName = isImplicitCwd ? basename3(process.env.PWD ?? dir) : basename3(dir);
26702
27161
  const indexPath = join31(dir, "index.html");
@@ -26715,7 +27174,8 @@ var init_preview2 = __esm({
26715
27174
  if (hasLocalStudio(dir)) {
26716
27175
  return runLocalStudioMode(dir, projectName);
26717
27176
  }
26718
- return runEmbeddedMode(dir, startPort, projectName);
27177
+ const forceNew = !!args["force-new"];
27178
+ return runEmbeddedMode(dir, startPort, projectName, forceNew);
26719
27179
  }
26720
27180
  });
26721
27181
  }
@@ -26736,7 +27196,7 @@ import {
26736
27196
  readFileSync as readFileSync21,
26737
27197
  readdirSync as readdirSync10
26738
27198
  } from "fs";
26739
- import { resolve as resolve17, basename as basename4, join as join32, dirname as dirname12 } from "path";
27199
+ import { resolve as resolve18, basename as basename4, join as join32, dirname as dirname12 } from "path";
26740
27200
  import { fileURLToPath as fileURLToPath5 } from "url";
26741
27201
  import { execFileSync as execFileSync4, spawn as spawn9 } from "child_process";
26742
27202
  function probeVideo(filePath) {
@@ -26806,8 +27266,8 @@ function transcodeToMp4(inputPath, outputPath) {
26806
27266
  }
26807
27267
  function resolveAssetDir(devSegments, builtSegments) {
26808
27268
  const base = dirname12(fileURLToPath5(import.meta.url));
26809
- const devPath = resolve17(base, ...devSegments);
26810
- const builtPath = resolve17(base, ...builtSegments);
27269
+ const devPath = resolve18(base, ...devSegments);
27270
+ const builtPath = resolve18(base, ...builtSegments);
26811
27271
  return existsSync31(devPath) ? devPath : builtPath;
26812
27272
  }
26813
27273
  function getStaticTemplateDir(templateId) {
@@ -26892,7 +27352,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26892
27352
  }
26893
27353
  if (shouldTranscode) {
26894
27354
  const mp4Name = localVideoName.replace(/\.[^.]+$/, ".mp4");
26895
- const mp4Path = resolve17(destDir, mp4Name);
27355
+ const mp4Path = resolve18(destDir, mp4Name);
26896
27356
  const spin = be();
26897
27357
  spin.start("Transcoding to H.264 MP4...");
26898
27358
  const ok = await transcodeToMp4(videoPath, mp4Path);
@@ -26901,10 +27361,10 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26901
27361
  localVideoName = mp4Name;
26902
27362
  } else {
26903
27363
  spin.stop(c.warn("Transcode failed \u2014 copying original file"));
26904
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27364
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26905
27365
  }
26906
27366
  } else {
26907
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27367
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26908
27368
  }
26909
27369
  } else {
26910
27370
  if (interactive) {
@@ -26914,10 +27374,10 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26914
27374
  console.log(c.warn("ffmpeg not installed \u2014 cannot transcode. Copying original."));
26915
27375
  console.log(c.dim("Install: ") + c.accent("brew install ffmpeg"));
26916
27376
  }
26917
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27377
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26918
27378
  }
26919
27379
  } else {
26920
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27380
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26921
27381
  }
26922
27382
  return { meta, localVideoName };
26923
27383
  }
@@ -26931,7 +27391,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
26931
27391
  }
26932
27392
  patchVideoSrc(destDir, localVideoName, durationSeconds);
26933
27393
  writeFileSync11(
26934
- resolve17(destDir, "meta.json"),
27394
+ resolve18(destDir, "meta.json"),
26935
27395
  JSON.stringify(
26936
27396
  {
26937
27397
  id: name,
@@ -26947,7 +27407,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
26947
27407
  if (existsSync31(sharedDir)) {
26948
27408
  for (const entry of readdirSync10(sharedDir, { withFileTypes: true })) {
26949
27409
  const src = join32(sharedDir, entry.name);
26950
- const dest = resolve17(destDir, entry.name);
27410
+ const dest = resolve18(destDir, entry.name);
26951
27411
  if (entry.isFile() || entry.isSymbolicLink()) {
26952
27412
  copyFileSync3(src, dest);
26953
27413
  }
@@ -27039,7 +27499,7 @@ var init_init = __esm({
27039
27499
  if (!interactive) {
27040
27500
  const templateId2 = templateFlag ?? "blank";
27041
27501
  const name2 = args.name ?? "my-video";
27042
- const destDir2 = resolve17(name2);
27502
+ const destDir2 = resolve18(name2);
27043
27503
  if (existsSync31(destDir2) && readdirSync10(destDir2).length > 0) {
27044
27504
  console.error(c.error(`Directory already exists and is not empty: ${name2}`));
27045
27505
  process.exit(1);
@@ -27053,7 +27513,7 @@ var init_init = __esm({
27053
27513
  process.exit(1);
27054
27514
  }
27055
27515
  if (videoFlag) {
27056
- const videoPath = resolve17(videoFlag);
27516
+ const videoPath = resolve18(videoFlag);
27057
27517
  if (!existsSync31(videoPath)) {
27058
27518
  console.error(c.error(`Video file not found: ${videoFlag}`));
27059
27519
  process.exit(1);
@@ -27067,13 +27527,13 @@ var init_init = __esm({
27067
27527
  );
27068
27528
  }
27069
27529
  if (audioFlag) {
27070
- const audioPath = resolve17(audioFlag);
27530
+ const audioPath = resolve18(audioFlag);
27071
27531
  if (!existsSync31(audioPath)) {
27072
27532
  console.error(c.error(`Audio file not found: ${audioFlag}`));
27073
27533
  process.exit(1);
27074
27534
  }
27075
27535
  sourceFilePath2 = audioPath;
27076
- copyFileSync3(audioPath, resolve17(destDir2, basename4(audioPath)));
27536
+ copyFileSync3(audioPath, resolve18(destDir2, basename4(audioPath)));
27077
27537
  console.log(`Audio: ${basename4(audioPath)}`);
27078
27538
  }
27079
27539
  if (sourceFilePath2 && !skipTranscribe) {
@@ -27113,7 +27573,7 @@ var init_init = __esm({
27113
27573
  process.exit(1);
27114
27574
  }
27115
27575
  trackInitTemplate(templateId2);
27116
- const transcriptFile2 = resolve17(destDir2, "transcript.json");
27576
+ const transcriptFile2 = resolve18(destDir2, "transcript.json");
27117
27577
  if (existsSync31(transcriptFile2)) {
27118
27578
  await patchTranscript(destDir2, transcriptFile2);
27119
27579
  }
@@ -27159,7 +27619,7 @@ var init_init = __esm({
27159
27619
  }
27160
27620
  name = nameResult;
27161
27621
  }
27162
- const destDir = resolve17(name);
27622
+ const destDir = resolve18(name);
27163
27623
  if (existsSync31(destDir) && readdirSync10(destDir).length > 0) {
27164
27624
  const overwrite = await Rt({
27165
27625
  message: `Directory ${c.accent(name)} already exists and is not empty. Overwrite?`,
@@ -27174,7 +27634,7 @@ var init_init = __esm({
27174
27634
  let sourceFilePath;
27175
27635
  let videoDuration;
27176
27636
  if (videoFlag) {
27177
- const videoPath = resolve17(videoFlag);
27637
+ const videoPath = resolve18(videoFlag);
27178
27638
  if (!existsSync31(videoPath)) {
27179
27639
  R2.error(`File not found: ${videoFlag}`);
27180
27640
  Nt("Setup cancelled.");
@@ -27186,7 +27646,7 @@ var init_init = __esm({
27186
27646
  localVideoName = result.localVideoName;
27187
27647
  videoDuration = result.meta.durationSeconds;
27188
27648
  } else if (audioFlag) {
27189
- const audioPath = resolve17(audioFlag);
27649
+ const audioPath = resolve18(audioFlag);
27190
27650
  if (!existsSync31(audioPath)) {
27191
27651
  R2.error(`File not found: ${audioFlag}`);
27192
27652
  Nt("Setup cancelled.");
@@ -27194,7 +27654,7 @@ var init_init = __esm({
27194
27654
  }
27195
27655
  mkdirSync19(destDir, { recursive: true });
27196
27656
  sourceFilePath = audioPath;
27197
- copyFileSync3(audioPath, resolve17(destDir, basename4(audioPath)));
27657
+ copyFileSync3(audioPath, resolve18(destDir, basename4(audioPath)));
27198
27658
  R2.info(`Audio copied to ${c.accent(basename4(audioPath))}`);
27199
27659
  }
27200
27660
  if (sourceFilePath) {
@@ -27279,7 +27739,7 @@ ${c.dim("Use --template blank for offline use.")}`
27279
27739
  process.exit(1);
27280
27740
  }
27281
27741
  trackInitTemplate(templateId);
27282
- const transcriptFile = resolve17(destDir, "transcript.json");
27742
+ const transcriptFile = resolve18(destDir, "transcript.json");
27283
27743
  if (existsSync31(transcriptFile)) {
27284
27744
  await patchTranscript(destDir, transcriptFile);
27285
27745
  }
@@ -27344,11 +27804,11 @@ var init_format = __esm({
27344
27804
 
27345
27805
  // src/utils/project.ts
27346
27806
  import { existsSync as existsSync32, statSync as statSync11 } from "fs";
27347
- import { resolve as resolve18, basename as basename5 } from "path";
27807
+ import { resolve as resolve19, basename as basename5 } from "path";
27348
27808
  function resolveProject(dirArg) {
27349
- const dir = resolve18(dirArg ?? ".");
27809
+ const dir = resolve19(dirArg ?? ".");
27350
27810
  const name = basename5(dir);
27351
- const indexPath = resolve18(dir, "index.html");
27811
+ const indexPath = resolve19(dir, "index.html");
27352
27812
  if (!existsSync32(dir) || !statSync11(dir).isDirectory()) {
27353
27813
  errorBox("Not a directory: " + dir);
27354
27814
  process.exit(1);
@@ -27377,7 +27837,7 @@ __export(play_exports, {
27377
27837
  examples: () => examples3
27378
27838
  });
27379
27839
  import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
27380
- import { resolve as resolve19, dirname as dirname13 } from "path";
27840
+ import { resolve as resolve20, dirname as dirname13 } from "path";
27381
27841
  function commandDir() {
27382
27842
  return dirname13(new URL(import.meta.url).pathname);
27383
27843
  }
@@ -27385,10 +27845,10 @@ function resolveRuntimePath2() {
27385
27845
  const d = commandDir();
27386
27846
  const candidates = [
27387
27847
  // Bundled with CLI dist
27388
- resolve19(d, "hyperframe-runtime.js"),
27389
- resolve19(d, "..", "hyperframe-runtime.js"),
27848
+ resolve20(d, "hyperframe-runtime.js"),
27849
+ resolve20(d, "..", "hyperframe-runtime.js"),
27390
27850
  // Monorepo dev: commands/ → src/ → cli/ → packages/ then into core/dist/
27391
- resolve19(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
27851
+ resolve20(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
27392
27852
  ];
27393
27853
  for (const p of candidates) {
27394
27854
  if (existsSync33(p)) return p;
@@ -27399,10 +27859,10 @@ function resolvePlayerPath() {
27399
27859
  const d = commandDir();
27400
27860
  const candidates = [
27401
27861
  // Monorepo dev: commands/ → src/ → cli/ → packages/ then into player/dist/
27402
- resolve19(d, "..", "..", "..", "player", "dist", "hyperframes-player.global.js"),
27862
+ resolve20(d, "..", "..", "..", "player", "dist", "hyperframes-player.global.js"),
27403
27863
  // Bundled with CLI dist
27404
- resolve19(d, "hyperframes-player.global.js"),
27405
- resolve19(d, "..", "hyperframes-player.global.js")
27864
+ resolve20(d, "hyperframes-player.global.js"),
27865
+ resolve20(d, "..", "hyperframes-player.global.js")
27406
27866
  ];
27407
27867
  for (const p of candidates) {
27408
27868
  if (existsSync33(p)) return p;
@@ -27505,7 +27965,7 @@ var init_play = __esm({
27505
27965
  });
27506
27966
  app.get("/composition/*", async (ctx) => {
27507
27967
  const reqPath = ctx.req.path.replace("/composition/", "");
27508
- const filePath = resolve19(project.dir, reqPath);
27968
+ const filePath = resolve20(project.dir, reqPath);
27509
27969
  if (!filePath.startsWith(project.dir)) return ctx.text("Forbidden", 403);
27510
27970
  if (!existsSync33(filePath)) return ctx.text("Not found", 404);
27511
27971
  const content = readFileSync22(filePath, "utf-8");
@@ -27657,7 +28117,7 @@ __export(render_exports, {
27657
28117
  });
27658
28118
  import { mkdirSync as mkdirSync20, readFileSync as readFileSync23, statSync as statSync12, writeFileSync as writeFileSync12, rmSync as rmSync8 } from "fs";
27659
28119
  import { cpus as cpus3, freemem as freemem3, tmpdir as tmpdir3 } from "os";
27660
- import { resolve as resolve20, dirname as dirname14, join as join33, basename as basename6 } from "path";
28120
+ import { resolve as resolve21, dirname as dirname14, join as join33, basename as basename6 } from "path";
27661
28121
  import { execFileSync as execFileSync5, spawn as spawn10 } from "child_process";
27662
28122
  function defaultWorkerCount() {
27663
28123
  return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT * 3 / 4), 8));
@@ -27666,8 +28126,8 @@ function dockerImageTag(version) {
27666
28126
  return `${DOCKER_IMAGE_PREFIX}:${version}`;
27667
28127
  }
27668
28128
  function resolveDockerfilePath() {
27669
- const builtPath = resolve20(__dirname, "docker", "Dockerfile.render");
27670
- const devPath = resolve20(__dirname, "..", "src", "docker", "Dockerfile.render");
28129
+ const builtPath = resolve21(__dirname, "docker", "Dockerfile.render");
28130
+ const devPath = resolve21(__dirname, "..", "src", "docker", "Dockerfile.render");
27671
28131
  for (const p of [builtPath, devPath]) {
27672
28132
  try {
27673
28133
  statSync12(p);
@@ -27751,9 +28211,9 @@ async function renderDocker(projectDir, outputPath, options) {
27751
28211
  // GPU encoding requires host GPU passthrough
27752
28212
  ...options.gpu ? ["--gpus", "all"] : [],
27753
28213
  "-v",
27754
- `${resolve20(projectDir)}:/project:ro`,
28214
+ `${resolve21(projectDir)}:/project:ro`,
27755
28215
  "-v",
27756
- `${resolve20(outputDir)}:/output`,
28216
+ `${resolve21(outputDir)}:/output`,
27757
28217
  imageTag,
27758
28218
  "/project",
27759
28219
  "--output",
@@ -27962,6 +28422,10 @@ var init_render2 = __esm({
27962
28422
  type: "boolean",
27963
28423
  description: "Fail render on lint errors AND warnings",
27964
28424
  default: false
28425
+ },
28426
+ "max-concurrent-renders": {
28427
+ type: "string",
28428
+ description: "Max concurrent renders when using the producer server (1-10). Default: 2."
27965
28429
  }
27966
28430
  },
27967
28431
  async run({ args }) {
@@ -27993,12 +28457,23 @@ var init_render2 = __esm({
27993
28457
  }
27994
28458
  workers = parsed;
27995
28459
  }
27996
- const rendersDir = resolve20("renders");
28460
+ if (args["max-concurrent-renders"] != null) {
28461
+ const parsed = parseInt(args["max-concurrent-renders"], 10);
28462
+ if (isNaN(parsed) || parsed < 1 || parsed > 10) {
28463
+ errorBox(
28464
+ "Invalid max-concurrent-renders",
28465
+ `Got "${args["max-concurrent-renders"]}". Must be a number between 1 and 10.`
28466
+ );
28467
+ process.exit(1);
28468
+ }
28469
+ process.env.PRODUCER_MAX_CONCURRENT_RENDERS = String(parsed);
28470
+ }
28471
+ const rendersDir = resolve21("renders");
27997
28472
  const ext = FORMAT_EXT[format] ?? ".mp4";
27998
28473
  const now = /* @__PURE__ */ new Date();
27999
28474
  const datePart = now.toISOString().slice(0, 10);
28000
28475
  const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
28001
- const outputPath = args.output ? resolve20(args.output) : join33(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
28476
+ const outputPath = args.output ? resolve21(args.output) : join33(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
28002
28477
  mkdirSync20(dirname14(outputPath), { recursive: true });
28003
28478
  const useDocker = args.docker ?? false;
28004
28479
  const useGpu = args.gpu ?? false;
@@ -28412,7 +28887,7 @@ __export(compositions_exports, {
28412
28887
  examples: () => examples7
28413
28888
  });
28414
28889
  import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
28415
- import { resolve as resolve21, dirname as dirname15 } from "path";
28890
+ import { resolve as resolve22, dirname as dirname15 } from "path";
28416
28891
  function parseCompositions(html, baseDir) {
28417
28892
  const parser = new DOMParser();
28418
28893
  const doc = parser.parseFromString(html, "text/html");
@@ -28424,7 +28899,7 @@ function parseCompositions(html, baseDir) {
28424
28899
  const height = parseInt(div.getAttribute("data-height") ?? "1080", 10);
28425
28900
  const compositionSrc = div.getAttribute("data-composition-src");
28426
28901
  if (compositionSrc) {
28427
- const subPath = resolve21(baseDir, compositionSrc);
28902
+ const subPath = resolve22(baseDir, compositionSrc);
28428
28903
  if (existsSync34(subPath)) {
28429
28904
  const subHtml = readFileSync25(subPath, "utf-8");
28430
28905
  const subInfo = parseSubComposition(subHtml, id, width, height);
@@ -28559,7 +29034,7 @@ __export(benchmark_exports, {
28559
29034
  examples: () => examples8
28560
29035
  });
28561
29036
  import { existsSync as existsSync35, statSync as statSync14 } from "fs";
28562
- import { resolve as resolve22, join as join35 } from "path";
29037
+ import { resolve as resolve23, join as join35 } from "path";
28563
29038
  var examples8, DEFAULT_CONFIGS, benchmark_default;
28564
29039
  var init_benchmark = __esm({
28565
29040
  "src/commands/benchmark.ts"() {
@@ -28601,7 +29076,7 @@ var init_benchmark = __esm({
28601
29076
  process.exit(1);
28602
29077
  }
28603
29078
  const jsonOutput = args.json ?? false;
28604
- const benchDir = resolve22("renders", ".benchmark");
29079
+ const benchDir = resolve23("renders", ".benchmark");
28605
29080
  let producer = null;
28606
29081
  try {
28607
29082
  producer = await loadProducer();
@@ -28865,7 +29340,7 @@ __export(transcribe_exports2, {
28865
29340
  examples: () => examples10
28866
29341
  });
28867
29342
  import { existsSync as existsSync36, writeFileSync as writeFileSync13 } from "fs";
28868
- import { resolve as resolve23, join as join36, extname as extname7 } from "path";
29343
+ import { resolve as resolve24, join as join36, extname as extname7 } from "path";
28869
29344
  async function importTranscript(inputPath, dir, json) {
28870
29345
  const { loadTranscript: loadTranscript2, patchCaptionHtml: patchCaptionHtml2 } = await Promise.resolve().then(() => (init_normalize(), normalize_exports));
28871
29346
  const { words, format } = loadTranscript2(inputPath);
@@ -28989,12 +29464,12 @@ var init_transcribe2 = __esm({
28989
29464
  }
28990
29465
  },
28991
29466
  async run({ args }) {
28992
- const inputPath = resolve23(args.input);
29467
+ const inputPath = resolve24(args.input);
28993
29468
  if (!existsSync36(inputPath)) {
28994
29469
  console.error(c.error(`File not found: ${args.input}`));
28995
29470
  process.exit(1);
28996
29471
  }
28997
- const dir = resolve23(args.dir ?? ".");
29472
+ const dir = resolve24(args.dir ?? ".");
28998
29473
  const ext = extname7(inputPath).toLowerCase();
28999
29474
  const isImport = ext === ".json" || ext === ".srt" || ext === ".vtt";
29000
29475
  if (isImport) {
@@ -29217,7 +29692,7 @@ __export(tts_exports, {
29217
29692
  examples: () => examples11
29218
29693
  });
29219
29694
  import { existsSync as existsSync39, readFileSync as readFileSync26 } from "fs";
29220
- import { resolve as resolve24, extname as extname8 } from "path";
29695
+ import { resolve as resolve25, extname as extname8 } from "path";
29221
29696
  function listVoices(json) {
29222
29697
  if (json) {
29223
29698
  console.log(JSON.stringify(BUNDLED_VOICES));
@@ -29305,7 +29780,7 @@ var init_tts = __esm({
29305
29780
  process.exit(1);
29306
29781
  }
29307
29782
  let text;
29308
- const maybeFile = resolve24(args.input);
29783
+ const maybeFile = resolve25(args.input);
29309
29784
  if (existsSync39(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
29310
29785
  text = readFileSync26(maybeFile, "utf-8").trim();
29311
29786
  if (!text) {
@@ -29319,7 +29794,7 @@ var init_tts = __esm({
29319
29794
  console.error(c.error("No text provided."));
29320
29795
  process.exit(1);
29321
29796
  }
29322
- const output = resolve24(args.output ?? "speech.wav");
29797
+ const output = resolve25(args.output ?? "speech.wav");
29323
29798
  const voice = args.voice ?? DEFAULT_VOICE;
29324
29799
  const speed = args.speed ? parseFloat(args.speed) : 1;
29325
29800
  if (isNaN(speed) || speed <= 0 || speed > 3) {
@@ -29373,13 +29848,13 @@ __export(docs_exports, {
29373
29848
  examples: () => examples12
29374
29849
  });
29375
29850
  import { readFileSync as readFileSync27, existsSync as existsSync40 } from "fs";
29376
- import { resolve as resolve25, dirname as dirname17, join as join39 } from "path";
29851
+ import { resolve as resolve26, dirname as dirname17, join as join39 } from "path";
29377
29852
  import { fileURLToPath as fileURLToPath6 } from "url";
29378
29853
  function docsDir() {
29379
29854
  const thisFile = fileURLToPath6(import.meta.url);
29380
29855
  const dir = dirname17(thisFile);
29381
- const devPath = resolve25(dir, "..", "docs");
29382
- const builtPath = resolve25(dir, "docs");
29856
+ const devPath = resolve26(dir, "..", "docs");
29857
+ const builtPath = resolve26(dir, "docs");
29383
29858
  return existsSync40(devPath) ? devPath : builtPath;
29384
29859
  }
29385
29860
  function formatInlineCode(line) {
@@ -29897,13 +30372,13 @@ __export(validate_exports, {
29897
30372
  default: () => validate_default
29898
30373
  });
29899
30374
  import { existsSync as existsSync41, readFileSync as readFileSync28 } from "fs";
29900
- import { resolve as resolve26, join as join40, dirname as dirname18 } from "path";
30375
+ import { resolve as resolve27, join as join40, dirname as dirname18 } from "path";
29901
30376
  import { fileURLToPath as fileURLToPath7 } from "url";
29902
30377
  async function validateInBrowser(projectDir, opts) {
29903
30378
  const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
29904
30379
  const { ensureBrowser: ensureBrowser2 } = await Promise.resolve().then(() => (init_manager2(), manager_exports2));
29905
30380
  let html = await bundleToSingleHtml2(projectDir);
29906
- const runtimePath = resolve26(
30381
+ const runtimePath = resolve27(
29907
30382
  __dirname2,
29908
30383
  "..",
29909
30384
  "..",
@@ -29916,7 +30391,7 @@ async function validateInBrowser(projectDir, opts) {
29916
30391
  const runtimeSource = readFileSync28(runtimePath, "utf-8");
29917
30392
  html = html.replace(
29918
30393
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
29919
- `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
30394
+ () => `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
29920
30395
  );
29921
30396
  }
29922
30397
  const { createServer } = await import("http");
@@ -30246,7 +30721,8 @@ var init_help = __esm({
30246
30721
  [
30247
30722
  "transcribe",
30248
30723
  "Transcribe audio/video to word-level timestamps, or import an existing transcript"
30249
- ]
30724
+ ],
30725
+ ["tts", "Generate speech audio from text using a local AI model (Kokoro-82M)"]
30250
30726
  ]
30251
30727
  },
30252
30728
  {