@underpostnet/cyberia 3.3.0 → 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 (568) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +17 -5
  3. package/.github/workflows/coverall.cyberia.ci.yml +39 -14
  4. package/.github/workflows/cyberia-client.cd.yml +5 -0
  5. package/.github/workflows/cyberia-server.cd.yml +5 -0
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +3 -0
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +35 -20
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +1 -0
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +96 -0
  22. package/CHANGELOG.md +653 -2513
  23. package/CLI-HELP.md +323 -157
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +5 -5
  27. package/README.md +48 -28
  28. package/bin/build.js +86 -41
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +1494 -1403
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +1494 -1403
  33. package/compose.env +17 -5
  34. package/conf.js +98 -7
  35. package/deploy/cyberia-client/deploy.sh +46 -0
  36. package/deploy/cyberia-client/package.sh +17 -0
  37. package/deploy/cyberia-client/state.sh +15 -0
  38. package/deploy/cyberia-server/deploy.sh +46 -0
  39. package/deploy/cyberia-server/package.sh +17 -0
  40. package/deploy/cyberia-server/state.sh +15 -0
  41. package/deploy/dd-cyberia/deploy.sh +9 -29
  42. package/deploy/dd-cyberia/init.sh +38 -45
  43. package/deploy/dd-cyberia/package.sh +16 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +133 -147
  46. package/deploy/lib/config.sh +29 -0
  47. package/deploy/lib/github-actions-logging.sh +610 -0
  48. package/deploy/lib/host.sh +194 -0
  49. package/deploy/lib/state.sh +65 -0
  50. package/deploy/pwa-microservices-template/deploy.sh +17 -42
  51. package/deploy/release/deploy.sh +9 -41
  52. package/deployment.yaml +7 -2
  53. package/docker-compose.yml +23 -17
  54. package/examples/static-page/README.md +19 -19
  55. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  56. package/hardhat/hardhat.config.js +1 -6
  57. package/hardhat/package-lock.json +101 -40
  58. package/hardhat/package.json +2 -2
  59. package/manifests/alertmanager/deployment.yaml +81 -0
  60. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  61. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  62. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  63. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  64. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  65. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  66. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  67. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  68. package/manifests/deployment/mongo-express/service.yaml +16 -0
  69. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  70. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  71. package/manifests/grafana/deployment.yaml +26 -7
  72. package/manifests/grafana/kustomization.yaml +1 -0
  73. package/manifests/grafana/service.yaml +6 -1
  74. package/manifests/mariadb/pv.yaml +1 -0
  75. package/manifests/mongodb/statefulset.yaml +9 -2
  76. package/manifests/mysql/pv-pvc.yaml +14 -1
  77. package/manifests/postgresql/pv.yaml +12 -1
  78. package/manifests/postgresql/pvc.yaml +1 -0
  79. package/manifests/prometheus/deployment.yaml +18 -22
  80. package/package.json +87 -12
  81. package/scripts/event-monitor.sh +56 -0
  82. package/scripts/k3s-node-setup.sh +27 -14
  83. package/scripts/kubeadm-node-setup.sh +31 -11
  84. package/scripts/maas-setup.sh +8 -8
  85. package/scripts/selinux-normalize.sh +907 -0
  86. package/scripts/shutdown-machine.sh +843 -0
  87. package/scripts/test-monitor.sh +5 -5
  88. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +15 -10
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +7 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +90 -186
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +3 -3
  99. package/src/api/cyberia-action/cyberia-action.service.js +5 -6
  100. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  101. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  102. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  103. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  104. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +2 -4
  105. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  106. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +13 -35
  107. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +17 -34
  108. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  109. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  110. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
  111. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  112. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  113. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  114. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  115. package/src/api/cyberia-entity/cyberia-entity.service.js +3 -3
  116. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  117. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  118. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -2
  119. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +468 -5
  120. package/src/api/cyberia-instance/cyberia-fallback-capture.js +502 -0
  121. package/src/api/cyberia-instance/cyberia-fallback-world.js +29 -40
  122. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  123. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  124. package/src/api/cyberia-instance/cyberia-instance-map.service.js +35 -142
  125. package/src/api/cyberia-instance/cyberia-instance.controller.js +2 -5
  126. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  127. package/src/api/cyberia-instance/cyberia-instance.router.js +5 -18
  128. package/src/api/cyberia-instance/cyberia-instance.service.js +5 -45
  129. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  130. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  131. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  132. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  133. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +76 -3
  134. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  135. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  136. package/src/api/cyberia-map/cyberia-map.service.js +8 -4
  137. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  138. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  139. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  140. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  141. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  142. package/src/api/cyberia-quest/cyberia-quest.router.js +4 -4
  143. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  144. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  145. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  146. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  147. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  148. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  149. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  150. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  151. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -214
  152. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  153. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  154. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  155. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  156. package/src/api/default/default.controller.js +1 -1
  157. package/src/api/default/default.router.js +1 -1
  158. package/src/api/default/default.service.js +2 -2
  159. package/src/api/document/document.controller.js +1 -1
  160. package/src/api/document/document.router.js +1 -1
  161. package/src/api/document/document.service.js +2 -2
  162. package/src/api/file/file.controller.js +1 -1
  163. package/src/api/file/file.ref.js +153 -0
  164. package/src/api/file/file.ref.json +11 -2
  165. package/src/api/file/file.router.js +1 -1
  166. package/src/api/file/file.service.js +2 -2
  167. package/src/api/instance/instance.controller.js +1 -1
  168. package/src/api/instance/instance.router.js +1 -1
  169. package/src/api/instance/instance.service.js +2 -2
  170. package/src/api/ipfs/ipfs.controller.js +1 -1
  171. package/src/api/ipfs/ipfs.router.js +2 -2
  172. package/src/api/ipfs/ipfs.service.js +2 -2
  173. package/src/api/object-layer/object-layer.controller.js +1 -1
  174. package/src/api/object-layer/object-layer.model.js +47 -12
  175. package/src/api/object-layer/object-layer.router.js +12 -5
  176. package/src/api/object-layer/object-layer.service.js +116 -123
  177. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  178. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  179. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  180. package/src/api/test/test.controller.js +1 -1
  181. package/src/api/test/test.service.js +1 -1
  182. package/src/api/user/guest.service.js +3 -3
  183. package/src/api/user/user.controller.js +1 -1
  184. package/src/api/user/user.router.js +2 -2
  185. package/src/api/user/user.service.js +4 -4
  186. package/src/api.js +5 -9
  187. package/src/cli/app.js +402 -0
  188. package/src/cli/baremetal.js +30 -35
  189. package/src/cli/client.js +306 -0
  190. package/src/cli/cloud-init.js +3 -3
  191. package/src/cli/cluster.js +191 -82
  192. package/src/cli/db.js +120 -68
  193. package/src/cli/deploy.js +157 -75
  194. package/src/cli/docker-compose.js +5 -5
  195. package/src/cli/domains.js +184 -0
  196. package/src/cli/dotenv-store.js +143 -0
  197. package/src/cli/event.js +2281 -0
  198. package/src/cli/fs-selection.js +115 -0
  199. package/src/cli/fs.js +159 -343
  200. package/src/cli/host.js +672 -0
  201. package/src/cli/image.js +4 -6
  202. package/src/cli/index.js +343 -151
  203. package/src/cli/ipfs.js +34 -30
  204. package/src/cli/kickstart.js +1 -1
  205. package/src/cli/kubectl.js +218 -41
  206. package/src/cli/lxd.js +3 -3
  207. package/src/cli/monitor.js +1359 -26
  208. package/src/cli/package.js +90 -0
  209. package/src/cli/release.js +16 -12
  210. package/src/cli/repository.js +586 -363
  211. package/src/cli/run.js +398 -843
  212. package/src/cli/secrets.js +2158 -964
  213. package/src/cli/ssh.js +478 -279
  214. package/src/cli/state.js +394 -0
  215. package/src/cli/static.js +2 -2
  216. package/src/cli/system.js +1 -1
  217. package/src/cli/test.js +203 -125
  218. package/src/cli/vultr.js +61 -21
  219. package/src/cli/wireguard.js +1112 -306
  220. package/src/client/components/core/CalendarCore.js +53 -96
  221. package/src/client/components/core/Docs.js +327 -303
  222. package/src/client/components/core/FileExplorer.js +0 -21
  223. package/src/client/components/core/Modal.js +56 -1
  224. package/src/client/components/core/PanelForm.js +4 -4
  225. package/src/client/components/core/Repository.js +56 -0
  226. package/src/client/components/core/Translate.js +7 -0
  227. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  228. package/src/client/components/core/Wallet.js +0 -11
  229. package/src/client/components/core/Worker.js +7 -1
  230. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  231. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  232. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  233. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  234. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  235. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  236. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  237. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  238. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  239. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  240. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -35
  241. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  242. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  243. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  244. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  245. package/src/client/components/default/AppShellDefault.js +1 -1
  246. package/src/client/components/default/SettingsDefault.js +2 -2
  247. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  248. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  249. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  250. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  251. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +313 -30
  252. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  253. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  254. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  255. package/src/client/public/cyberia-docs/CYBERIA.md +48 -28
  256. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  257. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  258. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  259. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  260. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  261. package/src/client/public/default/android-chrome-144x144.png +0 -0
  262. package/src/client/public/default/android-chrome-192x192.png +0 -0
  263. package/src/client/public/default/android-chrome-256x256.png +0 -0
  264. package/src/client/public/default/android-chrome-36x36.png +0 -0
  265. package/src/client/public/default/android-chrome-384x384.png +0 -0
  266. package/src/client/public/default/android-chrome-48x48.png +0 -0
  267. package/src/client/public/default/android-chrome-512x512.png +0 -0
  268. package/src/client/public/default/android-chrome-72x72.png +0 -0
  269. package/src/client/public/default/android-chrome-96x96.png +0 -0
  270. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  271. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  272. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  273. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  274. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  275. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  276. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  277. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  278. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  279. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  280. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  281. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  282. package/src/client/public/default/apple-touch-icon.png +0 -0
  283. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  284. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  285. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  286. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  287. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  288. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  289. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  290. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  291. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  292. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  293. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  294. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  295. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  296. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  297. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  298. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  299. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  300. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  301. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  302. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  303. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  304. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  305. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  306. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  307. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  308. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  309. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  310. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  311. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  312. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  313. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  314. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  315. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  316. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  317. package/src/client/public/default/assets/background/white.jpg +0 -0
  318. package/src/client/public/default/browserconfig.xml +1 -1
  319. package/src/client/public/default/favicon-16x16.png +0 -0
  320. package/src/client/public/default/favicon-32x32.png +0 -0
  321. package/src/client/public/default/favicon-48x48.png +0 -0
  322. package/src/client/public/default/favicon.ico +0 -0
  323. package/src/client/public/default/manifest.webmanifest +5 -5
  324. package/src/client/public/default/mstile-144x144.png +0 -0
  325. package/src/client/public/default/mstile-150x150.png +0 -0
  326. package/src/client/public/default/mstile-310x150.png +0 -0
  327. package/src/client/public/default/mstile-310x310.png +0 -0
  328. package/src/client/public/default/mstile-70x70.png +0 -0
  329. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  330. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  331. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  332. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  333. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  334. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  335. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  336. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  337. package/src/client/services/object-layer/object-layer.management.js +6 -10
  338. package/src/client/services/object-layer/object-layer.service.js +3 -1
  339. package/src/client/ssr/RootDocument.js +1 -1
  340. package/src/client/ssr/body/404.js +1 -1
  341. package/src/client/ssr/body/500.js +1 -1
  342. package/src/client/ssr/body/CacheControl.js +1 -1
  343. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  344. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  345. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  346. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  347. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  348. package/src/client/ssr/head/Css.js +1 -1
  349. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  350. package/src/client/ssr/head/DefaultScripts.js +1 -1
  351. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  352. package/src/client/ssr/head/Microdata.js +1 -1
  353. package/src/client/ssr/head/Production.js +1 -1
  354. package/src/client/ssr/head/Pwa.js +1 -1
  355. package/src/client/ssr/head/PwaItemledger.js +1 -1
  356. package/src/client/ssr/head/Seo.js +1 -1
  357. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  358. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  359. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  360. package/src/client/ssr/views/Cyberia404.js +7 -5
  361. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  362. package/src/client/ssr/views/Maintenance.js +1 -1
  363. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  364. package/src/client/ssr/views/Test.js +1 -1
  365. package/src/client/sw/core.sw.js +20 -14
  366. package/src/client-builder/client-build-docs.js +92 -77
  367. package/src/client-builder/client-build-live.js +2 -2
  368. package/src/client-builder/client-build.js +105 -74
  369. package/src/client-builder/client-bundle.js +347 -0
  370. package/src/client-builder/client-dev-server.js +3 -3
  371. package/src/client-builder/client-icons.js +2 -2
  372. package/src/client-builder/ssr.js +3 -3
  373. package/src/client.build.js +3 -6
  374. package/src/client.dev.js +3 -6
  375. package/src/db/DataBaseProvider.js +186 -3
  376. package/src/db/mariadb/MariaDB.js +3 -2
  377. package/src/db/mongo/MongoBootstrap.js +273 -101
  378. package/src/db/mongo/MongoExpress.js +187 -0
  379. package/src/db/mongo/MongooseDB.js +7 -8
  380. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  381. package/src/grpc/cyberia/grpc-server.js +6 -8
  382. package/src/index.js +79 -23
  383. package/src/mailer/EmailRender.js +2 -2
  384. package/src/mailer/MailerInterceptor.js +87 -0
  385. package/src/mailer/MailerProvider.js +50 -6
  386. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +144 -147
  387. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  388. package/src/projects/cyberia/besu-genesis-generator.js +8 -17
  389. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  390. package/src/projects/cyberia/catalog-cyberia.js +54 -10
  391. package/src/projects/cyberia/gemini-client.js +4 -4
  392. package/src/projects/cyberia/generate-saga.js +5 -12
  393. package/src/projects/cyberia/hot-reload-trigger.js +18 -29
  394. package/src/projects/cyberia/instance-backup.js +210 -0
  395. package/src/projects/cyberia/instance-data.js +178 -258
  396. package/src/projects/cyberia/ipfs-client.js +4 -4
  397. package/src/projects/cyberia/map-preview-generator.js +33 -69
  398. package/src/projects/cyberia/object-layer.js +153 -320
  399. package/src/projects/cyberia/seed-audio.js +139 -0
  400. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  401. package/src/projects/cyberia/semantic-layer-generator.js +3 -19
  402. package/src/projects/cyberia/stat-balance.js +81 -0
  403. package/src/projects/cyberia/stat-commands.js +23 -0
  404. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  405. package/src/projects/underpost/catalog-underpost.js +9 -1
  406. package/src/proxy.js +4 -7
  407. package/src/runtime/cyberia-client/Dockerfile +17 -8
  408. package/src/runtime/cyberia-client/Dockerfile.dev +13 -4
  409. package/src/runtime/cyberia-server/Dockerfile +18 -6
  410. package/src/runtime/cyberia-server/Dockerfile.dev +16 -3
  411. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  412. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  413. package/src/runtime/engine-cyberia/Dockerfile.test +5 -5
  414. package/src/runtime/engine-cyberia/compose.env +17 -5
  415. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  416. package/src/runtime/express/Express.js +32 -27
  417. package/src/runtime/lampp/Lampp.js +22 -70
  418. package/src/runtime/nginx/Nginx.js +1 -1
  419. package/src/runtime/wp/Wp.js +16 -48
  420. package/src/server/{catalog.js → build/catalog.js} +15 -5
  421. package/src/server/build/coverage.js +188 -0
  422. package/src/server/build/execution.js +402 -0
  423. package/src/server/build/package.js +508 -0
  424. package/src/server/build/testing.js +578 -0
  425. package/src/server/{dns.js → network/dns.js} +144 -11
  426. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  427. package/src/server/{middlewares.js → network/middlewares.js} +11 -5
  428. package/src/server/network/node-capability.js +98 -0
  429. package/src/server/{peer.js → network/peer.js} +3 -3
  430. package/src/server/{proxy.js → network/proxy.js} +7 -13
  431. package/src/server/network/router.js +356 -0
  432. package/src/server/{tls.js → network/tls.js} +2 -2
  433. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  434. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  435. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  436. package/src/server/{cri.js → ops/cri.js} +2 -2
  437. package/src/server/{cron.js → ops/cron.js} +349 -74
  438. package/src/server/ops/event-notification.js +284 -0
  439. package/src/server/{logger.js → ops/logger.js} +143 -23
  440. package/src/server/ops/monitoring.js +1724 -0
  441. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  442. package/src/server/{conf.js → runtime/conf.js} +516 -423
  443. package/src/server/runtime/config-scope.js +215 -0
  444. package/src/server/runtime/environment.js +149 -0
  445. package/src/server/{process.js → runtime/process.js} +153 -12
  446. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  447. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  448. package/src/server/{start.js → runtime/start.js} +128 -39
  449. package/src/server/{auth.js → security/auth.js} +7 -8
  450. package/src/server/security/container-storage.js +382 -0
  451. package/src/server/{crypto.js → security/crypto.js} +1 -1
  452. package/src/server/{selinux.js → security/selinux.js} +1 -1
  453. package/src/server/{backup.js → storage/backup.js} +17 -8
  454. package/src/server/{data-query.js → storage/data-query.js} +17 -1
  455. package/src/server/storage/downloader.js +121 -0
  456. package/src/server/storage/repository.js +68 -0
  457. package/src/server/storage/zip.js +153 -0
  458. package/src/server.js +4 -7
  459. package/src/ws/IoInterface.js +1 -1
  460. package/src/ws/IoServer.js +1 -1
  461. package/src/ws/core/core.ws.connection.js +1 -1
  462. package/src/ws/core/core.ws.emit.js +1 -1
  463. package/src/ws/core/core.ws.server.js +1 -1
  464. package/src/ws/default/default.ws.connection.js +1 -1
  465. package/src/ws/default/default.ws.emit.js +1 -1
  466. package/src/ws/default/default.ws.server.js +1 -1
  467. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  468. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  469. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  470. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  471. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  472. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  473. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +64 -0
  474. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  475. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  476. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +17 -23
  477. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  478. package/test/integration/app/cyberia/cyberia-load.test.js +466 -0
  479. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  480. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  481. package/test/integration/app/cyberia/fallback-world-capture.test.js +338 -0
  482. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  483. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  484. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  485. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  486. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  487. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  488. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  489. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  490. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  491. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  492. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  493. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  494. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  495. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  496. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  497. package/test/integration/infra/2-network/wireguard-cli.test.js +1187 -0
  498. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +649 -55
  499. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  500. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  501. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  502. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  503. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  504. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  505. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  506. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  507. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  508. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  509. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  510. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  511. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  512. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  513. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  514. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  515. package/test/support/shell-harness.js +73 -0
  516. package/test/unit/build-template.test.js +97 -0
  517. package/test/unit/build-workflow-offline.test.js +132 -0
  518. package/test/unit/catalog.test.js +71 -0
  519. package/test/unit/client-build-docs.test.js +361 -0
  520. package/test/unit/client-bundle.test.js +313 -0
  521. package/test/unit/conf-loading.test.js +303 -0
  522. package/test/unit/conf-resolution.test.js +996 -0
  523. package/test/unit/coverage-artifact.test.js +221 -0
  524. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  525. package/test/unit/cyberia/instance-backup.test.js +183 -0
  526. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  527. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  528. package/test/unit/cyberia/stat-balance.test.js +69 -0
  529. package/test/unit/deploy-env-resolution.test.js +38 -0
  530. package/test/unit/deploy-log-table.test.js +518 -0
  531. package/test/unit/downloader-integrity.test.js +98 -0
  532. package/test/unit/execution-profiles.test.js +219 -0
  533. package/test/unit/file-reference-registry.test.js +141 -0
  534. package/test/unit/fleet-sync-source.test.js +149 -0
  535. package/test/unit/fs-storage-paths.test.js +596 -0
  536. package/test/unit/logger-redaction.test.js +141 -0
  537. package/test/unit/package.test.js +457 -0
  538. package/test/unit/prepare-host.test.js +382 -0
  539. package/test/unit/process-environment.test.js +18 -0
  540. package/test/unit/propagation-message.test.js +78 -0
  541. package/test/unit/redeploy-plan.test.js +24 -0
  542. package/test/unit/release-bump.test.js +474 -0
  543. package/test/unit/ssh-output-redaction.test.js +28 -0
  544. package/test/unit/start-options.test.js +46 -0
  545. package/test/unit/test-tiers.test.js +220 -0
  546. package/test/unit/zip-archive.test.js +142 -0
  547. package/vitest.config.js +66 -0
  548. package/.nycrc +0 -9
  549. package/deploy/lib/logging.sh +0 -96
  550. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  551. package/manifests/mongodb/pv-pvc.yaml +0 -59
  552. package/scripts/coverall-test-cyberia.sh +0 -24
  553. package/scripts/coverall-test.sh +0 -24
  554. package/scripts/link-local-underpost-cli.sh +0 -6
  555. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  556. package/src/api/user/user.build.js +0 -16
  557. package/src/cli/env.js +0 -177
  558. package/src/client/components/core/Blockchain.js +0 -41
  559. package/src/client/components/core/Blog.js +0 -9
  560. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  561. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
  562. package/src/client/public/default/assets/background/dark.jpg +0 -0
  563. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  564. package/src/client/public/default/assets/background/white0.jpg +0 -0
  565. package/src/client/public/doc/favicon.ico +0 -0
  566. package/src/client/public/doc/sitemap +0 -148
  567. package/src/server/downloader.js +0 -73
  568. package/src/server/environment.js +0 -98
