@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.
Files changed (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. 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 `worlds[].entry` its OWN `vgai.game.json` declares
62
- * (default-adapter, code-authored worlds; scene-only worlds have no `entry`
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 `worlds[].entry` files never reach that chain: they are the
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.worlds) {
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
+ }