@underpostnet/cyberia 3.3.0 → 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 (471) hide show
  1. package/.dockerignore +1 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +34 -14
  3. package/.github/workflows/cyberia-client.cd.yml +5 -0
  4. package/.github/workflows/cyberia-server.cd.yml +5 -0
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -0
  12. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  13. package/.github/workflows/ghpkg.ci.yml +35 -20
  14. package/.github/workflows/hardhat.ci.yml +12 -11
  15. package/.github/workflows/npmpkg.ci.yml +17 -15
  16. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  17. package/.github/workflows/release.cd.yml +1 -0
  18. package/.prettierignore +1 -0
  19. package/AGENTS.md +47 -0
  20. package/CHANGELOG.md +565 -2510
  21. package/CLI-HELP.md +306 -147
  22. package/Dockerfile +25 -9
  23. package/Dockerfile.dev +25 -9
  24. package/Dockerfile.test +4 -4
  25. package/README.md +21 -1
  26. package/bin/build.js +66 -41
  27. package/bin/build.template.js +8 -3
  28. package/bin/cyberia.js +308 -91
  29. package/bin/deploy.js +18 -104
  30. package/bin/index.js +308 -91
  31. package/conf.js +91 -4
  32. package/deploy/cyberia-client/deploy.sh +47 -0
  33. package/deploy/cyberia-client/package.sh +18 -0
  34. package/deploy/cyberia-client/state.sh +14 -0
  35. package/deploy/cyberia-server/deploy.sh +47 -0
  36. package/deploy/cyberia-server/package.sh +18 -0
  37. package/deploy/cyberia-server/state.sh +14 -0
  38. package/deploy/dd-cyberia/deploy.sh +6 -26
  39. package/deploy/dd-cyberia/init.sh +35 -43
  40. package/deploy/dd-cyberia/package.sh +17 -0
  41. package/deploy/dd-cyberia/state.sh +14 -0
  42. package/deploy/dd-cyberia/sync-deploy.sh +92 -170
  43. package/deploy/lib/github-actions-logging.sh +594 -0
  44. package/deploy/lib/host.sh +185 -0
  45. package/deploy/lib/state.sh +62 -0
  46. package/deploy/pwa-microservices-template/deploy.sh +12 -39
  47. package/deploy/release/deploy.sh +9 -39
  48. package/deployment.yaml +7 -2
  49. package/docker-compose.yml +7 -7
  50. package/examples/static-page/README.md +19 -19
  51. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  52. package/hardhat/hardhat.config.js +1 -6
  53. package/hardhat/package-lock.json +101 -40
  54. package/hardhat/package.json +2 -2
  55. package/manifests/alertmanager/deployment.yaml +81 -0
  56. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  57. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  58. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  59. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  60. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  61. package/manifests/grafana/deployment.yaml +26 -7
  62. package/manifests/grafana/kustomization.yaml +1 -0
  63. package/manifests/grafana/service.yaml +6 -1
  64. package/manifests/mariadb/pv.yaml +1 -0
  65. package/manifests/mongodb/statefulset.yaml +9 -2
  66. package/manifests/mysql/pv-pvc.yaml +14 -1
  67. package/manifests/postgresql/pv.yaml +12 -1
  68. package/manifests/postgresql/pvc.yaml +1 -0
  69. package/manifests/prometheus/deployment.yaml +18 -22
  70. package/package.json +83 -10
  71. package/scripts/event-monitor.sh +56 -0
  72. package/scripts/k3s-node-setup.sh +27 -14
  73. package/scripts/kubeadm-node-setup.sh +31 -11
  74. package/scripts/maas-setup.sh +8 -8
  75. package/scripts/selinux-normalize.sh +907 -0
  76. package/scripts/shutdown-machine.sh +843 -0
  77. package/scripts/test-monitor.sh +3 -3
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  80. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  81. package/src/api/core/core.controller.js +1 -1
  82. package/src/api/core/core.router.js +2 -2
  83. package/src/api/core/core.service.js +2 -2
  84. package/src/api/crypto/crypto.controller.js +1 -1
  85. package/src/api/crypto/crypto.service.js +1 -1
  86. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  87. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  88. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  90. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  91. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  92. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  93. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  94. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  95. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  96. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  97. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  98. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  99. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  100. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  101. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  102. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -0
  103. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  104. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -1
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +2 -2
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +2 -2
  107. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  110. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  111. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  112. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  113. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  114. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  115. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  116. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  118. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  119. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  120. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  121. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  122. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +0 -26
  123. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  124. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  125. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  126. package/src/api/default/default.controller.js +1 -1
  127. package/src/api/default/default.router.js +1 -1
  128. package/src/api/default/default.service.js +2 -2
  129. package/src/api/document/document.controller.js +1 -1
  130. package/src/api/document/document.router.js +1 -1
  131. package/src/api/document/document.service.js +2 -2
  132. package/src/api/file/file.controller.js +1 -1
  133. package/src/api/file/file.router.js +1 -1
  134. package/src/api/file/file.service.js +2 -2
  135. package/src/api/instance/instance.controller.js +1 -1
  136. package/src/api/instance/instance.router.js +1 -1
  137. package/src/api/instance/instance.service.js +2 -2
  138. package/src/api/ipfs/ipfs.controller.js +1 -1
  139. package/src/api/ipfs/ipfs.router.js +2 -2
  140. package/src/api/ipfs/ipfs.service.js +2 -2
  141. package/src/api/object-layer/object-layer.controller.js +1 -1
  142. package/src/api/object-layer/object-layer.router.js +2 -2
  143. package/src/api/object-layer/object-layer.service.js +3 -3
  144. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  145. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  146. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  147. package/src/api/test/test.controller.js +1 -1
  148. package/src/api/test/test.service.js +1 -1
  149. package/src/api/user/guest.service.js +3 -3
  150. package/src/api/user/user.controller.js +1 -1
  151. package/src/api/user/user.router.js +2 -2
  152. package/src/api/user/user.service.js +4 -4
  153. package/src/api.js +5 -9
  154. package/src/cli/app.js +391 -0
  155. package/src/cli/baremetal.js +30 -35
  156. package/src/cli/client.js +306 -0
  157. package/src/cli/cloud-init.js +3 -3
  158. package/src/cli/cluster.js +184 -82
  159. package/src/cli/db.js +120 -68
  160. package/src/cli/deploy.js +153 -71
  161. package/src/cli/docker-compose.js +5 -5
  162. package/src/cli/domains.js +184 -0
  163. package/src/cli/dotenv-store.js +143 -0
  164. package/src/cli/event.js +2281 -0
  165. package/src/cli/fs.js +3 -3
  166. package/src/cli/host.js +672 -0
  167. package/src/cli/image.js +4 -6
  168. package/src/cli/index.js +322 -142
  169. package/src/cli/ipfs.js +34 -30
  170. package/src/cli/kickstart.js +1 -1
  171. package/src/cli/kubectl.js +218 -41
  172. package/src/cli/lxd.js +3 -3
  173. package/src/cli/monitor.js +1359 -26
  174. package/src/cli/package.js +90 -0
  175. package/src/cli/release.js +16 -12
  176. package/src/cli/repository.js +558 -363
  177. package/src/cli/run.js +331 -650
  178. package/src/cli/secrets.js +2158 -964
  179. package/src/cli/ssh.js +478 -279
  180. package/src/cli/state.js +394 -0
  181. package/src/cli/static.js +2 -2
  182. package/src/cli/system.js +1 -1
  183. package/src/cli/test.js +199 -125
  184. package/src/cli/vultr.js +61 -21
  185. package/src/cli/wireguard.js +1111 -306
  186. package/src/client/components/core/CalendarCore.js +53 -96
  187. package/src/client/components/core/Docs.js +258 -211
  188. package/src/client/components/core/FileExplorer.js +0 -21
  189. package/src/client/components/core/PanelForm.js +4 -4
  190. package/src/client/components/core/Repository.js +56 -0
  191. package/src/client/components/core/Translate.js +7 -0
  192. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  193. package/src/client/components/core/Wallet.js +0 -11
  194. package/src/client/components/core/Worker.js +7 -1
  195. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  196. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -1
  197. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  198. package/src/client/components/default/AppShellDefault.js +1 -1
  199. package/src/client/components/default/SettingsDefault.js +2 -2
  200. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  201. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  202. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  203. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  204. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  205. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  206. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  207. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  208. package/src/client/public/default/android-chrome-144x144.png +0 -0
  209. package/src/client/public/default/android-chrome-192x192.png +0 -0
  210. package/src/client/public/default/android-chrome-256x256.png +0 -0
  211. package/src/client/public/default/android-chrome-36x36.png +0 -0
  212. package/src/client/public/default/android-chrome-384x384.png +0 -0
  213. package/src/client/public/default/android-chrome-48x48.png +0 -0
  214. package/src/client/public/default/android-chrome-512x512.png +0 -0
  215. package/src/client/public/default/android-chrome-72x72.png +0 -0
  216. package/src/client/public/default/android-chrome-96x96.png +0 -0
  217. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  218. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  219. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  220. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  221. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  222. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  223. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  224. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  225. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  226. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  227. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  228. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  229. package/src/client/public/default/apple-touch-icon.png +0 -0
  230. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  231. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  232. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  233. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  234. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  235. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  236. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  237. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  238. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  239. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  240. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  241. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  242. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  243. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  244. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  245. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  246. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  247. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  248. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  264. package/src/client/public/default/assets/background/white.jpg +0 -0
  265. package/src/client/public/default/browserconfig.xml +1 -1
  266. package/src/client/public/default/favicon-16x16.png +0 -0
  267. package/src/client/public/default/favicon-32x32.png +0 -0
  268. package/src/client/public/default/favicon-48x48.png +0 -0
  269. package/src/client/public/default/favicon.ico +0 -0
  270. package/src/client/public/default/manifest.webmanifest +5 -5
  271. package/src/client/public/default/mstile-144x144.png +0 -0
  272. package/src/client/public/default/mstile-150x150.png +0 -0
  273. package/src/client/public/default/mstile-310x150.png +0 -0
  274. package/src/client/public/default/mstile-310x310.png +0 -0
  275. package/src/client/public/default/mstile-70x70.png +0 -0
  276. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  277. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  278. package/src/client/ssr/RootDocument.js +1 -1
  279. package/src/client/ssr/body/404.js +1 -1
  280. package/src/client/ssr/body/500.js +1 -1
  281. package/src/client/ssr/body/CacheControl.js +1 -1
  282. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  283. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  284. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  285. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  286. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  287. package/src/client/ssr/head/Css.js +1 -1
  288. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  289. package/src/client/ssr/head/DefaultScripts.js +1 -1
  290. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  291. package/src/client/ssr/head/Microdata.js +1 -1
  292. package/src/client/ssr/head/Production.js +1 -1
  293. package/src/client/ssr/head/Pwa.js +1 -1
  294. package/src/client/ssr/head/PwaItemledger.js +1 -1
  295. package/src/client/ssr/head/Seo.js +1 -1
  296. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  299. package/src/client/ssr/views/Cyberia404.js +7 -5
  300. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  301. package/src/client/ssr/views/Maintenance.js +1 -1
  302. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  303. package/src/client/ssr/views/Test.js +1 -1
  304. package/src/client/sw/core.sw.js +20 -14
  305. package/src/client-builder/client-build-docs.js +61 -46
  306. package/src/client-builder/client-build-live.js +2 -2
  307. package/src/client-builder/client-build.js +38 -30
  308. package/src/client-builder/client-dev-server.js +3 -3
  309. package/src/client-builder/client-icons.js +2 -2
  310. package/src/client-builder/ssr.js +3 -3
  311. package/src/client.build.js +3 -6
  312. package/src/client.dev.js +3 -6
  313. package/src/db/DataBaseProvider.js +186 -3
  314. package/src/db/mariadb/MariaDB.js +3 -2
  315. package/src/db/mongo/MongoBootstrap.js +273 -101
  316. package/src/db/mongo/MongooseDB.js +7 -8
  317. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  318. package/src/grpc/cyberia/grpc-server.js +1 -1
  319. package/src/index.js +79 -23
  320. package/src/mailer/EmailRender.js +2 -2
  321. package/src/mailer/MailerInterceptor.js +87 -0
  322. package/src/mailer/MailerProvider.js +50 -6
  323. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  324. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  325. package/src/projects/cyberia/catalog-cyberia.js +53 -10
  326. package/src/projects/cyberia/gemini-client.js +1 -1
  327. package/src/projects/cyberia/generate-saga.js +1 -1
  328. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  329. package/src/projects/cyberia/instance-data.js +1 -1
  330. package/src/projects/cyberia/ipfs-client.js +4 -4
  331. package/src/projects/cyberia/map-preview-generator.js +1 -1
  332. package/src/projects/cyberia/object-layer.js +1 -1
  333. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  334. package/src/projects/underpost/catalog-underpost.js +9 -1
  335. package/src/proxy.js +4 -7
  336. package/src/runtime/cyberia-client/Dockerfile +9 -4
  337. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  338. package/src/runtime/cyberia-server/Dockerfile +9 -4
  339. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  340. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  341. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  342. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  343. package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
  344. package/src/runtime/express/Express.js +32 -27
  345. package/src/runtime/lampp/Lampp.js +22 -70
  346. package/src/runtime/nginx/Nginx.js +1 -1
  347. package/src/runtime/wp/Wp.js +16 -48
  348. package/src/server/{catalog.js → build/catalog.js} +15 -5
  349. package/src/server/build/coverage.js +117 -0
  350. package/src/server/build/execution.js +402 -0
  351. package/src/server/build/package.js +465 -0
  352. package/src/server/build/testing.js +528 -0
  353. package/src/server/{dns.js → network/dns.js} +144 -11
  354. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  355. package/src/server/{middlewares.js → network/middlewares.js} +3 -3
  356. package/src/server/network/node-capability.js +98 -0
  357. package/src/server/{peer.js → network/peer.js} +3 -3
  358. package/src/server/{proxy.js → network/proxy.js} +7 -13
  359. package/src/server/network/router.js +356 -0
  360. package/src/server/{tls.js → network/tls.js} +2 -2
  361. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  362. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  363. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  364. package/src/server/{cri.js → ops/cri.js} +2 -2
  365. package/src/server/{cron.js → ops/cron.js} +349 -74
  366. package/src/server/ops/event-notification.js +284 -0
  367. package/src/server/{logger.js → ops/logger.js} +108 -23
  368. package/src/server/ops/monitoring.js +1724 -0
  369. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  370. package/src/server/{conf.js → runtime/conf.js} +513 -422
  371. package/src/server/runtime/config-scope.js +215 -0
  372. package/src/server/runtime/environment.js +149 -0
  373. package/src/server/{process.js → runtime/process.js} +153 -12
  374. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  375. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  376. package/src/server/{start.js → runtime/start.js} +128 -39
  377. package/src/server/{auth.js → security/auth.js} +7 -8
  378. package/src/server/security/container-storage.js +382 -0
  379. package/src/server/{crypto.js → security/crypto.js} +1 -1
  380. package/src/server/{selinux.js → security/selinux.js} +1 -1
  381. package/src/server/{backup.js → storage/backup.js} +17 -8
  382. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  383. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  384. package/src/server/storage/repository.js +68 -0
  385. package/src/server.js +4 -7
  386. package/src/ws/IoInterface.js +1 -1
  387. package/src/ws/IoServer.js +1 -1
  388. package/src/ws/core/core.ws.connection.js +1 -1
  389. package/src/ws/core/core.ws.emit.js +1 -1
  390. package/src/ws/core/core.ws.server.js +1 -1
  391. package/src/ws/default/default.ws.connection.js +1 -1
  392. package/src/ws/default/default.ws.emit.js +1 -1
  393. package/src/ws/default/default.ws.server.js +1 -1
  394. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  395. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  396. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  397. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  398. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  399. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +1 -1
  400. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  401. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  402. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  403. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  404. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  405. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  406. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  407. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  408. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  409. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  410. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  411. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  412. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  413. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  414. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  415. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +609 -55
  416. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  417. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  418. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  419. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  420. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  421. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  422. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  423. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  424. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  425. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  426. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  427. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  428. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  429. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  430. package/test/support/shell-harness.js +73 -0
  431. package/test/unit/build-template.test.js +97 -0
  432. package/test/unit/build-workflow-offline.test.js +132 -0
  433. package/test/unit/catalog.test.js +71 -0
  434. package/test/unit/client-build-docs.test.js +334 -0
  435. package/test/unit/conf-loading.test.js +303 -0
  436. package/test/unit/conf-resolution.test.js +979 -0
  437. package/test/unit/coverage-artifact.test.js +130 -0
  438. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  439. package/test/unit/deploy-env-resolution.test.js +38 -0
  440. package/test/unit/deploy-log-table.test.js +447 -0
  441. package/test/unit/execution-profiles.test.js +219 -0
  442. package/test/unit/fleet-sync-source.test.js +149 -0
  443. package/test/unit/logger-redaction.test.js +112 -0
  444. package/test/unit/package.test.js +403 -0
  445. package/test/unit/prepare-host.test.js +299 -0
  446. package/test/unit/process-environment.test.js +18 -0
  447. package/test/unit/propagation-message.test.js +78 -0
  448. package/test/unit/redeploy-plan.test.js +24 -0
  449. package/test/unit/release-bump.test.js +474 -0
  450. package/test/unit/ssh-output-redaction.test.js +28 -0
  451. package/test/unit/start-options.test.js +46 -0
  452. package/test/unit/test-tiers.test.js +193 -0
  453. package/vitest.config.js +58 -0
  454. package/.nycrc +0 -9
  455. package/deploy/lib/logging.sh +0 -96
  456. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  457. package/manifests/mongodb/pv-pvc.yaml +0 -59
  458. package/scripts/coverall-test-cyberia.sh +0 -24
  459. package/scripts/coverall-test.sh +0 -24
  460. package/scripts/link-local-underpost-cli.sh +0 -6
  461. package/src/api/user/user.build.js +0 -16
  462. package/src/cli/env.js +0 -177
  463. package/src/client/components/core/Blockchain.js +0 -41
  464. package/src/client/components/core/Blog.js +0 -9
  465. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  466. package/src/client/public/default/assets/background/dark.jpg +0 -0
  467. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  468. package/src/client/public/default/assets/background/white0.jpg +0 -0
  469. package/src/client/public/doc/favicon.ico +0 -0
  470. package/src/client/public/doc/sitemap +0 -148
  471. package/src/server/environment.js +0 -98
