hyperframes 0.1.6 → 0.1.8

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
@@ -422,7 +422,7 @@ var VERSION;
422
422
  var init_version = __esm({
423
423
  "src/version.ts"() {
424
424
  "use strict";
425
- VERSION = true ? "0.1.6" : "0.0.0-dev";
425
+ VERSION = true ? "0.1.8" : "0.0.0-dev";
426
426
  }
427
427
  });
428
428
 
@@ -2748,6 +2748,25 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, rmSync
2748
2748
  import { join as join3, dirname } from "path";
2749
2749
  import { homedir as homedir3 } from "os";
2750
2750
  import { execFileSync as execFileSync2 } from "child_process";
2751
+ function hasNpx() {
2752
+ try {
2753
+ execFileSync2("npx", ["--version"], { stdio: "ignore", timeout: 5e3 });
2754
+ return true;
2755
+ } catch {
2756
+ return false;
2757
+ }
2758
+ }
2759
+ function runSkillsAdd(repo, agents, global) {
2760
+ const args = ["skills", "add", repo, "-y"];
2761
+ if (global) args.push("-g");
2762
+ for (const agent of agents) {
2763
+ args.push("-a", agent);
2764
+ }
2765
+ execFileSync2("npx", args, {
2766
+ stdio: "ignore",
2767
+ timeout: 12e4
2768
+ });
2769
+ }
2751
2770
  function hasGit2() {
2752
2771
  try {
2753
2772
  execFileSync2("git", ["--version"], { stdio: "ignore", timeout: 5e3 });
@@ -2764,6 +2783,7 @@ function gitClone(repo, dest) {
2764
2783
  });
2765
2784
  }
