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,167 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fix @typescript-eslint/no-confusing-void-expression errors by converting
4
+ arrow functions with expression bodies into block bodies:
5
+ () => doThing() -> () => { doThing(); }
6
+
7
+ Usage:
8
+ ./scripts/fix-wave-b-void-expr.py <path> [<path>...]
9
+
10
+ Strategy:
11
+ 1. Run `npx eslint <paths> -f json` to discover error locations.
12
+ 2. For each flagged (line, column), find the nearest `=>` token strictly
13
+ before that column on the same line.
14
+ 3. Locate the end of the expression after `=>`. For an expression body,
15
+ we walk forward tracking paren/bracket/brace depth and string quoting
16
+ until we hit a terminator: `,` `)` `]` `}` `;` or end-of-line.
17
+ 4. Rewrite `=> EXPR` to `=> { EXPR; }`. (Skip if already a block.)
18
+ """
19
+ import json
20
+ import subprocess
21
+ import sys
22
+ from collections import defaultdict
23
+
24
+
25
+ def run_eslint(paths: list[str]) -> list:
26
+ res = subprocess.run(
27
+ ['npx', 'eslint', *paths, '-f', 'json'],
28
+ capture_output=True, text=True
29
+ )
30
+ return json.loads(res.stdout)
31
+
32
+
33
+ def collect_errors(eslint_data) -> dict:
34
+ by_file = defaultdict(list)
35
+ for f in eslint_data:
36
+ for m in f['messages']:
37
+ if m.get('severity') != 2: continue
38
+ if m.get('ruleId') != '@typescript-eslint/no-confusing-void-expression': continue
39
+ by_file[f['filePath']].append((m['line'], m['column']))
40
+ return by_file
41
+
42
+
43
+ def find_arrow_start(line: str, col_zero: int) -> int:
44
+ """Find the index of '=>' strictly before col_zero. Return index of '=' or -1."""
45
+ # Search backward for '=>'
46
+ for k in range(col_zero - 1, 0, -1):
47
+ if line[k] == '>' and line[k-1] == '=':
48
+ return k - 1
49
+ return -1
50
+
51
+
52
+ def find_expr_end(line: str, start: int) -> int:
53
+ """Walk forward from `start` until the expression terminates.
54
+ Return the index *after* the last char of the expression.
55
+ Terminators at depth 0: `,` `)` `]` `}` `;`.
56
+ Strings, template literals, and regex are not tracked deeply — we just
57
+ track the common cases (quotes and template backticks).
58
+ """
59
+ n = len(line)
60
+ depth = 0
61
+ i = start
62
+ in_str = None # None or quote char
63
+ while i < n:
64
+ c = line[i]
65
+ if in_str is not None:
66
+ if c == '\\':
67
+ i += 2
68
+ continue
69
+ if c == in_str:
70
+ in_str = None
71
+ elif in_str == '`' and c == '$' and i+1 < n and line[i+1] == '{':
72
+ # template expression: track as extra { }
73
+ depth += 1
74
+ i += 2
75
+ # switch out of string mode temporarily
76
+ in_str = None
77
+ # Push template marker — we approximate: once depth returns
78
+ # to the saved level, we flip back to backtick mode. To keep
79
+ # the script simple we use a side stack.
80
+ tpl_stack.append(depth)
81
+ continue
82
+ i += 1
83
+ continue
84
+ if c in ('"', "'", '`'):
85
+ in_str = c
86
+ i += 1
87
+ continue
88
+ if c == '(' or c == '[' or c == '{':
89
+ depth += 1
90
+ i += 1
91
+ continue
92
+ if c == ')' or c == ']' or c == '}':
93
+ if depth == 0:
94
+ return i
95
+ depth -= 1
96
+ # check template stack: if we closed a template ${...} block,
97
+ # resume backtick string mode
98
+ if tpl_stack and depth == tpl_stack[-1] - 1:
99
+ tpl_stack.pop()
100
+ in_str = '`'
101
+ i += 1
102
+ continue
103
+ if depth == 0 and (c == ',' or c == ';'):
104
+ return i
105
+ i += 1
106
+ return n
107
+
108
+
109
+ def fix_file(path: str, locations: list[tuple[int, int]]) -> int:
110
+ with open(path, 'r') as f:
111
+ lines = f.readlines()
112
+ # Process bottom-to-top so indices stay valid.
113
+ uniq = sorted(set(locations), key=lambda lc: (-lc[0], -lc[1]))
114
+ fixed = 0
115
+ for line_no, col in uniq:
116
+ idx = line_no - 1
117
+ if idx >= len(lines): continue
118
+ line = lines[idx]
119
+ col0 = col - 1
120
+ arrow_eq = find_arrow_start(line, col0)
121
+ if arrow_eq < 0:
122
+ continue
123
+ # Position just after '=>'
124
+ after = arrow_eq + 2
125
+ # Skip whitespace
126
+ while after < len(line) and line[after] == ' ':
127
+ after += 1
128
+ if after >= len(line):
129
+ continue
130
+ if line[after] == '{':
131
+ continue # already a block
132
+ global tpl_stack
133
+ tpl_stack = []
134
+ end = find_expr_end(line, after)
135
+ expr = line[after:end].rstrip()
136
+ if not expr:
137
+ continue
138
+ # Rebuild
139
+ before_arrow = line[:after]
140
+ trailer = line[end:]
141
+ # Ensure one space between `=>` and `{`
142
+ # `before_arrow` ends with space(s) usually since we advanced past them.
143
+ # Strip trailing spaces to normalize.
144
+ before_arrow = before_arrow.rstrip(' ')
145
+ new_line = before_arrow + ' { ' + expr + '; }' + trailer
146
+ lines[idx] = new_line
147
+ fixed += 1
148
+ if fixed > 0:
149
+ with open(path, 'w') as f:
150
+ f.writelines(lines)
151
+ return fixed
152
+
153
+
154
+ if __name__ == '__main__':
155
+ if len(sys.argv) < 2:
156
+ print("Usage: fix-wave-b-void-expr.py <path> [<path>...]", file=sys.stderr)
157
+ sys.exit(2)
158
+ paths = sys.argv[1:]
159
+ data = run_eslint(paths)
160
+ by_file = collect_errors(data)
161
+ total = 0
162
+ for fpath, locs in by_file.items():
163
+ n = fix_file(fpath, locs)
164
+ if n > 0:
165
+ print(f'{fpath}: {n} fixes')
166
+ total += n
167
+ print(f'TOTAL: {total}')
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-generate TypeScript interfaces from addCommand() param casts.
4
+
5
+ Scans all .ts files under src/ and plugins/ for api.addCommand(...) calls,
6
+ extracts params["key"] as Type patterns from execute/undo/describe bodies,
7
+ and generates:
8
+ - A named interface per command (e.g. FloodFillParams)
9
+ - A declare global { interface CommandParamsMap { ... } } augmentation
10
+
11
+ Usage:
12
+ python scripts/generate-command-params.py # full output to stdout
13
+ python scripts/generate-command-params.py --dry-run # list found commands
14
+ python scripts/generate-command-params.py --root /path/to/project
15
+ python scripts/generate-command-params.py > src/lib/core/command-params.generated.ts
16
+ """
17
+
18
+ import argparse
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def snake_to_pascal(name: str) -> str:
29
+ """Convert snake_case command id to PascalCase + 'Params'."""
30
+ return "".join(part.capitalize() for part in name.split("_")) + "Params"
31
+
32
+
33
+ def find_ts_files(root: Path) -> list[Path]:
34
+ """Collect all .ts files under src/ and plugins/."""
35
+ dirs = [root / "src", root / "plugins"]
36
+ files: list[Path] = []
37
+ for d in dirs:
38
+ if d.is_dir():
39
+ files.extend(sorted(d.rglob("*.ts")))
40
+ return files
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Brace-matching: extract the full object literal passed to addCommand()
45
+ # ---------------------------------------------------------------------------
46
+
47
+ def extract_add_command_blocks(source: str) -> list[tuple[str, str, int]]:
48
+ """
49
+ Find all addCommand('name', { ... }) calls in source.
50
+
51
+ Returns a list of (command_name, body_text, start_line) tuples.
52
+ The body_text is everything inside the outermost { ... } of the second
53
+ argument (the command definition object).
54
+
55
+ Uses brace counting to handle nested objects, arrow functions, template
56
+ literals, and string literals.
57
+ """
58
+ results: list[tuple[str, str, int]] = []
59
+
60
+ # Match the start of an addCommand call with a string-literal first arg
61
+ pattern = re.compile(
62
+ r"""addCommand\(\s*(['"`])(\w+)\1\s*,\s*\{""",
63
+ )
64
+
65
+ for m in pattern.finditer(source):
66
+ cmd_name = m.group(2)
67
+ # Start counting braces from the opening { we just matched
68
+ brace_start = m.end() - 1 # index of the '{'
69
+ start_line = source[:brace_start].count("\n") + 1
70
+
71
+ depth = 0
72
+ i = brace_start
73
+ in_string: str | None = None # tracks quote char of current string
74
+ in_template = False
75
+ prev_char = ""
76
+
77
+ while i < len(source):
78
+ ch = source[i]
79
+
80
+ # Handle escape sequences inside strings
81
+ if prev_char == "\\":
82
+ prev_char = ""
83
+ i += 1
84
+ continue
85
+
86
+ # String/template literal tracking
87
+ if in_string is not None:
88
+ if ch == in_string:
89
+ in_string = None
90
+ elif in_template:
91
+ if ch == "`" and prev_char != "\\":
92
+ in_template = False
93
+ elif ch in ("'", '"'):
94
+ in_string = ch
95
+ elif ch == "`":
96
+ in_template = True
97
+ elif ch == "/" and i + 1 < len(source):
98
+ next_ch = source[i + 1]
99
+ if next_ch == "/":
100
+ # Line comment -- skip to end of line
101
+ nl = source.find("\n", i)
102
+ i = nl if nl != -1 else len(source)
103
+ prev_char = ""
104
+ continue
105
+ elif next_ch == "*":
106
+ # Block comment -- skip to */
107
+ end = source.find("*/", i + 2)
108
+ i = end + 2 if end != -1 else len(source)
109
+ prev_char = ""
110
+ continue
111
+ elif ch == "{":
112
+ depth += 1
113
+ elif ch == "}":
114
+ depth -= 1
115
+ if depth == 0:
116
+ body = source[brace_start + 1 : i]
117
+ results.append((cmd_name, body, start_line))
118
+ break
119
+
120
+ prev_char = ch
121
+ i += 1
122
+
123
+ return results
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Extract params["key"] as Type patterns
128
+ # ---------------------------------------------------------------------------
129
+
130
+ # Pattern: params["key"] as Type
131
+ # Handles:
132
+ # params["key"] as Type
133
+ # (params["key"] as Type | undefined) ?? default
134
+ # (params["key"] ?? default) as Type
135
+ # params["key"] as Type | undefined
136
+ # Also matches params['key'] (single quotes)
137
+ PARAM_PATTERN = re.compile(
138
+ r"""params\[(['"])(\w+)\1\]"""
139
+ )
140
+
141
+ # After finding a params["key"], look for 'as Type' in the surrounding expression
142
+ # to determine the TypeScript type and optionality.
143
+ AS_CAST_PATTERN = re.compile(
144
+ r"""as\s+([\w\[\]{}|() '<>:,]+?)(?:\s*[;)\]}]|\s*$)"""
145
+ )
146
+
147
+
148
+ def extract_as_type(text: str) -> str | None:
149
+ """
150
+ Extract the type from an 'as Type' cast in the given text.
151
+
152
+ Handles balanced angle brackets so types like Record<string, string>
153
+ are captured fully. Returns None if no 'as' cast is found.
154
+ """
155
+ as_match = re.search(r"\bas\s+", text)
156
+ if not as_match:
157
+ return None
158
+
159
+ # Start reading the type after 'as '
160
+ i = as_match.end()
161
+ result: list[str] = []
162
+ angle_depth = 0 # track nested < >
163
+ paren_depth = 0 # track nested ( )
164
+
165
+ bracket_depth = 0 # track nested [ ]
166
+ curly_depth = 0 # track nested { } (for inline object types)
167
+
168
+ def at_top_level() -> bool:
169
+ """True when not inside any nested brackets."""
170
+ return angle_depth == 0 and paren_depth == 0 and bracket_depth == 0 and curly_depth == 0
171
+
172
+ while i < len(text):
173
+ ch = text[i]
174
+
175
+ if ch == "<":
176
+ angle_depth += 1
177
+ result.append(ch)
178
+ elif ch == ">":
179
+ if angle_depth > 0:
180
+ angle_depth -= 1
181
+ result.append(ch)
182
+ else:
183
+ break
184
+ elif ch == "(":
185
+ paren_depth += 1
186
+ result.append(ch)
187
+ elif ch == ")":
188
+ if paren_depth > 0:
189
+ paren_depth -= 1
190
+ result.append(ch)
191
+ else:
192
+ break
193
+ elif ch == "[":
194
+ bracket_depth += 1
195
+ result.append(ch)
196
+ elif ch == "]":
197
+ if bracket_depth > 0:
198
+ bracket_depth -= 1
199
+ result.append(ch)
200
+ else:
201
+ break
202
+ elif ch == "{":
203
+ curly_depth += 1
204
+ result.append(ch)
205
+ elif ch == "}":
206
+ if curly_depth > 0:
207
+ curly_depth -= 1
208
+ result.append(ch)
209
+ else:
210
+ break
211
+ elif ch == ";" and at_top_level():
212
+ break
213
+ elif ch == "," and at_top_level():
214
+ # Comma outside any brackets = end of this expression
215
+ break
216
+ elif ch == "\n" and at_top_level():
217
+ # Newline may indicate the end of the expression, but only if
218
+ # we already have content (types can span lines inside brackets)
219
+ if result and result[-1].strip():
220
+ break
221
+ else:
222
+ result.append(ch)
223
+
224
+ i += 1
225
+
226
+ return "".join(result).strip() or None
227
+
228
+
229
+ def find_top_level_semicolon(text: str, start: int) -> int:
230
+ """
231
+ Find the next semicolon in text that is NOT inside curly braces,
232
+ square brackets, angle brackets, or parentheses.
233
+
234
+ Returns the index of the semicolon, or len(text) if not found.
235
+ """
236
+ depth = {"(": 0, "[": 0, "{": 0, "<": 0}
237
+ openers = {"(", "[", "{", "<"}
238
+ closers = {")": "(", "]": "[", "}": "{", ">": "<"}
239
+
240
+ i = start
241
+ while i < len(text):
242
+ ch = text[i]
243
+ if ch in openers:
244
+ depth[ch] += 1
245
+ elif ch in closers:
246
+ opener = closers[ch]
247
+ if depth[opener] > 0:
248
+ depth[opener] -= 1
249
+ elif ch == ";" and all(v == 0 for v in depth.values()):
250
+ return i
251
+ i += 1
252
+
253
+ return len(text)
254
+
255
+
256
+ def extract_params_from_body(body: str) -> dict[str, dict[str, str | bool]]:
257
+ """
258
+ Extract all params["key"] patterns and their inferred types from a
259
+ command definition body.
260
+
261
+ Returns a dict mapping param_name -> { type, optional }.
262
+ Skips internal keys (starting with '_') which are used for stashing
263
+ data on params for describe/undo.
264
+ """
265
+ params: dict[str, dict[str, str | bool]] = {}
266
+
267
+ for m in PARAM_PATTERN.finditer(body):
268
+ key = m.group(2)
269
+
270
+ # Skip internal stash keys like _prevColor, _removedName
271
+ if key.startswith("_"):
272
+ continue
273
+
274
+ # Already found this key -- keep the first (usually from execute)
275
+ if key in params:
276
+ continue
277
+
278
+ # Extract the containing statement for this params reference.
279
+ # We find the next TOP-LEVEL semicolon (not inside braces/brackets)
280
+ # to scope our analysis to just THIS statement.
281
+ start = m.start()
282
+ semi_pos = find_top_level_semicolon(body, m.end())
283
+ # The statement context: from param reference to its semicolon
284
+ stmt = body[start : semi_pos + 1]
285
+
286
+ # Determine if optional: look for ??, | undefined, or !== undefined
287
+ # guard within this statement
288
+ is_optional = False
289
+ if "??" in stmt or "| undefined" in stmt or "!== undefined" in stmt:
290
+ is_optional = True
291
+
292
+ # Find the 'as' cast within the statement
293
+ after_bracket = stmt[m.end() - start:]
294
+
295
+ ts_type = "unknown"
296
+ raw_type = extract_as_type(after_bracket)
297
+ if raw_type:
298
+ # Clean up the type -- remove trailing | undefined, parens
299
+ raw_type = raw_type.rstrip(")")
300
+ if raw_type.endswith("| undefined"):
301
+ raw_type = raw_type[: -len("| undefined")].strip()
302
+ is_optional = True
303
+ # Clean up leading parens
304
+ raw_type = raw_type.lstrip("(").strip()
305
+ if raw_type:
306
+ ts_type = raw_type
307
+
308
+ params[key] = {"type": ts_type, "optional": is_optional}
309
+
310
+ return params
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Detect dynamic commands (registered in a loop)
315
+ # ---------------------------------------------------------------------------
316
+
317
+ def is_dynamic_command(source: str, cmd_name: str) -> bool:
318
+ """
319
+ Check if a command is registered dynamically (e.g., in a for loop with
320
+ template literal names like `layout_${presetKey}`).
321
+
322
+ We detect this by checking if the addCommand call uses a template literal
323
+ or variable for the command name instead of a plain string.
324
+ """
325
+ # Look for addCommand(`...${...}...`) or addCommand(variable)
326
+ dynamic_pattern = re.compile(
327
+ r"""addCommand\(\s*`[^`]*\$\{[^}]+\}[^`]*`"""
328
+ )
329
+ return bool(dynamic_pattern.search(source))
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # Main pipeline
334
+ # ---------------------------------------------------------------------------
335
+
336
+ def process_file(
337
+ filepath: Path,
338
+ root: Path,
339
+ ) -> list[dict]:
340
+ """
341
+ Process a single .ts file: find all addCommand calls, extract params.
342
+
343
+ Returns a list of dicts with keys: name, interface_name, params,
344
+ relative_path, dynamic, line.
345
+ """
346
+ source = filepath.read_text(encoding="utf-8")
347
+ results: list[dict] = []
348
+
349
+ # Check for dynamic commands (layout_* in a loop)
350
+ has_dynamic = is_dynamic_command(source, "")
351
+
352
+ blocks = extract_add_command_blocks(source)
353
+ for cmd_name, body, line in blocks:
354
+ params = extract_params_from_body(body)
355
+ rel_path = filepath.relative_to(root)
356
+ results.append({
357
+ "name": cmd_name,
358
+ "interface_name": snake_to_pascal(cmd_name),
359
+ "params": params,
360
+ "relative_path": str(rel_path),
361
+ "line": line,
362
+ "dynamic": False,
363
+ })
364
+
365
+ # Also detect template-literal addCommand calls (dynamic names)
366
+ dynamic_pattern = re.compile(
367
+ r"""addCommand\(\s*`([^`]*\$\{[^}]+\}[^`]*)`\s*,\s*\{"""
368
+ )
369
+ for dm in dynamic_pattern.finditer(source):
370
+ template = dm.group(1)
371
+ line = source[: dm.start()].count("\n") + 1
372
+ rel_path = filepath.relative_to(root)
373
+ results.append({
374
+ "name": template,
375
+ "interface_name": None,
376
+ "params": {},
377
+ "relative_path": str(rel_path),
378
+ "line": line,
379
+ "dynamic": True,
380
+ })
381
+
382
+ # Detect variable-based addCommand calls (e.g. addCommand(config.commandName, ...))
383
+ # These are used by factory functions like makeStrokeTool.
384
+ var_pattern = re.compile(
385
+ r"""addCommand\(\s*(\w+(?:\.\w+)+)\s*,\s*\{"""
386
+ )
387
+ for vm in var_pattern.finditer(source):
388
+ var_name = vm.group(1)
389
+ line = source[: vm.start()].count("\n") + 1
390
+ rel_path = filepath.relative_to(root)
391
+ results.append({
392
+ "name": var_name,
393
+ "interface_name": None,
394
+ "params": {},
395
+ "relative_path": str(rel_path),
396
+ "line": line,
397
+ "dynamic": True,
398
+ })
399
+
400
+ return results
401
+
402
+
403
+ def generate_output(
404
+ all_commands: list[dict],
405
+ ) -> str:
406
+ """
407
+ Generate the full TypeScript output from the extracted command data.
408
+
409
+ Groups commands by source file and generates:
410
+ - An interface per command (or Record<string, never> for no-param commands)
411
+ - A declare global block per command
412
+ """
413
+ lines: list[str] = []
414
+ lines.append("// Auto-generated by scripts/generate-command-params.py")
415
+ lines.append(
416
+ "// Review and adjust before committing "
417
+ "-- types are inferred from `as` casts."
418
+ )
419
+ lines.append("")
420
+
421
+ # Group by relative path
422
+ by_file: dict[str, list[dict]] = {}
423
+ for cmd in all_commands:
424
+ path = cmd["relative_path"]
425
+ by_file.setdefault(path, []).append(cmd)
426
+
427
+ for filepath, commands in by_file.items():
428
+ lines.append(f"// --- {filepath} ---")
429
+ lines.append("")
430
+
431
+ for cmd in commands:
432
+ if cmd["dynamic"]:
433
+ lines.append(
434
+ f"// SKIPPED: dynamic command `{cmd['name']}` "
435
+ f"(registered in a loop, line {cmd['line']})"
436
+ )
437
+ lines.append("")
438
+ continue
439
+
440
+ name = cmd["name"]
441
+ iface = cmd["interface_name"]
442
+ params = cmd["params"]
443
+
444
+ if not params:
445
+ # No params -- use Record<string, never>
446
+ lines.append(f"export interface {iface} extends Record<string, never> {{}}")
447
+ else:
448
+ lines.append(f"export interface {iface} {{")
449
+ for pname, pinfo in params.items():
450
+ ts_type = pinfo["type"]
451
+ optional = pinfo["optional"]
452
+ opt_mark = "?" if optional else ""
453
+ # Add a TODO comment for uncertain types
454
+ comment = ""
455
+ if ts_type == "unknown":
456
+ comment = " // TODO: verify"
457
+ lines.append(f" {pname}{opt_mark}: {ts_type};{comment}")
458
+ lines.append("}")
459
+
460
+ lines.append("")
461
+ lines.append("declare global {")
462
+ lines.append(" interface CommandParamsMap {")
463
+ lines.append(f" {name}: {iface};")
464
+ lines.append(" }")
465
+ lines.append("}")
466
+ lines.append("")
467
+
468
+ return "\n".join(lines)
469
+
470
+
471
+ def print_dry_run(all_commands: list[dict]) -> None:
472
+ """Print a summary table of found commands (dry-run mode)."""
473
+ print(f"Found {len(all_commands)} command(s):\n")
474
+ print(f"{'Command':<35} {'Interface':<35} {'Params':<6} {'File'}")
475
+ print("-" * 120)
476
+ for cmd in all_commands:
477
+ name = cmd["name"]
478
+ if cmd["dynamic"]:
479
+ iface = "(dynamic -- skipped)"
480
+ else:
481
+ iface = cmd["interface_name"]
482
+ param_count = len(cmd["params"])
483
+ filepath = cmd["relative_path"]
484
+ print(f"{name:<35} {iface:<35} {param_count:<6} {filepath}")
485
+
486
+
487
+ def main() -> None:
488
+ parser = argparse.ArgumentParser(
489
+ description="Generate TypeScript command params interfaces from addCommand() casts."
490
+ )
491
+ parser.add_argument(
492
+ "--root",
493
+ type=Path,
494
+ default=Path.cwd(),
495
+ help="Project root directory (default: cwd)",
496
+ )
497
+ parser.add_argument(
498
+ "--dry-run",
499
+ action="store_true",
500
+ help="List found commands without generating code",
501
+ )
502
+ args = parser.parse_args()
503
+ root: Path = args.root.resolve()
504
+
505
+ # Validate root
506
+ src = root / "src"
507
+ plugins = root / "plugins"
508
+ if not src.is_dir() and not plugins.is_dir():
509
+ print(
510
+ f"Error: neither {src} nor {plugins} exists. "
511
+ f"Is --root pointing to the PixelWeaver project?",
512
+ file=sys.stderr,
513
+ )
514
+ sys.exit(1)
515
+
516
+ # Collect all .ts files
517
+ ts_files = find_ts_files(root)
518
+ if not ts_files:
519
+ print("No .ts files found.", file=sys.stderr)
520
+ sys.exit(1)
521
+
522
+ # Process each file
523
+ all_commands: list[dict] = []
524
+ for filepath in ts_files:
525
+ cmds = process_file(filepath, root)
526
+ all_commands.extend(cmds)
527
+
528
+ if not all_commands:
529
+ print("No addCommand() calls found.", file=sys.stderr)
530
+ sys.exit(1)
531
+
532
+ if args.dry_run:
533
+ print_dry_run(all_commands)
534
+ else:
535
+ output = generate_output(all_commands)
536
+ print(output)
537
+
538
+
539
+ if __name__ == "__main__":
540
+ main()