package/src/cli/run.js CHANGED
@@ -4,10 +4,12 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, pbcopy, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, pbcopy, shellArgumentFactory, shellCd, shellExec } from '../server/runtime/process.js';
8
+ import { cli, withExecutionProfile } from '../server/build/execution.js';
9
+ import { syncDeployPackages } from '../server/build/package.js';
10
+ import { RUNTIME_STATUS } from '../server/runtime/runtime-status.js';
8
11
  import {
9
12
  awaitDeployMonitor,
10
- buildKindPorts,
11
13
  clusterContextFactory,
12
14
  clusterTypeFactory,
13
15
  dispatchBuildInstanceEnv,
@@ -22,38 +24,41 @@ import {
22
24
  generateSecurePassword,
23
25
  instanceHttpRouteRulesFactory,
24
26
  instanceInterceptStatusesFactory,
27
+ instanceEnvFilePath,
25
28
  instancePortFactory,
26
29
  instanceProxyRoutesFactory,
27
30
  instanceStatusPageEntriesFactory,
28
- isDeployRunnerContext,
29
31
  loadConfInstances,
30
32
  loadProjectInstanceEnvBuilder,
31
33
  loadConfServerJson,
32
34
  loadReplicas,
33
- resolveDeployList,
34
35
  resolveEnvScoped,
35
36
  selectConfInstances,
36
37
  waitForPort,
37
38
  clusterInstancesFactory,
38
39
  deployTrafficEntriesFactory,
39
- curlStatusChainFactory,
40
+ publicIngressProbeFactory,
40
41
  hostIngressFactsFactory,
41
42
  hostRenderInstancesFactory,
42
43
  instanceTrafficPlanFactory,
43
44
  trafficTableRowsFactory,
44
45
  isTrafficServingFactory,
45
46
  nextTrafficFactory,
47
+ redeployPlanFactory,
46
48
  stopPlanFactory,
47
49
  trafficFromRoutingInfoFactory,
48
- } from '../server/conf.js';
49
- import { cronDeployIdResolve } from '../server/cron.js';
50
- import { getNpmRootPath, writeEnv } from '../server/environment.js';
51
- import { actionInitLog, loggerFactory } from '../server/logger.js';
50
+ } from '../server/runtime/conf.js';
51
+ import { buildKindPorts, deployRoutesExists, readDeployRoutes, resolveDeployList } from '../server/network/router.js';
52
+ import { 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';
52
55
 
53
56
  import fs from 'fs-extra';
54
57
  import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
55
58
 
56
59
  import os from 'os';
60
+ import nodePath from 'node:path';
61
+ import { domainContextFactory } from './domains.js';
57
62
  import Underpost from '../index.js';
58
63
  import dotenv from 'dotenv';
59
64
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
@@ -69,7 +74,7 @@ import {
69
74
  readHostInstanceRegistry,
70
75
  writeHostInstanceRegistry,
71
76
  writeHostServerConf,
72
- } from '../server/underpost-gateway.js';
77
+ } from '../server/network/underpost-gateway.js';
73
78
  const logger = loggerFactory(import.meta);
