@underpostnet/cyberia 3.2.90 → 3.3.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
@@ -6,24 +6,14 @@
6
6
 
7
7
  import dotenv from 'dotenv';
8
8
  import { commitData } from '../client/components/core/CommonJs.js';
9
- import { pbcopy, shellCd, shellExec } from '../server/process.js';
10
- import { actionInitLog, loggerFactory } from '../server/logger.js';
9
+ import { pbcopy, shellArgumentFactory, shellCd, shellExec } from '../server/runtime/process.js';
10
+ import { actionInitLog, loggerFactory, redactSensitiveText } from '../server/ops/logger.js';
11
11
  import path from 'path';
12
12
  import fs from 'fs-extra';
13
- import {
14
- getNpmRootPath,
15
- Config,
16
- loadConf,
17
- readConfJson,
18
- getConfFilePath,
19
- loadReplicas,
20
- loadConfServerJson,
21
- getDataDeploy,
22
- buildReplicaId,
23
- writeEnv,
24
- readConfInstances,
25
- } from '../server/conf.js';
26
- import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../client-builder/client-build.js';
13
+ import { Config, readConfJson, readConfInstances } from '../server/runtime/conf.js';
14
+ import { readDeployRoutes, registerDeployRoute } from '../server/network/router.js';
15
+ import { getUnderpostRootPath } from '../server/runtime/environment.js';
16
+ import { githubCommitUrlFactory, repositoryIdentityFactory } from '../server/storage/repository.js';
27
17
  import { DefaultConf } from '../../conf.js';
28
18
  import Underpost from '../index.js';
29
19
 
@@ -51,18 +41,25 @@ class UnderpostRepository {
51
41
  clone(gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`, options = { bare: false, g8: false }) {
52
42
  const gExtension = options.g8 === true ? '.g8' : '.git';
53
43
  const repoName = gitUri.split('/').pop();
54
- if (fs.existsSync(`./${repoName}`)) fs.removeSync(`./${repoName}`);
55
- shellExec(
56
- `git clone ${options?.bare === true ? ` --bare ` : ''}https://${
57
- process.env.GITHUB_TOKEN ? `${process.env.GITHUB_TOKEN}@` : ''
58
- }github.com/${gitUri}${gExtension}`,
59
- {
60
- disableLog: true,
61
- },
62
- );
44
+ const auth = Underpost.repo.gitAuthFactory(`https://github.com/${gitUri}${gExtension}`);
45
+ // A bare clone lands in `<repo>.git`: clearing `<repo>` instead would both leave the
46
+ // real target in place and delete an unrelated work tree that happens to share the name.
47
+ const clonePath = `./${repoName}${options?.bare === true ? gExtension : ''}`;
48
+ if (fs.existsSync(clonePath)) fs.removeSync(clonePath);
49
+ shellExec(`git clone ${options?.bare === true ? ` --bare ` : ''}"${auth.url}"`, {
50
+ disableLog: true,
51
+ env: auth.env,
52
+ });
63
53
  },
64
54
  /**
65
55
  * Pulls updates from a GitHub repository.
56
+ *
57
+ * `--ff-only` because the reconcile strategy must not come from the host: without it git
58
+ * refuses a divergent pull outright on a node that configured neither `pull.rebase` nor
59
+ * `pull.ff`, and silently merges or rebases on one that did — the same command producing a
60
+ * different history per machine. Advancing to the remote is what every caller wants; a
61
+ * checkout that has drifted onto commits of its own is replaced through
62
+ * {@link UnderpostRepository.switchRemote}, not reconciled here.
66
63
  * @param {string} [repoPath='./'] - The local path to the repository.
67
64
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository.
68
65
  * @param {object} [options={ g8: false }] - Pulling options.
@@ -75,14 +72,11 @@ class UnderpostRepository {
75
72
  options = { g8: false },
76
73
  ) {
77
74
  const gExtension = options.g8 === true ? '.g8' : '.git';
78
- shellExec(
79
- `cd ${repoPath} && git pull https://${
80
- process.env.GITHUB_TOKEN ? `${process.env.GITHUB_TOKEN}@` : ''
81
- }github.com/${gitUri}${gExtension}`,
82
- {
83
- disableLog: true,
84
- },
85
- );
75
+ const auth = Underpost.repo.gitAuthFactory(`https://github.com/${gitUri}${gExtension}`);
76
+ shellExec(`cd ${repoPath} && git pull --ff-only "${auth.url}"`, {
77
+ disableLog: true,
78
+ env: auth.env,
79
+ });
86
80
  },
87
81
  /**
88
82
  * Creates a Git commit with a conventional commit message.
@@ -104,12 +98,12 @@ class UnderpostRepository {
104
98
  * @param {string} [options.hashes=''] - If provided with diff option, shows the diff between two hashes.
105
99
  * @param {string} [options.extension=''] - If provided with diff option, filters the diff by this file extension.
106
100
  * @param {boolean|string} [options.changelog=undefined] - If true, prints the changelog since the last CI integration commit (starting with 'ci(package-pwa-microservices-'). If a number string, prints the changelog of the last N commits split by version sections. Only considers commits starting with '[<tag>]'.
107
- * @param {boolean} [options.changelogBuild=false] - If true, scrapes all git history and builds a full CHANGELOG.md. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
101
+ * @param {boolean} [options.changelogBuild=false] - If true, scrapes git history and builds a CHANGELOG.md from the five latest versions. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
108
102
  * @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
109
103
  * @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
110
104
  * @param {boolean} [options.remoteUrl=false] - If true, prints the current git remote URL (origin) in plain text and returns.
111
105
  * @param {string} [options.switchRepo=''] - If set, switches the remote `origin` to this URL and force-pulls the target branch, overwriting the current working tree.
112
- * @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to `master`).
106
+ * @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to the remote's default branch).
113
107
  * @memberof UnderpostRepository
114
108
  */
