@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -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/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import dotenv from 'dotenv';
8
+ import { execFileSync } from 'node:child_process';
8
9
  import { commitData } from '../client/components/core/CommonJs.js';
9
10
  import { pbcopy, shellArgumentFactory, shellCd, shellExec } from '../server/runtime/process.js';
10
11
  import { actionInitLog, loggerFactory, redactSensitiveText } from '../server/ops/logger.js';
@@ -788,6 +789,16 @@ class UnderpostRepository {
788
789
  return diffUntrackOutput.toString().split('\n').filter(Boolean);
789
790
  },
790
791
 
792
+ // Read tracked paths without changing the index or working tree.
793
+ getTrackedFiles(directory = '.') {
794
+ const options = { cwd: path.resolve(directory), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 };
795
+ const root = execFileSync('git', ['rev-parse', '--show-toplevel'], options).replace(/\r?\n$/, '');
796
+ return execFileSync('git', ['ls-files', '--cached', '--full-name', '-z', '--', '.'], options)
797
+ .split('\0')
798
+ .filter(Boolean)
799
+ .map((file) => path.resolve(root, file));
800
+ },
801
+
791
802
  /**
792
803
  * Gets a list of changed (modified and untracked) files in a Git repository.
793
804
  * @param {string} [path='.'] - The path to the repository.
@@ -1736,6 +1747,23 @@ Prevent build private config repo.`,
1736
1747
  }
1737
1748
  },
1738
1749
 
1750
+ /**
1751
+ * Reports whether a path already sits inside a git work tree, so callers can skip
1752
+ * initializing a repository that would nest inside an existing one.
1753
+ * @param {string} repoPath - Directory to probe.
1754
+ * @returns {boolean} True when the path is tracked by an enclosing repository.
1755
+ * @memberof UnderpostRepository
1756
+ */
1757
+ isInsideWorkTree(repoPath) {
1758
+ const output = shellExec(`cd "${repoPath}" && git rev-parse --is-inside-work-tree`, {
1759
+ stdout: true,
1760
+ silent: true,
1761
+ disableLog: true,
1762
+ silentOnError: true,
1763
+ });
1764
+ return `${output ?? ''}`.trim() === 'true';
1765
+ },
1766
+
1739
1767
  /**
1740
1768
  * Declares a path safe for git, skipping the write when it is already declared so repeated
1741
1769
  * provisioning does not append duplicate global config entries.
@@ -2132,13 +2160,13 @@ Prevent build private config repo.`,
2132
2160
 
2133
2161
  /**
2134
2162
  * Ensures a deploy's public source repo (e.g. `engine-prototype`) is present
2135
- * next to the engine and reset to a pristine HEAD, so catalog `sourceMoves`
2136
- * can (re)pull custom sources even after a previous build moved them out of
2137
- * the source tree.
2163
+ * next to the engine, so catalog `sourceMoves` the engine tree lacks can be
2164
+ * copied in.
2138
2165
  *
2139
- * Clones `../<repoName>` when missing; otherwise restores a clean checkout
2140
- * (`git checkout .` brings back any moved-out tracked files) and pulls latest.
2141
- * Mirrors the sibling-repo handling used by `syncPrivateConf`.
2166
+ * Clones `../<repoName>` when missing; otherwise fast-forwards it. The working
2167
+ * tree is never reset: it is the git view of edits made in the engine tree,
2168
+ * which ignores those paths, so a pending change there is review material, not
2169
+ * something to discard.
2142
2170
  *
2143
2171
  * @param {string} repoName - Public source repo name (e.g. `engine-prototype`).
2144
2172
  * @returns {boolean} `true` when the repo is available on disk.
@@ -2154,9 +2182,7 @@ Prevent build private config repo.`,
2154
2182
  } else {
2155
2183
  const repoAbsPath = path.resolve(repoPath);
2156
2184
  shellExec(`git config --global --add safe.directory '${repoAbsPath}'`);
2157
- shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
2158
- silent: true,
2159
- });
2185
+ shellExec(`cd ${repoPath} && underpost pull . ${gitUri}`, { silent: true });
2160
2186
  }
2161
2187
  return fs.existsSync(repoPath);
2162
2188
  },
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.77';
3070
3083
  const instanceOptionsFactory = (deployId, instanceId) => ({
3071
3084
  ...options,
3072
3085
  ...clusterContextFactory(clusterType),
@@ -3505,10 +3518,17 @@ EOF`);
3505
3518
  logger.info('[gateway-status] Backend probe (same Host and path, bypassing Envoy)', { backendProbes });
3506
3519
  // Only the rule the browser actually hit is comparable, so judge on
3507
3520
  // the root rule rather than on every rule of the route.
3521
+ // The gateway answer carries the `server` header (`502 (nginx)`), the
3522
+ // direct one is the bare code: compared as strings, every relayed status
3523
+ // read as a routing fault, and the data plane log was dumped on a healthy
3524
+ // edge whose only problem was an absent workload.
3525
+ const statusCode = (answer) => /^([0-9]{3})/.exec(`${answer ?? ''}`)?.[1];
3508
3526
  const rootProbes = backendProbes.filter((entry) => entry.request.endsWith('/'));
3509
- const appFault = rootProbes.filter((entry) => `${entry.direct}` === `${entry.gateway}`);
3527
+ const appFault = rootProbes.filter(
3528
+ (entry) => statusCode(entry.direct) && statusCode(entry.direct) === statusCode(entry.gateway),
3529
+ );
3510
3530
  const gatewayFault = rootProbes.filter(
3511
- (entry) => /^[0-9]{3}$/.test(entry.direct) && `${entry.direct}` !== `${entry.gateway}`,
3531
+ (entry) => statusCode(entry.direct) && statusCode(entry.direct) !== statusCode(entry.gateway),
3512
3532
  );
3513
3533
  if (gatewayFault.length > 0) {
3514
3534
  logger.warn(
@@ -3850,9 +3870,9 @@ EOF`);
3850
3870
  );
3851
3871
  switch (serviceId) {
3852
3872
  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}`);
3873
+ // One deploy path for the client, shared with `cluster --mongo-express`: the secret,
3874
+ // the auth-mode overlay selection and the readiness wait live there.
3875
+ await MongoExpress.deploy({ namespace: options.namespace, options });
3856
3876
  podToMonitor = 'mongo-express';
3857
3877
  break;
3858
3878
  }
@@ -4398,186 +4418,47 @@ EOF`;
4398
4418
 
4399
4419
  /**
4400
4420
  * @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).
4421
+ * @description Builds the deployment's client bundles and uploads them to file storage.
4422
+ * Steps: set env, build and split the zips, then upload each resolved route's artifacts
4423
+ * under the deployment's `storage.bundle.json`. Routes the client build skips redirects,
4424
+ * rebuild-disabled routes, and `singleReplica` routes that carry their own deploy id —
4425
+ * produce no bundle and are not pushed.
4426
+ * @param {string} path - Optional comma-separated host filter (e.g. 'a.com' or 'a.com,b.com').
4427
+ * Empty selects every host in `engine-private/conf/<deployId>/conf.server.json`.
4406
4428
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4407
- * @param {string} [options.deployId] - Override deploy ID.
4429
+ * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
4430
+ * @param {string} [options.split] - Zip part size in MB, or 'none' to upload one zip. Defaults to 8.
4408
4431
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4409
4432
  * @memberof UnderpostRun
4410
4433
  */
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
- },
4434
+ 'push-bundle': (path = '', options = DEFAULT_OPTION) =>
4435
+ pushClientBundle({
4436
+ deployId: options.deployId || 'dd-default',
4437
+ hosts: clientBundleHostFilter(path),
4438
+ split: options.split,
4439
+ dev: options.dev,
4440
+ }),
4485
4441
 