package/src/cli/run.js CHANGED
@@ -4,10 +4,12 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, pbcopy, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, pbcopy, shellArgumentFactory, shellCd, shellExec } from '../server/runtime/process.js';
8
+ import { cli, withExecutionProfile } from '../server/build/execution.js';
9
+ import { syncDeployPackages } from '../server/build/package.js';
10
+ import { RUNTIME_STATUS } from '../server/runtime/runtime-status.js';
8
11
  import {
9
12
  awaitDeployMonitor,
10
- buildKindPorts,
11
13
  clusterContextFactory,
12
14
  clusterTypeFactory,
13
15
  dispatchBuildInstanceEnv,
@@ -22,41 +24,46 @@ import {
22
24
  generateSecurePassword,
23
25
  instanceHttpRouteRulesFactory,
24
26
  instanceInterceptStatusesFactory,
27
+ instanceEnvFilePath,
25
28
  instancePortFactory,
26
29
  instanceProxyRoutesFactory,
27
30
  instanceStatusPageEntriesFactory,
28
- isDeployRunnerContext,
31
+ instanceProjectPathFactory,
29
32
  loadConfInstances,
30
33
  loadProjectInstanceEnvBuilder,
31
34
  loadConfServerJson,
32
- loadReplicas,
33
- resolveDeployList,
34
35
  resolveEnvScoped,
35
36
  selectConfInstances,
36
37
  waitForPort,
37
38
  clusterInstancesFactory,
38
39
  deployTrafficEntriesFactory,
39
- curlStatusChainFactory,
40
+ publicIngressProbeFactory,
40
41
  hostIngressFactsFactory,
41
42
  hostRenderInstancesFactory,
42
43
  instanceTrafficPlanFactory,
43
44
  trafficTableRowsFactory,
44
45
  isTrafficServingFactory,
45
46
  nextTrafficFactory,
47
+ redeployPlanFactory,
46
48
  stopPlanFactory,
47
49
  trafficFromRoutingInfoFactory,
48
- } from '../server/conf.js';
49
- import { cronDeployIdResolve } from '../server/cron.js';
50
- import { getNpmRootPath, writeEnv } from '../server/environment.js';
51
- import { actionInitLog, loggerFactory } from '../server/logger.js';
50
+ } from '../server/runtime/conf.js';
51
+ import { buildKindPorts, deployRoutesExists, readDeployRoutes, resolveDeployList } from '../server/network/router.js';
52
+ import { clientBundleHostFilter, pullClientBundle, pushClientBundle } from '../client-builder/client-bundle.js';
53
+ import { cronDeployIdResolve } from '../server/ops/cron.js';
54
+ import { deployEnvFactory, getNpmRootPath, writeEnv } from '../server/runtime/environment.js';
55
+ import { actionInitLog, loggerFactory } from '../server/ops/logger.js';
52
56
 
53
57
  import fs from 'fs-extra';
54
58
  import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
55
59
 
56
60
  import os from 'os';
61
+ import nodePath from 'node:path';
62
+ import { domainContextFactory } from './domains.js';
57
63
  import Underpost from '../index.js';
58
64
  import dotenv from 'dotenv';
59
65
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
66
+ import { MongoExpress } from '../db/mongo/MongoExpress.js';
60
67
  import {
61
68
  UNDERPOST_GATEWAY,
62
69
  assertStaticAssets,
@@ -69,7 +76,7 @@ import {
69
76
  readHostInstanceRegistry,
70
77
  writeHostInstanceRegistry,
71
78
  writeHostServerConf,
72
- } from '../server/underpost-gateway.js';
79
+ } from '../server/network/underpost-gateway.js';
73
80
  const logger = loggerFactory(import.meta);
74
81
 
75
82
  /**
@@ -89,6 +96,9 @@ const logger = loggerFactory(import.meta);
89
96
  * @property {string} volumeHostPath - The host path for the volume.
90
97
  * @property {string} volumeMountPath - The mount path for the volume.
91
98
  * @property {string} imageName - The name of the image to run.
99
+ * @property {string} runtimeImage - `src/runtime/<name>` image family the cluster runner brings up (`cluster`).
100
+ * @property {string} image - Container image the deployment pulls and runs (`sync`).
101
+ * @property {string} versions - Comma-separated blue/green deployment versions (`sync`).
92
102
  * @property {string} containerName - The name of the container to run.
93
103
  * @property {string} namespace - The namespace to run in.
94
104
  * @property {string} timeoutResponse - The response timeout duration.
@@ -132,11 +142,13 @@ const logger = loggerFactory(import.meta);
132
142
  * @property {boolean} kind - Whether to run in kind mode.
133
143
  * @property {boolean} k3s - Whether to run in k3s mode.
134
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.
135
146
  * @property {string} deployId - The deployment ID.
136
147
  * @property {string} instanceId - The instance ID.
137
148
  * @property {string} user - The user to run as.
138
149
  * @property {string} group - The group to use.
139
150
  * @property {string} pid - The process ID.
151
+ * @property {string} repoEnginePrivate - Private configuration repository the pull runner checks out, overriding the pairing derived from the engine source.
140
152
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
141
153
  * @property {string} monitorStatus - The monitor status option.
142
154
  * @property {string} monitorStatusKindType - The monitor status kind type option.
@@ -183,6 +195,9 @@ const DEFAULT_OPTION = {
183
195
  volumeHostPath: '',
184
196
  volumeMountPath: '',
185
197
  imageName: '',
198
+ image: '',
199
+ runtimeImage: '',
200
+ versions: '',
186
201
  containerName: '',
187
202
  namespace: 'default',
188
203
  timeoutResponse: '',
@@ -226,11 +241,13 @@ const DEFAULT_OPTION = {
226
241
  kind: false,
227
242
  k3s: false,
228
243
  hosts: '',
244
+ split: '',
229
245
  deployId: '',
230
246
  instanceId: '',
231
247
  user: '',
232
248
  group: '',
233
249
  pid: '',
250
+ repoEnginePrivate: '',
234
251
  disablePrivateConfUpdate: false,
235
252
  monitorStatus: '',
236
253
  monitorStatusKindType: '',
@@ -271,33 +288,6 @@ const DEFAULT_OPTION = {
271
288
  * @memberof UnderpostRun
272
289
  */
