cyberia 3.2.9 → 3.2.22

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 (184) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +7 -0
  2. package/.github/workflows/engine-cyberia.ci.yml +14 -2
  3. package/.github/workflows/ghpkg.ci.yml +1 -0
  4. package/.github/workflows/npmpkg.ci.yml +10 -5
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  6. package/.github/workflows/release.cd.yml +1 -0
  7. package/.vscode/extensions.json +9 -9
  8. package/.vscode/settings.json +20 -4
  9. package/CHANGELOG.md +363 -1
  10. package/CLI-HELP.md +975 -1061
  11. package/README.md +190 -348
  12. package/bin/build.js +102 -125
  13. package/bin/build.template.js +33 -0
  14. package/bin/cyberia.js +238 -56
  15. package/bin/deploy.js +16 -3
  16. package/bin/index.js +238 -56
  17. package/bump.config.js +26 -0
  18. package/conf.js +131 -24
  19. package/deployment.yaml +76 -2
  20. package/hardhat/package-lock.json +113 -144
  21. package/hardhat/package.json +4 -3
  22. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  23. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  24. package/manifests/deployment/dd-cyberia-development/deployment.yaml +76 -2
  25. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  26. package/manifests/kind-config-dev.yaml +8 -0
  27. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  28. package/manifests/mongodb/pv-pvc.yaml +44 -8
  29. package/manifests/mongodb/statefulset.yaml +55 -68
  30. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  31. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  32. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  33. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  34. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  35. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  36. package/manifests/valkey/statefulset.yaml +1 -1
  37. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  38. package/package.json +31 -19
  39. package/scripts/ipxe-setup.sh +52 -49
  40. package/scripts/k3s-node-setup.sh +81 -46
  41. package/scripts/link-local-underpost-cli.sh +6 -0
  42. package/scripts/lxd-vm-setup.sh +193 -8
  43. package/scripts/maas-nat-firewalld.sh +145 -0
  44. package/scripts/test-monitor.sh +250 -0
  45. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  46. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  47. package/src/api/core/core.router.js +19 -14
  48. package/src/api/core/core.service.js +5 -5
  49. package/src/api/crypto/crypto.router.js +18 -12
  50. package/src/api/crypto/crypto.service.js +3 -3
  51. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  52. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  53. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  55. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  56. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  57. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  58. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  59. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  60. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  61. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  62. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  63. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  64. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  65. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  66. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  67. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  68. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  69. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  70. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  71. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  72. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  73. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  74. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  75. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  76. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +458 -0
  77. package/src/api/default/default.router.js +22 -18
  78. package/src/api/default/default.service.js +5 -5
  79. package/src/api/document/document.router.js +28 -23
  80. package/src/api/document/document.service.js +100 -23
  81. package/src/api/file/file.router.js +19 -13
  82. package/src/api/file/file.service.js +9 -7
  83. package/src/api/instance/instance.router.js +29 -24
  84. package/src/api/instance/instance.service.js +6 -6
  85. package/src/api/ipfs/ipfs.router.js +21 -16
  86. package/src/api/ipfs/ipfs.service.js +8 -8
  87. package/src/api/object-layer/object-layer.router.js +512 -507
  88. package/src/api/object-layer/object-layer.service.js +17 -14
  89. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  90. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  91. package/src/api/test/test.router.js +17 -12
  92. package/src/api/types.js +24 -0
  93. package/src/api/user/guest.service.js +5 -4
  94. package/src/api/user/user.router.js +297 -288
  95. package/src/api/user/user.service.js +100 -35
  96. package/src/cli/baremetal.js +132 -101
  97. package/src/cli/cluster.js +700 -232
  98. package/src/cli/db.js +59 -60
  99. package/src/cli/deploy.js +291 -294
  100. package/src/cli/env.js +1 -4
  101. package/src/cli/fs.js +13 -3
  102. package/src/cli/image.js +58 -4
  103. package/src/cli/index.js +127 -15
  104. package/src/cli/ipfs.js +4 -6
  105. package/src/cli/kubectl.js +4 -1
  106. package/src/cli/lxd.js +1099 -223
  107. package/src/cli/monitor.js +396 -9
  108. package/src/cli/release.js +355 -146
  109. package/src/cli/repository.js +169 -30
  110. package/src/cli/run.js +347 -117
  111. package/src/cli/secrets.js +11 -2
  112. package/src/cli/test.js +9 -3
  113. package/src/client/Default.index.js +9 -3
  114. package/src/client/components/core/Auth.js +5 -0
  115. package/src/client/components/core/ClientEvents.js +76 -0
  116. package/src/client/components/core/EventBus.js +4 -0
  117. package/src/client/components/core/Modal.js +82 -41
  118. package/src/client/components/core/PanelForm.js +14 -10
  119. package/src/client/components/core/Worker.js +162 -363
  120. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  121. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  122. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
  123. package/src/client/public/cyberia-docs/ARCHITECTURE.md +223 -361
  124. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  125. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  126. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +212 -185
  127. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  128. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  129. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
  130. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  131. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  132. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  133. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  134. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  135. package/src/client/sw/core.sw.js +174 -112
  136. package/src/db/DataBaseProvider.js +115 -15
  137. package/src/db/mariadb/MariaDB.js +2 -1
  138. package/src/db/mongo/MongoBootstrap.js +657 -0
  139. package/src/db/mongo/MongooseDB.js +130 -21
  140. package/src/grpc/cyberia/grpc-server.js +25 -57
  141. package/src/index.js +1 -1
  142. package/src/runtime/cyberia-client/Dockerfile +10 -7
  143. package/src/runtime/cyberia-client/Dockerfile.dev +67 -0
  144. package/src/runtime/cyberia-server/Dockerfile +11 -6
  145. package/src/runtime/cyberia-server/Dockerfile.dev +47 -0
  146. package/src/runtime/express/Express.js +2 -2
  147. package/src/runtime/wp/Dockerfile +3 -3
  148. package/src/runtime/wp/Wp.js +8 -5
  149. package/src/server/auth.js +2 -2
  150. package/src/server/catalog-underpost.js +61 -0
  151. package/src/server/catalog.js +77 -0
  152. package/src/server/client-build-docs.js +1 -1
  153. package/src/server/client-build.js +94 -129
  154. package/src/server/conf.js +496 -135
  155. package/src/server/ipfs-client.js +5 -3
  156. package/src/server/process.js +180 -19
  157. package/src/server/proxy.js +9 -2
  158. package/src/server/runtime-status.js +235 -0
  159. package/src/server/runtime.js +1 -1
  160. package/src/server/start.js +44 -11
  161. package/src/server/valkey.js +2 -0
  162. package/src/ws/IoInterface.js +16 -16
  163. package/src/ws/core/channels/core.ws.chat.js +11 -11
  164. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  165. package/src/ws/core/channels/core.ws.stream.js +19 -19
  166. package/src/ws/core/core.ws.connection.js +8 -8
  167. package/src/ws/core/core.ws.server.js +6 -5
  168. package/src/ws/default/channels/default.ws.main.js +10 -10
  169. package/src/ws/default/default.ws.connection.js +4 -4
  170. package/src/ws/default/default.ws.server.js +4 -3
  171. package/test/deploy-monitor.test.js +251 -0
  172. package/bin/file.js +0 -202
  173. package/bin/vs.js +0 -74
  174. package/bin/zed.js +0 -84
  175. package/manifests/deployment/dd-test-development/deployment.yaml +0 -254
  176. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  177. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  178. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  179. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  180. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  181. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  182. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  183. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  184. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/run.js CHANGED
