hyperframes 0.2.3-alpha.2 → 0.2.4

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-alpha.2" : "0.0.0-dev";
57
+ VERSION = true ? "0.2.4" : "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,
@@ -4971,6 +4973,56 @@ var init_composition = __esm({
4971
4973
  }
4972
4974
  return findings;
4973
4975
  },
4976
+ // root_composition_missing_data_start
4977
+ ({ rootTag }) => {
4978
+ const findings = [];
4979
+ if (!rootTag) return findings;
4980
+ const compId = readAttr(rootTag.raw, "data-composition-id");
4981
+ if (!compId) return findings;
4982
+ const hasStart = readAttr(rootTag.raw, "data-start") !== null;
4983
+ if (!hasStart) {
4984
+ findings.push({
4985
+ code: "root_composition_missing_data_start",
4986
+ severity: "warning",
4987
+ message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
4988
+ fixHint: 'Add data-start="0" to the root composition element.',
4989
+ snippet: truncateSnippet(rootTag.raw)
4990
+ });
4991
+ }
4992
+ return findings;
4993
+ },
4994
+ // standalone_composition_wrapped_in_template
4995
+ ({ rawSource, options }) => {
4996
+ const findings = [];
4997
+ if (options.isSubComposition) return findings;
4998
+ const trimmed = rawSource.trimStart().toLowerCase();
4999
+ if (trimmed.startsWith("<template")) {
5000
+ findings.push({
5001
+ code: "standalone_composition_wrapped_in_template",
5002
+ severity: "warning",
5003
+ 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.",
5004
+ fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
5005
+ });
5006
+ }
5007
+ return findings;
5008
+ },
5009
+ // root_composition_missing_html_wrapper
5010
+ ({ rawSource, options }) => {
5011
+ const findings = [];
5012
+ if (options.isSubComposition) return findings;
5013
+ const trimmed = rawSource.trimStart().toLowerCase();
5014
+ const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
5015
+ const hasComposition = rawSource.includes("data-composition-id");
5016
+ if (hasComposition && !hasDoctype) {
5017
+ findings.push({
5018
+ code: "root_composition_missing_html_wrapper",
5019
+ severity: "warning",
5020
+ message: "Composition is missing <!DOCTYPE html> and <html> wrapper. The bundler and preview expect a complete HTML document for index.html files.",
5021
+ fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.'
5022
+ });
5023
+ }
5024
+ return findings;
5025
+ },
4974
5026
  // requestanimationframe_in_composition