115
109
  commit(
@@ -181,7 +175,7 @@ class UnderpostRepository {
181
175
  Underpost.repo.switchRemote({
182
176
  path: repoPath,
183
177
  url: options.switchRepo,
184
- branch: options.targetBranch || 'master',
178
+ branch: options.targetBranch,
185
179
  });
186
180
  return;
187
181
  }
@@ -227,6 +221,19 @@ class UnderpostRepository {
227
221
  return;
228
222
  }
229
223
 
224
+ if (options.propagateMsg) {
225
+ // Chain hop: carry the payload the previous workflow sent instead of regenerating it, so
226
+ // every repository downstream of the dispatch commits the same entries.
227
+ const propagated = Underpost.repo.resolvePropagationMessage(process.env.PROPAGATED_MESSAGE);
228
+ if (propagated) {
229
+ console.log(propagated);
230
+ return;
231
+ }
232
+ // Nothing arrived — fall through to this repository's own changelog for the last N commits.
233
+ options.changelogMsg = true;
234
+ options.changelogNoHash = true;
235
+ }
236
+
230
237
  if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
231
238
  const releaseMatch = 'New release v:';
232
239
  // Helper: parse [<tag>] commits into grouped sections
@@ -280,9 +287,7 @@ class UnderpostRepository {
280
287
  .filter((c) => c.hash);
281
288
  };
282
289
 
283
- const githubUser = process.env.GITHUB_USERNAME || 'underpostnet';
284
- const commitUrl = (shortHash, fullHash) =>
285
- `[${shortHash}](https://github.com/${githubUser}/engine/commit/${fullHash})`;
290
+ const commitUrl = (shortHash, hash) => githubCommitUrlFactory({ shortHash, hash });
286
291
 
287
292
  // Helper: extract version from commit message containing 'New release v:'
288
293
  const extractVersion = (message) => {
@@ -332,17 +337,19 @@ class UnderpostRepository {
332
337
  const changelogMinVersion = options.changelogMinVersion || '2.97.1';
333
338
 
334
339
  if (options.changelogBuild) {
335
- // --changelog-build: scrape ALL history, split by 'New release v:' commits as version sections
340
+ // --changelog-build: retain the current changes and the five latest version sections.
336
341
  const allCommits = fetchHistory();
337
342
  const sections = buildVersionSections(allCommits);
338
343
 
339
- // Filter sections: stop at changelogMinVersion boundary
344
+ // Stop at either the fifth release section or the configured minimum-version boundary.
340
345
  const limitedSections = [];
346
+ let versionCount = 0;
341
347
  for (const section of sections) {
342
348
  limitedSections.push(section);
343
349
  if (section.title) {
350
+ versionCount++;
344
351
  const versionStr = section.title.replace(releaseMatch, '').trim();
345
- if (versionStr === changelogMinVersion) break;
352
+ if (versionCount === 5 || versionStr === changelogMinVersion) break;
346
353
  }
347
354
  }
348
355
 
@@ -442,7 +449,10 @@ class UnderpostRepository {
442
449
  } ${message ? message : commitData[commitType].description}`;
443
450
  if (options.copy) return pbcopy(_message);
444
451
  shellExec(
445
- `cd ${repoPath} && git commit ${options?.empty ? `--allow-empty ` : ''}${options.edit ? `--amend --no-edit ` : `-m "${_message}"`}`,
452
+ `cd ${repoPath} && git commit ${options?.empty ? `--allow-empty ` : ''}${
453
+ // A propagated payload is multi-line and may hold `$`: quoting keeps every entry intact.
454
+ options.edit ? `--amend --no-edit ` : `-m ${shellArgumentFactory(_message)}`
455
+ }`,
446
456
  );
447
457
  },
448
458
 
@@ -475,14 +485,11 @@ class UnderpostRepository {
475
485
  options = { f: false, g8: false },
476
486
  ) {
477
487
  const gExtension = options.g8 === true ? '.g8' : '.git';
478
- shellExec(
479
- `cd ${repoPath} && git push https://${process.env.GITHUB_TOKEN}@github.com/${gitUri}${gExtension}${
480
- options?.f === true ? ' --force' : ''
481
- }`,
482
- {
483
- disableLog: true,
484
- },
485
- );
488
+ const auth = Underpost.repo.gitAuthFactory(`https://github.com/${gitUri}${gExtension}`);
489
+ shellExec(`cd ${repoPath} && git push "${auth.url}"${options?.f === true ? ' --force' : ''}`, {
490
+ disableLog: true,
491
+ env: auth.env,
492
+ });
486
493
  logger.info(
487
494
  'commit url',
488
495
  `http://github.com/${gitUri}${gExtension === '.g8' ? '.g8' : ''}/commit/${shellExec(
@@ -494,13 +501,96 @@ class UnderpostRepository {
494
501
  );
495
502
  },
496
503
 
504
+ /**
505
+ * Initializes the base cluster deploy folder `engine-private/deploy` from `./conf.js` (DefaultConf).
506
+ *
507
+ * Writes the minimum set required by the cluster runtime:
508
+ * `conf.event.json`, `conf.users.json`, `conf.wireguard.json`, `dd.cron`, `dd.routes`, `id_rsa`, `id_rsa.pub`.
509
+ *
510
+ * When a deploy ID is explicitly provided, it also generates the deploy CI/CD workflows and its default conf.
511
+ *
512
+ * Idempotent: existing files are never overwritten, so it is safe to rerun over a provisioned cluster.
513
+ * @param {string} [deployId=''] - Deploy ID registered in the route table and used as default cron deploy ID (defaults to `dd-default`, `dd-` prefix is normalized).
514
+ * @returns {{ deployPath: string, created: string[] }} The deploy folder and the files created by this run.
515
+ * @memberof UnderpostRepository
516
+ */
517
+ clusterDeployFactory(deployId = '') {
518
+ const deployPath = './engine-private/deploy';
519
+ const created = [];
520
+
521
+ const hasDeployId = `${deployId || ''}`.trim().length > 0;
522
+ deployId = `${deployId || ''}`.trim() || 'dd-default';
523
+ if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
524
+
525
+ fs.mkdirSync(deployPath, { recursive: true });
526
+
527
+ const confFiles = {
528
+ 'conf.event.json': DefaultConf.event,
529
+ 'conf.users.json': DefaultConf.users,
530
+ 'conf.wireguard.json': DefaultConf.wireguard,
531
+ };
532
+
533
+ for (const [fileName, conf] of Object.entries(confFiles)) {
534
+ const confPath = `${deployPath}/${fileName}`;
535
+ if (fs.existsSync(confPath)) continue;
536
+ fs.writeFileSync(confPath, JSON.stringify(conf, null, 4), 'utf8');
537
+ created.push(confPath);
538
+ }
539
+
540
+ const cronPath = `${deployPath}/dd.cron`;
541
+ if (!fs.existsSync(cronPath)) {
542
+ fs.writeFileSync(cronPath, deployId, 'utf8');
543
+ created.push(cronPath);
544
+ }
545
+
546
+ const routesPath = `${deployPath}/dd.routes`;
547
+ const routesExists = fs.existsSync(routesPath);
548
+ registerDeployRoute(deployId);
549
+ if (!routesExists) created.push(routesPath);
550
+
551
+ const keyPath = `${deployPath}/id_rsa`;
552
+ const pubKeyPath = `${deployPath}/id_rsa.pub`;
553
+ if (!fs.existsSync(keyPath)) {
554
+ shellExec(`ssh-keygen -t ed25519 -f ${keyPath} -N "" -q -C "root@${deployId}"`, { disableLog: true });
555
+ fs.chmodSync(keyPath, 0o600);
556
+ created.push(keyPath);
557
+ created.push(pubKeyPath);
558
+ } else if (!fs.existsSync(pubKeyPath)) {
559
+ shellExec(`ssh-keygen -y -f ${keyPath} -P "" > ${pubKeyPath}`, { disableLog: true });
560
+ created.push(pubKeyPath);
561
+ }
562
+
563
+ if (hasDeployId) {
564
+ const repoName = `engine-${deployId.split('dd-')[1]}`;
565
+ fs.writeFileSync(
566
+ `./.github/workflows/${repoName}.cd.yml`,
567
+ fs
568
+ .readFileSync(`./.github/workflows/engine-test.cd.yml`, 'utf8')
569
+ .replaceAll('test', deployId.split('dd-')[1]),
570
+ 'utf8',
571
+ );
572
+ fs.writeFileSync(
573
+ `./.github/workflows/${repoName}.ci.yml`,
574
+ fs
575
+ .readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8')
576
+ .replaceAll('test', deployId.split('dd-')[1]),
577
+ 'utf8',
578
+ );
579
+ shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
580
+ }
581
+
582
+ logger.info('Cluster deploy base', { deployPath, created });
583
+
584
+ return { deployPath, created };
585
+ },
586
+
497
587
  /**
498
588
  * Initializes a new Underpost repository, optionally setting up a deploy ID or sub-configuration.
499
589
  * @param {string} [projectName=''] - The name of the project to create.
500
590
  * @param {object} [options] - Initialization options.
501
591
  * @param {string} [options.deployId=''] - The deployment ID to set up.
502
592
  * @param {string} [options.subConf=''] - The sub-configuration to create.
503
- * @param {boolean} [options.cluster=false] - If true, sets up a clustered configuration.
593
+ * @param {boolean} [options.cluster=false] - If true, initializes only the base cluster deploy folder (`engine-private/deploy`) from `./conf.js` and returns; no project or deploy ID scaffolding runs.
504
594
  * @param {boolean} [options.dev=false] - If true, uses development settings.
505
595
  * @param {boolean} [options.buildRepos=false] - If true, creates the deployment repositories (engine-*, engine-*-private, engine-*-cron-backups).
506
596
  * @param {boolean} [options.purge=false] - If true, removes the deploy ID conf and all related repositories (requires deployId).
@@ -535,6 +625,12 @@ class UnderpostRepository {
535
625
  await logger.setUpInfo();
536
626
  actionInitLog();
537
627
 
628
+ // Handle cluster deploy base operation (standalone: never scaffolds a project)
629
+ if (options.cluster === true) {
630
+ UnderpostRepository.API.clusterDeployFactory(options.deployId || projectName);
631
+ return resolve(true);
632
+ }
633
+
538
634
  // Handle cleanTemplate operation
539
635
  if (options.cleanTemplate) {
540
636
  logger.info('Cleaning build directory');
@@ -631,8 +727,6 @@ class UnderpostRepository {
631
727
  ];
632
728
 
633
729
  const username = process.env.GITHUB_USERNAME;
634
- const token = process.env.GITHUB_TOKEN;
635
-
636
730
  if (!username) {
637
731
  logger.error('GITHUB_USERNAME environment variable not set');
638
732
  return reject(false);
@@ -644,7 +738,7 @@ class UnderpostRepository {
644
738
  logger.info(`Created repository directory: ${repo.path}`);
645
739
  }
646
740
 
647
- const remoteUrl = `https://${token ? `${token}@` : ''}github.com/${username}/${repo.name}.git`;
741
+ const remoteUrl = `https://github.com/${username}/${repo.name}.git`;
648
742
  UnderpostRepository.API.initLocalRepo({ path: repo.path, origin: remoteUrl });
649
743
  logger.info(`Initialized git repository with remote: ${repo.name}`);
650
744
  }
@@ -675,255 +769,7 @@ class UnderpostRepository {
675
769
  }
676
770
  return resolve(true);
677
771
  } catch (error) {
678
- console.log(error);
679
- logger.error(error, error.stack);
680
- return reject(false);
681
- }
682
- });
683
- },
684
-
685
- /**
686
- * Builds client assets, single replicas, and/or syncs environment ports.
687
- * @param {string} [deployId='dd-default'] - The deployment ID.
688
- * @param {string} [subConf=''] - The sub-configuration for the build.
689
- * @param {string} [host=''] - Comma-separated hosts to filter the build.
690
- * @param {string} [path=''] - Comma-separated paths to filter the build.
691
- * @param {object} [options] - Build options.
692
- * @param {boolean} [options.syncEnvPort=false] - If true, syncs environment port assignments across all deploy IDs.
693
- * @param {boolean} [options.singleReplica=false] - If true, builds single replica folders instead of full client.
694
- * @param {boolean} [options.buildZip=false] - If true, creates zip files of the builds.
695
- * @param {string|number} [options.split=''] - Optional ZIP part size in MB. When set with buildZip, writes split parts.
696
- * @param {string} [options.unzip=''] - Optional build ZIP prefix to extract from ./build.
697
- * @param {string} [options.mergeZip=''] - Optional build prefix to merge split ZIP parts into a single ZIP.
698
- * @param {boolean} [options.liteBuild=false] - If true, skips full build (default is full build).
699
- * @param {boolean} [options.iconsBuild=false] - If true, builds icons.
700
- * @returns {Promise<boolean>} A promise that resolves when the build is complete.
701
- * @memberof UnderpostRepository
702
- */
703
- client(
704
- deployId = 'dd-default',
705
- subConf = '',
706
- host = '',
707
- path = '',
708
- options = {
709
- syncEnvPort: false,
710
- singleReplica: false,
711
- buildZip: false,
712
- split: '',
713
- unzip: '',
714
- mergeZip: '',
715
- liteBuild: false,
716
- iconsBuild: false,
717
- },
718
- ) {
719
- return new Promise(async (resolve, reject) => {
720
- try {
721
- if (options.mergeZip) {
722
- mergeClientBuildZip({
723
- buildPrefix: options.mergeZip,
724
- logger,
725
- });
726
- return resolve(true);
727
- }
728
-
729
- if (options.unzip) {
730
- unzipClientBuild({
731
- buildPrefix: options.unzip,
732
- logger,
733
- });
734
- return resolve(true);
735
- }
736
-
737
- // Handle singleReplica operation (must run before syncEnvPort to ensure replica dirs exist)
738
- if (options.singleReplica) {
739
- const replicaPath = path;
740
- if (!deployId || !host || !replicaPath) {
741
- logger.error('client --single-replica requires deploy-id, host, and path arguments');
742
- return reject(false);
743
- }
744
- const serverConf = loadReplicas(
745
- deployId,
746
- loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
747
- );
748
-
749
- if (serverConf[host][replicaPath].replicas) {
750
- {
751
- let replicaIndex = -1;
752
- for (const replica of serverConf[host][replicaPath].replicas) {
753
- replicaIndex++;
754
- const replicaDeployId = `${deployId}-${serverConf[host][replicaPath].replicas[replicaIndex].slice(1)}`;
755
- await fs.copy(`./engine-private/conf/${deployId}`, `./engine-private/replica/${replicaDeployId}`);
756
- fs.writeFileSync(
757
- `./engine-private/replica/${replicaDeployId}/package.json`,
758
- fs
759
- .readFileSync(`./engine-private/replica/${replicaDeployId}/package.json`, 'utf8')
760
- .replaceAll(`${deployId}`, `${replicaDeployId}`),
761
- 'utf8',
762
- );
763
- const replicaFolder = `./engine-private/replica/${replicaDeployId}`;
764
- for (const envFile of ['.env.production', '.env.development', '.env.test']) {
765
- const envFilePath = `${replicaFolder}/${envFile}`;
766
- if (fs.existsSync(envFilePath)) {
767
- fs.writeFileSync(
768
- envFilePath,
769
- fs
770
- .readFileSync(envFilePath, 'utf8')
771
- .replaceAll(`DEPLOY_ID=${deployId}`, `DEPLOY_ID=${replicaDeployId}`),
772
- 'utf8',
773
- );
774
- }
775
- }
776
- }
777
- }
778
- {
779
- let replicaIndex = -1;
780
- for (const replica of serverConf[host][replicaPath].replicas) {
781
- replicaIndex++;
782
- const replicaDeployId = `${deployId}-${serverConf[host][replicaPath].replicas[replicaIndex].slice(1)}`;
783
- let replicaServerConf = JSON.parse(
784
- fs.readFileSync(`./engine-private/replica/${replicaDeployId}/conf.server.json`, 'utf8'),
785
- );
786
-
787
- const singleReplicaConf = replicaServerConf[host][replicaPath];
788
- singleReplicaConf.replicas = undefined;
789
- singleReplicaConf.singleReplica = undefined;
790
-
791
- replicaServerConf = {};
792
- replicaServerConf[host] = {};
793
- replicaServerConf[host][replica] = singleReplicaConf;
794
-
795
- fs.writeFileSync(
796
- `./engine-private/replica/${replicaDeployId}/conf.server.json`,
797
- JSON.stringify(replicaServerConf, null, 4),
798
- 'utf8',
799
- );
800
- }
801
- }
802
- }
803
- if (!options.syncEnvPort) return resolve(true);
804
- }
805
-
806
- // Handle syncEnvPort operation
807
- if (options.syncEnvPort) {
808
- const dataDeploy = await getDataDeploy({ disableSyncEnvPort: true });
809
- const dataEnv = [
810
- { env: 'production', port: 3000 },
811
- { env: 'development', port: 4000 },
812
- { env: 'test', port: 5000 },
813
- ];
814
- let portOffset = 0;
815
- const singleReplicaPortOffsets = {};
816
- for (const deployIdObj of dataDeploy) {
817
- const { deployId } = deployIdObj;
818
- const baseConfPath = fs.existsSync(`./engine-private/replica/${deployId}`)
819
- ? `./engine-private/replica`
820
- : `./engine-private/conf`;
821
-
822
- const effectivePortOffset =
823
- singleReplicaPortOffsets[deployId] !== undefined ? singleReplicaPortOffsets[deployId] : portOffset;
824
-
825
- let skipDeploy = false;
826
- for (const envInstanceObj of dataEnv) {
827
- const envPath = `${baseConfPath}/${deployId}/.env.${envInstanceObj.env}`;
828
- if (!fs.existsSync(envPath)) {
829
- logger.warn(`Skipping ${deployId}: ${envPath} not found`);
830
- skipDeploy = true;
831
- break;
832
- }
833
- const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
834
- envObj.PORT = `${envInstanceObj.port + effectivePortOffset}`;
835
- writeEnv(envPath, envObj);
836
- }
837
-
838
- if (skipDeploy) continue;
839
- if (singleReplicaPortOffsets[deployId] !== undefined) continue;
840
-
841
- const serverConf = loadReplicas(
842
- deployId,
843
- loadConfServerJson(`${baseConfPath}/${deployId}/conf.server.json`),
844
- );
845
- for (const host of Object.keys(serverConf)) {
846
- let deferredSingleReplicaSlots = [];
847
- for (const path of Object.keys(serverConf[host])) {
848
- if (serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
849
- deferredSingleReplicaSlots.push({
850
- replicas: serverConf[host][path].replicas,
851
- peer: !!serverConf[host][path].peer,
852
- });
853
- continue;
854
- }
855
- portOffset++;
856
- if (serverConf[host][path].peer) portOffset++;
857
- }
858
- for (const slot of deferredSingleReplicaSlots) {
859
- for (const replica of slot.replicas) {
860
- const replicaDeployId = buildReplicaId({ deployId, replica });
861
- singleReplicaPortOffsets[replicaDeployId] = portOffset;
862
- portOffset++;
863
- if (slot.peer) portOffset++;
864
- }
865
- }
866
- }
867
- }
868
- return resolve(true);
869
- }
870
-
871
- // Handle buildFullClient operation (default)
872
- {
873
- const { deployId: resolvedDeployId } = loadConf(deployId, subConf ?? '');
874
-
875
- let argHost = host ? host.split(',') : [];
876
- let argPath = path ? path.split(',') : [];
877
- let deployIdSingleReplicas = [];
878
- let singleReplicaHosts = [];
879
- const isReplicaContext = resolvedDeployId
880
- ? fs.existsSync(`./engine-private/replica/${resolvedDeployId}`)
881
- : false;
882
- const serverConf = resolvedDeployId
883
- ? readConfJson(resolvedDeployId, 'server', { loadReplicas: true })
884
- : Config.default.server;
885
- const confFilePath = resolvedDeployId ? getConfFilePath(resolvedDeployId, 'server') : null;
886
- const originalConfBackup = confFilePath ? fs.readFileSync(confFilePath, 'utf8') : null;
887
- for (const host of Object.keys(serverConf)) {
888
- for (const path of Object.keys(serverConf[host])) {
889
- if (argHost.length && argPath.length && (!argHost.includes(host) || !argPath.includes(path))) {
890
- delete serverConf[host][path];
891
- } else {
892
- if (!isReplicaContext && serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
893
- singleReplicaHosts.push({ host, path });
894
- deployIdSingleReplicas = deployIdSingleReplicas.concat(
895
- serverConf[host][path].replicas.map((replica) =>
896
- buildReplicaId({ deployId: resolvedDeployId, replica }),
897
- ),
898
- );
899
- }
900
- }
901
- }
902
- }
903
- await buildClient({
904
- deployId: resolvedDeployId,
905
- buildZip: options.buildZip || false,
906
- split: options.split || '',
907
- fullBuild: options.liteBuild ? false : true,
908
- iconsBuild: options.iconsBuild || false,
909
- });
910
- for (const replicaDeployId of deployIdSingleReplicas) {
911
- if (!fs.existsSync(`./engine-private/replica/${replicaDeployId}`)) {
912
- logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
913
- continue;
914
- }
915
- await Underpost.repo.client(replicaDeployId, '', '', '', {
916
- buildZip: options.buildZip || false,
917
- split: options.split || '',
918
- liteBuild: options.liteBuild || false,
919
- iconsBuild: options.iconsBuild || false,
920
- });
921
- }
922
-
923
- return resolve(true);
924
- }
925
- } catch (error) {
926
- console.log(error);
772
+ console.error(error);
927
773
  logger.error(error, error.stack);
928
774
  return reject(false);
929
775
  }
@@ -972,7 +818,7 @@ class UnderpostRepository {
972
818
  */
973
819
  privateConfUpdate(deployId) {
974
820
  shellCd(`/home/dd/engine`);
975
- const privateRepoName = `engine-${deployId.split('dd-')[1]}-private`;
821
+ const privateRepoName = Underpost.repo.privateRepoFactory(deployId);
976
822
  const privateRepoPath = `../${privateRepoName}`;
977
823
  if (fs.existsSync(privateRepoPath)) fs.removeSync(privateRepoPath);
978
824
  shellExec(`cd .. && underpost clone ${process.env.GITHUB_USERNAME}/${privateRepoName}`);
@@ -1273,23 +1119,32 @@ Prevent build private config repo.`,
1273
1119
  /**
1274
1120
  * Resolves the default branch for a remote GitHub repository by querying
1275
1121
  * `git ls-remote --symref` and extracting the HEAD ref target.
1276
- * Falls back to `main` when detection fails.
1277
1122
  * @param {string} repo - The GitHub repository (e.g., "owner/repo").
1278
1123
  * @returns {string} The default branch name (e.g. "main" or "master").
1279
1124
  * @memberof UnderpostRepository
1280
1125
  */
1281
1126
  getDefaultBranch(repo) {
1282
- if (!repo) return 'main';
1283
- const authUrl = Underpost.repo.resolveAuthUrl(repo);
1284
- const raw = shellExec(
1285
- `GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --symref "${authUrl}" HEAD 2>&1`,
1286
- { stdout: true, silent: true, disableLog: true, silentOnError: true },
1287
- );
1127
+ if (!repo) throw new Error('[repo] getDefaultBranch requires a repository reference');
1128
+ const auth = Underpost.repo.gitAuthFactory(repo);
1129
+ const raw = shellExec(`git ls-remote --symref "${auth.url}" HEAD 2>&1`, {
1130
+ stdout: true,
1131
+ silent: true,
1132
+ disableLog: true,
1133
+ silentOnError: true,
1134
+ env: auth.env,
1135
+ });
1288
1136
  // --symref emits a line like: ref: refs/heads/main HEAD
1289
1137
  const match = typeof raw === 'string' ? raw.match(/^ref:\s*refs\/heads\/(\S+)\tHEAD$/m) : null;
1290
- const branch = match ? match[1] : 'main';
1291
- logger.info('getDefaultBranch', { repo, branch });
1292
- return branch;
1138
+ // Guessing here is worse than failing: `main` against a `master` remote
1139
+ // fetches a ref that does not exist, and the caller has already pointed
1140
+ // origin at the new URL by the time git says so.
1141
+ if (!match)
1142
+ throw new Error(
1143
+ `[repo] cannot read the default branch of '${repo}'; it may be empty, unreachable, ` +
1144
+ `or need credentials: ${redactSensitiveText(`${raw || ''}`.trim().split('\n').pop() || 'no ref reported')}`,
1145
+ );
1146
+ logger.info('getDefaultBranch', { repo, branch: match[1] });
1147
+ return match[1];
1293
1148
  },
1294
1149
 
1295
1150
  /**
@@ -1322,7 +1177,7 @@ Prevent build private config repo.`,
1322
1177
  } else {
1323
1178
  let token = process.env.GITHUB_TOKEN;
1324
1179
  if (!token) {
1325
- const envPath = `${getNpmRootPath()}/underpost/.env`;
1180
+ const envPath = `${getUnderpostRootPath()}/.env`;
1326
1181
  if (fs.existsSync(envPath) && fs.statSync(envPath).isFile()) {
1327
1182
  const envVars = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
1328
1183
  token = envVars.GITHUB_TOKEN;
@@ -1347,46 +1202,220 @@ Prevent build private config repo.`,
1347
1202
  },
1348
1203
 
1349
1204
  /**
1350
- * Resolves a Git remote URL, normalizing short-form owner/repo references to full
1351
- * GitHub HTTPS URLs and injecting GITHUB_TOKEN when available.
1352
- * @param {string} url - The repository URL or short-form (e.g. "owner/repo" or full HTTPS URL).
1353
- * @returns {string} The resolved (and optionally authenticated) HTTPS URL.
1205
+ * Normalizes a repository reference to a clone URL, expanding the short
1206
+ * `owner/repo` form against GitHub.
1207
+ * @param {string} url - Repository URL or `owner/repo` short form.
1208
+ * @returns {string} Clone URL, unchanged when one was already given.
1209
+ * @memberof UnderpostRepository
1210
+ */
1211
+ repoUrlFactory(url = '') {
1212
+ const value = `${url || ''}`.trim();
1213
+ if (!value) return value;
1214
+ const remote = value.startsWith('http://') || value.startsWith('https://') || value.startsWith('git@');
1215
+ return remote ? value : `https://github.com/${value}`;
1216
+ },
1217
+
1218
+ /**
1219
+ * Reduces a repository reference to its `owner/repo` slug, the form the
1220
+ * clone and pull commands take.
1221
+ * @param {string} url - Repository URL or `owner/repo` short form.
1222
+ * @returns {string} Slug.
1223
+ * @throws {Error} When no owner and repository can be read from the reference.
1224
+ * @memberof UnderpostRepository
1225
+ */
1226
+ repoSlugFactory(url = '') {
1227
+ const path = Underpost.repo
1228
+ .repoUrlFactory(url)
1229
+ .replace(/^git@[^:]+:/, '')
1230
+ .replace(/^https?:\/\/[^/]+\//, '')
1231
+ .replace(/\.git$/, '')
1232
+ .replace(/\/+$/, '');
1233
+ const [owner, repo] = path.split('/');
1234
+ if (!owner || !repo) throw new Error(`[repo] '${url}' is not an owner/repo reference or a clone URL`);
1235
+ return `${owner}/${repo}`;
1236
+ },
1237
+
1238
+ /**
1239
+ * Reduces any deploy-scoped reference to the conf id its repositories share.
1240
+ *
1241
+ * Single source of that parsing: deploy ids (`dd-lampp`), source repos (`engine-lampp`),
1242
+ * test source repos (`engine-test-lampp`) and private conf repos (`engine-lampp-private`)
1243
+ * all name the same deployment, and the monorepo (`engine` / `engine-private`) names none.
1244
+ * @param {string} reference - Deploy id, repository name, `owner/repo` slug, or clone URL.
1245
+ * @returns {string} Conf id, or `''` for the monorepo.
1246
+ * @memberof UnderpostRepository
1247
+ */
1248
+ confIdFactory(reference = '') {
1249
+ const name = `${reference || ''}`
1250
+ .trim()
1251
+ .split('/')
1252
+ .pop()
1253
+ .replace(/\.git$/, '');
1254
+ const confId = name
1255
+ .replace(/^dd-/, '')
1256
+ .replace(/^engine-test-/, '')
1257
+ .replace(/^engine-/, '')
1258
+ .replace(/-private$/, '');
1259
+ return confId === 'engine' || confId === 'private' ? '' : confId;
1260
+ },
1261
+
1262
+ /**
1263
+ * Names the engine source repository a reference deploys from.
1264
+ * @param {string} reference - Deploy id or repository reference.
1265
+ * @param {object} [options] - Naming options.
1266
+ * @param {boolean} [options.test=false] - Name the private test source repo instead.
1267
+ * @returns {string} Repository name, without owner.
1268
+ * @memberof UnderpostRepository
1269
+ */
1270
+ engineRepoFactory(reference = '', options = { test: false }) {
1271
+ const confId = Underpost.repo.confIdFactory(reference);
1272
+ if (!confId) return 'engine';
1273
+ return options?.test === true ? `engine-test-${confId}` : `engine-${confId}`;
1274
+ },
1275
+
1276
+ /**
1277
+ * Names the private configuration repository a reference's conf lives in.
1278
+ * @param {string} reference - Deploy id or repository reference.
1279
+ * @returns {string} Repository name, without owner.
1280
+ * @memberof UnderpostRepository
1281
+ */
1282
+ privateRepoFactory(reference = '') {
1283
+ const confId = Underpost.repo.confIdFactory(reference);
1284
+ return confId ? `engine-${confId}-private` : 'engine-private';
1285
+ },
1286
+
1287
+ /**
1288
+ * Names the canonical GitHub package repository a reference's engine mirrors into.
1289
+ *
1290
+ * The monorepo has none — `.github/workflows/ghpkg.ci.yml` builds `engine-ghpkg-<conf_id>`
1291
+ * per deploy, and the template lineage mirrors into `pwa-microservices-template-ghpkg`
1292
+ * instead — so an unnamed deploy resolves to `''` rather than an invented name.
1293
+ * @param {string} reference - Deploy id or repository reference.
1294
+ * @returns {string} Repository name without owner, or `''` for the monorepo.
1295
+ * @memberof UnderpostRepository
1296
+ */
1297
+ ghpkgRepoFactory(reference = '') {
1298
+ const confId = Underpost.repo.confIdFactory(reference);
1299
+ return confId ? `engine-ghpkg-${confId}` : '';
1300
+ },
1301
+
1302
+ /**
1303
+ * Every distinct repository a deploy's instances are built from, as declared by
1304
+ * `metadata.repository` in its `conf.instances.json`.
1305
+ *
1306
+ * An instance is a separate product with its own repository and its own workflows —
1307
+ * `dd-cyberia` builds from `cyberia-server` and `cyberia-client` — so anything acting on
1308
+ * "the repositories of a deploy" has to include them or it covers only half the deploy.
1309
+ * The path is left to {@link readConfInstances} rather than rebuilt here.
1310
+ * @param {string} deployId - Deploy id (e.g. `dd-cyberia`).
1311
+ * @returns {Array<string>} Deduplicated `owner/repo` slugs, empty when the deploy declares none.
1354
1312
  * @memberof UnderpostRepository
1355
1313
  */
1356
- resolveAuthUrl(url) {
1357
- if (!url) return url;
1358
- // Normalize short form "owner/repo" → full GitHub HTTPS URL
1359
- let normalized = url;
1360
- if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('git@')) {
1361
- normalized = `https://github.com/${url}`;
1314
+ instanceRepos(deployId = '') {
1315
+ const id = `${deployId || ''}`.trim();
1316
+ if (!id) return [];
1317
+ try {
1318
+ return [
1319
+ ...new Set(
1320
+ readConfInstances(id)
1321
+ .map((entry) => entry?.metadata?.repository)
1322
+ .filter(Boolean),
1323
+ ),
1324
+ ];
1325
+ } catch (error) {
1326
+ // A deploy that declares no instances has no file, which is not a fault; anything else is.
1327
+ if (error.code !== 'ENOENT')
1328
+ logger.warn(`[repo] unreadable conf.instances.json for ${id}`, { error: error.message });
1329
+ return [];
1362
1330
  }
1331
+ },
1332
+
1333
+ /**
1334
+ * Resolves the repository pair a node checks out: the engine source and the private
1335
+ * configuration that belongs to it.
1336
+ *
1337
+ * One source of that pairing, because two callers compose it — `run pull` on the node
1338
+ * itself and `edge --sync` composing the same pull for a fleet — and a node whose engine
1339
+ * and conf came from two different resolutions reads a conf that does not describe it.
1340
+ * The private repository is derived from the conf id the two share and takes the engine's
1341
+ * owner, so naming only the source can never split the pair: `underpostnet/engine-test-lampp`
1342
+ * pairs with `underpostnet/engine-lampp-private`, as does `underpostnet/engine-lampp`.
1343
+ * Naming `enginePrivate` overrides that derivation with its own `owner/repo` or clone URL.
1344
+ * @param {object} [options] - Pair options.
1345
+ * @param {string} [options.engine] - Engine source, as `owner/repo` or a clone URL.
1346
+ * @param {string} [options.enginePrivate] - Private configuration repository, overriding the derivation.
1347
+ * @param {string} [options.account] - Owner the monorepo defaults to; `GITHUB_USERNAME` otherwise.
1348
+ * @returns {{engine: string, enginePrivate: string}} Both slugs.
1349
+ * @memberof UnderpostRepository
1350
+ */
1351
+ enginePairFactory({ engine = '', enginePrivate = '', account = '' } = {}) {
1352
+ const owner = `${account || ''}`.trim() || process.env.GITHUB_USERNAME || 'underpostnet';
1353
+ const source = Underpost.repo.repoSlugFactory(`${engine || ''}`.trim() || `${owner}/engine`);
1354
+ const conf = `${enginePrivate || ''}`.trim();
1355
+ return {
1356
+ engine: source,
1357
+ enginePrivate: conf
1358
+ ? Underpost.repo.repoSlugFactory(conf)
1359
+ : `${source.split('/')[0]}/${Underpost.repo.privateRepoFactory(source)}`,
1360
+ };
1361
+ },
1362
+
1363
+ /**
1364
+ * Fast-forwards the engine and engine-private checkouts ahead of a template deploy.
1365
+ *
1366
+ * The deploy runners publish local commits, so they must not take the `run pull` route: that one
1367
+ * is for provisioning a node and force-replaces both trees at the remote tip, discarding the very
1368
+ * commits being published. `underpost pull` is `git pull --ff-only`, so a diverged checkout stops
1369
+ * the deploy instead of losing history.
1370
+ * @param {string} baseCommand - Resolved `underpost` CLI invocation.
1371
+ * @returns {void}
1372
+ */
1373
+ fastForwardEnginePair(baseCommand) {
1374
+ shellExec(`${baseCommand} pull . ${process.env.GITHUB_USERNAME}/engine`);
1375
+ shellExec(`${baseCommand} pull ./engine-private ${process.env.GITHUB_USERNAME}/engine-private`);
1376
+ },
1377
+
1378
+ /**
1379
+ * Keeps GitHub credentials in the child environment instead of command arguments or remotes.
1380
+ * @param {string} url - Repository URL or owner/repo reference.
1381
+ * @returns {{url: string, env: NodeJS.ProcessEnv}} Token-free URL and Git child environment.
1382
+ * @memberof UnderpostRepository
1383
+ */
1384
+ gitAuthFactory(url) {
1385
+ const normalized = Underpost.repo.repoUrlFactory(url).replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/i, '$1');
1386
+ const env = {
1387
+ ...process.env,
1388
+ GIT_TERMINAL_PROMPT: '0',
1389
+ GIT_CONFIG_COUNT: '1',
1390
+ GIT_CONFIG_KEY_0: 'credential.helper',
1391
+ GIT_CONFIG_VALUE_0: '',
1392
+ };
1363
1393
  if (process.env.GITHUB_TOKEN && normalized.startsWith('https://github.com/')) {
1364
- return normalized.replace(
1365
- 'https://github.com/',
1366
- `https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/`,
1367
- );
1394
+ env.GIT_CONFIG_COUNT = '2';
1395
+ env.GIT_CONFIG_KEY_1 = 'http.https://github.com/.extraheader';
1396
+ env.GIT_CONFIG_VALUE_1 = `AUTHORIZATION: basic ${Buffer.from(
1397
+ `x-access-token:${process.env.GITHUB_TOKEN}`,
1398
+ ).toString('base64')}`;
1368
1399
  }
1369
- return normalized;
1400
+ return { url: normalized, env };
1370
1401
  },
1371
1402
  /**
1372
1403
  * Checks whether a remote Git repository URL is reachable.
1373
1404
  * Uses `silentOnError` so a non-reachable remote returns false instead of throwing.
1374
- * Injects `GITHUB_TOKEN` into GitHub HTTPS URLs when available.
1405
+ * Passes `GITHUB_TOKEN` to Git through its child environment when available.
1375
1406
  * @param {string} url - Full HTTPS clone URL to test (e.g. "https://github.com/org/repo.git").
1376
1407
  * @returns {boolean} `true` when the remote responded with at least one ref hash.
1377
1408
  * @memberof UnderpostRepository
1378
1409
  */
1379
1410
  isRemoteRepo(url) {
1380
1411
  if (!url) return false;
1381
- const authUrl = Underpost.repo.resolveAuthUrl(url);
1382
- // GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
1383
- // `-c credential.helper=` disables any host-configured credential helper so its stderr
1384
- // warnings don't pollute the ls-remote output; the token is already embedded in authUrl.
1385
- const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote "${authUrl}" HEAD 2>&1`, {
1412
+ const auth = Underpost.repo.gitAuthFactory(url);
1413
+ const raw = shellExec(`git ls-remote "${auth.url}" HEAD 2>&1`, {
1386
1414
  stdout: true,
1387
1415
  silent: true,
1388
1416
  disableLog: true,
1389
1417
  silentOnError: true,
1418
+ env: auth.env,
1390
1419
  });
1391
1420
  const refLine = typeof raw === 'string' ? (raw.match(/^[0-9a-f]{40}\t.*$/m) || [])[0] : undefined;
1392
1421
  const accessible = !!refLine;
@@ -1419,7 +1448,7 @@ Prevent build private config repo.`,
1419
1448
  * Sequence (idempotent, re-runnable):
1420
1449
  * 1. Normalize the URL (`owner/repo` → full GitHub HTTPS) and set/add the
1421
1450
  * remote, storing the token-free URL so no secret leaks into `.git/config`.
1422
- * 2. Force-fetch the target branch (auth injected inline for private repos).
1451
+ * 2. Force-fetch the target branch with credentials isolated in the child environment.
1423
1452
  * 3. Reset the working tree to the fetched tip and check out the target
1424
1453
  * branch, overwriting any current tracked content.
1425
1454
  *
@@ -1428,29 +1457,65 @@ Prevent build private config repo.`,
1428
1457
  * @param {object} opts
1429
1458
  * @param {string} opts.url - New remote URL (full URL or "owner/repo" short form).
1430
1459
  * @param {string} [opts.path='.'] - Path to the git repository.
1431
- * @param {string} [opts.branch='master'] - Target branch to overwrite the current tree with.
1460
+ * @param {string} [opts.branch] - Target branch to overwrite the current tree with. Defaults to the remote's default branch.
1432
1461
  * @param {string} [opts.remote='origin'] - Remote name to set and fetch from.
1433
1462
  * @returns {void}
1434
1463
  * @memberof UnderpostRepository
1435
1464
  */
1436
- switchRemote({ url, path: repoPath = '.', branch = 'master', remote = 'origin' }) {
1465
+ switchRemote({ url, path: repoPath = '.', branch = '', remote = 'origin' }) {
1437
1466
  if (!url) throw new Error('switchRemote requires a target remote url');
1438
1467
  if (!fs.existsSync(`${repoPath}/.git`)) throw new Error(`switchRemote: not a git repository: ${repoPath}`);
1439
- // Token-free URL for the stored remote; auth-injected URL only for the fetch.
1440
- let normalized = url;
1441
- if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('git@')) {
1442
- normalized = `https://github.com/${url}`;
1443
- }
1444
- const authUrl = Underpost.repo.resolveAuthUrl(url);
1468
+ const targetBranch = branch || Underpost.repo.getDefaultBranch(url);
1469
+ const auth = Underpost.repo.gitAuthFactory(url);
1445
1470
  const current = Underpost.repo.getRemoteUrl({ path: repoPath, remote });
1446
- if (!current) shellExec(`cd "${repoPath}" && git remote add ${remote} "${normalized}"`);
1447
- else shellExec(`cd "${repoPath}" && git remote set-url ${remote} "${normalized}"`);
1448
- logger.info('switchRemote', { path: repoPath, remote, branch, url: normalized });
1449
- shellExec(`cd "${repoPath}" && GIT_TERMINAL_PROMPT=0 git fetch --force "${authUrl}" ${branch}`);
1471
+ if (!current) shellExec(`cd "${repoPath}" && git remote add ${remote} "${auth.url}"`);
1472
+ else shellExec(`cd "${repoPath}" && git remote set-url ${remote} "${auth.url}"`);
1473
+ logger.info('switchRemote', { path: repoPath, remote, branch: targetBranch, url: auth.url });
1474
+ shellExec(`cd "${repoPath}" && git fetch --force "${auth.url}" ${targetBranch}`, { env: auth.env });
1450
1475
  // reset --hard first clears the worktree so the checkout cannot be blocked
1451
1476
  // by conflicting local changes; -B points the target branch at the fetched tip.
1452
1477
  shellExec(`cd "${repoPath}" && git reset --hard FETCH_HEAD`);
1453
- shellExec(`cd "${repoPath}" && git checkout -B ${branch} FETCH_HEAD`);
1478
+ shellExec(`cd "${repoPath}" && git checkout -B ${targetBranch} FETCH_HEAD`);
1479
+ },
1480
+
1481
+ /**
1482
+ * Brings a local checkout to the requested repository at its tip, whatever state it is in.
1483
+ *
1484
+ * Clones when the path is absent, and otherwise replaces the checkout through
1485
+ * {@link UnderpostRepository.switchRemote} — the same operation `underpost cmt --switch-repo`
1486
+ * runs, so a node reached by `run pull` and a node reached by `edge --sync` end on the same
1487
+ * commit by the same route.
1488
+ *
1489
+ * One operation covers both the retarget and the same-repository case, because they only
1490
+ * look different: an `--ff-only` pull refuses a checkout that has drifted onto commits of
1491
+ * its own, which is exactly the node that most needs bringing back. A deploy checkout is a
1492
+ * projection of its remote, never a place work is authored, so it is replaced rather than
1493
+ * reconciled.
1494
+ *
1495
+ * The clone lands under the repository's own name and is renamed into place, because the
1496
+ * checkout path is fixed by the deployment layout while the repository name is not.
1497
+ *
1498
+ * @param {object} opts
1499
+ * @param {string} opts.path - Absolute checkout path.
1500
+ * @param {string} opts.repo - Repository `owner/repo` slug or clone URL.
1501
+ * @param {string} [opts.branch] - Branch to land on; the remote's default branch otherwise.
1502
+ * @returns {'cloned'|'switched'} What the call had to do.
1503
+ * @memberof UnderpostRepository
1504
+ */
1505
+ syncCheckout({ path: checkoutPath, repo, branch = '' }) {
1506
+ const slug = Underpost.repo.repoSlugFactory(repo);
1507
+ const repoName = slug.split('/')[1];
1508
+ const parent = path.dirname(checkoutPath);
1509
+
1510
+ if (!fs.existsSync(checkoutPath)) {
1511
+ fs.mkdirSync(parent, { recursive: true });
1512
+ shellExec(`cd ${parent} && underpost clone ${slug}`, { silent: true });
1513
+ if (`${parent}/${repoName}` !== checkoutPath) shellExec(`sudo mv ${parent}/${repoName} ${checkoutPath}`);
1514
+ return 'cloned';
1515
+ }
1516
+
1517
+ Underpost.repo.switchRemote({ url: slug, path: checkoutPath, branch });
1518
+ return 'switched';
1454
1519
  },
1455
1520
 
1456
1521
  /**
@@ -1517,6 +1582,41 @@ Prevent build private config repo.`,
1517
1582
  // callers fall back to their own generic default.
1518
1583
  return sanitized === 'No changelog entries found.' ? '' : sanitized;
1519
1584
  },
1585
+ /**
1586
+ * Resolves the changelog payload a workflow received from the previous link of the
1587
+ * propagation chain.
1588
+ *
1589
+ * Every hop commits the payload with `cmt`, which wraps it as
1590
+ * `type(scope): <emoji> <first entry>` — the newest entry rides on the subject line, so a
1591
+ * downstream hop that drops the whole subject line loses it. This strips only the wrapper
1592
+ * and keeps every entry, on its own line, in order.
1593
+ *
1594
+ * @param {string} rawMessage - The message as received (a commit message or a dispatch input).
1595
+ * @returns {string} The propagated entries, or an empty string when there is nothing to carry.
1596
+ * @memberof UnderpostRepository
1597
+ */
1598
+ resolvePropagationMessage(rawMessage) {
1599
+ if (!rawMessage) return '';
1600
+ const types = Object.keys(commitData).join('|');
1601
+ const emojis = Object.values(commitData)
1602
+ .map(({ emoji }) => emoji)
1603
+ .filter(Boolean);
1604
+ const lines = `${rawMessage}`
1605
+ .replace(new RegExp(`^(${types})(\\([^)]*\\))?:\\s*`), '')
1606
+ .split('\n')
1607
+ .map((line) => line.trim())
1608
+ .filter(Boolean);
1609
+ if (lines[0]) {
1610
+ for (const emoji of emojis)
1611
+ if (lines[0].startsWith(emoji)) {
1612
+ lines[0] = lines[0].slice(emoji.length).trim();
1613
+ break;
1614
+ }
1615
+ if (!lines[0]) lines.shift();
1616
+ }
1617
+ return lines.join('\n');
1618
+ },
1619
+
1520
1620
  /**
1521
1621
  * Initializes a git repository at the given path and configures user identity
1522
1622
  * from environment variables (`GITHUB_USERNAME` / `GITHUB_EMAIL`).
@@ -1528,9 +1628,13 @@ Prevent build private config repo.`,
1528
1628
  * @memberof UnderpostRepository
1529
1629
  */
1530
1630
  initLocalRepo({ path: repoPath, origin }) {
1531
- const gitUsername = process.env.GITHUB_USERNAME || 'underpostnet';
1631
+ const gitUsername = repositoryIdentityFactory().owner;
1532
1632
  const gitEmail = process.env.GITHUB_EMAIL || `development@underpost.net`;
1533
1633
 
1634
+ // Runtime document roots are chowned to the web server user, and git refuses to operate on
1635
+ // a tree owned by someone else until it is declared safe.
1636
+ Underpost.repo.declareSafeDirectory(repoPath);
1637
+
1534
1638
  if (!fs.existsSync(`${repoPath}/.git`)) {
1535
1639
  shellExec(`mkdir -p "${repoPath}" && git init "${repoPath}"`);
1536
1640
  }
@@ -1596,6 +1700,18 @@ Prevent build private config repo.`,
1596
1700
  case 'commit':
1597
1701
  if (fs.existsSync(repoPath)) {
1598
1702
  shellExec(`cd ${repoPath} && git add .`);
1703
+ // A backup run that produced no new bytes is a normal outcome, not a
1704
+ // failure: committing anyway exits non-zero and buries real errors.
1705
+ const pending = shellExec(`cd ${repoPath} && git status --porcelain`, {
1706
+ stdout: true,
1707
+ silent: true,
1708
+ disableLog: true,
1709
+ silentOnError: true,
1710
+ });
1711
+ if (!`${pending || ''}`.trim()) {
1712
+ logger.info(`No changes to commit: ${repoName}`, { message });
1713
+ break;
1714
+ }
1599
1715
  shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
1600
1716
  logger.info(`Committed to repository: ${repoName}`, { message });
1601
1717
  }
@@ -1620,6 +1736,91 @@ Prevent build private config repo.`,
1620
1736
  }
1621
1737
  },
1622
1738
 
1739
+ /**
1740
+ * Declares a path safe for git, skipping the write when it is already declared so repeated
1741
+ * provisioning does not append duplicate global config entries.
1742
+ * @param {string} repoPath - Absolute path to declare.
1743
+ * @returns {void}
1744
+ * @memberof UnderpostRepository
1745
+ */
1746
+ declareSafeDirectory(repoPath) {
1747
+ const declared = shellExec(`git config --global --get-all safe.directory`, {
1748
+ stdout: true,
1749
+ silent: true,
1750
+ disableLog: true,
1751
+ silentOnError: true,
1752
+ });
1753
+ if (`${declared ?? ''}`.split('\n').some((entry) => entry.trim() === repoPath)) return;
1754
+ shellExec(`git config --global --add safe.directory "${repoPath}"`, { silent: true });
1755
+ },
1756
+
1757
+ /**
1758
+ * Materializes a runtime's document root from the repository its conf route declares.
1759
+ *
1760
+ * Shared by every repository-backed runtime — {@link WpService} for WordPress site roots and
1761
+ * {@link LamppService} for static Apache document roots — so one deployment can serve many
1762
+ * repositories on the same host through a single provisioning contract.
1763
+ *
1764
+ * Idempotent: an existing directory is left alone, and the clone lands on a temporary path
1765
+ * first so an interrupted run never leaves a partial tree at the document root.
1766
+ *
1767
+ * @param {object} opts
1768
+ * @param {string} opts.host - Virtual-host name, for logging.
1769
+ * @param {string} opts.siteRoot - Absolute path the checkout must occupy.
1770
+ * @param {string} opts.repository - Repository URL or `owner/repo` short form.
1771
+ * @param {string} [opts.owner='daemon:daemon'] - Filesystem owner of the served tree.
1772
+ * @returns {{accessible: boolean, cloned: boolean}} Whether the remote answered, and whether
1773
+ * this call created the checkout.
1774
+ * @memberof UnderpostRepository
1775
+ */
1776
+ provisionSiteRoot({ host, siteRoot, repository, owner = 'daemon:daemon' }) {
1777
+ // An `env:` reference to a variable the process never received resolves to an empty string,
1778
+ // so a conf fault and an unreachable remote arrive here as the same falsy value. Reported
1779
+ // together they read as a GitHub problem and send the search to the wrong place.
1780
+ const reference = `${repository ?? ''}`.trim();
1781
+ if (!reference) {
1782
+ logger.error(
1783
+ `${host}: no repository resolved for ${siteRoot} — the conf route's reference is empty, ` +
1784
+ `so its 'env:' variable is missing from this process's environment`,
1785
+ );
1786
+ return { accessible: false, cloned: false };
1787
+ }
1788
+
1789
+ if (!process.env.GITHUB_TOKEN && Underpost.repo.repoUrlFactory(reference).startsWith('https://github.com/'))
1790
+ logger.warn(`${host}: GITHUB_TOKEN not set — git operations will fail for private repositories`);
1791
+
1792
+ if (!Underpost.repo.isRemoteRepo(reference)) {
1793
+ logger.warn(`${host}: remote repository not accessible (${reference})`);
1794
+ return { accessible: false, cloned: false };
1795
+ }
1796
+ // Provisioned means a checkout is there, not merely that the path is. The runtime image
1797
+ // ships the document-root directories, so an existence test calls every fresh pod already
1798
+ // provisioned, skips the clone, and leaves Apache serving 403 from a directory nothing
1799
+ // populates. `.git` is the only mark that the declared repository actually landed here.
1800
+ if (fs.existsSync(`${siteRoot}/.git`)) {
1801
+ logger.info(`${host}: repo already present at ${siteRoot}`);
1802
+ return { accessible: true, cloned: false };
1803
+ }
1804
+ const populateInPlace = fs.existsSync(siteRoot);
1805
+ if (populateInPlace) logger.warn(`${host}: ${siteRoot} holds no checkout; provisioning ${reference} into it`);
1806
+
1807
+ logger.info(`${host}: cloning ${reference} → ${siteRoot}`);
1808
+ const tmp = `${siteRoot}.tmp`;
1809
+ if (fs.existsSync(tmp)) shellExec(`sudo rm -rf "${tmp}"`);
1810
+ fs.mkdirSync(path.dirname(siteRoot), { recursive: true });
1811
+ const auth = Underpost.repo.gitAuthFactory(reference);
1812
+ shellExec(`git clone "${auth.url}" "${tmp}"`, { env: auth.env });
1813
+ // Copied into a directory that already exists rather than renamed over it: that directory
1814
+ // can be image-provided or a mount point, and a rename onto one fails where a copy works.
1815
+ if (populateInPlace) {
1816
+ shellExec(`sudo cp -a "${tmp}/." "${siteRoot}/"`);
1817
+ shellExec(`sudo rm -rf "${tmp}"`);
1818
+ } else shellExec(`sudo mv "${tmp}" "${siteRoot}"`);
1819
+ shellExec(`sudo chmod -R 755 "${siteRoot}"`);
1820
+ shellExec(`sudo chown -R ${owner} "${siteRoot}"`);
1821
+ return { accessible: true, cloned: true };
1822
+ },
1823
+
1623
1824
  /**
1624
1825
  * Resolves the in-pod site-root directory where a conf route's repository lives.
1625
1826
  *
@@ -1699,7 +1900,7 @@ Prevent build private config repo.`,
1699
1900
  backupPodRepositories({ deployId, namespace = 'default', env = 'production' }) {
1700
1901
  const confServer = readConfJson(deployId, 'server', { resolve: true });
1701
1902
  const githubToken = process.env.GITHUB_TOKEN || '';
1702
- const githubUsername = process.env.GITHUB_USERNAME || 'underpostnet';
1903
+ const githubUsername = repositoryIdentityFactory().owner;
1703
1904
 
1704
1905
  if (!githubToken) {
1705
1906
  logger.warn('backupPodRepositories: GITHUB_TOKEN not available — git push will fail');
@@ -1758,7 +1959,7 @@ Prevent build private config repo.`,
1758
1959
  `git config --global --add safe.directory '${siteRoot}' 2>/dev/null || true`,
1759
1960
  `cd '${siteRoot}' && git add -A && git commit -m 'backup $(date -u +%Y-%m-%dT%H:%M:%SZ)' || true`,
1760
1961
  `cd '${siteRoot}' && underpost push . ${githubUsername}/${repoName}`,
1761
- `cd /home/dd/engine && node bin secret underpost --global-clean`,
1962
+ `cd /home/dd/engine && node bin host clean --force`,
1762
1963
  ].join(' && ');
1763
1964
 
1764
1965
  try {
@@ -1803,7 +2004,7 @@ Prevent build private config repo.`,
1803
2004
  throw new Error('privateEngineRepoFactory: GITHUB_USERNAME not set');
1804
2005
  }
1805
2006
 
1806
- const repoName = effectiveDeployId ? `engine-${effectiveDeployId.split('-')[1]}-private` : 'engine-private';
2007
+ const repoName = Underpost.repo.privateRepoFactory(effectiveDeployId);
1807
2008
  logger.info(`engine-private missing — cloning ${username}/${repoName}`);
1808
2009
  shellExec(`underpost clone ${username}/${repoName}`);
1809
2010
  if (!fs.existsSync(`./${repoName}`)) {
@@ -1830,11 +2031,11 @@ Prevent build private config repo.`,
1830
2031
 
1831
2032
  /**
1832
2033
  * Resolves the GitHub repository for a given instance runtime by scanning
1833
- * every `conf.instances.json` listed in `./engine-private/deploy/dd.router`.
2034
+ * every `conf.instances.json` listed in `./engine-private/deploy/dd.routes`.
1834
2035
  *
1835
2036
  * Resolution order:
1836
2037
  * 1. If `runtime` is falsy, returns `${GITHUB_USERNAME}/engine`.
1837
- * 2. Iterates each deploy ID found in `dd.router` and looks for an instance
2038
+ * 2. Iterates each deploy ID found in `dd.routes` and looks for an instance
1838
2039
  * whose `runtime` field matches the supplied value.
1839
2040
  * 3. When a match is found, returns `instance.metadata.repository`.
1840
2041
  * 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
@@ -1851,14 +2052,7 @@ Prevent build private config repo.`,
1851
2052
  // (docker-image.<runtime>.dev.ci.yml) but resolves to the same instance
1852
2053
  // repo as its production counterpart, so strip it before matching.
1853
2054
  runtime = runtime.replace(/\.dev$/, '');
1854
- const ddRouter = './engine-private/deploy/dd.router';
1855
- const deployIds = fs.existsSync(ddRouter)
1856
- ? fs
1857
- .readFileSync(ddRouter, 'utf8')
1858
- .split(',')
1859
- .map((s) => s.trim())
1860
- .filter(Boolean)
1861
- : [];
2055
+ const deployIds = readDeployRoutes();
1862
2056
  for (const deployId of deployIds) {
1863
2057
  const confPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1864
2058
  if (!fs.existsSync(confPath)) continue;
@@ -1926,8 +2120,11 @@ Prevent build private config repo.`,
1926
2120
  logger.info('[sparseCheckoutDirectory] path already present, skipping', localPath);
1927
2121
  return false;
1928
2122
  }
1929
- const authUrl = `https://${process.env.GITHUB_TOKEN}@github.com/${repoOwner}/${repoName}.git`;
1930
- shellExec(`git clone --depth 1 --no-checkout ${authUrl} ${targetDir}`, { disableLog: true });
2123
+ const auth = Underpost.repo.gitAuthFactory(`https://github.com/${repoOwner}/${repoName}.git`);
2124
+ shellExec(`git clone --depth 1 --no-checkout "${auth.url}" ${targetDir}`, {
2125
+ disableLog: true,
2126
+ env: auth.env,
2127
+ });
1931
2128
  shellExec(`cd ${targetDir} && git sparse-checkout set ${subPath} && git checkout`, { disableLog: true });
1932
2129
  logger.info('[sparseCheckoutDirectory] sparse checkout complete', localPath);
1933
2130
  return true;