@@ -10,9 +10,12 @@ import {
10
10
  awaitDeployMonitor,
11
11
  buildKindPorts,
12
12
  Config,
13
+ cronDeployIdResolve,
14
+ etcHostFactory,
13
15
  getNpmRootPath,
14
16
  isDeployRunnerContext,
15
17
  loadConfServerJson,
18
+ loadReplicas,
16
19
  writeEnv,
17
20
  } from '../server/conf.js';
18
21
  import { actionInitLog, loggerFactory } from '../server/logger.js';
@@ -24,6 +27,7 @@ import { range, setPad, timer } from '../client/components/core/CommonJs.js';
24
27
  import os from 'os';
25
28
  import Underpost from '../index.js';
26
29
  import dotenv from 'dotenv';
30
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
27
31
 
28
32
  const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
29
33
  new Promise((resolve, reject) => {
@@ -97,6 +101,7 @@ const logger = loggerFactory(import.meta);
97
101
  * @property {string} deployId - The deployment ID.
98
102
  * @property {string} instanceId - The instance ID.
99
103
  * @property {string} user - The user to run as.
104
+ * @property {string} group - The group to use.
100
105
  * @property {string} pid - The process ID.
101
106
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
102
107
  * @property {string} monitorStatus - The monitor status option.
@@ -114,6 +119,8 @@ const logger = loggerFactory(import.meta);
114
119
  * @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
115
120
  * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
116
121
  * @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
122
+ * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
123
+ * @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
117
124
  * @memberof UnderpostRun
118
125
  */
119
126
  const DEFAULT_OPTION = {
@@ -165,6 +172,7 @@ const DEFAULT_OPTION = {
165
172
  deployId: '',
166
173
  instanceId: '',
167
174
  user: '',
175
+ group: '',
168
176
  pid: '',
169
177
  disablePrivateConfUpdate: false,
170
178
  monitorStatus: '',
@@ -180,6 +188,8 @@ const DEFAULT_OPTION = {
180
188
  copy: false,
181
189
  skipFullBuild: false,
182
190
  pullBundle: false,
191
+ remove: false,
192
+ test: false,
183
193
  };
184
194
 
185
195
  /**
@@ -209,28 +219,39 @@ class UnderpostRun {
209
219
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
210
220
  const baseCommand = options.dev ? 'node bin' : 'underpost';
211
221
  const mongoHosts = ['mongodb-0.mongodb-service'];
222
+ let primaryMongoHost = 'mongodb-0.mongodb-service';
212
223
  if (!options.expose) {
213
224
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --reset`);
214
225
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
215
226
 
216
227
  shellExec(
217
- `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --mongo-db-host ${mongoHosts.join(
228
+ `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
218
229
  ',',
219
230
  )} --pull-image`,
220
231
  );
221
232
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --valkey --pull-image`);
222
233
  }
223
-
224
- {
225
- // Detect MongoDB primary pod using method
226
- let primaryMongoHost = 'mongodb-0.mongodb-service';
234
+ if (options.k3s) {
235
+ if (options.remove) {
236
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:27017`);
237
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:6379`);
238
+ } else {
239
+ shellExec(`${baseCommand} lxd --expose k3s-control:27017 --node-port 32017`);
240
+ shellExec(`${baseCommand} lxd --expose k3s-control:6379 --node-port 32079`);
241
+ }
242
+ shellExec(`lxc config device show k3s-control`);
243
+ } else {
227
244
  try {
228
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({
229
- namespace: options.namespace,
230
- podName: 'mongodb-0',
231
- });
232
- // shellExec(`${baseCommand} deploy --expose --disable-update-underpost-config mongo`, { async: true });
233
- shellExec(`kubectl port-forward -n ${options.namespace} pod/${primaryPodName} 27017:27017`, { async: true });
245
+ const primaryPodName =
246
+ MongoBootstrap.getPrimaryPodName({
247
+ namespace: options.namespace,
248
+ podName: 'mongodb-0',
249
+ disableAuth: options.dev,
250
+ }) || 'mongodb-0';
251
+ shellExec(
252
+ `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config mongo`,
253
+ { async: true },
254
+ );
234
255
  shellExec(
235
256
  `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config valkey`,
236
257
  { async: true },
@@ -241,10 +262,19 @@ class UnderpostRun {
241
262
  default: primaryMongoHost,
242
263
  });
243
264
  }
244
-
245
- const hostListenResult = Underpost.deploy.etcHostFactory([primaryMongoHost]);
246
- logger.info(hostListenResult.renderHosts);
247
265
  }
266
+ const hostListenResult = etcHostFactory([primaryMongoHost]);
267
+ logger.info(hostListenResult.renderHosts);
268
+ },
269
+
270
+ /**
271
+ * @method etc-hosts
272
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
273
+ * based on the provided path input.
274
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
275
+ */
276
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
277
+ etcHostFactory(path.split(','));
248
278
  },
249
279
 
250
280
  /**
@@ -405,6 +435,7 @@ class UnderpostRun {
405
435
  return;
406
436
  }
407
437
  shellExec(`${baseCommand} run pull`);
438
+ shellExec(`${baseCommand} run shared-dir`);
408
439
 
409
440
  // Capture last N commit messages for propagation.
410
441
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -485,6 +516,7 @@ class UnderpostRun {
485
516
  return;
486
517
  }
487
518
  shellExec(`${baseCommand} run pull`);
519
+ shellExec(`${baseCommand} run shared-dir`);
488
520
 
489
521
  // Capture last N commit messages from the engine repo.
490
522
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -505,14 +537,16 @@ class UnderpostRun {
505
537
  },
506
538
  /**
507
539
  * @method docker-image
508
- * @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
509
- * @param {string} path - The input value, identifier, or path for the operation.
540
+ * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
541
+ * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
542
+ * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
510
543
  * @param {Object} options - The default underpost runner options for customizing workflow
511
544
  * @memberof UnderpostRun
512
545
  */
513
546
  'docker-image': (path, options = DEFAULT_OPTION) => {
547
+ const repo = Underpost.repo.resolveInstanceRepo(path);
514
548
  Underpost.repo.dispatchWorkflow({
515
- repo: `${process.env.GITHUB_USERNAME}/engine`,
549
+ repo,
516
550
  workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
517
551
  ref: 'master',
518
552
  inputs: {},
@@ -527,6 +561,7 @@ class UnderpostRun {
527
561
  */
528
562
  clean: (path = '', options = DEFAULT_OPTION) => {
529
563
  Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
564
+ if (options.dev) shellExec(`node bin run shared-dir ${path ? path : '/home/dd/engine'}`);
530
565
  },
531
566
  /**
532
567
  * @method pull
@@ -536,16 +571,14 @@ class UnderpostRun {
536
571
  * @memberof UnderpostRun
537
572
  */
538
573
  pull: (path, options = DEFAULT_OPTION) => {
574
+ // shellExec is fail-fast by default — any non-zero exit throws and
575
+ // propagates up to the workflow step. No per-call flag required.
539
576
  if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
540
577
  fs.mkdirSync(`/home/dd`, { recursive: true });
541
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, {
542
- silent: true,
543
- });
578
+ shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
544
579
  } else {
545
580
  shellExec(`underpost run clean`);
546
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, {
547
- silent: true,
548
- });
581
+ shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
549
582
  }
550
583
  if (!fs.existsSync(`/home/dd/engine/engine-private`))
551
584
  shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
@@ -554,9 +587,7 @@ class UnderpostRun {
554
587
  else
555
588
  shellExec(
556
589
  `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
557
- {
558
- silent: true,
559
- },
590
+ { silent: true },
560
591
  );
561
592
  },
562
593
  /**
@@ -643,6 +674,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
643
674
  /**
644
675
  * @method sync
645
676
  * @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`) using parameters parsed from `path` (deployId, replicas, versions, image, node).
677
+ *
678
+ * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
679
+ * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
680
+ * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
681
+ * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
646
682
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
647
683
  * @param {Object} options - The default underpost runner options for customizing workflow
648
684
  * @memberof UnderpostRun
@@ -692,6 +728,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
692
728
  let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
693
729
  if (targetTraffic) versions = versions ? versions : targetTraffic;
694
730
 
731
+ const ignorePods =
732
+ isDeployRunnerContext(path, options) && targetTraffic
733
+ ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
734
+ : [];
735
+
695
736
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
696
737
  const cmdString = options.cmd
697
738
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
@@ -700,13 +741,14 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
700
741
 
701
742
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
702
743
  const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
744
+ const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
703
745
 
704
746
  shellExec(
705
747
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
706
748
  image ? ` --image ${image}` : ''
707
749
  }${versions ? ` --versions ${versions}` : ''}${
708
750
  options.namespace ? ` --namespace ${options.namespace}` : ''
709
- }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag} ${deployId} ${env}`,
751
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
710
752
  );
711
753
 
712
754
  if (isDeployRunnerContext(path, options)) {
@@ -717,11 +759,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
717
759
  shellExec(
718
760
  `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
719
761
  options.namespace ? ` --namespace ${options.namespace}` : ''
720
- }${timeoutFlags}${gitCleanFlag}`,
762
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
721
763
  );
722
764
  if (!targetTraffic)
723
765
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
724
- await Underpost.deploy.monitorReadyRunner(deployId, env, targetTraffic, [], options.namespace, 'underpost');
766
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
725
767
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
726
768
  } else
727
769
  logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
@@ -967,8 +1009,19 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
967
1009
  // pathRewritePolicy,
968
1010
  });
969
1011
  if (options.tls) {
970
- shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
971
- proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1012
+ if (options.test) {
1013
+ const sslDir = `./engine-private/ssl/${_host}`;
1014
+ const nameSafe = _host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1015
+ fs.mkdirpSync(sslDir);
1016
+ shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${_host}"`);
1017
+ shellExec(`kubectl delete secret ${_host} -n ${options.namespace} --ignore-not-found`);
1018
+ shellExec(
1019
+ `kubectl create secret tls ${_host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1020
+ );
1021
+ } else {
1022
+ shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
1023
+ proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1024
+ }
972
1025
  }
973
1026
  // console.log(proxyYaml);
974
1027
  shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
@@ -1023,6 +1076,9 @@ EOF
1023
1076
  cmd: _cmd,
1024
1077
  volumes: _volumes,
1025
1078
  metadata: _metadata,
1079
+ lifecycle: _lifecycle,
1080
+ readinessProbe: _readinessProbe,
1081
+ livenessProbe: _livenessProbe,
1026
1082
  } = instance;
1027
1083
  if (id !== _id) continue;
1028
1084
  const _deployId = `${deployId}-${_id}`;
@@ -1034,6 +1090,7 @@ EOF
1034
1090
  // Examples images:
1035
1091
  // `underpost/underpost-engine:${Underpost.version}`
1036
1092
  // `localhost/rockylinux9-underpost:${Underpost.version}`
1093
+ if (options.imageName) _image = options.imageName;
1037
1094
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1038
1095
 
1039
1096
  if (_image && !_image.startsWith('localhost'))
@@ -1087,6 +1144,20 @@ EOF
1087
1144
  ),
1088
1145
  );
1089
1146
 
1147
+ // Resolve env-scoped lifecycle/probe blocks: each can be either
1148
+ // { ...envObj } // shared shape
1149
+ // { development: {...}, production: {...} } // env-specific
1150
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1151
+
1152
+ // Convention: an instance config may place `imagePullPolicy` inside
1153
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1154
+ // Extract it onto the container spec (where K8S expects it) and
1155
+ // strip it from the lifecycle hash so the rendered YAML stays valid.
1156
+ // CLI override (`--image-pull-policy`) wins over the conf value.
1157
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1158
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1159
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1160
+
1090
1161
  let deploymentYaml = `---
1091
1162
  ${Underpost.deploy
1092
1163
  .deploymentYamlPartsFactory({
@@ -1099,6 +1170,11 @@ ${Underpost.deploy
1099
1170
  namespace: options.namespace,
1100
1171
  volumes: _volumes,
1101
1172
  cmd: resolvedCmd,
1173
+ lifecycle: lifecycleForManifest,
1174
+ readinessProbe: pickEnv(_readinessProbe),
1175
+ livenessProbe: pickEnv(_livenessProbe),
1176
+ containerPort: _toPort,
1177
+ imagePullPolicy: instanceImagePullPolicy,
1102
1178
  })
1103
1179
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
1104
1180
  `;
@@ -1110,12 +1186,16 @@ EOF
1110
1186
  `,
1111
1187
  { disableLog: true },
1112
1188
  );
1113
- const { ready, readyPods } = await Underpost.deploy.monitorReadyRunner(
1189
+ // Custom instances run a bare binary (no `underpost start` / internal
1190
+ // HTTP endpoint): Kubernetes readiness is the running signal and
1191
+ // container-status is read via exec. See `Deploy custom instance to K8S.md`.
1192
+ const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1114
1193
  _deployId,
1115
1194
  env,
1116
1195
  targetTraffic,
1117
1196
  ignorePods,
1118
1197
  options.namespace,
1198
+ { readyGate: 'kubernetes', statusTransport: 'exec' },
1119
1199
  );
1120
1200
 
1121
1201
  if (!ready) {
@@ -1125,12 +1205,12 @@ EOF
1125
1205
  shellExec(
1126
1206
  `${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
1127
1207
  `${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
1128
- `${options.tls ? ` --tls` : ''}` +
1208
+ `${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
1129
1209
  ` instance-promote '${path}'`,
1130
1210
  );
1131
1211
  }
1132
1212
  if (options.etcHosts) {
1133
- const hostListenResult = Underpost.deploy.etcHostFactory(etcHosts);
1213
+ const hostListenResult = etcHostFactory(etcHosts);
1134
1214
  logger.info(hostListenResult.renderHosts);
1135
1215
  }
1136
1216
  },
@@ -1187,14 +1267,34 @@ EOF
1187
1267
  volumes: _volumes,
1188
1268
  metadata: _metadata,
1189
1269
  runtime: _runtime,
1270
+ lifecycle: _lifecycle,
1271
+ readinessProbe: _readinessProbe,
1272
+ livenessProbe: _livenessProbe,
1190
1273
  } = instance;
1191
1274
 
1192
- // Resolve Dockerfile source: use runtime-specific path when instance defines a runtime.
1193
- const dockerfileSourcePath = _runtime ? `src/runtime/${_runtime}/Dockerfile` : `${rootPath}/Dockerfile`;
1194
- if (fs.existsSync(dockerfileSourcePath)) {
1275
+ // Resolve Dockerfile source. Dev/prod variant rules:
1276
+ // - When the instance defines a `runtime`, look under
1277
+ // `src/runtime/<runtime>/`. In `--dev` mode prefer `Dockerfile.dev`
1278
+ // when it exists, falling back to `Dockerfile`.
1279
+ // - When `runtime` is not set, look in the project root with the
1280
+ // same `.dev` → no-suffix precedence.
1281
+ // Dockerfile.dev is a full Dockerfile (not an overlay) — each runtime
1282
+ // owns the contract between its dev image and its prod image (debug
1283
+ // build flags, extra tooling, default ports, etc.).
1284
+ const dockerfileBase = _runtime ? `src/runtime/${_runtime}` : rootPath;
1285
+ const dockerfileCandidates = options.dev
1286
+ ? [`${dockerfileBase}/Dockerfile.dev`, `${dockerfileBase}/Dockerfile`]
1287
+ : [`${dockerfileBase}/Dockerfile`];
1288
+ const dockerfileSourcePath = dockerfileCandidates.find((p) => fs.existsSync(p));
1289
+ if (dockerfileSourcePath) {
1290
+ if (options.dev && !dockerfileSourcePath.endsWith('.dev')) {
1291
+ logger.warn(
1292
+ `[instance-build-manifest] --dev requested but no Dockerfile.dev present; falling back to ${dockerfileSourcePath}`,
1293
+ );
1294
+ }
1195
1295
  fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
1196
1296
  } else {
1197
- logger.warn(`[instance-build-manifest] Dockerfile not found at ${dockerfileSourcePath}`);
1297
+ logger.warn(`[instance-build-manifest] Dockerfile not found; tried: ${dockerfileCandidates.join(', ')}`);
1198
1298
  }
1199
1299
 
1200
1300
  const _deployId = `${deployId}-${_id}`;
@@ -1239,6 +1339,17 @@ EOF
1239
1339
  ),
1240
1340
  );
1241
1341
 
1342
+ // Env-aware lifecycle / probe selection. Each block may either be
1343
+ // a single object (shared across envs) or `{ development, production }`.
1344
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1345
+
1346
+ // Convention: an instance config may place `imagePullPolicy` inside
1347
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1348
+ // Extract it onto the container spec and strip it from the lifecycle hash.
1349
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1350
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1351
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1352
+
1242
1353
  const deploymentYaml =
1243
1354
  `---\n` +
1244
1355
  Underpost.deploy
@@ -1252,6 +1363,11 @@ EOF
1252
1363
  namespace: options.namespace,
1253
1364
  volumes: _volumes,
1254
1365
  cmd: resolvedCmd,
1366
+ lifecycle: lifecycleForManifest,
1367
+ readinessProbe: pickEnv(_readinessProbe),
1368
+ livenessProbe: pickEnv(_livenessProbe),
1369
+ containerPort: _toPort,
1370
+ imagePullPolicy: instanceImagePullPolicy,
1255
1371
  })
1256
1372
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
1257
1373
 
@@ -1330,9 +1446,9 @@ EOF`);
1330
1446
  // crictl is in the kubernetes repo but excluded by default — install it explicitly
1331
1447
  shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
1332
1448
  // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1333
- shellExec(
1334
- `sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf 2>/dev/null || true`,
1335
- );
1449
+ shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1450
+ silentOnError: true,
1451
+ });
1336
1452
  shellExec(`sudo systemctl enable --now crio`);
1337
1453
  logger.info('CRI-O installed and started.');
1338
1454
  // Write crictl config so all crictl calls default to the CRI-O socket
@@ -1356,8 +1472,8 @@ EOF`);
1356
1472
  const baseClusterCommand = options.dev ? ' --dev' : '';
1357
1473
  const currentImage = options.imageName
1358
1474
  ? options.imageName
1359
- : Underpost.deploy
1360
- .getCurrentLoadedImages(options.nodeName ? options.nodeName : 'kind-worker', false)
1475
+ : Underpost.image
1476
+ .getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
1361
1477
  .find((o) => o.IMAGE.match('underpost'));
1362
1478
  const podName = options.podName || `underpost-dev-container`;
1363
1479
  const volumeHostPath = options.claimName || '/home/dd';
@@ -1466,7 +1582,8 @@ EOF`);
1466
1582
  `git config user.name '${username}' && ` +
1467
1583
  `git config user.email '${email}' && ` +
1468
1584
  `git config credential.interactive always &&` +
1469
- `git config pull.rebase false`,
1585
+ `git config pull.rebase false && ` +
1586
+ `git config core.filemode false`,
1470
1587
  {
1471
1588
  disableLog: true,
1472
1589
  silent: true,
@@ -1633,20 +1750,13 @@ EOF`);
1633
1750
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1634
1751
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1635
1752
  const env = options.dev ? 'development' : 'production';
1636
- const ignorePods = Underpost.deploy
1753
+ const ignorePods = Underpost.kubectl
1637
1754
  .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
1638
1755
  .map((p) => p.NAME);
1639
1756
 
1640
1757
  shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
1641
1758
 
1642
- await Underpost.deploy.monitorReadyRunner(
1643
- deployId,
1644
- env,
1645
- targetTraffic,
1646
- ignorePods,
1647
- options.namespace,
1648
- 'underpost',
1649
- );
1759
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
1650
1760
 
1651
1761
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
1652
1762
  },
@@ -1738,7 +1848,7 @@ EOF`);
1738
1848
  }`;
1739
1849
  shellExec(cmd, { async: true });
1740
1850
  }
1741
- await awaitDeployMonitor(true);
1851
+ if ((await awaitDeployMonitor()) !== true) return;
1742
1852
  {
1743
1853
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1744
1854
 
@@ -1746,7 +1856,7 @@ EOF`);
1746
1856
  async: true,
1747
1857
  });