2766
2785
  function fetchRepo(source) {
2786
+ const gitUrl = `https://github.com/${source.repo}.git`;
2767
2787
  if (existsSync3(source.cache)) {
2768
2788
  try {
2769
2789
  execFileSync2("git", ["pull", "--ff-only"], {
@@ -2773,18 +2793,17 @@ function fetchRepo(source) {
2773
2793
  env: GIT_ENV
2774
2794
  });
2775
2795
  } catch {
2776
- const skillsDir = join3(source.cache, source.skillsPath);
2777
- if (existsSync3(skillsDir)) {
2778
- return skillsDir;
2779
- }
2796
+ const skillsDir2 = join3(source.cache, source.skillsPath);
2797
+ if (existsSync3(skillsDir2)) return skillsDir2;
2780
2798
  rmSync2(source.cache, { recursive: true, force: true });
2781
- gitClone(source.repo, source.cache);
2799
+ gitClone(gitUrl, source.cache);
2782
2800
  }
2783
2801
  } else {
2784
2802
  mkdirSync3(dirname(source.cache), { recursive: true });
2785
- gitClone(source.repo, source.cache);
2803
+ gitClone(gitUrl, source.cache);
2786
2804
  }
2787
- return join3(source.cache, source.skillsPath);
2805
+ const skillsDir = join3(source.cache, source.skillsPath);
2806
+ return existsSync3(skillsDir) ? skillsDir : void 0;
2788
2807
  }
2789
2808
  function installSkillsFromDir(sourceDir, targetDir, sourceName) {
2790
2809
  const installed = [];
@@ -2795,24 +2814,20 @@ function installSkillsFromDir(sourceDir, targetDir, sourceName) {
2795
2814
  const skillFile = join3(sourceDir, entry.name, "SKILL.md");
2796
2815
  if (!existsSync3(skillFile)) continue;
2797
2816
  const destDir = join3(targetDir, entry.name);
2798
- const overwritten = existsSync3(destDir);
2799
- if (overwritten) rmSync2(destDir, { recursive: true, force: true });
2817
+ if (existsSync3(destDir)) rmSync2(destDir, { recursive: true, force: true });
2800
2818
  mkdirSync3(destDir, { recursive: true });
2801
2819
  cpSync(join3(sourceDir, entry.name), destDir, { recursive: true });
2802
- installed.push({ name: entry.name, source: sourceName, overwritten });
2820
+ installed.push({ name: entry.name, source: sourceName });
2803
2821
  }
2804
2822
  return installed;
2805
2823
  }
2806
- async function installAllSkills(targetNames) {
2807
- if (!hasGit2()) return { count: 0, targets: [], skipped: SOURCES.map((s) => s.name) };
2808
- const targets = targetNames ? TARGETS.filter((t2) => targetNames.includes(t2.flag)) : TARGETS.filter((t2) => t2.defaultEnabled);
2809
- let totalCount = 0;
2824
+ function fallbackInstall(targets) {
2810
2825
  const skipped = [];
2811
2826
  const fetched = [];
2812
2827
  for (const source of SOURCES) {
2813
2828
  try {
2814
2829
  const skillsDir = fetchRepo(source);
2815
- if (existsSync3(skillsDir)) {
2830
+ if (skillsDir) {
2816
2831
  fetched.push({ source, skillsDir });
2817
2832
  } else {
2818
2833
  skipped.push(source.name);
@@ -2821,21 +2836,45 @@ async function installAllSkills(targetNames) {
2821
2836
  skipped.push(source.name);
2822
2837
  }
2823
2838
  }
2824
- const [firstTarget, ...remainingTargets] = targets;
2825
- if (firstTarget) {
2826
- mkdirSync3(firstTarget.dir, { recursive: true });
2839
+ const [first, ...rest] = targets;
2840
+ const allInstalled = [];
2841
+ if (first) {
2842
+ mkdirSync3(first.dir, { recursive: true });
2827
2843
  for (const { skillsDir, source } of fetched) {
2828
- const skills = installSkillsFromDir(skillsDir, firstTarget.dir, source.name);
2829
- totalCount += skills.length;
2844
+ allInstalled.push(...installSkillsFromDir(skillsDir, first.dir, source.name));
2830
2845
  }
2831
2846
  }
2832
- for (const target of remainingTargets) {
2847
+ for (const target of rest) {
2833
2848
  mkdirSync3(target.dir, { recursive: true });
2834
2849
  for (const { skillsDir, source } of fetched) {
2835
2850
  installSkillsFromDir(skillsDir, target.dir, source.name);
2836
2851
  }
2837
2852
  }
2838
- return { count: totalCount, targets: targets.map((t2) => t2.name), skipped };
2853
+ return { count: allInstalled.length, installed: allInstalled, skipped };
2854
+ }
2855
+ async function installAllSkills(targetNames) {
2856
+ const targets = targetNames ? TARGETS.filter((t2) => targetNames.includes(t2.flag)) : TARGETS.filter((t2) => t2.defaultEnabled);
2857
+ const agents = targets.map((t2) => t2.skillsAgent);
2858
+ if (hasNpx()) {
2859
+ const skipped = [];
2860
+ let count = 0;
2861
+ for (const source of SOURCES) {
2862
+ try {
2863
+ runSkillsAdd(source.repo, agents, true);
2864
+ count += 1;
2865
+ } catch {
2866
+ skipped.push(source.name);
2867
+ }
2868
+ }
2869
+ if (count > 0) {
2870
+ return { count, targets: targets.map((t2) => t2.name), skipped };
2871
+ }
2872
+ }
2873
+ if (!hasGit2()) {
2874
+ return { count: 0, targets: [], skipped: SOURCES.map((s) => s.name) };
2875
+ }
2876
+ const result = fallbackInstall(targets);
2877
+ return { count: result.count, targets: targets.map((t2) => t2.name), skipped: result.skipped };
2839
2878
  }
2840
2879
  function resolveTargets(args) {
2841
2880
  const hasAnyFlag = TARGETS.some((t2) => args[t2.flag] === true);
@@ -2846,70 +2885,57 @@ function resolveTargets(args) {
2846
2885
  }
2847
2886
  async function runInstall({ args }) {
2848
2887
  Wt2(c.bold("hyperframes skills"));
2849
- if (!hasGit2()) {
2850
- R2.error(c.error("git is required to install skills. Install git and retry."));
2851
- Gt(c.warn("No skills installed."));
2852
- return;
2853
- }
2854
2888
  const targets = resolveTargets(args);
2855
- const fetched = [];
2856
- for (const source of SOURCES) {
2857
- const spinner = be();
2858
- spinner.start(`Fetching ${source.name} skills...`);
2859
- try {
2860
- const skillsDir = fetchRepo(source);
2861
- if (existsSync3(skillsDir)) {
2862
- fetched.push({ source, skillsDir });
2863
- spinner.stop(c.success(`${source.name} skills fetched`));
2864
- } else {
2865
- spinner.stop(c.warn(`${source.name}: no skills directory found`));
2889
+ const agents = targets.map((t2) => t2.skillsAgent);
2890
+ if (hasNpx()) {
2891
+ const installed = [];
2892
+ const skippedSources = [];
2893
+ for (const source of SOURCES) {
2894
+ const spinner = be();
2895
+ spinner.start(`Installing ${source.name} skills...`);
2896
+ try {
2897
+ runSkillsAdd(source.repo, agents, true);
2898
+ installed.push(source.name);
2899
+ spinner.stop(c.success(`${source.name} skills installed`));
2900
+ } catch {
2901
+ skippedSources.push(source.name);
2902
+ spinner.stop(c.dim(`${source.name} skills skipped (unavailable)`));
2866
2903
  }
2867
- } catch {
2868
- spinner.stop(c.dim(`${source.name} skills skipped (repo not accessible)`));
2869
2904
  }
2870
- }
2871
- const allInstalled = [];
2872
- let counted = false;
2873
- for (const target of targets) {
2874
- const spinner = be();
2875
- spinner.start(`Installing to ${target.name}...`);
2876
- mkdirSync3(target.dir, { recursive: true });
2877
- let count = 0;
2878
- for (const { source, skillsDir } of fetched) {
2879
- const skills = installSkillsFromDir(skillsDir, target.dir, source.name);
2880
- count += skills.length;
2881
- if (!counted) allInstalled.push(...skills);
2905
+ console.log();
2906
+ console.log(` ${c.dim("Targets:")} ${targets.map((t2) => t2.name).join(", ")}`);
2907
+ if (skippedSources.length > 0) {
2908
+ console.log(` ${c.dim("Skipped:")} ${skippedSources.join(", ")}`);
2882
2909
  }
2883
- counted = true;
2884
- spinner.stop(c.success(`${count} skills \u2192 ${target.name} ${c.dim(target.dir)}`));
2910
+ console.log();
2911
+ if (installed.length > 0) {
2912
+ Gt(c.success(`${installed.join(" + ")} skills installed.`));
2913
+ return;
2914
+ }
2915
+ R2.warn("npx skills add failed \u2014 trying fallback...");
2916
+ }
2917
+ if (!hasGit2()) {
2918
+ R2.error(c.error("Neither npx nor git available. Install Node.js or git and retry."));
2919
+ Gt(c.warn("No skills installed."));
2920
+ return;
2885
2921
  }
2922
+ R2.info(c.dim("Using git fallback..."));
2923
+ const result = fallbackInstall(targets);
2886
2924
  console.log();
2887
2925
  for (const source of SOURCES) {
2888
- const names = allInstalled.filter((s) => s.source === source.name).map((s) => s.name);
2926
+ const names = result.installed.filter((s) => s.source === source.name).map((s) => s.name);
2889
2927
  if (names.length > 0) {
2890
2928
  const label2 = `${source.name}:`.padEnd(14);
2891
2929
  console.log(` ${c.dim(label2)} ${names.map((s) => c.accent(s)).join(", ")}`);
2892
2930
  }
2893
2931
  }
2894
2932
  console.log(` ${c.dim("Targets:")} ${targets.map((t2) => t2.name).join(", ")}`);
2895
- console.log();
2896
- const skippedSources = SOURCES.filter((s) => !fetched.some((f) => f.source.name === s.name));
2897
- if (skippedSources.length > 0) {
2898
- console.log(
2899
- ` ${c.dim("Skipped:")} ${skippedSources.map((s) => s.name).join(", ")} (repo not accessible)`
2900
- );
2933
+ if (result.skipped.length > 0) {
2934
+ console.log(` ${c.dim("Skipped:")} ${result.skipped.join(", ")}`);
2901
2935
  }
2902
2936
  console.log();
2903
- if (allInstalled.length > 0 && skippedSources.length > 0) {
2904
- const readySources = fetched.map((f) => f.source.name).join(", ");
2905
- const skippedNames = skippedSources.map((s) => s.name).join(", ");
2906
- Gt(
2907
- c.warn(
2908
- `${allInstalled.length} skills ready (${readySources}). Unavailable: ${skippedNames}.`
2909
- )
2910
- );
2911
- } else if (allInstalled.length > 0) {
2912
- Gt(c.success(`${allInstalled.length} skills ready.`));
2937
+ if (result.count > 0) {
2938
+ Gt(c.success(`${result.count} skills ready.`));
2913
2939
  } else {
2914
2940
  Gt(c.warn("No skills installed."));
2915
2941
  }
@@ -2925,24 +2951,28 @@ var init_install_skills = __esm({
2925
2951
  {
2926
2952
  name: "Claude Code",
2927
2953
  flag: "claude",
2954
+ skillsAgent: "claude-code",
2928
2955
  dir: join3(homedir3(), ".claude", "skills"),
2929
2956
  defaultEnabled: true
2930
2957
  },
2931
2958
  {
2932
2959
  name: "Gemini CLI",
2933
2960
  flag: "gemini",
2961
+ skillsAgent: "gemini-cli",
2934
2962
  dir: join3(homedir3(), ".gemini", "skills"),
2935
2963
  defaultEnabled: true
2936
2964
  },
2937
2965
  {
2938
2966
  name: "Codex CLI",
2939
2967
  flag: "codex",
2968
+ skillsAgent: "codex",
2940
2969
  dir: join3(homedir3(), ".codex", "skills"),
2941
2970
  defaultEnabled: true
2942
2971
  },
2943
2972
  {
2944
2973
  name: "Cursor",
2945
2974
  flag: "cursor",
2975
+ skillsAgent: "cursor",
2946
2976
  get dir() {
2947
2977
  return join3(process.cwd(), ".cursor", "skills");
2948
2978
  },
@@ -2952,13 +2982,13 @@ var init_install_skills = __esm({
2952
2982
  SOURCES = [
2953
2983
  {
2954
2984
  name: "HyperFrames",
2955
- repo: "https://github.com/heygen-com/hyperframes.git",
2985
+ repo: "heygen-com/hyperframes",
2956
2986
  skillsPath: "skills",
2957
2987
  cache: join3(homedir3(), ".cache", "hyperframes", "hyperframes-skills")
2958
2988
  },
2959
2989
  {
2960
2990
  name: "GSAP",
2961
- repo: "https://github.com/greensock/gsap-skills.git",
2991
+ repo: "greensock/gsap-skills",
2962
2992
  skillsPath: "skills",
2963
2993
  cache: join3(homedir3(), ".cache", "hyperframes", "gsap-skills")
2964
2994
  }
@@ -3469,6 +3499,37 @@ function lintHyperframeHtml(html, options = {}) {
3469
3499
  fixHint: "Register each composition timeline on `window.__timelines[compositionId]`."
3470
3500
  });
3471
3501
  }
3502
+ if (TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_INIT_PATTERN.test(source)) {
3503
+ pushFinding({
3504
+ code: "timeline_registry_missing_init",
3505
+ severity: "error",
3506
+ message: "`window.__timelines[\u2026] = \u2026` is used without initializing `window.__timelines` first.",
3507
+ fixHint: "Add `window.__timelines = window.__timelines || {};` before any timeline assignment."
3508
+ });
3509
+ }
3510
+ {
3511
+ const htmlCompIds = /* @__PURE__ */ new Set();
3512
+ const timelineRegKeys = /* @__PURE__ */ new Set();
3513
+ const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi;
3514
+ const tlKeyRe = /window\.__timelines\[\s*["']([^"']+)["']\s*\]/g;
3515
+ let m;
3516
+ while ((m = compIdRe.exec(source)) !== null) {
3517
+ if (m[1]) htmlCompIds.add(m[1]);
3518
+ }
3519
+ while ((m = tlKeyRe.exec(source)) !== null) {
3520
+ if (m[1]) timelineRegKeys.add(m[1]);
3521
+ }
3522
+ for (const key2 of timelineRegKeys) {
3523
+ if (!htmlCompIds.has(key2)) {
3524
+ pushFinding({
3525
+ code: "timeline_id_mismatch",
3526
+ severity: "error",
3527
+ message: `Timeline registered as "${key2}" but no element has data-composition-id="${key2}". The runtime cannot auto-nest this timeline.`,
3528
+ fixHint: `Change window.__timelines["${key2}"] to match the data-composition-id attribute, or vice versa.`
3529
+ });
3530
+ }
3531
+ }
3532
+ }
3472
3533
  if (INVALID_SCRIPT_CLOSE_PATTERN.test(source)) {
3473
3534
  pushFinding({
3474
3535
  code: "invalid_inline_script_syntax",
@@ -23766,7 +23827,11 @@ __export(render_exports, {
23766
23827
  default: () => render_default
23767
23828
  });
23768
23829
  import { existsSync as existsSync24, mkdirSync as mkdirSync16, statSync as statSync9 } from "fs";
23830
+ import { cpus as cpus2 } from "os";
23769
23831
  import { resolve as resolve13, dirname as dirname12, join as join22 } from "path";
23832
+ function defaultWorkerCount() {
23833
+ return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
23834
+ }
23770
23835
  async function renderDocker(projectDir, outputPath, options) {
23771
23836
  const producer = await loadProducer();
23772
23837
  const startTime = Date.now();
@@ -23780,7 +23845,11 @@ async function renderDocker(projectDir, outputPath, options) {
23780
23845
  });
23781
23846
  await producer.executeRenderJob(job, projectDir, outputPath);
23782
23847
  } catch (error) {
23783
- trackRenderError({ fps: options.fps, quality: options.quality, docker: true });
23848
+ trackRenderError({
23849
+ fps: options.fps,
23850
+ quality: options.quality,
23851
+ docker: true
23852
+ });
23784
23853
  const message = error instanceof Error ? error.message : String(error);
23785
23854
  errorBox("Render failed", message, "Check Docker is running: docker info");
23786
23855
  process.exit(1);
@@ -23790,7 +23859,7 @@ async function renderDocker(projectDir, outputPath, options) {
23790
23859
  durationMs: elapsed,
23791
23860
  fps: options.fps,
23792
23861
  quality: options.quality,
23793
- workers: options.workers ?? 4,
23862
+ workers: options.workers,
23794
23863
  docker: true,
23795
23864
  gpu: options.gpu
23796
23865
  });
@@ -23815,7 +23884,11 @@ async function renderLocal(projectDir, outputPath, options) {
23815
23884
  try {
23816
23885
  await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
23817
23886
  } catch (error) {
23818
- trackRenderError({ fps: options.fps, quality: options.quality, docker: false });
23887
+ trackRenderError({
23888
+ fps: options.fps,
23889
+ quality: options.quality,
23890
+ docker: false
23891
+ });
23819
23892
  const message = error instanceof Error ? error.message : String(error);
23820
23893
  errorBox("Render failed", message, "Try --docker for containerized rendering");
23821
23894
  process.exit(1);
@@ -23825,7 +23898,7 @@ async function renderLocal(projectDir, outputPath, options) {
23825
23898
  durationMs: elapsed,
23826
23899
  fps: options.fps,
23827
23900
  quality: options.quality,
23828
- workers: options.workers ?? 4,
23901
+ workers: options.workers,
23829
23902
  docker: false,
23830
23903
  gpu: options.gpu
23831
23904
  });
@@ -23843,7 +23916,7 @@ function printRenderComplete(outputPath, elapsedMs, quiet) {
23843
23916
  console.log(c.success("\u25C7") + " " + c.accent(outputPath));
23844
23917
  console.log(" " + c.bold(fileSize) + c.dim(" \xB7 " + duration + " \xB7 completed"));
23845
23918
  }
23846
- var VALID_FPS, VALID_QUALITY, VALID_FORMAT, render_default;
23919
+ var VALID_FPS, VALID_QUALITY, VALID_FORMAT, CPU_CORE_COUNT, render_default;
23847
23920
  var init_render = __esm({
23848
23921
  "src/commands/render.ts"() {
23849
23922
  "use strict";
@@ -23857,6 +23930,7 @@ var init_render = __esm({
23857
23930
  VALID_FPS = /* @__PURE__ */ new Set([24, 30, 60]);
23858
23931
  VALID_QUALITY = /* @__PURE__ */ new Set(["draft", "standard", "high"]);
23859
23932
  VALID_FORMAT = /* @__PURE__ */ new Set(["mp4", "webm"]);
23933
+ CPU_CORE_COUNT = cpus2().length;
23860
23934
  render_default = defineCommand({
23861
23935
  meta: {
23862
23936
  name: "render",
@@ -23869,19 +23943,45 @@ Examples:
23869
23943
  hyperframes render --docker --output deterministic.mp4`
23870
23944
  },
23871
23945
  args: {
23872
- dir: { type: "positional", description: "Project directory", required: false },
23873
- output: { type: "string", description: "Output path (default: renders/<name>.mp4)" },
23874
- fps: { type: "string", description: "Frame rate: 24, 30, 60", default: "30" },
23875
- quality: { type: "string", description: "Quality: draft, standard, high", default: "standard" },
23946
+ dir: {
23947
+ type: "positional",
23948
+ description: "Project directory",
23949
+ required: false
23950
+ },
23951
+ output: {
23952
+ type: "string",
23953
+ description: "Output path (default: renders/<name>.mp4)"
23954
+ },
23955
+ fps: {
23956
+ type: "string",
23957
+ description: "Frame rate: 24, 30, 60",
23958
+ default: "30"
23959
+ },
23960
+ quality: {
23961
+ type: "string",
23962
+ description: "Quality: draft, standard, high",
23963
+ default: "standard"
23964
+ },
23876
23965
  format: {
23877
23966
  type: "string",
23878
23967
  description: "Output format: mp4, webm (WebM renders with transparency)",
23879
23968
  default: "mp4"
23880
23969
  },
23881
- workers: { type: "string", description: "Parallel workers 1-8" },
23882
- docker: { type: "boolean", description: "Use Docker for deterministic render", default: false },
23970
+ workers: {
23971
+ type: "string",
23972
+ description: "Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. Each worker launches a separate Chrome process."
23973
+ },
23974
+ docker: {
23975
+ type: "boolean",
23976
+ description: "Use Docker for deterministic render",
23977
+ default: false
23978
+ },
23883
23979
  gpu: { type: "boolean", description: "Use GPU encoding", default: false },
23884
- quiet: { type: "boolean", description: "Suppress verbose output", default: false }
23980
+ quiet: {
23981
+ type: "boolean",
23982
+ description: "Suppress verbose output",
23983
+ default: false
23984
+ }
23885
23985
  },
23886
23986
  async run({ args }) {
23887
23987
  const project = resolveProject(args.dir);
@@ -23904,10 +24004,10 @@ Examples:
23904
24004
  }
23905
24005
  const format = formatRaw;
23906
24006
  let workers;
23907
- if (args.workers != null) {
24007
+ if (args.workers != null && args.workers !== "auto") {
23908
24008
  const parsed = parseInt(args.workers, 10);
23909
24009
  if (isNaN(parsed) || parsed < 1 || parsed > 8) {
23910
- errorBox("Invalid workers", `Got "${args.workers}". Must be between 1 and 8.`);
24010
+ errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
23911
24011
  process.exit(1);
23912
24012
  }
23913
24013
  workers = parsed;
@@ -23915,22 +24015,18 @@ Examples:
23915
24015
  const rendersDir = resolve13("renders");
23916
24016
  const ext = format === "webm" ? ".webm" : ".mp4";
23917
24017
  const outputPath = args.output ? resolve13(args.output) : join22(rendersDir, `${project.name}${ext}`);
23918
- const outputDir = dirname12(outputPath);
23919
- if (!existsSync24(outputDir)) {
23920
- mkdirSync16(outputDir, { recursive: true });
23921
- }
24018
+ mkdirSync16(dirname12(outputPath), { recursive: true });
23922
24019
  const useDocker = args.docker ?? false;
23923
24020
  const useGpu = args.gpu ?? false;
23924
24021
  const quiet = args.quiet ?? false;
23925
- const workerCount = workers ?? 4;
24022
+ const workerCount = workers ?? defaultWorkerCount();
23926
24023
  if (!quiet) {
24024
+ const workerLabel = args.workers != null ? `${workerCount} workers` : `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
23927
24025
  console.log("");
23928
24026
  console.log(
23929
24027
  c.accent("\u25C6") + " Rendering " + c.accent(project.name) + c.dim(" \u2192 " + outputPath)
23930
24028
  );
23931
- console.log(
23932
- c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerCount + " workers")
23933
- );
24029
+ console.log(c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerLabel));
23934
24030
  console.log("");
23935
24031
  }
23936
24032
  if (!useDocker) {
@@ -23977,7 +24073,7 @@ Examples:
23977
24073
  fps,
23978
24074
  quality,
23979
24075
  format,
23980
- workers,
24076
+ workers: workerCount,
23981
24077
  gpu: useGpu,
23982
24078
  quiet
23983
24079
  });
@@ -23986,7 +24082,7 @@ Examples:
23986
24082
  fps,
23987
24083
  quality,
23988
24084
  format,
23989
- workers,
24085
+ workers: workerCount,
23990
24086
  gpu: useGpu,
23991
24087
  quiet,
23992
24088
  browserPath
@@ -24261,12 +24357,21 @@ function transcodeToMp4(inputPath, outputPath) {
24261
24357
  child.on("error", () => resolvePromise(false));
24262
24358
  });
24263
24359
  }
24264
- function getStaticTemplateDir(templateId) {
24265
- const dir = dirname13(fileURLToPath5(import.meta.url));
24266
- const devPath = resolve14(dir, "..", "templates", templateId);
24267
- const builtPath = resolve14(dir, "templates", templateId);
24360
+ function resolveAssetDir(devSegments, builtSegments) {
24361
+ const base = dirname13(fileURLToPath5(import.meta.url));
24362
+ const devPath = resolve14(base, ...devSegments);
24363
+ const builtPath = resolve14(base, ...builtSegments);
24268
24364
  return existsSync26(devPath) ? devPath : builtPath;
24269
24365
  }
24366
+ function getStaticTemplateDir(templateId) {
24367
+ return resolveAssetDir(["..", "templates", templateId], ["templates", templateId]);
24368
+ }
24369
+ function getSharedTemplateDir() {
24370
+ return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
24371
+ }
24372
+ function getBundledSkillsDir() {
24373
+ return resolveAssetDir(["..", "..", "..", "..", "skills"], ["skills"]);
24374
+ }
24270
24375
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
24271
24376
  const htmlFiles = readdirSync7(dir, { withFileTypes: true, recursive: true }).filter((e) => e.isFile() && e.name.endsWith(".html")).map((e) => join24(e.parentPath ?? e.path, e.name));
24272
24377
  for (const file of htmlFiles) {
@@ -24417,6 +24522,28 @@ function scaffoldProject(destDir, name, templateId, localVideoName, durationSeco
24417
24522
  ),
24418
24523
  "utf-8"
24419
24524
  );
24525
+ const sharedDir = getSharedTemplateDir();
24526
+ if (existsSync26(sharedDir)) {
24527
+ for (const entry of readdirSync7(sharedDir, { withFileTypes: true })) {
24528
+ const src = join24(sharedDir, entry.name);
24529
+ const dest = resolve14(destDir, entry.name);
24530
+ if (entry.isFile() || entry.isSymbolicLink()) {
24531
+ copyFileSync3(src, dest);
24532
+ }
24533
+ }
24534
+ }
24535
+ const skillsSrcDir = getBundledSkillsDir();
24536
+ if (existsSync26(skillsSrcDir)) {
24537
+ const projectSkills = ["compose-video", "captions"];
24538
+ for (const skill of projectSkills) {
24539
+ const src = join24(skillsSrcDir, skill);
24540
+ if (existsSync26(src)) {
24541
+ const dest = resolve14(destDir, ".claude", "skills", skill);
24542
+ mkdirSync18(dest, { recursive: true });
24543
+ cpSync2(src, dest, { recursive: true });
24544
+ }
24545
+ }
24546
+ }
24420
24547
  }
24421
24548
  async function nextStepLoop(destDir) {
24422
24549
  while (true) {
@@ -24571,9 +24698,28 @@ Example: hyperframes init my-video --template blank --video video.mp4`);
24571
24698
  await installSkills(false);
24572
24699
  }
24573
24700
  console.log(c.success(`Created ${c.accent(name2 + "/")}`));
24574
- for (const f of readdirSync7(destDir2)) {
24701
+ for (const f of readdirSync7(destDir2).filter((f2) => !f2.startsWith("."))) {
24575
24702
  console.log(` ${c.accent(f)}`);
24576
24703
  }
24704
+ console.log();
24705
+ console.log("Next steps:");
24706
+ console.log(
24707
+ ` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes dev")} ${c.dim("# preview in studio")}`
24708
+ );
24709
+ console.log(
24710
+ ` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes render")} ${c.dim("# render to MP4")}`
24711
+ );
24712
+ console.log(
24713
+ ` ${c.accent("npx hyperframes docs")} ${c.dim("<topic>")} ${c.dim("# learn composition syntax")}`
24714
+ );
24715
+ console.log(
24716
+ ` ${c.dim("topics: data-attributes, gsap, compositions, rendering, templates, troubleshooting")}`
24717
+ );
24718
+ console.log();
24719
+ console.log(
24720
+ ` ${c.dim("AI skills installed \u2014 open this folder in your AI coding agent to get started.")}`
24721
+ );
24722
+ console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`);
24577
24723
  return;
24578
24724
  }
24579
24725
  Wt2("Create a new HyperFrames project");