@1agh/maude 0.42.0 → 0.44.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 (332) 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/acp/bridge.ts +44 -10
  4. package/apps/studio/acp/env.ts +23 -0
  5. package/apps/studio/acp/login-state.ts +263 -0
  6. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  7. package/apps/studio/acp/probe.ts +152 -1
  8. package/apps/studio/annotations-layer.tsx +1451 -324
  9. package/apps/studio/annotations-model.ts +58 -0
  10. package/apps/studio/api.ts +568 -24
  11. package/apps/studio/bin/_audio-search.mjs +140 -0
  12. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  13. package/apps/studio/bin/_import-asset.mjs +815 -0
  14. package/apps/studio/bin/_import-brand.mjs +635 -0
  15. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  16. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  17. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  18. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  19. package/apps/studio/bin/_transcribe.mjs +419 -0
  20. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  21. package/apps/studio/bin/_video-playwright.mjs +141 -71
  22. package/apps/studio/bin/audio-search.sh +28 -0
  23. package/apps/studio/bin/generate.sh +154 -0
  24. package/apps/studio/bin/import-asset.sh +34 -0
  25. package/apps/studio/bin/import-brand.sh +29 -0
  26. package/apps/studio/bin/import-tokens.sh +33 -0
  27. package/apps/studio/bin/ingest-footage.sh +27 -0
  28. package/apps/studio/bin/photo-adjust.sh +163 -0
  29. package/apps/studio/bin/photo-bg-remove.sh +258 -0
  30. package/apps/studio/bin/probe-footage.sh +28 -0
  31. package/apps/studio/bin/read-annotations.mjs +125 -2
  32. package/apps/studio/bin/transcribe.sh +39 -0
  33. package/apps/studio/canvas-edit.ts +656 -12
  34. package/apps/studio/canvas-lib.tsx +637 -10
  35. package/apps/studio/canvas-pipeline.ts +65 -7
  36. package/apps/studio/canvas-shell.tsx +156 -16
  37. package/apps/studio/client/app.jsx +2385 -699
  38. package/apps/studio/client/export-center.jsx +35 -1
  39. package/apps/studio/client/generate-dialog.jsx +352 -0
  40. package/apps/studio/client/github.js +18 -0
  41. package/apps/studio/client/inspector-controls.jsx +781 -0
  42. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  43. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  44. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  45. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  46. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  47. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  48. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  49. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  50. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  51. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  52. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  53. package/apps/studio/client/photo-knobs.jsx +432 -0
  54. package/apps/studio/client/styles/3-shell-maude.css +376 -28
  55. package/apps/studio/client/styles/4-components.css +223 -0
  56. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  57. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  58. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  59. package/apps/studio/commands/edit-source-command.ts +31 -4
  60. package/apps/studio/config.schema.json +70 -0
  61. package/apps/studio/context-menu.tsx +42 -8
  62. package/apps/studio/context.ts +16 -0
  63. package/apps/studio/design-setup-readiness.ts +115 -0
  64. package/apps/studio/dist/client.bundle.js +5572 -21
  65. package/apps/studio/dist/comment-mount.js +2 -2
  66. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  67. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  68. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  69. package/apps/studio/dist/styles.css +1 -1
  70. package/apps/studio/dom-selection.ts +25 -0
  71. package/apps/studio/draw/palette.ts +34 -0
  72. package/apps/studio/exporters/jobs.ts +47 -5
  73. package/apps/studio/exporters/video.ts +24 -4
  74. package/apps/studio/footage/schema.test.ts +287 -0
  75. package/apps/studio/footage/schema.ts +662 -0
  76. package/apps/studio/footage-store.ts +235 -0
  77. package/apps/studio/fs-watch.ts +10 -0
  78. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  79. package/apps/studio/generation/adapters/gemini.ts +542 -0
  80. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  81. package/apps/studio/generation/adapters/groq.ts +247 -0
  82. package/apps/studio/generation/audio-library.test.ts +95 -0
  83. package/apps/studio/generation/audio-library.ts +156 -0
  84. package/apps/studio/generation/captions.test.ts +78 -0
  85. package/apps/studio/generation/captions.ts +132 -0
  86. package/apps/studio/generation/download.ts +76 -0
  87. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  88. package/apps/studio/generation/gemini.test.ts +350 -0
  89. package/apps/studio/generation/jobs.test.ts +128 -0
  90. package/apps/studio/generation/jobs.ts +243 -0
  91. package/apps/studio/generation/keys.test.ts +75 -0
  92. package/apps/studio/generation/keys.ts +162 -0
  93. package/apps/studio/generation/prefs.test.ts +95 -0
  94. package/apps/studio/generation/prefs.ts +82 -0
  95. package/apps/studio/generation/registry.ts +88 -0
  96. package/apps/studio/generation/types.test.ts +123 -0
  97. package/apps/studio/generation/types.ts +266 -0
  98. package/apps/studio/generation/whisper-models.test.ts +152 -0
  99. package/apps/studio/generation/whisper-models.ts +257 -0
  100. package/apps/studio/generation/whisper-spike-results.md +61 -0
  101. package/apps/studio/git/endpoints.ts +68 -7
  102. package/apps/studio/git/service.ts +199 -21
  103. package/apps/studio/github/service.ts +67 -0
  104. package/apps/studio/handoff.ts +3 -2
  105. package/apps/studio/http.ts +1039 -22
  106. package/apps/studio/input-router.tsx +10 -0
  107. package/apps/studio/inspect.ts +53 -1
  108. package/apps/studio/media/intro.mp4 +0 -0
  109. package/apps/studio/media-commit-chain.ts +119 -0
  110. package/apps/studio/paths.ts +20 -0
  111. package/apps/studio/photo/filters.ts +285 -0
  112. package/apps/studio/photo/pipeline.ts +607 -0
  113. package/apps/studio/photo/schema.ts +367 -0
  114. package/apps/studio/photo-store.ts +154 -0
  115. package/apps/studio/readiness.ts +108 -20
  116. package/apps/studio/runtime-bundle.ts +29 -6
  117. package/apps/studio/scaffold-design.ts +1145 -0
  118. package/apps/studio/server.ts +18 -2
  119. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  120. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  121. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  122. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  123. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  124. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  125. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  126. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  127. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  128. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  129. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  130. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  131. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  132. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  133. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  134. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  135. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  136. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  137. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  138. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  139. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  140. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  141. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  142. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  143. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  144. package/apps/studio/stickers/life-style/best.png +0 -0
  145. package/apps/studio/stickers/life-style/come-on.png +0 -0
  146. package/apps/studio/stickers/life-style/cut.png +0 -0
  147. package/apps/studio/stickers/life-style/detox.png +0 -0
  148. package/apps/studio/stickers/life-style/just.png +0 -0
  149. package/apps/studio/stickers/life-style/manifest.json +68 -0
  150. package/apps/studio/stickers/life-style/mirror.png +0 -0
  151. package/apps/studio/stickers/life-style/nice.png +0 -0
  152. package/apps/studio/stickers/life-style/objects.png +0 -0
  153. package/apps/studio/stickers/life-style/ok.png +0 -0
  154. package/apps/studio/stickers/life-style/queen.png +0 -0
  155. package/apps/studio/stickers/life-style/star.png +0 -0
  156. package/apps/studio/stickers/life-style/sun.png +0 -0
  157. package/apps/studio/stickers/life-style/water.png +0 -0
  158. package/apps/studio/stickers/life-style/yeah.png +0 -0
  159. package/apps/studio/stickers/life-style/you-can.png +0 -0
  160. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  161. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  162. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  163. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  164. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  165. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  166. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  167. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  168. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  169. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  170. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  171. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  172. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  173. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  174. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  175. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  176. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  177. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  178. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  179. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  180. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  181. package/apps/studio/stickers/project-status/group-100.png +0 -0
  182. package/apps/studio/stickers/project-status/group-101.png +0 -0
  183. package/apps/studio/stickers/project-status/group-102.png +0 -0
  184. package/apps/studio/stickers/project-status/group-103.png +0 -0
  185. package/apps/studio/stickers/project-status/group-104.png +0 -0
  186. package/apps/studio/stickers/project-status/group-105.png +0 -0
  187. package/apps/studio/stickers/project-status/group-106.png +0 -0
  188. package/apps/studio/stickers/project-status/group-107.png +0 -0
  189. package/apps/studio/stickers/project-status/group-108.png +0 -0
  190. package/apps/studio/stickers/project-status/group-109.png +0 -0
  191. package/apps/studio/stickers/project-status/group-110.png +0 -0
  192. package/apps/studio/stickers/project-status/group-111.png +0 -0
  193. package/apps/studio/stickers/project-status/group-112.png +0 -0
  194. package/apps/studio/stickers/project-status/group-113.png +0 -0
  195. package/apps/studio/stickers/project-status/group-114.png +0 -0
  196. package/apps/studio/stickers/project-status/group-115.png +0 -0
  197. package/apps/studio/stickers/project-status/group-117.png +0 -0
  198. package/apps/studio/stickers/project-status/group-118.png +0 -0
  199. package/apps/studio/stickers/project-status/group-119.png +0 -0
  200. package/apps/studio/stickers/project-status/group-120.png +0 -0
  201. package/apps/studio/stickers/project-status/group-121.png +0 -0
  202. package/apps/studio/stickers/project-status/group-122.png +0 -0
  203. package/apps/studio/stickers/project-status/group-41.png +0 -0
  204. package/apps/studio/stickers/project-status/group-42.png +0 -0
  205. package/apps/studio/stickers/project-status/group-43.png +0 -0
  206. package/apps/studio/stickers/project-status/group-44.png +0 -0
  207. package/apps/studio/stickers/project-status/group-45.png +0 -0
  208. package/apps/studio/stickers/project-status/group-46.png +0 -0
  209. package/apps/studio/stickers/project-status/group-47.png +0 -0
  210. package/apps/studio/stickers/project-status/group-48.png +0 -0
  211. package/apps/studio/stickers/project-status/group-49.png +0 -0
  212. package/apps/studio/stickers/project-status/group-50.png +0 -0
  213. package/apps/studio/stickers/project-status/group-51.png +0 -0
  214. package/apps/studio/stickers/project-status/group-52.png +0 -0
  215. package/apps/studio/stickers/project-status/group-53.png +0 -0
  216. package/apps/studio/stickers/project-status/group-54.png +0 -0
  217. package/apps/studio/stickers/project-status/group-55.png +0 -0
  218. package/apps/studio/stickers/project-status/group-56.png +0 -0
  219. package/apps/studio/stickers/project-status/group-57.png +0 -0
  220. package/apps/studio/stickers/project-status/group-58.png +0 -0
  221. package/apps/studio/stickers/project-status/group-59.png +0 -0
  222. package/apps/studio/stickers/project-status/group-60.png +0 -0
  223. package/apps/studio/stickers/project-status/group-61.png +0 -0
  224. package/apps/studio/stickers/project-status/group-62.png +0 -0
  225. package/apps/studio/stickers/project-status/group-63.png +0 -0
  226. package/apps/studio/stickers/project-status/group-64.png +0 -0
  227. package/apps/studio/stickers/project-status/group-65.png +0 -0
  228. package/apps/studio/stickers/project-status/group-66.png +0 -0
  229. package/apps/studio/stickers/project-status/group-67.png +0 -0
  230. package/apps/studio/stickers/project-status/group-68.png +0 -0
  231. package/apps/studio/stickers/project-status/group-69.png +0 -0
  232. package/apps/studio/stickers/project-status/group-70.png +0 -0
  233. package/apps/studio/stickers/project-status/group-71.png +0 -0
  234. package/apps/studio/stickers/project-status/group-72.png +0 -0
  235. package/apps/studio/stickers/project-status/group-73.png +0 -0
  236. package/apps/studio/stickers/project-status/group-74.png +0 -0
  237. package/apps/studio/stickers/project-status/group-75.png +0 -0
  238. package/apps/studio/stickers/project-status/group-76.png +0 -0
  239. package/apps/studio/stickers/project-status/group-77.png +0 -0
  240. package/apps/studio/stickers/project-status/group-78.png +0 -0
  241. package/apps/studio/stickers/project-status/group-79.png +0 -0
  242. package/apps/studio/stickers/project-status/group-80.png +0 -0
  243. package/apps/studio/stickers/project-status/group-81.png +0 -0
  244. package/apps/studio/stickers/project-status/group-82.png +0 -0
  245. package/apps/studio/stickers/project-status/group-83.png +0 -0
  246. package/apps/studio/stickers/project-status/group-84.png +0 -0
  247. package/apps/studio/stickers/project-status/group-85.png +0 -0
  248. package/apps/studio/stickers/project-status/group-86.png +0 -0
  249. package/apps/studio/stickers/project-status/group-87.png +0 -0
  250. package/apps/studio/stickers/project-status/group-88.png +0 -0
  251. package/apps/studio/stickers/project-status/group-89.png +0 -0
  252. package/apps/studio/stickers/project-status/group-90.png +0 -0
  253. package/apps/studio/stickers/project-status/group-91.png +0 -0
  254. package/apps/studio/stickers/project-status/group-92.png +0 -0
  255. package/apps/studio/stickers/project-status/group-93.png +0 -0
  256. package/apps/studio/stickers/project-status/group-94.png +0 -0
  257. package/apps/studio/stickers/project-status/group-96.png +0 -0
  258. package/apps/studio/stickers/project-status/group-97.png +0 -0
  259. package/apps/studio/stickers/project-status/group-98.png +0 -0
  260. package/apps/studio/stickers/project-status/group-99.png +0 -0
  261. package/apps/studio/stickers/project-status/manifest.json +328 -0
  262. package/apps/studio/test/_helpers.ts +15 -2
  263. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  264. package/apps/studio/test/acp-bridge.test.ts +34 -1
  265. package/apps/studio/test/acp-commands.test.ts +2 -1
  266. package/apps/studio/test/acp-env.test.ts +30 -0
  267. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  268. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  269. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  270. package/apps/studio/test/annotations-layer.test.ts +174 -0
  271. package/apps/studio/test/asset-api.test.ts +3 -3
  272. package/apps/studio/test/canvas-edit.test.ts +17 -6
  273. package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
  274. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  275. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  276. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  277. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  278. package/apps/studio/test/edit-source-command.test.ts +23 -0
  279. package/apps/studio/test/element-structural-edit.test.ts +154 -0
  280. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  281. package/apps/studio/test/footage-store.test.ts +100 -0
  282. package/apps/studio/test/generate-route.test.ts +106 -0
  283. package/apps/studio/test/git-branches.test.ts +97 -0
  284. package/apps/studio/test/github-api.test.ts +56 -1
  285. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  286. package/apps/studio/test/import-asset-route.test.ts +71 -0
  287. package/apps/studio/test/import-asset.test.ts +399 -0
  288. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  289. package/apps/studio/test/import-brand-route.test.ts +120 -0
  290. package/apps/studio/test/import-brand.test.ts +206 -0
  291. package/apps/studio/test/import-tokens.test.ts +722 -0
  292. package/apps/studio/test/input-router.test.ts +33 -0
  293. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  294. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  295. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  296. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  297. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  298. package/apps/studio/test/photo-filters.test.ts +149 -0
  299. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  300. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  301. package/apps/studio/test/read-annotations.test.ts +164 -0
  302. package/apps/studio/test/readiness.test.ts +13 -11
  303. package/apps/studio/test/scaffold-design.test.ts +122 -0
  304. package/apps/studio/test/shell-importmap.test.ts +49 -0
  305. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  306. package/apps/studio/test/timeline-parse.test.ts +57 -0
  307. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  308. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  309. package/apps/studio/test/undo-stack.test.ts +33 -0
  310. package/apps/studio/test/video-asset.test.ts +5 -5
  311. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  312. package/apps/studio/text-caret.ts +239 -0
  313. package/apps/studio/tool-palette.tsx +49 -21
  314. package/apps/studio/ui-prefs.ts +130 -0
  315. package/apps/studio/undo-stack.ts +94 -2
  316. package/apps/studio/use-annotation-resize.tsx +4 -4
  317. package/apps/studio/use-canvas-media-drop.tsx +40 -1
  318. package/apps/studio/use-chrome-visibility.tsx +8 -2
  319. package/apps/studio/use-selection-set.tsx +21 -0
  320. package/apps/studio/video-comp.tsx +48 -7
  321. package/apps/studio/whats-new.json +149 -0
  322. package/apps/studio/ws.ts +5 -0
  323. package/cli/bin/maude.mjs +6 -6
  324. package/cli/commands/design.mjs +67 -3
  325. package/cli/lib/gitignore-block.mjs +2 -0
  326. package/cli/lib/pkg-root.mjs +107 -0
  327. package/cli/lib/pkg-root.test.mjs +79 -0
  328. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  329. package/cli/lib/update-check.mjs +8 -0
  330. package/package.json +13 -11
  331. package/plugins/design/dependencies.json +18 -0
  332. package/plugins/design/templates/_shell.html +1 -0