74
79
 
75
80
  /**
@@ -89,6 +94,9 @@ const logger = loggerFactory(import.meta);
89
94
  * @property {string} volumeHostPath - The host path for the volume.
90
95
  * @property {string} volumeMountPath - The mount path for the volume.
91
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`).
92
100
  * @property {string} containerName - The name of the container to run.
93
101
  * @property {string} namespace - The namespace to run in.
94
102
  * @property {string} timeoutResponse - The response timeout duration.
@@ -137,6 +145,7 @@ const logger = loggerFactory(import.meta);
137
145
  * @property {string} user - The user to run as.
138
146
  * @property {string} group - The group to use.
139
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.
140
149
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
141
150
  * @property {string} monitorStatus - The monitor status option.
142
151
  * @property {string} monitorStatusKindType - The monitor status kind type option.
@@ -183,6 +192,9 @@ const DEFAULT_OPTION = {
183
192
  volumeHostPath: '',
184
193
  volumeMountPath: '',
185
194
  imageName: '',
195
+ image: '',
196
+ runtimeImage: '',
197
+ versions: '',
186
198
  containerName: '',
187
199
  namespace: 'default',
188
200
  timeoutResponse: '',
@@ -231,6 +243,7 @@ const DEFAULT_OPTION = {
231
243
  user: '',
232
244
  group: '',
233
245
  pid: '',
246
+ repoEnginePrivate: '',
234
247
  disablePrivateConfUpdate: false,
235
248
  monitorStatus: '',
236
249
  monitorStatusKindType: '',
@@ -271,33 +284,6 @@ const DEFAULT_OPTION = {
271
284
  * @memberof UnderpostRun
272
285
  */
273
286
 
274
- // Secrets `sops-setup` onboards when no explicit list is passed: the full self-hosted data tier.
275
- // `mongodb-keyfile` is listed alongside `mongodb-secret` because the MongoDB StatefulSet mounts
276
- // it as a volume for intra-replica-set auth and will not start without it, so onboarding the
277
- // credentials alone would leave Mongo broken.
278
- const SOPS_SETUP_DEFAULT_SECRETS = ['postgres-secret', 'mariadb-secret', 'mongodb-secret', 'mongodb-keyfile'];
279
-
280
- /**
281
- * Produces a value for a Secret data key that has no origin seed file and no `--args` override.
282
- * Key-aware because the data tier does not want one shape of secret: a replica-set keyfile is a
283
- * long base64 blob, a username is an identifier, and everything else is a password.
284
- * @param {string} key - Secret data key (e.g. 'password', 'username', 'mongodb-keyfile').
285
- * @returns {string} Generated value.
286
- * @memberof UnderpostRun
287
- */
288
- const generateSeedValue = (key) => {
289
- if (key === 'username') return 'admin';
290
- // MongoDB keyfile: 6-1024 base64 characters shared by every replica-set member. Newlines are
291
- // stripped so the value round-trips identically through YAML and through
292
- // MongoBootstrap.readCredential, which strips them too.
293
- if (key === 'mongodb-keyfile')
294
- return shellExec(`openssl rand -base64 756`, { stdout: true, silent: true, disableLog: true }).replace(
295
- /\r?\n/g,
296
- '',
297
- );
298
- return generateSecurePassword(24);
299
- };
300
-
301
287
  class UnderpostRun {
302
288
  /**
303
289
  * @static
@@ -326,13 +312,12 @@ class UnderpostRun {
326
312
  throw new Error(`Invalid Kubernetes node name: ${options.nodeName}`);
327
313
  const env = options.dev ? 'development' : 'production';
328
314
  const requestedDeploys = `${path || options.deployId || ''}`.trim();
329
- const routerPath = './engine-private/deploy/dd.router';
330
315
  const confRoot = './engine-private/conf';
331
316
  const deployIds = [
332
317
  ...new Set(
333
318
  requestedDeploys
334
319
  ? resolveDeployList(requestedDeploys)
335
- : fs.existsSync(routerPath)
320
+ : deployRoutesExists()
336
321
  ? resolveDeployList('dd')
337
322
  : fs.existsSync(confRoot)
338
323
  ? fs
@@ -488,7 +473,7 @@ class UnderpostRun {
488
473
  * @memberof UnderpostRun
489
474
  */
490
475
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
491
- const baseCommand = options.dev ? 'node bin' : 'underpost';
476
+ const baseCommand = cli('underpost', { local: true });
492
477
  const mongoHosts = ['mongodb-0.mongodb-service'];
493
478
  let primaryMongoHost = 'mongodb-0.mongodb-service';
494
479
  const clusterType = clusterTypeFactory(options);
@@ -826,52 +811,21 @@ class UnderpostRun {
826
811
  return results;
827
812
  },
828
813
 
829
- /**
830
- * @method dev-hosts-expose
831
- * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
832
- * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
833
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
834
- * @memberof UnderpostRun
835
- */
836
- 'dev-hosts-expose': async (path, options = DEFAULT_OPTION) => {
837
- shellExec(`node bin deploy ${path} development --disable-update-deployment --disable-update-proxy --kubeadm`);
838
- // /etc/hosts is written here, not by `deploy`: that command has no
839
- // --etc-hosts option, and the `etc-hosts` runner already resolves a
840
- // deploy's hosts from its conf.server.json.
841
- await UnderpostRun.RUNNERS['etc-hosts']('', { ...options, deployId: path });
842
- },
843
-
844
- /**
845
- * @method dev-hosts-restore
846
- * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
847
- * @param {string} path - The input value, identifier, or path for the operation.
848
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
849
- * @memberof UnderpostRun
850
- */
851
- 'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
852
- // Rewrite /etc/hosts with the loopback block alone, dropping the deploy
853
- // host entries `dev-hosts-expose` (and the `cluster` runner) added.
854
- const hostListenResult = etcHostFactory([]);
855
- logger.info(hostListenResult.renderHosts);
856
- },
857
-
858
814
  /**
859
815
  * @method cluster-build
860
- * @description Build configuration for cluster deployment.
861
- * @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.
862
822
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
863
823
  * @memberof UnderpostRun
864
824
  */
