@underpostnet/cyberia 3.2.90 → 3.3.73

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 (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  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 +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  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 +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
package/src/cli/run.js CHANGED
@@ -4,13 +4,14 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, getTerminalPid, 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
- cronDeployIdResolve,
14
15
  dispatchBuildInstanceEnv,
15
16
  deployHostsFactory,
16
17
  etcHostFactory,
@@ -21,40 +22,43 @@ import {
21
22
  exposeTcpPortsFactory,
22
23
  gatewayApiEnabledFactory,
23
24
  generateSecurePassword,
24
- getNpmRootPath,
25
25
  instanceHttpRouteRulesFactory,
26
26
  instanceInterceptStatusesFactory,
27
+ instanceEnvFilePath,
27
28
  instancePortFactory,
28
29
  instanceProxyRoutesFactory,
29
30
  instanceStatusPageEntriesFactory,
30
- isDeployRunnerContext,
31
31
  loadConfInstances,
32
32
  loadProjectInstanceEnvBuilder,
33
33
  loadConfServerJson,
34
34
  loadReplicas,
35
- resolveDeployList,
36
35
  resolveEnvScoped,
37
36
  selectConfInstances,
38
37
  waitForPort,
39
- writeEnv,
40
38
  clusterInstancesFactory,
41
39
  deployTrafficEntriesFactory,
42
- curlStatusChainFactory,
40
+ publicIngressProbeFactory,
43
41
  hostIngressFactsFactory,
44
42
  hostRenderInstancesFactory,
45
43
  instanceTrafficPlanFactory,
46
44
  trafficTableRowsFactory,
47
45
  isTrafficServingFactory,
48
46
  nextTrafficFactory,
47
+ redeployPlanFactory,
49
48
  stopPlanFactory,
50
49
  trafficFromRoutingInfoFactory,
51
- } from '../server/conf.js';
52
- 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 { cronDeployIdResolve } from '../server/ops/cron.js';
53
+ import { deployEnvFactory, getNpmRootPath, writeEnv } from '../server/runtime/environment.js';
54
+ import { actionInitLog, loggerFactory } from '../server/ops/logger.js';
53
55
 
54
56
  import fs from 'fs-extra';
55
57
  import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
56
58
 
57
59
  import os from 'os';
60
+ import nodePath from 'node:path';
61
+ import { domainContextFactory } from './domains.js';
58
62
  import Underpost from '../index.js';
59
63
  import dotenv from 'dotenv';
60
64
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
@@ -70,7 +74,7 @@ import {
70
74
  readHostInstanceRegistry,
71
75
  writeHostInstanceRegistry,
72
76
  writeHostServerConf,
73
- } from '../server/underpost-gateway.js';
77
+ } from '../server/network/underpost-gateway.js';
74
78
  const logger = loggerFactory(import.meta);
75
79
 
76
80
  /**
@@ -90,6 +94,9 @@ const logger = loggerFactory(import.meta);
90
94
  * @property {string} volumeHostPath - The host path for the volume.
91
95
  * @property {string} volumeMountPath - The mount path for the volume.
92
96
  * @property {string} imageName - The name of the image to run.
97
+ * @property {string} runtimeImage - `src/runtime/<name>` image family the cluster runner brings up (`cluster`).
98
+ * @property {string} image - Container image the deployment pulls and runs (`sync`).
99
+ * @property {string} versions - Comma-separated blue/green deployment versions (`sync`).
93
100
  * @property {string} containerName - The name of the container to run.
94
101
  * @property {string} namespace - The namespace to run in.
95
102
  * @property {string} timeoutResponse - The response timeout duration.
@@ -127,7 +134,7 @@ const logger = loggerFactory(import.meta);
127
134
  * @property {string} confServerPath - The configuration server path.
128
135
  * @property {string} underpostRoot - The root path of the Underpost installation.
129
136
  * @property {string} cmdCronJobs - Pre-script commands to run before cron job execution.
130
- * @property {string} deployIdCronJobs - The deployment ID for cron jobs.
137
+ * @property {string} deployIdCronJobs - Cron deploy-id passed to `cron --setup-start`; unset resolves dd.cron, `none` skips cron setup.
131
138
  * @property {string} timezone - The timezone to set.
132
139
  * @property {boolean} kubeadm - Whether to run in kubeadm mode.
133
140
  * @property {boolean} kind - Whether to run in kind mode.
@@ -138,6 +145,7 @@ const logger = loggerFactory(import.meta);
138
145
  * @property {string} user - The user to run as.
139
146
  * @property {string} group - The group to use.
140
147
  * @property {string} pid - The process ID.
148
+ * @property {string} repoEnginePrivate - Private configuration repository the pull runner checks out, overriding the pairing derived from the engine source.
141
149
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
142
150
  * @property {string} monitorStatus - The monitor status option.
143
151
  * @property {string} monitorStatusKindType - The monitor status kind type option.
@@ -184,6 +192,9 @@ const DEFAULT_OPTION = {
184
192
  volumeHostPath: '',
185
193
  volumeMountPath: '',
186
194
  imageName: '',
195
+ image: '',
196
+ runtimeImage: '',
197
+ versions: '',
187
198
  containerName: '',
188
199
  namespace: 'default',
189
200
  timeoutResponse: '',
@@ -232,6 +243,7 @@ const DEFAULT_OPTION = {
232
243
  user: '',
233
244
  group: '',
234
245
  pid: '',
246
+ repoEnginePrivate: '',
235
247
  disablePrivateConfUpdate: false,
236
248
  monitorStatus: '',
237
249
  monitorStatusKindType: '',
@@ -272,33 +284,6 @@ const DEFAULT_OPTION = {
272
284
  * @memberof UnderpostRun
273
285
  */
274
286
 
275
- // Secrets `sops-setup` onboards when no explicit list is passed: the full self-hosted data tier.
276
- // `mongodb-keyfile` is listed alongside `mongodb-secret` because the MongoDB StatefulSet mounts
277
- // it as a volume for intra-replica-set auth and will not start without it, so onboarding the
278
- // credentials alone would leave Mongo broken.
279
- const SOPS_SETUP_DEFAULT_SECRETS = ['postgres-secret', 'mariadb-secret', 'mongodb-secret', 'mongodb-keyfile'];
280
-
281
- /**
282
- * Produces a value for a Secret data key that has no origin seed file and no `--args` override.
283
- * Key-aware because the data tier does not want one shape of secret: a replica-set keyfile is a
284
- * long base64 blob, a username is an identifier, and everything else is a password.
285
- * @param {string} key - Secret data key (e.g. 'password', 'username', 'mongodb-keyfile').
286
- * @returns {string} Generated value.
287
- * @memberof UnderpostRun
288
- */
289
- const generateSeedValue = (key) => {
290
- if (key === 'username') return 'admin';
291
- // MongoDB keyfile: 6-1024 base64 characters shared by every replica-set member. Newlines are
292
- // stripped so the value round-trips identically through YAML and through
293
- // MongoBootstrap.readCredential, which strips them too.
294
- if (key === 'mongodb-keyfile')
295
- return shellExec(`openssl rand -base64 756`, { stdout: true, silent: true, disableLog: true }).replace(
296
- /\r?\n/g,
297
- '',
298
- );
299
- return generateSecurePassword(24);
300
- };
301
-
302
287
  class UnderpostRun {
303
288
  /**
304
289
  * @static
@@ -327,13 +312,12 @@ class UnderpostRun {
327
312
  throw new Error(`Invalid Kubernetes node name: ${options.nodeName}`);
328
313
  const env = options.dev ? 'development' : 'production';
329
314
  const requestedDeploys = `${path || options.deployId || ''}`.trim();
330
- const routerPath = './engine-private/deploy/dd.router';
331
315
  const confRoot = './engine-private/conf';
332
316
  const deployIds = [
333
317
  ...new Set(
334
318
  requestedDeploys
335
319
  ? resolveDeployList(requestedDeploys)
336
- : fs.existsSync(routerPath)
320
+ : deployRoutesExists()
337
321
  ? resolveDeployList('dd')
338
322
  : fs.existsSync(confRoot)
339
323
  ? fs
@@ -489,7 +473,7 @@ class UnderpostRun {
489
473
  * @memberof UnderpostRun
490
474
  */
491
475
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
492
- const baseCommand = options.dev ? 'node bin' : 'underpost';
476
+ const baseCommand = cli('underpost', { local: true });
493
477
  const mongoHosts = ['mongodb-0.mongodb-service'];
494
478
  let primaryMongoHost = 'mongodb-0.mongodb-service';
495
479
  const clusterType = clusterTypeFactory(options);
@@ -827,52 +811,21 @@ class UnderpostRun {
827
811
  return results;
828
812
  },
829
813
 
830
- /**
831
- * @method dev-hosts-expose
832
- * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
833
- * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
834
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
835
- * @memberof UnderpostRun
836
- */
837
- 'dev-hosts-expose': async (path, options = DEFAULT_OPTION) => {
838
- shellExec(`node bin deploy ${path} development --disable-update-deployment --disable-update-proxy --kubeadm`);
839
- // /etc/hosts is written here, not by `deploy`: that command has no
840
- // --etc-hosts option, and the `etc-hosts` runner already resolves a
841
- // deploy's hosts from its conf.server.json.
842
- await UnderpostRun.RUNNERS['etc-hosts']('', { ...options, deployId: path });
843
- },
844
-
845
- /**
846
- * @method dev-hosts-restore
847
- * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
848
- * @param {string} path - The input value, identifier, or path for the operation.
849
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
850
- * @memberof UnderpostRun
851
- */
852
- 'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
853
- // Rewrite /etc/hosts with the loopback block alone, dropping the deploy
854
- // host entries `dev-hosts-expose` (and the `cluster` runner) added.
855
- const hostListenResult = etcHostFactory([]);
856
- logger.info(hostListenResult.renderHosts);
857
- },
858
-
859
814
  /**
860
815
  * @method cluster-build
861
- * @description Build configuration for cluster deployment.
862
- * @param {string} path - The input value, identifier, or path for the operation.
816
+ * @description Resets the workspace and regenerates every routed deploy's default
817
+ * configuration, optionally committing the result to both repositories.
818
+ *
819
+ * Single-replica materialization is not done here: `deploy --sync` builds those folders
820
+ * as part of the deploy itself (`getDataDeploy({ buildSingleReplica: true })`).
821
+ * @param {string} path - `cmt` to commit the regenerated configuration; otherwise unused.
863
822
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
864
823
  * @memberof UnderpostRun
865
824
  */