@@ -22,7 +22,13 @@ const PLUGINS: SdkPluginConfig[] = [
22
22
 
23
23
  type Meta = {
24
24
  systemPrompt?: { append?: string };
25
- claudeCode?: { options?: { plugins?: SdkPluginConfig[]; settingSources?: string[] } };
25
+ claudeCode?: {
26
+ options?: {
27
+ plugins?: SdkPluginConfig[];
28
+ settingSources?: string[];
29
+ settings?: { enabledPlugins?: Record<string, boolean> };
30
+ };
31
+ };
26
32
  };
27
33
 
28
34
  describe('newSessionParams — plugin carrier shape', () => {
@@ -79,6 +85,27 @@ describe('newSessionParams — plugin carrier shape', () => {
79
85
  expect((p._meta as Meta).claudeCode?.options?.settingSources).toEqual(['user']);
80
86
  }
81
87
  });
88
+
89
+ // DDR-168 — the structural double-registration guard: a non-empty `plugins`
90
+ // must also force off any natively-loaded user-level copy of the same id via
91
+ // the SDK's "flag" settings layer, so the bundled copy is the ONLY one that
92
+ // ever loads.
93
+ test('with plugins: forces design@maude off via options.settings.enabledPlugins (double-registration guard)', () => {
94
+ const p = newSessionParams('/repo', undefined, PLUGINS);
95
+ expect((p._meta as Meta).claudeCode?.options?.settings?.enabledPlugins).toEqual({
96
+ 'design@maude': false,
97
+ });
98
+ });
99
+
100
+ test('empty plugins: options.settings is entirely absent (web/npm path unaffected)', () => {
101
+ const p = newSessionParams('/repo', 'BRIEF', []);
102
+ expect((p._meta as Meta).claudeCode?.options?.settings).toBeUndefined();
103
+ });
104
+
105
+ test('undefined plugins: options.settings is entirely absent', () => {
106
+ const p = newSessionParams('/repo', 'BRIEF');
107
+ expect((p._meta as Meta).claudeCode?.options?.settings).toBeUndefined();
108
+ });
82
109
  });