273
290
 
274
- // Secrets `sops-setup` onboards when no explicit list is passed: the full self-hosted data tier.
275
- // `mongodb-keyfile` is listed alongside `mongodb-secret` because the MongoDB StatefulSet mounts
276
- // it as a volume for intra-replica-set auth and will not start without it, so onboarding the
277
- // credentials alone would leave Mongo broken.
278
- const SOPS_SETUP_DEFAULT_SECRETS = ['postgres-secret', 'mariadb-secret', 'mongodb-secret', 'mongodb-keyfile'];
279
-
280
- /**
281
- * Produces a value for a Secret data key that has no origin seed file and no `--args` override.
282
- * Key-aware because the data tier does not want one shape of secret: a replica-set keyfile is a
283
- * long base64 blob, a username is an identifier, and everything else is a password.
284
- * @param {string} key - Secret data key (e.g. 'password', 'username', 'mongodb-keyfile').
285
- * @returns {string} Generated value.
286
- * @memberof UnderpostRun
287
- */
288
- const generateSeedValue = (key) => {
289
- if (key === 'username') return 'admin';
290
- // MongoDB keyfile: 6-1024 base64 characters shared by every replica-set member. Newlines are
291
- // stripped so the value round-trips identically through YAML and through
292
- // MongoBootstrap.readCredential, which strips them too.
293
- if (key === 'mongodb-keyfile')
294
- return shellExec(`openssl rand -base64 756`, { stdout: true, silent: true, disableLog: true }).replace(
295
- /\r?\n/g,
296
- '',
297
- );
298
- return generateSecurePassword(24);
299
- };
300
-
301
291
  class UnderpostRun {
302
292
  /**
303
293
  * @static
@@ -326,13 +316,12 @@ class UnderpostRun {
326
316
  throw new Error(`Invalid Kubernetes node name: ${options.nodeName}`);
327
317
  const env = options.dev ? 'development' : 'production';
328
318
  const requestedDeploys = `${path || options.deployId || ''}`.trim();
329
- const routerPath = './engine-private/deploy/dd.router';
330
319
  const confRoot = './engine-private/conf';
331
320
  const deployIds = [
332
321
  ...new Set(
333
322
  requestedDeploys
334
323
  ? resolveDeployList(requestedDeploys)
335
- : fs.existsSync(routerPath)
324
+ : deployRoutesExists()
336
325
  ? resolveDeployList('dd')
337
326
  : fs.existsSync(confRoot)
338
327
  ? fs
@@ -488,7 +477,7 @@ class UnderpostRun {
488
477
  * @memberof UnderpostRun
489
478
  */
490
479
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
491
- const baseCommand = options.dev ? 'node bin' : 'underpost';
480
+ const baseCommand = cli('underpost', { local: true });
492
481
  const mongoHosts = ['mongodb-0.mongodb-service'];
493
482
  let primaryMongoHost = 'mongodb-0.mongodb-service';
494
483
  const clusterType = clusterTypeFactory(options);
@@ -826,52 +815,21 @@ class UnderpostRun {
826
815
  return results;
827
816
  },
828
817
 
829
- /**
830
- * @method dev-hosts-expose
831
- * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
832
- * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
833
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
834
- * @memberof UnderpostRun
835
- */
836
- 'dev-hosts-expose': async (path, options = DEFAULT_OPTION) => {
837
- shellExec(`node bin deploy ${path} development --disable-update-deployment --disable-update-proxy --kubeadm`);
838
- // /etc/hosts is written here, not by `deploy`: that command has no
839
- // --etc-hosts option, and the `etc-hosts` runner already resolves a
840
- // deploy's hosts from its conf.server.json.
841
- await UnderpostRun.RUNNERS['etc-hosts']('', { ...options, deployId: path });
842
- },
843
-
844
- /**
845
- * @method dev-hosts-restore
846
- * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
847
- * @param {string} path - The input value, identifier, or path for the operation.
848
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
849
- * @memberof UnderpostRun
850
- */
851
- 'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
852
- // Rewrite /etc/hosts with the loopback block alone, dropping the deploy
853
- // host entries `dev-hosts-expose` (and the `cluster` runner) added.
854
- const hostListenResult = etcHostFactory([]);
855
- logger.info(hostListenResult.renderHosts);
856
- },
857
-
858
818
  /**
859
819
  * @method cluster-build
860
- * @description Build configuration for cluster deployment.
861
- * @param {string} path - The input value, identifier, or path for the operation.
820
+ * @description Resets the workspace and regenerates every routed deploy's default
821
+ * configuration, optionally committing the result to both repositories.
822
+ *
823
+ * Single-replica materialization is not done here: `deploy --sync` builds those folders
824
+ * as part of the deploy itself (`getDataDeploy({ buildSingleReplica: true })`).
825
+ * @param {string} path - `cmt` to commit the regenerated configuration; otherwise unused.
862
826
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
863
827
  * @memberof UnderpostRun
864
828
  */
