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.
- package/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- 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()
|