@underpostnet/cyberia 3.3.73 → 3.3.76

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 (228) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +5 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  10. package/AGENTS.md +61 -12
  11. package/CHANGELOG.md +126 -41
  12. package/CLI-HELP.md +19 -12
  13. package/Dockerfile +1 -1
  14. package/Dockerfile.dev +1 -1
  15. package/Dockerfile.test +2 -2
  16. package/README.md +28 -28
  17. package/bin/build.js +37 -17
  18. package/bin/cyberia.js +1247 -1373
  19. package/bin/index.js +1247 -1373
  20. package/compose.env +17 -5
  21. package/conf.js +7 -3
  22. package/deploy/cyberia-client/deploy.sh +10 -11
  23. package/deploy/cyberia-client/package.sh +0 -1
  24. package/deploy/cyberia-client/state.sh +3 -2
  25. package/deploy/cyberia-server/deploy.sh +10 -11
  26. package/deploy/cyberia-server/package.sh +0 -1
  27. package/deploy/cyberia-server/state.sh +3 -2
  28. package/deploy/dd-cyberia/deploy.sh +5 -5
  29. package/deploy/dd-cyberia/init.sh +15 -14
  30. package/deploy/dd-cyberia/package.sh +0 -1
  31. package/deploy/dd-cyberia/state.sh +2 -2
  32. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  33. package/deploy/lib/config.sh +29 -0
  34. package/deploy/lib/github-actions-logging.sh +92 -76
  35. package/deploy/lib/host.sh +27 -18
  36. package/deploy/lib/state.sh +4 -1
  37. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  38. package/deploy/release/deploy.sh +0 -2
  39. package/deployment.yaml +1 -1
  40. package/docker-compose.yml +23 -17
  41. package/hardhat/package-lock.json +2 -2
  42. package/hardhat/package.json +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  44. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  46. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  47. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  48. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  49. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  50. package/manifests/deployment/mongo-express/service.yaml +16 -0
  51. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  52. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  53. package/package.json +19 -17
  54. package/scripts/test-monitor.sh +3 -3
  55. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  56. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  59. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  60. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  61. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  62. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  63. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  64. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  66. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  67. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  68. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  69. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  71. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  72. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  73. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  74. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  75. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  76. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  77. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  79. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  80. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  81. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  82. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  83. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  84. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  85. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  86. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  89. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  90. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  91. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  92. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  93. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  94. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  95. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  96. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  97. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  98. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  99. package/src/api/file/file.ref.js +153 -0
  100. package/src/api/file/file.ref.json +11 -2
  101. package/src/api/object-layer/object-layer.model.js +47 -12
  102. package/src/api/object-layer/object-layer.router.js +10 -3
  103. package/src/api/object-layer/object-layer.service.js +113 -120
  104. package/src/cli/app.js +12 -1
  105. package/src/cli/client.js +2 -2
  106. package/src/cli/cluster.js +7 -0
  107. package/src/cli/deploy.js +4 -4
  108. package/src/cli/docker-compose.js +1 -1
  109. package/src/cli/fs-selection.js +115 -0
  110. package/src/cli/fs.js +158 -342
  111. package/src/cli/index.js +22 -10
  112. package/src/cli/repository.js +28 -0
  113. package/src/cli/run.js +71 -197
  114. package/src/cli/test.js +4 -0
  115. package/src/cli/wireguard.js +32 -31
  116. package/src/client/components/core/Docs.js +77 -100
  117. package/src/client/components/core/Modal.js +56 -1
  118. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  119. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  120. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  121. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  122. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  123. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  124. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  125. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  126. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  127. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  128. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  129. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  130. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  131. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  132. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  133. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  134. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  135. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  136. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  137. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  138. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  139. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  140. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  141. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  142. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  143. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  144. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  145. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  146. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  147. package/src/client/services/object-layer/object-layer.management.js +6 -10
  148. package/src/client/services/object-layer/object-layer.service.js +3 -1
  149. package/src/client-builder/client-build-docs.js +52 -52
  150. package/src/client-builder/client-build.js +67 -44
  151. package/src/client-builder/client-bundle.js +347 -0
  152. package/src/db/mongo/MongoExpress.js +187 -0
  153. package/src/grpc/cyberia/grpc-server.js +5 -7
  154. package/src/index.js +1 -1
  155. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  156. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  157. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  158. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  159. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  160. package/src/projects/cyberia/gemini-client.js +3 -3
  161. package/src/projects/cyberia/generate-saga.js +4 -11
  162. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  163. package/src/projects/cyberia/instance-backup.js +210 -0
  164. package/src/projects/cyberia/instance-data.js +177 -257
  165. package/src/projects/cyberia/map-preview-generator.js +32 -68
  166. package/src/projects/cyberia/object-layer.js +152 -319
  167. package/src/projects/cyberia/seed-audio.js +139 -0
  168. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  169. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  170. package/src/projects/cyberia/stat-balance.js +81 -0
  171. package/src/projects/cyberia/stat-commands.js +23 -0
  172. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  173. package/src/runtime/cyberia-client/Dockerfile +8 -4
  174. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  175. package/src/runtime/cyberia-server/Dockerfile +9 -2
  176. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  177. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  178. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  179. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  180. package/src/runtime/engine-cyberia/compose.env +17 -5
  181. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  182. package/src/server/build/coverage.js +113 -42
  183. package/src/server/build/package.js +55 -12
  184. package/src/server/build/testing.js +59 -9
  185. package/src/server/network/middlewares.js +8 -2
  186. package/src/server/ops/logger.js +70 -35
  187. package/src/server/runtime/conf.js +4 -2
  188. package/src/server/storage/data-query.js +16 -0
  189. package/src/server/storage/downloader.js +62 -14
  190. package/src/server/storage/zip.js +153 -0
  191. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  192. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  193. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  194. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  195. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  196. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  197. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  198. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  199. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  200. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  201. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  202. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  203. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  204. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  205. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  206. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  207. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  208. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  209. package/test/unit/client-build-docs.test.js +97 -70
  210. package/test/unit/client-bundle.test.js +313 -0
  211. package/test/unit/conf-resolution.test.js +17 -0
  212. package/test/unit/coverage-artifact.test.js +120 -29
  213. package/test/unit/cyberia/instance-backup.test.js +183 -0
  214. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  215. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  216. package/test/unit/cyberia/stat-balance.test.js +69 -0
  217. package/test/unit/deploy-log-table.test.js +76 -5
  218. package/test/unit/downloader-integrity.test.js +98 -0
  219. package/test/unit/file-reference-registry.test.js +141 -0
  220. package/test/unit/fs-storage-paths.test.js +596 -0
  221. package/test/unit/logger-redaction.test.js +29 -0
  222. package/test/unit/package.test.js +70 -16
  223. package/test/unit/prepare-host.test.js +84 -1
  224. package/test/unit/test-tiers.test.js +27 -0
  225. package/test/unit/zip-archive.test.js +142 -0
  226. package/vitest.config.js +11 -3
  227. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  228. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