4975
5027
  ({ scripts }) => {
4976
5028
  const findings = [];
@@ -5192,7 +5244,7 @@ function lintProject(project) {
5192
5244
  const filePath = join6(compositionsDir, file);
5193
5245
  const html = readFileSync6(filePath, "utf-8");
5194
5246
  allHtmlSources.push(html);
5195
- const result = lintHyperframeHtml(html, { filePath });
5247
+ const result = lintHyperframeHtml(html, { filePath, isSubComposition: true });
5196
5248
  results.push({ file: `compositions/${file}`, result });
5197
5249
  totalErrors += result.errorCount;
5198
5250
  totalWarnings += result.warningCount;
@@ -5328,6 +5380,238 @@ var init_lintFormat = __esm({
5328
5380
  }
5329
5381
  });
5330
5382
 
5383
+ // src/server/portUtils.ts
5384
+ import net from "net";
5385
+ import http from "http";
5386
+ import { execFile } from "child_process";
5387
+ import { promisify } from "util";
5388
+ import { resolve as resolve2 } from "path";
5389
+ function isPortAvailableOnHost(port, host) {
5390
+ return new Promise((resolve28) => {
5391
+ const server = net.createServer();
5392
+ server.unref();
5393
+ server.on("error", (err) => {
5394
+ resolve28(err.code !== "EADDRINUSE");
5395
+ });
5396
+ server.listen({ port, host }, () => {
5397
+ server.close(() => {
5398
+ resolve28(true);
5399
+ });
5400
+ });
5401
+ });
5402
+ }
5403
+ async function testPortOnAllHosts(port) {
5404
+ const hosts = ["127.0.0.1", "0.0.0.0", "::1", "::"];
5405
+ const results = await Promise.all(hosts.map((h2) => isPortAvailableOnHost(port, h2)));
5406
+ return results.every(Boolean);
5407
+ }
5408
+ function detectHyperframesServer(port, normalizedProjectDir) {
5409
+ return new Promise((resolveResult) => {
5410
+ const req = http.get(
5411
+ {
5412
+ hostname: "127.0.0.1",
5413
+ port,
5414
+ path: "/__hyperframes_config",
5415
+ timeout: PROBE_TIMEOUT_MS
5416
+ },
5417
+ (res) => {
5418
+ if (res.statusCode !== 200) {
5419
+ res.resume();
5420
+ return resolveResult({ type: "not-hyperframes" });
5421
+ }
5422
+ let data = "";
5423
+ let bytes = 0;
5424
+ res.on("data", (chunk) => {
5425
+ bytes += typeof chunk === "string" ? chunk.length : chunk.byteLength;
5426
+ if (bytes > PROBE_MAX_BYTES) {
5427
+ req.destroy();
5428
+ return resolveResult({ type: "not-hyperframes" });
5429
+ }
5430
+ data += chunk;
5431
+ });
5432
+ res.on("error", () => {
5433
+ resolveResult({ type: "not-hyperframes" });
5434
+ });
5435
+ res.on("end", () => {
5436
+ try {
5437
+ const json = JSON.parse(data);
5438
+ if (json.isHyperframes !== true) {
5439
+ return resolveResult({ type: "not-hyperframes" });
5440
+ }
5441
+ const normalize = (p) => resolve2(p).replace(/\\/g, "/").toLowerCase();
5442
+ if (normalize(json.projectDir) === normalizedProjectDir) {
5443
+ return resolveResult({ type: "match" });
5444
+ }
5445
+ return resolveResult({ type: "mismatch", projectName: json.projectName });
5446
+ } catch {
5447
+ resolveResult({ type: "not-hyperframes" });
5448
+ }
5449
+ });
5450
+ }
5451
+ );
5452
+ req.on("error", () => {
5453
+ resolveResult({ type: "not-hyperframes" });
5454
+ });
5455
+ req.on("timeout", () => {
5456
+ req.destroy();
5457
+ resolveResult({ type: "not-hyperframes" });
5458
+ });
5459
+ });
5460
+ }
5461
+ async function getProcessOnPort(port) {
5462
+ if (process.platform === "win32") return null;
5463
+ try {
5464
+ const { stdout: stdout2 } = await execFileAsync("lsof", [`-ti:${port}`, "-sTCP:LISTEN"], {
5465
+ timeout: 2e3
5466
+ });
5467
+ const pid = stdout2.trim().split("\n")[0]?.trim();
5468
+ return pid || null;
5469
+ } catch {
5470
+ return null;
5471
+ }
5472
+ }
5473
+ function probePort(port) {
5474
+ return new Promise((resolveResult) => {
5475
+ const req = http.get(
5476
+ { hostname: "127.0.0.1", port, path: "/__hyperframes_config", timeout: PROBE_TIMEOUT_MS },
5477
+ (res) => {
5478
+ if (res.statusCode !== 200) {
5479
+ res.resume();
5480
+ return resolveResult(null);
5481
+ }
5482
+ let data = "";
5483
+ let bytes = 0;
5484
+ res.on("data", (chunk) => {
5485
+ bytes += typeof chunk === "string" ? chunk.length : chunk.byteLength;
5486
+ if (bytes > PROBE_MAX_BYTES) {
5487
+ req.destroy();
5488
+ return resolveResult(null);
5489
+ }
5490
+ data += chunk;
5491
+ });
5492
+ res.on("error", () => resolveResult(null));
5493
+ res.on("end", () => {
5494
+ try {
5495
+ const json = JSON.parse(data);
5496
+ resolveResult(json.isHyperframes === true ? json : null);
5497
+ } catch {
5498
+ resolveResult(null);
5499
+ }
5500
+ });
5501
+ }
5502
+ );
5503
+ req.on("error", () => resolveResult(null));
5504
+ req.on("timeout", () => {
5505
+ req.destroy();
5506
+ resolveResult(null);
5507
+ });
5508
+ });
5509
+ }
5510
+ async function scanActiveServers(startPort = 3002) {
5511
+ const endPort = startPort + MAX_PORT_SCAN - 1;
5512
+ const servers = [];
5513
+ const batchSize = 20;
5514
+ for (let batchStart = startPort; batchStart <= endPort; batchStart += batchSize) {
5515
+ const batchEnd = Math.min(batchStart + batchSize - 1, endPort);
5516
+ const ports = Array.from({ length: batchEnd - batchStart + 1 }, (_2, i) => batchStart + i);
5517
+ const results = await Promise.all(
5518
+ ports.map(async (port) => {
5519
+ const config = await probePort(port);
5520
+ if (!config) return null;
5521
+ const pid = await getProcessOnPort(port);
5522
+ return {
5523
+ port,
5524
+ projectName: config.projectName,
5525
+ projectDir: config.projectDir,
5526
+ version: config.version,
5527
+ pid
5528
+ };
5529
+ })
5530
+ );
5531
+ for (const r of results) {
5532
+ if (r) servers.push(r);
5533
+ }
5534
+ }
5535
+ return servers;
5536
+ }
5537
+ async function killActiveServers(startPort = 3002) {
5538
+ const servers = await scanActiveServers(startPort);
5539
+ let killed = 0;
5540
+ for (const server of servers) {
5541
+ if (server.pid) {
5542
+ try {
5543
+ process.kill(parseInt(server.pid, 10), "SIGTERM");
5544
+ killed++;
5545
+ } catch {
5546
+ }
5547
+ }
5548
+ }
5549
+ return killed;
5550
+ }
5551
+ async function findPortAndServe(fetch3, startPort, projectDir, forceNew) {
5552
+ const { createAdaptorServer } = await import("@hono/node-server");
5553
+ const normalizedDir = resolve2(projectDir).replace(/\\/g, "/").toLowerCase();
5554
+ const endPort = startPort + MAX_PORT_SCAN - 1;
5555
+ let server = null;
5556
+ for (let port = startPort; port <= endPort; port++) {
5557
+ const available = await testPortOnAllHosts(port);
5558
+ if (available) {
5559
+ if (!server) server = createAdaptorServer({ fetch: fetch3 });
5560
+ try {
5561
+ await new Promise((resolveListener, rejectListener) => {
5562
+ const onError = (err) => {
5563
+ server.removeListener("listening", onListening);
5564
+ rejectListener(err);
5565
+ };
5566
+ const onListening = () => {
5567
+ server.removeListener("error", onError);
5568
+ resolveListener();
5569
+ };
5570
+ server.once("error", onError);
5571
+ server.once("listening", onListening);
5572
+ server.listen(port);
5573
+ });
5574
+ return { type: "started", server, port };
5575
+ } catch (err) {
5576
+ if (err.code === "EADDRINUSE") {
5577
+ continue;
5578
+ }
5579
+ throw err;
5580
+ }
5581
+ }
5582
+ if (!forceNew) {
5583
+ const detection = await detectHyperframesServer(port, normalizedDir);
5584
+ if (detection.type === "match") {
5585
+ return { type: "already-running", port };
5586
+ }
5587
+ if (detection.type === "mismatch") {
5588
+ console.log(
5589
+ ` ${c.dim(`Port ${port} in use by HyperFrames project "${detection.projectName}" \u2014 skipping`)}`
5590
+ );
5591
+ continue;
5592
+ }
5593
+ }
5594
+ const pid = await getProcessOnPort(port);
5595
+ if (pid) {
5596
+ console.log(` ${c.dim(`Port ${port} in use by PID ${pid} \u2014 skipping`)}`);
5597
+ }
5598
+ }
5599
+ throw new Error(
5600
+ `Ports ${startPort}\u2013${endPort} are all in use. Use --port to specify a different starting port.`
5601
+ );
5602
+ }
5603
+ var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES;
5604
+ var init_portUtils = __esm({
5605
+ "src/server/portUtils.ts"() {
5606
+ "use strict";
5607
+ init_colors();
5608
+ execFileAsync = promisify(execFile);
5609
+ MAX_PORT_SCAN = 100;
5610
+ PROBE_TIMEOUT_MS = 300;
5611
+ PROBE_MAX_BYTES = 4096;
5612
+ }
5613
+ });
5614
+
5331
5615
  // src/server/fileWatcher.ts
5332
5616
  import { watch } from "fs";
5333
5617
  function createProjectWatcher(projectDir) {
@@ -5372,11 +5656,11 @@ var init_fileWatcher = __esm({
5372
5656
  });
5373
5657
 
5374
5658
  // ../core/src/studio-api/helpers/safePath.ts
5375
- import { resolve as resolve2, sep, join as join7 } from "path";
5659
+ import { resolve as resolve3, sep, join as join7 } from "path";
5376
5660
  import { readdirSync as readdirSync3 } from "fs";
5377
5661
  function isSafePath(base, resolved) {
5378
- const norm = resolve2(base) + sep;
5379
- return resolved.startsWith(norm) || resolved === resolve2(base);
5662
+ const norm = resolve3(base) + sep;
5663
+ return resolved.startsWith(norm) || resolved === resolve3(base);
5380
5664
  }
5381
5665
  function walkDir(dir, prefix = "") {
5382
5666
  const files = [];
@@ -5441,7 +5725,7 @@ import {
5441
5725
  renameSync as renameSync2,
5442
5726
  readdirSync as readdirSync4
5443
5727
  } from "fs";
5444
- import { resolve as resolve3, dirname, join as join8 } from "path";
5728
+ import { resolve as resolve4, dirname, join as join8 } from "path";
5445
5729
  async function resolveProjectFile(c2, adapter2, opts) {
5446
5730
  const id = c2.req.param("id");
5447
5731
  const project = await adapter2.resolveProject(id);
@@ -5452,7 +5736,7 @@ async function resolveProjectFile(c2, adapter2, opts) {
5452
5736
  if (filePath.includes("\0")) {
5453
5737
  return { error: c2.json({ error: "forbidden" }, 403) };
5454
5738
  }
5455
- const absPath = resolve3(project.dir, filePath);
5739
+ const absPath = resolve4(project.dir, filePath);
5456
5740
  if (!isSafePath(project.dir, absPath)) {
5457
5741
  return { error: c2.json({ error: "forbidden" }, 403) };
5458
5742
  }
@@ -5472,7 +5756,7 @@ function generateCopyPath(projectDir, originalPath) {
5472
5756
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
5473
5757
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
5474
5758
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
5475
- while (existsSync7(resolve3(projectDir, candidate))) {
5759
+ while (existsSync7(resolve4(projectDir, candidate))) {
5476
5760
  num++;
5477
5761
  candidate = `${cleanBase} (copy ${num})${ext}`;
5478
5762
  }
@@ -5553,7 +5837,7 @@ function registerFileRoutes(api, adapter2) {
5553
5837
  if (!body.newPath || body.newPath.includes("\0")) {
5554
5838
  return c2.json({ error: "newPath required" }, 400);
5555
5839
  }
5556
- const newAbs = resolve3(res.project.dir, body.newPath);
5840
+ const newAbs = resolve4(res.project.dir, body.newPath);
5557
5841
  if (!isSafePath(res.project.dir, newAbs)) {
5558
5842
  return c2.json({ error: "forbidden" }, 403);
5559
5843
  }
@@ -5572,12 +5856,12 @@ function registerFileRoutes(api, adapter2) {
5572
5856
  if (!body.path || body.path.includes("\0")) {
5573
5857
  return c2.json({ error: "path required" }, 400);
5574
5858
  }
5575
- const srcAbs = resolve3(project.dir, body.path);
5859
+ const srcAbs = resolve4(project.dir, body.path);
5576
5860
  if (!isSafePath(project.dir, srcAbs) || !existsSync7(srcAbs)) {
5577
5861
  return c2.json({ error: "not found" }, 404);
5578
5862
  }
5579
5863
  const copyPath = generateCopyPath(project.dir, body.path);
5580
- const destAbs = resolve3(project.dir, copyPath);
5864
+ const destAbs = resolve4(project.dir, copyPath);
5581
5865
  if (!isSafePath(project.dir, destAbs)) {
5582
5866
  return c2.json({ error: "forbidden" }, 403);
5583
5867
  }
@@ -5596,7 +5880,7 @@ function registerFileRoutes(api, adapter2) {
5596
5880
  const project = await adapter2.resolveProject(c2.req.param("id"));
5597
5881
  if (!project) return c2.json({ error: "not found" }, 404);
5598
5882
  const subDir = c2.req.query("dir") ?? "";
5599
- const targetDir = subDir ? resolve3(project.dir, subDir) : project.dir;
5883
+ const targetDir = subDir ? resolve4(project.dir, subDir) : project.dir;
5600
5884
  if (!isSafePath(project.dir, targetDir)) return c2.json({ error: "forbidden" }, 403);
5601
5885
  if (subDir && !existsSync7(targetDir)) mkdirSync5(targetDir, { recursive: true });
5602
5886
  const formData = await c2.req.formData();
@@ -5610,7 +5894,7 @@ function registerFileRoutes(api, adapter2) {
5610
5894
  skipped.push(name);
5611
5895
  continue;
5612
5896
  }
5613
- const destPath = resolve3(targetDir, name);
5897
+ const destPath = resolve4(targetDir, name);
5614
5898
  if (!isSafePath(project.dir, destPath)) continue;
5615
5899
  let finalPath = destPath;
5616
5900
  let finalName = name;
@@ -5619,13 +5903,13 @@ function registerFileRoutes(api, adapter2) {
5619
5903
  const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
5620
5904
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
5621
5905
  let n = 2;
5622
- while (n < 1e4 && existsSync7(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
5906
+ while (n < 1e4 && existsSync7(resolve4(targetDir, `${base} (${n})${ext}`))) n++;
5623
5907
  if (n >= 1e4) {
5624
5908
  skipped.push(name);
5625
5909
  continue;
5626
5910
  }
5627
5911
  finalName = `${base} (${n})${ext}`;
5628
- finalPath = resolve3(targetDir, finalName);
5912
+ finalPath = resolve4(targetDir, finalName);
5629
5913
  }
5630
5914
  const buffer = Buffer.from(await value.arrayBuffer());
5631
5915
  writeFileSync4(finalPath, buffer);
@@ -9535,8 +9819,8 @@ var init_custom_element_registry = __esm({
9535
9819
  } : (element) => element.localName === localName;
9536
9820
  registry.set(localName, { Class, check });
9537
9821
  if (waiting.has(localName)) {
9538
- for (const resolve27 of waiting.get(localName))
9539
- resolve27(Class);
9822
+ for (const resolve28 of waiting.get(localName))
9823
+ resolve28(Class);
9540
9824
  waiting.delete(localName);
9541
9825
  }
9542
9826
  ownerDocument.querySelectorAll(
@@ -9576,13 +9860,13 @@ var init_custom_element_registry = __esm({
9576
9860
  */
9577
9861
  whenDefined(localName) {
9578
9862
  const { registry, waiting } = this;
9579
- return new Promise((resolve27) => {
9863
+ return new Promise((resolve28) => {
9580
9864
  if (registry.has(localName))
9581
- resolve27(registry.get(localName).Class);
9865
+ resolve28(registry.get(localName).Class);
9582
9866
  else {
9583
9867
  if (!waiting.has(localName))
9584
9868
  waiting.set(localName, []);
9585
- waiting.get(localName).push(resolve27);
9869
+ waiting.get(localName).push(resolve28);
9586
9870
  }
9587
9871
  });
9588
9872
  }
@@ -18283,7 +18567,7 @@ var init_esm10 = __esm({
18283
18567
  });
18284
18568
 
18285
18569
  // ../core/src/compiler/rewriteSubCompPaths.ts
18286
- import { join as join9, resolve as resolve4, dirname as dirname2 } from "path";
18570
+ import { join as join9, resolve as resolve5, dirname as dirname2 } from "path";
18287
18571
  function isAbsoluteOrSpecial(val) {
18288
18572
  return !val || val.startsWith("http://") || val.startsWith("https://") || val.startsWith("//") || val.startsWith("data:") || val.startsWith("#");
18289
18573
  }
@@ -18296,7 +18580,7 @@ function rewriteAssetPath(compSrcPath, relativePath) {
18296
18580
  const compDir = dirname2(compSrcPath);
18297
18581
  if (!compDir || compDir === ".") return relativePath;
18298
18582
  const resolved = join9(compDir, relativePath);
18299
- const normalized = resolve4("/", resolved).slice(1);
18583
+ const normalized = resolve5("/", resolved).slice(1);
18300
18584
  return normalized;
18301
18585
  }
18302
18586
  function rewriteAssetPaths(elements, compSrcPath, getAttr2, setAttr) {
@@ -18308,7 +18592,7 @@ function rewriteAssetPaths(elements, compSrcPath, getAttr2, setAttr) {
18308
18592
  if (isAbsoluteOrSpecial(val)) continue;
18309
18593
  if (!needsRewrite(val)) continue;
18310
18594
  const rewritten = join9(compDir, val);
18311
- const normalized = resolve4("/", rewritten).slice(1);
18595
+ const normalized = resolve5("/", rewritten).slice(1);
18312
18596
  if (normalized !== val) {
18313
18597
  setAttr(el, attr, normalized);
18314
18598
  }
@@ -18397,7 +18681,7 @@ var init_subComposition = __esm({
18397
18681
 
18398
18682
  // ../core/src/studio-api/routes/preview.ts
18399
18683
  import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
18400
- import { resolve as resolve5 } from "path";
18684
+ import { resolve as resolve6 } from "path";
18401
18685
  function registerPreviewRoutes(api, adapter2) {
18402
18686
  api.get("/projects/:id/preview", async (c2) => {
18403
18687
  const project = await adapter2.resolveProject(c2.req.param("id"));
@@ -18405,7 +18689,7 @@ function registerPreviewRoutes(api, adapter2) {
18405
18689
  try {
18406
18690
  let bundled = await adapter2.bundle(project.dir);
18407
18691
  if (!bundled) {
18408
- const indexPath = resolve5(project.dir, "index.html");
18692
+ const indexPath = resolve6(project.dir, "index.html");
18409
18693
  if (!existsSync9(indexPath)) return c2.text("not found", 404);
18410
18694
  bundled = readFileSync9(indexPath, "utf-8");
18411
18695
  }
@@ -18421,7 +18705,7 @@ ${runtimeTag}`;
18421
18705
  }
18422
18706
  return c2.html(bundled);
18423
18707
  } catch {
18424
- const file = resolve5(project.dir, "index.html");
18708
+ const file = resolve6(project.dir, "index.html");
18425
18709
  if (existsSync9(file)) return c2.html(readFileSync9(file, "utf-8"));
18426
18710
  return c2.text("not found", 404);
18427
18711
  }
@@ -18432,7 +18716,7 @@ ${runtimeTag}`;
18432
18716
  const compPath = decodeURIComponent(
18433
18717
  c2.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
18434
18718
  );
18435
- const compFile = resolve5(project.dir, compPath);
18719
+ const compFile = resolve6(project.dir, compPath);
18436
18720
  if (!isSafePath(project.dir, compFile) || !existsSync9(compFile) || !statSync2(compFile).isFile()) {
18437
18721
  return c2.text("not found", 404);
18438
18722
  }
@@ -18447,7 +18731,7 @@ ${runtimeTag}`;
18447
18731
  const subPath = decodeURIComponent(
18448
18732
  c2.req.path.replace(`/projects/${project.id}/preview/`, "").split("?")[0] ?? ""
18449
18733
  );
18450
- const file = resolve5(project.dir, subPath);
18734
+ const file = resolve6(project.dir, subPath);
18451
18735
  if (!isSafePath(project.dir, file) || !existsSync9(file) || !statSync2(file).isFile()) {
18452
18736
  return c2.text("not found", 404);
18453
18737
  }
@@ -19823,7 +20107,7 @@ var init_timingCompiler = __esm({
19823
20107
 
19824
20108
  // ../core/src/inline-scripts/hyperframesRuntime.engine.ts
19825
20109
  import { buildSync } from "esbuild";
19826
- import { dirname as dirname3, resolve as resolve6 } from "path";
20110
+ import { dirname as dirname3, resolve as resolve7 } from "path";
19827
20111
  import { fileURLToPath } from "url";
19828
20112
  var init_hyperframesRuntime_engine = __esm({
19829
20113
  "../core/src/inline-scripts/hyperframesRuntime.engine.ts"() {
@@ -19990,18 +20274,35 @@ async function getCdpSession(page) {
19990
20274
  }
19991
20275
  return client;
19992
20276
  }
20277
+ async function sendBeginFrame(client, params) {
20278
+ for (let attempt = 0; ; attempt++) {
20279
+ try {
20280
+ return await client.send("HeadlessExperimental.beginFrame", params);
20281
+ } catch (err) {
20282
+ const msg = err instanceof Error ? err.message : String(err);
20283
+ const isPending = msg.includes("Another frame is pending");
20284
+ if (isPending && attempt < PENDING_FRAME_RETRIES) {
20285
+ await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
20286
+ continue;
20287
+ }
20288
+ if (isPending) {
20289
+ throw new Error(
20290
+ `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
20291
+ );
20292
+ }
20293
+ throw err;
20294
+ }
20295
+ }
20296
+ }
19993
20297
  async function beginFrameCapture(page, options, frameTimeTicks, interval) {
19994
20298
  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
- });
20299
+ const isPng = options.format === "png";
20300
+ const screenshot = {
20301
+ format: isPng ? "png" : "jpeg",
20302
+ quality: isPng ? void 0 : options.quality ?? 80,
20303
+ optimizeForSpeed: true
20304
+ };
20305
+ const result = await sendBeginFrame(client, { frameTimeTicks, interval, screenshot });
20005
20306
  let buffer;
20006
20307
  if (result.screenshotData) {
20007
20308
  buffer = Buffer.from(result.screenshotData, "base64");
@@ -20011,16 +20312,12 @@ async function beginFrameCapture(page, options, frameTimeTicks, interval) {
20011
20312
  if (cached2) {
20012
20313
  buffer = cached2;
20013
20314
  } else {
20014
- const retry = await client.send("HeadlessExperimental.beginFrame", {
20315
+ const fallback = await sendBeginFrame(client, {
20015
20316
  frameTimeTicks: frameTimeTicks + 1e-3,
20016
20317
  interval,
20017
- screenshot: {
20018
- format,
20019
- quality: format === "jpeg" ? options.quality ?? 80 : void 0,
20020
- optimizeForSpeed: true
20021
- }
20318
+ screenshot
20022
20319
  });
20023
- buffer = retry.screenshotData ? Buffer.from(retry.screenshotData, "base64") : Buffer.alloc(0);
20320
+ buffer = fallback.screenshotData ? Buffer.from(fallback.screenshotData, "base64") : Buffer.alloc(0);
20024
20321
  if (buffer.length > 0) lastFrameCache.set(page, buffer);
20025
20322
  }
20026
20323
  }
@@ -20134,13 +20431,14 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
20134
20431
  }
20135
20432
  }, activeVideoIds);
20136
20433
  }
20137
- var cdpSessionCache, lastFrameCache;
20434
+ var cdpSessionCache, lastFrameCache, PENDING_FRAME_RETRIES;
20138
20435
  var init_screenshotService = __esm({
20139
20436
  "../engine/src/services/screenshotService.ts"() {
20140
20437
  "use strict";
20141
20438
  init_src();
20142
20439
  cdpSessionCache = /* @__PURE__ */ new WeakMap();
20143
20440
  lastFrameCache = /* @__PURE__ */ new WeakMap();
20441
+ PENDING_FRAME_RETRIES = 5;
20144
20442
  }
20145
20443
  });
20146
20444
 
@@ -20472,7 +20770,7 @@ var init_frameCapture = __esm({
20472
20770
  // ../engine/src/utils/gpuEncoder.ts
20473
20771
  import { spawn as spawn2 } from "child_process";
20474
20772
  async function detectGpuEncoder() {
20475
- return new Promise((resolve27) => {
20773
+ return new Promise((resolve28) => {
20476
20774
  const ffmpeg = spawn2("ffmpeg", ["-encoders"], {
20477
20775
  stdio: ["pipe", "pipe", "pipe"]
20478
20776
  });
@@ -20481,13 +20779,13 @@ async function detectGpuEncoder() {
20481
20779
  stdout2 += data.toString();
20482
20780
  });
20483
20781
  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);
20782
+ if (stdout2.includes("h264_nvenc")) resolve28("nvenc");
20783
+ else if (stdout2.includes("h264_videotoolbox")) resolve28("videotoolbox");
20784
+ else if (stdout2.includes("h264_vaapi")) resolve28("vaapi");
20785
+ else if (stdout2.includes("h264_qsv")) resolve28("qsv");
20786
+ else resolve28(null);
20489
20787
  });
20490
- ffmpeg.on("error", () => resolve27(null));
20788
+ ffmpeg.on("error", () => resolve28(null));
20491
20789
  });
20492
20790
  }
20493
20791
  async function getCachedGpuEncoder() {
@@ -20526,7 +20824,7 @@ async function runFfmpeg(args, opts) {
20526
20824
  const signal = opts?.signal;
20527
20825
  const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
20528
20826
  const onStderr = opts?.onStderr;
20529
- return new Promise((resolve27) => {
20827
+ return new Promise((resolve28) => {
20530
20828
  const ffmpeg = spawn3("ffmpeg", args);
20531
20829
  let stderr = "";
20532
20830
  const onAbort = () => {
@@ -20552,7 +20850,7 @@ async function runFfmpeg(args, opts) {
20552
20850
  ffmpeg.on("close", (code) => {
20553
20851
  clearTimeout(timer);
20554
20852
  if (signal) signal.removeEventListener("abort", onAbort);
20555
- resolve27({
20853
+ resolve28({
20556
20854
  success: !signal?.aborted && code === 0,
20557
20855
  exitCode: code,
20558
20856
  stderr,
@@ -20562,7 +20860,7 @@ async function runFfmpeg(args, opts) {
20562
20860
  ffmpeg.on("error", (err) => {
20563
20861
  clearTimeout(timer);
20564
20862
  if (signal) signal.removeEventListener("abort", onAbort);
20565
- resolve27({
20863
+ resolve28({
20566
20864
  success: false,
20567
20865
  exitCode: null,
20568
20866
  stderr: err.message,
@@ -20721,7 +21019,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20721
21019
  const inputPath = join17(framesDir, framePattern);
20722
21020
  const inputArgs = ["-framerate", String(options.fps), "-i", inputPath];
20723
21021
  const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder);
20724
- return new Promise((resolve27) => {
21022
+ return new Promise((resolve28) => {
20725
21023
  const ffmpeg = spawn4("ffmpeg", args);
20726
21024
  let stderr = "";
20727
21025
  const onAbort = () => {
@@ -20746,7 +21044,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20746
21044
  if (signal) signal.removeEventListener("abort", onAbort);
20747
21045
  const durationMs = Date.now() - startTime;
20748
21046
  if (signal?.aborted) {
20749
- resolve27({
21047
+ resolve28({
20750
21048
  success: false,
20751
21049
  outputPath,
20752
21050
  durationMs,
@@ -20757,7 +21055,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20757
21055
  return;
20758
21056
  }
20759
21057
  if (code !== 0) {
20760
- resolve27({
21058
+ resolve28({
20761
21059
  success: false,
20762
21060
  outputPath,
20763
21061
  durationMs,
@@ -20768,12 +21066,12 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
20768
21066
  return;
20769
21067
  }
20770
21068
  const fileSize = existsSync15(outputPath) ? statSync4(outputPath).size : 0;
20771
- resolve27({ success: true, outputPath, durationMs, framesEncoded: frameCount, fileSize });
21069
+ resolve28({ success: true, outputPath, durationMs, framesEncoded: frameCount, fileSize });
20772
21070
  });
20773
21071
  ffmpeg.on("error", (err) => {
20774
21072
  clearTimeout(timer);
20775
21073
  if (signal) signal.removeEventListener("abort", onAbort);
20776
- resolve27({
21074
+ resolve28({
20777
21075
  success: false,
20778
21076
  outputPath,
20779
21077
  durationMs: Date.now() - startTime,
@@ -20831,18 +21129,18 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
20831
21129
  let gpuEncoder = null;
20832
21130
  if (options.useGpu) gpuEncoder = await getCachedGpuEncoder();
20833
21131
  const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder);
20834
- const chunkResult = await new Promise((resolve27) => {
21132
+ const chunkResult = await new Promise((resolve28) => {
20835
21133
  const ffmpeg = spawn4("ffmpeg", args);
20836
21134
  let stderr = "";
20837
21135
  ffmpeg.stderr.on("data", (d) => {
20838
21136
  stderr += d.toString();
20839
21137
  });
20840
21138
  ffmpeg.on("close", (code) => {
20841
- if (code === 0) resolve27({ success: true });
20842
- else resolve27({ success: false, error: `Chunk ${i} encode failed: ${stderr.slice(-400)}` });
21139
+ if (code === 0) resolve28({ success: true });
21140
+ else resolve28({ success: false, error: `Chunk ${i} encode failed: ${stderr.slice(-400)}` });
20843
21141
  });
20844
21142
  ffmpeg.on("error", (err) => {
20845
- resolve27({ success: false, error: `Chunk ${i} encode error: ${err.message}` });
21143
+ resolve28({ success: false, error: `Chunk ${i} encode error: ${err.message}` });
20846
21144
  });
20847
21145
  });
20848
21146
  if (!chunkResult.success) {
@@ -20872,18 +21170,18 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
20872
21170
  "-y",
20873
21171
  outputPath
20874
21172
  ];
20875
- const concatResult = await new Promise((resolve27) => {
21173
+ const concatResult = await new Promise((resolve28) => {
20876
21174
  const ffmpeg = spawn4("ffmpeg", concatArgs);
20877
21175
  let stderr = "";
20878
21176
  ffmpeg.stderr.on("data", (d) => {
20879
21177
  stderr += d.toString();
20880
21178
  });
20881
21179
  ffmpeg.on("close", (code) => {
20882
- if (code === 0) resolve27({ success: true });
20883
- else resolve27({ success: false, error: `Chunk concat failed: ${stderr.slice(-400)}` });
21180
+ if (code === 0) resolve28({ success: true });
21181
+ else resolve28({ success: false, error: `Chunk concat failed: ${stderr.slice(-400)}` });
20884
21182
  });
20885
21183
  ffmpeg.on("error", (err) => {
20886
- resolve27({ success: false, error: `Chunk concat error: ${err.message}` });
21184
+ resolve28({ success: false, error: `Chunk concat error: ${err.message}` });
20887
21185
  });
20888
21186
  });
20889
21187
  if (!concatResult.success) {
@@ -20991,16 +21289,16 @@ function createFrameReorderBuffer(startFrame, endFrame) {
20991
21289
  }
20992
21290
  };
20993
21291
  return {
20994
- waitForFrame: (frame) => new Promise((resolve27) => {
20995
- waiters.push({ frame, resolve: resolve27 });
21292
+ waitForFrame: (frame) => new Promise((resolve28) => {
21293
+ waiters.push({ frame, resolve: resolve28 });
20996
21294
  resolveWaiters();
20997
21295
  }),
20998
21296
  advanceTo: (frame) => {
20999
21297
  nextFrame = frame;
21000
21298
  resolveWaiters();
21001
21299
  },
21002
- waitForAllDone: () => new Promise((resolve27) => {
21003
- waiters.push({ frame: endFrame, resolve: resolve27 });
21300
+ waitForAllDone: () => new Promise((resolve28) => {
21301
+ waiters.push({ frame: endFrame, resolve: resolve28 });
21004
21302
  resolveWaiters();
21005
21303
  })
21006
21304
  };
@@ -21129,7 +21427,7 @@ async function spawnStreamingEncoder(outputPath, options, signal, config) {
21129
21427
  let stderr = "";
21130
21428
  let exitCode = null;
21131
21429
  let exitPromiseResolve = null;
21132
- const exitPromise = new Promise((resolve27) => exitPromiseResolve = resolve27);
21430
+ const exitPromise = new Promise((resolve28) => exitPromiseResolve = resolve28);
21133
21431
  ffmpeg.stderr?.on("data", (data) => {
21134
21432
  stderr += data.toString();
21135
21433
  });
@@ -21173,8 +21471,8 @@ Process error: ${err.message}`;
21173
21471
  clearTimeout(timer);
21174
21472
  if (signal) signal.removeEventListener("abort", onAbort);
21175
21473
  if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
21176
- await new Promise((resolve27) => {
21177
- ffmpeg.stdin.end(() => resolve27());
21474
+ await new Promise((resolve28) => {
21475
+ ffmpeg.stdin.end(() => resolve28());
21178
21476
  });
21179
21477
  }
21180
21478
  await exitPromise;
@@ -21213,7 +21511,7 @@ var init_streamingEncoder = __esm({
21213
21511
  // ../engine/src/utils/ffprobe.ts
21214
21512
  import { spawn as spawn6 } from "child_process";
21215
21513
  function runFfprobe(args) {
21216
- return new Promise((resolve27, reject) => {
21514
+ return new Promise((resolve28, reject) => {
21217
21515
  const proc = spawn6("ffprobe", args);
21218
21516
  let stdout2 = "";
21219
21517
  let stderr = "";
@@ -21227,7 +21525,7 @@ function runFfprobe(args) {
21227
21525
  if (code !== 0) {
21228
21526
  reject(new Error(`[FFmpeg] ffprobe exited with code ${code}: ${stderr}`));
21229
21527
  } else {
21230
- resolve27(stdout2);
21528
+ resolve28(stdout2);
21231
21529
  }
21232
21530
  });
21233
21531
  proc.on("error", (err) => {
@@ -21524,7 +21822,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21524
21822
  ];
21525
21823
  if (format === "png") args.push("-compression_level", "6");
21526
21824
  args.push("-y", outputPattern);
21527
- return new Promise((resolve27, reject) => {
21825
+ return new Promise((resolve28, reject) => {
21528
21826
  const ffmpeg = spawn7("ffmpeg", args);
21529
21827
  let stderr = "";
21530
21828
  const onAbort = () => {
@@ -21559,7 +21857,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21559
21857
  files.forEach((file, index) => {
21560
21858
  framePaths.set(index, join19(videoOutputDir, file));
21561
21859
  });
21562
- resolve27({
21860
+ resolve28({
21563
21861
  videoId,
21564
21862
  srcPath: videoPath,
21565
21863
  outputDir: videoOutputDir,
@@ -21581,7 +21879,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
21581
21879
  });
21582
21880
  });
21583
21881
  }
21584
- async function extractAllVideoFrames(videos, baseDir, options, signal, config) {
21882
+ async function extractAllVideoFrames(videos, baseDir, options, signal, config, compiledDir) {
21585
21883
  const startTime = Date.now();
21586
21884
  const extracted = [];
21587
21885
  const errors = [];
@@ -21594,7 +21892,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config) {
21594
21892
  try {
21595
21893
  let videoPath = video.src;
21596
21894
  if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
21597
- videoPath = join19(baseDir, videoPath);
21895
+ const fromCompiled = compiledDir ? join19(compiledDir, videoPath) : null;
21896
+ videoPath = fromCompiled && existsSync18(fromCompiled) ? fromCompiled : join19(baseDir, videoPath);
21598
21897
  }
21599
21898
  if (isHttpUrl(videoPath)) {
21600
21899
  const downloadDir = join19(options.outputDir, "_downloads");
@@ -22072,7 +22371,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
22072
22371
  tracksProcessed: tracks.length
22073
22372
  };
22074
22373
  }
22075
- async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config) {
22374
+ async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config, compiledDir) {
22076
22375
  const startMs = Date.now();
22077
22376
  const tracks = [];
22078
22377
  const errors = [];
@@ -22086,7 +22385,8 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
22086
22385
  try {
22087
22386
  let srcPath = element.src;
22088
22387
  if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
22089
- srcPath = join20(baseDir, srcPath);
22388
+ const fromCompiled = compiledDir ? join20(compiledDir, srcPath) : null;
22389
+ srcPath = fromCompiled && existsSync19(fromCompiled) ? fromCompiled : join20(baseDir, srcPath);
22090
22390
  }
22091
22391
  if (isHttpUrl(srcPath)) {
22092
22392
  try {
@@ -22099,7 +22399,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
22099
22399
  }
22100
22400
  }
22101
22401
  if (!existsSync19(srcPath)) {
22102
- errors.push(`Source not found: ${element.id}`);
22402
+ errors.push(`Source not found: ${element.id} (${element.src})`);
22103
22403
  return;
22104
22404
  }
22105
22405
  if (element.end - element.start <= 0) {
@@ -22460,11 +22760,11 @@ function createFileServer(options) {
22460
22760
  headers: { "Content-Type": contentType }
22461
22761
  });
22462
22762
  });
22463
- return new Promise((resolve27) => {
22763
+ return new Promise((resolve28) => {
22464
22764
  const server = serve({ fetch: app.fetch, port }, (info) => {
22465
22765
  const actualPort = info.port;
22466
22766
  const url = `http://localhost:${actualPort}`;
22467
- resolve27({
22767
+ resolve28({
22468
22768
  url,
22469
22769
  port: actualPort,
22470
22770
  close: () => server.close()
@@ -22578,9 +22878,9 @@ var init_src2 = __esm({
22578
22878
  });
22579
22879
 
22580
22880
  // ../core/src/compiler/htmlCompiler.ts
22581
- import { resolve as resolve7 } from "path";
22881
+ import { resolve as resolve8 } from "path";
22582
22882
  function resolveMediaSrc(src, projectDir) {
22583
- return src.startsWith("http://") || src.startsWith("https://") ? src : resolve7(projectDir, src);
22883
+ return src.startsWith("http://") || src.startsWith("https://") ? src : resolve8(projectDir, src);
22584
22884
  }
22585
22885
  async function compileHtml(rawHtml, projectDir, probeMediaDuration) {
22586
22886
  const { html: staticCompiled, unresolved } = compileTimingAttrs(rawHtml);
@@ -22659,7 +22959,7 @@ var init_staticGuard = __esm({
22659
22959
 
22660
22960
  // ../core/src/compiler/htmlBundler.ts
22661
22961
  import { readFileSync as readFileSync14, existsSync as existsSync22 } from "fs";
22662
- import { join as join23, resolve as resolve8, isAbsolute, sep as sep2 } from "path";
22962
+ import { join as join23, resolve as resolve9, isAbsolute, sep as sep2 } from "path";
22663
22963
  import { transformSync } from "esbuild";
22664
22964
  function parseHTMLContent(html) {
22665
22965
  const trimmed = html.trimStart().toLowerCase();
@@ -22669,9 +22969,9 @@ function parseHTMLContent(html) {
22669
22969
  return parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`).document;
22670
22970
  }
22671
22971
  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;
22972
+ const resolved = resolve9(projectDir, relativePath);
22973
+ const normalizedBase = resolve9(projectDir) + sep2;
22974
+ if (!resolved.startsWith(normalizedBase) && resolved !== resolve9(projectDir)) return null;
22675
22975
  return resolved;
22676
22976
  }
22677
22977
  function stripEmbeddedRuntimeScripts2(html) {
@@ -23206,7 +23506,7 @@ var init_compiler = __esm({
23206
23506
  // ../producer/src/services/hyperframeRuntimeLoader.ts
23207
23507
  import { createHash as createHash2 } from "crypto";
23208
23508
  import { existsSync as existsSync23, readFileSync as readFileSync15 } from "fs";
23209
- import { dirname as dirname7, resolve as resolve9 } from "path";
23509
+ import { dirname as dirname7, resolve as resolve10 } from "path";
23210
23510
  import { fileURLToPath as fileURLToPath2 } from "url";
23211
23511
  function resolveHyperframeManifestPath() {
23212
23512
  if (process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH) {
@@ -23242,7 +23542,7 @@ function resolveVerifiedHyperframeRuntime() {
23242
23542
  `[HyperframeRuntimeLoader] Invalid manifest at ${manifestPath}; missing iife artifact or sha256.`
23243
23543
  );
23244
23544
  }
23245
- const runtimePath = resolve9(dirname7(manifestPath), runtimeFileName);
23545
+ const runtimePath = resolve10(dirname7(manifestPath), runtimeFileName);
23246
23546
  if (!existsSync23(runtimePath)) {
23247
23547
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
23248
23548
  }
@@ -23266,18 +23566,18 @@ var init_hyperframeRuntimeLoader = __esm({
23266
23566
  "../producer/src/services/hyperframeRuntimeLoader.ts"() {
23267
23567
  "use strict";
23268
23568
  PRODUCER_DIR = dirname7(fileURLToPath2(import.meta.url));
23269
- SIBLING_MANIFEST_PATH = resolve9(PRODUCER_DIR, "hyperframe.manifest.json");
23270
- MODULE_RELATIVE_MANIFEST_PATH = resolve9(
23569
+ SIBLING_MANIFEST_PATH = resolve10(PRODUCER_DIR, "hyperframe.manifest.json");
23570
+ MODULE_RELATIVE_MANIFEST_PATH = resolve10(
23271
23571
  PRODUCER_DIR,
23272
23572
  "../../../core/dist/hyperframe.manifest.json"
23273
23573
  );
23274
23574
  CWD_RELATIVE_MANIFEST_PATHS = [
23275
23575
  // When bundled to a single file (dist/public-server.js), the manifest
23276
23576
  // 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")
23577
+ resolve10(PRODUCER_DIR, "hyperframe.manifest.json"),
23578
+ resolve10(process.cwd(), "packages/core/dist/hyperframe.manifest.json"),
23579
+ resolve10(process.cwd(), "../core/dist/hyperframe.manifest.json"),
23580
+ resolve10(process.cwd(), "core/dist/hyperframe.manifest.json")
23281
23581
  ];
23282
23582
  }
23283
23583
  });
@@ -23380,10 +23680,10 @@ function createFileServer2(options) {
23380
23680
  headers: { "Content-Type": contentType }
23381
23681
  });
23382
23682
  });
23383
- return new Promise((resolve27) => {
23683
+ return new Promise((resolve28) => {
23384
23684
  const connections = /* @__PURE__ */ new Set();
23385
23685
  const server = serve2({ fetch: app.fetch, port }, (info) => {
23386
- resolve27({
23686
+ resolve28({
23387
23687
  url: `http://localhost:${info.port}`,
23388
23688
  port: info.port,
23389
23689
  close: () => {
@@ -23906,7 +24206,7 @@ var init_deterministicFonts = __esm({
23906
24206
 
23907
24207
  // ../producer/src/services/htmlCompiler.ts
23908
24208
  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";
24209
+ import { join as join25, dirname as dirname8, resolve as resolve11 } from "path";
23910
24210
  import postcss from "postcss";
23911
24211
  function dedupeElementsById(elements) {
23912
24212
  const deduped = /* @__PURE__ */ new Map();
@@ -23991,7 +24291,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
23991
24291
  const elEnd = elEndRaw ? parseFloat(elEndRaw) : Infinity;
23992
24292
  const absoluteStart = parentOffset + elStart;
23993
24293
  const absoluteEnd = Math.min(parentEnd, isFinite(elEnd) ? parentOffset + elEnd : Infinity);
23994
- const filePath = resolve10(projectDir, srcPath);
24294
+ const filePath = resolve11(projectDir, srcPath);
23995
24295
  if (visited.has(filePath)) {
23996
24296
  continue;
23997
24297
  }
@@ -24191,7 +24491,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
24191
24491
  if (!srcPath) continue;
24192
24492
  let compHtml = subCompositions.get(srcPath) || null;
24193
24493
  if (!compHtml) {
24194
- const filePath = resolve10(projectDir, srcPath);
24494
+ const filePath = resolve11(projectDir, srcPath);
24195
24495
  if (existsSync25(filePath)) {
24196
24496
  compHtml = readFileSync17(filePath, "utf-8");
24197
24497
  }
@@ -24404,7 +24704,7 @@ ${safeText}
24404
24704
  return result;
24405
24705
  }
24406
24706
  function collectExternalAssets(html, projectDir) {
24407
- const absProjectDir = resolve10(projectDir);
24707
+ const absProjectDir = resolve11(projectDir);
24408
24708
  const externalAssets = /* @__PURE__ */ new Map();
24409
24709
  const CSS_URL_RE2 = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
24410
24710
  function processPath(rawPath) {
@@ -24412,7 +24712,7 @@ function collectExternalAssets(html, projectDir) {
24412
24712
  if (!trimmed || trimmed.startsWith("/") || trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("//") || trimmed.startsWith("data:") || trimmed.startsWith("#")) {
24413
24713
  return null;
24414
24714
  }
24415
- const absPath = resolve10(absProjectDir, trimmed);
24715
+ const absPath = resolve11(absProjectDir, trimmed);
24416
24716
  if (absPath.startsWith(absProjectDir + "/") || absPath === absProjectDir) {
24417
24717
  return null;
24418
24718
  }
@@ -24489,7 +24789,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
24489
24789
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
24490
24790
  for (const video of videos) {
24491
24791
  if (isHttpUrl(video.src)) continue;
24492
- const videoPath = resolve10(projectDir, video.src);
24792
+ const videoPath = resolve11(projectDir, video.src);
24493
24793
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
24494
24794
  Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
24495
24795
  if (analysis.isProblematic) {
@@ -24682,7 +24982,7 @@ import {
24682
24982
  copyFileSync as copyFileSync2,
24683
24983
  appendFileSync
24684
24984
  } from "fs";
24685
- import { join as join26, dirname as dirname9, resolve as resolve11 } from "path";
24985
+ import { join as join26, dirname as dirname9, resolve as resolve12 } from "path";
24686
24986
  import { randomUUID as randomUUID2 } from "crypto";
24687
24987
  import { freemem as freemem2 } from "os";
24688
24988
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -24747,7 +25047,7 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
24747
25047
  writeFileSync8(outPath, html, "utf-8");
24748
25048
  }
24749
25049
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
24750
- const outPath = resolve11(join26(compileDir, relativePath));
25050
+ const outPath = resolve12(join26(compileDir, relativePath));
24751
25051
  if (!outPath.startsWith(compileDir + "/")) {
24752
25052
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
24753
25053
  continue;
@@ -24820,7 +25120,7 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
24820
25120
  }
24821
25121
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
24822
25122
  const moduleDir = dirname9(fileURLToPath3(import.meta.url));
24823
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve11(process.env.PRODUCER_RENDERS_DIR, "..") : resolve11(moduleDir, "../..");
25123
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve12(process.env.PRODUCER_RENDERS_DIR, "..") : resolve12(moduleDir, "../..");
24824
25124
  const debugDir = join26(producerRoot, ".debug");
24825
25125
  const workDir = job.config.debug ? join26(debugDir, job.id) : join26(dirname9(outputPath), `work-${job.id}`);
24826
25126
  const pipelineStart = Date.now();
@@ -25097,12 +25397,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25097
25397
  const stage2Start = Date.now();
25098
25398
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
25099
25399
  let frameLookup = null;
25400
+ const compiledDir = join26(workDir, "compiled");
25100
25401
  if (composition.videos.length > 0) {
25101
25402
  const extractionResult = await extractAllVideoFrames(
25102
25403
  composition.videos,
25103
25404
  projectDir,
25104
25405
  { fps: job.config.fps, outputDir: join26(workDir, "video-frames") },
25105
- abortSignal
25406
+ abortSignal,
25407
+ void 0,
25408
+ compiledDir
25106
25409
  );
25107
25410
  assertNotAborted();
25108
25411
  if (extractionResult.extracted.length > 0) {
@@ -25142,7 +25445,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25142
25445
  join26(workDir, "audio-work"),
25143
25446
  audioOutputPath,
25144
25447
  job.duration,
25145
- abortSignal
25448
+ abortSignal,
25449
+ void 0,
25450
+ compiledDir
25146
25451
  );
25147
25452
  assertNotAborted();
25148
25453
  hasAudio = audioResult.success;
@@ -25589,7 +25894,7 @@ var init_config3 = __esm({
25589
25894
 
25590
25895
  // ../producer/src/services/hyperframeLint.ts
25591
25896
  import { existsSync as existsSync27, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
25592
- import { resolve as resolve12, join as join27 } from "path";
25897
+ import { resolve as resolve13, join as join27 } from "path";
25593
25898
  function isStringRecord(value) {
25594
25899
  if (!value || typeof value !== "object" || Array.isArray(value)) {
25595
25900
  return false;
@@ -25616,7 +25921,7 @@ function pickEntryFile(files, preferredEntryFile) {
25616
25921
  return null;
25617
25922
  }
25618
25923
  function readProjectEntryFile(projectDir, preferredEntryFile) {
25619
- const absProjectDir = resolve12(projectDir);
25924
+ const absProjectDir = resolve13(projectDir);
25620
25925
  if (!existsSync27(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
25621
25926
  return { error: `Project directory not found: ${absProjectDir}` };
25622
25927
  }
@@ -25624,7 +25929,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
25624
25929
  (value) => typeof value === "string" && value.trim().length > 0
25625
25930
  );
25626
25931
  for (const entryFile of entryCandidates) {
25627
- const absoluteEntryPath = resolve12(absProjectDir, entryFile);
25932
+ const absoluteEntryPath = resolve13(absProjectDir, entryFile);
25628
25933
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
25629
25934
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
25630
25935
  }
@@ -25684,19 +25989,59 @@ var init_hyperframeLint = __esm({
25684
25989
  });
25685
25990
 
25686
25991
  // ../producer/src/utils/paths.ts
25687
- import { resolve as resolve13, basename, join as join28 } from "path";
25992
+ import { resolve as resolve14, basename, join as join28 } from "path";
25688
25993
  function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
25689
- const absoluteProjectDir = resolve13(projectDir);
25994
+ const absoluteProjectDir = resolve14(projectDir);
25690
25995
  const projectName = basename(absoluteProjectDir);
25691
25996
  const resolvedOutputPath = outputPath ?? join28(rendersDir, `${projectName}.mp4`);
25692
- const absoluteOutputPath = resolve13(resolvedOutputPath);
25997
+ const absoluteOutputPath = resolve14(resolvedOutputPath);
25693
25998
  return { absoluteProjectDir, absoluteOutputPath };
25694
25999
  }
25695
26000
  var DEFAULT_RENDERS_DIR;
25696
26001
  var init_paths = __esm({
25697
26002
  "../producer/src/utils/paths.ts"() {
25698
26003
  "use strict";
25699
- DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve13(new URL(import.meta.url).pathname, "../../..", "renders");
26004
+ DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve14(new URL(import.meta.url).pathname, "../../..", "renders");
26005
+ }
26006
+ });
26007
+
26008
+ // ../producer/src/utils/semaphore.ts
26009
+ var Semaphore;
26010
+ var init_semaphore = __esm({
26011
+ "../producer/src/utils/semaphore.ts"() {
26012
+ "use strict";
26013
+ Semaphore = class {
26014
+ constructor(maxConcurrent) {
26015
+ this.maxConcurrent = maxConcurrent;
26016
+ }
26017
+ queue = [];
26018
+ active = 0;
26019
+ async acquire() {
26020
+ if (this.active < this.maxConcurrent) {
26021
+ this.active++;
26022
+ return () => this.release();
26023
+ }
26024
+ return new Promise((resolve28) => {
26025
+ this.queue.push(() => {
26026
+ this.active++;
26027
+ resolve28(() => this.release());
26028
+ });
26029
+ });
26030
+ }
26031
+ release() {
26032
+ this.active--;
26033
+ const next = this.queue.shift();
26034
+ if (next) next();
26035
+ }
26036
+ /** Current number of active slots. */
26037
+ get activeCount() {
26038
+ return this.active;
26039
+ }
26040
+ /** Number of waiters in the queue. */
26041
+ get waitingCount() {
26042
+ return this.queue.length;
26043
+ }
26044
+ };
25700
26045
  }
25701
26046
  });
25702
26047
 
@@ -25710,7 +26055,7 @@ import {
25710
26055
  rmSync as rmSync7,
25711
26056
  createReadStream
25712
26057
  } from "fs";
25713
- import { resolve as resolve14, dirname as dirname10, join as join29 } from "path";
26058
+ import { resolve as resolve15, dirname as dirname10, join as join29 } from "path";
25714
26059
  import { tmpdir as tmpdir2 } from "os";
25715
26060
  import { parseArgs as parseArgs2 } from "util";
25716
26061
  import crypto from "crypto";
@@ -25732,12 +26077,12 @@ async function prepareRenderBody(body) {
25732
26077
  const options = parseRenderOptions(body);
25733
26078
  const projectDir = typeof body.projectDir === "string" ? body.projectDir : void 0;
25734
26079
  if (projectDir) {
25735
- const absProjectDir = resolve14(projectDir);
26080
+ const absProjectDir = resolve15(projectDir);
25736
26081
  if (!existsSync28(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
25737
26082
  return { error: `Project directory not found: ${absProjectDir}` };
25738
26083
  }
25739
26084
  const entry = options.entryFile || "index.html";
25740
- if (!existsSync28(resolve14(absProjectDir, entry))) {
26085
+ if (!existsSync28(resolve15(absProjectDir, entry))) {
25741
26086
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
25742
26087
  }
25743
26088
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -25778,7 +26123,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
25778
26123
  try {
25779
26124
  return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
25780
26125
  } catch (error) {
25781
- const fallbackPath = resolve14(rendersDir, `producer-fallback-${Date.now()}.mp4`);
26126
+ const fallbackPath = resolve15(rendersDir, `producer-fallback-${Date.now()}.mp4`);
25782
26127
  log.warn("Failed to resolve output path, using fallback", {
25783
26128
  fallback: fallbackPath,
25784
26129
  error: error instanceof Error ? error.message : String(error)
@@ -25829,6 +26174,8 @@ function createRenderHandlers(options = {}) {
25829
26174
  const rendersDir = options.rendersDir ?? process.env.PRODUCER_RENDERS_DIR ?? "/tmp";
25830
26175
  const artifactTtlMs = options.artifactTtlMs ?? Number(process.env.PRODUCER_OUTPUT_ARTIFACT_TTL_MS || 15 * 60 * 1e3);
25831
26176
  const store = createArtifactStore(artifactTtlMs);
26177
+ const maxConcurrentRenders = options.maxConcurrentRenders ?? Number(process.env.PRODUCER_MAX_CONCURRENT_RENDERS || 2);
26178
+ const renderSemaphore = new Semaphore(maxConcurrentRenders);
25832
26179
  const startTime = Date.now();
25833
26180
  const health = (c2) => c2.json({
25834
26181
  status: "ok",
@@ -25885,6 +26232,7 @@ function createRenderHandlers(options = {}) {
25885
26232
  );
25886
26233
  const outputDir = dirname10(absoluteOutputPath);
25887
26234
  if (!existsSync28(outputDir)) mkdirSync17(outputDir, { recursive: true });
26235
+ const release2 = await renderSemaphore.acquire();
25888
26236
  log.info("render started", {
25889
26237
  requestId,
25890
26238
  projectDir: input.projectDir,
@@ -25952,6 +26300,7 @@ function createRenderHandlers(options = {}) {
25952
26300
  500
25953
26301
  );
25954
26302
  } finally {
26303
+ release2();
25955
26304
  cleanupTempDir(cleanupProjectDir, log);
25956
26305
  }
25957
26306
  };
@@ -26008,6 +26357,16 @@ function createRenderHandlers(options = {}) {
26008
26357
  const abortController = new AbortController();
26009
26358
  const onRequestAbort = () => abortController.abort(new RenderCancelledError("request_aborted"));
26010
26359
  c2.req.raw.signal.addEventListener("abort", onRequestAbort, { once: true });
26360
+ if (renderSemaphore.activeCount >= maxConcurrentRenders) {
26361
+ await stream.writeSSE({
26362
+ data: JSON.stringify({
26363
+ type: "queued",
26364
+ requestId,
26365
+ position: renderSemaphore.waitingCount
26366
+ })
26367
+ });
26368
+ }
26369
+ const release2 = await renderSemaphore.acquire();
26011
26370
  try {
26012
26371
  await executeRenderJob(
26013
26372
  job,
@@ -26075,6 +26434,7 @@ function createRenderHandlers(options = {}) {
26075
26434
  })
26076
26435
  });
26077
26436
  } finally {
26437
+ release2();
26078
26438
  c2.req.raw.signal.removeEventListener("abort", onRequestAbort);
26079
26439
  cleanupTempDir(cleanupProjectDir, log);
26080
26440
  }
@@ -26099,7 +26459,12 @@ function createRenderHandlers(options = {}) {
26099
26459
  }
26100
26460
  });
26101
26461
  };
26102
- return { render: render2, renderStream, lint, health, outputs };
26462
+ const queue = (c2) => c2.json({
26463
+ maxConcurrentRenders,
26464
+ activeRenders: renderSemaphore.activeCount,
26465
+ queuedRenders: renderSemaphore.waitingCount
26466
+ });
26467
+ return { render: render2, renderStream, lint, health, outputs, queue };
26103
26468
  }
26104
26469
  function createProducerApp(options = {}) {
26105
26470
  const app = new Hono4();
@@ -26107,6 +26472,7 @@ function createProducerApp(options = {}) {
26107
26472
  app.get("/health", handlers.health);
26108
26473
  app.post("/render", handlers.render);
26109
26474
  app.post("/render/stream", handlers.renderStream);
26475
+ app.get("/render/queue", handlers.queue);
26110
26476
  app.post("/lint", handlers.lint);
26111
26477
  app.get("/outputs/:token", handlers.outputs);
26112
26478
  return app;
@@ -26144,7 +26510,8 @@ var init_server = __esm({
26144
26510
  init_hyperframeLint();
26145
26511
  init_paths();
26146
26512
  init_logger();
26147
- entryScript = process.argv[1] ? resolve14(process.argv[1]) : "";
26513
+ init_semaphore();
26514
+ entryScript = process.argv[1] ? resolve15(process.argv[1]) : "";
26148
26515
  isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
26149
26516
  if (isPublicServerEntry) {
26150
26517
  const { values } = parseArgs2({
@@ -26217,18 +26584,18 @@ __export(studioServer_exports, {
26217
26584
  import { Hono as Hono5 } from "hono";
26218
26585
  import { streamSSE as streamSSE3 } from "hono/streaming";
26219
26586
  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";
26587
+ import { resolve as resolve16, join as join30, basename as basename2 } from "path";
26221
26588
  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;
26589
+ const builtPath = resolve16(__dirname, "studio");
26590
+ if (existsSync29(resolve16(builtPath, "index.html"))) return builtPath;
26591
+ const devPath = resolve16(__dirname, "..", "..", "..", "studio", "dist");
26592
+ if (existsSync29(resolve16(devPath, "index.html"))) return devPath;
26226
26593
  return builtPath;
26227
26594
  }
26228
26595
  function resolveRuntimePath() {
26229
- const builtPath = resolve15(__dirname, "hyperframe-runtime.js");
26596
+ const builtPath = resolve16(__dirname, "hyperframe-runtime.js");
26230
26597
  if (existsSync29(builtPath)) return builtPath;
26231
- const devPath = resolve15(
26598
+ const devPath = resolve16(
26232
26599
  __dirname,
26233
26600
  "..",
26234
26601
  "..",
@@ -26379,6 +26746,14 @@ function createStudioServer(options) {
26379
26746
  }
26380
26747
  };
26381
26748
  const app = new Hono5();
26749
+ app.get("/__hyperframes_config", (c2) => {
26750
+ return c2.json({
26751
+ isHyperframes: true,
26752
+ projectName: projectId,
26753
+ projectDir,
26754
+ version: VERSION
26755
+ });
26756
+ });
26382
26757
  app.get("/api/runtime.js", (c2) => {
26383
26758
  if (!existsSync29(runtimePath)) return c2.text("runtime not built", 404);
26384
26759
  return c2.body(readFileSync20(runtimePath, "utf-8"), 200, {
@@ -26412,7 +26787,7 @@ function createStudioServer(options) {
26412
26787
  return api.fetch(forwardReq);
26413
26788
  });
26414
26789
  app.get("/assets/*", (c2) => {
26415
- const filePath = resolve15(studioDir, c2.req.path.slice(1));
26790
+ const filePath = resolve16(studioDir, c2.req.path.slice(1));
26416
26791
  if (!existsSync29(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
26417
26792
  const content = readFileSync20(filePath);
26418
26793
  return new Response(content, {
@@ -26420,7 +26795,7 @@ function createStudioServer(options) {
26420
26795
  });
26421
26796
  });
26422
26797
  app.get("/icons/*", (c2) => {
26423
- const filePath = resolve15(studioDir, c2.req.path.slice(1));
26798
+ const filePath = resolve16(studioDir, c2.req.path.slice(1));
26424
26799
  if (!existsSync29(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
26425
26800
  const content = readFileSync20(filePath);
26426
26801
  return new Response(content, {
@@ -26428,7 +26803,7 @@ function createStudioServer(options) {
26428
26803
  });
26429
26804
  });
26430
26805
  app.get("*", (c2) => {
26431
- const indexPath = resolve15(studioDir, "index.html");
26806
+ const indexPath = resolve16(studioDir, "index.html");
26432
26807
  if (!existsSync29(indexPath)) {
26433
26808
  return c2.text("Studio not found. Rebuild with: pnpm run build", 500);
26434
26809
  }
@@ -26441,6 +26816,7 @@ var init_studioServer = __esm({
26441
26816
  "src/server/studioServer.ts"() {
26442
26817
  "use strict";
26443
26818
  init_fileWatcher();
26819
+ init_version();
26444
26820
  init_studio_api();
26445
26821
  _thumbnailBrowser = null;
26446
26822
  _thumbnailBrowserInitializing = null;
@@ -26455,45 +26831,12 @@ __export(preview_exports, {
26455
26831
  });
26456
26832
  import { spawn as spawn8 } from "child_process";
26457
26833
  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";
26834
+ import { resolve as resolve17, dirname as dirname11, basename as basename3, join as join31 } from "path";
26459
26835
  import { fileURLToPath as fileURLToPath4 } from "url";
26460
26836
  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
26837
  async function runDevMode(dir, projectName) {
26495
26838
  const thisFile = fileURLToPath4(import.meta.url);
26496
- const repoRoot = resolve16(dirname11(thisFile), "..", "..", "..", "..");
26839
+ const repoRoot = resolve17(dirname11(thisFile), "..", "..", "..", "..");
26497
26840
  const projectsDir = join31(repoRoot, "packages", "studio", "data", "projects");
26498
26841
  const pName = projectName ?? basename3(dir);
26499
26842
  const symlinkPath = join31(projectsDir, pName);
@@ -26505,7 +26848,7 @@ async function runDevMode(dir, projectName) {
26505
26848
  const stat = lstatSync(symlinkPath);
26506
26849
  if (stat.isSymbolicLink()) {
26507
26850
  const target = readlinkSync(symlinkPath);
26508
- if (resolve16(target) !== resolve16(dir)) {
26851
+ if (resolve17(target) !== resolve17(dir)) {
26509
26852
  unlinkSync5(symlinkPath);
26510
26853
  }
26511
26854
  }
@@ -26559,8 +26902,8 @@ async function runDevMode(dir, projectName) {
26559
26902
  }
26560
26903
  });
26561
26904
  }
26562
- return new Promise((resolve27) => {
26563
- child.on("close", () => resolve27());
26905
+ return new Promise((resolve28) => {
26906
+ child.on("close", () => resolve28());
26564
26907
  });
26565
26908
  }
26566
26909
  function hasLocalStudio(dir) {
@@ -26582,7 +26925,7 @@ async function runLocalStudioMode(dir, projectName) {
26582
26925
  let createdSymlink = false;
26583
26926
  if (dir !== symlinkPath) {
26584
26927
  if (existsSync30(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
26585
- if (resolve16(readlinkSync(symlinkPath)) !== resolve16(dir)) {
26928
+ if (resolve17(readlinkSync(symlinkPath)) !== resolve17(dir)) {
26586
26929
  unlinkSync5(symlinkPath);
26587
26930
  }
26588
26931
  }
@@ -26630,20 +26973,20 @@ async function runLocalStudioMode(dir, projectName) {
26630
26973
  }
26631
26974
  });
26632
26975
  }
26633
- return new Promise((resolve27) => {
26634
- child.on("close", () => resolve27());
26976
+ return new Promise((resolve28) => {
26977
+ child.on("close", () => resolve28());
26635
26978
  });
26636
26979
  }
26637
- async function runEmbeddedMode(dir, startPort, projectName) {
26980
+ async function runEmbeddedMode(dir, startPort, projectName, forceNew = false) {
26638
26981
  const { createStudioServer: createStudioServer2 } = await Promise.resolve().then(() => (init_studioServer(), studioServer_exports));
26639
26982
  const pName = projectName ?? basename3(dir);
26640
26983
  const { app } = createStudioServer2({ projectDir: dir, projectName: pName });
26641
26984
  Wt2(c.bold("hyperframes preview"));
26642
26985
  const s = be();
26643
26986
  s.start("Starting studio...");
26644
- let actualPort;
26987
+ let result;
26645
26988
  try {
26646
- ({ port: actualPort } = await serveWithPortFallback(app.fetch, startPort));
26989
+ result = await findPortAndServe(app.fetch, startPort, dir, forceNew);
26647
26990
  } catch (err) {
26648
26991
  s.stop(c.error("Failed to start studio"));
26649
26992
  console.error();
@@ -26652,11 +26995,26 @@ async function runEmbeddedMode(dir, startPort, projectName) {
26652
26995
  process.exitCode = 1;
26653
26996
  return;
26654
26997
  }
26655
- const url = `http://localhost:${actualPort}`;
26998
+ if (result.type === "already-running") {
26999
+ const url2 = `http://localhost:${result.port}`;
27000
+ s.stop(c.success("Already running"));
27001
+ console.log();
27002
+ console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
27003
+ console.log(` ${c.dim("Studio")} ${c.accent(url2)}`);
27004
+ console.log();
27005
+ console.log(
27006
+ ` ${c.dim("Reusing existing server. Use --force-new to start a fresh instance.")}`
27007
+ );
27008
+ console.log();
27009
+ import("open").then((mod) => mod.default(`${url2}#project/${pName}`)).catch(() => {
27010
+ });
27011
+ return;
27012
+ }
27013
+ const url = `http://localhost:${result.port}`;
26656
27014
  s.stop(c.success("Studio running"));
26657
27015
  console.log();
26658
- if (actualPort !== startPort) {
26659
- console.log(` ${c.warn(`Port ${startPort} is in use, using ${actualPort} instead`)}`);
27016
+ if (result.port !== startPort) {
27017
+ console.log(` ${c.warn(`Port ${startPort} is in use, using ${result.port} instead`)}`);
26660
27018
  console.log();
26661
27019
  }
26662
27020
  console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
@@ -26682,21 +27040,72 @@ var init_preview2 = __esm({
26682
27040
  init_env();
26683
27041
  init_lintProject();
26684
27042
  init_lintFormat();
27043
+ init_portUtils();
26685
27044
  examples = [
26686
27045
  ["Preview the current project", "hyperframes preview"],
26687
27046
  ["Preview a specific project directory", "hyperframes preview ./my-video"],
26688
- ["Use a custom port", "hyperframes preview --port 8080"]
27047
+ ["Use a custom port", "hyperframes preview --port 8080"],
27048
+ ["Force a new server even if one is already running", "hyperframes preview --force-new"],
27049
+ ["List all active preview servers", "hyperframes preview --list"],
27050
+ ["Kill all active preview servers", "hyperframes preview --kill-all"]
26689
27051
  ];
26690
27052
  preview_default = defineCommand({
26691
27053
  meta: { name: "preview", description: "Start the studio for previewing compositions" },
26692
27054
  args: {
26693
27055
  dir: { type: "positional", description: "Project directory", required: false },
26694
- port: { type: "string", description: "Port to run the preview server on", default: "3002" }
27056
+ port: { type: "string", description: "Port to run the preview server on", default: "3002" },
27057
+ "force-new": {
27058
+ type: "boolean",
27059
+ description: "Start a new server even if one is already running for this project",
27060
+ default: false
27061
+ },
27062
+ list: {
27063
+ type: "boolean",
27064
+ description: "List all active preview servers and exit",
27065
+ default: false
27066
+ },
27067
+ "kill-all": {
27068
+ type: "boolean",
27069
+ description: "Kill all active preview servers and exit",
27070
+ default: false
27071
+ }
26695
27072
  },
26696
27073
  async run({ args }) {
26697
- const rawArg = args.dir;
26698
- const dir = resolve16(rawArg ?? ".");
26699
27074
  const startPort = parseInt(args.port ?? "3002", 10);
27075
+ if (args.list) {
27076
+ const servers = await scanActiveServers(startPort);
27077
+ if (servers.length === 0) {
27078
+ console.log("\n No active preview servers found.\n");
27079
+ return;
27080
+ }
27081
+ console.log(`
27082
+ ${c.bold("Active preview servers:")}
27083
+ `);
27084
+ for (const s of servers) {
27085
+ const pidStr = s.pid ? c.dim(` (PID ${s.pid})`) : "";
27086
+ console.log(
27087
+ ` ${c.accent(`Port ${s.port}`)} ${s.projectName} ${c.dim(s.projectDir)}${pidStr}`
27088
+ );
27089
+ }
27090
+ console.log(`
27091
+ ${servers.length} server${servers.length === 1 ? "" : "s"} running.
27092
+ `);
27093
+ return;
27094
+ }
27095
+ if (args["kill-all"]) {
27096
+ const servers = await scanActiveServers(startPort);
27097
+ if (servers.length === 0) {
27098
+ console.log("\n No active preview servers to kill.\n");
27099
+ return;
27100
+ }
27101
+ const killed = await killActiveServers(startPort);
27102
+ console.log(`
27103
+ Killed ${killed} preview server${killed === 1 ? "" : "s"}.
27104
+ `);
27105
+ return;
27106
+ }
27107
+ const rawArg = args.dir;
27108
+ const dir = resolve17(rawArg ?? ".");
26700
27109
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
26701
27110
  const projectName = isImplicitCwd ? basename3(process.env.PWD ?? dir) : basename3(dir);
26702
27111
  const indexPath = join31(dir, "index.html");
@@ -26715,7 +27124,8 @@ var init_preview2 = __esm({
26715
27124
  if (hasLocalStudio(dir)) {
26716
27125
  return runLocalStudioMode(dir, projectName);
26717
27126
  }
26718
- return runEmbeddedMode(dir, startPort, projectName);
27127
+ const forceNew = !!args["force-new"];
27128
+ return runEmbeddedMode(dir, startPort, projectName, forceNew);
26719
27129
  }
26720
27130
  });
26721
27131
  }
@@ -26736,7 +27146,7 @@ import {
26736
27146
  readFileSync as readFileSync21,
26737
27147
  readdirSync as readdirSync10
26738
27148
  } from "fs";
26739
- import { resolve as resolve17, basename as basename4, join as join32, dirname as dirname12 } from "path";
27149
+ import { resolve as resolve18, basename as basename4, join as join32, dirname as dirname12 } from "path";
26740
27150
  import { fileURLToPath as fileURLToPath5 } from "url";
26741
27151
  import { execFileSync as execFileSync4, spawn as spawn9 } from "child_process";
26742
27152
  function probeVideo(filePath) {
@@ -26806,8 +27216,8 @@ function transcodeToMp4(inputPath, outputPath) {
26806
27216
  }
26807
27217
  function resolveAssetDir(devSegments, builtSegments) {
26808
27218
  const base = dirname12(fileURLToPath5(import.meta.url));
26809
- const devPath = resolve17(base, ...devSegments);
26810
- const builtPath = resolve17(base, ...builtSegments);
27219
+ const devPath = resolve18(base, ...devSegments);
27220
+ const builtPath = resolve18(base, ...builtSegments);
26811
27221
  return existsSync31(devPath) ? devPath : builtPath;
26812
27222
  }
26813
27223
  function getStaticTemplateDir(templateId) {
@@ -26892,7 +27302,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26892
27302
  }
26893
27303
  if (shouldTranscode) {
26894
27304
  const mp4Name = localVideoName.replace(/\.[^.]+$/, ".mp4");
26895
- const mp4Path = resolve17(destDir, mp4Name);
27305
+ const mp4Path = resolve18(destDir, mp4Name);
26896
27306
  const spin = be();
26897
27307
  spin.start("Transcoding to H.264 MP4...");
26898
27308
  const ok = await transcodeToMp4(videoPath, mp4Path);
@@ -26901,10 +27311,10 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26901
27311
  localVideoName = mp4Name;
26902
27312
  } else {
26903
27313
  spin.stop(c.warn("Transcode failed \u2014 copying original file"));
26904
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27314
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26905
27315
  }
26906
27316
  } else {
26907
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27317
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26908
27318
  }
26909
27319
  } else {
26910
27320
  if (interactive) {
@@ -26914,10 +27324,10 @@ async function handleVideoFile(videoPath, destDir, interactive) {
26914
27324
  console.log(c.warn("ffmpeg not installed \u2014 cannot transcode. Copying original."));
26915
27325
  console.log(c.dim("Install: ") + c.accent("brew install ffmpeg"));
26916
27326
  }
26917
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27327
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26918
27328
  }
26919
27329
  } else {
26920
- copyFileSync3(videoPath, resolve17(destDir, localVideoName));
27330
+ copyFileSync3(videoPath, resolve18(destDir, localVideoName));
26921
27331
  }
26922
27332
  return { meta, localVideoName };
26923
27333
  }
@@ -26931,7 +27341,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
26931
27341
  }
26932
27342
  patchVideoSrc(destDir, localVideoName, durationSeconds);
26933
27343
  writeFileSync11(
26934
- resolve17(destDir, "meta.json"),
27344
+ resolve18(destDir, "meta.json"),
26935
27345
  JSON.stringify(
26936
27346
  {
26937
27347
  id: name,
@@ -26947,7 +27357,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
26947
27357
  if (existsSync31(sharedDir)) {
26948
27358
  for (const entry of readdirSync10(sharedDir, { withFileTypes: true })) {
26949
27359
  const src = join32(sharedDir, entry.name);
26950
- const dest = resolve17(destDir, entry.name);
27360
+ const dest = resolve18(destDir, entry.name);
26951
27361
  if (entry.isFile() || entry.isSymbolicLink()) {
26952
27362
  copyFileSync3(src, dest);
26953
27363
  }
@@ -27039,7 +27449,7 @@ var init_init = __esm({
27039
27449
  if (!interactive) {
27040
27450
  const templateId2 = templateFlag ?? "blank";
27041
27451
  const name2 = args.name ?? "my-video";
27042
- const destDir2 = resolve17(name2);
27452
+ const destDir2 = resolve18(name2);
27043
27453
  if (existsSync31(destDir2) && readdirSync10(destDir2).length > 0) {
27044
27454
  console.error(c.error(`Directory already exists and is not empty: ${name2}`));
27045
27455
  process.exit(1);
@@ -27053,7 +27463,7 @@ var init_init = __esm({
27053
27463
  process.exit(1);
27054
27464
  }
27055
27465
  if (videoFlag) {
27056
- const videoPath = resolve17(videoFlag);
27466
+ const videoPath = resolve18(videoFlag);
27057
27467
  if (!existsSync31(videoPath)) {
27058
27468
  console.error(c.error(`Video file not found: ${videoFlag}`));
27059
27469
  process.exit(1);
@@ -27067,13 +27477,13 @@ var init_init = __esm({
27067
27477
  );
27068
27478
  }
27069
27479
  if (audioFlag) {
27070
- const audioPath = resolve17(audioFlag);
27480
+ const audioPath = resolve18(audioFlag);
27071
27481
  if (!existsSync31(audioPath)) {
27072
27482
  console.error(c.error(`Audio file not found: ${audioFlag}`));
27073
27483
  process.exit(1);
27074
27484
  }
27075
27485
  sourceFilePath2 = audioPath;
27076
- copyFileSync3(audioPath, resolve17(destDir2, basename4(audioPath)));
27486
+ copyFileSync3(audioPath, resolve18(destDir2, basename4(audioPath)));
27077
27487
  console.log(`Audio: ${basename4(audioPath)}`);
27078
27488
  }
27079
27489
  if (sourceFilePath2 && !skipTranscribe) {
@@ -27113,7 +27523,7 @@ var init_init = __esm({
27113
27523
  process.exit(1);
27114
27524
  }
27115
27525
  trackInitTemplate(templateId2);
27116
- const transcriptFile2 = resolve17(destDir2, "transcript.json");
27526
+ const transcriptFile2 = resolve18(destDir2, "transcript.json");
27117
27527
  if (existsSync31(transcriptFile2)) {
27118
27528
  await patchTranscript(destDir2, transcriptFile2);
27119
27529
  }
@@ -27159,7 +27569,7 @@ var init_init = __esm({
27159
27569
  }
27160
27570
  name = nameResult;
27161
27571
  }
27162
- const destDir = resolve17(name);
27572
+ const destDir = resolve18(name);
27163
27573
  if (existsSync31(destDir) && readdirSync10(destDir).length > 0) {
27164
27574
  const overwrite = await Rt({
27165
27575
  message: `Directory ${c.accent(name)} already exists and is not empty. Overwrite?`,
@@ -27174,7 +27584,7 @@ var init_init = __esm({
27174
27584
  let sourceFilePath;
27175
27585
  let videoDuration;
27176
27586
  if (videoFlag) {
27177
- const videoPath = resolve17(videoFlag);
27587
+ const videoPath = resolve18(videoFlag);
27178
27588
  if (!existsSync31(videoPath)) {
27179
27589
  R2.error(`File not found: ${videoFlag}`);
27180
27590
  Nt("Setup cancelled.");
@@ -27186,7 +27596,7 @@ var init_init = __esm({
27186
27596
  localVideoName = result.localVideoName;
27187
27597
  videoDuration = result.meta.durationSeconds;
27188
27598
  } else if (audioFlag) {
27189
- const audioPath = resolve17(audioFlag);
27599
+ const audioPath = resolve18(audioFlag);
27190
27600
  if (!existsSync31(audioPath)) {
27191
27601
  R2.error(`File not found: ${audioFlag}`);
27192
27602
  Nt("Setup cancelled.");
@@ -27194,7 +27604,7 @@ var init_init = __esm({
27194
27604
  }
27195
27605
  mkdirSync19(destDir, { recursive: true });
27196
27606
  sourceFilePath = audioPath;
27197
- copyFileSync3(audioPath, resolve17(destDir, basename4(audioPath)));
27607
+ copyFileSync3(audioPath, resolve18(destDir, basename4(audioPath)));
27198
27608
  R2.info(`Audio copied to ${c.accent(basename4(audioPath))}`);
27199
27609
  }
27200
27610
  if (sourceFilePath) {
@@ -27279,7 +27689,7 @@ ${c.dim("Use --template blank for offline use.")}`
27279
27689
  process.exit(1);
27280
27690
  }
27281
27691
  trackInitTemplate(templateId);
27282
- const transcriptFile = resolve17(destDir, "transcript.json");
27692
+ const transcriptFile = resolve18(destDir, "transcript.json");
27283
27693
  if (existsSync31(transcriptFile)) {
27284
27694
  await patchTranscript(destDir, transcriptFile);
27285
27695
  }
@@ -27344,11 +27754,11 @@ var init_format = __esm({
27344
27754
 
27345
27755
  // src/utils/project.ts
27346
27756
  import { existsSync as existsSync32, statSync as statSync11 } from "fs";
27347
- import { resolve as resolve18, basename as basename5 } from "path";
27757
+ import { resolve as resolve19, basename as basename5 } from "path";
27348
27758
  function resolveProject(dirArg) {
27349
- const dir = resolve18(dirArg ?? ".");
27759
+ const dir = resolve19(dirArg ?? ".");
27350
27760
  const name = basename5(dir);
27351
- const indexPath = resolve18(dir, "index.html");
27761
+ const indexPath = resolve19(dir, "index.html");
27352
27762
  if (!existsSync32(dir) || !statSync11(dir).isDirectory()) {
27353
27763
  errorBox("Not a directory: " + dir);
27354
27764
  process.exit(1);
@@ -27377,7 +27787,7 @@ __export(play_exports, {
27377
27787
  examples: () => examples3
27378
27788
  });
27379
27789
  import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
27380
- import { resolve as resolve19, dirname as dirname13 } from "path";
27790
+ import { resolve as resolve20, dirname as dirname13 } from "path";
27381
27791
  function commandDir() {
27382
27792
  return dirname13(new URL(import.meta.url).pathname);
27383
27793
  }
@@ -27385,10 +27795,10 @@ function resolveRuntimePath2() {
27385
27795
  const d = commandDir();
27386
27796
  const candidates = [
27387
27797
  // Bundled with CLI dist
27388
- resolve19(d, "hyperframe-runtime.js"),
27389
- resolve19(d, "..", "hyperframe-runtime.js"),
27798
+ resolve20(d, "hyperframe-runtime.js"),
27799
+ resolve20(d, "..", "hyperframe-runtime.js"),
27390
27800
  // Monorepo dev: commands/ → src/ → cli/ → packages/ then into core/dist/
27391
- resolve19(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
27801
+ resolve20(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
27392
27802
  ];
27393
27803
  for (const p of candidates) {
27394
27804
  if (existsSync33(p)) return p;
@@ -27399,10 +27809,10 @@ function resolvePlayerPath() {
27399
27809
  const d = commandDir();
27400
27810
  const candidates = [
27401
27811
  // Monorepo dev: commands/ → src/ → cli/ → packages/ then into player/dist/
27402
- resolve19(d, "..", "..", "..", "player", "dist", "hyperframes-player.global.js"),
27812
+ resolve20(d, "..", "..", "..", "player", "dist", "hyperframes-player.global.js"),
27403
27813
  // Bundled with CLI dist
27404
- resolve19(d, "hyperframes-player.global.js"),
27405
- resolve19(d, "..", "hyperframes-player.global.js")
27814
+ resolve20(d, "hyperframes-player.global.js"),
27815
+ resolve20(d, "..", "hyperframes-player.global.js")
27406
27816
  ];
27407
27817
  for (const p of candidates) {
27408
27818
  if (existsSync33(p)) return p;
@@ -27505,7 +27915,7 @@ var init_play = __esm({
27505
27915
  });
27506
27916
  app.get("/composition/*", async (ctx) => {
27507
27917
  const reqPath = ctx.req.path.replace("/composition/", "");
27508
- const filePath = resolve19(project.dir, reqPath);
27918
+ const filePath = resolve20(project.dir, reqPath);
27509
27919
  if (!filePath.startsWith(project.dir)) return ctx.text("Forbidden", 403);
27510
27920
  if (!existsSync33(filePath)) return ctx.text("Not found", 404);
27511
27921
  const content = readFileSync22(filePath, "utf-8");
@@ -27657,7 +28067,7 @@ __export(render_exports, {
27657
28067
  });
27658
28068
  import { mkdirSync as mkdirSync20, readFileSync as readFileSync23, statSync as statSync12, writeFileSync as writeFileSync12, rmSync as rmSync8 } from "fs";
27659
28069
  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";
28070
+ import { resolve as resolve21, dirname as dirname14, join as join33, basename as basename6 } from "path";
27661
28071
  import { execFileSync as execFileSync5, spawn as spawn10 } from "child_process";
27662
28072
  function defaultWorkerCount() {
27663
28073
  return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT * 3 / 4), 8));
@@ -27666,8 +28076,8 @@ function dockerImageTag(version) {
27666
28076
  return `${DOCKER_IMAGE_PREFIX}:${version}`;
27667
28077
  }
27668
28078
  function resolveDockerfilePath() {
27669
- const builtPath = resolve20(__dirname, "docker", "Dockerfile.render");
27670
- const devPath = resolve20(__dirname, "..", "src", "docker", "Dockerfile.render");
28079
+ const builtPath = resolve21(__dirname, "docker", "Dockerfile.render");
28080
+ const devPath = resolve21(__dirname, "..", "src", "docker", "Dockerfile.render");
27671
28081
  for (const p of [builtPath, devPath]) {
27672
28082
  try {
27673
28083
  statSync12(p);
@@ -27751,9 +28161,9 @@ async function renderDocker(projectDir, outputPath, options) {
27751
28161
  // GPU encoding requires host GPU passthrough
27752
28162
  ...options.gpu ? ["--gpus", "all"] : [],
27753
28163
  "-v",
27754
- `${resolve20(projectDir)}:/project:ro`,
28164
+ `${resolve21(projectDir)}:/project:ro`,
27755
28165
  "-v",
27756
- `${resolve20(outputDir)}:/output`,
28166
+ `${resolve21(outputDir)}:/output`,
27757
28167
  imageTag,
27758
28168
  "/project",
27759
28169
  "--output",
@@ -27962,6 +28372,10 @@ var init_render2 = __esm({
27962
28372
  type: "boolean",
27963
28373
  description: "Fail render on lint errors AND warnings",
27964
28374
  default: false
28375
+ },
28376
+ "max-concurrent-renders": {
28377
+ type: "string",
28378
+ description: "Max concurrent renders when using the producer server (1-10). Default: 2."
27965
28379
  }
27966
28380
  },
27967
28381
  async run({ args }) {
@@ -27993,12 +28407,23 @@ var init_render2 = __esm({
27993
28407
  }
27994
28408
  workers = parsed;
27995
28409
  }
27996
- const rendersDir = resolve20("renders");
28410
+ if (args["max-concurrent-renders"] != null) {
28411
+ const parsed = parseInt(args["max-concurrent-renders"], 10);
28412
+ if (isNaN(parsed) || parsed < 1 || parsed > 10) {
28413
+ errorBox(
28414
+ "Invalid max-concurrent-renders",
28415
+ `Got "${args["max-concurrent-renders"]}". Must be a number between 1 and 10.`
28416
+ );
28417
+ process.exit(1);
28418
+ }
28419
+ process.env.PRODUCER_MAX_CONCURRENT_RENDERS = String(parsed);
28420
+ }
28421
+ const rendersDir = resolve21("renders");
27997
28422
  const ext = FORMAT_EXT[format] ?? ".mp4";
27998
28423
  const now = /* @__PURE__ */ new Date();
27999
28424
  const datePart = now.toISOString().slice(0, 10);
28000
28425
  const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
28001
- const outputPath = args.output ? resolve20(args.output) : join33(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
28426
+ const outputPath = args.output ? resolve21(args.output) : join33(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
28002
28427
  mkdirSync20(dirname14(outputPath), { recursive: true });
28003
28428
  const useDocker = args.docker ?? false;
28004
28429
  const useGpu = args.gpu ?? false;
@@ -28412,7 +28837,7 @@ __export(compositions_exports, {
28412
28837
  examples: () => examples7
28413
28838
  });
28414
28839
  import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
28415
- import { resolve as resolve21, dirname as dirname15 } from "path";
28840
+ import { resolve as resolve22, dirname as dirname15 } from "path";
28416
28841
  function parseCompositions(html, baseDir) {
28417
28842
  const parser = new DOMParser();
28418
28843
  const doc = parser.parseFromString(html, "text/html");
@@ -28424,7 +28849,7 @@ function parseCompositions(html, baseDir) {
28424
28849
  const height = parseInt(div.getAttribute("data-height") ?? "1080", 10);
28425
28850
  const compositionSrc = div.getAttribute("data-composition-src");
28426
28851
  if (compositionSrc) {
28427
- const subPath = resolve21(baseDir, compositionSrc);
28852
+ const subPath = resolve22(baseDir, compositionSrc);
28428
28853
  if (existsSync34(subPath)) {
28429
28854
  const subHtml = readFileSync25(subPath, "utf-8");
28430
28855
  const subInfo = parseSubComposition(subHtml, id, width, height);
@@ -28559,7 +28984,7 @@ __export(benchmark_exports, {
28559
28984
  examples: () => examples8
28560
28985
  });
28561
28986
  import { existsSync as existsSync35, statSync as statSync14 } from "fs";
28562
- import { resolve as resolve22, join as join35 } from "path";
28987
+ import { resolve as resolve23, join as join35 } from "path";
28563
28988
  var examples8, DEFAULT_CONFIGS, benchmark_default;
28564
28989
  var init_benchmark = __esm({
28565
28990
  "src/commands/benchmark.ts"() {
@@ -28601,7 +29026,7 @@ var init_benchmark = __esm({
28601
29026
  process.exit(1);
28602
29027
  }
28603
29028
  const jsonOutput = args.json ?? false;
28604
- const benchDir = resolve22("renders", ".benchmark");
29029
+ const benchDir = resolve23("renders", ".benchmark");
28605
29030
  let producer = null;
28606
29031
  try {
28607
29032
  producer = await loadProducer();
@@ -28865,7 +29290,7 @@ __export(transcribe_exports2, {
28865
29290
  examples: () => examples10
28866
29291
  });
28867
29292
  import { existsSync as existsSync36, writeFileSync as writeFileSync13 } from "fs";
28868
- import { resolve as resolve23, join as join36, extname as extname7 } from "path";
29293
+ import { resolve as resolve24, join as join36, extname as extname7 } from "path";
28869
29294
  async function importTranscript(inputPath, dir, json) {
28870
29295
  const { loadTranscript: loadTranscript2, patchCaptionHtml: patchCaptionHtml2 } = await Promise.resolve().then(() => (init_normalize(), normalize_exports));
28871
29296
  const { words, format } = loadTranscript2(inputPath);
@@ -28989,12 +29414,12 @@ var init_transcribe2 = __esm({
28989
29414
  }
28990
29415
  },
28991
29416
  async run({ args }) {
28992
- const inputPath = resolve23(args.input);
29417
+ const inputPath = resolve24(args.input);
28993
29418
  if (!existsSync36(inputPath)) {
28994
29419
  console.error(c.error(`File not found: ${args.input}`));
28995
29420
  process.exit(1);
28996
29421
  }
28997
- const dir = resolve23(args.dir ?? ".");
29422
+ const dir = resolve24(args.dir ?? ".");
28998
29423
  const ext = extname7(inputPath).toLowerCase();
28999
29424
  const isImport = ext === ".json" || ext === ".srt" || ext === ".vtt";
29000
29425
  if (isImport) {
@@ -29217,7 +29642,7 @@ __export(tts_exports, {
29217
29642
  examples: () => examples11
29218
29643
  });
29219
29644
  import { existsSync as existsSync39, readFileSync as readFileSync26 } from "fs";
29220
- import { resolve as resolve24, extname as extname8 } from "path";
29645
+ import { resolve as resolve25, extname as extname8 } from "path";
29221
29646
  function listVoices(json) {
29222
29647
  if (json) {
29223
29648
  console.log(JSON.stringify(BUNDLED_VOICES));
@@ -29305,7 +29730,7 @@ var init_tts = __esm({
29305
29730
  process.exit(1);
29306
29731
  }
29307
29732
  let text;
29308
- const maybeFile = resolve24(args.input);
29733
+ const maybeFile = resolve25(args.input);
29309
29734
  if (existsSync39(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
29310
29735
  text = readFileSync26(maybeFile, "utf-8").trim();
29311
29736
  if (!text) {
@@ -29319,7 +29744,7 @@ var init_tts = __esm({
29319
29744
  console.error(c.error("No text provided."));
29320
29745
  process.exit(1);
29321
29746
  }
29322
- const output = resolve24(args.output ?? "speech.wav");
29747
+ const output = resolve25(args.output ?? "speech.wav");
29323
29748
  const voice = args.voice ?? DEFAULT_VOICE;
29324
29749
  const speed = args.speed ? parseFloat(args.speed) : 1;
29325
29750
  if (isNaN(speed) || speed <= 0 || speed > 3) {
@@ -29373,13 +29798,13 @@ __export(docs_exports, {
29373
29798
  examples: () => examples12
29374
29799
  });
29375
29800
  import { readFileSync as readFileSync27, existsSync as existsSync40 } from "fs";
29376
- import { resolve as resolve25, dirname as dirname17, join as join39 } from "path";
29801
+ import { resolve as resolve26, dirname as dirname17, join as join39 } from "path";
29377
29802
  import { fileURLToPath as fileURLToPath6 } from "url";
29378
29803
  function docsDir() {
29379
29804
  const thisFile = fileURLToPath6(import.meta.url);
29380
29805
  const dir = dirname17(thisFile);
29381
- const devPath = resolve25(dir, "..", "docs");
29382
- const builtPath = resolve25(dir, "docs");
29806
+ const devPath = resolve26(dir, "..", "docs");
29807
+ const builtPath = resolve26(dir, "docs");
29383
29808
  return existsSync40(devPath) ? devPath : builtPath;
29384
29809
  }
29385
29810
  function formatInlineCode(line) {
@@ -29897,13 +30322,13 @@ __export(validate_exports, {
29897
30322
  default: () => validate_default
29898
30323
  });
29899
30324
  import { existsSync as existsSync41, readFileSync as readFileSync28 } from "fs";
29900
- import { resolve as resolve26, join as join40, dirname as dirname18 } from "path";
30325
+ import { resolve as resolve27, join as join40, dirname as dirname18 } from "path";
29901
30326
  import { fileURLToPath as fileURLToPath7 } from "url";
29902
30327
  async function validateInBrowser(projectDir, opts) {
29903
30328
  const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
29904
30329
  const { ensureBrowser: ensureBrowser2 } = await Promise.resolve().then(() => (init_manager2(), manager_exports2));
29905
30330
  let html = await bundleToSingleHtml2(projectDir);
29906
- const runtimePath = resolve26(
30331
+ const runtimePath = resolve27(
29907
30332
  __dirname2,
29908
30333
  "..",
29909
30334
  "..",
@@ -29916,7 +30341,7 @@ async function validateInBrowser(projectDir, opts) {
29916
30341
  const runtimeSource = readFileSync28(runtimePath, "utf-8");
29917
30342
  html = html.replace(
29918
30343
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
29919
- `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
30344
+ () => `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
29920
30345
  );
29921
30346
  }
29922
30347
  const { createServer } = await import("http");