@underpostnet/cyberia 3.2.90 → 3.3.73

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 (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
package/bin/index.js CHANGED
@@ -13,11 +13,17 @@ import dotenv from 'dotenv';
13
13
  import { Command } from 'commander';
14
14
  import fs from 'fs-extra';
15
15
  import stringify from 'fast-json-stable-stringify';
16
- import { shellExec } from '../src/server/process.js';
17
- import { loggerFactory } from '../src/server/logger.js';
16
+ import { shellExec } from '../src/server/runtime/process.js';
17
+ import { cli } from '../src/server/build/execution.js';
18
+ import { loggerFactory } from '../src/server/ops/logger.js';
18
19
  import { generateBesuManifests, deployBesu, removeBesu } from '../src/projects/cyberia/besu-genesis-generator.js';
19
20
  import { DataBaseProviderService } from '../src/db/DataBaseProvider.js';
20
- import { etcHostFactory, loadConfServerJson, normalizeInstanceTopology } from '../src/server/conf.js';
21
+ import {
22
+ deployEnvFilePath,
23
+ etcHostFactory,
24
+ loadConfServerJson,
25
+ normalizeInstanceTopology,
26
+ } from '../src/server/runtime/conf.js';
21
27
  import {
22
28
  ObjectLayerEngine,
23
29
  resolveCanonicalCid,
@@ -46,16 +52,22 @@ import {
46
52
  DefaultCyberiaQuests,
47
53
  ENTITY_TYPE_DEFAULTS,
48
54
  fillInstanceConfDefaults,
49
- DOCKER_SCRIPTS,
50
- CyberiaDependencies,
51
55
  } from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
56
+ import cyberiaCatalog from '../src/projects/cyberia/catalog-cyberia.js';
52
57
 
53
58
  import {
54
59
  DEFAULT_INSTANCE_CODE,
55
60
  ITEM_TYPES as itemTypes,
56
61
  DefaultCyberiaItems,
57
62
  } from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
58
- import { loadDeployCatalog } from '../src/server/catalog.js';
63
+ import { loadDeployCatalog } from '../src/server/build/catalog.js';
64
+ import {
65
+ DEPLOY_MANIFEST_INDENT,
66
+ STAGED_CLI_PACKAGE,
67
+ buildDeployPackageJson,
68
+ deployPackagePathFactory,
69
+ stageCliPackage,
70
+ } from '../src/server/build/package.js';
59
71
 
60
72
  /**
61
73
  * Connect to the project MongoDB instance using the standard env / conf layout.
@@ -256,6 +268,17 @@ try {
256
268
  /** @type {import('mongoose').Model} */
257
269
  const Ipfs = DataBaseProviderService.getModel('ipfs', { host, path });
258
270
 
271
+ // Idempotent repair, run only before flows that write: collapses legacy
272
+ // duplicates and upgrades the data.item.id index to unique so every
273
+ // later write has exactly one document to land on. Read-only
274
+ // subcommands stay side-effect free — findByItemId already resolves the
275
+ // same canonical document whether or not duplicates are still present.
276
+ if (options.import || options.importTypes || options.drop || options.generate) {
277
+ const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
278
+ if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
279
+ if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
280
+ }
281
+
259
282
  if (options.drop) {
260
283
  // Parse comma-separated item IDs for targeted drop; if none provided, drop everything
261
284
  const dropItemIds = itemId
@@ -355,7 +378,7 @@ try {
355
378
  `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemIdsToClean.size} MFS paths removed`,
356
379
  );
357
380
  if (options.gitClean) {
358
- shellExec(`cd src/client/public/cyberia && underpost run clean .`);
381
+ shellExec(`cd src/client/public/cyberia && ${cli()} run clean .`);
359
382
  logger.info('Asset directory cleaned');
360
383
  }
361
384
 
@@ -439,7 +462,7 @@ try {
439
462
  });
440
463
 
441
464
  // Check if an ObjectLayer with the same item.id already exists (upsert by item ID)
442
- const existingOL = await ObjectLayer.findOne({ 'data.item.id': currentItemId });
465
+ const existingOL = await ObjectLayer.findByItemId(currentItemId);
443
466
  let objectLayer;
444
467
 
445
468
  if (existingOL) {
@@ -457,7 +480,6 @@ try {
457
480
  let stagingFileDoc = null;
458
481
  let stagingAtlasDoc = null;
459
482
  let stagingCid = '';
460
- let stagingSha256 = '';
461
483
  try {
462
484
  const itemKey = currentItemId;
463
485
 
@@ -546,8 +568,7 @@ try {
546
568
  stagingData.render.cid = importItemCid;
547
569
  stagingData.render.metadataCid = importItemMetadataCid;
548
570
 
549
- // Pin data JSON to IPFS (compute final SHA-256 in memory)
550
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
571
+ // Pin data JSON to IPFS
551
572
  try {
552
573
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
553
574
  stagingData,
@@ -585,10 +606,9 @@ try {
585
606
  // Create the new RenderFrames doc (only now touches DB)
586
607
  const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
587
608
 
588
- // Single atomic update of the live document
589
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
609
+ // Single atomic upsert on the natural key; empty staged CIDs keep their stored value
610
+ objectLayer = await ObjectLayer.upsertByItemId({
590
611
  data: stagingData,
591
- sha256: stagingSha256,
592
612
  cid: stagingCid,
593
613
  objectLayerRenderFramesId: newRenderFrames._id,
594
614
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -599,14 +619,13 @@ try {
599
619
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
600
620
  }
601
621
 
602
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
622
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
603
623
  } else {
604
624
  // Rollback: only File/AtlasSpriteSheet were written, clean those up
605
625
  if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
606
626
  logger.warn(`[cut-over] Staging rolled back for ${currentItemId}, live document preserved`);
627
+ objectLayer = await ObjectLayer.findByItemId(currentItemId);
607
628
  }
608
-
609
- objectLayer = await ObjectLayer.findById(existingOL._id);
610
629
  } else {
611
630
  // ── New item: stage everything before creating (same cut-over pattern) ──
612
631
  logger.info(`ObjectLayer '${currentItemId}' is new, staging creation...`);
@@ -621,7 +640,6 @@ try {
621
640
  let stagingFileDoc = null;
622
641
  let stagingAtlasDoc = null;
623
642
  let stagingCid = '';
624
- let stagingSha256 = '';
625
643
  try {
626
644
  const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
627
645
  objectLayerRenderFramesData,
@@ -705,7 +723,6 @@ try {
705
723
  stagingData.render.cid = importItemCid;
706
724
  stagingData.render.metadataCid = importItemMetadataCid;
707
725
 
708
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
709
726
  try {
710
727
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
711
728
  stagingData,
@@ -738,9 +755,8 @@ try {
738
755
 
739
756
  if (cutoverReady) {
740
757
  const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
741
- objectLayer = await ObjectLayer.create({
758
+ objectLayer = await ObjectLayer.upsertByItemId({
742
759
  data: stagingData,
743
- sha256: stagingSha256,
744
760
  cid: stagingCid,
745
761
  objectLayerRenderFramesId: newRenderFrames._id,
746
762
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -842,7 +858,7 @@ try {
842
858
 
843
859
  if (shouldGenerateAtlas) {
844
860
  // Check if an ObjectLayer with the same item.id already exists (upsert by item ID)
845
- const existingOL = await ObjectLayer.findOne({ 'data.item.id': objectLayerId });
861
+ const existingOL = await ObjectLayer.findByItemId(objectLayerId);
846
862
  let objectLayer;
847
863
 
848
864
  if (existingOL) {
@@ -860,7 +876,6 @@ try {
860
876
  let stagingFileDoc = null;
861
877
  let stagingAtlasDoc = null;
862
878
  let stagingCid = '';
863
- let stagingSha256 = '';
864
879
  try {
865
880
  const itemKey = objectLayerId;
866
881
 
@@ -948,8 +963,7 @@ try {
948
963
  stagingData.render.cid = importAtlasCid;
949
964
  stagingData.render.metadataCid = importAtlasMetadataCid;
950
965
 
951
- // Pin data JSON to IPFS (compute final SHA-256 in memory)
952
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
966
+ // Pin data JSON to IPFS
953
967
  try {
954
968
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
955
969
  stagingData,
@@ -985,9 +999,8 @@ try {
985
999
  const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
986
1000
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
987
1001
 
988
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
1002
+ objectLayer = await ObjectLayer.upsertByItemId({
989
1003
  data: stagingData,
990
- sha256: stagingSha256,
991
1004
  cid: stagingCid,
992
1005
  objectLayerRenderFramesId: newRenderFrames._id,
993
1006
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -996,13 +1009,12 @@ try {
996
1009
  if (oldRenderFramesId) {
997
1010
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
998
1011
  }
999
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
1012
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
1000
1013
  } else {
1001
1014
  if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
1002
1015
  logger.warn(`[cut-over] Staging rolled back for ${objectLayerId}, live document preserved`);
1016
+ objectLayer = await ObjectLayer.findByItemId(objectLayerId);
1003
1017
  }
1004
-
1005
- objectLayer = await ObjectLayer.findById(existingOL._id);
1006
1018
  } else {
1007
1019
  // ── New item: stage everything before creating (same cut-over pattern) ──
1008
1020
  logger.info(`ObjectLayer '${objectLayerId}' is new, staging creation...`);
@@ -1017,7 +1029,6 @@ try {
1017
1029
  let stagingFileDoc = null;
1018
1030
  let stagingAtlasDoc = null;
1019
1031
  let stagingCid = '';
1020
- let stagingSha256 = '';
1021
1032
  try {
1022
1033
  const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
1023
1034
  entry.objectLayerRenderFramesData,
@@ -1101,7 +1112,6 @@ try {
1101
1112
  stagingData.render.cid = importAtlasCid;
1102
1113
  stagingData.render.metadataCid = importAtlasMetadataCid;
1103
1114
 
1104
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1105
1115
  try {
1106
1116
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
1107
1117
  stagingData,
@@ -1134,9 +1144,8 @@ try {
1134
1144
 
1135
1145
  if (cutoverReady) {
1136
1146
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1137
- objectLayer = await ObjectLayer.create({
1147
+ objectLayer = await ObjectLayer.upsertByItemId({
1138
1148
  data: stagingData,
1139
- sha256: stagingSha256,
1140
1149
  cid: stagingCid,
1141
1150
  objectLayerRenderFramesId: newRenderFrames._id,
1142
1151
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -1159,7 +1168,7 @@ try {
1159
1168
  if (existingItemIds.has(objectLayerId)) continue;
1160
1169
 
1161
1170
  // --import all: create documents without atlas generation
1162
- const existingOL = await ObjectLayer.findOne({ 'data.item.id': objectLayerId });
1171
+ const existingOL = await ObjectLayer.findByItemId(objectLayerId);
1163
1172
  let objectLayer;
1164
1173
 
1165
1174
  if (existingOL) {
@@ -1172,15 +1181,13 @@ try {
1172
1181
  if (!stagingData.render) stagingData.render = {};
1173
1182
  stagingData.render.cid = '';
1174
1183
  stagingData.render.metadataCid = '';
1175
- const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1176
1184
 
1177
1185
  // Atomic cut-over: create new RenderFrames, swap live doc, delete old
1178
1186
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1179
1187
  const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
1180
1188
 
1181
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
1189
+ objectLayer = await ObjectLayer.upsertByItemId({
1182
1190
  data: stagingData,
1183
- sha256: stagingSha256,
1184
1191
  objectLayerRenderFramesId: newRenderFrames._id,
1185
1192
  });
1186
1193
 
@@ -1188,20 +1195,17 @@ try {
1188
1195
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
1189
1196
  }
1190
1197
 
1191
- objectLayer = await ObjectLayer.findById(existingOL._id);
1192
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically (atlas skipped)`);
1198
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically (atlas skipped)`);
1193
1199
  } else {
1194
- // New item: create with sha256 populated (no atlas for bulk import)
1200
+ // New item (no atlas for bulk import)
1195
1201
  const stagingData = JSON.parse(JSON.stringify(entry.data));
1196
1202
  if (!stagingData.render) stagingData.render = {};
1197
1203
  stagingData.render.cid = '';
1198
1204
  stagingData.render.metadataCid = '';
1199
- const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1200
1205
 
1201
1206
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1202
- objectLayer = await ObjectLayer.create({
1207
+ objectLayer = await ObjectLayer.upsertByItemId({
1203
1208
  data: stagingData,
1204
- sha256: stagingSha256,
1205
1209
  objectLayerRenderFramesId: newRenderFrames._id,
1206
1210
  });
1207
1211
  }
@@ -1231,9 +1235,7 @@ try {
1231
1235
  logger.info(`Showing frame for item: ${itemId}, direction: ${direction}, frame: ${frameIndexNum}`);
1232
1236
 
1233
1237
  // Find ObjectLayer by item-id
1234
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId }).populate(
1235
- 'objectLayerRenderFramesId',
1236
- );
1238
+ const objectLayer = await ObjectLayer.findByItemId(itemId).populate('objectLayerRenderFramesId');
1237
1239
 
1238
1240
  if (!objectLayer) {
1239
1241
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -1317,9 +1319,7 @@ try {
1317
1319
  }
1318
1320
 
1319
1321
  // Find ObjectLayer by item-id
1320
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId }).populate(
1321
- 'objectLayerRenderFramesId',
1322
- );
1322
+ const objectLayer = await ObjectLayer.findByItemId(itemId).populate('objectLayerRenderFramesId');
1323
1323
 
1324
1324
  if (!objectLayer) {
1325
1325
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -1453,7 +1453,7 @@ try {
1453
1453
  logger.info(`Looking up atlas sprite sheet for item: ${itemId}`);
1454
1454
 
1455
1455
  // Find ObjectLayer by item-id
1456
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId });
1456
+ const objectLayer = await ObjectLayer.findByItemId(itemId);
1457
1457
 
1458
1458
  if (!objectLayer) {
1459
1459
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -1733,6 +1733,18 @@ try {
1733
1733
  'When used with --export or --import, only process cyberia-instance.json and cyberia-instance-conf.json',
1734
1734
  )
1735
1735
  .option('--drop', 'Drop all documents associated with the instance code before importing or as a standalone action')
1736
+ .option(
1737
+ '--export-current-fallbackworld',
1738
+ 'Capture the in-memory procedural fallback world as instance [instance-code]: materialize it into MongoDB (maps, conf, actions, quests, missing content defaults) and then export it',
1739
+ )
1740
+ .option(
1741
+ '--keep-fallback-codes',
1742
+ 'With --export-current-fallbackworld, keep the raw fallback-map-* / canonical action-quest codes instead of namespacing them under the instance code',
1743
+ )
1744
+ .option(
1745
+ '--fallback-url <url>',
1746
+ 'With --export-current-fallbackworld, capture the world a running engine currently serves (e.g. http://localhost:4001) instead of regenerating it locally',
1747
+ )
1736
1748
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
1737
1749
  .option('--mongo-host <mongo-host>', 'Mongo host override')
1738
1750
  .option('--dev', 'Force development environment')
@@ -1747,16 +1759,40 @@ try {
1747
1759
  .description('Export/import a Cyberia instance with all related maps, entities and object layers')
1748
1760
  .action(async (instanceCode, options = {}) => {
1749
1761
  if (options.revert) {
1750
- shellExec(`cd /home/dd/cyberia-instances && underpost cmt . reset && underpost run clean .`);
1751
- shellExec(`cd /home/dd/engine/cyberia-server && underpost cmt . reset && underpost run clean .`);
1752
- shellExec(`cd /home/dd/engine/cyberia-client && underpost cmt . reset && underpost run clean .`);
1762
+ Underpost.repo.declareSafeDirectory('/home/dd/cyberia-instances');
1763
+ shellExec(`cd /home/dd/cyberia-instances && ${cli()} cmt . reset && ${cli()} run clean .`);
1764
+ shellExec(`cd /home/dd/engine/cyberia-server && ${cli()} cmt . reset && ${cli()} run clean .`);
1765
+ shellExec(`cd /home/dd/engine/cyberia-client && ${cli()} cmt . reset && ${cli()} run clean .`);
1753
1766
  return;
1754
1767
  }
1768
+ if (options.exportCurrentFallbackworld) {
1769
+ // A capture writes one named instance: no default list, no import in the
1770
+ // same run, and never the reserved code the in-memory world itself uses.
1771
+ if (!instanceCode || instanceCode.includes(',')) {
1772
+ logger.error('--export-current-fallbackworld requires a single [instance-code] to capture the world under');
1773
+ process.exit(1);
1774
+ }
1775
+ if (instanceCode === 'fallback') {
1776
+ logger.error('"fallback" is reserved for the in-memory world — capture it under a different instance code');
1777
+ process.exit(1);
1778
+ }
1779
+ if (options.import !== undefined) {
1780
+ logger.error('--export-current-fallbackworld cannot be combined with --import');
1781
+ process.exit(1);
1782
+ }
1783
+ }
1784
+
1755
1785
  if (!instanceCode) {
1756
- instanceCode = 'amethyst-strata-expansion,FOREST';
1786
+ instanceCode = 'amethyst-strata-expansion,FOREST,TEST';
1757
1787
  logger.warn(`No instance code provided, defaulting to: ${instanceCode}`);
1758
1788
  }
1759
1789
 
1790
+ // An explicitly named env must exist: falling through to the ambient `./.env` resolves the
1791
+ // instance against whatever deployment was loaded last, which is not the one the caller named.
1792
+ if (options.envPath && !fs.existsSync(options.envPath)) {
1793
+ logger.error(`Env file not found: ${options.envPath}`);
1794
+ process.exit(1);
1795
+ }
1760
1796
  if (!options.envPath) options.envPath = `./.env`;
1761
1797
  if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
1762
1798
 
@@ -1778,12 +1814,15 @@ try {
1778
1814
  }
1779
1815
 
1780
1816
  if (options.publish || options.publishBuild || options.publishRemove) {
1817
+ // The instances checkout is cloned by the deploy user and driven by root during a deploy,
1818
+ // and git refuses to touch a tree owned by someone else until it is declared safe.
1819
+ Underpost.repo.declareSafeDirectory('/home/dd/cyberia-instances');
1781
1820
  if (options.publishBuild) {
1782
1821
  if (!fs.existsSync('/home/dd/cyberia-instances')) {
1783
- shellExec('cd /home/dd && underpost clone underpostnet/cyberia-instances');
1822
+ shellExec(`cd /home/dd && ${cli()} clone underpostnet/cyberia-instances`);
1784
1823
  } else {
1785
- shellExec(`underpost run clean /home/dd/cyberia-instances`);
1786
- shellExec(`cd /home/dd/cyberia-instances && underpost pull . underpostnet/cyberia-instances`, {
1824
+ shellExec(`${cli()} run clean /home/dd/cyberia-instances`);
1825
+ shellExec(`cd /home/dd/cyberia-instances && ${cli()} pull . underpostnet/cyberia-instances`, {
1787
1826
  silentOnError: true,
1788
1827
  });
1789
1828
  }
@@ -1810,40 +1849,24 @@ try {
1810
1849
  `/home/dd/cyberia-instances/conf/dd-cyberia/conf.volume.json`,
1811
1850
  );
1812
1851
  {
1813
- const catalog = await loadDeployCatalog('dd-cyberia');
1814
- fs.copyFileSync(
1815
- `./engine-private/conf/dd-cyberia/package.json`,
1816
- `/home/dd/cyberia-instances/conf/dd-cyberia/package.json`,
1817
- );
1818
- const originPackageJson = JSON.parse(
1819
- fs.readFileSync(`./engine-private/conf/dd-cyberia/package.json`, 'utf-8'),
1820
- );
1821
- const scriptsOrigin = originPackageJson.scripts;
1822
- const scriptsTarget = JSON.parse(fs.readFileSync(`./package.json`, 'utf-8')).scripts;
1823
- originPackageJson.name = 'cyberia';
1824
- originPackageJson.bin = {
1825
- cyberia: 'bin/index.js',
1826
- };
1827
- originPackageJson.keywords = catalog.keywords;
1828
- originPackageJson.description = catalog.description;
1852
+ // The published manifest is the deploy's, under the product's own identity — one
1853
+ // builder for every generated package.json in the project, so the instances repo
1854
+ // cannot drift from what the deploy and the product CLI declare.
1855
+ const deployPackagePath = deployPackagePathFactory('dd-cyberia');
1829
1856
  fs.writeFileSync(
1830
1857
  `/home/dd/cyberia-instances/conf/dd-cyberia/package.json`,
1831
- JSON.stringify(
1832
- {
1833
- ...originPackageJson,
1834
- scripts: {
1835
- ...scriptsTarget,
1836
- start: scriptsOrigin.start,
1837
- ...DOCKER_SCRIPTS,
1838
- },
1839
- dependencies: {
1840
- ...originPackageJson.dependencies,
1841
- ...CyberiaDependencies,
1842
- },
1843
- },
1858
+ `${JSON.stringify(
1859
+ buildDeployPackageJson({
1860
+ deployId: 'dd-cyberia',
1861
+ enginePackageJson: JSON.parse(fs.readFileSync(`./package.json`, 'utf-8')),
1862
+ catalog: await loadDeployCatalog('dd-cyberia'),
1863
+ currentPackageJson: JSON.parse(fs.readFileSync(deployPackagePath, 'utf-8')),
1864
+ productIdentity: true,
1865
+ }),
1844
1866
  null,
1845
- 2,
1846
- ),
1867
+ DEPLOY_MANIFEST_INDENT,
1868
+ )}\n`,
1869
+ 'utf8',
1847
1870
  );
1848
1871
  }
1849
1872
  fs.copyFileSync(
@@ -1860,7 +1883,11 @@ try {
1860
1883
  );
1861
1884
 
1862
1885
  fs.mkdirpSync(`/home/dd/cyberia-instances/deployments`);
1863
- fs.copySync(`./src/runtime/engine-cyberia`, `/home/dd/cyberia-instances/deployments/engine-cyberia`);
1886
+ // The staged CLI package is a local image-build artifact, not a deployment manifest —
1887
+ // it must never be published into the instances repository.
1888
+ fs.copySync(`./src/runtime/engine-cyberia`, `/home/dd/cyberia-instances/deployments/engine-cyberia`, {
1889
+ filter: (src) => nodePath.basename(src) !== STAGED_CLI_PACKAGE,
1890
+ });
1864
1891
  fs.copySync(
1865
1892
  `./manifests/deployment/dd-cyberia-development/.`,
1866
1893
  `/home/dd/cyberia-instances/deployments/engine-cyberia/.`,
@@ -1875,32 +1902,8 @@ try {
1875
1902
  `./engine-private/conf/dd-cyberia/instances/mmo-server/build/development/.`,
1876
1903
  `/home/dd/cyberia-instances/deployments/cyberia-server/.`,
1877
1904
  );
1878
- fs.removeSync(`/home/dd/cyberia-instances/public/cyberia`);
1879
- fs.mkdirpSync(`/home/dd/cyberia-instances/public/cyberia`);
1880
- for (const assetPath of Object.keys(
1881
- JSON.parse(fs.readFileSync(`./engine-private/conf/dd-cyberia/storage.engine-cyberia.json`, 'utf-8')),
1882
- )) {
1883
- const relativePath = assetPath.replace(/^src\/client\/public\/cyberia\//, '');
1884
- const targetPath = `/home/dd/cyberia-instances/public/cyberia/${relativePath}`;
1885
- fs.mkdirpSync(nodePath.dirname(targetPath));
1886
- logger.info(`Copying asset: ${assetPath} → ${targetPath}`);
1887
- fs.copySync(`./${assetPath}`, targetPath);
1888
- }
1889
-
1890
- // Copy default-items asset folders (src/client/public/cyberia/assets/<type>/<id>/ → public/cyberia/assets/<type>/<id>/)
1891
- for (const entry of DefaultCyberiaItems) {
1892
- const { id, type } = entry.item;
1893
- const srcDir = `src/client/public/cyberia/assets/${type}/${id}`;
1894
- const targetDir = `/home/dd/cyberia-instances/public/cyberia/assets/${type}/${id}`;
1895
- if (fs.existsSync(srcDir)) {
1896
- fs.mkdirpSync(nodePath.dirname(targetDir));
1897
- logger.info(`Copying default-item asset: ${srcDir} → ${targetDir}`);
1898
- fs.copySync(srcDir, targetDir);
1899
- } else {
1900
- logger.warn(`Default-item asset directory not found, skipping: ${srcDir}`);
1901
- }
1902
- }
1903
-
1905
+ // fs.removeSync(`/home/dd/cyberia-instances/public/cyberia`);
1906
+ // fs.mkdirpSync(`/home/dd/cyberia-instances/public/cyberia`);
1904
1907
  fs.mkdirpSync(`/home/dd/cyberia-instances/instances`);
1905
1908
  fs.mkdirpSync(`/home/dd/cyberia-instances/sagas`);
1906
1909
  for (const _instanceCode of instanceCode.split(',')) {
@@ -1957,7 +1960,7 @@ try {
1957
1960
  shellExec(`rm -rf /home/dd/cyberia-instances/sagas/${instanceCode}.json`);
1958
1961
  return;
1959
1962
  }
1960
- shellExec(`cd /home/dd/cyberia-instances && underpost push . underpostnet/cyberia-instances`);
1963
+ shellExec(`cd /home/dd/cyberia-instances && ${cli()} push . underpostnet/cyberia-instances`);
1961
1964
  return;
1962
1965
  }
1963
1966
 
@@ -2136,6 +2139,89 @@ try {
2136
2139
  }
2137
2140
  };
2138
2141
 
2142
+ // ── CAPTURE CURRENT FALLBACK WORLD ──────────────────────────────
2143
+ //
2144
+ // The procedural fallback world only exists in engine memory (see
2145
+ // cyberia-fallback-world.js): it is rebuilt from code defaults on every
2146
+ // boot and intercepts any service that asks for an absent instance.
2147
+ // Capturing writes that exact world into MongoDB under [instance-code] —
2148
+ // together with the content collections the fallback path serves from
2149
+ // code defaults rather than the DB — so the export below can back it up
2150
+ // and `--import` can restore it as an ordinary persisted instance.
2151
+ if (options.exportCurrentFallbackworld) {
2152
+ const { generateFallbackWorld } = await import('../src/api/cyberia-instance/cyberia-fallback-world.js');
2153
+ const { captureFallbackWorld } = await import('../src/api/cyberia-instance/cyberia-fallback-capture.js');
2154
+
2155
+ let world;
2156
+ if (options.fallbackUrl) {
2157
+ // Staged fallback default items live only in the serving engine
2158
+ // process, so a faithful capture of a live world must read it back
2159
+ // over REST instead of regenerating it here.
2160
+ const base = options.fallbackUrl.replace(/\/+$/, '');
2161
+ const worldUrl = base.includes('/fallback-world') ? base : `${base}/api/cyberia-instance/fallback-world`;
2162
+ logger.info('Fetching live fallback world', { url: worldUrl });
2163
+ const response = await fetch(worldUrl);
2164
+ if (!response.ok) {
2165
+ logger.error(`Fallback world fetch failed: ${response.status} ${response.statusText}`, { url: worldUrl });
2166
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2167
+ process.exit(1);
2168
+ }
2169
+ const payload = await response.json();
2170
+ world = payload?.data ?? payload;
2171
+ } else {
2172
+ world = generateFallbackWorld();
2173
+ }
2174
+
2175
+ if (!world?.instance || !Array.isArray(world.maps) || world.maps.length === 0) {
2176
+ logger.error('Fallback world payload has no maps — nothing to capture');
2177
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2178
+ process.exit(1);
2179
+ }
2180
+
2181
+ const capture = await captureFallbackWorld({
2182
+ models: {
2183
+ CyberiaInstance,
2184
+ CyberiaInstanceConf,
2185
+ CyberiaMap,
2186
+ CyberiaAction,
2187
+ CyberiaQuest,
2188
+ CyberiaSkill,
2189
+ CyberiaEntityTypeDefault,
2190
+ CyberiaDialogue,
2191
+ ObjectLayer,
2192
+ },
2193
+ world,
2194
+ instanceCode,
2195
+ keepFallbackCodes: !!options.keepFallbackCodes,
2196
+ });
2197
+
2198
+ // Sprites are the one thing a capture cannot synthesise: without their
2199
+ // ObjectLayer documents the backup would export atlas-less items and
2200
+ // the restored world would render solid-colour rectangles.
2201
+ if (capture.missingObjectLayerItemIds.length > 0) {
2202
+ logger.error(
2203
+ `Capture aborted: ${capture.missingObjectLayerItemIds.length} referenced item id(s) have no ObjectLayer in MongoDB:`,
2204
+ capture.missingObjectLayerItemIds.join(', '),
2205
+ `— run \`node bin/cyberia ol ${capture.missingObjectLayerItemIds.join(' ')} --import\` (or ` +
2206
+ '`node bin/cyberia run-workflow import-default-items`) first.',
2207
+ );
2208
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2209
+ process.exit(1);
2210
+ }
2211
+
2212
+ logger.info('Captured fallback world into MongoDB', {
2213
+ code: instanceCode,
2214
+ mapCodes: capture.plan.instance.cyberiaMapCodes,
2215
+ actions: capture.plan.actions.length,
2216
+ quests: capture.plan.quests.length,
2217
+ objectLayerItemIds: capture.plan.itemIds.length,
2218
+ });
2219
+
2220
+ // The capture is only half the command — fall through to the export so
2221
+ // it lands in ./engine-private/cyberia-instances/<instance-code>.
2222
+ if (options.export === undefined) options.export = true;
2223
+ }
2224
+
2139
2225
  // ── EXPORT ──────────────────────────────────────────────────────
2140
2226
  if (options.export !== undefined) {
2141
2227
  const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
@@ -2307,6 +2393,37 @@ try {
2307
2393
  }
2308
2394
  }
2309
2395
 
2396
+ // 4c-bis. Add the item ids the instance's vendor / assembler / quest
2397
+ // catalogs name. The interact modal draws an icon for every shop
2398
+ // item, recipe ingredient/output and quest reward, so their atlases
2399
+ // must travel with the backup even when no map entity wears them.
2400
+ // Kept out of contentItemIds: catalogs must not widen the
2401
+ // entity-type-default match surface below.
2402
+ for (const action of actions) {
2403
+ for (const shopItem of action.shopItems || []) {
2404
+ if (shopItem.itemId) objectLayerItemIds.add(shopItem.itemId);
2405
+ if (shopItem.priceItemId) objectLayerItemIds.add(shopItem.priceItemId);
2406
+ }
2407
+ for (const recipe of action.craftRecipes || []) {
2408
+ for (const ingredient of recipe.ingredients || []) {
2409
+ if (ingredient.itemId) objectLayerItemIds.add(ingredient.itemId);
2410
+ }
2411
+ for (const output of recipe.outputItems || []) {
2412
+ if (output.itemId) objectLayerItemIds.add(output.itemId);
2413
+ }
2414
+ }
2415
+ }
2416
+ for (const quest of quests) {
2417
+ for (const step of quest.steps || []) {
2418
+ for (const objective of step.objectives || []) {
2419
+ if (objective.itemId) objectLayerItemIds.add(objective.itemId);
2420
+ }
2421
+ }
2422
+ for (const reward of quest.rewards || []) {
2423
+ if (reward.itemId) objectLayerItemIds.add(reward.itemId);
2424
+ }
2425
+ }
2426
+
2310
2427
  // 4d. Export skills whose trigger item belongs to this instance (own model:
2311
2428
  // CyberiaSkill, keyed by triggerItemId). Their summoned-entity items are
2312
2429
  // added to the OL set so those atlases export too. Runs before the
@@ -2695,6 +2812,12 @@ try {
2695
2812
 
2696
2813
  logger.info('Importing instance', { code: instanceCode, backupDir });
2697
2814
 
2815
+ // Idempotent: a backup restore writes object layers, so collapse any
2816
+ // legacy duplicates and make the data.item.id index unique first.
2817
+ const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
2818
+ if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
2819
+ if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
2820
+
2698
2821
  // Item ids belonging to this instance (collected from imported object
2699
2822
  // layers + the instance doc) — used to backfill missing skills from the
2700
2823
  // canonical DefaultSkillConfig when the backup predates the skill model.
@@ -3029,12 +3152,21 @@ try {
3029
3152
  let olCount = 0;
3030
3153
  for (const file of olFiles) {
3031
3154
  const olData = fs.readJsonSync(`${olDir}/${file}`);
3032
- await ObjectLayer.deleteOne({ _id: olData._id });
3033
- if (olData.sha256) {
3034
- await ObjectLayer.deleteOne({ sha256: olData.sha256 });
3155
+ const olItemId = olData.data?.item?.id;
3156
+ if (!olItemId) {
3157
+ logger.warn(`Skipping object-layer file '${file}' missing data.item.id`);
3158
+ continue;
3035
3159
  }
3036
- await ObjectLayer.create(olData);
3037
- if (olData.data?.item?.id) importedItemIds.add(olData.data.item.id);
3160
+ // preserveUUID only applies to items this database does not have yet.
3161
+ // An item that already exists keeps its own _id and simply absorbs the
3162
+ // backup's attribute values, so a restore can never fork data.item.id
3163
+ // into two documents. sha256 needs no separate cleanup: it is derived
3164
+ // from data, so an identical hash implies the same item id.
3165
+ const existingOL = await ObjectLayer.findByItemId(olItemId);
3166
+ if (!existingOL && olData._id) await ObjectLayer.deleteOne({ _id: olData._id });
3167
+
3168
+ await ObjectLayer.upsertByItemId(olData);
3169
+ importedItemIds.add(olItemId);
3038
3170
  olCount++;
3039
3171
  }
3040
3172
  logger.info(`Imported ${olCount} ObjectLayer document(s)`);
@@ -4827,16 +4959,23 @@ try {
4827
4959
  shellExec(
4828
4960
  `node bin/cyberia generate-saga --import engine-private/cyberia-sagas/${sagaCode}.json${devFlag}${mongoHostFlag}`,
4829
4961
  );
4830
- shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}${mongoHostFlag}`);
4831
- shellExec(`node bin/cyberia run-workflow seed-skills${devFlag}${mongoHostFlag}`);
4832
- shellExec(`node bin/cyberia run-workflow seed-entities${devFlag}${mongoHostFlag}`);
4833
- shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}${mongoHostFlag}`);
4834
- shellExec(`node bin/cyberia run-workflow seed-actions-quests${devFlag}${mongoHostFlag}`);
4835
- shellExec(`node bin/cyberia client-hints ${instanceHintsCode} --seed-defaults${devFlag}${mongoHostFlag}`);
4962
+ // shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}${mongoHostFlag}`);
4963
+ // shellExec(`node bin/cyberia run-workflow seed-skills${devFlag}${mongoHostFlag}`);
4964
+ // shellExec(`node bin/cyberia run-workflow seed-entities${devFlag}${mongoHostFlag}`);
4965
+ // shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}${mongoHostFlag}`);
4966
+ // shellExec(`node bin/cyberia run-workflow seed-actions-quests${devFlag}${mongoHostFlag}`);
4967
+ // shellExec(`node bin/cyberia client-hints ${instanceHintsCode} --seed-defaults${devFlag}${mongoHostFlag}`);
4836
4968
  shellExec(`node bin/cyberia instance ${sagaCode} --import${devFlag}`);
4837
4969
  shellExec(`node bin/cyberia instance FOREST --import${devFlag}`);
4970
+ shellExec(`node bin/cyberia instance TEST --import${devFlag}`);
4838
4971
  });
4839
4972
 
4973
+ runner
4974
+ .command('stage-cli')
4975
+ .option('--output-path <output-path>', "Build context to stage the package in (default: '.')")
4976
+ .description('Packs this engine checkout as underpost-cli.tgz for a runtime image build context')
4977
+ .action((options) => stageCliPackage(options.outputPath || '.'));
4978
+
4840
4979
  runner.command('sync-src').action(() => {
4841
4980
  fs.copyFileSync('./cyberia-server/README.md', './src/client/public/cyberia-docs/CYBERIA-SERVER.md');
4842
4981
  fs.copyFileSync('./cyberia-server/Dockerfile', './src/runtime/cyberia-server/Dockerfile');
@@ -4844,6 +4983,25 @@ try {
4844
4983
  fs.copyFileSync('./cyberia-client/Dockerfile', './src/runtime/cyberia-client/Dockerfile');
4845
4984
  });
4846
4985
 
4986
+ runner.command('setup-workspace').action(() => {
4987
+ shellExec(`node bin fs src/client/public/cyberia --git --recursive --pull --deploy-id dd-cyberia`);
4988
+ shellExec(`node bin/deploy.js cyberia`);
4989
+ if (!fs.existsSync('./cyberia-server')) shellExec(`${cli()} clone underpostnet/cyberia-server`);
4990
+ if (!fs.existsSync('./cyberia-client')) shellExec(`${cli()} clone underpostnet/cyberia-client`);
4991
+ });
4992
+
4993
+ runner.command('e2e-build').action(() => {
4994
+ shellExec(`node bin run build-cluster-deployment-manifests`);
4995
+ shellExec(`node bin/cyberia run-workflow build-manifest`);
4996
+ shellExec(`node bin/cyberia run-workflow publish --dry-run`);
4997
+ shellExec(`npm run security`);
4998
+ shellExec(`sudo rm -rf ./conf.dd*.js`);
4999
+ });
5000
+
5001
+ runner.command('cluster').action(() => {
5002
+ shellExec(`node bin run cluster --runtime-image express --deploy-id dd-cyberia --instance-id mmo-server --dev`);
5003
+ });
5004
+
4847
5005
  runner
4848
5006
  .command('dev-env')
4849
5007
  .option('--run', 'Run docker:reset, cluster --dev --reset, docker-image, and docker:up after updating compose.env')
@@ -5026,6 +5184,57 @@ try {
5026
5184
  shellExec(`gh workflow run ${id}.cd.yml -R underpostnet/${id} -f job=deploy`);
5027
5185
  });
5028
5186
 
5187
+ runner.command('cp-assets').action(() => {
5188
+ for (const assetPath of Object.keys(
5189
+ JSON.parse(fs.readFileSync(`./engine-private/conf/dd-cyberia/storage.engine-cyberia.json`, 'utf-8')),
5190
+ )) {
5191
+ const relativePath = assetPath.replace(/^src\/client\/public\/cyberia\//, '');
5192
+ const targetPath = `/home/dd/cyberia-instances/public/cyberia/${relativePath}`;
5193
+ fs.mkdirpSync(nodePath.dirname(targetPath));
5194
+ logger.info(`Copying asset: ${assetPath} → ${targetPath}`);
5195
+ fs.copySync(`./${assetPath}`, targetPath);
5196
+ }
5197
+
5198
+ // Copy default-items asset folders (src/client/public/cyberia/assets/<type>/<id>/ → public/cyberia/assets/<type>/<id>/)
5199
+ for (const entry of DefaultCyberiaItems) {
5200
+ const { id, type } = entry.item;
5201
+ const srcDir = `src/client/public/cyberia/assets/${type}/${id}`;
5202
+ const targetDir = `/home/dd/cyberia-instances/public/cyberia/assets/${type}/${id}`;
5203
+ if (fs.existsSync(srcDir)) {
5204
+ fs.mkdirpSync(nodePath.dirname(targetDir));
5205
+ logger.info(`Copying default-item asset: ${srcDir} → ${targetDir}`);
5206
+ fs.copySync(srcDir, targetDir);
5207
+ } else {
5208
+ logger.warn(`Default-item asset directory not found, skipping: ${srcDir}`);
5209
+ }
5210
+ }
5211
+ });
5212
+
5213
+ runner
5214
+ .command('sync-cluster')
5215
+ .option('--build')
5216
+ .action((options) => {
5217
+ shellExec(`node bin/build dd-cyberia --update-private`);
5218
+ shellExec(`node bin/build dd-core --update-private`);
5219
+ if (options.build) return;
5220
+ shellExec(
5221
+ `node bin wireguard --sync --repo-engine underpostnet/engine-test-cyberia --repo-engine-private underpostnet/engine-private`,
5222
+ );
5223
+ });
5224
+
5225
+ runner
5226
+ .command('test')
5227
+ .option('--n-con <connections>', 'Number of concurrent WebSocket connections')
5228
+ .option('--duration <ms>', 'Load test duration in milliseconds')
5229
+ .option('--tap-freq <seconds>', 'Frequency of tap events in seconds')
5230
+ .action((options) => {
5231
+ shellExec(`CYBERIA_LOAD_WS_URL=ws://localhost:8081/ws \
5232
+ CYBERIA_LOAD_CONNECTIONS=${options.nCon ?? 40} \
5233
+ CYBERIA_LOAD_TAP_FREQUENCY=${options.tapFreq ?? 5} \
5234
+ CYBERIA_LOAD_DURATION_MS=${options.duration ?? 1000 * 60 * 30} \
5235
+ node bin test cyberia --grep 'Cyberia load'`);
5236
+ });
5237
+
5029
5238
  runner
5030
5239
  .command('docker-image [id]')
5031
5240
  .option('--load-tar', 'Load a pre-built image tar archive into the enabled target(s) without building.')
@@ -5033,46 +5242,49 @@ try {
5033
5242
  // no funca
5034
5243
  if (options.loadTar) {
5035
5244
  for (const imageId of dockerImageIds)
5036
- if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.2.90.tar`);
5245
+ if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.3.73.tar`);
5037
5246
  return;
5038
5247
  }
5039
5248
  switch (id) {
5040
5249
  case 'engine-cyberia':
5250
+ stageCliPackage(`./src/runtime/engine-cyberia`);
5041
5251
  shellExec(`
5042
5252
  node bin/build dd-cyberia --conf
5043
5253
  node bin/build dd-cyberia --update-private
5044
5254
  node bin image --path src/runtime/engine-cyberia \
5045
5255
  --docker-compose --pull-base --build \
5046
5256
  --dockerfile-name Dockerfile.dev \
5047
- --image-name engine-cyberia-dev:v3.2.90 \
5257
+ --image-name engine-cyberia-dev:v3.3.73 \
5048
5258
  --image-out-path .
5049
5259
  `);
5050
5260
  break;
5051
5261
 
5052
5262
  case 'cyberia-server':
5263
+ stageCliPackage(`./cyberia-server`);
5053
5264
  shellExec(`
5054
5265
  cp -f src/runtime/cyberia-server/Dockerfile.dev cyberia-server/Dockerfile.dev
5055
5266
  node bin image --path cyberia-server \
5056
5267
  --docker-compose --pull-base --build \
5057
5268
  --dockerfile-name Dockerfile.dev \
5058
- --image-name cyberia-server-dev:v3.2.90 \
5269
+ --image-name cyberia-server-dev:v3.3.73 \
5059
5270
  --image-out-path .
5060
5271
  `);
5061
5272
  break;
5062
5273
  case 'cyberia-client':
5274
+ stageCliPackage(`./cyberia-client`);
5063
5275
  shellExec(`
5064
5276
  cp -f src/runtime/cyberia-client/Dockerfile.dev cyberia-client/Dockerfile.dev
5065
5277
  node bin image --path cyberia-client \
5066
5278
  --docker-compose --pull-base --build \
5067
5279
  --dockerfile-name Dockerfile.dev \
5068
- --image-name cyberia-client-dev:v3.2.90 \
5280
+ --image-name cyberia-client-dev:v3.3.73 \
5069
5281
  --image-out-path .
5070
5282
  `);
5071
5283
  break;
5072
5284
  }
5073
5285
  });
5074
5286
 
5075
- for (const [cmd, action] of Object.entries(DOCKER_SCRIPTS))
5287
+ for (const [cmd, action] of Object.entries(cyberiaCatalog.packageScripts))
5076
5288
  runner.command(cmd).action(() => {
5077
5289
  if (cmd === 'docker:up' || cmd === 'docker:up:build' || cmd === 'docker:restart') {
5078
5290
  const { aliases, changed } = installCyberiaDockerHostAliases();
@@ -5626,6 +5838,23 @@ node bin image --path cyberia-client \
5626
5838
 
5627
5839
  // Copy canonical doc sources into the generated project READMEs.
5628
5840
  // Edit the canonical sources; never hand-edit these generated outputs.
5841
+ // The mirrored deploy tree is a generated artifact, rebuilt from scratch so a file
5842
+ // dropped or renamed upstream cannot linger in the published repo. It reproduces the
5843
+ // engine layout exactly — the <deploy-id> directory beside lib/ — because every deploy
5844
+ // script sources `$SCRIPT_DIR/../lib/logging.sh`, which only resolves when lib/ is the
5845
+ // script directory's sibling there too.
5846
+ for (const project of ['cyberia-client', 'cyberia-server']) {
5847
+ const scripts = `./deploy/${project}`;
5848
+ // A tree assembled before these scripts were packaged does not carry them; mirroring is
5849
+ // what publishes them, so say so and continue rather than failing the whole manifest build.
5850
+ if (!fs.existsSync(scripts)) {
5851
+ logger.warn(`[build-manifest] No deploy scripts to mirror for ${project}`, { path: scripts });
5852
+ continue;
5853
+ }
5854
+ fs.removeSync(`./${project}/deploy`);
5855
+ fs.copySync('./deploy/lib', `./${project}/deploy/lib`);
5856
+ fs.copySync(scripts, `./${project}/deploy/${project}`);
5857
+ }
5629
5858
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
5630
5859
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md');
5631
5860
  fs.copyFileSync(
@@ -5637,7 +5866,15 @@ node bin image --path cyberia-client \
5637
5866
  './cyberia-server/.github/workflows/cyberia-server.cd.yml',
5638
5867
  );
5639
5868
  shellExec('cp -a ./engine-private/conf/dd-cyberia/docker-compose/cyberia/. ./src/runtime/engine-cyberia/');
5640
- shellExec('node bin/cyberia.js instance --publish-build');
5869
+ // The publish is scoped to the deployment this workflow builds — every other step here is —
5870
+ // so it reads dd-cyberia's own environment instead of the working-tree `./.env`, which names
5871
+ // whichever deployment `app load` ran for last.
5872
+ shellExec(
5873
+ `node bin/cyberia.js instance --publish-build --env-path ${deployEnvFilePath(
5874
+ 'dd-cyberia',
5875
+ isDev ? 'development' : 'production',
5876
+ )}`,
5877
+ );
5641
5878
  logger.info(`run-workflow build-manifest complete (${isDev ? 'dev' : 'prod'})`);
5642
5879
  });
5643
5880
 
@@ -5767,7 +6004,7 @@ node bin image --path cyberia-client \
5767
6004
  // line.
5768
6005
  if (error && error.message === 'Trigger underpost passthrough') {
5769
6006
  process.argv = process.argv.filter((c) => c !== 'underpost');
5770
- logger.warn('Rerouting to underpost cli...');
6007
+ if (!process.argv.includes('--plain')) logger.info('Rerouting to underpost cli...');
5771
6008
  try {
5772
6009
  await underpostProgram.parseAsync();
5773
6010
  } catch (err) {