@underpostnet/cyberia 3.2.90 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.env.example +16 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +87 -0
  3. package/.github/workflows/cyberia-client.cd.yml +1 -18
  4. package/.github/workflows/cyberia-server.cd.yml +1 -18
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -78
  12. package/.github/workflows/ghpkg.ci.yml +7 -1
  13. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  14. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  15. package/.github/workflows/release.cd.yml +1 -9
  16. package/CHANGELOG.md +110 -1
  17. package/CLI-HELP.md +139 -9
  18. package/Dockerfile +1 -1
  19. package/Dockerfile.dev +1 -1
  20. package/Dockerfile.test +1 -1
  21. package/bin/build.js +7 -5
  22. package/bin/cyberia.js +73 -53
  23. package/bin/deploy.js +1 -1
  24. package/bin/index.js +73 -53
  25. package/compose.env +16 -0
  26. package/conf.js +2 -0
  27. package/deploy/dd-cyberia/deploy.sh +42 -0
  28. package/deploy/dd-cyberia/init.sh +63 -0
  29. package/deploy/dd-cyberia/sync-deploy.sh +194 -0
  30. package/deploy/lib/logging.sh +96 -0
  31. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  32. package/deploy/release/deploy.sh +62 -0
  33. package/deployment.yaml +1 -1
  34. package/docker-compose.yml +31 -37
  35. package/hardhat/package-lock.json +9 -9
  36. package/hardhat/package.json +3 -3
  37. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  38. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  39. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  40. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  41. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  42. package/package.json +2 -2
  43. package/scripts/audit-selinux.sh +64 -0
  44. package/scripts/coverall-test-cyberia.sh +24 -0
  45. package/scripts/coverall-test.sh +24 -0
  46. package/scripts/gpu-diag.sh +0 -0
  47. package/scripts/ip-info.sh +0 -0
  48. package/scripts/k3s-node-setup.sh +18 -15
  49. package/scripts/kubeadm-node-setup.sh +12 -23
  50. package/scripts/link-local-underpost-cli.sh +0 -0
  51. package/scripts/lxd-vm-setup.sh +0 -0
  52. package/scripts/maas-nat-firewalld.sh +0 -0
  53. package/scripts/nat-iptables.sh +2 -0
  54. package/scripts/rhel-grpc-setup.sh +0 -0
  55. package/scripts/rocky-kickstart.sh +25 -9
  56. package/scripts/test-monitor.sh +1 -1
  57. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  58. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -1
  59. package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
  62. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  63. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -3
  64. package/src/api/object-layer/object-layer.model.js +204 -3
  65. package/src/api/object-layer/object-layer.service.js +1 -14
  66. package/src/cli/baremetal.js +1 -2
  67. package/src/cli/cloud-init.js +1 -1
  68. package/src/cli/cluster.js +73 -68
  69. package/src/cli/db.js +9 -2
  70. package/src/cli/deploy.js +21 -5
  71. package/src/cli/docker-compose.js +1 -1
  72. package/src/cli/env.js +1 -1
  73. package/src/cli/image.js +0 -1
  74. package/src/cli/index.js +121 -9
  75. package/src/cli/lxd.js +1 -1
  76. package/src/cli/monitor.js +1 -1
  77. package/src/cli/release.js +57 -22
  78. package/src/cli/repository.js +11 -9
  79. package/src/cli/run.js +36 -9
  80. package/src/cli/ssh.js +198 -77
  81. package/src/cli/system.js +26 -13
  82. package/src/cli/test.js +1 -1
  83. package/src/cli/vultr.js +583 -0
  84. package/src/cli/wireguard.js +2125 -0
  85. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  86. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
  87. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  88. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  89. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  90. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  91. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  92. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  93. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  94. package/src/client/services/object-layer/object-layer.management.js +4 -4
  95. package/src/client-builder/client-build.js +20 -14
  96. package/src/db/mongo/MongooseDB.js +4 -0
  97. package/src/index.js +25 -1
  98. package/src/projects/cyberia/catalog-cyberia.js +5 -0
  99. package/src/projects/cyberia/generate-saga.js +14 -23
  100. package/src/projects/cyberia/instance-data.js +21 -2
  101. package/src/projects/cyberia/object-layer.js +11 -21
  102. package/src/projects/underpost/catalog-underpost.js +4 -1
  103. package/src/runtime/cyberia-client/Dockerfile +1 -1
  104. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  105. package/src/runtime/cyberia-server/Dockerfile +1 -1
  106. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  107. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  108. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  109. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  110. package/src/runtime/engine-cyberia/compose.env +16 -0
  111. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  112. package/src/server/backup.js +1 -1
  113. package/src/server/conf.js +18 -108
  114. package/src/server/cron.js +249 -51
  115. package/src/server/dns.js +100 -6
  116. package/src/server/environment.js +98 -0
  117. package/src/server/forward-proxy.js +549 -0
  118. package/src/server/middlewares.js +56 -1
  119. package/src/server/process.js +0 -1
  120. package/src/server/selinux.js +185 -0
  121. package/src/server/systemd.js +205 -0
  122. package/src/server/underpost-compression.js +186 -0
  123. package/src/server/underpost-gateway.js +20 -10
  124. package/src/server/underpost-ingress.js +18 -2
  125. package/test/cyberia-instance-conf-defaults.test.js +1 -0
  126. package/test/selinux.test.js +71 -0
  127. package/test/underpost-gateway.test.js +41 -0
  128. package/test/underpost-ingress.test.js +52 -0
  129. package/test/wireguard-edge.test.js +1177 -0