83
110
 
84
111
  describe('upgrade guard — installed adapter + SDK still honor the plugins contract', () => {
@@ -129,4 +156,48 @@ describe('upgrade guard — installed adapter + SDK still honor the plugins cont
129
156
  expect(src).toMatch(/SdkPluginConfig\s*=\s*\{/);
130
157
  expect(src).toMatch(/type:\s*'local'/);
131
158
  });
159
+
160
+ // DDR-168 — the same style of guard, for the `options.settings` mechanism the
161
+ // double-registration fix depends on: `settings` must NOT be among the fields
162
+ // the adapter overrides after the `...userProvidedOptions` spread (else our
163
+ // `enabledPlugins: false` override would be silently discarded), and the SDK
164
+ // must still declare `Settings.enabledPlugins` as an accepted field.
165
+ test('adapter forwards `settings` from userProvidedOptions and only ever overrides it conditionally when the caller left it unset', () => {
166
+ expect(existsSync(adapterEntry)).toBe(true);
167
+ const src = readFileSync(adapterEntry, 'utf8');
168
+ // Locate the `...userProvidedOptions` spread and inspect what follows it up
169
+ // to the `query({ options })` call. Verified live at claude-agent-acp@0.57.0:
170
+ // the object DOES contain a later `settings:` assignment (a
171
+ // CLAUDE_MODEL_CONFIG env-var fallback) — but it's gated behind
172
+ // `!userProvidedOptions?.settings && …`, i.e. it only fires when the caller
173
+ // did NOT already provide a settings object. A caller-provided `settings`
174
+ // (ours, carrying `enabledPlugins`) always wins. A bump that drops that
175
+ // guard — making the fallback unconditional — must fail here, since it
176
+ // would silently clobber DDR-168's double-registration override.
177
+ const spreadIdx = src.indexOf('...userProvidedOptions');
178
+ expect(spreadIdx).toBeGreaterThan(-1);
179
+ const queryIdx = src.indexOf('query(', spreadIdx);
180
+ expect(queryIdx).toBeGreaterThan(spreadIdx);
181
+ const window = src.slice(spreadIdx, queryIdx);
182
+ expect(window).toMatch(/!userProvidedOptions\?\.settings\s*&&/);
183
+ });
184
+
185
+ test("SDK Options declares settings (Settings.enabledPlugins is the field DDR-168's guard relies on)", () => {
186
+ let sdkPkg: string | null = null;
187
+ try {
188
+ const realAdapterDir = dirname(realpathSync(adapterEntry));
189
+ sdkPkg = Bun.resolveSync('@anthropic-ai/claude-agent-sdk/package.json', realAdapterDir);
190
+ } catch {
191
+ sdkPkg = null;
192
+ }
193
+ expect(sdkPkg).not.toBeNull();
194
+ const dts = join(dirname(sdkPkg as string), 'sdk.d.ts');
195
+ expect(existsSync(dts)).toBe(true);
196
+ const src = readFileSync(dts, 'utf8');
197
+ // sdk.d.ts:1831 `settings?: Settings;` (or similarly named field on Options) +
198
+ // :5193 `Settings.enabledPlugins` — a bump that renames/drops either must
199
+ // fail here, then adapt, rather than silently reopening double-registration.
200
+ expect(src).toMatch(/settings\?\s*:/);
201
+ expect(src).toMatch(/enabledPlugins\??\s*:/);
202
+ });
132
203
  });