1748
1858
  }
1749
- await awaitDeployMonitor(true);
1859
+ if ((await awaitDeployMonitor()) !== true) return;
1750
1860
  shellExec(
1751
1861
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1752
1862
  );
@@ -1843,7 +1953,7 @@ EOF`);
1843
1953
  );
1844
1954
  } else logger.error(`Service pod ${podToMonitor} failed to start in time.`);
1845
1955
  if (options.etcHosts === true) {
1846
- const hostListenResult = Underpost.deploy.etcHostFactory([host]);
1956
+ const hostListenResult = etcHostFactory([host]);
1847
1957
  logger.info(hostListenResult.renderHosts);
1848
1958
  }
1849
1959
  },
@@ -1861,7 +1971,7 @@ EOF`);
1861
1971
  const confServer = loadConfServerJson(`./engine-private/conf/${options.deployId}/conf.server.json`);
1862
1972
  hosts.push(...Object.keys(confServer));
1863
1973
  }
1864
- const hostListenResult = Underpost.deploy.etcHostFactory(hosts);
1974
+ const hostListenResult = etcHostFactory(hosts);
1865
1975
  logger.info(hostListenResult.renderHosts);
1866
1976
  },
1867
1977
 
@@ -2180,15 +2290,26 @@ EOF`);
2180
2290
  * @memberof UnderpostRun
2181
2291
  */
2182
2292
  kill: (path = '', options = DEFAULT_OPTION) => {
2183
- if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
2293
+ if (options.pid)
2294
+ return shellExec(`sudo kill -9 ${options.pid}`, {
2295
+ silentOnError: true,
2296
+ });
2184
2297
  for (const _path of path.split(',')) {
2185
2298
  if (_path.split('+')[1]) {
2186
2299
  let [port, sumPortOffSet] = _path.split('+');
2187
2300
  port = parseInt(port);
2188
2301
  sumPortOffSet = parseInt(sumPortOffSet);
2189
2302
  for (const sumPort of range(0, sumPortOffSet))
2190
- shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
2191
- } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
2303
+ shellExec(
2304
+ `PIDS=$(lsof -t -i:${parseInt(port) + parseInt(sumPort)}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`,
2305
+ {
2306
+ silentOnError: true,
2307
+ },
2308
+ );
2309
+ } else
2310
+ shellExec(`PIDS=$(lsof -t -i:${_path}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`, {
2311
+ silentOnError: true,
2312
+ });
2192
2313
  }
2193
2314
  },
2194
2315
  /**
@@ -2235,9 +2356,10 @@ EOF`);
2235
2356
  * @memberof UnderpostRun
2236
2357
  */
2237
2358
  secret: (path, options = DEFAULT_OPTION) => {
2238
- const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
2239
- const command = `${options.dev ? 'node bin' : 'underpost'} secret underpost --create-from-file ${secretPath}`;
2240
- shellExec(command);
2359
+ const cronDeployId = cronDeployIdResolve() || 'dd-cron';
2360
+ Underpost.secret.underpost.createFromEnvFile(
2361
+ `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
2362
+ );
2241
2363
  },
2242
2364
  /**
2243
2365
  * @method underpost-config
@@ -2411,7 +2533,8 @@ EOF`;
2411
2533
  /**
2412
2534
  * @method push-bundle
2413
2535
  * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
2414
- * Steps: set env, build+split zip, switch to cron env, upload parts to storage.
2536
+ * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
2537
+ * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2415
2538
  * @param {string} path - Optional `fsPath.splitOption` string.
2416
2539
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2417
2540
  * @param {Object} options - The default underpost runner options for customizing workflow.
@@ -2420,7 +2543,7 @@ EOF`;
2420
2543
  * @memberof UnderpostRun
2421
2544
  */
2422
2545
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2423
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2546
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2424
2547
  const env = options.dev ? 'development' : 'production';
2425
2548
  const deployId = options.deployId || 'dd-default';
2426
2549
  const pathParts = (path || '').split('.');
@@ -2444,11 +2567,54 @@ EOF`;
2444
2567
  }
2445
2568
  }
2446
2569
 
2570
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2571
+ const confServer = fs.existsSync(confServerPath)
2572
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2573
+ : {};
2574
+ const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
2575
+
2447
2576
  shellExec(`${baseCommand} env ${deployId} ${env}`);
2448
2577
  shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
2449
- shellExec(
2450
- `${baseCommand} fs ${fsPath} --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --force`,
2451
- );
2578
+
2579
+ const pushBundleFiles = (host, routePath) => {
2580
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2581
+ const buildDir = `./${fsPath}`;
2582
+ if (!fs.existsSync(buildDir)) return;
2583
+ const partFiles = fs
2584
+ .readdirSync(buildDir)
2585
+ .filter(
2586
+ (name) =>
2587
+ name.startsWith(`${buildId}.zip.part`) ||
2588
+ name.startsWith(`${buildId}.zip-part`) ||
2589
+ name === `${buildId}.zip`,
2590
+ )
2591
+ .map((name) => `${fsPath}/${name}`);
2592
+ if (partFiles.length === 0) {
2593
+ logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
2594
+ return;
2595
+ }
2596
+ for (const partFile of partFiles) {
2597
+ shellExec(
2598
+ `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
2599
+ );
2600
+ }
2601
+ };
2602
+
2603
+ for (const host of Object.keys(confServer)) {
2604
+ for (const routePath of Object.keys(confServer[host])) {
2605
+ const routeConf = confServer[host][routePath] || {};
2606
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2607
+ if (routeConf.singleReplica) {
2608
+ if (routeConf.replicas) {
2609
+ for (const replica of routeConf.replicas) {
2610
+ pushBundleFiles(host, replica);
2611
+ }
2612
+ }
2613
+ continue;
2614
+ }
2615
+ pushBundleFiles(host, routePath);
2616
+ }
2617
+ }
2452
2618
  },