866
825
  'cluster-build': (path, options = DEFAULT_OPTION) => {
867
- const nodeOptions =
868
- (options.nodeName ? ` --node-name ${options.nodeName}` : '') +
869
- (options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
870
826
  shellExec(`node bin run clean`);
871
- shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
872
- shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
873
- shellExec(`node bin env clean`);
874
- for (const deployId of fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').split(','))
875
- shellExec(`node bin new --default-conf --deploy-id ${deployId.trim()}`);
827
+ shellExec(`node bin app clean`);
828
+ for (const deployId of readDeployRoutes()) shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
876
829
  if (path === 'cmt') {
877
830
  shellExec(`git add . && underpost cmt . build cluster-build`);
878
831
  shellExec(`cd engine-private && git add . && underpost cmt . build cluster-build`);
@@ -888,14 +841,14 @@ class UnderpostRun {
888
841
  * @memberof UnderpostRun
889
842
  */
890
843
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
891
- const baseCommand = options.dev ? 'node bin' : 'underpost';
844
+ const baseCommand = cli('underpost', { local: true });
892
845
  shellExec(`npm run security:secrets`);
893
846
  const reportPath = './gitleaks-report.json';
894
847
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
895
848
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
896
849
  return;
897
850
  }
898
- shellExec(`${baseCommand} run pull`);
851
+ Underpost.repo.fastForwardEnginePair(baseCommand);
899
852
  shellExec(`${baseCommand} run shared-dir`);
900
853
 
901
854
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
@@ -965,14 +918,14 @@ class UnderpostRun {
965
918
  * @memberof UnderpostRun
966
919
  */
967
920
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
968
- const baseCommand = options.dev ? 'node bin' : 'underpost';
921
+ const baseCommand = cli('underpost', { local: true });
969
922
  shellExec(`npm run security:secrets`);
970
923
  const reportPath = './gitleaks-report.json';
971
924
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
972
925
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
973
926
  return;
974
927
  }
975
- shellExec(`${baseCommand} run pull`);
928
+ Underpost.repo.fastForwardEnginePair(baseCommand);
976
929
  shellExec(`${baseCommand} run shared-dir`);
977
930
 
978
931
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
@@ -1016,47 +969,51 @@ class UnderpostRun {
1016
969
  },
1017
970
  /**
1018
971
  * @method pull
1019
- * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
1020
- * @param {string} path - The input value, identifier, or path for the operation.
972
+ * @description Brings the node's engine checkout and its private configuration to the tip of
973
+ * their repositories, at `/home/dd/engine` and `/home/dd/engine/engine-private`.
974
+ *
975
+ * With no argument this is the monorepo pair, `engine` and `engine-private`. Naming a source
976
+ * repository deploys that source instead, and its private conf repository is derived from the
977
+ * conf id they share rather than passed separately — `underpostnet/engine-test-lampp` pairs
978
+ * with `underpostnet/engine-lampp-private`, as does `underpostnet/engine-lampp`.
979
+ * `--repo-engine-private` overrides that derivation for a conf repository the pairing cannot
980
+ * name, and is the flag `deploy/lib/host.sh` forwards `ENGINE_SRC_PRIVATE_REPO` through.
981
+ *
982
+ * Both checkouts are replaced rather than merged, by the same `--switch-repo` route the fleet
983
+ * sync takes — so a node moves onto a test source repo and back without being reprovisioned,
984
+ * and one that has drifted onto commits of its own is brought back instead of refused.
985
+ *
986
+ * The replacement checkout is installed before this returns: it is a different commit than
987
+ * the one the caller started from, and nothing that follows can run against uninstalled
988
+ * dependencies.
989
+ * @param {string} path - Engine source repository, as `owner/repo` or a clone URL. Defaults to
990
+ * the `GITHUB_USERNAME` monorepo.
1021
991
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1022
992
  * @memberof UnderpostRun
1023
993
  */
1024
- pull: (path, options = DEFAULT_OPTION) => {
994
+ pull: (path = '', options = DEFAULT_OPTION) => {
1025
995
  // shellExec is fail-fast by default — any non-zero exit throws and
1026
996
  // propagates up to the workflow step. No per-call flag required.
1027
- if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
1028
- fs.mkdirSync(`/home/dd`, { recursive: true });
1029
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1030
- } else {
1031
- shellExec(`underpost run clean`);
1032
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1033
- }
1034
- if (!fs.existsSync(`/home/dd/engine/engine-private`))
1035
- shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
1036
- silent: true,
1037
- });
1038
- else
1039
- shellExec(
1040
- `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
1041
- { silent: true },
1042
- );
1043
- },
1044
- /**
1045
- * @method release-deploy
1046
- * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
1047
- * @param {string} path - The input value, identifier, or path for the operation.
1048
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1049
- * @memberof UnderpostRun
1050
- */
1051
- 'release-deploy': (path, options = DEFAULT_OPTION) => {
1052
- actionInitLog();
1053
- shellExec(`underpost --version`);
1054
- shellCd(`/home/dd/engine`);
1055
- for (const _deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1056
- const deployId = _deployId.trim();
1057
- shellExec(`underpost run deploy ${deployId}`, { async: true });
1058
- }
997
+ const { engine: source, enginePrivate: privateRepo } = Underpost.repo.enginePairFactory({
998
+ engine: path,
999
+ enginePrivate: options.repoEnginePrivate,
1000
+ });
1001
+ logger.info('Pulling engine source', { source, privateRepo });
1002
+
1003
+ const engineRoot = `/home/dd/engine`;
1004
+ // Cleaning is the in-process API rather than a shell round trip through the
1005
+ // global `underpost`: that bin is linked at the very checkout being replaced,
1006
+ // so a tree left without its dependencies made the pre-clean — and with it the
1007
+ // whole pull — exit non-zero on exactly the node that needed replacing.
1008
+ if (fs.existsSync(engineRoot)) Underpost.repo.clean({ paths: [engineRoot, `${engineRoot}/engine-private`] });
1009
+ Underpost.repo.syncCheckout({ path: engineRoot, repo: source });
1010
+ Underpost.repo.syncCheckout({ path: `${engineRoot}/engine-private`, repo: privateRepo });
1011
+ // The checkout that lands here is a different commit than the one this process
1012
+ // started from, and its package.json is only a description until it is installed.
1013
+ // Everything after this — the linked global bin included — runs from this tree.
1014
+ shellExec(`cd ${engineRoot} && npm install`);
1059
1015
  },
1016
+
1060
1017
  /**
1061
1018
  * @method ssh-deploy
1062
1019
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
@@ -1126,36 +1083,35 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1126
1083
  },
1127
1084
  /**
1128
1085
  * @method sync
1129
- * @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).
1086
+ * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`).
1087
+ *
1088
+ * Every input is a flag: `--deploy-id`, `--replicas`, `--versions`, `--image`, `--node-name`.
1130
1089
  *
1131
1090
  * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
1132
1091
  * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
1133
1092
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
1134
- * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
1135
- * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
1093
+ * `node bin run sync --deploy-id dd-core --kubeadm --image-pull-policy Always`
1094
+ *
1095
+ * Runs in one of two modes. Given `--deploy-id`, it deploys. Passed `--build`, or no
1096
+ * `--deploy-id` at all, it renders the manifests and the router table and stops before
1097
+ * touching the cluster:
1098
+ * `node bin run --dev --build sync --deploy-id dd-default`
1099
+ * @param {string} [path] - Unused; every input is a flag.
1136
1100
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1137
1101
  * @memberof UnderpostRun
1138
1102
  */
1139
1103
  sync: async (path, options = DEFAULT_OPTION) => {
1140
- // Dev usage: node bin run --dev --build sync dd-default
1141
- const env = options.dev ? 'development' : 'production';
1104
+ const env = deployEnvFactory(options);
1142
1105
  options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
1143
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
1106
+ const baseCommand = cli('underpost', { local: true });
1144
1107
  const baseClusterCommand = options.dev ? ' --dev' : '';
1145
1108
  const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
1146
- const defaultPath = [
1147
- 'dd-default',
1148
- options.replicas,
1149
- ``,
1150
- ``,
1151
- !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
1152
- ];
1153
- let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
1154
- deployId = deployId ? deployId : defaultPath[0];
1155
- replicas = replicas ? replicas : defaultPath[1];
1156
- versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
1157
- image = image ? image : defaultPath[3];
1158
- node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
1109
+ const deploying = !options.build && !!options.deployId;
1110
+ const deployId = options.deployId || 'dd-default';
1111
+ const replicas = options.replicas || 1;
1112
+ const image = options.image || '';
1113
+ const node = options.nodeName || (!options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname());
1114
+ let versions = options.versions || '';
1159
1115
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
1160
1116
 
1161
1117
  if (image && !image.startsWith('localhost'))
@@ -1166,17 +1122,25 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1166
1122
  k3s: options.k3s,
1167
1123
  });
1168
1124
 
1169
- if (isDeployRunnerContext(path, options)) {
1125
+ if (deploying) {
1170
1126
  if (!options.disablePrivateConfUpdate) {
1171
1127
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
1172
1128
  if (!validVersion) throw new Error('Version mismatch');
1173
1129
  }
1174
1130
  if (options.timezone !== 'none') shellExec(`${baseCommand} run${baseClusterCommand} tz`);
1175
- if (options.deployIdCronJobs !== 'none')
1176
- shellExec(`node bin cron${baseClusterCommand}${clusterFlag} --setup-start --git --apply`);
1131
+ if (options.deployIdCronJobs !== 'none') {
1132
+ // --deploy-id-cron-jobs overrides the cron deploy-id; unset falls back to
1133
+ // dd.cron, the same source `cron --setup-start` resolves against on its own.
1134
+ const cronDeployId = options.deployIdCronJobs || cronDeployIdResolve() || '';
1135
+ if (cronDeployId && !/^[a-zA-Z0-9._,-]+$/.test(cronDeployId))
1136
+ throw new Error(`Invalid cron deploy-id: ${cronDeployId}`);
1137
+ shellExec(
1138
+ `node bin cron${cronDeployId ? ` ${cronDeployId}` : ''}${baseClusterCommand}${clusterFlag} --setup-start --git --apply`,
1139
+ );
1140
+ }
1177
1141
  }
1178
1142
 
1179
- const currentTraffic = isDeployRunnerContext(path, options)
1143
+ const currentTraffic = deploying
1180
1144
  ? Underpost.deploy.getCurrentTraffic(deployId, {
1181
1145
  namespace: options.namespace,
1182
1146
  env,
@@ -1200,14 +1164,12 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1200
1164
  });
1201
1165
 
1202
1166
  const ignorePods =
1203
- isDeployRunnerContext(path, options) && targetTraffic
1167
+ deploying && targetTraffic
1204
1168
  ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
1205
1169
  : [];
1206
1170
 
1207
1171
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
1208
- const cmdString = options.cmd
1209
- ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
1210
- : '';
1172
+ const cmdString = options.cmd ? ` --cmd ${shellArgumentFactory(options.cmd)}` : '';
1211
1173
  const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
1212
1174
 
1213
1175
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
@@ -1216,11 +1178,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1216
1178
  const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
1217
1179
  const gatewayApiFlags = Underpost.deploy.gatewayApiFlagsFactory(options);
1218
1180
 
1181
+ // Retarget `underpost-config` at this sync's environment before anything is
1182
+ // deployed. `underpost host load` reads it back inside the
1183
+ // pod, so a stale Secret makes the container resolve the wrong NODE_ENV.
1184
+ if (deploying) {
1185
+ Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
1186
+ Underpost.app.apply(
1187
+ domainContextFactory({ env, namespace: options.namespace, args: { 'deploy-id': deployId } }),
1188
+ );
1189
+ }
1190
+
1219
1191
  // A direct sync owns the same gateway-first contract as the full cluster
1220
1192
  // runner. Build the host-side SSR documents before generating routes unless
1221
- // the caller explicitly selected a pre-built bundle workflow.
1222
- if (isDeployRunnerContext(path, options) && !options.skipFullBuild)
1223
- shellExec(`${baseCommand} client ${deployId} ${env}`);
1193
+ // the caller explicitly selected a pre-built bundle workflow. `--env` is
1194
+ // explicit: the SSR pass must render for the target environment, never for
1195
+ // whatever NODE_ENV the calling shell happens to carry.
1196
+ if (deploying && !options.skipFullBuild) shellExec(`${baseCommand} client ${deployId} --env ${env} --ssr`);
1224
1197
 
1225
1198
  shellExec(
1226
1199
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
@@ -1230,7 +1203,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1230
1203
  }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag}${gatewayApiFlags} ${deployId} ${env}`,
1231
1204
  );
1232
1205
 
1233
- if (isDeployRunnerContext(path, options)) {
1206
+ if (deploying) {
1234
1207
  if (options.gatewayApi) {
1235
1208
  const namespace = options.namespace || 'default';
1236
1209
  const gatewayRoot = Underpost.deploy.underpostGatewayRootFactory(options);
@@ -1301,6 +1274,48 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1301
1274
  );
1302
1275
  },
1303
1276
 
1277
+ /**
1278
+ * @method net-tables
1279
+ * @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`.
1280
+ * @param {string} path - The input value, identifier, or path for the operation (unused).
1281
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow (unused).
1282
+ * @memberof UnderpostRun
1283
+ */
1284
+ 'net-tables': (path, options = DEFAULT_OPTION) => {
1285
+ logger.info(`INTERFACES & LINKS`);
1286
+ shellExec(`(echo "INTERFACE STATE MAC FLAGS"; ip -br link show) | column -t`);
1287
+ logger.info(`IP ADDRESSES`);
1288
+ shellExec(`(echo "INTERFACE STATE IPV4 IPV6"; ip -br addr show) | column -t`);
1289
+ logger.info('KERNEL ROUTING TABLE');
1290
+ shellExec(`(echo "DESTINATION GATEWAY INTERFACE PROTO SCOPE METRIC"; \
1291
+ ip route show | awk '{
1292
+ dst = $1; gw = "on-link"; dev = "-"; proto = "-"; scope = "-"; metric = "-";
1293
+ for (i=1; i<=NF; i++) {
1294
+ if ($i == "via") gw = $(i+1);
1295
+ if ($i == "dev") dev = $(i+1);
1296
+ if ($i == "proto") proto = $(i+1);
1297
+ if ($i == "scope") scope = $(i+1);
1298
+ if ($i == "metric") metric = $(i+1);
1299
+ }
1300
+ print dst, gw, dev, proto, scope, metric;
1301
+ }') | column -t`);
1302
+ logger.info(`ACTIVE NAT FLOWS`);
1303
+ shellExec(`(echo "CHAIN TYPE TARGET_IP_PORT PACKETS BYTES"; \
1304
+ sudo nft list table ip nat | awk '
1305
+ /chain (PREROUTING|POSTROUTING)/ {chain=$2}
1306
+ /dnat to|masquerade|snat to/ && !/packets 0/ {
1307
+ type = ($0 ~ "dnat") ? "DNAT" : (($0 ~ "masquerade") ? "MASQUERADE" : "SNAT");
1308
+ target = "N/A";
1309
+ pkts = "0"; bytes = "0";
1310
+ for(i=1; i<=NF; i++) {
1311
+ if ($i == "packets") pkts = $(i+1);
1312
+ if ($i == "bytes") bytes = $(i+1);
1313
+ if ($i == "to") target = $(i+1);
1314
+ }
1315
+ print chain, type, target, pkts, bytes;
1316
+ }') | column -t`);
1317
+ },
1318
+
1304
1319
  /**
1305
1320
  * @method stop
1306
1321
  * @description Deletes colour-suffixed Deployments and their Services, leaving routing untouched.
@@ -1412,8 +1427,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1412
1427
  ? options.timezone
1413
1428
  : path
1414
1429
  ? path
1415
- : Underpost.env.get('TIME_ZONE', undefined, { disableLog: true })
1416
- ? Underpost.env.get('TIME_ZONE')
1430
+ : Underpost.host.store.get('TIME_ZONE', undefined, { disableLog: true })
1431
+ ? Underpost.host.store.get('TIME_ZONE')
1417
1432
  : process.env.TIME_ZONE
1418
1433
  ? process.env.TIME_ZONE
1419
1434
  : 'America/New_York';
@@ -1556,7 +1571,6 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1556
1571
  // Probe the exact public URL represented by each host/path row. PWA rows
1557
1572
  // can carry several configured paths in one cell, while instance rows
1558
1573
  // carry one; cache by URL so shared rows never repeat network work.
1559
- const shellArg = (value) => `'${`${value}`.replaceAll("'", "'\\''")}'`;
1560
1574
  const probeCache = new Map();
1561
1575
  const probePath = (host, routePath, tls) => {
1562
1576
  const normalizedPath = `${routePath || '/'}`.startsWith('/') ? `${routePath || '/'}` : `/${routePath}`;
@@ -1566,18 +1580,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1566
1580
  } catch {
1567
1581
  return { path: normalizedPath, url: '', statuses: ['000'] };
1568
1582
  }
1569
- if (!probeCache.has(url)) {
1570
- // -L follows the real redirect chain, -v supplies one response line
1571
- // per hop, -i keeps full response headers available, and -s removes
1572
- // only the progress meter. The body is discarded to keep a status
1573
- // report bounded even when a host returns a large application page.
1574
- const raw = shellExec(
1575
- `curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
1576
- `-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${shellArg(url)} 2>&1 || true`,
1577
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
1578
- );
1579
- probeCache.set(url, curlStatusChainFactory(raw));
1580
- }
1583
+ // Cache by URL so rows that share one never repeat network work.
1584
+ if (!probeCache.has(url)) probeCache.set(url, publicIngressProbeFactory(url));
1581
1585
  return { path: normalizedPath, url, statuses: probeCache.get(url) };
1582
1586
  };
1583
1587
 
@@ -1618,7 +1622,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1618
1622
 
1619
1623
  // Padded on the raw values, coloured afterwards: an ANSI escape counts
1620
1624
  // toward String.length and would skew every column right of it.
1621
- const columns = ['HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1625
+ const columns = ['#', 'HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1622
1626
  const deploymentStatus = (status, includeServing = false) => {
1623
1627
  if (!status) return `unrouted - missing${includeServing ? ' not-serving' : ''}`;
1624
1628
  return [
@@ -1628,10 +1632,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1628
1632
  ...(includeServing ? [status.serving ? 'serving' : 'not-serving'] : []),
1629
1633
  ].join(' ');
1630
1634
  };
1631
- const cellOf = (row) => {
1635
+ const cellOf = (row, index) => {
1632
1636
  const facts = ingressFacts[row.host] || {};
1633
1637
  const pathStatus = row.probes.map((probe) => `${probe.path} [${probe.statuses.join('→')}]`).join(' ');
1634
1638
  return [
1639
+ `${index + 1}(${row.probes.length})`,
1635
1640
  row.host,
1636
1641
  pathStatus,
1637
1642
  row.kind,
@@ -1686,7 +1691,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1686
1691
  for (const cell of cells) console.log(line(cell, true));
1687
1692
  };
1688
1693
  for (const env of envs) {
1689
- const envCells = reportRows.filter((row) => row.env === env).map(cellOf);
1694
+ const envRows = reportRows.filter((row) => row.env === env);
1695
+ const envCells = envRows.map((row, index) => cellOf(row, index));
1690
1696
  if (envCells.length > 0) printTable(envCells, `[${env.toUpperCase()}]`);
1691
1697
  }
1692
1698
  console.log('');
@@ -1971,22 +1977,33 @@ EOF
1971
1977
 
1972
1978
  /**
1973
1979
  * @method instance
1974
- * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1980
+ * @description Deploys the custom instances a deploy declares in `conf.instances.json`.
1981
+ *
1982
+ * Every input is a flag: `--deploy-id`, `--instance-id`, `--replicas`, `--node-name`.
1983
+ * `--instance-id` naming a template id selects its whole variant family.
1984
+ * @param {string} path - Unused; every input is a flag.
1975
1985
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1976
1986
  * @memberof UnderpostRun
1977
1987
  */
1978
1988
  instance: async (path = '', options = DEFAULT_OPTION) => {
1979
- const env = options.dev ? 'development' : 'production';
1989
+ const env = deployEnvFactory(options);
1980
1990
  options = {
1981
1991
  ...options,
1982
1992
  gatewayApi: gatewayApiEnabledFactory(options),
1983
1993
  namespace: options.namespace || 'default',
1984
1994
  };
1985
- const baseCommand = options.dev ? 'node bin' : 'underpost';
1995
+ const baseCommand = cli('underpost', { local: true });
1986
1996
  const baseClusterCommand = options.dev ? ' --dev' : '';
1987
- let [deployId, id, replicas] = path.split(',');
1988
- if (!replicas) replicas = options.replicas;
1997
+ const deployId = options.deployId;
1998
+ const id = options.instanceId;
1999
+ const replicas = options.replicas || 1;
2000
+ if (!deployId) throw new Error('The --deploy-id option is required by the instance runner');
2001
+ if (!id) throw new Error('The --instance-id option is required by the instance runner');
1989
2002
  const confInstances = selectConfInstances(loadConfInstances(deployId), id);
2003
+ // Retarget `underpost-config` at this run's environment before any instance
2004
+ // Deployment is submitted: it is what `underpost host load` reads inside the pod,
2005
+ // and a stale one makes the container resolve the wrong NODE_ENV.
2006
+ if (!options.expose) Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
1990
2007
  const { liveTrafficById, targetTrafficById, serving } = instanceTrafficPlanFactory({
1991
2008
  instances: confInstances,
1992
2009
  requestedTraffic: options.traffic,
@@ -2082,7 +2099,6 @@ EOF
2082
2099
  const targetTraffic = targetTrafficById[instance.id];
2083
2100
  const podId = `${_deployId}-${env}-${targetTraffic}`;
2084
2101
  const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
2085
- Underpost.deploy.configMap(env, options.namespace);
2086
2102
  shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
2087
2103
  shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
2088
2104
  for (const _volume of _volumes)
@@ -2212,11 +2228,10 @@ EOF
2212
2228
 
2213
2229
  /**
2214
2230
  * @method deploy-key
2215
- * @description Copies the deploy key for a specific user and deployId to a temporary location on the local machine.
2231
+ * @description Copies the cluster scoped deploy key for a specific user to a temporary location on the local machine.
2216
2232
  * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
2217
2233
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2218
2234
  * @param {string} options.user - The user for which to copy the deploy key.
2219
- * @param {string} options.deployId - The deployment identifier associated with the deploy key.
2220
2235
  * @memberof UnderpostRun
2221
2236
  */
2222
2237
  'deploy-key': (path, options = DEFAULT_OPTION) => {
@@ -2225,13 +2240,18 @@ EOF
2225
2240
  shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
2226
2241
  return;
2227
2242
  }
2228
- if (!options.user || !options.deployId) {
2229
- logger.error('Both --user and --deploy-id options are required to copy the deploy key.');
2243
+ if (!options.user) {
2244
+ logger.error('The --user option is required to copy the deploy key.');
2245
+ return;
2246
+ }
2247
+ const sourcePath = `${Underpost.ssh.userKeyDir(options.user)}/id_rsa`;
2248
+ if (!fs.existsSync(sourcePath)) {
2249
+ logger.error(`No deploy key found at ${sourcePath}; run: node bin ssh --user ${options.user} --user-add`);
2230
2250
  return;
2231
2251
  }
2232
2252
  const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
2233
2253
  fs.mkdirSync('/home/dd/tmp', { recursive: true });
2234
- fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
2254
+ fs.copyFileSync(sourcePath, targetPath);
2235
2255
  logger.info(`Copied deploy key to ${targetPath}`);
2236
2256
  if (options.copy) pbcopy(targetPath);
2237
2257
  },
@@ -2562,11 +2582,11 @@ EOF
2562
2582
  // mode this build ran. The default/template instance owns the committed
2563
2583
  // source files, so only its current-mode file is idempotently refreshed.
2564
2584
  if (instance.templateId) {
2565
- const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
2585
+ const instanceEnvDir = nodePath.dirname(instanceEnvFilePath(deployId, _id, env));
2566
2586
  fs.mkdirpSync(instanceEnvDir);
2567
2587
  const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
2568
2588
  for (const targetEnv of envsToWrite) {
2569
- const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
2589
+ const templateEnvPath = instanceEnvFilePath(deployId, instance.templateId, targetEnv);
2570
2590
  if (!fs.existsSync(templateEnvPath))
2571
2591
  throw new Error(`[instance-build-manifest] Missing canonical env file: ${templateEnvPath}`);
2572
2592
  const baseEnv = dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8'));
@@ -2578,7 +2598,7 @@ EOF
2578
2598
  containerDeployId: `${_deployId}-${targetEnv}`,
2579
2599
  builders: instanceEnvBuilder ? { [deployId]: instanceEnvBuilder } : {},
2580
2600
  });
2581
- writeEnv(`${instanceEnvDir}/${targetEnv}.env`, builtEnv);
2601
+ writeEnv(instanceEnvFilePath(deployId, _id, targetEnv), builtEnv);
2582
2602
  }
2583
2603
  logger.info('[instance-build-manifest] Instance env written', {
2584
2604
  dir: instanceEnvDir,
@@ -2660,7 +2680,6 @@ EOF
2660
2680
  * @memberof UnderpostRun
2661
2681
  */
2662
2682
  'host-update': async (path, options = DEFAULT_OPTION) => {
2663
- // const baseCommand = options.dev ? 'node bin' : 'underpost';
2664
2683
  shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
2665
2684
  shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
2666
2685
  },
@@ -2727,7 +2746,7 @@ EOF`);
2727
2746
  * @memberof UnderpostRun
2728
2747
  */
2729
2748
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
2730
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2749
+ const baseCommand = cli('underpost', { local: true });
2731
2750
  const baseClusterCommand = options.dev ? ' --dev' : '';
2732
2751
  const currentImage = options.imageName
2733
2752
  ? options.imageName
@@ -2824,15 +2843,15 @@ EOF`);
2824
2843
  * @memberof UnderpostRun
2825
2844
  */
2826
2845
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
2827
- const defaultUsername = Underpost.env.get('GITHUB_USERNAME');
2828
- const defaultEmail = Underpost.env.get('GITHUB_EMAIL');
2846
+ const defaultUsername = Underpost.host.store.get('GITHUB_USERNAME');
2847
+ const defaultEmail = Underpost.host.store.get('GITHUB_EMAIL');
2829
2848
  const validPath = path && path.split(',').length;
2830
2849
  const [username, email] = validPath ? path.split(',') : [defaultUsername, defaultEmail];
2831
2850
  if (validPath) {
2832
- Underpost.env.set('GITHUB_USERNAME', username);
2833
- Underpost.env.set('GITHUB_EMAIL', email);
2834
- Underpost.env.get('GITHUB_USERNAME');
2835
- Underpost.env.get('GITHUB_EMAIL');
2851
+ Underpost.host.store.set('GITHUB_USERNAME', username);
2852
+ Underpost.host.store.set('GITHUB_EMAIL', email);
2853
+ Underpost.host.store.get('GITHUB_USERNAME');
2854
+ Underpost.host.store.get('GITHUB_EMAIL');
2836
2855
  }
2837
2856
  shellExec(
2838
2857
  `git config --global credential.helper "" && ` +
@@ -2863,7 +2882,7 @@ EOF`);
2863
2882
 
2864
2883
  /**
2865
2884
  * @method promote
2866
- * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
2885
+ * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.routes` for 'dd', or a specific ID).
2867
2886
  * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
2868
2887
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
2869
2888
  * (cert-manager Certificate resources) for each affected deployment.
@@ -2899,7 +2918,7 @@ EOF`);
2899
2918
  };
2900
2919
 
2901
2920
  if (inputDeployId === 'dd') {
2902
- for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
2921
+ for (const deployId of readDeployRoutes()) {
2903
2922
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
2904
2923
  namespace: options.namespace,
2905
2924
  env: inputEnv,
@@ -2925,44 +2944,6 @@ EOF`);
2925
2944
  applyCerts(inputDeployId, targetTraffic);
2926
2945
  }
2927
2946
  },
2928
- /**
2929
- * @method metrics
2930
- * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
2931
- * @param {string} path - The input value, identifier, or path for the operation.
2932
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2933
- * @memberof UnderpostRun
2934
- */
2935
- metrics: async (path, options = DEFAULT_OPTION) => {
2936
- if (path === 'server') {
2937
- shellExec(
2938
- `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2939
- );
2940
- await timer(2000);
2941
-
2942
- shellExec(`kubectl patch deployment metrics-server -n kube-system \
2943
- --type='json' \
2944
- -p='[
2945
- {
2946
- "op":"add",
2947
- "path":"/spec/template/spec/containers/0/args/-",
2948
- "value":"--kubelet-insecure-tls"
2949
- }
2950
- ]'`);
2951
- shellExec(`kubectl scale deployment metrics-server \
2952
- -n kube-system \
2953
- --replicas=1`);
2954
-
2955
- return;
2956
- }
2957
- const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
2958
- let hosts = [];
2959
- for (const deployId of deployList) {
2960
- const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
2961
- hosts = hosts.concat(Object.keys(confServer));
2962
- }
2963
- shellExec(`node bin cluster --prom ${hosts.join(',')}`);
2964
- shellExec(`node bin cluster --grafana`);
2965
- },
2966
2947
  /**
2967
2948
  * @method cluster
2968
2949
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB,
@@ -2975,21 +2956,24 @@ EOF`);
2975
2956
  * references, and every host is written to `/etc/hosts` so the operator's browser reaches the PWA at
2976
2957
  * `https://<host>` on the local machine.
2977
2958
  *
2978
- * Custom instances are the optional third segment of `path`. They are resolved per deploy against that deploy's
2959
+ * Custom instances come from `--instance-id`. They are resolved per deploy against that deploy's
2979
2960
  * own `conf.instances.json`, so an id only runs where its deploy declares it, and each one is deployed after its
2980
2961
  * deploy's default workload is serving — an instance reads the parent's world configuration over the parent's
2981
2962
  * gRPC ClusterIP at boot. Instance hosts share the deploy's environment: the same self-signed certificates and
2982
2963
  * `/etc/hosts` pass in development, the same cert-manager issuance in production.
2983
- * @param {string} path - `<runtime-image>,<deploy-list>[,<instance-list>]` — `+`-separated lists, e.g.
2984
- * `express,dd-cyberia,mmo-server` or `express,dd-cyberia+dd-core,mmo-server+mmo-client`. An instance list entry
2985
- * may be a template id (`mmo-server`), which selects its whole variant family.
2964
+ *
2965
+ * Every input is a flag: `--runtime-image` (default `express`), `--deploy-id` (comma-separated, or the `dd` meta
2966
+ * id; unset reads the route table) and `--instance-id` (comma-separated; a template id such as `mmo-server`
2967
+ * selects its whole variant family):
2968
+ * `node bin run cluster --runtime-image express --deploy-id dd-cyberia --instance-id mmo-server --dev`
2969
+ * @param {string} path - Unused; every input is a flag.
2986
2970
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2987
2971
  * @memberof UnderpostRun
2988
2972
  */
2989
2973
  cluster: async (path = '', options = DEFAULT_OPTION) => {
2990
2974
  const { underpostRoot } = options;
2991
- const env = options.dev ? 'development' : 'production';
2992
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2975
+ const env = deployEnvFactory(options);
2976
+ const baseCommand = cli('underpost', { local: true });
2993
2977
  const baseClusterCommand = options.dev ? ' --dev' : '';
2994
2978
  const clusterType = clusterTypeFactory(options, 'kubeadm');
2995
2979
  shellCd(`/home/dd/engine`);
@@ -2997,23 +2981,15 @@ EOF`);
2997
2981
  await timer(5000);
2998
2982
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
2999
2983
  await timer(5000);
3000
- let [runtimeImage, deployList, instanceListId] =
3001
- path && path.trim() && path.split(',')
3002
- ? path.split(',')
3003
- : [
3004
- 'express',
3005
- fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
3006
- '',
3007
- ];
2984
+ const runtimeImage = options.runtimeImage || 'express';
2985
+ const instanceListId = options.instanceId || '';
3008
2986
  // shellExec(
3009
2987
  // `${baseCommand} image${baseClusterCommand} --build ${
3010
2988
  // runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
3011
2989
  // } --${clusterType}`,
3012
2990
  // );
3013
- if (!deployList) {
3014
- deployList = [];
3015
- logger.warn('No deploy list provided');
3016
- } else deployList = deployList.split('+');
2991
+ const deployList = options.deployId ? resolveDeployList(options.deployId) : readDeployRoutes();
2992
+ if (deployList.length === 0) logger.warn('No deploy list provided');
3017
2993
  await timer(5000);
3018
2994
  // --reset-mongodb wipes the retained hostPath volumes before the rollout.
3019
2995
  // This workflow already tore the node down and re-imports every database
@@ -3090,10 +3066,13 @@ EOF`);
3090
3066
  const hostListenResult = etcHostFactory(hosts);
3091
3067
  logger.info(hostListenResult.renderHosts);
3092
3068
  }
3093
- const version = 'v3.2.90';
3069
+ const version = 'v3.3.73';
3094
3070
  const instanceOptionsFactory = (deployId, instanceId) => ({
3095
3071
  ...options,
3096
3072
  ...clusterContextFactory(clusterType),
3073
+ deployId,
3074
+ instanceId,
3075
+ replicas: options.replicas || 1,
3097
3076
  gatewayApi,
3098
3077
  gatewayBootstrapComplete: true,
3099
3078
  tls: true,
@@ -3140,17 +3119,16 @@ EOF`);
3140
3119
  sudo rm -rf ./engine-private/, \
3141
3120
  node bin clone underpostnet/engine-cyberia-private, \
3142
3121
  sudo mv ./engine-cyberia-private ./engine-private, \
3143
- node bin env dd-cyberia ${env}, \
3144
- node ./engine-private/itc-scripts/dd-cyberia-0.js, \
3122
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3145
3123
  sudo chown -R dd:dd /home/dd/engine/src/client/public/cyberia, \
3146
- node bin env dd-cyberia ${env}, \
3124
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3147
3125
  node bin client dd-cyberia ${env}, \
3148
3126
  node bin start dd-cyberia ${env} --run'`
3149
3127
  : '');
3150
3128
  deployFlagsById[deployId] = deployFlags;
3151
3129
  // SSR status and context documents belong to the ingress bootstrap, so
3152
3130
  // build them on the host before any workload Deployment is submitted.
3153
- shellExec(`${baseCommand} client ${deployId} ${env}`);
3131
+ shellExec(`${baseCommand} client ${deployId} --env ${env}`);
3154
3132
  shellExec(`${baseCommand} deploy ${deployId} ${env} --build-manifest ${deployFlags}`);
3155
3133
  // Seed the static tree before the routes exist, so every status page and
3156
3134
  // intercepted context the manifests just pointed at resolves from the
@@ -3253,10 +3231,7 @@ EOF`);
3253
3231
  // keep serving the custom fallback until the atomic promotion completes.
3254
3232
  for (const instanceId of instancesByDeployId[deployId].ids) {
3255
3233
  logger.info('[cluster] Deploying custom instance', { deployId, instanceId, env, clusterType });
3256
- await UnderpostRun.RUNNERS.instance(
3257
- `${deployId},${instanceId},${options.replicas || 1}`,
3258
- instanceOptionsFactory(deployId, instanceId),
3259
- );
3234
+ await UnderpostRun.RUNNERS.instance('', instanceOptionsFactory(deployId, instanceId));
3260
3235
  }
3261
3236
  }
3262
3237
  logger.info('[cluster] Ingress stack deployed', {
@@ -3648,6 +3623,10 @@ EOF`);
3648
3623
  /**
3649
3624
  * @method deploy
3650
3625
  * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
3626
+ *
3627
+ * The target colour is restarted when it is already deployed, and built and applied when it is not:
3628
+ * between cycles only the routed colour has a Deployment, so its counterpart being absent is the
3629
+ * normal state rather than a precondition.
3651
3630
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
3652
3631
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3653
3632
  * @memberof UnderpostRun
@@ -3655,19 +3634,57 @@ EOF`);
3655
3634
  deploy: async (path, options = DEFAULT_OPTION) => {
3656
3635
  const deployId = path;
3657
3636
  const env = options.dev ? 'development' : 'production';
3637
+ const namespace = options.namespace || 'default';
3658
3638
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
3659
3639
  if (!validVersion) throw new Error('Version mismatch');
3660
- const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace, env });
3661
- const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
3640
+ options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
3641
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
3642
+ namespace,
3643
+ env,
3644
+ gatewayApi: options.gatewayApi,
3645
+ });
3646
+ const { targetTraffic, create } = redeployPlanFactory({
3647
+ liveTraffic: currentTraffic,
3648
+ hasDeployment: (colour) =>
3649
+ Underpost.deploy.deploymentExists({ deployment: `${deployId}-${env}-${colour}`, namespace }),
3650
+ });
3662
3651
  const ignorePods = Underpost.kubectl
3663
- .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
3652
+ .get(`${deployId}-${env}-${targetTraffic}`, 'pods', namespace)
3664
3653
  .map((p) => p.NAME);
3665
3654
 
3666
- shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
3655
+ if (create) {
3656
+ // The target colour having no Deployment is the steady state of a
3657
+ // blue/green cycle, not a missing prerequisite: build that colour's
3658
+ // manifest and apply it, leaving the live colour routed until the
3659
+ // promotion below.
3660
+ const replicas = options.replicas || 1;
3661
+ const node =
3662
+ (currentTraffic
3663
+ ? Underpost.deploy.deploymentNode({ deployment: `${deployId}-${env}-${currentTraffic}`, namespace })
3664
+ : '') || options.nodeName;
3665
+ logger.info('Target traffic colour is not deployed; creating it', {
3666
+ deployId,
3667
+ env,
3668
+ targetTraffic,
3669
+ node: node || '(unpinned)',
3670
+ });
3671
+ const deployFlags =
3672
+ ` --versions ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${node ? ` --node ${node}` : ''}` +
3673
+ `${options.image ? ` --image ${options.image}` : ''}` +
3674
+ `${Underpost.deploy.timeoutFlagsFactory(options)}${options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : ''}` +
3675
+ `${Underpost.deploy.gatewayApiFlagsFactory(options)}`;
3676
+ // `--build-manifest` returns before any cluster mutation, so the colour is
3677
+ // rendered first and applied by the second call. `--traffic` keeps the
3678
+ // generated traffic Service on the colour still serving.
3679
+ shellExec(
3680
+ `node bin deploy --build-manifest --info-router --traffic ${currentTraffic || targetTraffic}${deployFlags} ${deployId} ${env}`,
3681
+ );
3682
+ shellExec(`node bin deploy --disable-update-proxy${deployFlags} ${deployId} ${env}`);
3683
+ } else shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${namespace}`);
3667
3684
 
3668
- await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
3685
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, namespace);
3669
3686
 
3670
- Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
3687
+ Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, namespace, options);
3671
3688
  },
3672
3689
 
3673
3690
  /**
@@ -3757,7 +3774,10 @@ EOF`);
3757
3774
  }`;
3758
3775
  shellExec(cmd, { async: true });
3759
3776
  }
3760
- if ((await awaitDeployMonitor()) !== true) return;
3777
+ // A bare `return` here exited 0, so a runtime that latched `container-status=error` looked
3778
+ // like a clean start to everything upstream — the shell, and any CI job wrapping it.
3779
+ if ((await awaitDeployMonitor()) !== true)
3780
+ throw new Error(`[run dev] ${deployId} api runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
3761
3781
  {
3762
3782
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
3763
3783
 
@@ -3765,7 +3785,8 @@ EOF`);
3765
3785
  async: true,
3766
3786
  });
3767
3787
  }
3768
- if ((await awaitDeployMonitor()) !== true) return;
3788
+ if ((await awaitDeployMonitor()) !== true)
3789
+ throw new Error(`[run dev] ${deployId} client runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
3769
3790
  shellExec(
3770
3791
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
3771
3792
  );
@@ -3780,7 +3801,7 @@ EOF`);
3780
3801
  */
3781
3802
  service: async (path = '', options = DEFAULT_OPTION) => {
3782
3803
  const env = options.dev ? 'development' : 'production';
3783
- const baseCommand = options.dev ? 'node bin' : 'underpost';
3804
+ const baseCommand = cli('underpost', { local: true });
3784
3805
  const baseClusterCommand = options.dev ? ' --dev' : '';
3785
3806
  shellCd(`/home/dd/engine`);
3786
3807
  let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
@@ -3885,26 +3906,6 @@ EOF`);
3885
3906
  logger.info(hostListenResult.renderHosts);
3886
3907
  },
3887
3908
 
3888
- /**
3889
- * @method sh
3890
- * @description Enables remote control for the Kitty terminal emulator.
3891
- * @param {string} path - The input value, identifier, or path for the operation.
3892
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3893
- * @memberof UnderpostRun
3894
- */
3895
- sh: async (path = '', options = DEFAULT_OPTION) => {
3896
- let [operator, arg0, arg1] = path.split(',');
3897
- if (operator == 'copy') {
3898
- shellExec(
3899
- `kitty @ get-text ${arg0 === 'all' ? '--match all' : '--self'} --extent all${
3900
- arg1 === 'ansi' ? ' --ansi yes' : ''
3901
- } | kitty +kitten clipboard`,
3902
- );
3903
- return;
3904
- }
3905
- shellExec(`kitty -o allow_remote_control=yes`);
3906
- },
3907
-
3908
3909
  /**
3909
3910
  * @method log
3910
3911
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
@@ -3963,28 +3964,44 @@ EOF`);
3963
3964
 
3964
3965
  // Services
3965
3966
  logger.info('Process info');
3966
- shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`);
3967
+ shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`, {
3968
+ silentOnError: true,
3969
+ });
3967
3970
  logger.info('Command line');