@@ -90,6 +90,23 @@ describe('createAnnotationStrokesCommand', () => {
90
90
  expect(putFn.mock.calls[1]?.[0]).toEqual([pen1]);
91
91
  });
92
92
 
93
+ test("do() passes the pre-op BEFORE as putFn's second arg; undo() passes the post-op AFTER", async () => {
94
+ // The live-bug regression this locks in: putStrokes needs its own
95
+ // command's baseline to tell a delete (an id `before` had that `next`
96
+ // dropped) apart from a genuinely concurrent sibling addition (an id
97
+ // `prev` has that this command's OWN before/next never knew about).
98
+ const putFn = mock(() => Promise.resolve());
99
+ const cmd = createAnnotationStrokesCommand({
100
+ before: [pen1, pen2],
101
+ after: [pen1],
102
+ putFn,
103
+ });
104
+ await cmd.do();
105
+ expect(putFn.mock.calls[0]).toEqual([[pen1], [pen1, pen2]]);
106
+ await cmd.undo();
107
+ expect(putFn.mock.calls[1]).toEqual([[pen1, pen2], [pen1]]);
108
+ });
109
+
93
110
  test('default label — add 2 strokes', () => {
94
111
  const cmd = createAnnotationStrokesCommand({
95
112
  before: [pen1],
@@ -21,6 +21,9 @@ import {
21
21
  polygonPoints,
22
22
  polygonVertices,
23
23
  type RectStroke,
24
+ reconcileCommit,
25
+ reconcileForeignEcho,
26
+ resolveImageUploadSwap,
24
27
  rid,
25
28
  STICKY_PALETTE,
26
29
  type StickyStroke,
@@ -427,6 +430,177 @@ describe('annotations-layer / strokes round-trip is stable for arrays', () => {
427
430
  });
428
431
  });
429
432
 
433
+ describe('annotations-layer / reconcileCommit (live-bug regression — delete must stick)', () => {
434
+ const a: RectStroke = {
435
+ id: 'a',
436
+ tool: 'rect',
437
+ color: '#000',
438
+ width: 2,
439
+ x: 0,
440
+ y: 0,
441
+ w: 10,
442
+ h: 10,
443
+ };
444
+ const b: RectStroke = {
445
+ id: 'b',
446
+ tool: 'rect',
447
+ color: '#000',
448
+ width: 2,
449
+ x: 20,
450
+ y: 0,
451
+ w: 10,
452
+ h: 10,
453
+ };
454
+ const c: RectStroke = {
455
+ id: 'c',
456
+ tool: 'rect',
457
+ color: '#000',
458
+ width: 2,
459
+ x: 40,
460
+ y: 0,
461
+ w: 10,
462
+ h: 10,
463
+ };
464
+ const d: RectStroke = {
465
+ id: 'd',
466
+ tool: 'rect',
467
+ color: '#000',
468
+ width: 2,
469
+ x: 60,
470
+ y: 0,
471
+ w: 10,
472
+ h: 10,
473
+ };
474
+
475
+ test('a delete stays deleted even when the rendered `prev` has not caught up yet', () => {
476
+ // This is exactly the reported bug: Backspace computes next = before
477
+ // minus the deleted id, but React's `prev` (read by the functional
478
+ // updater) still shows the pre-delete set. The old reconcileIncoming
479
+ // treated "id missing from next" as "not caught up, fold it back" —
480
+ // reverting every delete locally while the smaller set still went out
481
+ // over PUT.
482
+ const prev = [a, b, c]; // rendered state, hasn't caught up to the delete
483
+ const opBefore = [a, b, c]; // this command's own baseline
484
+ const next = [a, b]; // c deleted
485
+ expect(reconcileCommit(prev, opBefore, next)).toEqual([a, b]);
486
+ });
487
+
488
+ test('a genuinely concurrent sibling addition (unknown to this commit) is still folded in', () => {
489
+ // `prev` has `d`, which neither this commit's `opBefore` nor `next` knows
490
+ // about — a different in-flight commit added it after `opBefore` was
491
+ // captured. It must survive.
492
+ const prev = [a, b, d];
493
+ const opBefore = [a, b];
494
+ const next = [a, b, c]; // this commit adds c
495
+ expect(reconcileCommit(prev, opBefore, next)).toEqual([a, b, c, d]);
496
+ });
497
+
498
+ test('delete + concurrent addition combine correctly (delete wins for its own id, addition still folds in)', () => {
499
+ const prev = [a, b, c, d]; // rendered: hasn't caught up to the delete OR seen `d` reconciled yet
500
+ const opBefore = [a, b, c];
501
+ const next = [a, b]; // deletes c
502
+ expect(reconcileCommit(prev, opBefore, next)).toEqual([a, b, d]);
503
+ });
504
+ });
505
+
506
+ describe('annotations-layer / reconcileForeignEcho (deletes must sync across tabs/peers)', () => {
507
+ const a: RectStroke = {
508
+ id: 'a',
509
+ tool: 'rect',
510
+ color: '#000',
511
+ width: 2,
512
+ x: 0,
513
+ y: 0,
514
+ w: 10,
515
+ h: 10,
516
+ };
517
+ const b: RectStroke = {
518
+ id: 'b',
519
+ tool: 'rect',
520
+ color: '#000',
521
+ width: 2,
522
+ x: 20,
523
+ y: 0,
524
+ w: 10,
525
+ h: 10,
526
+ };
527
+ const optimisticImage: ImageStroke = {
528
+ id: 'img',
529
+ tool: 'image',
530
+ x: 0,
531
+ y: 0,
532
+ w: 10,
533
+ h: 10,
534
+ href: 'blob:local-preview',
535
+ };
536
+
537
+ test('a foreign echo missing a non-ephemeral id (a real delete) is NOT reverted', () => {
538
+ const prev = [a, b];
539
+ const incoming = [a]; // peer deleted b
540
+ expect(reconcileForeignEcho(prev, incoming)).toEqual([a]);
541
+ });
542
+
543
+ test('a still-uploading local optimistic image (ephemeral href) survives a foreign echo that predates it', () => {
544
+ const prev = [a, optimisticImage];
545
+ const incoming = [a]; // the peer's broadcast was authored before our upload started
546
+ expect(reconcileForeignEcho(prev, incoming)).toEqual([a, optimisticImage]);
547
+ });
548
+ });
549
+
550
+ describe('annotations-layer / resolveImageUploadSwap (delete-during-upload must not resurrect)', () => {
551
+ // Adversarial-review finding: dropping an image, then Backspacing it
552
+ // before its upload resolves, used to resurrect it once the upload
553
+ // landed — the swap read "id absent from `before`" as render lag
554
+ // unconditionally, with no way to tell that apart from a genuine delete.
555
+ const optimistic: ImageStroke = {
556
+ id: 'img-1',
557
+ tool: 'image',
558
+ x: 0,
559
+ y: 0,
560
+ w: 10,
561
+ h: 10,
562
+ href: 'blob:optimistic-preview',
563
+ };
564
+ const sibling: RectStroke = {
565
+ id: 'sib',
566
+ tool: 'rect',
567
+ color: '#000',
568
+ width: 2,
569
+ x: 20,
570
+ y: 0,
571
+ w: 10,
572
+ h: 10,
573
+ };
574
+
575
+ test('deleted before the chain caught up to the optimistic insert (common case) → stays gone, no undo record', () => {
576
+ const before = [sibling]; // render lag — optimistic insert not reflected yet
577
+ const result = resolveImageUploadSwap(before, 'img-1', optimistic, 'assets/real.png', true);
578
+ expect(result).toBeNull();
579
+ });
580
+
581
+ test('deleted after the chain already had the optimistic entry → explicitly stripped from `after`', () => {
582
+ const before = [sibling, optimistic];
583
+ const result = resolveImageUploadSwap(before, 'img-1', optimistic, 'assets/real.png', true);
584
+ expect(result?.after).toEqual([sibling]);
585
+ });
586
+
587
+ test('NOT deleted + render lag (the original, still-valid case) → swaps to the real href', () => {
588
+ const before = [sibling]; // optimistic insert hasn't rendered into `before` yet
589
+ const result = resolveImageUploadSwap(before, 'img-1', optimistic, 'assets/real.png', false);
590
+ const img = result?.after.find((s) => s.id === 'img-1') as unknown as { href: string };
591
+ expect(img?.href).toBe('assets/real.png');
592
+ expect(result?.commitBefore).toEqual([sibling]); // undo excludes the never-committed blob: entry
593
+ });
594
+
595
+ test('NOT deleted, already present in `before` → swaps in place, sibling order preserved', () => {
596
+ const before = [sibling, optimistic];
597
+ const result = resolveImageUploadSwap(before, 'img-1', optimistic, 'assets/real.png', false);
598
+ expect(result?.after.map((s) => s.id)).toEqual(['sib', 'img-1']);
599
+ const img = result?.after.find((s) => s.id === 'img-1') as unknown as { href: string };
600
+ expect(img?.href).toBe('assets/real.png');
601
+ });
602
+ });
603
+
430
604
  describe('annotations-layer / strokesShallowEqual (drag no-op gate)', () => {
431
605
  const pen: PenStroke = {
432
606
  id: 'p',
@@ -11,7 +11,7 @@ import { describe, expect, test } from 'bun:test';
11
11
  import { existsSync, readdirSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
 
14
- import { sniffImageType } from '../api.ts';
14
+ import { ASSET_MAX_BYTES, sniffImageType } from '../api.ts';
15
15
  import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
16
16
 
17
17
  // ── Minimal valid magic-byte headers (padded so length checks pass). ──────────
@@ -107,9 +107,9 @@ describe('asset-api / POST /_api/asset (endpoint round-trip)', () => {
107
107
  });
108
108
  });
109
109
 
110
- test('rejects an oversize body (> 10 MB) with 413', async () => {
110
+ test('rejects an oversize body (> ASSET_MAX_BYTES) with 413', async () => {
111
111
  await withServer(async (port) => {
112
- const big = new Uint8Array(10 * 1024 * 1024 + 1);
112
+ const big = new Uint8Array(ASSET_MAX_BYTES + 1);
113
113
  big.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); // valid PNG header
114
114
  const res = await postAsset(port, big);
115
115
  expect(res.status).toBe(413);
@@ -377,10 +377,13 @@ describe('canvas-edit / applyTextEdit', () => {
377
377
  expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
378
378
  });
379
379
 
380
- test('refuses an element whose only child is a JSX expression', () => {
381
- const src = 'function Demo() { const t = "x"; return <h1>{t}</h1>; }';
380
+ // unified-text-editing follow-up a `{identifier}` bound to a const string
381
+ // is now traced back to that const and edited THERE (was a hard refusal).
382
+ test('edits an {identifier} child bound to a const string (traces to the const)', () => {
383
+ const src = 'const t = "x";\nfunction Demo() { return <h1>{t}</h1>; }';
382
384
  const id = idsOf(src).h1 as string;
383
- expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
385
+ const out = applyTextEdit(CANVAS, src, id, 'New', { before: 'x' });
386
+ expect(out.source).toContain('const t = "New"');
384
387
  });
385
388
 
386
389
  test('refuses a self-closing / empty element (no text to edit)', () => {
@@ -412,10 +415,18 @@ describe('canvas-edit / applyTextEdit', () => {
412
415
  expect(() => transpileCanvasSource(CANVAS, out.source)).not.toThrow();
413
416
  });
414
417
 
415
- test('still refuses a dynamic {identifier} child (routes to /design:edit)', () => {
416
- const src = 'function Demo() { const title = "x"; return <h1>{title}</h1>; }';
418
+ test('still refuses a genuinely-computed expression child (routes to /design:edit)', () => {
419
+ // Call expression no single source string to rewrite hard refusal.
420
+ const src = 'const price = 9;\nfunction Demo() { return <h1>{price.toFixed(2)}</h1>; }';
417
421
  const id = idsOf(src).h1 as string;
418
- expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
422
+ expect(() => applyTextEdit(CANVAS, src, id, 'x', { before: '9.00' })).toThrow(CanvasEditError);
423
+ });
424
+
425
+ test('refuses an {identifier} that does not resolve to a string const', () => {
426
+ // `count` is a number const — no string to trace to → refuse.
427
+ const src = 'const count = 3;\nfunction Demo() { return <h1>{count}</h1>; }';
428
+ const id = idsOf(src).h1 as string;
429
+ expect(() => applyTextEdit(CANVAS, src, id, 'x', { before: '3' })).toThrow(CanvasEditError);
419
430
  });
420
431
  });
421
432
 
@@ -162,6 +162,56 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
162
162
  // untrusted canvas origin must never read (or write) the user's pasted
163
163
  // chat images; a GET here 403s at the gate, not 404 from the handler.
164
164
  '/_api/acp/attachment',
165
+ // Phase 4 (feature-whiteboard-annotation-improvements) — the sticker
166
+ // catalogue + bundled sticker PNGs are MAIN-ORIGIN ONLY, same posture
167
+ // as /_api/assets above: the StickerPicker is shell UI, absent from
168
+ // CANVAS_SAFE_API + startCanvasServer's routes. A GET here 403s at
169
+ // the gate on both — the catalogue route and the static PNG serve.
170
+ '/_api/stickers',
171
+ '/_stickers/some-pack/some-sticker.png',
172
+ // DDR-166 Phase 1 / T2 — the bundled intro-video media serve is
173
+ // MAIN-ORIGIN ONLY, same posture as /_stickers above: a Maude-product
174
+ // asset, absent from CANVAS_SAFE_API + startCanvasServer's routes.
175
+ '/_media/intro.mp4',
176
+ // feature-footage-analysis-director — the footage-analysis + EDL sidecar
177
+ // route is MAIN-ORIGIN ONLY (written by the analyst/director agents over
178
+ // loopback; absent from CANVAS_SAFE_API + startCanvasServer's routes). The
179
+ // untrusted canvas origin must never read/write the director's analysis —
180
+ // a GET here 403s at the gate, not 405 from a reached handler.
181
+ '/_api/footage',
182
+ // feature-ai-media-generation (DDR-16x) — the generation job queue +
183
+ // provider catalogue + key routes are MAIN-ORIGIN ONLY (they resolve a
184
+ // provider KEY and make outbound provider calls). The untrusted canvas
185
+ // origin must never reach them — it sees only the produced /assets/<sha8>.
186
+ // A GET here 403s at the gate, not 405/200 from a reached handler.
187
+ '/_api/generate-jobs',
188
+ '/_api/generate/providers',
189
+ '/_api/generate/keys',
190
+ // Task 2.6 — the non-secret transcription-engine preference writes into
191
+ // .design/config.json + hot-reloads; still MAIN-ORIGIN ONLY (the canvas
192
+ // must not steer the user's engine choice).
193
+ '/_api/generate/prefs',
194
+ // Task 2.5 — reuse-before-you-pay audio search + history re-download
195
+ // resolve the provider key server-side; MAIN-ORIGIN ONLY.
196
+ '/_api/generate/audio-search',
197
+ '/_api/generate/audio-reuse',
198
+ // Task 2.7 — managed whisper-model download (egress + local disk write);
199
+ // MAIN-ORIGIN ONLY.
200
+ '/_api/generate/whisper-model',
201
+ // DDR-166 plan, Phase 2 (T6) — the design-setup readiness probe (project/
202
+ // design-system/first-canvas/brand-assets progress) is a shell/onboarding
203
+ // concern, same posture as /_api/preflight above; MAIN-ORIGIN ONLY.
204
+ '/_api/setup-readiness',
205
+ // DDR-167 (Phase 3 / T10) — local-file SVG/PDF ingestion is a
206
+ // file-write + local-file-adjacent surface (Decision 4); MAIN-ORIGIN
207
+ // ONLY, same posture as /_api/asset's containment but privileged
208
+ // rather than canvas-reachable — the untrusted canvas iframe must
209
+ // never drive an import.
210
+ '/_api/import-asset',
211
+ // DDR-173 (Phase 3 / T12) — brand-file typed-cue extraction is a
212
+ // file-write surface (writes assets/logos/*), same privileged
213
+ // posture as /_api/import-asset above; MAIN-ORIGIN ONLY.
214
+ '/_api/import-brand',
165
215
  '/package.json',
166
216
  ]) {
167
217
  expect(await code(p)).toBe(403);
@@ -48,4 +48,15 @@ describe('cspForCanvasShell — A6 hardening', () => {
48
48
  const csp = cspForCanvasShell(SHELL);
49
49
  expect(csp).not.toContain('frame-ancestors');
50
50
  });
51
+
52
+ // fix-photo-editor-followup-debt Task 22 — the narrow, documented
53
+ // `connect-src` exception for @imgly/background-removal's model-weight CDN.
54
+ // Regression guard against both directions of drift: the exception being
55
+ // silently reverted, AND further hosts creeping in beside it.
56
+ test('connect-src carries the exact staticimgly.com exception, no more', () => {
57
+ const csp = cspForCanvasShell(SHELL);
58
+ expect(csp).toContain("connect-src 'self' https://staticimgly.com");
59
+ const connectSrc = csp.split('; ').find((d) => d.startsWith('connect-src'));
60
+ expect(connectSrc).toBe("connect-src 'self' https://staticimgly.com");
61
+ });
51
62
  });
@@ -18,7 +18,7 @@ import { describe, expect, test } from 'bun:test';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import { fileURLToPath } from 'node:url';
20
20
 
21
- import { sameOriginWrite } from '../http.ts';
21
+ import { sameOriginRead, sameOriginWrite } from '../http.ts';
22
22
 
23
23
  const SELF = 'http://localhost:4399';
24
24
  const post = (origin?: string): Request =>
@@ -53,6 +53,28 @@ describe('CSRF Origin guard — sameOriginWrite (DDR-105)', () => {
53
53
  });
54
54
  });
55
55
 
56
+ describe('CSRF read guard — sameOriginRead (Task 2.5 F1, Fetch-Metadata)', () => {
57
+ const get = (secFetchSite?: string): Request =>
58
+ new Request(`${SELF}/_api/generate/audio-search?q=x`, {
59
+ method: 'GET',
60
+ headers: { ...(secFetchSite ? { 'sec-fetch-site': secFetchSite } : {}) },
61
+ });
62
+
63
+ test('rejects a cross-site browser GET (key-bearing fan-out must not be CSRF-triggerable)', () => {
64
+ expect(sameOriginRead(get('cross-site'))).toBe(false);
65
+ expect(sameOriginRead(get('same-site'))).toBe(false);
66
+ });
67
+
68
+ test('allows a same-origin browser GET', () => {
69
+ expect(sameOriginRead(get('same-origin'))).toBe(true);
70
+ expect(sameOriginRead(get('none'))).toBe(true); // direct navigation
71
+ });
72
+
73
+ test('allows a non-browser client (CLI / curl — no Sec-Fetch-Site header)', () => {
74
+ expect(sameOriginRead(get())).toBe(true);
75
+ });
76
+ });
77
+
56
78
  // phase-30 / DDR-120: the ai-activity bridge now projects `/_api/ai/*` POSTs
57
79
  // onto room awareness, which crosses the hub to every connected peer. So a
58
80
  // forged cross-origin POST to /start /heartbeat /end is no longer a harmless
@@ -0,0 +1,155 @@
1
+ // Design-setup readiness probe (DDR-166 plan, Phase 2 / T6). A pure function
2
+ // over a Context-shaped fixture — no server boot needed, `probeSetupReadiness`
3
+ // only touches `ctx.cfg` + `ctx.paths` (both destructured, so a minimal fake
4
+ // satisfies the type).
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ import type { Context, DevServerConfig } from '../context.ts';
12
+ import { probeSetupReadiness } from '../design-setup-readiness.ts';
13
+
14
+ function fixtureCtx(opts: {
15
+ hasConfig?: boolean;
16
+ designSystems?: DevServerConfig['designSystems'];
17
+ canvasGroups?: DevServerConfig['canvasGroups'];
18
+ seedCanvas?: boolean;
19
+ seedTokens?: string[]; // ds names to write a real colors_and_type.css for
20
+ seedLogo?: string[]; // ds names to write a preview/logo.svg for
21
+ }): Context {
22
+ const root = mkdtempSync(join(tmpdir(), 'maude-setup-readiness-'));
23
+ const designRoot = join(root, '.design');
24
+ mkdirSync(designRoot, { recursive: true });
25
+
26
+ const designSystems = opts.designSystems ?? [];
27
+ const canvasGroups = opts.canvasGroups ?? [
28
+ { label: 'Design system', path: 'system' },
29
+ { label: 'UI kit', path: 'ui' },
30
+ ];
31
+
32
+ for (const ds of designSystems) {
33
+ mkdirSync(join(designRoot, ds.path), { recursive: true });
34
+ if (opts.seedTokens?.includes(ds.name)) {
35
+ writeFileSync(join(designRoot, ds.path, 'colors_and_type.css'), ':root{}');
36
+ }
37
+ if (opts.seedLogo?.includes(ds.name)) {
38
+ mkdirSync(join(designRoot, ds.path, 'preview'), { recursive: true });
39
+ writeFileSync(join(designRoot, ds.path, 'preview', 'logo.svg'), '<svg/>');
40
+ }
41
+ }
42
+ if (opts.seedCanvas) {
43
+ mkdirSync(join(designRoot, 'ui'), { recursive: true });
44
+ writeFileSync(
45
+ join(designRoot, 'ui', 'Welcome.tsx'),
46
+ 'export default function W(){return <main/>}\n'
47
+ );
48
+ }
49
+
50
+ const cfg: DevServerConfig = {
51
+ name: 'test',
52
+ projectLabel: null,
53
+ designRoot: '.design',
54
+ canvasGroups,
55
+ designSystems,
56
+ rootClass: 'app',
57
+ themeDefault: 'dark',
58
+ tokensCssRel: 'system/colors_and_type.css',
59
+ teamAccentDefault: null,
60
+ handoffTargets: [],
61
+ newCanvasDir: 'ui',
62
+ newComponentDir: 'ui/components',
63
+ _source: opts.hasConfig === false ? 'defaults' : '.design/config.json',
64
+ };
65
+
66
+ return {
67
+ cfg,
68
+ projectLabel: 'test',
69
+ paths: {
70
+ repoRoot: root,
71
+ designRel: '.design',
72
+ designRoot,
73
+ serverInfoFile: join(designRoot, '_server.json'),
74
+ activeFile: join(designRoot, '_active.json'),
75
+ commentsDir: join(designRoot, '_comments'),
76
+ canvasStateDir: join(designRoot, '_canvas-state'),
77
+ historyDir: join(designRoot, '_history'),
78
+ tokensUrlRel: 'system/colors_and_type.css',
79
+ systemDirRel: 'system',
80
+ },
81
+ bus: { on: () => () => {}, emit: () => {} },
82
+ } as Context;
83
+ }
84
+
85
+ describe('probeSetupReadiness', () => {
86
+ test('bare write_minimal_design scaffold: config missing, everything else missing too', async () => {
87
+ const report = await probeSetupReadiness(fixtureCtx({ hasConfig: false }));
88
+ expect(report.ready).toBe(false);
89
+ const byId = Object.fromEntries(report.items.map((i) => [i.id, i.status]));
90
+ expect(byId).toEqual({
91
+ project: 'missing',
92
+ 'design-system': 'missing',
93
+ 'first-canvas': 'missing',
94
+ 'brand-assets': 'missing',
95
+ });
96
+ });
97
+
98
+ test('real config, no DS, no canvas: project present, rest missing', async () => {
99
+ const report = await probeSetupReadiness(fixtureCtx({ hasConfig: true }));
100
+ const byId = Object.fromEntries(report.items.map((i) => [i.id, i.status]));
101
+ expect(byId.project).toBe('present');
102
+ expect(byId['design-system']).toBe('missing');
103
+ expect(byId['first-canvas']).toBe('missing');
104
+ expect(byId['brand-assets']).toBe('missing');
105
+ expect(report.ready).toBe(false);
106
+ });
107
+
108
+ test('DS declared but tokens file absent: still missing (declared != on-disk)', async () => {
109
+ const report = await probeSetupReadiness(
110
+ fixtureCtx({
111
+ hasConfig: true,
112
+ designSystems: [
113
+ { name: 'acme', path: 'system/acme', tokensCssRel: 'system/acme/colors_and_type.css' },
114
+ ],
115
+ })
116
+ );
117
+ const item = report.items.find((i) => i.id === 'design-system')!;
118
+ expect(item.status).toBe('missing');
119
+ expect(item.detail).toContain('Declared in config');
120
+ });
121
+
122
+ test('all four present → ready: true', async () => {
123
+ const report = await probeSetupReadiness(
124
+ fixtureCtx({
125
+ hasConfig: true,
126
+ designSystems: [
127
+ { name: 'acme', path: 'system/acme', tokensCssRel: 'system/acme/colors_and_type.css' },
128
+ ],
129
+ seedTokens: ['acme'],
130
+ seedLogo: ['acme'],
131
+ seedCanvas: true,
132
+ })
133
+ );
134
+ expect(report.items.every((i) => i.status === 'present')).toBe(true);
135
+ expect(report.ready).toBe(true);
136
+ });
137
+
138
+ test('a "system" canvas group never counts as the first user canvas', async () => {
139
+ const root = mkdtempSync(join(tmpdir(), 'maude-setup-readiness-'));
140
+ const designRoot = join(root, '.design');
141
+ const canvasGroups = [{ label: 'Design system', path: 'system' }];
142
+ mkdirSync(join(designRoot, 'system'), { recursive: true });
143
+ writeFileSync(
144
+ join(designRoot, 'system', 'specimen.tsx'),
145
+ 'export default function S(){return <main/>}\n'
146
+ );
147
+ const ctx = fixtureCtx({ hasConfig: true, canvasGroups });
148
+ // Overwrite paths.designRoot/repoRoot to point at this second fixture dir
149
+ // (fixtureCtx already made its own scratch dir we don't need here).
150
+ ctx.paths.repoRoot = root;
151
+ ctx.paths.designRoot = designRoot;
152
+ const report = await probeSetupReadiness(ctx);
153
+ expect(report.items.find((i) => i.id === 'first-canvas')!.status).toBe('missing');
154
+ });
155
+ });