cyberia 3.2.9 → 3.2.12

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 (169) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +6 -0
  2. package/.github/workflows/npmpkg.ci.yml +1 -0
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +1 -0
  5. package/.vscode/extensions.json +9 -9
  6. package/.vscode/settings.json +20 -4
  7. package/CHANGELOG.md +213 -1
  8. package/CLI-HELP.md +92 -23
  9. package/README.md +190 -348
  10. package/bin/build.js +24 -8
  11. package/bin/build.template.js +187 -0
  12. package/bin/cyberia.js +229 -52
  13. package/bin/deploy.js +12 -2
  14. package/bin/index.js +229 -52
  15. package/bump.config.js +26 -0
  16. package/conf.js +130 -24
  17. package/deployment.yaml +4 -2
  18. package/hardhat/package-lock.json +113 -144
  19. package/hardhat/package.json +4 -3
  20. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  21. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  22. package/manifests/deployment/dd-cyberia-development/deployment.yaml +4 -2
  23. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  24. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  25. package/manifests/kind-config-dev.yaml +8 -0
  26. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  27. package/manifests/mongodb/pv-pvc.yaml +44 -8
  28. package/manifests/mongodb/statefulset.yaml +55 -68
  29. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  30. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  31. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  32. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  33. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  34. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  35. package/manifests/valkey/statefulset.yaml +1 -1
  36. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  37. package/package.json +27 -15
  38. package/scripts/ipxe-setup.sh +52 -49
  39. package/scripts/k3s-node-setup.sh +81 -46
  40. package/scripts/lxd-vm-setup.sh +193 -8
  41. package/scripts/maas-nat-firewalld.sh +145 -0
  42. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  43. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  44. package/src/api/core/core.router.js +19 -14
  45. package/src/api/core/core.service.js +5 -5
  46. package/src/api/crypto/crypto.router.js +18 -12
  47. package/src/api/crypto/crypto.service.js +3 -3
  48. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  49. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  50. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  51. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  52. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  53. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  55. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  56. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  57. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  58. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  59. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  62. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  63. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  64. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  65. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  66. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  67. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  68. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  69. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  70. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  71. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  72. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  73. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +451 -0
  74. package/src/api/default/default.router.js +22 -18
  75. package/src/api/default/default.service.js +5 -5
  76. package/src/api/document/document.router.js +28 -23
  77. package/src/api/document/document.service.js +100 -23
  78. package/src/api/file/file.router.js +19 -13
  79. package/src/api/file/file.service.js +9 -7
  80. package/src/api/instance/instance.router.js +29 -24
  81. package/src/api/instance/instance.service.js +6 -6
  82. package/src/api/ipfs/ipfs.router.js +21 -16
  83. package/src/api/ipfs/ipfs.service.js +8 -8
  84. package/src/api/object-layer/object-layer.router.js +512 -507
  85. package/src/api/object-layer/object-layer.service.js +17 -14
  86. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  87. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  88. package/src/api/test/test.router.js +17 -12
  89. package/src/api/types.js +24 -0
  90. package/src/api/user/guest.service.js +5 -4
  91. package/src/api/user/user.router.js +297 -288
  92. package/src/api/user/user.service.js +100 -35
  93. package/src/cli/baremetal.js +132 -101
  94. package/src/cli/cluster.js +700 -232
  95. package/src/cli/db.js +59 -60
  96. package/src/cli/deploy.js +216 -137
  97. package/src/cli/fs.js +13 -3
  98. package/src/cli/index.js +80 -15
  99. package/src/cli/ipfs.js +4 -6
  100. package/src/cli/kubectl.js +4 -1
  101. package/src/cli/lxd.js +1099 -223
  102. package/src/cli/monitor.js +9 -3
  103. package/src/cli/release.js +334 -140
  104. package/src/cli/repository.js +68 -23
  105. package/src/cli/run.js +193 -49
  106. package/src/cli/secrets.js +11 -2
  107. package/src/cli/test.js +9 -3
  108. package/src/client/Default.index.js +9 -3
  109. package/src/client/components/core/Auth.js +5 -0
  110. package/src/client/components/core/ClientEvents.js +76 -0
  111. package/src/client/components/core/EventBus.js +4 -0
  112. package/src/client/components/core/Modal.js +82 -41
  113. package/src/client/components/core/PanelForm.js +56 -52
  114. package/src/client/components/core/Worker.js +162 -363
  115. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  116. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  117. package/src/client/public/cyberia-docs/ARCHITECTURE.md +50 -410
  118. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  119. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  120. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +203 -185
  121. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  122. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  123. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  124. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  125. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  126. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  127. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  128. package/src/client/sw/core.sw.js +174 -112
  129. package/src/db/DataBaseProvider.js +115 -15
  130. package/src/db/mariadb/MariaDB.js +2 -1
  131. package/src/db/mongo/MongoBootstrap.js +657 -0
  132. package/src/db/mongo/MongooseDB.js +129 -21
  133. package/src/grpc/cyberia/grpc-server.js +25 -57
  134. package/src/index.js +1 -1
  135. package/src/runtime/cyberia-client/Dockerfile +24 -3
  136. package/src/runtime/cyberia-client/Dockerfile.dev +82 -0
  137. package/src/runtime/cyberia-server/Dockerfile +29 -4
  138. package/src/runtime/cyberia-server/Dockerfile.dev +71 -0
  139. package/src/runtime/express/Express.js +2 -2
  140. package/src/runtime/wp/Wp.js +8 -5
  141. package/src/server/auth.js +2 -2
  142. package/src/server/client-build-docs.js +1 -1
  143. package/src/server/client-build.js +94 -129
  144. package/src/server/conf.js +86 -83
  145. package/src/server/process.js +180 -19
  146. package/src/server/proxy.js +9 -2
  147. package/src/server/runtime.js +1 -1
  148. package/src/server/start.js +17 -5
  149. package/src/server/valkey.js +2 -0
  150. package/src/ws/IoInterface.js +16 -16
  151. package/src/ws/core/channels/core.ws.chat.js +11 -11
  152. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  153. package/src/ws/core/channels/core.ws.stream.js +19 -19
  154. package/src/ws/core/core.ws.connection.js +8 -8
  155. package/src/ws/core/core.ws.server.js +6 -5
  156. package/src/ws/default/channels/default.ws.main.js +10 -10
  157. package/src/ws/default/default.ws.connection.js +4 -4
  158. package/src/ws/default/default.ws.server.js +4 -3
  159. package/bin/file.js +0 -202
  160. package/bin/vs.js +0 -74
  161. package/bin/zed.js +0 -84
  162. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  163. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  164. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  165. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  166. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  167. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  168. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  169. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/run.js CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  awaitDeployMonitor,
