cyberia 3.2.12 → 3.2.70

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 (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
package/bin/index.js CHANGED
@@ -5,10 +5,6 @@
5
5
  * Provides commands for importing, viewing, and managing object layer assets,
6
6
  * render frames, and atlas sprite sheets from the command line.
7
7
  *
8
- * Delegates shared object layer creation logic to {@link ObjectLayerEngine} in
9
- * `src/server/object-layer.js` to keep a single source of truth shared with
10
- * the REST API service layer.
11
- *
12
8
  * @module bin/cyberia.js
13
9
  * @namespace CyberiaCLI
14
10
  */
@@ -19,34 +15,44 @@ import fs from 'fs-extra';
19
15
  import stringify from 'fast-json-stable-stringify';
20
16
  import { shellExec } from '../src/server/process.js';
21
17
  import { loggerFactory } from '../src/server/logger.js';
22
- import { generateBesuManifests, deployBesu, removeBesu } from '../src/server/besu-genesis-generator.js';
18
+ import { generateBesuManifests, deployBesu, removeBesu } from '../src/projects/cyberia/besu-genesis-generator.js';
23
19
  import { DataBaseProviderService } from '../src/db/DataBaseProvider.js';
24
20
  import { loadConfServerJson } from '../src/server/conf.js';
25
21
  import {
26
22
  ObjectLayerEngine,
27
23
  resolveCanonicalCid,
28
24
  pngDirectoryIteratorByObjectLayerType,
29
- getKeyFramesDirectionsFromNumberFolderDirection,
30
25
  buildImgFromTile,
31
- } from '../src/server/object-layer.js';
32
- import { AtlasSpriteSheetGenerator } from '../src/server/atlas-sprite-sheet-generator.js';
33
- import { generateMultiFrame, lookupSemantic, semanticRegistry } from '../src/server/semantic-layer-generator.js';
34
- import { IpfsClient } from '../src/server/ipfs-client.js';
26
+ } from '../src/projects/cyberia/object-layer.js';
27
+ import { getKeyframeDirectionsByCode } from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
28
+ import { AtlasSpriteSheetGenerator } from '../src/projects/cyberia/atlas-sprite-sheet-generator.js';
29
+ import {
30
+ generateMultiFrame,
31
+ lookupSemantic,
32
+ semanticRegistry,
33
+ } from '../src/projects/cyberia/semantic-layer-generator.js';
34
+ import { IpfsClient } from '../src/projects/cyberia/ipfs-client.js';
35
35
  import { createPinRecord } from '../src/api/ipfs/ipfs.service.js';
36
36
  import { program as underpostProgram } from '../src/cli/index.js';
37
+ import { generateSaga, importSaga } from '../src/projects/cyberia/generate-saga.js';
37
38
  import crypto from 'crypto';
38
39
  import nodePath from 'path';
39
40
  import Underpost from '../src/index.js';
40
41
  import { newInstance } from '../src/client/components/core/CommonJs.js';
41
42
  import {
42
- ITEM_TYPES as itemTypes,
43
- DefaultCyberiaItems,
44
43
  DefaultSkillConfig,
45
44
  DefaultCyberiaDialogues,
46
45
  DefaultCyberiaActions,
47
46
  DefaultCyberiaQuests,
47
+ ENTITY_TYPE_DEFAULTS,
48
+ fillInstanceConfDefaults,
48
49
  } from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
49
50
 
51
+ import {
52
+ ITEM_TYPES as itemTypes,
53
+ DefaultCyberiaItems,
54
+ } from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
55
+
50
56
  /**
51
57
  * Connect to the project MongoDB instance using the standard env / conf layout.
52
58
  *
@@ -744,7 +750,7 @@ try {
744
750
 
745
751
  /**
746
752
  * Accumulated object layer data keyed by objectLayerId.
747
- * @type {Object<string, import('../src/server/object-layer.js').ObjectLayerData>}
753
+ * @type {Object<string, import('../src/projects/cyberia/object-layer.js').ObjectLayerData>}
748
754
  */
749
755
  const objectLayers = {};
750
756
 
@@ -1221,7 +1227,7 @@ try {
1221
1227
  }
1222
1228
 
1223
1229
  // Get the keyframe direction name from the numerical direction code
