@underpostnet/cyberia 3.2.80

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 (821) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.example +144 -0
  3. package/.github/workflows/cyberia-client.cd.yml +40 -0
  4. package/.github/workflows/cyberia-server.cd.yml +40 -0
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +71 -0
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +70 -0
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +71 -0
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +71 -0
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +53 -0
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +53 -0
  11. package/.github/workflows/engine-cyberia.cd.yml +157 -0
  12. package/.github/workflows/engine-cyberia.ci.yml +125 -0
  13. package/.github/workflows/ghpkg.ci.yml +160 -0
  14. package/.github/workflows/gitlab.ci.yml +20 -0
  15. package/.github/workflows/hardhat.ci.yml +82 -0
  16. package/.github/workflows/npmpkg.ci.yml +126 -0
  17. package/.github/workflows/publish.ci.yml +87 -0
  18. package/.github/workflows/publish.cyberia.ci.yml +79 -0
  19. package/.github/workflows/pwa-microservices-template-page.cd.yml +65 -0
  20. package/.github/workflows/pwa-microservices-template-test.ci.yml +33 -0
  21. package/.github/workflows/release.cd.yml +46 -0
  22. package/.nycrc +9 -0
  23. package/.prettierignore +12 -0
  24. package/.prettierrc +9 -0
  25. package/.vscode/extensions.json +10 -0
  26. package/.vscode/settings.json +26 -0
  27. package/CHANGELOG.md +2516 -0
  28. package/CLI-HELP.md +1099 -0
  29. package/Dockerfile +161 -0
  30. package/Dockerfile.dev +161 -0
  31. package/Dockerfile.test +165 -0
  32. package/LICENSE +21 -0
  33. package/README.md +259 -0
  34. package/baremetal/commission-workflows.json +211 -0
  35. package/baremetal/packer-workflows.json +24 -0
  36. package/bin/build.js +261 -0
  37. package/bin/build.template.js +33 -0
  38. package/bin/cyberia.js +5627 -0
  39. package/bin/deploy.js +1547 -0
  40. package/bin/index.js +5627 -0
  41. package/bump.config.js +27 -0
  42. package/compose.env +144 -0
  43. package/conf.js +952 -0
  44. package/deployment.yaml +418 -0
  45. package/docker-compose.yml +434 -0
  46. package/examples/static-page/README.md +150 -0
  47. package/examples/static-page/ssr-components/CustomPage.js +554 -0
  48. package/examples/static-page/static-config-example.json +57 -0
  49. package/hardhat/.env.example +31 -0
  50. package/hardhat/contracts/ObjectLayerToken.sol +391 -0
  51. package/hardhat/deployments/.gitkeep +0 -0
  52. package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
  53. package/hardhat/hardhat.config.js +136 -0
  54. package/hardhat/ignition/modules/.gitkeep +0 -0
  55. package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
  56. package/hardhat/networks/besu-object-layer.network.json +138 -0
  57. package/hardhat/package-lock.json +2871 -0
  58. package/hardhat/package.json +43 -0
  59. package/hardhat/scripts/deployObjectLayerToken.js +98 -0
  60. package/hardhat/test/ObjectLayerToken.js +696 -0
  61. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  62. package/hardhat/types/ethers-contracts/common.ts +92 -0
  63. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  64. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  65. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  66. package/hardhat/types/ethers-contracts/index.ts +6 -0
  67. package/ipfs/configure-ipfs.sh +13 -0
  68. package/jsconfig.json +9 -0
  69. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +48 -0
  70. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +48 -0
  71. package/manifests/deployment/adminer/deployment.yaml +32 -0
  72. package/manifests/deployment/adminer/kustomization.yaml +7 -0
  73. package/manifests/deployment/adminer/service.yaml +13 -0
  74. package/manifests/deployment/dd-cyberia-development/deployment.yaml +418 -0
  75. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  76. package/manifests/deployment/dd-cyberia-development/proxy.yaml +189 -0
  77. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +164 -0
  78. package/manifests/deployment/dd-default-development/deployment.yaml +163 -0
  79. package/manifests/deployment/dd-default-development/proxy.yaml +46 -0
  80. package/manifests/deployment/fastapi/backend-deployment.yml +120 -0
  81. package/manifests/deployment/fastapi/backend-service.yml +19 -0
  82. package/manifests/deployment/fastapi/frontend-deployment.yml +54 -0
  83. package/manifests/deployment/fastapi/frontend-service.yml +15 -0
  84. package/manifests/deployment/fastapi/initial_data.sh +8 -0
  85. package/manifests/deployment/kafka/deployment.yaml +67 -0
  86. package/manifests/deployment/mongo-express/deployment.yaml +62 -0
  87. package/manifests/deployment/phpmyadmin/deployment.yaml +54 -0
  88. package/manifests/deployment/playwright/deployment.yaml +52 -0
  89. package/manifests/deployment/spark/spark-pi-py.yaml +20 -0
  90. package/manifests/deployment/tensorflow/tf-gpu-test.yaml +63 -0
  91. package/manifests/envoy-service-nodeport.yaml +22 -0
  92. package/manifests/grafana/deployment.yaml +64 -0
  93. package/manifests/grafana/kustomization.yaml +7 -0
  94. package/manifests/grafana/pvc.yaml +12 -0
  95. package/manifests/grafana/service.yaml +14 -0
  96. package/manifests/grafana/storage-class.yaml +9 -0
  97. package/manifests/ipfs/configmap.yaml +64 -0
  98. package/manifests/ipfs/headless-service.yaml +35 -0
  99. package/manifests/ipfs/kustomization.yaml +8 -0
  100. package/manifests/ipfs/statefulset.yaml +149 -0
  101. package/manifests/ipfs/storage-class.yaml +9 -0
  102. package/manifests/kind-config-dev.yaml +20 -0
  103. package/manifests/kind-config.yaml +12 -0
  104. package/manifests/kubeadm-calico-config.yaml +14 -0
  105. package/manifests/letsencrypt-prod.yaml +14 -0
  106. package/manifests/lxd/lxd-admin-profile.yaml +26 -0
  107. package/manifests/lxd/lxd-preseed.yaml +30 -0
  108. package/manifests/mariadb/kustomization.yaml +9 -0
  109. package/manifests/mariadb/pv.yaml +12 -0
  110. package/manifests/mariadb/pvc.yaml +10 -0
  111. package/manifests/mariadb/service.yaml +10 -0
  112. package/manifests/mariadb/statefulset.yaml +56 -0
  113. package/manifests/mariadb/storage-class.yaml +10 -0
  114. package/manifests/mongodb/headless-service.yaml +10 -0
  115. package/manifests/mongodb/kustomization.yaml +11 -0
  116. package/manifests/mongodb/pv-pvc.yaml +59 -0
  117. package/manifests/mongodb/statefulset.yaml +113 -0
  118. package/manifests/mongodb/storage-class.yaml +9 -0
  119. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  120. package/manifests/mongodb-4.4/kustomization.yaml +9 -0
  121. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  122. package/manifests/mongodb-4.4/pv-pvc.yaml +19 -0
  123. package/manifests/mongodb-4.4/service-deployment.yaml +61 -0
  124. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  125. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  126. package/manifests/mysql/kustomization.yaml +7 -0
  127. package/manifests/mysql/pv-pvc.yaml +27 -0
  128. package/manifests/mysql/statefulset.yaml +55 -0
  129. package/manifests/postgresql/configmap.yaml +9 -0
  130. package/manifests/postgresql/kustomization.yaml +10 -0
  131. package/manifests/postgresql/pv.yaml +15 -0
  132. package/manifests/postgresql/pvc.yaml +13 -0
  133. package/manifests/postgresql/service.yaml +10 -0
  134. package/manifests/postgresql/statefulset.yaml +37 -0
  135. package/manifests/prometheus/deployment.yaml +82 -0
  136. package/manifests/pv-pvc-dd.yaml +34 -0
  137. package/manifests/valkey/kustomization.yaml +7 -0
  138. package/manifests/valkey/service.yaml +10 -0
  139. package/manifests/valkey/statefulset.yaml +37 -0
  140. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  141. package/mongodb/entrypoint.sh +76 -0
  142. package/nginx.conf +128 -0
  143. package/nodemon.json +6 -0
  144. package/package.json +101 -0
  145. package/packer/images/Rocky9Amd64/Makefile +62 -0
  146. package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
  147. package/packer/images/Rocky9Amd64/README.md +122 -0
  148. package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
  149. package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +164 -0
  150. package/packer/images/Rocky9Arm64/Makefile +69 -0
  151. package/packer/images/Rocky9Arm64/README.md +122 -0
  152. package/packer/images/Rocky9Arm64/http/rocky9.ks.pkrtpl.hcl +114 -0
  153. package/packer/images/Rocky9Arm64/rocky9.pkr.hcl +171 -0
  154. package/packer/scripts/fuse-nbd +64 -0
  155. package/packer/scripts/fuse-tar-root +63 -0
  156. package/proxy.yaml +189 -0
  157. package/pv-pvc.yaml +164 -0
  158. package/scripts/device-scan.sh +65 -0
  159. package/scripts/disk-clean.sh +182 -0
  160. package/scripts/disk-devices.sh +13 -0
  161. package/scripts/gen-fqdns.sh +14 -0
  162. package/scripts/gpu-diag.sh +19 -0
  163. package/scripts/ip-info.sh +118 -0
  164. package/scripts/ipxe-setup.sh +200 -0
  165. package/scripts/k3s-node-setup.sh +124 -0
  166. package/scripts/kubeadm-node-setup.sh +317 -0
  167. package/scripts/link-local-underpost-cli.sh +6 -0
  168. package/scripts/lxd-vm-setup.sh +208 -0
  169. package/scripts/maas-nat-firewalld.sh +145 -0
  170. package/scripts/maas-setup.sh +135 -0
  171. package/scripts/maas-upload-boot-resource.sh +183 -0
  172. package/scripts/nat-iptables.sh +111 -0
  173. package/scripts/nvim.sh +91 -0
  174. package/scripts/packer-init-vars-file.sh +40 -0
  175. package/scripts/packer-setup.sh +289 -0
  176. package/scripts/ports-ls.sh +33 -0
  177. package/scripts/quick-tftp.sh +19 -0
  178. package/scripts/rhel-grpc-setup.sh +56 -0
  179. package/scripts/rocky-kickstart.sh +986 -0
  180. package/scripts/rocky-pwa.sh +200 -0
  181. package/scripts/rocky-setup.sh +106 -0
  182. package/scripts/rpmfusion-ffmpeg-setup.sh +32 -0
  183. package/scripts/ssl.sh +170 -0
  184. package/scripts/test-monitor.sh +248 -0
  185. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +30 -0
  186. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +155 -0
  187. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +39 -0
  188. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +382 -0
  189. package/src/api/core/core.controller.js +8 -0
  190. package/src/api/core/core.model.js +11 -0
  191. package/src/api/core/core.router.js +23 -0
  192. package/src/api/core/core.service.js +30 -0
  193. package/src/api/crypto/crypto.controller.js +10 -0
  194. package/src/api/crypto/crypto.model.js +23 -0
  195. package/src/api/crypto/crypto.router.js +23 -0
  196. package/src/api/crypto/crypto.service.js +64 -0
  197. package/src/api/cyberia-action/cyberia-action.controller.js +8 -0
  198. package/src/api/cyberia-action/cyberia-action.model.js +79 -0
  199. package/src/api/cyberia-action/cyberia-action.router.js +21 -0
  200. package/src/api/cyberia-action/cyberia-action.service.js +58 -0
  201. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +27 -0
  202. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +100 -0
  203. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +70 -0
  204. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  205. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +8 -0
  206. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  207. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +20 -0
  208. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +57 -0
  209. package/src/api/cyberia-entity/cyberia-entity.controller.js +6 -0
  210. package/src/api/cyberia-entity/cyberia-entity.model.js +25 -0
  211. package/src/api/cyberia-entity/cyberia-entity.router.js +17 -0
  212. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  213. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +6 -0
  214. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  215. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -0
  216. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  217. package/src/api/cyberia-instance/cyberia-fallback-world.js +337 -0
  218. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +57 -0
  219. package/src/api/cyberia-instance/cyberia-instance-map.service.js +583 -0
  220. package/src/api/cyberia-instance/cyberia-instance.controller.js +42 -0
  221. package/src/api/cyberia-instance/cyberia-instance.model.js +117 -0
  222. package/src/api/cyberia-instance/cyberia-instance.router.js +75 -0
  223. package/src/api/cyberia-instance/cyberia-instance.service.js +190 -0
  224. package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
  225. package/src/api/cyberia-instance/cyberia-world-generator.js +618 -0
  226. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +6 -0
  227. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +200 -0
  228. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +17 -0
  229. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
  230. package/src/api/cyberia-map/cyberia-map.controller.js +6 -0
  231. package/src/api/cyberia-map/cyberia-map.model.js +33 -0
  232. package/src/api/cyberia-map/cyberia-map.router.js +19 -0
  233. package/src/api/cyberia-map/cyberia-map.service.js +76 -0
  234. package/src/api/cyberia-quest/cyberia-quest.controller.js +9 -0
  235. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  236. package/src/api/cyberia-quest/cyberia-quest.router.js +26 -0
  237. package/src/api/cyberia-quest/cyberia-quest.service.js +97 -0
  238. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +6 -0
  239. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  240. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +21 -0
  241. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  242. package/src/api/cyberia-saga/cyberia-saga.controller.js +6 -0
  243. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  244. package/src/api/cyberia-saga/cyberia-saga.router.js +17 -0
  245. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  246. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +1076 -0
  247. package/src/api/cyberia-skill/cyberia-skill.controller.js +6 -0
  248. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  249. package/src/api/cyberia-skill/cyberia-skill.router.js +17 -0
  250. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  251. package/src/api/default/default.controller.js +6 -0
  252. package/src/api/default/default.model.js +20 -0
  253. package/src/api/default/default.router.js +21 -0
  254. package/src/api/default/default.service.js +42 -0
  255. package/src/api/document/document.controller.js +9 -0
  256. package/src/api/document/document.model.js +95 -0
  257. package/src/api/document/document.router.js +32 -0
  258. package/src/api/document/document.service.js +633 -0
  259. package/src/api/file/file.controller.js +15 -0
  260. package/src/api/file/file.model.js +127 -0
  261. package/src/api/file/file.ref.json +39 -0
  262. package/src/api/file/file.router.js +36 -0
  263. package/src/api/file/file.service.js +499 -0
  264. package/src/api/instance/instance.controller.js +8 -0
  265. package/src/api/instance/instance.model.js +34 -0
  266. package/src/api/instance/instance.router.js +27 -0
  267. package/src/api/instance/instance.service.js +75 -0
  268. package/src/api/ipfs/ipfs.controller.js +8 -0
  269. package/src/api/ipfs/ipfs.model.js +77 -0
  270. package/src/api/ipfs/ipfs.router.js +26 -0
  271. package/src/api/ipfs/ipfs.service.js +145 -0
  272. package/src/api/object-layer/object-layer.controller.js +18 -0
  273. package/src/api/object-layer/object-layer.model.js +200 -0
  274. package/src/api/object-layer/object-layer.router.js +729 -0
  275. package/src/api/object-layer/object-layer.service.js +894 -0
  276. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +6 -0
  277. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +93 -0
  278. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +17 -0
  279. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +46 -0
  280. package/src/api/test/test.controller.js +14 -0
  281. package/src/api/test/test.model.js +14 -0
  282. package/src/api/test/test.router.js +23 -0
  283. package/src/api/test/test.service.js +35 -0
  284. package/src/api/types.js +24 -0
  285. package/src/api/user/guest.service.js +100 -0
  286. package/src/api/user/postman_collection.json +216 -0
  287. package/src/api/user/user.build.js +16 -0
  288. package/src/api/user/user.controller.js +35 -0
  289. package/src/api/user/user.model.js +142 -0
  290. package/src/api/user/user.router.js +367 -0
  291. package/src/api/user/user.service.js +582 -0
  292. package/src/api.js +23 -0
  293. package/src/cli/baremetal.js +3947 -0
  294. package/src/cli/cloud-init.js +673 -0
  295. package/src/cli/cluster.js +1395 -0
  296. package/src/cli/db.js +1533 -0
  297. package/src/cli/deploy.js +1542 -0
  298. package/src/cli/docker-compose.js +802 -0
  299. package/src/cli/env.js +177 -0
  300. package/src/cli/fs.js +380 -0
  301. package/src/cli/image.js +389 -0
  302. package/src/cli/index.js +1059 -0
  303. package/src/cli/ipfs.js +182 -0
  304. package/src/cli/kickstart.js +271 -0
  305. package/src/cli/kubectl.js +214 -0
  306. package/src/cli/lxd.js +1232 -0
  307. package/src/cli/monitor.js +659 -0
  308. package/src/cli/release.js +595 -0
  309. package/src/cli/repository.js +1969 -0
  310. package/src/cli/run.js +3326 -0
  311. package/src/cli/secrets.js +151 -0
  312. package/src/cli/ssh.js +975 -0
  313. package/src/cli/static.js +653 -0
  314. package/src/cli/system.js +332 -0
  315. package/src/cli/test.js +176 -0
  316. package/src/client/Cryptokoyn.index.js +30 -0
  317. package/src/client/CyberiaPortal.index.js +31 -0
  318. package/src/client/Default.index.js +77 -0
  319. package/src/client/Itemledger.index.js +35 -0
  320. package/src/client/Underpost.index.js +31 -0
  321. package/src/client/components/core/404.js +20 -0
  322. package/src/client/components/core/500.js +20 -0
  323. package/src/client/components/core/Account.js +630 -0
  324. package/src/client/components/core/AgGrid.js +238 -0
  325. package/src/client/components/core/Alert.js +76 -0
  326. package/src/client/components/core/AppStore.js +69 -0
  327. package/src/client/components/core/Auth.js +353 -0
  328. package/src/client/components/core/Badge.js +26 -0
  329. package/src/client/components/core/Blockchain.js +41 -0
  330. package/src/client/components/core/Blog.js +9 -0
  331. package/src/client/components/core/BtnIcon.js +109 -0
  332. package/src/client/components/core/CalendarCore.js +437 -0
  333. package/src/client/components/core/Chat.js +62 -0
  334. package/src/client/components/core/ClientEvents.js +163 -0
  335. package/src/client/components/core/ColorPaletteElement.js +309 -0
  336. package/src/client/components/core/CommonJs.js +1014 -0
  337. package/src/client/components/core/Content.js +349 -0
  338. package/src/client/components/core/Css.js +1134 -0
  339. package/src/client/components/core/CssCore.js +881 -0
  340. package/src/client/components/core/D3Chart.js +44 -0
  341. package/src/client/components/core/Docs.js +478 -0
  342. package/src/client/components/core/DropDown.js +321 -0
  343. package/src/client/components/core/EventBus.js +96 -0
  344. package/src/client/components/core/EventsUI.js +130 -0
  345. package/src/client/components/core/FileExplorer.js +1406 -0
  346. package/src/client/components/core/FullScreen.js +201 -0
  347. package/src/client/components/core/Input.js +537 -0
  348. package/src/client/components/core/Keyboard.js +80 -0
  349. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  350. package/src/client/components/core/LoadingAnimation.js +152 -0
  351. package/src/client/components/core/LogIn.js +212 -0
  352. package/src/client/components/core/LogOut.js +72 -0
  353. package/src/client/components/core/Logger.js +22 -0
  354. package/src/client/components/core/Modal.js +2956 -0
  355. package/src/client/components/core/NotificationManager.js +80 -0
  356. package/src/client/components/core/Pagination.js +252 -0
  357. package/src/client/components/core/Panel.js +1308 -0
  358. package/src/client/components/core/PanelForm.js +915 -0
  359. package/src/client/components/core/Polyhedron.js +947 -0
  360. package/src/client/components/core/PublicProfile.js +895 -0
  361. package/src/client/components/core/Recover.js +211 -0
  362. package/src/client/components/core/Responsive.js +146 -0
  363. package/src/client/components/core/RichText.js +33 -0
  364. package/src/client/components/core/Router.js +523 -0
  365. package/src/client/components/core/Scroll.js +76 -0
  366. package/src/client/components/core/SearchBox.js +1007 -0
  367. package/src/client/components/core/SignUp.js +153 -0
  368. package/src/client/components/core/SocketIo.js +163 -0
  369. package/src/client/components/core/SocketIoHandler.js +75 -0
  370. package/src/client/components/core/Stream.js +161 -0
  371. package/src/client/components/core/ToggleSwitch.js +89 -0
  372. package/src/client/components/core/ToolTip.js +78 -0
  373. package/src/client/components/core/Translate.js +694 -0
  374. package/src/client/components/core/Validator.js +140 -0
  375. package/src/client/components/core/VanillaJs.js +485 -0
  376. package/src/client/components/core/Wallet.js +95 -0
  377. package/src/client/components/core/WebComponent.js +44 -0
  378. package/src/client/components/core/Webhook.js +58 -0
  379. package/src/client/components/core/Worker.js +313 -0
  380. package/src/client/components/core/windowGetDimensions.js +269 -0
  381. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +352 -0
  382. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  383. package/src/client/components/cryptokoyn/CssCryptokoyn.js +212 -0
  384. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +16 -0
  385. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +12 -0
  386. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  387. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +16 -0
  388. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +11 -0
  389. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +6 -0
  390. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  391. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  392. package/src/client/components/cyberia/InstanceEngineCyberia.js +1088 -0
  393. package/src/client/components/cyberia/InstanceSelectionView.js +900 -0
  394. package/src/client/components/cyberia/MapEngineCyberia.js +1976 -0
  395. package/src/client/components/cyberia/ObjectLayerEngine.js +1540 -0
  396. package/src/client/components/cyberia/ObjectLayerEngineModal.js +2420 -0
  397. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +1522 -0
  398. package/src/client/components/cyberia/SharedDefaultsCyberia.js +528 -0
  399. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +772 -0
  400. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  401. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +254 -0
  402. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +21 -0
  403. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +16 -0
  404. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +518 -0
  405. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +72 -0
  406. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +16 -0
  407. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +13 -0
  408. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +6 -0
  409. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +48 -0
  410. package/src/client/components/default/AppShellDefault.js +765 -0
  411. package/src/client/components/default/AppStoreDefault.js +5 -0
  412. package/src/client/components/default/CommonDefault.js +29 -0
  413. package/src/client/components/default/CssDefault.js +27 -0
  414. package/src/client/components/default/LogInDefault.js +16 -0
  415. package/src/client/components/default/LogOutDefault.js +12 -0
  416. package/src/client/components/default/RouterDefault.js +47 -0
  417. package/src/client/components/default/SettingsDefault.js +16 -0
  418. package/src/client/components/default/SignUpDefault.js +11 -0
  419. package/src/client/components/default/SocketIoDefault.js +6 -0
  420. package/src/client/components/default/TranslateDefault.js +7 -0
  421. package/src/client/components/itemledger/AppShellItemledger.js +353 -0
  422. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  423. package/src/client/components/itemledger/CssItemledger.js +212 -0
  424. package/src/client/components/itemledger/LogInItemledger.js +17 -0
  425. package/src/client/components/itemledger/LogOutItemledger.js +12 -0
  426. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  427. package/src/client/components/itemledger/SettingsItemledger.js +16 -0
  428. package/src/client/components/itemledger/SignUpItemledger.js +11 -0
  429. package/src/client/components/itemledger/SocketIoItemledger.js +6 -0
  430. package/src/client/components/itemledger/TranslateItemledger.js +7 -0
  431. package/src/client/components/underpost/AppShellUnderpost.js +691 -0
  432. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  433. package/src/client/components/underpost/CssUnderpost.js +281 -0
  434. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +879 -0
  435. package/src/client/components/underpost/DocumentSearchProvider.js +448 -0
  436. package/src/client/components/underpost/LabGalleryUnderpost.js +79 -0
  437. package/src/client/components/underpost/LogInUnderpost.js +25 -0
  438. package/src/client/components/underpost/LogOutUnderpost.js +17 -0
  439. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  440. package/src/client/components/underpost/SettingsUnderpost.js +16 -0
  441. package/src/client/components/underpost/SignUpUnderpost.js +11 -0
  442. package/src/client/components/underpost/SocketIoUnderpost.js +6 -0
  443. package/src/client/components/underpost/TranslateUnderpost.js +10 -0
  444. package/src/client/public/cryptokoyn/assets/banner/koyn-social.png +0 -0
  445. package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
  446. package/src/client/public/cryptokoyn/assets/logo/cryptokoyn.png +0 -0
  447. package/src/client/public/cryptokoyn/assets/splash/android-chrome-144x144.png +0 -0
  448. package/src/client/public/cryptokoyn/assets/splash/android-chrome-192x192.png +0 -0
  449. package/src/client/public/cryptokoyn/assets/splash/android-chrome-256x256.png +0 -0
  450. package/src/client/public/cryptokoyn/assets/splash/android-chrome-36x36.png +0 -0
  451. package/src/client/public/cryptokoyn/assets/splash/android-chrome-48x48.png +0 -0
  452. package/src/client/public/cryptokoyn/assets/splash/android-chrome-72x72.png +0 -0
  453. package/src/client/public/cryptokoyn/assets/splash/android-chrome-96x96.png +0 -0
  454. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-114x114-precomposed.png +0 -0
  455. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-114x114.png +0 -0
  456. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-120x120-precomposed.png +0 -0
  457. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-120x120.png +0 -0
  458. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-144x144-precomposed.png +0 -0
  459. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-144x144.png +0 -0
  460. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-152x152-precomposed.png +0 -0
  461. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-152x152.png +0 -0
  462. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-180x180-precomposed.png +0 -0
  463. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-180x180.png +0 -0
  464. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-57x57-precomposed.png +0 -0
  465. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-57x57.png +0 -0
  466. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-60x60-precomposed.png +0 -0
  467. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-60x60.png +0 -0
  468. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-72x72-precomposed.png +0 -0
  469. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-72x72.png +0 -0
  470. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-76x76-precomposed.png +0 -0
  471. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-76x76.png +0 -0
  472. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-precomposed.png +0 -0
  473. package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon.png +0 -0
  474. package/src/client/public/cryptokoyn/assets/splash/favicon-16x16.png +0 -0
  475. package/src/client/public/cryptokoyn/assets/splash/favicon-32x32.png +0 -0
  476. package/src/client/public/cryptokoyn/assets/splash/mstile-144x144.png +0 -0
  477. package/src/client/public/cryptokoyn/assets/splash/mstile-150x150.png +0 -0
  478. package/src/client/public/cryptokoyn/assets/splash/mstile-310x150.png +0 -0
  479. package/src/client/public/cryptokoyn/assets/splash/mstile-310x310.png +0 -0
  480. package/src/client/public/cryptokoyn/assets/splash/mstile-70x70.png +0 -0
  481. package/src/client/public/cryptokoyn/assets/splash/safari-pinned-tab.svg +42 -0
  482. package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
  483. package/src/client/public/cryptokoyn/favicon.ico +0 -0
  484. package/src/client/public/cryptokoyn/microdata.json +85 -0
  485. package/src/client/public/cryptokoyn/site.webmanifest +57 -0
  486. package/src/client/public/cryptokoyn/sitemap +148 -0
  487. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +307 -0
  488. package/src/client/public/cyberia-docs/ARCHITECTURE.md +309 -0
  489. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +204 -0
  490. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +341 -0
  491. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +84 -0
  492. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  493. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +294 -0
  494. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  495. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +242 -0
  496. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  497. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +309 -0
  498. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +228 -0
  499. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  500. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  501. package/src/client/public/cyberia-docs/WHITE-PAPER.md +740 -0
  502. package/src/client/public/default/android-chrome-144x144.png +0 -0
  503. package/src/client/public/default/android-chrome-192x192.png +0 -0
  504. package/src/client/public/default/android-chrome-256x256.png +0 -0
  505. package/src/client/public/default/android-chrome-36x36.png +0 -0
  506. package/src/client/public/default/android-chrome-384x384.png +0 -0
  507. package/src/client/public/default/android-chrome-48x48.png +0 -0
  508. package/src/client/public/default/android-chrome-512x512.png +0 -0
  509. package/src/client/public/default/android-chrome-72x72.png +0 -0
  510. package/src/client/public/default/android-chrome-96x96.png +0 -0
  511. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  512. package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
  513. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  514. package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
  515. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  516. package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
  517. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  518. package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
  519. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  520. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  521. package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
  522. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  523. package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
  524. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  525. package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
  526. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  527. package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
  528. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  529. package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
  530. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  531. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  532. package/src/client/public/default/apple-touch-icon.png +0 -0
  533. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  534. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  535. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  536. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  537. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  538. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  539. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  540. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  541. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  542. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  543. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  544. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  545. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  546. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  547. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  548. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  549. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  550. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  551. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  552. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  553. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  554. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  555. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  556. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  557. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  558. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  559. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  560. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  561. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  562. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  563. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  564. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  565. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  566. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  567. package/src/client/public/default/assets/background/dark.jpg +0 -0
  568. package/src/client/public/default/assets/background/dark.svg +557 -0
  569. package/src/client/public/default/assets/background/white.jpg +0 -0
  570. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  571. package/src/client/public/default/assets/background/white0.jpg +0 -0
  572. package/src/client/public/default/assets/logo/base-icon.png +0 -0
  573. package/src/client/public/default/assets/logo/underpost.gif +0 -0
  574. package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
  575. package/src/client/public/default/assets/mailer/api-user-default-avatar.png +0 -0
  576. package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
  577. package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
  578. package/src/client/public/default/browserconfig.xml +12 -0
  579. package/src/client/public/default/favicon-16x16.png +0 -0
  580. package/src/client/public/default/favicon-32x32.png +0 -0
  581. package/src/client/public/default/favicon-48x48.png +0 -0
  582. package/src/client/public/default/favicon.ico +0 -0
  583. package/src/client/public/default/manifest.webmanifest +69 -0
  584. package/src/client/public/default/mstile-144x144.png +0 -0
  585. package/src/client/public/default/mstile-150x150.png +0 -0
  586. package/src/client/public/default/mstile-310x150.png +0 -0
  587. package/src/client/public/default/mstile-310x310.png +0 -0
  588. package/src/client/public/default/mstile-70x70.png +0 -0
  589. package/src/client/public/default/safari-pinned-tab.svg +24 -0
  590. package/src/client/public/default/site.webmanifest +69 -0
  591. package/src/client/public/default/sitemap +148 -0
  592. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  593. package/src/client/public/default/yandex-browser-manifest.json +9 -0
  594. package/src/client/public/doc/favicon.ico +0 -0
  595. package/src/client/public/doc/sitemap +148 -0
  596. package/src/client/public/itemledger/android-chrome-144x144.png +0 -0
  597. package/src/client/public/itemledger/android-chrome-192x192.png +0 -0
  598. package/src/client/public/itemledger/android-chrome-256x256.png +0 -0
  599. package/src/client/public/itemledger/android-chrome-36x36.png +0 -0
  600. package/src/client/public/itemledger/android-chrome-384x384.png +0 -0
  601. package/src/client/public/itemledger/android-chrome-48x48.png +0 -0
  602. package/src/client/public/itemledger/android-chrome-512x512.png +0 -0
  603. package/src/client/public/itemledger/android-chrome-72x72.png +0 -0
  604. package/src/client/public/itemledger/android-chrome-96x96.png +0 -0
  605. package/src/client/public/itemledger/apple-touch-icon-1024x1024.png +0 -0
  606. package/src/client/public/itemledger/apple-touch-icon-114x114.png +0 -0
  607. package/src/client/public/itemledger/apple-touch-icon-120x120.png +0 -0
  608. package/src/client/public/itemledger/apple-touch-icon-144x144.png +0 -0
  609. package/src/client/public/itemledger/apple-touch-icon-152x152.png +0 -0
  610. package/src/client/public/itemledger/apple-touch-icon-167x167.png +0 -0
  611. package/src/client/public/itemledger/apple-touch-icon-180x180.png +0 -0
  612. package/src/client/public/itemledger/apple-touch-icon-57x57.png +0 -0
  613. package/src/client/public/itemledger/apple-touch-icon-60x60.png +0 -0
  614. package/src/client/public/itemledger/apple-touch-icon-72x72.png +0 -0
  615. package/src/client/public/itemledger/apple-touch-icon-76x76.png +0 -0
  616. package/src/client/public/itemledger/apple-touch-icon-precomposed.png +0 -0
  617. package/src/client/public/itemledger/apple-touch-icon.png +0 -0
  618. package/src/client/public/itemledger/apple-touch-startup-image-1125x2436.png +0 -0
  619. package/src/client/public/itemledger/apple-touch-startup-image-1136x640.png +0 -0
  620. package/src/client/public/itemledger/apple-touch-startup-image-1170x2532.png +0 -0
  621. package/src/client/public/itemledger/apple-touch-startup-image-1179x2556.png +0 -0
  622. package/src/client/public/itemledger/apple-touch-startup-image-1242x2208.png +0 -0
  623. package/src/client/public/itemledger/apple-touch-startup-image-1242x2688.png +0 -0
  624. package/src/client/public/itemledger/apple-touch-startup-image-1284x2778.png +0 -0
  625. package/src/client/public/itemledger/apple-touch-startup-image-1290x2796.png +0 -0
  626. package/src/client/public/itemledger/apple-touch-startup-image-1334x750.png +0 -0
  627. package/src/client/public/itemledger/apple-touch-startup-image-1488x2266.png +0 -0
  628. package/src/client/public/itemledger/apple-touch-startup-image-1536x2048.png +0 -0
  629. package/src/client/public/itemledger/apple-touch-startup-image-1620x2160.png +0 -0
  630. package/src/client/public/itemledger/apple-touch-startup-image-1640x2160.png +0 -0
  631. package/src/client/public/itemledger/apple-touch-startup-image-1668x2224.png +0 -0
  632. package/src/client/public/itemledger/apple-touch-startup-image-1668x2388.png +0 -0
  633. package/src/client/public/itemledger/apple-touch-startup-image-1792x828.png +0 -0
  634. package/src/client/public/itemledger/apple-touch-startup-image-2048x1536.png +0 -0
  635. package/src/client/public/itemledger/apple-touch-startup-image-2048x2732.png +0 -0
  636. package/src/client/public/itemledger/apple-touch-startup-image-2160x1620.png +0 -0
  637. package/src/client/public/itemledger/apple-touch-startup-image-2160x1640.png +0 -0
  638. package/src/client/public/itemledger/apple-touch-startup-image-2208x1242.png +0 -0
  639. package/src/client/public/itemledger/apple-touch-startup-image-2224x1668.png +0 -0
  640. package/src/client/public/itemledger/apple-touch-startup-image-2266x1488.png +0 -0
  641. package/src/client/public/itemledger/apple-touch-startup-image-2388x1668.png +0 -0
  642. package/src/client/public/itemledger/apple-touch-startup-image-2436x1125.png +0 -0
  643. package/src/client/public/itemledger/apple-touch-startup-image-2532x1170.png +0 -0
  644. package/src/client/public/itemledger/apple-touch-startup-image-2556x1179.png +0 -0
  645. package/src/client/public/itemledger/apple-touch-startup-image-2688x1242.png +0 -0
  646. package/src/client/public/itemledger/apple-touch-startup-image-2732x2048.png +0 -0
  647. package/src/client/public/itemledger/apple-touch-startup-image-2778x1284.png +0 -0
  648. package/src/client/public/itemledger/apple-touch-startup-image-2796x1290.png +0 -0
  649. package/src/client/public/itemledger/apple-touch-startup-image-640x1136.png +0 -0
  650. package/src/client/public/itemledger/apple-touch-startup-image-750x1334.png +0 -0
  651. package/src/client/public/itemledger/apple-touch-startup-image-828x1792.png +0 -0
  652. package/src/client/public/itemledger/assets/logo/base-icon.png +0 -0
  653. package/src/client/public/itemledger/browserconfig.xml +12 -0
  654. package/src/client/public/itemledger/favicon-16x16.png +0 -0
  655. package/src/client/public/itemledger/favicon-32x32.png +0 -0
  656. package/src/client/public/itemledger/favicon-48x48.png +0 -0
  657. package/src/client/public/itemledger/favicon.ico +0 -0
  658. package/src/client/public/itemledger/manifest.webmanifest +69 -0
  659. package/src/client/public/itemledger/microdata.json +71 -0
  660. package/src/client/public/itemledger/mstile-144x144.png +0 -0
  661. package/src/client/public/itemledger/mstile-150x150.png +0 -0
  662. package/src/client/public/itemledger/mstile-310x150.png +0 -0
  663. package/src/client/public/itemledger/mstile-310x310.png +0 -0
  664. package/src/client/public/itemledger/mstile-70x70.png +0 -0
  665. package/src/client/public/itemledger/sitemap +148 -0
  666. package/src/client/public/itemledger/yandex-browser-50x50.png +0 -0
  667. package/src/client/public/itemledger/yandex-browser-manifest.json +9 -0
  668. package/src/client/public/test/favicon.ico +0 -0
  669. package/src/client/public/test/sitemap +148 -0
  670. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +137 -0
  671. package/src/client/services/core/core.service.js +309 -0
  672. package/src/client/services/crypto/crypto.service.js +68 -0
  673. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  674. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  675. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
  676. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  677. package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
  678. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  679. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  680. package/src/client/services/cyberia-instance/cyberia-instance.service.js +137 -0
  681. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
  682. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  683. package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
  684. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  685. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  686. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  687. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  688. package/src/client/services/default/default.management.js +1015 -0
  689. package/src/client/services/default/default.service.js +99 -0
  690. package/src/client/services/document/document.service.js +134 -0
  691. package/src/client/services/file/file.service.js +94 -0
  692. package/src/client/services/instance/instance.management.js +78 -0
  693. package/src/client/services/instance/instance.service.js +107 -0
  694. package/src/client/services/ipfs/ipfs.service.js +118 -0
  695. package/src/client/services/object-layer/object-layer.management.js +414 -0
  696. package/src/client/services/object-layer/object-layer.service.js +208 -0
  697. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +99 -0
  698. package/src/client/services/test/test.service.js +68 -0
  699. package/src/client/services/user/guest.service.js +86 -0
  700. package/src/client/services/user/user.management.js +51 -0
  701. package/src/client/services/user/user.service.js +106 -0
  702. package/src/client/ssr/RootDocument.js +237 -0
  703. package/src/client/ssr/body/404.js +77 -0
  704. package/src/client/ssr/body/500.js +76 -0
  705. package/src/client/ssr/body/CacheControl.js +117 -0
  706. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +97 -0
  707. package/src/client/ssr/body/DefaultSplashScreen.js +97 -0
  708. package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
  709. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +90 -0
  710. package/src/client/ssr/head/CryptokoynScripts.js +4 -0
  711. package/src/client/ssr/head/Css.js +241 -0
  712. package/src/client/ssr/head/CyberiaPortalScripts.js +6 -0
  713. package/src/client/ssr/head/DefaultScripts.js +6 -0
  714. package/src/client/ssr/head/ItemledgerScripts.js +4 -0
  715. package/src/client/ssr/head/Microdata.js +11 -0
  716. package/src/client/ssr/head/Production.js +1 -0
  717. package/src/client/ssr/head/Pwa.js +146 -0
  718. package/src/client/ssr/head/PwaItemledger.js +197 -0
  719. package/src/client/ssr/head/Seo.js +15 -0
  720. package/src/client/ssr/head/UnderpostScripts.js +6 -0
  721. package/src/client/ssr/mailer/DefaultRecoverEmail.js +20 -0
  722. package/src/client/ssr/mailer/DefaultVerifyEmail.js +16 -0
  723. package/src/client/ssr/views/Cyberia404.js +328 -0
  724. package/src/client/ssr/views/CyberiaServerMetrics.js +1070 -0
  725. package/src/client/ssr/views/Maintenance.js +64 -0
  726. package/src/client/ssr/views/NoNetworkConnection.js +68 -0
  727. package/src/client/ssr/views/Test.js +199 -0
  728. package/src/client/sw/core.sw.js +274 -0
  729. package/src/client-builder/client-build-docs.js +493 -0
  730. package/src/client-builder/client-build-live.js +100 -0
  731. package/src/client-builder/client-build.js +1092 -0
  732. package/src/client-builder/client-dev-server.js +88 -0
  733. package/src/client-builder/client-formatted.js +170 -0
  734. package/src/client-builder/client-icons.js +108 -0
  735. package/src/client-builder/ssr.js +124 -0
  736. package/src/client.build.js +17 -0
  737. package/src/client.dev.js +21 -0
  738. package/src/db/DataBaseProvider.js +213 -0
  739. package/src/db/mariadb/MariaDB.js +72 -0
  740. package/src/db/mongo/MongoBootstrap.js +657 -0
  741. package/src/db/mongo/MongooseDB.js +199 -0
  742. package/src/grpc/cyberia/grpc-server.js +162 -0
  743. package/src/index.js +357 -0
  744. package/src/mailer/EmailRender.js +116 -0
  745. package/src/mailer/MailerProvider.js +213 -0
  746. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +413 -0
  747. package/src/projects/cyberia/besu-genesis-generator.js +1630 -0
  748. package/src/projects/cyberia/catalog-cyberia.js +90 -0
  749. package/src/projects/cyberia/gemini-client.js +175 -0
  750. package/src/projects/cyberia/generate-saga.js +2137 -0
  751. package/src/projects/cyberia/hot-reload-trigger.js +163 -0
  752. package/src/projects/cyberia/instance-data.js +719 -0
  753. package/src/projects/cyberia/ipfs-client.js +599 -0
  754. package/src/projects/cyberia/map-preview-generator.js +304 -0
  755. package/src/projects/cyberia/object-layer.js +978 -0
  756. package/src/projects/cyberia/semantic-layer-generator-floor.js +319 -0
  757. package/src/projects/cyberia/semantic-layer-generator-resource.js +259 -0
  758. package/src/projects/cyberia/semantic-layer-generator-skin.js +1164 -0
  759. package/src/projects/cyberia/semantic-layer-generator.js +753 -0
  760. package/src/projects/cyberia/shape-generator.js +1060 -0
  761. package/src/projects/underpost/catalog-underpost.js +60 -0
  762. package/src/proxy.js +19 -0
  763. package/src/runtime/cyberia-client/Dockerfile +49 -0
  764. package/src/runtime/cyberia-client/Dockerfile.dev +75 -0
  765. package/src/runtime/cyberia-server/Dockerfile +43 -0
  766. package/src/runtime/cyberia-server/Dockerfile.dev +56 -0
  767. package/src/runtime/engine-cyberia/Dockerfile +161 -0
  768. package/src/runtime/engine-cyberia/Dockerfile.dev +161 -0
  769. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  770. package/src/runtime/engine-cyberia/compose.env +144 -0
  771. package/src/runtime/engine-cyberia/docker-compose.yml +434 -0
  772. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  773. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  774. package/src/runtime/engine-cyberia/nginx.conf +128 -0
  775. package/src/runtime/express/Dockerfile +45 -0
  776. package/src/runtime/express/Express.js +285 -0
  777. package/src/runtime/lampp/Dockerfile +62 -0
  778. package/src/runtime/lampp/Lampp.js +359 -0
  779. package/src/runtime/nginx/Nginx.js +250 -0
  780. package/src/runtime/wp/Dockerfile +62 -0
  781. package/src/runtime/wp/Wp.js +642 -0
  782. package/src/server/auth.js +713 -0
  783. package/src/server/backup.js +102 -0
  784. package/src/server/catalog.js +72 -0
  785. package/src/server/conf.js +2316 -0
  786. package/src/server/cron.js +476 -0
  787. package/src/server/crypto.js +210 -0
  788. package/src/server/data-query.js +461 -0
  789. package/src/server/dns.js +553 -0
  790. package/src/server/downloader.js +73 -0
  791. package/src/server/logger.js +229 -0
  792. package/src/server/middlewares.js +152 -0
  793. package/src/server/peer.js +75 -0
  794. package/src/server/process.js +299 -0
  795. package/src/server/proxy.js +139 -0
  796. package/src/server/runtime-status.js +252 -0
  797. package/src/server/runtime.js +183 -0
  798. package/src/server/start.js +272 -0
  799. package/src/server/tls.js +254 -0
  800. package/src/server/valkey.js +201 -0
  801. package/src/server.js +25 -0
  802. package/src/ws/IoInterface.js +129 -0
  803. package/src/ws/IoServer.js +68 -0
  804. package/src/ws/core/channels/core.ws.chat.js +68 -0
  805. package/src/ws/core/channels/core.ws.mailer.js +116 -0
  806. package/src/ws/core/channels/core.ws.stream.js +90 -0
  807. package/src/ws/core/core.ws.connection.js +41 -0
  808. package/src/ws/core/core.ws.emit.js +37 -0
  809. package/src/ws/core/core.ws.server.js +44 -0
  810. package/src/ws/default/channels/default.ws.main.js +57 -0
  811. package/src/ws/default/default.ws.connection.js +35 -0
  812. package/src/ws/default/default.ws.emit.js +14 -0
  813. package/src/ws/default/default.ws.server.js +39 -0
  814. package/test/api.test.js +53 -0
  815. package/test/crypto.test.js +117 -0
  816. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  817. package/test/deploy-monitor.test.js +267 -0
  818. package/test/shape-generator.test.js +463 -0
  819. package/tsconfig.docs.json +15 -0
  820. package/typedoc.dd-cyberia.json +31 -0
  821. package/typedoc.json +31 -0