4486
4442
  /**
4487
4443
  * @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.
4444
+ * @description Downloads the deployment's client bundles from file storage and installs them
4445
+ * into the served public directory.
4446
+ * Steps: set env, download the parts, merge and extract each resolved route's zip, then
4447
+ * replace `public/<host>[<route>]` with the extracted tree. Route selection matches
4448
+ * push-bundle, so the two directions always address the same set of bundles.
4449
+ * @param {string} path - Optional comma-separated host filter (e.g. 'a.com' or 'a.com,b.com').
4450
+ * Empty selects every host in `engine-private/conf/<deployId>/conf.server.json`.
4494
4451
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4495
4452
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
4496
4453
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4497
4454
  * @memberof UnderpostRun
4498
4455
  */
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
- },
4456
+ 'pull-bundle': async (path = '', options = DEFAULT_OPTION) =>
4457
+ await pullClientBundle({
4458
+ deployId: options.deployId || 'dd-default',
4459
+ hosts: clientBundleHostFilter(path),
4460
+ dev: options.dev,
4461
+ }),
4581
4462
 
4582
4463
  /**
4583
4464
  * @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
  };
@@ -7,7 +7,6 @@ class Badge {
7
7
  static async instance(options = { id: '', type: 'circle-red', classList: '', text: '', style: '' }) {
8
8
  if (!options.id) options.id = getId(Badge.Tokens, 'badge-');
9
9
  else options.id = 'badge-' + options.id;
10
- if (options && options.style && !options.style.color) options.style.color = 'white';
11
10
  if (!options.classList) options.classList = '';
12
11
  const { id, type } = options;
13
12
  Badge.Tokens[id] = { ...options };
@@ -53,23 +53,30 @@ const CssCommonCore = async () => {
53
53
  .btn-label-content {
54
54
  top: 15px;
55
55
  }
56
+ /* Badge: one dark chip in every theme, so its lettering is always white — a caller
57
+ only sets a color when it also sets the background (see Badge.instance style). */
56
58
  .badge {
57
59
  min-width: 20px;
58
60
  height: 20px;
59
61
  border-radius: 3px;
60
62
  background: #3b434b;
63
+ color: white;
61
64
  position: relative;
62
65
  font-size: 12px;
63
66
  font-family: arial;
64
67
  }
65
68
  .badge-text {
66
- /* color: white; */
67
69
  padding: 3px 6px 0px 6px;
68
70
  }
69
71
  .badge-notification-circle-red {
70
72
  background: #bd0c0c;
71
73
  border-radius: 50%;
72
74
  }
75
+ /* Unread dot over a menu button icon; the label reads left-to-right in both menu modes. */
76
+ .badge-notification-menu {
77
+ top: -33px;
78
+ left: 24px;
79
+ }
73
80
  .top-box-profile-container {
74
81
  top: 0px;
75
82
  right: 0px;