865
825
  'cluster-build': (path, options = DEFAULT_OPTION) => {
866
- const nodeOptions =
867
- (options.nodeName ? ` --node-name ${options.nodeName}` : '') +
868
- (options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
869
826
  shellExec(`node bin run clean`);
870
- shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
871
- shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
872
- shellExec(`node bin env clean`);
873
- for (const deployId of fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').split(','))
874
- shellExec(`node bin new --default-conf --deploy-id ${deployId.trim()}`);
827
+ shellExec(`node bin app clean`);
828
+ for (const deployId of readDeployRoutes()) shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
875
829
  if (path === 'cmt') {
876
830
  shellExec(`git add . && underpost cmt . build cluster-build`);
877
831
  shellExec(`cd engine-private && git add . && underpost cmt . build cluster-build`);
@@ -887,14 +841,14 @@ class UnderpostRun {
887
841
  * @memberof UnderpostRun
888
842
  */
889
843
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
890
- const baseCommand = options.dev ? 'node bin' : 'underpost';
844
+ const baseCommand = cli('underpost', { local: true });
891
845
  shellExec(`npm run security:secrets`);
892
846
  const reportPath = './gitleaks-report.json';
893
847
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
894
848
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
895
849
  return;
896
850
  }
897
- shellExec(`${baseCommand} run pull`);
851
+ Underpost.repo.fastForwardEnginePair(baseCommand);
898
852
  shellExec(`${baseCommand} run shared-dir`);
899
853
 
900
854
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
@@ -964,14 +918,14 @@ class UnderpostRun {
964
918
  * @memberof UnderpostRun
965
919
  */
966
920
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
967
- const baseCommand = options.dev ? 'node bin' : 'underpost';
921
+ const baseCommand = cli('underpost', { local: true });
968
922
  shellExec(`npm run security:secrets`);
969
923
  const reportPath = './gitleaks-report.json';
970
924
  if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
971
925
  logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
972
926
  return;
973
927
  }
974
- shellExec(`${baseCommand} run pull`);
928
+ Underpost.repo.fastForwardEnginePair(baseCommand);
975
929
  shellExec(`${baseCommand} run shared-dir`);
976
930
 
977
931
  // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
@@ -1015,47 +969,51 @@ class UnderpostRun {
1015
969
  },
1016
970
  /**
1017
971
  * @method pull
1018
- * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
1019
- * @param {string} path - The input value, identifier, or path for the operation.
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.
1020
991
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1021
992
  * @memberof UnderpostRun
1022
993
  */
1023
- pull: (path, options = DEFAULT_OPTION) => {
994
+ pull: (path = '', options = DEFAULT_OPTION) => {
1024
995
  // shellExec is fail-fast by default — any non-zero exit throws and
1025
996
  // propagates up to the workflow step. No per-call flag required.
1026
- if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
1027
- fs.mkdirSync(`/home/dd`, { recursive: true });
1028
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1029
- } else {
1030
- shellExec(`underpost run clean`);
1031
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1032
- }
1033
- if (!fs.existsSync(`/home/dd/engine/engine-private`))
1034
- shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
1035
- silent: true,
1036
- });
1037
- else
1038
- shellExec(
1039
- `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
1040
- { silent: true },
1041
- );
1042
- },
1043
- /**
1044
- * @method release-deploy
1045
- * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
1046
- * @param {string} path - The input value, identifier, or path for the operation.
1047
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1048
- * @memberof UnderpostRun
1049
- */
1050
- 'release-deploy': (path, options = DEFAULT_OPTION) => {
1051
- actionInitLog();
1052
- shellExec(`underpost --version`);
1053
- shellCd(`/home/dd/engine`);
1054
- for (const _deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1055
- const deployId = _deployId.trim();
1056
- shellExec(`underpost run deploy ${deployId}`, { async: true });
1057
- }
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`);
1058
1015
  },
1016
+
1059
1017
  /**
1060
1018
  * @method ssh-deploy
1061
1019
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
@@ -1125,36 +1083,35 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1125
1083
  },
1126
1084
  /**
1127
1085
  * @method sync
1128
- * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`) using parameters parsed from `path` (deployId, replicas, versions, image, node).
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`.
1129
1089
  *
1130
1090
  * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
1131
1091
  * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
1132
1092
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
1133
- * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
1134
- * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
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.
1135
1100
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1136
1101
  * @memberof UnderpostRun
1137
1102
  */
1138
1103
  sync: async (path, options = DEFAULT_OPTION) => {
1139
- // Dev usage: node bin run --dev --build sync dd-default
1140
- const env = options.dev ? 'development' : 'production';
1104
+ const env = deployEnvFactory(options);
1141
1105
  options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
1142
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
1106
+ const baseCommand = cli('underpost', { local: true });
1143
1107
  const baseClusterCommand = options.dev ? ' --dev' : '';
1144
1108
  const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
1145
- const defaultPath = [
1146
- 'dd-default',
1147
- options.replicas,
1148
- ``,
1149
- ``,
1150
- !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
1151
- ];
1152
- let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
1153
- deployId = deployId ? deployId : defaultPath[0];
1154
- replicas = replicas ? replicas : defaultPath[1];
1155
- versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
1156
- image = image ? image : defaultPath[3];
1157
- node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
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 || '';
1158
1115
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
1159
1116
 
1160
1117
  if (image && !image.startsWith('localhost'))
@@ -1165,7 +1122,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1165
1122
  k3s: options.k3s,
1166
1123
  });
1167
1124
 
1168
- if (isDeployRunnerContext(path, options)) {
1125
+ if (deploying) {
1169
1126
  if (!options.disablePrivateConfUpdate) {
1170
1127
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
1171
1128
  if (!validVersion) throw new Error('Version mismatch');
@@ -1183,7 +1140,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1183
1140
  }
1184
1141
  }
1185
1142
 
1186
- const currentTraffic = isDeployRunnerContext(path, options)
1143
+ const currentTraffic = deploying
1187
1144
  ? Underpost.deploy.getCurrentTraffic(deployId, {
1188
1145
  namespace: options.namespace,
1189
1146
  env,
@@ -1207,14 +1164,12 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1207
1164
  });
1208
1165
 
1209
1166
  const ignorePods =
1210
- isDeployRunnerContext(path, options) && targetTraffic
1167
+ deploying && targetTraffic
1211
1168
  ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
1212
1169
  : [];
1213
1170
 
1214
1171
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
1215
- const cmdString = options.cmd
1216
- ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
1217
- : '';
1172
+ const cmdString = options.cmd ? ` --cmd ${shellArgumentFactory(options.cmd)}` : '';
1218
1173
  const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
1219
1174
 
1220
1175
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
@@ -1223,11 +1178,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1223
1178
  const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
1224
1179
  const gatewayApiFlags = Underpost.deploy.gatewayApiFlagsFactory(options);
1225
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
+
1226
1191
  // A direct sync owns the same gateway-first contract as the full cluster
1227
1192
  // runner. Build the host-side SSR documents before generating routes unless
1228
- // the caller explicitly selected a pre-built bundle workflow.
1229
- if (isDeployRunnerContext(path, options) && !options.skipFullBuild)
1230
- shellExec(`${baseCommand} client ${deployId} ${env} --ssr`);
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`);
1231
1197
 
1232
1198
  shellExec(
1233
1199
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
@@ -1237,7 +1203,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1237
1203
  }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag}${gatewayApiFlags} ${deployId} ${env}`,
1238
1204
  );
1239
1205
 
1240
- if (isDeployRunnerContext(path, options)) {
1206
+ if (deploying) {
1241
1207
  if (options.gatewayApi) {
1242
1208
  const namespace = options.namespace || 'default';
1243
1209
  const gatewayRoot = Underpost.deploy.underpostGatewayRootFactory(options);
@@ -1308,6 +1274,48 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1308
1274
  );
1309
1275
  },
1310
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
+
1311
1319
  /**
1312
1320
  * @method stop
1313
1321
  * @description Deletes colour-suffixed Deployments and their Services, leaving routing untouched.
@@ -1419,8 +1427,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1419
1427
  ? options.timezone
1420
1428
  : path
1421
1429
  ? path
1422
- : Underpost.env.get('TIME_ZONE', undefined, { disableLog: true })
1423
- ? Underpost.env.get('TIME_ZONE')
1430
+ : Underpost.host.store.get('TIME_ZONE', undefined, { disableLog: true })
1431
+ ? Underpost.host.store.get('TIME_ZONE')
1424
1432
  : process.env.TIME_ZONE
1425
1433
  ? process.env.TIME_ZONE
1426
1434
  : 'America/New_York';
@@ -1563,7 +1571,6 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1563
1571
  // Probe the exact public URL represented by each host/path row. PWA rows
1564
1572
  // can carry several configured paths in one cell, while instance rows
1565
1573
  // carry one; cache by URL so shared rows never repeat network work.
1566
- const shellArg = (value) => `'${`${value}`.replaceAll("'", "'\\''")}'`;
1567
1574
  const probeCache = new Map();
1568
1575
  const probePath = (host, routePath, tls) => {
1569
1576
  const normalizedPath = `${routePath || '/'}`.startsWith('/') ? `${routePath || '/'}` : `/${routePath}`;
@@ -1573,18 +1580,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1573
1580
  } catch {
1574
1581
  return { path: normalizedPath, url: '', statuses: ['000'] };
1575
1582
  }
1576
- if (!probeCache.has(url)) {
1577
- // -L follows the real redirect chain, -v supplies one response line
1578
- // per hop, -i keeps full response headers available, and -s removes
1579
- // only the progress meter. The body is discarded to keep a status
1580
- // report bounded even when a host returns a large application page.
1581
- const raw = shellExec(
1582
- `curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
1583
- `-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${shellArg(url)} 2>&1 || true`,
1584
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
1585
- );
1586
- probeCache.set(url, curlStatusChainFactory(raw));
1587
- }
1583
+ // Cache by URL so rows that share one never repeat network work.
1584
+ if (!probeCache.has(url)) probeCache.set(url, publicIngressProbeFactory(url));
1588
1585
  return { path: normalizedPath, url, statuses: probeCache.get(url) };
1589
1586
  };
1590
1587
 
@@ -1625,7 +1622,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1625
1622
 
1626
1623
  // Padded on the raw values, coloured afterwards: an ANSI escape counts
1627
1624
  // toward String.length and would skew every column right of it.
1628
- const columns = ['HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1625
+ const columns = ['#', 'HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
1629
1626
  const deploymentStatus = (status, includeServing = false) => {
1630
1627
  if (!status) return `unrouted - missing${includeServing ? ' not-serving' : ''}`;
1631
1628
  return [
@@ -1635,10 +1632,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1635
1632
  ...(includeServing ? [status.serving ? 'serving' : 'not-serving'] : []),
1636
1633
  ].join(' ');
1637
1634
  };
1638
- const cellOf = (row) => {
1635
+ const cellOf = (row, index) => {
1639
1636
  const facts = ingressFacts[row.host] || {};
1640
1637
  const pathStatus = row.probes.map((probe) => `${probe.path} [${probe.statuses.join('→')}]`).join(' ');
1641
1638
  return [
1639
+ `${index + 1}(${row.probes.length})`,
1642
1640
  row.host,
1643
1641
  pathStatus,
1644
1642
  row.kind,
@@ -1693,7 +1691,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1693
1691
  for (const cell of cells) console.log(line(cell, true));
1694
1692
  };
1695
1693
  for (const env of envs) {
1696
- 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));
1697
1696
  if (envCells.length > 0) printTable(envCells, `[${env.toUpperCase()}]`);
1698
1697
  }
1699
1698
  console.log('');
@@ -1978,22 +1977,33 @@ EOF
1978
1977
 
1979
1978
  /**
1980
1979
  * @method instance
1981
- * @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.
1982
1985
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1983
1986
  * @memberof UnderpostRun
1984
1987
  */
1985
1988
  instance: async (path = '', options = DEFAULT_OPTION) => {
1986
- const env = options.dev ? 'development' : 'production';
1989
+ const env = deployEnvFactory(options);
1987
1990
  options = {
1988
1991
  ...options,
1989
1992
  gatewayApi: gatewayApiEnabledFactory(options),
1990
1993
  namespace: options.namespace || 'default',
1991
1994
  };
1992
- const baseCommand = options.dev ? 'node bin' : 'underpost';
1995
+ const baseCommand = cli('underpost', { local: true });
1993
1996
  const baseClusterCommand = options.dev ? ' --dev' : '';
1994
- let [deployId, id, replicas] = path.split(',');
1995
- 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');
1996
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 }));
1997
2007
  const { liveTrafficById, targetTrafficById, serving } = instanceTrafficPlanFactory({
1998
2008
  instances: confInstances,
1999
2009
  requestedTraffic: options.traffic,
@@ -2089,7 +2099,6 @@ EOF
2089
2099
  const targetTraffic = targetTrafficById[instance.id];
2090
2100
  const podId = `${_deployId}-${env}-${targetTraffic}`;
2091
2101
  const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
2092
- Underpost.deploy.configMap(env, options.namespace);
2093
2102
  shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
2094
2103
  shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
2095
2104
  for (const _volume of _volumes)
@@ -2219,11 +2228,10 @@ EOF
2219
2228
 
2220
2229
  /**
2221
2230
  * @method deploy-key
2222
- * @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.
2223
2232
  * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
2224
2233
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2225
2234
  * @param {string} options.user - The user for which to copy the deploy key.
2226
- * @param {string} options.deployId - The deployment identifier associated with the deploy key.
2227
2235
  * @memberof UnderpostRun
2228
2236
  */
2229
2237
  'deploy-key': (path, options = DEFAULT_OPTION) => {
@@ -2232,13 +2240,18 @@ EOF
2232
2240
  shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
2233
2241
  return;
2234
2242
  }
2235
- if (!options.user || !options.deployId) {
2236
- 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`);
2237
2250
  return;
2238
2251
  }
2239
2252
  const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
2240
2253
  fs.mkdirSync('/home/dd/tmp', { recursive: true });
2241
- fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
2254
+ fs.copyFileSync(sourcePath, targetPath);
2242
2255
  logger.info(`Copied deploy key to ${targetPath}`);
2243
2256
  if (options.copy) pbcopy(targetPath);
2244
2257
  },
@@ -2569,11 +2582,11 @@ EOF
2569
2582
  // mode this build ran. The default/template instance owns the committed
2570
2583
  // source files, so only its current-mode file is idempotently refreshed.
2571
2584
  if (instance.templateId) {
2572
- const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
2585
+ const instanceEnvDir = nodePath.dirname(instanceEnvFilePath(deployId, _id, env));
2573
2586
  fs.mkdirpSync(instanceEnvDir);
2574
2587
  const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
2575
2588
  for (const targetEnv of envsToWrite) {
2576
- const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
2589
+ const templateEnvPath = instanceEnvFilePath(deployId, instance.templateId, targetEnv);
2577
2590
  if (!fs.existsSync(templateEnvPath))
2578
2591
  throw new Error(`[instance-build-manifest] Missing canonical env file: ${templateEnvPath}`);
2579
2592
  const baseEnv = dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8'));
@@ -2585,7 +2598,7 @@ EOF
2585
2598
  containerDeployId: `${_deployId}-${targetEnv}`,
2586
2599
  builders: instanceEnvBuilder ? { [deployId]: instanceEnvBuilder } : {},
2587
2600
  });
2588
- writeEnv(`${instanceEnvDir}/${targetEnv}.env`, builtEnv);
2601
+ writeEnv(instanceEnvFilePath(deployId, _id, targetEnv), builtEnv);
2589
2602
  }
2590
2603
  logger.info('[instance-build-manifest] Instance env written', {
2591
2604
  dir: instanceEnvDir,
@@ -2667,7 +2680,6 @@ EOF
2667
2680
  * @memberof UnderpostRun
2668
2681
  */
2669
2682
  'host-update': async (path, options = DEFAULT_OPTION) => {
2670
- // const baseCommand = options.dev ? 'node bin' : 'underpost';
2671
2683
  shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
2672
2684
  shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
2673
2685
  },
@@ -2734,7 +2746,7 @@ EOF`);
2734
2746
  * @memberof UnderpostRun
2735
2747
  */
2736
2748
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
2737
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2749
+ const baseCommand = cli('underpost', { local: true });
2738
2750
  const baseClusterCommand = options.dev ? ' --dev' : '';
2739
2751
  const currentImage = options.imageName
2740
2752
  ? options.imageName
@@ -2831,15 +2843,15 @@ EOF`);
2831
2843
  * @memberof UnderpostRun
2832
2844
  */
2833
2845
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
2834
- const defaultUsername = Underpost.env.get('GITHUB_USERNAME');
2835
- 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');
2836
2848
  const validPath = path && path.split(',').length;
2837
2849
  const [username, email] = validPath ? path.split(',') : [defaultUsername, defaultEmail];
2838
2850
  if (validPath) {
2839
- Underpost.env.set('GITHUB_USERNAME', username);
2840
- Underpost.env.set('GITHUB_EMAIL', email);
2841
- Underpost.env.get('GITHUB_USERNAME');
2842
- Underpost.env.get('GITHUB_EMAIL');
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');
2843
2855
  }
2844
2856
  shellExec(
2845
2857
  `git config --global credential.helper "" && ` +
@@ -2870,7 +2882,7 @@ EOF`);
2870
2882
 
2871
2883
  /**
2872
2884
  * @method promote
2873
- * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
2885
+ * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.routes` for 'dd', or a specific ID).
2874
2886
  * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
2875
2887
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
2876
2888
  * (cert-manager Certificate resources) for each affected deployment.
@@ -2906,7 +2918,7 @@ EOF`);
2906
2918
  };
2907
2919
 
2908
2920
  if (inputDeployId === 'dd') {
2909
- for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
2921
+ for (const deployId of readDeployRoutes()) {
2910
2922
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
2911
2923
  namespace: options.namespace,
2912
2924
  env: inputEnv,
@@ -2932,44 +2944,6 @@ EOF`);
2932
2944
  applyCerts(inputDeployId, targetTraffic);
2933
2945
  }
2934
2946
  },
2935
- /**
2936
- * @method metrics
2937
- * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
2938
- * @param {string} path - The input value, identifier, or path for the operation.
2939
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2940
- * @memberof UnderpostRun
2941
- */
2942
- metrics: async (path, options = DEFAULT_OPTION) => {
2943
- if (path === 'server') {
2944
- shellExec(
2945
- `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2946
- );
2947
- await timer(2000);
2948
-
2949
- shellExec(`kubectl patch deployment metrics-server -n kube-system \
2950
- --type='json' \
2951
- -p='[
2952
- {
2953
- "op":"add",
2954
- "path":"/spec/template/spec/containers/0/args/-",
2955
- "value":"--kubelet-insecure-tls"
2956
- }
2957
- ]'`);
2958
- shellExec(`kubectl scale deployment metrics-server \
2959
- -n kube-system \
2960
- --replicas=1`);
2961
-
2962
- return;
2963
- }
2964
- const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
2965
- let hosts = [];
2966
- for (const deployId of deployList) {
2967
- const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
2968
- hosts = hosts.concat(Object.keys(confServer));
2969
- }
2970
- shellExec(`node bin cluster --prom ${hosts.join(',')}`);
2971
- shellExec(`node bin cluster --grafana`);
2972
- },
2973
2947
  /**
2974
2948
  * @method cluster
2975
2949
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB,
@@ -2982,21 +2956,24 @@ EOF`);
2982
2956
  * references, and every host is written to `/etc/hosts` so the operator's browser reaches the PWA at
2983
2957
  * `https://<host>` on the local machine.
2984
2958
  *
2985
- * 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
2986
2960
  * own `conf.instances.json`, so an id only runs where its deploy declares it, and each one is deployed after its
2987
2961
  * deploy's default workload is serving — an instance reads the parent's world configuration over the parent's
2988
2962
  * gRPC ClusterIP at boot. Instance hosts share the deploy's environment: the same self-signed certificates and
2989
2963
  * `/etc/hosts` pass in development, the same cert-manager issuance in production.
2990
- * @param {string} path - `<runtime-image>,<deploy-list>[,<instance-list>]` — `+`-separated lists, e.g.
2991
- * `express,dd-cyberia,mmo-server` or `express,dd-cyberia+dd-core,mmo-server+mmo-client`. An instance list entry
2992
- * may be a template id (`mmo-server`), which selects its whole variant family.
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.
2993
2970
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2994
2971
  * @memberof UnderpostRun
2995
2972
  */
2996
2973
  cluster: async (path = '', options = DEFAULT_OPTION) => {
2997
2974
  const { underpostRoot } = options;
2998
- const env = options.dev ? 'development' : 'production';
2999
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2975
+ const env = deployEnvFactory(options);
2976
+ const baseCommand = cli('underpost', { local: true });
3000
2977
  const baseClusterCommand = options.dev ? ' --dev' : '';
3001
2978
  const clusterType = clusterTypeFactory(options, 'kubeadm');
3002
2979
  shellCd(`/home/dd/engine`);
@@ -3004,23 +2981,15 @@ EOF`);
3004
2981
  await timer(5000);
3005
2982
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
3006
2983
  await timer(5000);
3007
- let [runtimeImage, deployList, instanceListId] =
3008
- path && path.trim() && path.split(',')
3009
- ? path.split(',')
3010
- : [
3011
- 'express',
3012
- fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
3013
- '',
3014
- ];
2984
+ const runtimeImage = options.runtimeImage || 'express';
2985
+ const instanceListId = options.instanceId || '';
3015
2986
  // shellExec(
3016
2987
  // `${baseCommand} image${baseClusterCommand} --build ${
3017
2988
  // runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
3018
2989
  // } --${clusterType}`,
3019
2990
  // );
3020
- if (!deployList) {
3021
- deployList = [];
3022
- logger.warn('No deploy list provided');
3023
- } else deployList = deployList.split('+');
2991
+ const deployList = options.deployId ? resolveDeployList(options.deployId) : readDeployRoutes();
2992
+ if (deployList.length === 0) logger.warn('No deploy list provided');
3024
2993
  await timer(5000);
3025
2994
  // --reset-mongodb wipes the retained hostPath volumes before the rollout.
3026
2995
  // This workflow already tore the node down and re-imports every database
@@ -3097,10 +3066,13 @@ EOF`);
3097
3066
  const hostListenResult = etcHostFactory(hosts);
3098
3067
  logger.info(hostListenResult.renderHosts);
3099
3068
  }
3100
- const version = 'v3.3.0';
3069
+ const version = 'v3.3.73';
3101
3070
  const instanceOptionsFactory = (deployId, instanceId) => ({
3102
3071
  ...options,
3103
3072
  ...clusterContextFactory(clusterType),
3073
+ deployId,
3074
+ instanceId,
3075
+ replicas: options.replicas || 1,
3104
3076
  gatewayApi,
3105
3077
  gatewayBootstrapComplete: true,
3106
3078
  tls: true,
@@ -3147,17 +3119,16 @@ EOF`);
3147
3119
  sudo rm -rf ./engine-private/, \
3148
3120
  node bin clone underpostnet/engine-cyberia-private, \
3149
3121
  sudo mv ./engine-cyberia-private ./engine-private, \
3150
- node bin env dd-cyberia ${env}, \
3151
- node ./engine-private/itc-scripts/dd-cyberia-0.js, \
3122
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3152
3123
  sudo chown -R dd:dd /home/dd/engine/src/client/public/cyberia, \
3153
- node bin env dd-cyberia ${env}, \
3124
+ node bin app load --env ${env} --args deploy-id=dd-cyberia, \
3154
3125
  node bin client dd-cyberia ${env}, \
3155
3126
  node bin start dd-cyberia ${env} --run'`
3156
3127
  : '');
3157
3128
  deployFlagsById[deployId] = deployFlags;
3158
3129
  // SSR status and context documents belong to the ingress bootstrap, so
3159
3130
  // build them on the host before any workload Deployment is submitted.
3160
- shellExec(`${baseCommand} client ${deployId} ${env}`);
3131
+ shellExec(`${baseCommand} client ${deployId} --env ${env}`);
3161
3132
  shellExec(`${baseCommand} deploy ${deployId} ${env} --build-manifest ${deployFlags}`);
3162
3133
  // Seed the static tree before the routes exist, so every status page and
3163
3134
  // intercepted context the manifests just pointed at resolves from the
@@ -3260,10 +3231,7 @@ EOF`);
3260
3231
  // keep serving the custom fallback until the atomic promotion completes.
3261
3232
  for (const instanceId of instancesByDeployId[deployId].ids) {
3262
3233
  logger.info('[cluster] Deploying custom instance', { deployId, instanceId, env, clusterType });
3263
- await UnderpostRun.RUNNERS.instance(
3264
- `${deployId},${instanceId},${options.replicas || 1}`,
3265
- instanceOptionsFactory(deployId, instanceId),
3266
- );
3234
+ await UnderpostRun.RUNNERS.instance('', instanceOptionsFactory(deployId, instanceId));
3267
3235
  }
3268
3236
  }
3269
3237
  logger.info('[cluster] Ingress stack deployed', {
@@ -3655,6 +3623,10 @@ EOF`);
3655
3623
  /**
3656
3624
  * @method deploy
3657
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.
3658
3630
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
3659
3631
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3660
3632
  * @memberof UnderpostRun
@@ -3662,19 +3634,57 @@ EOF`);
3662
3634
  deploy: async (path, options = DEFAULT_OPTION) => {
3663
3635
  const deployId = path;
3664
3636
  const env = options.dev ? 'development' : 'production';
3637
+ const namespace = options.namespace || 'default';
3665
3638
  const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
3666
3639
  if (!validVersion) throw new Error('Version mismatch');
3667
- const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace, env });
3668
- const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
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
+ });
3669
3651
  const ignorePods = Underpost.kubectl
3670
- .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
3652
+ .get(`${deployId}-${env}-${targetTraffic}`, 'pods', namespace)
3671
3653
  .map((p) => p.NAME);
3672
3654
 
3673
- 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}`);
3674
3684
 
3675
- await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
3685
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, namespace);
3676
3686
 
3677
- Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
3687
+ Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, namespace, options);
3678
3688
  },
3679
3689
 
3680
3690
  /**
@@ -3764,7 +3774,10 @@ EOF`);
3764
3774
  }`;
3765
3775
  shellExec(cmd, { async: true });
3766
3776
  }
3767
- 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`);
3768
3781
  {
3769
3782
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
3770
3783
 
@@ -3772,7 +3785,8 @@ EOF`);
3772
3785
  async: true,
3773
3786
  });
3774
3787
  }
3775
- 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`);
3776
3790
  shellExec(
3777
3791
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
3778
3792
  );
@@ -3787,7 +3801,7 @@ EOF`);
3787
3801
  */
