@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
@@ -2,6 +2,15 @@ import { createHash } from 'node:crypto';
2
2
  import { readFile, stat } from 'node:fs/promises';
3
3
  import { extname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { assetCapabilities } from '../src/asset-workflow/asset-capabilities';
6
+ import {
7
+ type AssetCatalog,
8
+ type AssetFamily,
9
+ type AssetFile,
10
+ type AssetVariant,
11
+ parseAssetCatalog,
12
+ } from '../src/asset-workflow/asset-types';
13
+ import { migrateLegacyAssetCatalog } from './asset-catalog-v2';
5
14
  import { isPathInside } from './server-utils';
6
15
 
7
16
  export const DEFAULT_LOCAL_ASSET_LIBRARY_ROOT = '/Volumes/PeakSSD/vgai-asset-library';
@@ -10,17 +19,12 @@ export const REPOSITORY_ASSET_CATALOG_PATH = fileURLToPath(
10
19
  );
11
20
 
12
21
  export type LocalAssetType = 'model' | 'animation' | 'source';
13
-
14
- export interface LocalAssetFile {
15
- label: string;
16
- path: string;
17
- format: string;
18
- sizeBytes: number;
19
- dependencies?: string[];
20
- }
22
+ export type LocalAssetFile = AssetFile;
21
23
 
22
24
  export interface LocalCatalogAsset {
25
+ /** Variant identity. Family identity is explicit and separately exposed. */
23
26
  id: string;
27
+ familyId: string;
24
28
  source: string;
25
29
  name: string;
26
30
  type: LocalAssetType;
@@ -33,12 +37,12 @@ export interface LocalCatalogAsset {
33
37
  files: LocalAssetFile[];
34
38
  animationCount?: number;
35
39
  clipNames?: string[];
40
+ variants: readonly AssetVariant[];
36
41
  }
37
42
 
38
- export interface LocalAssetCatalog {
39
- version: 1;
40
- generatedAt: string;
41
- assets: LocalCatalogAsset[];
43
+ export interface LocalAssetCatalog extends AssetCatalog {
44
+ /** Valid variant projections, derived once at load for existing server consumers. */
45
+ readonly assets: readonly LocalCatalogAsset[];
42
46
  }
43
47
 
44
48
  export function getLocalAssetLibraryRoot(): string {
@@ -49,9 +53,7 @@ export function getLocalCatalogPath(root = getLocalAssetLibraryRoot()): string {
49
53
  return resolve(root, 'catalog', 'catalog.json');
50
54
  }
51
55
 
52
- export async function resolveLocalCatalogPath(
53
- root = getLocalAssetLibraryRoot(),
54
- ): Promise<string> {
56
+ export async function resolveLocalCatalogPath(root = getLocalAssetLibraryRoot()): Promise<string> {
55
57
  const externalPath = getLocalCatalogPath(root);
56
58
  try {
57
59
  await stat(externalPath);
@@ -74,10 +76,17 @@ export async function loadLocalAssetCatalog(
74
76
  return cachedCatalog;
75
77
  }
76
78
 
77
- const parsed = JSON.parse(await readFile(catalogPath, 'utf8')) as LocalAssetCatalog;
78
- if (parsed.version !== 1 || !Array.isArray(parsed.assets)) {
79
- throw new Error(`Unsupported local asset catalog at ${catalogPath}`);
80
- }
79
+ const input: unknown = JSON.parse(await readFile(catalogPath, 'utf8'));
80
+ const normalized =
81
+ input && typeof input === 'object' && 'version' in input && input.version === 1
82
+ ? migrateLegacyAssetCatalog(input)
83
+ : parseAssetCatalog(input);
84
+ const parsed: LocalAssetCatalog = {
85
+ ...normalized,
86
+ assets: normalized.families.flatMap((family) =>
87
+ family.variants.map((variant) => projectCatalogVariant(family, variant)),
88
+ ),
89
+ };
81
90
  cachedCatalog = parsed;
82
91
  cachedPath = catalogPath;
83
92
  cachedMtimeMs = catalogStat.mtimeMs;
@@ -89,7 +98,49 @@ export async function findLocalAsset(
89
98
  root = getLocalAssetLibraryRoot(),
90
99
  ): Promise<LocalCatalogAsset | null> {
91
100
  const catalog = await loadLocalAssetCatalog(root);
92
- return catalog.assets.find((asset) => asset.id === id) ?? null;
101
+ const exactVariant = catalog.assets.find((asset) => asset.id === id);
102
+ if (exactVariant) return exactVariant;
103
+ const family = catalog.families.find((candidate) => candidate.id === id);
104
+ return family ? preferredLocalAssetVariant(family) : null;
105
+ }
106
+
107
+ export async function findLocalAssetFamily(
108
+ id: string,
109
+ root = getLocalAssetLibraryRoot(),
110
+ ): Promise<AssetFamily | null> {
111
+ const catalog = await loadLocalAssetCatalog(root);
112
+ return (
113
+ catalog.families.find((family) => family.id === id) ??
114
+ catalog.families.find((family) => family.variants.some((variant) => variant.id === id)) ??
115
+ null
116
+ );
117
+ }
118
+
119
+ function projectCatalogVariant(family: AssetFamily, variant: AssetVariant): LocalCatalogAsset {
120
+ return {
121
+ id: variant.id,
122
+ familyId: family.id,
123
+ source: family.source,
124
+ name: family.name,
125
+ type: variant.type,
126
+ categories: family.categories,
127
+ tags: family.tags,
128
+ license: family.license,
129
+ author: family.author,
130
+ sourceUrl: family.sourceUrl,
131
+ thumbnail: variant.thumbnail.path,
132
+ files: variant.files,
133
+ ...(variant.animationCount !== undefined ? { animationCount: variant.animationCount } : {}),
134
+ ...(variant.clipNames !== undefined ? { clipNames: variant.clipNames } : {}),
135
+ variants: family.variants,
136
+ };
137
+ }
138
+
139
+ export function preferredLocalAssetVariant(family: AssetFamily): LocalCatalogAsset {
140
+ const ranked = rankLocalAssetPreviewVariants(
141
+ family.variants.map((variant) => projectCatalogVariant(family, variant)),
142
+ );
143
+ return ranked[0] ?? projectCatalogVariant(family, family.variants[0]!);
93
144
  }
94
145
 
95
146
  export function localAssetMatchesType(asset: LocalCatalogAsset, type?: string): boolean {
@@ -113,6 +164,76 @@ export function resolveCatalogFile(root: string, relativePath: string): string {
113
164
  return absolutePath;
114
165
  }
115
166
 
167
+ /** Stable SSD cache location for a catalog card's generated preview. */
168
+ export function localAssetThumbnailPath(asset: LocalCatalogAsset): string {
169
+ return asset.thumbnail ?? `thumbnails/${asset.id}.webp`;
170
+ }
171
+
172
+ /** Resolve a generated/upstream thumbnail without allowing catalog path escapes. */
173
+ export function resolveLocalAssetThumbnail(root: string, asset: LocalCatalogAsset): string {
174
+ return resolveCatalogFile(root, localAssetThumbnailPath(asset));
175
+ }
176
+
177
+ const PREVIEW_FORMAT_PRIORITY: Record<string, number> = {
178
+ glb: 0,
179
+ gltf: 1,
180
+ obj: 2,
181
+ dae: 3,
182
+ fbx: 4,
183
+ stl: 5,
184
+ ply: 6,
185
+ '3ds': 7,
186
+ bvh: 8,
187
+ };
188
+
189
+ /**
190
+ * Identify format variants of the same catalog item without relying on category order.
191
+ * Catalog sources consistently use `sources/<source-root>/<collection>/...`; collection plus
192
+ * normalized display name distinguishes same-named assets from different packs and GSO objects.
193
+ */
194
+ export function localAssetVariantKey(asset: LocalCatalogAsset): string {
195
+ return asset.familyId;
196
+ }
197
+
198
+ /** Best-first preview candidates for every source-format variant of an asset. */
199
+ export function localAssetPreviewVariants(
200
+ asset: LocalCatalogAsset,
201
+ catalogAssets: readonly LocalCatalogAsset[],
202
+ ): LocalCatalogAsset[] {
203
+ const key = localAssetVariantKey(asset);
204
+ return rankLocalAssetPreviewVariants(
205
+ catalogAssets.filter((candidate) => localAssetVariantKey(candidate) === key),
206
+ );
207
+ }
208
+
209
+ /** Rank an already-associated set of semantic variants without regrouping it. */
210
+ export function rankLocalAssetPreviewVariants(
211
+ variants: readonly LocalCatalogAsset[],
212
+ ): LocalCatalogAsset[] {
213
+ return [...variants].sort((left, right) => {
214
+ const leftFile = left.files[0];
215
+ const rightFile = right.files[0];
216
+ const formatDifference =
217
+ (PREVIEW_FORMAT_PRIORITY[leftFile?.format ?? ''] ?? 99) -
218
+ (PREVIEW_FORMAT_PRIORITY[rightFile?.format ?? ''] ?? 99);
219
+ if (formatDifference !== 0) return formatDifference;
220
+ const qualityDifference =
221
+ previewPathPenalty(leftFile?.path) - previewPathPenalty(rightFile?.path);
222
+ if (qualityDifference !== 0) return qualityDifference;
223
+ return (leftFile?.path ?? '').localeCompare(rightFile?.path ?? '');
224
+ });
225
+ }
226
+
227
+ function previewPathPenalty(path: string | undefined): number {
228
+ const normalized = path?.toLowerCase() ?? '';
229
+ let penalty = 0;
230
+ if (normalized.includes('unity')) penalty += 4;
231
+ if (normalized.includes('draco')) penalty += 3;
232
+ if (normalized.includes('quantized')) penalty += 2;
233
+ if (normalized.includes('embedded')) penalty += 1;
234
+ return penalty;
235
+ }
236
+
116
237
  export function searchLocalCatalog(
117
238
  catalog: LocalAssetCatalog,
118
239
  options: { q?: string; type?: string; category?: string; offset?: number; limit?: number },
@@ -121,7 +242,7 @@ export function searchLocalCatalog(
121
242
  const category = options.category?.toLowerCase();
122
243
  const type = options.type === 'all' ? undefined : options.type;
123
244
 
124
- const matches = catalog.assets.filter((asset) => {
245
+ const matches = catalog.families.map(preferredLocalAssetVariant).filter((asset) => {
125
246
  if (!localAssetMatchesType(asset, type)) return false;
126
247
  if (category && !asset.categories.some((value) => value.toLowerCase() === category)) {
127
248
  return false;
@@ -151,7 +272,21 @@ export function localAssetSlug(asset: LocalCatalogAsset): string {
151
272
  .toLowerCase()
152
273
  .replace(/[^a-z0-9]+/g, '-')
153
274
  .replace(/^-|-$/g, '');
154
- return `${base || 'asset'}-${asset.id.slice(0, 8)}`;
275
+ return `${base || 'asset'}-${asset.familyId.slice(0, 8)}`;
276
+ }
277
+
278
+ export function localAssetCapabilities(asset: LocalCatalogAsset) {
279
+ const capabilities = assetCapabilities(asset.files[0]?.format ?? 'unknown');
280
+ return {
281
+ previewable: capabilities.previewable,
282
+ importable: capabilities.importable,
283
+ runtimeReady: capabilities.runtimeReady,
284
+ animated: capabilities.animated || (asset.animationCount ?? 0) > 0,
285
+ deliveryAvailable: asset.variants.some((variant) =>
286
+ variant.deliveries.some((delivery) => delivery.available),
287
+ ),
288
+ health: capabilities.runtimeReady ? 'healthy' : capabilities.importable ? 'source-only' : 'unsupported',
289
+ } as const;
155
290
  }
156
291
 
157
292
  export function makeLocalAssetId(source: string, key: string): string {
@@ -28,7 +28,7 @@
28
28
  * ENABLED) whose job is ONLY to resolve/transform the opened PROJECT's own
29
29
  * source. Because that Vite instance's `root` is the project directory,
30
30
  * Vite's own resolver naturally walks the PROJECT's OWN `node_modules` for
31
- * every bare specifier (`three`, `zod`, `react`, `@vgai/scene-ui`, ...) —
31
+ * every bare specifier (`three`, `zod`, `react`, ...) —
32
32
  * for free, with zero hand-written import rewriting. This is the same
33
33
  * pattern `packages/editor/scripts/run-conformance.ts` uses to run the
34
34
  * adapter conformance kit against an arbitrary external folder, adapted for
@@ -39,7 +39,7 @@
39
39
  * slice) is reconstructed here too — pointed at the PROJECT's own installed
40
40
  * `@vgai/engine`, not a copy baked into this package's own tarball. A
41
41
  * project pinning `@vgai/engine@0.3.0` must be served ITS 0.3.0 source, so
42
- * `@vgai/engine`/`@vgai/scene-ui` are resolved via `createRequire`, rooted
42
+ * `@vgai/engine` is resolved via `createRequire`, rooted
43
43
  * at the project directory — Node's own resolution algorithm, not a
44
44
  * hardcoded path (see `resolveInstalledPackageSrcDir` in `server-utils.ts`,
45
45
  * shared with `editor-server.ts`'s equivalent fix for `prod.ts`'s route).
@@ -122,7 +122,7 @@
122
122
  * packaged editor (`vgai.game.json`'s `kind: 'react'` "hud" world,
123
123
  * `useGameState`) threw exactly this at mount: `TypeError: Cannot read
124
124
  * properties of null (reading 'useContext')` inside `useGame()`
125
- * (`src/ui/game-state.tsx`), called from the project's `Hud` component,
125
+ * (`@vgai/engine/react/game-state`), called from the project's `Hud` component,
126
126
  * rendered by the editor's OWN bundled `react_stack_bottom_frame`
127
127
  * (`assets/index-*.js`).
128
128
  *
@@ -208,21 +208,22 @@
208
208
  * is the supported path today.
209
209
  */
210
210
 
211
- import { realpathSync } from 'node:fs';
211
+ import { readFileSync, realpathSync } from 'node:fs';
212
212
  import path from 'node:path';
213
213
  import { fileURLToPath } from 'node:url';
214
214
  import express from 'express';
215
215
  import { createServer as createViteServer } from 'vite';
216
216
  import { projectGameStaticPlugin } from '../vite-plugin-project-game-static';
217
- import { reactWorldProviderPlugin } from '../vite-plugin-react-world-provider';
218
217
  import { reactWorldRuntimePlugin } from '../vite-plugin-react-world-runtime';
219
218
  import { uiOidPlugin } from '../vite-plugin-ui-oid';
220
219
  import { canonicalProjectRoot } from './canonical-path';
221
220
  import { createEditorServer } from './editor-server';
222
221
  import { shouldShadowGameGlobals } from './game-globals-shadow';
222
+ import { closeHttpServer, createProcessShutdown } from './process-shutdown';
223
+ import { classifyProjectHotUpdate } from './project-hmr-files';
223
224
  import {
224
- SERVER_ONLY_PREBUNDLE_EXCLUDE,
225
225
  computeProjectOptimizeDepsEntries,
226
+ SERVER_ONLY_PREBUNDLE_EXCLUDE,
226
227
  } from './project-optimize-deps-entries';
227
228
  import {
228
229
  friendlyListenError,
@@ -243,7 +244,11 @@ const editorPackageRoot = path.resolve(__dirname, '..');
243
244
  const distPath = path.resolve(editorPackageRoot, 'dist');
244
245
 
245
246
  const PORT = Number(process.env['PORT']) || Number(process.env['VGAI_EDITOR_PORT']) || 5173;
246
- const HMR_PORT = Number(process.env['VGAI_HMR_PORT']) || (PORT === 5173 ? 24678 : PORT + 100);
247
+ // Keep generated projects' paired HMR sockets in a disjoint band from their
248
+ // persisted 20000-29999 editor-port preferences (plus launcher fall-forward).
249
+ const DEFAULT_HMR_PORT =
250
+ PORT >= 20_000 && PORT < 30_100 ? PORT + 10_000 : PORT === 5173 ? 24678 : PORT + 100;
251
+ const HMR_PORT = Number(process.env['VGAI_HMR_PORT']) || DEFAULT_HMR_PORT;
247
252
  // S3: bind to loopback by default; opt-in to a wider interface via env.
248
253
  const HOST = resolveBindHost();
249
254
 
@@ -320,6 +325,14 @@ async function main(): Promise<void> {
320
325
  configFile: false,
321
326
  root: projectPath,
322
327
  appType: 'custom', // no HTML serving/SPA-fallback — this instance only serves project modules
328
+ // This Vite instance transforms not only project TSX (which uiOidPlugin
329
+ // already sends through automatic JSX) but also the installed engine's
330
+ // React bridge. Vite/esbuild otherwise compiles that dependency source to
331
+ // `React.createElement` even though the canonical bridge uses named React
332
+ // imports and intentionally has no global `React` binding. Keep Fast
333
+ // Refresh out (the custom app has no preamble host), but make the JSX
334
+ // runtime explicit for every TSX module served through this graph.
335
+ esbuild: { jsx: 'automatic' },
323
336
  plugins: [
324
337
  // React-world OID authoring parity (see this file's header doc comment,
325
338
  // "React-world OID authoring parity"): live `data-oid` instrumentation +
@@ -328,12 +341,6 @@ async function main(): Promise<void> {
328
341
  // (already project-scoped) unmodified — no extra scoping needed since this
329
342
  // instance's `root` IS the project already.
330
343
  uiOidPlugin(),
331
- // Resolves the synthetic `/__vgai-game-provider` module
332
- // `adapter-resolver.ts`'s `loadProjectGameProvider` imports for every
333
- // `kind: 'react'` world's mount — WITHOUT this, a react world 404s at
334
- // mount time in packaged mode (this is a correctness fix, not just an
335
- // authoring nicety). See vite-plugin-react-world-provider.ts's doc comment.
336
- reactWorldProviderPlugin(),
337
344
  // Resolves the synthetic `/__vgai-react-world-runtime` module
338
345
  // `adapter-resolver.ts`'s `resolveReactWorldMountRuntime` imports —
339
346
  // the dual-React-instance fix (this file's header doc comment,
@@ -367,11 +374,25 @@ async function main(): Promise<void> {
367
374
  file: string;
368
375
  server: { ws: { send: (msg: unknown) => void } };
369
376
  }) {
370
- if (/\.(ts|tsx|js|jsx)$/.test(file)) {
377
+ const kind = classifyProjectHotUpdate(
378
+ file,
379
+ currentProjectRoot,
380
+ /\.[jt]sx?$/.test(file) ? readFileSync(file, 'utf-8') : '',
381
+ );
382
+ if (kind === 'outside' || kind === 'stock-data') return;
383
+ if (kind === 'react') return;
384
+ if (kind === 'component') {
371
385
  server.ws.send({ type: 'custom', event: 'vgai:script-update', data: { file } });
372
386
  }
373
- // Swallow all project file changes — the editor handles scene/asset
374
- // updates via its own SSE channel (mirrors dev.ts's `return []`).
387
+ if (kind === 'restart') {
388
+ server.ws.send({
389
+ type: 'custom',
390
+ event: 'vgai:restart-required',
391
+ data: { file },
392
+ });
393
+ }
394
+ // Swallow non-runtime project changes. In particular, building or
395
+ // running a tool under scripts/ must never import it into Play mode.
375
396
  return [];
376
397
  },
377
398
  },
@@ -465,6 +486,12 @@ async function main(): Promise<void> {
465
486
  },
466
487
  resolve: {
467
488
  alias: [
489
+ // Project-authored editor tools import the editor's public widget kit
490
+ // through `@editor/widgets` (the scaffolder writes the matching
491
+ // tsconfig path). In checkout mode the root Vite config supplies this
492
+ // alias; this project-rooted packaged Vite instance has no configFile,
493
+ // so it must reconstruct the runtime half from its own package root.
494
+ { find: /^@editor\//, replacement: `${path.join(editorPackageRoot, 'src')}/` },
468
495
  // The `@engine/*` alias project source still uses (pending the
469
496
  // `@engine/*` → `@vgai/engine/*` migration deferred to a follow-up
470
497
  // slice) — pointed at THIS project's own installed engine version,
@@ -496,6 +523,7 @@ async function main(): Promise<void> {
496
523
  const editorRouter = createEditorServer({
497
524
  engineRoot: editorPackageRoot,
498
525
  projectPath,
526
+ loadProjectModule: (moduleId) => vite.ssrLoadModule(moduleId),
499
527
  onProjectOpened(newProjectPath: string) {
500
528
  const allowed = vite.config.server.fs.allow;
501
529
  if (!allowed.includes(newProjectPath)) {
@@ -555,13 +583,15 @@ async function main(): Promise<void> {
555
583
  process.exit(1);
556
584
  });
557
585
 
558
- const shutdown = () => {
586
+ const shutdown = createProcessShutdown({
587
+ tasks: [() => closeHttpServer(server), () => editorRouter.close(), () => vite.close()],
588
+ });
589
+ const unregisterAndShutdown = (signal: string) => {
559
590
  unregisterSession(process.pid);
560
- vite.close();
561
- server.close();
591
+ void shutdown(signal);
562
592
  };
563
- process.once('SIGINT', shutdown);
564
- process.once('SIGTERM', shutdown);
593
+ process.once('SIGINT', () => unregisterAndShutdown('SIGINT'));
594
+ process.once('SIGTERM', () => unregisterAndShutdown('SIGTERM'));
565
595
  process.once('exit', () => unregisterSession(process.pid));
566
596
  }
567
597
 
@@ -0,0 +1,136 @@
1
+ import { type ChildProcess, execFileSync } from 'node:child_process';
2
+ import type { Server } from 'node:http';
3
+
4
+ /**
5
+ * Terminate a detached child and its whole process tree. Shared by the e2e
6
+ * server harness and the packaged probe runner: keeping this under `server/`
7
+ * makes the runtime utility part of the editor package instead of forcing a
8
+ * published command to import an unshipped e2e helper.
9
+ */
10
+ export function stopProcess(proc: ChildProcess): Promise<void> {
11
+ const signalGroup = (signal: NodeJS.Signals) => {
12
+ if (proc.pid === undefined) return;
13
+ if (process.platform === 'win32') {
14
+ try {
15
+ execFileSync('taskkill', ['/PID', String(proc.pid), '/T', '/F'], { stdio: 'ignore' });
16
+ } catch {
17
+ /* already dead, or taskkill unavailable */
18
+ }
19
+ try {
20
+ proc.kill(signal);
21
+ } catch {
22
+ /* already dead */
23
+ }
24
+ return;
25
+ }
26
+ try {
27
+ process.kill(-proc.pid, signal);
28
+ } catch {
29
+ try {
30
+ proc.kill(signal);
31
+ } catch {
32
+ /* already dead */
33
+ }
34
+ }
35
+ };
36
+
37
+ return new Promise((resolve) => {
38
+ if (proc.exitCode !== null || proc.signalCode !== null) {
39
+ signalGroup('SIGKILL');
40
+ resolve();
41
+ return;
42
+ }
43
+ const timer = setTimeout(() => {
44
+ signalGroup('SIGKILL');
45
+ resolve();
46
+ }, 5000);
47
+ proc.on('exit', () => {
48
+ clearTimeout(timer);
49
+ signalGroup('SIGKILL');
50
+ resolve();
51
+ });
52
+ signalGroup('SIGTERM');
53
+ });
54
+ }
55
+
56
+ export interface ProcessShutdownOptions {
57
+ timeoutMs?: number;
58
+ tasks: ReadonlyArray<() => void | Promise<void>>;
59
+ exit?: (code: number) => void;
60
+ log?: (message: string) => void;
61
+ }
62
+
63
+ /** Stop accepting requests and forcibly release persistent HTTP/SSE sockets. */
64
+ export function closeHttpServer(server: Server): Promise<void> {
65
+ return new Promise((resolve, reject) => {
66
+ if (!server.listening) {
67
+ server.closeAllConnections?.();
68
+ resolve();
69
+ return;
70
+ }
71
+ server.close((error) => (error ? reject(error) : resolve()));
72
+ // `server.close()` waits for persistent SSE/keep-alive connections. They
73
+ // have no useful work after process shutdown begins, so close them after
74
+ // removing the listener and eliminate the restart race on the HTTP port.
75
+ server.closeAllConnections?.();
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Build an idempotent, bounded signal handler.
81
+ *
82
+ * Node's default signal exit is disabled as soon as an application installs
83
+ * a SIGTERM/SIGINT listener. Tooling loaders (notably tsx/esbuild) may retain
84
+ * internal handles even after every application server and watcher closes,
85
+ * so waiting for an empty event loop is not a shutdown contract. Resources
86
+ * get one bounded graceful window and then the process exits explicitly.
87
+ */
88
+ export function createProcessShutdown(
89
+ options: ProcessShutdownOptions,
90
+ ): (why: string) => Promise<void> {
91
+ const timeoutMs = options.timeoutMs ?? 5_000;
92
+ const exit = options.exit ?? ((code: number) => process.exit(code));
93
+ const log = options.log ?? ((message: string) => process.stderr.write(`${message}\n`));
94
+ let pending: Promise<void> | null = null;
95
+
96
+ return (why: string) => {
97
+ if (pending) return pending;
98
+ pending = (async () => {
99
+ let timeout: ReturnType<typeof setTimeout> | undefined;
100
+ const cleanup = Promise.allSettled(
101
+ options.tasks.map(async (task) => {
102
+ await task();
103
+ }),
104
+ );
105
+ const result = await Promise.race([
106
+ cleanup.then((settled) => ({ type: 'settled' as const, settled })),
107
+ new Promise<{ type: 'timeout' }>((resolve) => {
108
+ timeout = setTimeout(() => resolve({ type: 'timeout' }), timeoutMs);
109
+ }),
110
+ ]);
111
+ if (timeout) clearTimeout(timeout);
112
+
113
+ if (result.type === 'timeout') {
114
+ log(`[vgai-editor] cleanup timed out after ${timeoutMs}ms (${why}); forcing exit`);
115
+ exit(1);
116
+ return;
117
+ }
118
+ const failures = result.settled.filter(
119
+ (item): item is PromiseRejectedResult => item.status === 'rejected',
120
+ );
121
+ if (failures.length > 0) {
122
+ log(
123
+ `[vgai-editor] cleanup failed (${why}): ${failures
124
+ .map((item) =>
125
+ item.reason instanceof Error ? item.reason.message : String(item.reason),
126
+ )
127
+ .join('; ')}`,
128
+ );
129
+ exit(1);
130
+ return;
131
+ }
132
+ exit(0);
133
+ })();
134
+ return pending;
135
+ };
136
+ }
package/server/prod.ts CHANGED
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
  import express from 'express';
14
14
  import { canonicalProjectRoot } from './canonical-path';
15
15
  import { createEditorServer } from './editor-server';
16
+ import { closeHttpServer, createProcessShutdown } from './process-shutdown';
16
17
  import { friendlyListenError, resolveBindHost } from './server-utils';
17
18
 
18
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -47,7 +48,9 @@ async function main(): Promise<void> {
47
48
  const server = app.listen(PORT, HOST, () => {
48
49
  const addr = server.address();
49
50
  const actualPort = typeof addr === 'object' && addr ? addr.port : PORT;
50
- console.log(`\n \x1b[32mProduction server\x1b[0m running at \x1b[36mhttp://${HOST}:${actualPort}\x1b[0m`);
51
+ console.log(
52
+ `\n \x1b[32mProduction server\x1b[0m running at \x1b[36mhttp://${HOST}:${actualPort}\x1b[0m`,
53
+ );
51
54
  if (projectPath) {
52
55
  console.log(` \x1b[33mProject\x1b[0m: ${projectPath}`);
53
56
  }
@@ -62,11 +65,11 @@ async function main(): Promise<void> {
62
65
  process.exit(1);
63
66
  });
64
67
 
65
- const shutdown = () => {
66
- server.close();
67
- };
68
- process.once('SIGINT', shutdown);
69
- process.once('SIGTERM', shutdown);
68
+ const shutdown = createProcessShutdown({
69
+ tasks: [() => closeHttpServer(server), () => editorRouter.close()],
70
+ });
71
+ process.once('SIGINT', () => void shutdown('SIGINT'));
72
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
70
73
  }
71
74
 
72
75
  main().catch((err) => {
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+
3
+ export type ProjectHotUpdateKind =
4
+ | 'outside'
5
+ | 'stock-data'
6
+ | 'react'
7
+ | 'component'
8
+ | 'restart'
9
+ | 'ignore';
10
+
11
+ /**
12
+ * Classify a Vite watcher update for the editor's project-script HMR seam.
13
+ *
14
+ * Only modules below the project's `src/` directory are game code. Build
15
+ * output, tests, one-off scripts, and dependency caches may also contain
16
+ * JavaScript, but importing them into the live game is both surprising and
17
+ * unsafe (a project build used to make the editor import `dist/assets/*`).
18
+ */
19
+ export function classifyProjectHotUpdate(
20
+ file: string,
21
+ projectRoot: string,
22
+ source = '',
23
+ ): ProjectHotUpdateKind {
24
+ const normalizedFile = file.replaceAll('\\', '/');
25
+ const normalizedRoot = projectRoot.replaceAll('\\', '/').replace(/\/$/, '');
26
+ const relative = path.posix.relative(normalizedRoot, normalizedFile);
27
+
28
+ if (relative === '' || relative === '..' || relative.startsWith('../')) return 'outside';
29
+ if (relative === 'vgai.game.json') return 'restart';
30
+ if (!relative.startsWith('src/')) return 'ignore';
31
+ // Project-operation wrappers are editor/server tooling. A game that wants
32
+ // shared generation imports the underlying ordinary module, not this
33
+ // discoverability wrapper, so editing an operation must not request a full
34
+ // game restart.
35
+ if (/^src\/operations\/.*\.operation\.(ts|js)$/.test(relative)) return 'ignore';
36
+ if (/\.data\.json$/.test(relative)) return 'stock-data';
37
+ if (!/\.(ts|tsx|js|jsx)$/.test(relative)) return 'ignore';
38
+ if (/\bextends\s+(?:[A-Za-z_$][\w$]*\.)?GameComponent\b/.test(source)) return 'component';
39
+ if (/\.(tsx|jsx)$/.test(relative)) return 'react';
40
+ return 'restart';
41
+ }