@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,714 @@
1
+ /**
2
+ * Autosync Server — automatic commit + push watcher.
3
+ *
4
+ * Dev-server middleware that provides git automation:
5
+ * - List branches (excluding main/master)
6
+ * - Enable/disable autosync per scope (canvas/prototype)
7
+ * - Direct commit + push on the current branch (scoped files only)
8
+ * - Push watcher: every 30s runs enabled scopes in relay sequence
9
+ * - Persists state to .storyboard/autosync.json to survive server restarts
10
+ * - Pauses on branch change, resumes when user returns to target branch
11
+ *
12
+ * Routes (mounted at /_storyboard/autosync/):
13
+ * GET /branches — list local git branches (excludes main/master)
14
+ * GET /status — current state (branch, enabled scopes, last sync/errors)
15
+ * POST /enable — enable autosync for a scope on a branch
16
+ * POST /disable — disable autosync for a scope (or all scopes)
17
+ * POST /sync — trigger a single sync cycle manually
18
+ */
19
+
20
+ import { execFileSync } from 'node:child_process'
21
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync } from 'node:fs'
22
+ import { join, resolve } from 'node:path'
23
+
24
+ // ── Module-level watcher state (singleton, survives page reloads) ──
25
+
26
+ let schedulerInterval = null
27
+ let schedulerTimeout = null
28
+ let targetBranch = null
29
+ let originalBranch = null
30
+ let lastSyncTime = null
31
+ let lastError = null
32
+ let syncing = false
33
+ let syncingScope = null
34
+ let pausedOnBranchChange = false
35
+ let previousActiveBranch = null
36
+
37
+ let enabledScopes = { canvas: false, prototype: false }
38
+ let lastSyncByScope = { canvas: null, prototype: null }
39
+ let lastErrorByScope = { canvas: null, prototype: null }
40
+
41
+ const SYNC_INTERVAL_MS = 30_000
42
+ const PUSH_RETRY_LIMIT = 3
43
+ const SCOPE_ORDER = ['canvas', 'prototype']
44
+ const AUTOSYNC_SCOPES = new Set(SCOPE_ORDER)
45
+
46
+ // Branch names must match git ref format — alphanumeric, hyphens, dots, slashes
47
+ const BRANCH_NAME_RE = /^[\w][\w.\-/]*$/
48
+
49
+ // ── Persistence (.storyboard/autosync.json) ──
50
+
51
+ const PERSIST_DIR = '.storyboard'
52
+ const PERSIST_FILE = 'autosync.json'
53
+
54
+ /**
55
+ * Load persisted autosync state from disk. Returns null on missing/corrupt files.
56
+ */
57
+ export function loadPersistedState(root) {
58
+ const filePath = join(root, PERSIST_DIR, PERSIST_FILE)
59
+ try {
60
+ if (!existsSync(filePath)) return null
61
+ const raw = readFileSync(filePath, 'utf-8')
62
+ const data = JSON.parse(raw)
63
+ if (!data || typeof data !== 'object') return null
64
+ return validatePersistedState(data)
65
+ } catch {
66
+ return null
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Validate loaded state — reject invalid/protected branches and bad types.
72
+ */
73
+ function validatePersistedState(data) {
74
+ const result = {}
75
+
76
+ if (data.targetBranch && isValidBranch(data.targetBranch) && !isProtectedBranch(data.targetBranch)) {
77
+ result.targetBranch = data.targetBranch
78
+ }
79
+ if (data.originalBranch && isValidBranch(data.originalBranch)) {
80
+ result.originalBranch = data.originalBranch
81
+ }
82
+ if (data.previousActiveBranch && isValidBranch(data.previousActiveBranch)) {
83
+ result.previousActiveBranch = data.previousActiveBranch
84
+ }
85
+
86
+ result.pausedOnBranchChange = data.pausedOnBranchChange === true
87
+ result.lastSyncTime = typeof data.lastSyncTime === 'string' ? data.lastSyncTime : null
88
+ result.lastSyncByScope = {
89
+ canvas: typeof data.lastSyncByScope?.canvas === 'string' ? data.lastSyncByScope.canvas : null,
90
+ prototype: typeof data.lastSyncByScope?.prototype === 'string' ? data.lastSyncByScope.prototype : null,
91
+ }
92
+
93
+ if (data.enabledScopes && typeof data.enabledScopes === 'object') {
94
+ result.enabledScopes = {
95
+ canvas: data.enabledScopes.canvas === true,
96
+ prototype: data.enabledScopes.prototype === true,
97
+ }
98
+ } else {
99
+ result.enabledScopes = { canvas: false, prototype: false }
100
+ }
101
+
102
+ // Must have a targetBranch and at least one enabled scope to be restorable
103
+ if (!result.targetBranch || (!result.enabledScopes.canvas && !result.enabledScopes.prototype)) {
104
+ return null
105
+ }
106
+
107
+ return result
108
+ }
109
+
110
+ /**
111
+ * Persist current autosync state to disk (atomic write via tmp + rename).
112
+ */
113
+ export function persistState(root) {
114
+ const dirPath = join(root, PERSIST_DIR)
115
+ const filePath = join(dirPath, PERSIST_FILE)
116
+ const tmpPath = filePath + '.tmp'
117
+ try {
118
+ const currentBranch = getCurrentBranch(root)
119
+ const data = {
120
+ enabledScopes: { ...enabledScopes },
121
+ targetBranch,
122
+ originalBranch,
123
+ previousActiveBranch,
124
+ currentBranch,
125
+ pausedOnBranchChange,
126
+ lastSyncTime,
127
+ lastSyncByScope: { ...lastSyncByScope },
128
+ }
129
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true })
130
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8')
131
+ renameSync(tmpPath, filePath)
132
+ } catch {
133
+ // Best-effort persistence — don't break autosync if disk write fails
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Remove persisted state file (on explicit full disable).
139
+ */
140
+ export function clearPersistedState(root) {
141
+ const filePath = join(root, PERSIST_DIR, PERSIST_FILE)
142
+ try {
143
+ if (existsSync(filePath)) unlinkSync(filePath)
144
+ } catch { /* ignore — file may already be gone */ }
145
+ }
146
+
147
+ /**
148
+ * Restore module-level state from a validated persisted snapshot.
149
+ */
150
+ function applyPersistedState(data) {
151
+ enabledScopes = { ...data.enabledScopes }
152
+ targetBranch = data.targetBranch
153
+ originalBranch = data.originalBranch || data.targetBranch
154
+ previousActiveBranch = data.previousActiveBranch || null
155
+ pausedOnBranchChange = data.pausedOnBranchChange || false
156
+ lastSyncTime = data.lastSyncTime || null
157
+ lastSyncByScope = { ...data.lastSyncByScope }
158
+ }
159
+
160
+ // ── Branch reconciliation ──
161
+
162
+ /**
163
+ * Reconcile branch state — pause on drift, resume on return.
164
+ * Called at server startup and on each scheduler tick.
165
+ * Returns true if autosync is active (not paused), false if paused.
166
+ */
167
+ export function reconcileBranch(root) {
168
+ if (!targetBranch || !hasAnyScopeEnabled()) return true
169
+
170
+ let current
171
+ try {
172
+ current = getCurrentBranch(root)
173
+ } catch {
174
+ return false // can't determine branch — don't sync
175
+ }
176
+
177
+ if (current === targetBranch) {
178
+ // Back on the target branch — resume if we were paused
179
+ if (pausedOnBranchChange) {
180
+ pausedOnBranchChange = false
181
+ previousActiveBranch = null
182
+ persistState(root)
183
+ }
184
+ return true
185
+ }
186
+
187
+ // Branch drift — pause if not already paused
188
+ if (!pausedOnBranchChange) {
189
+ pausedOnBranchChange = true
190
+ previousActiveBranch = targetBranch
191
+ persistState(root)
192
+ }
193
+ return false
194
+ }
195
+
196
+ function isProtectedBranch(name) {
197
+ const normalized = String(name || '').toLowerCase()
198
+ return normalized === 'main' || normalized === 'master'
199
+ }
200
+
201
+ // ── Git helpers (argv-based, no shell) ──
202
+
203
+ function git(args, root) {
204
+ return execFileSync('git', args, { cwd: root, encoding: 'utf-8', timeout: 30_000 }).trim()
205
+ }
206
+
207
+ function getCurrentBranch(root) {
208
+ return git(['rev-parse', '--abbrev-ref', 'HEAD'], root)
209
+ }
210
+
211
+ function getUsername(root) {
212
+ try {
213
+ return git(['config', 'user.name'], root)
214
+ } catch {
215
+ return 'autosync'
216
+ }
217
+ }
218
+
219
+ function getBranches(root) {
220
+ const raw = git(['branch', '--list', '--format=%(refname:short)'], root)
221
+ return raw
222
+ .split('\n')
223
+ .map((b) => b.trim())
224
+ .filter((b) => b && b.toLowerCase() !== 'main' && b.toLowerCase() !== 'master')
225
+ }
226
+
227
+ function getGitDir(root) {
228
+ return resolve(root, git(['rev-parse', '--git-dir'], root))
229
+ }
230
+
231
+ function hasScopedStagedChanges(root, files) {
232
+ if (!files || files.length === 0) return false
233
+ const changed = git(['diff', '--cached', '--name-only', '--', ...files], root)
234
+ return changed.length > 0
235
+ }
236
+
237
+ function listChangedFiles(root) {
238
+ const tracked = git(['diff', '--name-only'], root)
239
+ const untracked = git(['ls-files', '--others', '--exclude-standard'], root)
240
+ return [tracked, untracked]
241
+ .flatMap((raw) => raw.split('\n'))
242
+ .map((file) => file.trim())
243
+ .filter(Boolean)
244
+ .filter((file, idx, arr) => arr.indexOf(file) === idx)
245
+ }
246
+
247
+ // ── Repo-busy guards ──
248
+
249
+ /**
250
+ * Check if the repo is in a state where autosync should defer.
251
+ * Returns { busy: true, reason } if unsafe, { busy: false } otherwise.
252
+ */
253
+ export function isRepoBusy(root) {
254
+ const gitDir = getGitDir(root)
255
+
256
+ if (existsSync(join(gitDir, 'index.lock'))) {
257
+ return { busy: true, reason: 'index.lock exists — another git process is active' }
258
+ }
259
+ if (existsSync(join(gitDir, 'rebase-merge')) || existsSync(join(gitDir, 'rebase-apply'))) {
260
+ return { busy: true, reason: 'rebase in progress' }
261
+ }
262
+ if (existsSync(join(gitDir, 'MERGE_HEAD'))) {
263
+ return { busy: true, reason: 'merge in progress' }
264
+ }
265
+ if (existsSync(join(gitDir, 'CHERRY_PICK_HEAD'))) {
266
+ return { busy: true, reason: 'cherry-pick in progress' }
267
+ }
268
+
269
+ if (targetBranch && getCurrentBranch(root) !== targetBranch) {
270
+ return { busy: true, reason: `branch drift: expected ${targetBranch}, on ${getCurrentBranch(root)}` }
271
+ }
272
+
273
+ return { busy: false }
274
+ }
275
+
276
+ export function normalizeAutosyncScope(scope) {
277
+ return AUTOSYNC_SCOPES.has(scope) ? scope : 'canvas'
278
+ }
279
+
280
+ export function matchesAutosyncScope(scope, filePath) {
281
+ const normalizedScope = normalizeAutosyncScope(scope)
282
+ const file = String(filePath || '').replaceAll('\\', '/').replace(/^\.\//, '')
283
+ if (!file) return false
284
+
285
+ if (normalizedScope === 'prototype') {
286
+ return file === 'src/prototypes' || file.startsWith('src/prototypes/')
287
+ }
288
+
289
+ // canvas scope — includes canvas data, canvas assets, and public storyboard assets
290
+ return (
291
+ file === 'src/canvas' ||
292
+ file.startsWith('src/canvas/') ||
293
+ file.endsWith('.canvas.jsonl') ||
294
+ file.startsWith('assets/canvas/') ||
295
+ file.startsWith('assets/.storyboard-public/')
296
+ )
297
+ }
298
+
299
+ export function filterFilesForAutosyncScope(scope, files) {
300
+ return (files || []).filter((file) => matchesAutosyncScope(scope, file))
301
+ }
302
+
303
+ function listScopedChangedFiles(root, scope) {
304
+ return filterFilesForAutosyncScope(scope, listChangedFiles(root))
305
+ }
306
+
307
+ function isValidBranch(name) {
308
+ return typeof name === 'string' && BRANCH_NAME_RE.test(name) && name.length < 256
309
+ }
310
+
311
+ function formatTime() {
312
+ return new Date().toLocaleString('en-US', {
313
+ month: 'short',
314
+ day: 'numeric',
315
+ hour: 'numeric',
316
+ minute: '2-digit',
317
+ hour12: true,
318
+ })
319
+ }
320
+
321
+ export function isRetryablePushError(message) {
322
+ const normalized = String(message || '').toLowerCase()
323
+ return (
324
+ normalized.includes('failed to push some refs') ||
325
+ normalized.includes('non-fast-forward') ||
326
+ normalized.includes('updates were rejected') ||
327
+ normalized.includes('tip of your current branch is behind') ||
328
+ normalized.includes('fetch first') ||
329
+ normalized.includes('[rejected]')
330
+ )
331
+ }
332
+
333
+ function hasAnyScopeEnabled() {
334
+ return enabledScopes.canvas || enabledScopes.prototype
335
+ }
336
+
337
+ function getEnabledScopesInOrder() {
338
+ return SCOPE_ORDER.filter((scope) => enabledScopes[scope])
339
+ }
340
+
341
+ function stopScheduler() {
342
+ if (schedulerTimeout) {
343
+ clearTimeout(schedulerTimeout)
344
+ schedulerTimeout = null
345
+ }
346
+ if (schedulerInterval) {
347
+ clearInterval(schedulerInterval)
348
+ schedulerInterval = null
349
+ }
350
+ }
351
+
352
+ function getAlignedDelay() {
353
+ const remainder = Date.now() % SYNC_INTERVAL_MS
354
+ return remainder === 0 ? SYNC_INTERVAL_MS : SYNC_INTERVAL_MS - remainder
355
+ }
356
+
357
+ function resetRuntimeState({ clearBranch = true } = {}) {
358
+ enabledScopes = { canvas: false, prototype: false }
359
+ syncing = false
360
+ syncingScope = null
361
+ pausedOnBranchChange = false
362
+ previousActiveBranch = null
363
+ if (clearBranch) {
364
+ targetBranch = null
365
+ originalBranch = null
366
+ }
367
+ }
368
+
369
+ /** Undo a commit that was never pushed, leaving files staged then unstaged. */
370
+ function rollbackUnpushedCommit(root, scopedFiles) {
371
+ try {
372
+ git(['reset', '--soft', 'HEAD~1'], root)
373
+ git(['reset', '--', ...scopedFiles], root)
374
+ } catch {
375
+ // Best-effort rollback; if this fails the user's tree is still valid.
376
+ }
377
+ }
378
+
379
+ function buildStatusPayload(root) {
380
+ const singleScope = enabledScopes.canvas === enabledScopes.prototype
381
+ ? null
382
+ : (enabledScopes.canvas ? 'canvas' : 'prototype')
383
+
384
+ return {
385
+ enabled: hasAnyScopeEnabled(),
386
+ enabledScopes: { ...enabledScopes },
387
+ scope: singleScope, // legacy field for older clients
388
+ branch: getCurrentBranch(root),
389
+ targetBranch,
390
+ originalBranch,
391
+ availableScopes: [...AUTOSYNC_SCOPES],
392
+ lastSyncTime,
393
+ lastSyncByScope: { ...lastSyncByScope },
394
+ lastError,
395
+ lastErrorByScope: { ...lastErrorByScope },
396
+ syncing,
397
+ syncingScope,
398
+ pausedOnBranchChange,
399
+ previousActiveBranch,
400
+ }
401
+ }
402
+
403
+ function stopAutosync(root, { clearBranch = true, clearErrors = false } = {}) {
404
+ stopScheduler()
405
+ resetRuntimeState({ clearBranch })
406
+ if (clearErrors) {
407
+ lastError = null
408
+ lastErrorByScope = { canvas: null, prototype: null }
409
+ }
410
+ }
411
+
412
+ // ── Sync cycle ──
413
+
414
+ /** Run one scoped sync — stage, commit, and push scoped files directly. */
415
+ function runSyncCycle(root, scope) {
416
+ if (syncing) return false
417
+ syncing = true
418
+ syncingScope = scope
419
+ let cycleSucceeded = false
420
+ let committed = false
421
+ let scopedFiles = []
422
+
423
+ try {
424
+ if (!targetBranch) {
425
+ throw new Error('Autosync branch is not configured')
426
+ }
427
+
428
+ // Guard: skip if repo is busy (index lock, rebase, merge, branch drift)
429
+ const busy = isRepoBusy(root)
430
+ if (busy.busy) {
431
+ cycleSucceeded = true // defer, not failure
432
+ return true
433
+ }
434
+
435
+ scopedFiles = listScopedChangedFiles(root, scope)
436
+ if (scopedFiles.length === 0) {
437
+ cycleSucceeded = true
438
+ return true
439
+ }
440
+
441
+ // Guard: skip if scoped files already have user-staged changes
442
+ if (hasScopedStagedChanges(root, scopedFiles)) {
443
+ cycleSucceeded = true // defer, not failure
444
+ return true
445
+ }
446
+
447
+ git(['add', '-A', '--', ...scopedFiles], root)
448
+
449
+ if (!hasScopedStagedChanges(root, scopedFiles)) {
450
+ cycleSucceeded = true
451
+ return true
452
+ }
453
+
454
+ const username = getUsername(root)
455
+ const time = formatTime()
456
+ git(
457
+ ['commit', '-m', `[auto:${scope}] ${username} update at ${time}`, '--', ...scopedFiles],
458
+ root,
459
+ )
460
+ committed = true
461
+
462
+ for (let attempt = 1; attempt <= PUSH_RETRY_LIMIT; attempt += 1) {
463
+ // Re-check guards before push/rebase
464
+ const pushBusy = isRepoBusy(root)
465
+ if (pushBusy.busy) {
466
+ rollbackUnpushedCommit(root, scopedFiles)
467
+ committed = false
468
+ cycleSucceeded = true // defer
469
+ return true
470
+ }
471
+
472
+ try {
473
+ git(['push', 'origin', `HEAD:refs/heads/${targetBranch}`], root)
474
+ cycleSucceeded = true
475
+ break
476
+ } catch (pushErr) {
477
+ if (!isRetryablePushError(pushErr?.message) || attempt === PUSH_RETRY_LIMIT) {
478
+ throw pushErr
479
+ }
480
+
481
+ // Fetch and rebase with autostash to handle non-fast-forward
482
+ try {
483
+ git(['fetch', 'origin', targetBranch], root)
484
+ git(['rebase', '--autostash', 'FETCH_HEAD'], root)
485
+ } catch {
486
+ // Rebase failed — abort and defer
487
+ try { git(['rebase', '--abort'], root) } catch { /* no rebase in progress */ }
488
+ rollbackUnpushedCommit(root, scopedFiles)
489
+ committed = false
490
+ cycleSucceeded = true // defer, try again next cycle
491
+ return true
492
+ }
493
+ }
494
+ }
495
+ } catch (err) {
496
+ lastError = err.message || 'Sync failed'
497
+ lastErrorByScope[scope] = lastError
498
+
499
+ // Rollback the commit if we made one but never pushed
500
+ if (committed) {
501
+ rollbackUnpushedCommit(root, scopedFiles)
502
+ }
503
+ } finally {
504
+ if (cycleSucceeded) {
505
+ const nowIso = new Date().toISOString()
506
+ lastSyncTime = nowIso
507
+ lastSyncByScope[scope] = nowIso
508
+ lastErrorByScope[scope] = null
509
+ lastError = null
510
+ // Persist state after actual commit+push (committed is still true only if push succeeded)
511
+ if (committed) persistState(root)
512
+ }
513
+ syncing = false
514
+ syncingScope = null
515
+ }
516
+
517
+ return cycleSucceeded
518
+ }
519
+
520
+ function runRelayCycle(root, scopes = getEnabledScopesInOrder()) {
521
+ if (syncing || scopes.length === 0) return true
522
+
523
+ // Reconcile branch state — pause on drift, resume on return
524
+ if (!reconcileBranch(root)) return true // paused — skip sync
525
+
526
+ let ok = true
527
+ let firstRelaySyncTime = null
528
+
529
+ for (const scope of scopes) {
530
+ if (!runSyncCycle(root, scope)) {
531
+ ok = false
532
+ break
533
+ }
534
+
535
+ if (!firstRelaySyncTime && lastSyncByScope[scope]) {
536
+ firstRelaySyncTime = lastSyncByScope[scope]
537
+ }
538
+ }
539
+
540
+ // Keep a single "last sync" timestamp for relay cycles — the first synced scope.
541
+ if (firstRelaySyncTime) {
542
+ lastSyncTime = firstRelaySyncTime
543
+ }
544
+
545
+ return ok
546
+ }
547
+
548
+ function startScheduler(root) {
549
+ if (schedulerInterval || schedulerTimeout) return
550
+ schedulerTimeout = setTimeout(() => {
551
+ schedulerTimeout = null
552
+ runRelayCycle(root)
553
+ schedulerInterval = setInterval(() => runRelayCycle(root), SYNC_INTERVAL_MS)
554
+ }, getAlignedDelay())
555
+ }
556
+
557
+ // ── Route handler ──
558
+
559
+ export function createAutosyncHandler({ root, sendJson }) {
560
+ // ── Restore persisted state on server startup ──
561
+ const persisted = loadPersistedState(root)
562
+ if (persisted) {
563
+ applyPersistedState(persisted)
564
+ reconcileBranch(root)
565
+
566
+ // Start scheduler if any scope is enabled — reconcileBranch handles
567
+ // pause/resume on each tick, so the scheduler runs even when paused
568
+ if (hasAnyScopeEnabled()) {
569
+ startScheduler(root)
570
+ }
571
+ }
572
+
573
+ return async (req, res, { body, path: routePath, method }) => {
574
+ // GET /branches — list local branches
575
+ if (routePath === '/branches' && method === 'GET') {
576
+ try {
577
+ const branches = getBranches(root)
578
+ const current = getCurrentBranch(root)
579
+ sendJson(res, 200, { branches, current })
580
+ } catch (err) {
581
+ sendJson(res, 500, { error: err.message })
582
+ }
583
+ return
584
+ }
585
+
586
+ // GET /status — current autosync state
587
+ if (routePath === '/status' && method === 'GET') {
588
+ try {
589
+ sendJson(res, 200, buildStatusPayload(root))
590
+ } catch (err) {
591
+ sendJson(res, 500, { error: err.message })
592
+ }
593
+ return
594
+ }
595
+
596
+ // POST /enable — enable autosync for a scope
597
+ if (routePath === '/enable' && method === 'POST') {
598
+ try {
599
+ const { branch, scope } = body || {}
600
+ if (!branch) {
601
+ sendJson(res, 400, { error: 'branch is required' })
602
+ return
603
+ }
604
+ if (!isValidBranch(branch)) {
605
+ sendJson(res, 400, { error: 'Invalid branch name' })
606
+ return
607
+ }
608
+ if (branch.toLowerCase() === 'main' || branch.toLowerCase() === 'master') {
609
+ sendJson(res, 400, { error: 'Cannot autosync to main/master' })
610
+ return
611
+ }
612
+
613
+ const currentBranch = getCurrentBranch(root)
614
+ if (branch !== currentBranch) {
615
+ sendJson(res, 400, {
616
+ error: `Autosync requires you to be on the target branch. Current: ${currentBranch}, requested: ${branch}`,
617
+ })
618
+ return
619
+ }
620
+
621
+ const normalizedScope = normalizeAutosyncScope(scope)
622
+ const hadEnabledScopes = hasAnyScopeEnabled()
623
+
624
+ // Allow retargeting when paused on a different branch
625
+ if (hadEnabledScopes && targetBranch !== branch) {
626
+ if (pausedOnBranchChange) {
627
+ // Clear pause and retarget to the new branch
628
+ pausedOnBranchChange = false
629
+ previousActiveBranch = null
630
+ targetBranch = branch
631
+ originalBranch = currentBranch
632
+ } else {
633
+ sendJson(res, 409, { error: `Autosync is active on ${targetBranch}. Disable all scopes before switching branch.` })
634
+ return
635
+ }
636
+ }
637
+
638
+ if (!hadEnabledScopes) {
639
+ originalBranch = currentBranch
640
+ targetBranch = branch
641
+ }
642
+
643
+ enabledScopes[normalizedScope] = true
644
+ lastErrorByScope[normalizedScope] = null
645
+ pausedOnBranchChange = false
646
+ previousActiveBranch = null
647
+ persistState(root)
648
+ startScheduler(root)
649
+
650
+ // Immediate first sync for the enabled scope.
651
+ runSyncCycle(root, normalizedScope)
652
+ sendJson(res, 200, buildStatusPayload(root))
653
+ } catch (err) {
654
+ sendJson(res, 500, { error: err.message })
655
+ }
656
+ return
657
+ }
658
+
659
+ // POST /disable — disable one scope or all scopes
660
+ if (routePath === '/disable' && method === 'POST') {
661
+ try {
662
+ const requestedScope = body?.scope
663
+ if (requestedScope) {
664
+ const normalizedScope = normalizeAutosyncScope(requestedScope)
665
+ enabledScopes[normalizedScope] = false
666
+ } else {
667
+ enabledScopes = { canvas: false, prototype: false }
668
+ }
669
+
670
+ if (!hasAnyScopeEnabled()) {
671
+ // Explicit full disable — clear persisted state entirely
672
+ stopAutosync(root, { clearBranch: true, clearErrors: true })
673
+ clearPersistedState(root)
674
+ } else {
675
+ // Partial disable — persist the remaining state
676
+ persistState(root)
677
+ }
678
+
679
+ sendJson(res, 200, buildStatusPayload(root))
680
+ } catch (err) {
681
+ sendJson(res, 500, { error: err.message })
682
+ }
683
+ return
684
+ }
685
+
686
+ // POST /sync — manual single relay cycle
687
+ if (routePath === '/sync' && method === 'POST') {
688
+ try {
689
+ if (syncing) {
690
+ sendJson(res, 409, { error: 'Autosync is already running' })
691
+ return
692
+ }
693
+
694
+ let ok = true
695
+ if (body?.scope) {
696
+ const scope = normalizeAutosyncScope(body.scope)
697
+ ok = runSyncCycle(root, scope)
698
+ } else {
699
+ ok = runRelayCycle(root)
700
+ }
701
+
702
+ sendJson(res, ok ? 200 : 500, {
703
+ ok,
704
+ ...buildStatusPayload(root),
705
+ })
706
+ } catch (err) {
707
+ sendJson(res, 500, { error: err.message })
708
+ }
709
+ return
710
+ }
711
+
712
+ sendJson(res, 404, { error: `Unknown autosync route: ${method} ${routePath}` })
713
+ }
714
+ }