865
829
  'cluster-build': (path, options = DEFAULT_OPTION) => {
866
- const nodeOptions =
867
- (options.nodeName ? ` --node-name ${options.nodeName}` : '') +
868
- (options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
869
830
  shellExec(`node bin run clean`);
870
- shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
871
- shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
872
- shellExec(`node bin env clean`);
873
- for (const deployId of fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').split(','))
874
- shellExec(`node bin new --default-conf --deploy-id ${deployId.trim()}`);
831
+ shellExec(`node bin app clean`);
832
+ for (const deployId of readDeployRoutes()) shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
875
833
  if (path === 'cmt') {
876
834
  shellExec(`git add . && underpost cmt . build cluster-build`);
877
835
  shellExec(`cd engine-private && git add . && underpost cmt . build cluster-build`);
@@ -887,14 +845,14 @@ class UnderpostRun {
887
845
  * @memberof UnderpostRun
888
846
  */
889
847
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
890
- const baseCommand = options.dev ? 'node bin' : 'underpost';
848
+ const baseCommand = cli('underpost', { local: true });
891
849
  shellExec(`npm run security:secrets`);
892
850
  const reportPath = './gitleaks-report.json';
893
851
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
894
852
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
895
853
  return;
896
854
  }
897
- shellExec(`${baseCommand} run pull`);
855
+ Underpost.repo.fastForwardEnginePair(baseCommand);
898
856
  shellExec(`${baseCommand} run shared-dir`);
899
857
 
900
858
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
@@ -964,14 +922,14 @@ class UnderpostRun {
964
922
  * @memberof UnderpostRun
965
923
  */
966
924
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
967
- const baseCommand = options.dev ? 'node bin' : 'underpost';
925
+ const baseCommand = cli('underpost', { local: true });
968
926
  shellExec(`npm run security:secrets`);
969
927
  const reportPath = './gitleaks-report.json';
970
928
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
971
929
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
972
930
  return;
973
931
  }
974
- shellExec(`${baseCommand} run pull`);
932
+ Underpost.repo.fastForwardEnginePair(baseCommand);
975
933
  shellExec(`${baseCommand} run shared-dir`);
976
934
 
977
935
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
@@ -1015,47 +973,51 @@ class UnderpostRun {
1015
973
  },
1016
974
  /**
1017
975
  * @method pull
1018
- * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
1019
- * @param {string} path - The input value, identifier, or path for the operation.
976
+ * @description Brings the node's engine checkout and its private configuration to the tip of
977
+ * their repositories, at `/home/dd/engine` and `/home/dd/engine/engine-private`.
978
+ *
979
+ * With no argument this is the monorepo pair, `engine` and `engine-private`. Naming a source
980
+ * repository deploys that source instead, and its private conf repository is derived from the
981
+ * conf id they share rather than passed separately — `underpostnet/engine-test-lampp` pairs
982
+ * with `underpostnet/engine-lampp-private`, as does `underpostnet/engine-lampp`.
983
+ * `--repo-engine-private` overrides that derivation for a conf repository the pairing cannot
984
+ * name, and is the flag `deploy/lib/host.sh` forwards `ENGINE_SRC_PRIVATE_REPO` through.
985
+ *
986
+ * Both checkouts are replaced rather than merged, by the same `--switch-repo` route the fleet
987
+ * sync takes — so a node moves onto a test source repo and back without being reprovisioned,
988
+ * and one that has drifted onto commits of its own is brought back instead of refused.
989
+ *
990
+ * The replacement checkout is installed before this returns: it is a different commit than
991
+ * the one the caller started from, and nothing that follows can run against uninstalled
992
+ * dependencies.
993
+ * @param {string} path - Engine source repository, as `owner/repo` or a clone URL. Defaults to
994
+ * the `GITHUB_USERNAME` monorepo.
1020
995
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1021
996
  * @memberof UnderpostRun
1022
997
  */
1023
- pull: (path, options = DEFAULT_OPTION) => {
998
+ pull: (path = '', options = DEFAULT_OPTION) => {
1024
999
  // shellExec is fail-fast by default — any non-zero exit throws and
1025
1000
  // propagates up to the workflow step. No per-call flag required.
1026
- if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
1027
- fs.mkdirSync(`/home/dd`, { recursive: true });
1028
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1029
- } else {
1030
- shellExec(`underpost run clean`);
1031
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1032
- }
1033
- if (!fs.existsSync(`/home/dd/engine/engine-private`))
1034
- shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
1035
- silent: true,
1036
- });
1037
- else
1038
- shellExec(
1039
- `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
1040
- { silent: true },
1041
- );
1042
- },
1043
- /**
1044
- * @method release-deploy
1045
- * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
1046
- * @param {string} path - The input value, identifier, or path for the operation.
1047
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1048
- * @memberof UnderpostRun
1049
- */
1050
- 'release-deploy': (path, options = DEFAULT_OPTION) => {
1051
- actionInitLog();
1052
- shellExec(`underpost --version`);
1053
- shellCd(`/home/dd/engine`);
1054
- for (const _deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1055
- const deployId = _deployId.trim();
1056
- shellExec(`underpost run deploy ${deployId}`, { async: true });
1057
- }
1001
+ const { engine: source, enginePrivate: privateRepo } = Underpost.repo.enginePairFactory({
1002
+ engine: path,
1003
+ enginePrivate: options.repoEnginePrivate,
1004
+ });
1005
+ logger.info('Pulling engine source', { source, privateRepo });
1006
+
1007
+ const engineRoot = `/home/dd/engine`;
1008
+ // Cleaning is the in-process API rather than a shell round trip through the
1009
+ // global `underpost`: that bin is linked at the very checkout being replaced,
1010
+ // so a tree left without its dependencies made the pre-clean — and with it the
1011
+ // whole pull — exit non-zero on exactly the node that needed replacing.
1012
+ if (fs.existsSync(engineRoot)) Underpost.repo.clean({ paths: [engineRoot, `${engineRoot}/engine-private`] });
1013
+ Underpost.repo.syncCheckout({ path: engineRoot, repo: source });
1014
+ Underpost.repo.syncCheckout({ path: `${engineRoot}/engine-private`, repo: privateRepo });
1015
+ // The checkout that lands here is a different commit than the one this process
1016
+ // started from, and its package.json is only a description until it is installed.
1017
+ // Everything after this — the linked global bin included — runs from this tree.
1018
+ shellExec(`cd ${engineRoot} && npm install`);
1058
1019
  },
1020
+
1059
1021
  /**
1060
1022
  * @method ssh-deploy
1061
1023
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
@@ -1125,36 +1087,35 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1125
1087
  },
1126
1088
  /**
1127
1089
  * @method sync
1128
- * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`) using parameters parsed from `path` (deployId, replicas, versions, image, node).
1090
+ * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`).
1091
+ *
1092
+ * Every input is a flag: `--deploy-id`, `--replicas`, `--versions`, `--image`, `--node-name`.
1129
1093
  *
1130
1094
  * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
1131
1095
  * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
1132
1096
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
1133
- * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
1134
- * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
1097
+ * `node bin run sync --deploy-id dd-core --kubeadm --image-pull-policy Always`
1098
+ *
1099
+ * Runs in one of two modes. Given `--deploy-id`, it deploys. Passed `--build`, or no
1100
+ * `--deploy-id` at all, it renders the manifests and the router table and stops before
1101
+ * touching the cluster:
1102
+ * `node bin run --dev --build sync --deploy-id dd-default`
1103
+ * @param {string} [path] - Unused; every input is a flag.
1135
1104
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1136
1105
  * @memberof UnderpostRun
1137
1106
  */
1138
1107
  sync: async (path, options = DEFAULT_OPTION) => {
1139
- // Dev usage: node bin run --dev --build sync dd-default
1140
- const env = options.dev ? 'development' : 'production';
1108
+ const env = deployEnvFactory(options);
1141
1109
  options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
1142
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
1110
+ const baseCommand = cli('underpost', { local: true });
1143
1111
  const baseClusterCommand = options.dev ? ' --dev' : '';
1144
1112
  const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
1145
- const defaultPath = [
1146
- 'dd-default',
1147
- options.replicas,
1148
- ``,
1149
- ``,
1150
- !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
1151
- ];
1152
- let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
1153
- deployId = deployId ? deployId : defaultPath[0];
1154
- replicas = replicas ? replicas : defaultPath[1];
1155
- versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
1156
- image = image ? image : defaultPath[3];
1157
- node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
1113
+ const deploying = !options.build && !!options.deployId;
1114
+ const deployId = options.deployId || 'dd-default';
1115
+ const replicas = options.replicas || 1;
1116
+ const image = options.image || '';
1117
+ const node = options.nodeName || (!options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname());
1118
+ let versions = options.versions || '';
1158
1119
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
1159
1120
 
1160
1121
  if (image && !image.startsWith('localhost'))
@@ -1165,7 +1126,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1165
1126
  k3s: options.k3s,
1166
1127
  });
1167
1128
 
1168
- if (isDeployRunnerContext(path, options)) {
1129
+ if (deploying) {
1169
1130
  if (!options.disablePrivateConfUpdate) {
1170
1131
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
1171
1132
  if (!validVersion) throw new Error('Version mismatch');
@@ -1183,7 +1144,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1183
1144
  }
1184
1145
  }
1185
1146
 
1186
- const currentTraffic = isDeployRunnerContext(path, options)
1147
+ const currentTraffic = deploying
1187
1148
  ? Underpost.deploy.getCurrentTraffic(deployId, {
1188
1149
  namespace: options.namespace,
1189
1150
  env,
@@ -1207,14 +1168,12 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1207
1168
  });
1208
1169
 
1209
1170
  const ignorePods =
1210
- isDeployRunnerContext(path, options) && targetTraffic
1171
+ deploying && targetTraffic
1211
1172
  ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
1212
1173
  : [];
1213
1174
 
1214
1175
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
1215
- const cmdString = options.cmd
1216
- ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
1217
- : '';
1176
+ const cmdString = options.cmd ? ` --cmd ${shellArgumentFactory(options.cmd)}` : '';
1218
1177
  const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
1219
1178
 
1220
1179
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
@@ -1223,11 +1182,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1223
1182
  const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
1224
1183
  const gatewayApiFlags = Underpost.deploy.gatewayApiFlagsFactory(options);
1225
1184
 
1185
+ // Retarget `underpost-config` at this sync's environment before anything is
1186
+ // deployed. `underpost host load` reads it back inside the
1187
+ // pod, so a stale Secret makes the container resolve the wrong NODE_ENV.
1188
+ if (deploying) {
1189
+ Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
1190
+ Underpost.app.apply(
1191
+ domainContextFactory({ env, namespace: options.namespace, args: { 'deploy-id': deployId } }),
1192
+ );
1193
+ }
1194
+
1226
1195
  // A direct sync owns the same gateway-first contract as the full cluster
1227
1196
  // runner. Build the host-side SSR documents before generating routes unless
1228
- // the caller explicitly selected a pre-built bundle workflow.
1229
- if (isDeployRunnerContext(path, options) && !options.skipFullBuild)
1230
- shellExec(`${baseCommand} client ${deployId} ${env} --ssr`);
1197
+ // the caller explicitly selected a pre-built bundle workflow. `--env` is
1198
+ // explicit: the SSR pass must render for the target environment, never for
1199
+ // whatever NODE_ENV the calling shell happens to carry.
1200
+ if (deploying && !options.skipFullBuild) shellExec(`${baseCommand} client ${deployId} --env ${env} --ssr`);
1231
1201
 
1232
1202
  shellExec(
1233
1203
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
@@ -1237,7 +1207,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1237
1207
  }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag}${gatewayApiFlags} ${deployId} ${env}`,
1238
1208
  );
1239
1209
 
1240
- if (isDeployRunnerContext(path, options)) {
1210
+ if (deploying) {
1241
1211
  if (options.gatewayApi) {
1242
1212
  const namespace = options.namespace || 'default';
1243
1213
  const gatewayRoot = Underpost.deploy.underpostGatewayRootFactory(options);
@@ -1308,6 +1278,48 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1308
1278
  );
1309
1279
  },
1310
1280
 
1281
+ /**
1282
+ * @method net-tables
1283
+ * @description Prints the host's network tables: interfaces and links (`ip -br link show`) with state, MAC address, and flags, followed by IP addresses (`ip -br addr show`) with IPv4/IPv6, each aligned via `column -t`.
1284
+ * @param {string} path - The input value, identifier, or path for the operation (unused).
1285
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow (unused).
1286
+ * @memberof UnderpostRun
1287
+ */
1288
+ 'net-tables': (path, options = DEFAULT_OPTION) => {
1289
+ logger.info(`INTERFACES & LINKS`);
1290
+ shellExec(`(echo "INTERFACE STATE MAC FLAGS"; ip -br link show) | column -t`);
1291
+ logger.info(`IP ADDRESSES`);
1292
+ shellExec(`(echo "INTERFACE STATE IPV4 IPV6"; ip -br addr show) | column -t`);
1293
+ logger.info('KERNEL ROUTING TABLE');
1294
+ shellExec(`(echo "DESTINATION GATEWAY INTERFACE PROTO SCOPE METRIC"; \
1295
+ ip route show | awk '{
1296
+ dst = $1; gw = "on-link"; dev = "-"; proto = "-"; scope = "-"; metric = "-";
1297
+ for (i=1; i<=NF; i++) {
1298
+ if ($i == "via") gw = $(i+1);
1299
+ if ($i == "dev") dev = $(i+1);
1300
+ if ($i == "proto") proto = $(i+1);
1301
+ if ($i == "scope") scope = $(i+1);
1302
+ if ($i == "metric") metric = $(i+1);
1303
+ }
1304
+ print dst, gw, dev, proto, scope, metric;
1305
+ }') | column -t`);
1306
+ logger.info(`ACTIVE NAT FLOWS`);
1307
+ shellExec(`(echo "CHAIN TYPE TARGET_IP_PORT PACKETS BYTES"; \
1308
+ sudo nft list table ip nat | awk '
1309
+ /chain (PREROUTING|POSTROUTING)/ {chain=$2}
1310
+ /dnat to|masquerade|snat to/ && !/packets 0/ {
1311
+ type = ($0 ~ "dnat") ? "DNAT" : (($0 ~ "masquerade") ? "MASQUERADE" : "SNAT");
1312
+ target = "N/A";
1313
+ pkts = "0"; bytes = "0";
1314
+ for(i=1; i<=NF; i++) {
1315
+ if ($i == "packets") pkts = $(i+1);
1316
+ if ($i == "bytes") bytes = $(i+1);
1317
+ if ($i == "to") target = $(i+1);
1318
+ }
1319
+ print chain, type, target, pkts, bytes;
1320
+ }') | column -t`);
1321
+ },
1322
+
1311
1323
  /**
1312
1324
  * @method stop
1313
1325
  * @description Deletes colour-suffixed Deployments and their Services, leaving routing untouched.
@@ -1419,8 +1431,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1419
1431
  ? options.timezone
1420
1432
  : path
1421
1433
  ? path
1422
- : Underpost.env.get('TIME_ZONE', undefined, { disableLog: true })
1423
- ? Underpost.env.get('TIME_ZONE')
1434
+ : Underpost.host.store.get('TIME_ZONE', undefined, { disableLog: true })
1435
+ ? Underpost.host.store.get('TIME_ZONE')
1424
1436
  : process.env.TIME_ZONE
1425
1437
  ? process.env.TIME_ZONE
1426
1438
  : 'America/New_York';
@@ -1563,7 +1575,6 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1563
1575
  // Probe the exact public URL represented by each host/path row. PWA rows
1564
1576
  // can carry several configured paths in one cell, while instance rows
1565
1577
  // carry one; cache by URL so shared rows never repeat network work.
1566
- const shellArg = (value) => `'${`${value}`.replaceAll("'", "'\\''")}'`;
1567
1578
  const probeCache = new Map();
1568
1579
  const probePath = (host, routePath, tls) => {
1569
1580
  const normalizedPath = `${routePath || '/'}`.startsWith('/') ? `${routePath || '/'}` : `/${routePath}`;
@@ -1573,18 +1584,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1573
1584
  } catch {
1574
1585
  return { path: normalizedPath, url: '', statuses: ['000'] };
1575
1586
  }
1576
- if (!probeCache.has(url)) {
1577
- // -L follows the real redirect chain, -v supplies one response line
1578
- // per hop, -i keeps full response headers available, and -s removes
1579
- // only the progress meter. The body is discarded to keep a status
1580
- // report bounded even when a host returns a large application page.
1581
- const raw = shellExec(
1582
- `curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
1583
- `-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${shellArg(url)} 2>&1 || true`,
1584
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
1585
- );
1586
- probeCache.set(url, curlStatusChainFactory(raw));
1587
- }
1587
+ // Cache by URL so rows that share one never repeat network work.
1588
+ if (!probeCache.has(url)) probeCache.set(url, publicIngressProbeFactory(url));
1588
1589
  return { path: normalizedPath, url, statuses: probeCache.get(url) };
1589
1590
  };
1590
1591
 
@@ -1625,7 +1626,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1625
1626
 
1626
1627
  // Padded on the raw values, coloured afterwards: an ANSI escape counts
1627
1628
  // toward String.length and would skew every column right of it.
1628
- const columns = ['HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1629
+ const columns = ['#', 'HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1629
1630
  const deploymentStatus = (status, includeServing = false) => {
1630
1631
  if (!status) return `unrouted - missing${includeServing ? ' not-serving' : ''}`;
1631
1632
  return [
@@ -1635,10 +1636,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1635
1636
  ...(includeServing ? [status.serving ? 'serving' : 'not-serving'] : []),
1636
1637
  ].join(' ');
1637
1638
  };
1638
- const cellOf = (row) => {
1639
+ const cellOf = (row, index) => {
1639
1640
  const facts = ingressFacts[row.host] || {};
1640
1641
  const pathStatus = row.probes.map((probe) => `${probe.path} [${probe.statuses.join('→')}]`).join(' ');
1641
1642
  return [
1643
+ `${index + 1}(${row.probes.length})`,
1642
1644
  row.host,
1643
1645
  pathStatus,
1644
1646
  row.kind,
@@ -1693,7 +1695,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1693
1695
  for (const cell of cells) console.log(line(cell, true));
1694
1696
  };
1695
1697
  for (const env of envs) {
1696
- const envCells = reportRows.filter((row) => row.env === env).map(cellOf);
1698
+ const envRows = reportRows.filter((row) => row.env === env);
1699
+ const envCells = envRows.map((row, index) => cellOf(row, index));
1697
1700
  if (envCells.length > 0) printTable(envCells, `[${env.toUpperCase()}]`);
1698
1701
  }
1699
1702
  console.log('');
@@ -1978,22 +1981,33 @@ EOF
1978
1981
 
1979
1982
  /**
1980
1983
  * @method instance
1981
- * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1984
+ * @description Deploys the custom instances a deploy declares in `conf.instances.json`.
1985
+ *
1986
+ * Every input is a flag: `--deploy-id`, `--instance-id`, `--replicas`, `--node-name`.
1987
+ * `--instance-id` naming a template id selects its whole variant family.
1988
+ * @param {string} path - Unused; every input is a flag.
1982
1989
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1983
1990
  * @memberof UnderpostRun
1984
1991
  */
1985
1992
  instance: async (path = '', options = DEFAULT_OPTION) => {
1986
- const env = options.dev ? 'development' : 'production';
1993
+ const env = deployEnvFactory(options);
1987
1994
  options = {
1988
1995
  ...options,
1989
1996
  gatewayApi: gatewayApiEnabledFactory(options),
1990
1997
  namespace: options.namespace || 'default',
1991
1998
  };
1992
- const baseCommand = options.dev ? 'node bin' : 'underpost';
1999
+ const baseCommand = cli('underpost', { local: true });
1993
2000
  const baseClusterCommand = options.dev ? ' --dev' : '';
1994
- let [deployId, id, replicas] = path.split(',');
1995
- if (!replicas) replicas = options.replicas;
2001
+ const deployId = options.deployId;
2002
+ const id = options.instanceId;
2003
+ const replicas = options.replicas || 1;
2004
+ if (!deployId) throw new Error('The --deploy-id option is required by the instance runner');
2005
+ if (!id) throw new Error('The --instance-id option is required by the instance runner');
1996
2006
  const confInstances = selectConfInstances(loadConfInstances(deployId), id);
2007
+ // Retarget `underpost-config` at this run's environment before any instance
2008
+ // Deployment is submitted: it is what `underpost host load` reads inside the pod,
2009
+ // and a stale one makes the container resolve the wrong NODE_ENV.
2010
+ if (!options.expose) Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
1997
2011
  const { liveTrafficById, targetTrafficById, serving } = instanceTrafficPlanFactory({
1998
2012
  instances: confInstances,
1999
2013
  requestedTraffic: options.traffic,
@@ -2089,7 +2103,6 @@ EOF
2089
2103
  const targetTraffic = targetTrafficById[instance.id];
2090
2104
  const podId = `${_deployId}-${env}-${targetTraffic}`;
2091
2105
  const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
2092
- Underpost.deploy.configMap(env, options.namespace);
2093
2106
  shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
2094
2107
  shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
2095
2108
  for (const _volume of _volumes)
@@ -2219,11 +2232,10 @@ EOF
2219
2232
 
2220
2233
  /**
2221
2234
  * @method deploy-key
2222
- * @description Copies the deploy key for a specific user and deployId to a temporary location on the local machine.
2235
+ * @description Copies the cluster scoped deploy key for a specific user to a temporary location on the local machine.
2223
2236
  * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
2224
2237
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2225
2238
  * @param {string} options.user - The user for which to copy the deploy key.
2226
- * @param {string} options.deployId - The deployment identifier associated with the deploy key.
2227
2239
  * @memberof UnderpostRun
2228
2240
  */
2229
2241
  'deploy-key': (path, options = DEFAULT_OPTION) => {
@@ -2232,13 +2244,18 @@ EOF
2232
2244
  shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
2233
2245
  return;
2234
2246
  }
2235
- if (!options.user || !options.deployId) {
2236
- logger.error('Both --user and --deploy-id options are required to copy the deploy key.');
2247
+ if (!options.user) {
2248
+ logger.error('The --user option is required to copy the deploy key.');
2249
+ return;
2250
+ }
2251
+ const sourcePath = `${Underpost.ssh.userKeyDir(options.user)}/id_rsa`;
2252
+ if (!fs.existsSync(sourcePath)) {
2253
+ logger.error(`No deploy key found at ${sourcePath}; run: node bin ssh --user ${options.user} --user-add`);
2237
2254
  return;
2238
2255
  }
2239
2256
  const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
2240
2257
  fs.mkdirSync('/home/dd/tmp', { recursive: true });
2241
- fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
2258
+ fs.copyFileSync(sourcePath, targetPath);
2242
2259
  logger.info(`Copied deploy key to ${targetPath}`);
2243
2260
  if (options.copy) pbcopy(targetPath);
2244
2261
  },
@@ -2254,19 +2271,25 @@ EOF
2254
2271
  * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
2255
2272
  * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
2256
2273
  *
2257
- * @param {string} path - Comma-separated: `deployId,instanceId[,projectPath]`.
2258
- * `projectPath` is the root directory that contains the `Dockerfile` (e.g. `./cyberia-client`).
2259
- * Artifacts are written to `<projectPath>/manifests/<env>/Dockerfile` and
2260
- * `<projectPath>/manifests/<env>/deployment.yaml`.
2261
- * In production, files are also copied to `<projectPath>/Dockerfile` and
2262
- * `<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`.
2263
2282
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2264
2283
  * @memberof UnderpostRun
2265
2284
  */
2266
2285
  'instance-build-manifest': async (path, options = DEFAULT_OPTION) => {
2267
2286
  const env = options.dev ? 'development' : 'production';
2268
- let [deployId, id, projectPath] = path.split(',');
2269
- 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
+ }
2270
2293
 
2271
2294
  const confInstances = loadConfInstances(deployId);
2272
2295
  // Targeting a template id builds every world in the family. The fan-out
@@ -2284,20 +2307,23 @@ EOF
2284
2307
  }
2285
2308
  if (!options.instanceOnly && (selected.length > 1 || selected[0].id !== id)) {
2286
2309
  for (const instance of selected)
2287
- await UnderpostRun.RUNNERS['instance-build-manifest'](
2288
- [deployId, instance.id, projectPath].filter((v) => v !== undefined).join(','),
2289
- { ...options, instanceOnly: true },
2290
- );
2310
+ await UnderpostRun.RUNNERS['instance-build-manifest']('', {
2311
+ ...options,
2312
+ instanceId: instance.id,
2313
+ instanceOnly: true,
2314
+ });
2291
2315
  return;
2292
2316
  }
2293
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);
2294
2322
  const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
2295
2323
  const outputPath = `${envManifestPath}/deployment.yaml`;
2296
2324
  const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
2297
2325
 
2298
2326
  fs.mkdirpSync(envManifestPath);
2299
-
2300
- const instance = selected[0];
2301
2327
  const isDefaultInstance = instance.id === instance.templateId || !instance.templateId;
2302
2328
  const instanceEnvBuilder = await loadProjectInstanceEnvBuilder(deployId);
2303
2329
 
@@ -2344,14 +2370,15 @@ EOF
2344
2370
  const _fromPort = instancePortFactory({ instance, env });
2345
2371
  const _toPort = instancePortFactory({ instance, env, container: true });
2346
2372
 
2347
- // Build image from projectPath Dockerfile and load into cluster when --build is set.
2348
- 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) {
2349
2376
  const isKind = !options.kubeadm && !options.k3s;
2350
2377
  Underpost.image.build({
2351
- path: projectPath,
2378
+ path: rootPath,
2352
2379
  imageName: _image,
2353
2380
  podmanSave: true,
2354
- imageOutPath: projectPath,
2381
+ imageOutPath: rootPath,
2355
2382
  kind: isKind,
2356
2383
  kubeadm: !!options.kubeadm,
2357
2384
  k3s: !!options.k3s,
@@ -2471,12 +2498,11 @@ EOF
2471
2498
  // a rewrite never points at a document that cannot exist. The check is a
2472
2499
  // read: placing the document into the gateway volume is `deploy
2473
2500
  // --sync-static`'s job at apply time, and a build must not mutate the host.
2474
- // `projectPath` is passed through because this runner is given one
2475
- // explicitly; the sync derives the same root from the instance itself.
2476
- const statusPageEntries = instanceStatusPageEntriesFactory({
2477
- instances: [instance],
2478
- projectPath: rootPath,
2479
- }).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
+ );
2480
2506
 
2481
2507
  // httproute.yaml — this instance's own routes, including the status routes
2482
2508
  // that reach the static utility instead of this workload. No Gateway is
@@ -2569,11 +2595,11 @@ EOF
2569
2595
  // mode this build ran. The default/template instance owns the committed
2570
2596
  // source files, so only its current-mode file is idempotently refreshed.
2571
2597
  if (instance.templateId) {
2572
- const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
2598
+ const instanceEnvDir = nodePath.dirname(instanceEnvFilePath(deployId, _id, env));
2573
2599
  fs.mkdirpSync(instanceEnvDir);
2574
2600
  const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
2575
2601
  for (const targetEnv of envsToWrite) {
2576
- const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
2602
+ const templateEnvPath = instanceEnvFilePath(deployId, instance.templateId, targetEnv);
2577
2603
  if (!fs.existsSync(templateEnvPath))
2578
2604
  throw new Error(`[instance-build-manifest] Missing canonical env file: ${templateEnvPath}`);
2579
2605
  const baseEnv = dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8'));
@@ -2585,7 +2611,7 @@ EOF
2585
2611
  containerDeployId: `${_deployId}-${targetEnv}`,
2586
2612
  builders: instanceEnvBuilder ? { [deployId]: instanceEnvBuilder } : {},
2587
2613
  });
2588
- writeEnv(`${instanceEnvDir}/${targetEnv}.env`, builtEnv);
2614
+ writeEnv(instanceEnvFilePath(deployId, _id, targetEnv), builtEnv);
2589
2615
  }
2590
2616
  logger.info('[instance-build-manifest] Instance env written', {
2591
2617
  dir: instanceEnvDir,
@@ -2667,7 +2693,6 @@ EOF
2667
2693
  * @memberof UnderpostRun
2668
2694
  */
2669
2695
  'host-update': async (path, options = DEFAULT_OPTION) => {
2670
- // const baseCommand = options.dev ? 'node bin' : 'underpost';
2671
2696
  shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
2672
2697
  shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
2673
2698
  },
@@ -2734,7 +2759,7 @@ EOF`);
2734
2759
  * @memberof UnderpostRun
2735
2760
  */
2736
2761
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
2737
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2762
+ const baseCommand = cli('underpost', { local: true });
2738
2763
  const baseClusterCommand = options.dev ? ' --dev' : '';
2739
2764
  const currentImage = options.imageName
2740
2765
  ? options.imageName
@@ -2831,15 +2856,15 @@ EOF`);
2831
2856
  * @memberof UnderpostRun
2832
2857
  */
2833
2858
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
2834
- const defaultUsername = Underpost.env.get('GITHUB_USERNAME');
2835
- const defaultEmail = Underpost.env.get('GITHUB_EMAIL');
2859
+ const defaultUsername = Underpost.host.store.get('GITHUB_USERNAME');
2860
+ const defaultEmail = Underpost.host.store.get('GITHUB_EMAIL');
2836
2861
  const validPath = path && path.split(',').length;
2837
2862
  const [username, email] = validPath ? path.split(',') : [defaultUsername, defaultEmail];
2838
2863
  if (validPath) {
2839
- Underpost.env.set('GITHUB_USERNAME', username);
2840
- Underpost.env.set('GITHUB_EMAIL', email);
2841
- Underpost.env.get('GITHUB_USERNAME');
2842
- Underpost.env.get('GITHUB_EMAIL');
2864
+ Underpost.host.store.set('GITHUB_USERNAME', username);
2865
+ Underpost.host.store.set('GITHUB_EMAIL', email);
2866
+ Underpost.host.store.get('GITHUB_USERNAME');
2867
+ Underpost.host.store.get('GITHUB_EMAIL');
2843
2868
  }
2844
2869
  shellExec(
2845
2870
  `git config --global credential.helper "" && ` +
@@ -2870,7 +2895,7 @@ EOF`);
2870
2895
 
2871
2896
  /**
2872
2897
  * @method promote
2873
- * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
2898
+ * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.routes` for 'dd', or a specific ID).
2874
2899
  * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
2875
2900
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
2876
2901
  * (cert-manager Certificate resources) for each affected deployment.
@@ -2906,7 +2931,7 @@ EOF`);
2906
2931
  };
2907
2932
 
2908
2933
  if (inputDeployId === 'dd') {
2909
- for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
2934
+ for (const deployId of readDeployRoutes()) {
2910
2935
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
2911
2936
  namespace: options.namespace,
2912
2937
  env: inputEnv,
@@ -2932,44 +2957,6 @@ EOF`);
2932
2957
  applyCerts(inputDeployId, targetTraffic);
2933
2958
  }
2934
2959
  },
2935
- /**
2936
- * @method metrics
2937
- * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
2938
- * @param {string} path - The input value, identifier, or path for the operation.
2939
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2940
- * @memberof UnderpostRun
2941
- */
2942
- metrics: async (path, options = DEFAULT_OPTION) => {
2943
- if (path === 'server') {
2944
- shellExec(
2945
- `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2946
- );
2947
- await timer(2000);
2948
-
2949
- shellExec(`kubectl patch deployment metrics-server -n kube-system \
2950
- --type='json' \
2951
- -p='[
2952
- {
2953
- "op":"add",
2954
- "path":"/spec/template/spec/containers/0/args/-",
2955
- "value":"--kubelet-insecure-tls"
2956
- }
2957
- ]'`);
2958
- shellExec(`kubectl scale deployment metrics-server \
2959
- -n kube-system \
2960
- --replicas=1`);
2961
-
2962
- return;
2963
- }
2964
- const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
2965
- let hosts = [];
2966
- for (const deployId of deployList) {
2967
- const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
2968
- hosts = hosts.concat(Object.keys(confServer));
2969
- }
2970
- shellExec(`node bin cluster --prom ${hosts.join(',')}`);
2971
- shellExec(`node bin cluster --grafana`);
2972
- },
2973
2960
  /**
2974
2961
  * @method cluster
2975
2962
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB,
@@ -2982,21 +2969,24 @@ EOF`);
2982
2969
  * references, and every host is written to `/etc/hosts` so the operator's browser reaches the PWA at
2983
2970
  * `https://<host>` on the local machine.
2984
2971
  *
2985
- * Custom instances are the optional third segment of `path`. They are resolved per deploy against that deploy's
2972
+ * Custom instances come from `--instance-id`. They are resolved per deploy against that deploy's
2986
2973
  * own `conf.instances.json`, so an id only runs where its deploy declares it, and each one is deployed after its
2987
2974
  * deploy's default workload is serving — an instance reads the parent's world configuration over the parent's
2988
2975
  * gRPC ClusterIP at boot. Instance hosts share the deploy's environment: the same self-signed certificates and
2989
2976
  * `/etc/hosts` pass in development, the same cert-manager issuance in production.
2990
- * @param {string} path - `<runtime-image>,<deploy-list>[,<instance-list>]` — `+`-separated lists, e.g.
2991
- * `express,dd-cyberia,mmo-server` or `express,dd-cyberia+dd-core,mmo-server+mmo-client`. An instance list entry
2992
- * may be a template id (`mmo-server`), which selects its whole variant family.
2977
+ *
2978
+ * Every input is a flag: `--runtime-image` (default `express`), `--deploy-id` (comma-separated, or the `dd` meta
2979
+ * id; unset reads the route table) and `--instance-id` (comma-separated; a template id such as `mmo-server`
2980
+ * selects its whole variant family):
2981
+ * `node bin run cluster --runtime-image express --deploy-id dd-cyberia --instance-id mmo-server --dev`
2982
+ * @param {string} path - Unused; every input is a flag.
2993
2983
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2994
2984
  * @memberof UnderpostRun
2995
2985
  */
2996
2986
  cluster: async (path = '', options = DEFAULT_OPTION) => {
2997
2987
  const { underpostRoot } = options;
2998
- const env = options.dev ? 'development' : 'production';
2999
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2988
+ const env = deployEnvFactory(options);
2989
+ const baseCommand = cli('underpost', { local: true });
3000
2990
  const baseClusterCommand = options.dev ? ' --dev' : '';
3001
2991
  const clusterType = clusterTypeFactory(options, 'kubeadm');
3002
2992
  shellCd(`/home/dd/engine`);
@@ -3004,23 +2994,15 @@ EOF`);
3004
2994
  await timer(5000);
3005
2995
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
3006
2996
  await timer(5000);
3007
- let [runtimeImage, deployList, instanceListId] =
3008
- path && path.trim() && path.split(',')
3009
- ? path.split(',')
3010
- : [
3011
- 'express',
3012
- fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
3013
- '',
3014
- ];
2997
+ const runtimeImage = options.runtimeImage || 'express';
2998
+ const instanceListId = options.instanceId || '';
3015
2999
  // shellExec(
3016
3000
  // `${baseCommand} image${baseClusterCommand} --build ${
3017
3001
  // runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
3018
3002
  // } --${clusterType}`,
3019
3003
  // );
3020
- if (!deployList) {
3021
- deployList = [];
3022
- logger.warn('No deploy list provided');
3023
- } else deployList = deployList.split('+');
3004
+ const deployList = options.deployId ? resolveDeployList(options.deployId) : readDeployRoutes();
3005
+ if (deployList.length === 0) logger.warn('No deploy list provided');
3024
3006
  await timer(5000);
3025
3007
  // --reset-mongodb wipes the retained hostPath volumes before the rollout.
3026
3008
  // This workflow already tore the node down and re-imports every database
@@ -3097,10 +3079,13 @@ EOF`);
3097
3079
  const hostListenResult = etcHostFactory(hosts);
3098
3080
  logger.info(hostListenResult.renderHosts);
3099
3081
  }
3100
- const version = 'v3.3.0';
3082
+ const version = 'v3.3.76';
3101
3083
  const instanceOptionsFactory = (deployId, instanceId) => ({
3102
3084
  ...options,
3103
3085
  ...clusterContextFactory(clusterType),
3086
+ deployId,
3087
+ instanceId,
3088
+ replicas: options.replicas || 1,
3104
3089
  gatewayApi,
3105
3090
  gatewayBootstrapComplete: true,
3106
3091
  tls: true,
@@ -3147,17 +3132,16 @@ EOF`);
3147
3132
  sudo rm -rf ./engine-private/, \
3148
3133
  node bin clone underpostnet/engine-cyberia-private, \
3149
3134
  sudo mv ./engine-cyberia-private ./engine-private, \
3150
- node bin env dd-cyberia ${env}, \
3151
- node ./engine-private/itc-scripts/dd-cyberia-0.js, \
3135
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3152
3136
  sudo chown -R dd:dd /home/dd/engine/src/client/public/cyberia, \
3153
- node bin env dd-cyberia ${env}, \
3137
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3154
3138
  node bin client dd-cyberia ${env}, \
3155
3139
  node bin start dd-cyberia ${env} --run'`
3156
3140
  : '');
3157
3141
  deployFlagsById[deployId] = deployFlags;
3158
3142
  // SSR status and context documents belong to the ingress bootstrap, so
3159
3143
  // build them on the host before any workload Deployment is submitted.
3160
- shellExec(`${baseCommand} client ${deployId} ${env}`);
3144
+ shellExec(`${baseCommand} client ${deployId} --env ${env}`);
3161
3145
  shellExec(`${baseCommand} deploy ${deployId} ${env} --build-manifest ${deployFlags}`);
3162
3146
  // Seed the static tree before the routes exist, so every status page and
3163
3147
  // intercepted context the manifests just pointed at resolves from the
@@ -3260,10 +3244,7 @@ EOF`);
3260
3244
  // keep serving the custom fallback until the atomic promotion completes.
3261
3245
  for (const instanceId of instancesByDeployId[deployId].ids) {
3262
3246
  logger.info('[cluster] Deploying custom instance', { deployId, instanceId, env, clusterType });
3263
- await UnderpostRun.RUNNERS.instance(
3264
- `${deployId},${instanceId},${options.replicas || 1}`,
3265
- instanceOptionsFactory(deployId, instanceId),
3266
- );
3247
+ await UnderpostRun.RUNNERS.instance('', instanceOptionsFactory(deployId, instanceId));
3267
3248
  }
3268
3249
  }
3269
3250
  logger.info('[cluster] Ingress stack deployed', {
@@ -3655,6 +3636,10 @@ EOF`);
3655
3636
  /**
3656
3637
  * @method deploy
3657
3638
  * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
3639
+ *
3640
+ * The target colour is restarted when it is already deployed, and built and applied when it is not:
3641
+ * between cycles only the routed colour has a Deployment, so its counterpart being absent is the
3642
+ * normal state rather than a precondition.
3658
3643
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
3659
3644
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3660
3645
  * @memberof UnderpostRun
@@ -3662,19 +3647,57 @@ EOF`);
3662
3647
  deploy: async (path, options = DEFAULT_OPTION) => {
3663
3648
  const deployId = path;
3664
3649
  const env = options.dev ? 'development' : 'production';
3650
+ const namespace = options.namespace || 'default';
3665
3651
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
3666
3652
  if (!validVersion) throw new Error('Version mismatch');
3667
- const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace, env });
3668
- const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
3653
+ options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
3654
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
3655
+ namespace,
3656
+ env,
3657
+ gatewayApi: options.gatewayApi,
3658
+ });
3659
+ const { targetTraffic, create } = redeployPlanFactory({
3660
+ liveTraffic: currentTraffic,
3661
+ hasDeployment: (colour) =>
3662
+ Underpost.deploy.deploymentExists({ deployment: `${deployId}-${env}-${colour}`, namespace }),
3663
+ });
3669
3664
  const ignorePods = Underpost.kubectl
3670
- .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
3665
+ .get(`${deployId}-${env}-${targetTraffic}`, 'pods', namespace)
3671
3666
  .map((p) => p.NAME);
3672
3667
 
3673
- shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
3668
+ if (create) {
3669
+ // The target colour having no Deployment is the steady state of a
3670
+ // blue/green cycle, not a missing prerequisite: build that colour's
3671
+ // manifest and apply it, leaving the live colour routed until the
3672
+ // promotion below.
3673
+ const replicas = options.replicas || 1;
3674
+ const node =
3675
+ (currentTraffic
3676
+ ? Underpost.deploy.deploymentNode({ deployment: `${deployId}-${env}-${currentTraffic}`, namespace })
3677
+ : '') || options.nodeName;
3678
+ logger.info('Target traffic colour is not deployed; creating it', {
3679
+ deployId,
3680
+ env,
3681
+ targetTraffic,
3682
+ node: node || '(unpinned)',
3683
+ });
3684
+ const deployFlags =
3685
+ ` --versions ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${node ? ` --node ${node}` : ''}` +
3686
+ `${options.image ? ` --image ${options.image}` : ''}` +
3687
+ `${Underpost.deploy.timeoutFlagsFactory(options)}${options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : ''}` +
3688
+ `${Underpost.deploy.gatewayApiFlagsFactory(options)}`;
3689
+ // `--build-manifest` returns before any cluster mutation, so the colour is
3690
+ // rendered first and applied by the second call. `--traffic` keeps the
3691
+ // generated traffic Service on the colour still serving.
3692
+ shellExec(
3693
+ `node bin deploy --build-manifest --info-router --traffic ${currentTraffic || targetTraffic}${deployFlags} ${deployId} ${env}`,
3694
+ );
3695
+ shellExec(`node bin deploy --disable-update-proxy${deployFlags} ${deployId} ${env}`);
3696
+ } else shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${namespace}`);
3674
3697
 
3675
- await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
3698
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, namespace);
3676
3699
 
3677
- Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
3700
+ Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, namespace, options);
3678
3701
  },
3679
3702
 
3680
3703
  /**
@@ -3764,7 +3787,10 @@ EOF`);
3764
3787
  }`;
3765
3788
  shellExec(cmd, { async: true });
3766
3789
  }
3767
- if ((await awaitDeployMonitor()) !== true) return;
3790
+ // A bare `return` here exited 0, so a runtime that latched `container-status=error` looked
3791
+ // like a clean start to everything upstream — the shell, and any CI job wrapping it.
3792
+ if ((await awaitDeployMonitor()) !== true)
3793
+ throw new Error(`[run dev] ${deployId} api runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
3768
3794
  {
3769
3795
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
3770
3796
 
@@ -3772,7 +3798,8 @@ EOF`);
3772
3798
  async: true,
3773
3799
  });
3774
3800
  }
3775
- if ((await awaitDeployMonitor()) !== true) return;
3801
+ if ((await awaitDeployMonitor()) !== true)
3802
+ throw new Error(`[run dev] ${deployId} client runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
3776
3803
  shellExec(
3777
3804
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
3778
3805
  );
@@ -3787,7 +3814,7 @@ EOF`);
3787
3814
  */
3788
3815
  service: async (path = '', options = DEFAULT_OPTION) => {
3789
3816
  const env = options.dev ? 'development' : 'production';
3790
- const baseCommand = options.dev ? 'node bin' : 'underpost';
3817
+ const baseCommand = cli('underpost', { local: true });
3791
3818
  const baseClusterCommand = options.dev ? ' --dev' : '';
3792
3819
  shellCd(`/home/dd/engine`);
3793
3820
  let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
@@ -3836,9 +3863,9 @@ EOF`);
3836
3863
  );
3837
3864
  switch (serviceId) {
3838
3865
  case 'mongo-express-service': {
3839
- shellExec(`kubectl delete svc mongo-express-service -n ${options.namespace} --ignore-not-found`);
3840
- shellExec(`kubectl delete deployment mongo-express -n ${options.namespace} --ignore-not-found`);
3841
- 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 });
3842
3869
  podToMonitor = 'mongo-express';
3843
3870
  break;
3844
3871
  }
@@ -3892,26 +3919,6 @@ EOF`);
3892
3919
  logger.info(hostListenResult.renderHosts);
3893
3920
  },