package/bin/cyberia.js CHANGED
@@ -256,6 +256,17 @@ try {
256
256
  /** @type {import('mongoose').Model} */
257
257
  const Ipfs = DataBaseProviderService.getModel('ipfs', { host, path });
258
258
 
259
+ // Idempotent repair, run only before flows that write: collapses legacy
260
+ // duplicates and upgrades the data.item.id index to unique so every
261
+ // later write has exactly one document to land on. Read-only
262
+ // subcommands stay side-effect free — findByItemId already resolves the
263
+ // same canonical document whether or not duplicates are still present.
264
+ if (options.import || options.importTypes || options.drop || options.generate) {
265
+ const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
266
+ if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
267
+ if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
268
+ }
269
+
259
270
  if (options.drop) {
260
271
  // Parse comma-separated item IDs for targeted drop; if none provided, drop everything
261
272
  const dropItemIds = itemId
@@ -439,7 +450,7 @@ try {
439
450
  });
440
451
 
441
452
  // 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 });
453
+ const existingOL = await ObjectLayer.findByItemId(currentItemId);
443
454
  let objectLayer;
444
455
 
445
456
  if (existingOL) {
@@ -457,7 +468,6 @@ try {
457
468
  let stagingFileDoc = null;
458
469
  let stagingAtlasDoc = null;
459
470
  let stagingCid = '';
460
- let stagingSha256 = '';
461
471
  try {
462
472
  const itemKey = currentItemId;
463
473
 
@@ -546,8 +556,7 @@ try {
546
556
  stagingData.render.cid = importItemCid;
547
557
  stagingData.render.metadataCid = importItemMetadataCid;
548
558
 
549
- // Pin data JSON to IPFS (compute final SHA-256 in memory)
550
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
559
+ // Pin data JSON to IPFS
551
560
  try {
552
561
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
553
562
  stagingData,
@@ -585,10 +594,9 @@ try {
585
594
  // Create the new RenderFrames doc (only now touches DB)
586
595
  const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
587
596
 
588
- // Single atomic update of the live document
589
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
597
+ // Single atomic upsert on the natural key; empty staged CIDs keep their stored value
598
+ objectLayer = await ObjectLayer.upsertByItemId({
590
599
  data: stagingData,
591
- sha256: stagingSha256,
592
600
  cid: stagingCid,
593
601
  objectLayerRenderFramesId: newRenderFrames._id,
594
602
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -599,14 +607,13 @@ try {
599
607
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
600
608
  }
601
609
 
602
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
610
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
603
611
  } else {
604
612
  // Rollback: only File/AtlasSpriteSheet were written, clean those up
605
613
  if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
606
614
  logger.warn(`[cut-over] Staging rolled back for ${currentItemId}, live document preserved`);
615
+ objectLayer = await ObjectLayer.findByItemId(currentItemId);
607
616
  }
608
-
609
- objectLayer = await ObjectLayer.findById(existingOL._id);
610
617
  } else {
611
618
  // ── New item: stage everything before creating (same cut-over pattern) ──
612
619
  logger.info(`ObjectLayer '${currentItemId}' is new, staging creation...`);
@@ -621,7 +628,6 @@ try {
621
628
  let stagingFileDoc = null;
622
629
  let stagingAtlasDoc = null;
623
630
  let stagingCid = '';
624
- let stagingSha256 = '';
625
631
  try {
626
632
  const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
627
633
  objectLayerRenderFramesData,
@@ -705,7 +711,6 @@ try {
705
711
  stagingData.render.cid = importItemCid;
706
712
  stagingData.render.metadataCid = importItemMetadataCid;
707
713
 
708
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
709
714
  try {
710
715
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
711
716
  stagingData,
@@ -738,9 +743,8 @@ try {
738
743
 
739
744
  if (cutoverReady) {
740
745
  const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
741
- objectLayer = await ObjectLayer.create({
746
+ objectLayer = await ObjectLayer.upsertByItemId({
742
747
  data: stagingData,
743
- sha256: stagingSha256,
744
748
  cid: stagingCid,
745
749
  objectLayerRenderFramesId: newRenderFrames._id,
746
750
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -842,7 +846,7 @@ try {
842
846
 
843
847
  if (shouldGenerateAtlas) {
844
848
  // 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 });
849
+ const existingOL = await ObjectLayer.findByItemId(objectLayerId);
846
850
  let objectLayer;
847
851
 
848
852
  if (existingOL) {
@@ -860,7 +864,6 @@ try {
860
864
  let stagingFileDoc = null;
861
865
  let stagingAtlasDoc = null;
862
866
  let stagingCid = '';
863
- let stagingSha256 = '';
864
867
  try {
865
868
  const itemKey = objectLayerId;
866
869
 
@@ -948,8 +951,7 @@ try {
948
951
  stagingData.render.cid = importAtlasCid;
949
952
  stagingData.render.metadataCid = importAtlasMetadataCid;
950
953
 
951
- // Pin data JSON to IPFS (compute final SHA-256 in memory)
952
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
954
+ // Pin data JSON to IPFS
953
955
  try {
954
956
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
955
957
  stagingData,
@@ -985,9 +987,8 @@ try {
985
987
  const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
986
988
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
987
989
 
988
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
990
+ objectLayer = await ObjectLayer.upsertByItemId({
989
991
  data: stagingData,
990
- sha256: stagingSha256,
991
992
  cid: stagingCid,
992
993
  objectLayerRenderFramesId: newRenderFrames._id,
993
994
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -996,13 +997,12 @@ try {
996
997
  if (oldRenderFramesId) {
997
998
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
998
999
  }
999
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
1000
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
1000
1001
  } else {
1001
1002
  if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
1002
1003
  logger.warn(`[cut-over] Staging rolled back for ${objectLayerId}, live document preserved`);
1004
+ objectLayer = await ObjectLayer.findByItemId(objectLayerId);
1003
1005
  }
1004
-
1005
- objectLayer = await ObjectLayer.findById(existingOL._id);
1006
1006
  } else {
1007
1007
  // ── New item: stage everything before creating (same cut-over pattern) ──
1008
1008
  logger.info(`ObjectLayer '${objectLayerId}' is new, staging creation...`);
@@ -1017,7 +1017,6 @@ try {
1017
1017
  let stagingFileDoc = null;
1018
1018
  let stagingAtlasDoc = null;
1019
1019
  let stagingCid = '';
1020
- let stagingSha256 = '';
1021
1020
  try {
1022
1021
  const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
1023
1022
  entry.objectLayerRenderFramesData,
@@ -1101,7 +1100,6 @@ try {
1101
1100
  stagingData.render.cid = importAtlasCid;
1102
1101
  stagingData.render.metadataCid = importAtlasMetadataCid;
1103
1102
 
1104
- stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1105
1103
  try {
1106
1104
  const ipfsDataResult = await IpfsClient.addJsonToIpfs(
1107
1105
  stagingData,
@@ -1134,9 +1132,8 @@ try {
1134
1132
 
1135
1133
  if (cutoverReady) {
1136
1134
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1137
- objectLayer = await ObjectLayer.create({
1135
+ objectLayer = await ObjectLayer.upsertByItemId({
1138
1136
  data: stagingData,
1139
- sha256: stagingSha256,
1140
1137
  cid: stagingCid,
1141
1138
  objectLayerRenderFramesId: newRenderFrames._id,
1142
1139
  atlasSpriteSheetId: stagingAtlasDoc._id,
@@ -1159,7 +1156,7 @@ try {
1159
1156
  if (existingItemIds.has(objectLayerId)) continue;
1160
1157
 
1161
1158
  // --import all: create documents without atlas generation
1162
- const existingOL = await ObjectLayer.findOne({ 'data.item.id': objectLayerId });
1159
+ const existingOL = await ObjectLayer.findByItemId(objectLayerId);
1163
1160
  let objectLayer;
1164
1161
 
1165
1162
  if (existingOL) {
@@ -1172,15 +1169,13 @@ try {
1172
1169
  if (!stagingData.render) stagingData.render = {};
1173
1170
  stagingData.render.cid = '';
1174
1171
  stagingData.render.metadataCid = '';
1175
- const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1176
1172
 
1177
1173
  // Atomic cut-over: create new RenderFrames, swap live doc, delete old
1178
1174
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1179
1175
  const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
1180
1176
 
1181
- await ObjectLayer.findByIdAndUpdate(existingOL._id, {
1177
+ objectLayer = await ObjectLayer.upsertByItemId({
1182
1178
  data: stagingData,
1183
- sha256: stagingSha256,
1184
1179
  objectLayerRenderFramesId: newRenderFrames._id,
1185
1180
  });
1186
1181
 
@@ -1188,20 +1183,17 @@ try {
1188
1183
  await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
1189
1184
  }
1190
1185
 
1191
- objectLayer = await ObjectLayer.findById(existingOL._id);
1192
- logger.info(`[cut-over] Live document ${existingOL._id} updated atomically (atlas skipped)`);
1186
+ logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically (atlas skipped)`);
1193
1187
  } else {
1194
- // New item: create with sha256 populated (no atlas for bulk import)
1188
+ // New item (no atlas for bulk import)
1195
1189
  const stagingData = JSON.parse(JSON.stringify(entry.data));
1196
1190
  if (!stagingData.render) stagingData.render = {};
1197
1191
  stagingData.render.cid = '';
1198
1192
  stagingData.render.metadataCid = '';
1199
- const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
1200
1193
 
1201
1194
  const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
1202
- objectLayer = await ObjectLayer.create({
1195
+ objectLayer = await ObjectLayer.upsertByItemId({
1203
1196
  data: stagingData,
1204
- sha256: stagingSha256,
1205
1197
  objectLayerRenderFramesId: newRenderFrames._id,
1206
1198
  });
1207
1199
  }
@@ -1231,9 +1223,7 @@ try {
1231
1223
  logger.info(`Showing frame for item: ${itemId}, direction: ${direction}, frame: ${frameIndexNum}`);
1232
1224
 
1233
1225
  // Find ObjectLayer by item-id
1234
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId }).populate(
1235
- 'objectLayerRenderFramesId',
1236
- );
1226
+ const objectLayer = await ObjectLayer.findByItemId(itemId).populate('objectLayerRenderFramesId');
1237
1227
 
1238
1228
  if (!objectLayer) {
1239
1229
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -1317,9 +1307,7 @@ try {
1317
1307
  }
1318
1308
 
1319
1309
  // Find ObjectLayer by item-id
1320
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId }).populate(
1321
- 'objectLayerRenderFramesId',
1322
- );
1310
+ const objectLayer = await ObjectLayer.findByItemId(itemId).populate('objectLayerRenderFramesId');
1323
1311
 
1324
1312
  if (!objectLayer) {
1325
1313
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -1453,7 +1441,7 @@ try {
1453
1441
  logger.info(`Looking up atlas sprite sheet for item: ${itemId}`);
1454
1442
 
1455
1443
  // Find ObjectLayer by item-id
1456
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId });
1444
+ const objectLayer = await ObjectLayer.findByItemId(itemId);
1457
1445
 
1458
1446
  if (!objectLayer) {
1459
1447
  logger.error(`ObjectLayer not found for item-id: ${itemId}`);
@@ -2695,6 +2683,12 @@ try {
2695
2683
 
2696
2684
  logger.info('Importing instance', { code: instanceCode, backupDir });
2697
2685
 
2686
+ // Idempotent: a backup restore writes object layers, so collapse any
2687
+ // legacy duplicates and make the data.item.id index unique first.
2688
+ const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
2689
+ if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
2690
+ if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
2691
+
2698
2692
  // Item ids belonging to this instance (collected from imported object
2699
2693
  // layers + the instance doc) — used to backfill missing skills from the
2700
2694
  // canonical DefaultSkillConfig when the backup predates the skill model.
@@ -3029,12 +3023,21 @@ try {
3029
3023
  let olCount = 0;
3030
3024
  for (const file of olFiles) {
3031
3025
  const olData = fs.readJsonSync(`${olDir}/${file}`);
3032
- await ObjectLayer.deleteOne({ _id: olData._id });
3033
- if (olData.sha256) {
3034
- await ObjectLayer.deleteOne({ sha256: olData.sha256 });
3026
+ const olItemId = olData.data?.item?.id;
3027
+ if (!olItemId) {
3028
+ logger.warn(`Skipping object-layer file '${file}' missing data.item.id`);
3029
+ continue;
3035
3030
  }
3036
- await ObjectLayer.create(olData);
3037
- if (olData.data?.item?.id) importedItemIds.add(olData.data.item.id);
3031
+ // preserveUUID only applies to items this database does not have yet.
3032
+ // An item that already exists keeps its own _id and simply absorbs the
3033
+ // backup's attribute values, so a restore can never fork data.item.id
3034
+ // into two documents. sha256 needs no separate cleanup: it is derived
3035
+ // from data, so an identical hash implies the same item id.
3036
+ const existingOL = await ObjectLayer.findByItemId(olItemId);
3037
+ if (!existingOL && olData._id) await ObjectLayer.deleteOne({ _id: olData._id });
3038
+
3039
+ await ObjectLayer.upsertByItemId(olData);
3040
+ importedItemIds.add(olItemId);
3038
3041
  olCount++;
3039
3042
  }
3040
3043
  logger.info(`Imported ${olCount} ObjectLayer document(s)`);
@@ -5026,6 +5029,19 @@ try {
5026
5029
  shellExec(`gh workflow run ${id}.cd.yml -R underpostnet/${id} -f job=deploy`);
5027
5030
  });
5028
5031
 
5032
+ runner
5033
+ .command('test')
5034
+ .option('--n-con <connections>', 'Number of concurrent WebSocket connections')
5035
+ .option('--duration <ms>', 'Load test duration in milliseconds')
5036
+ .option('--tap-freq <seconds>', 'Frequency of tap events in seconds')
5037
+ .action((options) => {
5038
+ shellExec(`CYBERIA_LOAD_WS_URL=ws://localhost:8081/ws \
5039
+ CYBERIA_LOAD_CONNECTIONS=${options.nCon ?? 40} \
5040
+ CYBERIA_LOAD_TAP_FREQUENCY=${options.tapFreq ?? 5} \
5041
+ CYBERIA_LOAD_DURATION_MS=${options.duration ?? 1000 * 60 * 30} \
5042
+ c8 mocha test/cyberia-load.test.js`);
5043
+ });
5044
+
5029
5045
  runner
5030
5046
  .command('docker-image [id]')
5031
5047
  .option('--load-tar', 'Load a pre-built image tar archive into the enabled target(s) without building.')
@@ -5033,7 +5049,7 @@ try {
5033
5049
  // no funca
5034
5050
  if (options.loadTar) {
5035
5051
  for (const imageId of dockerImageIds)
5036
- if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.2.90.tar`);
5052
+ if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.3.0.tar`);
5037
5053
  return;
5038
5054
  }
5039
5055
  switch (id) {
@@ -5044,7 +5060,7 @@ node bin/build dd-cyberia --update-private
5044
5060
  node bin image --path src/runtime/engine-cyberia \
5045
5061
  --docker-compose --pull-base --build \
5046
5062
  --dockerfile-name Dockerfile.dev \
5047
- --image-name engine-cyberia-dev:v3.2.90 \
5063
+ --image-name engine-cyberia-dev:v3.3.0 \
5048
5064
  --image-out-path .
5049
5065
  `);
5050
5066
  break;
@@ -5055,7 +5071,7 @@ cp -f src/runtime/cyberia-server/Dockerfile.dev cyberia-server/Dockerfile.dev
5055
5071
  node bin image --path cyberia-server \
5056
5072
  --docker-compose --pull-base --build \
5057
5073
  --dockerfile-name Dockerfile.dev \
5058
- --image-name cyberia-server-dev:v3.2.90 \
5074
+ --image-name cyberia-server-dev:v3.3.0 \
5059
5075
  --image-out-path .
5060
5076
  `);
5061
5077
  break;
@@ -5065,7 +5081,7 @@ cp -f src/runtime/cyberia-client/Dockerfile.dev cyberia-client/Dockerfile.dev
5065
5081
  node bin image --path cyberia-client \
5066
5082
  --docker-compose --pull-base --build \
5067
5083
  --dockerfile-name Dockerfile.dev \
5068
- --image-name cyberia-client-dev:v3.2.90 \
5084
+ --image-name cyberia-client-dev:v3.3.0 \
5069
5085
  --image-out-path .
5070
5086
  `);
5071
5087
  break;
@@ -5626,8 +5642,12 @@ node bin image --path cyberia-client \
5626
5642
 
5627
5643
  // Copy canonical doc sources into the generated project READMEs.
5628
5644
  // Edit the canonical sources; never hand-edit these generated outputs.
5645
+ fs.copySync('./deploy/lib', './cyberia-server/deploy/lib', { overwrite: true });
5646
+ fs.copySync('./deploy/lib', './cyberia-client/deploy/lib', { overwrite: true });
5629
5647
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
5648
+ fs.copySync('./deploy/cyberia-client', './cyberia-client/deploy');
5630
5649
  fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md');
5650
+ fs.copySync('./deploy/cyberia-server', './cyberia-server/deploy');
5631
5651
  fs.copyFileSync(
5632
5652
  './.github/workflows/cyberia-client.cd.yml',
5633
5653
  './cyberia-client/.github/workflows/cyberia-client.cd.yml',
package/bin/deploy.js CHANGED
@@ -14,10 +14,10 @@ import {
14
14
  addWsConf,
15
15
  buildWsSrc,
16
16
  cloneSrcComponents,
17
- writeEnv,
18
17
  buildCliDoc,
19
18
  loadConf,
20
19
  } from '../src/server/conf.js';
20
+ import { writeEnv } from '../src/server/environment.js';
21
21
  import colors from 'colors';
22
22
  import { program } from '../src/cli/index.js';
23
23
  import { timer, getCapVariableName } from '../src/client/components/core/CommonJs.js';