11
11
  buildKindPorts,
12
12
  Config,
13
+ cronDeployIdResolve,
14
+ etcHostFactory,
13
15
  getNpmRootPath,
14
16
  isDeployRunnerContext,
15
17
  loadConfServerJson,
@@ -24,6 +26,7 @@ import { range, setPad, timer } from '../client/components/core/CommonJs.js';
24
26
  import os from 'os';
25
27
  import Underpost from '../index.js';
26
28
  import dotenv from 'dotenv';
29
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
27
30
 
28
31
  const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
29
32
  new Promise((resolve, reject) => {
@@ -97,6 +100,7 @@ const logger = loggerFactory(import.meta);
97
100
  * @property {string} deployId - The deployment ID.
98
101
  * @property {string} instanceId - The instance ID.
99
102
  * @property {string} user - The user to run as.
103
+ * @property {string} group - The group to use.
100
104
  * @property {string} pid - The process ID.
101
105
  * @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
102
106
  * @property {string} monitorStatus - The monitor status option.
@@ -114,6 +118,7 @@ const logger = loggerFactory(import.meta);
114
118
  * @property {boolean} copy - Whether to copy the command to the clipboard instead of executing it.
115
119
  * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
116
120
  * @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).
121
+ * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
117
122
  * @memberof UnderpostRun
118
123
  */
119
124
  const DEFAULT_OPTION = {
@@ -165,6 +170,7 @@ const DEFAULT_OPTION = {
165
170
  deployId: '',
166
171
  instanceId: '',
167
172
  user: '',
173
+ group: '',
168
174
  pid: '',
169
175
  disablePrivateConfUpdate: false,
170
176
  monitorStatus: '',
@@ -180,6 +186,7 @@ const DEFAULT_OPTION = {
180
186
  copy: false,
181
187
  skipFullBuild: false,
182
188
  pullBundle: false,
189
+ remove: false,
183
190
  };
184
191
 
185
192
  /**
@@ -209,28 +216,39 @@ class UnderpostRun {
209
216
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
210
217
  const baseCommand = options.dev ? 'node bin' : 'underpost';
211
218
  const mongoHosts = ['mongodb-0.mongodb-service'];
219
+ let primaryMongoHost = 'mongodb-0.mongodb-service';
212
220
  if (!options.expose) {
213
221
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --reset`);
214
222
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
215
223
 
216
224
  shellExec(
217
- `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --mongo-db-host ${mongoHosts.join(
225
+ `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb4 --service-host ${mongoHosts.join(
218
226
  ',',
219
227
  )} --pull-image`,
220
228
  );
221
229
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''} --valkey --pull-image`);
222
230
  }
223
-
224
- {
225
- // Detect MongoDB primary pod using method
226
- let primaryMongoHost = 'mongodb-0.mongodb-service';
231
+ if (options.k3s) {
232
+ if (options.remove) {
233
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:27017`);
234
+ shellExec(`${baseCommand} lxd --delete-expose k3s-control:6379`);
235
+ } else {
236
+ shellExec(`${baseCommand} lxd --expose k3s-control:27017 --node-port 32017`);
237
+ shellExec(`${baseCommand} lxd --expose k3s-control:6379 --node-port 32079`);
238
+ }
239
+ shellExec(`lxc config device show k3s-control`);
240
+ } else {
227
241
  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 });
242
+ const primaryPodName =
243
+ MongoBootstrap.getPrimaryPodName({
244
+ namespace: options.namespace,
245
+ podName: 'mongodb-0',
246
+ disableAuth: options.dev,
247
+ }) || 'mongodb-0';
248
+ shellExec(
249
+ `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config mongo`,
250
+ { async: true },
251
+ );
234
252
  shellExec(
235
253
  `${baseCommand} deploy --expose --namespace ${options.namespace} --disable-update-underpost-config valkey`,
236
254
  { async: true },
@@ -241,10 +259,9 @@ class UnderpostRun {
241
259
  default: primaryMongoHost,
242
260
  });
243
261
  }
244
-
245
- const hostListenResult = Underpost.deploy.etcHostFactory([primaryMongoHost]);
246
- logger.info(hostListenResult.renderHosts);
247
262
  }
263
+ const hostListenResult = etcHostFactory([primaryMongoHost]);
264
+ logger.info(hostListenResult.renderHosts);
248
265
  },
249
266
 
250
267
  /**
@@ -505,14 +522,16 @@ class UnderpostRun {
505
522
  },
506
523
  /**
507
524
  * @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.
525
+ * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
526
+ * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
527
+ * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
510
528
  * @param {Object} options - The default underpost runner options for customizing workflow
511
529
  * @memberof UnderpostRun
512
530
  */
513
531
  'docker-image': (path, options = DEFAULT_OPTION) => {
532
+ const repo = Underpost.repo.resolveInstanceRepo(path);
514
533
  Underpost.repo.dispatchWorkflow({
515
- repo: `${process.env.GITHUB_USERNAME}/engine`,
534
+ repo,
516
535
  workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
517
536
  ref: 'master',
518
537
  inputs: {},
@@ -527,6 +546,7 @@ class UnderpostRun {
527
546
  */
528
547
  clean: (path = '', options = DEFAULT_OPTION) => {
529
548
  Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
549
+ if (options.dev) shellExec(`node bin run shared-dir ${path ? path : '/home/dd/engine'}`);
530
550
  },
531
551
  /**
532
552
  * @method pull
@@ -536,16 +556,14 @@ class UnderpostRun {
536
556
  * @memberof UnderpostRun
537
557
  */
538
558
  pull: (path, options = DEFAULT_OPTION) => {
559
+ // shellExec is fail-fast by default — any non-zero exit throws and
560
+ // propagates up to the workflow step. No per-call flag required.
539
561
  if (!fs.existsSync(`/home/dd`) || !fs.existsSync(`/home/dd/engine`)) {
540
562
  fs.mkdirSync(`/home/dd`, { recursive: true });
541
- shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, {
542
- silent: true,
543
- });
563
+ shellExec(`cd /home/dd && underpost clone ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
544
564
  } else {
545
565
  shellExec(`underpost run clean`);
546
- shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, {
547
- silent: true,
548
- });
566
+ shellExec(`cd /home/dd/engine && underpost pull . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
549
567
  }
550
568
  if (!fs.existsSync(`/home/dd/engine/engine-private`))
551
569
  shellExec(`cd /home/dd/engine && underpost clone ${process.env.GITHUB_USERNAME}/engine-private`, {
@@ -554,9 +572,7 @@ class UnderpostRun {
554
572
  else
555
573
  shellExec(
556
574
  `cd /home/dd/engine/engine-private && underpost pull . ${process.env.GITHUB_USERNAME}/engine-private`,
557
- {
558
- silent: true,
559
- },
575
+ { silent: true },
560
576
  );
561
577
  },
562
578
  /**
@@ -643,6 +659,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
643
659
  /**
644
660
  * @method sync
645
661
  * @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).
662
+ *
663
+ * Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
664
+ * which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
665
+ * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
666
+ * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
646
667
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
647
668
  * @param {Object} options - The default underpost runner options for customizing workflow
648
669
  * @memberof UnderpostRun
@@ -700,13 +721,14 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
700
721
 
701
722
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
702
723
  const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
724
+ const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
703
725
 
704
726
  shellExec(
705
727
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
706
728
  image ? ` --image ${image}` : ''
707
729
  }${versions ? ` --versions ${versions}` : ''}${
708
730
  options.namespace ? ` --namespace ${options.namespace}` : ''
709
- }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag} ${deployId} ${env}`,
731
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
710
732
  );
711
733
 
712
734
  if (isDeployRunnerContext(path, options)) {
@@ -717,7 +739,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
717
739
  shellExec(
718
740
  `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
719
741
  options.namespace ? ` --namespace ${options.namespace}` : ''
720
- }${timeoutFlags}${gitCleanFlag}`,
742
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
721
743
  );
722
744
  if (!targetTraffic)
723
745
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -1023,6 +1045,9 @@ EOF
1023
1045
  cmd: _cmd,
1024
1046
  volumes: _volumes,
1025
1047
  metadata: _metadata,
1048
+ lifecycle: _lifecycle,
1049
+ readinessProbe: _readinessProbe,
1050
+ livenessProbe: _livenessProbe,
1026
1051
  } = instance;
1027
1052
  if (id !== _id) continue;
1028
1053
  const _deployId = `${deployId}-${_id}`;
@@ -1087,6 +1112,20 @@ EOF
1087
1112
  ),
1088
1113
  );
1089
1114
 
1115
+ // Resolve env-scoped lifecycle/probe blocks: each can be either
1116
+ // { ...envObj } // shared shape
1117
+ // { development: {...}, production: {...} } // env-specific
1118
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1119
+
1120
+ // Convention: an instance config may place `imagePullPolicy` inside
1121
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1122
+ // Extract it onto the container spec (where K8S expects it) and
1123
+ // strip it from the lifecycle hash so the rendered YAML stays valid.
1124
+ // CLI override (`--image-pull-policy`) wins over the conf value.
1125
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1126
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1127
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1128
+
1090
1129
  let deploymentYaml = `---
1091
1130
  ${Underpost.deploy
1092
1131
  .deploymentYamlPartsFactory({
@@ -1099,6 +1138,11 @@ ${Underpost.deploy
1099
1138
  namespace: options.namespace,
1100
1139
  volumes: _volumes,
1101
1140
  cmd: resolvedCmd,
1141
+ lifecycle: lifecycleForManifest,
1142
+ readinessProbe: pickEnv(_readinessProbe),
1143
+ livenessProbe: pickEnv(_livenessProbe),
1144
+ containerPort: _toPort,
1145
+ imagePullPolicy: instanceImagePullPolicy,
1102
1146
  })
1103
1147
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
1104
1148
  `;
@@ -1130,7 +1174,7 @@ EOF
1130
1174
  );
1131
1175
  }
1132
1176
  if (options.etcHosts) {
1133
- const hostListenResult = Underpost.deploy.etcHostFactory(etcHosts);
1177
+ const hostListenResult = etcHostFactory(etcHosts);
1134
1178
  logger.info(hostListenResult.renderHosts);
1135
1179
  }
1136
1180
  },
@@ -1187,14 +1231,34 @@ EOF
1187
1231
  volumes: _volumes,
1188
1232
  metadata: _metadata,
1189
1233
  runtime: _runtime,
1234
+ lifecycle: _lifecycle,
1235
+ readinessProbe: _readinessProbe,
1236
+ livenessProbe: _livenessProbe,
1190
1237
  } = instance;
1191
1238
 
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)) {
1239
+ // Resolve Dockerfile source. Dev/prod variant rules:
1240
+ // - When the instance defines a `runtime`, look under
1241
+ // `src/runtime/<runtime>/`. In `--dev` mode prefer `Dockerfile.dev`
1242
+ // when it exists, falling back to `Dockerfile`.
1243
+ // - When `runtime` is not set, look in the project root with the
1244
+ // same `.dev` → no-suffix precedence.
1245
+ // Dockerfile.dev is a full Dockerfile (not an overlay) — each runtime
1246
+ // owns the contract between its dev image and its prod image (debug
1247
+ // build flags, extra tooling, default ports, etc.).
1248
+ const dockerfileBase = _runtime ? `src/runtime/${_runtime}` : rootPath;
1249
+ const dockerfileCandidates = options.dev
1250
+ ? [`${dockerfileBase}/Dockerfile.dev`, `${dockerfileBase}/Dockerfile`]
1251
+ : [`${dockerfileBase}/Dockerfile`];
1252
+ const dockerfileSourcePath = dockerfileCandidates.find((p) => fs.existsSync(p));
1253
+ if (dockerfileSourcePath) {
1254
+ if (options.dev && !dockerfileSourcePath.endsWith('.dev')) {
1255
+ logger.warn(
1256
+ `[instance-build-manifest] --dev requested but no Dockerfile.dev present; falling back to ${dockerfileSourcePath}`,
1257
+ );
1258
+ }
1195
1259
  fs.copyFileSync(dockerfileSourcePath, dockerfileManifestPath);
1196
1260
  } else {
1197
- logger.warn(`[instance-build-manifest] Dockerfile not found at ${dockerfileSourcePath}`);
1261
+ logger.warn(`[instance-build-manifest] Dockerfile not found; tried: ${dockerfileCandidates.join(', ')}`);
1198
1262
  }
1199
1263
 
1200
1264
  const _deployId = `${deployId}-${_id}`;
@@ -1239,6 +1303,17 @@ EOF
1239
1303
  ),
1240
1304
  );
1241
1305
 
1306
+ // Env-aware lifecycle / probe selection. Each block may either be
1307
+ // a single object (shared across envs) or `{ development, production }`.
1308
+ const pickEnv = (v) => (v && (v.development || v.production) ? v[env] : v);
1309
+
1310
+ // Convention: an instance config may place `imagePullPolicy` inside
1311
+ // the env-scoped lifecycle block (alongside postStart/preStop).
1312
+ // Extract it onto the container spec and strip it from the lifecycle hash.
1313
+ const { lifecycle: lifecycleForManifest, imagePullPolicy: lifecycleImagePullPolicy } =
1314
+ Underpost.deploy.extractInstanceImagePullPolicy(pickEnv(_lifecycle));
1315
+ const instanceImagePullPolicy = options.imagePullPolicy || lifecycleImagePullPolicy;
1316
+
1242
1317
  const deploymentYaml =
1243
1318
  `---\n` +
1244
1319
  Underpost.deploy
@@ -1252,6 +1327,11 @@ EOF
1252
1327
  namespace: options.namespace,
1253
1328
  volumes: _volumes,
1254
1329
  cmd: resolvedCmd,
1330
+ lifecycle: lifecycleForManifest,
1331
+ readinessProbe: pickEnv(_readinessProbe),
1332
+ livenessProbe: pickEnv(_livenessProbe),
1333
+ containerPort: _toPort,
1334
+ imagePullPolicy: instanceImagePullPolicy,
1255
1335
  })
1256
1336
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort));
1257
1337
 
@@ -1330,9 +1410,9 @@ EOF`);
1330
1410
  // crictl is in the kubernetes repo but excluded by default — install it explicitly
1331
1411
  shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
1332
1412
  // 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
- );
1413
+ shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1414
+ silentOnError: true,
1415
+ });
1336
1416
  shellExec(`sudo systemctl enable --now crio`);
1337
1417
  logger.info('CRI-O installed and started.');
1338
1418
  // Write crictl config so all crictl calls default to the CRI-O socket
@@ -1466,7 +1546,8 @@ EOF`);
1466
1546
  `git config user.name '${username}' && ` +
1467
1547
  `git config user.email '${email}' && ` +
1468
1548
  `git config credential.interactive always &&` +
1469
- `git config pull.rebase false`,
1549
+ `git config pull.rebase false && ` +
1550
+ `git config core.filemode false`,
1470
1551
  {
1471
1552
  disableLog: true,
1472
1553
  silent: true,
@@ -1738,7 +1819,7 @@ EOF`);
1738
1819
  }`;
1739
1820
  shellExec(cmd, { async: true });
1740
1821
  }
1741
- await awaitDeployMonitor(true);
1822
+ await awaitDeployMonitor();
1742
1823
  {
1743
1824
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1744
1825
 
@@ -1746,7 +1827,7 @@ EOF`);
1746
1827
  async: true,
1747
1828
  });
1748
1829
  }
1749
- await awaitDeployMonitor(true);
1830
+ await awaitDeployMonitor();
1750
1831
  shellExec(
1751
1832
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1752
1833
  );
@@ -1843,7 +1924,7 @@ EOF`);
1843
1924
  );
1844
1925
  } else logger.error(`Service pod ${podToMonitor} failed to start in time.`);
1845
1926
  if (options.etcHosts === true) {
1846
- const hostListenResult = Underpost.deploy.etcHostFactory([host]);
1927
+ const hostListenResult = etcHostFactory([host]);
1847
1928
  logger.info(hostListenResult.renderHosts);
1848
1929
  }
1849
1930
  },
@@ -1861,7 +1942,7 @@ EOF`);
1861
1942
  const confServer = loadConfServerJson(`./engine-private/conf/${options.deployId}/conf.server.json`);
1862
1943
  hosts.push(...Object.keys(confServer));
1863
1944
  }
1864
- const hostListenResult = Underpost.deploy.etcHostFactory(hosts);
1945
+ const hostListenResult = etcHostFactory(hosts);
1865
1946
  logger.info(hostListenResult.renderHosts);
1866
1947
  },
1867
1948
 
@@ -2180,15 +2261,26 @@ EOF`);
2180
2261
  * @memberof UnderpostRun
2181
2262
  */
2182
2263
  kill: (path = '', options = DEFAULT_OPTION) => {
2183
- if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
2264
+ if (options.pid)
2265
+ return shellExec(`sudo kill -9 ${options.pid}`, {
2266
+ silentOnError: true,
2267
+ });
2184
2268
  for (const _path of path.split(',')) {
2185
2269
  if (_path.split('+')[1]) {
2186
2270
  let [port, sumPortOffSet] = _path.split('+');
2187
2271
  port = parseInt(port);
2188
2272
  sumPortOffSet = parseInt(sumPortOffSet);
2189
2273
  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})`);
2274
+ shellExec(
2275
+ `PIDS=$(lsof -t -i:${parseInt(port) + parseInt(sumPort)}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`,
2276
+ {
2277
+ silentOnError: true,
2278
+ },
2279
+ );
2280
+ } else
2281
+ shellExec(`PIDS=$(lsof -t -i:${_path}); [ -n "$PIDS" ] && sudo kill -9 $PIDS || true`, {
2282
+ silentOnError: true,
2283
+ });
2192
2284
  }
2193
2285
  },
2194
2286
  /**
@@ -2235,9 +2327,10 @@ EOF`);
2235
2327
  * @memberof UnderpostRun
2236
2328
  */
2237
2329
  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);
2330
+ const cronDeployId = cronDeployIdResolve() || 'dd-cron';
2331
+ Underpost.secret.underpost.createFromEnvFile(
2332
+ `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
2333
+ );
2241
2334
  },
2242
2335
  /**
2243
2336
  * @method underpost-config
@@ -2545,6 +2638,57 @@ EOF`;
2545
2638
  }
2546
2639
  }
2547
2640
  },
2641
+
2642
+ /**
2643
+ * @method monitor-ui
2644
+ * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
2645
+ * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
2646
+ * @param {string} path - Unused.
2647
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2648
+ * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
2649
+ * @memberof UnderpostRun
2650
+ */
2651
+ 'monitor-ui': (path, options = DEFAULT_OPTION) => {
2652
+ if (options.remove) {
2653
+ shellExec(`sudo firewall-cmd --zone=public --remove-service=cockpit --permanent`);
2654
+ shellExec(`sudo firewall-cmd --reload`);
2655
+ return;
2656
+ }
2657
+ shellExec(`sudo dnf install -y cockpit cockpit-machines libvirt`);
2658
+ shellExec(`sudo systemctl enable --now cockpit.socket libvirtd`);
2659
+ shellExec(`sudo firewall-cmd --permanent --add-service=cockpit`);
2660
+ shellExec(`sudo firewall-cmd --reload`);
2661
+ },
2662
+
2663
+ /**
2664
+ * @method shared-dir
2665
+ * @description Run once for initial shared-directory setup. Creates the group, adds the user,
2666
+ * creates the directory, sets ownership, applies the SGID bit, and configures default ACLs so
2667
+ * all future files inside the directory automatically inherit group write permissions.
2668
+ * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
2669
+ * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
2670
+ * Customise via the `path` argument or leave empty to use the default.
2671
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2672
+ * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
2673
+ * @memberof UnderpostRun
2674
+ */
2675
+ 'shared-dir': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
2676
+ const dir = path || '/home/dd/engine';
2677
+ const user = options.user || 'admin';
2678
+ const group = options.group || 'engine-dev';
2679
+
2680
+ logger.info(`[setup-shared-dir] dir=${dir} user=${user} group=${group}`);
2681
+
2682
+ shellExec(`sudo groupadd ${group} 2>/dev/null || true`);
2683
+ shellExec(`sudo usermod -aG ${group} ${user}`);
2684
+ shellExec(`sudo mkdir -p ${dir}`);
2685
+ shellExec(`sudo chown -R ${user}:${group} ${dir}`);
2686
+ shellExec(`sudo chmod -R 2775 ${dir}`);
2687
+ shellExec(`sudo setfacl -d -m g:${group}:rwx ${dir}`);
2688
+ shellExec(`sudo setfacl -m g:${group}:rwx ${dir}`);
2689
+
2690
+ logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
2691
+ },
2548
2692
  };
2549
2693
 
2550
2694
  static API = {
@@ -2601,14 +2745,14 @@ EOF`;
2601
2745
  if (options.replicas === '' || options.replicas === null || options.replicas === undefined)
2602
2746
  options.replicas = 1;
2603
2747
  options.npmRoot = npmRoot;
2604
- logger.info('callback', { path, options });
2748
+ logger.info(`Executing runner`, { runner, namespace: options.namespace });
2605
2749
  if (!Underpost.run.RUNNERS.includes(runner)) throw new Error(`Runner not found: ${runner}`);
2606
2750
  const result = await Underpost.run.CALL(runner, path, options);
2607
2751
  return result;
2608
2752
  } catch (error) {
2609
2753
  console.log(error);
2610
2754
  logger.error(error);
2611
- return null;
2755
+ process.exit(1);
2612
2756
  }
2613
2757
  },
2614
2758
  };
@@ -26,17 +26,26 @@ class UnderpostSecret {
26
26
  * @memberof UnderpostSecret
27
27
  */
28
28
  underpost: {
29
- createFromEnvFile(envPath) {
29
+ /**
30
+ * @method createFromEnvFile
31
+ * @description Reads application secrets from a .env file and writes them to the underpost .env file. Used for local development and testing.
32
+ * @param {string} envPath - The path to the .env file to read secrets from. Defaults to './.env'.
33
+ * @memberof UnderpostSecret
34
+ */
35
+ createFromEnvFile(envPath = './.env') {
30
36
  Underpost.env.clean();
31
37
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
32
38
  for (const key of Object.keys(envObj)) {
33
39
  Underpost.env.set(key, envObj[key]);
34
40
  }
35
41
  },
36
- /** Reads application secrets from process.env (injected via envFrom: secretRef)
42
+ /**
43
+ * @method createFromContainerEnv
44
+ * @description Reads application secrets from process.env (injected via envFrom: secretRef)
37
45
  * and writes them to the underpost .env file, filtering out known system and
38
46
  * Kubernetes-injected environment variables. Replaces the fragile shell-based
39
47
  * `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
48
+ * @memberof UnderpostSecret
40
49
  */
41
50
  createFromContainerEnv() {
42
51
  Underpost.env.clean();
package/src/cli/test.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * @namespace UnderpostTest
5
5
  */
6
6
 
7
+ import fs from 'fs-extra';
7
8
  import { timer } from '../client/components/core/CommonJs.js';
8
9
  import { MariaDB } from '../db/mariadb/MariaDB.js';
9
10
  import { getNpmRootPath } from '../server/conf.js';
@@ -42,7 +43,13 @@ class UnderpostTest {
42
43
  */
43
44
  run() {
44
45
  actionInitLog();
45
- shellExec(`cd ${getNpmRootPath()}/underpost && npm run test`);
46
+ const underpostTestPath = `${getNpmRootPath()}/underpost`;
47
+ if (fs.existsSync(underpostTestPath)) {
48
+ shellExec(`cd ${underpostTestPath} && npm run test`);
49
+ } else {
50
+ logger.warn(`Global underpost not found at ${underpostTestPath}, running local npm test instead`);
51
+ shellExec('npm test');
52
+ }
46
53
  },
47
54
  /**
48
55
  * @method callback
@@ -148,8 +155,7 @@ class UnderpostTest {
148
155
  const pods = Underpost.kubectl.get(podName, kindType);
149
156
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
150
157
  logger.info(
151
- `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
152
- index + 1
158
+ `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${index + 1
153
159
  }/${maxAttempts}`,
154
160
  pods[0] ? pods[0].STATUS : 'Not found kind object',
155
161
  );
@@ -19,6 +19,12 @@ const DefaultTemplate = async () => {
19
19
  });
20
20
  });
21
21
  return html`
22
+ <style>
23
+ .feature-icon {
24
+ font-size: 2.5rem;
25
+ margin-bottom: 1rem;
26
+ }
27
+ </style>
22
28
  <div class="landing-container">
23
29
  <div class="content-wrapper">
24
30
  <h1 class="animated-text">
@@ -27,17 +33,17 @@ const DefaultTemplate = async () => {
27
33
  </h1>
28
34
  <div class="features">
29
35
  <div class="feature-card">
30
- <i class="icon">🚀</i>
36
+ <i class="fas fa-rocket feature-icon"></i>
31
37
  <h3>Fast &amp; Reliable</h3>
32
38
  <p>Lightning-fast performance with 99.9% uptime</p>
33
39
  </div>
34
40
  <div class="feature-card">
35
- <i class="icon">🎨</i>
41
+ <i class="fas fa-palette feature-icon"></i>
36
42
  <h3>Beautiful UI</h3>
37
43
  <p>Modern and intuitive user interface</p>
38
44
  </div>
39
45
  <div class="feature-card">
40
- <i class="icon">⚡</i>
46
+ <i class="fas fa-bolt feature-icon"></i>
41
47
  <h3>Powerful Features</h3>
42
48
  <p>Everything you need in one place</p>
43
49
  </div>
@@ -319,6 +319,11 @@ class Auth {
319
319
  // Close any open login/signup modals
320
320
  if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
321
321
  if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
322
+ if (!s(`.main-body-btn-ui-open`).classList.contains('hide')) s(`.main-body-btn-ui-open`).click();
323
+ if (!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')) {
324
+ SearchBox.Data.skipOpen = true;
325
+ s(`.main-body-btn-ui-bar-custom-open`).click();
326
+ }
322
327
  });
323
328
  }
324
329