3968
- shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`);
3971
+ shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`, {
3972
+ silentOnError: true,
3973
+ });
3969
3974
  logger.info('Executable path');
3970
3975
  shellExec(`sudo readlink -f /proc/${pid}/exe`);
3971
3976
  logger.info('Working directory');
3972
- shellExec(`sudo readlink -f /proc/${pid}/cwd`);
3977
+ shellExec(`sudo readlink -f /proc/${pid}/cwd`, {
3978
+ silentOnError: true,
3979
+ });
3973
3980
  logger.info('Environment variables (first 200)');
3974
- shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`);
3981
+ shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`, {
3982
+ silentOnError: true,
3983
+ });
3975
3984
 
3976
3985
  // Parent
3977
3986
  logger.info('Parent process');
3978
- const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, { stdout: true, silent: true });
3987
+ const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, {
3988
+ stdout: true,
3989
+ silent: true,
3990
+ silentOnError: true,
3991
+ });
3979
3992
  console.log(parentInfo);
3980
3993
  const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
3981
3994
  if (ppidMatch) {
3982
3995
  const ppid = ppidMatch.trim().split(/\s+/)[1];
3983
3996
  logger.info(`Parent PID: ${ppid}`);
3984
- shellExec(`ps -fp ${ppid}`);
3997
+ shellExec(`ps -fp ${ppid}`, {
3998
+ silentOnError: true,
3999
+ });
3985
4000
  }
