pixelweaver 0.1.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 (392) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +18 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +16 -0
  6. package/.python-version +1 -0
  7. package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
  8. package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
  9. package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
  10. package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
  11. package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
  12. package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
  13. package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
  14. package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
  15. package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
  16. package/.rlsbl/bases/CHANGELOG.md +5 -0
  17. package/.rlsbl/bases/LICENSE +21 -0
  18. package/.rlsbl/changes/.validated +1 -0
  19. package/.rlsbl/changes/0.1.0.jsonl +85 -0
  20. package/.rlsbl/changes/0.1.0.md +13 -0
  21. package/.rlsbl/changes/unreleased.jsonl +0 -0
  22. package/.rlsbl/config.json +6 -0
  23. package/.rlsbl/hashes.json +14 -0
  24. package/.rlsbl/hooks/post-release.sh +8 -0
  25. package/.rlsbl/hooks/pre-checks.sh +5 -0
  26. package/.rlsbl/hooks/pre-release.sh +8 -0
  27. package/.rlsbl/lint/go.toml +17 -0
  28. package/.rlsbl/lint/npm.toml +19 -0
  29. package/.rlsbl/lint/python.toml +25 -0
  30. package/.rlsbl/releases/unreleased.toml +0 -0
  31. package/.rlsbl/releases/v0.1.0.toml +3 -0
  32. package/.rlsbl/version +1 -0
  33. package/.selfdoc/hashes/hashes.json +146 -0
  34. package/.strictcli/schema.json +227 -0
  35. package/CHANGELOG.md +17 -0
  36. package/CLAUDE.md +100 -0
  37. package/LICENSE +21 -0
  38. package/README.md +116 -0
  39. package/assets/icon.png +0 -0
  40. package/docs/_README.md +117 -0
  41. package/docs/cli-config.md +35 -0
  42. package/docs/cli-dev.md +21 -0
  43. package/docs/cli-diagnose.md +12 -0
  44. package/docs/cli-index.md +30 -0
  45. package/docs/cli-list.md +18 -0
  46. package/docs/cli-mcp.md +18 -0
  47. package/docs/cli-new.md +26 -0
  48. package/docs/cli-open.md +21 -0
  49. package/docs/cli-serve.md +21 -0
  50. package/docs/cli-stop.md +12 -0
  51. package/docs/gen-index.md +36 -0
  52. package/docs/index.md +13 -0
  53. package/docs/server-src-pixelweaver-__main__.md +12 -0
  54. package/docs/server-src-pixelweaver-autosave.md +12 -0
  55. package/docs/server-src-pixelweaver-bridge.md +12 -0
  56. package/docs/server-src-pixelweaver-cli.md +12 -0
  57. package/docs/server-src-pixelweaver-config.md +12 -0
  58. package/docs/server-src-pixelweaver-connections.md +12 -0
  59. package/docs/server-src-pixelweaver-main.md +12 -0
  60. package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
  61. package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
  62. package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
  63. package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
  64. package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
  65. package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
  66. package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
  67. package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
  68. package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
  69. package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
  70. package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
  71. package/docs/server-src-pixelweaver-mcp_server.md +12 -0
  72. package/docs/server-src-pixelweaver-protocol.md +12 -0
  73. package/docs/server-src-pixelweaver-state.md +12 -0
  74. package/docs/server-src-pixelweaver-storage.md +12 -0
  75. package/docs/server-src-pixelweaver-websocket.md +12 -0
  76. package/docs/server-src-pixelweaver.md +12 -0
  77. package/e2e/app-launch.test.ts +35 -0
  78. package/e2e/menus.test.ts +26 -0
  79. package/e2e/tools.test.ts +27 -0
  80. package/e2e/undo-redo.test.ts +11 -0
  81. package/eslint.config.js +62 -0
  82. package/index.html +13 -0
  83. package/package.json +48 -0
  84. package/playwright.config.ts +19 -0
  85. package/plugins/builtin/.gitkeep +0 -0
  86. package/plugins/builtin/advanced-fill-tool.ts +146 -0
  87. package/plugins/builtin/circle-tool.ts +186 -0
  88. package/plugins/builtin/diamond-tool.ts +182 -0
  89. package/plugins/builtin/dither-tool.ts +186 -0
  90. package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
  91. package/plugins/builtin/drawing-utils.test.ts +495 -0
  92. package/plugins/builtin/drawing-utils.ts +431 -0
  93. package/plugins/builtin/effects/blur.ts +97 -0
  94. package/plugins/builtin/effects/color-effects.ts +278 -0
  95. package/plugins/builtin/effects/flip.ts +83 -0
  96. package/plugins/builtin/effects/glow.ts +118 -0
  97. package/plugins/builtin/effects/outline.ts +110 -0
  98. package/plugins/builtin/effects/rotate.ts +357 -0
  99. package/plugins/builtin/effects/scale.ts +258 -0
  100. package/plugins/builtin/effects/shadow.ts +111 -0
  101. package/plugins/builtin/effects/sharpen.ts +102 -0
  102. package/plugins/builtin/effects.test.ts +715 -0
  103. package/plugins/builtin/eraser-tool.ts +23 -0
  104. package/plugins/builtin/eyedropper-tool.ts +83 -0
  105. package/plugins/builtin/fill-tool.ts +93 -0
  106. package/plugins/builtin/gradient-tool.ts +204 -0
  107. package/plugins/builtin/gradient.test.ts +142 -0
  108. package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
  109. package/plugins/builtin/importers/aseprite-parser.ts +497 -0
  110. package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
  111. package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
  112. package/plugins/builtin/line-tool.ts +267 -0
  113. package/plugins/builtin/make-stroke-tool.ts +271 -0
  114. package/plugins/builtin/noise-dither.test.ts +151 -0
  115. package/plugins/builtin/noise-tool.ts +131 -0
  116. package/plugins/builtin/pattern-stamp-tool.ts +162 -0
  117. package/plugins/builtin/pencil-tool.ts +25 -0
  118. package/plugins/builtin/rect-tool.ts +179 -0
  119. package/plugins/builtin/selection-tool.ts +388 -0
  120. package/plugins/builtin/selection.test.ts +195 -0
  121. package/plugins/builtin/tool-plugins.test.ts +529 -0
  122. package/public/favicon.svg +7 -0
  123. package/public/sw.js +91 -0
  124. package/pyproject.toml +49 -0
  125. package/scripts/eslint-wave-a-list.sh +24 -0
  126. package/scripts/eslint-wave-a-status.sh +25 -0
  127. package/scripts/fix-index-signature-access.py +127 -0
  128. package/scripts/fix-unchecked-index.py +128 -0
  129. package/scripts/fix-unchecked-vars.py +127 -0
  130. package/scripts/fix-wave-a-bangs.py +36 -0
  131. package/scripts/fix-wave-a-templates.py +108 -0
  132. package/scripts/fix-wave-b-void-expr.py +167 -0
  133. package/scripts/generate-command-params.py +540 -0
  134. package/scripts/migrate-single-frame-to-multi.py +171 -0
  135. package/scripts/smoke-test.sh +77 -0
  136. package/selfdoc.json +10 -0
  137. package/server/README.md +0 -0
  138. package/server/src/pixelweaver/__init__.py +1 -0
  139. package/server/src/pixelweaver/__main__.py +4 -0
  140. package/server/src/pixelweaver/autosave.py +114 -0
  141. package/server/src/pixelweaver/bridge.py +127 -0
  142. package/server/src/pixelweaver/cli.py +199 -0
  143. package/server/src/pixelweaver/config.py +24 -0
  144. package/server/src/pixelweaver/connections.py +54 -0
  145. package/server/src/pixelweaver/main.py +271 -0
  146. package/server/src/pixelweaver/mcp_bridge.py +189 -0
  147. package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
  148. package/server/src/pixelweaver/mcp_export_tools.py +291 -0
  149. package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
  150. package/server/src/pixelweaver/mcp_history_tools.py +106 -0
  151. package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
  152. package/server/src/pixelweaver/mcp_lock.py +37 -0
  153. package/server/src/pixelweaver/mcp_project_tools.py +302 -0
  154. package/server/src/pixelweaver/mcp_read_tools.py +163 -0
  155. package/server/src/pixelweaver/mcp_registry.py +247 -0
  156. package/server/src/pixelweaver/mcp_resources.py +312 -0
  157. package/server/src/pixelweaver/mcp_server.py +234 -0
  158. package/server/src/pixelweaver/protocol.py +219 -0
  159. package/server/src/pixelweaver/state.py +267 -0
  160. package/server/src/pixelweaver/storage.py +293 -0
  161. package/server/src/pixelweaver/websocket.py +282 -0
  162. package/server/tests/__init__.py +0 -0
  163. package/server/tests/conftest.py +17 -0
  164. package/server/tests/test_api.py +96 -0
  165. package/server/tests/test_bridge.py +161 -0
  166. package/server/tests/test_health.py +9 -0
  167. package/server/tests/test_integration.py +86 -0
  168. package/server/tests/test_mcp_bridge.py +293 -0
  169. package/server/tests/test_mcp_lock.py +34 -0
  170. package/server/tests/test_mcp_registry.py +679 -0
  171. package/server/tests/test_mcp_resources.py +648 -0
  172. package/server/tests/test_protocol.py +125 -0
  173. package/server/tests/test_state.py +87 -0
  174. package/server/tests/test_storage.py +306 -0
  175. package/server/tests/test_websocket.py +275 -0
  176. package/src/App.svelte +107 -0
  177. package/src/app.css +215 -0
  178. package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
  179. package/src/lib/animation/animation-commands.test.ts +228 -0
  180. package/src/lib/animation/animation-commands.ts +540 -0
  181. package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
  182. package/src/lib/animation/animation-preview.svelte.ts +151 -0
  183. package/src/lib/animation/clipboard.ts +134 -0
  184. package/src/lib/animation/frame-model.svelte.ts +437 -0
  185. package/src/lib/animation/frame-model.test.ts +314 -0
  186. package/src/lib/animation/frame-selection.svelte.ts +77 -0
  187. package/src/lib/animation/frame-tags.svelte.ts +238 -0
  188. package/src/lib/animation/frame-tags.test.ts +136 -0
  189. package/src/lib/animation/import.test.ts +141 -0
  190. package/src/lib/animation/import.ts +112 -0
  191. package/src/lib/animation/spritesheet-export.test.ts +239 -0
  192. package/src/lib/animation/spritesheet-export.ts +314 -0
  193. package/src/lib/canvas/CanvasViewport.svelte +216 -0
  194. package/src/lib/canvas/canvas-init-plugin.ts +178 -0
  195. package/src/lib/canvas/canvas-renderer.ts +408 -0
  196. package/src/lib/canvas/canvas-state.svelte.ts +232 -0
  197. package/src/lib/canvas/canvas-state.test.ts +139 -0
  198. package/src/lib/canvas/input-handler.ts +221 -0
  199. package/src/lib/canvas/input-plugin.ts +150 -0
  200. package/src/lib/canvas/onion-skin.ts +94 -0
  201. package/src/lib/canvas/pixel-buffer.test.ts +249 -0
  202. package/src/lib/canvas/pixel-buffer.ts +151 -0
  203. package/src/lib/canvas/render-state.svelte.ts +18 -0
  204. package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
  205. package/src/lib/canvas/tile-mode.test.ts +53 -0
  206. package/src/lib/canvas/tile-mode.ts +92 -0
  207. package/src/lib/canvas/viewport-utils.ts +24 -0
  208. package/src/lib/canvas/zoom-utils.ts +31 -0
  209. package/src/lib/color/.gitkeep +0 -0
  210. package/src/lib/color/color-commands.ts +87 -0
  211. package/src/lib/color/color-state.svelte.ts +98 -0
  212. package/src/lib/color/color-state.test.ts +91 -0
  213. package/src/lib/color/color-utils.test.ts +220 -0
  214. package/src/lib/color/color-utils.ts +243 -0
  215. package/src/lib/color/palette-state.svelte.ts +127 -0
  216. package/src/lib/color/palette-state.test.ts +154 -0
  217. package/src/lib/color/palette.ts +79 -0
  218. package/src/lib/core/bootstrap.ts +66 -0
  219. package/src/lib/core/command-params.generated.ts +1549 -0
  220. package/src/lib/core/command-params.ts +20 -0
  221. package/src/lib/core/command-runner.ts +79 -0
  222. package/src/lib/core/commands.ts +134 -0
  223. package/src/lib/core/dispatcher.test.ts +548 -0
  224. package/src/lib/core/dispatcher.ts +361 -0
  225. package/src/lib/core/index.test.ts +7 -0
  226. package/src/lib/core/notification-state.svelte.ts +119 -0
  227. package/src/lib/core/plugin-api.ts +210 -0
  228. package/src/lib/core/plugin-discovery.test.ts +53 -0
  229. package/src/lib/core/plugin-discovery.ts +65 -0
  230. package/src/lib/core/plugin-loader.test.ts +159 -0
  231. package/src/lib/core/plugin-loader.ts +240 -0
  232. package/src/lib/core/plugin-types.ts +286 -0
  233. package/src/lib/core/registries.svelte.ts +74 -0
  234. package/src/lib/core/tool-options-state.svelte.ts +61 -0
  235. package/src/lib/dock/DockLayout.svelte +375 -0
  236. package/src/lib/dock/TabAddMenu.svelte +90 -0
  237. package/src/lib/dock/dock-persistence.ts +46 -0
  238. package/src/lib/dock/dock-plugin.ts +49 -0
  239. package/src/lib/dock/dock-presets.ts +156 -0
  240. package/src/lib/dock/dock-state.svelte.ts +77 -0
  241. package/src/lib/dock/dock-types.ts +2 -0
  242. package/src/lib/dock/dockview-theme.css +226 -0
  243. package/src/lib/dock/dockview-theme.ts +17 -0
  244. package/src/lib/dock/header-action-renderer.ts +77 -0
  245. package/src/lib/edit/clipboard-state.ts +34 -0
  246. package/src/lib/export/download.ts +201 -0
  247. package/src/lib/export/png-metadata.ts +181 -0
  248. package/src/lib/history/ActionLogPanel.svelte +418 -0
  249. package/src/lib/history/action-log-panel-plugin.ts +24 -0
  250. package/src/lib/history/action-log.svelte.ts +172 -0
  251. package/src/lib/history/action-log.test.ts +168 -0
  252. package/src/lib/history/history-commands.ts +403 -0
  253. package/src/lib/history/macros.svelte.ts +320 -0
  254. package/src/lib/history/macros.test.ts +224 -0
  255. package/src/lib/history/replay-engine.test.ts +241 -0
  256. package/src/lib/history/replay-engine.ts +149 -0
  257. package/src/lib/history/spec-format.test.ts +250 -0
  258. package/src/lib/history/spec-format.ts +210 -0
  259. package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
  260. package/src/lib/iso/iso-math.test.ts +77 -0
  261. package/src/lib/iso/iso-math.ts +77 -0
  262. package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
  263. package/src/lib/iso/seam-checker.test.ts +126 -0
  264. package/src/lib/iso/seam-checker.ts +93 -0
  265. package/src/lib/iso/tessellation.ts +67 -0
  266. package/src/lib/layers/compositor.test.ts +193 -0
  267. package/src/lib/layers/compositor.ts +175 -0
  268. package/src/lib/layers/layer-commands.test.ts +263 -0
  269. package/src/lib/layers/layer-commands.ts +429 -0
  270. package/src/lib/layers/layer-tree.svelte.ts +516 -0
  271. package/src/lib/layers/layer-tree.test.ts +383 -0
  272. package/src/lib/layers/layer-types.ts +56 -0
  273. package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
  274. package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
  275. package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
  276. package/src/lib/leveleditor/depth-sort.test.ts +70 -0
  277. package/src/lib/leveleditor/depth-sort.ts +39 -0
  278. package/src/lib/leveleditor/level-editor-commands.ts +353 -0
  279. package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
  280. package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
  281. package/src/lib/leveleditor/map-state.svelte.ts +372 -0
  282. package/src/lib/leveleditor/map-state.test.ts +243 -0
  283. package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
  284. package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
  285. package/src/lib/leveleditor/tiled-export.test.ts +144 -0
  286. package/src/lib/leveleditor/tiled-export.ts +374 -0
  287. package/src/lib/pywebview.d.ts +15 -0
  288. package/src/lib/recovery/.gitkeep +0 -0
  289. package/src/lib/recovery/idb-store.ts +118 -0
  290. package/src/lib/recovery/recovery-manager.test.ts +321 -0
  291. package/src/lib/recovery/recovery-manager.ts +115 -0
  292. package/src/lib/recovery/recovery-plugin.ts +55 -0
  293. package/src/lib/recovery/recovery-state.svelte.ts +21 -0
  294. package/src/lib/recovery/sw-plugin.ts +18 -0
  295. package/src/lib/recovery/sw-register.ts +17 -0
  296. package/src/lib/save/directory-format.ts +42 -0
  297. package/src/lib/save/project-snapshot.ts +139 -0
  298. package/src/lib/save/recent-projects.ts +56 -0
  299. package/src/lib/save/save-state.svelte.ts +35 -0
  300. package/src/lib/save/storage.ts +167 -0
  301. package/src/lib/save/zip-format.ts +45 -0
  302. package/src/lib/shortcuts/.gitkeep +0 -0
  303. package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
  304. package/src/lib/shortcuts/default-bindings.ts +61 -0
  305. package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
  306. package/src/lib/shortcuts/shortcut-init.ts +380 -0
  307. package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
  308. package/src/lib/shortcuts/shortcut-manager.ts +281 -0
  309. package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
  310. package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
  311. package/src/lib/sync/patch-applicator.ts +300 -0
  312. package/src/lib/sync/patch-types.ts +65 -0
  313. package/src/lib/sync/project-manager.ts +108 -0
  314. package/src/lib/sync/sync-init.ts +152 -0
  315. package/src/lib/sync/sync-plugin.ts +19 -0
  316. package/src/lib/sync/sync-state.svelte.ts +56 -0
  317. package/src/lib/sync/ws-client.test.ts +604 -0
  318. package/src/lib/sync/ws-client.ts +574 -0
  319. package/src/lib/tools/.gitkeep +0 -0
  320. package/src/lib/ui/.gitkeep +0 -0
  321. package/src/lib/ui/AboutDialog.svelte +113 -0
  322. package/src/lib/ui/ColorPicker.svelte +761 -0
  323. package/src/lib/ui/ContextMenu.svelte +216 -0
  324. package/src/lib/ui/ExportDialog.svelte +747 -0
  325. package/src/lib/ui/FrameStrip.svelte +854 -0
  326. package/src/lib/ui/LayerPanel.svelte +810 -0
  327. package/src/lib/ui/MenuBar.svelte +590 -0
  328. package/src/lib/ui/NewProjectDialog.svelte +803 -0
  329. package/src/lib/ui/PluginManagerPanel.svelte +475 -0
  330. package/src/lib/ui/PromptDialog.svelte +252 -0
  331. package/src/lib/ui/RecoveryDialog.svelte +295 -0
  332. package/src/lib/ui/ResizeDialog.svelte +416 -0
  333. package/src/lib/ui/StatusBar.svelte +145 -0
  334. package/src/lib/ui/ToolbarPanel.svelte +488 -0
  335. package/src/lib/ui/animation-menu-commands.ts +194 -0
  336. package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
  337. package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
  338. package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
  339. package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
  340. package/src/lib/ui/dialog-state.svelte.ts +70 -0
  341. package/src/lib/ui/edit-commands.ts +271 -0
  342. package/src/lib/ui/file-commands.ts +275 -0
  343. package/src/lib/ui/file-open.ts +99 -0
  344. package/src/lib/ui/help-commands.ts +93 -0
  345. package/src/lib/ui/image-commands.ts +181 -0
  346. package/src/lib/ui/layer-menu-commands.ts +420 -0
  347. package/src/lib/ui/menu-builder.ts +224 -0
  348. package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
  349. package/src/lib/ui/notifications/notification-plugin.ts +29 -0
  350. package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
  351. package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
  352. package/src/lib/ui/plugin-state.svelte.ts +62 -0
  353. package/src/lib/ui/select-commands.ts +75 -0
  354. package/src/lib/ui/theme-plugin.ts +18 -0
  355. package/src/lib/ui/theme.svelte.ts +45 -0
  356. package/src/lib/ui/theme.test.ts +51 -0
  357. package/src/lib/ui/toolbar-config.ts +90 -0
  358. package/src/lib/ui/toolbar-plugin.ts +39 -0
  359. package/src/lib/ui/view-commands.ts +629 -0
  360. package/src/lib/variants/BisectionExportDialog.svelte +500 -0
  361. package/src/lib/variants/VariantPanel.svelte +822 -0
  362. package/src/lib/variants/bisection-export.test.ts +113 -0
  363. package/src/lib/variants/bisection-export.ts +148 -0
  364. package/src/lib/variants/palette-extraction.test.ts +111 -0
  365. package/src/lib/variants/palette-extraction.ts +84 -0
  366. package/src/lib/variants/palette-interpolation.test.ts +113 -0
  367. package/src/lib/variants/palette-interpolation.ts +87 -0
  368. package/src/lib/variants/palette-swap.test.ts +101 -0
  369. package/src/lib/variants/palette-swap.ts +114 -0
  370. package/src/lib/variants/variant-commands.ts +594 -0
  371. package/src/lib/variants/variant-panel-plugin.ts +27 -0
  372. package/src/lib/variants/variant-randomizer.ts +101 -0
  373. package/src/lib/variants/variant-state.svelte.ts +166 -0
  374. package/src/lib/variants/variant-state.test.ts +138 -0
  375. package/src/main.ts +14 -0
  376. package/src/vite-env.d.ts +3 -0
  377. package/svelte.config.js +2 -0
  378. package/todo/.done/audit-design-decisions.md +812 -0
  379. package/todo/.done/audit-implementation-plan.md +1235 -0
  380. package/todo/.done/happy-path-polish.md +177 -0
  381. package/todo/.done/pixelweaver-full-build.md +937 -0
  382. package/todo/.done/server-multi-frame-design.md +405 -0
  383. package/todo/.done/typed-dispatcher-design.md +435 -0
  384. package/todo/.done/unified-toolbar-and-action-system.md +323 -0
  385. package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
  386. package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
  387. package/todo/comprehensive-audit.md +1085 -0
  388. package/tsconfig.app.json +26 -0
  389. package/tsconfig.json +7 -0
  390. package/tsconfig.node.json +20 -0
  391. package/uv.lock +1167 -0
  392. package/vite.config.ts +32 -0