3788
3802
  service: async (path = '', options = DEFAULT_OPTION) => {
3789
3803
  const env = options.dev ? 'development' : 'production';
3790
- const baseCommand = options.dev ? 'node bin' : 'underpost';
3804
+ const baseCommand = cli('underpost', { local: true });
3791
3805
  const baseClusterCommand = options.dev ? ' --dev' : '';
3792
3806
  shellCd(`/home/dd/engine`);
3793
3807
  let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
@@ -3892,26 +3906,6 @@ EOF`);
3892
3906
  logger.info(hostListenResult.renderHosts);
3893
3907
  },
3894
3908
 
3895
- /**
3896
- * @method sh
3897
- * @description Enables remote control for the Kitty terminal emulator.
3898
- * @param {string} path - The input value, identifier, or path for the operation.
3899
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3900
- * @memberof UnderpostRun
3901
- */
3902
- sh: async (path = '', options = DEFAULT_OPTION) => {
3903
- let [operator, arg0, arg1] = path.split(',');
3904
- if (operator == 'copy') {
3905
- shellExec(
3906
- `kitty @ get-text ${arg0 === 'all' ? '--match all' : '--self'} --extent all${
3907
- arg1 === 'ansi' ? ' --ansi yes' : ''
3908
- } | kitty +kitten clipboard`,
3909
- );
3910
- return;
3911
- }
3912
- shellExec(`kitty -o allow_remote_control=yes`);
3913
- },
3914
-
3915
3909
  /**
3916
3910
  * @method log
3917
3911
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
@@ -3970,28 +3964,44 @@ EOF`);
3970
3964
 