1224
- const objectLayerFrameDirections = getKeyFramesDirectionsFromNumberFolderDirection(direction);
1230
+ const objectLayerFrameDirections = getKeyframeDirectionsByCode(direction);
1225
1231
  if (objectLayerFrameDirections.length === 0) {
1226
1232
  logger.error(`Invalid direction code: ${direction}. Valid codes: 08, 18, 02, 12, 04, 14, 06, 16`);
1227
1233
  process.exit(1);
@@ -1711,11 +1717,18 @@ try {
1711
1717
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
1712
1718
  .option('--mongo-host <mongo-host>', 'Mongo host override')
1713
1719
  .option('--dev', 'Force development environment')
1720
+ .option('--publish-build', 'Build instance backup directory with all related maps, entities and object layers')
1721
+ .option('--publish-remove', 'Remove published instance from underpostnet/cyberia-instances repository')
1722
+ .option('--publish', 'Publish instance in underpostnet/cyberia-instances repository')
1723
+ .option(
1724
+ '--from-n-commit <n>',
1725
+ 'Number of latest engine commits to use for the publish commit message (default: 1).',
1726
+ )
1714
1727
  .description('Export/import a Cyberia instance with all related maps, entities and object layers')
1715
1728
  .action(async (instanceCode, options = {}) => {
1716
1729
  if (!instanceCode) {
1717
- logger.error('instance-code argument is required');
1718
- process.exit(1);
1730
+ instanceCode = 'amethyst-strata-expansion';
1731
+ logger.warn(`No instance code provided, defaulting to: ${instanceCode}`);
1719
1732
  }
1720
1733
 
1721
1734
  if (!options.envPath) options.envPath = `./.env`;
@@ -1737,6 +1750,113 @@ try {
1737
1750
  logger.error(`Server config not found: ${confServerPath}`);
1738
1751
  process.exit(1);
1739
1752
  }
1753
+
1754
+ if (options.publish || options.publishBuild || options.publishRemove) {
1755
+ if (options.publishBuild) {
1756
+ if (!fs.existsSync('/home/dd/cyberia-instances')) {
1757
+ shellExec('cd /home/dd && underpost clone underpostnet/cyberia-instances');
1758
+ } else {
1759
+ shellExec(`underpost run clean /home/dd/cyberia-instances`);
1760
+ shellExec(`cd /home/dd/cyberia-instances && underpost pull . underpostnet/cyberia-instances`, {
1761
+ silentOnError: true,
1762
+ });
1763
+ }
1764
+
1765
+ fs.mkdirpSync(`/home/dd/cyberia-instances/conf/dd-cyberia`);
1766
+ fs.copyFileSync(
1767
+ `./engine-private/conf/dd-cyberia/conf.server.dev.dev.json`,
1768
+ `/home/dd/cyberia-instances/conf/dd-cyberia/conf.server.json`,
1769
+ );
1770
+ fs.copyFileSync(
1771
+ `./engine-private/conf/dd-cyberia/conf.client.json`,
1772
+ `/home/dd/cyberia-instances/conf/dd-cyberia/conf.client.json`,
1773
+ );
1774
+ fs.copyFileSync(
1775
+ `./engine-private/conf/dd-cyberia/conf.cron.json`,
1776
+ `/home/dd/cyberia-instances/conf/dd-cyberia/conf.cron.json`,
1777
+ );
1778
+ fs.copyFileSync(
1779
+ `./engine-private/conf/dd-cyberia/conf.ssr.json`,
1780
+ `/home/dd/cyberia-instances/conf/dd-cyberia/conf.ssr.json`,
1781
+ );
1782
+ fs.copyFileSync(
1783
+ `./engine-private/conf/dd-cyberia/conf.volume.json`,
1784
+ `/home/dd/cyberia-instances/conf/dd-cyberia/conf.volume.json`,
1785
+ );
1786
+ fs.copyFileSync(
1787
+ `./engine-private/conf/dd-cyberia/package.json`,
1788
+ `/home/dd/cyberia-instances/conf/dd-cyberia/package.json`,
1789
+ );
1790
+ fs.copyFileSync(
1791
+ `./engine-private/conf/dd-cyberia/docker-compose/cyberia/compose.env`,
1792
+ `/home/dd/cyberia-instances/conf/dd-cyberia/.env.production`,
1793
+ );
1794
+ fs.copyFileSync(
1795
+ `./engine-private/conf/dd-cyberia/docker-compose/cyberia/compose.env`,
1796
+ `/home/dd/cyberia-instances/conf/dd-cyberia/.env.development`,
1797
+ );
1798
+ fs.copyFileSync(
1799
+ `./engine-private/conf/dd-cyberia/docker-compose/cyberia/compose.env`,
1800
+ `/home/dd/cyberia-instances/conf/dd-cyberia/.env.test`,
1801
+ );
1802
+
1803
+ fs.mkdirpSync(`/home/dd/cyberia-instances/deployments`);
1804
+ fs.copySync(`./src/runtime/engine-cyberia`, `/home/dd/cyberia-instances/deployments/engine-cyberia`);
1805
+ fs.copySync(
1806
+ `./manifests/deployment/dd-cyberia-development/.`,
1807
+ `/home/dd/cyberia-instances/deployments/engine-cyberia/.`,
1808
+ );
1809
+ fs.copySync(`./src/runtime/cyberia-client`, `/home/dd/cyberia-instances/deployments/cyberia-client`);
1810
+ fs.copySync(
1811
+ `./engine-private/conf/dd-cyberia/instances/mmo-client/build/development/.`,
1812
+ `/home/dd/cyberia-instances/deployments/cyberia-client/.`,
1813
+ );
1814
+ fs.copySync(`./src/runtime/cyberia-server`, `/home/dd/cyberia-instances/deployments/cyberia-server`);
1815
+ fs.copySync(
1816
+ `./engine-private/conf/dd-cyberia/instances/mmo-server/build/development/.`,
1817
+ `/home/dd/cyberia-instances/deployments/cyberia-server/.`,
1818
+ );
1819
+ fs.removeSync(`/home/dd/cyberia-instances/public/cyberia`);
1820
+ fs.mkdirpSync(`/home/dd/cyberia-instances/public/cyberia`);
1821
+ for (const assetPath of Object.keys(
1822
+ JSON.parse(fs.readFileSync(`./engine-private/conf/dd-cyberia/storage.engine-cyberia.json`, 'utf-8')),
1823
+ )) {
1824
+ const relativePath = assetPath.replace(/^src\/client\/public\/cyberia\//, '');
1825
+ const targetPath = `/home/dd/cyberia-instances/public/cyberia/${relativePath}`;
1826
+ fs.mkdirpSync(nodePath.dirname(targetPath));
1827
+ logger.info(`Copying asset: ${assetPath} → ${targetPath}`);
1828
+ fs.copySync(`./${assetPath}`, targetPath);
1829
+ }
1830
+
1831
+ fs.mkdirpSync(`/home/dd/cyberia-instances/instances`);
1832
+ fs.copySync(
1833
+ `./engine-private/cyberia-instances/${instanceCode}`,
1834
+ `/home/dd/cyberia-instances/instances/${instanceCode}`,
1835
+ );
1836
+ fs.mkdirpSync(`/home/dd/cyberia-instances/sagas`);
1837
+ fs.copyFileSync(
1838
+ `./engine-private/cyberia-sagas/${instanceCode}.json`,
1839
+ `/home/dd/cyberia-instances/sagas/${instanceCode}.json`,
1840
+ );
1841
+ return;
1842
+ } else if (options.publishRemove) {
1843
+ shellExec(`rm -rf /home/dd/cyberia-instances/instances/${instanceCode}`);
1844
+ shellExec(`rm -rf /home/dd/cyberia-instances/sagas/${instanceCode}.json`);
1845
+ return;
1846
+ }
1847
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
1848
+ const publishMessage =
1849
+ shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
1850
+ stdout: true,
1851
+ silent: true,
1852
+ }).trim() || `Update instance ${instanceCode}`;
1853
+ shellExec(`cd /home/dd/cyberia-instances \
1854
+ && git add . \
1855
+ && git commit -m "${publishMessage.replace(/"/g, '\\"')}" \
1856
+ && underpost push . underpostnet/cyberia-instances`);
1857
+ return;
1858
+ }
1859
+
1740
1860
  const confServer = loadConfServerJson(confServerPath, { resolve: true });
1741
1861
  const { db } = confServer[host][path];
1742
1862
 
@@ -1755,6 +1875,11 @@ try {
1755
1875
  'cyberia-dialogue',
1756
1876
  'cyberia-map',
1757
1877
  'cyberia-entity',
1878
+ 'cyberia-quest',
1879
+ 'cyberia-action',
1880
+ 'cyberia-skill',
1881
+ 'cyberia-entity-type-default',
1882
+ 'cyberia-saga',
1758
1883
  'object-layer',
1759
1884
  'object-layer-render-frames',
1760
1885
  'atlas-sprite-sheet',
@@ -1770,6 +1895,11 @@ try {
1770
1895
  const CyberiaInstanceConf = DataBaseProviderService.getModel('cyberia-instance-conf', { host, path });
1771
1896
  const CyberiaDialogue = DataBaseProviderService.getModel('cyberia-dialogue', { host, path });
1772
1897
  const CyberiaMap = DataBaseProviderService.getModel('cyberia-map', { host, path });
1898
+ const CyberiaQuest = DataBaseProviderService.getModel('cyberia-quest', { host, path });
1899
+ const CyberiaAction = DataBaseProviderService.getModel('cyberia-action', { host, path });
1900
+ const CyberiaSkill = DataBaseProviderService.getModel('cyberia-skill', { host, path });
1901
+ const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('cyberia-entity-type-default', { host, path });
1902
+ const CyberiaSaga = DataBaseProviderService.getModel('cyberia-saga', { host, path });
1773
1903
  const ObjectLayer = DataBaseProviderService.getModel('object-layer', { host, path });
1774
1904
  const ObjectLayerRenderFrames = DataBaseProviderService.getModel('object-layer-render-frames', { host, path });
1775
1905
  const AtlasSpriteSheet = DataBaseProviderService.getModel('atlas-sprite-sheet', { host, path });
@@ -1852,36 +1982,6 @@ try {
1852
1982
  ...(entry.mfsPaths.length ? { mfsPaths: entry.mfsPaths } : {}),
1853
1983
  }));
1854
1984
 
1855
- const getDefaultDialoguesByItemId = (itemIds = []) => {
1856
- const requestedItemIds = new Set(itemIds.filter(Boolean));
1857
- const defaultsByItemId = new Map();
1858
-
1859
- for (const dialogue of DefaultCyberiaDialogues) {
1860
- // Match by code prefix: "default-<itemId>" covers the common case;
1861
- // callers may also pass a full code directly.
1862
- const matchingIds = [...requestedItemIds].filter(
1863
- (id) => dialogue.code === `default-${id}` || dialogue.code === id,
1864
- );
1865
- if (!matchingIds.length) continue;
1866
- for (const id of matchingIds) {
1867
- if (!defaultsByItemId.has(id)) defaultsByItemId.set(id, []);
1868
- defaultsByItemId.get(id).push({
1869
- code: dialogue.code,
1870
- order: dialogue.order ?? 0,
1871
- speaker: dialogue.speaker ?? '',
1872
- text: dialogue.text,
1873
- mood: dialogue.mood ?? 'neutral',
1874
- });
1875
- }
1876
- }
1877
-
1878
- for (const dialogues of defaultsByItemId.values()) {
1879
- dialogues.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1880
- }
1881
-
1882
- return defaultsByItemId;
1883
- };
1884
-
1885
1985
  const rewriteImportedCidReferences = async ({ oldCid, newCid, resourceType }) => {
1886
1986
  if (!oldCid || !newCid || oldCid === newCid) return;
1887
1987
 
@@ -1965,6 +2065,10 @@ try {
1965
2065
  instanceConf = created?.toObject ? created.toObject() : created;
1966
2066
  }
1967
2067
  if (instanceConf) {
2068
+ // `.lean()` skips Mongoose schema defaults and older docs may predate
2069
+ // some fields, so backfill every CyberiaInstanceConfSchema field from
2070
+ // the canonical defaults before writing the backup.
2071
+ instanceConf = fillInstanceConfDefaults(instanceConf);
1968
2072
  fs.writeJsonSync(`${backupDir}/cyberia-instance-conf.json`, instanceConf, { spaces: 2 });
1969
2073
  logger.info('Exported CyberiaInstanceConf', { instanceCode });
1970
2074
  } else {
@@ -1998,6 +2102,36 @@ try {
1998
2102
  }
1999
2103
  logger.info(`Exported ${maps.length} CyberiaMap document(s)`, { codes: maps.map((m) => m.code) });
2000
2104
 
2105
+ // 3b. Export quests + actions bound to THIS instance's maps (sourceMapCode
2106
+ // in the instance's map codes) — only the content tied to this instance.
2107
+ // Dialogue codes the actions reference are collected so they travel too.
2108
+ const actionDialogueCodes = new Set();
2109
+ const quests = await CyberiaQuest.find({ sourceMapCode: { $in: [...mapCodes] } }).lean();
2110
+ if (quests.length > 0) {
2111
+ fs.ensureDirSync(`${backupDir}/cyberia-quests`);
2112
+ for (const quest of quests) {
2113
+ fs.writeJsonSync(`${backupDir}/cyberia-quests/${encodeURIComponent(quest.code)}.json`, quest, {
2114
+ spaces: 2,
2115
+ });
2116
+ }
2117
+ logger.info(`Exported ${quests.length} CyberiaQuest document(s)`, { codes: quests.map((q) => q.code) });
2118
+ }
2119
+
2120
+ const actions = await CyberiaAction.find({ sourceMapCode: { $in: [...mapCodes] } }).lean();
2121
+ if (actions.length > 0) {
2122
+ fs.ensureDirSync(`${backupDir}/cyberia-actions`);
2123
+ for (const action of actions) {
2124
+ fs.writeJsonSync(`${backupDir}/cyberia-actions/${encodeURIComponent(action.code)}.json`, action, {
2125
+ spaces: 2,
2126
+ });
2127
+ if (action.dialogCode) actionDialogueCodes.add(action.dialogCode);
2128
+ for (const qd of action.questDialogueCodes || []) {
2129
+ if (qd.dialogCode) actionDialogueCodes.add(qd.dialogCode);
2130
+ }
2131
+ }
2132
+ logger.info(`Exported ${actions.length} CyberiaAction document(s)`, { codes: actions.map((a) => a.code) });
2133
+ }
2134
+
2001
2135
  // 4. Collect all objectLayerItemIds from map entities
2002
2136
  const objectLayerItemIds = new Set();
2003
2137
  for (const map of maps) {
@@ -2008,11 +2142,14 @@ try {
2008
2142
  }
2009
2143
  }
2010
2144
 
2011
- // 4b. Add instance-level itemIds
2012
- for (const id of instance.itemIds || []) {
2145
+ // 4b. Add instance-level itemIds ({ id, defaultPlayerInventory }).
2146
+ for (const entry of instance.itemIds || []) {
2147
+ const id = typeof entry === 'string' ? entry : entry?.id;
2013
2148
  if (id) objectLayerItemIds.add(id);
2014
2149
  }
2015
2150
 
2151
+ const contentItemIds = new Set(objectLayerItemIds);
2152
+
2016
2153
  // 4c. Add all itemIds referenced by CyberiaInstanceConf (entityDefaults + skillConfig).
2017
2154
  // This ensures liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers and
2018
2155
  // skill trigger items are included even if no map entity currently uses them.
@@ -2035,50 +2172,106 @@ try {
2035
2172
  }
2036
2173
  }
2037
2174
 
2038
- // 4d. Export dialogues for all relevant object-layer items. Codes follow the pattern
2039
- // "default-<itemId>". If an item has no dialogue docs yet but ships with
2040
- // DefaultCyberiaDialogues, seed those defaults into Mongo first.
2175
+ // 4d. Export skills whose trigger item belongs to this instance (own model:
2176
+ // CyberiaSkill, keyed by triggerItemId). Their summoned-entity items are
2177
+ // added to the OL set so those atlases export too. Runs before the
2178
+ // dialogue + OL queries so the summoned ids are included.
2041
2179
  if (objectLayerItemIds.size > 0) {
2042
- const requestedItemIds = [...objectLayerItemIds];
2043
- const requestedCodes = requestedItemIds.map((id) => `default-${id}`);
2044
- const defaultDialoguesByItemId = getDefaultDialoguesByItemId(requestedItemIds);
2045
- const existingDialogueDocs = await CyberiaDialogue.find({
2046
- code: { $in: requestedCodes },
2047
- })
2048
- .sort({ code: 1, order: 1 })
2049
- .lean();
2050
-
2051
- const existingDialogueCodes = new Set(existingDialogueDocs.map((dialogue) => dialogue.code).filter(Boolean));
2052
- let seededDialogueCount = 0;
2053
-
2054
- for (const [itemId, dialogues] of defaultDialoguesByItemId.entries()) {
2055
- const firstCode = dialogues[0]?.code;
2056
- if (firstCode && existingDialogueCodes.has(firstCode)) continue;
2180
+ const skills = await CyberiaSkill.find({ triggerItemId: { $in: [...objectLayerItemIds] } }).lean();
2181
+ if (skills.length > 0) {
2182
+ fs.ensureDirSync(`${backupDir}/cyberia-skills`);
2183
+ for (const skill of skills) {
2184
+ fs.writeJsonSync(`${backupDir}/cyberia-skills/${encodeURIComponent(skill.triggerItemId)}.json`, skill, {
2185
+ spaces: 2,
2186
+ });
2187
+ for (const def of skill.skills || []) {
2188
+ if (def.summonedEntityItemId && !def.summonedEntityItemId.startsWith('$')) {
2189
+ objectLayerItemIds.add(def.summonedEntityItemId);
2190
+ }
2191
+ }
2192
+ }
2193
+ logger.info(`Exported ${skills.length} CyberiaSkill document(s)`, {
2194
+ triggerItemIds: skills.map((sk) => sk.triggerItemId),
2195
+ });
2196
+ }
2197
+ }
2057
2198
 
2058
- for (const dialogue of dialogues) {
2059
- await CyberiaDialogue.findOneAndUpdate(
2060
- { code: dialogue.code, order: dialogue.order },
2061
- {
2062
- $set: {
2063
- speaker: dialogue.speaker,
2064
- text: dialogue.text,
2065
- mood: dialogue.mood,
2066
- },
2067
- },
2068
- { upsert: true },
2069
- );
2070
- seededDialogueCount++;
2199
+ // 4d-bis. Export entity-type defaults whose item ids belong to this
2200
+ // instance's real content (own model: CyberiaEntityTypeDefault). A
2201
+ // default is related when any of its live/dead/drop ids or default
2202
+ // object-layer ids appears in contentItemIds — map/instance content
2203
+ // only, NOT the canonical conf defaults every instance shares (which
2204
+ // would spuriously drag in the global seed entity-type-defaults).
2205
+ // Matched ids are folded back into objectLayerItemIds so the related
2206
+ // atlases + dialogues export too.
2207
+ if (contentItemIds.size > 0) {
2208
+ const idsForMatch = [...contentItemIds];
2209
+ const entityDefaults = await CyberiaEntityTypeDefault.find({
2210
+ $or: [
2211
+ { liveItemIds: { $in: idsForMatch } },
2212
+ { deadItemIds: { $in: idsForMatch } },
2213
+ { dropItemIds: { $in: idsForMatch } },
2214
+ { 'defaultObjectLayers.itemId': { $in: idsForMatch } },
2215
+ ],
2216
+ }).lean();
2217
+ if (entityDefaults.length > 0) {
2218
+ fs.ensureDirSync(`${backupDir}/cyberia-entity-type-defaults`);
2219
+ for (const ed of entityDefaults) {
2220
+ fs.writeJsonSync(`${backupDir}/cyberia-entity-type-defaults/${ed._id}.json`, ed, { spaces: 2 });
2221
+ for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
2222
+ for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
2223
+ for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
2224
+ for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) objectLayerItemIds.add(slot.itemId);
2071
2225
  }
2226
+ logger.info(`Exported ${entityDefaults.length} CyberiaEntityTypeDefault document(s)`, {
2227
+ entityTypes: entityDefaults.map((ed) => ed.entityType),
2228
+ });
2229
+ }
2230
+ }
2231
+
2232
+ // 4e. Export sagas related to this instance. A saga is considered related
2233
+ // when its code matches the instance code (direct namespace match), or
2234
+ // when its mapCodes or itemIds overlap with the instance's data.
2235
+ // At this point objectLayerItemIds contains all map-entity, instance-level,
2236
+ // conf-default, and skill-summoned item IDs — giving the broadest possible
2237
+ // match surface for saga discovery.
2238
+ const sagaCodeMatch = instanceCode ? await CyberiaSaga.find({ code: instanceCode }).lean() : [];
2239
+ // Disabling overlaps queries for now because they can be very expensive and are not strictly necessary for a backup.
2240
+ const sagaMapOverlap = true
2241
+ ? []
2242
+ : mapCodes.size > 0
2243
+ ? await CyberiaSaga.find({ mapCodes: { $in: [...mapCodes] } }).lean()
2244
+ : [];
2245
+ const sagaItemOverlap = true
2246
+ ? []
2247
+ : objectLayerItemIds.size > 0
2248
+ ? await CyberiaSaga.find({ itemIds: { $in: [...objectLayerItemIds] } }).lean()
2249
+ : [];
2250
+ const allSagas = [
2251
+ ...new Map(
2252
+ [...sagaCodeMatch, ...sagaMapOverlap, ...sagaItemOverlap].map((s) => [s._id.toString(), s]),
2253
+ ).values(),
2254
+ ];
2255
+ if (allSagas.length > 0) {
2256
+ fs.ensureDirSync(`${backupDir}/cyberia-sagas`);
2257
+ for (const saga of allSagas) {
2258
+ fs.writeJsonSync(`${backupDir}/cyberia-sagas/${encodeURIComponent(saga.code)}.json`, saga, { spaces: 2 });
2072
2259
  }
2260
+ logger.info(`Exported ${allSagas.length} CyberiaSaga document(s)`, { codes: allSagas.map((s) => s.code) });
2261
+ }
2073
2262
 
2263
+ // 4f. Export dialogues for all relevant object-layer items (codes follow the
2264
+ // pattern "default-<itemId>") plus the dialogue codes the instance's
2265
+ // actions reference. If an item has no dialogue docs yet but ships with
2266
+ // DefaultCyberiaDialogues, seed those defaults into Mongo first.
2267
+ if (objectLayerItemIds.size > 0 || actionDialogueCodes.size > 0) {
2268
+ const requestedItemIds = [...objectLayerItemIds];
2269
+ const requestedCodes = [
2270
+ ...new Set([...requestedItemIds.map((id) => `default-${id}`), ...actionDialogueCodes]),
2271
+ ];
2074
2272
  const dialogueDocs = await CyberiaDialogue.find({ code: { $in: requestedCodes } })
2075
2273
  .sort({ code: 1, order: 1 })
2076
2274
  .lean();
2077
-
2078
- if (seededDialogueCount > 0) {
2079
- logger.info(`Seeded ${seededDialogueCount} CyberiaDialogue default record(s) for export`);
2080
- }
2081
-
2082
2275
  if (dialogueDocs.length > 0) {
2083
2276
  fs.ensureDirSync(`${backupDir}/cyberia-dialogues`);
2084
2277
  const dialoguesByCode = new Map();
@@ -2164,20 +2357,12 @@ try {
2164
2357
  }
2165
2358
  }
2166
2359
 
2167
- // Export AtlasSpriteSheet + its File using canonical payload bytes from the DB state.
2168
- if (ol.atlasSpriteSheetId) {
2169
- const atlas = await AtlasSpriteSheet.findById(ol.atlasSpriteSheetId).lean();
2170
- if (!atlas) {
2171
- ipfsPayloadFailures.push({
2172
- itemKey,
2173
- resourceType: 'atlas-sprite-sheet',
2174
- mfsPath: itemPaths.atlasSpriteSheet,
2175
- reason: 'AtlasSpriteSheet document not found in MongoDB',
2176
- });
2177
- continue;
2178
- }
2179
-
2360
+ const atlas =
2361
+ (ol.atlasSpriteSheetId ? await AtlasSpriteSheet.findById(ol.atlasSpriteSheetId).lean() : null) ||
2362
+ (await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey }).lean());
2363
+ if (atlas) {
2180
2364
  const atlasExport = newInstance(atlas);
2365
+ objectLayerExport.atlasSpriteSheetId = atlas._id;
2181
2366
  if (atlas.fileId) {
2182
2367
  await exportFileDoc(atlas.fileId, `atlas-${itemKey}`);
2183
2368
  }
@@ -2376,6 +2561,11 @@ try {
2376
2561
 
2377
2562
  logger.info('Importing instance', { code: instanceCode, backupDir });
2378
2563
 
2564
+ // Item ids belonging to this instance (collected from imported object
2565
+ // layers + the instance doc) — used to backfill missing skills from the
2566
+ // canonical DefaultSkillConfig when the backup predates the skill model.
2567
+ const importedItemIds = new Set();
2568
+
2379
2569
  // 0. Drop existing documents if --drop is set
2380
2570
  if (options.drop && !options.conf) {
2381
2571
  const existingInstance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
@@ -2395,7 +2585,10 @@ try {
2395
2585
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
2396
2586
 
2397
2587
  // Add instance-level itemIds (may not appear in any map entity)
2398
- for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
2588
+ for (const entry of existingInstance.itemIds || []) {
2589
+ const id = typeof entry === 'string' ? entry : entry?.id;
2590
+ if (id) dropOlItemIds.add(id);
2591
+ }
2399
2592
 
2400
2593
  // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
2401
2594
  const existingConf =
@@ -2435,6 +2628,37 @@ try {
2435
2628
 
2436
2629
  const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2437
2630
  logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
2631
+
2632
+ // Quests + actions are bound to maps by sourceMapCode, so they drop
2633
+ // with this instance's maps — only the content tied to this instance.
2634
+ const questResult = await CyberiaQuest.deleteMany({ sourceMapCode: { $in: [...dropMapCodes] } });
2635
+ if (questResult.deletedCount > 0)
2636
+ logger.info(`Dropped ${questResult.deletedCount} CyberiaQuest document(s)`);
2637
+
2638
+ // Collect instance-specific dialogue codes the actions reference
2639
+ // (e.g. quest-talk-<questCode>) before deleting the actions. The
2640
+ // shared "default-<itemId>" greetings are left to the item-shared
2641
+ // logic below so dialogues shared with other instances survive.
2642
+ const dropActions = await CyberiaAction.find(
2643
+ { sourceMapCode: { $in: [...dropMapCodes] } },
2644
+ { dialogCode: 1, 'questDialogueCodes.dialogCode': 1 },
2645
+ ).lean();
2646
+ const dropActionDialogueCodes = new Set();
2647
+ const collectDlg = (code) => {
2648
+ if (code && !code.startsWith('default-')) dropActionDialogueCodes.add(code);
2649
+ };
2650
+ for (const a of dropActions) {
2651
+ collectDlg(a.dialogCode);
2652
+ for (const qd of a.questDialogueCodes || []) collectDlg(qd.dialogCode);
2653
+ }
2654
+ const actionResult = await CyberiaAction.deleteMany({ sourceMapCode: { $in: [...dropMapCodes] } });
2655
+ if (actionResult.deletedCount > 0)
2656
+ logger.info(`Dropped ${actionResult.deletedCount} CyberiaAction document(s)`);
2657
+ if (dropActionDialogueCodes.size > 0) {
2658
+ const advResult = await CyberiaDialogue.deleteMany({ code: { $in: [...dropActionDialogueCodes] } });
2659
+ if (advResult.deletedCount > 0)
2660
+ logger.info(`Dropped ${advResult.deletedCount} CyberiaDialogue document(s) (action-referenced)`);
2661
+ }
2438
2662
  }
2439
2663
 
2440
2664
  // Exclude OL item IDs referenced by maps outside this instance
@@ -2463,6 +2687,12 @@ try {
2463
2687
  const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
2464
2688
  const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
2465
2689
  logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
2690
+
2691
+ // Skills are keyed by triggerItemId — drop those whose trigger item is
2692
+ // being removed (i.e. not shared with another instance's maps).
2693
+ const skillResult = await CyberiaSkill.deleteMany({ triggerItemId: { $in: [...dropOlItemIds] } });
2694
+ if (skillResult.deletedCount > 0)
2695
+ logger.info(`Dropped ${skillResult.deletedCount} CyberiaSkill document(s)`);
2466
2696
  const olDocs = await ObjectLayer.find(
2467
2697
  { 'data.item.id': { $in: [...dropOlItemIds] } },
2468
2698
  {
@@ -2557,7 +2787,9 @@ try {
2557
2787
  const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2558
2788
  let importedConf = null;
2559
2789
  if (fs.existsSync(confImportPath)) {
2560
- const confData = fs.readJsonSync(confImportPath);
2790
+ // Backfill any missing schema fields so older backups import a
2791
+ // complete, playable config into the DB.
2792
+ const confData = fillInstanceConfDefaults(fs.readJsonSync(confImportPath));
2561
2793
  if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2562
2794
  await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2563
2795
  // Always bump updatedAt so the Go server's version hash changes and
@@ -2664,6 +2896,7 @@ try {
2664
2896
  await ObjectLayer.deleteOne({ sha256: olData.sha256 });
2665
2897
  }
2666
2898
  await ObjectLayer.create(olData);
2899
+ if (olData.data?.item?.id) importedItemIds.add(olData.data.item.id);
2667
2900
  olCount++;
2668
2901
  }
2669
2902
  logger.info(`Imported ${olCount} ObjectLayer document(s)`);
@@ -2735,7 +2968,9 @@ try {
2735
2968
  // 6. Import CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
2736
2969
  const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2737
2970
  if (fs.existsSync(confImportPath)) {
2738
- const confData = fs.readJsonSync(confImportPath);
2971
+ // Backfill any missing schema fields so older backups import a
2972
+ // complete, playable config into the DB.
2973
+ const confData = fillInstanceConfDefaults(fs.readJsonSync(confImportPath));
2739
2974
  if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2740
2975
  await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2741
2976
  await CyberiaInstanceConf.create(confData);
@@ -2748,6 +2983,13 @@ try {
2748
2983
  const instancePath = `${backupDir}/cyberia-instance.json`;
2749
2984
  if (fs.existsSync(instancePath)) {
2750
2985
  const instanceData = fs.readJsonSync(instancePath);
2986
+ // Heal legacy shapes against the current model. itemIds migrated from a
2987
+ // flat string[] to [{ id, defaultPlayerInventory }] — a raw old backup
2988
+ // would fail Mongoose embedded-cast validation, so normalize it here.
2989
+ instanceData.itemIds = (instanceData.itemIds || [])
2990
+ .map((entry) => (typeof entry === 'string' ? { id: entry, defaultPlayerInventory: false } : entry))
2991
+ .filter((entry) => entry && entry.id);
2992
+ for (const entry of instanceData.itemIds) importedItemIds.add(entry.id);
2751
2993
  await CyberiaInstance.deleteOne({ code: instanceCode });
2752
2994
  await CyberiaInstance.deleteOne({ _id: instanceData._id });
2753
2995
  await CyberiaInstance.create(instanceData);
@@ -2785,6 +3027,126 @@ try {
2785
3027
  logger.info(`Imported ${dialogueCount} CyberiaDialogue document(s)`);
2786
3028
  }
2787
3029
 
3030
+ // 8b. Import CyberiaQuest documents (overwrite by code).
3031
+ const questsDir = `${backupDir}/cyberia-quests`;
3032
+ if (fs.existsSync(questsDir)) {
3033
+ const questFiles = fs.readdirSync(questsDir).filter((f) => f.endsWith('.json'));
3034
+ let questCount = 0;
3035
+ for (const file of questFiles) {
3036
+ const questData = fs.readJsonSync(`${questsDir}/${file}`);
3037
+ if (!questData.code) {
3038
+ logger.warn(`Skipping CyberiaQuest backup without code: ${file}`);
3039
+ continue;
3040
+ }
3041
+ await CyberiaQuest.deleteOne({ code: questData.code });
3042
+ if (questData._id) await CyberiaQuest.deleteOne({ _id: questData._id });
3043
+ await CyberiaQuest.create(questData);
3044
+ questCount++;
3045
+ }
3046
+ logger.info(`Imported ${questCount} CyberiaQuest document(s)`);
3047
+ }
3048
+
3049
+ // 8c. Import CyberiaAction documents (overwrite by code).
3050
+ const actionsDir = `${backupDir}/cyberia-actions`;
3051
+ if (fs.existsSync(actionsDir)) {
3052
+ const actionFiles = fs.readdirSync(actionsDir).filter((f) => f.endsWith('.json'));
3053
+ let actionCount = 0;
3054
+ for (const file of actionFiles) {
3055
+ const actionData = fs.readJsonSync(`${actionsDir}/${file}`);
3056
+ if (!actionData.code) {
3057
+ logger.warn(`Skipping CyberiaAction backup without code: ${file}`);
3058
+ continue;
3059
+ }
3060
+ await CyberiaAction.deleteOne({ code: actionData.code });
3061
+ if (actionData._id) await CyberiaAction.deleteOne({ _id: actionData._id });
3062
+ await CyberiaAction.create(actionData);
3063
+ actionCount++;
3064
+ }
3065
+ logger.info(`Imported ${actionCount} CyberiaAction document(s)`);
3066
+ }
3067
+
3068
+ // 8d. Import CyberiaSkill documents (own model, overwrite by triggerItemId).
3069
+ const skillsDir = `${backupDir}/cyberia-skills`;
3070
+ if (fs.existsSync(skillsDir)) {
3071
+ const skillFiles = fs.readdirSync(skillsDir).filter((f) => f.endsWith('.json'));
3072
+ let skillCount = 0;
3073
+ for (const file of skillFiles) {
3074
+ const skillData = fs.readJsonSync(`${skillsDir}/${file}`);
3075
+ if (!skillData.triggerItemId) {
3076
+ logger.warn(`Skipping CyberiaSkill backup without triggerItemId: ${file}`);
3077
+ continue;
3078
+ }
3079
+ await CyberiaSkill.deleteOne({ triggerItemId: skillData.triggerItemId });
3080
+ if (skillData._id) await CyberiaSkill.deleteOne({ _id: skillData._id });
3081
+ await CyberiaSkill.create(skillData);
3082
+ skillCount++;
3083
+ }
3084
+ logger.info(`Imported ${skillCount} CyberiaSkill document(s)`);
3085
+ }
3086
+
3087
+ // 8d-bis. Import CyberiaEntityTypeDefault documents (own model, overwrite
3088
+ // by the natural (entityType, liveItemIds) key, then by _id).
3089
+ const entityDefaultsDir = `${backupDir}/cyberia-entity-type-defaults`;
3090
+ if (fs.existsSync(entityDefaultsDir)) {
3091
+ const entityDefaultFiles = fs.readdirSync(entityDefaultsDir).filter((f) => f.endsWith('.json'));
3092
+ let entityDefaultCount = 0;
3093
+ for (const file of entityDefaultFiles) {
3094
+ const edData = fs.readJsonSync(`${entityDefaultsDir}/${file}`);
3095
+ if (!edData.entityType) {
3096
+ logger.warn(`Skipping CyberiaEntityTypeDefault backup without entityType: ${file}`);
3097
+ continue;
3098
+ }
3099
+ await CyberiaEntityTypeDefault.deleteMany({
3100
+ entityType: edData.entityType,
3101
+ liveItemIds: edData.liveItemIds || [],
3102
+ });
3103
+ if (edData._id) await CyberiaEntityTypeDefault.deleteOne({ _id: edData._id });
3104
+ await CyberiaEntityTypeDefault.create(edData);
3105
+ entityDefaultCount++;
3106
+ }
3107
+ logger.info(`Imported ${entityDefaultCount} CyberiaEntityTypeDefault document(s)`);
3108
+ }
3109
+
3110
+ // 8e. Backfill missing skills from the canonical DefaultSkillConfig. Old
3111
+ // backups predate the CyberiaSkill model and ship no skills/ dir, so
3112
+ // any instance item that has a canonical skill (e.g. atlas_pistol_mk2,
3113
+ // coin, hatchet) but no document yet is seeded from defaults. Existing
3114
+ // skills are never overwritten.
3115
+ let backfilledSkillCount = 0;
3116
+ for (const sk of DefaultSkillConfig) {
3117
+ if (!importedItemIds.has(sk.triggerItemId)) continue;
3118
+ const exists = await CyberiaSkill.findOne({ triggerItemId: sk.triggerItemId }).lean();
3119
+ if (exists) continue;
3120
+ await CyberiaSkill.create({
3121
+ triggerItemId: sk.triggerItemId,
3122
+ logicEventIds: sk.logicEventIds || [],
3123
+ skills: sk.skills || [],
3124
+ });
3125
+ backfilledSkillCount++;
3126
+ }
3127
+ if (backfilledSkillCount > 0) {
3128
+ logger.info(`Backfilled ${backfilledSkillCount} CyberiaSkill document(s) from DefaultSkillConfig`);
3129
+ }
3130
+
3131
+ // 8f. Import CyberiaSaga documents (overwrite by code).
3132
+ const sagasDir = `${backupDir}/cyberia-sagas`;
3133
+ if (fs.existsSync(sagasDir)) {
3134
+ const sagaFiles = fs.readdirSync(sagasDir).filter((f) => f.endsWith('.json'));
3135
+ let sagaCount = 0;
3136
+ for (const file of sagaFiles) {
3137
+ const sagaData = fs.readJsonSync(`${sagasDir}/${file}`);
3138
+ if (!sagaData.code) {
3139
+ logger.warn(`Skipping CyberiaSaga backup without code: ${file}`);
3140
+ continue;
3141
+ }
3142
+ await CyberiaSaga.deleteOne({ code: sagaData.code });
3143
+ if (sagaData._id) await CyberiaSaga.deleteOne({ _id: sagaData._id });
3144
+ await CyberiaSaga.create(sagaData);
3145
+ sagaCount++;
3146
+ }
3147
+ logger.info(`Imported ${sagaCount} CyberiaSaga document(s)`);
3148
+ }
3149
+
2788
3150
  // 9. Restore IPFS pin records and payloads
2789
3151
  const ipfsFile = `${backupDir}/ipfs/pins.json`;
2790
3152
  if (fs.existsSync(ipfsFile)) {
@@ -2815,10 +3177,6 @@ try {
2815
3177
  }
2816
3178
 
2817
3179
  const backupPinEntries = serialiseCanonicalPins(backupPins);
2818
- const backupCids = [...new Set(backupPinEntries.map((entry) => entry.cid).filter(Boolean))];
2819
- if (backupCids.length > 0) {
2820
- await Ipfs.deleteMany({ cid: { $in: backupCids } });
2821
- }
2822
3180
 
2823
3181
  const restoreAdditionalMfsPaths = async (cid, mfsPaths, primaryPath) => {
2824
3182
  let restoredCount = 0;
@@ -2830,78 +3188,99 @@ try {
2830
3188
  return restoredCount;
2831
3189
  };
2832
3190
 
2833
- const upsertImportedPin = async ({ cid, resourceType, mfsPath }) => {
2834
- if (!cid || !resourceType) return;
2835
- await Ipfs.deleteMany({ cid, resourceType });
2836
- await createPinRecord({ cid, resourceType, mfsPath: mfsPath || '', options: { host, path } });
2837
- };
2838
-
2839
3191
  if (fs.existsSync(ipfsContentDir)) {
2840
3192
  let cidRewriteCount = 0;
2841
3193
  let extraMfsRestoreCount = 0;
3194
+ let ipfsAlreadyPresent = 0;
2842
3195
 
2843
3196
  for (const [index, doc] of backupPinEntries.entries()) {
2844
3197
  const mfsPaths = collectMfsPaths(doc);
2845
3198
  const primaryPath = mfsPaths[0] || '';
2846
3199
  const payloadPath = `${ipfsContentDir}/${doc.cid}.bin`;
2847
3200
 
2848
- logger.info('IPFS raw payload restore start', {
2849
- index: index + 1,
2850
- total: backupPinEntries.length,
2851
- cid: doc.cid,
2852
- resourceType: doc.resourceType,
2853
- mfsPath: primaryPath || null,
2854
- });
3201
+ try {
3202
+ const existing = await Ipfs.findOne({ cid: doc.cid, resourceType: doc.resourceType })
3203
+ .select({ _id: 1 })
3204
+ .lean();
3205
+ if (existing) {
3206
+ ipfsAlreadyPresent++;
3207
+ continue;
3208
+ }
2855
3209
 
2856
- if (!fs.existsSync(payloadPath)) {
2857
- logger.warn('IPFS raw payload file missing from backup', {
3210
+ logger.info('IPFS raw payload restore start', {
3211
+ index: index + 1,
3212
+ total: backupPinEntries.length,
2858
3213
  cid: doc.cid,
2859
3214
  resourceType: doc.resourceType,
2860
3215
  mfsPath: primaryPath || null,
2861
3216
  });
2862
- ipfsSkipped++;
2863
- continue;
2864
- }
2865
3217
 
2866
- const addResult = await IpfsClient.addToIpfs(
2867
- fs.readFileSync(payloadPath),
2868
- nodePath.basename(primaryPath || doc.cid),
2869
- primaryPath || undefined,
2870
- );
3218
+ if (!fs.existsSync(payloadPath)) {
3219
+ logger.warn('IPFS raw payload file missing from backup', {
3220
+ cid: doc.cid,
3221
+ resourceType: doc.resourceType,
3222
+ mfsPath: primaryPath || null,
3223
+ });
3224
+ ipfsSkipped++;
3225
+ continue;
3226
+ }
2871
3227
 
2872
- if (!addResult?.cid) {
2873
- logger.warn('IPFS raw payload restore failed', {
2874
- cid: doc.cid,
2875
- resourceType: doc.resourceType,
2876
- mfsPath: primaryPath || null,
2877
- });
2878
- ipfsSkipped++;
2879
- continue;
2880
- }
3228
+ const addResult = await IpfsClient.addToIpfs(
3229
+ fs.readFileSync(payloadPath),
3230
+ nodePath.basename(primaryPath || doc.cid),
3231
+ primaryPath || undefined,
3232
+ );
3233
+
3234
+ if (!addResult?.cid) {
3235
+ logger.warn('IPFS raw payload restore failed', {
3236
+ cid: doc.cid,
3237
+ resourceType: doc.resourceType,
3238
+ mfsPath: primaryPath || null,
3239
+ });
3240
+ ipfsSkipped++;
3241
+ continue;
3242
+ }
3243
+
3244
+ const finalCid = addResult.cid;
3245
+ if (doc.cid !== finalCid) {
3246
+ await rewriteImportedCidReferences({
3247
+ oldCid: doc.cid,
3248
+ newCid: finalCid,
3249
+ resourceType: doc.resourceType,
3250
+ });
3251
+ cidRewriteCount++;
3252
+ logger.warn('IPFS raw payload CID mismatch during import; rewriting imported references', {
3253
+ oldCid: doc.cid,
3254
+ newCid: finalCid,
3255
+ resourceType: doc.resourceType,
3256
+ mfsPath: primaryPath || null,
3257
+ });
3258
+ }
2881
3259
 
2882
- const finalCid = addResult.cid;
2883
- if (doc.cid !== finalCid) {
2884
- await rewriteImportedCidReferences({
2885
- oldCid: doc.cid,
2886
- newCid: finalCid,
3260
+ extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
3261
+ // createPinRecord is an idempotent upsert on (cid, resourceType).
3262
+ await createPinRecord({
3263
+ cid: finalCid,
2887
3264
  resourceType: doc.resourceType,
3265
+ mfsPath: primaryPath || '',
3266
+ options: { host, path },
2888
3267
  });
2889
- cidRewriteCount++;
2890
- logger.warn('IPFS raw payload CID mismatch during import; rewriting imported references', {
2891
- oldCid: doc.cid,
2892
- newCid: finalCid,
3268
+ ipfsCount++;
3269
+ } catch (entryError) {
3270
+ logger.warn('IPFS pin restore failed for one entry — skipping it, the import continues', {
3271
+ cid: doc.cid,
2893
3272
  resourceType: doc.resourceType,
2894
3273
  mfsPath: primaryPath || null,
3274
+ error: entryError?.message ?? String(entryError),
2895
3275
  });
3276
+ ipfsSkipped++;
2896
3277
  }
2897
-
2898
- extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
2899
- await upsertImportedPin({ cid: finalCid, resourceType: doc.resourceType, mfsPath: primaryPath });
2900
- ipfsCount++;
2901
3278
  }
2902
3279
 
2903
3280
  logger.info(
2904
- `Imported ${ipfsCount} Ipfs pin record(s) from exact backup payloads${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
3281
+ `Imported ${ipfsCount} Ipfs pin record(s) from exact backup payloads` +
3282
+ `${ipfsAlreadyPresent ? `, ${ipfsAlreadyPresent} already present (skipped)` : ''}` +
3283
+ `${ipfsSkipped ? `, ${ipfsSkipped} skipped` : ''}`,
2905
3284
  );
2906
3285
  logger.info(
2907
3286
  `IPFS raw payload restore: ${ipfsCount}/${backupPinEntries.length} record(s) restored, ${extraMfsRestoreCount} additional MFS path(s) restored${cidRewriteCount ? `, ${cidRewriteCount} CID rewrite(s)` : ''}`,
@@ -3055,7 +3434,10 @@ try {
3055
3434
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
3056
3435
 
3057
3436
  // Add instance-level itemIds (may not appear in any map entity)
3058
- for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
3437
+ for (const entry of existingInstance.itemIds || []) {
3438
+ const id = typeof entry === 'string' ? entry : entry?.id;
3439
+ if (id) dropOlItemIds.add(id);
3440
+ }
3059
3441
 
3060
3442
  // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
3061
3443
  const existingConf =
@@ -3311,6 +3693,154 @@ try {
3311
3693
  }
3312
3694
  });
3313
3695
 
3696
+ // ── generate-saga: Top-Down PCG guided by LLMs (Semantic Reverse-Engineering) ──
3697
+ program
3698
+ .command('generate-saga')
3699
+ .option(
3700
+ '--prompt <theme>',
3701
+ 'Theme seed for the saga. If omitted, a distinct theme is auto-generated from the Cyberia base lore',
3702
+ )
3703
+ .option('--import <file>', 'Load a previously generated payload file (the shape --out writes) into the database')
3704
+ .option('--model <model>', 'Gemini model id (default: gemma-4-26b-a4b-it)')
3705
+ .option('--timeout <ms>', 'Per-request timeout in ms (default: 300000)', (v) => parseInt(v, 10))
3706
+ .option('--thinking-level <level>', 'Gemini thinking level: low | medium | high (default: high)')
3707
+ .option(
3708
+ '--lore-path <path>',
3709
+ 'Override path to the base-lore doc (default: src/client/public/cyberia-docs/CYBERIA-LORE.md)',
3710
+ )
3711
+ .option(
3712
+ '--space-context <context>',
3713
+ 'Force the auto-theme spatial layer: physical | mixed | hyperspace (default: random ~33% each)',
3714
+ )
3715
+ .option(
3716
+ '--tone <tone>',
3717
+ 'Force the auto-theme narrative type: adventure | politics | tragic | comedy (default: random ~25% each)',
3718
+ )
3719
+ .option(
3720
+ '--faction-context <keys>',
3721
+ 'Comma-separated factions that DRIVE the auto-theme: zenith | nova | atlas | neutral ' +
3722
+ "(e.g. 'nova,zenith'). If unset, confederations stay background, not the main theme",
3723
+ )
3724
+ .option(
3725
+ '--character-context <keys>',
3726
+ 'Comma-separated CHARACTER_NAMES_POOL keys to inspire NPC/character names: low_level_synthetics | ' +
3727
+ 'high_fidelity_synthetics | global_latin_diaspora | east_asian_pacific_diaspora | ' +
3728
+ 'middle_eastern_turkish_diaspora | sub_saharan_african_diaspora | classic_western_scifi | ' +
3729
+ 'mutagen_clans (inspiration only). If unset, a random subset is chosen',
3730
+ )
3731
+ .option(
3732
+ '--cultural-exposure <mode>',
3733
+ 'Naming diversity mode: cosmopolitan (high mixing) | local (isolated, consistent). ' +
3734
+ 'If unset, chosen at random',
3735
+ )
3736
+ .option(
3737
+ '--temperature <value>',
3738
+ 'Model sampling temperature, valid range 0.0 (deterministic) to 2.0 (most creative); ' +
3739
+ 'higher = more creative/divergent (default: 2.0 for theme synthesis)',
3740
+ parseFloat,
3741
+ )
3742
+ .option('--out <file>', 'Path to dump the payload JSON (default: ./engine-private/cyberia-sagas/<saga-code>.json)')
3743
+ .option('--dry-run', 'Generate and normalize without writing to the database')
3744
+ .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
3745
+ .option('--mongo-host <mongo-host>', 'Mongo host override')
3746
+ .option('--dev', 'Force development environment')
3747
+ .description('Generate (via Google Gemini) or import the non-spatial textual layer of a CyberiaSaga ecosystem')
3748
+ .action(async (options) => {
3749
+ if (!options.envPath) options.envPath = `./.env`;
3750
+ if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
3751
+
3752
+ if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
3753
+ const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
3754
+ if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
3755
+ }
3756
+
3757
+ let models = null;
3758
+ let host;
3759
+ let path;
3760
+
3761
+ if (!options.dryRun) {
3762
+ const deployId = process.env.DEFAULT_DEPLOY_ID;
3763
+ host = process.env.DEFAULT_DEPLOY_HOST;
3764
+ path = process.env.DEFAULT_DEPLOY_PATH;
3765
+
3766
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
3767
+ if (!fs.existsSync(confServerPath)) {
3768
+ logger.error(`Server config not found: ${confServerPath}`);
3769
+ process.exit(1);
3770
+ }
3771
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
3772
+ const { db } = confServer[host][path];
3773
+
3774
+ db.host = options.mongoHost
3775
+ ? options.mongoHost
3776
+ : options.dev
3777
+ ? db.host
3778
+ : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
3779
+
3780
+ logger.info('generate-saga', { deployId, host, path, db });
3781
+
3782
+ await DataBaseProviderService.load({
3783
+ apis: [
3784
+ 'cyberia-saga',
3785
+ 'cyberia-map',
3786
+ 'cyberia-quest',
3787
+ 'cyberia-dialogue',
3788
+ 'cyberia-action',
3789
+ 'cyberia-skill',
3790
+ 'cyberia-instance',
3791
+ 'object-layer',
3792
+ ],
3793
+ host,
3794
+ path,
3795
+ db,
3796
+ });
3797
+
3798
+ models = {
3799
+ CyberiaSaga: DataBaseProviderService.getModel('cyberia-saga', { host, path }),
3800
+ CyberiaMap: DataBaseProviderService.getModel('cyberia-map', { host, path }),
3801
+ CyberiaQuest: DataBaseProviderService.getModel('cyberia-quest', { host, path }),
3802
+ CyberiaDialogue: DataBaseProviderService.getModel('cyberia-dialogue', { host, path }),
3803
+ CyberiaAction: DataBaseProviderService.getModel('cyberia-action', { host, path }),
3804
+ CyberiaSkill: DataBaseProviderService.getModel('cyberia-skill', { host, path }),
3805
+ CyberiaInstance: DataBaseProviderService.getModel('cyberia-instance', { host, path }),
3806
+ ObjectLayer: DataBaseProviderService.getModel('object-layer', { host, path }),
3807
+ };
3808
+ }
3809
+
3810
+ try {
3811
+ if (options.import) {
3812
+ await importSaga({
3813
+ file: options.import,
3814
+ models,
3815
+ dryRun: !!options.dryRun,
3816
+ out: options.out,
3817
+ });
3818
+ } else {
3819
+ await generateSaga({
3820
+ prompt: options.prompt,
3821
+ models,
3822
+ model: options.model,
3823
+ timeout: options.timeout,
3824
+ thinkingLevel: options.thinkingLevel,
3825
+ lorePath: options.lorePath,
3826
+ spaceContext: options.spaceContext,
3827
+ tone: options.tone,
3828
+ factionContext: options.factionContext,
3829
+ characterContext: options.characterContext,
3830
+ culturalExposure: options.culturalExposure,
3831
+ temperature: options.temperature,
3832
+ dryRun: !!options.dryRun,
3833
+ out: options.out,
3834
+ });
3835
+ }
3836
+ } catch (err) {
3837
+ logger.error('generate-saga command error:', err);
3838
+ process.exitCode = 1;
3839
+ } finally {
3840
+ if (models) await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3841
+ }
3842
+ });
3843
+
3314
3844
  // ── chain: Hyperledger Besu / ERC-1155 lifecycle commands ────────────────
3315
3845
  const chain = program.command('chain').description('Hyperledger Besu chain & ERC-1155 ObjectLayerToken lifecycle');
3316
3846
 
@@ -4131,7 +4661,13 @@ try {
4131
4661
  runner
4132
4662
  .command('import-default-items')
4133
4663
  .option('--dev', 'Force development environment (loads .env.development for IPFS localhost, etc.)')
4134
- .description('Import default Object Layer items, skill config, dialogues, and client-hints into MongoDB')
4664
+ .option(
4665
+ '--mongo-host <mongo-host>',
4666
+ 'Mongo host override (forwarded to ol, seed-skills, seed-entities, seed-dialogues, seed-actions-quests, client-hints)',
4667
+ )
4668
+ .description(
4669
+ 'Import default Object Layer items, skills, entity defaults, dialogues, actions/quests, and client-hints into MongoDB',
4670
+ )
4135
4671
  .action(async (options) => {
4136
4672
  // Pre-flight: every item id referenced by the fallback world must
4137
4673
  // exist in DefaultCyberiaItems. Drift here causes silent missing
@@ -4148,21 +4684,92 @@ try {
4148
4684
  }
4149
4685
 
4150
4686
  const devFlag = options.dev ? ' --dev' : '';
4687
+ const mongoHostFlag = options.mongoHost ? ` --mongo-host ${options.mongoHost}` : '';
4151
4688
  const instanceCode = process.env.INSTANCE_CODE || 'cyberia-main';
4152
- shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}`);
4153
- shellExec(`node bin/cyberia run-workflow seed-skill-config${devFlag}`);
4154
- shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}`);
4155
- shellExec(`node bin/cyberia client-hints ${instanceCode} --seed-defaults${devFlag}`);
4689
+ shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}${mongoHostFlag}`);
4690
+ shellExec(`node bin/cyberia run-workflow seed-skills${devFlag}${mongoHostFlag}`);
4691
+ shellExec(`node bin/cyberia run-workflow seed-entities${devFlag}${mongoHostFlag}`);
4692
+ shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}${mongoHostFlag}`);
4693
+ shellExec(`node bin/cyberia run-workflow seed-actions-quests${devFlag}${mongoHostFlag}`);
4694
+ shellExec(`node bin/cyberia client-hints ${instanceCode} --seed-defaults${devFlag}${mongoHostFlag}`);
4695
+ });
4696
+
4697
+ runner.command('sync-src').action(() => {
4698
+ fs.copyFileSync('./cyberia-server/README.md', './src/client/public/cyberia-docs/CYBERIA-SERVER.md');
4699
+ fs.copyFileSync('./cyberia-server/Dockerfile', './src/runtime/cyberia-server/Dockerfile');
4700
+ fs.copyFileSync('./cyberia-client/README.md', './src/client/public/cyberia-docs/CYBERIA-CLIENT.md');
4701
+ fs.copyFileSync('./cyberia-client/Dockerfile', './src/runtime/cyberia-client/Dockerfile');
4702
+ shellExec('cp -a ./engine-private/conf/dd-cyberia/docker-compose/cyberia/. ./src/runtime/engine-cyberia/');
4703
+ });
4704
+
4705
+ runner
4706
+ .command('dev-env')
4707
+ .option('--run', 'Run docker:reset, cluster --dev --reset, docker-image, and docker:up after updating compose.env')
4708
+ .option(
4709
+ '--clean',
4710
+ 'Restore repositories to canonical state (git checkout Dockerfile, etc.) before updating compose.env',
4711
+ )
4712
+ .action((options) => {
4713
+ if (options.clean) {
4714
+ shellExec(`node bin run clean`);
4715
+ shellExec(`node bin run clean ./cyberia-server`);
4716
+ shellExec(`node bin run clean ./cyberia-client`);
4717
+ return;
4718
+ }
4719
+ const envPath = `./engine-private/conf/dd-cyberia/docker-compose/cyberia/compose.env`;
4720
+ const canonicalDevDockerfile = './src/runtime/engine-cyberia/Dockerfile.dev';
4721
+ fs.writeFileSync(
4722
+ canonicalDevDockerfile,
4723
+ fs
4724
+ .readFileSync(canonicalDevDockerfile, 'utf8')
4725
+ .replace('ENGINE_CYBERIA_REPO="engine-cyberia"', 'ENGINE_CYBERIA_REPO="engine-test-cyberia"')
4726
+ .replace(` # --mount=type=secret,id=github_token`, ` --mount=type=secret,id=github_token`)
4727
+ .replace(
4728
+ ` # export GITHUB_TOKEN="$(cat /run/secrets/github_token)";`,
4729
+ ` export GITHUB_TOKEN="$(cat /run/secrets/github_token)";`,
4730
+ )
4731
+ .replace(` for _secret in "$GITHUB_USERNAME"; do`, ` # for _secret in "$GITHUB_USERNAME"; do`)
4732
+ .replace(` unset GITHUB_USERNAME;`, ` # unset GITHUB_USERNAME;`)
4733
+ .replace(
4734
+ ` # for _secret in "$GITHUB_USERNAME" "$GITHUB_TOKEN"; do`,
4735
+ ` for _secret in "$GITHUB_USERNAME" "$GITHUB_TOKEN"; do`,
4736
+ )
4737
+ .replace(` # unset GITHUB_TOKEN GITHUB_USERNAME;`, ` unset GITHUB_TOKEN GITHUB_USERNAME;`),
4738
+
4739
+ 'utf8',
4740
+ );
4741
+ fs.writeFileSync(
4742
+ './src/cli/image.js',
4743
+ fs
4744
+ .readFileSync('./src/cli/image.js', 'utf8')
4745
+ .replace(
4746
+ ` // addBuildSecret('github_token', process.env.GITHUB_TOKEN);`,
4747
+ ` addBuildSecret('github_token', process.env.GITHUB_TOKEN);`,
4748
+ ),
4749
+ 'utf8',
4750
+ );
4751
+ fs.writeFileSync(envPath, fs.readFileSync(envPath, 'utf8').replaceAll('underpost/', 'localhost/'), 'utf8');
4752
+ shellExec('node bin/cyberia run-workflow sync-src');
4753
+ shellExec(`git checkout './src/runtime/cyberia-server/Dockerfile'`);
4754
+ shellExec(`git checkout './src/runtime/cyberia-client/Dockerfile'`);
4755
+ shellExec('node bin/cyberia run-workflow build-manifest');
4756
+ if (options.run) {
4757
+ shellExec('node bin/cyberia run-workflow docker:reset');
4758
+ shellExec('node bin cluster --dev --reset');
4759
+ shellExec('node bin/cyberia run-workflow docker-image engine-cyberia');
4760
+ shellExec('node bin/cyberia run-workflow docker-image cyberia-server');
4761
+ shellExec('node bin/cyberia run-workflow docker-image cyberia-client');
4762
+ shellExec('node bin/cyberia run-workflow docker:up');
4763
+ }
4156
4764
  });
4157
4765
 
4158
4766
  runner
4159
- .command('seed-skill-config')
4160
- .option('--instance-code <code>', 'CyberiaInstance code to update (default: $INSTANCE_CODE env or "default")')
4767
+ .command('drop-db')
4161
4768
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
4162
4769
  .option('--mongo-host <mongo-host>', 'Mongo host override')
4163
4770
  .option('--dev', 'Force development environment')
4164
- .description('Upsert default skillConfig entries into a CyberiaInstance document')
4165
- .action(async (options) => {
4771
+ .description('Drop all Cyberia collections and remove File documents referenced by instance/map thumbnails')
4772
+ .action(async (options = {}) => {
4166
4773
  if (!options.envPath) options.envPath = `./.env`;
4167
4774
  if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
4168
4775
 
@@ -4174,7 +4781,6 @@ try {
4174
4781
  const deployId = process.env.DEFAULT_DEPLOY_ID;
4175
4782
  const host = process.env.DEFAULT_DEPLOY_HOST;
4176
4783
  const path = process.env.DEFAULT_DEPLOY_PATH;
4177
- const instanceCode = options.instanceCode || process.env.INSTANCE_CODE || 'default';
4178
4784
 
4179
4785
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
4180
4786
  if (!fs.existsSync(confServerPath)) {
@@ -4190,42 +4796,133 @@ try {
4190
4796
  ? db.host
4191
4797
  : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
4192
4798
 
4193
- logger.info('seed-skill-config', { instanceCode, deployId, host, path, db });
4799
+ logger.info('drop-db', { deployId, host, path, db });
4800
+
4801
+ const cyberiaCollections = [
4802
+ 'cyberia-entity',
4803
+ 'cyberia-map',
4804
+ 'cyberia-instance',
4805
+ 'cyberia-instance-conf',
4806
+ 'cyberia-dialogue',
4807
+ 'cyberia-quest',
4808
+ 'cyberia-quest-progress',
4809
+ 'cyberia-action',
4810
+ 'cyberia-skill',
4811
+ 'cyberia-entity-type-default',
4812
+ 'cyberia-client-hints',
4813
+ 'cyberia-saga',
4814
+ ];
4194
4815
 
4195
- await DataBaseProviderService.load({ apis: ['cyberia-instance', 'cyberia-instance-conf'], host, path, db });
4816
+ await DataBaseProviderService.load({ apis: [...cyberiaCollections, 'file'], host, path, db });
4196
4817
 
4197
- const CyberiaInstance = DataBaseProviderService.getModel('cyberia-instance', { host, path });
4198
- const CyberiaInstanceConf = DataBaseProviderService.getModel('cyberia-instance-conf', { host, path });
4199
-
4200
- const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
4818
+ const File = DataBaseProviderService.getModel('file', { host, path });
4201
4819
 
4202
- if (!instance) {
4203
- logger.info(
4204
- `CyberiaInstance "${instanceCode}" not found — seeding skillConfig into conf using fallback defaults. ` +
4205
- `To link to a live instance, create or import it with: node bin/cyberia instance ${instanceCode} --import`,
4206
- );
4820
+ // Thumbnails on instances/maps are File _id references; collect them before
4821
+ // dropping so the backing File documents don't leak as orphans.
4822
+ const thumbnailFileIds = new Set();
4823
+ for (const api of ['cyberia-instance', 'cyberia-map']) {
4824
+ const Model = DataBaseProviderService.getModel(api, { host, path });
4825
+ const docs = await Model.find({ thumbnail: { $ne: null } }, { thumbnail: 1 }).lean();
4826
+ for (const doc of docs) {
4827
+ if (doc.thumbnail) thumbnailFileIds.add(doc.thumbnail.toString());
4828
+ }
4207
4829
  }
4208
4830
 
4209
- // Always upsert the conf with DefaultSkillConfig — idempotent regardless of instance existence.
4210
- const conf = await CyberiaInstanceConf.findOneAndUpdate(
4211
- { instanceCode },
4212
- { $set: { skillConfig: DefaultSkillConfig } },
4213
- { upsert: true, returnDocument: 'after' },
4214
- );
4215
-
4216
- // If a live instance exists, ensure its conf ref is linked.
4217
- if (instance && (!instance.conf || String(instance.conf) !== String(conf._id))) {
4218
- await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: conf._id });
4831
+ if (thumbnailFileIds.size > 0) {
4832
+ const result = await File.deleteMany({ _id: { $in: [...thumbnailFileIds] } });
4833
+ logger.info(`Removed ${result.deletedCount} thumbnail File document(s)`);
4219
4834
  }
4220
4835
 
4221
- logger.info(
4222
- `skillConfig seeded for instance "${instanceCode}" (${DefaultSkillConfig.length} entries)`,
4223
- DefaultSkillConfig.map((e) => `${e.triggerItemId} [${e.logicEventIds.join(', ')}]`),
4224
- );
4836
+ for (const api of cyberiaCollections) {
4837
+ const Model = DataBaseProviderService.getModel(api, { host, path });
4838
+ const result = await Model.deleteMany();
4839
+ logger.info(`Dropped ${result.deletedCount} ${api} document(s)`);
4840
+ }
4225
4841
 
4226
4842
  await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
4227
4843
  });
4228
4844
 
4845
+ const dockerImageIds = ['engine-cyberia', 'cyberia-server', 'cyberia-client'];
4846
+
4847
+ runner.command('deploy [id]').action((id) => {
4848
+ if (!dockerImageIds.includes(id)) {
4849
+ logger.error(`Invalid deploy id: ${id}. Must be one of: ${dockerImageIds.join(', ')}`);
4850
+ process.exit(1);
4851
+ }
4852
+ shellExec(`gh workflow run ${id}.cd.yml -R underpostnet/${id} -f job=deploy`);
4853
+ });
4854
+
4855
+ runner
4856
+ .command('docker-image [id]')
4857
+ .option('--load-tar', 'Load a pre-built image tar archive into the enabled target(s) without building.')
4858
+ .action((id, options) => {
4859
+ // no funca
4860
+ if (options.loadTar) {
4861
+ for (const imageId of dockerImageIds)
4862
+ if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.2.70.tar`);
4863
+ return;
4864
+ }
4865
+ switch (id) {
4866
+ case 'engine-cyberia':
4867
+ shellExec(`clear
4868
+ node bin/build dd-cyberia --conf
4869
+ node bin/build dd-cyberia --update-private
4870
+ node bin image --path src/runtime/engine-cyberia \
4871
+ --docker-compose --pull-base --build \
4872
+ --dockerfile-name Dockerfile.dev \
4873
+ --image-name engine-cyberia-dev:v3.2.70 \
4874
+ --image-out-path .
4875
+ `);
4876
+ break;
4877
+
4878
+ case 'cyberia-server':
4879
+ shellExec(
4880
+ `clear && node bin/cyberia run-workflow build-server-dashboard --output-path ./cyberia-server/public/index.html`,
4881
+ );
4882
+ shellExec(`
4883
+ cp -f src/runtime/cyberia-server/Dockerfile.dev cyberia-server/Dockerfile.dev
4884
+ node bin image --path cyberia-server \
4885
+ --docker-compose --pull-base --build \
4886
+ --dockerfile-name Dockerfile.dev \
4887
+ --image-name cyberia-server-dev:v3.2.70 \
4888
+ --image-out-path .
4889
+ `);
4890
+ break;
4891
+ case 'cyberia-client':
4892
+ shellExec(`clear
4893
+ cp -f src/runtime/cyberia-client/Dockerfile.dev cyberia-client/Dockerfile.dev
4894
+ node bin image --path cyberia-client \
4895
+ --docker-compose --pull-base --build \
4896
+ --dockerfile-name Dockerfile.dev \
4897
+ --image-name cyberia-client-dev:v3.2.70 \
4898
+ --image-out-path .
4899
+ `);
4900
+ break;
4901
+ }
4902
+ });
4903
+
4904
+ {
4905
+ // docker compose lyfe cycle commands for the dd-cyberia deployment
4906
+ const dockerComposeId = 'cyberia';
4907
+ const deployId = 'dd-cyberia';
4908
+ const commands = {
4909
+ 'docker:generate': `node bin docker-compose --generate --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4910
+ 'docker:up': `node bin docker-compose --up --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4911
+ 'docker:up:build': `node bin docker-compose --up --build --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4912
+ 'docker:down': `node bin docker-compose --down --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4913
+ 'docker:down:volumes': `node bin docker-compose --down --volumes --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4914
+ 'docker:restart': `node bin docker-compose --restart --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4915
+ 'docker:pull': `node bin docker-compose --pull --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4916
+ 'docker:logs': `node bin docker-compose --logs --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4917
+ 'docker:status': `node bin docker-compose --status --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4918
+ 'docker:reset': `node bin docker-compose --reset --deploy-id ${deployId} --docker-compose-id ${dockerComposeId}`,
4919
+ };
4920
+ for (const [cmd, action] of Object.entries(commands))
4921
+ runner.command(cmd).action(() => {
4922
+ shellExec(action);
4923
+ });
4924
+ }
4925
+
4229
4926
  runner
4230
4927
  .command('seed-dialogues')
4231
4928
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
@@ -4281,6 +4978,202 @@ try {
4281
4978
  await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
4282
4979
  });
4283
4980
 
4981
+ runner
4982
+ .command('seed-actions-quests')
4983
+ .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
4984
+ .option('--mongo-host <mongo-host>', 'Mongo host override')
4985
+ .option('--dev', 'Force development environment')
4986
+ .description('Upsert DefaultCyberiaActions + DefaultCyberiaQuests into Mongo (idempotent)')
4987
+ .action(async (options) => {
4988
+ if (!options.envPath) options.envPath = `./.env`;
4989
+ if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
4990
+
4991
+ if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
4992
+ const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
4993
+ if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
4994
+ }
4995
+
4996
+ const deployId = process.env.DEFAULT_DEPLOY_ID;
4997
+ const host = process.env.DEFAULT_DEPLOY_HOST;
4998
+ const path = process.env.DEFAULT_DEPLOY_PATH;
4999
+
5000
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
5001
+ if (!fs.existsSync(confServerPath)) {
5002
+ logger.error(`Server config not found: ${confServerPath}`);
5003
+ process.exit(1);
5004
+ }
5005
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
5006
+ const { db } = confServer[host][path];
5007
+
5008
+ db.host = options.mongoHost
5009
+ ? options.mongoHost
5010
+ : options.dev
5011
+ ? db.host
5012
+ : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5013
+
5014
+ logger.info('seed-actions-quests', { deployId, host, path, db });
5015
+
5016
+ await DataBaseProviderService.load({ apis: ['cyberia-action', 'cyberia-quest'], host, path, db });
5017
+
5018
+ const CyberiaAction = DataBaseProviderService.getModel('cyberia-action', { host, path });
5019
+ const CyberiaQuest = DataBaseProviderService.getModel('cyberia-quest', { host, path });
5020
+
5021
+ let actions = 0;
5022
+ for (const a of DefaultCyberiaActions) {
5023
+ await CyberiaAction.findOneAndUpdate({ code: a.code }, { $set: a }, { upsert: true });
5024
+ actions++;
5025
+ }
5026
+ let quests = 0;
5027
+ for (const q of DefaultCyberiaQuests) {
5028
+ await CyberiaQuest.findOneAndUpdate({ code: q.code }, { $set: q }, { upsert: true });
5029
+ quests++;
5030
+ }
5031
+
5032
+ logger.info(`seed-actions-quests: ${actions} actions, ${quests} quests upserted`);
5033
+
5034
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
5035
+ });
5036
+
5037
+ runner
5038
+ .command('seed-skills')
5039
+ .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
5040
+ .option('--mongo-host <mongo-host>', 'Mongo host override')
5041
+ .option('--dev', 'Force development environment')
5042
+ .description('Upsert DefaultSkillConfig into the cyberia-skill collection (full records, idempotent)')
5043
+ .action(async (options) => {
5044
+ if (!options.envPath) options.envPath = `./.env`;
5045
+ if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
5046
+
5047
+ if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
5048
+ const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
5049
+ if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
5050
+ }
5051
+
5052
+ const deployId = process.env.DEFAULT_DEPLOY_ID;
5053
+ const host = process.env.DEFAULT_DEPLOY_HOST;
5054
+ const path = process.env.DEFAULT_DEPLOY_PATH;
5055
+
5056
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
5057
+ if (!fs.existsSync(confServerPath)) {
5058
+ logger.error(`Server config not found: ${confServerPath}`);
5059
+ process.exit(1);
5060
+ }
5061
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
5062
+ const { db } = confServer[host][path];
5063
+
5064
+ db.host = options.mongoHost
5065
+ ? options.mongoHost
5066
+ : options.dev
5067
+ ? db.host
5068
+ : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5069
+
5070
+ logger.info('seed-skills', { deployId, host, path, db });
5071
+
5072
+ await DataBaseProviderService.load({ apis: ['cyberia-skill'], host, path, db });
5073
+
5074
+ const CyberiaSkill = DataBaseProviderService.getModel('cyberia-skill', { host, path });
5075
+
5076
+ // Upsert each skill record keyed by triggerItemId — full record (logic
5077
+ // event keys + expanded skills metadata), unlike the instance-conf
5078
+ // skillConfig schema which keeps only triggerItemId + logicEventIds.
5079
+ let upserted = 0;
5080
+ for (const sk of DefaultSkillConfig) {
5081
+ await CyberiaSkill.findOneAndUpdate(
5082
+ { triggerItemId: sk.triggerItemId },
5083
+ { $set: { logicEventIds: sk.logicEventIds || [], skills: sk.skills || [] } },
5084
+ { upsert: true },
5085
+ );
5086
+ upserted++;
5087
+ }
5088
+
5089
+ logger.info(
5090
+ `seed-skills: ${upserted} skill records upserted`,
5091
+ DefaultSkillConfig.map((e) => `${e.triggerItemId} → [${(e.logicEventIds || []).join(', ')}]`),
5092
+ );
5093
+
5094
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
5095
+ });
5096
+
5097
+ runner
5098
+ .command('seed-entities')
5099
+ .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
5100
+ .option('--mongo-host <mongo-host>', 'Mongo host override')
5101
+ .option('--dev', 'Force development environment')
5102
+ .description('Upsert ENTITY_TYPE_DEFAULTS into the cyberia-entity-type-default collection (idempotent)')
5103
+ .action(async (options) => {
5104
+ if (!options.envPath) options.envPath = `./.env`;
5105
+ if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
5106
+
5107
+ if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
5108
+ const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
5109
+ if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
5110
+ }
5111
+
5112
+ const deployId = process.env.DEFAULT_DEPLOY_ID;
5113
+ const host = process.env.DEFAULT_DEPLOY_HOST;
5114
+ const path = process.env.DEFAULT_DEPLOY_PATH;
5115
+
5116
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
5117
+ if (!fs.existsSync(confServerPath)) {
5118
+ logger.error(`Server config not found: ${confServerPath}`);
5119
+ process.exit(1);
5120
+ }
5121
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
5122
+ const { db } = confServer[host][path];
5123
+
5124
+ db.host = options.mongoHost
5125
+ ? options.mongoHost
5126
+ : options.dev
5127
+ ? db.host
5128
+ : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
5129
+
5130
+ logger.info('seed-entities', { deployId, host, path, db });
5131
+
5132
+ await DataBaseProviderService.load({ apis: ['cyberia-entity-type-default'], host, path, db });
5133
+
5134
+ const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('cyberia-entity-type-default', { host, path });
5135
+
5136
+ // Reconcile DB indexes with the current schema. This drops the obsolete
5137
+ // unique (entityType, liveItemIds) index from earlier builds so the same
5138
+ // itemId may appear in multiple same-type defaults (subset matching), and
5139
+ // (re)creates the non-unique liveItemIds lookup index.
5140
+ try {
5141
+ await CyberiaEntityTypeDefault.syncIndexes();
5142
+ } catch (error) {
5143
+ logger.warn(`seed-entities: syncIndexes skipped: ${error?.message || error}`);
5144
+ }
5145
+
5146
+ // Resolution is by subset containment (most-specific match wins), so there
5147
+ // is NO per-itemId uniqueness — the same itemId may appear in many entries
5148
+ // (across entity types, or within one type at different specificity). Every
5149
+ // entry is upserted, idempotently, by its exact (entityType, liveItemIds) key.
5150
+ let upserted = 0;
5151
+ for (const ed of ENTITY_TYPE_DEFAULTS) {
5152
+ await CyberiaEntityTypeDefault.findOneAndUpdate(
5153
+ { entityType: ed.entityType, liveItemIds: ed.liveItemIds || [] },
5154
+ {
5155
+ $set: {
5156
+ entityType: ed.entityType,
5157
+ liveItemIds: ed.liveItemIds || [],
5158
+ deadItemIds: ed.deadItemIds || [],
5159
+ dropItemIds: ed.dropItemIds || [],
5160
+ defaultObjectLayers: ed.defaultObjectLayers || [],
5161
+ behavior: ed.behavior || '',
5162
+ },
5163
+ },
5164
+ { upsert: true },
5165
+ );
5166
+ upserted++;
5167
+ }
5168
+
5169
+ logger.info(
5170
+ `seed-entities: ${upserted} entity-type-default records upserted`,
5171
+ ENTITY_TYPE_DEFAULTS.map((e) => `${e.entityType} → [${(e.liveItemIds || []).join(', ')}]`),
5172
+ );
5173
+
5174
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
5175
+ });
5176
+
4284
5177
  runner
4285
5178
  .command('generate-semantic-examples')
4286
5179
  .option('--seed <seed>', 'Base seed string (each type gets a unique suffix appended)', 'example')
@@ -4364,6 +5257,14 @@ try {
4364
5257
  // Edit the canonical sources; never hand-edit these generated outputs.
4365
5258
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
4366
5259
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md');
5260
+ fs.copyFileSync(
5261
+ './.github/workflows/cyberia-client.cd.yml',
5262
+ './cyberia-client/.github/workflows/cyberia-client.cd.yml',
5263
+ );
5264
+ fs.copyFileSync(
5265
+ './.github/workflows/cyberia-server.cd.yml',
5266
+ './cyberia-server/.github/workflows/cyberia-server.cd.yml',
5267
+ );
4367
5268
  logger.info(`run-workflow build-manifest complete (${isDev ? 'dev' : 'prod'})`);
4368
5269
  });
4369
5270
 
@@ -4407,7 +5308,7 @@ try {
4407
5308
  throw new Error('Trigger underpost passthrough');
4408
5309
  }
4409
5310
 
4410
- program.parse();
5311
+ await program.parseAsync();
4411
5312
  } catch (error) {
4412
5313
  // ONLY reroute on the explicit passthrough sentinel. Any other thrown
4413
5314
  // error (subprocess non-zero from shellExec's fail-fast default, CLI
@@ -4420,7 +5321,7 @@ try {
4420
5321
  process.argv = process.argv.filter((c) => c !== 'underpost');
4421
5322
  logger.warn('Rerouting to underpost cli...');
4422
5323
  try {
4423
- underpostProgram.parse();
5324
+ await underpostProgram.parseAsync();
4424
5325
  } catch (err) {
4425
5326
  logger.error(err);
4426
5327
  process.exit(1);