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,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
|
+
)
|