@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command relay's HTTP surface: `/__editor/command`,
|
|
3
|
+
* `/__editor/command-result`, `/__editor/command-received`,
|
|
4
|
+
* `/__editor/command-listener`, `/__editor/page-error`,
|
|
5
|
+
* `/__editor/play-phase`, `/__editor/console-entries`, `/__editor/console`,
|
|
6
|
+
* `/__editor/console/ack`, and the `/__editor/recording/*` verbs.
|
|
7
|
+
*
|
|
8
|
+
* Every one of these is a THIN door onto `routes/control-plane.ts`: the POSTs
|
|
9
|
+
* a tab makes land on exactly the handlers its duplex control socket's frames
|
|
10
|
+
* land on, so the two transports cannot answer differently. Nothing here
|
|
11
|
+
* decides anything — if a rule seems to be missing from this file, it is in
|
|
12
|
+
* the control plane, which is the point.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
17
|
+
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
20
|
+
import { isContainedRelativePath } from '@volter/editor-sdk/session/relative-path-guard';
|
|
21
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
22
|
+
import { projectOutputRootOf } from '@volter/editor-sdk/project/output-roots';
|
|
23
|
+
import { PROJECT_SESSION_WRITE_OPERATION } from '../support/project/provenance';
|
|
24
|
+
import {
|
|
25
|
+
BLENDER_WALI_ARTIFACT,
|
|
26
|
+
BLENDER_WASM_FILES,
|
|
27
|
+
blenderGlueText,
|
|
28
|
+
blenderWasmOnDisk,
|
|
29
|
+
blenderWasmReadStream,
|
|
30
|
+
blenderWasmStatus,
|
|
31
|
+
runtimeIndex,
|
|
32
|
+
substrateModule,
|
|
33
|
+
writeRuntimeBlob,
|
|
34
|
+
} from '../blender-wasm-artifact';
|
|
35
|
+
import { type EditorServerRouter, playRunSlug } from '../editor-server';
|
|
36
|
+
import { pruneGameplaySessionClips } from '../gameplay-session-retention';
|
|
37
|
+
import { isProjectOwnedRelativePath, projectFileIndex } from '../project-file-scan';
|
|
38
|
+
import { createProjectOutputWriter, projectOutputMediaType } from '../project-output-writer';
|
|
39
|
+
import {
|
|
40
|
+
allowCrossOriginFrameEmbedding,
|
|
41
|
+
commandResponseFor,
|
|
42
|
+
isCanonicalPathInside,
|
|
43
|
+
} from '../server-utils';
|
|
44
|
+
import type { RouteContext } from './context';
|
|
45
|
+
import type { ControlPlane } from './control-plane';
|
|
46
|
+
|
|
47
|
+
// ---- File bodies, which are NOT commands.
|
|
48
|
+
//
|
|
49
|
+
// A FILE IS A BODY, NOT A JSON FIELD. `blender-read-file` used to answer
|
|
50
|
+
// with the whole file base64'd inside a control-plane message, so a mesh the
|
|
51
|
+
// twin had written came back as a single JSON frame on the command socket:
|
|
52
|
+
// +33% for the encoding, a string the page built one 32 KB `fromCharCode`
|
|
53
|
+
// chunk at a time on the main thread, and a 60 s COMMAND budget applied to
|
|
54
|
+
// what is really a transfer.
|
|
55
|
+
//
|
|
56
|
+
// MEASURED 2026-09-15: `02-hinged-vise` writes a 31 MB
|
|
57
|
+
// `raw-FINAL-HandleKnobA.json`, which became ~41.5 MB of base64 in one
|
|
58
|
+
// message -- within 20% of the server's 50 MB JSON body cap, and the only
|
|
59
|
+
// file in the battery whose read killed the socket (1006). Nothing about
|
|
60
|
+
// that is specific to the parity battery; an exported GLB or a baked texture
|
|
61
|
+
// would do the same.
|
|
62
|
+
//
|
|
63
|
+
// So bytes move as bytes, the way `/__editor/recording/chunk` above already
|
|
64
|
+
// moves video. The page POSTs an octet-stream; the requester GETs it once
|
|
65
|
+
// and it is gone. The command result carries only a length.
|
|
66
|
+
//
|
|
67
|
+
// The spool deliberately keeps PATH AUTHORITY WITH THE REQUESTER: the page
|
|
68
|
+
// never names a destination on disk, it only hands over bytes under an id
|
|
69
|
+
// the caller minted. Adding a page-driven write-anywhere route to make one
|
|
70
|
+
// harness faster would be a far worse trade than the copy this costs.
|
|
71
|
+
function registerBlenderFileRoutes(router: EditorServerRouter, ctx: RouteContext): void {
|
|
72
|
+
const transferRoot = join(tmpdir(), 'vgai-blender-transfer');
|
|
73
|
+
const transferPath = (id: string): string => join(transferRoot, id);
|
|
74
|
+
|
|
75
|
+
// A spool is deleted by the GET that reads it, so the only thing that can
|
|
76
|
+
// accumulate is a transfer nobody ever collected -- a command that failed
|
|
77
|
+
// between the page's POST and the caller's GET. Clearing the whole directory
|
|
78
|
+
// once at startup bounds that exactly, with no age to guess at: every id is
|
|
79
|
+
// a uuid minted by a caller in a PREVIOUS process, so nothing live can be in
|
|
80
|
+
// here yet. A TTL sweep would be a number nothing measured.
|
|
81
|
+
void rm(transferRoot, { recursive: true, force: true });
|
|
82
|
+
|
|
83
|
+
const transferId = (value: unknown): string | null =>
|
|
84
|
+
typeof value === 'string' && /^[0-9a-f-]{36}$/.test(value) ? value : null;
|
|
85
|
+
|
|
86
|
+
// ---- The engine itself: headless Blender, compiled to WebAssembly.
|
|
87
|
+
//
|
|
88
|
+
// `blender-wasm-artifact.ts` owns where it is and the one patch its glue
|
|
89
|
+
// takes; this is the door. The runtime spawns its pthreads by loading
|
|
90
|
+
// `blender_browser.js` from this same URL as a classic worker, so the
|
|
91
|
+
// patched text has to be what the URL answers with -- not something the
|
|
92
|
+
// worker rewrites for itself.
|
|
93
|
+
router.get('/__editor/blender-wasm/status', async (_req: Request, res: Response) => {
|
|
94
|
+
allowCrossOriginFrameEmbedding(res);
|
|
95
|
+
res.json(await blenderWasmStatus());
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// THE SUBSTRATE SKEW'S OWN THREE DOORS, beside the standalone bundle's.
|
|
99
|
+
//
|
|
100
|
+
// `blender.wasm` goes out through the same `:file` route below. These are
|
|
101
|
+
// what that skew needs and the other does not: Blender's runtime tree as one
|
|
102
|
+
// index and one blob (a request per file would be ~2,300 round trips), and
|
|
103
|
+
// the substrate's browser ESM with its bare specifiers rewritten -- which is
|
|
104
|
+
// the ONLY way `@volter/*` reaches the tab, and therefore the reason
|
|
105
|
+
// `@volter/editor-blender` can declare no dependency on browser-substrate at all.
|
|
106
|
+
router.get('/__editor/blender-wasm/runtime.idx', async (_req: Request, res: Response) => {
|
|
107
|
+
const status = await blenderWasmStatus();
|
|
108
|
+
if (status.skew !== 'wali' || status.dir === null) {
|
|
109
|
+
res.status(404).json({ error: 'This editor does not serve the substrate skew of Blender.' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.json(await runtimeIndex(status.dir));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
router.get('/__editor/blender-wasm/runtime.bin', async (_req: Request, res: Response) => {
|
|
116
|
+
const status = await blenderWasmStatus();
|
|
117
|
+
if (status.skew !== 'wali' || status.dir === null) {
|
|
118
|
+
res.status(404).json({ error: 'This editor does not serve the substrate skew of Blender.' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
res.type('application/octet-stream');
|
|
122
|
+
await writeRuntimeBlob(status.dir, (chunk) => void res.write(chunk));
|
|
123
|
+
res.end();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.get('/__editor/blender-wasm/wali/:segment/:file', async (req: Request, res: Response) => {
|
|
127
|
+
const served = await substrateModule(String(req.params['segment']), String(req.params['file']));
|
|
128
|
+
if (!served) {
|
|
129
|
+
res.status(404).json({
|
|
130
|
+
error:
|
|
131
|
+
`This editor does not serve ${req.params['segment']}/${req.params['file']}. ` +
|
|
132
|
+
'The substrate skew runs Blender through @volter/browser-wali and ' +
|
|
133
|
+
'@volter/browser-runtime; a project that opts into it declares them.',
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.type('text/javascript').send(served.text);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
router.get('/__editor/blender-wasm/:file', async (req: Request, res: Response) => {
|
|
141
|
+
const file = String(req.params['file']);
|
|
142
|
+
// The frame's page is cross-origin-isolated and this session is not its
|
|
143
|
+
// origin, so every byte of the engine has to say it may be embedded — the
|
|
144
|
+
// glue above all, which Blender's pthreads load with `importScripts`
|
|
145
|
+
// (`server-utils.ts`'s allowCrossOriginFrameEmbedding says what that cost
|
|
146
|
+
// before it was measured).
|
|
147
|
+
allowCrossOriginFrameEmbedding(res);
|
|
148
|
+
const bundled = (BLENDER_WASM_FILES as readonly string[]).includes(file);
|
|
149
|
+
if (!bundled && file !== BLENDER_WALI_ARTIFACT) {
|
|
150
|
+
res.status(404).json({ error: `The Blender build has no ${String(file)}.` });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const status = await blenderWasmStatus();
|
|
154
|
+
if (!status.available) {
|
|
155
|
+
res.status(404).json({
|
|
156
|
+
error:
|
|
157
|
+
'The headless Blender WebAssembly build is not served by this editor: ' +
|
|
158
|
+
status.missing.join('; '),
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (file === 'blender_browser.js') {
|
|
163
|
+
res.type('text/javascript').send(await blenderGlueText(status.dir!));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const found = await blenderWasmOnDisk(status.dir!, file);
|
|
167
|
+
if (!found) {
|
|
168
|
+
res.status(404).json({ error: `${file} vanished from ${status.dir}.` });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// A pre-compressed file goes out as stored, declared as brotli: the
|
|
172
|
+
// browser inflates it before `instantiateStreaming` / the `.data` preload
|
|
173
|
+
// read a byte, so `content-type` stays what the runtime requires.
|
|
174
|
+
res.type(file.endsWith('.wasm') ? 'application/wasm' : 'application/octet-stream');
|
|
175
|
+
if (found.encoding) res.setHeader('content-encoding', found.encoding);
|
|
176
|
+
res.setHeader('content-length', String(found.size));
|
|
177
|
+
blenderWasmReadStream(found).pipe(res);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
router.post('/__editor/blender-file', async (req: Request, res: Response) => {
|
|
181
|
+
const id = transferId(req.query['id']);
|
|
182
|
+
if (id === null) {
|
|
183
|
+
res.status(400).json({ error: 'blender-file requires a uuid `id`.' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const path = transferPath(id);
|
|
187
|
+
await mkdir(dirname(path), { recursive: true });
|
|
188
|
+
const chunks: Buffer[] = [];
|
|
189
|
+
let bytes = 0;
|
|
190
|
+
for await (const value of req) {
|
|
191
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
192
|
+
bytes += chunk.byteLength;
|
|
193
|
+
chunks.push(chunk);
|
|
194
|
+
}
|
|
195
|
+
await writeFile(path, new Uint8Array(Buffer.concat(chunks)));
|
|
196
|
+
res.json({ bytes });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
router.get('/__editor/blender-file', async (req: Request, res: Response) => {
|
|
200
|
+
const id = transferId(req.query['id']);
|
|
201
|
+
if (id === null) {
|
|
202
|
+
res.status(400).json({ error: 'blender-file requires a uuid `id`.' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const path = transferPath(id);
|
|
206
|
+
const info = await stat(path).catch(() => null);
|
|
207
|
+
if (info === null) {
|
|
208
|
+
res.status(404).json({ error: 'No transfer is spooled under that id.' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
res.setHeader('content-type', 'application/octet-stream');
|
|
212
|
+
res.setHeader('content-length', String(info.size));
|
|
213
|
+
const stream = createReadStream(path);
|
|
214
|
+
stream.pipe(res);
|
|
215
|
+
// One reader, then it is gone -- a spool that outlives its GET is a
|
|
216
|
+
// tmpdir leak proportional to everything Blender ever wrote.
|
|
217
|
+
stream.on('close', () => {
|
|
218
|
+
void unlink(path).catch(() => {});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ---- The project's own files, read INTO the session.
|
|
223
|
+
//
|
|
224
|
+
// The direction `blender-file` above does not cover: the session's Python
|
|
225
|
+
// opening the project's own files. Scope is the project root and nothing
|
|
226
|
+
// else, through the estate `collaborationSourceSnapshot` already publishes
|
|
227
|
+
// (`isProjectOwnedRelativePath`). The client names a project-RELATIVE path
|
|
228
|
+
// and the server joins it to its own root -- no host path on the wire -- and
|
|
229
|
+
// the join is canonicalized before any byte is read.
|
|
230
|
+
router.get('/__editor/blender-project-index', async (_req: Request, res: Response) => {
|
|
231
|
+
if (ctx.projectRoot === ctx.engineRoot) {
|
|
232
|
+
res.status(404).json({ error: 'No project open.' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
res.json({ root: ctx.projectRoot, files: await projectFileIndex(ctx.projectRoot) });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
router.get('/__editor/blender-project-file', async (req: Request, res: Response) => {
|
|
239
|
+
const relPath = (req.query['path'] as string) ?? '';
|
|
240
|
+
// Saved scratch documents can be opened explicitly without indexing logs,
|
|
241
|
+
// recordings, credentials or the rest of the editor's private directory.
|
|
242
|
+
const scratchDocument =
|
|
243
|
+
isContainedRelativePath(relPath) &&
|
|
244
|
+
relPath.startsWith('.vgai/tmp/') &&
|
|
245
|
+
relPath.endsWith('.blend');
|
|
246
|
+
if (!isProjectOwnedRelativePath(relPath) && !scratchDocument) {
|
|
247
|
+
res.status(400).json({ error: 'Not a project-owned path.' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (ctx.projectRoot === ctx.engineRoot) {
|
|
251
|
+
res.status(404).json({ error: 'No project open.' });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const absolute = join(ctx.projectRoot, relPath);
|
|
255
|
+
if (!(await isCanonicalPathInside(ctx.projectRoot, absolute))) {
|
|
256
|
+
res.status(403).json({ error: 'That path leaves the project root.' });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
res.type('application/octet-stream').send(await readFile(absolute));
|
|
261
|
+
} catch {
|
|
262
|
+
res.status(404).json({ error: 'Not found.' });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ---- The session's SHIPPED OUTPUTS, written back OUT into the project
|
|
267
|
+
// WITH a provenance record.
|
|
268
|
+
//
|
|
269
|
+
// The third direction, and the one that must record. A Blender script's
|
|
270
|
+
// `export_scene.gltf` to `<project>/public/models/lantern.glb` is a
|
|
271
|
+
// generated binary the project SHIPS, and a shipped binary with no record in
|
|
272
|
+
// `.vgai/provenance.json` is the one state
|
|
273
|
+
// `scripts/validate-project-provenance.mjs` calls fatal.
|
|
274
|
+
//
|
|
275
|
+
// `vgai blender-mcp` used to commit those bytes itself, by importing
|
|
276
|
+
// `server/project-output-writer.ts` straight out of this package. That is a
|
|
277
|
+
// LICENCE break before it is anything else — the CLI is Apache-2.0 and this
|
|
278
|
+
// server is AGPL-3.0-only, and licence follows the import graph — and it put
|
|
279
|
+
// a second PROCESS on a ledger whose write lock had to grow a cross-process
|
|
280
|
+
// half to survive it. The session face is the one door the CLI rides, so it
|
|
281
|
+
// rides it here: the bytes arrive as a body, the writer runs in the process
|
|
282
|
+
// that owns the project, and there is exactly one writer.
|
|
283
|
+
//
|
|
284
|
+
// The destination shape is pinned BY THIS SERVER, like `blender-document`
|
|
285
|
+
// below and unlike the `blender-file` spool above: a project-relative path
|
|
286
|
+
// under `public/`, no traversal, joined to the server's own root. The SESSION
|
|
287
|
+
// facts are the caller's to state, because only it watched the frames — and
|
|
288
|
+
// an unnamed session is REFUSED rather than invented
|
|
289
|
+
// (`ProjectProvenanceSessionSchema`: a kind that can be claimed without
|
|
290
|
+
// evidence is the cheapest way past the gate).
|
|
291
|
+
router.post('/__editor/blender-output', async (req: Request, res: Response) => {
|
|
292
|
+
const path = (req.query['path'] as string) ?? '';
|
|
293
|
+
const session = (req.query['session'] as string) ?? '';
|
|
294
|
+
const source = (req.query['source'] as string) ?? '';
|
|
295
|
+
const revision = Number(req.query['revision']);
|
|
296
|
+
const callId = (req.query['callId'] as string) ?? '';
|
|
297
|
+
if (!isContainedRelativePath(path) || projectOutputRootOf(path) !== 'public') {
|
|
298
|
+
res.status(400).json({
|
|
299
|
+
error:
|
|
300
|
+
'A shipped output is a project-relative path under public/ with no traversal ' +
|
|
301
|
+
`(public/models/model.glb); got ${JSON.stringify(path)}.`,
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (session === '' || !Number.isInteger(revision) || revision < 0) {
|
|
306
|
+
res.status(400).json({
|
|
307
|
+
error:
|
|
308
|
+
'A shipped output must name the session that produced it — `session` and a ' +
|
|
309
|
+
'non-negative integer `revision` — so the record can say what re-derives the file.',
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (source === '' || source.length > 64) {
|
|
314
|
+
res
|
|
315
|
+
.status(400)
|
|
316
|
+
.json({ error: '`source` names the transport that wrote the file, in 1-64 characters.' });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// WHAT THE BYTES WERE MADE FROM — project FILES, validated the same way
|
|
320
|
+
// the destination is. A session-written `.glb` names the
|
|
321
|
+
// `src/models/<name>.blend` it was exported from, which is what lets a
|
|
322
|
+
// reader drill DOWN by kind from a prefab's glTF to its model
|
|
323
|
+
// (ARCHITECTURE-CORE §Roots). Repeatable; each value is a
|
|
324
|
+
// project-relative path with no traversal, and unlike the destination it
|
|
325
|
+
// is NOT restricted to `public/` — a source lives in `src/`.
|
|
326
|
+
const inputs = (
|
|
327
|
+
Array.isArray(req.query['inputs'])
|
|
328
|
+
? (req.query['inputs'] as unknown[])
|
|
329
|
+
: req.query['inputs'] === undefined
|
|
330
|
+
? []
|
|
331
|
+
: [req.query['inputs']]
|
|
332
|
+
).map((value) => String(value));
|
|
333
|
+
const badInput = inputs.find((value) => !isContainedRelativePath(value));
|
|
334
|
+
if (badInput !== undefined) {
|
|
335
|
+
res.status(400).json({
|
|
336
|
+
error:
|
|
337
|
+
'Each `inputs` value is a project-relative path with no traversal (the source file ' +
|
|
338
|
+
`these bytes were produced from); got ${JSON.stringify(badInput)}.`,
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (ctx.projectRoot === ctx.engineRoot) {
|
|
343
|
+
res.status(404).json({ error: 'No project open, so there is nowhere to record the output.' });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const chunks: Buffer[] = [];
|
|
347
|
+
let bytes = 0;
|
|
348
|
+
for await (const value of req) {
|
|
349
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
350
|
+
bytes += chunk.byteLength;
|
|
351
|
+
chunks.push(chunk);
|
|
352
|
+
}
|
|
353
|
+
if (bytes === 0) {
|
|
354
|
+
res.status(400).json({ error: 'The output body was empty; nothing was written.' });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const mediaType = projectOutputMediaType(path);
|
|
358
|
+
try {
|
|
359
|
+
const written = await createProjectOutputWriter(ctx.projectRoot, {
|
|
360
|
+
operationName: PROJECT_SESSION_WRITE_OPERATION,
|
|
361
|
+
operationSource: source,
|
|
362
|
+
session: {
|
|
363
|
+
id: session,
|
|
364
|
+
// The server's OWN port, read off the connection rather than taken
|
|
365
|
+
// from the caller: it is the port a person gets back to this tab on,
|
|
366
|
+
// and the one fact here nobody else should be asserting.
|
|
367
|
+
port: req.socket.localPort ?? 0,
|
|
368
|
+
revision,
|
|
369
|
+
...(callId === '' ? {} : { callId }),
|
|
370
|
+
},
|
|
371
|
+
...(inputs.length > 0 ? { inputs } : {}),
|
|
372
|
+
}).write([
|
|
373
|
+
{
|
|
374
|
+
path,
|
|
375
|
+
content: Buffer.concat(chunks),
|
|
376
|
+
role: 'asset',
|
|
377
|
+
...(mediaType ? { mediaType } : {}),
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
res.json({ ok: true, bytes, provenanceOperationId: written.provenanceOperationId ?? null });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ---- The session's DOCUMENT, written back OUT into the project.
|
|
387
|
+
//
|
|
388
|
+
// The one direction the two routes above do not cover. A Blender session
|
|
389
|
+
// holds one `.blend` (`browser/session.py`, "THE SESSION HAS A DOCUMENT");
|
|
390
|
+
// Blender's own `save_as_mainfile` writes it into the engine's filesystem,
|
|
391
|
+
// and this is how those bytes reach the disk the project actually lives on.
|
|
392
|
+
//
|
|
393
|
+
// NOT the `blender-file` spool above, and the difference is the one that
|
|
394
|
+
// spool exists to protect: there the page hands over bytes under an id and
|
|
395
|
+
// the REQUESTER names the destination, because the requester (`vgai
|
|
396
|
+
// blender-mcp`'s Mirror) writes anywhere a script wrote. Here the
|
|
397
|
+
// destination shape is pinned BY THIS SERVER — a project-relative `.blend`,
|
|
398
|
+
// no traversal, joined to the server's own root — so the page names a
|
|
399
|
+
// document, never a place on disk. Same defence-in-depth as `/__editor/
|
|
400
|
+
// data-file`, whose write half accepts only `src/data/**/*.data.json`, and
|
|
401
|
+
// the same reason: a purpose-scoped route beats widening a general one.
|
|
402
|
+
//
|
|
403
|
+
// The write is `commitProjectMutation` — the project's one attributed,
|
|
404
|
+
// conflict-checked transaction. A `.blend` is the project's file like any
|
|
405
|
+
// other, and there is deliberately no second write path.
|
|
406
|
+
//
|
|
407
|
+
// No provenance record, deliberately: the ledger is about what the project
|
|
408
|
+
// SHIPS and `public/` is what ships (see `vgai blender-mcp`'s Mirror). A
|
|
409
|
+
// document is the source the shipped artifact is exported FROM.
|
|
410
|
+
const isDocumentPath = (path: string): boolean => {
|
|
411
|
+
if (!path.endsWith('.blend') || path.startsWith('/') || path.includes('\\')) return false;
|
|
412
|
+
if (!isContainedRelativePath(path)) return false;
|
|
413
|
+
return path.split('/').every((segment) => segment !== '' && !segment.startsWith('.'));
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
router.post('/__editor/blender-document', async (req: Request, res: Response) => {
|
|
417
|
+
const path = (req.query['path'] as string) ?? '';
|
|
418
|
+
if (!isDocumentPath(path)) {
|
|
419
|
+
res.status(400).json({
|
|
420
|
+
error:
|
|
421
|
+
'A Blender document is a project-relative .blend path with no traversal ' +
|
|
422
|
+
`(models/model.blend); got ${JSON.stringify(path)}.`,
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (ctx.projectRoot === ctx.engineRoot) {
|
|
427
|
+
res.status(404).json({ error: 'No project open, so there is nowhere to save the document.' });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const chunks: Buffer[] = [];
|
|
431
|
+
let bytes = 0;
|
|
432
|
+
for await (const value of req) {
|
|
433
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
434
|
+
bytes += chunk.byteLength;
|
|
435
|
+
chunks.push(chunk);
|
|
436
|
+
}
|
|
437
|
+
// A zero-byte .blend is not a document; writing one would replace a good
|
|
438
|
+
// file with an empty one at exactly the moment something went wrong.
|
|
439
|
+
if (bytes === 0) {
|
|
440
|
+
res.status(400).json({ error: 'The Blender document body was empty; nothing was written.' });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const revision = await ctx.commitProjectMutation(req, [
|
|
445
|
+
{ path, content: Buffer.concat(chunks) },
|
|
446
|
+
]);
|
|
447
|
+
res.json({ ok: true, bytes, revision: revision?.revision ?? null });
|
|
448
|
+
} catch (error) {
|
|
449
|
+
ctx.projectMutationError(res, error);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function registerRelayRoutes(
|
|
455
|
+
router: EditorServerRouter,
|
|
456
|
+
ctx: RouteContext,
|
|
457
|
+
plane: ControlPlane,
|
|
458
|
+
): void {
|
|
459
|
+
registerBlenderFileRoutes(router, ctx);
|
|
460
|
+
const { consoleLedger, engineRoot, journalEvent, trustedShareIdentity } = ctx;
|
|
461
|
+
const {
|
|
462
|
+
handleCommandListener,
|
|
463
|
+
handleCommandReceived,
|
|
464
|
+
handleCommandResult,
|
|
465
|
+
handleConsoleEntries,
|
|
466
|
+
handleConsoleResolved,
|
|
467
|
+
handlePageError,
|
|
468
|
+
handleContributedCommands,
|
|
469
|
+
handlePlayPhase,
|
|
470
|
+
MAX_RECORDING_CHUNK_BYTES,
|
|
471
|
+
openGameplayRecording,
|
|
472
|
+
openGameplayRecordings,
|
|
473
|
+
relayCommand,
|
|
474
|
+
} = plane;
|
|
475
|
+
|
|
476
|
+
// small for a full QA playthrough.
|
|
477
|
+
router.post('/__editor/recording/start', async (req: Request, res: Response) => {
|
|
478
|
+
if (ctx.projectRoot === engineRoot) {
|
|
479
|
+
res.status(409).json({ error: 'Open a game project before recording gameplay.' });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const startedAt = req.body?.startedAt;
|
|
483
|
+
const mimeType = req.body?.mimeType;
|
|
484
|
+
if (
|
|
485
|
+
typeof startedAt !== 'string' ||
|
|
486
|
+
!Number.isFinite(Date.parse(startedAt)) ||
|
|
487
|
+
typeof mimeType !== 'string' ||
|
|
488
|
+
!mimeType.startsWith('video/webm')
|
|
489
|
+
) {
|
|
490
|
+
res
|
|
491
|
+
.status(400)
|
|
492
|
+
.json({ error: 'Recording start requires a WebM mimeType and ISO start time.' });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const purpose = req.body?.purpose;
|
|
496
|
+
if (purpose !== undefined && purpose !== 'export') {
|
|
497
|
+
res.status(400).json({ error: 'Unknown recording purpose.' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const format = req.body?.format ?? 'composite-webm';
|
|
501
|
+
if (format !== 'composite-webm' && format !== 'canvas-dom') {
|
|
502
|
+
res.status(400).json({ error: 'Unknown gameplay recording format.' });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (purpose === 'export' && format !== 'composite-webm') {
|
|
506
|
+
res.status(400).json({ error: 'Fixed-step export requires composite-webm.' });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const id = randomUUID();
|
|
510
|
+
// WHERE A RECORDING LIVES, AND HOW LONG.
|
|
511
|
+
//
|
|
512
|
+
// Two ownership rules, because the disk cost of "always record" has to be
|
|
513
|
+
// bounded by the DESIGN rather than by a cleanup chore nobody runs.
|
|
514
|
+
//
|
|
515
|
+
// - Inside a Gameplay Session, an unnamed clip takes the log's stem. It
|
|
516
|
+
// participates in the managed clip BYTE budget; the durable log has a
|
|
517
|
+
// separate, much larger history and survives ordinary clip eviction.
|
|
518
|
+
// - Outside a Gameplay Session, the unnamed fallback is the single
|
|
519
|
+
// rotating `play-latest.webm`.
|
|
520
|
+
// - Named (`vgai play --record <name>`): the caller's own keepsake, and
|
|
521
|
+
// NOTHING rotates it. Reusing a name overwrites that name's own file —
|
|
522
|
+
// an explicit instruction, unlike rotation — so a name is also how you
|
|
523
|
+
// keep a clip past the next play.
|
|
524
|
+
//
|
|
525
|
+
// The directory is gitignored in the scaffold (`.vgai/recordings/`), so
|
|
526
|
+
// neither kind is ever a commit's problem.
|
|
527
|
+
const slug = playRunSlug(req.body?.name);
|
|
528
|
+
if (purpose === 'export' && slug === null) {
|
|
529
|
+
res.status(400).json({ error: 'Video export requires a non-empty filename.' });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const sessionStem = ctx.activeLogFile
|
|
533
|
+
? basename(ctx.activeLogFile, extname(ctx.activeLogFile))
|
|
534
|
+
: null;
|
|
535
|
+
const path = resolve(
|
|
536
|
+
ctx.projectRoot,
|
|
537
|
+
'.vgai',
|
|
538
|
+
'recordings',
|
|
539
|
+
slug === null && sessionStem !== null
|
|
540
|
+
? `${sessionStem}.webm`
|
|
541
|
+
: `${slug ?? 'play-latest'}.webm`,
|
|
542
|
+
);
|
|
543
|
+
const replayPath =
|
|
544
|
+
format === 'canvas-dom' ? `${path.slice(0, -extname(path).length)}.replay` : null;
|
|
545
|
+
// Export must not replace a source capture and delete its DOM sidecar.
|
|
546
|
+
if (purpose === 'export' && existsSync(`${path.slice(0, -extname(path).length)}.replay`)) {
|
|
547
|
+
res
|
|
548
|
+
.status(409)
|
|
549
|
+
.json({ error: 'Export name belongs to a source replay. Choose a different name.' });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
await mkdir(dirname(path), { recursive: true });
|
|
553
|
+
// Truncate the chosen target before chunks append. For a session-derived
|
|
554
|
+
// name it is a fresh file; for an explicit reused name it is intentional.
|
|
555
|
+
await writeFile(path, new Uint8Array());
|
|
556
|
+
await rm(`${path.slice(0, -extname(path).length)}.replay`, { recursive: true, force: true });
|
|
557
|
+
if (replayPath !== null) {
|
|
558
|
+
await mkdir(join(replayPath, 'assets'), { recursive: true });
|
|
559
|
+
await writeFile(join(replayPath, 'events.json'), '[', 'utf-8');
|
|
560
|
+
await writeFile(
|
|
561
|
+
join(replayPath, 'manifest.json'),
|
|
562
|
+
`${JSON.stringify({ version: 1, status: 'recording', format, startedAt }, null, 2)}\n`,
|
|
563
|
+
'utf-8',
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
openGameplayRecordings.set(id, {
|
|
567
|
+
path,
|
|
568
|
+
format,
|
|
569
|
+
replayPath,
|
|
570
|
+
nextSequence: 0,
|
|
571
|
+
nextDomSequence: 0,
|
|
572
|
+
domEventCount: 0,
|
|
573
|
+
domEventsStarted: false,
|
|
574
|
+
replayAssets: new Set(),
|
|
575
|
+
...(purpose ? { purpose } : {}),
|
|
576
|
+
});
|
|
577
|
+
if (ctx.activeLogFile !== null) {
|
|
578
|
+
await appendFile(
|
|
579
|
+
ctx.activeLogFile,
|
|
580
|
+
`${JSON.stringify({
|
|
581
|
+
t: Date.now(),
|
|
582
|
+
level: 'info',
|
|
583
|
+
source: purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
|
|
584
|
+
msg: 'started',
|
|
585
|
+
meta: { file: basename(path), format, startedAt: Date.parse(startedAt) },
|
|
586
|
+
})}\n`,
|
|
587
|
+
'utf-8',
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
journalEvent({
|
|
591
|
+
kind: 'play-recording',
|
|
592
|
+
action: 'started',
|
|
593
|
+
file: basename(path),
|
|
594
|
+
rotates: slug === null && sessionStem === null,
|
|
595
|
+
reason: null,
|
|
596
|
+
});
|
|
597
|
+
res.json({
|
|
598
|
+
id,
|
|
599
|
+
path,
|
|
600
|
+
format,
|
|
601
|
+
replayPath,
|
|
602
|
+
rotates: slug === null && sessionStem === null,
|
|
603
|
+
logFile: ctx.activeLogFile === null ? null : basename(ctx.activeLogFile),
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
router.post('/__editor/recording/dom', async (req: Request, res: Response) => {
|
|
608
|
+
const recording = openGameplayRecording(req.query['id']);
|
|
609
|
+
const sequence = Number(req.query['sequence']);
|
|
610
|
+
if (!recording) {
|
|
611
|
+
res.status(404).json({ error: 'No open gameplay recording has that id.' });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (recording.format !== 'canvas-dom' || recording.replayPath === null) {
|
|
615
|
+
res.status(409).json({ error: 'This recording has no DOM replay stream.' });
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!Number.isInteger(sequence) || sequence !== recording.nextDomSequence) {
|
|
619
|
+
res.status(409).json({
|
|
620
|
+
error: `DOM event sequence ${String(req.query['sequence'])} arrived; expected ${recording.nextDomSequence}.`,
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const chunks: Buffer[] = [];
|
|
625
|
+
let bytes = 0;
|
|
626
|
+
for await (const value of req) {
|
|
627
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
628
|
+
bytes += chunk.byteLength;
|
|
629
|
+
if (bytes > MAX_RECORDING_CHUNK_BYTES) {
|
|
630
|
+
res.status(413).json({
|
|
631
|
+
error: `DOM event batches are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
chunks.push(chunk);
|
|
636
|
+
}
|
|
637
|
+
const text = Buffer.concat(chunks, bytes).toString('utf-8').trim();
|
|
638
|
+
let events: unknown;
|
|
639
|
+
try {
|
|
640
|
+
events = JSON.parse(text);
|
|
641
|
+
} catch {
|
|
642
|
+
res.status(400).json({ error: 'DOM event batch was not valid JSON.' });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
646
|
+
res.status(400).json({ error: 'DOM event batch must be a non-empty array.' });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const serialized = JSON.stringify(events).slice(1, -1);
|
|
650
|
+
await appendFile(
|
|
651
|
+
join(recording.replayPath, 'events.json'),
|
|
652
|
+
`${recording.domEventsStarted ? ',' : ''}${serialized}`,
|
|
653
|
+
'utf-8',
|
|
654
|
+
);
|
|
655
|
+
recording.domEventsStarted = true;
|
|
656
|
+
recording.domEventCount += events.length;
|
|
657
|
+
recording.nextDomSequence += 1;
|
|
658
|
+
res.json({ ok: true });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
router.post('/__editor/recording/asset', async (req: Request, res: Response) => {
|
|
662
|
+
const recording = openGameplayRecording(req.query['id']);
|
|
663
|
+
const name = req.query['name'];
|
|
664
|
+
if (!recording) {
|
|
665
|
+
res.status(404).json({ error: 'No open gameplay recording has that id.' });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (recording.format !== 'canvas-dom' || recording.replayPath === null) {
|
|
669
|
+
res.status(409).json({ error: 'This recording has no replay asset store.' });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (typeof name !== 'string' || !/^\d{6}\.(?:png|jpg|webp|gif|svg)$/.test(name)) {
|
|
673
|
+
res.status(400).json({ error: 'Replay asset name is invalid.' });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (recording.replayAssets.has(name)) {
|
|
677
|
+
res.status(409).json({ error: 'Replay asset already exists.' });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const chunks: Buffer[] = [];
|
|
681
|
+
let bytes = 0;
|
|
682
|
+
for await (const value of req) {
|
|
683
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
684
|
+
bytes += chunk.byteLength;
|
|
685
|
+
if (bytes > MAX_RECORDING_CHUNK_BYTES) {
|
|
686
|
+
res.status(413).json({
|
|
687
|
+
error: `Replay assets are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
chunks.push(chunk);
|
|
692
|
+
}
|
|
693
|
+
if (bytes === 0) {
|
|
694
|
+
res.status(400).json({ error: 'Replay asset was empty.' });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
await writeFile(join(recording.replayPath, 'assets', name), Buffer.concat(chunks, bytes));
|
|
698
|
+
recording.replayAssets.add(name);
|
|
699
|
+
res.json({ ok: true });
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
router.post('/__editor/recording/chunk', async (req: Request, res: Response) => {
|
|
703
|
+
const recording = openGameplayRecording(req.query['id']);
|
|
704
|
+
const sequence = Number(req.query['sequence']);
|
|
705
|
+
if (!recording) {
|
|
706
|
+
res.status(404).json({ error: 'No open gameplay recording has that id.' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (!Number.isInteger(sequence) || sequence !== recording.nextSequence) {
|
|
710
|
+
res.status(409).json({
|
|
711
|
+
error: `Recording chunk sequence ${String(req.query['sequence'])} arrived; expected ${recording.nextSequence}.`,
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const chunks: Buffer[] = [];
|
|
716
|
+
let bytes = 0;
|
|
717
|
+
for await (const value of req) {
|
|
718
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
719
|
+
bytes += chunk.byteLength;
|
|
720
|
+
if (bytes > MAX_RECORDING_CHUNK_BYTES) {
|
|
721
|
+
res.status(413).json({
|
|
722
|
+
error: `Recording chunks are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
chunks.push(chunk);
|
|
727
|
+
}
|
|
728
|
+
if (bytes === 0) {
|
|
729
|
+
res.status(400).json({ error: 'Recording chunk was empty.' });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await appendFile(recording.path, Buffer.concat(chunks, bytes));
|
|
733
|
+
recording.nextSequence += 1;
|
|
734
|
+
res.json({ ok: true });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
router.post('/__editor/recording/finish', async (req: Request, res: Response) => {
|
|
738
|
+
const id = req.body?.id;
|
|
739
|
+
const recording = openGameplayRecording(id);
|
|
740
|
+
if (!recording || typeof id !== 'string') {
|
|
741
|
+
res.status(404).json({ error: 'No open gameplay recording has that id.' });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const file = await stat(recording.path);
|
|
745
|
+
if (file.size === 0) {
|
|
746
|
+
res.status(409).json({ error: 'The browser finalized an empty gameplay recording.' });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (recording.format === 'canvas-dom') {
|
|
750
|
+
const replay = req.body?.replay;
|
|
751
|
+
if (
|
|
752
|
+
recording.replayPath === null ||
|
|
753
|
+
!replay ||
|
|
754
|
+
typeof replay !== 'object' ||
|
|
755
|
+
replay.version !== 1 ||
|
|
756
|
+
typeof replay.eventCount !== 'number' ||
|
|
757
|
+
replay.eventCount !== recording.domEventCount ||
|
|
758
|
+
typeof replay.assetCount !== 'number' ||
|
|
759
|
+
replay.assetCount !== recording.replayAssets.size
|
|
760
|
+
) {
|
|
761
|
+
res.status(409).json({
|
|
762
|
+
error: 'The canvas-dom replay manifest does not match its uploaded events and assets.',
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await appendFile(join(recording.replayPath, 'events.json'), ']', 'utf-8');
|
|
767
|
+
await writeFile(
|
|
768
|
+
join(recording.replayPath, 'manifest.json'),
|
|
769
|
+
`${JSON.stringify(
|
|
770
|
+
{
|
|
771
|
+
...replay,
|
|
772
|
+
video: basename(recording.path),
|
|
773
|
+
status: 'complete',
|
|
774
|
+
format: recording.format,
|
|
775
|
+
completedAt: new Date().toISOString(),
|
|
776
|
+
},
|
|
777
|
+
null,
|
|
778
|
+
2,
|
|
779
|
+
)}\n`,
|
|
780
|
+
'utf-8',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
openGameplayRecordings.delete(id);
|
|
784
|
+
// The finalized clip lands in the journal so "where is the evidence for
|
|
785
|
+
// that run" is answerable from the session record alone — including for a
|
|
786
|
+
// run nobody was watching, which is the case this whole path exists for.
|
|
787
|
+
const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
|
|
788
|
+
if (ctx.activeLogFile !== null) {
|
|
789
|
+
await appendFile(
|
|
790
|
+
ctx.activeLogFile,
|
|
791
|
+
`${JSON.stringify({
|
|
792
|
+
t: Date.now(),
|
|
793
|
+
level: 'info',
|
|
794
|
+
source: recording.purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
|
|
795
|
+
msg: 'finalized',
|
|
796
|
+
meta: {
|
|
797
|
+
file: basename(recording.path),
|
|
798
|
+
bytes: file.size,
|
|
799
|
+
format: recording.format,
|
|
800
|
+
replay: recording.replayPath === null ? null : basename(recording.replayPath),
|
|
801
|
+
reason,
|
|
802
|
+
},
|
|
803
|
+
})}\n`,
|
|
804
|
+
'utf-8',
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
journalEvent({
|
|
808
|
+
kind: 'play-recording',
|
|
809
|
+
action: 'finalized',
|
|
810
|
+
file: basename(recording.path),
|
|
811
|
+
rotates: basename(recording.path) === 'play-latest.webm',
|
|
812
|
+
reason,
|
|
813
|
+
});
|
|
814
|
+
await pruneGameplaySessionClips(ctx.projectRoot);
|
|
815
|
+
res.json({ ok: true, path: recording.path, bytes: file.size });
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
router.post('/__editor/recording/abort', async (req: Request, res: Response) => {
|
|
819
|
+
const id = req.body?.id;
|
|
820
|
+
const recording = openGameplayRecording(id);
|
|
821
|
+
if (recording && typeof id === 'string') {
|
|
822
|
+
openGameplayRecordings.delete(id);
|
|
823
|
+
if (ctx.activeLogFile !== null) {
|
|
824
|
+
await appendFile(
|
|
825
|
+
ctx.activeLogFile,
|
|
826
|
+
`${JSON.stringify({
|
|
827
|
+
t: Date.now(),
|
|
828
|
+
level: 'info',
|
|
829
|
+
source: recording.purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
|
|
830
|
+
msg: 'aborted',
|
|
831
|
+
meta: { file: basename(recording.path) },
|
|
832
|
+
})}\n`,
|
|
833
|
+
'utf-8',
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
await unlink(recording.path).catch(() => undefined);
|
|
837
|
+
if (recording.replayPath !== null) {
|
|
838
|
+
await rm(recording.replayPath, { recursive: true, force: true }).catch(() => undefined);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
res.json({ ok: true });
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
/** Serve one completed local replay artifact to the editor's in-page viewer.
|
|
845
|
+
* The caller names a project recording or its absolute path; containment rejects
|
|
846
|
+
* symlinks and every path outside this project's recording directory. */
|
|
847
|
+
router.get(
|
|
848
|
+
'/__editor/recording/replay-file',
|
|
849
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
850
|
+
const replay = req.query['replay'];
|
|
851
|
+
const file = req.query['file'];
|
|
852
|
+
if (
|
|
853
|
+
typeof replay !== 'string' ||
|
|
854
|
+
!/^[a-zA-Z0-9._-]+\.replay$/.test(basename(replay)) ||
|
|
855
|
+
typeof file !== 'string'
|
|
856
|
+
) {
|
|
857
|
+
res.status(400).json({ error: 'Replay file request is invalid.' });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const recordingsRoot = resolve(ctx.projectRoot, '.vgai', 'recordings');
|
|
861
|
+
const replayRoot =
|
|
862
|
+
replay === basename(replay)
|
|
863
|
+
? resolve(recordingsRoot, replay)
|
|
864
|
+
: resolve(ctx.projectRoot, replay);
|
|
865
|
+
if (
|
|
866
|
+
dirname(replayRoot) !== recordingsRoot ||
|
|
867
|
+
!(await isCanonicalPathInside(recordingsRoot, replayRoot))
|
|
868
|
+
) {
|
|
869
|
+
res.status(400).json({
|
|
870
|
+
error:
|
|
871
|
+
'Replay must be inside this project’s .vgai/recordings directory; use the original recording, not an external copy.',
|
|
872
|
+
});
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const manifestPath = join(replayRoot, 'manifest.json');
|
|
876
|
+
const manifest = await readFile(manifestPath, 'utf-8')
|
|
877
|
+
.then((text) => JSON.parse(text) as { status?: unknown; video?: unknown })
|
|
878
|
+
.catch(() => null);
|
|
879
|
+
if (manifest?.status !== 'complete' || typeof manifest.video !== 'string') {
|
|
880
|
+
res.status(404).json({ error: 'Completed replay not found.' });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
let path: string | null = null;
|
|
884
|
+
if (file === 'manifest.json') path = manifestPath;
|
|
885
|
+
else if (file === 'events.json') path = join(replayRoot, 'events.json');
|
|
886
|
+
else if (file === 'video') path = join(recordingsRoot, manifest.video);
|
|
887
|
+
else if (/^assets\/\d{6}\.(?:png|jpg|webp|gif|svg)$/.test(file)) {
|
|
888
|
+
path = join(replayRoot, file);
|
|
889
|
+
}
|
|
890
|
+
if (path === null || !(await isCanonicalPathInside(recordingsRoot, path))) {
|
|
891
|
+
res.status(404).json({ error: 'Replay file not found.' });
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
res.sendFile(path, { dotfiles: 'allow' }, (error: Error | undefined) => {
|
|
895
|
+
if (!error) return;
|
|
896
|
+
if (!res.headersSent) res.status(404).json({ error: 'Replay file not found.' });
|
|
897
|
+
else next(error);
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
router.post('/__editor/command', (req: Request, res: Response) => {
|
|
903
|
+
relayCommand(
|
|
904
|
+
req.body as Record<string, unknown>,
|
|
905
|
+
trustedShareIdentity(req)?.participantId,
|
|
906
|
+
).then(({ result, callerReceipt }) => {
|
|
907
|
+
if (callerReceipt) res.once('finish', callerReceipt);
|
|
908
|
+
const { status, body } = commandResponseFor(result, consoleLedger.summary());
|
|
909
|
+
res.status(status).json(body);
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// ---- Editor command result (browser reports back) ----
|
|
914
|
+
router.post('/__editor/command-result', (req: Request, res: Response) => {
|
|
915
|
+
const outcome = handleCommandResult(req.body as Record<string, unknown>);
|
|
916
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
917
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// ---- Delivery receipt (browser reports it PICKED THE COMMAND UP) ----
|
|
921
|
+
//
|
|
922
|
+
// The other half of `relayCommandAckDeadlineMs`: the tab sends this the
|
|
923
|
+
// instant its command handler sees the event, before it starts the work. It
|
|
924
|
+
// only cancels the receipt window — the command's own budget keeps running,
|
|
925
|
+
// so a genuinely slow game boot still gets its full 120s.
|
|
926
|
+
router.post('/__editor/command-received', (req: Request, res: Response) => {
|
|
927
|
+
const outcome = handleCommandReceived((req.body ?? {}) as Record<string, unknown>);
|
|
928
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
929
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// ---- Command-listener presence (browser reports it CAN run commands) ----
|
|
933
|
+
//
|
|
934
|
+
// The POST fallback for pages with no upstream socket (the share tunnel's
|
|
935
|
+
// one-way SSE bridge). See `src/editor-api.ts`'s `reportCommandListener`.
|
|
936
|
+
router.post('/__editor/command-listener', (req: Request, res: Response) => {
|
|
937
|
+
const outcome = handleCommandListener((req.body ?? {}) as Record<string, unknown>);
|
|
938
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
939
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// The POST twin of the `page-error` control frame, for the same reason the
|
|
943
|
+
// one above has one: a page whose control socket never came up (the share
|
|
944
|
+
// tunnel's one-way SSE bridge, or a boot that died before the upgrade) must
|
|
945
|
+
// still be able to say what killed it. `sendBeacon`-friendly.
|
|
946
|
+
router.post('/__editor/contributed-commands', (req: Request, res: Response) => {
|
|
947
|
+
const outcome = handleContributedCommands((req.body ?? {}) as Record<string, unknown>);
|
|
948
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
949
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
router.post('/__editor/page-error', (req: Request, res: Response) => {
|
|
953
|
+
const outcome = handlePageError((req.body ?? {}) as Record<string, unknown>);
|
|
954
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
955
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// The POST twin of the `play-phase` frame — same reason as the ones above.
|
|
959
|
+
router.post('/__editor/play-phase', (req: Request, res: Response) => {
|
|
960
|
+
const outcome = handlePlayPhase((req.body ?? {}) as Record<string, unknown>);
|
|
961
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
962
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// The POST twin of the `console-entries` frame — same reason as the two
|
|
966
|
+
// above (a page with no upstream socket must still be able to report).
|
|
967
|
+
router.post('/__editor/console-entries', (req: Request, res: Response) => {
|
|
968
|
+
const outcome = handleConsoleEntries((req.body ?? {}) as Record<string, unknown>);
|
|
969
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
970
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Clearing rule (c): the owner of a condition reporting it resolved. The POST
|
|
974
|
+
// twin exists for the same reason as the frame above.
|
|
975
|
+
router.post('/__editor/console-resolved', (req: Request, res: Response) => {
|
|
976
|
+
const outcome = handleConsoleResolved((req.body ?? {}) as Record<string, unknown>);
|
|
977
|
+
if (outcome.status === 200) res.json({ ok: true });
|
|
978
|
+
else res.status(outcome.status).json({ error: outcome.error });
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// ---- The unresolved console set (CLI reads; `vgai status` prints in full) ----
|
|
982
|
+
//
|
|
983
|
+
// The counts ride every command envelope (`commandResponseFor`), so this
|
|
984
|
+
// route exists for the DETAIL: the complete list of distinct conditions with
|
|
985
|
+
// their occurrence counts, which is what `vgai status` / `vgai console`
|
|
986
|
+
// print. Deliberately a plain GET with no tab cooperation — the whole point
|
|
987
|
+
// is that it answers when the tab is dead.
|
|
988
|
+
router.get('/__editor/console', (req: Request, res: Response) => {
|
|
989
|
+
const all = req.query['all'] === '1' || req.query['all'] === 'true';
|
|
990
|
+
res.json({
|
|
991
|
+
unresolvedConsole: consoleLedger.summary(),
|
|
992
|
+
entries: all ? consoleLedger.all() : consoleLedger.unresolved(),
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Clearing rule (b): a NAMED acknowledgment, with who and why. It never
|
|
997
|
+
// deletes the record — the entry stays, marked, and a `console-ack` row goes
|
|
998
|
+
// into the session ctx.journal — because the point of an acknowledgment is that
|
|
999
|
+
// somebody can later read what was waved through and on whose say-so.
|
|
1000
|
+
router.post('/__editor/console/ack', (req: Request, res: Response) => {
|
|
1001
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
1002
|
+
const id = typeof body['id'] === 'string' ? body['id'].trim() : '';
|
|
1003
|
+
const reason = typeof body['reason'] === 'string' ? body['reason'].trim() : '';
|
|
1004
|
+
const by =
|
|
1005
|
+
typeof body['by'] === 'string' && body['by'].trim() !== '' ? body['by'].trim() : null;
|
|
1006
|
+
if (id === '') {
|
|
1007
|
+
res.status(400).json({ ok: false, error: 'console ack requires { id }.' });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (reason === '') {
|
|
1011
|
+
res.status(400).json({
|
|
1012
|
+
ok: false,
|
|
1013
|
+
error:
|
|
1014
|
+
'console ack requires { reason } — an acknowledgment with no stated reason is a silent reset, ' +
|
|
1015
|
+
'which is the exact failure this ledger exists to prevent.',
|
|
1016
|
+
});
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const acked = consoleLedger.ack(id, { by: by ?? 'unknown', reason });
|
|
1020
|
+
if (acked === null) {
|
|
1021
|
+
res.status(404).json({
|
|
1022
|
+
ok: false,
|
|
1023
|
+
error: `No unresolved console entry with id "${id}". Run \`volter-editor status\` for the current set.`,
|
|
1024
|
+
});
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
res.json({ unresolvedConsole: consoleLedger.summary(), ok: true, entry: acked });
|
|
1028
|
+
});
|
|
1029
|
+
}
|