3971
3965
  // Services
3972
3966
  logger.info('Process info');
3973
- 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
+ });
3974
3970
  logger.info('Command line');
3975
- shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`);
3971
+ shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`, {
3972
+ silentOnError: true,
3973
+ });
3976
3974
  logger.info('Executable path');
3977
3975
  shellExec(`sudo readlink -f /proc/${pid}/exe`);
3978
3976
  logger.info('Working directory');
3979
- shellExec(`sudo readlink -f /proc/${pid}/cwd`);
3977
+ shellExec(`sudo readlink -f /proc/${pid}/cwd`, {
3978
+ silentOnError: true,
3979
+ });
3980
3980
  logger.info('Environment variables (first 200)');
3981
- 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
+ });
3982
3984
 
3983
3985
  // Parent
3984
3986
  logger.info('Parent process');
3985
- 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
+ });
3986
3992
  console.log(parentInfo);
3987
3993
  const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
3988
3994
  if (ppidMatch) {
3989
3995
  const ppid = ppidMatch.trim().split(/\s+/)[1];
3990
3996
  logger.info(`Parent PID: ${ppid}`);
3991
- shellExec(`ps -fp ${ppid}`);
3997
+ shellExec(`ps -fp ${ppid}`, {
3998
+ silentOnError: true,
3999
+ });
3992
4000
  }
3993
4001
  logger.info('Process tree');
3994
- shellExec(`pstree -s ${pid}`);
4002
+ shellExec(`pstree -s ${pid}`, {
4003
+ silentOnError: true,
4004
+ });
3995
4005
  },
3996
4006
 
