@prestyj/cli 5.28.1 → 5.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/assets/motion/bin/contact-sheet.mjs +9 -3
  2. package/assets/motion/bin/cues.mjs +337 -0
  3. package/assets/motion/bin/library.mjs +53 -10
  4. package/assets/motion/bin/motion-blur.mjs +943 -0
  5. package/assets/motion/bin/motion-check.mjs +354 -3
  6. package/assets/motion/bin/music-fit.mjs +436 -0
  7. package/assets/motion/bin/pdf-extract.mjs +22 -10
  8. package/assets/motion/bin/reference-study.mjs +346 -0
  9. package/assets/motion/bin/score-synth.mjs +1052 -93
  10. package/assets/motion/library/README.md +50 -14
  11. package/assets/motion/library/kit/moves.js +1981 -0
  12. package/assets/motion/library/library.json +232 -0
  13. package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
  14. package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
  15. package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
  16. package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
  17. package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
  18. package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
  19. package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
  20. package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
  21. package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
  22. package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
  23. package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
  24. package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
  25. package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
  26. package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
  27. package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
  28. package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
  29. package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
  30. package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
  31. package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
  32. package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
  33. package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
  34. package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
  35. package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
  36. package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
  37. package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
  38. package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
  39. package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
  40. package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
  41. package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
  42. package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
  43. package/assets/motion/library/sheets/diagram.jpg +0 -0
  44. package/assets/motion/library/sheets/frame.jpg +0 -0
  45. package/assets/motion/library/sheets/transition.jpg +0 -0
  46. package/assets/motion/library/sheets/ui.jpg +0 -0
  47. package/assets/motion/references/build-sheet.md +206 -0
  48. package/assets/motion/references/runtime/determinism-rules.md +1 -1
  49. package/assets/motion/references/runtime/gsap-easing-and-stagger.md +29 -29
  50. package/assets/motion/references/runtime/inputs-and-assets.md +7 -12
  51. package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
  52. package/assets/motion/references/runtime/minimal-composition.md +1 -1
  53. package/assets/motion/references/runtime/preview-render.md +3 -3
  54. package/assets/motion/skills/app-walkthrough/SKILL.md +66 -0
  55. package/assets/motion/skills/before-after/SKILL.md +53 -0
  56. package/assets/motion/skills/brand-kit/SKILL.md +3 -3
  57. package/assets/motion/skills/dev-tool-video/SKILL.md +57 -0
  58. package/assets/motion/skills/launch-video/SKILL.md +62 -0
  59. package/assets/motion/skills/match-reference/SKILL.md +58 -0
  60. package/assets/motion/skills/motion/SKILL.md +78 -85
  61. package/assets/motion/skills/source-ingest/SKILL.md +17 -6
  62. package/assets/motion/skills/website-video/SKILL.md +59 -0
  63. package/assets/skills/bulletproof/SKILL.md +36 -11
  64. package/assets/skills/bulletproof/references/agent-surface.md +19 -9
  65. package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
  66. package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
  67. package/assets/skills/bulletproof/references/provenance.md +26 -1
  68. package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
  69. package/assets/skills/bulletproof/references/supply-chain.md +21 -17
  70. package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
  71. package/assets/skills/bulletproof/references/verification.md +2 -0
  72. package/assets/skills/clarify/SKILL.md +25 -16
  73. package/assets/skills/code-review/SKILL.md +71 -13
  74. package/assets/skills/code-review/references/agent-diffs.md +27 -0
  75. package/assets/skills/code-review/references/tests.md +19 -0
  76. package/assets/skills/compliance-guard/SKILL.md +20 -5
  77. package/assets/skills/compliance-guard/references/artifacts.md +1 -1
  78. package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
  79. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
  80. package/assets/skills/compliance-guard/references/provenance.md +41 -2
  81. package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
  82. package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
  83. package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
  84. package/assets/skills/compliance-guard/references/us.md +27 -21
  85. package/assets/skills/durable/SKILL.md +87 -79
  86. package/assets/skills/durable/references/agent-db-safety.md +69 -0
  87. package/assets/skills/durable/references/backups-and-runtime.md +19 -12
  88. package/assets/skills/durable/references/migrations-and-schema.md +13 -6
  89. package/assets/skills/evidence-led-ui/SKILL.md +69 -127
  90. package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
  91. package/assets/skills/evidence-led-ui/references/direction.md +124 -0
  92. package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
  93. package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
  94. package/assets/skills/lean/SKILL.md +90 -71
  95. package/assets/skills/lean/references/memory-and-processes.md +3 -2
  96. package/assets/skills/lean/references/playbooks.md +37 -12
  97. package/assets/skills/refactoring/SKILL.md +24 -3
  98. package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
  99. package/assets/skills/refactoring/references/legacy.md +21 -0
  100. package/assets/skills/root-cause/SKILL.md +20 -10
  101. package/assets/skills/shared-language/SKILL.md +16 -14
  102. package/assets/skills/tdd/SKILL.md +27 -15
  103. package/dist/app-sidecar.js +203 -47
  104. package/dist/app-sidecar.js.map +1 -1
  105. package/dist/cli.js +17 -26
  106. package/dist/cli.js.map +1 -1
  107. package/dist/core/acceptance-checks.d.ts +48 -0
  108. package/dist/core/acceptance-checks.js +144 -0
  109. package/dist/core/acceptance-checks.js.map +1 -0
  110. package/dist/core/agent-session.d.ts +106 -72
  111. package/dist/core/agent-session.js +539 -399
  112. package/dist/core/agent-session.js.map +1 -1
  113. package/dist/core/agents.d.ts +6 -5
  114. package/dist/core/agents.js.map +1 -1
  115. package/dist/core/ask-user.d.ts +90 -8
  116. package/dist/core/ask-user.js +124 -13
  117. package/dist/core/ask-user.js.map +1 -1
  118. package/dist/core/bundled-agents.js +1 -3
  119. package/dist/core/bundled-agents.js.map +1 -1
  120. package/dist/core/cache-diagnostics.d.ts +68 -0
  121. package/dist/core/cache-diagnostics.js +196 -0
  122. package/dist/core/cache-diagnostics.js.map +1 -0
  123. package/dist/core/cache-expiry.d.ts +87 -0
  124. package/dist/core/cache-expiry.js +111 -0
  125. package/dist/core/cache-expiry.js.map +1 -0
  126. package/dist/core/compaction/compactor.js +78 -48
  127. package/dist/core/compaction/compactor.js.map +1 -1
  128. package/dist/core/compaction/plan-step-policy.d.ts +46 -0
  129. package/dist/core/compaction/plan-step-policy.js +57 -0
  130. package/dist/core/compaction/plan-step-policy.js.map +1 -0
  131. package/dist/core/destructive-git-guard.d.ts +90 -0
  132. package/dist/core/destructive-git-guard.js +871 -0
  133. package/dist/core/destructive-git-guard.js.map +1 -0
  134. package/dist/core/event-bus.d.ts +3 -0
  135. package/dist/core/event-bus.js +5 -0
  136. package/dist/core/event-bus.js.map +1 -1
  137. package/dist/core/injection-detect.d.ts +38 -0
  138. package/dist/core/injection-detect.js +232 -0
  139. package/dist/core/injection-detect.js.map +1 -0
  140. package/dist/core/keep-awake.d.ts +88 -0
  141. package/dist/core/keep-awake.js +251 -0
  142. package/dist/core/keep-awake.js.map +1 -0
  143. package/dist/core/mcp/client.d.ts +72 -0
  144. package/dist/core/mcp/client.js +264 -41
  145. package/dist/core/mcp/client.js.map +1 -1
  146. package/dist/core/mcp/content.js +6 -2
  147. package/dist/core/mcp/content.js.map +1 -1
  148. package/dist/core/mcp/store.d.ts +6 -1
  149. package/dist/core/mcp/store.js +12 -1
  150. package/dist/core/mcp/store.js.map +1 -1
  151. package/dist/core/mcp/types.d.ts +18 -0
  152. package/dist/core/model-unavailable.d.ts +14 -0
  153. package/dist/core/model-unavailable.js +23 -0
  154. package/dist/core/model-unavailable.js.map +1 -0
  155. package/dist/core/node-debugger.d.ts +148 -0
  156. package/dist/core/node-debugger.js +642 -0
  157. package/dist/core/node-debugger.js.map +1 -0
  158. package/dist/core/package-threats.d.ts +18 -0
  159. package/dist/core/package-threats.js +168 -0
  160. package/dist/core/package-threats.js.map +1 -0
  161. package/dist/core/persistent-shell.d.ts +58 -6
  162. package/dist/core/persistent-shell.js +331 -49
  163. package/dist/core/persistent-shell.js.map +1 -1
  164. package/dist/core/process-manager.d.ts +14 -0
  165. package/dist/core/process-manager.js +61 -0
  166. package/dist/core/process-manager.js.map +1 -1
  167. package/dist/core/progress/git-xp.js +8 -14
  168. package/dist/core/progress/git-xp.js.map +1 -1
  169. package/dist/core/session-history.d.ts +12 -0
  170. package/dist/core/session-history.js +27 -0
  171. package/dist/core/session-history.js.map +1 -1
  172. package/dist/core/session-manager.d.ts +13 -1
  173. package/dist/core/session-manager.js +38 -18
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/session-summary-index.d.ts +37 -0
  176. package/dist/core/session-summary-index.js +172 -0
  177. package/dist/core/session-summary-index.js.map +1 -0
  178. package/dist/core/settings-manager.d.ts +2 -0
  179. package/dist/core/settings-manager.js +10 -0
  180. package/dist/core/settings-manager.js.map +1 -1
  181. package/dist/core/shell-threats-popular-packages.d.ts +11 -0
  182. package/dist/core/shell-threats-popular-packages.js +675 -0
  183. package/dist/core/shell-threats-popular-packages.js.map +1 -0
  184. package/dist/core/shell-threats.d.ts +8 -0
  185. package/dist/core/shell-threats.js +186 -0
  186. package/dist/core/shell-threats.js.map +1 -0
  187. package/dist/core/skills.js +3 -1
  188. package/dist/core/skills.js.map +1 -1
  189. package/dist/core/stream-rules.d.ts +30 -0
  190. package/dist/core/stream-rules.js +151 -0
  191. package/dist/core/stream-rules.js.map +1 -0
  192. package/dist/core/subagent-manager.d.ts +20 -5
  193. package/dist/core/subagent-manager.js +22 -8
  194. package/dist/core/subagent-manager.js.map +1 -1
  195. package/dist/core/subagent-receipt.d.ts +54 -0
  196. package/dist/core/subagent-receipt.js +276 -0
  197. package/dist/core/subagent-receipt.js.map +1 -0
  198. package/dist/core/subagent-turn-record.d.ts +2 -0
  199. package/dist/core/subagent-turn-record.js.map +1 -1
  200. package/dist/core/test-impact.d.ts +73 -0
  201. package/dist/core/test-impact.js +467 -0
  202. package/dist/core/test-impact.js.map +1 -0
  203. package/dist/core/thinking-level.d.ts +1 -1
  204. package/dist/core/thinking-level.js +1 -1
  205. package/dist/core/thinking-level.js.map +1 -1
  206. package/dist/core/verification-gate.d.ts +2 -0
  207. package/dist/core/verification-gate.js +4 -0
  208. package/dist/core/verification-gate.js.map +1 -1
  209. package/dist/core/verification-snapshot.js +3 -5
  210. package/dist/core/verification-snapshot.js.map +1 -1
  211. package/dist/core/workspace-guard.d.ts +18 -7
  212. package/dist/core/workspace-guard.js +227 -60
  213. package/dist/core/workspace-guard.js.map +1 -1
  214. package/dist/interactive.js +2 -1
  215. package/dist/interactive.js.map +1 -1
  216. package/dist/modes/subagent-worker-mode.js +34 -6
  217. package/dist/modes/subagent-worker-mode.js.map +1 -1
  218. package/dist/motion-agent/motion-agent.d.ts +6 -2
  219. package/dist/motion-agent/motion-agent.js +5 -7
  220. package/dist/motion-agent/motion-agent.js.map +1 -1
  221. package/dist/motion-agent/motion-prompt.d.ts +1 -1
  222. package/dist/motion-agent/motion-prompt.js +14 -19
  223. package/dist/motion-agent/motion-prompt.js.map +1 -1
  224. package/dist/motion-agent/motion-review.d.ts +10 -3
  225. package/dist/motion-agent/motion-review.js +15 -7
  226. package/dist/motion-agent/motion-review.js.map +1 -1
  227. package/dist/motion-agent/motion-studio-context.js +1 -1
  228. package/dist/motion-agent/motion-studio-context.js.map +1 -1
  229. package/dist/system-prompt.js +3 -1
  230. package/dist/system-prompt.js.map +1 -1
  231. package/dist/test-support/keep-alive.d.ts +14 -0
  232. package/dist/test-support/keep-alive.js +17 -0
  233. package/dist/test-support/keep-alive.js.map +1 -0
  234. package/dist/tools/ask-user.js +3 -3
  235. package/dist/tools/ask-user.js.map +1 -1
  236. package/dist/tools/bash-read-evidence.d.ts +10 -0
  237. package/dist/tools/bash-read-evidence.js +133 -0
  238. package/dist/tools/bash-read-evidence.js.map +1 -0
  239. package/dist/tools/bash.d.ts +10 -1
  240. package/dist/tools/bash.js +115 -7
  241. package/dist/tools/bash.js.map +1 -1
  242. package/dist/tools/debug.d.ts +54 -0
  243. package/dist/tools/debug.js +233 -0
  244. package/dist/tools/debug.js.map +1 -0
  245. package/dist/tools/edit.js +12 -4
  246. package/dist/tools/edit.js.map +1 -1
  247. package/dist/tools/goals.d.ts +1 -1
  248. package/dist/tools/index.d.ts +19 -2
  249. package/dist/tools/index.js +47 -7
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/prompt-hints.js +2 -0
  252. package/dist/tools/prompt-hints.js.map +1 -1
  253. package/dist/tools/read-tracker.d.ts +5 -0
  254. package/dist/tools/read-tracker.js +19 -9
  255. package/dist/tools/read-tracker.js.map +1 -1
  256. package/dist/tools/read.js +3 -2
  257. package/dist/tools/read.js.map +1 -1
  258. package/dist/tools/skill.js +5 -0
  259. package/dist/tools/skill.js.map +1 -1
  260. package/dist/tools/subagent-control.js +44 -8
  261. package/dist/tools/subagent-control.js.map +1 -1
  262. package/dist/tools/subagent-shared.d.ts +29 -8
  263. package/dist/tools/subagent-shared.js +45 -14
  264. package/dist/tools/subagent-shared.js.map +1 -1
  265. package/dist/tools/subagent.d.ts +8 -2
  266. package/dist/tools/subagent.js +28 -10
  267. package/dist/tools/subagent.js.map +1 -1
  268. package/dist/tools/task-output.js +3 -2
  269. package/dist/tools/task-output.js.map +1 -1
  270. package/dist/tools/task-send.d.ts +1 -1
  271. package/dist/tools/task-send.js +15 -1
  272. package/dist/tools/task-send.js.map +1 -1
  273. package/dist/tools/tool-tiers.d.ts +2 -2
  274. package/dist/tools/tool-tiers.js +3 -2
  275. package/dist/tools/tool-tiers.js.map +1 -1
  276. package/dist/tools/truncate.d.ts +21 -0
  277. package/dist/tools/truncate.js +187 -0
  278. package/dist/tools/truncate.js.map +1 -1
  279. package/dist/tools/ui-adopt.js +2 -0
  280. package/dist/tools/ui-adopt.js.map +1 -1
  281. package/dist/ui/App.d.ts +0 -4
  282. package/dist/ui/App.js +5 -28
  283. package/dist/ui/App.js.map +1 -1
  284. package/dist/ui/components/ActivityIndicator.js +1 -0
  285. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  286. package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
  287. package/dist/ui/hooks/useAgentLoop.js +1 -119
  288. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  289. package/dist/ui/render.d.ts +0 -4
  290. package/dist/ui/render.js +0 -2
  291. package/dist/ui/render.js.map +1 -1
  292. package/dist/utils/git.d.ts +77 -0
  293. package/dist/utils/git.js +285 -21
  294. package/dist/utils/git.js.map +1 -1
  295. package/dist/utils/github-ci.js +2 -1
  296. package/dist/utils/github-ci.js.map +1 -1
  297. package/dist/utils/github.js +11 -9
  298. package/dist/utils/github.js.map +1 -1
  299. package/dist/utils/image.d.ts +14 -0
  300. package/dist/utils/image.js +16 -0
  301. package/dist/utils/image.js.map +1 -1
  302. package/dist/utils/process.d.ts +20 -0
  303. package/dist/utils/process.js +98 -0
  304. package/dist/utils/process.js.map +1 -1
  305. package/package.json +5 -5
  306. package/assets/motion/references/motion-language.md +0 -128
  307. package/assets/motion/skills/video-qa/SKILL.md +0 -89
  308. package/dist/core/ideal-review-subagent.d.ts +0 -56
  309. package/dist/core/ideal-review-subagent.js +0 -112
  310. package/dist/core/ideal-review-subagent.js.map +0 -1
  311. package/dist/core/ideal-review.d.ts +0 -82
  312. package/dist/core/ideal-review.js +0 -242
  313. package/dist/core/ideal-review.js.map +0 -1
  314. package/dist/motion-agent/motion-check-tool.d.ts +0 -35
  315. package/dist/motion-agent/motion-check-tool.js +0 -514
  316. package/dist/motion-agent/motion-check-tool.js.map +0 -1
