@underpostnet/cyberia 3.3.0 → 3.3.76

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