package/src/cli/run.js ADDED
@@ -0,0 +1,3326 @@
1
+ /**
2
+ * @description The main entry point for the Underpost CLI applications.
3
+ * @module src/cli/run.js
4
+ * @namespace UnderpostRun
5
+ */
6
+
7
+ import { daemonProcess, getTerminalPid, pbcopy, shellCd, shellExec } from '../server/process.js';
8
+ import crypto from 'crypto';
9
+ import {
10
+ awaitDeployMonitor,
11
+ buildKindPorts,
12
+ Config,
13
+ cronDeployIdResolve,
14
+ etcHostFactory,
15
+ getNpmRootPath,
16
+ isDeployRunnerContext,
17
+ loadConfInstances,
18
+ loadConfServerJson,
19
+ selectConfInstances,
20
+ loadReplicas,
21
+ writeEnv,
22
+ } from '../server/conf.js';
23
+ import { actionInitLog, loggerFactory } from '../server/logger.js';
24
+
25
+ import fs from 'fs-extra';
26
+ import net from 'net';
27
+ import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
28
+
29
+ import os from 'os';
30
+ import Underpost from '../index.js';
31
+ import dotenv from 'dotenv';
32
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
33
+
34
+ const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
35
+ new Promise((resolve, reject) => {
36
+ let attempts = 0;
37
+ const tryConnect = () => {
38
+ attempts++;
39
+ const socket = net.createConnection({ port, host }, () => {
40
+ socket.destroy();
41
+ resolve();
42
+ });
43
+ socket.on('error', () => {
44
+ socket.destroy();
45
+ if (attempts >= maxAttempts) return reject(new Error(`Port ${port} not ready after ${maxAttempts} attempts`));
46
+ setTimeout(tryConnect, interval);
47
+ });
48
+ };
49
+ tryConnect();
50
+ });
51
+
52
+ const logger = loggerFactory(import.meta);
53
+
54
+ /**
55
+ * @method instanceProxyRoutesFactory
56
+ * @description Renders the HTTPProxy route block for every instance sharing a host.
57
+ * Routes are emitted longest-prefix first so a specific instance path (`/FOREST`)
58
+ * is never shadowed by the default instance's catch-all (`/`).
59
+ * @param {string} deployId - Parent deployment identifier.
60
+ * @param {Array<object>} instances - Expanded instance entries bound to one host.
61
+ * @param {string} env - `development` | `production`.
62
+ * @param {Object<string,string>} trafficById - Instance id → traffic colour.
63
+ * @returns {string} Concatenated route YAML.
64
+ */
65
+ const instanceProxyRoutesFactory = ({ deployId, instances, env, trafficById }) =>
66
+ [...instances]
67
+ .sort((a, b) => (b.path || '/').length - (a.path || '/').length)
68
+ .map((instance) =>
69
+ Underpost.deploy.deploymentYamlServiceFactory({
70
+ path: instance.path,
71
+ port: env === 'development' && instance.fromDebugPort ? instance.fromDebugPort : instance.fromPort,
72
+ deployId: `${deployId}-${instance.id}`,
73
+ env,
74
+ deploymentVersions: [trafficById[instance.id] || 'blue'],
75
+ pathRewritePolicy: instance.pathRewritePolicy,
76
+ }),
77
+ )
78
+ .join('');
79
+
80
+ /**
81
+ * @constant DEFAULT_OPTION
82
+ * @description Default options for the UnderpostRun class.
83
+ * @typedef {Object} UnderpostRunDefaultOptions
84
+ * @type {Object}
85
+ * @property {boolean} dev - Whether to run in development mode.
86
+ * @property {string} podName - The name of the pod to run.
87
+ * @property {string} nodeName - The name of the node to run.
88
+ * @property {string} sshKeyPath - Private key path for node SSH operations, forwarded to volume shipping over SSH.
89
+ * @property {number} port - Custom port to use.
90
+ * @property {string} volumeHostPath - The host path for the volume.
91
+ * @property {string} volumeMountPath - The mount path for the volume.
92
+ * @property {string} imageName - The name of the image to run.
93
+ * @property {string} containerName - The name of the container to run.
94
+ * @property {string} namespace - The namespace to run in.
95
+ * @property {string} timeoutResponse - The response timeout duration.
96
+ * @property {string} timeoutIdle - The idle timeout duration.
97
+ * @property {string} retryCount - The number of retries.
98
+ * @property {string} retryPerTryTimeout - The timeout duration per retry.
99
+ * @property {boolean} build - Whether to build the image.
100
+ * @property {number} replicas - The number of replicas to run.
101
+ * @property {boolean} force - Whether to force the operation.
102
+ * @property {boolean} reset - Whether to reset the operation.
103
+ * @property {boolean} tls - Whether to use TLS.
104
+ * @property {string} cmd - The command to run in the container.
105
+ * @property {string} tty - The TTY option for the container.
106
+ * @property {string} stdin - The stdin option for the container.
107
+ * @property {string} restartPolicy - The restart policy for the container.
108
+ * @property {string} runtimeClassName - The runtime class name for the container.
109
+ * @property {string} imagePullPolicy - The image pull policy for the container.
110
+ * @property {string} apiVersion - The API version for the container.
111
+ * @property {string} claimName - The claim name for the volume.
112
+ * @property {string} kindType - The kind of resource to create.
113
+ * @property {number} devProxyPortOffset - The port offset for the development proxy.
114
+ * @property {boolean} hostNetwork - Whether to use host networking.
115
+ * @property {string} requestsMemory - The memory request for the container.
116
+ * @property {string} requestsCpu - The CPU request for the container.
117
+ * @property {string} limitsMemory - The memory limit for the container.
118
+ * @property {string} limitsCpu - The CPU limit for the container.
119
+ * @property {string} resourceTemplateId - The resource template ID.
120
+ * @property {boolean} expose - Whether to expose the service.
121
+ * @property {boolean} etcHosts - Whether to modify /etc/hosts.
122
+ * @property {string} confServerPath - The configuration server path.
123
+ * @property {string} underpostRoot - The root path of the Underpost installation.
124
+ * @property {string} cmdCronJobs - Pre-script commands to run before cron job execution.
125
+ * @property {string} deployIdCronJobs - The deployment ID for cron jobs.
126
+ * @property {string} timezone - The timezone to set.
127
+ * @property {boolean} kubeadm - Whether to run in kubeadm mode.
128
+ * @property {boolean} kind - Whether to run in kind mode.
129
+ * @property {boolean} k3s - Whether to run in k3s mode.
130
+ * @property {string} hosts - The hosts to use.
131
+ * @property {string} deployId - The deployment ID.
132
+ * @property {string} instanceId - The instance ID.
133
+ * @property {string} user - The user to run as.
134
+ * @property {string} group - The group to use.
135
+ * @property {string} pid - The process ID.
136
+ * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
137
+ * @property {string} monitorStatus - The monitor status option.
138
+ * @property {string} monitorStatusKindType - The monitor status kind type option.
139
+ * @property {string} monitorStatusDeltaMs - The monitor status delta in milliseconds.
140
+ * @property {string} monitorStatusMaxAttempts - The maximum number of attempts for monitor status.
141
+ * @property {boolean} logs - Whether to enable logs.
142
+ * @property {boolean} dryRun - Whether to perform a dry run.
143
+ * @property {boolean} createJobNow - Whether to create the job immediately.
144
+ * @property {number} fromNCommit - Number of commits back to use for message propagation (default: 1, last commit only).
145
+ * @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
146
+ * As a string (CLI): semicolon-separated entries of "ip=hostname1,hostname2" (e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote").
147
+ * As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
148
+ * @property {boolean} gitClean - Whether to perform a `git clean` before running.
149
+ * @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
150
+ * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
151
+ * @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
152
+ * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
153
+ * @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
154
+ * @property {string} branch - The Git branch to use for operations (e.g., for template-deploy, ssh-deploy).
155
+ * @memberof UnderpostRun
156
+ */
157
+ const DEFAULT_OPTION = {
158
+ dev: false,
159
+ podName: '',
160
+ nodeName: '',
161
+ sshKeyPath: '',
162
+ port: 0,
163
+ volumeHostPath: '',
164
+ volumeMountPath: '',
165
+ imageName: '',
166
+ containerName: '',
167
+ namespace: 'default',
168
+ timeoutResponse: '',
169
+ timeoutIdle: '',
170
+ retryCount: '',
171
+ retryPerTryTimeout: '',
172
+ build: false,
173
+ replicas: 1,
174
+ force: false,
175
+ reset: false,
176
+ tls: false,
177
+ cmd: '',
178
+ tty: '',
179
+ stdin: '',
180
+ restartPolicy: '',
181
+ runtimeClassName: '',
182
+ imagePullPolicy: '',
183
+ apiVersion: '',
184
+ claimName: '',
185
+ kindType: '',
186
+ devProxyPortOffset: 0,
187
+ hostNetwork: false,
188
+ requestsMemory: '',
189
+ requestsCpu: '',
190
+ limitsMemory: '',
191
+ limitsCpu: '',
192
+ resourceTemplateId: '',
193
+ expose: false,
194
+ etcHosts: false,
195
+ confServerPath: '',
196
+ underpostRoot: '',
197
+ cmdCronJobs: '',
198
+ deployIdCronJobs: '',
199
+ timezone: '',
200
+ kubeadm: false,
201
+ kind: false,
202
+ k3s: false,
203
+ hosts: '',
204
+ deployId: '',
205
+ instanceId: '',
206
+ user: '',
207
+ group: '',
208
+ pid: '',
209
+ disablePrivateConfUpdate: false,
210
+ monitorStatus: '',
211
+ monitorStatusKindType: '',
212
+ monitorStatusDeltaMs: '',
213
+ monitorStatusMaxAttempts: '',
214
+ logs: false,
215
+ dryRun: false,
216
+ createJobNow: false,
217
+ fromNCommit: 0,
218
+ hostAliases: '',
219
+ gitClean: false,
220
+ copy: false,
221
+ skipFullBuild: false,
222
+ pullBundle: false,
223
+ remove: false,
224
+ test: false,
225
+ branch: '',
226
+ };
227
+
228
+ /**
229
+ * @class UnderpostRun
230
+ * @description Manages the execution of various CLI commands and operations.
231
+ * This class provides a set of static methods to perform different tasks
232
+ * such as running tests, deploying applications, managing environment variables,
233
+ * and more. It also includes a default option configuration and a collection of
234
+ * runners for executing specific commands.
235
+ * @memberof UnderpostRun
236
+ */
237
+ class UnderpostRun {
238
+ /**
239
+ * @static
240
+ * @description Collection of runners for executing specific commands.
241
+ * @type {Object}
242
+ * @memberof UnderpostRun
243
+ */
244
+ static RUNNERS = {
245
+ /**
246
+ * @method dev-cluster
247
+ * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
248
+ * @param {string} path - The input value, identifier, or path for the operation.
249
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
250
+ * @memberof UnderpostRun
251
+ */
252
+ 'dev-cluster': (path, options = DEFAULT_OPTION) => {
253
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
254
+ const mongoHosts = ['mongodb-0.mongodb-service'];
255
+ let primaryMongoHost = 'mongodb-0.mongodb-service';
256
+ if (!options.expose) {
257
+ shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --reset`);
258
+ shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
259
+
260
+ shellExec(
261
+ `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
262
+ ',',
263
+ )} --pull-image`,
264
+ );
265
+ shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --valkey --pull-image`);
266
+ }
267
+ if (options.k3s) {
268
+ if (options.remove) {
269
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:27017`);
270
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:6379`);
271
+ } else {
272
+ shellExec(`${baseCommand} lxd --expose k3s-control:27017 --node-port 32017`);
273
+ shellExec(`${baseCommand} lxd --expose k3s-control:6379 --node-port 32079`);
274
+ }
275
+ shellExec(`lxc config device show k3s-control`);
276
+ } else {
277
+ try {
278
+ const primaryPodName =
279
+ MongoBootstrap.getPrimaryPodName({
280
+ namespace: options.namespace,
281
+ podName: 'mongodb-0',
282
+ disableAuth: options.dev,
283
+ }) || 'mongodb-0';
284
+ shellExec(
285
+ `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config mongo`,
286
+ { async: true },
287
+ );
288
+ shellExec(
289
+ `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config valkey`,
290
+ { async: true },
291
+ );
292
+ } catch (error) {
293
+ logger.warn('Failed to detect MongoDB primary pod, using default', {
294
+ error: error.message,
295
+ default: primaryMongoHost,
296
+ });
297
+ }
298
+ }
299
+ const hostListenResult = etcHostFactory([primaryMongoHost]);
300
+ logger.info(hostListenResult.renderHosts);
301
+ },
302
+
303
+ /**
304
+ * @method etc-hosts
305
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
306
+ * based on the provided path input.
307
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
308
+ */
309
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
310
+ etcHostFactory(path.split(','));
311
+ },
312
+
313
+ /**
314
+ * @method ipfs-expose
315
+ * @description Exposes IPFS Cluster services on specified ports for local access.
316
+ * @type {Function}
317
+ * @memberof UnderpostRun
318
+ */
319
+ 'ipfs-expose': (path, options = DEFAULT_OPTION) => {
320
+ const ports = [5001, 9094, 8080];
321
+ for (const port of ports)
322
+ shellExec(`node bin deploy --expose ipfs-cluster --expose-port ${port} --disable-update-underpost-config`, {
323
+ async: true,
324
+ });
325
+ },
326
+
327
+ /**
328
+ * @method metadata
329
+ * @description Generates metadata for the specified path after exposing the development cluster.
330
+ * @param {string} path - The input value, identifier, or path for the operation.
331
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
332
+ * @memberof UnderpostRun
333
+ */
334
+ metadata: async (path, options = DEFAULT_OPTION) => {
335
+ const ports = '6379,27017';
336
+ shellExec(`node bin run kill '${ports}'`);
337
+ shellExec(`node bin run dev-cluster --dev --expose --namespace ${options.namespace}`, { async: true });
338
+ logger.info('Waiting for port-forward services to be ready...');
339
+ try {
340
+ await Promise.all([waitForPort(27017), waitForPort(6379)]);
341
+ logger.info('Port-forward services are ready');
342
+ } catch (err) {
343
+ logger.error('Port-forward services failed to become ready', { error: err.message });
344
+ shellExec(`node bin run kill '${ports}'`);
345
+ throw err;
346
+ }
347
+ shellExec(`node bin metadata --generate ${path}`);
348
+ shellExec(`node bin db --dev --clean-fs-collection dd`);
349
+ shellExec(`node bin run kill '${ports}'`);
350
+ },
351
+
352
+ /**
353
+ * @method svc-ls
354
+ * @description Lists systemd services and installed packages, optionally filtering by the provided path.
355
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional filter for services and packages).
356
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
357
+ * @memberof UnderpostRun
358
+ */
359
+ 'svc-ls': (path, options = DEFAULT_OPTION) => {
360
+ const log = shellExec(`systemctl list-units --type=service${path ? ` | grep ${path}` : ''}`, {
361
+ silent: true,
362
+ stdout: true,
363
+ });
364
+ console.log(path ? log.replaceAll(path, path.red) : log);
365
+ const log0 = shellExec(`sudo dnf list installed${path ? ` | grep ${path}` : ''}`, {
366
+ silent: true,
367
+ stdout: true,
368
+ });
369
+ console.log(path ? log0.replaceAll(path, path.red) : log0);
370
+ },
371
+
372
+ /**
373
+ * @method svc-rm
374
+ * @description Removes a systemd service by stopping it, disabling it, uninstalling the package, and deleting related files.
375
+ * @param {string} path - The input value, identifier, or path for the operation (used as the service name).
376
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
377
+ * @memberof UnderpostRun
378
+ */
379
+ 'svc-rm': (path, options = DEFAULT_OPTION) => {
380
+ shellExec(`sudo systemctl stop ${path}`);
381
+ shellExec(`sudo systemctl disable --now ${path}`);
382
+ shellExec(`sudo dnf remove -y ${path}*`);
383
+ shellExec(`sudo rm -f /usr/lib/systemd/system/${path}.service`);
384
+ shellExec(`sudo rm -f /etc/yum.repos.d/${path}*.repo`);
385
+ },
386
+
387
+ /**
388
+ * @method ssh-deploy-info
389
+ * @description Retrieves deployment status and pod information from a remote server via SSH.
390
+ * @param {string} path - The input value, identifier, or path for the operation.
391
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
392
+ * @memberof UnderpostRun
393
+ */
394
+ 'ssh-deploy-info': async (path = '', options = DEFAULT_OPTION) => {
395
+ const env = options.dev ? 'development' : 'production';
396
+ await Underpost.ssh.sshRemoteRunner(
397
+ `node bin deploy ${path ? path : 'dd'} ${env} --status && kubectl get pods -A`,
398
+ {
399
+ deployId: options.deployId,
400
+ user: options.user,
401
+ dev: options.dev,
402
+ remote: true,
403
+ useSudo: true,
404
+ cd: '/home/dd/engine',
405
+ },
406
+ );
407
+ },
408
+
409
+ /**
410
+ * @method node-move
411
+ * @description Abstract runner that relocates any schedulable Kubernetes workload
412
+ * (Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob, ReplicationController)
413
+ * onto a target node by patching its pod-template `nodeSelector` and rolling it out.
414
+ * Resource-kind agnostic: it resolves the kind dynamically and applies the right
415
+ * patch path, so it works for `sts`, `deployment`, etc. without bespoke logic.
416
+ *
417
+ * Selection grammar via `path`:
418
+ * - `<kind>/<name>` -> a single resource (e.g. `deployment/dd-core-production-blue`)
419
+ * - `<kind>` -> every resource of that kind in the namespace (e.g. `statefulset`)
420
+ * - `` -> all movable workloads (deployment, statefulset, daemonset) in the namespace
421
+ *
422
+ * Placement:
423
+ * - default: built-in `kubernetes.io/hostname=<node>` (no node mutation required)
424
+ * - `--labels k=v,...`: label the target node with those pairs and use them as the
425
+ * nodeSelector (matches the "label node + nodeSelector" pattern), enabling reusable
426
+ * workload pools instead of pinning to a single hostname.
427
+ *
428
+ * Flags: `--node-name <node>` (target), `--namespace <ns>`, `--dry-run` (preview only),
429
+ * `--remove` (clear the nodeSelector / unpin placement).
430
+ *
431
+ * Caveats: Services/ConfigMaps and bare Pods are not schedulable controllers and are
432
+ * skipped (move the owning controller). StatefulSets bound to node-local PVs may stay
433
+ * Pending after a move until their volume is available on the target node.
434
+ * @param {string} path - Resource selector (`kind/name`, `kind`, or empty).
435
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
436
+ * @memberof UnderpostRun
437
+ * @returns {Array<{ref:string,kind:string,status:string,node?:string}>} Per-resource outcome.
438
+ */
439
+ 'node-move': (path = '', options = DEFAULT_OPTION) => {
440
+ const node = options.nodeName;
441
+ const ns = options.namespace || 'default';
442
+ const dryRun = options.dryRun === true;
443
+ const remove = options.remove === true;
444
+
445
+ if (!remove && !node) {
446
+ throw new Error('node-move requires --node-name <target-node> (or --remove to clear placement)');
447
+ }
448
+
449
+ const normalizeKind = (k) =>
450
+ ({
451
+ deploy: 'deployment',
452
+ deployments: 'deployment',
453
+ deployment: 'deployment',
454
+ sts: 'statefulset',
455
+ statefulsets: 'statefulset',
456
+ statefulset: 'statefulset',
457
+ ds: 'daemonset',
458
+ daemonsets: 'daemonset',
459
+ daemonset: 'daemonset',
460
+ rs: 'replicaset',
461
+ replicasets: 'replicaset',
462
+ replicaset: 'replicaset',
463
+ rc: 'replicationcontroller',
464
+ replicationcontroller: 'replicationcontroller',
465
+ job: 'job',
466
+ jobs: 'job',
467
+ cj: 'cronjob',
468
+ cronjob: 'cronjob',
469
+ cronjobs: 'cronjob',
470
+ po: 'pod',
471
+ pod: 'pod',
472
+ pods: 'pod',
473
+ svc: 'service',
474
+ service: 'service',
475
+ services: 'service',
476
+ })[k] || k;
477
+
478
+ // Kinds that own a pod template we can patch; rolloutKinds additionally
479
+ // support `kubectl rollout restart` to reschedule existing pods now.
480
+ const templated = [
481
+ 'deployment',
482
+ 'statefulset',
483
+ 'daemonset',
484
+ 'replicaset',
485
+ 'job',
486
+ 'cronjob',
487
+ 'replicationcontroller',
488
+ ];
489
+ const rolloutKinds = ['deployment', 'statefulset', 'daemonset'];
490
+ const templateSelectorPath = (kind) =>
491
+ kind === 'cronjob'
492
+ ? ['spec', 'jobTemplate', 'spec', 'template', 'spec', 'nodeSelector']
493
+ : ['spec', 'template', 'spec', 'nodeSelector'];
494
+
495
+ // Resolve the desired nodeSelector. Custom --labels enables reusable pools;
496
+ // otherwise pin by the always-present hostname label.
497
+ let selector = { 'kubernetes.io/hostname': node };
498
+ if (!remove && options.labels) {
499
+ selector = {};
500
+ for (const pair of `${options.labels}`
501
+ .split(',')
502
+ .map((s) => s.trim())
503
+ .filter(Boolean)) {
504
+ const eq = pair.indexOf('=');
505
+ if (eq < 0) continue;
506
+ selector[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
507
+ }
508
+ }
509
+
510
+ // Verify the target node exists, and apply custom labels to it if provided.
511
+ if (!remove) {
512
+ const found = shellExec(`kubectl get node ${node} -o name`, {
513
+ silent: true,
514
+ stdout: true,
515
+ silentOnError: true,
516
+ }).trim();
517
+ if (!found) throw new Error(`Target node not found: ${node}`);
518
+ if (options.labels) {
519
+ const labelArgs = Object.entries(selector)
520
+ .map(([k, v]) => `${k}=${v}`)
521
+ .join(' ');
522
+ const labelCmd = `kubectl label node ${node} ${labelArgs} --overwrite`;
523
+ if (dryRun) logger.info(`[dry-run] ${labelCmd}`);
524
+ else shellExec(labelCmd);
525
+ }
526
+ }
527
+
528
+ const kubectlNames = (kind) =>
529
+ (
530
+ shellExec(`kubectl get ${kind} -n ${ns} -o name`, {
531
+ silent: true,
532
+ stdout: true,
533
+ silentOnError: true,
534
+ }).trim() || ''
535
+ )
536
+ .split('\n')
537
+ .map((s) => s.trim())
538
+ .filter(Boolean);
539
+
540
+ // Build the list of "kind/name" targets from the selection grammar.
541
+ let targets = [];
542
+ if (!path) {
543
+ for (const kind of ['deployment', 'statefulset', 'daemonset']) targets.push(...kubectlNames(kind));
544
+ } else if (path.includes('/')) {
545
+ targets = [path];
546
+ } else {
547
+ targets = kubectlNames(path);
548
+ }
549
+
550
+ if (targets.length === 0) {
551
+ logger.warn('node-move: no matching resources found', { path, namespace: ns });
552
+ return [];
553
+ }
554
+
555
+ // Merge-patch body that sets (or clears, when --remove) the nodeSelector.
556
+ const buildPatch = (kind) => {
557
+ const keys = templateSelectorPath(kind);
558
+ let obj = remove ? null : selector;
559
+ for (let i = keys.length - 1; i >= 0; i--) obj = { [keys[i]]: obj };
560
+ return JSON.stringify(obj);
561
+ };
562
+
563
+ const results = [];
564
+ for (const ref of targets) {
565
+ const slash = ref.indexOf('/');
566
+ const rawKind = slash >= 0 ? ref.slice(0, slash) : path && !path.includes('/') ? path : '';
567
+ const name = slash >= 0 ? ref.slice(slash + 1) : ref;
568
+ const kind = normalizeKind(`${rawKind}`.split('.')[0].toLowerCase());
569
+
570
+ if (!templated.includes(kind)) {
571
+ logger.warn(`node-move: ${kind}/${name} is not a schedulable controller; skipping`, {
572
+ hint: 'move its owning controller (deployment/statefulset/daemonset) instead',
573
+ });
574
+ results.push({ ref, kind, status: 'skipped' });
575
+ continue;
576
+ }
577
+
578
+ // Idempotency: skip the patch + rollout if the resource is already where
579
+ // we want it. Compares the live pod-template nodeSelector against the
580
+ // desired placement so a repeated run does not trigger an unnecessary
581
+ // rollout restart.
582
+ const basePath = kind === 'cronjob' ? 'spec.jobTemplate.spec.template.spec' : 'spec.template.spec';
583
+ const jsonpath = (expr) =>
584
+ shellExec(`kubectl get ${kind} ${name} -n ${ns} -o jsonpath='${expr}'`, {
585
+ silent: true,
586
+ stdout: true,
587
+ silentOnError: true,
588
+ disableLog: true,
589
+ }).trim();
590
+
591
+ if (remove) {
592
+ const current = jsonpath(`{.${basePath}.nodeSelector}`);
593
+ if (!current || current === 'map[]') {
594
+ logger.info(`node-move: ${kind}/${name} already has no nodeSelector; nothing to clear`);
595
+ results.push({ ref, kind, status: 'already-cleared' });
596
+ continue;
597
+ }
598
+ } else {
599
+ const alreadyOnNode = Object.entries(selector).every(([k, v]) => {
600
+ const esc = k.replace(/\./g, '\\.');
601
+ return jsonpath(`{.${basePath}.nodeSelector.${esc}}`) === v;
602
+ });
603
+ if (alreadyOnNode) {
604
+ logger.info(`node-move: ${kind}/${name} already pinned to ${node}; skipping`, { namespace: ns });
605
+ results.push({ ref, kind, status: 'already-on-node', node });
606
+ continue;
607
+ }
608
+ }
609
+
610
+ const patchCmd = `kubectl patch ${kind} ${name} -n ${ns} --type=merge -p '${buildPatch(kind)}'`;
611
+ const restartCmd = `kubectl rollout restart ${kind} ${name} -n ${ns}`;
612
+ if (dryRun) {
613
+ logger.info(`[dry-run] ${patchCmd}`);
614
+ if (rolloutKinds.includes(kind)) logger.info(`[dry-run] ${restartCmd}`);
615
+ results.push({ ref, kind, status: 'dry-run', node: remove ? undefined : node });
616
+ continue;
617
+ }
618
+
619
+ shellExec(patchCmd);
620
+ if (rolloutKinds.includes(kind)) shellExec(restartCmd);
621
+ logger.info(remove ? `Cleared node placement: ${kind}/${name}` : `Moved ${kind}/${name} -> ${node}`, {
622
+ namespace: ns,
623
+ });
624
+ results.push({ ref, kind, status: remove ? 'cleared' : 'moved', node: remove ? undefined : node });
625
+ }
626
+
627
+ logger.info('node-move complete', { namespace: ns, node: remove ? null : node, count: results.length });
628
+ return results;
629
+ },
630
+
631
+ /**
632
+ * @method dev-hosts-expose
633
+ * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
634
+ * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
635
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
636
+ * @memberof UnderpostRun
637
+ */
638
+ 'dev-hosts-expose': (path, options = DEFAULT_OPTION) => {
639
+ shellExec(
640
+ `node bin deploy ${path} development --disable-update-deployment --disable-update-proxy --kubeadm --etc-hosts`,
641
+ );
642
+ },
643
+
644
+ /**
645
+ * @method dev-hosts-restore
646
+ * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
647
+ * @param {string} path - The input value, identifier, or path for the operation.
648
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
649
+ * @memberof UnderpostRun
650
+ */
651
+ 'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
652
+ shellExec(`node bin deploy --restore-hosts`);
653
+ },
654
+
655
+ /**
656
+ * @method cluster-build
657
+ * @description Build configuration for cluster deployment.
658
+ * @param {string} path - The input value, identifier, or path for the operation.
659
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
660
+ * @memberof UnderpostRun
661
+ */
662
+ 'cluster-build': (path, options = DEFAULT_OPTION) => {
663
+ const nodeOptions =
664
+ (options.nodeName ? ` --node-name ${options.nodeName}` : '') +
665
+ (options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
666
+ shellExec(`node bin run clean`);
667
+ shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
668
+ shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
669
+ shellExec(`node bin env clean`);
670
+ for (const deployId of fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').split(','))
671
+ shellExec(`node bin new --default-conf --deploy-id ${deployId.trim()}`);
672
+ if (path === 'cmt') {
673
+ shellExec(`git add . && underpost cmt . build cluster-build`);
674
+ shellExec(`cd engine-private && git add . && underpost cmt . build cluster-build`);
675
+ }
676
+ },
677
+ /**
678
+ * @method template-deploy
679
+ * @description Pushes `engine-private`, dispatches CI workflow to build `pwa-microservices-template`,
680
+ * and optionally triggers engine-<conf-id> CI with sync/init which in turn dispatches the CD workflow
681
+ * after the build chain completes (template → ghpkg → engine-<conf-id> → CD).
682
+ * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
683
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
684
+ * @memberof UnderpostRun
685
+ */
686
+ 'template-deploy': (path = '', options = DEFAULT_OPTION) => {
687
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
688
+ shellExec(`npm run security:secrets`);
689
+ const reportPath = './gitleaks-report.json';
690
+ if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
691
+ logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
692
+ return;
693
+ }
694
+ shellExec(`${baseCommand} run pull`);
695
+ shellExec(`${baseCommand} run shared-dir`);
696
+
697
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
698
+ // propagation to pwa-microservices-template and every engine-* repo.
699
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
700
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
701
+ silent: true,
702
+ stdout: true,
703
+ }).trim();
704
+
705
+ shellExec(
706
+ `${baseCommand} push ./engine-private ${options.force ? '-f ' : ''}${
707
+ process.env.GITHUB_USERNAME
708
+ }/engine-private`,
709
+ );
710
+ shellCd('/home/dd/engine');
711
+
712
+ // Push engine repo so workflow YAML changes reach GitHub
713
+ shellExec(`git reset`);
714
+ shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
715
+
716
+ // Determine deploy conf and type from path (sync-engine-core, init-engine-core, etc.)
717
+ let deployConfId = '';
718
+ let deployType = '';
719
+ if (path.startsWith('sync-')) {
720
+ deployConfId = path.replace(/^sync-/, '');
721
+ deployType = 'sync-and-deploy';
722
+ } else if (path.startsWith('init-')) {
723
+ deployConfId = path.replace(/^init-/, '');
724
+ deployType = 'init';
725
+ }
726
+
727
+ // If --build is set and path is a sync-engine-* target, push the pre-built client bundle
728
+ // to Cloudinary so the remote container can pull it instead of rebuilding from source.
729
+ if (options.build && deployConfId && deployConfId.startsWith('engine-')) {
730
+ const confName = deployConfId.replace(/^engine-/, '');
731
+ const pushDeployId = options.deployId || `dd-${confName}`;
732
+ logger.info(`[template-deploy] Running push-bundle for deployId: ${pushDeployId}`);
733
+ shellExec(`${baseCommand} run push-bundle --deploy-id ${pushDeployId}`);
734
+ }
735
+
736
+ // Dispatch npmpkg CI workflow — this builds pwa-microservices-template first.
737
+ // If deployConfId is set, npmpkg.ci.yml will dispatch the engine-<conf-id> CI
738
+ // with sync=true after template build completes. The engine CI then dispatches
739
+ // the CD workflow after the engine repo build finishes — ensuring correct sequence:
740
+ // npmpkg.ci → engine-<id>.ci → engine-<id>.cd
741
+ const repo = `${process.env.GITHUB_USERNAME}/engine`;
742
+ const inputs = {};
743
+ if (sanitizedMessage) inputs.message = sanitizedMessage;
744
+ if (deployConfId) inputs.deploy_conf_id = deployConfId;
745
+ if (deployType) inputs.deploy_type = deployType;
746
+
747
+ // Omit `ref` so dispatchWorkflow auto-detects the repo's default branch
748
+ // (a fork may default to `main` rather than the monorepo's `master`).
749
+ Underpost.repo.dispatchWorkflow({
750
+ repo,
751
+ workflowFile: 'npmpkg.ci.yml',
752
+ inputs,
753
+ });
754
+ },
755
+
756
+ /**
757
+ * @method template-deploy-local
758
+ * @description Similar to `template-deploy` but runs the workflow locally without dispatching GitHub Actions. It pulls the latest changes, pushes to GitHub, builds the template, and optionally triggers a local release with CI push.
759
+ * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
760
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
761
+ * @memberof UnderpostRun
762
+ */
763
+ 'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
764
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
765
+ shellExec(`npm run security:secrets`);
766
+ const reportPath = './gitleaks-report.json';
767
+ if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
768
+ logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
769
+ return;
770
+ }
771
+ shellExec(`${baseCommand} run pull`);
772
+ shellExec(`${baseCommand} run shared-dir`);
773
+
774
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
775
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
776
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
777
+ silent: true,
778
+ stdout: true,
779
+ }).trim();
780
+
781
+ const { triggerCmd } = path
782
+ ? await Underpost.release.ci(path, sanitizedMessage, options)
783
+ : await Underpost.release.pwa(sanitizedMessage, options);
784
+ pbcopy(triggerCmd + ' && cd /home/dd/engine');
785
+ },
786
+ /**
787
+ * @method docker-image
788
+ * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
789
+ * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
790
+ * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
791
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
792
+ * @memberof UnderpostRun
793
+ */
794
+ 'docker-image': (path, options = DEFAULT_OPTION) => {
795
+ const repo = Underpost.repo.resolveInstanceRepo(path, !options.test);
796
+ Underpost.repo.dispatchWorkflow({
797
+ repo,
798
+ workflowFile: `docker-image${path ? `.${path}` : ''}${options.dev ? '.dev' : ''}.ci.yml`,
799
+ inputs: {},
800
+ });
801
+ },
802
+ /**
803
+ * @method clean
804
+ * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
805
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional directory path).
806
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
807
+ * @memberof UnderpostRun
808
+ */
809
+ clean: (path = '', options = DEFAULT_OPTION) => {
810
+ Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
811
+ if (options.dev) shellExec(`node bin run shared-dir ${path ? path : '/home/dd/engine'}`);
812
+ },
813
+ /**
814
+ * @method pull
815
+ * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
816
+ * @param {string} path - The input value, identifier, or path for the operation.
817
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
818
+ * @memberof UnderpostRun
819
+ */
820
+ pull: (path, options = DEFAULT_OPTION) => {
821
+ // shellExec is fail-fast by default — any non-zero exit throws and
822
+ // propagates up to the workflow step. No per-call flag required.
823
+ if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
824
+ fs.mkdirSync(`/home/dd`, { recursive: true });
825
+ shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
826
+ } else {
827
+ shellExec(`underpost run clean`);
828
+ shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
829
+ }
830
+ if (!fs.existsSync(`/home/dd/engine/engine-private`))
831
+ shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
832
+ silent: true,
833
+ });
834
+ else
835
+ shellExec(
836
+ `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
837
+ { silent: true },
838
+ );
839
+ },
840
+ /**
841
+ * @method release-deploy
842
+ * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
843
+ * @param {string} path - The input value, identifier, or path for the operation.
844
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
845
+ * @memberof UnderpostRun
846
+ */
847
+ 'release-deploy': (path, options = DEFAULT_OPTION) => {
848
+ actionInitLog();
849
+ shellExec(`underpost --version`);
850
+ shellCd(`/home/dd/engine`);
851
+ for (const _deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
852
+ const deployId = _deployId.trim();
853
+ shellExec(`underpost run deploy ${deployId}`, { async: true });
854
+ }
855
+ },
856
+ /**
857
+ * @method ssh-deploy
858
+ * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
859
+ * @param {string} path - The deployment identifier (e.g., 'engine-core', 'sync-engine-core', 'init-engine-core').
860
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
861
+ * @memberof UnderpostRun
862
+ */
863
+ 'ssh-deploy': (path, options = DEFAULT_OPTION) => {
864
+ actionInitLog();
865
+
866
+ let job = 'deploy';
867
+ let confId = path;
868
+ if (path.startsWith('sync-')) {
869
+ job = 'sync-and-deploy';
870
+ confId = path.replace(/^sync-/, '');
871
+ } else if (path.startsWith('init-')) {
872
+ job = 'init';
873
+ confId = path.replace(/^init-/, '');
874
+ }
875
+ const repo = Underpost.repo.resolveInstanceRepo(confId, !options.test);
876
+ // Omit `ref` so dispatchWorkflow auto-detects the target repo's default
877
+ // branch (getDefaultBranch): the monorepo is `master` but instance repos
878
+ // like engine-cyberia default to `main` — hardcoding either 422s.
879
+ Underpost.repo.dispatchWorkflow({
880
+ repo,
881
+ workflowFile: `${confId}.cd.yml`,
882
+ inputs: { job },
883
+ });
884
+ },
885
+ /**
886
+ * @method ide
887
+ * @description Opens a Visual Studio Code (VS Code) session for the specified path using `node ${underpostRoot}/bin/zed ${path}`,
888
+ * or installs Zed and sublime-text IDE if `path` is 'install'.
889
+ * @param {string} path - The input value, identifier, or path for the operation (used as the path to the directory to open in the IDE).
890
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
891
+ * @memberof UnderpostRun
892
+ */
893
+ ide: (path = '', options = DEFAULT_OPTION) => {
894
+ const underpostRoot = options.dev ? '.' : options.underpostRoot;
895
+ const [projectPath, customIde] = path.split(',');
896
+ if (projectPath === 'install') {
897
+ if (customIde === 'zed') shellExec(`sudo curl -f https://zed.dev/install.sh | sh`);
898
+ else if (customIde === 'subl') {
899
+ shellExec(
900
+ `sudo dnf config-manager --add-repo https://download.sublimetext.com/rpm/stable/x86_64/sublime-text.repo`,
901
+ );
902
+ shellExec(`sudo dnf install -y sublime-text`);
903
+ } else {
904
+ shellExec(`sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc &&
905
+ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\nautorefresh=1\ntype=rpm-md\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" | sudo tee /etc/yum.repos.d/vscode.repo > /dev/null`);
906
+ shellExec(`sudo dnf install -y code`);
907
+ }
908
+ return;
909
+ }
910
+ if (customIde === 'zed') shellExec(`node ${underpostRoot}/bin/zed ${projectPath}`);
911
+ else shellExec(`node ${underpostRoot}/bin/vs ${projectPath}`);
912
+ },
913
+ /**
914
+ * @method crypto-policy
915
+ * @description Sets the system's crypto policies to `DEFAULT:SHA1` using `update-crypto-policies` command.
916
+ * @param {string} path - The input value, identifier, or path for the operation.
917
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
918
+ * @memberof UnderpostRun
919
+ */
920
+ 'crypto-policy': (path, options = DEFAULT_OPTION) => {
921
+ shellExec(`sudo update-crypto-policies --set DEFAULT:SHA1`);
922
+ },
923
+ /**
924
+ * @method sync
925
+ * @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).
926
+ *
927
+ * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
928
+ * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
929
+ * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
930
+ * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
931
+ * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
932
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
933
+ * @memberof UnderpostRun
934
+ */
935
+ sync: async (path, options = DEFAULT_OPTION) => {
936
+ // Dev usage: node bin run --dev --build sync dd-default
937
+ const env = options.dev ? 'development' : 'production';
938
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
939
+ const baseClusterCommand = options.dev ? ' --dev' : '';
940
+ const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
941
+ const defaultPath = [
942
+ 'dd-default',
943
+ options.replicas,
944
+ ``,
945
+ ``,
946
+ !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
947
+ ];
948
+ let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
949
+ deployId = deployId ? deployId : defaultPath[0];
950
+ replicas = replicas ? replicas : defaultPath[1];
951
+ versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
952
+ image = image ? image : defaultPath[3];
953
+ node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
954
+ shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
955
+
956
+ if (image && !image.startsWith('localhost'))
957
+ Underpost.image.pullDockerHubImage({
958
+ dockerhubImage: image,
959
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
960
+ kubeadm: options.nodeName || options.kubeadm,
961
+ k3s: options.k3s,
962
+ });
963
+
964
+ if (isDeployRunnerContext(path, options)) {
965
+ if (!options.disablePrivateConfUpdate) {
966
+ const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
967
+ if (!validVersion) throw new Error('Version mismatch');
968
+ }
969
+ if (options.timezone !== 'none') shellExec(`${baseCommand} run${baseClusterCommand} tz`);
970
+ if (options.deployIdCronJobs !== 'none')
971
+ shellExec(`node bin cron${baseClusterCommand}${clusterFlag} --setup-start --git --apply`);
972
+ }
973
+
974
+ const currentTraffic = isDeployRunnerContext(path, options)
975
+ ? Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace })
976
+ : '';
977
+ let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
978
+ if (targetTraffic) versions = versions ? versions : targetTraffic;
979
+
980
+ const ignorePods =
981
+ isDeployRunnerContext(path, options) && targetTraffic
982
+ ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
983
+ : [];
984
+
985
+ const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
986
+ const cmdString = options.cmd
987
+ ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
988
+ : '';
989
+ const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
990
+
991
+ const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
992
+ const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
993
+ const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
994
+ const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
995
+
996
+ shellExec(
997
+ `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
998
+ image ? ` --image ${image}` : ''
999
+ }${versions ? ` --versions ${versions}` : ''}${
1000
+ options.namespace ? ` --namespace ${options.namespace}` : ''
1001
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag} ${deployId} ${env}`,
1002
+ );
1003
+
1004
+ if (isDeployRunnerContext(path, options)) {
1005
+ // Backup app/services repositories with repo-backup configured
1006
+ shellExec(
1007
+ `${baseCommand} db ${deployId} ${clusterFlag}${baseClusterCommand} --repo-backup --primary-pod --git --force-clone --preserveUUID ${options.namespace ? ` --ns ${options.namespace}` : ''}`,
1008
+ );
1009
+ shellExec(
1010
+ `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --node ${node} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
1011
+ options.namespace ? ` --namespace ${options.namespace}` : ''
1012
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}${sshKeyPathFlag}`,
1013
+ );
1014
+ if (!targetTraffic)
1015
+ targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1016
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
1017
+ Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
1018
+ } else
1019
+ logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
1020
+ },
1021
+
1022
+ /**
1023
+ * @method stop
1024
+ * @description Stops a deployment by deleting the corresponding Kubernetes deployment and service resources.
1025
+ * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
1026
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1027
+ * @memberof UnderpostRun
1028
+ */
1029
+ stop: async (path = '', options = DEFAULT_OPTION) => {
1030
+ let currentTraffic = Underpost.deploy.getCurrentTraffic(options.deployId, {
1031
+ namespace: options.namespace,
1032
+ hostTest: options.hosts,
1033
+ });
1034
+ const env = options.dev ? 'development' : 'production';
1035
+
1036
+ if (!path.match('current')) currentTraffic === 'blue' ? (currentTraffic = 'green') : (currentTraffic = 'blue');
1037
+ const [_deployId] = path.split(',');
1038
+ const deploymentId = `${_deployId ? _deployId : options.deployId}${
1039
+ options.instanceId ? `-${options.instanceId}` : ''
1040
+ }-${env}-${currentTraffic}`;
1041
+
1042
+ shellExec(`kubectl delete deployment ${deploymentId} -n ${options.namespace}`);
1043
+ shellExec(`kubectl delete svc ${deploymentId}-service -n ${options.namespace}`);
1044
+ },
1045
+
1046
+ /**
1047
+ * @method ssh-deploy-stop
1048
+ * @description Stops a remote deployment via SSH by executing the appropriate Underpost command on the remote server.
1049
+ * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
1050
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1051
+ * @memberof UnderpostRun
1052
+ */
1053
+ 'ssh-deploy-stop': async (path, options = DEFAULT_OPTION) => {
1054
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1055
+ const baseClusterCommand = options.dev ? ' --dev' : '';
1056
+
1057
+ const remoteCommand = [
1058
+ `${baseCommand} run${baseClusterCommand} stop${path ? ` ${path}` : ''}`,
1059
+ ` --deploy-id ${options.deployId}${options.instanceId ? ` --instance-id ${options.instanceId}` : ''}`,
1060
+ ` --namespace ${options.namespace}${options.hosts ? ` --hosts ${options.hosts}` : ''}`,
1061
+ ].join('');
1062
+
1063
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
1064
+ deployId: options.deployId,
1065
+ user: options.user,
1066
+ dev: options.dev,
1067
+ remote: true,
1068
+ useSudo: true,
1069
+ cd: '/home/dd/engine',
1070
+ });
1071
+ },
1072
+
1073
+ /**
1074
+ * @method ssh-deploy-db-rollback
1075
+ * @description Performs a database rollback on remote deployment via SSH.
1076
+ * @param {string} path - Comma-separated deployId and optional number of commits to reset (format: "deployId,nCommits")
1077
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1078
+ * @param {string} options.deployId - The deployment identifier
1079
+ * @param {string} options.user - The SSH user for credential lookup
1080
+ * @param {boolean} options.dev - Development mode flag
1081
+ * @memberof UnderpostRun
1082
+ */
1083
+ 'ssh-deploy-db-rollback': async (path = '', options = DEFAULT_OPTION) => {
1084
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1085
+ let [deployId, nCommitsReset] = path.split(',');
1086
+ if (!nCommitsReset) nCommitsReset = 1;
1087
+
1088
+ const remoteCommand = `${baseCommand} db ${deployId} --git --kubeadm --primary-pod --force-clone --macro-rollback-export ${nCommitsReset}${options.namespace ? ` --ns ${options.namespace}` : ''}`;
1089
+
1090
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
1091
+ deployId: options.deployId,
1092
+ user: options.user,
1093
+ dev: options.dev,
1094
+ remote: true,
1095
+ useSudo: true,
1096
+ cd: '/home/dd/engine',
1097
+ });
1098
+ },
1099
+
1100
+ /**
1101
+ * @method ssh-deploy-db
1102
+ * @description Imports/restores a database on remote deployment via SSH.
1103
+ * @param {string} path - The deployment ID for database import
1104
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1105
+ * @param {string} options.deployId - The deployment identifier
1106
+ * @param {string} options.user - The SSH user for credential lookup
1107
+ * @param {boolean} options.dev - Development mode flag
1108
+ * @memberof UnderpostRun
1109
+ */
1110
+ 'ssh-deploy-db': async (path, options = DEFAULT_OPTION) => {
1111
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1112
+
1113
+ const remoteCommand = `${baseCommand} db ${path} --import --drop --preserveUUID --git --kubeadm --primary-pod --force-clone${options.namespace ? ` --ns ${options.namespace}` : ''}`;
1114
+
1115
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
1116
+ deployId: options.deployId,
1117
+ user: options.user,
1118
+ dev: options.dev,
1119
+ remote: true,
1120
+ useSudo: true,
1121
+ cd: '/home/dd/engine',
1122
+ });
1123
+ },
1124
+
1125
+ /**
1126
+ * @method ssh-deploy-db-status
1127
+ * @description Retrieves database status/stats for a deployment (or all deployments from dd.router) via SSH.
1128
+ * @param {string} path - Comma-separated deployId(s) or 'dd' to use the dd.router list.
1129
+ * @param {UnderpostRunDefaultOptions} options - Runner options (uses options.deployId for SSH host lookup).
1130
+ * @param {string} options.deployId - Deployment identifier used for SSH config lookup.
1131
+ * @param {string} options.user - SSH user for credential lookup.
1132
+ * @param {boolean} options.dev - Development mode flag.
1133
+ * @param {string} [options.namespace] - Kubernetes namespace to pass to the db check.
1134
+ * @memberof UnderpostRun
1135
+ */
1136
+ 'ssh-deploy-db-status': async (path = '', options = DEFAULT_OPTION) => {
1137
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1138
+
1139
+ let deployList = [];
1140
+ if (!path || path === 'dd') {
1141
+ if (!fs.existsSync('./engine-private/deploy/dd.router')) {
1142
+ logger.warn('dd.router not found; nothing to run');
1143
+ return;
1144
+ }
1145
+ deployList = fs
1146
+ .readFileSync('./engine-private/deploy/dd.router', 'utf8')
1147
+ .split(',')
1148
+ .map((d) => d.trim())
1149
+ .filter(Boolean);
1150
+ } else {
1151
+ deployList = path
1152
+ .split(',')
1153
+ .map((d) => d.trim())
1154
+ .filter(Boolean);
1155
+ }
1156
+
1157
+ for (const deployId of deployList) {
1158
+ const remoteCommand = `${baseCommand} db ${deployId} --stats --kubeadm --primary-pod${options.namespace ? ` --ns ${options.namespace}` : ''}`;
1159
+
1160
+ await Underpost.ssh.sshRemoteRunner(remoteCommand, {
1161
+ deployId: options.deployId,
1162
+ user: options.user,
1163
+ dev: options.dev,
1164
+ remote: true,
1165
+ useSudo: true,
1166
+ cd: '/home/dd/engine',
1167
+ });
1168
+ }
1169
+ },
1170
+
1171
+ /**
1172
+ * @method tz
1173
+ * @description Sets the system timezone using `timedatectl set-timezone` command.
1174
+ * @param {string} path - The input value, identifier, or path for the operation (used as the timezone string).
1175
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1176
+ * @memberof UnderpostRun
1177
+ */
1178
+ tz: (path, options = DEFAULT_OPTION) => {
1179
+ const tz =
1180
+ options.timezone && options.timezone !== 'none'
1181
+ ? options.timezone
1182
+ : path
1183
+ ? path
1184
+ : Underpost.env.get('TIME_ZONE', undefined, { disableLog: true })
1185
+ ? Underpost.env.get('TIME_ZONE')
1186
+ : process.env.TIME_ZONE
1187
+ ? process.env.TIME_ZONE
1188
+ : 'America/New_York';
1189
+ shellExec(`sudo timedatectl set-timezone ${tz}`);
1190
+ },
1191
+
1192
+ /**
1193
+ * @method get-proxy
1194
+ * @description Retrieves and logs the HTTPProxy resources in the specified namespace using `kubectl get HTTPProxy`.
1195
+ * @param {string} path - The input value, identifier, or path for the operation (used as an optional filter for the HTTPProxy resources).
1196
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1197
+ * @memberof UnderpostRun
1198
+ */
1199
+ 'get-proxy': async (path = '', options = DEFAULT_OPTION) => {
1200
+ console.log(
1201
+ shellExec(`kubectl get HTTPProxy -n ${options.namespace} ${path} -o yaml`, {
1202
+ silent: true,
1203
+ stdout: true,
1204
+ })
1205
+ .replaceAll(`blue`, `blue`.bgBlue.bold.black)
1206
+ .replaceAll('green', 'green'.bgGreen.bold.black)
1207
+ .replaceAll('Error', 'Error'.bold.red)
1208
+ .replaceAll('error', 'error'.bold.red)
1209
+ .replaceAll('ERROR', 'ERROR'.bold.red)
1210
+ .replaceAll('Invalid', 'Invalid'.bold.red)
1211
+ .replaceAll('invalid', 'invalid'.bold.red)
1212
+ .replaceAll('INVALID', 'INVALID'.bold.red),
1213
+ );
1214
+ },
1215
+
1216
+ 'instance-promote': async (path, options = DEFAULT_OPTION) => {
1217
+ const env = options.dev ? 'development' : 'production';
1218
+ let [deployId, id] = path.split(',');
1219
+ const confInstances = loadConfInstances(deployId);
1220
+ const promoted = selectConfInstances(confInstances, id);
1221
+ let promotedTraffic = '';
1222
+
1223
+ // A Contour HTTPProxy is named after its host, so every instance sharing a
1224
+ // host shares one object. Rebuilding it from the promoted instance alone
1225
+ // would drop its siblings' routes, so each host is rendered from the full
1226
+ // set: promoted instances flip colour, the rest keep their live colour.
1227
+ const promotedIds = new Set(promoted.map((instance) => instance.id));
1228
+ const hosts = [...new Set(promoted.map((instance) => instance.host))];
1229
+ const affected = confInstances.filter((instance) => hosts.includes(instance.host));
1230
+ const trafficById = {};
1231
+ for (const instance of affected) {
1232
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(`${deployId}-${instance.id}`, {
1233
+ hostTest: instance.host,
1234
+ namespace: options.namespace,
1235
+ env,
1236
+ });
1237
+ if (!promotedIds.has(instance.id)) {
1238
+ trafficById[instance.id] = currentTraffic || 'blue';
1239
+ continue;
1240
+ }
1241
+ trafficById[instance.id] = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1242
+ promotedTraffic = trafficById[instance.id];
1243
+ }
1244
+
1245
+ for (const host of hosts) {
1246
+ const hostInstances = affected.filter((instance) => instance.host === host);
1247
+ let proxyYaml =
1248
+ Underpost.deploy.baseProxyYamlFactory({ host, env: options.tls ? 'production' : env, options }) +
1249
+ instanceProxyRoutesFactory({ deployId, instances: hostInstances, env, trafficById });
1250
+ if (options.tls) {
1251
+ if (options.test) {
1252
+ const sslDir = `./engine-private/ssl/${host}`;
1253
+ const nameSafe = host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1254
+ fs.mkdirpSync(sslDir);
1255
+ shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${host}"`);
1256
+ shellExec(`kubectl delete secret ${host} -n ${options.namespace} --ignore-not-found`);
1257
+ shellExec(
1258
+ `kubectl create secret tls ${host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1259
+ );
1260
+ } else {
1261
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
1262
+ proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host });
1263
+ }
1264
+ }
1265
+ shellExec(`kubectl delete HTTPProxy ${host} --namespace ${options.namespace} --ignore-not-found`);
1266
+ shellExec(
1267
+ `kubectl apply -f - -n ${options.namespace} <<EOF
1268
+ ${proxyYaml}
1269
+ EOF
1270
+ `,
1271
+ { disableLog: true },
1272
+ );
1273
+ }
1274
+ // Refresh the gRPC service to ensure it points to the parent deploy's current traffic.
1275
+ if (promotedTraffic) {
1276
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1277
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
1278
+ deployId,
1279
+ env,
1280
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
1281
+ namespace: options.namespace,
1282
+ traffic: [parentTraffic],
1283
+ });
1284
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
1285
+ }
1286
+ },
1287
+
1288
+ /**
1289
+ * @method instance
1290
+ * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1291
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1292
+ * @memberof UnderpostRun
1293
+ */
1294
+ instance: async (path = '', options = DEFAULT_OPTION) => {
1295
+ const env = options.dev ? 'development' : 'production';
1296
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1297
+ const baseClusterCommand = options.dev ? ' --dev' : '';
1298
+ let [deployId, id, replicas] = path.split(',');
1299
+ if (!replicas) replicas = options.replicas;
1300
+ const confInstances = selectConfInstances(loadConfInstances(deployId), id);
1301
+ const etcHosts = [];
1302
+ for (const instance of confInstances) {
1303
+ let {
1304
+ id: _id,
1305
+ host: _host,
1306
+ path: _path,
1307
+ image: _image,
1308
+ fromPort: _fromPort,
1309
+ toPort: _toPort,
1310
+ fromDebugPort: _fromDebugPort,
1311
+ toDebugPort: _toDebugPort,
1312
+ cmd: _cmd,
1313
+ volumes: _volumes,
1314
+ metadata: _metadata,
1315
+ lifecycle: _lifecycle,
1316
+ readinessProbe: _readinessProbe,
1317
+ livenessProbe: _livenessProbe,
1318
+ } = instance;
1319
+ const _deployId = `${deployId}-${_id}`;
1320
+ // Use debug ports in development when defined, fall back to production ports.
1321
+ if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
1322
+ if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
1323
+ etcHosts.push(_host);
1324
+ if (options.expose) continue;
1325
+ // Examples images:
1326
+ // `underpost/underpost-engine:${Underpost.version}`
1327
+ // `localhost/rockylinux9-underpost:${Underpost.version}`
1328
+ if (options.imageName) _image = options.imageName;
1329
+ if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1330
+
1331
+ if (_image && !_image.startsWith('localhost'))
1332
+ Underpost.image.pullDockerHubImage({
1333
+ dockerhubImage: _image,
1334
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
1335
+ kubeadm: options.nodeName || options.kubeadm,
1336
+ k3s: options.k3s,
1337
+ });
1338
+
1339
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1340
+ hostTest: _host,
1341
+ namespace: options.namespace,
1342
+ env,
1343
+ });
1344
+
1345
+ const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1346
+ const podId = `${_deployId}-${env}-${targetTraffic}`;
1347
+ const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
1348
+ Underpost.deploy.configMap(env, options.namespace);
1349
+ shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
1350
+ shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
1351
+ for (const _volume of _volumes)
1352
+ if (_volume.claimName)
1353
+ Underpost.deploy.deployVolume(_volume, {
1354
+ namespace: options.namespace,
1355
+ deployId: _deployId,
1356
+ env,
1357
+ version: targetTraffic,
1358
+ nodeName: Underpost.deploy.resolveDeployNode({
1359
+ node: options.nodeName,
1360
+ kind: options.kind,
1361
+ kubeadm: options.kubeadm,
1362
+ k3s: options.k3s,
1363
+ env,
1364
+ }),
1365
+ clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
1366
+ gitClean: options.gitClean || false,
1367
+ sshKeyPath: options.sshKeyPath || '',
1368
+ });
1369
+ // Regenerate the parent deploy's gRPC ClusterIP service pointing to the
1370
+ // parent's current traffic colour and apply it before the instance pod starts so
1371
+ // DNS is resolvable the moment the pod boots.
1372
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1373
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
1374
+ deployId,
1375
+ env,
1376
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
1377
+ namespace: options.namespace,
1378
+ traffic: [targetTraffic],
1379
+ host: _host,
1380
+ });
1381
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
1382
+
1383
+ const resolvedCmd = _cmd[env].map((c) =>
1384
+ c.replaceAll(
1385
+ '{{grpc-service-dns}}',
1386
+ `${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
1387
+ ),
1388
+ );
1389
+
1390
+ // Resolve env-scoped lifecycle/probe blocks: each can be either
1391
+ // { ...envObj } // shared shape
1392
+ // { development: {...}, production: {...} } // env-specific
1393
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1394
+
1395
+ // Convention: an instance config may place `imagePullPolicy` inside
1396
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1397
+ // Extract it onto the container spec (where K8S expects it) and
1398
+ // strip it from the lifecycle hash so the rendered YAML stays valid.
1399
+ // CLI override (`--image-pull-policy`) wins over the conf value.
1400
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1401
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1402
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1403
+
1404
+ let deploymentYaml = `---
1405
+ ${Underpost.deploy
1406
+ .deploymentYamlPartsFactory({
1407
+ deployId: _deployId,
1408
+ env,
1409
+ suffix: targetTraffic,
1410
+ resources: Underpost.deploy.resourcesFactory(options),
1411
+ replicas,
1412
+ image: _image,
1413
+ namespace: options.namespace,
1414
+ volumes: _volumes,
1415
+ cmd: resolvedCmd,
1416
+ lifecycle: lifecycleForManifest,
1417
+ readinessProbe: pickEnv(_readinessProbe),
1418
+ livenessProbe: pickEnv(_livenessProbe),
1419
+ containerPort: _toPort,
1420
+ imagePullPolicy: instanceImagePullPolicy,
1421
+ })
1422
+ .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
1423
+ `;
1424
+ // console.log(deploymentYaml);
1425
+ shellExec(
1426
+ `kubectl apply -f - -n ${options.namespace} <<EOF
1427
+ ${deploymentYaml}
1428
+ EOF
1429
+ `,
1430
+ { disableLog: true },
1431
+ );
1432
+ // Custom instances run a bare binary (no `underpost start` / internal
1433
+ // HTTP endpoint): Kubernetes readiness is the running signal and
1434
+ // container-status is read via exec. See `Deploy custom instance to K8S.md`.
1435
+ const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1436
+ _deployId,
1437
+ env,
1438
+ targetTraffic,
1439
+ ignorePods,
1440
+ options.namespace,
1441
+ { readyGate: 'kubernetes', statusTransport: 'exec' },
1442
+ );
1443
+
1444
+ if (!ready) {
1445
+ logger.error(`Deployment ${deployId} did not become ready in time.`);
1446
+ return;
1447
+ }
1448
+ }
1449
+ // Promote (switch the HTTPProxy to the new colour) ONLY after EVERY
1450
+ // instance in the family is deployed and ready — never per-instance inside
1451
+ // the loop. A per-variant switch would flip server.cyberiaonline.com to the
1452
+ // default's new deployment while -forest/-test are still being created,
1453
+ // pointing routes at services that don't exist yet. instance-promote
1454
+ // rebuilds the whole host proxy (all variant routes) in one shot, so a
1455
+ // single call with the family id promotes the family atomically.
1456
+ if (!options.expose) await UnderpostRun.RUNNERS['instance-promote'](`${deployId},${id}`, options);
1457
+ if (options.etcHosts) {
1458
+ const hostListenResult = etcHostFactory(etcHosts);
1459
+ logger.info(hostListenResult.renderHosts);
1460
+ }
1461
+ },
1462
+
1463
+ /**
1464
+ * @method deploy-key
1465
+ * @description Copies the deploy key for a specific user and deployId to a temporary location on the local machine.
1466
+ * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
1467
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1468
+ * @param {string} options.user - The user for which to copy the deploy key.
1469
+ * @param {string} options.deployId - The deployment identifier associated with the deploy key.
1470
+ * @memberof UnderpostRun
1471
+ */
1472
+ 'deploy-key': (path, options = DEFAULT_OPTION) => {
1473
+ const prefix = 'dd-key';
1474
+ if (options.reset) {
1475
+ shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
1476
+ return;
1477
+ }
1478
+ if (!options.user || !options.deployId) {
1479
+ logger.error('Both --user and --deploy-id options are required to copy the deploy key.');
1480
+ return;
1481
+ }
1482
+ const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
1483
+ fs.mkdirSync('/home/dd/tmp', { recursive: true });
1484
+ fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
1485
+ logger.info(`Copied deploy key to ${targetPath}`);
1486
+ if (options.copy) pbcopy(targetPath);
1487
+ },
1488
+
1489
+ /**
1490
+ * @method instance-build-manifest
1491
+ * @description Builds a Kubernetes Deployment + Service manifest for a specific instance entry
1492
+ * from `conf.instances.json` and writes it to a file. This is a purely local
1493
+ * artifact generator: it never probes a live cluster. Traffic colour defaults
1494
+ * to the canonical initial `blue` and can be overridden with `--traffic`; the
1495
+ * real blue/green swap is resolved at deploy time (`deploy --sync`).
1496
+ *
1497
+ * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
1498
+ * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
1499
+ *
1500
+ * @param {string} path - Comma-separated: `deployId,instanceId[,projectPath]`.
1501
+ * `projectPath` is the root directory that contains the `Dockerfile` (e.g. `./cyberia-client`).
1502
+ * Artifacts are written to `<projectPath>/manifests/<env>/Dockerfile` and
1503
+ * `<projectPath>/manifests/<env>/deployment.yaml`.
1504
+ * In production, files are also copied to `<projectPath>/Dockerfile` and
1505
+ * `<projectPath>/deployment.yaml`.
1506
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1507
+ * @memberof UnderpostRun
1508
+ */
1509
+ 'instance-build-manifest': (path, options = DEFAULT_OPTION) => {
1510
+ const env = options.dev ? 'development' : 'production';
1511
+ let [deployId, id, projectPath] = path.split(',');
1512
+ const rootPath = projectPath ? projectPath : '.';
1513
+
1514
+ const confInstances = loadConfInstances(deployId);
1515
+ // Targeting a template id builds every world in the family. The fan-out
1516
+ // re-enters this runner with `instanceOnly` because the default world
1517
+ // keeps the template id verbatim — without it, selecting `mmo-server`
1518
+ // would match the family again and recurse forever. Only that default
1519
+ // world publishes to the project root, so the repo keeps exactly one
1520
+ // canonical Dockerfile/deployment.yaml pair.
1521
+ const selected = options.instanceOnly
1522
+ ? confInstances.filter((instance) => instance.id === id)
1523
+ : selectConfInstances(confInstances, id);
1524
+ if (selected.length === 0) {
1525
+ logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
1526
+ return;
1527
+ }
1528
+ if (!options.instanceOnly && (selected.length > 1 || selected[0].id !== id)) {
1529
+ for (const instance of selected)
1530
+ UnderpostRun.RUNNERS['instance-build-manifest'](
1531
+ [deployId, instance.id, projectPath].filter((v) => v !== undefined).join(','),
1532
+ { ...options, instanceOnly: true },
1533
+ );
1534
+ return;
1535
+ }
1536
+
1537
+ const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
1538
+ const outputPath = `${envManifestPath}/deployment.yaml`;
1539
+ const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
1540
+
1541
+ fs.mkdirpSync(envManifestPath);
1542
+
1543
+ const instance = selected[0];
1544
+ const isDefaultInstance = instance.id === instance.templateId || !instance.templateId;
1545
+
1546
+ let {
1547
+ id: _id,
1548
+ host: _host,
1549
+ image: _image,
1550
+ fromPort: _fromPort,
1551
+ toPort: _toPort,
1552
+ fromDebugPort: _fromDebugPort,
1553
+ toDebugPort: _toDebugPort,
1554
+ cmd: _cmd,
1555
+ volumes: _volumes,
1556
+ metadata: _metadata,
1557
+ runtime: _runtime,
1558
+ lifecycle: _lifecycle,
1559
+ readinessProbe: _readinessProbe,
1560
+ livenessProbe: _livenessProbe,
1561
+ } = instance;
1562
+
1563
+ // Resolve Dockerfile source. Dev/prod variant rules:
1564
+ // - When the instance defines a `runtime`, look under
1565
+ // `src/runtime/<runtime>/`. In `--dev` mode prefer `Dockerfile.dev`
1566
+ // when it exists, falling back to `Dockerfile`.
1567
+ // - When `runtime` is not set, look in the project root with the
1568
+ // same `.dev` → no-suffix precedence.
1569
+ // Dockerfile.dev is a full Dockerfile (not an overlay) — each runtime
1570
+ // owns the contract between its dev image and its prod image (debug
1571
+ // build flags, extra tooling, default ports, etc.).
1572
+ const dockerfileBase = _runtime ? `src/runtime/${_runtime}` : rootPath;
1573
+ const dockerfileCandidates = options.dev
1574
+ ? [`${dockerfileBase}/Dockerfile.dev`, `${dockerfileBase}/Dockerfile`]
1575
+ : [`${dockerfileBase}/Dockerfile`];
1576
+ const dockerfileSourcePath = dockerfileCandidates.find((p) => fs.existsSync(p));
1577
+ if (dockerfileSourcePath) {
1578
+ if (options.dev && !dockerfileSourcePath.endsWith('.dev')) {
1579
+ logger.warn(
1580
+ `[instance-build-manifest] --dev requested but no Dockerfile.dev present; falling back to ${dockerfileSourcePath}`,
1581
+ );
1582
+ }
1583
+ fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
1584
+ } else {
1585
+ logger.warn(`[instance-build-manifest] Dockerfile not found; tried: ${dockerfileCandidates.join(', ')}`);
1586
+ }
1587
+
1588
+ const _deployId = `${deployId}-${_id}`;
1589
+ if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1590
+ // Use debug ports in development when defined, fall back to production ports.
1591
+ if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
1592
+ if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
1593
+
1594
+ // Build image from projectPath Dockerfile and load into cluster when --build is set.
1595
+ if (options.build && projectPath) {
1596
+ const isKind = !options.kubeadm && !options.k3s;
1597
+ Underpost.image.build({
1598
+ path: projectPath,
1599
+ imageName: _image,
1600
+ podmanSave: true,
1601
+ imageOutPath: projectPath,
1602
+ kind: isKind,
1603
+ kubeadm: !!options.kubeadm,
1604
+ k3s: !!options.k3s,
1605
+ reset: !!options.reset,
1606
+ dev: options.dev,
1607
+ });
1608
+ logger.info(`[instance-build-manifest] Image built and loaded`, {
1609
+ image: _image,
1610
+ cluster: isKind ? 'kind' : options.kubeadm ? 'kubeadm' : 'k3s',
1611
+ });
1612
+ }
1613
+
1614
+ const targetTraffic = options.traffic || 'blue';
1615
+ const parentTraffic = targetTraffic;
1616
+ const resolvedCmd = _cmd[env].map((c) =>
1617
+ c.replaceAll(
1618
+ '{{grpc-service-dns}}',
1619
+ `${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
1620
+ ),
1621
+ );
1622
+
1623
+ // Env-aware lifecycle / probe selection. Each block may either be
1624
+ // a single object (shared across envs) or `{ development, production }`.
1625
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1626
+
1627
+ // Convention: an instance config may place `imagePullPolicy` inside
1628
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1629
+ // Extract it onto the container spec and strip it from the lifecycle hash.
1630
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1631
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1632
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1633
+
1634
+ const deploymentYaml =
1635
+ `---\n` +
1636
+ Underpost.deploy
1637
+ .deploymentYamlPartsFactory({
1638
+ deployId: _deployId,
1639
+ env,
1640
+ suffix: targetTraffic,
1641
+ resources: Underpost.deploy.resourcesFactory(options),
1642
+ replicas: options.replicas,
1643
+ image: _image,
1644
+ namespace: options.namespace,
1645
+ volumes: _volumes,
1646
+ cmd: resolvedCmd,
1647
+ lifecycle: lifecycleForManifest,
1648
+ readinessProbe: pickEnv(_readinessProbe),
1649
+ livenessProbe: pickEnv(_livenessProbe),
1650
+ containerPort: _toPort,
1651
+ imagePullPolicy: instanceImagePullPolicy,
1652
+ })
1653
+ .replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
1654
+
1655
+ fs.writeFileSync(outputPath, deploymentYaml, 'utf8');
1656
+ logger.info(`[instance-build-manifest] Manifest written to ${outputPath}`, {
1657
+ deployId: _deployId,
1658
+ env,
1659
+ traffic: targetTraffic,
1660
+ image: _image,
1661
+ });
1662
+
1663
+ // --- Sibling manifests (pv-pvc, proxy, grpc-service) ------------------
1664
+ // Emit the same apply-able set the parent deploy writes to build/<env>,
1665
+ // scoped to this instance so the project repo and engine-private ship
1666
+ // more than just deployment.yaml. Content matches what `run instance`
1667
+ // creates dynamically at deploy time (deployVolume / instance-promote /
1668
+ // the parent gRPC ClusterIP), so a static `kubectl apply` is equivalent.
1669
+ const pvDataNode = Underpost.deploy.resolveDeployNode({
1670
+ node: options.nodeName,
1671
+ kind: options.kind,
1672
+ kubeadm: options.kubeadm,
1673
+ k3s: options.k3s,
1674
+ env,
1675
+ });
1676
+
1677
+ // pv-pvc.yaml — one PV+PVC per instance volume; names mirror deployVolume.
1678
+ let pvPvcYaml = '';
1679
+ for (const volume of _volumes || []) {
1680
+ if (!volume.claimName) continue;
1681
+ const pvcId = `${volume.claimName}-${_deployId}-${env}-${targetTraffic}`;
1682
+ const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${_deployId}-${env}-${targetTraffic}`;
1683
+ pvPvcYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
1684
+ pvcId,
1685
+ namespace: options.namespace,
1686
+ hostPath: `/home/dd/engine/volume/${pvId}`,
1687
+ nodeName: pvDataNode,
1688
+ })}\n`;
1689
+ }
1690
+
1691
+ // proxy.yaml — this instance's OWN route only (its sub-path → its own
1692
+ // service), so each variant's build dir carries a distinct, instance-scoped
1693
+ // fragment rather than an identical copy of the whole host proxy. The
1694
+ // complete host HTTPProxy — every variant's route aggregated onto the one
1695
+ // fqdn, each pointing at its live colour — is assembled and applied by
1696
+ // `instance-promote` at deploy time, and only once EVERY variant is ready.
1697
+ const proxyYaml =
1698
+ Underpost.deploy.baseProxyYamlFactory({ host: _host, env, options }) +
1699
+ instanceProxyRoutesFactory({
1700
+ deployId,
1701
+ instances: [instance],
1702
+ env,
1703
+ trafficById: { [instance.id]: targetTraffic },
1704
+ });
1705
+
1706
+ // grpc-service.yaml — the parent deploy's gRPC ClusterIP (shared; the
1707
+ // instance cmd resolves {{grpc-service-dns}} to it). Reuse the parent's
1708
+ // generated manifest when present rather than regenerating it here.
1709
+ const parentGrpcServicePath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
1710
+ const grpcServiceYaml = fs.existsSync(parentGrpcServicePath)
1711
+ ? fs.readFileSync(parentGrpcServicePath, 'utf8')
1712
+ : '';
1713
+
1714
+ // Write the sibling set next to deployment.yaml (project) and into the
1715
+ // engine-private per-instance build dir (mirrors instances/<id>/ layout).
1716
+ const instanceBuildDir = `./engine-private/conf/${deployId}/instances/${_id}/build/${env}`;
1717
+ fs.mkdirpSync(instanceBuildDir);
1718
+ fs.writeFileSync(`${instanceBuildDir}/deployment.yaml`, deploymentYaml, 'utf8');
1719
+ const siblingManifests = {
1720
+ 'pv-pvc.yaml': pvPvcYaml,
1721
+ 'proxy.yaml': proxyYaml,
1722
+ 'grpc-service.yaml': grpcServiceYaml,
1723
+ };
1724
+ for (const [name, content] of Object.entries(siblingManifests)) {
1725
+ if (!content) continue;
1726
+ fs.writeFileSync(`${envManifestPath}/${name}`, content, 'utf8');
1727
+ fs.writeFileSync(`${instanceBuildDir}/${name}`, content, 'utf8');
1728
+ }
1729
+ logger.info('[instance-build-manifest] Sibling manifests written', {
1730
+ project: envManifestPath,
1731
+ enginePrivate: instanceBuildDir,
1732
+ pvPvc: !!pvPvcYaml,
1733
+ proxy: !!proxyYaml,
1734
+ grpcService: !!grpcServiceYaml,
1735
+ });
1736
+
1737
+ // --- Per-instance env files -----------------------------------------
1738
+ // Each env file is seeded from the template instance's env file for the
1739
+ // same mode so operator-owned keys (API keys, memory limits, service URLs)
1740
+ // are inherited verbatim; only the keys the instance declares under
1741
+ // `multiInstance.env` — plus the derived container id — are (re)written.
1742
+ //
1743
+ // A derived instance's env dir is generated in full: both development.env
1744
+ // and production.env are written on every build, so a deploy in either
1745
+ // environment always finds the env file its `cmd` sources, no matter which
1746
+ // mode this build ran. The default/template instance owns the committed
1747
+ // source files, so only its current-mode file is idempotently refreshed.
1748
+ if (instance.templateId) {
1749
+ const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
1750
+ fs.mkdirpSync(instanceEnvDir);
1751
+ const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
1752
+ for (const targetEnv of envsToWrite) {
1753
+ const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
1754
+ const baseEnv = fs.existsSync(templateEnvPath) ? dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8')) : {};
1755
+ writeEnv(`${instanceEnvDir}/${targetEnv}.env`, {
1756
+ ...baseEnv,
1757
+ ...(instance.instanceEnv?.[targetEnv] || {}),
1758
+ CONTAINER_DEPLOY_ID: `${_deployId}-${targetEnv}`,
1759
+ });
1760
+ }
1761
+ logger.info('[instance-build-manifest] Instance env written', {
1762
+ dir: instanceEnvDir,
1763
+ instanceCode: instance.instanceCode,
1764
+ envs: envsToWrite,
1765
+ keys: Object.keys(instance.instanceEnv?.[env] || {}),
1766
+ });
1767
+ }
1768
+
1769
+ if (env === 'production' && isDefaultInstance) {
1770
+ if (fs.existsSync(dockerfileManifestPath)) {
1771
+ fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
1772
+ }
1773
+ fs.copyFileSync(outputPath, `${rootPath}/deployment.yaml`);
1774
+ // Sibling manifests alongside deployment.yaml at the project root.
1775
+ for (const name of ['pv-pvc.yaml', 'proxy.yaml', 'grpc-service.yaml']) {
1776
+ const src = `${envManifestPath}/${name}`;
1777
+ if (fs.existsSync(src)) fs.copyFileSync(src, `${rootPath}/${name}`);
1778
+ }
1779
+ logger.info('[instance-build-manifest] Production artifacts copied to project root', {
1780
+ rootPath,
1781
+ dockerfile: `${rootPath}/Dockerfile`,
1782
+ deployment: `${rootPath}/deployment.yaml`,
1783
+ });
1784
+ const ciSrc = `./.github/workflows/docker-image.${_runtime}.ci.yml`;
1785
+ if (fs.existsSync(ciSrc)) {
1786
+ if (!fs.existsSync(`${rootPath}/.github/workflows`)) fs.mkdirpSync(`${rootPath}/.github/workflows`);
1787
+ fs.copyFileSync(ciSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.ci.yml`);
1788
+ logger.info(`[instance-build-manifest] CI workflow copied`, { src: ciSrc });
1789
+ }
1790
+
1791
+ // Ship the development variant alongside production so the instance repo
1792
+ // is self-contained: the dev Dockerfile (built by the -dev CI workflow
1793
+ // into underpost/<runtime>-dev, consumed by the development compose
1794
+ // stack) and its dispatchable workflow. Both are optional — synced only
1795
+ // when the source-of-truth files exist in the engine repo.
1796
+ if (_runtime) {
1797
+ const devDockerfileSrc = `src/runtime/${_runtime}/Dockerfile.dev`;
1798
+ if (fs.existsSync(devDockerfileSrc)) {
1799
+ fs.copyFileSync(devDockerfileSrc, `${rootPath}/Dockerfile.dev`);
1800
+ logger.info('[instance-build-manifest] Dev Dockerfile copied', { src: devDockerfileSrc });
1801
+ }
1802
+ const devCiSrc = `./.github/workflows/docker-image.${_runtime}.dev.ci.yml`;
1803
+ if (fs.existsSync(devCiSrc)) {
1804
+ if (!fs.existsSync(`${rootPath}/.github/workflows`)) fs.mkdirpSync(`${rootPath}/.github/workflows`);
1805
+ fs.copyFileSync(devCiSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.dev.ci.yml`);
1806
+ logger.info(`[instance-build-manifest] Dev CI workflow copied`, { src: devCiSrc });
1807
+ }
1808
+ }
1809
+ }
1810
+ },
1811
+
1812
+ /**
1813
+ * @method ls-deployments
1814
+ * @description Retrieves and logs a table of Kubernetes deployments using `Underpost.deploy.get`.
1815
+ * @param {string} path - The input value, identifier, or path for the operation (used as an optional deployment name filter).
1816
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1817
+ * @memberof UnderpostRun
1818
+ */
1819
+ 'ls-deployments': async (path, options = DEFAULT_OPTION) => {
1820
+ console.table(await Underpost.kubectl.get(path, 'deployments', options.namespace));
1821
+ },
1822
+
1823
+ /**
1824
+ * @method host-update
1825
+ * @description Executes the `rocky-setup.sh` script to update the host system configuration.
1826
+ * @param {string} path - The input value, identifier, or path for the operation.
1827
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1828
+ * @memberof UnderpostRun
1829
+ */
1830
+ 'host-update': async (path, options = DEFAULT_OPTION) => {
1831
+ // const baseCommand = options.dev ? 'node bin' : 'underpost';
1832
+ shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
1833
+ shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
1834
+ },
1835
+
1836
+ /**
1837
+ * @method install-crio
1838
+ * @description Installs and configures CRI-O as the container runtime for kubeadm clusters.
1839
+ * Adds the stable v1.33 CRI-O yum repository, installs the `cri-o` package, configures
1840
+ * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1841
+ * so that `crictl` targets the CRI-O socket by default.
1842
+ * @param {string} path - Unused.
1843
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
1844
+ * @memberof UnderpostRun
1845
+ */
1846
+ 'install-crio': (path, options = DEFAULT_OPTION) => {
1847
+ logger.info('Installing CRI-O...');
1848
+ shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
1849
+ [cri-o]
1850
+ name=CRI-O
1851
+ baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/
1852
+ enabled=1
1853
+ gpgcheck=1
1854
+ gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
1855
+ EOF`);
1856
+ shellExec(`sudo dnf -y install cri-o`);
1857
+ // Add the Kubernetes repo so cri-tools (crictl CLI) is available.
1858
+ // The repo has exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni, so we
1859
+ // use --disableexcludes=kubernetes to override and install cri-tools.
1860
+ shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
1861
+ [kubernetes]
1862
+ name=Kubernetes
1863
+ baseurl=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/
1864
+ enabled=1
1865
+ gpgcheck=1
1866
+ gpgkey=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/repodata/repomd.xml.key
1867
+ exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
1868
+ EOF`);
1869
+ shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes 2>/dev/null || {
1870
+ CRICTL_VERSION="v1.31.0"
1871
+ ARCH="amd64"
1872
+ log "Kubernetes repo not available, downloading crictl binary directly..."
1873
+ curl -sL "https://github.com/kubernetes-sigs/cri-tools/releases/download/\${CRICTL_VERSION}/crictl-\${CRICTL_VERSION}-\${ARCH}.tar.gz" | sudo tar -C /usr/local/bin -xz
1874
+ }`);
1875
+ // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1876
+ shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1877
+ silentOnError: true,
1878
+ });
1879
+ shellExec(`sudo systemctl enable --now crio`);
1880
+ logger.info('CRI-O installed and started.');
1881
+ // Write crictl config so all crictl calls default to the CRI-O socket
1882
+ shellExec(`cat <<EOF | sudo tee /etc/crictl.yaml
1883
+ runtime-endpoint: unix:///var/run/crio/crio.sock
1884
+ image-endpoint: unix:///var/run/crio/crio.sock
1885
+ timeout: 10
1886
+ debug: false
1887
+ EOF`);
1888
+ },
1889
+
1890
+ /**
1891
+ * @method dd-container
1892
+ * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
1893
+ * @param {string} path - The input value, identifier, or path for the operation (used as the command to run inside the container).
1894
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1895
+ * @memberof UnderpostRun
1896
+ */
1897
+ 'dd-container': async (path = '', options = DEFAULT_OPTION) => {
1898
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1899
+ const baseClusterCommand = options.dev ? ' --dev' : '';
1900
+ const currentImage = options.imageName
1901
+ ? options.imageName
1902
+ : Underpost.image
1903
+ .getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
1904
+ .find((o) => o.IMAGE.match('underpost'));
1905
+ const podName = options.podName || `underpost-dev-container`;
1906
+ const volumeHostPath = options.claimName || '/home/dd';
1907
+ const claimName = options.claimName || `pvc-dd`;
1908
+
1909
+ if (!options.nodeName) {
1910
+ shellExec(`docker exec -i kind-worker bash -c "rm -rf ${volumeHostPath}"`);
1911
+ shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${volumeHostPath}"`);
1912
+ shellExec(`docker cp ${volumeHostPath}/engine kind-worker:${volumeHostPath}/engine`);
1913
+ shellExec(
1914
+ `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath}; chmod -R 755 ${volumeHostPath}"`,
1915
+ );
1916
+ } else {
1917
+ shellExec(`kubectl apply -f ${options.underpostRoot}/manifests/pv-pvc-dd.yaml -n ${options.namespace}`);
1918
+ }
1919
+
1920
+ if (!currentImage)
1921
+ shellExec(
1922
+ `${baseCommand} image${baseClusterCommand} --pull-base --build --path ${
1923
+ options.dev ? '.' : options.underpostRoot
1924
+ } ${options.dev ? '--kind' : '--kubeadm'}`,
1925
+ );
1926
+ // shellExec(`kubectl delete pod ${podName} --ignore-not-found`);
1927
+
1928
+ const payload = {
1929
+ ...options,
1930
+ podName,
1931
+ imageName: currentImage
1932
+ ? currentImage.image
1933
+ ? currentImage.image
1934
+ : currentImage.IMAGE
1935
+ ? `${currentImage.IMAGE}:${currentImage.TAG}`
1936
+ : `localhost/rockylinux9-underpost:${Underpost.version}`
1937
+ : `localhost/rockylinux9-underpost:${Underpost.version}`,
1938
+ volumeMountPath: volumeHostPath,
1939
+ ...(options.dev ? { volumeHostPath } : { claimName }),
1940
+ on: {
1941
+ init: async () => {},
1942
+ },
1943
+ args: [daemonProcess(path ? path : `cd /home/dd/engine && npm install && npm run test`)],
1944
+ };
1945
+
1946
+ await Underpost.run.CALL('deploy-job', path, payload);
1947
+ },
1948
+
1949
+ /**
1950
+ * @method ip-info
1951
+ * @description Executes the `ip-info.sh` script to display IP-related information for the specified path.
1952
+ * @param {string} path - The input value, identifier, or path for the operation (used as an argument to the script).
1953
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1954
+ * @memberof UnderpostRun
1955
+ */
1956
+ 'ip-info': (path, options = DEFAULT_OPTION) => {
1957
+ const { underpostRoot } = options;
1958
+ shellExec(`chmod +x ${underpostRoot}/scripts/ip-info.sh`);
1959
+ shellExec(`${underpostRoot}/scripts/ip-info.sh ${path}`);
1960
+ },
1961
+
1962
+ /**
1963
+ * @method db-client
1964
+ * @description Deploys and exposes the Adminer database client application (using `adminer:4.7.6-standalone` image) on the cluster.
1965
+ * @param {string} path - The input value, identifier, or path for the operation.
1966
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1967
+ * @memberof UnderpostRun
1968
+ */
1969
+ 'db-client': async (path, options = DEFAULT_OPTION) => {
1970
+ const { underpostRoot } = options;
1971
+
1972
+ Underpost.image.pullDockerHubImage({
1973
+ dockerhubImage: 'adminer',
1974
+ version: '4.7.6-standalone',
1975
+ kind: options.kind,
1976
+ kubeadm: options.kubeadm,
1977
+ k3s: options.k3s,
1978
+ });
1979
+
1980
+ shellExec(`kubectl delete deployment adminer -n ${options.namespace} --ignore-not-found`);
1981
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/deployment/adminer/. -n ${options.namespace}`);
1982
+ const successInstance = await Underpost.test.statusMonitor('adminer', 'Running', 'pods', 1000, 60 * 10);
1983
+
1984
+ if (successInstance) {
1985
+ shellExec(`underpost deploy --expose adminer --namespace ${options.namespace}`);
1986
+ }
1987
+ },
1988
+
1989
+ /**
1990
+ * @method git-conf
1991
+ * @description Configures Git global and local user name and email settings based on the provided `path` (formatted as `username,email`), or defaults to environment variables.
1992
+ * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `username,email`).
1993
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1994
+ * @memberof UnderpostRun
1995
+ */
1996
+ 'git-conf': (path = '', options = DEFAULT_OPTION) => {
1997
+ const defaultUsername = Underpost.env.get('GITHUB_USERNAME');
1998
+ const defaultEmail = Underpost.env.get('GITHUB_EMAIL');
1999
+ const validPath = path && path.split(',').length;
2000
+ const [username, email] = validPath ? path.split(',') : [defaultUsername, defaultEmail];
2001
+ if (validPath) {
2002
+ Underpost.env.set('GITHUB_USERNAME', username);
2003
+ Underpost.env.set('GITHUB_EMAIL', email);
2004
+ Underpost.env.get('GITHUB_USERNAME');
2005
+ Underpost.env.get('GITHUB_EMAIL');
2006
+ }
2007
+ shellExec(
2008
+ `git config --global credential.helper "" && ` +
2009
+ `git config credential.helper "" && ` +
2010
+ `git config --global user.name '${username}' && ` +
2011
+ `git config --global user.email '${email}' && ` +
2012
+ `git config --global credential.interactive always && ` +
2013
+ `git config user.name '${username}' && ` +
2014
+ `git config user.email '${email}' && ` +
2015
+ `git config credential.interactive always &&` +
2016
+ `git config pull.rebase false && ` +
2017
+ `git config core.filemode false`,
2018
+ {
2019
+ disableLog: true,
2020
+ silent: true,
2021
+ },
2022
+ );
2023
+
2024
+ if (options.logs)
2025
+ console.log(
2026
+ shellExec(`git config list`, { silent: true, stdout: true })
2027
+ .replaceAll('user.email', 'user.email'.yellow)
2028
+ .replaceAll(username, username.green)
2029
+ .replaceAll('user.name', 'user.name'.yellow)
2030
+ .replaceAll(email, email.green),
2031
+ );
2032
+ },
2033
+
2034
+ /**
2035
+ * @method promote
2036
+ * @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.router` for 'dd', or a specific ID).
2037
+ * When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
2038
+ * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
2039
+ * (cert-manager Certificate resources) for each affected deployment.
2040
+ * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
2041
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2042
+ * @memberof UnderpostRun
2043
+ */
2044
+ promote: async (path, options = DEFAULT_OPTION) => {
2045
+ let [inputDeployId, inputEnv, inputReplicas] = path.split(',');
2046
+ if (!inputEnv) inputEnv = 'production';
2047
+ if (!inputReplicas) inputReplicas = 1;
2048
+ // TODO: normalize: --tls maps to --cert for deploy.js isValidTLSContext compatibility
2049
+ if (options.tls) options.cert = true;
2050
+
2051
+ const applyCerts = (deployId, targetTraffic) => {
2052
+ if (!options.tls) return;
2053
+ // Rebuild proxy.yaml with --cert so the HTTPProxy includes TLS virtualhost config
2054
+ shellExec(
2055
+ `node bin deploy --build-manifest --cert --traffic ${targetTraffic} --replicas ${inputReplicas} --namespace ${options.namespace} ${deployId} ${inputEnv}`,
2056
+ );
2057
+ // Delete stale Certificate resources before reapplying
2058
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2059
+ if (fs.existsSync(confServerPath)) {
2060
+ for (const host of Object.keys(JSON.parse(fs.readFileSync(confServerPath, 'utf8'))))
2061
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
2062
+ }
2063
+ shellExec(
2064
+ `sudo kubectl apply -f ./engine-private/conf/${deployId}/build/${inputEnv}/proxy.yaml -n ${options.namespace}`,
2065
+ );
2066
+ const secretPath = `./engine-private/conf/${deployId}/build/${inputEnv}/secret.yaml`;
2067
+ if (fs.existsSync(secretPath)) shellExec(`kubectl apply -f ${secretPath} -n ${options.namespace}`);
2068
+ };
2069
+
2070
+ if (inputDeployId === 'dd') {
2071
+ for (const deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
2072
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
2073
+ const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
2074
+ Underpost.deploy.switchTraffic(deployId, inputEnv, targetTraffic, inputReplicas, options.namespace, options);
2075
+ applyCerts(deployId, targetTraffic);
2076
+ }
2077
+ } else {
2078
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(inputDeployId, { namespace: options.namespace });
2079
+ const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
2080
+ Underpost.deploy.switchTraffic(
2081
+ inputDeployId,
2082
+ inputEnv,
2083
+ targetTraffic,
2084
+ inputReplicas,
2085
+ options.namespace,
2086
+ options,
2087
+ );
2088
+ applyCerts(inputDeployId, targetTraffic);
2089
+ }
2090
+ },
2091
+ /**
2092
+ * @method metrics
2093
+ * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
2094
+ * @param {string} path - The input value, identifier, or path for the operation.
2095
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2096
+ * @memberof UnderpostRun
2097
+ */
2098
+ metrics: async (path, options = DEFAULT_OPTION) => {
2099
+ if (path === 'server') {
2100
+ shellExec(
2101
+ `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2102
+ );
2103
+ await timer(2000);
2104
+
2105
+ shellExec(`kubectl patch deployment metrics-server -n kube-system \
2106
+ --type='json' \
2107
+ -p='[
2108
+ {
2109
+ "op":"add",
2110
+ "path":"/spec/template/spec/containers/0/args/-",
2111
+ "value":"--kubelet-insecure-tls"
2112
+ }
2113
+ ]'`);
2114
+ shellExec(`kubectl scale deployment metrics-server \
2115
+ -n kube-system \
2116
+ --replicas=1`);
2117
+
2118
+ return;
2119
+ }
2120
+ const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
2121
+ let hosts = [];
2122
+ for (const deployId of deployList) {
2123
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
2124
+ hosts = hosts.concat(Object.keys(confServer));
2125
+ }
2126
+ shellExec(`node bin cluster --prom ${hosts.join(',')}`);
2127
+ shellExec(`node bin cluster --grafana`);
2128
+ },
2129
+ /**
2130
+ * @method cluster
2131
+ * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB, MariaDB, Valkey, Contour (Ingress), and Cert-Manager, and deploys all services.
2132
+ * @param {string} path - The input value, identifier, or path for the operation.
2133
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2134
+ * @memberof UnderpostRun
2135
+ */
2136
+ cluster: async (path = '', options = DEFAULT_OPTION) => {
2137
+ const { underpostRoot } = options;
2138
+ const env = options.dev ? 'development' : 'production';
2139
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
2140
+ const baseClusterCommand = options.dev ? ' --dev' : '';
2141
+ const clusterType = options.k3s ? 'k3s' : 'kubeadm';
2142
+ shellCd(`/home/dd/engine`);
2143
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --reset --${clusterType}`);
2144
+ await timer(5000);
2145
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
2146
+ await timer(5000);
2147
+ let [runtimeImage, deployList] =
2148
+ path && path.trim() && path.split(',')
2149
+ ? path.split(',')
2150
+ : [
2151
+ 'express',
2152
+ fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
2153
+ ];
2154
+ shellExec(
2155
+ `${baseCommand} image${baseClusterCommand} --build ${
2156
+ runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
2157
+ } --${clusterType}`,
2158
+ );
2159
+ if (!deployList) {
2160
+ deployList = [];
2161
+ logger.warn('No deploy list provided');
2162
+ } else deployList = deployList.split('+');
2163
+ await timer(5000);
2164
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --mongodb`);
2165
+ if (runtimeImage === 'lampp') {
2166
+ await timer(5000);
2167
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --mariadb`);
2168
+ }
2169
+ await timer(5000);
2170
+ for (const deployId of deployList) {
2171
+ shellExec(
2172
+ `${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod${options.namespace ? ` --ns ${options.namespace}` : ''}`,
2173
+ );
2174
+ }
2175
+ await timer(5000);
2176
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --valkey`);
2177
+ await timer(5000);
2178
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --contour`);
2179
+ if (env === 'production') {
2180
+ await timer(5000);
2181
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --cert-manager`);
2182
+ }
2183
+ for (const deployId of deployList) {
2184
+ shellExec(
2185
+ `${baseCommand} deploy ${deployId} ${env} --${clusterType}${env === 'production' ? ' --cert' : ''}${
2186
+ env === 'development' ? ' --etc-hosts' : ''
2187
+ }${options.namespace ? ` --namespace ${options.namespace}` : ''}`,
2188
+ );
2189
+ }
2190
+ },
2191
+ /**
2192
+ * @method deploy
2193
+ * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
2194
+ * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
2195
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2196
+ * @memberof UnderpostRun
2197
+ */
2198
+ deploy: async (path, options = DEFAULT_OPTION) => {
2199
+ const deployId = path;
2200
+ const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
2201
+ if (!validVersion) throw new Error('Version mismatch');
2202
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
2203
+ const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
2204
+ const env = options.dev ? 'development' : 'production';
2205
+ const ignorePods = Underpost.kubectl
2206
+ .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
2207
+ .map((p) => p.NAME);
2208
+
2209
+ shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
2210
+
2211
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
2212
+
2213
+ Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
2214
+ },
2215
+
2216
+ /**
2217
+ * @method disk-clean
2218
+ * @description Executes the `disk-clean-sh` script to perform disk cleanup operations.
2219
+ * @param {string} path - The input value, identifier, or path for the operation.
2220
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2221
+ * @memberof UnderpostRun
2222
+ */
2223
+ 'disk-clean': async (path, options = DEFAULT_OPTION) => {
2224
+ const { underpostRoot } = options;
2225
+ shellExec(`chmod +x ${underpostRoot}/scripts/disk-clean.sh`);
2226
+ shellExec(`./scripts/disk-clean.sh`);
2227
+ },
2228
+
2229
+ /**
2230
+ * @method disk-devices
2231
+ * @description Executes the `disk-devices.sh` script to display information about disk devices.
2232
+ * @param {string} path - The input value, identifier, or path for the operation.
2233
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2234
+ * @memberof UnderpostRun
2235
+ */
2236
+ 'disk-devices': async (path = '/', options = DEFAULT_OPTION) => {
2237
+ const { underpostRoot } = options;
2238
+ shellExec(`chmod +x ${underpostRoot}/scripts/disk-devices.sh`);
2239
+ shellExec(`${underpostRoot}/scripts/disk-devices.sh`);
2240
+ },
2241
+
2242
+ /**
2243
+ * @method disk-usage
2244
+ * @description Displays disk usage statistics using the `du` command, sorted by size.
2245
+ * @param {string} path - The input value, identifier, or path for the operation.
2246
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2247
+ * @memberof UnderpostRun
2248
+ */
2249
+ 'disk-usage': async (path = '/', options = DEFAULT_OPTION) => {
2250
+ if (!path) path = '/';
2251
+ logger.info('Mount filesystem');
2252
+ shellExec(`df -h${path === '/' ? '' : ` ${path}`}`);
2253
+ logger.info('Files disks usage');
2254
+ shellExec(`du -xh ${path} --max-depth=1 | sort -h`);
2255
+ },
2256
+
2257
+ /**
2258
+ * @method dev
2259
+ * @description Starts development servers for client, API, and proxy based on provided parameters (deployId, host, path, clientHostPort).
2260
+ * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,subConf,host,path,clientHostPort`).
2261
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2262
+ * @memberof UnderpostRun
2263
+ */
2264
+ dev: async (path = '', options = DEFAULT_OPTION) => {
2265
+ let [deployId, subConf, host, _path, clientHostPort] = path.split(',');
2266
+ if (options.confServerPath) {
2267
+ const confServer = JSON.parse(fs.readFileSync(options.confServerPath, 'utf8'));
2268
+ fs.writeFileSync(
2269
+ `./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`,
2270
+ JSON.stringify(
2271
+ {
2272
+ [host]: {
2273
+ [_path]: confServer[host][_path],
2274
+ },
2275
+ },
2276
+ null,
2277
+ 4,
2278
+ ),
2279
+ 'utf8',
2280
+ );
2281
+ }
2282
+ if (!deployId) deployId = 'dd-default';
2283
+ if (!host) host = 'default.net';
2284
+ if (!_path) _path = '/';
2285
+ if (!clientHostPort) clientHostPort = 'localhost:4004';
2286
+ if (!subConf) subConf = 'local';
2287
+ if (options.reset && fs.existsSync(`./engine-private/conf/${deployId}`))
2288
+ fs.removeSync(`./engine-private/conf/${deployId}`);
2289
+ if (options.devProxyPortOffset) {
2290
+ const envPath = `./engine-private/conf/${deployId}/.env.development`;
2291
+ const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
2292
+ envObj.DEV_PROXY_PORT_OFFSET = options.devProxyPortOffset;
2293
+ writeEnv(envPath, envObj);
2294
+ }
2295
+ dotenv.config({ path: `./engine-private/conf/${deployId}/.env.development`, override: true });
2296
+ shellExec(`node bin run dev-cluster --expose --namespace ${options.namespace}`, { async: true });
2297
+ {
2298
+ const cmd = `npm run dev:api ${deployId} ${subConf} ${host} ${_path} ${clientHostPort} proxy${
2299
+ options.tls ? ' tls' : ''
2300
+ }`;
2301
+ shellExec(cmd, { async: true });
2302
+ }
2303
+ if ((await awaitDeployMonitor()) !== true) return;
2304
+ {
2305
+ const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
2306
+
2307
+ shellExec(cmd, {
2308
+ async: true,
2309
+ });
2310
+ }
2311
+ if ((await awaitDeployMonitor()) !== true) return;
2312
+ shellExec(
2313
+ `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
2314
+ );
2315
+ },
2316
+
2317
+ /**
2318
+ * @method service
2319
+ * @description Deploys and exposes specific services (like `mongo-express-service`) on the cluster, updating deployment configurations and monitoring status.
2320
+ * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,serviceId,host,path,replicas,image,node`).
2321
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2322
+ * @memberof UnderpostRun
2323
+ */
2324
+ service: async (path = '', options = DEFAULT_OPTION) => {
2325
+ const env = options.dev ? 'development' : 'production';
2326
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
2327
+ const baseClusterCommand = options.dev ? ' --dev' : '';
2328
+ shellCd(`/home/dd/engine`);
2329
+ let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
2330
+ if (!replicas) replicas = options.replicas;
2331
+ // const confClient = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.client.json`, 'utf8'));
2332
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
2333
+ // const confSSR = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.ssr.json`, 'utf8'));
2334
+ // const packageData = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/package.json`, 'utf8'));
2335
+ const services = fs.existsSync(`./engine-private/deploy/${deployId}/conf.services.json`)
2336
+ ? JSON.parse(fs.readFileSync(`./engine-private/deploy/${deployId}/conf.services.json`, 'utf8'))
2337
+ : [];
2338
+ let serviceData = services.findIndex((s) => s.serviceId === serviceId);
2339
+ const payload = {
2340
+ serviceId,
2341
+ path: _path,
2342
+ port: options.port,
2343
+ host,
2344
+ };
2345
+ let podToMonitor;
2346
+ if (!payload.port)
2347
+ switch (serviceId) {
2348
+ case 'mongo-express-service': {
2349
+ payload.port = 8081;
2350
+ break;
2351
+ }
2352
+ case 'grafana': {
2353
+ payload.port = 3000;
2354
+ // payload.pathRewritePolicy = [
2355
+ // {
2356
+ // prefix: '/grafana',
2357
+ // replacement: '/',
2358
+ // },
2359
+ // ];
2360
+ break;
2361
+ }
2362
+ }
2363
+ if (serviceData == -1) {
2364
+ services.push(payload);
2365
+ } else {
2366
+ services[serviceData] = payload;
2367
+ }
2368
+ fs.writeFileSync(
2369
+ `./engine-private/conf/${deployId}/conf.services.json`,
2370
+ JSON.stringify(services, null, 4),
2371
+ 'utf8',
2372
+ );
2373
+ switch (serviceId) {
2374
+ case 'mongo-express-service': {
2375
+ shellExec(`kubectl delete svc mongo-express-service -n ${options.namespace} --ignore-not-found`);
2376
+ shellExec(`kubectl delete deployment mongo-express -n ${options.namespace} --ignore-not-found`);
2377
+ shellExec(`kubectl apply -f manifests/deployment/mongo-express/deployment.yaml -n ${options.namespace}`);
2378
+ podToMonitor = 'mongo-express';
2379
+ break;
2380
+ }
2381
+ case 'grafana': {
2382
+ shellExec(
2383
+ `node bin cluster${baseClusterCommand} --grafana --hosts '${host}' --prom '${Object.keys(confServer)}'`,
2384
+ );
2385
+ podToMonitor = 'grafana';
2386
+ break;
2387
+ }
2388
+ }
2389
+ const success = await Underpost.test.statusMonitor(podToMonitor);
2390
+ if (success) {
2391
+ const versions = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
2392
+ if (!node) node = os.hostname();
2393
+ const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
2394
+ shellExec(
2395
+ `${baseCommand} deploy${options.dev ? '' : ' --kubeadm'}${
2396
+ options.devProxyPortOffset ? ' --disable-deployment-proxy' : ''
2397
+ } --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
2398
+ image ? ` --image ${image}` : ''
2399
+ }${versions ? ` --versions ${versions}` : ''}${timeoutFlags} dd ${env}`,
2400
+ );
2401
+ shellExec(
2402
+ `${baseCommand} deploy${options.dev ? '' : ' --kubeadm'}${
2403
+ options.devProxyPortOffset ? ' --disable-deployment-proxy' : ''
2404
+ } --disable-update-deployment ${deployId} ${env} --versions ${versions}`,
2405
+ );
2406
+ } else logger.error(`Service pod ${podToMonitor} failed to start in time.`);
2407
+ if (options.etcHosts === true) {
2408
+ const hostListenResult = etcHostFactory([host]);
2409
+ logger.info(hostListenResult.renderHosts);
2410
+ }
2411
+ },
2412
+
2413
+ /**
2414
+ * @method etc-hosts
2415
+ * @description Generates and logs the contents for the `/etc/hosts` file based on provided hosts or deployment configurations.
2416
+ * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated list of hosts).
2417
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2418
+ * @memberof UnderpostRun
2419
+ */
2420
+ 'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
2421
+ const hosts = path ? path.split(',') : [];
2422
+ if (options.deployId) {
2423
+ const confServer = loadConfServerJson(`./engine-private/conf/${options.deployId}/conf.server.json`);
2424
+ hosts.push(...Object.keys(confServer));
2425
+ }
2426
+ const hostListenResult = etcHostFactory(hosts);
2427
+ logger.info(hostListenResult.renderHosts);
2428
+ },
2429
+
2430
+ /**
2431
+ * @method sh
2432
+ * @description Enables remote control for the Kitty terminal emulator.
2433
+ * @param {string} path - The input value, identifier, or path for the operation.
2434
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2435
+ * @memberof UnderpostRun
2436
+ */
2437
+ sh: async (path = '', options = DEFAULT_OPTION) => {
2438
+ let [operator, arg0, arg1] = path.split(',');
2439
+ if (operator == 'copy') {
2440
+ shellExec(
2441
+ `kitty @ get-text ${arg0 === 'all' ? '--match all' : '--self'} --extent all${
2442
+ arg1 === 'ansi' ? ' --ansi yes' : ''
2443
+ } | kitty +kitten clipboard`,
2444
+ );
2445
+ return;
2446
+ }
2447
+ shellExec(`kitty -o allow_remote_control=yes`);
2448
+ },
2449
+
2450
+ /**
2451
+ * @method log
2452
+ * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
2453
+ * @param {string} path - The input value, identifier, or path for the operation (formatted as `filePath,keywords,lines`).
2454
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2455
+ * @memberof UnderpostRun
2456
+ */
2457
+ log: async (path, options = DEFAULT_OPTION) => {
2458
+ const [filePath, keywords, lines] = path.split(',');
2459
+ let result = shellExec(`grep -i -E ${lines ? `-C ${lines} ` : ''}'${keywords}' ${filePath}`, {
2460
+ stdout: true,
2461
+ silent: true,
2462
+ }).replaceAll(`--`, `==============================`.green.bold);
2463
+ for (const keyword of keywords.split('|')) result = result.replaceAll(keyword, keyword.bgYellow.black.bold);
2464
+ console.log(result);
2465
+ },
2466
+
2467
+ /**
2468
+ * @method ps
2469
+ * @description Displays running processes that match a specified path or keyword.
2470
+ * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter processes).
2471
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2472
+ * @memberof UnderpostRun
2473
+ */
2474
+ ps: async (path = '', options = DEFAULT_OPTION) => {
2475
+ const out = shellExec(
2476
+ path.startsWith('top-consumers')
2477
+ ? `ps -eo pid,%cpu,%mem,rss,cmd --sort=-%cpu | head -n ${path.split(',')[1] || 15}`
2478
+ : path
2479
+ ? `(ps -eo pid,%cpu,%mem,rss,cmd -ww | head -n1; ps -eo pid,%cpu,%mem,rss,cmd -ww | tail -n +2 | grep -F ${path})`
2480
+ : `ps -eo pid,%cpu,%mem,rss,cmd -ww`,
2481
+ {
2482
+ stdout: true,
2483
+ silent: true,
2484
+ },
2485
+ );
2486
+
2487
+ console.log(
2488
+ path ? out.replaceAll(path.split(',')[2] || path, (path.split(',')[2] || path).bgYellow.black.bold) : out,
2489
+ );
2490
+ },
2491
+
2492
+ /**
2493
+ * @method pid-info
2494
+ * @description Displays detailed information about a process by PID, including service details, command line, executable path, working directory, environment variables, and parent process tree.
2495
+ * @param {string} path - The PID of the process to inspect.
2496
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2497
+ * @memberof UnderpostRun
2498
+ */
2499
+ 'pid-info': (path, options = DEFAULT_OPTION) => {
2500
+ const pid = path;
2501
+ if (!pid) {
2502
+ logger.error('PID is required. Usage: underpost run pid-info <pid>');
2503
+ return;
2504
+ }
2505
+
2506
+ // Services
2507
+ logger.info('Process info');
2508
+ shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`);
2509
+ logger.info('Command line');
2510
+ shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`);
2511
+ logger.info('Executable path');
2512
+ shellExec(`sudo readlink -f /proc/${pid}/exe`);
2513
+ logger.info('Working directory');
2514
+ shellExec(`sudo readlink -f /proc/${pid}/cwd`);
2515
+ logger.info('Environment variables (first 200)');
2516
+ shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`);
2517
+
2518
+ // Parent
2519
+ logger.info('Parent process');
2520
+ const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, { stdout: true, silent: true });
2521
+ console.log(parentInfo);
2522
+ const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
2523
+ if (ppidMatch) {
2524
+ const ppid = ppidMatch.trim().split(/\s+/)[1];
2525
+ logger.info(`Parent PID: ${ppid}`);
2526
+ shellExec(`ps -fp ${ppid}`);
2527
+ }
2528
+ logger.info('Process tree');
2529
+ shellExec(`pstree -s ${pid}`);
2530
+ },
2531
+
2532
+ /**
2533
+ * @method background
2534
+ * @description Runs a custom command in the background using nohup, logging output to `/var/log/<id>.log` and saving the PID to `/var/run/<id>.pid`.
2535
+ * @param {string} path - The command to run in the background (e.g. 'npm run prod:container dd-cyberia-r3').
2536
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2537
+ * @memberof UnderpostRun
2538
+ */
2539
+ background: (path, options = DEFAULT_OPTION) => {
2540
+ if (!path) {
2541
+ logger.error('Command is required. Usage: underpost run background <command>');
2542
+ return;
2543
+ }
2544
+ const id = path.split(/\s+/).pop();
2545
+ const logFile = `/var/log/${id}.log`;
2546
+ const pidFile = `/var/run/${id}.pid`;
2547
+ logger.info(`Starting background process`, { id, logFile, pidFile });
2548
+ shellExec(`nohup ${path} > ${logFile} 2>&1 & pid=$!; echo $pid > ${pidFile}; disown`);
2549
+ logger.info(`Background process started for '${id}'`);
2550
+ },
2551
+
2552
+ /**
2553
+ * @method ports
2554
+ * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
2555
+ * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
2556
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2557
+ * @memberof UnderpostRun
2558
+ */
2559
+ ports: async (path = '', options = DEFAULT_OPTION) => {
2560
+ shellExec(`chmod +x ${options.underpostRoot}/scripts/ports-ls.sh`);
2561
+ shellExec(`${options.underpostRoot}/scripts/ports-ls.sh`);
2562
+ },
2563
+
2564
+ /**
2565
+ * @method deploy-test
2566
+ * @description Deploys a test deployment (`dd-test`) in either development or production mode, setting up necessary secrets and starting the deployment.
2567
+ * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID).
2568
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2569
+ * @memberof UnderpostRun
2570
+ */
2571
+ 'deploy-test': async (path, options = DEFAULT_OPTION) => {
2572
+ // Note: use recomendation empty deploy cluster: node bin --dev cluster
2573
+ const env = options.dev ? 'development' : 'production';
2574
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
2575
+ const baseClusterCommand = options.dev ? ' --dev' : '';
2576
+ const inputs = path ? path.split(',') : [];
2577
+ const deployId = inputs[0] ? inputs[0] : 'dd-test';
2578
+ const cmd = options.cmd
2579
+ ? options.cmd
2580
+ : [
2581
+ `npm install -g npm@11.2.0`,
2582
+ `npm install -g underpost`,
2583
+ `${baseCommand} secret underpost --create-from-env`,
2584
+ `${baseCommand} start --build --run ${deployId} ${env}`,
2585
+ ];
2586
+ shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
2587
+ },
2588
+
2589
+ /**
2590
+ * @method tf-vae-test
2591
+ * @description Creates and runs a job pod (`tf-vae-test`) that installs TensorFlow dependencies, clones the TensorFlow docs, and runs the CVAE tutorial script, with a terminal monitor attached.
2592
+ * @param {string} path - The input value, identifier, or path for the operation.
2593
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2594
+ * @memberof UnderpostRun
2595
+ */
2596
+ 'tf-vae-test': async (path, options = DEFAULT_OPTION) => {
2597
+ const podName = 'tf-vae-test';
2598
+ await Underpost.run.CALL('deploy-job', '', {
2599
+ logs: options.logs,
2600
+ podName,
2601
+ // volumeMountPath: '/custom_images',
2602
+ // volumeHostPath: '/home/dd/engine/src/client/public/cyberia/assets/skin',
2603
+ on: {
2604
+ init: async () => {
2605
+ // const pid = getTerminalPid();
2606
+ // shellExec(`sudo kill -9 ${pid}`);
2607
+ (async () => {
2608
+ const nameSpace = options.namespace;
2609
+ const basePath = '/home/dd';
2610
+ const scriptPath = '/site/en/tutorials/generative/cvae.py';
2611
+
2612
+ const { close } = await (async () => {
2613
+ const checkAwaitPath = '/await';
2614
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
2615
+ logger.info('monitor', checkAwaitPath);
2616
+ await timer(1000);
2617
+ }
2618
+
2619
+ return {
2620
+ close: () => shellExec(`sudo kubectl exec -i ${podName} -- sh -c "rm -rf ${checkAwaitPath}"`),
2621
+ };
2622
+ })();
2623
+
2624
+ const localScriptPath = `${basePath}/lab/src/${scriptPath.split('/').pop()}`;
2625
+ if (!fs.existsSync(localScriptPath)) {
2626
+ throw new Error(`Local override script not found: ${localScriptPath}`);
2627
+ }
2628
+
2629
+ shellExec(`sudo kubectl cp ${localScriptPath} ${nameSpace}/${podName}:${basePath}/docs${scriptPath}`);
2630
+
2631
+ close();
2632
+
2633
+ {
2634
+ const checkPath = `/latent_space_plot.png`;
2635
+ const outsPaths = [];
2636
+ const labDir = `${basePath}/lab`;
2637
+
2638
+ logger.info('monitor', checkPath);
2639
+ {
2640
+ const checkAwaitPath = `/home/dd/docs${checkPath}`;
2641
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
2642
+ logger.info('waiting for', checkAwaitPath);
2643
+ await timer(1000);
2644
+ }
2645
+ }
2646
+
2647
+ {
2648
+ const toPath = `${labDir}${checkPath}`;
2649
+ outsPaths.push(toPath);
2650
+ shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${checkPath} ${toPath}`);
2651
+ }
2652
+
2653
+ for (let i of range(1, 10)) {
2654
+ const fileName = `image_at_epoch_${setPad(i, '0', 4)}.png`;
2655
+ const fromPath = `/${fileName}`;
2656
+ const toPath = `${labDir}/${fileName}`;
2657
+ outsPaths.push(toPath);
2658
+ shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${fromPath} ${toPath}`);
2659
+ }
2660
+
2661
+ shellExec(`firefox ${outsPaths.join(' ')}`);
2662
+ process.exit(0);
2663
+ }
2664
+ })();
2665
+ },
2666
+ },
2667
+ args: [
2668
+ `pip install --upgrade \
2669
+ nbconvert \
2670
+ tensorflow-probability==0.23.0 \
2671
+ imageio \
2672
+ git+https://github.com/tensorflow/docs \
2673
+ matplotlib \
2674
+ "numpy<1.25,>=1.21"`,
2675
+ 'mkdir -p /home/dd',
2676
+ 'cd /home/dd',
2677
+ 'git clone https://github.com/tensorflow/docs.git',
2678
+ 'cd docs',
2679
+ 'jupyter nbconvert --to python site/en/tutorials/generative/cvae.ipynb',
2680
+ `echo '' > /await`,
2681
+ `echo '=== WAITING SCRIPT LAUNCH ==='`,
2682
+ `while [ -f /await ]; do sleep 1; done`,
2683
+ `echo '=== FINISHED ==='`,
2684
+ daemonProcess(`ipython site/en/tutorials/generative/cvae.py`),
2685
+ ],
2686
+ });
2687
+ },
2688
+
2689
+ /**
2690
+ * @method spark-template
2691
+ * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
2692
+ * @param {string} path - The input value, identifier, or path for the operation.
2693
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2694
+ * @memberof UnderpostRun
2695
+ */
2696
+ 'spark-template': (path, options = DEFAULT_OPTION) => {
2697
+ const dir = '/home/dd/spark-template';
2698
+ shellExec(`sudo rm -rf ${dir}`);
2699
+ shellCd('/home/dd');
2700
+
2701
+ // pbcopy(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8`);
2702
+ // await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
2703
+ shellExec(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8 '--name=spark-template'`);
2704
+
2705
+ shellCd(dir);
2706
+
2707
+ Underpost.repo.initLocalRepo({ path: dir });
2708
+ shellExec(`git add . && git commit -m "Base implementation"`);
2709
+ shellExec(`chmod +x ./replace_params.sh`);
2710
+ shellExec(`chmod +x ./build.sh`);
2711
+
2712
+ shellExec(`./replace_params.sh`);
2713
+ shellExec(`./build.sh`);
2714
+
2715
+ shellCd('/home/dd/engine');
2716
+ },
2717
+ /**
2718
+ * @method pull-rocky-image
2719
+ * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
2720
+ * @param {string} path - The input value, identifier, or path for the operation.
2721
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2722
+ * @memberof UnderpostRun
2723
+ */
2724
+ 'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
2725
+ shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
2726
+ },
2727
+ /**
2728
+ * @method rmi
2729
+ * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
2730
+ * @param {string} path - The input value, identifier, or path for the operation.
2731
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2732
+ * @memberof UnderpostRun
2733
+ */
2734
+ rmi: (path, options = DEFAULT_OPTION) => {
2735
+ shellExec(`podman rmi $(podman images -qa) --force`);
2736
+ },
2737
+ /**
2738
+ * @method kill
2739
+ * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
2740
+ * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
2741
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2742
+ * @memberof UnderpostRun
2743
+ */
2744
+ kill: (path = '', options = DEFAULT_OPTION) => {
2745
+ if (options.pid)
2746
+ return shellExec(`sudo kill -9 ${options.pid}`, {
2747
+ silentOnError: true,
2748
+ });
2749
+ for (const _path of path.split(',')) {
2750
+ if (_path.split('+')[1]) {
2751
+ let [port, sumPortOffSet] = _path.split('+');
2752
+ port = parseInt(port);
2753
+ sumPortOffSet = parseInt(sumPortOffSet);
2754
+ for (const sumPort of range(0, sumPortOffSet))
2755
+ shellExec(
2756
+ `PIDS=$(lsof -t -i:${parseInt(port) + parseInt(sumPort)}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`,
2757
+ {
2758
+ silentOnError: true,
2759
+ },
2760
+ );
2761
+ } else
2762
+ shellExec(`PIDS=$(lsof -t -i:${_path}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`, {
2763
+ silentOnError: true,
2764
+ });
2765
+ }
2766
+ },
2767
+ /**
2768
+ * @method generate-pass
2769
+ * @description Generates a cryptographically secure random password that satisfies all validatePassword
2770
+ * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
2771
+ * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
2772
+ * @param {string} path - Optional password length (default: 16).
2773
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2774
+ * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
2775
+ * @memberof UnderpostRun
2776
+ */
2777
+ 'generate-pass': (path, options = DEFAULT_OPTION) => {
2778
+ const length = path && parseInt(path) > 0 ? parseInt(path) : 16;
2779
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
2780
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
2781
+ const digits = '0123456789';
2782
+ const special = '@#$%^&*()_+';
2783
+ const all = lower + upper + digits + special;
2784
+ const buf = crypto.randomBytes(length + 4);
2785
+ // Guarantee at least one character from each required class
2786
+ const chars = [
2787
+ lower[buf[0] % lower.length],
2788
+ upper[buf[1] % upper.length],
2789
+ digits[buf[2] % digits.length],
2790
+ special[buf[3] % special.length],
2791
+ ];
2792
+ for (let i = 4; i < length; i++) chars.push(all[buf[i] % all.length]);
2793
+ // Fisher-Yates shuffle using an independent random buffer
2794
+ const shuf = crypto.randomBytes(length);
2795
+ for (let i = chars.length - 1; i > 0; i--) {
2796
+ const j = shuf[i % shuf.length] % (i + 1);
2797
+ [chars[i], chars[j]] = [chars[j], chars[i]];
2798
+ }
2799
+ const password = chars.join('');
2800
+ if (options.copy) pbcopy(password);
2801
+ else console.log(password);
2802
+ },
2803
+ /**
2804
+ * @method secret
2805
+ * @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.
2806
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
2807
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2808
+ * @memberof UnderpostRun
2809
+ */
2810
+ secret: (path, options = DEFAULT_OPTION) => {
2811
+ const cronDeployId = cronDeployIdResolve() || 'dd-cron';
2812
+ Underpost.secret.underpost.createFromEnvFile(
2813
+ `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
2814
+ );
2815
+ },
2816
+ /**
2817
+ * @method underpost-config
2818
+ * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
2819
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
2820
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2821
+ * @memberof UnderpostRun
2822
+ */
2823
+ 'underpost-config': (path = '', options = DEFAULT_OPTION) => {
2824
+ Underpost.deploy.configMap(path ? path : 'production', options.namespace);
2825
+ },
2826
+ /**
2827
+ * @method gpu-env
2828
+ * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
2829
+ * @param {string} path - The input value, identifier, or path for the operation.
2830
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2831
+ * @memberof UnderpostRun
2832
+ */
2833
+ 'gpu-env': (path, options = DEFAULT_OPTION) => {
2834
+ const clusterType = 'kubeadm';
2835
+ shellExec(
2836
+ `node bin cluster --dev --reset --${clusterType} && node bin cluster --dev --dedicated-gpu --${clusterType} && kubectl get pods --all-namespaces -o wide -w`,
2837
+ );
2838
+ },
2839
+ /**
2840
+ * @method tf-gpu-test
2841
+ * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
2842
+ * @param {string} path - The input value, identifier, or path for the operation.
2843
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2844
+ * @memberof UnderpostRun
2845
+ */
2846
+ 'tf-gpu-test': (path, options = DEFAULT_OPTION) => {
2847
+ const { underpostRoot, namespace } = options;
2848
+ shellExec(`kubectl delete configmap tf-gpu-test-script -n ${namespace} --ignore-not-found`);
2849
+ shellExec(`kubectl delete pod tf-gpu-test-pod -n ${namespace} --ignore-not-found`);
2850
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/deployment/tensorflow/tf-gpu-test.yaml -n ${namespace}`);
2851
+ },
2852
+
2853
+ /**
2854
+ * @method deploy-job
2855
+ * @description Creates and applies a custom Kubernetes Pod manifest (Job) for running arbitrary commands inside a container image (defaulting to a TensorFlow/NVIDIA image).
2856
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional script path or job argument).
2857
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2858
+ * @memberof UnderpostRun
2859
+ */
2860
+ 'deploy-job': async (path, options = DEFAULT_OPTION) => {
2861
+ const podName = options.podName || 'deploy-job';
2862
+ const volumeName = `${podName}-volume`;
2863
+ if (typeof options.args === 'string') options.args = options.args.split(',');
2864
+ const args = (options.args ? options.args : path ? [path] : [`python ${path}`]).filter((c) => c.trim());
2865
+ const imageName = options.imageName || 'nvcr.io/nvidia/tensorflow:24.04-tf2-py3';
2866
+ const containerName = options.containerName || `${podName}-container`;
2867
+ const gpuEnable = imageName.match('nvidia');
2868
+ const runtimeClassName = options.runtimeClassName ? options.runtimeClassName : gpuEnable ? 'nvidia' : '';
2869
+ const namespace = options.namespace || 'default';
2870
+ const volumeMountPath = options.volumeMountPath || path;
2871
+ const volumeHostPath = options.volumeHostPath || path;
2872
+ const claimName = options.claimName || '';
2873
+ const enableVolumeMount = volumeMountPath && (volumeHostPath || claimName);
2874
+ const tty = options.tty ? 'true' : 'false';
2875
+ const stdin = options.stdin ? 'true' : 'false';
2876
+ const restartPolicy = options.restartPolicy || 'Never';
2877
+ const kindType = options.kindType || 'Pod';
2878
+ const imagePullPolicy = options.imagePullPolicy || 'IfNotPresent';
2879
+ const hostNetwork = options.hostNetwork ? options.hostNetwork : '';
2880
+ const apiVersion = options.apiVersion || 'v1';
2881
+ // Parse hostAliases option:
2882
+ // - string from CLI: "ip1=host1,host2;ip2=host3,host4"
2883
+ // - array from programmatic callers: [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]
2884
+ const hostAliases = options.hostAliases
2885
+ ? Array.isArray(options.hostAliases)
2886
+ ? options.hostAliases
2887
+ : options.hostAliases
2888
+ .split(';')
2889
+ .filter((entry) => entry.trim())
2890
+ .map((entry) => {
2891
+ const [ip, hostnamesStr] = entry.split('=');
2892
+ const hostnames = hostnamesStr ? hostnamesStr.split(',').map((h) => h.trim()) : [];
2893
+ return { ip: ip.trim(), hostnames };
2894
+ })
2895
+ : [];
2896
+ const hostAliasesYaml =
2897
+ hostAliases.length > 0
2898
+ ? ` hostAliases:\n${hostAliases
2899
+ .map(
2900
+ (alias) =>
2901
+ ` - ip: "${alias.ip}"\n hostnames:\n${alias.hostnames.map((h) => ` - "${h}"`).join('\n')}`,
2902
+ )
2903
+ .join('\n')}`
2904
+ : '';
2905
+ const labels = options.labels
2906
+ ? options.labels
2907
+ .split(',')
2908
+ .map((keyValue) => {
2909
+ const [key, value] = keyValue.split('=');
2910
+ return ` ${key}: ${value}
2911
+ `;
2912
+ })
2913
+ .join('')
2914
+ : ` app: ${podName}`;
2915
+ if (options.volumeType === 'dev') options.volumeType = 'FileOrCreate';
2916
+ const volumeType =
2917
+ options.volumeType || (enableVolumeMount && volumeHostPath && fs.statSync(volumeHostPath).isDirectory())
2918
+ ? 'Directory'
2919
+ : 'File';
2920
+
2921
+ const envs = Underpost.env.list();
2922
+
2923
+ const cmd = `kubectl apply -f - <<EOF
2924
+ apiVersion: ${apiVersion}
2925
+ kind: ${kindType}
2926
+ metadata:
2927
+ name: ${podName}
2928
+ namespace: ${namespace}
2929
+ labels:
2930
+ ${labels}
2931
+ spec:
2932
+ restartPolicy: ${restartPolicy}
2933
+ ${runtimeClassName ? ` runtimeClassName: ${runtimeClassName}` : ''}
2934
+ ${hostNetwork ? ` hostNetwork: ${hostNetwork}` : ''}
2935
+ ${hostAliasesYaml}
2936
+ containers:
2937
+ - name: ${containerName}
2938
+ image: ${imageName}
2939
+ imagePullPolicy: ${imagePullPolicy}
2940
+ tty: ${tty}
2941
+ stdin: ${stdin}
2942
+ command: ${JSON.stringify(options.cmd ? options.cmd : ['/bin/bash', '-c'])}
2943
+ ${
2944
+ args.length > 0
2945
+ ? ` args:
2946
+ - |
2947
+ ${args.map((arg) => ` ${arg}`).join('\n')}`
2948
+ : ''
2949
+ }
2950
+ ${`${
2951
+ gpuEnable
2952
+ ? ` resources:
2953
+ limits:
2954
+ nvidia.com/gpu: '1'
2955
+ `
2956
+ : ''
2957
+ } env:
2958
+ ${Object.keys(envs)
2959
+ .map((key) => ({ key, value: typeof envs[key] === 'number' ? envs[key] : `"${envs[key]}"` }))
2960
+ .concat(gpuEnable ? [{ key: 'NVIDIA_VISIBLE_DEVICES', value: 'all' }] : [])
2961
+ .map((env) => ` - name: ${env.key}\n value: ${env.value}`)
2962
+ .join('\n')}`}
2963
+ ${
2964
+ enableVolumeMount
2965
+ ? Underpost.deploy.volumeFactory([{ volumeMountPath, volumeName, volumeHostPath, volumeType, claimName }]).render
2966
+ : ''
2967
+ }
2968
+ EOF`;
2969
+ shellExec(`kubectl delete pod ${podName} -n ${namespace} --ignore-not-found`);
2970
+ console.log(cmd);
2971
+ shellExec(cmd, { disableLog: true });
2972
+ const successInstance = await Underpost.test.statusMonitor(
2973
+ podName,
2974
+ options.monitorStatus || 'Running',
2975
+ options.monitorStatusKindType || 'pods',
2976
+ options.monitorStatusDeltaMs || 1000,
2977
+ options.monitorStatusMaxAttempts || 600,
2978
+ );
2979
+ if (successInstance) {
2980
+ options.on?.init ? await options.on.init() : null;
2981
+ if (options.logs) shellExec(`kubectl logs -f ${podName} -n ${namespace}`, { async: true });
2982
+ }
2983
+ },
2984
+
2985
+ /**
2986
+ * @method push-bundle
2987
+ * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
2988
+ * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
2989
+ * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2990
+ * @param {string} path - Optional `fsPath.splitOption` string.
2991
+ * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2992
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2993
+ * @param {string} [options.deployId] - Override deploy ID.
2994
+ * @param {boolean} [options.dev] - Use development environment; defaults to production.
2995
+ * @memberof UnderpostRun
2996
+ */
2997
+ 'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2998
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2999
+ const env = options.dev ? 'development' : 'production';
3000
+ const deployId = options.deployId || 'dd-default';
3001
+ const pathParts = (path || '').split('.');
3002
+ const fsPath = (pathParts[0] || '').trim() || 'build';
3003
+ const splitOption = (pathParts[1] || '').trim();
3004
+
3005
+ let splitFlag = '--split 8';
3006
+ if (splitOption) {
3007
+ if (splitOption === 'none-split') {
3008
+ splitFlag = '';
3009
+ } else {
3010
+ const splitMb = Number(splitOption);
3011
+ if (Number.isFinite(splitMb) && splitMb > 0) {
3012
+ splitFlag = `--split ${splitMb}`;
3013
+ } else {
3014
+ logger.warn('push-bundle: invalid split option, using default split 8', {
3015
+ path,
3016
+ splitOption,
3017
+ });
3018
+ }
3019
+ }
3020
+ }
3021
+
3022
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
3023
+ const confServer = fs.existsSync(confServerPath)
3024
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
3025
+ : {};
3026
+ const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
3027
+
3028
+ shellExec(`${baseCommand} env ${deployId} ${env}`);
3029
+ shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
3030
+
3031
+ const pushBundleFiles = (host, routePath) => {
3032
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
3033
+ const buildDir = `./${fsPath}`;
3034
+ if (!fs.existsSync(buildDir)) return;
3035
+ const partFiles = fs
3036
+ .readdirSync(buildDir)
3037
+ .filter(
3038
+ (name) =>
3039
+ name.startsWith(`${buildId}.zip.part`) ||
3040
+ name.startsWith(`${buildId}.zip-part`) ||
3041
+ name === `${buildId}.zip`,
3042
+ )
3043
+ .map((name) => `${fsPath}/${name}`);
3044
+ if (partFiles.length === 0) {
3045
+ logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
3046
+ return;
3047
+ }
3048
+ for (const partFile of partFiles) {
3049
+ shellExec(
3050
+ `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
3051
+ );
3052
+ }
3053
+ };
3054
+
3055
+ for (const host of Object.keys(confServer)) {
3056
+ for (const routePath of Object.keys(confServer[host])) {
3057
+ const routeConf = confServer[host][routePath] || {};
3058
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
3059
+ if (routeConf.singleReplica) {
3060
+ if (routeConf.replicas) {
3061
+ for (const replica of routeConf.replicas) {
3062
+ pushBundleFiles(host, replica);
3063
+ }
3064
+ }
3065
+ continue;
3066
+ }
3067
+ pushBundleFiles(host, routePath);
3068
+ }
3069
+ }
3070
+ },
3071
+
3072
+ /**
3073
+ * @method pull-bundle
3074
+ * @description Downloads split zip parts from file storage, merges and extracts them, and moves the result into the public directory.
3075
+ * Steps: set env, download parts (omit-unzip), merge zip, unzip, remove zip + parts, move to public/<host>[/path].
3076
+ * Iterates over every non-singleReplica, non-redirect, non-disabledRebuild route in conf.server.json
3077
+ * so that multi-path deployments are handled correctly.
3078
+ * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
3079
+ * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
3080
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3081
+ * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
3082
+ * @param {boolean} [options.dev] - Use development environment; defaults to production.
3083
+ * @memberof UnderpostRun
3084
+ */
3085
+ 'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
3086
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
3087
+ const env = options.dev ? 'development' : 'production';
3088
+ const deployId = options.deployId || 'dd-default';
3089
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
3090
+ const confServer = fs.existsSync(confServerPath)
3091
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
3092
+ : {};
3093
+ const hostsArg = path
3094
+ ? path
3095
+ .split(',')
3096
+ .map((h) => h.trim())
3097
+ .filter(Boolean)
3098
+ : Object.keys(confServer);
3099
+
3100
+ if (hostsArg.length === 0) {
3101
+ logger.error('pull-bundle: no hosts resolved', { deployId, path, confServerPath });
3102
+ return;
3103
+ }
3104
+
3105
+ shellExec(`${baseCommand} env ${deployId} ${env}`);
3106
+ if (!fs.existsSync('./build')) fs.mkdirSync('./build', { recursive: true });
3107
+ shellExec(
3108
+ `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
3109
+ );
3110
+
3111
+ const pullBundleRoute = (host, routePath) => {
3112
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
3113
+ const zipPath = `build/${buildId}.zip`;
3114
+ const buildDir = './build';
3115
+ const hasZip = fs.existsSync(zipPath);
3116
+ const hasParts =
3117
+ fs.existsSync(buildDir) &&
3118
+ fs
3119
+ .readdirSync(buildDir)
3120
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
3121
+
3122
+ if (!hasZip && !hasParts) {
3123
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
3124
+ return;
3125
+ }
3126
+
3127
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
3128
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
3129
+ shellExec(`sudo rm -rf ${zipPath}`);
3130
+
3131
+ if (fs.existsSync(buildDir)) {
3132
+ fs.readdirSync(buildDir)
3133
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
3134
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
3135
+ }
3136
+
3137
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
3138
+ if (!fs.existsSync(extractedDir)) {
3139
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
3140
+ return;
3141
+ }
3142
+
3143
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
3144
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
3145
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
3146
+ fs.copySync(`${extractedDir}`, `${publicDestPath}`);
3147
+ };
3148
+
3149
+ for (const host of hostsArg) {
3150
+ const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
3151
+
3152
+ for (const routePath of routePaths) {
3153
+ const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
3154
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
3155
+ if (routeConf.singleReplica) {
3156
+ if (routeConf.replicas) {
3157
+ for (const replica of routeConf.replicas) {
3158
+ pullBundleRoute(host, replica);
3159
+ }
3160
+ }
3161
+ continue;
3162
+ }
3163
+ pullBundleRoute(host, routePath);
3164
+ }
3165
+ }
3166
+ },
3167
+
3168
+ /**
3169
+ * @method build-cluster-deployment-manifests
3170
+ * @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.
3171
+ * @param {string} path - Unused.
3172
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3173
+ * @memberof UnderpostRun
3174
+ */
3175
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
3176
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
3177
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
3178
+ },
3179
+
3180
+ /**
3181
+ * @method monitor-ui
3182
+ * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
3183
+ * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
3184
+ * @param {string} path - Unused.
3185
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3186
+ * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
3187
+ * @memberof UnderpostRun
3188
+ */
3189
+ 'monitor-ui': (path, options = DEFAULT_OPTION) => {
3190
+ if (options.remove) {
3191
+ shellExec(`sudo firewall-cmd --zone=public --remove-service=cockpit --permanent`);
3192
+ shellExec(`sudo firewall-cmd --reload`);
3193
+ return;
3194
+ }
3195
+ shellExec(`sudo dnf install -y cockpit cockpit-machines libvirt`);
3196
+ shellExec(`sudo systemctl enable --now cockpit.socket libvirtd`);
3197
+ shellExec(`sudo firewall-cmd --permanent --add-service=cockpit`);
3198
+ shellExec(`sudo firewall-cmd --reload`);
3199
+ },
3200
+
3201
+ /**
3202
+ * @method shared-dir
3203
+ * @description Run once for initial shared-directory setup. Creates the group, adds the user,
3204
+ * creates the directory, sets ownership, applies the SGID bit, and configures default ACLs so
3205
+ * all future files inside the directory automatically inherit group write permissions.
3206
+ * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
3207
+ * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
3208
+ * Customise via the `path` argument or leave empty to use the default.
3209
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3210
+ * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
3211
+ * @memberof UnderpostRun
3212
+ */
3213
+ 'shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
3214
+ const dir = path || '/home/dd/engine';
3215
+ const user = options.user || 'admin';
3216
+ const group = options.group || 'engine-dev';
3217
+
3218
+ logger.info(`[setup-shared-dir] dir=${dir} user=${user} group=${group}`);
3219
+
3220
+ shellExec(`sudo groupadd ${group} 2>/dev/null || true`);
3221
+ shellExec(`sudo usermod -aG ${group} ${user}`);
3222
+ shellExec(`sudo mkdir -p ${dir}`);
3223
+ shellExec(`sudo chown -R ${user}:${group} ${dir}`);
3224
+ shellExec(`sudo chmod -R 2775 ${dir}`);
3225
+ shellExec(`sudo setfacl -d -m g:${group}:rwx ${dir}`);
3226
+ shellExec(`sudo setfacl -m g:${group}:rwx ${dir}`);
3227
+
3228
+ logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
3229
+ },
3230
+ /**
3231
+ * @method shared-dir-add-user
3232
+ * @description Add a user to an existing shared directory without changing
3233
+ * file owners. Grants recursive group/ACL access so the user can read and
3234
+ * write throughout the shared workspace while preserving existing ownership.
3235
+ *
3236
+ * @param {string} path - Shared directory (defaults to `/home/dd/engine`).
3237
+ * @param {UnderpostRunDefaultOptions} options - Underpost runner options.
3238
+ * Key fields:
3239
+ * - options.user (default: 'admin')
3240
+ * - options.group (default: 'engine-dev')
3241
+ *
3242
+ * @memberof UnderpostRun
3243
+ */ 'shared-dir-add-user': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
3244
+ const dir = path || '/home/dd/engine';
3245
+ const user = options.user || 'admin';
3246
+
3247
+ logger.info(`[shared-dir-add-user] dir=${dir} user=${user}`);
3248
+
3249
+ // Give the user direct access without changing owners or group ownership.
3250
+ shellExec(`sudo setfacl -R -m u:${user}:rwx ${dir}`);
3251
+
3252
+ // Make future files/directories inherit the same user ACL.
3253
+ shellExec(`sudo find ${dir} -type d -exec setfacl -d -m u:${user}:rwx {} \\;`);
3254
+
3255
+ logger.info(`[shared-dir-add-user] User '${user}' added to shared directory: ${dir}`);
3256
+ },
3257
+ };
3258
+
3259
+ static API = {
3260
+ /**
3261
+ * @method DEFAULT_OPTION
3262
+ * @description The default options for Underpost runners, including development mode, namespace, replicas, and underpost root path.
3263
+ * @memberof UnderpostRun
3264
+ * @static
3265
+ * @returns {Object} The default options object.
3266
+ */
3267
+ get DEFAULT_OPTION() {
3268
+ return DEFAULT_OPTION;
3269
+ },
3270
+ /**
3271
+ * @method RUNNERS
3272
+ * @description Retrieves the list of available runner IDs from the UnderpostRun class.
3273
+ * @memberof UnderpostRun
3274
+ * @returns {string[]} An array of runner IDs.
3275
+ */
3276
+ get RUNNERS() {
3277
+ return Object.keys(UnderpostRun.RUNNERS);
3278
+ },
3279
+
3280
+ /**
3281
+ * @method CALL
3282
+ * @description Executes a specified runner function from the UnderpostRun class with the provided path and options.
3283
+ * @param {string} runner - The name of the runner to execute.
3284
+ * @param {string} path - The input value, identifier, or path for the operation.
3285
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3286
+ * @memberof UnderpostRun
3287
+ * @returns {Promise<any>} The result of the runner execution.
3288
+ */
3289
+ async CALL(runner = '', path = '', options = DEFAULT_OPTION) {
3290
+ return await UnderpostRun.RUNNERS[runner](path, options);
3291
+ },
3292
+
3293
+ /**
3294
+ * @method callback
3295
+ * @description Initiates the execution of a specified CLI command (runner) with the given input value (`path`) and processed options.
3296
+ * @param {string} runner - The name of the runner to execute.
3297
+ * @param {string} path - The input value, identifier, or path for the operation.
3298
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3299
+ * @memberof UnderpostRun
3300
+ * @returns {Promise<any>} The result of the callback execution.
3301
+ */
3302
+ async callback(runner, path, options = DEFAULT_OPTION) {
3303
+ try {
3304
+ const npmRoot = getNpmRootPath();
3305
+ const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
3306
+ if (options.cmd) options.cmd = options.cmd.split(',');
3307
+ if (options.args) options.args = options.args.split(',');
3308
+ if (!options.underpostRoot) options.underpostRoot = underpostRoot;
3309
+ if (!options.namespace) options.namespace = 'default';
3310
+ if (options.replicas === '' || options.replicas === null || options.replicas === undefined)
3311
+ options.replicas = 1;
3312
+ options.npmRoot = npmRoot;
3313
+ logger.info(`Executing runner`, { runner, namespace: options.namespace });
3314
+ if (!Underpost.run.RUNNERS.includes(runner)) throw new Error(`Runner not found: ${runner}`);
3315
+ const result = await Underpost.run.CALL(runner, path, options);
3316
+ return result;
3317
+ } catch (error) {
3318
+ console.log(error);
3319
+ logger.error(error);
3320
+ process.exit(1);
3321
+ }
3322
+ },
3323
+ };
3324
+ }
3325
+
3326
+ export default UnderpostRun;