@@ -17,7 +17,9 @@ function fail(message) {
17
17
  }
18
18
 
19
19
  const args = process.argv.slice(2);
20
- const positional = args.filter((a, i) => !a.startsWith("--") && !args[i - 1]?.match(/^--(cols|width)$/));
20
+ const positional = args.filter(
21
+ (a, i) => !a.startsWith("--") && !args[i - 1]?.match(/^--(cols|width)$/),
22
+ );
21
23
  const [framesArg, outArg] = positional;
22
24
  if (!framesArg || !outArg) {
23
25
  fail("usage: contact-sheet.mjs <frames-dir> <out.jpg> [--cols 4] [--width 1600] [--phone]");
@@ -39,13 +41,17 @@ try {
39
41
  const hfPkg = require.resolve("hyperframes/package.json");
40
42
  sharp = createRequire(hfPkg)("sharp");
41
43
  } catch {
42
- fail("Contact sheets need the HyperFrames bundled with EZ. Reinstall EZ Coder to restore Motion mode.");
44
+ fail(
45
+ "Contact sheets need the HyperFrames bundled with EZ. Reinstall EZ Coder to restore Motion mode.",
46
+ );
43
47
  }
44
48
 
45
49
  const framesDir = resolve(framesArg);
46
50
  const names = await readdir(framesDir).catch(() => fail(`Frames folder not found: ${framesDir}`));
47
51
  // Skip sheets from earlier runs (snapshot writes contact-sheet.jpg beside its frames).
48
- const entries = names.filter((n) => /\.(png|jpe?g|webp)$/i.test(n) && !/contact-sheet|phone-strip/i.test(n));
52
+ const entries = names.filter(
53
+ (n) => /\.(png|jpe?g|webp)$/i.test(n) && !/contact-sheet|phone-strip/i.test(n),
54
+ );
49
55
  if (entries.length === 0) fail(`No frame images in ${framesDir}`);
50
56
  const order = (name) => {
51
57
  const m = basename(name, extname(name)).match(/(\d+(?:\.\d+)?)(?!.*\d)/);
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ // EZ Motion cue export: load a composition the way a render does and write the
3
+ // sound cues its script marked with `kit.cue(tl, at, sfx)` to a JSON file that
4
+ // score-synth reads (`"cues": "cues.json"` in the score). Retime the animation,
5
+ // export again, and every sound moves with its event.
6
+ //
7
+ // Usage: node cues.mjs <project-dir> [--out <cues.json>] [--composition <file.html>]
8
+ // Default out: <project>/cues.json. Prints a one-line JSON summary.
9
+ //
10
+ // The page is served read-only from 127.0.0.1 to HyperFrames' own Chrome
11
+ // (`hf browser path`), driven over DevTools by HyperFrames' own puppeteer-core,
12
+ // which runs the script and returns the finished DOM. (Chrome's one-shot
13
+ // `--dump-dom` hangs in a full Chrome build on macOS, the fallback when the
14
+ // managed headless shell is missing.)
15
+ import { execFile } from "node:child_process";
16
+ import { createReadStream } from "node:fs";
17
+ import { mkdtemp, realpath, rm, stat, writeFile } from "node:fs/promises";
18
+ import { createServer } from "node:http";
19
+ import { createRequire } from "node:module";
20
+ import { tmpdir } from "node:os";
21
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
22
+ import { fileURLToPath, pathToFileURL } from "node:url";
23
+ import { promisify } from "node:util";
24
+
25
+ const run = promisify(execFile);
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ export const CUES_VERSION = 1;
28
+ const MAX_CUES = 2000;
29
+ const MAX_AIR_SAMPLES = 50 * 60 * 15;
30
+ const MAX_DOM_BYTES = 64 * 1024 * 1024;
31
+
32
+ const TYPES = {
33
+ ".html": "text/html; charset=utf-8",
34
+ ".js": "text/javascript",
35
+ ".mjs": "text/javascript",
36
+ ".css": "text/css",
37
+ ".json": "application/json",
38
+ ".svg": "image/svg+xml",
39
+ ".png": "image/png",
40
+ ".jpg": "image/jpeg",
41
+ ".jpeg": "image/jpeg",
42
+ ".webp": "image/webp",
43
+ ".gif": "image/gif",
44
+ ".woff2": "font/woff2",
45
+ ".woff": "font/woff",
46
+ ".ttf": "font/ttf",
47
+ ".otf": "font/otf",
48
+ ".mp4": "video/mp4",
49
+ ".webm": "video/webm",
50
+ ".wav": "audio/wav",
51
+ ".mp3": "audio/mpeg",
52
+ };
53
+
54
+ const ENTITIES = { quot: '"', amp: "&", lt: "<", gt: ">", apos: "'", "#39": "'", "#34": '"' };
55
+
56
+ /**
57
+ * Validate one cue list as the kit writes it.
58
+ * @param {unknown} list
59
+ * @returns {{ t: number; sfx: string; gain: number; pan: number; send?: number; name?: string }[]}
60
+ */
61
+ export function validateCues(list) {
62
+ if (!Array.isArray(list)) throw new Error("cues must be a list");
63
+ if (list.length > MAX_CUES) throw new Error(`more than ${MAX_CUES} cues`);
64
+ return list.map((cue, index) => {
65
+ if (!cue || typeof cue !== "object") throw new Error(`cue ${index} is not an object`);
66
+ const { t, sfx, gain = 1, pan = 0, send, name } = cue;
67
+ if (!(typeof t === "number" && Number.isFinite(t) && t >= 0))
68
+ throw new Error(`cue ${index}: t must be a time >= 0`);
69
+ if (typeof sfx !== "string" || !/^[a-z][a-z-]*$/.test(sfx))
70
+ throw new Error(`cue ${index}: sfx must be a sound name`);
71
+ if (!(typeof gain === "number" && gain >= 0 && gain <= 2))
72
+ throw new Error(`cue ${index}: gain must be 0..2`);
73
+ if (!(typeof pan === "number" && pan >= -1 && pan <= 1))
74
+ throw new Error(`cue ${index}: pan must be -1..1`);
75
+ if (send !== undefined && !(typeof send === "number" && send >= 0 && send <= 1))
76
+ throw new Error(`cue ${index}: send must be 0..1`);
77
+ if (name !== undefined && typeof name !== "string")
78
+ throw new Error(`cue ${index}: name must be text`);
79
+ return {
80
+ t,
81
+ sfx,
82
+ gain,
83
+ pan,
84
+ ...(send === undefined ? {} : { send }),
85
+ ...(name ? { name: name.slice(0, 60) } : {}),
86
+ };
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Validate the camera's speed curve as the kit writes it.
92
+ * @param {unknown} curve
93
+ * @returns {{ rate: number; start: number; values: number[] }}
94
+ */
95
+ export function validateAir(curve) {
96
+ if (!curve || typeof curve !== "object") throw new Error("air must be an object");
97
+ const { rate, start, values } = /** @type {Record<string, unknown>} */ (curve);
98
+ if (!(typeof rate === "number" && rate >= 10 && rate <= 240))
99
+ throw new Error("air: rate must be 10..240");
100
+ if (!(typeof start === "number" && Number.isFinite(start) && start >= 0))
101
+ throw new Error("air: start must be a time >= 0");
102
+ if (!Array.isArray(values) || values.length > MAX_AIR_SAMPLES)
103
+ throw new Error(`air: values must be a list of at most ${MAX_AIR_SAMPLES} samples`);
104
+ if (!values.every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0))
105
+ throw new Error("air: every value must be a speed >= 0");
106
+ return { rate, start, values };
107
+ }
108
+
109
+ /** Read a JSON attribute the kit wrote on the root <html> element, or null. */
110
+ function readRootAttribute(dom, attribute, label) {
111
+ // Chrome escapes `&` and `"` in attribute values but not `<` or `>`, so skip
112
+ // over quoted values instead of stopping at the first `>`.
113
+ const tag = /<html\b(?:[^>"']|"[^"]*"|'[^']*')*>/i.exec(dom)?.[0] ?? "";
114
+ const match = new RegExp(`\\s${attribute}="([^"]*)"`).exec(tag);
115
+ if (!match) return null;
116
+ const json = (match[1] ?? "").replace(/&(quot|amp|lt|gt|apos|#39|#34);/g, (_, e) => ENTITIES[e]);
117
+ try {
118
+ return { value: JSON.parse(json) };
119
+ } catch {
120
+ throw new Error(`the page wrote an unreadable ${label}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The camera speed curve a loaded page wrote, or null without a kit camera.
126
+ * @param {string} dom serialized DOM
127
+ */
128
+ export function readAir(dom) {
129
+ const found = readRootAttribute(dom, "data-ez-air", "camera curve");
130
+ return found ? validateAir(found.value) : null;
131
+ }
132
+
133
+ /**
134
+ * The cue list a loaded page wrote on its root element, or null when the
135
+ * composition marked none.
136
+ * @param {string} dom serialized DOM, as `chrome --dump-dom` prints it
137
+ */
138
+ export function readCues(dom) {
139
+ const found = readRootAttribute(dom, "data-ez-cues", "cue list");
140
+ return found ? validateCues(found.value) : null;
141
+ }
142
+
143
+ /** Serve `root` read-only on 127.0.0.1; never outside it. */
144
+ export async function serve(root) {
145
+ const base = await realpath(root);
146
+ const server = createServer(async (request, response) => {
147
+ try {
148
+ if (request.method !== "GET" && request.method !== "HEAD") {
149
+ response.writeHead(405).end();
150
+ return;
151
+ }
152
+ const path = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname);
153
+ const file = await realpath(resolve(base, `.${path}`));
154
+ const inside = relative(base, file);
155
+ if (inside.startsWith("..") || isAbsolute(inside) || !(await stat(file)).isFile()) {
156
+ response.writeHead(404).end();
157
+ return;
158
+ }
159
+ response.writeHead(200, {
160
+ "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream",
161
+ });
162
+ if (request.method === "HEAD") response.end();
163
+ else createReadStream(file).pipe(response);
164
+ } catch {
165
+ response.writeHead(404).end();
166
+ }
167
+ });
168
+ await new Promise((resolveListen, reject) => {
169
+ server.once("error", reject);
170
+ server.listen(0, "127.0.0.1", resolveListen);
171
+ });
172
+ const address = server.address();
173
+ if (!address || typeof address === "string") throw new Error("Could not start the page server");
174
+ return { server, port: address.port };
175
+ }
176
+
177
+ export async function chromePath() {
178
+ const override = process.env.EZ_MOTION_CHROME?.trim();
179
+ if (override) return resolve(override);
180
+ try {
181
+ const { stdout } = await run(
182
+ process.execPath,
183
+ [join(here, "hyperframes.mjs"), "browser", "path"],
184
+ {
185
+ timeout: 60_000,
186
+ windowsHide: true,
187
+ },
188
+ );
189
+ const found = stdout.trim().split(/\r?\n/).at(-1)?.trim();
190
+ if (found) return found;
191
+ } catch {
192
+ /* fall through to the instruction below */
193
+ }
194
+ throw new Error("Chrome for HyperFrames is missing; run `hf browser ensure` first");
195
+ }
196
+
197
+ /** The puppeteer-core HyperFrames itself drives Chrome with. */
198
+ async function loadPuppeteer() {
199
+ const fromHere = createRequire(import.meta.url);
200
+ const fromHyperframes = createRequire(fromHere.resolve("hyperframes/package.json"));
201
+ const mod = await import(pathToFileURL(fromHyperframes.resolve("puppeteer-core")).href);
202
+ return mod.default ?? mod;
203
+ }
204
+
205
+ /** Load the page in headless Chrome and return its DOM after the script ran. */
206
+ async function dumpDom(chrome, url, signal) {
207
+ const puppeteer = await loadPuppeteer();
208
+ const profile = await mkdtemp(join(tmpdir(), "ez-motion-cues-"));
209
+ let browser;
210
+ try {
211
+ browser = await puppeteer.launch({
212
+ executablePath: chrome,
213
+ headless: /chrome-headless-shell/i.test(basename(chrome)) ? "shell" : true,
214
+ userDataDir: profile,
215
+ // Linux CI containers often lack the user namespaces Chrome's sandbox needs.
216
+ args: [
217
+ "--disable-gpu",
218
+ "--mute-audio",
219
+ ...(process.platform === "linux" ? ["--no-sandbox"] : []),
220
+ ],
221
+ handleSIGINT: false,
222
+ handleSIGTERM: false,
223
+ handleSIGHUP: false,
224
+ timeout: 60_000,
225
+ signal,
226
+ });
227
+ const page = await browser.newPage();
228
+ page.setDefaultTimeout(60_000);
229
+ await page.goto(url, { waitUntil: "load" });
230
+ // Timelines may be built after fonts load (the documented async setup) and
231
+ // are registered last; the kit writes its cues in a microtask after that.
232
+ await page.evaluate(async () => {
233
+ await document.fonts.ready;
234
+ });
235
+ await page
236
+ .waitForFunction(() => Object.keys(window.__timelines ?? {}).length > 0, { timeout: 8000 })
237
+ .catch(() => undefined);
238
+ await page.evaluate(() => new Promise((done) => setTimeout(done, 0)));
239
+ const dom = await page.content();
240
+ if (!dom) throw new Error("empty page");
241
+ return dom.slice(0, MAX_DOM_BYTES);
242
+ } catch (error) {
243
+ if (signal?.aborted) throw new Error("cancelled");
244
+ throw new Error(
245
+ `Chrome could not load the page: ${error instanceof Error ? error.message : String(error)}`,
246
+ );
247
+ } finally {
248
+ await browser?.close().catch(() => undefined);
249
+ await rm(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
250
+ }
251
+ }
252
+
253
+ function parseArgs(argv) {
254
+ const [project, ...rest] = argv;
255
+ if (!project || project.startsWith("--"))
256
+ throw new Error(
257
+ "usage: cues.mjs <project-dir> [--out <cues.json>] [--composition <file.html>]",
258
+ );
259
+ const options = { project: resolve(project), composition: "index.html", out: "" };
260
+ for (let index = 0; index < rest.length; index += 2) {
261
+ const flag = rest[index];
262
+ const value = rest[index + 1];
263
+ if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value`);
264
+ if (flag === "--out") options.out = resolve(value);
265
+ else if (flag === "--composition") options.composition = value;
266
+ else throw new Error(`Unsupported option ${flag}`);
267
+ }
268
+ options.out ||= join(options.project, "cues.json");
269
+ return options;
270
+ }
271
+
272
+ async function main() {
273
+ const started = performance.now();
274
+ const options = parseArgs(process.argv.slice(2));
275
+ if (!(await stat(options.project).catch(() => null))?.isDirectory())
276
+ throw new Error("Expected a project folder");
277
+ const page = resolve(options.project, options.composition);
278
+ if (!page.startsWith(options.project + sep) || extname(page).toLowerCase() !== ".html")
279
+ throw new Error("--composition must be an .html file inside the project");
280
+ if (!(await stat(page).catch(() => null))?.isFile())
281
+ throw new Error(`${options.composition} not found`);
282
+ const chrome = await chromePath();
283
+ const controller = new AbortController();
284
+ const abort = () => controller.abort();
285
+ process.once("SIGTERM", abort);
286
+ process.once("SIGINT", abort);
287
+ const { server, port } = await serve(options.project);
288
+ try {
289
+ const route = relative(options.project, page).split(sep).map(encodeURIComponent).join("/");
290
+ const dom = await dumpDom(chrome, `http://127.0.0.1:${port}/${route}`, controller.signal);
291
+ const cues = readCues(dom);
292
+ const air = readAir(dom);
293
+ // A plain page load does not mount sub-composition files, so their scripts never run.
294
+ const unloaded = (dom.match(/\sdata-composition-src=/g) ?? []).length;
295
+ const note = unloaded
296
+ ? `${unloaded} sub-composition file(s) were not loaded; cues marked inside them are missing. Mark cues in the root composition's script.`
297
+ : undefined;
298
+ if (!cues && !air)
299
+ throw new Error(
300
+ `The composition marked no cues. Load the kit and call kit.cue(tl, at, sfx) where each sound belongs.${note ? ` ${note}` : ""}`,
301
+ );
302
+ const file = {
303
+ version: CUES_VERSION,
304
+ composition: options.composition,
305
+ cues: cues ?? [],
306
+ ...(air ? { air } : {}),
307
+ };
308
+ await writeFile(options.out, `${JSON.stringify(file, null, 2)}\n`);
309
+ process.stdout.write(
310
+ `${JSON.stringify({
311
+ ok: true,
312
+ out: options.out,
313
+ cues: file.cues.length,
314
+ sounds: [...new Set(file.cues.map((cue) => cue.sfx))].sort(),
315
+ ...(air ? { cameraAir: true } : {}),
316
+ ...(note ? { note } : {}),
317
+ elapsedMs: Math.round(performance.now() - started),
318
+ })}\n`,
319
+ );
320
+ } finally {
321
+ process.removeListener("SIGTERM", abort);
322
+ process.removeListener("SIGINT", abort);
323
+ server.closeAllConnections();
324
+ server.close();
325
+ }
326
+ }
327
+
328
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
329
+ try {
330
+ await main();
331
+ } catch (error) {
332
+ process.stderr.write(
333
+ `Cue export failed: ${error instanceof Error ? error.message : "invalid input"}\n`,
334
+ );
335
+ process.exitCode = 1;
336
+ }
337
+ }
@@ -8,11 +8,14 @@
8
8
  // node library.mjs show <id>
9
9
  // node library.mjs look <project-dir> <look-id>
10
10
  // node library.mjs add <project-dir> <piece-id> [<piece-id> ...] [--force]
11
+ // node library.mjs kit <project-dir>
11
12
  //
12
13
  // `look` writes <project>/assets/looks/<id>.css (design tokens as CSS
13
14
  // variables) and installs the look's fonts. `add` copies pieces into
14
15
  // <project>/compositions/<id>.html, installs their fonts, and records
15
- // third-party credits in CREDITS.md. Every command prints one JSON line.
16
+ // third-party credits in CREDITS.md. `kit` installs the move kit
17
+ // (library/kit/moves.js) as <project>/assets/kit/moves.js; `add` does the
18
+ // same for pieces that require it. Every command prints one JSON line.
16
19
  import { execFileSync } from "node:child_process";
17
20
  import { appendFile, copyFile, mkdir, readFile, stat } from "node:fs/promises";
18
21
  import { dirname, join, resolve } from "node:path";
@@ -49,15 +52,28 @@ async function projectDir(arg) {
49
52
  /** Returns the installed families and fonts.mjs's page-ready <style> block. */
50
53
  function installFonts(project, families) {
51
54
  if (families.length === 0) return { installed: [], head: [] };
52
- const out = execFileSync(process.execPath, [join(BIN, "fonts.mjs"), "add", project, ...families], {
53
- encoding: "utf8",
54
- });
55
+ const out = execFileSync(
56
+ process.execPath,
57
+ [join(BIN, "fonts.mjs"), "add", project, ...families],
58
+ {
59
+ encoding: "utf8",
60
+ },
61
+ );
55
62
  const result = JSON.parse(out);
56
63
  if (!result.ok) fail(`Font install failed: ${result.error}`);
57
64
  // Inline, not a linked fonts.css: `hf check` only sees @font-face written in the page.
58
65
  return { installed: result.installed, head: [result.head] };
59
66
  }
60
67
 
68
+ const KIT_HEAD = '<script src="assets/kit/moves.js"></script>';
69
+
70
+ /** Copies the move kit into the project; returns the <script> for the root head. */
71
+ async function installKit(project) {
72
+ await mkdir(join(project, "assets", "kit"), { recursive: true });
73
+ await copyFile(join(LIBRARY, "kit", "moves.js"), join(project, "assets", "kit", "moves.js"));
74
+ return KIT_HEAD;
75
+ }
76
+
61
77
  const summary = (entry, type) => ({
62
78
  id: entry.id,
63
79
  type,
@@ -76,7 +92,8 @@ if (command === "list") {
76
92
  const which = rest[0] === "looks" || rest[0] === "pieces" ? rest[0] : undefined;
77
93
  const kindIndex = rest.indexOf("--kind");
78
94
  const kind = kindIndex >= 0 ? rest[kindIndex + 1] : undefined;
79
- if (kind && !lib.kinds.includes(kind)) fail(`Unknown kind "${kind}". Kinds: ${lib.kinds.join(", ")}`);
95
+ if (kind && !lib.kinds.includes(kind))
96
+ fail(`Unknown kind "${kind}". Kinds: ${lib.kinds.join(", ")}`);
80
97
  const looks = which === "pieces" || kind ? [] : lib.looks.map((l) => summary(l, "look"));
81
98
  const pieces =
82
99
  which === "looks"
@@ -85,7 +102,8 @@ if (command === "list") {
85
102
  // Contact sheets show many previews in one image read.
86
103
  const sheets = {};
87
104
  if (looks.length > 0) sheets.looks = join(LIBRARY, "sheets", "looks.jpg");
88
- for (const k of new Set(pieces.map((p) => p.kind))) sheets[k] = join(LIBRARY, "sheets", `${k}.jpg`);
105
+ for (const k of new Set(pieces.map((p) => p.kind)))
106
+ sheets[k] = join(LIBRARY, "sheets", `${k}.jpg`);
89
107
  done({ ok: true, kinds: lib.kinds, sheets, looks, pieces });
90
108
  }
91
109
 
@@ -120,7 +138,11 @@ if (command === "show") {
120
138
  ...(look ?? piece),
121
139
  preview: join(dir, "preview.jpg"),
122
140
  files: look
123
- ? { spec: join(dir, "look.md"), tokens: join(dir, "tokens.css"), sample: join(dir, "sample.html") }
141
+ ? {
142
+ spec: join(dir, "look.md"),
143
+ tokens: join(dir, "tokens.css"),
144
+ sample: join(dir, "sample.html"),
145
+ }
124
146
  : { source: join(dir, "piece.html") },
125
147
  });
126
148
  }
@@ -145,6 +167,18 @@ if (command === "look") {
145
167
  });
146
168
  }
147
169
 
170
+ if (command === "kit") {
171
+ const project = await projectDir(rest[0]);
172
+ const kit = await installKit(project);
173
+ done({
174
+ ok: true,
175
+ installed: join(project, "assets", "kit", "moves.js"),
176
+ kit,
177
+ use: "Put kit in the root index.html <head> after GSAP; scripts then use window.GGMotionKit.",
178
+ reference: join(LIBRARY, "README.md"),
179
+ });
180
+ }
181
+
148
182
  if (command === "add") {
149
183
  const force = rest.includes("--force");
150
184
  const [projectArg, ...ids] = rest.filter((a) => a !== "--force");
@@ -187,17 +221,26 @@ if (command === "add") {
187
221
  let importmap;
188
222
  if (pieces.some((p) => (p.requires ?? []).includes("three"))) {
189
223
  const three = JSON.parse(
190
- execFileSync(process.execPath, [join(BIN, "three.mjs"), "add", project], { encoding: "utf8" }),
224
+ execFileSync(process.execPath, [join(BIN, "three.mjs"), "add", project], {
225
+ encoding: "utf8",
226
+ }),
191
227
  );
192
228
  if (!three.ok) fail(`Three.js install failed: ${three.error}`);
193
229
  importmap = three.importmap;
194
230
  }
231
+ // Kit pieces call window.GGMotionKit, loaded once by the root page.
232
+ const kit = pieces.some((p) => (p.requires ?? []).includes("kit"))
233
+ ? await installKit(project)
234
+ : undefined;
195
235
  done({
196
236
  ok: true,
197
237
  added,
198
238
  kept,
199
239
  fonts: fonts.installed,
200
- ...(importmap ? { importmap, note: "put importmap in index.html <head> before any module script" } : {}),
240
+ ...(importmap
241
+ ? { importmap, note: "put importmap in index.html <head> before any module script" }
242
+ : {}),
243
+ ...(kit ? { kit, kitNote: "put kit in index.html <head> after GSAP" } : {}),
201
244
  mount: pieces.map(
202
245
  (p) =>
203
246
  `<div id="${p.id}-clip" data-composition-id="${p.id}" data-composition-src="compositions/${p.id}.html" data-start="0" data-duration="${p.duration}" data-width="1920" data-height="1080" data-track-index="0"></div>`,
@@ -206,5 +249,5 @@ if (command === "add") {
206
249
  }
207
250
 
208
251
  fail(
209
- "usage: library.mjs list [looks|pieces] [--kind k] | search <words> | show <id> | look <project> <look-id> | add <project> <piece-id>... [--force]",
252
+ "usage: library.mjs list [looks|pieces] [--kind k] | search <words> | show <id> | look <project> <look-id> | add <project> <piece-id>... [--force] | kit <project>",
210
253
  );