2453
2619
 
2454
2620
  /**
@@ -2465,11 +2631,13 @@ EOF`;
2465
2631
  * @memberof UnderpostRun
2466
2632
  */
2467
2633
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
2468
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2634
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2469
2635
  const env = options.dev ? 'development' : 'production';
2470
2636
  const deployId = options.deployId || 'dd-default';
2471
2637
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2472
- const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
2638
+ const confServer = fs.existsSync(confServerPath)
2639
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2640
+ : {};
2473
2641
  const hostsArg = path
2474
2642
  ? path
2475
2643
  .split(',')
@@ -2488,62 +2656,124 @@ EOF`;
2488
2656
  `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2489
2657
  );
2490
2658
 
2659
+ const pullBundleRoute = (host, routePath) => {
2660
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2661
+ const zipPath = `build/${buildId}.zip`;
2662
+ const buildDir = './build';
2663
+ const hasZip = fs.existsSync(zipPath);
2664
+ const hasParts =
2665
+ fs.existsSync(buildDir) &&
2666
+ fs
2667
+ .readdirSync(buildDir)
2668
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2669
+
2670
+ if (!hasZip && !hasParts) {
2671
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2672
+ return;
2673
+ }
2674
+
2675
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2676
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
2677
+ shellExec(`sudo rm -rf ${zipPath}`);
2678
+
2679
+ if (fs.existsSync(buildDir)) {
2680
+ fs.readdirSync(buildDir)
2681
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2682
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2683
+ }
2684
+
2685
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2686
+ if (!fs.existsSync(extractedDir)) {
2687
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2688
+ return;
2689
+ }
2690
+
2691
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2692
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2693
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2694
+ fs.copySync(`${extractedDir}`, `${publicDestPath}`);
2695
+ };
2696
+
2491
2697
  for (const host of hostsArg) {
2492
- // Gather all routes for this host; fall back to root '/' when host is not in confServer
2493
- // (e.g. when hosts were provided explicitly via the path argument).
2494
2698
  const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2495
2699
 
2496
2700
  for (const routePath of routePaths) {
2497
2701
  const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
2498
- // Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
2499
- if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
2500
-
2501
- // buildClient names the zip as "<host>-<path-no-slashes>.zip"
2502
- // e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
2503
- // e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
2504
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2505
- const zipPath = `build/${buildId}.zip`;
2506
- const buildDir = './build';
2507
- const hasZip = fs.existsSync(zipPath);
2508
- const hasParts =
2509
- fs.existsSync(buildDir) &&
2510
- fs
2511
- .readdirSync(buildDir)
2512
- .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2513
-
2514
- if (!hasZip && !hasParts) {
2515
- logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2702
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2703
+ if (routeConf.singleReplica) {
2704
+ if (routeConf.replicas) {
2705
+ for (const replica of routeConf.replicas) {
2706
+ pullBundleRoute(host, replica);
2707
+ }
2708
+ }
2516
2709
  continue;
2517
2710
  }
2711
+ pullBundleRoute(host, routePath);
2712
+ }
2713
+ }
2714
+ },
2518
2715
 
