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,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Migrate PixelWeaver projects from old single-frame format to multi-frame format.
|
|
3
|
+
|
|
4
|
+
Old format (per canvas):
|
|
5
|
+
canvases/<canvas_name>/
|
|
6
|
+
canvas.json
|
|
7
|
+
frames/0/
|
|
8
|
+
layer-<id>.png
|
|
9
|
+
|
|
10
|
+
New format (per canvas):
|
|
11
|
+
canvases/<canvas_name>/
|
|
12
|
+
canvas.json
|
|
13
|
+
frames.json -- frame manifest with IDs, durations, playback state
|
|
14
|
+
frames/<uuid>/ -- UUID-named directory instead of "0"
|
|
15
|
+
layer-<id>.png
|
|
16
|
+
|
|
17
|
+
This script is idempotent: canvases that already have a frames.json are skipped.
|
|
18
|
+
It can safely be run multiple times against the same data directory.
|
|
19
|
+
|
|
20
|
+
The default PixelWeaver data directory is "./projects" (relative to the server
|
|
21
|
+
working directory), configurable via the --data-dir CLI flag when running the
|
|
22
|
+
PixelWeaver server.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
import uuid
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_projects(data_dir: Path) -> list[Path]:
|
|
35
|
+
"""Return paths to all project directories (those containing project.json)."""
|
|
36
|
+
if not data_dir.is_dir():
|
|
37
|
+
return []
|
|
38
|
+
return sorted(
|
|
39
|
+
d for d in data_dir.iterdir()
|
|
40
|
+
if d.is_dir() and (d / "project.json").exists()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_canvases(project_dir: Path) -> list[Path]:
|
|
45
|
+
"""Return paths to all canvas directories within a project."""
|
|
46
|
+
canvases_dir = project_dir / "canvases"
|
|
47
|
+
if not canvases_dir.is_dir():
|
|
48
|
+
return []
|
|
49
|
+
return sorted(
|
|
50
|
+
d for d in canvases_dir.iterdir() if d.is_dir()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def migrate_canvas(canvas_dir: Path, *, dry_run: bool) -> str | None:
|
|
55
|
+
"""Migrate a single canvas from old format to new format.
|
|
56
|
+
|
|
57
|
+
Returns a description of what was done, or None if the canvas was skipped.
|
|
58
|
+
"""
|
|
59
|
+
frames_json_path = canvas_dir / "frames.json"
|
|
60
|
+
old_frame_dir = canvas_dir / "frames" / "0"
|
|
61
|
+
|
|
62
|
+
# Already migrated -- frames.json exists
|
|
63
|
+
if frames_json_path.exists():
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# No old-format data to migrate
|
|
67
|
+
if not old_frame_dir.is_dir():
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
frame_id = str(uuid.uuid4())
|
|
71
|
+
new_frame_dir = canvas_dir / "frames" / frame_id
|
|
72
|
+
|
|
73
|
+
frames_manifest = {
|
|
74
|
+
"frames": [{"id": frame_id, "duration_ms": None}],
|
|
75
|
+
"current_frame_index": 0,
|
|
76
|
+
"global_fps": 12.0,
|
|
77
|
+
"origin_x": 0,
|
|
78
|
+
"origin_y": 0,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if dry_run:
|
|
82
|
+
return (
|
|
83
|
+
f" Would rename frames/0/ -> frames/{frame_id}/\n"
|
|
84
|
+
f" Would create frames.json"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Rename the directory first, then write the manifest.
|
|
88
|
+
# If the rename succeeds but the write fails, re-running the script
|
|
89
|
+
# won't find frames/0/ and will skip -- no data loss.
|
|
90
|
+
old_frame_dir.rename(new_frame_dir)
|
|
91
|
+
frames_json_path.write_text(json.dumps(frames_manifest, indent=2))
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
f" Renamed frames/0/ -> frames/{frame_id}/\n"
|
|
95
|
+
f" Created frames.json"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(argv: list[str] | None = None) -> int:
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
description=(
|
|
102
|
+
"Migrate PixelWeaver projects from old single-frame (frames/0/) format "
|
|
103
|
+
"to the new multi-frame (frames/<uuid>/ + frames.json) format."
|
|
104
|
+
),
|
|
105
|
+
epilog=(
|
|
106
|
+
"The default PixelWeaver data directory is './projects' (relative to the "
|
|
107
|
+
"server working directory), configurable via the --data-dir flag on the "
|
|
108
|
+
"PixelWeaver CLI."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--data-dir",
|
|
113
|
+
type=Path,
|
|
114
|
+
required=True,
|
|
115
|
+
help="Base directory containing PixelWeaver projects (e.g. ./projects)",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--dry-run",
|
|
119
|
+
action="store_true",
|
|
120
|
+
help="Show what would be done without making any changes",
|
|
121
|
+
)
|
|
122
|
+
args = parser.parse_args(argv)
|
|
123
|
+
|
|
124
|
+
data_dir: Path = args.data_dir.expanduser().resolve()
|
|
125
|
+
dry_run: bool = args.dry_run
|
|
126
|
+
|
|
127
|
+
if not data_dir.is_dir():
|
|
128
|
+
print(f"Error: data directory does not exist: {data_dir}", file=sys.stderr)
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
if dry_run:
|
|
132
|
+
print("[DRY RUN] No changes will be made.\n")
|
|
133
|
+
|
|
134
|
+
projects = find_projects(data_dir)
|
|
135
|
+
if not projects:
|
|
136
|
+
print(f"No projects found in {data_dir}")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
total_migrated = 0
|
|
140
|
+
total_skipped = 0
|
|
141
|
+
|
|
142
|
+
for project_dir in projects:
|
|
143
|
+
project_name = project_dir.name
|
|
144
|
+
canvases = find_canvases(project_dir)
|
|
145
|
+
|
|
146
|
+
if not canvases:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
print(f"Project: {project_name}")
|
|
150
|
+
|
|
151
|
+
for canvas_dir in canvases:
|
|
152
|
+
canvas_name = canvas_dir.name
|
|
153
|
+
result = migrate_canvas(canvas_dir, dry_run=dry_run)
|
|
154
|
+
|
|
155
|
+
if result is None:
|
|
156
|
+
total_skipped += 1
|
|
157
|
+
else:
|
|
158
|
+
total_migrated += 1
|
|
159
|
+
print(f" Canvas: {canvas_name}")
|
|
160
|
+
print(result)
|
|
161
|
+
|
|
162
|
+
# Summary
|
|
163
|
+
print()
|
|
164
|
+
verb = "would be" if dry_run else "were"
|
|
165
|
+
print(f"Done. {total_migrated} canvas(es) {verb} migrated, {total_skipped} skipped.")
|
|
166
|
+
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Smoke test: start server, run CRUD checks, verify SPA serving
|
|
5
|
+
# Usage: ./scripts/smoke-test.sh [--with-frontend]
|
|
6
|
+
#
|
|
7
|
+
# --with-frontend: also build the frontend and verify SPA/static serving
|
|
8
|
+
|
|
9
|
+
PORT=7799
|
|
10
|
+
BASE="http://127.0.0.1:$PORT"
|
|
11
|
+
SERVER_PID=""
|
|
12
|
+
|
|
13
|
+
cleanup() {
|
|
14
|
+
if [ -n "$SERVER_PID" ]; then
|
|
15
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
16
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
trap cleanup EXIT
|
|
20
|
+
|
|
21
|
+
WITH_FRONTEND=false
|
|
22
|
+
if [ "${1:-}" = "--with-frontend" ]; then
|
|
23
|
+
WITH_FRONTEND=true
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "=== Building frontend ==="
|
|
27
|
+
if $WITH_FRONTEND; then
|
|
28
|
+
npm run build
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "=== Starting server on port $PORT ==="
|
|
32
|
+
uv run pixelweaver serve --port "$PORT" &
|
|
33
|
+
SERVER_PID=$!
|
|
34
|
+
|
|
35
|
+
# Wait for server
|
|
36
|
+
for i in $(seq 1 30); do
|
|
37
|
+
if curl -sf "$BASE/health" > /dev/null 2>&1; then
|
|
38
|
+
break
|
|
39
|
+
fi
|
|
40
|
+
sleep 0.5
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
echo "=== Health check ==="
|
|
44
|
+
curl -sf "$BASE/health" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok'; print('PASS')"
|
|
45
|
+
|
|
46
|
+
echo "=== Create project ==="
|
|
47
|
+
curl -sf -X POST "$BASE/api/projects" \
|
|
48
|
+
-H "Content-Type: application/json" \
|
|
49
|
+
-d '{"name":"smoke","width":32,"height":32}' > /dev/null
|
|
50
|
+
echo "PASS"
|
|
51
|
+
|
|
52
|
+
echo "=== List projects ==="
|
|
53
|
+
curl -sf "$BASE/api/projects" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'smoke' in d['projects']; print('PASS')"
|
|
54
|
+
|
|
55
|
+
echo "=== Get project ==="
|
|
56
|
+
curl -sf "$BASE/api/projects/smoke" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'canvases' in d; print('PASS')"
|
|
57
|
+
|
|
58
|
+
echo "=== Export PNG ==="
|
|
59
|
+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/api/projects/smoke/export/png/canvas-0/0")
|
|
60
|
+
[ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
|
|
61
|
+
|
|
62
|
+
echo "=== Delete project ==="
|
|
63
|
+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE "$BASE/api/projects/smoke")
|
|
64
|
+
[ "$STATUS" = "204" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
|
|
65
|
+
|
|
66
|
+
if $WITH_FRONTEND && [ -f dist/index.html ]; then
|
|
67
|
+
echo "=== SPA root ==="
|
|
68
|
+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/")
|
|
69
|
+
[ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
|
|
70
|
+
|
|
71
|
+
echo "=== SPA fallback ==="
|
|
72
|
+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/unknown/path")
|
|
73
|
+
[ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
echo "=== ALL SMOKE TESTS PASSED ==="
|
package/selfdoc.json
ADDED
package/server/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PixelWeaver collaboration server."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Auto-save: periodic flush of in-memory state to disk.
|
|
2
|
+
|
|
3
|
+
The auto-saver runs as an asyncio background task. State is saved when:
|
|
4
|
+
- A debounce timer expires after the last state mutation, OR
|
|
5
|
+
- A fixed interval elapses since the last save
|
|
6
|
+
|
|
7
|
+
Whichever fires first triggers a save, preventing both data loss and
|
|
8
|
+
excessive I/O.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from pixelweaver.state import ServerState
|
|
18
|
+
from pixelweaver.storage import save_project
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AutoSaver:
|
|
24
|
+
"""Periodically flushes server state to disk."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
state: ServerState,
|
|
29
|
+
save_dir: str,
|
|
30
|
+
debounce_ms: int = 500,
|
|
31
|
+
interval_s: int = 30,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.state = state
|
|
34
|
+
self.save_dir = save_dir
|
|
35
|
+
self.debounce_ms = debounce_ms
|
|
36
|
+
self.interval_s = interval_s
|
|
37
|
+
self._dirty = False
|
|
38
|
+
self._last_save: float = 0
|
|
39
|
+
self._task: asyncio.Task[None] | None = None
|
|
40
|
+
self._debounce_task: asyncio.Task[None] | None = None
|
|
41
|
+
|
|
42
|
+
def mark_dirty(self) -> None:
|
|
43
|
+
"""Called after any state change to schedule a debounced save."""
|
|
44
|
+
self._dirty = True
|
|
45
|
+
# Reset the debounce timer
|
|
46
|
+
if self._debounce_task is not None and not self._debounce_task.done():
|
|
47
|
+
self._debounce_task.cancel()
|
|
48
|
+
self._debounce_task = asyncio.ensure_future(self._debounce_save())
|
|
49
|
+
|
|
50
|
+
async def _debounce_save(self) -> None:
|
|
51
|
+
"""Wait for the debounce period, then save if still dirty."""
|
|
52
|
+
try:
|
|
53
|
+
await asyncio.sleep(self.debounce_ms / 1000.0)
|
|
54
|
+
if self._dirty:
|
|
55
|
+
await self.save_now()
|
|
56
|
+
except asyncio.CancelledError:
|
|
57
|
+
pass # Debounce was reset by a newer mutation
|
|
58
|
+
|
|
59
|
+
async def start(self) -> None:
|
|
60
|
+
"""Start the auto-save background task (periodic interval saves)."""
|
|
61
|
+
self._task = asyncio.ensure_future(self._periodic_loop())
|
|
62
|
+
logger.info(
|
|
63
|
+
"Auto-saver started (debounce=%dms, interval=%ds, dir=%s)",
|
|
64
|
+
self.debounce_ms,
|
|
65
|
+
self.interval_s,
|
|
66
|
+
self.save_dir,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def stop(self) -> None:
|
|
70
|
+
"""Stop the background task gracefully, saving any pending changes."""
|
|
71
|
+
if self._task is not None:
|
|
72
|
+
self._task.cancel()
|
|
73
|
+
try:
|
|
74
|
+
await self._task
|
|
75
|
+
except asyncio.CancelledError:
|
|
76
|
+
pass
|
|
77
|
+
if self._debounce_task is not None:
|
|
78
|
+
self._debounce_task.cancel()
|
|
79
|
+
# Final save if dirty
|
|
80
|
+
if self._dirty:
|
|
81
|
+
await self.save_now()
|
|
82
|
+
|
|
83
|
+
async def _periodic_loop(self) -> None:
|
|
84
|
+
"""Run periodic saves on a fixed interval."""
|
|
85
|
+
try:
|
|
86
|
+
while True:
|
|
87
|
+
await asyncio.sleep(self.interval_s)
|
|
88
|
+
if self._dirty:
|
|
89
|
+
await self.save_now()
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
async def save_now(self) -> None:
|
|
94
|
+
"""Flush all project state to disk immediately."""
|
|
95
|
+
if not self.state.projects:
|
|
96
|
+
self._dirty = False
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Track per-loop failures so a single failed project doesn't lose
|
|
100
|
+
# the dirty marker for the rest. _dirty is a single flag here, so
|
|
101
|
+
# if anything failed we leave it set and the next debounce/interval
|
|
102
|
+
# tick will retry the whole batch.
|
|
103
|
+
any_failed = False
|
|
104
|
+
for project in self.state.projects.values():
|
|
105
|
+
try:
|
|
106
|
+
await asyncio.to_thread(save_project, project, self.save_dir)
|
|
107
|
+
except Exception:
|
|
108
|
+
any_failed = True
|
|
109
|
+
logger.error("Failed to save project %s", project.name, exc_info=True)
|
|
110
|
+
|
|
111
|
+
if not any_failed:
|
|
112
|
+
self._dirty = False
|
|
113
|
+
self._last_save = time.monotonic()
|
|
114
|
+
logger.debug("Auto-saved %d project(s) to %s", len(self.state.projects), self.save_dir)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Desktop file I/O bridge for pywebview.
|
|
2
|
+
|
|
3
|
+
Provides native file dialogs and filesystem access callable from JavaScript
|
|
4
|
+
via pywebview's js_api mechanism. All binary data is transferred as
|
|
5
|
+
base64-encoded strings since the JS bridge serializes return values as JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DesktopBridge:
|
|
18
|
+
"""File I/O bridge exposed to the frontend via pywebview js_api.
|
|
19
|
+
|
|
20
|
+
Methods on this class become callable from JavaScript as:
|
|
21
|
+
window.pywebview.api.method_name(args)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def save_file(
|
|
25
|
+
self,
|
|
26
|
+
data_b64: str,
|
|
27
|
+
default_name: str,
|
|
28
|
+
filter_name: str,
|
|
29
|
+
extensions: list[str],
|
|
30
|
+
) -> str | None:
|
|
31
|
+
"""Show a native Save dialog and write the file.
|
|
32
|
+
|
|
33
|
+
Returns the saved file path, or None if the user cancelled.
|
|
34
|
+
"""
|
|
35
|
+
import webview
|
|
36
|
+
|
|
37
|
+
window = webview.windows[0]
|
|
38
|
+
file_types = (f"{filter_name} ({' '.join('*.' + e for e in extensions)})",)
|
|
39
|
+
result = window.create_file_dialog(
|
|
40
|
+
webview.SAVE_DIALOG,
|
|
41
|
+
save_filename=default_name,
|
|
42
|
+
file_types=file_types,
|
|
43
|
+
)
|
|
44
|
+
if not result:
|
|
45
|
+
return None
|
|
46
|
+
path = result if isinstance(result, str) else result[0]
|
|
47
|
+
try:
|
|
48
|
+
data = base64.b64decode(data_b64)
|
|
49
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
Path(path).write_bytes(data)
|
|
51
|
+
return path
|
|
52
|
+
except Exception:
|
|
53
|
+
logger.exception("Failed to write file %s", path)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def open_file(
|
|
57
|
+
self,
|
|
58
|
+
filter_name: str,
|
|
59
|
+
extensions: list[str],
|
|
60
|
+
) -> dict | None:
|
|
61
|
+
"""Show a native Open dialog and read the file.
|
|
62
|
+
|
|
63
|
+
Returns {"path": str, "data": base64_string} or None if cancelled.
|
|
64
|
+
"""
|
|
65
|
+
import webview
|
|
66
|
+
|
|
67
|
+
window = webview.windows[0]
|
|
68
|
+
file_types = (f"{filter_name} ({' '.join('*.' + e for e in extensions)})",)
|
|
69
|
+
result = window.create_file_dialog(
|
|
70
|
+
webview.OPEN_DIALOG,
|
|
71
|
+
file_types=file_types,
|
|
72
|
+
)
|
|
73
|
+
if not result:
|
|
74
|
+
return None
|
|
75
|
+
path = result[0] if isinstance(result, (tuple, list)) else result
|
|
76
|
+
try:
|
|
77
|
+
data = Path(path).read_bytes()
|
|
78
|
+
return {
|
|
79
|
+
"path": str(path),
|
|
80
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
81
|
+
}
|
|
82
|
+
except Exception:
|
|
83
|
+
logger.exception("Failed to read file %s", path)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def write_file(self, path: str, data_b64: str) -> bool:
|
|
87
|
+
"""Write binary data to a specific path (no dialog)."""
|
|
88
|
+
try:
|
|
89
|
+
data = base64.b64decode(data_b64)
|
|
90
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
Path(path).write_bytes(data)
|
|
92
|
+
return True
|
|
93
|
+
except Exception:
|
|
94
|
+
logger.exception("Failed to write file %s", path)
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def pick_directory(self) -> str | None:
|
|
98
|
+
"""Show a native directory picker. Returns path or None if cancelled."""
|
|
99
|
+
import webview
|
|
100
|
+
|
|
101
|
+
window = webview.windows[0]
|
|
102
|
+
result = window.create_file_dialog(webview.FOLDER_DIALOG)
|
|
103
|
+
if not result:
|
|
104
|
+
return None
|
|
105
|
+
return result[0] if isinstance(result, (tuple, list)) else str(result)
|
|
106
|
+
|
|
107
|
+
def write_files_to_directory(
|
|
108
|
+
self,
|
|
109
|
+
dir_path: str,
|
|
110
|
+
files: list[dict],
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Write multiple files to a directory.
|
|
113
|
+
|
|
114
|
+
Each entry in files should be {"name": str, "data": base64_string}.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
base = Path(dir_path)
|
|
118
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
for entry in files:
|
|
120
|
+
file_path = base / entry["name"]
|
|
121
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
data = base64.b64decode(entry["data"])
|
|
123
|
+
file_path.write_bytes(data)
|
|
124
|
+
return True
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.exception("Failed to write files to %s", dir_path)
|
|
127
|
+
return False
|