3997
4007
  /**
@@ -4036,7 +4046,7 @@ EOF`);
4036
4046
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
4037
4047
  // Note: use recomendation empty deploy cluster: node bin --dev cluster
4038
4048
  const env = options.dev ? 'development' : 'production';
4039
- const baseCommand = options.dev ? 'node bin' : 'underpost';
4049
+ const baseCommand = cli('underpost', { local: true });
4040
4050
  const baseClusterCommand = options.dev ? ' --dev' : '';
4041
4051
  const inputs = path ? path.split(',') : [];
4042
4052
  const deployId = inputs[0] ? inputs[0] : 'dd-test';
@@ -4045,10 +4055,12 @@ EOF`);
4045
4055
  : [
4046
4056
  `npm install -g npm@11.2.0`,
4047
4057
  `npm install -g underpost`,
4048
- `${baseCommand} secret underpost --create-from-env`,
4058
+ `${baseCommand} host load`,
4049
4059
  `${baseCommand} start --build --run ${deployId} ${env}`,
4050
4060
  ];
4051
- 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
+ );
4052
4064
  },
4053
4065
 
4054
4066
  /**
@@ -4179,26 +4191,7 @@ EOF`);
4179
4191
 
4180
4192
  shellCd('/home/dd/engine');
4181
4193
  },
4182
- /**
4183
- * @method pull-rocky-image
4184
- * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
4185
- * @param {string} path - The input value, identifier, or path for the operation.
4186
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4187
- * @memberof UnderpostRun
4188
- */
4189
- 'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
4190
- shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
4191
- },
4192
- /**
4193
- * @method rmi
4194
- * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
4195
- * @param {string} path - The input value, identifier, or path for the operation.
4196
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4197
- * @memberof UnderpostRun
4198
- */
4199
- rmi: (path, options = DEFAULT_OPTION) => {
4200
- shellExec(`podman rmi $(podman images -qa) --force`);
4201
- },
4194
+
4202
4195
  /**
4203
4196
  * @method kill
4204
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.
@@ -4244,333 +4237,6 @@ EOF`);
4244
4237
  if (options.copy) pbcopy(password);
4245
4238
  else console.log(password);
4246
4239
  },
4247
- /**
4248
- * @method sops-setup
4249
- * @description End-to-end SOPS/Age onboarding for a host: installs tooling, generates the Age
4250
- * keypair and creation rules, pins the key path for non-interactive runs, encrypts the
4251
- * requested Secrets into the Git-tracked store, then validates and applies them.
4252
- *
4253
- * Every step is idempotent and re-runnable. Notably it delegates key generation to
4254
- * `secret sops --init` rather than calling `age-keygen` directly: a bare `age-keygen -o`
4255
- * overwrites an existing key, which would orphan every manifest already encrypted to the
4256
- * previous recipient with no way to recover them.
4257
- *
4258
- * On a host that pulled a store created elsewhere, the freshly generated key is not a recipient
4259
- * of the inherited manifests. `init()` registers this host in the creation rules so what it
4260
- * encrypts from here on stays readable, but existing manifests can only be re-keyed from a host
4261
- * that still holds a decrypting key. That case is reported per secret and then raised by the
4262
- * apply pre-flight with the available remedies, rather than surfacing as a sops decrypt error.
4263
- *
4264
- * Onboards the whole self-hosted data tier by default — PostgreSQL, MariaDB, and MongoDB
4265
- * (`postgres-secret`, `mariadb-secret`, `mongodb-secret`, `mongodb-keyfile`). The MongoDB
4266
- * keyfile is included because the StatefulSet mounts it for intra-replica-set auth and will
4267
- * not start without it. Pass an explicit comma-separated list to narrow the set.
4268
- *
4269
- * Secret values are resolved per data key, in order:
4270
- * 1. the origin seed file, when one exists (`engine-private/postgresql-password`) — this is
4271
- * the real onboarding path, carrying the credential the cluster already runs on;
4272
- * 2. `--args` as `key=value` pairs, for a value supplied by the operator;
4273
- * 3. a freshly generated value: a base64 keyfile for `mongodb-keyfile`, `admin` for a
4274
- * `username`, otherwise a 24-character secure password.
4275
- *
4276
- * Plaintext manifests are written by Node under `/dev/shm` at mode 600 and shredded by
4277
- * `encrypt()`. They are never emitted through a shell heredoc, which would place the
4278
- * credential in the command string and therefore in the process table and the command log.
4279
- *
4280
- * Usage:
4281
- * underpost run sops-setup # postgres + mariadb + mongo
4282
- * underpost run sops-setup mongodb-secret,mongodb-keyfile --namespace prod
4283
- * underpost run sops-setup postgres-secret --args "password=s3cr3t"
4284
- * underpost run sops-setup --dry-run # stop before mutating cluster
4285
- * underpost run sops-setup --force # replace stored manifests
4286
- * @param {string} path - Comma-separated Secret names to onboard. Defaults to the full data
4287
- * tier: postgres-secret, mariadb-secret, mongodb-secret, mongodb-keyfile.
4288
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4289
- * @param {string} options.namespace - Target namespace for the store and the apply (default: 'default').
4290
- * @param {string} options.args - Comma-separated `key=value` overrides for Secret data keys.
4291
- * @param {boolean} options.dryRun - Validate and server-dry-run only; never apply.
4292
- * @param {boolean} options.force - Replace encrypted manifests that already exist.
4293
- * @memberof UnderpostRun
4294
- */
4295
- 'sops-setup': (path = '', options = DEFAULT_OPTION) => {
4296
- const namespace = options.namespace || 'default';
4297
- const secretNames = (path || SOPS_SETUP_DEFAULT_SECRETS.join(','))
4298
- .split(',')
4299
- .map((name) => name.trim())
4300
- .filter(Boolean);
4301
-
4302
- // `--args key=value,key2=value2` overrides, applied to any secret that declares that key.
4303
- const overrides = `${options.args || ''}`.split(',').reduce((acc, pair) => {
4304
- const separator = pair.indexOf('=');
4305
- if (separator > 0) acc[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
4306
- return acc;
4307
- }, {});
4308
-
4309
- logger.info('sops-setup', { secretNames, namespace, dryRun: !!options.dryRun, force: !!options.force });
4310
-
4311
- // 1. Host tooling, then keypair + creation rules. Both no-op when already present.
4312
- Underpost.secret.sops.installTooling();
4313
- Underpost.secret.sops.init();
4314
-
4315
- // 2. Pin the resolved key path for non-interactive runs (systemd units, CronJobs, sudo).
4316
- // Written with the concrete path rather than a guessed default, because `sudo` resets
4317
- // HOME and a wrong guess surfaces later as an opaque decrypt failure.
4318
- const keyFile = Underpost.secret.sops.keyFile();
4319
- shellExec(
4320
- `sudo tee /etc/profile.d/underpost-sops.sh >/dev/null <<'UNDERPOST_SOPS_ENV_EOF'
4321
- export SOPS_AGE_KEY_FILE="\${SOPS_AGE_KEY_FILE:-${keyFile}}"
4322
- UNDERPOST_SOPS_ENV_EOF`,
4323
- );
4324
- shellExec(`sudo chmod 644 /etc/profile.d/underpost-sops.sh`);
4325
-
4326
- // 3. Build and encrypt each requested Secret.
4327
- const stageDir = '/dev/shm/underpost-secrets';
4328
- const held = Underpost.secret.sops.localRecipients();
4329
- fs.ensureDirSync(stageDir);
4330
- fs.chmodSync(stageDir, 0o700);
4331
- try {
4332
- for (const name of secretNames) {
4333
- const stored = Underpost.secret.sops.has(name, namespace);
4334
- if (stored && !options.force) {
4335
- // A stored manifest this host cannot open is present but unusable here, so reporting it
4336
- // as onboarded would send the operator on to an apply that is guaranteed to fail.
4337
- if (Underpost.secret.sops.decryptable(Underpost.secret.sops.manifestPath(name, namespace), held))
4338
- logger.info(`${name} is already onboarded in ns/${namespace}; skipping (use --force to replace)`);
4339
- else
4340
- logger.warn(
4341
- `${name} is stored in ns/${namespace} but is sealed to an Age recipient this host does not hold; ` +
4342
- `skipping. Adopt the store's key, re-key it from a host that holds one, or re-onboard from the ` +
4343
- `origin seed files with --force.`,
4344
- );
4345
- continue;
4346
- }
4347
-
4348
- // Data keys come from the secret's origin seed contract, so an onboarded manifest
4349
- // carries exactly the keys the workload's secretKeyRef already expects.
4350
- const seedSources = Underpost.secret.sops.seedSources(name);
4351
- const dataKeys = Object.keys(seedSources).length > 0 ? Object.keys(seedSources) : ['password'];
4352
- const stringData = {};
4353
- for (const key of dataKeys) {
4354
- const seedPath = seedSources[key];
4355
- if (seedPath && fs.existsSync(seedPath)) {
4356
- stringData[key] = fs.readFileSync(seedPath, 'utf8').trim();
4357
- logger.info(`${name}.${key} seeded from ${seedPath}`);
4358
- } else if (overrides[key] !== undefined) {
4359
- stringData[key] = overrides[key];
4360
- logger.info(`${name}.${key} taken from --args`);
4361
- } else {
4362
- stringData[key] = generateSeedValue(key);
4363
- // Replacing a stored manifest with a value nothing seeded means the credential the
4364
- // running datastore still authenticates against is being thrown away.
4365
- if (stored)
4366
- logger.warn(
4367
- `${name}.${key} generated while replacing the stored manifest — no seed file at ` +
4368
- `${seedPath || '(unmapped)'} and no --args override. The running datastore keeps its old ` +
4369
- `credential until this value is applied to it; pass --args "${key}=<value>" to keep the ` +
4370
- `existing one.`,
4371
- );
4372
- else logger.info(`${name}.${key} generated`);
4373
- }
4374
- }
4375
-
4376
- const stagePath = `${stageDir}/${name}.yaml`;
4377
- fs.outputFileSync(
4378
- stagePath,
4379
- [
4380
- 'apiVersion: v1',
4381
- 'kind: Secret',
4382
- 'metadata:',
4383
- ` name: ${name}`,
4384
- ` namespace: ${namespace}`,
4385
- ' labels:',
4386
- ' app.kubernetes.io/managed-by: underpost',
4387
- 'type: Opaque',
4388
- 'stringData:',
4389
- // Single-quoted YAML scalars with doubled internal quotes: values are generated or
4390
- // operator-supplied and may contain characters YAML would otherwise interpret.
4391
- ...Object.entries(stringData).map(([key, value]) => ` ${key}: '${`${value}`.replace(/'/g, "''")}'`),
4392
- '',
4393
- ].join('\n'),
4394
- 'utf8',
4395
- );
4396
- fs.chmodSync(stagePath, 0o600);
4397
- // encrypt() stages, validates, moves into place, and shreds the plaintext source.
4398
- Underpost.secret.sops.encrypt(stagePath, namespace, options);
4399
- }
4400
- } finally {
4401
- // Defense in depth: encrypt() shreds each source, but a throw mid-loop must not leave a
4402
- // plaintext manifest sitting in shared memory.
4403
- fs.removeSync(stageDir);
4404
- }
4405
-
4406
- Underpost.secret.sops.list();
4407
-
4408
- // 4. Validate every manifest in the namespace, then apply unless this is a dry run.
4409
- Underpost.secret.sops.apply(namespace, { dryRun: true });
4410
- if (options.dryRun) return logger.info('--dry-run: validated only, cluster left unchanged');
4411
- Underpost.secret.sops.apply(namespace);
4412
- },
4413
- /**
4414
- * @method sops-status
4415
- * @description Reports the live state of the SOPS/Age secret system: host tooling, the Age
4416
- * key and its recipient, the committed creation rules, every stored manifest with whether the
4417
- * local key can open it and whether the cluster still matches, and which managed Secrets are
4418
- * onboarded versus still seeding from their origin path.
4419
- *
4420
- * Read-only and safe to run anywhere. Decryption happens only for the drift check, only for
4421
- * manifests the local key is a recipient of, and only into `kubectl diff` with its output
4422
- * discarded — no secret value is ever printed or written to disk.
4423
- *
4424
- * Usage:
4425
- * underpost run sops-status # every managed key, ns default
4426
- * underpost run sops-status mongo # partial match: both mongo keys
4427
- * underpost run sops-status --namespace prod # every managed key in ns prod
4428
- * @param {string} path - Comma-separated managed Secret keys to report on; empty reports all.
4429
- * Matched as case-insensitive substrings (`mongo` selects mongodb-secret and mongodb-keyfile).
4430
- * Filters both the stored-manifest listing and the coverage table.
4431
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4432
- * @param {string} options.namespace - Namespace to inspect (DEFAULT_OPTION scheme, default 'default').
4433
- * @memberof UnderpostRun
4434
- */
4435
- 'sops-status': (path = '', options = DEFAULT_OPTION) => {
4436
- const sops = Underpost.secret.sops;
4437
- // `--namespace` selects the namespace (DEFAULT_OPTION scheme); `path` narrows which managed
4438
- // Secret keys to report on, so the two axes stay independent.
4439
- const namespace = options.namespace || 'default';
4440
- const manageSecretKeyFilter = path
4441
- .split(',')
4442
- .map((key) => key.trim().toLowerCase())
4443
- .filter(Boolean);
4444
- // Partial, case-insensitive substring match, so `mongo` reaches both `mongodb-secret` and
4445
- // `mongodb-keyfile` without having to spell either out.
4446
- const matchesKeyFilter = (name) =>
4447
- manageSecretKeyFilter.length === 0 || manageSecretKeyFilter.some((key) => name.toLowerCase().includes(key));
4448
- const mark = (ok) => (ok ? 'yes' : 'no');
4449
-
4450
- // ── Tooling ────────────────────────────────────────────────────────────
4451
- const version = (bin, flag) =>
4452
- sops.hasBinary(bin)
4453
- ? shellExec(`${bin} ${flag} 2>/dev/null | head -1`, { stdout: true, silent: true, disableLog: true }).trim()
4454
- : '(not installed)';
4455
- logger.info(
4456
- '[sops-status] Tooling\n' +
4457
- ` sops ${version('sops', '--version')}\n` +
4458
- ` age ${version('age', '--version')}\n` +
4459
- ` age-keygen ${sops.hasBinary('age-keygen') ? 'installed' : '(not installed)'}`,
4460
- );
4461
-
4462
- // ── Age key ────────────────────────────────────────────────────────────
4463
- const keyFile = sops.keyFile();
4464
- const keyExists = fs.existsSync(keyFile);
4465
- // A key file may hold several identities — that is how a host joins a store it did not
4466
- // create — so every check below works against the whole held set, not one recipient.
4467
- const held = sops.localRecipients();
4468
- const keyMode = keyExists ? (fs.statSync(keyFile).mode & 0o777).toString(8) : '';
4469
- logger.info(
4470
- '[sops-status] Age key\n' +
4471
- ` path ${keyFile}\n` +
4472
- ` present ${mark(keyExists)}${keyExists ? ` (mode ${keyMode}${keyMode === '600' || keyMode === '400' ? '' : ' — INSECURE, run chmod 600'})` : ''}\n` +
4473
- ` recipients ${held.join(', ') || (keyExists ? '(none — unreadable key file)' : '(none)')}` +
4474
- (keyExists ? '' : `\n searched ${sops.keyFileCandidates().join(', ')}`),
4475
- );
4476
-
4477
- // ── Creation rules ─────────────────────────────────────────────────────
4478
- const confPath = './engine-private/secrets/.sops.yaml';
4479
- const ruleRecipients = sops.creationRecipients();
4480
- logger.info(
4481
- '[sops-status] Creation rules\n' +
4482
- ` config ${confPath} ${fs.existsSync(confPath) ? '' : '(missing — run: underpost secret sops --init)'}\n` +
4483
- ` recipients ${ruleRecipients.length > 0 ? ruleRecipients.join(', ') : '(none)'}\n` +
4484
- ` local key listed ${mark(held.some((recipient) => ruleRecipients.includes(recipient)))}`,
4485
- );
4486
-
4487
- // ── Stored manifests ───────────────────────────────────────────────────
4488
- const manifests = sops.manifests(namespace).filter((manifest) => matchesKeyFilter(manifest.name));
4489
- const onboarded = new Set();
4490
- if (manifests.length === 0)
4491
- logger.warn(
4492
- `[sops-status] Store\n no encrypted manifests in ns/${namespace}` +
4493
- (manageSecretKeyFilter.length > 0 ? ` matching ${manageSecretKeyFilter.join(', ')}` : ''),
4494
- );
4495
- else {
4496
- const rows = manifests.map((manifest) => {
4497
- onboarded.add(manifest.name);
4498
- const recipients = sops.manifestRecipients(manifest.path);
4499
- const decryptable = sops.decryptable(manifest.path, held);
4500
- const live = shellExec(
4501
- `kubectl get secret ${manifest.name} -n ${manifest.namespace} --ignore-not-found -o name 2>/dev/null || true`,
4502
- { stdout: true, silent: true, silentOnError: true, disableLog: true },
4503
- ).trim();
4504
- // Drift is decided by kubectl's exit code; its stdout would contain the decrypted
4505
- // values, so it is discarded rather than captured.
4506
- let sync = 'n/a';
4507
- if (live && decryptable) {
4508
- const result = shellExec(
4509
- `bash -c 'set -o pipefail; SOPS_AGE_KEY_FILE="${keyFile}" sops --decrypt "${manifest.path}" ` +
4510
- `| kubectl diff -f - -n "${manifest.namespace}" >/dev/null 2>&1'`,
4511
- { silentOnError: true, disableLog: true, stdout: false },
4512
- );
4513
- sync = result.code === 0 ? 'in-sync' : result.code === 1 ? 'DRIFT' : 'error';
4514
- } else if (!live) sync = 'not applied';
4515
- else if (!decryptable) sync = 'no local key';
4516
- return (
4517
- ` ${`${manifest.namespace}/${manifest.name}`.padEnd(34)} ` +
4518
- `recipients=${String(recipients.length).padEnd(3)} ` +
4519
- `decryptable=${mark(decryptable).padEnd(4)} ` +
4520
- `live=${mark(!!live).padEnd(4)} ` +
4521
- `${sync}`
4522
- );
4523
- });
4524
- logger.info(`[sops-status] Store — ns/${namespace} (${manifests.length} manifest(s))\n` + rows.join('\n'));
4525
- }
4526
-
4527
- // ── Coverage ───────────────────────────────────────────────────────────
4528
- const coverage = sops
4529
- .managedSecrets()
4530
- .filter(matchesKeyFilter)
4531
- .map((name) => {
4532
- const seeds = Object.values(sops.seedSources(name));
4533
- const seedPresent = seeds.length > 0 && seeds.every((seed) => fs.existsSync(seed));
4534
- const source = onboarded.has(name)
4535
- ? 'sops'
4536
- : seedPresent
4537
- ? 'origin seed'
4538
- : seeds.length
4539
- ? 'MISSING'
4540
- : 'unmapped';
4541
- return ` ${name.padEnd(24)} ${source.padEnd(12)} ${seeds.length ? `seed=${mark(seedPresent)}` : ''}`;
4542
- });
4543
- if (coverage.length === 0)
4544
- logger.warn(
4545
- `[sops-status] Coverage\n no managed Secret matches ${manageSecretKeyFilter.join(', ')}\n` +
4546
- ` known keys: ${sops.managedSecrets().join(', ')}`,
4547
- );
4548
- else
4549
- logger.info('[sops-status] Coverage (which source each managed Secret deploys from)\n' + coverage.join('\n'));
4550
- },
4551
- /**
4552
- * @method secret
4553
- * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
4554
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
4555
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4556
- * @memberof UnderpostRun
4557
- */
4558
- secret: (path, options = DEFAULT_OPTION) => {
4559
- const cronDeployId = cronDeployIdResolve() || 'dd-cron';
4560
- Underpost.secret.underpost.createFromEnvFile(
4561
- `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
4562
- );
4563
- },
4564
- /**
4565
- * @method underpost-config
4566
- * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
4567
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
4568
- * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
4569
- * @memberof UnderpostRun
4570
- */
4571
- 'underpost-config': (path = '', options = DEFAULT_OPTION) => {
4572
- Underpost.deploy.configMap(path ? path : 'production', options.namespace);
4573
- },
4574
4240
  /**
4575
4241
  * @method gpu-env
4576
4242
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
@@ -4666,7 +4332,7 @@ UNDERPOST_SOPS_ENV_EOF`,
4666
4332
  ? 'Directory'
4667
4333
  : 'File';
4668
4334
 
4669
- const envs = Underpost.env.list();
4335
+ const envs = Underpost.host.store.list();
4670
4336
 
4671
4337
  const cmd = `kubectl apply -f - <<'EOF'
4672
4338
  apiVersion: ${apiVersion}
@@ -4743,7 +4409,7 @@ EOF`;
4743
4409
  * @memberof UnderpostRun
4744
4410
  */
4745
4411
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
4746
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4412
+ const baseCommand = cli('underpost', { local: true });
4747
4413
  const env = options.dev ? 'development' : 'production';
4748
4414
  const deployId = options.deployId || 'dd-default';
4749
4415
  const pathParts = (path || '').split('.');
@@ -4774,7 +4440,7 @@ EOF`;
4774
4440
  const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
4775
4441
 
4776
4442
  shellExec(`${baseCommand} env ${deployId} ${env}`);
4777
- shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4443
+ shellExec(`${baseCommand} client ${deployId} --env ${env} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
4778
4444
 
4779
4445
  const pushBundleFiles = (host, routePath) => {
4780
4446
  const buildId = `${host}-${routePath.replaceAll('/', '')}`;
@@ -4831,7 +4497,7 @@ EOF`;
4831
4497
  * @memberof UnderpostRun
4832
4498
  */
4833
4499
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
4834
- const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
4500
+ const baseCommand = cli('underpost', { local: true });
4835
4501
  const env = options.dev ? 'development' : 'production';
4836
4502
  const deployId = options.deployId || 'dd-default';
4837
4503
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
@@ -4935,15 +4601,30 @@ EOF`;
4935
4601
 
4936
4602
  /**
4937
4603
  * @method build-cluster-deployment-manifests
4938
- * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
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.
4939
4605
  * @param {string} path - Unused.
4940
4606
  * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
4941
4607
  * @memberof UnderpostRun
4942
4608
  */
4943
- 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
4944
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
4945
- shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
4946
- },
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
+ }),
4947
4628
 
4948
4629
  /**
4949
4630
  * @method monitor-ui
@@ -5083,7 +4764,7 @@ EOF`;
5083
4764
  const result = await Underpost.run.CALL(runner, path, options);
5084
4765
  return result;
5085
4766
  } catch (error) {
5086
- console.log(error);
4767
+ console.error(error);
5087
4768
  logger.error(error);
5088
4769
  process.exit(1);
5089
4770
  }