3986
4001
  logger.info('Process tree');
3987
- shellExec(`pstree -s ${pid}`);
4002
+ shellExec(`pstree -s ${pid}`, {
4003
+ silentOnError: true,
4004
+ });
3988
4005
  },
3989
4006
 
3990
4007
  /**
@@ -4029,7 +4046,7 @@ EOF`);
4029
4046
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
4030
4047
  // Note: use recomendation empty deploy cluster: node bin --dev cluster
4031
4048
  const env = options.dev ? 'development' : 'production';
4032
- const baseCommand = options.dev ? 'node bin' : 'underpost';
4049
+ const baseCommand = cli('underpost', { local: true });
4033
4050
  const baseClusterCommand = options.dev ? ' --dev' : '';
4034
4051
  const inputs = path ? path.split(',') : [];
4035
4052
  const deployId = inputs[0] ? inputs[0] : 'dd-test';
@@ -4038,10 +4055,12 @@ EOF`);
4038
4055
  : [
4039
4056
  `npm install -g npm@11.2.0`,
4040
4057
  `npm install -g underpost`,
4041
- `${baseCommand} secret underpost --create-from-env`,
4058
+ `${baseCommand} host load`,
4042
4059
  `${baseCommand} start --build --run ${deployId} ${env}`,
4043
4060
  ];
4044
- shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
4061
+ shellExec(
4062
+ `node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none --deploy-id dd-test --cmd ${shellArgumentFactory(cmd)}`,
4063
+ );
4045
4064
  },
4046
4065
 
4047
4066
  /**
@@ -4172,26 +4191,7 @@ EOF`);
4172
4191
 
4173
4192
  shellCd('/home/dd/engine');
4174
4193
  },
4175
- /**
4176
- * @method pull-rocky-image
4177
- * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
4178
- * @param {string} path - The input value, identifier, or path for the operation.
4179
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4180
- * @memberof UnderpostRun
4181
- */
4182
- 'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
4183
- shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
4184
- },
4185
- /**
4186
- * @method rmi
4187
- * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
4188
- * @param {string} path - The input value, identifier, or path for the operation.
4189
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4190
- * @memberof UnderpostRun
4191
- */
4192
- rmi: (path, options = DEFAULT_OPTION) => {
4193
- shellExec(`podman rmi $(podman images -qa) --force`);
4194
- },
4194
+
4195
4195
  /**
4196
4196
  * @method kill
4197
4197
  * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
@@ -4237,333 +4237,6 @@ EOF`);
4237
4237
  if (options.copy) pbcopy(password);
4238
4238
  else console.log(password);
4239
4239
  },
4240
- /**
4241
- * @method sops-setup
4242
- * @description End-to-end SOPS/Age onboarding for a host: installs tooling, generates the Age
4243
- * keypair and creation rules, pins the key path for non-interactive runs, encrypts the
4244
- * requested Secrets into the Git-tracked store, then validates and applies them.
4245
- *
4246
- * Every step is idempotent and re-runnable. Notably it delegates key generation to
4247
- * `secret sops --init` rather than calling `age-keygen` directly: a bare `age-keygen -o`
4248
- * overwrites an existing key, which would orphan every manifest already encrypted to the
4249
- * previous recipient with no way to recover them.
4250
- *
4251
- * On a host that pulled a store created elsewhere, the freshly generated key is not a recipient
4252
- * of the inherited manifests. `init()` registers this host in the creation rules so what it
4253
- * encrypts from here on stays readable, but existing manifests can only be re-keyed from a host
4254
- * that still holds a decrypting key. That case is reported per secret and then raised by the
4255
- * apply pre-flight with the available remedies, rather than surfacing as a sops decrypt error.
4256
- *
4257
- * Onboards the whole self-hosted data tier by default — PostgreSQL, MariaDB, and MongoDB
4258
- * (`postgres-secret`, `mariadb-secret`, `mongodb-secret`, `mongodb-keyfile`). The MongoDB
4259
- * keyfile is included because the StatefulSet mounts it for intra-replica-set auth and will
4260
- * not start without it. Pass an explicit comma-separated list to narrow the set.
4261
- *
4262
- * Secret values are resolved per data key, in order:
4263
- * 1. the origin seed file, when one exists (`engine-private/postgresql-password`) — this is
4264
- * the real onboarding path, carrying the credential the cluster already runs on;
4265
- * 2. `--args` as `key=value` pairs, for a value supplied by the operator;
4266
- * 3. a freshly generated value: a base64 keyfile for `mongodb-keyfile`, `admin` for a
4267
- * `username`, otherwise a 24-character secure password.
4268
- *
4269
- * Plaintext manifests are written by Node under `/dev/shm` at mode 600 and shredded by
4270
- * `encrypt()`. They are never emitted through a shell heredoc, which would place the
4271
- * credential in the command string and therefore in the process table and the command log.
4272
- *
4273
- * Usage:
4274
- * underpost run sops-setup # postgres + mariadb + mongo
4275
- * underpost run sops-setup mongodb-secret,mongodb-keyfile --namespace prod
4276
- * underpost run sops-setup postgres-secret --args "password=s3cr3t"
4277
- * underpost run sops-setup --dry-run # stop before mutating cluster
4278
- * underpost run sops-setup --force # replace stored manifests
4279
- * @param {string} path - Comma-separated Secret names to onboard. Defaults to the full data
4280
- * tier: postgres-secret, mariadb-secret, mongodb-secret, mongodb-keyfile.
4281
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4282
- * @param {string} options.namespace - Target namespace for the store and the apply (default: 'default').
4283
- * @param {string} options.args - Comma-separated `key=value` overrides for Secret data keys.
4284
- * @param {boolean} options.dryRun - Validate and server-dry-run only; never apply.
4285
- * @param {boolean} options.force - Replace encrypted manifests that already exist.
4286
- * @memberof UnderpostRun
4287
- */
4288
- 'sops-setup': (path = '', options = DEFAULT_OPTION) => {
4289
- const namespace = options.namespace || 'default';
4290
- const secretNames = (path || SOPS_SETUP_DEFAULT_SECRETS.join(','))
4291
- .split(',')
4292
- .map((name) => name.trim())
4293
- .filter(Boolean);
4294
-
4295
- // `--args key=value,key2=value2` overrides, applied to any secret that declares that key.
4296
- const overrides = `${options.args || ''}`.split(',').reduce((acc, pair) => {
4297
- const separator = pair.indexOf('=');
4298
- if (separator > 0) acc[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
4299
- return acc;
4300
- }, {});
4301
-
4302
- logger.info('sops-setup', { secretNames, namespace, dryRun: !!options.dryRun, force: !!options.force });
4303
-
4304
- // 1. Host tooling, then keypair + creation rules. Both no-op when already present.
4305
- Underpost.secret.sops.installTooling();
4306
- Underpost.secret.sops.init();
4307
-
4308
- // 2. Pin the resolved key path for non-interactive runs (systemd units, CronJobs, sudo).
4309
- // Written with the concrete path rather than a guessed default, because `sudo` resets
4310
- // HOME and a wrong guess surfaces later as an opaque decrypt failure.
4311
- const keyFile = Underpost.secret.sops.keyFile();
4312
- shellExec(
4313
- `sudo tee /etc/profile.d/underpost-sops.sh >/dev/null <<'UNDERPOST_SOPS_ENV_EOF'
4314
- export SOPS_AGE_KEY_FILE="\${SOPS_AGE_KEY_FILE:-${keyFile}}"
4315
- UNDERPOST_SOPS_ENV_EOF`,
4316
- );
4317
- shellExec(`sudo chmod 644 /etc/profile.d/underpost-sops.sh`);
4318
-
4319
- // 3. Build and encrypt each requested Secret.
4320
- const stageDir = '/dev/shm/underpost-secrets';
4321
- const held = Underpost.secret.sops.localRecipients();
4322
- fs.ensureDirSync(stageDir);
4323
- fs.chmodSync(stageDir, 0o700);
4324
- try {
4325
- for (const name of secretNames) {
4326
- const stored = Underpost.secret.sops.has(name, namespace);
4327
- if (stored && !options.force) {
4328
- // A stored manifest this host cannot open is present but unusable here, so reporting it
4329
- // as onboarded would send the operator on to an apply that is guaranteed to fail.
4330
- if (Underpost.secret.sops.decryptable(Underpost.secret.sops.manifestPath(name, namespace), held))
4331
- logger.info(`${name} is already onboarded in ns/${namespace}; skipping (use --force to replace)`);
4332
- else
4333
- logger.warn(
4334
- `${name} is stored in ns/${namespace} but is sealed to an Age recipient this host does not hold; ` +
4335
- `skipping. Adopt the store's key, re-key it from a host that holds one, or re-onboard from the ` +
4336
- `origin seed files with --force.`,
4337
- );
4338
- continue;
4339
- }
4340
-
4341
- // Data keys come from the secret's origin seed contract, so an onboarded manifest
4342
- // carries exactly the keys the workload's secretKeyRef already expects.
4343
- const seedSources = Underpost.secret.sops.seedSources(name);
4344
- const dataKeys = Object.keys(seedSources).length > 0 ? Object.keys(seedSources) : ['password'];
4345
- const stringData = {};
4346
- for (const key of dataKeys) {
4347
- const seedPath = seedSources[key];
4348
- if (seedPath && fs.existsSync(seedPath)) {
4349
- stringData[key] = fs.readFileSync(seedPath, 'utf8').trim();
4350
- logger.info(`${name}.${key} seeded from ${seedPath}`);
4351
- } else if (overrides[key] !== undefined) {
4352
- stringData[key] = overrides[key];
4353
- logger.info(`${name}.${key} taken from --args`);
4354
- } else {
4355
- stringData[key] = generateSeedValue(key);
4356
- // Replacing a stored manifest with a value nothing seeded means the credential the
4357
- // running datastore still authenticates against is being thrown away.
4358
- if (stored)
4359
- logger.warn(
4360
- `${name}.${key} generated while replacing the stored manifest — no seed file at ` +
4361
- `${seedPath || '(unmapped)'} and no --args override. The running datastore keeps its old ` +
4362
- `credential until this value is applied to it; pass --args "${key}=<value>" to keep the ` +
4363
- `existing one.`,
4364
- );
4365
- else logger.info(`${name}.${key} generated`);
4366
- }
4367
- }
4368
-
4369
- const stagePath = `${stageDir}/${name}.yaml`;
4370
- fs.outputFileSync(
4371
- stagePath,
4372
- [
4373
- 'apiVersion: v1',
4374
- 'kind: Secret',
4375
- 'metadata:',
4376
- ` name: ${name}`,
4377
- ` namespace: ${namespace}`,
4378
- ' labels:',
4379
- ' app.kubernetes.io/managed-by: underpost',
4380
- 'type: Opaque',
4381
- 'stringData:',
4382
- // Single-quoted YAML scalars with doubled internal quotes: values are generated or
4383
- // operator-supplied and may contain characters YAML would otherwise interpret.
4384
- ...Object.entries(stringData).map(([key, value]) => ` ${key}: '${`${value}`.replace(/'/g, "''")}'`),
4385
- '',
4386
- ].join('\n'),
4387
- 'utf8',
4388
- );
4389
- fs.chmodSync(stagePath, 0o600);
4390
- // encrypt() stages, validates, moves into place, and shreds the plaintext source.
4391
- Underpost.secret.sops.encrypt(stagePath, namespace, options);
4392
- }
4393
- } finally {
4394
- // Defense in depth: encrypt() shreds each source, but a throw mid-loop must not leave a
4395
- // plaintext manifest sitting in shared memory.
4396
- fs.removeSync(stageDir);
4397
- }
4398
-
4399
- Underpost.secret.sops.list();
4400
-
4401
- // 4. Validate every manifest in the namespace, then apply unless this is a dry run.
4402
- Underpost.secret.sops.apply(namespace, { dryRun: true });
4403
- if (options.dryRun) return logger.info('--dry-run: validated only, cluster left unchanged');
4404
- Underpost.secret.sops.apply(namespace);
4405
- },
4406
- /**
4407
- * @method sops-status
4408
- * @description Reports the live state of the SOPS/Age secret system: host tooling, the Age
4409
- * key and its recipient, the committed creation rules, every stored manifest with whether the
4410
- * local key can open it and whether the cluster still matches, and which managed Secrets are
4411
- * onboarded versus still seeding from their origin path.
4412
- *
4413
- * Read-only and safe to run anywhere. Decryption happens only for the drift check, only for
4414
- * manifests the local key is a recipient of, and only into `kubectl diff` with its output
4415
- * discarded — no secret value is ever printed or written to disk.
4416
- *
4417
- * Usage:
4418
- * underpost run sops-status # every managed key, ns default
4419
- * underpost run sops-status mongo # partial match: both mongo keys
4420
- * underpost run sops-status --namespace prod # every managed key in ns prod
4421
- * @param {string} path - Comma-separated managed Secret keys to report on; empty reports all.
4422
- * Matched as case-insensitive substrings (`mongo` selects mongodb-secret and mongodb-keyfile).
4423
- * Filters both the stored-manifest listing and the coverage table.
4424
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4425
- * @param {string} options.namespace - Namespace to inspect (DEFAULT_OPTION scheme, default 'default').
4426
- * @memberof UnderpostRun
4427
- */
4428
- 'sops-status': (path = '', options = DEFAULT_OPTION) => {
4429
- const sops = Underpost.secret.sops;
4430
- // `--namespace` selects the namespace (DEFAULT_OPTION scheme); `path` narrows which managed
4431
- // Secret keys to report on, so the two axes stay independent.
4432
- const namespace = options.namespace || 'default';
4433
- const manageSecretKeyFilter = path
4434
- .split(',')
4435
- .map((key) => key.trim().toLowerCase())
4436
- .filter(Boolean);
4437
- // Partial, case-insensitive substring match, so `mongo` reaches both `mongodb-secret` and
4438
- // `mongodb-keyfile` without having to spell either out.
4439
- const matchesKeyFilter = (name) =>
4440
- manageSecretKeyFilter.length === 0 || manageSecretKeyFilter.some((key) => name.toLowerCase().includes(key));
4441
- const mark = (ok) => (ok ? 'yes' : 'no');
4442
-
4443
- // ── Tooling ────────────────────────────────────────────────────────────
4444
- const version = (bin, flag) =>
4445
- sops.hasBinary(bin)
4446
- ? shellExec(`${bin} ${flag} 2>/dev/null | head -1`, { stdout: true, silent: true, disableLog: true }).trim()
4447
- : '(not installed)';
4448
- logger.info(
4449
- '[sops-status] Tooling\n' +
4450
- ` sops ${version('sops', '--version')}\n` +
4451
- ` age ${version('age', '--version')}\n` +
4452
- ` age-keygen ${sops.hasBinary('age-keygen') ? 'installed' : '(not installed)'}`,
4453
- );
4454
-
4455
- // ── Age key ────────────────────────────────────────────────────────────
4456
- const keyFile = sops.keyFile();
4457
- const keyExists = fs.existsSync(keyFile);
4458
- // A key file may hold several identities — that is how a host joins a store it did not
4459
- // create — so every check below works against the whole held set, not one recipient.
4460
- const held = sops.localRecipients();
4461
- const keyMode = keyExists ? (fs.statSync(keyFile).mode & 0o777).toString(8) : '';
4462
- logger.info(
4463
- '[sops-status] Age key\n' +
4464
- ` path ${keyFile}\n` +
4465
- ` present ${mark(keyExists)}${keyExists ? ` (mode ${keyMode}${keyMode === '600' || keyMode === '400' ? '' : ' — INSECURE, run chmod 600'})` : ''}\n` +
4466
- ` recipients ${held.join(', ') || (keyExists ? '(none — unreadable key file)' : '(none)')}` +
4467
- (keyExists ? '' : `\n searched ${sops.keyFileCandidates().join(', ')}`),
4468
- );
4469
-
4470
- // ── Creation rules ─────────────────────────────────────────────────────
4471
- const confPath = './engine-private/secrets/.sops.yaml';
4472
- const ruleRecipients = sops.creationRecipients();
4473
- logger.info(
4474
- '[sops-status] Creation rules\n' +
4475
- ` config ${confPath} ${fs.existsSync(confPath) ? '' : '(missing — run: underpost secret sops --init)'}\n` +
4476
- ` recipients ${ruleRecipients.length > 0 ? ruleRecipients.join(', ') : '(none)'}\n` +
4477
- ` local key listed ${mark(held.some((recipient) => ruleRecipients.includes(recipient)))}`,
4478
- );
4479
-
4480
- // ── Stored manifests ───────────────────────────────────────────────────
4481
- const manifests = sops.manifests(namespace).filter((manifest) => matchesKeyFilter(manifest.name));
4482
- const onboarded = new Set();
4483
- if (manifests.length === 0)
4484
- logger.warn(
4485
- `[sops-status] Store\n no encrypted manifests in ns/${namespace}` +
4486
- (manageSecretKeyFilter.length > 0 ? ` matching ${manageSecretKeyFilter.join(', ')}` : ''),
4487
- );
4488
- else {
4489
- const rows = manifests.map((manifest) => {
4490
- onboarded.add(manifest.name);
4491
- const recipients = sops.manifestRecipients(manifest.path);
4492
- const decryptable = sops.decryptable(manifest.path, held);
4493
- const live = shellExec(
4494
- `kubectl get secret ${manifest.name} -n ${manifest.namespace} --ignore-not-found -o name 2>/dev/null || true`,
4495
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
4496
- ).trim();
4497
- // Drift is decided by kubectl's exit code; its stdout would contain the decrypted
4498
- // values, so it is discarded rather than captured.
4499
- let sync = 'n/a';
4500
- if (live && decryptable) {
4501
- const result = shellExec(
4502
- `bash -c 'set -o pipefail; SOPS_AGE_KEY_FILE="${keyFile}" sops --decrypt "${manifest.path}" ` +
4503
- `| kubectl diff -f - -n "${manifest.namespace}" >/dev/null 2>&1'`,
4504
- { silentOnError: true, disableLog: true, stdout: false },
4505
- );
4506
- sync = result.code === 0 ? 'in-sync' : result.code === 1 ? 'DRIFT' : 'error';
4507
- } else if (!live) sync = 'not applied';
4508
- else if (!decryptable) sync = 'no local key';
4509
- return (
4510
- ` ${`${manifest.namespace}/${manifest.name}`.padEnd(34)} ` +
4511
- `recipients=${String(recipients.length).padEnd(3)} ` +
4512
- `decryptable=${mark(decryptable).padEnd(4)} ` +
4513
- `live=${mark(!!live).padEnd(4)} ` +
4514
- `${sync}`
4515
- );
4516
- });
4517
- logger.info(`[sops-status] Store — ns/${namespace} (${manifests.length} manifest(s))\n` + rows.join('\n'));
4518
- }
4519
-
4520
- // ── Coverage ───────────────────────────────────────────────────────────
4521
- const coverage = sops
4522
- .managedSecrets()
4523
- .filter(matchesKeyFilter)
4524
- .map((name) => {
4525
- const seeds = Object.values(sops.seedSources(name));
4526
- const seedPresent = seeds.length > 0 && seeds.every((seed) => fs.existsSync(seed));
4527
- const source = onboarded.has(name)
4528
- ? 'sops'
4529
- : seedPresent
4530
- ? 'origin seed'
4531
- : seeds.length
4532
- ? 'MISSING'
4533
- : 'unmapped';
4534
- return ` ${name.padEnd(24)} ${source.padEnd(12)} ${seeds.length ? `seed=${mark(seedPresent)}` : ''}`;
4535
- });
4536
- if (coverage.length === 0)
4537
- logger.warn(
4538
- `[sops-status] Coverage\n no managed Secret matches ${manageSecretKeyFilter.join(', ')}\n` +
4539
- ` known keys: ${sops.managedSecrets().join(', ')}`,
4540
- );
4541
- else
4542
- logger.info('[sops-status] Coverage (which source each managed Secret deploys from)\n' + coverage.join('\n'));
4543
- },
4544
- /**
4545
- * @method secret
4546
- * @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.
4547
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
4548
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4549
- * @memberof UnderpostRun
4550
- */
4551
- secret: (path, options = DEFAULT_OPTION) => {
4552
- const cronDeployId = cronDeployIdResolve() || 'dd-cron';
4553
- Underpost.secret.underpost.createFromEnvFile(
4554
- `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
4555
- );
4556
- },
4557
- /**
4558
- * @method underpost-config
4559
- * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
4560
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
4561
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4562
- * @memberof UnderpostRun
4563
- */
4564
- 'underpost-config': (path = '', options = DEFAULT_OPTION) => {
4565
- Underpost.deploy.configMap(path ? path : 'production', options.namespace);
4566
- },
4567
4240
  /**
4568
4241
  * @method gpu-env
4569
4242
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
@@ -4659,7 +4332,7 @@ UNDERPOST_SOPS_ENV_EOF`,
4659
4332
  ? 'Directory'
4660
4333
  : 'File';
4661
4334
 
4662
- const envs = Underpost.env.list();
4335
+ const envs = Underpost.host.store.list();
4663
4336
 
4664
4337
  const cmd = `kubectl apply -f - <<'EOF'
4665
4338
  apiVersion: ${apiVersion}
@@ -4736,7 +4409,7 @@ EOF`;
4736
4409
  * @memberof UnderpostRun
4737
4410
  */
4738
4411
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
4739
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4412
+ const baseCommand = cli('underpost', { local: true });
4740
4413
  const env = options.dev ? 'development' : 'production';
4741
4414
  const deployId = options.deployId || 'dd-default';
4742
4415
  const pathParts = (path || '').split('.');
@@ -4767,7 +4440,7 @@ EOF`;
4767
4440
  const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
4768
4441
 
4769
4442
  shellExec(`${baseCommand} env ${deployId} ${env}`);
4770
- shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4443
+ shellExec(`${baseCommand} client ${deployId} --env ${env} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4771
4444
 
4772
4445
  const pushBundleFiles = (host, routePath) => {
4773
4446
  const buildId = `${host}-${routePath.replaceAll('/', '')}`;
@@ -4824,7 +4497,7 @@ EOF`;
4824
4497
  * @memberof UnderpostRun
4825
4498
  */
4826
4499
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
4827
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4500
+ const baseCommand = cli('underpost', { local: true });
4828
4501
  const env = options.dev ? 'development' : 'production';
4829
4502
  const deployId = options.deployId || 'dd-default';
4830
4503
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
@@ -4906,17 +4579,52 @@ EOF`;
4906
4579
  }
4907
4580
  },
4908
4581
 
4582
+ /**
4583
+ * @method kubeadm-wireguard
4584
+ * @description
4585
+ * Configures Calico to keep using the Kubernetes NodeInternalIP after WireGuard
4586
+ * is added to the node. This prevents the WireGuard interface (for example
4587
+ * 10.0.0.2) from being selected as the Calico/BGP node address.
4588
+ *
4589
+ * @param {string} path - Unused.
4590
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options.
4591
+ * @memberof UnderpostRun
4592
+ */
4593
+ 'kubeadm-wireguard': (path, options = DEFAULT_OPTION) => {
4594
+ shellExec(`kubectl patch installation.operator.tigera.io default --type='json' \
4595
+ -p='[
4596
+ {"op":"replace","path":"/spec/calicoNetwork/nodeAddressAutodetectionV4","value":{"kubernetes":"NodeInternalIP"}}
4597
+ ]'`);
4598
+
4599
+ shellExec(`kubectl rollout restart daemonset/calico-node -n calico-system`);
4600
+ },
4601
+
4909
4602
  /**
4910
4603
  * @method build-cluster-deployment-manifests
4911
- * @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.
4604
+ * @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.
4912
4605
  * @param {string} path - Unused.
4913
4606
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4914
4607
  * @memberof UnderpostRun
4915
4608
  */
4916
- 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
4917
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
4918
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
4919
- },
4609
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) =>
4610
+ // Manifest generation writes files; it does not touch a cluster. Declaring that once
4611
+ // here is what lets the whole tree below run on a build box with no apiserver the
4612
+ // profile rides into both child processes through the environment.
4613
+ withExecutionProfile('HERMETIC_BUILD', async () => {
4614
+ // Each deploy's package manifest is generated from the engine's before the cluster
4615
+ // manifests that ship it: a deploy id declares only its identity and whatever its
4616
+ // catalog adds, so a dependency the engine gained reaches every deploy from here.
4617
+ await syncDeployPackages();
4618
+ // `local` is load-bearing: this builds the manifests of the checkout it was
4619
+ // launched from, and a globally installed underpost is a different package that
4620
+ // reads different paths out of the same cwd.
4621
+ const underpost = cli('underpost', { local: true });
4622
+ const flags = '--build-manifest --sync --info-router --replicas 1';
4623
+ shellExec(`${underpost} deploy ${flags} dd development`);
4624
+ shellExec(`${underpost} deploy ${flags} dd production --cert`);
4625
+ // What the build just produced is what the project repository carries.
4626
+ if (deployRoutesExists()) Underpost.deploy.mirrorDefaultManifests();
4627
+ }),
4920
4628
 
4921
4629
  /**
4922
4630
  * @method monitor-ui
@@ -5056,7 +4764,7 @@ EOF`;
5056
4764
  const result = await Underpost.run.CALL(runner, path, options);
5057
4765
  return result;
5058
4766
  } catch (error) {
5059
- console.log(error);
4767
+ console.error(error);
5060
4768
  logger.error(error);
5061
4769
  process.exit(1);
5062
4770
  }