2519
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2520
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
2521
- shellExec(`sudo rm -rf ${zipPath}`);
2716
+ /**
2717
+ * @method build-cluster-deployment-manifests
2718
+ * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
2719
+ * @param {string} path - Unused.
2720
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2721
+ * @memberof UnderpostRun
2722
+ */
2723
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
2724
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
2725
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
2726
+ },
2522
2727
 
2523
- // Clean up downloaded part wrapper zips left by --omit-unzip pull
2524
- if (fs.existsSync(buildDir)) {
2525
- fs.readdirSync(buildDir)
2526
- .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2527
- .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2528
- }
2728
+ /**
2729
+ * @method monitor-ui
2730
+ * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
2731
+ * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
2732
+ * @param {string} path - Unused.
2733
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2734
+ * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
2735
+ * @memberof UnderpostRun
2736
+ */
2737
+ 'monitor-ui': (path, options = DEFAULT_OPTION) => {
2738
+ if (options.remove) {
2739
+ shellExec(`sudo firewall-cmd --zone=public --remove-service=cockpit --permanent`);
2740
+ shellExec(`sudo firewall-cmd --reload`);
2741
+ return;
2742
+ }
2743
+ shellExec(`sudo dnf install -y cockpit cockpit-machines libvirt`);
2744
+ shellExec(`sudo systemctl enable --now cockpit.socket libvirtd`);
2745
+ shellExec(`sudo firewall-cmd --permanent --add-service=cockpit`);
2746
+ shellExec(`sudo firewall-cmd --reload`);
2747
+ },
2529
2748
 
