@dfosco/storyboard 0.5.0-alpha.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 (592) hide show
  1. package/commandpalette.config.json +152 -0
  2. package/dist/storyboard-ui.css +1 -0
  3. package/dist/storyboard-ui.js +21328 -0
  4. package/dist/storyboard-ui.js.map +1 -0
  5. package/dist/tailwind.css +2 -0
  6. package/dist/tiny-canvas.css +1 -0
  7. package/dist/tiny-canvas.js +389 -0
  8. package/package.json +121 -0
  9. package/paste.config.json +67 -0
  10. package/scaffold/AGENTS.md +432 -0
  11. package/scaffold/agents/prompt-agent.agent.md +181 -0
  12. package/scaffold/agents/terminal-agent.agent.md +351 -0
  13. package/scaffold/codex/config.toml +246 -0
  14. package/scaffold/deploy.yml +103 -0
  15. package/scaffold/githooks/pre-push +114 -0
  16. package/scaffold/gitignore +64 -0
  17. package/scaffold/manifest.json +56 -0
  18. package/scaffold/preview.yml +181 -0
  19. package/scaffold/scripts/link.sh +26 -0
  20. package/scaffold/scripts/unlink.sh +10 -0
  21. package/scaffold/skills/agent-browser/SKILL.md +260 -0
  22. package/scaffold/skills/canvas/SKILL.md +364 -0
  23. package/scaffold/skills/create/SKILL.md +501 -0
  24. package/scaffold/skills/ship/SKILL.md +237 -0
  25. package/scaffold/skills/storyboard/SKILL.md +360 -0
  26. package/scaffold/skills/update-storyboard/SKILL.md +16 -0
  27. package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
  28. package/scaffold/skills/vitest/GENERATION.md +5 -0
  29. package/scaffold/skills/vitest/SKILL.md +52 -0
  30. package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
  31. package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
  32. package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
  33. package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
  34. package/scaffold/skills/vitest/references/core-cli.md +166 -0
  35. package/scaffold/skills/vitest/references/core-config.md +174 -0
  36. package/scaffold/skills/vitest/references/core-describe.md +193 -0
  37. package/scaffold/skills/vitest/references/core-expect.md +219 -0
  38. package/scaffold/skills/vitest/references/core-hooks.md +244 -0
  39. package/scaffold/skills/vitest/references/core-test-api.md +233 -0
  40. package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
  41. package/scaffold/skills/vitest/references/features-context.md +238 -0
  42. package/scaffold/skills/vitest/references/features-coverage.md +207 -0
  43. package/scaffold/skills/vitest/references/features-filtering.md +211 -0
  44. package/scaffold/skills/vitest/references/features-mocking.md +265 -0
  45. package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
  46. package/scaffold/skills/worktree/SKILL.md +93 -0
  47. package/scaffold/storyboard.config.json +44 -0
  48. package/src/canvas/Canvas.jsx +78 -0
  49. package/src/canvas/Draggable.jsx +235 -0
  50. package/src/canvas/index.d.ts +41 -0
  51. package/src/canvas/index.js +6 -0
  52. package/src/canvas/style.css +118 -0
  53. package/src/canvas/useResetCanvas.js +17 -0
  54. package/src/canvas/utils.js +136 -0
  55. package/src/core/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
  56. package/src/core/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
  57. package/src/core/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
  58. package/src/core/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
  59. package/src/core/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
  60. package/src/core/autosync/server.js +714 -0
  61. package/src/core/autosync/server.test.js +158 -0
  62. package/src/core/canvas/__tests__/agent-integration.test.js +596 -0
  63. package/src/core/canvas/__tests__/helpers/browser.js +95 -0
  64. package/src/core/canvas/__tests__/helpers/canvas-api.js +129 -0
  65. package/src/core/canvas/__tests__/helpers/perf.js +118 -0
  66. package/src/core/canvas/__tests__/helpers/setup.js +176 -0
  67. package/src/core/canvas/__tests__/helpers/tmux.js +130 -0
  68. package/src/core/canvas/__tests__/helpers/transcript.js +132 -0
  69. package/src/core/canvas/__tests__/terminal-integration.test.js +177 -0
  70. package/src/core/canvas/collision.js +292 -0
  71. package/src/core/canvas/collision.test.js +371 -0
  72. package/src/core/canvas/compact.js +83 -0
  73. package/src/core/canvas/deriveCanvasId.test.js +40 -0
  74. package/src/core/canvas/githubEmbeds.js +527 -0
  75. package/src/core/canvas/githubEmbeds.test.js +302 -0
  76. package/src/core/canvas/hot-pool.js +766 -0
  77. package/src/core/canvas/identity.js +107 -0
  78. package/src/core/canvas/identity.test.js +100 -0
  79. package/src/core/canvas/materializer.js +259 -0
  80. package/src/core/canvas/materializer.test.js +356 -0
  81. package/src/core/canvas/selectedWidgets.js +270 -0
  82. package/src/core/canvas/selectedWidgets.test.js +321 -0
  83. package/src/core/canvas/server.js +3134 -0
  84. package/src/core/canvas/server.test.js +379 -0
  85. package/src/core/canvas/terminal-config.js +330 -0
  86. package/src/core/canvas/terminal-registry.js +465 -0
  87. package/src/core/canvas/terminal-server.js +1436 -0
  88. package/src/core/canvas/writeGuard.js +53 -0
  89. package/src/core/cli/agent.js +85 -0
  90. package/src/core/cli/branch.js +386 -0
  91. package/src/core/cli/canvasAdd.js +241 -0
  92. package/src/core/cli/canvasBatch.js +98 -0
  93. package/src/core/cli/canvasBounds.js +160 -0
  94. package/src/core/cli/canvasRead.js +236 -0
  95. package/src/core/cli/canvasUpdate.js +179 -0
  96. package/src/core/cli/code.js +67 -0
  97. package/src/core/cli/compact.js +62 -0
  98. package/src/core/cli/create.js +674 -0
  99. package/src/core/cli/dev-helpers.js +53 -0
  100. package/src/core/cli/dev-helpers.test.js +53 -0
  101. package/src/core/cli/dev.js +430 -0
  102. package/src/core/cli/exit.js +38 -0
  103. package/src/core/cli/flags.js +174 -0
  104. package/src/core/cli/flags.test.js +155 -0
  105. package/src/core/cli/index.js +233 -0
  106. package/src/core/cli/intro.js +37 -0
  107. package/src/core/cli/proxy.js +319 -0
  108. package/src/core/cli/proxy.test.js +63 -0
  109. package/src/core/cli/schemas.js +223 -0
  110. package/src/core/cli/server.js +192 -0
  111. package/src/core/cli/serverUrl.js +61 -0
  112. package/src/core/cli/sessions.js +459 -0
  113. package/src/core/cli/setup.js +404 -0
  114. package/src/core/cli/terminal-commands.js +287 -0
  115. package/src/core/cli/terminal-messaging.js +231 -0
  116. package/src/core/cli/terminal-welcome.js +515 -0
  117. package/src/core/cli/updateVersion.js +124 -0
  118. package/src/core/comments/api.js +284 -0
  119. package/src/core/comments/api.test.js +282 -0
  120. package/src/core/comments/auth.js +151 -0
  121. package/src/core/comments/auth.test.js +167 -0
  122. package/src/core/comments/commentCache.js +109 -0
  123. package/src/core/comments/commentCache.test.js +48 -0
  124. package/src/core/comments/commentDrafts.js +68 -0
  125. package/src/core/comments/commentMode.js +63 -0
  126. package/src/core/comments/commentMode.test.js +90 -0
  127. package/src/core/comments/config.js +47 -0
  128. package/src/core/comments/config.test.js +77 -0
  129. package/src/core/comments/graphql.js +65 -0
  130. package/src/core/comments/graphql.test.js +95 -0
  131. package/src/core/comments/index.js +42 -0
  132. package/src/core/comments/metadata.js +52 -0
  133. package/src/core/comments/metadata.test.js +110 -0
  134. package/src/core/comments/queries.js +245 -0
  135. package/src/core/comments/ui/AuthModal.jsx +114 -0
  136. package/src/core/comments/ui/CommentOverlay.js +52 -0
  137. package/src/core/comments/ui/CommentWindow.jsx +329 -0
  138. package/src/core/comments/ui/CommentsDrawer.jsx +102 -0
  139. package/src/core/comments/ui/Composer.jsx +64 -0
  140. package/src/core/comments/ui/authModal.js +66 -0
  141. package/src/core/comments/ui/authModal.test.js +76 -0
  142. package/src/core/comments/ui/comment-cursor-dark.svg +1 -0
  143. package/src/core/comments/ui/comment-cursor.svg +1 -0
  144. package/src/core/comments/ui/comment-layout.css +142 -0
  145. package/src/core/comments/ui/commentWindow.js +121 -0
  146. package/src/core/comments/ui/comments.css +242 -0
  147. package/src/core/comments/ui/commentsDrawer.js +84 -0
  148. package/src/core/comments/ui/composer.js +136 -0
  149. package/src/core/comments/ui/index.js +14 -0
  150. package/src/core/comments/ui/mount.js +687 -0
  151. package/src/core/comments/ui/mount.test.js +336 -0
  152. package/src/core/data/dotPath.js +53 -0
  153. package/src/core/data/dotPath.test.js +114 -0
  154. package/src/core/data/loader.js +409 -0
  155. package/src/core/data/loader.test.js +599 -0
  156. package/src/core/data/viewfinder.js +363 -0
  157. package/src/core/data/viewfinder.test.js +456 -0
  158. package/src/core/devtools/devtools-consumer.js +28 -0
  159. package/src/core/devtools/devtools.js +144 -0
  160. package/src/core/devtools/devtools.test.js +75 -0
  161. package/src/core/devtools/sceneDebug.js +112 -0
  162. package/src/core/devtools/sceneDebug.test.js +141 -0
  163. package/src/core/index.js +124 -0
  164. package/src/core/inspector/fiberWalker.js +239 -0
  165. package/src/core/inspector/highlighter.js +275 -0
  166. package/src/core/inspector/mouseMode.js +259 -0
  167. package/src/core/lib/components/ui/alert/alert-action.jsx +11 -0
  168. package/src/core/lib/components/ui/alert/alert-description.jsx +11 -0
  169. package/src/core/lib/components/ui/alert/alert-title.jsx +11 -0
  170. package/src/core/lib/components/ui/alert/alert.jsx +25 -0
  171. package/src/core/lib/components/ui/alert/index.js +17 -0
  172. package/src/core/lib/components/ui/avatar/avatar-badge.jsx +22 -0
  173. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
  174. package/src/core/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
  175. package/src/core/lib/components/ui/avatar/avatar-group.jsx +19 -0
  176. package/src/core/lib/components/ui/avatar/avatar-image.jsx +15 -0
  177. package/src/core/lib/components/ui/avatar/avatar.jsx +19 -0
  178. package/src/core/lib/components/ui/avatar/index.js +22 -0
  179. package/src/core/lib/components/ui/badge/badge.jsx +31 -0
  180. package/src/core/lib/components/ui/badge/index.js +2 -0
  181. package/src/core/lib/components/ui/button/button.jsx +100 -0
  182. package/src/core/lib/components/ui/button/index.js +12 -0
  183. package/src/core/lib/components/ui/card/card-action.jsx +11 -0
  184. package/src/core/lib/components/ui/card/card-content.jsx +11 -0
  185. package/src/core/lib/components/ui/card/card-description.jsx +11 -0
  186. package/src/core/lib/components/ui/card/card-footer.jsx +11 -0
  187. package/src/core/lib/components/ui/card/card-header.jsx +19 -0
  188. package/src/core/lib/components/ui/card/card-title.jsx +11 -0
  189. package/src/core/lib/components/ui/card/card.jsx +17 -0
  190. package/src/core/lib/components/ui/card/index.js +25 -0
  191. package/src/core/lib/components/ui/checkbox/checkbox.jsx +29 -0
  192. package/src/core/lib/components/ui/checkbox/index.js +6 -0
  193. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
  194. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
  195. package/src/core/lib/components/ui/collapsible/collapsible.jsx +7 -0
  196. package/src/core/lib/components/ui/collapsible/index.js +13 -0
  197. package/src/core/lib/components/ui/dialog/dialog-close.jsx +7 -0
  198. package/src/core/lib/components/ui/dialog/dialog-content.jsx +34 -0
  199. package/src/core/lib/components/ui/dialog/dialog-description.jsx +15 -0
  200. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +23 -0
  201. package/src/core/lib/components/ui/dialog/dialog-header.jsx +11 -0
  202. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
  203. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +4 -0
  204. package/src/core/lib/components/ui/dialog/dialog-title.jsx +15 -0
  205. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
  206. package/src/core/lib/components/ui/dialog/dialog.jsx +4 -0
  207. package/src/core/lib/components/ui/dialog/index.js +34 -0
  208. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
  209. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
  210. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
  211. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
  212. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
  213. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
  214. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
  215. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
  216. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
  217. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
  218. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
  219. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
  220. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
  221. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
  222. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
  223. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
  224. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
  225. package/src/core/lib/components/ui/dropdown-menu/index.js +54 -0
  226. package/src/core/lib/components/ui/input/index.js +7 -0
  227. package/src/core/lib/components/ui/input/input.jsx +19 -0
  228. package/src/core/lib/components/ui/label/index.js +7 -0
  229. package/src/core/lib/components/ui/label/label.jsx +19 -0
  230. package/src/core/lib/components/ui/panel/index.js +24 -0
  231. package/src/core/lib/components/ui/panel/panel-body.jsx +11 -0
  232. package/src/core/lib/components/ui/panel/panel-close.jsx +16 -0
  233. package/src/core/lib/components/ui/panel/panel-content.jsx +29 -0
  234. package/src/core/lib/components/ui/panel/panel-footer.jsx +11 -0
  235. package/src/core/lib/components/ui/panel/panel-header.jsx +11 -0
  236. package/src/core/lib/components/ui/panel/panel-title.jsx +12 -0
  237. package/src/core/lib/components/ui/panel/panel.jsx +4 -0
  238. package/src/core/lib/components/ui/popover/index.js +28 -0
  239. package/src/core/lib/components/ui/popover/popover-close.jsx +7 -0
  240. package/src/core/lib/components/ui/popover/popover-content.jsx +22 -0
  241. package/src/core/lib/components/ui/popover/popover-description.jsx +11 -0
  242. package/src/core/lib/components/ui/popover/popover-header.jsx +11 -0
  243. package/src/core/lib/components/ui/popover/popover-portal.jsx +4 -0
  244. package/src/core/lib/components/ui/popover/popover-title.jsx +11 -0
  245. package/src/core/lib/components/ui/popover/popover-trigger.jsx +8 -0
  246. package/src/core/lib/components/ui/popover/popover.jsx +4 -0
  247. package/src/core/lib/components/ui/searchable-list.jsx +160 -0
  248. package/src/core/lib/components/ui/select/index.js +37 -0
  249. package/src/core/lib/components/ui/select/select-content.jsx +30 -0
  250. package/src/core/lib/components/ui/select/select-group-heading.jsx +17 -0
  251. package/src/core/lib/components/ui/select/select-group.jsx +15 -0
  252. package/src/core/lib/components/ui/select/select-item.jsx +26 -0
  253. package/src/core/lib/components/ui/select/select-label.jsx +11 -0
  254. package/src/core/lib/components/ui/select/select-portal.jsx +4 -0
  255. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
  256. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
  257. package/src/core/lib/components/ui/select/select-separator.jsx +15 -0
  258. package/src/core/lib/components/ui/select/select-trigger.jsx +25 -0
  259. package/src/core/lib/components/ui/select/select.jsx +4 -0
  260. package/src/core/lib/components/ui/separator/index.js +7 -0
  261. package/src/core/lib/components/ui/separator/separator.jsx +22 -0
  262. package/src/core/lib/components/ui/sheet/index.js +34 -0
  263. package/src/core/lib/components/ui/sheet/sheet-close.jsx +7 -0
  264. package/src/core/lib/components/ui/sheet/sheet-content.jsx +35 -0
  265. package/src/core/lib/components/ui/sheet/sheet-description.jsx +15 -0
  266. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +11 -0
  267. package/src/core/lib/components/ui/sheet/sheet-header.jsx +11 -0
  268. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
  269. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +4 -0
  270. package/src/core/lib/components/ui/sheet/sheet-title.jsx +15 -0
  271. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
  272. package/src/core/lib/components/ui/sheet/sheet.jsx +4 -0
  273. package/src/core/lib/components/ui/textarea/index.js +7 -0
  274. package/src/core/lib/components/ui/textarea/textarea.jsx +18 -0
  275. package/src/core/lib/components/ui/toggle/index.js +8 -0
  276. package/src/core/lib/components/ui/toggle/toggle.jsx +36 -0
  277. package/src/core/lib/components/ui/toggle-group/index.js +10 -0
  278. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
  279. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
  280. package/src/core/lib/components/ui/tooltip/index.js +3 -0
  281. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
  282. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
  283. package/src/core/lib/components/ui/tooltip/tooltip.jsx +11 -0
  284. package/src/core/lib/components/ui/trigger-button/index.js +6 -0
  285. package/src/core/lib/components/ui/trigger-button/trigger-button.css +38 -0
  286. package/src/core/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
  287. package/src/core/lib/utils/index.js +6 -0
  288. package/src/core/logger/devLogger.js +238 -0
  289. package/src/core/logger/devLogger.test.js +193 -0
  290. package/src/core/modes/modes.css +98 -0
  291. package/src/core/modes/modes.js +492 -0
  292. package/src/core/modes/modes.test.js +562 -0
  293. package/src/core/mountStoryboardCore.js +478 -0
  294. package/src/core/rename-watcher/config.json +23 -0
  295. package/src/core/rename-watcher/watcher.js +531 -0
  296. package/src/core/scaffold.js +100 -0
  297. package/src/core/server/index.js +391 -0
  298. package/src/core/session/bodyClasses.js +128 -0
  299. package/src/core/session/bodyClasses.test.js +192 -0
  300. package/src/core/session/hashSubscribe.js +19 -0
  301. package/src/core/session/hashSubscribe.test.js +62 -0
  302. package/src/core/session/hideMode.js +424 -0
  303. package/src/core/session/hideMode.test.js +268 -0
  304. package/src/core/session/interceptHideParams.js +35 -0
  305. package/src/core/session/interceptHideParams.test.js +90 -0
  306. package/src/core/session/localStorage.js +134 -0
  307. package/src/core/session/localStorage.test.js +148 -0
  308. package/src/core/session/session.js +76 -0
  309. package/src/core/session/session.test.js +91 -0
  310. package/src/core/stores/canvasConfig.js +134 -0
  311. package/src/core/stores/canvasConfig.test.js +120 -0
  312. package/src/core/stores/commandActions.js +284 -0
  313. package/src/core/stores/commandPaletteConfig.js +31 -0
  314. package/src/core/stores/configSchema.js +232 -0
  315. package/src/core/stores/configSchema.test.js +72 -0
  316. package/src/core/stores/configStore.js +161 -0
  317. package/src/core/stores/customerModeConfig.js +30 -0
  318. package/src/core/stores/featureFlags.js +127 -0
  319. package/src/core/stores/paletteProviders.js +360 -0
  320. package/src/core/stores/paletteProviders.test.js +186 -0
  321. package/src/core/stores/plugins.js +40 -0
  322. package/src/core/stores/plugins.test.js +68 -0
  323. package/src/core/stores/recentArtifacts.js +68 -0
  324. package/src/core/stores/recentArtifacts.test.js +71 -0
  325. package/src/core/stores/sidePanelStore.ts +143 -0
  326. package/src/core/stores/themeStore.ts +291 -0
  327. package/src/core/stores/toolRegistry.js +227 -0
  328. package/src/core/stores/toolStateStore.js +183 -0
  329. package/src/core/stores/toolStateStore.test.js +220 -0
  330. package/src/core/stores/toolbarConfigStore.js +165 -0
  331. package/src/core/stores/uiConfig.js +64 -0
  332. package/src/core/stores/uiConfig.test.js +63 -0
  333. package/src/core/styles/tailwind.css +204 -0
  334. package/src/core/tools/handlers/autosync.js +12 -0
  335. package/src/core/tools/handlers/canvasAddWidget.js +11 -0
  336. package/src/core/tools/handlers/canvasAgents.js +20 -0
  337. package/src/core/tools/handlers/canvasToolbar.js +56 -0
  338. package/src/core/tools/handlers/commandPalette.js +9 -0
  339. package/src/core/tools/handlers/comments.js +16 -0
  340. package/src/core/tools/handlers/create.js +39 -0
  341. package/src/core/tools/handlers/devtools.js +122 -0
  342. package/src/core/tools/handlers/devtools.test.js +87 -0
  343. package/src/core/tools/handlers/featureFlags.js +21 -0
  344. package/src/core/tools/handlers/flows.js +68 -0
  345. package/src/core/tools/handlers/hideChrome.js +9 -0
  346. package/src/core/tools/handlers/hideToolbars.js +25 -0
  347. package/src/core/tools/handlers/inspector.js +19 -0
  348. package/src/core/tools/handlers/paletteTheme.js +35 -0
  349. package/src/core/tools/handlers/theme.js +9 -0
  350. package/src/core/tools/registry.js +26 -0
  351. package/src/core/tools/surfaces/canvasToolbar.js +10 -0
  352. package/src/core/tools/surfaces/commandList.js +10 -0
  353. package/src/core/tools/surfaces/mainToolbar.js +11 -0
  354. package/src/core/tools/surfaces/registry.js +19 -0
  355. package/src/core/ui/ActionMenuButton.jsx +114 -0
  356. package/src/core/ui/AutosyncMenuButton.css +67 -0
  357. package/src/core/ui/AutosyncMenuButton.jsx +242 -0
  358. package/src/core/ui/BranchSelect.jsx +29 -0
  359. package/src/core/ui/BranchSelect.module.css +30 -0
  360. package/src/core/ui/CanvasAgentsMenu.jsx +89 -0
  361. package/src/core/ui/CanvasCreateMenu.jsx +611 -0
  362. package/src/core/ui/CanvasSnap.css +27 -0
  363. package/src/core/ui/CanvasSnap.jsx +51 -0
  364. package/src/core/ui/CanvasUndoRedo.css +36 -0
  365. package/src/core/ui/CanvasUndoRedo.jsx +62 -0
  366. package/src/core/ui/CanvasZoomControl.css +53 -0
  367. package/src/core/ui/CanvasZoomControl.jsx +49 -0
  368. package/src/core/ui/CanvasZoomToFit.css +18 -0
  369. package/src/core/ui/CanvasZoomToFit.jsx +26 -0
  370. package/src/core/ui/CommandMenu.css +8 -0
  371. package/src/core/ui/CommandMenu.jsx +287 -0
  372. package/src/core/ui/CommandPalette.jsx +35 -0
  373. package/src/core/ui/CommandPaletteTrigger.jsx +25 -0
  374. package/src/core/ui/CommentsMenuButton.jsx +40 -0
  375. package/src/core/ui/CoreUIBar.css +47 -0
  376. package/src/core/ui/CoreUIBar.jsx +905 -0
  377. package/src/core/ui/CreateMenuButton.jsx +117 -0
  378. package/src/core/ui/HideChromeTrigger.jsx +48 -0
  379. package/src/core/ui/Icon.jsx +279 -0
  380. package/src/core/ui/InspectorPanel.css +109 -0
  381. package/src/core/ui/InspectorPanel.jsx +632 -0
  382. package/src/core/ui/PwaInstallBanner.css +42 -0
  383. package/src/core/ui/PwaInstallBanner.jsx +124 -0
  384. package/src/core/ui/SidePanel.jsx +261 -0
  385. package/src/core/ui/ThemeMenuButton.jsx +139 -0
  386. package/src/core/ui/core-ui-colors.css +129 -0
  387. package/src/core/ui/design-modes.ts +7 -0
  388. package/src/core/ui/sidepanel.css +301 -0
  389. package/src/core/ui/viewfinder.ts +7 -0
  390. package/src/core/ui-entry.js +30 -0
  391. package/src/core/utils/fuzzySearch.js +117 -0
  392. package/src/core/utils/fuzzySearch.test.js +119 -0
  393. package/src/core/utils/mobileViewport.js +57 -0
  394. package/src/core/utils/mobileViewport.test.js +68 -0
  395. package/src/core/utils/prodMode.js +38 -0
  396. package/src/core/utils/smoothCorners.js +20 -0
  397. package/src/core/vite/docs-handler.js +155 -0
  398. package/src/core/vite/server-plugin.js +797 -0
  399. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
  400. package/src/core/workshop/features/createCanvas/index.js +14 -0
  401. package/src/core/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
  402. package/src/core/workshop/features/createFlow/index.js +19 -0
  403. package/src/core/workshop/features/createFlow/server.js +663 -0
  404. package/src/core/workshop/features/createPage/CreatePageForm.jsx +304 -0
  405. package/src/core/workshop/features/createPage/index.js +11 -0
  406. package/src/core/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
  407. package/src/core/workshop/features/createPrototype/index.js +19 -0
  408. package/src/core/workshop/features/createPrototype/server.js +433 -0
  409. package/src/core/workshop/features/createStory/CreateStoryForm.jsx +208 -0
  410. package/src/core/workshop/features/createStory/index.js +14 -0
  411. package/src/core/workshop/features/registry-server.js +22 -0
  412. package/src/core/workshop/features/registry.js +28 -0
  413. package/src/core/workshop/features/templateIndex.js +155 -0
  414. package/src/core/workshop/ui/WorkshopPanel.jsx +98 -0
  415. package/src/core/workshop/ui/mount.ts +6 -0
  416. package/src/core/worktree/port.js +268 -0
  417. package/src/core/worktree/port.test.js +222 -0
  418. package/src/core/worktree/serverRegistry.js +120 -0
  419. package/src/internals/AuthModal/AuthModal.jsx +132 -0
  420. package/src/internals/AuthModal/AuthModal.module.css +221 -0
  421. package/src/internals/BranchBar/BranchBar.jsx +87 -0
  422. package/src/internals/BranchBar/BranchBar.module.css +247 -0
  423. package/src/internals/BranchBar/useBranches.js +93 -0
  424. package/src/internals/BranchBar/useBranches.test.js +68 -0
  425. package/src/internals/CommandPalette/CommandPalette.jsx +1361 -0
  426. package/src/internals/CommandPalette/CreateDialog.jsx +219 -0
  427. package/src/internals/CommandPalette/command-palette.css +180 -0
  428. package/src/internals/FlowError.module.css +30 -0
  429. package/src/internals/Icon.jsx +279 -0
  430. package/src/internals/StoryboardContext.js +3 -0
  431. package/src/internals/Viewfinder.jsx +1479 -0
  432. package/src/internals/Viewfinder.module.css +1540 -0
  433. package/src/internals/Workspace.jsx +7 -0
  434. package/src/internals/__mocks__/virtual-storyboard-data-index.js +4 -0
  435. package/src/internals/canvas/CanvasControls.jsx +112 -0
  436. package/src/internals/canvas/CanvasControls.module.css +135 -0
  437. package/src/internals/canvas/CanvasPage.bridge.test.jsx +387 -0
  438. package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +350 -0
  439. package/src/internals/canvas/CanvasPage.jsx +3092 -0
  440. package/src/internals/canvas/CanvasPage.module.css +187 -0
  441. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +358 -0
  442. package/src/internals/canvas/CanvasToolbar.jsx +73 -0
  443. package/src/internals/canvas/CanvasToolbar.module.css +92 -0
  444. package/src/internals/canvas/ComponentErrorBoundary.jsx +50 -0
  445. package/src/internals/canvas/ConnectorLayer.jsx +208 -0
  446. package/src/internals/canvas/ConnectorLayer.module.css +129 -0
  447. package/src/internals/canvas/MarqueeOverlay.jsx +20 -0
  448. package/src/internals/canvas/PageSelector.jsx +587 -0
  449. package/src/internals/canvas/PageSelector.module.css +261 -0
  450. package/src/internals/canvas/PageSelector.test.jsx +113 -0
  451. package/src/internals/canvas/WebGLContextPool.jsx +292 -0
  452. package/src/internals/canvas/WebGLContextPool.test.jsx +165 -0
  453. package/src/internals/canvas/canvasApi.js +164 -0
  454. package/src/internals/canvas/canvasReloadGuard.js +37 -0
  455. package/src/internals/canvas/canvasReloadGuard.test.js +27 -0
  456. package/src/internals/canvas/canvasTheme.js +118 -0
  457. package/src/internals/canvas/componentIsolate.jsx +165 -0
  458. package/src/internals/canvas/componentSetIsolate.jsx +257 -0
  459. package/src/internals/canvas/computeCanvasBounds.test.js +121 -0
  460. package/src/internals/canvas/connectorGeometry.js +132 -0
  461. package/src/internals/canvas/hotPoolDevLogs.js +25 -0
  462. package/src/internals/canvas/textSelection.js +10 -0
  463. package/src/internals/canvas/textSelection.test.js +26 -0
  464. package/src/internals/canvas/useCanvas.js +126 -0
  465. package/src/internals/canvas/useCanvas.test.js +26 -0
  466. package/src/internals/canvas/useMarqueeSelect.js +213 -0
  467. package/src/internals/canvas/useMarqueeSelect.test.js +78 -0
  468. package/src/internals/canvas/useUndoRedo.js +86 -0
  469. package/src/internals/canvas/useUndoRedo.test.js +231 -0
  470. package/src/internals/canvas/widgets/CodePenEmbed.jsx +293 -0
  471. package/src/internals/canvas/widgets/CodePenEmbed.module.css +161 -0
  472. package/src/internals/canvas/widgets/ComponentSetWidget.jsx +2 -0
  473. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +89 -0
  474. package/src/internals/canvas/widgets/ComponentWidget.jsx +14 -0
  475. package/src/internals/canvas/widgets/ComponentWidget.module.css +0 -0
  476. package/src/internals/canvas/widgets/CropOverlay.jsx +179 -0
  477. package/src/internals/canvas/widgets/CropOverlay.module.css +154 -0
  478. package/src/internals/canvas/widgets/ExpandedPane.jsx +474 -0
  479. package/src/internals/canvas/widgets/ExpandedPane.module.css +179 -0
  480. package/src/internals/canvas/widgets/ExpandedPane.test.jsx +240 -0
  481. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  482. package/src/internals/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  483. package/src/internals/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  484. package/src/internals/canvas/widgets/FigmaEmbed.jsx +296 -0
  485. package/src/internals/canvas/widgets/FigmaEmbed.module.css +222 -0
  486. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +151 -0
  487. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +83 -0
  488. package/src/internals/canvas/widgets/ImageWidget.jsx +287 -0
  489. package/src/internals/canvas/widgets/ImageWidget.module.css +81 -0
  490. package/src/internals/canvas/widgets/LinkPreview.jsx +439 -0
  491. package/src/internals/canvas/widgets/LinkPreview.module.css +585 -0
  492. package/src/internals/canvas/widgets/LinkPreview.test.jsx +193 -0
  493. package/src/internals/canvas/widgets/MarkdownBlock.jsx +354 -0
  494. package/src/internals/canvas/widgets/MarkdownBlock.module.css +377 -0
  495. package/src/internals/canvas/widgets/MarkdownBlock.test.jsx +92 -0
  496. package/src/internals/canvas/widgets/PromptWidget.jsx +428 -0
  497. package/src/internals/canvas/widgets/PromptWidget.module.css +273 -0
  498. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +463 -0
  499. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +579 -0
  500. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
  501. package/src/internals/canvas/widgets/ResizeHandle.jsx +67 -0
  502. package/src/internals/canvas/widgets/ResizeHandle.module.css +29 -0
  503. package/src/internals/canvas/widgets/StickyNote.jsx +92 -0
  504. package/src/internals/canvas/widgets/StickyNote.module.css +70 -0
  505. package/src/internals/canvas/widgets/StickyNote.test.jsx +116 -0
  506. package/src/internals/canvas/widgets/StorySetWidget.jsx +208 -0
  507. package/src/internals/canvas/widgets/StorySetWidget.module.css +89 -0
  508. package/src/internals/canvas/widgets/StoryWidget.jsx +334 -0
  509. package/src/internals/canvas/widgets/StoryWidget.module.css +211 -0
  510. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +146 -0
  511. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +94 -0
  512. package/src/internals/canvas/widgets/TerminalWidget.jsx +704 -0
  513. package/src/internals/canvas/widgets/TerminalWidget.module.css +444 -0
  514. package/src/internals/canvas/widgets/TilesWidget.jsx +300 -0
  515. package/src/internals/canvas/widgets/TilesWidget.module.css +133 -0
  516. package/src/internals/canvas/widgets/WidgetChrome.jsx +580 -0
  517. package/src/internals/canvas/widgets/WidgetChrome.module.css +421 -0
  518. package/src/internals/canvas/widgets/WidgetWrapper.jsx +15 -0
  519. package/src/internals/canvas/widgets/WidgetWrapper.module.css +25 -0
  520. package/src/internals/canvas/widgets/codepenUrl.js +75 -0
  521. package/src/internals/canvas/widgets/codepenUrl.test.js +76 -0
  522. package/src/internals/canvas/widgets/embedInteraction.test.jsx +173 -0
  523. package/src/internals/canvas/widgets/embedOverlay.module.css +35 -0
  524. package/src/internals/canvas/widgets/embedTheme.js +148 -0
  525. package/src/internals/canvas/widgets/expandUtils.js +559 -0
  526. package/src/internals/canvas/widgets/expandUtils.test.js +155 -0
  527. package/src/internals/canvas/widgets/figmaUrl.js +118 -0
  528. package/src/internals/canvas/widgets/figmaUrl.test.js +139 -0
  529. package/src/internals/canvas/widgets/githubUrl.js +82 -0
  530. package/src/internals/canvas/widgets/githubUrl.test.js +74 -0
  531. package/src/internals/canvas/widgets/iframeDevLogs.js +49 -0
  532. package/src/internals/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  533. package/src/internals/canvas/widgets/index.js +42 -0
  534. package/src/internals/canvas/widgets/pasteRules.js +295 -0
  535. package/src/internals/canvas/widgets/pasteRules.test.js +474 -0
  536. package/src/internals/canvas/widgets/snapshotDisplay.test.jsx +211 -0
  537. package/src/internals/canvas/widgets/tilePool.js +23 -0
  538. package/src/internals/canvas/widgets/tiles/diagonal-bl.png +0 -0
  539. package/src/internals/canvas/widgets/tiles/diagonal-br.png +0 -0
  540. package/src/internals/canvas/widgets/tiles/diagonal-tl.png +0 -0
  541. package/src/internals/canvas/widgets/tiles/leaf.png +0 -0
  542. package/src/internals/canvas/widgets/tiles/quarter-tl.png +0 -0
  543. package/src/internals/canvas/widgets/tiles/quarter-tr.png +0 -0
  544. package/src/internals/canvas/widgets/tiles/solid-a.png +0 -0
  545. package/src/internals/canvas/widgets/tiles/solid-b.png +0 -0
  546. package/src/internals/canvas/widgets/widgetConfig.js +291 -0
  547. package/src/internals/canvas/widgets/widgetConfig.test.js +68 -0
  548. package/src/internals/canvas/widgets/widgetIcons.jsx +190 -0
  549. package/src/internals/canvas/widgets/widgetProps.js +133 -0
  550. package/src/internals/context/FormContext.js +13 -0
  551. package/src/internals/context/FormContext.test.js +48 -0
  552. package/src/internals/context.jsx +481 -0
  553. package/src/internals/context.test.jsx +296 -0
  554. package/src/internals/hashPreserver.js +73 -0
  555. package/src/internals/hashPreserver.test.js +107 -0
  556. package/src/internals/hooks/useConfig.js +14 -0
  557. package/src/internals/hooks/useFeatureFlag.js +14 -0
  558. package/src/internals/hooks/useFlows.js +50 -0
  559. package/src/internals/hooks/useFlows.test.js +134 -0
  560. package/src/internals/hooks/useHideMode.js +31 -0
  561. package/src/internals/hooks/useHideMode.test.js +43 -0
  562. package/src/internals/hooks/useLocalStorage.js +57 -0
  563. package/src/internals/hooks/useLocalStorage.test.js +75 -0
  564. package/src/internals/hooks/useMode.js +43 -0
  565. package/src/internals/hooks/useObject.js +101 -0
  566. package/src/internals/hooks/useObject.test.js +74 -0
  567. package/src/internals/hooks/useOverride.js +84 -0
  568. package/src/internals/hooks/useOverride.test.js +71 -0
  569. package/src/internals/hooks/usePrototypeReloadGuard.js +64 -0
  570. package/src/internals/hooks/useRecord.js +158 -0
  571. package/src/internals/hooks/useRecord.test.js +221 -0
  572. package/src/internals/hooks/useScene.js +38 -0
  573. package/src/internals/hooks/useScene.test.js +66 -0
  574. package/src/internals/hooks/useSceneData.js +108 -0
  575. package/src/internals/hooks/useSceneData.test.js +136 -0
  576. package/src/internals/hooks/useSession.js +4 -0
  577. package/src/internals/hooks/useSession.test.js +8 -0
  578. package/src/internals/hooks/useThemeState.js +61 -0
  579. package/src/internals/hooks/useThemeState.test.js +66 -0
  580. package/src/internals/hooks/useUndoRedo.js +28 -0
  581. package/src/internals/hooks/useUndoRedo.test.js +64 -0
  582. package/src/internals/index.js +58 -0
  583. package/src/internals/story/ComponentSetPage.jsx +198 -0
  584. package/src/internals/story/ComponentSetPage.module.css +129 -0
  585. package/src/internals/story/StoryPage.jsx +147 -0
  586. package/src/internals/story/StoryPage.module.css +18 -0
  587. package/src/internals/test-utils.js +45 -0
  588. package/src/internals/vite/data-plugin.js +1508 -0
  589. package/src/internals/vite/data-plugin.test.js +1223 -0
  590. package/src/test-utils.js +44 -0
  591. package/toolbar.config.json +271 -0
  592. package/widgets.config.json +1537 -0