@@ -0,0 +1,199 @@
1
+ """PixelWeaver CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import strictcli
10
+ from strictcli import App
11
+
12
+ _DEFAULT_PID = Path(".pixelweaver.pid")
13
+
14
+
15
+ def _get_version() -> str:
16
+ from importlib.metadata import version
17
+ return version("pixelweaver")
18
+
19
+
20
+ def _pkg_version(package_name: str, module_name: str | None = None) -> str:
21
+ """Get package version, trying module attr first then importlib.metadata."""
22
+ if module_name:
23
+ try:
24
+ mod = __import__(module_name)
25
+ ver = getattr(mod, "__version__", None)
26
+ if ver:
27
+ return ver
28
+ except ImportError:
29
+ pass
30
+ try:
31
+ from importlib.metadata import version
32
+ return version(package_name)
33
+ except Exception:
34
+ return "NOT FOUND"
35
+
36
+
37
+ app = App(
38
+ name="pixelweaver",
39
+ help="PixelWeaver collaboration server",
40
+ version=_get_version(),
41
+ config=True,
42
+ )
43
+
44
+ # Reusable tag for --data-dir flag (shared by multiple commands)
45
+ _data_dir_tag = strictcli.Tag(name="data", flags=[
46
+ strictcli.Flag(
47
+ name="data-dir",
48
+ type=str,
49
+ help="Projects directory",
50
+ default="./projects",
51
+ ),
52
+ ])
53
+
54
+
55
+ @app.command("serve", help="Start the server", tags=[_data_dir_tag])
56
+ @strictcli.flag("host", type=str, help="Bind address", default="127.0.0.1")
57
+ @strictcli.flag("port", type=int, help="Port", default=7779)
58
+ @strictcli.flag("reload", type=bool, help="Auto-reload on file changes")
59
+ def cmd_serve(*, host: str, port: int, data_dir: str, reload: bool, **_kw: object) -> None:
60
+ from pixelweaver.config import set_data_dir
61
+ from wesktop import serve
62
+ set_data_dir(data_dir)
63
+ serve("pixelweaver.main:app", foreground=True, host=host, port=port, pid_path=_DEFAULT_PID, reload=reload, name="PIXELWEAVER")
64
+
65
+
66
+ @app.command(
67
+ "new",
68
+ help="Create a new project on disk",
69
+ args=[strictcli.Arg(name="name", help="Project name")],
70
+ tags=[_data_dir_tag],
71
+ )
72
+ @strictcli.flag("width", type=int, help="Canvas width in pixels")
73
+ @strictcli.flag("height", type=int, help="Canvas height in pixels")
74
+ def cmd_new(name: str, *, width: int, height: int, data_dir: str, **_kw: object) -> None:
75
+ from pixelweaver.state import ServerState
76
+ from pixelweaver.storage import save_project
77
+ state = ServerState()
78
+ project = state.create_project(name, width, height)
79
+ save_project(project, data_dir)
80
+ print(f"Created project '{name}' ({width}x{height}) in {data_dir}/")
81
+
82
+
83
+ @app.command("list", help="List projects on disk", tags=[_data_dir_tag])
84
+ def cmd_list(*, data_dir: str, **_kw: object) -> None:
85
+ from pixelweaver.storage import list_projects
86
+ projects = list_projects(data_dir)
87
+ if not projects:
88
+ print("No projects found.")
89
+ else:
90
+ for name in projects:
91
+ print(f" {name}")
92
+
93
+
94
+ @app.command("mcp", help="Start the MCP tool server (stdio)", tags=[_data_dir_tag])
95
+ def cmd_mcp(*, data_dir: str, **_kw: object) -> None:
96
+ from pixelweaver.config import set_data_dir
97
+ from pixelweaver.mcp_server import run_mcp_server
98
+ set_data_dir(data_dir)
99
+ run_mcp_server()
100
+
101
+
102
+ @app.command("dev", help="Development mode with Vite HMR", tags=[_data_dir_tag])
103
+ @strictcli.flag("host", type=str, help="Bind address", default="127.0.0.1")
104
+ @strictcli.flag("port", type=int, help="Port", default=7779)
105
+ @strictcli.flag("vite-port", type=int, help="Vite dev server port", default=5173)
106
+ def cmd_dev(*, host: str, port: int, data_dir: str, vite_port: int, **_kw: object) -> None:
107
+ from pixelweaver.config import set_data_dir
108
+ from wesktop import dev
109
+ set_data_dir(data_dir)
110
+ dev(
111
+ "pixelweaver.main:app",
112
+ vite_command="npm run dev",
113
+ vite_port=vite_port,
114
+ host=host,
115
+ port=port,
116
+ pid_path=_DEFAULT_PID,
117
+ name="PIXELWEAVER",
118
+ )
119
+
120
+
121
+ @app.command("open", help="Launch desktop window", tags=[_data_dir_tag])
122
+ @strictcli.flag("host", type=str, help="Bind address", default="127.0.0.1")
123
+ @strictcli.flag("port", type=int, help="Port", default=7779)
124
+ @strictcli.flag("browser", type=bool, help="Open in browser instead of native window")
125
+ def cmd_open(*, host: str, port: int, data_dir: str, browser: bool, **_kw: object) -> None:
126
+ from pixelweaver.config import set_data_dir
127
+ set_data_dir(data_dir)
128
+
129
+ if browser:
130
+ import threading
131
+ import webbrowser
132
+ from wesktop import serve
133
+ url = serve(
134
+ "pixelweaver.main:app",
135
+ foreground=False,
136
+ host=host,
137
+ port=port,
138
+ pid_path=_DEFAULT_PID,
139
+ name="PIXELWEAVER",
140
+ )
141
+ webbrowser.open(url)
142
+ try:
143
+ threading.Event().wait()
144
+ except KeyboardInterrupt:
145
+ pass
146
+ else:
147
+ from pixelweaver.bridge import DesktopBridge
148
+ from wesktop import run
149
+ run(
150
+ "pixelweaver.main:app",
151
+ title="PixelWeaver",
152
+ width=1280,
153
+ height=800,
154
+ host=host,
155
+ port=port,
156
+ pid_path=_DEFAULT_PID,
157
+ icon=str(Path("assets/icon.png").resolve()),
158
+ js_api=DesktopBridge(),
159
+ name="PIXELWEAVER",
160
+ )
161
+
162
+
163
+ @app.command("diagnose", help="Check runtime environment and dependencies")
164
+ def cmd_diagnose(**_kw: object) -> None:
165
+ rows: list[tuple[str, str]] = []
166
+ rows.append(("Python", f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"))
167
+ rows.append(("pixelweaver", _get_version()))
168
+
169
+ for label, pkg, mod in [
170
+ ("wesktop", "wesktop", "wesktop"),
171
+ ("pywebview", "pywebview", "webview"),
172
+ ("pydantic", "pydantic", "pydantic"),
173
+ ("pillow", "pillow", "PIL"),
174
+ ("mcp", "mcp", "mcp"),
175
+ ]:
176
+ ver = _pkg_version(pkg, mod)
177
+ suffix = " (ok)" if ver != "NOT FOUND" else ""
178
+ rows.append((label, f"{ver}{suffix}"))
179
+
180
+ rows.append(("platform", f"{platform.system()} {platform.machine()}"))
181
+ rows.append(("config", app.config_file_path))
182
+
183
+ label_width = max(len(label) for label, _ in rows)
184
+ for label, value in rows:
185
+ print(f" {label:<{label_width}} {value}")
186
+
187
+
188
+ @app.command("stop", help="Stop the running server")
189
+ def cmd_stop(**_kw: object) -> None:
190
+ from wesktop import stop
191
+ if not _DEFAULT_PID.exists():
192
+ print(f"No PID file found at {_DEFAULT_PID}")
193
+ return
194
+ stop(_DEFAULT_PID)
195
+ print("Server stopped.")
196
+
197
+
198
+ def main() -> None:
199
+ app.run()
@@ -0,0 +1,24 @@
1
+ """Process-wide configuration for PixelWeaver server.
2
+
3
+ Single source of truth for settings that are set once at startup (e.g. from
4
+ CLI arguments) and then read throughout the application. Both the HTTP
5
+ server (main.py) and the MCP server (mcp_server.py) share this module so
6
+ they never drift out of sync.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # Default matches the CLI's DEFAULT_DATA_DIR; overridden via set_data_dir()
12
+ # before the server app starts.
13
+ _data_dir: str = "./projects"
14
+
15
+
16
+ def get_data_dir() -> str:
17
+ """Return the currently configured projects data directory."""
18
+ return _data_dir
19
+
20
+
21
+ def set_data_dir(data_dir: str) -> None:
22
+ """Set the projects data directory (called from CLI before app starts)."""
23
+ global _data_dir
24
+ _data_dir = data_dir
@@ -0,0 +1,54 @@
1
+ """WebSocket connection manager for PixelWeaver.
2
+
3
+ Tracks active connections and provides helpers for sending messages
4
+ to individual clients or broadcasting to all/most clients.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from wesktop import WebSocket
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ConnectionManager:
18
+ """Manages active WebSocket connections."""
19
+
20
+ def __init__(self) -> None:
21
+ self.active_connections: list[WebSocket] = []
22
+
23
+ async def connect(self, websocket: WebSocket) -> None:
24
+ """Accept and register a new WebSocket connection."""
25
+ await websocket.accept()
26
+ self.active_connections.append(websocket)
27
+ logger.info("Client connected (%d total)", len(self.active_connections))
28
+
29
+ def disconnect(self, websocket: WebSocket) -> None:
30
+ """Unregister a WebSocket connection."""
31
+ try:
32
+ self.active_connections.remove(websocket)
33
+ except ValueError:
34
+ pass # Already removed
35
+ logger.info("Client disconnected (%d remaining)", len(self.active_connections))
36
+
37
+ async def broadcast(
38
+ self,
39
+ message: dict[str, Any],
40
+ exclude: WebSocket | None = None,
41
+ ) -> None:
42
+ """Send a JSON message to all connected clients, optionally excluding one."""
43
+ # Snapshot the list so disconnects triggered by send failures (via
44
+ # exception handlers mutating active_connections) don't corrupt iteration.
45
+ for connection in list(self.active_connections):
46
+ if connection is not exclude:
47
+ try:
48
+ await connection.send_json(message)
49
+ except Exception:
50
+ logger.warning("Failed to send broadcast to a client", exc_info=True)
51
+
52
+ async def send(self, websocket: WebSocket, message: dict[str, Any]) -> None:
53
+ """Send a JSON message to a single client."""
54
+ await websocket.send_json(message)
@@ -0,0 +1,271 @@
1
+ """wesktop application for PixelWeaver collaboration server.
2
+
3
+ Provides REST endpoints for project management and a WebSocket endpoint
4
+ for the real-time server-authoritative protocol.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ from contextlib import asynccontextmanager
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from wesktop import (
17
+ AppConfig,
18
+ BytesResponse,
19
+ JSONResponse,
20
+ Request,
21
+ Router,
22
+ TextResponse,
23
+ WebSocket,
24
+ create_app,
25
+ )
26
+ from wesktop.asgi import HTTPError
27
+ from pydantic import BaseModel, Field, field_validator
28
+
29
+ from pixelweaver.autosave import AutoSaver
30
+ from pixelweaver.config import get_data_dir
31
+ from pixelweaver.connections import ConnectionManager
32
+ from pixelweaver.mcp_bridge import deserialize_into_state, serialize_state
33
+ from pixelweaver.state import ServerState
34
+ from pixelweaver.storage import export_frame_png, list_projects, load_project, save_project
35
+ from pixelweaver.websocket import broadcast_full_state, handle_message
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Module-level singletons -- initialized before the app starts
40
+ state = ServerState()
41
+ manager = ConnectionManager()
42
+
43
+ _auto_saver: AutoSaver | None = None
44
+
45
+ router = Router()
46
+
47
+
48
+ @asynccontextmanager
49
+ async def lifespan(scope):
50
+ """Startup/shutdown lifecycle: load projects from disk, start auto-saver."""
51
+ global _auto_saver
52
+
53
+ data_dir = get_data_dir()
54
+
55
+ # Load existing projects from disk
56
+ for name in list_projects(data_dir):
57
+ try:
58
+ project_dir = f"{data_dir}/{name}"
59
+ project = load_project(project_dir)
60
+ state.projects[project.name] = project
61
+ if state.active_project is None:
62
+ state.active_project = project.name
63
+ logger.info("Loaded project: %s", name)
64
+ except Exception:
65
+ logger.error("Failed to load project: %s", name, exc_info=True)
66
+
67
+ # Start auto-saver
68
+ _auto_saver = AutoSaver(state, data_dir)
69
+ await _auto_saver.start()
70
+
71
+ yield {}
72
+
73
+ # Shutdown: flush pending saves
74
+ if _auto_saver is not None:
75
+ await _auto_saver.stop()
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Health
80
+ # ---------------------------------------------------------------------------
81
+
82
+
83
+ @router.get("/health")
84
+ async def health_check(req: Request):
85
+ """Health check endpoint."""
86
+ return {"status": "ok"}
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Project management REST API
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ class CreateProjectRequest(BaseModel):
95
+ name: str
96
+ width: int = Field(ge=1, le=4096)
97
+ height: int = Field(ge=1, le=4096)
98
+
99
+ @field_validator("name")
100
+ @classmethod
101
+ def name_must_be_safe(cls, v: str) -> str:
102
+ """Reject names that could escape the project data directory."""
103
+ import re
104
+
105
+ if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9 _.\-]{0,63}$", v):
106
+ raise ValueError(
107
+ "Project name must be 1-64 characters, start with an alphanumeric, "
108
+ "and contain only alphanumerics, spaces, underscores, hyphens, or dots."
109
+ )
110
+ return v
111
+
112
+
113
+ @router.get("/api/projects")
114
+ async def list_all_projects(req: Request):
115
+ """List all projects."""
116
+ return {"projects": state.list_projects()}
117
+
118
+
119
+ @router.post("/api/projects")
120
+ async def create_project(req: Request):
121
+ """Create a new project."""
122
+ data = req.json_as(CreateProjectRequest)
123
+
124
+ if data.name in state.projects:
125
+ return JSONResponse({"detail": f"Project {data.name!r} already exists"}, status=409)
126
+
127
+ project = state.create_project(data.name, data.width, data.height)
128
+
129
+ # Persist immediately
130
+ await asyncio.to_thread(save_project, project, get_data_dir())
131
+
132
+ return JSONResponse(
133
+ {"name": project.name, "width": project.width, "height": project.height},
134
+ status=201,
135
+ )
136
+
137
+
138
+ @router.get("/api/projects/{name}")
139
+ async def get_project(req: Request):
140
+ """Get project metadata."""
141
+ name = req.path_params["name"]
142
+ project = state.get_project(name)
143
+ if project is None:
144
+ raise HTTPError(404, f"Project {name!r} not found")
145
+ return project.to_dict()
146
+
147
+
148
+ @router.delete("/api/projects/{name}")
149
+ async def delete_project(req: Request):
150
+ """Delete a project from memory (does not delete files on disk)."""
151
+ name = req.path_params["name"]
152
+ if not state.delete_project(name):
153
+ raise HTTPError(404, f"Project {name!r} not found")
154
+ return TextResponse("", status=204)
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Export
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ @router.get("/api/projects/{name}/export/png/{canvas}/{frame:int}")
163
+ async def export_png(req: Request):
164
+ """Export a frame as a composited PNG."""
165
+ name = req.path_params["name"]
166
+ canvas = req.path_params["canvas"]
167
+ frame = req.path_params["frame"]
168
+ project = state.get_project(name)
169
+ if project is None:
170
+ raise HTTPError(404, f"Project {name!r} not found")
171
+
172
+ try:
173
+ png_bytes = await asyncio.to_thread(export_frame_png, project, canvas, frame)
174
+ except ValueError as exc:
175
+ raise HTTPError(404, str(exc))
176
+
177
+ return BytesResponse(png_bytes, content_type="image/png")
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # MCP state sync (used by the MCP bridge to round-trip state)
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ @router.get("/api/state/full")
186
+ async def get_full_state(req: Request):
187
+ """Return the full serialized server state for MCP sync.
188
+
189
+ The MCP server calls this before executing a tool so it operates on
190
+ the latest state held by the collab server.
191
+ """
192
+ return serialize_state(state)
193
+
194
+
195
+ @router.post("/api/state/sync")
196
+ async def sync_state_from_mcp(req: Request):
197
+ """Accept full state from the MCP server and broadcast changes.
198
+
199
+ After the MCP server executes a tool, it pushes the modified state
200
+ here. The collab server overwrites its in-memory state and broadcasts
201
+ a full-state patch to every connected WebSocket client so the frontend
202
+ sees the mutation.
203
+ """
204
+ payload: dict[str, Any] = req.json
205
+
206
+ async with state.mutation_lock:
207
+ deserialize_into_state(state, payload)
208
+
209
+ # Broadcast full-state to all WebSocket clients for every project
210
+ # that has changed. In practice MCP works on the active project,
211
+ # so broadcasting that one is sufficient.
212
+ active = state.active_project
213
+ if active and active in state.projects:
214
+ await broadcast_full_state(state, manager, project_name=active)
215
+
216
+ # Mark dirty so the auto-saver persists the change
217
+ if _auto_saver is not None:
218
+ _auto_saver.mark_dirty()
219
+
220
+ return {"success": True}
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # WebSocket
225
+ # ---------------------------------------------------------------------------
226
+
227
+
228
+ def _on_dirty() -> None:
229
+ """Callback passed to message handler to mark state as dirty."""
230
+ if _auto_saver is not None:
231
+ _auto_saver.mark_dirty()
232
+
233
+
234
+ @router.ws("/ws")
235
+ async def websocket_endpoint(ws: WebSocket):
236
+ """WebSocket endpoint for the server-authoritative protocol."""
237
+ await manager.connect(ws)
238
+ try:
239
+ while True:
240
+ msg = await ws.receive_raw()
241
+ if msg.get("type") == "websocket.disconnect":
242
+ break
243
+ raw: dict[str, Any] = json.loads(msg.get("text", ""))
244
+ await handle_message(ws, raw, state, manager, on_dirty=_on_dirty)
245
+ except Exception:
246
+ logger.error("WebSocket error", exc_info=True)
247
+ try:
248
+ await ws.close(code=1011)
249
+ except Exception:
250
+ pass
251
+ finally:
252
+ manager.disconnect(ws)
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # App creation
257
+ # ---------------------------------------------------------------------------
258
+
259
+ _dist = Path("dist")
260
+ _spa = _dist / "index.html"
261
+
262
+ app = create_app(
263
+ router,
264
+ AppConfig(
265
+ lifespan=lifespan,
266
+ cors_origins=["*"],
267
+ static_dir=_dist if _spa.is_file() else None,
268
+ spa_fallback=_spa if _spa.is_file() else None,
269
+ api_prefix="/api",
270
+ ),
271
+ )