@vgai/editor 0.2.0 → 0.4.0-canary.20260715.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/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-operation discovery and execution.
|
|
3
|
+
*
|
|
4
|
+
* Project modules stay ordinary TS/JS. This server-side boundary is the only
|
|
5
|
+
* automatic loader: it recursively scans src/operations for operation modules, asks the
|
|
6
|
+
* owning Vite server to load each module in Node, validates the exported
|
|
7
|
+
* OperationDefinition, and dispatches through the existing SDK registry.
|
|
8
|
+
* Nothing here imports an operation into the editor browser or a game build.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Dirent } from 'node:fs';
|
|
12
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { OperationDefinition, OperationOutcome } from '../../vgai-sdk/src/registry';
|
|
17
|
+
import { OperationRegistry } from '../../vgai-sdk/src/registry';
|
|
18
|
+
import { createProjectOutputWriter } from './project-output-writer';
|
|
19
|
+
|
|
20
|
+
export type ProjectModuleLoader = (absoluteModuleId: string) => Promise<unknown>;
|
|
21
|
+
|
|
22
|
+
export interface ProjectOperationErrorSummary {
|
|
23
|
+
code: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
dataSchema?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProjectOperationCatalogEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
description: string;
|
|
32
|
+
sourcePath: string;
|
|
33
|
+
inputSchema: unknown;
|
|
34
|
+
resultSchema: unknown;
|
|
35
|
+
errors: ProjectOperationErrorSummary[];
|
|
36
|
+
requires: Record<string, boolean | undefined>;
|
|
37
|
+
host: 'node' | 'editor-browser' | 'runtime-page';
|
|
38
|
+
mutates: boolean;
|
|
39
|
+
supportsDryRun: boolean;
|
|
40
|
+
longRunning: boolean;
|
|
41
|
+
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProjectOperationLoadError {
|
|
45
|
+
sourcePath: string;
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProjectOperationCatalog {
|
|
50
|
+
operations: ProjectOperationCatalogEntry[];
|
|
51
|
+
loadErrors: ProjectOperationLoadError[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LoadedCatalog extends ProjectOperationCatalog {
|
|
55
|
+
definitions: Map<string, OperationDefinition>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectPath(projectRoot: string, absolutePath: string): string {
|
|
59
|
+
return relative(projectRoot, absolutePath).split(sep).join('/');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function findOperationFiles(dir: string): Promise<string[]> {
|
|
63
|
+
const found: string[] = [];
|
|
64
|
+
let entries: Dirent[];
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
67
|
+
} catch {
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.name.startsWith('.')) continue;
|
|
72
|
+
const absolute = resolve(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
found.push(...(await findOperationFiles(absolute)));
|
|
75
|
+
} else if (entry.name.endsWith('.operation.ts') || entry.name.endsWith('.operation.js')) {
|
|
76
|
+
found.push(absolute);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface OperationFile {
|
|
83
|
+
absolutePath: string;
|
|
84
|
+
sourcePath: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface PackageJsonShape {
|
|
88
|
+
name?: string;
|
|
89
|
+
dependencies?: Record<string, string>;
|
|
90
|
+
devDependencies?: Record<string, string>;
|
|
91
|
+
vgai?: { operations?: string[] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function readPackageJson(path: string): Promise<PackageJsonShape | null> {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(await readFile(path, 'utf8')) as PackageJsonShape;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveDependencyPackageJson(
|
|
103
|
+
requireFromProject: ReturnType<typeof createRequire>,
|
|
104
|
+
dependencyName: string,
|
|
105
|
+
): string | null {
|
|
106
|
+
try {
|
|
107
|
+
return requireFromProject.resolve(`${dependencyName}/package.json`);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function advertisedDependencyOperations(
|
|
114
|
+
requireFromProject: ReturnType<typeof createRequire>,
|
|
115
|
+
dependencyName: string,
|
|
116
|
+
): Promise<OperationFile[]> {
|
|
117
|
+
const packageJsonPath = resolveDependencyPackageJson(requireFromProject, dependencyName);
|
|
118
|
+
if (!packageJsonPath) return [];
|
|
119
|
+
const dependencyPackage = await readPackageJson(packageJsonPath);
|
|
120
|
+
const specs = dependencyPackage?.vgai?.operations;
|
|
121
|
+
if (!Array.isArray(specs)) return [];
|
|
122
|
+
const packageRoot = dirname(packageJsonPath);
|
|
123
|
+
return specs.flatMap((spec): OperationFile[] => {
|
|
124
|
+
if (typeof spec !== 'string' || !spec.startsWith('./')) return [];
|
|
125
|
+
const absolutePath = resolve(packageRoot, spec);
|
|
126
|
+
if (!absolutePath.startsWith(`${packageRoot}${sep}`)) return [];
|
|
127
|
+
return [{ absolutePath, sourcePath: `${dependencyName}:${spec.slice(2)}` }];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function dependencyOperationFiles(projectRoot: string): Promise<OperationFile[]> {
|
|
132
|
+
const projectPackage = await readPackageJson(resolve(projectRoot, 'package.json'));
|
|
133
|
+
if (!projectPackage) return [];
|
|
134
|
+
const dependencyNames = [
|
|
135
|
+
...Object.keys(projectPackage.dependencies ?? {}),
|
|
136
|
+
...Object.keys(projectPackage.devDependencies ?? {}),
|
|
137
|
+
].sort();
|
|
138
|
+
const requireFromProject = createRequire(resolve(projectRoot, 'package.json'));
|
|
139
|
+
return (
|
|
140
|
+
await Promise.all(
|
|
141
|
+
dependencyNames.map((name) => advertisedDependencyOperations(requireFromProject, name)),
|
|
142
|
+
)
|
|
143
|
+
).flat();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function operationShape(value: unknown): value is OperationDefinition {
|
|
147
|
+
if (value === null || typeof value !== 'object') return false;
|
|
148
|
+
const candidate = value as Partial<OperationDefinition>;
|
|
149
|
+
return (
|
|
150
|
+
typeof candidate.name === 'string' &&
|
|
151
|
+
typeof candidate.summary === 'string' &&
|
|
152
|
+
typeof candidate.description === 'string' &&
|
|
153
|
+
candidate.input !== null &&
|
|
154
|
+
typeof candidate.input === 'object' &&
|
|
155
|
+
typeof candidate.input.safeParse === 'function' &&
|
|
156
|
+
candidate.result !== null &&
|
|
157
|
+
typeof candidate.result === 'object' &&
|
|
158
|
+
typeof candidate.result.safeParse === 'function' &&
|
|
159
|
+
Array.isArray(candidate.errors) &&
|
|
160
|
+
candidate.requires !== null &&
|
|
161
|
+
typeof candidate.requires === 'object' &&
|
|
162
|
+
(candidate.host === 'node' ||
|
|
163
|
+
candidate.host === 'editor-browser' ||
|
|
164
|
+
candidate.host === 'runtime-page') &&
|
|
165
|
+
typeof candidate.mutates === 'boolean' &&
|
|
166
|
+
typeof candidate.supportsDryRun === 'boolean' &&
|
|
167
|
+
candidate.permission !== null &&
|
|
168
|
+
typeof candidate.permission === 'object' &&
|
|
169
|
+
typeof candidate.impl === 'function'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function jsonSchema(schema: z.ZodType): unknown {
|
|
174
|
+
return z.toJSONSchema(schema);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toCatalogEntry(
|
|
178
|
+
definition: OperationDefinition,
|
|
179
|
+
sourcePath: string,
|
|
180
|
+
): ProjectOperationCatalogEntry {
|
|
181
|
+
return {
|
|
182
|
+
name: definition.name,
|
|
183
|
+
summary: definition.summary,
|
|
184
|
+
description: definition.description,
|
|
185
|
+
sourcePath,
|
|
186
|
+
inputSchema: jsonSchema(definition.input),
|
|
187
|
+
resultSchema: jsonSchema(definition.result),
|
|
188
|
+
errors: definition.errors.map((error) => ({
|
|
189
|
+
code: error.code,
|
|
190
|
+
summary: error.summary,
|
|
191
|
+
...(error.data ? { dataSchema: jsonSchema(error.data) } : {}),
|
|
192
|
+
})),
|
|
193
|
+
requires: { ...definition.requires },
|
|
194
|
+
host: definition.host,
|
|
195
|
+
mutates: definition.mutates,
|
|
196
|
+
supportsDryRun: definition.supportsDryRun,
|
|
197
|
+
longRunning: definition.longRunning === true,
|
|
198
|
+
permission: { ...definition.permission },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function loadCatalog(
|
|
203
|
+
projectRoot: string,
|
|
204
|
+
loadModule: ProjectModuleLoader | undefined,
|
|
205
|
+
): Promise<LoadedCatalog> {
|
|
206
|
+
const definitions = new Map<string, OperationDefinition>();
|
|
207
|
+
const operations: ProjectOperationCatalogEntry[] = [];
|
|
208
|
+
const loadErrors: ProjectOperationLoadError[] = [];
|
|
209
|
+
const projectFiles = (await findOperationFiles(resolve(projectRoot, 'src', 'operations')))
|
|
210
|
+
.sort()
|
|
211
|
+
.map((absolutePath) => ({ absolutePath, sourcePath: projectPath(projectRoot, absolutePath) }));
|
|
212
|
+
const files = [...projectFiles, ...(await dependencyOperationFiles(projectRoot))].sort((a, b) =>
|
|
213
|
+
a.sourcePath.localeCompare(b.sourcePath),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (files.length > 0 && !loadModule) {
|
|
217
|
+
loadErrors.push({
|
|
218
|
+
sourcePath: 'src/operations',
|
|
219
|
+
message:
|
|
220
|
+
'This editor host cannot load project TypeScript modules. Use the Vite-backed dev or packaged editor.',
|
|
221
|
+
});
|
|
222
|
+
return { operations, loadErrors, definitions };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const registry = new OperationRegistry();
|
|
226
|
+
for (const { absolutePath, sourcePath } of files) {
|
|
227
|
+
try {
|
|
228
|
+
const imported = (await loadModule?.(absolutePath)) as Record<string, unknown> | undefined;
|
|
229
|
+
const definition = imported?.['operation'];
|
|
230
|
+
if (!operationShape(definition)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
'does not export a complete `operation` definition; export `operation = defineOperation({...})` from @vgai/sdk/registry',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (!definition.name.startsWith('project.')) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`declares ${JSON.stringify(definition.name)}; project-local operation names must start with "project."`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (definition.mutates && definition.permission.risk === 'read') {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`declares mutates:true with read risk; mutating operations must declare write or destructive risk`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
registry.register(definition);
|
|
246
|
+
const entry = toCatalogEntry(definition, sourcePath);
|
|
247
|
+
definitions.set(definition.name, definition);
|
|
248
|
+
operations.push(entry);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
loadErrors.push({
|
|
251
|
+
sourcePath,
|
|
252
|
+
message: error instanceof Error ? error.message : String(error),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
operations.sort((a, b) => a.name.localeCompare(b.name));
|
|
258
|
+
return { operations, loadErrors, definitions };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function discoverProjectOperations(
|
|
262
|
+
projectRoot: string,
|
|
263
|
+
loadModule: ProjectModuleLoader | undefined,
|
|
264
|
+
): Promise<ProjectOperationCatalog> {
|
|
265
|
+
const { definitions: _definitions, ...catalog } = await loadCatalog(projectRoot, loadModule);
|
|
266
|
+
return catalog;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export type ProjectOperationExecution =
|
|
270
|
+
| { status: 200; body: OperationOutcome; changedProject: boolean }
|
|
271
|
+
| {
|
|
272
|
+
status: 404 | 409;
|
|
273
|
+
body: { ok: false; error: { code: string; message: string } };
|
|
274
|
+
changedProject: false;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export async function executeProjectOperation(options: {
|
|
278
|
+
projectRoot: string;
|
|
279
|
+
loadModule: ProjectModuleLoader | undefined;
|
|
280
|
+
name: string;
|
|
281
|
+
input: unknown;
|
|
282
|
+
confirmed: boolean;
|
|
283
|
+
signal?: AbortSignal;
|
|
284
|
+
}): Promise<ProjectOperationExecution> {
|
|
285
|
+
const loaded = await loadCatalog(options.projectRoot, options.loadModule);
|
|
286
|
+
const definition = loaded.definitions.get(options.name);
|
|
287
|
+
if (!definition) {
|
|
288
|
+
return {
|
|
289
|
+
status: 404,
|
|
290
|
+
changedProject: false,
|
|
291
|
+
body: {
|
|
292
|
+
ok: false,
|
|
293
|
+
error: {
|
|
294
|
+
code: 'PROJECT_OPERATION_NOT_FOUND',
|
|
295
|
+
message: `No loaded project operation is named ${JSON.stringify(options.name)}.`,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (definition.host !== 'node') {
|
|
301
|
+
return {
|
|
302
|
+
status: 409,
|
|
303
|
+
changedProject: false,
|
|
304
|
+
body: {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: {
|
|
307
|
+
code: 'PROJECT_OPERATION_HOST_UNAVAILABLE',
|
|
308
|
+
message:
|
|
309
|
+
`Project operation ${JSON.stringify(options.name)} declares host ${JSON.stringify(definition.host)}. ` +
|
|
310
|
+
'Slice 1 executes Node-hosted project operations; call reusable browser/runtime functions directly.',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if ((definition.mutates || definition.permission.risk !== 'read') && !options.confirmed) {
|
|
316
|
+
return {
|
|
317
|
+
status: 409,
|
|
318
|
+
changedProject: false,
|
|
319
|
+
body: {
|
|
320
|
+
ok: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: 'PROJECT_OPERATION_CONFIRMATION_REQUIRED',
|
|
323
|
+
message:
|
|
324
|
+
`${definition.permission.risk} operation ${JSON.stringify(options.name)} requires explicit confirmation: ` +
|
|
325
|
+
definition.permission.summary,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const registry = new OperationRegistry();
|
|
332
|
+
registry.register(definition);
|
|
333
|
+
const outcome = await registry.dispatch(options.name, options.input, {
|
|
334
|
+
projectRoot: options.projectRoot,
|
|
335
|
+
projectOutputs: createProjectOutputWriter(options.projectRoot),
|
|
336
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
status: 200,
|
|
340
|
+
body: outcome,
|
|
341
|
+
changedProject: definition.mutates && outcome.ok,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -58,8 +58,8 @@ export const SERVER_ONLY_PREBUNDLE_EXCLUDE: readonly string[] = [
|
|
|
58
58
|
/**
|
|
59
59
|
* The project-relative CLIENT entry files `optimizeDeps.entries` should scope
|
|
60
60
|
* Vite's dependency-DISCOVERY crawl to, for the project rooted at
|
|
61
|
-
* `projectRoot` — every `
|
|
62
|
-
* (default-adapter, code-authored
|
|
61
|
+
* `projectRoot` — every `roots[].entry` its OWN `vgai.game.json` declares
|
|
62
|
+
* (default-adapter, code-authored roots; scene-only roots have no `entry`
|
|
63
63
|
* and contribute nothing here — their gameplay code is registered
|
|
64
64
|
* elsewhere, reached transitively from whichever world DOES declare an
|
|
65
65
|
* `entry`, e.g. the template's single world), deduped, and filtered to paths
|
|
@@ -84,7 +84,7 @@ export const SERVER_ONLY_PREBUNDLE_EXCLUDE: readonly string[] = [
|
|
|
84
84
|
* `packaged.ts`'s ORIGINAL reason for `noDiscovery: true`, verified still
|
|
85
85
|
* true on this Vite version at the time of the C3 fix).
|
|
86
86
|
*
|
|
87
|
-
* A manifest's `
|
|
87
|
+
* A manifest's `roots[].entry` files never reach that chain: they are the
|
|
88
88
|
* PER-WORLD mount code the editor's own adapter-resolver dynamically imports
|
|
89
89
|
* directly for Play mode / world mounting (never `src/main.ts` — that
|
|
90
90
|
* bootstrap is the STANDALONE build's concern, unrelated to how the editor
|
|
@@ -140,7 +140,7 @@ export function computeProjectOptimizeDepsEntries(projectRoot: string): string[]
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const entries = new Set<string>();
|
|
143
|
-
for (const world of manifest.
|
|
143
|
+
for (const world of manifest.roots) {
|
|
144
144
|
if (!world.entry) continue;
|
|
145
145
|
if (!existsSync(join(projectRoot, world.entry))) continue;
|
|
146
146
|
entries.add(world.entry);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic persistence boundary for files produced by project operations.
|
|
3
|
+
*
|
|
4
|
+
* Generators remain ordinary project/package code. They hand their completed
|
|
5
|
+
* bytes to this writer only when they are ready to commit; the editor host
|
|
6
|
+
* validates every path, stages the whole batch, and rolls the batch back if
|
|
7
|
+
* any rename fails. A half-generated model/prefab pair must never become
|
|
8
|
+
* visible to the asset watcher.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { copyFile, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, posix, resolve, sep } from 'node:path';
|
|
14
|
+
import type {
|
|
15
|
+
ProjectGeneratedOutput,
|
|
16
|
+
ProjectGeneratedOutputFile,
|
|
17
|
+
ProjectOutputFile,
|
|
18
|
+
ProjectOutputWriter,
|
|
19
|
+
} from '../../vgai-sdk/src/types';
|
|
20
|
+
|
|
21
|
+
const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
type NormalizedOutputFile = ProjectOutputFile & { path: string };
|
|
24
|
+
|
|
25
|
+
function normalizeOutputPath(path: string): string {
|
|
26
|
+
const slashPath = path.split(sep).join('/');
|
|
27
|
+
const normalized = posix.normalize(slashPath).replace(/^\.\//, '');
|
|
28
|
+
if (
|
|
29
|
+
normalized.length === 0 ||
|
|
30
|
+
normalized.startsWith('/') ||
|
|
31
|
+
normalized === 'public' ||
|
|
32
|
+
!normalized.startsWith('public/') ||
|
|
33
|
+
normalized.includes('\0') ||
|
|
34
|
+
normalized.split('/').includes('..')
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Generated output path ${JSON.stringify(path)} is invalid; outputs must be project-relative files under public/.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function byteLength(content: string | Uint8Array): number {
|
|
44
|
+
return typeof content === 'string' ? Buffer.byteLength(content) : content.byteLength;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function exists(path: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
await stat(path);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateBatch(files: readonly ProjectOutputFile[]): {
|
|
57
|
+
normalized: NormalizedOutputFile[];
|
|
58
|
+
summaries: ProjectGeneratedOutputFile[];
|
|
59
|
+
totalBytes: number;
|
|
60
|
+
} {
|
|
61
|
+
if (files.length === 0) throw new Error('Generated output batch must contain at least one file.');
|
|
62
|
+
const normalized = files.map((file) => ({ ...file, path: normalizeOutputPath(file.path) }));
|
|
63
|
+
const paths = new Set<string>();
|
|
64
|
+
let totalBytes = 0;
|
|
65
|
+
for (const file of normalized) {
|
|
66
|
+
if (paths.has(file.path)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Generated output batch contains duplicate path ${JSON.stringify(file.path)}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
paths.add(file.path);
|
|
72
|
+
totalBytes += byteLength(file.content);
|
|
73
|
+
}
|
|
74
|
+
if (totalBytes > MAX_OUTPUT_BYTES) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Generated output batch is ${totalBytes} bytes; the atomic project-output limit is ${MAX_OUTPUT_BYTES} bytes.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
normalized,
|
|
81
|
+
totalBytes,
|
|
82
|
+
summaries: normalized.map((file) => ({
|
|
83
|
+
path: file.path,
|
|
84
|
+
bytes: byteLength(file.content),
|
|
85
|
+
...(file.mediaType ? { mediaType: file.mediaType } : {}),
|
|
86
|
+
...(file.role ? { role: file.role } : {}),
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function stageBatch(stagedRoot: string, files: readonly NormalizedOutputFile[]) {
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
const staged = resolve(stagedRoot, file.path);
|
|
94
|
+
await mkdir(dirname(staged), { recursive: true });
|
|
95
|
+
await writeFile(staged, file.content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function commitBatch(
|
|
100
|
+
projectRoot: string,
|
|
101
|
+
stagedRoot: string,
|
|
102
|
+
backupRoot: string,
|
|
103
|
+
files: readonly NormalizedOutputFile[],
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const committed: Array<{ target: string; backup: string | null }> = [];
|
|
106
|
+
try {
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const staged = resolve(stagedRoot, file.path);
|
|
109
|
+
const target = resolve(projectRoot, file.path);
|
|
110
|
+
const backup = resolve(backupRoot, file.path);
|
|
111
|
+
await mkdir(dirname(target), { recursive: true });
|
|
112
|
+
let prior: string | null = null;
|
|
113
|
+
if (await exists(target)) {
|
|
114
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
115
|
+
await copyFile(target, backup);
|
|
116
|
+
prior = backup;
|
|
117
|
+
}
|
|
118
|
+
await rename(staged, target);
|
|
119
|
+
committed.push({ target, backup: prior });
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
for (const entry of committed.reverse()) {
|
|
123
|
+
if (entry.backup) await copyFile(entry.backup, entry.target);
|
|
124
|
+
else await rm(entry.target, { force: true });
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createProjectOutputWriter(projectRoot: string): ProjectOutputWriter {
|
|
131
|
+
return {
|
|
132
|
+
async write(files, options = {}): Promise<ProjectGeneratedOutput> {
|
|
133
|
+
const { normalized, summaries, totalBytes } = validateBatch(files);
|
|
134
|
+
if (options.dryRun === true) {
|
|
135
|
+
return { files: summaries, totalBytes, dryRun: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const transactionRoot = resolve(
|
|
139
|
+
projectRoot,
|
|
140
|
+
'.vgai',
|
|
141
|
+
'tmp',
|
|
142
|
+
`project-output-${randomUUID()}`,
|
|
143
|
+
);
|
|
144
|
+
const stagedRoot = resolve(transactionRoot, 'staged');
|
|
145
|
+
const backupRoot = resolve(transactionRoot, 'backup');
|
|
146
|
+
try {
|
|
147
|
+
await stageBatch(stagedRoot, normalized);
|
|
148
|
+
await commitBatch(projectRoot, stagedRoot, backupRoot, normalized);
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(transactionRoot, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { files: summaries, totalBytes, dryRun: false };
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate-on-change (owner decision, GitHub issue #103): hand-editing
|
|
3
|
+
* project files is an accepted authoring path, so the dev server validates
|
|
4
|
+
* `*.vscn.json` / `*.prefab.json` / `vgai.game.json` on every write and
|
|
5
|
+
* surfaces failures immediately — instead of the previous behavior, where a
|
|
6
|
+
* bad hand-edit only errored at next load.
|
|
7
|
+
*
|
|
8
|
+
* This module owns CONTENT validation only (classify a path, parse+validate
|
|
9
|
+
* its JSON) — it deliberately reuses the exact Zod-backed parsers
|
|
10
|
+
* `validate-scenes.ts`/`validate-manifest.ts` ship to every project
|
|
11
|
+
* (`parseSceneFile`/`parsePrefabFile`/`loadGameManifest`), never
|
|
12
|
+
* re-implementing schema logic. Debouncing, terminal/SSE/state reporting,
|
|
13
|
+
* and file-watcher wiring live in `editor-server.ts`, which owns the
|
|
14
|
+
* chokidar watchers and mutable project state this needs no knowledge of.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { ZodError } from 'zod';
|
|
19
|
+
import { loadGameManifest } from '../../engine/src/manifest/load';
|
|
20
|
+
import { detectUnmanagedReactRoots } from '../../engine/src/react/unmanaged-root-detector';
|
|
21
|
+
import { parsePrefabFile, parseSceneFile, SceneParseError } from '../../engine/src/scene/parse';
|
|
22
|
+
|
|
23
|
+
export type ValidatableKind = 'scene' | 'prefab' | 'manifest' | 'source';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classify an absolute file path as one of the three validate-on-change
|
|
27
|
+
* formats, or `null` if it's none of them. Scene/prefab files can nest
|
|
28
|
+
* anywhere under a project's `public/` (matched by extension); the manifest
|
|
29
|
+
* is a single fixed-name file at the project root (matched by basename —
|
|
30
|
+
* `vgai.game.json` never appears anywhere else in a project).
|
|
31
|
+
*/
|
|
32
|
+
export function classifyValidatableFile(absPath: string): ValidatableKind | null {
|
|
33
|
+
if (absPath.endsWith('.vscn.json')) return 'scene';
|
|
34
|
+
if (absPath.endsWith('.prefab.json')) return 'prefab';
|
|
35
|
+
if (/[/\\]src[/\\].*\.[cm]?[jt]sx?$/.test(absPath)) return 'source';
|
|
36
|
+
const base = absPath.split(/[/\\]/).pop();
|
|
37
|
+
if (base === 'vgai.game.json') return 'manifest';
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ValidationOk {
|
|
42
|
+
ok: true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ValidationFail {
|
|
46
|
+
ok: false;
|
|
47
|
+
/** One formatted message per issue — `path.to.field: message` for Zod
|
|
48
|
+
* issues, or the raw error message for read/parse/cross-field failures. */
|
|
49
|
+
errors: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ValidationResult = ValidationOk | ValidationFail;
|
|
53
|
+
|
|
54
|
+
function formatIssues(
|
|
55
|
+
issues: readonly { path: readonly PropertyKey[]; message: string }[],
|
|
56
|
+
): string[] {
|
|
57
|
+
// Zod v4's issue `path` is typed `PropertyKey[]` (it can carry a `symbol`
|
|
58
|
+
// for branded keys) — `Array.prototype.join` calls the implicit `ToString`
|
|
59
|
+
// coercion on each element, which THROWS for a bare symbol. Map through
|
|
60
|
+
// `String()` explicitly so this stays safe even on that rare path.
|
|
61
|
+
return issues.map((issue) => `${issue.path.map(String).join('.') || '(root)'}: ${issue.message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Run the one Zod-backed parser `kind` dictates. Throws on failure (same
|
|
65
|
+
* contract as `parseSceneFile`/`parsePrefabFile`/`loadGameManifest`) —
|
|
66
|
+
* `validateProjectFile` below is the one place that catches it. */
|
|
67
|
+
function parseByKind(json: unknown, kind: ValidatableKind, absPath: string): void {
|
|
68
|
+
if (kind === 'scene') {
|
|
69
|
+
parseSceneFile(json, absPath);
|
|
70
|
+
} else if (kind === 'prefab') {
|
|
71
|
+
parsePrefabFile(json, absPath);
|
|
72
|
+
} else {
|
|
73
|
+
loadGameManifest(json);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Map a thrown parse error to its reported message list — one formatted
|
|
78
|
+
* entry per Zod issue for schema violations, or the raw message for a
|
|
79
|
+
* manifest cross-field `Error` (`loadGameManifest` throws those directly). */
|
|
80
|
+
function errorsFor(err: unknown): string[] {
|
|
81
|
+
if (err instanceof SceneParseError) return formatIssues(err.issues);
|
|
82
|
+
if (err instanceof ZodError) return formatIssues(err.issues);
|
|
83
|
+
return [err instanceof Error ? err.message : String(err)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate one project file's on-disk content against the same schema the
|
|
88
|
+
* engine's runtime loader and `validate-scenes.ts`/`validate-manifest.ts`
|
|
89
|
+
* enforce. Never throws — a file-watcher callback has no caller to catch a
|
|
90
|
+
* throw, so every failure mode (unreadable file, malformed JSON, schema
|
|
91
|
+
* violation, manifest cross-field rule) collapses into a `ValidationFail`
|
|
92
|
+
* with one or more human-readable messages.
|
|
93
|
+
*/
|
|
94
|
+
export async function validateProjectFile(
|
|
95
|
+
absPath: string,
|
|
96
|
+
kind: ValidatableKind,
|
|
97
|
+
): Promise<ValidationResult> {
|
|
98
|
+
let raw: string;
|
|
99
|
+
try {
|
|
100
|
+
raw = await readFile(absPath, 'utf-8');
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Most commonly ENOENT — a delete (or a rename's transient unlink) racing
|
|
103
|
+
// the debounce timer. Not this module's job to distinguish; just report.
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
errors: [`could not read file: ${err instanceof Error ? err.message : String(err)}`],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let json: unknown;
|
|
111
|
+
if (kind === 'source') {
|
|
112
|
+
const findings = detectUnmanagedReactRoots(raw, absPath);
|
|
113
|
+
return findings.length === 0
|
|
114
|
+
? { ok: true }
|
|
115
|
+
: {
|
|
116
|
+
ok: false,
|
|
117
|
+
errors: findings.map(
|
|
118
|
+
(finding) =>
|
|
119
|
+
`line ${finding.line}: ${finding.api}() creates an unmanaged React root; ` +
|
|
120
|
+
'declare a react adapter root in vgai.game.json and default-export the component.',
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
json = JSON.parse(raw);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
errors: [`invalid JSON: ${err instanceof Error ? err.message : String(err)}`],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
parseByKind(json, kind, absPath);
|
|
136
|
+
return { ok: true };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return { ok: false, errors: errorsFor(err) };
|
|
139
|
+
}
|
|
140
|
+
}
|