@1agh/maude 0.41.0 → 0.43.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 (260) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/annotations-layer.tsx +1055 -278
  4. package/apps/studio/annotations-model.ts +58 -0
  5. package/apps/studio/api.ts +173 -63
  6. package/apps/studio/bin/_html-playwright.mjs +26 -4
  7. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  8. package/apps/studio/bin/_pdf-playwright.mjs +13 -2
  9. package/apps/studio/bin/_png-playwright.mjs +15 -2
  10. package/apps/studio/bin/_pptx-playwright.mjs +17 -4
  11. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  12. package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
  13. package/apps/studio/bin/_svg-playwright.mjs +26 -4
  14. package/apps/studio/bin/_video-playwright.mjs +141 -71
  15. package/apps/studio/bin/ingest-footage.sh +27 -0
  16. package/apps/studio/bin/photo-adjust.sh +163 -0
  17. package/apps/studio/bin/photo-bg-remove.sh +160 -0
  18. package/apps/studio/bin/probe-footage.sh +28 -0
  19. package/apps/studio/bin/read-annotations.mjs +125 -2
  20. package/apps/studio/bin/screenshot.sh +53 -4
  21. package/apps/studio/canvas-edit.ts +459 -10
  22. package/apps/studio/canvas-lib.tsx +523 -5
  23. package/apps/studio/canvas-pipeline.ts +65 -7
  24. package/apps/studio/canvas-shell.tsx +156 -16
  25. package/apps/studio/client/app.jsx +476 -88
  26. package/apps/studio/client/export-center.jsx +460 -0
  27. package/apps/studio/client/github.js +4 -0
  28. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  29. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  30. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  31. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  32. package/apps/studio/client/photo-knobs.jsx +437 -0
  33. package/apps/studio/client/styles/3-shell-maude.css +59 -0
  34. package/apps/studio/client/styles/4-components.css +150 -0
  35. package/apps/studio/commands/edit-source-command.ts +31 -4
  36. package/apps/studio/config.schema.json +2 -2
  37. package/apps/studio/context-menu.tsx +31 -6
  38. package/apps/studio/dist/client.bundle.js +5561 -19
  39. package/apps/studio/dist/comment-mount.js +2 -2
  40. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  41. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  42. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  43. package/apps/studio/dist/styles.css +1 -1
  44. package/apps/studio/dom-selection.ts +25 -0
  45. package/apps/studio/export-dialog.tsx +18 -25
  46. package/apps/studio/exporters/_runtime.ts +104 -0
  47. package/apps/studio/exporters/html.ts +12 -20
  48. package/apps/studio/exporters/index.ts +14 -2
  49. package/apps/studio/exporters/jobs.ts +376 -0
  50. package/apps/studio/exporters/pdf.ts +16 -20
  51. package/apps/studio/exporters/png.ts +12 -20
  52. package/apps/studio/exporters/pptx.ts +22 -23
  53. package/apps/studio/exporters/scope.ts +1 -0
  54. package/apps/studio/exporters/svg.ts +14 -22
  55. package/apps/studio/exporters/video.ts +37 -19
  56. package/apps/studio/footage/schema.test.ts +195 -0
  57. package/apps/studio/footage/schema.ts +484 -0
  58. package/apps/studio/footage-store.ts +235 -0
  59. package/apps/studio/fs-watch.ts +10 -0
  60. package/apps/studio/git/endpoints.ts +68 -7
  61. package/apps/studio/git/service.ts +198 -20
  62. package/apps/studio/github/service.ts +67 -0
  63. package/apps/studio/handoff.ts +3 -2
  64. package/apps/studio/http.ts +318 -64
  65. package/apps/studio/input-router.tsx +10 -0
  66. package/apps/studio/inspect.ts +53 -1
  67. package/apps/studio/paths.ts +10 -0
  68. package/apps/studio/photo/filters.ts +285 -0
  69. package/apps/studio/photo/pipeline.ts +607 -0
  70. package/apps/studio/photo/schema.ts +367 -0
  71. package/apps/studio/photo-store.ts +154 -0
  72. package/apps/studio/runtime-bundle.ts +29 -6
  73. package/apps/studio/server.ts +8 -2
  74. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  75. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  76. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  77. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  78. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  79. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  80. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  81. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  82. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  83. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  84. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  85. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  86. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  87. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  88. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  89. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  90. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  91. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  92. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  93. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  94. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  95. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  96. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  97. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  98. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  99. package/apps/studio/stickers/life-style/best.png +0 -0
  100. package/apps/studio/stickers/life-style/come-on.png +0 -0
  101. package/apps/studio/stickers/life-style/cut.png +0 -0
  102. package/apps/studio/stickers/life-style/detox.png +0 -0
  103. package/apps/studio/stickers/life-style/just.png +0 -0
  104. package/apps/studio/stickers/life-style/manifest.json +68 -0
  105. package/apps/studio/stickers/life-style/mirror.png +0 -0
  106. package/apps/studio/stickers/life-style/nice.png +0 -0
  107. package/apps/studio/stickers/life-style/objects.png +0 -0
  108. package/apps/studio/stickers/life-style/ok.png +0 -0
  109. package/apps/studio/stickers/life-style/queen.png +0 -0
  110. package/apps/studio/stickers/life-style/star.png +0 -0
  111. package/apps/studio/stickers/life-style/sun.png +0 -0
  112. package/apps/studio/stickers/life-style/water.png +0 -0
  113. package/apps/studio/stickers/life-style/yeah.png +0 -0
  114. package/apps/studio/stickers/life-style/you-can.png +0 -0
  115. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  116. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  117. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  118. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  119. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  120. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  121. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  122. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  123. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  124. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  125. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  126. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  127. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  128. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  129. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  130. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  131. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  132. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  133. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  134. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  135. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  136. package/apps/studio/stickers/project-status/group-100.png +0 -0
  137. package/apps/studio/stickers/project-status/group-101.png +0 -0
  138. package/apps/studio/stickers/project-status/group-102.png +0 -0
  139. package/apps/studio/stickers/project-status/group-103.png +0 -0
  140. package/apps/studio/stickers/project-status/group-104.png +0 -0
  141. package/apps/studio/stickers/project-status/group-105.png +0 -0
  142. package/apps/studio/stickers/project-status/group-106.png +0 -0
  143. package/apps/studio/stickers/project-status/group-107.png +0 -0
  144. package/apps/studio/stickers/project-status/group-108.png +0 -0
  145. package/apps/studio/stickers/project-status/group-109.png +0 -0
  146. package/apps/studio/stickers/project-status/group-110.png +0 -0
  147. package/apps/studio/stickers/project-status/group-111.png +0 -0
  148. package/apps/studio/stickers/project-status/group-112.png +0 -0
  149. package/apps/studio/stickers/project-status/group-113.png +0 -0
  150. package/apps/studio/stickers/project-status/group-114.png +0 -0
  151. package/apps/studio/stickers/project-status/group-115.png +0 -0
  152. package/apps/studio/stickers/project-status/group-117.png +0 -0
  153. package/apps/studio/stickers/project-status/group-118.png +0 -0
  154. package/apps/studio/stickers/project-status/group-119.png +0 -0
  155. package/apps/studio/stickers/project-status/group-120.png +0 -0
  156. package/apps/studio/stickers/project-status/group-121.png +0 -0
  157. package/apps/studio/stickers/project-status/group-122.png +0 -0
  158. package/apps/studio/stickers/project-status/group-41.png +0 -0
  159. package/apps/studio/stickers/project-status/group-42.png +0 -0
  160. package/apps/studio/stickers/project-status/group-43.png +0 -0
  161. package/apps/studio/stickers/project-status/group-44.png +0 -0
  162. package/apps/studio/stickers/project-status/group-45.png +0 -0
  163. package/apps/studio/stickers/project-status/group-46.png +0 -0
  164. package/apps/studio/stickers/project-status/group-47.png +0 -0
  165. package/apps/studio/stickers/project-status/group-48.png +0 -0
  166. package/apps/studio/stickers/project-status/group-49.png +0 -0
  167. package/apps/studio/stickers/project-status/group-50.png +0 -0
  168. package/apps/studio/stickers/project-status/group-51.png +0 -0
  169. package/apps/studio/stickers/project-status/group-52.png +0 -0
  170. package/apps/studio/stickers/project-status/group-53.png +0 -0
  171. package/apps/studio/stickers/project-status/group-54.png +0 -0
  172. package/apps/studio/stickers/project-status/group-55.png +0 -0
  173. package/apps/studio/stickers/project-status/group-56.png +0 -0
  174. package/apps/studio/stickers/project-status/group-57.png +0 -0
  175. package/apps/studio/stickers/project-status/group-58.png +0 -0
  176. package/apps/studio/stickers/project-status/group-59.png +0 -0
  177. package/apps/studio/stickers/project-status/group-60.png +0 -0
  178. package/apps/studio/stickers/project-status/group-61.png +0 -0
  179. package/apps/studio/stickers/project-status/group-62.png +0 -0
  180. package/apps/studio/stickers/project-status/group-63.png +0 -0
  181. package/apps/studio/stickers/project-status/group-64.png +0 -0
  182. package/apps/studio/stickers/project-status/group-65.png +0 -0
  183. package/apps/studio/stickers/project-status/group-66.png +0 -0
  184. package/apps/studio/stickers/project-status/group-67.png +0 -0
  185. package/apps/studio/stickers/project-status/group-68.png +0 -0
  186. package/apps/studio/stickers/project-status/group-69.png +0 -0
  187. package/apps/studio/stickers/project-status/group-70.png +0 -0
  188. package/apps/studio/stickers/project-status/group-71.png +0 -0
  189. package/apps/studio/stickers/project-status/group-72.png +0 -0
  190. package/apps/studio/stickers/project-status/group-73.png +0 -0
  191. package/apps/studio/stickers/project-status/group-74.png +0 -0
  192. package/apps/studio/stickers/project-status/group-75.png +0 -0
  193. package/apps/studio/stickers/project-status/group-76.png +0 -0
  194. package/apps/studio/stickers/project-status/group-77.png +0 -0
  195. package/apps/studio/stickers/project-status/group-78.png +0 -0
  196. package/apps/studio/stickers/project-status/group-79.png +0 -0
  197. package/apps/studio/stickers/project-status/group-80.png +0 -0
  198. package/apps/studio/stickers/project-status/group-81.png +0 -0
  199. package/apps/studio/stickers/project-status/group-82.png +0 -0
  200. package/apps/studio/stickers/project-status/group-83.png +0 -0
  201. package/apps/studio/stickers/project-status/group-84.png +0 -0
  202. package/apps/studio/stickers/project-status/group-85.png +0 -0
  203. package/apps/studio/stickers/project-status/group-86.png +0 -0
  204. package/apps/studio/stickers/project-status/group-87.png +0 -0
  205. package/apps/studio/stickers/project-status/group-88.png +0 -0
  206. package/apps/studio/stickers/project-status/group-89.png +0 -0
  207. package/apps/studio/stickers/project-status/group-90.png +0 -0
  208. package/apps/studio/stickers/project-status/group-91.png +0 -0
  209. package/apps/studio/stickers/project-status/group-92.png +0 -0
  210. package/apps/studio/stickers/project-status/group-93.png +0 -0
  211. package/apps/studio/stickers/project-status/group-94.png +0 -0
  212. package/apps/studio/stickers/project-status/group-96.png +0 -0
  213. package/apps/studio/stickers/project-status/group-97.png +0 -0
  214. package/apps/studio/stickers/project-status/group-98.png +0 -0
  215. package/apps/studio/stickers/project-status/group-99.png +0 -0
  216. package/apps/studio/stickers/project-status/manifest.json +328 -0
  217. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  218. package/apps/studio/test/acp-commands.test.ts +2 -1
  219. package/apps/studio/test/asset-api.test.ts +3 -3
  220. package/apps/studio/test/canvas-edit.test.ts +17 -6
  221. package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
  222. package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
  223. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  224. package/apps/studio/test/edit-source-command.test.ts +23 -0
  225. package/apps/studio/test/element-structural-edit.test.ts +46 -0
  226. package/apps/studio/test/export-center.test.tsx +287 -0
  227. package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
  228. package/apps/studio/test/exporters/history.test.ts +32 -3
  229. package/apps/studio/test/exporters/jobs.test.ts +263 -0
  230. package/apps/studio/test/footage-store.test.ts +100 -0
  231. package/apps/studio/test/git-branches.test.ts +97 -0
  232. package/apps/studio/test/github-api.test.ts +56 -1
  233. package/apps/studio/test/input-router.test.ts +33 -0
  234. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  235. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  236. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  237. package/apps/studio/test/photo-filters.test.ts +149 -0
  238. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  239. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  240. package/apps/studio/test/read-annotations.test.ts +164 -0
  241. package/apps/studio/test/shell-importmap.test.ts +49 -0
  242. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  243. package/apps/studio/test/timeline-parse.test.ts +57 -0
  244. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  245. package/apps/studio/test/undo-stack.test.ts +33 -0
  246. package/apps/studio/test/video-asset.test.ts +5 -5
  247. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  248. package/apps/studio/text-caret.ts +239 -0
  249. package/apps/studio/tool-palette.tsx +43 -20
  250. package/apps/studio/undo-stack.ts +94 -2
  251. package/apps/studio/use-annotation-resize.tsx +4 -4
  252. package/apps/studio/use-canvas-media-drop.tsx +38 -1
  253. package/apps/studio/use-selection-set.tsx +21 -0
  254. package/apps/studio/video-comp.tsx +48 -7
  255. package/apps/studio/whats-new.json +70 -0
  256. package/apps/studio/ws.ts +6 -0
  257. package/cli/commands/design.mjs +34 -3
  258. package/cli/lib/gitignore-block.mjs +2 -0
  259. package/package.json +10 -10
  260. package/plugins/design/templates/_shell.html +1 -0
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ // _ingest-footage.mjs — ingest a DIRECTORY of raw footage into the design root's
3
+ // content-addressed asset store, behind `maude design ingest-footage` (DDR-062
4
+ // dispatch). The folder-scale counterpart to `_fetch-asset.mjs` (single URL).
5
+ //
6
+ // feature-footage-analysis-director, Task 3. WHY THIS EXISTS: the `/design:reel`
7
+ // director pipeline wants to start from a folder of client clips
8
+ // (`/…/podklady/video`). This verb copies each recognised video (and any image/
9
+ // audio) into `<designRoot>/assets/<sha8>.<ext>` — the SAME content-addressed
10
+ // convention `/_api/asset` + `_fetch-asset.mjs` use, so a clip references
11
+ // uniformly as `assets/<sha8>.mp4` on both the main and the canvas origin.
12
+ //
13
+ // SECURITY / DISCIPLINE (mirrors _fetch-asset.mjs):
14
+ // • magic-byte sniff decides the category + extension — the source name /
15
+ // extension is NEVER trusted (a `.mp4` that is really an HTML polyglot →
16
+ // skipped). Mirrors api.ts sniffAssetType (mp4/mov/m4v/webm video · mp3/wav/
17
+ // m4a audio · png/jpg/gif/webp image); SVG/script → null → skipped.
18
+ // • content-addressed `<sha8>.<ext>` name, `[a-z0-9._-]` only, written FLAT
19
+ // under assets/ with a realpath containment assertion.
20
+ // • per-file size cap (default 100 MB — the DDR-148 video ceiling); oversized
21
+ // files are SKIPPED LOUDLY (listed in the manifest), never silently dropped.
22
+ // • the source directory is READ-ONLY — this never writes back into it.
23
+ // • no network, no shell interpolation, no container parsing (magic bytes only).
24
+ //
25
+ // Reached via `maude design ingest-footage <dir> --root <repo>`, never a raw path.
26
+ // stdout (default) = a JSON manifest { clips:[{asset,src,bytes,category}],
27
+ // skipped:[{src,why}], assetsDir }. Exit: 0 ok (even with skips) · 2 usage ·
28
+ // 6 write/containment error · 1 other.
29
+
30
+ import { createHash } from 'node:crypto';
31
+ import {
32
+ closeSync,
33
+ copyFileSync,
34
+ createReadStream,
35
+ existsSync,
36
+ mkdirSync,
37
+ openSync,
38
+ readdirSync,
39
+ readSync,
40
+ statSync,
41
+ } from 'node:fs';
42
+ import { extname, join, resolve, sep } from 'node:path';
43
+ import { pathToFileURL } from 'node:url';
44
+
45
+ // The DDR-148 video ceiling (100 MB). Images/audio also ride this generous cap
46
+ // here — the reel pipeline's inputs are dominated by video. Override per-file
47
+ // via --max-bytes.
48
+ const DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
49
+
50
+ class IngestError extends Error {
51
+ constructor(code, message) {
52
+ super(message);
53
+ this.code = code;
54
+ }
55
+ }
56
+
57
+ // ── magic-byte sniff (mirrors apps/studio/api.ts sniffAssetType) ──────────────
58
+
59
+ /** Sniff → { ext, category } or null. Bytes decide; name/extension untrusted. */
60
+ export function sniffAsset(b) {
61
+ // Images (mirrors sniffImageType).
62
+ if (
63
+ b.length >= 8 &&
64
+ b[0] === 0x89 &&
65
+ b[1] === 0x50 &&
66
+ b[2] === 0x4e &&
67
+ b[3] === 0x47 &&
68
+ b[4] === 0x0d &&
69
+ b[5] === 0x0a &&
70
+ b[6] === 0x1a &&
71
+ b[7] === 0x0a
72
+ )
73
+ return { ext: 'png', category: 'image' };
74
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
75
+ return { ext: 'jpg', category: 'image' };
76
+ if (
77
+ b.length >= 6 &&
78
+ b[0] === 0x47 &&
79
+ b[1] === 0x49 &&
80
+ b[2] === 0x46 &&
81
+ b[3] === 0x38 &&
82
+ (b[4] === 0x37 || b[4] === 0x39) &&
83
+ b[5] === 0x61
84
+ )
85
+ return { ext: 'gif', category: 'image' };
86
+ if (
87
+ b.length >= 12 &&
88
+ b[0] === 0x52 &&
89
+ b[1] === 0x49 &&
90
+ b[2] === 0x46 &&
91
+ b[3] === 0x46 &&
92
+ b[8] === 0x57 &&
93
+ b[9] === 0x45 &&
94
+ b[10] === 0x42 &&
95
+ b[11] === 0x50
96
+ )
97
+ return { ext: 'webp', category: 'image' };
98
+ // ISO-BMFF (mp4 / mov / m4a / m4v): "ftyp" at offset 4, brand at 8.
99
+ if (b.length >= 12 && b[4] === 0x66 && b[5] === 0x74 && b[6] === 0x79 && b[7] === 0x70) {
100
+ const brand = String.fromCharCode(b[8] ?? 0, b[9] ?? 0, b[10] ?? 0, b[11] ?? 0);
101
+ if (brand === 'qt ') return { ext: 'mov', category: 'video' };
102
+ if (brand.startsWith('M4A')) return { ext: 'm4a', category: 'audio' };
103
+ if (brand.startsWith('M4V')) return { ext: 'm4v', category: 'video' };
104
+ return { ext: 'mp4', category: 'video' };
105
+ }
106
+ // Matroska / WebM — EBML header.
107
+ if (b.length >= 4 && b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
108
+ return { ext: 'webm', category: 'video' };
109
+ // MP3 — "ID3" or frame-sync.
110
+ if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
111
+ return { ext: 'mp3', category: 'audio' };
112
+ if (b.length >= 2 && b[0] === 0xff && ((b[1] ?? 0) & 0xe0) === 0xe0)
113
+ return { ext: 'mp3', category: 'audio' };
114
+ // WAV.
115
+ if (
116
+ b.length >= 12 &&
117
+ b[0] === 0x52 &&
118
+ b[1] === 0x49 &&
119
+ b[2] === 0x46 &&
120
+ b[3] === 0x46 &&
121
+ b[8] === 0x57 &&
122
+ b[9] === 0x41 &&
123
+ b[10] === 0x56 &&
124
+ b[11] === 0x45
125
+ )
126
+ return { ext: 'wav', category: 'audio' };
127
+ return null;
128
+ }
129
+
130
+ // ── path helpers (mirror _fetch-asset.mjs containment) ───────────────────────
131
+
132
+ /** Resolve `<root>/<designRootRel>/assets` with a containment assertion. */
133
+ export function assetsDirFor(root, designRootRel) {
134
+ const rootAbs = resolve(root);
135
+ const assetsDir = resolve(rootAbs, designRootRel, 'assets');
136
+ if (assetsDir !== rootAbs && !assetsDir.startsWith(rootAbs + sep))
137
+ throw new IngestError(6, `assets dir escapes root: ${assetsDir}`);
138
+ return assetsDir;
139
+ }
140
+
141
+ /** Assert a content-addressed name and resolve it inside assets/ (no traversal). */
142
+ export function containedName(assetsDir, name) {
143
+ if (!/^[a-z0-9]{8}\.(png|jpg|gif|webp|mp4|mov|m4v|webm|mp3|wav|m4a)$/.test(name))
144
+ throw new IngestError(6, `generated name failed the charset contract: ${name}`);
145
+ const fileAbs = resolve(assetsDir, name);
146
+ if (fileAbs !== join(assetsDir, name) || !fileAbs.startsWith(assetsDir + sep))
147
+ throw new IngestError(6, `resolved asset path escapes assets dir: ${fileAbs}`);
148
+ return fileAbs;
149
+ }
150
+
151
+ // ── sniff head + full-file hash (streamed — clips can be 100 MB) ─────────────
152
+
153
+ /** Read the first `n` bytes of a file for the magic-byte sniff. */
154
+ function readHead(file, n = 16) {
155
+ const fd = openSync(file, 'r');
156
+ try {
157
+ const buf = Buffer.alloc(n);
158
+ const bytesRead = readSync(fd, buf, 0, n, 0);
159
+ return buf.subarray(0, bytesRead);
160
+ } finally {
161
+ closeSync(fd);
162
+ }
163
+ }
164
+
165
+ /** Full-file sha256 (streamed) → first 8 hex chars. */
166
+ function hashFile(file) {
167
+ return new Promise((res, rej) => {
168
+ const h = createHash('sha256');
169
+ createReadStream(file)
170
+ .on('error', rej)
171
+ .on('data', (d) => h.update(d))
172
+ .on('end', () => res(h.digest('hex').slice(0, 8)));
173
+ });
174
+ }
175
+
176
+ // ── directory walk + ingest ──────────────────────────────────────────────────
177
+
178
+ function listFiles(dir, recursive) {
179
+ const out = [];
180
+ const walk = (d) => {
181
+ let entries;
182
+ try {
183
+ entries = readdirSync(d, { withFileTypes: true });
184
+ } catch (e) {
185
+ throw new IngestError(2, `cannot read directory ${d}: ${e?.message ?? e}`);
186
+ }
187
+ for (const ent of entries) {
188
+ if (ent.name.startsWith('.')) continue; // skip dotfiles / .DS_Store
189
+ const abs = join(d, ent.name);
190
+ if (ent.isDirectory()) {
191
+ if (recursive) walk(abs);
192
+ } else if (ent.isFile()) {
193
+ out.push(abs);
194
+ }
195
+ }
196
+ };
197
+ walk(dir);
198
+ return out.sort();
199
+ }
200
+
201
+ /**
202
+ * Ingest every recognised media file under `dir` into assets/. Returns
203
+ * { clips, skipped, assetsDir }. Never throws on a per-file problem (that file
204
+ * is skipped with a reason); throws IngestError only for whole-run failures
205
+ * (unreadable dir, containment breach).
206
+ */
207
+ export async function ingestFootage({
208
+ dir,
209
+ root,
210
+ designRootRel = '.design',
211
+ maxBytes = DEFAULT_MAX_BYTES,
212
+ recursive = false,
213
+ }) {
214
+ const srcDir = resolve(dir);
215
+ if (!existsSync(srcDir) || !statSync(srcDir).isDirectory())
216
+ throw new IngestError(2, `not a directory: ${srcDir}`);
217
+
218
+ const assetsDir = assetsDirFor(root, designRootRel);
219
+ mkdirSync(assetsDir, { recursive: true });
220
+
221
+ const clips = [];
222
+ const skipped = [];
223
+ const seen = new Set(); // sha8 dedupe within this run
224
+
225
+ for (const file of listFiles(srcDir, recursive)) {
226
+ let size;
227
+ try {
228
+ size = statSync(file).size;
229
+ } catch (e) {
230
+ skipped.push({ src: file, why: `stat failed: ${e?.message ?? e}` });
231
+ continue;
232
+ }
233
+ if (size === 0) {
234
+ skipped.push({ src: file, why: 'empty file' });
235
+ continue;
236
+ }
237
+ if (size > maxBytes) {
238
+ skipped.push({
239
+ src: file,
240
+ why: `exceeds the ${Math.round(maxBytes / (1024 * 1024))} MB per-file cap (${Math.round(size / (1024 * 1024))} MB)`,
241
+ });
242
+ continue;
243
+ }
244
+ let info;
245
+ try {
246
+ info = sniffAsset(readHead(file));
247
+ } catch (e) {
248
+ skipped.push({ src: file, why: `read failed: ${e?.message ?? e}` });
249
+ continue;
250
+ }
251
+ if (!info) {
252
+ skipped.push({
253
+ src: file,
254
+ why: `unrecognised media (not video/image/audio); ext ${extname(file) || '(none)'}`,
255
+ });
256
+ continue;
257
+ }
258
+ let sha8;
259
+ try {
260
+ sha8 = await hashFile(file);
261
+ } catch (e) {
262
+ skipped.push({ src: file, why: `hash failed: ${e?.message ?? e}` });
263
+ continue;
264
+ }
265
+ const name = `${sha8}.${info.ext}`;
266
+ let fileAbs;
267
+ try {
268
+ fileAbs = containedName(assetsDir, name);
269
+ } catch (e) {
270
+ skipped.push({ src: file, why: e?.message ?? 'containment error' });
271
+ continue;
272
+ }
273
+ // Content-addressed dedupe: identical bytes → identical name → copy once.
274
+ if (!existsSync(fileAbs)) {
275
+ try {
276
+ copyFileSync(file, fileAbs);
277
+ } catch (e) {
278
+ skipped.push({ src: file, why: `copy failed: ${e?.message ?? e}` });
279
+ continue;
280
+ }
281
+ }
282
+ if (!seen.has(sha8)) {
283
+ seen.add(sha8);
284
+ clips.push({ asset: `assets/${name}`, src: file, bytes: size, category: info.category });
285
+ }
286
+ }
287
+
288
+ return { clips, skipped, assetsDir };
289
+ }
290
+
291
+ // ── CLI ──────────────────────────────────────────────────────────────────────
292
+
293
+ function parseArgv(argv) {
294
+ const out = {
295
+ dir: null,
296
+ root: null,
297
+ designRoot: '.design',
298
+ maxBytes: DEFAULT_MAX_BYTES,
299
+ recursive: false,
300
+ json: true,
301
+ };
302
+ for (let i = 0; i < argv.length; i += 1) {
303
+ const a = argv[i];
304
+ switch (a) {
305
+ case '--root':
306
+ out.root = argv[++i];
307
+ break;
308
+ case '--design-root':
309
+ out.designRoot = argv[++i];
310
+ break;
311
+ case '--max-bytes':
312
+ out.maxBytes = Number(argv[++i]);
313
+ break;
314
+ case '--recursive':
315
+ out.recursive = true;
316
+ break;
317
+ case '--json':
318
+ out.json = true;
319
+ break;
320
+ case '--help':
321
+ case '-h':
322
+ out.help = true;
323
+ break;
324
+ default:
325
+ if (a.startsWith('-')) throw new IngestError(2, `unknown flag ${a}`);
326
+ if (out.dir === null) out.dir = a;
327
+ else throw new IngestError(2, `unexpected extra arg ${a}`);
328
+ }
329
+ }
330
+ return out;
331
+ }
332
+
333
+ const HELP = `ingest-footage — copy a folder of raw footage into the design asset store
334
+ (reached via \`maude design ingest-footage\`)
335
+
336
+ Usage:
337
+ maude design ingest-footage <dir> --root <repo> [--design-root .design]
338
+ [--recursive] [--max-bytes N]
339
+
340
+ Walks <dir>, magic-byte-sniffs each file (mp4/mov/m4v/webm video · mp3/wav/m4a
341
+ audio · png/jpg/gif/webp image; SVG/script/unrecognised skipped), content-
342
+ addresses each to <designRoot>/assets/<sha8>.<ext>, and prints a JSON manifest.
343
+ Oversized / unrecognised files are SKIPPED and listed under "skipped" — never
344
+ silently dropped. The source directory is read-only.
345
+
346
+ Exit: 0 ok (even with skips) · 2 usage · 6 write/containment error · 1 other.`;
347
+
348
+ async function main() {
349
+ let opts;
350
+ try {
351
+ opts = parseArgv(process.argv.slice(2));
352
+ } catch (err) {
353
+ process.stderr.write(`ingest-footage: ${err.message}\n`);
354
+ process.exit(err instanceof IngestError ? err.code : 2);
355
+ }
356
+ if (opts.help) {
357
+ process.stdout.write(`${HELP}\n`);
358
+ return;
359
+ }
360
+ if (!opts.dir) {
361
+ process.stderr.write('ingest-footage: <dir> required\n');
362
+ process.exit(2);
363
+ }
364
+ if (!Number.isFinite(opts.maxBytes) || opts.maxBytes <= 0) {
365
+ process.stderr.write('ingest-footage: --max-bytes must be a positive number\n');
366
+ process.exit(2);
367
+ }
368
+ const root = opts.root || process.env.CLAUDE_PROJECT_DIR || process.cwd();
369
+ try {
370
+ const r = await ingestFootage({
371
+ dir: opts.dir,
372
+ root,
373
+ designRootRel: opts.designRoot,
374
+ maxBytes: opts.maxBytes,
375
+ recursive: opts.recursive,
376
+ });
377
+ process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
378
+ } catch (err) {
379
+ process.stderr.write(`ingest-footage: ${err.message}\n`);
380
+ process.exit(err instanceof IngestError ? err.code : 1);
381
+ }
382
+ }
383
+
384
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
385
+ main();
386
+ }
@@ -88,11 +88,15 @@ try {
88
88
  : screens[i];
89
89
  // Pin the captured element's own artboard to (0,0) right before its
90
90
  // capture so the world's multi-artboard layout doesn't push the bbox off
91
- // the viewport.
92
- await handle.evaluate((el) => {
91
+ // the viewport. Save the prior value so it can be restored right after
92
+ // this artboard's PDF is written — otherwise every artboard captured
93
+ // earlier in the loop stays stacked at (0,0) and bleeds into later pages.
94
+ const savedPos = await handle.evaluate((el) => {
93
95
  const ab = el.closest('[data-dc-screen]') ?? el;
96
+ const prev = { left: ab.style.left, top: ab.style.top };
94
97
  ab.style.left = '0px';
95
98
  ab.style.top = '0px';
99
+ return prev;
96
100
  });
97
101
  const rect = await handle.evaluate((el) => {
98
102
  const r = el.getBoundingClientRect();
@@ -120,6 +124,13 @@ try {
120
124
  });
121
125
  writeFileSync(targetPath, pdf);
122
126
  written.push(targetPath);
127
+ // Restore the artboard's original position now that its page is written.
128
+ await handle.evaluate((el, prev) => {
129
+ const ab = el.closest('[data-dc-screen]') ?? el;
130
+ ab.style.left = prev.left;
131
+ ab.style.top = prev.top;
132
+ }, savedPos);
133
+ if (multi) console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
123
134
  }
124
135
 
125
136
  for (const w of written) console.log(w);
@@ -82,11 +82,16 @@ try {
82
82
  // artboard #2 (or any non-first selection's host) left the wrong artboard
83
83
  // at the origin (item 5 root cause). Each artboard carries
84
84
  // `style="left:…; top:…;"` for the world layout — zero the right one so the
85
- // screenshot clip starts at the viewport origin.
86
- await widenedHandle.evaluate((el) => {
85
+ // screenshot clip starts at the viewport origin. Save the prior value so it
86
+ // can be restored after this artboard's capture — otherwise every artboard
87
+ // processed earlier in a `--multi` loop is left stacked at (0,0) and bleeds
88
+ // into every subsequent target's clip region (the scatter bug).
89
+ const savedPos = await widenedHandle.evaluate((el) => {
87
90
  const ab = el.closest('[data-dc-screen]') ?? el;
91
+ const prev = { left: ab.style.left, top: ab.style.top };
88
92
  ab.style.left = '0px';
89
93
  ab.style.top = '0px';
94
+ return prev;
90
95
  });
91
96
  const rect = await widenedHandle.evaluate((el) => {
92
97
  const r = el.getBoundingClientRect();
@@ -118,6 +123,13 @@ try {
118
123
  },
119
124
  });
120
125
  written.push(target);
126
+ // Restore the artboard's original position now that its capture is done,
127
+ // so it doesn't stay pinned at (0,0) and corrupt the next target's clip.
128
+ await widenedHandle.evaluate((el, prev) => {
129
+ const ab = el.closest('[data-dc-screen]') ?? el;
130
+ ab.style.left = prev.left;
131
+ ab.style.top = prev.top;
132
+ }, savedPos);
121
133
  };
122
134
 
123
135
  if (multi) {
@@ -131,6 +143,7 @@ try {
131
143
  const handle = screens[i];
132
144
  const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
133
145
  await captureHandle(handle, join(outDir, `${id}.png`));
146
+ console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
134
147
  }
135
148
  } else {
136
149
  if (!out) {
@@ -48,10 +48,6 @@ try {
48
48
  world.style.zoom = '1';
49
49
  world.style.transform = 'none';
50
50
  }
51
- for (const el of document.querySelectorAll('[data-dc-screen]')) {
52
- el.style.left = '0px';
53
- el.style.top = '0px';
54
- }
55
51
  });
56
52
  // dom-to-pptx ships a UMD bundle; addScriptTag exposes `window.domToPptx`.
57
53
  await page.addScriptTag({ path: bundlePath });
@@ -74,6 +70,17 @@ try {
74
70
  }
75
71
  await handle.waitFor({ state: 'visible', timeout: timeoutMs });
76
72
 
73
+ // Pin this artboard to (0,0) for its capture, restoring afterwards so a
74
+ // future multi-slide implementation doesn't inherit a stacked-at-origin bug
75
+ // (the scatter bug fixed in the png/pdf/svg/html shims).
76
+ const savedPos = await handle.evaluate((el) => {
77
+ const ab = el.closest('[data-dc-screen]') ?? el;
78
+ const prev = { left: ab.style.left, top: ab.style.top };
79
+ ab.style.left = '0px';
80
+ ab.style.top = '0px';
81
+ return prev;
82
+ });
83
+
77
84
  // Run dom-to-pptx inside the page. It returns a Blob; we serialise to a
78
85
  // byte array for transport across the playwright boundary.
79
86
  const bytesArray = await handle.evaluate(async (el) => {
@@ -88,6 +95,12 @@ try {
88
95
  return Array.from(new Uint8Array(ab));
89
96
  });
90
97
 
98
+ await handle.evaluate((el, prev) => {
99
+ const ab = el.closest('[data-dc-screen]') ?? el;
100
+ ab.style.left = prev.left;
101
+ ab.style.top = prev.top;
102
+ }, savedPos);
103
+
91
104
  writeFileSync(out, new Uint8Array(bytesArray));
92
105
  console.log(out);
93
106
  console.error(