package/src/cli/run.js CHANGED
@@ -28,10 +28,10 @@ import {
28
28
  instancePortFactory,
29
29
  instanceProxyRoutesFactory,
30
30
  instanceStatusPageEntriesFactory,
31
+ instanceProjectPathFactory,
31
32
  loadConfInstances,
32
33
  loadProjectInstanceEnvBuilder,
33
34
  loadConfServerJson,
34
- loadReplicas,
35
35
  resolveEnvScoped,
36
36
  selectConfInstances,
37
37
  waitForPort,
@@ -49,6 +49,7 @@ import {
49
49
  trafficFromRoutingInfoFactory,
50
50
  } from '../server/runtime/conf.js';
51
51
  import { buildKindPorts, deployRoutesExists, readDeployRoutes, resolveDeployList } from '../server/network/router.js';
52
+ import { clientBundleHostFilter, pullClientBundle, pushClientBundle } from '../client-builder/client-bundle.js';
52
53
  import { cronDeployIdResolve } from '../server/ops/cron.js';
53
54
  import { deployEnvFactory, getNpmRootPath, writeEnv } from '../server/runtime/environment.js';
54
55
  import { actionInitLog, loggerFactory } from '../server/ops/logger.js';
@@ -62,6 +63,7 @@ import { domainContextFactory } from './domains.js';
62
63
  import Underpost from '../index.js';
63
64
  import dotenv from 'dotenv';
64
65
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
66
+ import { MongoExpress } from '../db/mongo/MongoExpress.js';
65
67
  import {
66
68
  UNDERPOST_GATEWAY,
67
69
  assertStaticAssets,
@@ -140,6 +142,7 @@ const logger = loggerFactory(import.meta);
140
142
  * @property {boolean} kind - Whether to run in kind mode.
141
143
  * @property {boolean} k3s - Whether to run in k3s mode.
142
144
  * @property {string} hosts - The hosts to use.
145
+ * @property {string} split - Zip part size in MB for push-bundle, or 'none' to upload a single zip.
143
146
  * @property {string} deployId - The deployment ID.
144
147
  * @property {string} instanceId - The instance ID.
145
148
  * @property {string} user - The user to run as.
@@ -238,6 +241,7 @@ const DEFAULT_OPTION = {
238
241
  kind: false,
239
242
  k3s: false,
240
243
  hosts: '',
244
+ split: '',
241
245
  deployId: '',
242
246
  instanceId: '',
243
247
  user: '',
@@ -2267,19 +2271,25 @@ EOF
2267
2271
  * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
2268
2272
  * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
2269
2273
  *
2270
- * @param {string} path - Comma-separated: `deployId,instanceId[,projectPath]`.
2271
- * `projectPath` is the root directory that contains the `Dockerfile` (e.g. `./cyberia-client`).
2272
- * Artifacts are written to `<projectPath>/manifests/<env>/Dockerfile` and
2273
- * `<projectPath>/manifests/<env>/deployment.yaml`.
2274
- * In production, files are also copied to `<projectPath>/Dockerfile` and
2275
- * `<projectPath>/deployment.yaml`.
2274
+ * The target is named by `--deploy-id` and `--instance-id`, like every other instance runner.
2275
+ * Where the artifacts land is not an argument: an instance's project directory is derived from
2276
+ * the conf entry itself (see {@link instanceProjectPathFactory}), so the caller cannot pair an
2277
+ * instance with someone else's checkout. Artifacts are written to
2278
+ * `<project>/manifests/deployments/<instance>-<env>/`, and in production the pair is also
2279
+ * copied to `<project>/Dockerfile` and `<project>/deployment.yaml`.
2280
+ *
2281
+ * @param {string} path - Unused; the target comes from `--deploy-id` and `--instance-id`.
2276
2282
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2277
2283
  * @memberof UnderpostRun
2278
2284
  */
2279
2285
  'instance-build-manifest': async (path, options = DEFAULT_OPTION) => {
2280
2286
  const env = options.dev ? 'development' : 'production';
2281
- let [deployId, id, projectPath] = path.split(',');
2282
- const rootPath = projectPath ? projectPath : '.';
2287
+ const deployId = options.deployId;
2288
+ const id = options.instanceId;
2289
+ if (!deployId || !id) {
2290
+ logger.error('[instance-build-manifest] --deploy-id and --instance-id are required');
2291
+ return;
2292
+ }
2283
2293
 
2284
2294
  const confInstances = loadConfInstances(deployId);
2285
2295
  // Targeting a template id builds every world in the family. The fan-out
@@ -2297,20 +2307,23 @@ EOF
2297
2307
  }
2298
2308
  if (!options.instanceOnly && (selected.length > 1 || selected[0].id !== id)) {
2299
2309
  for (const instance of selected)
2300
- await UnderpostRun.RUNNERS['instance-build-manifest'](
2301
- [deployId, instance.id, projectPath].filter((v) => v !== undefined).join(','),
2302
- { ...options, instanceOnly: true },
2303
- );
2310
+ await UnderpostRun.RUNNERS['instance-build-manifest']('', {
2311
+ ...options,
2312
+ instanceId: instance.id,
2313
+ instanceOnly: true,
2314
+ });
2304
2315
  return;
2305
2316
  }
2306
2317
 
2318
+ const instance = selected[0];
2319
+ // The conf names the project: its repository, else its runtime, else its id. A world and the
2320
+ // checkout it publishes to cannot disagree, because only one of them is stated.
2321
+ const rootPath = instanceProjectPathFactory(instance);
2307
2322
  const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
2308
2323
  const outputPath = `${envManifestPath}/deployment.yaml`;
2309
2324
  const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
2310
2325
 
2311
2326
  fs.mkdirpSync(envManifestPath);
2312
-
2313
- const instance = selected[0];
2314
2327
  const isDefaultInstance = instance.id === instance.templateId || !instance.templateId;
2315
2328
  const instanceEnvBuilder = await loadProjectInstanceEnvBuilder(deployId);
2316
2329
 
@@ -2357,14 +2370,15 @@ EOF
2357
2370
  const _fromPort = instancePortFactory({ instance, env });
2358
2371
  const _toPort = instancePortFactory({ instance, env, container: true });
2359
2372
 
2360
- // Build image from projectPath Dockerfile and load into cluster when --build is set.
2361
- if (options.build && projectPath) {
2373
+ // Build the image from the project's own Dockerfile and load it into the cluster when
2374
+ // --build is set.
2375
+ if (options.build) {
2362
2376
  const isKind = !options.kubeadm && !options.k3s;
2363
2377
  Underpost.image.build({
2364
- path: projectPath,
2378
+ path: rootPath,
2365
2379
  imageName: _image,
2366
2380
  podmanSave: true,
2367
- imageOutPath: projectPath,
2381
+ imageOutPath: rootPath,
2368
2382
  kind: isKind,
2369
2383
  kubeadm: !!options.kubeadm,
2370
2384
  k3s: !!options.k3s,
@@ -2484,12 +2498,11 @@ EOF
2484
2498
  // a rewrite never points at a document that cannot exist. The check is a
2485
2499
  // read: placing the document into the gateway volume is `deploy
2486
2500
  // --sync-static`'s job at apply time, and a build must not mutate the host.
2487
- // `projectPath` is passed through because this runner is given one
2488
- // explicitly; the sync derives the same root from the instance itself.
2489
- const statusPageEntries = instanceStatusPageEntriesFactory({
2490
- instances: [instance],
2491
- projectPath: rootPath,
2492
- }).filter((entry) => fs.existsSync(entry.sourcePath));
2501
+ // No project override: this runner and the sync now derive the root from the same instance
2502
+ // by the same rule, so passing one here could only make them disagree.
2503
+ const statusPageEntries = instanceStatusPageEntriesFactory({ instances: [instance] }).filter((entry) =>
2504
+ fs.existsSync(entry.sourcePath),
2505
+ );
2493
2506
 
2494
2507
  // httproute.yaml — this instance's own routes, including the status routes
2495
2508
  // that reach the static utility instead of this workload. No Gateway is
@@ -3066,7 +3079,7 @@ EOF`);
3066
3079
  const hostListenResult = etcHostFactory(hosts);
3067
3080
  logger.info(hostListenResult.renderHosts);
3068
3081
  }
3069
- const version = 'v3.3.73';
3082
+ const version = 'v3.3.76';
3070
3083
  const instanceOptionsFactory = (deployId, instanceId) => ({
3071
3084
  ...options,
3072
3085
  ...clusterContextFactory(clusterType),
@@ -3850,9 +3863,9 @@ EOF`);
3850
3863
  );
3851
3864
  switch (serviceId) {
3852
3865
  case 'mongo-express-service': {
3853
- shellExec(`kubectl delete svc mongo-express-service -n ${options.namespace} --ignore-not-found`);
3854
- shellExec(`kubectl delete deployment mongo-express -n ${options.namespace} --ignore-not-found`);
3855
- shellExec(`kubectl apply -f manifests/deployment/mongo-express/deployment.yaml -n ${options.namespace}`);
3866
+ // One deploy path for the client, shared with `cluster --mongo-express`: the secret,
3867
+ // the auth-mode overlay selection and the readiness wait live there.
3868
+ await MongoExpress.deploy({ namespace: options.namespace, options });
3856
3869
  podToMonitor = 'mongo-express';
3857
3870
  break;
3858
3871
  }
@@ -4398,186 +4411,47 @@ EOF`;
4398
4411
 
4399
4412
  /**
4400
4413
  * @method push-bundle
4401
- * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
4402
- * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
4403
- * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
4404
- * @param {string} path - Optional `fsPath.splitOption` string.
4405
- * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
4414
+ * @description Builds the deployment's client bundles and uploads them to file storage.
4415
+ * Steps: set env, build and split the zips, then upload each resolved route's artifacts
4416
+ * under the deployment's `storage.bundle.json`. Routes the client build skips redirects,
4417
+ * rebuild-disabled routes, and `singleReplica` routes that carry their own deploy id —
4418
+ * produce no bundle and are not pushed.
4419
+ * @param {string} path - Optional comma-separated host filter (e.g. 'a.com' or 'a.com,b.com').
4420
+ * Empty selects every host in `engine-private/conf/<deployId>/conf.server.json`.
4406
4421
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4407
- * @param {string} [options.deployId] - Override deploy ID.
4422
+ * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
4423
+ * @param {string} [options.split] - Zip part size in MB, or 'none' to upload one zip. Defaults to 8.
4408
4424
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4409
4425
  * @memberof UnderpostRun
4410
4426
  */
4411
- 'push-bundle': (path = '', options = DEFAULT_OPTION) => {
4412
- const baseCommand = cli('underpost', { local: true });
4413
- const env = options.dev ? 'development' : 'production';
4414
- const deployId = options.deployId || 'dd-default';
4415
- const pathParts = (path || '').split('.');
4416
- const fsPath = (pathParts[0] || '').trim() || 'build';
4417
- const splitOption = (pathParts[1] || '').trim();
4418
-
4419
- let splitFlag = '--split 8';
4420
- if (splitOption) {
4421
- if (splitOption === 'none-split') {
4422
- splitFlag = '';
4423
- } else {
4424
- const splitMb = Number(splitOption);
4425
- if (Number.isFinite(splitMb) && splitMb > 0) {
4426
- splitFlag = `--split ${splitMb}`;
4427
- } else {
4428
- logger.warn('push-bundle: invalid split option, using default split 8', {
4429
- path,
4430
- splitOption,
4431
- });
4432
- }
4433
- }
4434
- }
4435
-
4436
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
4437
- const confServer = fs.existsSync(confServerPath)
4438
- ? loadReplicas(deployId, loadConfServerJson(confServerPath))
4439
- : {};
4440
- const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
4441
-
4442
- shellExec(`${baseCommand} env ${deployId} ${env}`);
4443
- shellExec(`${baseCommand} client ${deployId} --env ${env} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4444
-
4445
- const pushBundleFiles = (host, routePath) => {
4446
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
4447
- const buildDir = `./${fsPath}`;
4448
- if (!fs.existsSync(buildDir)) return;
4449
- const partFiles = fs
4450
- .readdirSync(buildDir)
4451
- .filter(
4452
- (name) =>
4453
- name.startsWith(`${buildId}.zip.part`) ||
4454
- name.startsWith(`${buildId}.zip-part`) ||
4455
- name === `${buildId}.zip`,
4456
- )
4457
- .map((name) => `${fsPath}/${name}`);
4458
- if (partFiles.length === 0) {
4459
- logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
4460
- return;
4461
- }
4462
- for (const partFile of partFiles) {
4463
- shellExec(
4464
- `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
4465
- );
4466
- }
4467
- };
4468
-
4469
- for (const host of Object.keys(confServer)) {
4470
- for (const routePath of Object.keys(confServer[host])) {
4471
- const routeConf = confServer[host][routePath] || {};
4472
- if (routeConf.redirect || routeConf.disabledRebuild) continue;
4473
- if (routeConf.singleReplica) {
4474
- if (routeConf.replicas) {
4475
- for (const replica of routeConf.replicas) {
4476
- pushBundleFiles(host, replica);
4477
- }
4478
- }
4479
- continue;
4480
- }
4481
- pushBundleFiles(host, routePath);
4482
- }
4483
- }
4484
- },
4427
+ 'push-bundle': (path = '', options = DEFAULT_OPTION) =>
4428
+ pushClientBundle({
4429
+ deployId: options.deployId || 'dd-default',
4430
+ hosts: clientBundleHostFilter(path),
4431
+ split: options.split,
4432
+ dev: options.dev,
4433
+ }),
4485
4434
 
4486
4435
  /**
4487
4436
  * @method pull-bundle
4488
- * @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
4489
- * Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
4490
- * Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
4491
- * so that multi-path deployments are handled correctly.
4492
- * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
4493
- * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
4437
+ * @description Downloads the deployment's client bundles from file storage and installs them
4438
+ * into the served public directory.
4439
+ * Steps: set env, download the parts, merge and extract each resolved route's zip, then
4440
+ * replace `public/<host>[<route>]` with the extracted tree. Route selection matches
4441
+ * push-bundle, so the two directions always address the same set of bundles.
4442
+ * @param {string} path - Optional comma-separated host filter (e.g. 'a.com' or 'a.com,b.com').
4443
+ * Empty selects every host in `engine-private/conf/<deployId>/conf.server.json`.
4494
4444
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4495
4445
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
4496
4446
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4497
4447
  * @memberof UnderpostRun
4498
4448
  */
4499
- 'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
4500
- const baseCommand = cli('underpost', { local: true });
4501
- const env = options.dev ? 'development' : 'production';
4502
- const deployId = options.deployId || 'dd-default';
4503
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
4504
- const confServer = fs.existsSync(confServerPath)
4505
- ? loadReplicas(deployId, loadConfServerJson(confServerPath))
4506
- : {};
4507
- const hostsArg = path
4508
- ? path
4509
- .split(',')
4510
- .map((h) => h.trim())
4511
- .filter(Boolean)
4512
- : Object.keys(confServer);
4513
-
4514
- if (hostsArg.length === 0) {
4515
- logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
4516
- return;
4517
- }
4518
-
4519
- shellExec(`${baseCommand} env ${deployId} ${env}`);
4520
- if (!fs.existsSync('./build')) fs.mkdirSync('./build', { recursive: true });
4521
- shellExec(
4522
- `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
4523
- );
4524
-
4525
- const pullBundleRoute = (host, routePath) => {
4526
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
4527
- const zipPath = `build/${buildId}.zip`;
4528
- const buildDir = './build';
4529
- const hasZip = fs.existsSync(zipPath);
4530
- const hasParts =
4531
- fs.existsSync(buildDir) &&
4532
- fs
4533
- .readdirSync(buildDir)
4534
- .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
4535
-
4536
- if (!hasZip && !hasParts) {
4537
- logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
4538
- return;
4539
- }
4540
-
4541
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
4542
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
4543
- shellExec(`sudo rm -rf ${zipPath}`);
4544
-
4545
- if (fs.existsSync(buildDir)) {
4546
- fs.readdirSync(buildDir)
4547
- .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
4548
- .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
4549
- }
4550
-
4551
- const extractedDir = `build/${buildId.replace(/-$/, '')}`;
4552
- if (!fs.existsSync(extractedDir)) {
4553
- logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
4554
- return;
4555
- }
4556
-
4557
- const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
4558
- if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
4559
- if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
4560
- fs.copySync(`${extractedDir}`, `${publicDestPath}`);
4561
- };
4562
-
4563
- for (const host of hostsArg) {
4564
- const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
4565
-
4566
- for (const routePath of routePaths) {
4567
- const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
4568
- if (routeConf.redirect || routeConf.disabledRebuild) continue;
4569
- if (routeConf.singleReplica) {
4570
- if (routeConf.replicas) {
4571
- for (const replica of routeConf.replicas) {
4572
- pullBundleRoute(host, replica);
4573
- }
4574
- }
4575
- continue;
4576
- }
4577
- pullBundleRoute(host, routePath);
4578
- }
4579
- }
4580
- },
4449
+ 'pull-bundle': async (path = '', options = DEFAULT_OPTION) =>
4450
+ await pullClientBundle({
4451
+ deployId: options.deployId || 'dd-default',
4452
+ hosts: clientBundleHostFilter(path),
4453
+ dev: options.dev,
4454
+ }),
4581
4455
 
4582
4456
  /**
4583
4457
  * @method kubeadm-wireguard
package/src/cli/test.js CHANGED
@@ -19,6 +19,7 @@ import { shellExec } from '../server/runtime/process.js';
19
19
  import {
20
20
  UNDERPOST_TESTING,
21
21
  allureManifestsFactory,
22
+ coverageReportKey,
22
23
  resolveTestSelection,
23
24
  testJobManifestFactory,
24
25
  vitestArgsFactory,
@@ -82,6 +83,9 @@ class UnderpostTest {
82
83
  'NODE_ENV=test',
83
84
  ...(allureResultsDirectory ? [`${UNDERPOST_TESTING.allureResultsEnvKey}=${allureResultsDirectory}`] : []),
84
85
  ];
86
+ // Only this selection's report starts from nothing: the ones beside it belong to
87
+ // other selections a deploy on this host may publish.
88
+ if (coverage) fs.removeSync(`${root}/${UNDERPOST_TESTING.coverageDirectory}/${coverageReportKey(suite)}`);
85
89
  shellExec(
86
90
  `cd ${root} && ${env.join(' ')} npx vitest ${vitestArgsFactory({ projects, grep, watch, coverage }).join(' ')}`,
87
91
  );
@@ -95,42 +95,27 @@ const EVENT_SERVICE_UNIT_PATH = '/etc/systemd/system/underpost-event.service';
95
95
 
96
96
  /**
97
97
  * @constant ENGINE_SYNC_STEPS
98
- * @description What bringing one node's checkout up to date consists of.
98
+ * @description Sequence of steps to bring a node's checkout up to date.
99
99
  *
100
- * The deploy's package script runs twice, and that is the shape of the whole sequence: every
101
- * step between them is the node's own CLI, which cannot load from a checkout whose installed
102
- * packages no longer match its manifest — so the first run repairs the tree it finds, and the
103
- * second installs the manifest the switch just landed. The first is advisory because a node
104
- * whose tree predates that script must still reach the switch that gives it one.
100
+ * The sync runs the node's own CLI, bracketed by two critical installs:
105
101
  *
106
- * The switch moves the node between package scopes, and everything the node runs off that scope
107
- * has to follow it, or the sync leaves the node running the previous one:
108
- * - the repository pair is written into the node's host configuration, because that store is the
109
- * only place `deploy/lib/host.sh` reads the source a later `prepare_host` pulls from. Without
110
- * it that preparation resolves the default pairing and undoes the switch.
111
- * - the cluster's CronJob manifests are regenerated and republished, because their pod bodies run
112
- * the checkout that just changed.
113
- * - the supervised services are reconciled through their own generators, on the unit file's
114
- * existence rather than on the unit being active: one the previous scope's tree left dead is
115
- * exactly the one that has to come back, and `is-active` skipped precisely that case.
102
+ * 1. **Bootstrap Install:** Restores the original `package.json` from git and installs it.
103
+ * This ensures the CLI remains runnable with the current code tree before any changes are applied.
104
+ * 2. **Deploy Install:** Installs the deploy's `package.json` and links the CLI. This happens
105
+ * after both the configuration and source code switches have landed.
116
106
  *
117
- * All of those are scoped to the role of the node reached — see {@link NODE_ROLE_STEPS}.
118
- *
119
- * A step whose placeholder resolves to nothing is dropped rather than run empty: a monorepo sync
120
- * belongs to no deploy, a deploy that ships no package script has nothing to run, and a run
121
- * without a token must not overwrite the node's with a blank.
122
107
  * @memberof UnderpostWireguard
123
108
  */
124
109
  const ENGINE_SYNC_STEPS = [
125
- { command: 'bash <deploy-package-script>', halt: false },
110
+ { command: 'git checkout -- package.json; npm install', halt: false },
126
111
  { command: 'node bin host set GITHUB_TOKEN <github-token>' },
127
- { command: 'underpost run clean', halt: true },
128
- { command: 'underpost cmt --switch-repo <engine> --target-branch <engine-branch>', halt: true },
112
+ { command: 'node bin run clean', halt: true },
129
113
  {
130
- command: 'underpost cmt ./engine-private --switch-repo <engine-private> --target-branch <engine-private-branch>',
114
+ command: 'node bin cmt ./engine-private --switch-repo <engine-private> --target-branch <engine-private-branch>',
131
115
  halt: true,
132
116
  },
133
- { command: 'bash <deploy-package-script>', halt: true },
117
+ { command: 'node bin cmt --switch-repo <engine> --target-branch <engine-branch>', halt: true },
118
+ { command: '<package-install-command>', halt: true },
134
119
  { command: 'node bin host set ENGINE_SRC_REPO <engine>', halt: true },
135
120
  { command: 'node bin host set ENGINE_SRC_PRIVATE_REPO <engine-private>', halt: true },
136
121
  { command: '<cron-reconcile-command>', halt: false },
@@ -159,10 +144,9 @@ const deployIdFactory = (deployId) => {
159
144
  * being synced actually ships one.
160
145
  *
161
146
  * Both halves have to hold. A monorepo sync belongs to no deploy, and a deploy that ships no
162
- * script of its own would leave the node running `bash` against a path that does not exist
163
- * which the second, halting occurrence turns into a failed sync rather than a missing install.
164
- * The check reads this checkout because it is the tree the node lands on once the switch
165
- * completes, which is the only tree whose contents the step can be resolved against.
147
+ * script of its own would leave the node running `bash` against a path that does not exist. The
148
+ * check reads this checkout because it is the tree the node lands on once the switch completes,
149
+ * which is the only tree whose contents the step can be resolved against.
166
150
  * @param {string} engine - Engine source repository the node is switched onto.
167
151
  * @returns {string} Path to that deploy's package script, or `''` when there is none to run.
168
152
  * @memberof UnderpostWireguard
@@ -173,6 +157,23 @@ const deployPackageScriptFactory = (engine = '') => {
173
157
  return path && fs.existsSync(path) ? path : '';
174
158
  };
175
159
 
160
+ /**
161
+ * @method packageInstallCommandFactory
162
+ * @description How a node installs against the source a switch just landed.
163
+ *
164
+ * A deploy's own script is preferred because it installs that deploy's manifest and links the
165
+ * CLI. Where there is none — a monorepo sync, or a deploy that ships no script — the checkout's
166
+ * own manifest is installed instead, so the step is never dropped: every sync replaces the
167
+ * source, and source the installed packages predate is what breaks the next command.
168
+ * @param {string} engine - Engine source repository the node is switched onto.
169
+ * @returns {string} The install command for that source.
170
+ * @memberof UnderpostWireguard
171
+ */
172
+ const packageInstallCommandFactory = (engine = '') => {
173
+ const packageScript = deployPackageScriptFactory(engine);
174
+ return packageScript ? `bash ${packageScript}` : 'npm install';
175
+ };
176
+
176
177
  /**
177
178
  * @method deployListFactory
178
179
  * @description The deploys whose hostnames a run publishes.
@@ -2666,7 +2667,7 @@ class UnderpostWireguard {
2666
2667
  '<engine-private>': enginePrivate,
2667
2668
  '<engine-branch>': Underpost.repo.getDefaultBranch(engine),
2668
2669
  '<engine>': engine,
2669
- '<deploy-package-script>': deployPackageScriptFactory(engine),
2670
+ '<package-install-command>': packageInstallCommandFactory(engine),
2670
2671
  ...nodeRoleStepsFactory(options.nodeRole),
2671
2672
  '<github-token>': process.env.GITHUB_TOKEN || '',
2672
2673
  };