3894
3921
 
3895
- /**
3896
- * @method sh
3897
- * @description Enables remote control for the Kitty terminal emulator.
3898
- * @param {string} path - The input value, identifier, or path for the operation.
3899
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3900
- * @memberof UnderpostRun
3901
- */
3902
- sh: async (path = '', options = DEFAULT_OPTION) => {
3903
- let [operator, arg0, arg1] = path.split(',');
3904
- if (operator == 'copy') {
3905
- shellExec(
3906
- `kitty @ get-text ${arg0 === 'all' ? '--match all' : '--self'} --extent all${
3907
- arg1 === 'ansi' ? ' --ansi yes' : ''
3908
- } | kitty +kitten clipboard`,
3909
- );
3910
- return;
3911
- }
3912
- shellExec(`kitty -o allow_remote_control=yes`);
3913
- },
3914
-
3915
3922
  /**
3916
3923
  * @method log
3917
3924
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
@@ -3970,28 +3977,44 @@ EOF`);
3970
3977
 
3971
3978
  // Services
3972
3979
  logger.info('Process info');
3973
- shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`);
3980
+ shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`, {
3981
+ silentOnError: true,
3982
+ });
3974
3983
  logger.info('Command line');
3975
- shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`);
3984
+ shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`, {
3985
+ silentOnError: true,
3986
+ });
3976
3987
  logger.info('Executable path');
3977
3988
  shellExec(`sudo readlink -f /proc/${pid}/exe`);
3978
3989
  logger.info('Working directory');
3979
- shellExec(`sudo readlink -f /proc/${pid}/cwd`);
3990
+ shellExec(`sudo readlink -f /proc/${pid}/cwd`, {
3991
+ silentOnError: true,
3992
+ });
3980
3993
  logger.info('Environment variables (first 200)');
3981
- shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`);
3994
+ shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`, {
3995
+ silentOnError: true,
3996
+ });
3982
3997
 
3983
3998
  // Parent
3984
3999
  logger.info('Parent process');
3985
- const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, { stdout: true, silent: true });
4000
+ const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, {
4001
+ stdout: true,
4002
+ silent: true,
4003
+ silentOnError: true,
4004
+ });
3986
4005
  console.log(parentInfo);
3987
4006
  const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
3988
4007
  if (ppidMatch) {
3989
4008
  const ppid = ppidMatch.trim().split(/\s+/)[1];
3990
4009
  logger.info(`Parent PID: ${ppid}`);
3991
- shellExec(`ps -fp ${ppid}`);
4010
+ shellExec(`ps -fp ${ppid}`, {
4011
+ silentOnError: true,
4012
+ });
3992
4013
  }
3993
4014
  logger.info('Process tree');
3994
- shellExec(`pstree -s ${pid}`);
4015
+ shellExec(`pstree -s ${pid}`, {
4016
+ silentOnError: true,
4017
+ });
3995
4018
  },
3996
4019
 
3997
4020
  /**
@@ -4036,7 +4059,7 @@ EOF`);
4036
4059
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
4037
4060
  // Note: use recomendation empty deploy cluster: node bin --dev cluster
4038
4061
  const env = options.dev ? 'development' : 'production';
4039
- const baseCommand = options.dev ? 'node bin' : 'underpost';
4062
+ const baseCommand = cli('underpost', { local: true });
4040
4063
  const baseClusterCommand = options.dev ? ' --dev' : '';
4041
4064
  const inputs = path ? path.split(',') : [];
4042
4065
  const deployId = inputs[0] ? inputs[0] : 'dd-test';
@@ -4045,10 +4068,12 @@ EOF`);
4045
4068
  : [
4046
4069
  `npm install -g npm@11.2.0`,
4047
4070
  `npm install -g underpost`,
4048
- `${baseCommand} secret underpost --create-from-env`,
4071
+ `${baseCommand} host load`,
4049
4072
  `${baseCommand} start --build --run ${deployId} ${env}`,
4050
4073
  ];
4051
- shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
4074
+ shellExec(
4075
+ `node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none --deploy-id dd-test --cmd ${shellArgumentFactory(cmd)}`,
4076
+ );
4052
4077
  },
4053
4078
 
4054
4079
  /**
@@ -4179,26 +4204,7 @@ EOF`);
4179
4204
 
4180
4205
  shellCd('/home/dd/engine');
4181
4206
  },
4182
- /**
4183
- * @method pull-rocky-image
4184
- * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
4185
- * @param {string} path - The input value, identifier, or path for the operation.
4186
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4187
- * @memberof UnderpostRun
4188
- */
4189
- 'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
4190
- shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
4191
- },
4192
- /**
4193
- * @method rmi
4194
- * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
4195
- * @param {string} path - The input value, identifier, or path for the operation.
4196
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4197
- * @memberof UnderpostRun
4198
- */
4199
- rmi: (path, options = DEFAULT_OPTION) => {
4200
- shellExec(`podman rmi $(podman images -qa) --force`);
4201
- },
4207
+
4202
4208
  /**
4203
4209
  * @method kill
4204
4210
  * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
@@ -4244,333 +4250,6 @@ EOF`);
4244
4250
  if (options.copy) pbcopy(password);
4245
4251
  else console.log(password);
4246
4252
  },
4247
- /**
4248
- * @method sops-setup
4249
- * @description End-to-end SOPS/Age onboarding for a host: installs tooling, generates the Age
4250
- * keypair and creation rules, pins the key path for non-interactive runs, encrypts the
4251
- * requested Secrets into the Git-tracked store, then validates and applies them.
4252
- *
4253
- * Every step is idempotent and re-runnable. Notably it delegates key generation to
4254
- * `secret sops --init` rather than calling `age-keygen` directly: a bare `age-keygen -o`
4255
- * overwrites an existing key, which would orphan every manifest already encrypted to the
4256
- * previous recipient with no way to recover them.
4257
- *
4258
- * On a host that pulled a store created elsewhere, the freshly generated key is not a recipient
4259
- * of the inherited manifests. `init()` registers this host in the creation rules so what it
4260
- * encrypts from here on stays readable, but existing manifests can only be re-keyed from a host
4261
- * that still holds a decrypting key. That case is reported per secret and then raised by the
4262
- * apply pre-flight with the available remedies, rather than surfacing as a sops decrypt error.
4263
- *
4264
- * Onboards the whole self-hosted data tier by default — PostgreSQL, MariaDB, and MongoDB
4265
- * (`postgres-secret`, `mariadb-secret`, `mongodb-secret`, `mongodb-keyfile`). The MongoDB
4266
- * keyfile is included because the StatefulSet mounts it for intra-replica-set auth and will
4267
- * not start without it. Pass an explicit comma-separated list to narrow the set.
4268
- *
4269
- * Secret values are resolved per data key, in order:
4270
- * 1. the origin seed file, when one exists (`engine-private/postgresql-password`) — this is
4271
- * the real onboarding path, carrying the credential the cluster already runs on;
4272
- * 2. `--args` as `key=value` pairs, for a value supplied by the operator;
4273
- * 3. a freshly generated value: a base64 keyfile for `mongodb-keyfile`, `admin` for a
4274
- * `username`, otherwise a 24-character secure password.
4275
- *
4276
- * Plaintext manifests are written by Node under `/dev/shm` at mode 600 and shredded by
4277
- * `encrypt()`. They are never emitted through a shell heredoc, which would place the
4278
- * credential in the command string and therefore in the process table and the command log.
4279
- *
4280
- * Usage:
4281
- * underpost run sops-setup # postgres + mariadb + mongo
4282
- * underpost run sops-setup mongodb-secret,mongodb-keyfile --namespace prod
4283
- * underpost run sops-setup postgres-secret --args "password=s3cr3t"
4284
- * underpost run sops-setup --dry-run # stop before mutating cluster
4285
- * underpost run sops-setup --force # replace stored manifests
4286
- * @param {string} path - Comma-separated Secret names to onboard. Defaults to the full data
4287
- * tier: postgres-secret, mariadb-secret, mongodb-secret, mongodb-keyfile.
4288
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4289
- * @param {string} options.namespace - Target namespace for the store and the apply (default: 'default').
4290
- * @param {string} options.args - Comma-separated `key=value` overrides for Secret data keys.
4291
- * @param {boolean} options.dryRun - Validate and server-dry-run only; never apply.
4292
- * @param {boolean} options.force - Replace encrypted manifests that already exist.
4293
- * @memberof UnderpostRun
4294
- */
4295
- 'sops-setup': (path = '', options = DEFAULT_OPTION) => {
4296
- const namespace = options.namespace || 'default';
4297
- const secretNames = (path || SOPS_SETUP_DEFAULT_SECRETS.join(','))
4298
- .split(',')
4299
- .map((name) => name.trim())
4300
- .filter(Boolean);
4301
-
4302
- // `--args key=value,key2=value2` overrides, applied to any secret that declares that key.
4303
- const overrides = `${options.args || ''}`.split(',').reduce((acc, pair) => {
4304
- const separator = pair.indexOf('=');
4305
- if (separator > 0) acc[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
4306
- return acc;
4307
- }, {});
4308
-
4309
- logger.info('sops-setup', { secretNames, namespace, dryRun: !!options.dryRun, force: !!options.force });
4310
-
4311
- // 1. Host tooling, then keypair + creation rules. Both no-op when already present.
4312
- Underpost.secret.sops.installTooling();
4313
- Underpost.secret.sops.init();
4314
-
4315
- // 2. Pin the resolved key path for non-interactive runs (systemd units, CronJobs, sudo).
4316
- // Written with the concrete path rather than a guessed default, because `sudo` resets
4317
- // HOME and a wrong guess surfaces later as an opaque decrypt failure.
4318
- const keyFile = Underpost.secret.sops.keyFile();
4319
- shellExec(
4320
- `sudo tee /etc/profile.d/underpost-sops.sh >/dev/null <<'UNDERPOST_SOPS_ENV_EOF'
4321
- export SOPS_AGE_KEY_FILE="\${SOPS_AGE_KEY_FILE:-${keyFile}}"
4322
- UNDERPOST_SOPS_ENV_EOF`,
4323
- );
4324
- shellExec(`sudo chmod 644 /etc/profile.d/underpost-sops.sh`);
4325
-
4326
- // 3. Build and encrypt each requested Secret.
4327
- const stageDir = '/dev/shm/underpost-secrets';
4328
- const held = Underpost.secret.sops.localRecipients();
4329
- fs.ensureDirSync(stageDir);
4330
- fs.chmodSync(stageDir, 0o700);
4331
- try {
4332
- for (const name of secretNames) {
4333
- const stored = Underpost.secret.sops.has(name, namespace);
4334
- if (stored && !options.force) {
4335
- // A stored manifest this host cannot open is present but unusable here, so reporting it
4336
- // as onboarded would send the operator on to an apply that is guaranteed to fail.
4337
- if (Underpost.secret.sops.decryptable(Underpost.secret.sops.manifestPath(name, namespace), held))
4338
- logger.info(`${name} is already onboarded in ns/${namespace}; skipping (use --force to replace)`);
4339
- else
4340
- logger.warn(
4341
- `${name} is stored in ns/${namespace} but is sealed to an Age recipient this host does not hold; ` +
4342
- `skipping. Adopt the store's key, re-key it from a host that holds one, or re-onboard from the ` +
4343
- `origin seed files with --force.`,
4344
- );
4345
- continue;
4346
- }
4347
-
4348
- // Data keys come from the secret's origin seed contract, so an onboarded manifest
4349
- // carries exactly the keys the workload's secretKeyRef already expects.
4350
- const seedSources = Underpost.secret.sops.seedSources(name);
4351
- const dataKeys = Object.keys(seedSources).length > 0 ? Object.keys(seedSources) : ['password'];
4352
- const stringData = {};
4353
- for (const key of dataKeys) {
4354
- const seedPath = seedSources[key];
4355
- if (seedPath && fs.existsSync(seedPath)) {
4356
- stringData[key] = fs.readFileSync(seedPath, 'utf8').trim();
4357
- logger.info(`${name}.${key} seeded from ${seedPath}`);
4358
- } else if (overrides[key] !== undefined) {
4359
- stringData[key] = overrides[key];
4360
- logger.info(`${name}.${key} taken from --args`);
4361
- } else {
4362
- stringData[key] = generateSeedValue(key);
4363
- // Replacing a stored manifest with a value nothing seeded means the credential the
4364
- // running datastore still authenticates against is being thrown away.
4365
- if (stored)
4366
- logger.warn(
4367
- `${name}.${key} generated while replacing the stored manifest — no seed file at ` +
4368
- `${seedPath || '(unmapped)'} and no --args override. The running datastore keeps its old ` +
4369
- `credential until this value is applied to it; pass --args "${key}=<value>" to keep the ` +
4370
- `existing one.`,
4371
- );
4372
- else logger.info(`${name}.${key} generated`);
4373
- }
4374
- }
4375
-
4376
- const stagePath = `${stageDir}/${name}.yaml`;
4377
- fs.outputFileSync(
4378
- stagePath,
4379
- [
4380
- 'apiVersion: v1',
4381
- 'kind: Secret',
4382
- 'metadata:',
4383
- ` name: ${name}`,
4384
- ` namespace: ${namespace}`,
4385
- ' labels:',
4386
- ' app.kubernetes.io/managed-by: underpost',
4387
- 'type: Opaque',
4388
- 'stringData:',
4389
- // Single-quoted YAML scalars with doubled internal quotes: values are generated or
4390
- // operator-supplied and may contain characters YAML would otherwise interpret.
4391
- ...Object.entries(stringData).map(([key, value]) => ` ${key}: '${`${value}`.replace(/'/g, "''")}'`),
4392
- '',
4393
- ].join('\n'),
4394
- 'utf8',
4395
- );
4396
- fs.chmodSync(stagePath, 0o600);
4397
- // encrypt() stages, validates, moves into place, and shreds the plaintext source.
4398
- Underpost.secret.sops.encrypt(stagePath, namespace, options);
4399
- }
4400
- } finally {
4401
- // Defense in depth: encrypt() shreds each source, but a throw mid-loop must not leave a
4402
- // plaintext manifest sitting in shared memory.
4403
- fs.removeSync(stageDir);
4404
- }
4405
-
4406
- Underpost.secret.sops.list();
4407
-
4408
- // 4. Validate every manifest in the namespace, then apply unless this is a dry run.
4409
- Underpost.secret.sops.apply(namespace, { dryRun: true });
4410
- if (options.dryRun) return logger.info('--dry-run: validated only, cluster left unchanged');
4411
- Underpost.secret.sops.apply(namespace);
4412
- },
4413
- /**
4414
- * @method sops-status
4415
- * @description Reports the live state of the SOPS/Age secret system: host tooling, the Age
4416
- * key and its recipient, the committed creation rules, every stored manifest with whether the
4417
- * local key can open it and whether the cluster still matches, and which managed Secrets are
4418
- * onboarded versus still seeding from their origin path.
4419
- *
4420
- * Read-only and safe to run anywhere. Decryption happens only for the drift check, only for
4421
- * manifests the local key is a recipient of, and only into `kubectl diff` with its output
4422
- * discarded — no secret value is ever printed or written to disk.
4423
- *
4424
- * Usage:
4425
- * underpost run sops-status # every managed key, ns default
4426
- * underpost run sops-status mongo # partial match: both mongo keys
4427
- * underpost run sops-status --namespace prod # every managed key in ns prod
4428
- * @param {string} path - Comma-separated managed Secret keys to report on; empty reports all.
4429
- * Matched as case-insensitive substrings (`mongo` selects mongodb-secret and mongodb-keyfile).
4430
- * Filters both the stored-manifest listing and the coverage table.
4431
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4432
- * @param {string} options.namespace - Namespace to inspect (DEFAULT_OPTION scheme, default 'default').
4433
- * @memberof UnderpostRun
4434
- */
4435
- 'sops-status': (path = '', options = DEFAULT_OPTION) => {
4436
- const sops = Underpost.secret.sops;
4437
- // `--namespace` selects the namespace (DEFAULT_OPTION scheme); `path` narrows which managed
4438
- // Secret keys to report on, so the two axes stay independent.
4439
- const namespace = options.namespace || 'default';
4440
- const manageSecretKeyFilter = path
4441
- .split(',')
4442
- .map((key) => key.trim().toLowerCase())
4443
- .filter(Boolean);
4444
- // Partial, case-insensitive substring match, so `mongo` reaches both `mongodb-secret` and
4445
- // `mongodb-keyfile` without having to spell either out.
4446
- const matchesKeyFilter = (name) =>
4447
- manageSecretKeyFilter.length === 0 || manageSecretKeyFilter.some((key) => name.toLowerCase().includes(key));
4448
- const mark = (ok) => (ok ? 'yes' : 'no');
4449
-
4450
- // ── Tooling ────────────────────────────────────────────────────────────
4451
- const version = (bin, flag) =>
4452
- sops.hasBinary(bin)
4453
- ? shellExec(`${bin} ${flag} 2>/dev/null | head -1`, { stdout: true, silent: true, disableLog: true }).trim()
4454
- : '(not installed)';
4455
- logger.info(
4456
- '[sops-status] Tooling\n' +
4457
- ` sops ${version('sops', '--version')}\n` +
4458
- ` age ${version('age', '--version')}\n` +
4459
- ` age-keygen ${sops.hasBinary('age-keygen') ? 'installed' : '(not installed)'}`,
4460
- );
4461
-
4462
- // ── Age key ────────────────────────────────────────────────────────────
4463
- const keyFile = sops.keyFile();
4464
- const keyExists = fs.existsSync(keyFile);
4465
- // A key file may hold several identities — that is how a host joins a store it did not
4466
- // create — so every check below works against the whole held set, not one recipient.
4467
- const held = sops.localRecipients();
4468
- const keyMode = keyExists ? (fs.statSync(keyFile).mode & 0o777).toString(8) : '';
4469
- logger.info(
4470
- '[sops-status] Age key\n' +
4471
- ` path ${keyFile}\n` +
4472
- ` present ${mark(keyExists)}${keyExists ? ` (mode ${keyMode}${keyMode === '600' || keyMode === '400' ? '' : ' — INSECURE, run chmod 600'})` : ''}\n` +
4473
- ` recipients ${held.join(', ') || (keyExists ? '(none — unreadable key file)' : '(none)')}` +
4474
- (keyExists ? '' : `\n searched ${sops.keyFileCandidates().join(', ')}`),
4475
- );
4476
-
4477
- // ── Creation rules ─────────────────────────────────────────────────────
4478
- const confPath = './engine-private/secrets/.sops.yaml';
4479
- const ruleRecipients = sops.creationRecipients();
4480
- logger.info(
4481
- '[sops-status] Creation rules\n' +
4482
- ` config ${confPath} ${fs.existsSync(confPath) ? '' : '(missing — run: underpost secret sops --init)'}\n` +
4483
- ` recipients ${ruleRecipients.length > 0 ? ruleRecipients.join(', ') : '(none)'}\n` +
4484
- ` local key listed ${mark(held.some((recipient) => ruleRecipients.includes(recipient)))}`,
4485
- );
4486
-
4487
- // ── Stored manifests ───────────────────────────────────────────────────
4488
- const manifests = sops.manifests(namespace).filter((manifest) => matchesKeyFilter(manifest.name));
4489
- const onboarded = new Set();
4490
- if (manifests.length === 0)
4491
- logger.warn(
4492
- `[sops-status] Store\n no encrypted manifests in ns/${namespace}` +
4493
- (manageSecretKeyFilter.length > 0 ? ` matching ${manageSecretKeyFilter.join(', ')}` : ''),
4494
- );
4495
- else {
4496
- const rows = manifests.map((manifest) => {
4497
- onboarded.add(manifest.name);
4498
- const recipients = sops.manifestRecipients(manifest.path);
4499
- const decryptable = sops.decryptable(manifest.path, held);
4500
- const live = shellExec(
4501
- `kubectl get secret ${manifest.name} -n ${manifest.namespace} --ignore-not-found -o name 2>/dev/null || true`,
4502
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
4503
- ).trim();
4504
- // Drift is decided by kubectl's exit code; its stdout would contain the decrypted
4505
- // values, so it is discarded rather than captured.
4506
- let sync = 'n/a';
4507
- if (live && decryptable) {
4508
- const result = shellExec(
4509
- `bash -c 'set -o pipefail; SOPS_AGE_KEY_FILE="${keyFile}" sops --decrypt "${manifest.path}" ` +
4510
- `| kubectl diff -f - -n "${manifest.namespace}" >/dev/null 2>&1'`,
4511
- { silentOnError: true, disableLog: true, stdout: false },
4512
- );
4513
- sync = result.code === 0 ? 'in-sync' : result.code === 1 ? 'DRIFT' : 'error';
4514
- } else if (!live) sync = 'not applied';
4515
- else if (!decryptable) sync = 'no local key';
4516
- return (
4517
- ` ${`${manifest.namespace}/${manifest.name}`.padEnd(34)} ` +
4518
- `recipients=${String(recipients.length).padEnd(3)} ` +
4519
- `decryptable=${mark(decryptable).padEnd(4)} ` +
4520
- `live=${mark(!!live).padEnd(4)} ` +
4521
- `${sync}`
4522
- );
4523
- });
4524
- logger.info(`[sops-status] Store — ns/${namespace} (${manifests.length} manifest(s))\n` + rows.join('\n'));
4525
- }
4526
-
4527
- // ── Coverage ───────────────────────────────────────────────────────────
4528
- const coverage = sops
4529
- .managedSecrets()
4530
- .filter(matchesKeyFilter)
4531
- .map((name) => {
4532
- const seeds = Object.values(sops.seedSources(name));
4533
- const seedPresent = seeds.length > 0 && seeds.every((seed) => fs.existsSync(seed));
4534
- const source = onboarded.has(name)
4535
- ? 'sops'
4536
- : seedPresent
4537
- ? 'origin seed'
4538
- : seeds.length
4539
- ? 'MISSING'
4540
- : 'unmapped';
4541
- return ` ${name.padEnd(24)} ${source.padEnd(12)} ${seeds.length ? `seed=${mark(seedPresent)}` : ''}`;
4542
- });
4543
- if (coverage.length === 0)
4544
- logger.warn(
4545
- `[sops-status] Coverage\n no managed Secret matches ${manageSecretKeyFilter.join(', ')}\n` +
4546
- ` known keys: ${sops.managedSecrets().join(', ')}`,
4547
- );
4548
- else
4549
- logger.info('[sops-status] Coverage (which source each managed Secret deploys from)\n' + coverage.join('\n'));
4550
- },
4551
- /**
4552
- * @method secret
4553
- * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
4554
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
4555
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4556
- * @memberof UnderpostRun
4557
- */
4558
- secret: (path, options = DEFAULT_OPTION) => {
4559
- const cronDeployId = cronDeployIdResolve() || 'dd-cron';
4560
- Underpost.secret.underpost.createFromEnvFile(
4561
- `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
4562
- );
4563
- },
4564
- /**
4565
- * @method underpost-config
4566
- * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
4567
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
4568
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4569
- * @memberof UnderpostRun
4570
- */
4571
- 'underpost-config': (path = '', options = DEFAULT_OPTION) => {
4572
- Underpost.deploy.configMap(path ? path : 'production', options.namespace);
4573
- },
4574
4253
  /**
4575
4254
  * @method gpu-env
4576
4255
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
@@ -4666,7 +4345,7 @@ UNDERPOST_SOPS_ENV_EOF`,
4666
4345
  ? 'Directory'
4667
4346
  : 'File';
4668
4347
 
4669
- const envs = Underpost.env.list();
4348
+ const envs = Underpost.host.store.list();
4670
4349
 
4671
4350
  const cmd = `kubectl apply -f - <<'EOF'
4672
4351
  apiVersion: ${apiVersion}
@@ -4732,186 +4411,47 @@ EOF`;
4732
4411
 
4733
4412
  /**
4734
4413
  * @method push-bundle
4735
- * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
4736
- * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
4737
- * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
4738
- * @param {string} path - Optional `fsPath.splitOption` string.
4739
- * 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`.
4740
4421
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4741
- * @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.
4742
4424
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4743
4425
  * @memberof UnderpostRun
4744
4426
  */
4745
- 'push-bundle': (path = '', options = DEFAULT_OPTION) => {
4746
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4747
- const env = options.dev ? 'development' : 'production';
4748
- const deployId = options.deployId || 'dd-default';
4749
- const pathParts = (path || '').split('.');
4750
- const fsPath = (pathParts[0] || '').trim() || 'build';
4751
- const splitOption = (pathParts[1] || '').trim();
4752
-
4753
- let splitFlag = '--split 8';
4754
- if (splitOption) {
4755
- if (splitOption === 'none-split') {
4756
- splitFlag = '';
4757
- } else {
4758
- const splitMb = Number(splitOption);
4759
- if (Number.isFinite(splitMb) && splitMb > 0) {
4760
- splitFlag = `--split ${splitMb}`;
4761
- } else {
4762
- logger.warn('push-bundle: invalid split option, using default split 8', {
4763
- path,
4764
- splitOption,
4765
- });
4766
- }
4767
- }
4768
- }
4769
-
4770
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
4771
- const confServer = fs.existsSync(confServerPath)
4772
- ? loadReplicas(deployId, loadConfServerJson(confServerPath))
4773
- : {};
4774
- const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
4775
-
4776
- shellExec(`${baseCommand} env ${deployId} ${env}`);
4777
- shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4778
-
4779
- const pushBundleFiles = (host, routePath) => {
4780
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
4781
- const buildDir = `./${fsPath}`;
4782
- if (!fs.existsSync(buildDir)) return;
4783
- const partFiles = fs
4784
- .readdirSync(buildDir)
4785
- .filter(
4786
- (name) =>
4787
- name.startsWith(`${buildId}.zip.part`) ||
4788
- name.startsWith(`${buildId}.zip-part`) ||
4789
- name === `${buildId}.zip`,
4790
- )
4791
- .map((name) => `${fsPath}/${name}`);
4792
- if (partFiles.length === 0) {
4793
- logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
4794
- return;
4795
- }
4796
- for (const partFile of partFiles) {
4797
- shellExec(
4798
- `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
4799
- );
4800
- }
4801
- };
4802
-
4803
- for (const host of Object.keys(confServer)) {
4804
- for (const routePath of Object.keys(confServer[host])) {
4805
- const routeConf = confServer[host][routePath] || {};
4806
- if (routeConf.redirect || routeConf.disabledRebuild) continue;
4807
- if (routeConf.singleReplica) {
4808
- if (routeConf.replicas) {
4809
- for (const replica of routeConf.replicas) {
4810
- pushBundleFiles(host, replica);
4811
- }
4812
- }
4813
- continue;
4814
- }
4815
- pushBundleFiles(host, routePath);
4816
- }
4817
- }
4818
- },
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
+ }),
4819
4434
 
4820
4435
  /**
4821
4436
  * @method pull-bundle
4822
- * @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
4823
- * Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
4824
- * Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
4825
- * so that multi-path deployments are handled correctly.
4826
- * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
4827
- * 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`.
4828
4444
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4829
4445
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
4830
4446
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
4831
4447
  * @memberof UnderpostRun
4832
4448
  */
4833
- 'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
4834
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4835
- const env = options.dev ? 'development' : 'production';
4836
- const deployId = options.deployId || 'dd-default';
4837
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
4838
- const confServer = fs.existsSync(confServerPath)
4839
- ? loadReplicas(deployId, loadConfServerJson(confServerPath))
4840
- : {};
4841
- const hostsArg = path
4842
- ? path
4843
- .split(',')
4844
- .map((h) => h.trim())
4845
- .filter(Boolean)
4846
- : Object.keys(confServer);
4847
-
4848
- if (hostsArg.length === 0) {
4849
- logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
4850
- return;
4851
- }
4852
-
4853
- shellExec(`${baseCommand} env ${deployId} ${env}`);
4854
- if (!fs.existsSync('./build')) fs.mkdirSync('./build', { recursive: true });
4855
- shellExec(
4856
- `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
4857
- );
4858
-
4859
- const pullBundleRoute = (host, routePath) => {
4860
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
4861
- const zipPath = `build/${buildId}.zip`;
4862
- const buildDir = './build';
4863
- const hasZip = fs.existsSync(zipPath);
4864
- const hasParts =
4865
- fs.existsSync(buildDir) &&
4866
- fs
4867
- .readdirSync(buildDir)
4868
- .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
4869
-
4870
- if (!hasZip && !hasParts) {
4871
- logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
4872
- return;
4873
- }
4874
-
4875
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
4876
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
4877
- shellExec(`sudo rm -rf ${zipPath}`);
4878
-
4879
- if (fs.existsSync(buildDir)) {
4880
- fs.readdirSync(buildDir)
4881
- .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
4882
- .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
4883
- }
4884
-
4885
- const extractedDir = `build/${buildId.replace(/-$/, '')}`;
4886
- if (!fs.existsSync(extractedDir)) {
4887
- logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
4888
- return;
4889
- }
4890
-
4891
- const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
4892
- if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
4893
- if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
4894
- fs.copySync(`${extractedDir}`, `${publicDestPath}`);
4895
- };
4896
-
4897
- for (const host of hostsArg) {
4898
- const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
4899
-
4900
- for (const routePath of routePaths) {
4901
- const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
4902
- if (routeConf.redirect || routeConf.disabledRebuild) continue;
4903
- if (routeConf.singleReplica) {
4904
- if (routeConf.replicas) {
4905
- for (const replica of routeConf.replicas) {
4906
- pullBundleRoute(host, replica);
4907
- }
4908
- }
4909
- continue;
4910
- }
4911
- pullBundleRoute(host, routePath);
4912
- }
4913
- }
4914
- },
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
+ }),
4915
4455
 
4916
4456
  /**
4917
4457
  * @method kubeadm-wireguard
@@ -4935,15 +4475,30 @@ EOF`;
4935
4475
 
4936
4476
  /**
4937
4477
  * @method build-cluster-deployment-manifests
4938
- * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
4478
+ * @description Builds deployment manifests for both production and development environments using `underpost deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment. Runs under `HERMETIC_BUILD`, so it needs no reachable cluster.
4939
4479
  * @param {string} path - Unused.
4940
4480
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4941
4481
  * @memberof UnderpostRun
4942
4482
  */
4943
- 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
4944
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
4945
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
4946
- },
4483
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) =>
4484
+ // Manifest generation writes files; it does not touch a cluster. Declaring that once
4485
+ // here is what lets the whole tree below run on a build box with no apiserver the
4486
+ // profile rides into both child processes through the environment.
4487
+ withExecutionProfile('HERMETIC_BUILD', async () => {
4488
+ // Each deploy's package manifest is generated from the engine's before the cluster
4489
+ // manifests that ship it: a deploy id declares only its identity and whatever its
4490
+ // catalog adds, so a dependency the engine gained reaches every deploy from here.
4491
+ await syncDeployPackages();
4492
+ // `local` is load-bearing: this builds the manifests of the checkout it was
4493
+ // launched from, and a globally installed underpost is a different package that
4494
+ // reads different paths out of the same cwd.
4495
+ const underpost = cli('underpost', { local: true });
4496
+ const flags = '--build-manifest --sync --info-router --replicas 1';
4497
+ shellExec(`${underpost} deploy ${flags} dd development`);
4498
+ shellExec(`${underpost} deploy ${flags} dd production --cert`);
4499
+ // What the build just produced is what the project repository carries.
4500
+ if (deployRoutesExists()) Underpost.deploy.mirrorDefaultManifests();
4501
+ }),
4947
4502
 
4948
4503
  /**
4949
4504
  * @method monitor-ui
@@ -5083,7 +4638,7 @@ EOF`;
5083
4638
  const result = await Underpost.run.CALL(runner, path, options);
5084
4639
  return result;
5085
4640
  } catch (error) {
5086
- console.log(error);
4641
+ console.error(error);
5087
4642
  logger.error(error);
5088
4643
  process.exit(1);
5089
4644
  }