@@ -0,0 +1,797 @@
1
+ /**
2
+ * Storyboard Server Plugin — core dev-server infrastructure.
3
+ *
4
+ * Always-on Vite plugin that mounts a middleware backbone at `/_storyboard/`.
5
+ * Reads `storyboard.config.json` for workshop features and plugin config.
6
+ * Workshop API routes are wired directly; plugins register via the registry.
7
+ *
8
+ * Usage in vite.config.js:
9
+ * import storyboardServer from '@dfosco/storyboard/vite/server'
10
+ * storyboardServer() // reads storyboard.config.json, no args needed
11
+ */
12
+
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+ import { parse as parseJsonc } from 'jsonc-parser'
16
+ import { getConfig } from '../stores/configSchema.js'
17
+ import { createDevLogger, setDevLogger } from '../logger/devLogger.js'
18
+ import { serverFeatures as workshopFeatures } from '../workshop/features/registry-server.js'
19
+ import { docsHandler, collectFiles } from './docs-handler.js'
20
+ import { createCanvasHandler } from '../canvas/server.js'
21
+ import { setupSelectedWidgets } from '../canvas/selectedWidgets.js'
22
+ import { HotPoolManager } from '../canvas/hot-pool.js'
23
+ import { createAutosyncHandler } from '../autosync/server.js'
24
+ import { setupTerminalServer } from '../canvas/terminal-server.js'
25
+ import { listSessions, detachSession, killSession, orphanSession, bulkCleanup, getSessionStats } from '../canvas/terminal-registry.js'
26
+ import { execSync as cpExecSync } from 'node:child_process'
27
+ import { list as listRunningServers } from '../worktree/serverRegistry.js'
28
+
29
+ const API_PREFIX = '/_storyboard/'
30
+
31
+ /**
32
+ * Parse JSON request body from an IncomingMessage.
33
+ */
34
+ function parseJsonBody(req) {
35
+ return new Promise((resolve, reject) => {
36
+ let body = ''
37
+ req.on('data', (chunk) => { body += chunk })
38
+ req.on('end', () => {
39
+ if (!body) return resolve({})
40
+ try { resolve(JSON.parse(body)) }
41
+ catch { reject(new Error('Invalid JSON body')) }
42
+ })
43
+ req.on('error', reject)
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Send a JSON response.
49
+ */
50
+ function sendJson(res, status, data) {
51
+ res.writeHead(status, { 'Content-Type': 'application/json' })
52
+ res.end(JSON.stringify(data))
53
+ }
54
+
55
+ /**
56
+ * Create a logging wrapper around sendJson.
57
+ * Reads per-request route context from res.__sbLogCtx (set by middleware).
58
+ */
59
+ function createLoggedSendJson(logger) {
60
+ return function sendJsonLogged(res, status, data) {
61
+ sendJson(res, status, data)
62
+ if (status >= 400 && logger) {
63
+ const ctx = res.__sbLogCtx || {}
64
+ logger.logResponse({
65
+ status,
66
+ method: ctx.method || 'UNKNOWN',
67
+ url: ctx.url || '',
68
+ route: ctx.route || null,
69
+ subRoute: ctx.subRoute || null,
70
+ error: data?.error || null,
71
+ })
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Read storyboard.config.json from the project root and apply defaults.
78
+ */
79
+ function readConfig(root) {
80
+ const configPath = path.join(root, 'storyboard.config.json')
81
+ if (!fs.existsSync(configPath)) return getConfig({})
82
+ try {
83
+ const raw = fs.readFileSync(configPath, 'utf-8')
84
+ return getConfig(parseJsonc(raw) || {})
85
+ } catch {
86
+ return getConfig({})
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Core storyboard server Vite plugin.
92
+ */
93
+ export default function storyboardServer() {
94
+ let root = ''
95
+ let base = '/'
96
+ let config = {}
97
+ let isDev = false
98
+
99
+ // Route handler registry — plugins register here during setup
100
+ const routeHandlers = new Map()
101
+ const clientScripts = []
102
+
103
+ return {
104
+ name: 'storyboard-server',
105
+
106
+ config() {
107
+ return {
108
+ optimizeDeps: {
109
+ include: [
110
+ 'highlight.js/lib/core',
111
+ 'highlight.js/lib/languages/javascript',
112
+ 'highlight.js/lib/languages/typescript',
113
+ 'highlight.js/lib/languages/xml',
114
+ ],
115
+ },
116
+ }
117
+ },
118
+
119
+ configResolved(viteConfig) {
120
+ root = viteConfig.root
121
+ base = viteConfig.base || '/'
122
+ config = readConfig(root)
123
+ isDev = viteConfig.command === 'serve'
124
+ },
125
+
126
+ configureServer(server) {
127
+ // --- Reload guard ----------------------------------------------------------
128
+ // Suppress full-reloads and HMR updates for guarded clients.
129
+ //
130
+ // Two guard channels:
131
+ // 1. Canvas guard — canvas pages send heartbeats via storyboard:canvas-hmr-guard.
132
+ // Controlled by the "canvas-auto-reload" feature flag (default: false = guard ON).
133
+ // 2. Prototype guard — all pages send heartbeats via storyboard:prototype-reload-guard.
134
+ // Controlled by the "prototype-auto-reload" feature flag (default: true = guard OFF).
135
+ //
136
+ // Both guards auto-expire 5s after the last heartbeat so closed tabs never
137
+ // leave them stuck. Custom storyboard events always pass through.
138
+ {
139
+ let recentCanvasMutationAt = 0
140
+ const CANVAS_WINDOW_MS = 1500
141
+ const GUARD_TTL_MS = 5000
142
+ const isCanvasFile = (file = '') => /\.canvas\.jsonl$/i.test(file.replace(/\\/g, '/'))
143
+
144
+ const markCanvasMutation = (file = '') => {
145
+ if (isCanvasFile(file)) recentCanvasMutationAt = Date.now()
146
+ }
147
+
148
+ server.watcher.on('change', markCanvasMutation)
149
+ server.watcher.on('add', markCanvasMutation)
150
+ server.watcher.on('unlink', markCanvasMutation)
151
+
152
+ const canvasGuardedClients = new Map()
153
+ const prototypeGuardedClients = new Map()
154
+
155
+ server.hot.on('storyboard:canvas-hmr-guard', (data, client) => {
156
+ if (data.active) {
157
+ canvasGuardedClients.set(client, Date.now() + GUARD_TTL_MS)
158
+ } else {
159
+ canvasGuardedClients.delete(client)
160
+ }
161
+ })
162
+
163
+ server.hot.on('storyboard:prototype-reload-guard', (data, client) => {
164
+ if (data.active) {
165
+ prototypeGuardedClients.set(client, Date.now() + GUARD_TTL_MS)
166
+ } else {
167
+ prototypeGuardedClients.delete(client)
168
+ }
169
+ })
170
+
171
+ const cleanup = setInterval(() => {
172
+ const now = Date.now()
173
+ for (const [client, until] of canvasGuardedClients) {
174
+ if (now > until || !server.ws.clients.has(client)) {
175
+ canvasGuardedClients.delete(client)
176
+ }
177
+ }
178
+ for (const [client, until] of prototypeGuardedClients) {
179
+ if (now > until || !server.ws.clients.has(client)) {
180
+ prototypeGuardedClients.delete(client)
181
+ }
182
+ }
183
+ }, 10000)
184
+ server.httpServer?.on('close', () => clearInterval(cleanup))
185
+
186
+ function isClientGuarded(client) {
187
+ const cu = canvasGuardedClients.get(client)
188
+ if (cu != null && Date.now() < cu) return true
189
+ const pu = prototypeGuardedClients.get(client)
190
+ if (pu != null && Date.now() < pu) return true
191
+ return false
192
+ }
193
+
194
+ const originalSend = server.ws.send.bind(server.ws)
195
+ server.ws.send = (payload, ...rest) => {
196
+ // Suppress broadcast reloads within the canvas mutation window
197
+ if (
198
+ payload &&
199
+ payload.type === 'full-reload' &&
200
+ Date.now() - recentCanvasMutationAt < CANVAS_WINDOW_MS
201
+ ) {
202
+ return
203
+ }
204
+
205
+ // No guarded clients → broadcast normally
206
+ if (canvasGuardedClients.size === 0 && prototypeGuardedClients.size === 0) {
207
+ return originalSend(payload, ...rest)
208
+ }
209
+
210
+ // For reload/update payloads, send only to unguarded clients
211
+ if (payload && (payload.type === 'full-reload' || payload.type === 'update')) {
212
+ for (const client of server.ws.clients) {
213
+ if (!isClientGuarded(client)) {
214
+ client.send(payload)
215
+ }
216
+ }
217
+ return
218
+ }
219
+
220
+ // Everything else (custom events, errors) broadcasts normally
221
+ return originalSend(payload, ...rest)
222
+ }
223
+ }
224
+ // --- End reload guard ------------------------------------------------------
225
+
226
+ // Initialize dev logger for structured o11y logging
227
+ const devDomain = config.devDomain || null
228
+ let currentBranch = null
229
+ try { currentBranch = cpExecSync('git branch --show-current', { encoding: 'utf8', cwd: root }).trim() } catch { /* empty */ }
230
+ const logVerbose = config.featureFlags?.['dev-logs'] || false
231
+ const devLogger = createDevLogger({ root, devDomain, branch: currentBranch, verbose: logVerbose })
232
+ setDevLogger(devLogger) // make available to all server-side modules via devLog()
233
+ const sendJsonLogged = createLoggedSendJson(devLogger)
234
+
235
+ // Listen for browser-side console errors forwarded via HMR
236
+ server.hot.on('storyboard:client-error', (data) => {
237
+ devLogger.logEvent(data.level || 'error', data.message || 'Unknown browser error', {
238
+ source: 'browser',
239
+ url: data.url || null,
240
+ line: data.line || null,
241
+ col: data.col || null,
242
+ stack: data.stack || null,
243
+ route: data.route || null,
244
+ })
245
+ })
246
+
247
+ const workshopConfig = config.workshop || {}
248
+ const enabledFeatures = workshopConfig.features || {}
249
+
250
+ // Wire workshop API routes — compose handlers from all enabled features
251
+ const workshopHandlers = []
252
+ for (const [featureName, featureModule] of Object.entries(workshopFeatures)) {
253
+ if (enabledFeatures[featureName] === false) continue
254
+ if (featureModule.serverSetup) {
255
+ workshopHandlers.push(featureModule.serverSetup({ root, sendJson: sendJsonLogged, workshopConfig }))
256
+ }
257
+ }
258
+ if (workshopHandlers.length > 0) {
259
+ routeHandlers.set('workshop', async (req, res, ctx) => {
260
+ for (const handler of workshopHandlers) {
261
+ await handler(req, res, ctx)
262
+ if (res.writableEnded) return
263
+ }
264
+ sendJsonLogged(res, 404, { error: `Unknown workshop route: ${ctx.method} ${ctx.path}` })
265
+ })
266
+ }
267
+
268
+ // Wire docs API routes (always enabled — serves README + source files)
269
+ routeHandlers.set('docs', docsHandler({ root, sendJson: sendJsonLogged }))
270
+
271
+ // Create shared hot pool manager (per-type pre-warmed sessions)
272
+ const hotPoolConfig = config.hotPool || {}
273
+ const agentsConfig = config.canvas?.agents || {}
274
+ const wsSend = server.ws.send.bind(server.ws)
275
+ const hotPool = new HotPoolManager({ root, config: hotPoolConfig, agentsConfig, wsSend })
276
+ hotPool.start().catch((err) => {
277
+ devLogger.logEvent('error', 'Hot pool failed to start', { error: err.message })
278
+ })
279
+
280
+ // Wire canvas API routes (always enabled — CRUD for .canvas.jsonl files)
281
+ routeHandlers.set('canvas', createCanvasHandler({ root, sendJson: sendJsonLogged, hotPool }))
282
+
283
+ // Selected widgets bridge — writes .selectedwidgets.json for Copilot context
284
+ setupSelectedWidgets(server, root)
285
+
286
+ // Terminal WebSocket server — PTY backend for terminal canvas widgets
287
+ if (server.httpServer) {
288
+ let branch = 'unknown'
289
+ try {
290
+ branch = cpExecSync('git branch --show-current', { encoding: 'utf8', cwd: root }).trim()
291
+ } catch { /* empty */ }
292
+ setupTerminalServer(server.httpServer, base, branch, hotPool)
293
+ }
294
+
295
+ // Ignore assets/canvas/ so image/snapshot writes don't trigger reloads
296
+ server.watcher.unwatch(path.join(root, 'assets', 'canvas', 'images'))
297
+ server.watcher.unwatch(path.join(root, 'assets', 'canvas', 'snapshots'))
298
+ server.watcher.unwatch(path.join(root, 'assets', '.storyboard-public', 'terminal-snapshots'))
299
+
300
+ // Wire autosync API routes (always enabled — git automation for dev)
301
+ routeHandlers.set('autosync', createAutosyncHandler({ root, sendJson: sendJsonLogged }))
302
+
303
+ // Terminal sessions API — list, detach, kill sessions
304
+ routeHandlers.set('terminal', async (req, res, ctx) => {
305
+ // Strip query string and leading slash from path
306
+ const rawPath = (ctx.path || '/').replace(/^\//, '')
307
+ const subpath = rawPath.split('?')[0]
308
+
309
+ // GET /sessions — list all sessions (optional ?branch= filter)
310
+ if (ctx.method === 'GET' && (subpath === 'sessions' || subpath === 'sessions/')) {
311
+ const url = new URL(req.url, 'http://localhost')
312
+ const filterBranch = url.searchParams.get('branch') || null
313
+ sendJsonLogged(res, 200, { sessions: listSessions(filterBranch) })
314
+ return
315
+ }
316
+
317
+ // GET /sessions/stats — quick session counts by status
318
+ if (ctx.method === 'GET' && subpath === 'sessions/stats') {
319
+ sendJsonLogged(res, 200, getSessionStats())
320
+ return
321
+ }
322
+
323
+ // POST /sessions/cleanup — bulk remove sessions by status
324
+ if (ctx.method === 'POST' && subpath === 'sessions/cleanup') {
325
+ let body = ''
326
+ for await (const chunk of req) body += chunk
327
+ try {
328
+ const { statuses } = JSON.parse(body)
329
+ const allowed = new Set(['archived', 'background'])
330
+ if (!Array.isArray(statuses) || statuses.length === 0) {
331
+ sendJsonLogged(res, 400, { error: 'statuses must be a non-empty array' })
332
+ return
333
+ }
334
+ const invalid = statuses.filter(s => !allowed.has(s))
335
+ if (invalid.length > 0) {
336
+ sendJsonLogged(res, 400, { error: `Invalid statuses: ${invalid.join(', ')}. Allowed: archived, background` })
337
+ return
338
+ }
339
+ const result = bulkCleanup({ statuses })
340
+ sendJsonLogged(res, 200, { success: true, ...result })
341
+ } catch {
342
+ sendJsonLogged(res, 400, { error: 'Invalid JSON body' })
343
+ }
344
+ return
345
+ }
346
+
347
+ // POST /sessions/:name/detach — detach a session
348
+ const detachMatch = subpath.match(/^sessions\/(.+)\/detach$/)
349
+ if (ctx.method === 'POST' && detachMatch) {
350
+ const tmuxName = decodeURIComponent(detachMatch[1])
351
+ const entry = detachSession(tmuxName)
352
+ if (!entry) {
353
+ sendJsonLogged(res, 404, { error: 'Session not found' })
354
+ return
355
+ }
356
+ sendJsonLogged(res, 200, { success: true, session: entry })
357
+ return
358
+ }
359
+
360
+ // POST /sessions/:name/orphan — archive a session with grace timer
361
+ const orphanMatch = subpath.match(/^sessions\/(.+)\/orphan$/)
362
+ if (ctx.method === 'POST' && orphanMatch) {
363
+ const tmuxName = decodeURIComponent(orphanMatch[1])
364
+ orphanSession(tmuxName)
365
+ sendJsonLogged(res, 200, { success: true })
366
+ return
367
+ }
368
+
369
+ // DELETE /sessions/:name — kill a session immediately
370
+ const deleteMatch = subpath.match(/^sessions\/(.+)$/)
371
+ if (ctx.method === 'DELETE' && deleteMatch) {
372
+ const tmuxName = decodeURIComponent(deleteMatch[1])
373
+ killSession(tmuxName)
374
+ sendJsonLogged(res, 200, { success: true })
375
+ return
376
+ }
377
+
378
+ // ── Hot Pool routes (/terminal/hot-pool/*) ──────────────
379
+
380
+ // GET /hot-pool — pool status
381
+ if (ctx.method === 'GET' && subpath === 'hot-pool') {
382
+ sendJsonLogged(res, 200, hotPool.status())
383
+ return
384
+ }
385
+
386
+ // PUT /hot-pool — reconfigure pool
387
+ if (ctx.method === 'PUT' && subpath === 'hot-pool') {
388
+ hotPool.reconfigure(ctx.body || {})
389
+ sendJsonLogged(res, 200, hotPool.status())
390
+ return
391
+ }
392
+
393
+ // POST /hot-pool/acquire — acquire a warm session from a specific pool
394
+ if (ctx.method === 'POST' && subpath === 'hot-pool/acquire') {
395
+ const poolId = ctx.body?.poolId || 'terminal'
396
+ const session = hotPool.acquire(poolId)
397
+ if (!session) {
398
+ sendJsonLogged(res, 200, { acquired: false, poolId, session: null })
399
+ return
400
+ }
401
+ sendJsonLogged(res, 200, { acquired: true, poolId, session: { id: session.id, tmuxName: session.tmuxName, poolId: session.poolId } })
402
+ return
403
+ }
404
+
405
+ sendJsonLogged(res, 404, { error: 'Not found' })
406
+ })
407
+
408
+ // Worktrees API — lists running worktrees/branches from server registry
409
+ routeHandlers.set('worktrees', async (req, res) => {
410
+ try {
411
+ const servers = listRunningServers()
412
+ const branches = servers.map(srv => ({
413
+ branch: srv.worktree,
414
+ folder: srv.worktree === 'main' ? '' : `branch--${srv.worktree}/`,
415
+ }))
416
+ // Always include main
417
+ if (!branches.some(b => b.branch === 'main')) {
418
+ branches.unshift({ branch: 'main', folder: '' })
419
+ }
420
+ sendJsonLogged(res, 200, branches)
421
+ } catch { sendJsonLogged(res, 200, []) }
422
+ })
423
+
424
+ // Git user — return git config user name and GitHub login (via gh CLI)
425
+ routeHandlers.set('git-user', async (req, res) => {
426
+ try {
427
+ const { execSync } = await import('node:child_process')
428
+ const name = execSync('git config user.name', { cwd: root, encoding: 'utf8' }).trim()
429
+ let login = null
430
+ try {
431
+ const status = execSync('gh auth status 2>&1', { cwd: root, encoding: 'utf8' })
432
+ const m = status.match(/Logged in to github\.com account (\S+)/) || status.match(/Logged in to github\.com as (\S+)/)
433
+ if (m) login = m[1]
434
+ } catch { /* gh not installed or not logged in */ }
435
+ sendJsonLogged(res, 200, { name, login })
436
+ } catch { sendJsonLogged(res, 200, { name: null, login: null }) }
437
+ })
438
+
439
+ // Switch branch — proxy to storyboard server which manages worktree
440
+ // dev servers. The server port is derived from the devDomain.
441
+ routeHandlers.set('switch-branch', async (req, res, ctx) => {
442
+ if (ctx.method !== 'POST') {
443
+ sendJsonLogged(res, 405, { error: 'POST required' })
444
+ return
445
+ }
446
+ try {
447
+ // Derive storyboard server port (same algorithm as server/index.js)
448
+ // readDevDomain() returns "{devDomain}.localhost"
449
+ const domain = `${config.devDomain || 'storyboard'}.localhost`
450
+ let h = 0
451
+ for (let i = 0; i < domain.length; i++) {
452
+ h = ((h << 5) - h + domain.charCodeAt(i)) | 0
453
+ }
454
+ const serverPort = 4100 + (Math.abs(h) % 100)
455
+
456
+ const proxyRes = await fetch(`http://localhost:${serverPort}/_storyboard/switch-branch`, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json' },
459
+ body: JSON.stringify(ctx.body),
460
+ })
461
+ const data = await proxyRes.json()
462
+ sendJsonLogged(res, proxyRes.status, data)
463
+ } catch {
464
+ sendJsonLogged(res, 502, {
465
+ error: 'Storyboard server not running. Start it with: npx storyboard server',
466
+ })
467
+ }
468
+ })
469
+
470
+ // Watch toolbar.config.json for changes — trigger full reload so
471
+ // CoreUIBar.jsx picks up menu/mode config changes during dev
472
+ const toolbarConfigPath = path.resolve(
473
+ path.dirname(new URL(import.meta.url).pathname),
474
+ '../../toolbar.config.json'
475
+ )
476
+ server.watcher.add(toolbarConfigPath)
477
+ server.watcher.on('change', (filePath) => {
478
+ if (path.resolve(filePath) === toolbarConfigPath) {
479
+ // Invalidate the cached JSON module so Vite re-reads from disk
480
+ const mods = server.moduleGraph.getModulesByFile(toolbarConfigPath)
481
+ if (mods) {
482
+ for (const mod of mods) {
483
+ server.moduleGraph.invalidateModule(mod)
484
+ }
485
+ }
486
+ server.ws.send({ type: 'full-reload' })
487
+ }
488
+ })
489
+
490
+ // Workshop client UI is now mounted by mountStoryboardCore() via the
491
+ // compiled UI bundle. No script injection needed.
492
+
493
+ // Plugin registry for external plugins (future use).
494
+ // Plugins call registerRoutes/registerClientScript in their setup().
495
+ // const pluginCtx = { server, root, config, registerRoutes, registerClientScript }
496
+ // Future: auto-discover and initialize plugins from pluginsConfig here
497
+
498
+ // Mount the /_storyboard/ middleware router
499
+ // Vite's dev server strips the base path from req.url for middleware,
500
+ // but the base-redirect plugin may redirect bare URLs first.
501
+ // We check both with and without base prefix.
502
+ server.middlewares.use(async (req, res, next) => {
503
+ if (!req.url) return next()
504
+
505
+ // Strip base path if present to normalize the URL
506
+ let url = req.url
507
+ const baseNoTrail = base.replace(/\/$/, '')
508
+ if (baseNoTrail && url.startsWith(baseNoTrail)) {
509
+ url = url.slice(baseNoTrail.length) || '/'
510
+ }
511
+
512
+ if (!url.startsWith(API_PREFIX)) return next()
513
+
514
+ // Parse: /_storyboard/{prefix}/{rest}
515
+ const pathAfterPrefix = url.slice(API_PREFIX.length)
516
+ const slashIndex = pathAfterPrefix.indexOf('/')
517
+ const prefix = slashIndex === -1 ? pathAfterPrefix : pathAfterPrefix.slice(0, slashIndex)
518
+ const restPath = slashIndex === -1 ? '/' : pathAfterPrefix.slice(slashIndex)
519
+
520
+ // Attach route context for the logging sendJson wrapper
521
+ res.__sbLogCtx = { method: req.method, url, route: prefix, subRoute: restPath }
522
+
523
+ const handler = routeHandlers.get(prefix)
524
+ if (!handler) {
525
+ // Proxy to standalone storyboard server for unhandled prefixes
526
+ try {
527
+ const proxyReq = await import('node:http')
528
+ const proxyUrl = `http://localhost:4100${url}`
529
+ const proxy = proxyReq.default.request(proxyUrl, { method: req.method, headers: req.headers }, (proxyRes) => {
530
+ res.writeHead(proxyRes.statusCode, proxyRes.headers)
531
+ proxyRes.pipe(res)
532
+ })
533
+ proxy.on('error', () => {
534
+ sendJsonLogged(res, 502, { error: `Storyboard server not running. Start it with: npx storyboard server` })
535
+ })
536
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
537
+ req.pipe(proxy)
538
+ } else {
539
+ proxy.end()
540
+ }
541
+ } catch {
542
+ sendJsonLogged(res, 502, { error: 'Storyboard server not running' })
543
+ }
544
+ return
545
+ }
546
+
547
+ try {
548
+ let body = {}
549
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH' || req.method === 'DELETE') {
550
+ body = await parseJsonBody(req)
551
+ }
552
+ await handler(req, res, { body, path: restPath, method: req.method, __viteWs: server.ws })
553
+ } catch (err) {
554
+ sendJsonLogged(res, 500, { error: err.message || 'Internal server error' })
555
+ }
556
+ })
557
+ },
558
+
559
+ transformIndexHtml() {
560
+ const tags = []
561
+
562
+ // Inject local dev flag only during dev server (not production builds)
563
+ if (isDev) {
564
+ tags.push({
565
+ tag: 'script',
566
+ children: 'window.__SB_LOCAL_DEV__=true',
567
+ injectTo: 'head',
568
+ })
569
+
570
+ // Inject dev domain name for branch bar display
571
+ if (config.devDomain) {
572
+ tags.push({
573
+ tag: 'script',
574
+ children: `window.__SB_DEV_DOMAIN__=${JSON.stringify(config.devDomain)}`,
575
+ injectTo: 'head',
576
+ })
577
+ }
578
+
579
+ // Inject per-domain branch bar color (configurable via devDomainColor)
580
+ if (config.devDomainColor) {
581
+ tags.push({
582
+ tag: 'script',
583
+ children: `window.__SB_DEV_DOMAIN_COLOR__=${JSON.stringify(config.devDomainColor)}`,
584
+ injectTo: 'head',
585
+ })
586
+ }
587
+
588
+ // Browser error bridge — forwards console.error/warn and uncaught
589
+ // exceptions to the dev server via HMR for structured o11y logging
590
+ tags.push({
591
+ tag: 'script',
592
+ attrs: { type: 'module' },
593
+ children: `
594
+ (function() {
595
+ if (!import.meta.hot) return;
596
+ var MAX_LEN = 2000;
597
+ function trunc(s) { return typeof s === 'string' && s.length > MAX_LEN ? s.slice(0, MAX_LEN) + '…' : s; }
598
+ function route() { return location.pathname + location.hash; }
599
+ function send(level, msg, extra) {
600
+ try { import.meta.hot.send('storyboard:client-error', Object.assign({ level: level, message: trunc(msg), route: route() }, extra || {})); } catch {}
601
+ }
602
+ // Patch console.error and console.warn
603
+ ['error', 'warn'].forEach(function(level) {
604
+ var orig = console[level];
605
+ console[level] = function() {
606
+ orig.apply(console, arguments);
607
+ var parts = [];
608
+ for (var i = 0; i < arguments.length; i++) {
609
+ try { parts.push(typeof arguments[i] === 'string' ? arguments[i] : JSON.stringify(arguments[i])); } catch { parts.push(String(arguments[i])); }
610
+ }
611
+ send(level, parts.join(' '));
612
+ };
613
+ });
614
+ // Uncaught errors
615
+ window.addEventListener('error', function(e) {
616
+ send('error', e.message || 'Uncaught error', { url: e.filename, line: e.lineno, col: e.colno, stack: trunc(e.error && e.error.stack) });
617
+ });
618
+ // Unhandled promise rejections
619
+ window.addEventListener('unhandledrejection', function(e) {
620
+ var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled rejection';
621
+ send('error', msg, { stack: trunc(e.reason && e.reason.stack) });
622
+ });
623
+ })();
624
+ `.trim(),
625
+ injectTo: 'head',
626
+ })
627
+ }
628
+
629
+ // Inject base path so the inspector UI can resolve static assets
630
+ // (e.g. inspector.json) when deployed under a subpath
631
+ tags.push({
632
+ tag: 'script',
633
+ children: `window.__STORYBOARD_BASE_PATH__=${JSON.stringify(base)}`,
634
+ injectTo: 'head',
635
+ })
636
+
637
+ for (const src of clientScripts) {
638
+ tags.push({
639
+ tag: 'script',
640
+ attrs: { type: 'module', src: base + src.replace(/^\//, '') },
641
+ injectTo: 'body',
642
+ })
643
+ }
644
+
645
+ return tags
646
+ },
647
+
648
+ // Build-time: emit a static JSON with source files so the inspector
649
+ // works in deployed environments without the dev middleware.
650
+ async generateBundle() {
651
+ const srcDir = path.join(root, 'src')
652
+ const prototypesDir = path.join(root, 'src', 'prototypes')
653
+
654
+ // Collect file lists (prototypes for the files index, all src/ for sources)
655
+ const [prototypeFiles, allSrcFiles] = await Promise.all([
656
+ collectFiles(prototypesDir, root),
657
+ collectFiles(srcDir, root),
658
+ ])
659
+
660
+ // Read all source file contents
661
+ const sources = {}
662
+ await Promise.all(
663
+ allSrcFiles.map(async (relPath) => {
664
+ try {
665
+ sources[relPath] = await fs.promises.readFile(
666
+ path.join(root, relPath),
667
+ 'utf-8'
668
+ )
669
+ } catch { /* skip unreadable files */ }
670
+ })
671
+ )
672
+
673
+ // Resolve repo info (same logic as docs-handler)
674
+ let repo = null
675
+ try {
676
+ const { execSync } = await import('node:child_process')
677
+ const remote = execSync('git remote get-url origin', {
678
+ cwd: root,
679
+ encoding: 'utf-8',
680
+ }).trim()
681
+ const match = remote.match(/github\.com[:/]([^/]+)\/([^/.]+)/)
682
+ if (match) repo = { owner: match[1], name: match[2] }
683
+ } catch { /* no git or no remote */ }
684
+
685
+ if (!repo) {
686
+ const configPath = path.join(root, 'storyboard.config.json')
687
+ try {
688
+ const raw = await fs.promises.readFile(configPath, 'utf-8')
689
+ const cfg = JSON.parse(raw)
690
+ if (cfg.repository?.owner && cfg.repository?.name) {
691
+ repo = { owner: cfg.repository.owner, name: cfg.repository.name }
692
+ }
693
+ } catch { /* config not available */ }
694
+ }
695
+
696
+ this.emitFile({
697
+ type: 'asset',
698
+ fileName: '_storyboard/inspector.json',
699
+ source: JSON.stringify({
700
+ files: prototypeFiles.sort(),
701
+ sources,
702
+ repo,
703
+ }),
704
+ })
705
+
706
+ // Emit README as static JSON so the docs panel works in deployed builds.
707
+ // Dev server serves this dynamically; production needs the static file.
708
+ let readmeContent = null
709
+ for (const candidate of ['README.md', 'readme.md', 'Readme.md']) {
710
+ try {
711
+ readmeContent = await fs.promises.readFile(path.join(root, candidate), 'utf-8')
712
+ break
713
+ } catch { /* try next */ }
714
+ }
715
+ if (readmeContent) {
716
+ this.emitFile({
717
+ type: 'asset',
718
+ fileName: '_storyboard/docs/readme',
719
+ source: JSON.stringify({ content: readmeContent, path: 'README.md' }),
720
+ })
721
+ }
722
+
723
+ // Emit repo info so the docs panel GitHub link works in deployed builds.
724
+ if (repo) {
725
+ this.emitFile({
726
+ type: 'asset',
727
+ fileName: '_storyboard/docs/repo',
728
+ source: JSON.stringify(repo),
729
+ })
730
+ }
731
+
732
+ // Emit story sources JSON so the "show code" widget action works in
733
+ // deployed builds. In dev, StoryWidget uses Vite's ?raw import; in prod
734
+ // it fetches this static JSON instead.
735
+ const storySources = {}
736
+ const storyExts = ['.story.jsx', '.story.tsx', '.story.js', '.story.ts']
737
+ for (const relPath of allSrcFiles) {
738
+ if (storyExts.some(ext => relPath.endsWith(ext))) {
739
+ storySources[relPath] = sources[relPath] || ''
740
+ }
741
+ }
742
+ if (Object.keys(storySources).length > 0) {
743
+ this.emitFile({
744
+ type: 'asset',
745
+ fileName: '_storyboard/stories/sources.json',
746
+ source: JSON.stringify(storySources),
747
+ })
748
+ }
749
+
750
+ // Emit canvas images so they're available in deployed (static) builds.
751
+ // Dev server serves these dynamically; production needs the static files.
752
+ // Private images (prefixed with ~) are excluded from the build.
753
+ for (const dir of [
754
+ path.join(root, 'assets', 'canvas', 'images'),
755
+ path.join(root, 'assets', 'canvas', 'snapshots'),
756
+ ]) {
757
+ try {
758
+ const imageFiles = await fs.promises.readdir(dir)
759
+ const subdir = dir.endsWith('snapshots') ? 'snapshots' : 'images'
760
+ for (const file of imageFiles) {
761
+ if (file.startsWith('~') || file.startsWith('.')) continue
762
+ try {
763
+ const data = await fs.promises.readFile(path.join(dir, file))
764
+ this.emitFile({
765
+ type: 'asset',
766
+ fileName: `_storyboard/canvas/${subdir}/${file}`,
767
+ source: data,
768
+ })
769
+ } catch { /* skip unreadable files */ }
770
+ }
771
+ } catch { /* directory doesn't exist */ }
772
+ }
773
+
774
+ // GitHub Pages uses Jekyll which ignores _-prefixed directories.
775
+ // Emit .nojekyll to ensure _storyboard/ is served.
776
+ this.emitFile({
777
+ type: 'asset',
778
+ fileName: '.nojekyll',
779
+ source: '',
780
+ })
781
+
782
+ // Emit CNAME for GitHub Pages custom domain if configured.
783
+ // Without this, deploy scripts that clean the gh-pages root will
784
+ // delete the CNAME on every push, causing intermittent 404s.
785
+ const customDomain = (config.customDomain || '').trim()
786
+ if (customDomain && !customDomain.includes('/') && !customDomain.includes(':') && !customDomain.includes(' ')) {
787
+ this.emitFile({
788
+ type: 'asset',
789
+ fileName: 'CNAME',
790
+ source: customDomain + '\n',
791
+ })
792
+ }
793
+ },
794
+ }
795
+ }
796
+
797
+ export { sendJson }