2530
- // unzipClientBuild extracts to buildId with trailing '-' stripped
2531
- // e.g. "build/underpost.net-" → "build/underpost.net"
2532
- // e.g. "build/app.net-admin" "build/app.net-admin" (no trailing dash, no change)
2533
- const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2534
- if (!fs.existsSync(extractedDir)) {
2535
- logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2536
- continue;
2537
- }
2749
+ /**
2750
+ * @method shared-dir
2751
+ * @description Run once for initial shared-directory setup. Creates the group, adds the user,
2752
+ * creates the directory, sets ownership, applies the SGID bit, and configures default ACLs so
2753
+ * all future files inside the directory automatically inherit group write permissions.
2754
+ * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
2755
+ * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
2756
+ * Customise via the `path` argument or leave empty to use the default.
2757
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2758
+ * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
2759
+ * @memberof UnderpostRun
2760
+ */
2761
+ 'shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
2762
+ const dir = path || '/home/dd/engine';
2763
+ const user = options.user || 'admin';
2764
+ const group = options.group || 'engine-dev';
2538
2765
 
2539
- // Destination mirrors the public directory layout used by the server
2540
- const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2541
- if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2542
- // Ensure parent directory exists for sub-paths
2543
- if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2544
- shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
2545
- }
2546
- }
2766
+ logger.info(`[setup-shared-dir] dir=${dir} user=${user} group=${group}`);
2767
+
2768
+ shellExec(`sudo groupadd ${group} 2>/dev/null || true`);
2769
+ shellExec(`sudo usermod -aG ${group} ${user}`);
2770
+ shellExec(`sudo mkdir -p ${dir}`);
2771
+ shellExec(`sudo chown -R ${user}:${group} ${dir}`);
2772
+ shellExec(`sudo chmod -R 2775 ${dir}`);
2773
+ shellExec(`sudo setfacl -d -m g:${group}:rwx ${dir}`);
2774
+ shellExec(`sudo setfacl -m g:${group}:rwx ${dir}`);
2775
+
2776
+ logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
2547
2777
  },
2548
2778
  };
2549
2779
 
@@ -2601,14 +2831,14 @@ EOF`;
2601
2831
  if (options.replicas === '' || options.replicas === null || options.replicas === undefined)
2602
2832
  options.replicas = 1;
2603
2833
  options.npmRoot = npmRoot;
2604
- logger.info('callback', { path, options });
2834
+ logger.info(`Executing runner`, { runner, namespace: options.namespace });
2605
2835
  if (!Underpost.run.RUNNERS.includes(runner)) throw new Error(`Runner not found: ${runner}`);
2606
2836
  const result = await Underpost.run.CALL(runner, path, options);
2607
2837
  return result;
2608
2838
  } catch (error) {
2609
2839
  console.log(error);
2610
2840
  logger.error(error);
2611
- return null;
2841
+ process.exit(1);
2612
2842
  }
2613
2843
  },
2614
2844
  };