cyberia 3.2.12 → 3.2.70

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 (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
package/src/cli/run.js CHANGED
@@ -15,13 +15,14 @@ import {
15
15
  getNpmRootPath,
16
16
  isDeployRunnerContext,
17
17
  loadConfServerJson,
18
+ loadReplicas,
18
19
  writeEnv,
19
20
  } from '../server/conf.js';
20
21
  import { actionInitLog, loggerFactory } from '../server/logger.js';
21
22
 
22
23
  import fs from 'fs-extra';
23
24
  import net from 'net';
24
- import { range, setPad, timer } from '../client/components/core/CommonJs.js';
25
+ import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
25
26
 
26
27
  import os from 'os';
27
28
  import Underpost from '../index.js';
@@ -51,10 +52,12 @@ const logger = loggerFactory(import.meta);
51
52
  /**
52
53
  * @constant DEFAULT_OPTION
53
54
  * @description Default options for the UnderpostRun class.
55
+ * @typedef {Object} UnderpostRunDefaultOptions
54
56
  * @type {Object}
55
57
  * @property {boolean} dev - Whether to run in development mode.
56
58
  * @property {string} podName - The name of the pod to run.
57
59
  * @property {string} nodeName - The name of the node to run.
60
+ * @property {string} sshKeyPath - Private key path for node SSH operations, forwarded to volume shipping over SSH.
58
61
  * @property {number} port - Custom port to use.
59
62
  * @property {string} volumeHostPath - The host path for the volume.
60
63
  * @property {string} volumeMountPath - The mount path for the volume.
@@ -119,12 +122,14 @@ const logger = loggerFactory(import.meta);
119
122
  * @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
120
123
  * @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
124
  * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
125
+ * @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
122
126
  * @memberof UnderpostRun
123
127
  */
124
128
  const DEFAULT_OPTION = {
125
129
  dev: false,
126
130
  podName: '',
127
131
  nodeName: '',
132
+ sshKeyPath: '',
128
133
  port: 0,
129
134
  volumeHostPath: '',
130
135
  volumeMountPath: '',
@@ -187,6 +192,7 @@ const DEFAULT_OPTION = {
187
192
  skipFullBuild: false,
188
193
  pullBundle: false,
189
194
  remove: false,
195
+ test: false,
190
196
  };
191
197
 
192
198
  /**
@@ -210,7 +216,7 @@ class UnderpostRun {
210
216
  * @method dev-cluster
211
217
  * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
212
218
  * @param {string} path - The input value, identifier, or path for the operation.
213
- * @param {Object} options - The default underpost runner options for customizing workflow
219
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
214
220
  * @memberof UnderpostRun
215
221
  */
216
222
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
@@ -222,7 +228,7 @@ class UnderpostRun {
222
228
  shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
223
229
 
224
230
  shellExec(
225
- `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb4 --service-host ${mongoHosts.join(
231
+ `${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
226
232
  ',',
227
233
  )} --pull-image`,
228
234
  );
@@ -264,6 +270,16 @@ class UnderpostRun {
264
270
  logger.info(hostListenResult.renderHosts);
265
271
  },
266
272
 
273
+ /**
274
+ * @method etc-hosts
275
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
276
+ * based on the provided path input.
277
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
278
+ */
279
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
280
+ etcHostFactory(path.split(','));
281
+ },
282
+
267
283
  /**
268
284
  * @method ipfs-expose
269
285
  * @description Exposes IPFS Cluster services on specified ports for local access.
@@ -282,7 +298,7 @@ class UnderpostRun {
282
298
  * @method metadata
283
299
  * @description Generates metadata for the specified path after exposing the development cluster.
284
300
  * @param {string} path - The input value, identifier, or path for the operation.
285
- * @param {Object} options - The default underpost runner options for customizing workflow
301
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
286
302
  * @memberof UnderpostRun
287
303
  */
288
304
  metadata: async (path, options = DEFAULT_OPTION) => {
@@ -307,7 +323,7 @@ class UnderpostRun {
307
323
  * @method svc-ls
308
324
  * @description Lists systemd services and installed packages, optionally filtering by the provided path.
309
325
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional filter for services and packages).
310
- * @param {Object} options - The default underpost runner options for customizing workflow
326
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
311
327
  * @memberof UnderpostRun
312
328
  */
313
329
  'svc-ls': (path, options = DEFAULT_OPTION) => {
@@ -327,7 +343,7 @@ class UnderpostRun {
327
343
  * @method svc-rm
328
344
  * @description Removes a systemd service by stopping it, disabling it, uninstalling the package, and deleting related files.
329
345
  * @param {string} path - The input value, identifier, or path for the operation (used as the service name).
330
- * @param {Object} options - The default underpost runner options for customizing workflow
346
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
331
347
  * @memberof UnderpostRun
332
348
  */
333
349
  'svc-rm': (path, options = DEFAULT_OPTION) => {
@@ -342,7 +358,7 @@ class UnderpostRun {
342
358
  * @method ssh-deploy-info
343
359
  * @description Retrieves deployment status and pod information from a remote server via SSH.
344
360
  * @param {string} path - The input value, identifier, or path for the operation.
345
- * @param {Object} options - The default underpost runner options for customizing workflow
361
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
346
362
  * @memberof UnderpostRun
347
363
  */
348
364
  'ssh-deploy-info': async (path = '', options = DEFAULT_OPTION) => {
@@ -360,11 +376,233 @@ class UnderpostRun {
360
376
  );
361
377
  },
362
378
 
379
+ /**
380
+ * @method node-move
381
+ * @description Abstract runner that relocates any schedulable Kubernetes workload
382
+ * (Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob, ReplicationController)
383
+ * onto a target node by patching its pod-template `nodeSelector` and rolling it out.
384
+ * Resource-kind agnostic: it resolves the kind dynamically and applies the right
385
+ * patch path, so it works for `sts`, `deployment`, etc. without bespoke logic.
386
+ *
387
+ * Selection grammar via `path`:
388
+ * - `<kind>/<name>` -> a single resource (e.g. `deployment/dd-core-production-blue`)
389
+ * - `<kind>` -> every resource of that kind in the namespace (e.g. `statefulset`)
390
+ * - `` -> all movable workloads (deployment, statefulset, daemonset) in the namespace
391
+ *
392
+ * Placement:
393
+ * - default: built-in `kubernetes.io/hostname=<node>` (no node mutation required)
394
+ * - `--labels k=v,...`: label the target node with those pairs and use them as the
395
+ * nodeSelector (matches the "label node + nodeSelector" pattern), enabling reusable
396
+ * workload pools instead of pinning to a single hostname.
397
+ *
398
+ * Flags: `--node-name <node>` (target), `--namespace <ns>`, `--dry-run` (preview only),
399
+ * `--remove` (clear the nodeSelector / unpin placement).
400
+ *
401
+ * Caveats: Services/ConfigMaps and bare Pods are not schedulable controllers and are
402
+ * skipped (move the owning controller). StatefulSets bound to node-local PVs may stay
403
+ * Pending after a move until their volume is available on the target node.
404
+ * @param {string} path - Resource selector (`kind/name`, `kind`, or empty).
405
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
406
+ * @memberof UnderpostRun
407
+ * @returns {Array<{ref:string,kind:string,status:string,node?:string}>} Per-resource outcome.
408
+ */
409
+ 'node-move': (path = '', options = DEFAULT_OPTION) => {
410
+ const node = options.nodeName;
411
+ const ns = options.namespace || 'default';
412
+ const dryRun = options.dryRun === true;
413
+ const remove = options.remove === true;
414
+
415
+ if (!remove && !node) {
416
+ throw new Error('node-move requires --node-name <target-node> (or --remove to clear placement)');
417
+ }
418
+
419
+ const normalizeKind = (k) =>
420
+ ({
421
+ deploy: 'deployment',
422
+ deployments: 'deployment',
423
+ deployment: 'deployment',
424
+ sts: 'statefulset',
425
+ statefulsets: 'statefulset',
426
+ statefulset: 'statefulset',
427
+ ds: 'daemonset',
428
+ daemonsets: 'daemonset',
429
+ daemonset: 'daemonset',
430
+ rs: 'replicaset',
431
+ replicasets: 'replicaset',
432
+ replicaset: 'replicaset',
433
+ rc: 'replicationcontroller',
434
+ replicationcontroller: 'replicationcontroller',
435
+ job: 'job',
436
+ jobs: 'job',
437
+ cj: 'cronjob',
438
+ cronjob: 'cronjob',
439
+ cronjobs: 'cronjob',
440
+ po: 'pod',
441
+ pod: 'pod',
442
+ pods: 'pod',
443
+ svc: 'service',
444
+ service: 'service',
445
+ services: 'service',
446
+ })[k] || k;
447
+
448
+ // Kinds that own a pod template we can patch; rolloutKinds additionally
449
+ // support `kubectl rollout restart` to reschedule existing pods now.
450
+ const templated = [
451
+ 'deployment',
452
+ 'statefulset',
453
+ 'daemonset',
454
+ 'replicaset',
455
+ 'job',
456
+ 'cronjob',
457
+ 'replicationcontroller',
458
+ ];
459
+ const rolloutKinds = ['deployment', 'statefulset', 'daemonset'];
460
+ const templateSelectorPath = (kind) =>
461
+ kind === 'cronjob'
462
+ ? ['spec', 'jobTemplate', 'spec', 'template', 'spec', 'nodeSelector']
463
+ : ['spec', 'template', 'spec', 'nodeSelector'];
464
+
465
+ // Resolve the desired nodeSelector. Custom --labels enables reusable pools;
466
+ // otherwise pin by the always-present hostname label.
467
+ let selector = { 'kubernetes.io/hostname': node };
468
+ if (!remove && options.labels) {
469
+ selector = {};
470
+ for (const pair of `${options.labels}`
471
+ .split(',')
472
+ .map((s) => s.trim())
473
+ .filter(Boolean)) {
474
+ const eq = pair.indexOf('=');
475
+ if (eq < 0) continue;
476
+ selector[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
477
+ }
478
+ }
479
+
480
+ // Verify the target node exists, and apply custom labels to it if provided.
481
+ if (!remove) {
482
+ const found = shellExec(`kubectl get node ${node} -o name`, {
483
+ silent: true,
484
+ stdout: true,
485
+ silentOnError: true,
486
+ }).trim();
487
+ if (!found) throw new Error(`Target node not found: ${node}`);
488
+ if (options.labels) {
489
+ const labelArgs = Object.entries(selector)
490
+ .map(([k, v]) => `${k}=${v}`)
491
+ .join(' ');
492
+ const labelCmd = `kubectl label node ${node} ${labelArgs} --overwrite`;
493
+ if (dryRun) logger.info(`[dry-run] ${labelCmd}`);
494
+ else shellExec(labelCmd);
495
+ }
496
+ }
497
+
498
+ const kubectlNames = (kind) =>
499
+ (
500
+ shellExec(`kubectl get ${kind} -n ${ns} -o name`, {
501
+ silent: true,
502
+ stdout: true,
503
+ silentOnError: true,
504
+ }).trim() || ''
505
+ )
506
+ .split('\n')
507
+ .map((s) => s.trim())
508
+ .filter(Boolean);
509
+
510
+ // Build the list of "kind/name" targets from the selection grammar.
511
+ let targets = [];
512
+ if (!path) {
513
+ for (const kind of ['deployment', 'statefulset', 'daemonset']) targets.push(...kubectlNames(kind));
514
+ } else if (path.includes('/')) {
515
+ targets = [path];
516
+ } else {
517
+ targets = kubectlNames(path);
518
+ }
519
+
520
+ if (targets.length === 0) {
521
+ logger.warn('node-move: no matching resources found', { path, namespace: ns });
522
+ return [];
523
+ }
524
+
525
+ // Merge-patch body that sets (or clears, when --remove) the nodeSelector.
526
+ const buildPatch = (kind) => {
527
+ const keys = templateSelectorPath(kind);
528
+ let obj = remove ? null : selector;
529
+ for (let i = keys.length - 1; i >= 0; i--) obj = { [keys[i]]: obj };
530
+ return JSON.stringify(obj);
531
+ };
532
+
533
+ const results = [];
534
+ for (const ref of targets) {
535
+ const slash = ref.indexOf('/');
536
+ const rawKind = slash >= 0 ? ref.slice(0, slash) : path && !path.includes('/') ? path : '';
537
+ const name = slash >= 0 ? ref.slice(slash + 1) : ref;
538
+ const kind = normalizeKind(`${rawKind}`.split('.')[0].toLowerCase());
539
+
540
+ if (!templated.includes(kind)) {
541
+ logger.warn(`node-move: ${kind}/${name} is not a schedulable controller; skipping`, {
542
+ hint: 'move its owning controller (deployment/statefulset/daemonset) instead',
543
+ });
544
+ results.push({ ref, kind, status: 'skipped' });
545
+ continue;
546
+ }
547
+
548
+ // Idempotency: skip the patch + rollout if the resource is already where
549
+ // we want it. Compares the live pod-template nodeSelector against the
550
+ // desired placement so a repeated run does not trigger an unnecessary
551
+ // rollout restart.
552
+ const basePath = kind === 'cronjob' ? 'spec.jobTemplate.spec.template.spec' : 'spec.template.spec';
553
+ const jsonpath = (expr) =>
554
+ shellExec(`kubectl get ${kind} ${name} -n ${ns} -o jsonpath='${expr}'`, {
555
+ silent: true,
556
+ stdout: true,
557
+ silentOnError: true,
558
+ disableLog: true,
559
+ }).trim();
560
+
561
+ if (remove) {
562
+ const current = jsonpath(`{.${basePath}.nodeSelector}`);
563
+ if (!current || current === 'map[]') {
564
+ logger.info(`node-move: ${kind}/${name} already has no nodeSelector; nothing to clear`);
565
+ results.push({ ref, kind, status: 'already-cleared' });
566
+ continue;
567
+ }
568
+ } else {
569
+ const alreadyOnNode = Object.entries(selector).every(([k, v]) => {
570
+ const esc = k.replace(/\./g, '\\.');
571
+ return jsonpath(`{.${basePath}.nodeSelector.${esc}}`) === v;
572
+ });
573
+ if (alreadyOnNode) {
574
+ logger.info(`node-move: ${kind}/${name} already pinned to ${node}; skipping`, { namespace: ns });
575
+ results.push({ ref, kind, status: 'already-on-node', node });
576
+ continue;
577
+ }
578
+ }
579
+
580
+ const patchCmd = `kubectl patch ${kind} ${name} -n ${ns} --type=merge -p '${buildPatch(kind)}'`;
581
+ const restartCmd = `kubectl rollout restart ${kind} ${name} -n ${ns}`;
582
+ if (dryRun) {
583
+ logger.info(`[dry-run] ${patchCmd}`);
584
+ if (rolloutKinds.includes(kind)) logger.info(`[dry-run] ${restartCmd}`);
585
+ results.push({ ref, kind, status: 'dry-run', node: remove ? undefined : node });
586
+ continue;
587
+ }
588
+
589
+ shellExec(patchCmd);
590
+ if (rolloutKinds.includes(kind)) shellExec(restartCmd);
591
+ logger.info(remove ? `Cleared node placement: ${kind}/${name}` : `Moved ${kind}/${name} -> ${node}`, {
592
+ namespace: ns,
593
+ });
594
+ results.push({ ref, kind, status: remove ? 'cleared' : 'moved', node: remove ? undefined : node });
595
+ }
596
+
597
+ logger.info('node-move complete', { namespace: ns, node: remove ? null : node, count: results.length });
598
+ return results;
599
+ },
600
+
363
601
  /**
364
602
  * @method dev-hosts-expose
365
603
  * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
366
604
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
367
- * @param {Object} options - The default underpost runner options for customizing workflow
605
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
368
606
  * @memberof UnderpostRun
369
607
  */
370
608
  'dev-hosts-expose': (path, options = DEFAULT_OPTION) => {
@@ -377,7 +615,7 @@ class UnderpostRun {
377
615
  * @method dev-hosts-restore
378
616
  * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
379
617
  * @param {string} path - The input value, identifier, or path for the operation.
380
- * @param {Object} options - The default underpost runner options for customizing workflow
618
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
381
619
  * @memberof UnderpostRun
382
620
  */
383
621
  'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
@@ -388,11 +626,13 @@ class UnderpostRun {
388
626
  * @method cluster-build
389
627
  * @description Build configuration for cluster deployment.
390
628
  * @param {string} path - The input value, identifier, or path for the operation.
391
- * @param {Object} options - The default underpost runner options for customizing workflow
629
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
392
630
  * @memberof UnderpostRun
393
631
  */
394
632
  'cluster-build': (path, options = DEFAULT_OPTION) => {
395
- const nodeOptions = options.nodeName ? ` --node-name ${options.nodeName}` : '';
633
+ const nodeOptions =
634
+ (options.nodeName ? ` --node-name ${options.nodeName}` : '') +
635
+ (options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
396
636
  shellExec(`node bin run clean`);
397
637
  shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
398
638
  shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
@@ -410,7 +650,7 @@ class UnderpostRun {
410
650
  * and optionally triggers engine-<conf-id> CI with sync/init which in turn dispatches the CD workflow
411
651
  * after the build chain completes (template → ghpkg → engine-<conf-id> → CD).
412
652
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
413
- * @param {Object} options - The default underpost runner options for customizing workflow
653
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
414
654
  * @memberof UnderpostRun
415
655
  */
416
656
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
@@ -422,14 +662,12 @@ class UnderpostRun {
422
662
  return;
423
663
  }
424
664
  shellExec(`${baseCommand} run pull`);
665
+ shellExec(`${baseCommand} run shared-dir`);
425
666
 
426
- // Capture last N commit messages for propagation.
427
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
428
- const fromN =
429
- options.fromNCommit && parseInt(options.fromNCommit) > 0
430
- ? parseInt(options.fromNCommit)
431
- : Underpost.repo.getUnpushedCount('.').count;
432
- const message = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
667
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
668
+ // propagation to pwa-microservices-template and every engine-* repo.
669
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
670
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
433
671
  silent: true,
434
672
  stdout: true,
435
673
  }).trim();
@@ -441,8 +679,6 @@ class UnderpostRun {
441
679
  );
442
680
  shellCd('/home/dd/engine');
443
681
 
444
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(message);
445
-
446
682
  // Push engine repo so workflow YAML changes reach GitHub
447
683
  shellExec(`git reset`);
448
684
  shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
@@ -490,7 +726,7 @@ class UnderpostRun {
490
726
  * @method template-deploy-local
491
727
  * @description Similar to `template-deploy` but runs the workflow locally without dispatching GitHub Actions. It pulls the latest changes, pushes to GitHub, builds the template, and optionally triggers a local release with CI push.
492
728
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
493
- * @param {Object} options - The default underpost runner options for customizing workflow
729
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
494
730
  * @memberof UnderpostRun
495
731
  */
496
732
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
@@ -502,18 +738,14 @@ class UnderpostRun {
502
738
  return;
503
739
  }
504
740
  shellExec(`${baseCommand} run pull`);
741
+ shellExec(`${baseCommand} run shared-dir`);
505
742
 
506
- // Capture last N commit messages from the engine repo.
507
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
508
- const fromN =
509
- options.fromNCommit && parseInt(options.fromNCommit) > 0
510
- ? parseInt(options.fromNCommit)
511
- : Underpost.repo.getUnpushedCount('.').count;
512
- const rawMessage = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
743
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
744
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
745
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
513
746
  silent: true,
514
747
  stdout: true,
515
748
  }).trim();
516
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
517
749
 
518
750
  const { triggerCmd } = path
519
751
  ? await Underpost.release.ci(path, sanitizedMessage, options)
@@ -525,15 +757,14 @@ class UnderpostRun {
525
757
  * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
526
758
  * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
527
759
  * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
528
- * @param {Object} options - The default underpost runner options for customizing workflow
760
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
529
761
  * @memberof UnderpostRun
530
762
  */
531
763
  'docker-image': (path, options = DEFAULT_OPTION) => {
532
- const repo = Underpost.repo.resolveInstanceRepo(path);
764
+ const repo = Underpost.repo.resolveInstanceRepo(path, options.dev);
533
765
  Underpost.repo.dispatchWorkflow({
534
766
  repo,
535
- workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
536
- ref: 'master',
767
+ workflowFile: `docker-image${path ? `.${path}` : ''}${options.dev ? '.dev' : ''}.ci.yml`,
537
768
  inputs: {},
538
769
  });
539
770
  },
@@ -541,7 +772,7 @@ class UnderpostRun {
541
772
  * @method clean
542
773
  * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
543
774
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional directory path).
544
- * @param {Object} options - The default underpost runner options for customizing workflow
775
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
545
776
  * @memberof UnderpostRun
546
777
  */
547
778
  clean: (path = '', options = DEFAULT_OPTION) => {
@@ -552,7 +783,7 @@ class UnderpostRun {
552
783
  * @method pull
553
784
  * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
554
785
  * @param {string} path - The input value, identifier, or path for the operation.
555
- * @param {Object} options - The default underpost runner options for customizing workflow
786
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
556
787
  * @memberof UnderpostRun
557
788
  */
558
789
  pull: (path, options = DEFAULT_OPTION) => {
@@ -579,7 +810,7 @@ class UnderpostRun {
579
810
  * @method release-deploy
580
811
  * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
581
812
  * @param {string} path - The input value, identifier, or path for the operation.
582
- * @param {Object} options - The default underpost runner options for customizing workflow
813
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
583
814
  * @memberof UnderpostRun
584
815
  */
585
816
  'release-deploy': (path, options = DEFAULT_OPTION) => {
@@ -595,7 +826,7 @@ class UnderpostRun {
595
826
  * @method ssh-deploy
596
827
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
597
828
  * @param {string} path - The deployment identifier (e.g., 'engine-core', 'sync-engine-core', 'init-engine-core').
598
- * @param {Object} options - The default underpost runner options for customizing workflow
829
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
599
830
  * @memberof UnderpostRun
600
831
  */
601
832
  'ssh-deploy': (path, options = DEFAULT_OPTION) => {
@@ -610,9 +841,9 @@ class UnderpostRun {
610
841
  job = 'init';
611
842
  confId = path.replace(/^init-/, '');
612
843
  }
613
-
844
+ const repo = Underpost.repo.resolveInstanceRepo(confId, options.dev);
614
845
  Underpost.repo.dispatchWorkflow({
615
- repo: `${process.env.GITHUB_USERNAME}/engine`,
846
+ repo,
616
847
  workflowFile: `${confId}.cd.yml`,
617
848
  ref: 'master',
618
849
  inputs: { job },
@@ -623,7 +854,7 @@ class UnderpostRun {
623
854
  * @description Opens a Visual Studio Code (VS Code) session for the specified path using `node ${underpostRoot}/bin/zed ${path}`,
624
855
  * or installs Zed and sublime-text IDE if `path` is 'install'.
625
856
  * @param {string} path - The input value, identifier, or path for the operation (used as the path to the directory to open in the IDE).
626
- * @param {Object} options - The default underpost runner options for customizing workflow
857
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
627
858
  * @memberof UnderpostRun
628
859
  */
629
860
  ide: (path = '', options = DEFAULT_OPTION) => {
@@ -650,7 +881,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
650
881
  * @method crypto-policy
651
882
  * @description Sets the system's crypto policies to `DEFAULT:SHA1` using `update-crypto-policies` command.
652
883
  * @param {string} path - The input value, identifier, or path for the operation.
653
- * @param {Object} options - The default underpost runner options for customizing workflow
884
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
654
885
  * @memberof UnderpostRun
655
886
  */
656
887
  'crypto-policy': (path, options = DEFAULT_OPTION) => {
@@ -665,7 +896,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
665
896
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
666
897
  * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
667
898
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
668
- * @param {Object} options - The default underpost runner options for customizing workflow
899
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
669
900
  * @memberof UnderpostRun
670
901
  */
671
902
  sync: async (path, options = DEFAULT_OPTION) => {
@@ -686,7 +917,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
686
917
  replicas = replicas ? replicas : defaultPath[1];
687
918
  versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
688
919
  image = image ? image : defaultPath[3];
689
- node = node ? node : defaultPath[4];
920
+ node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
690
921
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
691
922
 
692
923
  if (image && !image.startsWith('localhost'))
@@ -713,6 +944,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
713
944
  let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
714
945
  if (targetTraffic) versions = versions ? versions : targetTraffic;
715
946
 
947
+ const ignorePods =
948
+ isDeployRunnerContext(path, options) && targetTraffic
949
+ ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
950
+ : [];
951
+
716
952
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
717
953
  const cmdString = options.cmd
718
954
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
@@ -722,13 +958,14 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
722
958
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
723
959
  const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
724
960
  const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
961
+ const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
725
962
 
726
963
  shellExec(
727
964
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
728
965
  image ? ` --image ${image}` : ''
729
966
  }${versions ? ` --versions ${versions}` : ''}${
730
967
  options.namespace ? ` --namespace ${options.namespace}` : ''
731
- }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
968
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag} ${deployId} ${env}`,
732
969
  );
733
970
 
734
971
  if (isDeployRunnerContext(path, options)) {
@@ -737,13 +974,13 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
737
974
  `${baseCommand} db ${deployId} ${clusterFlag}${baseClusterCommand} --repo-backup --primary-pod --git --force-clone --preserveUUID ${options.namespace ? ` --ns ${options.namespace}` : ''}`,
738
975
  );
739
976
  shellExec(
740
- `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
977
+ `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --node ${node} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
741
978
  options.namespace ? ` --namespace ${options.namespace}` : ''
742
- }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
979
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}${sshKeyPathFlag}`,
743
980
  );
744
981
  if (!targetTraffic)
745
982
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
746
- await Underpost.deploy.monitorReadyRunner(deployId, env, targetTraffic, [], options.namespace, 'underpost');
983
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
747
984
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
748
985
  } else
749
986
  logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
@@ -753,7 +990,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
753
990
  * @method stop
754
991
  * @description Stops a deployment by deleting the corresponding Kubernetes deployment and service resources.
755
992
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
756
- * @param {Object} options - The default underpost runner options for customizing workflow
993
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
757
994
  * @memberof UnderpostRun
758
995
  */
759
996
  stop: async (path = '', options = DEFAULT_OPTION) => {
@@ -777,7 +1014,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
777
1014
  * @method ssh-deploy-stop
778
1015
  * @description Stops a remote deployment via SSH by executing the appropriate Underpost command on the remote server.
779
1016
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
780
- * @param {Object} options - The default underpost runner options for customizing workflow
1017
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
781
1018
  * @memberof UnderpostRun
782
1019
  */
783
1020
  'ssh-deploy-stop': async (path, options = DEFAULT_OPTION) => {
@@ -804,7 +1041,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
804
1041
  * @method ssh-deploy-db-rollback
805
1042
  * @description Performs a database rollback on remote deployment via SSH.
806
1043
  * @param {string} path - Comma-separated deployId and optional number of commits to reset (format: "deployId,nCommits")
807
- * @param {Object} options - The default underpost runner options for customizing workflow
1044
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
808
1045
  * @param {string} options.deployId - The deployment identifier
809
1046
  * @param {string} options.user - The SSH user for credential lookup
810
1047
  * @param {boolean} options.dev - Development mode flag
@@ -831,7 +1068,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
831
1068
  * @method ssh-deploy-db
832
1069
  * @description Imports/restores a database on remote deployment via SSH.
833
1070
  * @param {string} path - The deployment ID for database import
834
- * @param {Object} options - The default underpost runner options for customizing workflow
1071
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
835
1072
  * @param {string} options.deployId - The deployment identifier
836
1073
  * @param {string} options.user - The SSH user for credential lookup
837
1074
  * @param {boolean} options.dev - Development mode flag
@@ -856,7 +1093,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
856
1093
  * @method ssh-deploy-db-status
857
1094
  * @description Retrieves database status/stats for a deployment (or all deployments from dd.router) via SSH.
858
1095
  * @param {string} path - Comma-separated deployId(s) or 'dd' to use the dd.router list.
859
- * @param {Object} options - Runner options (uses options.deployId for SSH host lookup).
1096
+ * @param {UnderpostRunDefaultOptions} options - Runner options (uses options.deployId for SSH host lookup).
860
1097
  * @param {string} options.deployId - Deployment identifier used for SSH config lookup.
861
1098
  * @param {string} options.user - SSH user for credential lookup.
862
1099
  * @param {boolean} options.dev - Development mode flag.
@@ -902,7 +1139,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
902
1139
  * @method tz
903
1140
  * @description Sets the system timezone using `timedatectl set-timezone` command.
904
1141
  * @param {string} path - The input value, identifier, or path for the operation (used as the timezone string).
905
- * @param {Object} options - The default underpost runner options for customizing workflow
1142
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
906
1143
  * @memberof UnderpostRun
907
1144
  */
908
1145
  tz: (path, options = DEFAULT_OPTION) => {
@@ -923,7 +1160,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
923
1160
  * @method get-proxy
924
1161
  * @description Retrieves and logs the HTTPProxy resources in the specified namespace using `kubectl get HTTPProxy`.
925
1162
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional filter for the HTTPProxy resources).
926
- * @param {Object} options - The default underpost runner options for customizing workflow
1163
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
927
1164
  * @memberof UnderpostRun
928
1165
  */
929
1166
  'get-proxy': async (path = '', options = DEFAULT_OPTION) => {
@@ -989,8 +1226,19 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
989
1226
  // pathRewritePolicy,
990
1227
  });
991
1228
  if (options.tls) {
992
- shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
993
- proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1229
+ if (options.test) {
1230
+ const sslDir = `./engine-private/ssl/${_host}`;
1231
+ const nameSafe = _host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1232
+ fs.mkdirpSync(sslDir);
1233
+ shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${_host}"`);
1234
+ shellExec(`kubectl delete secret ${_host} -n ${options.namespace} --ignore-not-found`);
1235
+ shellExec(
1236
+ `kubectl create secret tls ${_host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1237
+ );
1238
+ } else {
1239
+ shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
1240
+ proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1241
+ }
994
1242
  }
995
1243
  // console.log(proxyYaml);
996
1244
  shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
@@ -1019,7 +1267,7 @@ EOF
1019
1267
  /**
1020
1268
  * @method instance
1021
1269
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1022
- * @param {Object} options - The default underpost runner options for customizing workflow
1270
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1023
1271
  * @memberof UnderpostRun
1024
1272
  */
1025
1273
  instance: async (path = '', options = DEFAULT_OPTION) => {
@@ -1059,6 +1307,7 @@ EOF
1059
1307
  // Examples images:
1060
1308
  // `underpost/underpost-engine:${Underpost.version}`
1061
1309
  // `localhost/rockylinux9-underpost:${Underpost.version}`
1310
+ if (options.imageName) _image = options.imageName;
1062
1311
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
1063
1312
 
1064
1313
  if (_image && !_image.startsWith('localhost'))
@@ -1087,9 +1336,16 @@ EOF
1087
1336
  deployId: _deployId,
1088
1337
  env,
1089
1338
  version: targetTraffic,
1090
- nodeName: options.nodeName,
1339
+ nodeName: Underpost.deploy.resolveDeployNode({
1340
+ node: options.nodeName,
1341
+ kind: options.kind,
1342
+ kubeadm: options.kubeadm,
1343
+ k3s: options.k3s,
1344
+ env,
1345
+ }),
1091
1346
  clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
1092
1347
  gitClean: options.gitClean || false,
1348
+ sshKeyPath: options.sshKeyPath || '',
1093
1349
  });
1094
1350
  // Regenerate the parent deploy's gRPC ClusterIP service pointing to the
1095
1351
  // parent's current traffic colour and apply it before the instance pod starts so
@@ -1154,12 +1410,16 @@ EOF
1154
1410
  `,
1155
1411
  { disableLog: true },
1156
1412
  );
1157
- const { ready, readyPods } = await Underpost.deploy.monitorReadyRunner(
1413
+ // Custom instances run a bare binary (no `underpost start` / internal
1414
+ // HTTP endpoint): Kubernetes readiness is the running signal and
1415
+ // container-status is read via exec. See `Deploy custom instance to K8S.md`.
1416
+ const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1158
1417
  _deployId,
1159
1418
  env,
1160
1419
  targetTraffic,
1161
1420
  ignorePods,
1162
1421
  options.namespace,
1422
+ { readyGate: 'kubernetes', statusTransport: 'exec' },
1163
1423
  );
1164
1424
 
1165
1425
  if (!ready) {
@@ -1169,7 +1429,7 @@ EOF
1169
1429
  shellExec(
1170
1430
  `${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
1171
1431
  `${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
1172
- `${options.tls ? ` --tls` : ''}` +
1432
+ `${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
1173
1433
  ` instance-promote '${path}'`,
1174
1434
  );
1175
1435
  }
@@ -1179,12 +1439,39 @@ EOF
1179
1439
  }
1180
1440
  },
1181
1441
 
1442
+ /**
1443
+ * @method deploy-key
1444
+ * @description Copies the deploy key for a specific user and deployId to a temporary location on the local machine.
1445
+ * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
1446
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1447
+ * @param {string} options.user - The user for which to copy the deploy key.
1448
+ * @param {string} options.deployId - The deployment identifier associated with the deploy key.
1449
+ * @memberof UnderpostRun
1450
+ */
1451
+ 'deploy-key': (path, options = DEFAULT_OPTION) => {
1452
+ const prefix = 'dd-key';
1453
+ if (options.reset) {
1454
+ shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
1455
+ return;
1456
+ }
1457
+ if (!options.user || !options.deployId) {
1458
+ logger.error('Both --user and --deploy-id options are required to copy the deploy key.');
1459
+ return;
1460
+ }
1461
+ const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
1462
+ fs.mkdirSync('/home/dd/tmp', { recursive: true });
1463
+ fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
1464
+ logger.info(`Copied deploy key to ${targetPath}`);
1465
+ if (options.copy) pbcopy(targetPath);
1466
+ },
1467
+
1182
1468
  /**
1183
1469
  * @method instance-build-manifest
1184
1470
  * @description Builds a Kubernetes Deployment + Service manifest for a specific instance entry
1185
- * from `conf.instances.json` and writes it to a file.
1186
- * Traffic colour is automatically chosen as the opposite of the current live colour (blue/green),
1187
- * defaulting to `blue` when no deployment is running yet.
1471
+ * from `conf.instances.json` and writes it to a file. This is a purely local
1472
+ * artifact generator: it never probes a live cluster. Traffic colour defaults
1473
+ * to the canonical initial `blue` and can be overridden with `--traffic`; the
1474
+ * real blue/green swap is resolved at deploy time (`deploy --sync`).
1188
1475
  *
1189
1476
  * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
1190
1477
  * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
@@ -1195,7 +1482,7 @@ EOF
1195
1482
  * `<projectPath>/manifests/<env>/deployment.yaml`.
1196
1483
  * In production, files are also copied to `<projectPath>/Dockerfile` and
1197
1484
  * `<projectPath>/deployment.yaml`.
1198
- * @param {Object} options - The default underpost runner options for customizing workflow
1485
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1199
1486
  * @memberof UnderpostRun
1200
1487
  */
1201
1488
  'instance-build-manifest': (path, options = DEFAULT_OPTION) => {
@@ -1274,7 +1561,7 @@ EOF
1274
1561
  path: projectPath,
1275
1562
  imageName: _image,
1276
1563
  podmanSave: true,
1277
- imagePath: projectPath,
1564
+ imageOutPath: projectPath,
1278
1565
  kind: isKind,
1279
1566
  kubeadm: !!options.kubeadm,
1280
1567
  k3s: !!options.k3s,
@@ -1287,15 +1574,8 @@ EOF
1287
1574
  });
1288
1575
  }
1289
1576
 
1290
- // Determine target traffic: opposite of current, or 'blue' if nothing is running yet.
1291
- const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1292
- hostTest: _host,
1293
- namespace: options.namespace,
1294
- });
1295
- const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1296
-
1297
- // Resolve {{grpc-service-dns}} using the parent deploy's current (or default) traffic.
1298
- const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1577
+ const targetTraffic = options.traffic || 'blue';
1578
+ const parentTraffic = targetTraffic;
1299
1579
  const resolvedCmd = _cmd[env].map((c) =>
1300
1580
  c.replaceAll(
1301
1581
  '{{grpc-service-dns}}',
@@ -1343,11 +1623,86 @@ EOF
1343
1623
  image: _image,
1344
1624
  });
1345
1625
 
1626
+ // --- Sibling manifests (pv-pvc, proxy, grpc-service) ------------------
1627
+ // Emit the same apply-able set the parent deploy writes to build/<env>,
1628
+ // scoped to this instance so the project repo and engine-private ship
1629
+ // more than just deployment.yaml. Content matches what `run instance`
1630
+ // creates dynamically at deploy time (deployVolume / instance-promote /
1631
+ // the parent gRPC ClusterIP), so a static `kubectl apply` is equivalent.
1632
+ const pvDataNode = Underpost.deploy.resolveDeployNode({
1633
+ node: options.nodeName,
1634
+ kind: options.kind,
1635
+ kubeadm: options.kubeadm,
1636
+ k3s: options.k3s,
1637
+ env,
1638
+ });
1639
+
1640
+ // pv-pvc.yaml — one PV+PVC per instance volume; names mirror deployVolume.
1641
+ let pvPvcYaml = '';
1642
+ for (const volume of _volumes || []) {
1643
+ if (!volume.claimName) continue;
1644
+ const pvcId = `${volume.claimName}-${_deployId}-${env}-${targetTraffic}`;
1645
+ const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${_deployId}-${env}-${targetTraffic}`;
1646
+ pvPvcYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
1647
+ pvcId,
1648
+ namespace: options.namespace,
1649
+ hostPath: `/home/dd/engine/volume/${pvId}`,
1650
+ nodeName: pvDataNode,
1651
+ })}\n`;
1652
+ }
1653
+
1654
+ // proxy.yaml — HTTPProxy for the instance host (mirrors instance-promote).
1655
+ const proxyYaml =
1656
+ Underpost.deploy.baseProxyYamlFactory({ host: _host, env, options }) +
1657
+ Underpost.deploy.deploymentYamlServiceFactory({
1658
+ path: _path,
1659
+ port: _fromPort,
1660
+ deployId: _deployId,
1661
+ env,
1662
+ deploymentVersions: [targetTraffic],
1663
+ });
1664
+
1665
+ // grpc-service.yaml — the parent deploy's gRPC ClusterIP (shared; the
1666
+ // instance cmd resolves {{grpc-service-dns}} to it). Reuse the parent's
1667
+ // generated manifest when present rather than regenerating it here.
1668
+ const parentGrpcServicePath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
1669
+ const grpcServiceYaml = fs.existsSync(parentGrpcServicePath)
1670
+ ? fs.readFileSync(parentGrpcServicePath, 'utf8')
1671
+ : '';
1672
+
1673
+ // Write the sibling set next to deployment.yaml (project) and into the
1674
+ // engine-private per-instance build dir (mirrors instances/<id>/ layout).
1675
+ const instanceBuildDir = `./engine-private/conf/${deployId}/instances/${_id}/build/${env}`;
1676
+ fs.mkdirpSync(instanceBuildDir);
1677
+ fs.writeFileSync(`${instanceBuildDir}/deployment.yaml`, deploymentYaml, 'utf8');
1678
+ const siblingManifests = {
1679
+ 'pv-pvc.yaml': pvPvcYaml,
1680
+ 'proxy.yaml': proxyYaml,
1681
+ 'grpc-service.yaml': grpcServiceYaml,
1682
+ };
1683
+ for (const [name, content] of Object.entries(siblingManifests)) {
1684
+ if (!content) continue;
1685
+ fs.writeFileSync(`${envManifestPath}/${name}`, content, 'utf8');
1686
+ fs.writeFileSync(`${instanceBuildDir}/${name}`, content, 'utf8');
1687
+ }
1688
+ logger.info('[instance-build-manifest] Sibling manifests written', {
1689
+ project: envManifestPath,
1690
+ enginePrivate: instanceBuildDir,
1691
+ pvPvc: !!pvPvcYaml,
1692
+ proxy: !!proxyYaml,
1693
+ grpcService: !!grpcServiceYaml,
1694
+ });
1695
+
1346
1696
  if (env === 'production') {
1347
1697
  if (fs.existsSync(dockerfileManifestPath)) {
1348
1698
  fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
1349
1699
  }
1350
1700
  fs.copyFileSync(outputPath, `${rootPath}/deployment.yaml`);
1701
+ // Sibling manifests alongside deployment.yaml at the project root.
1702
+ for (const name of ['pv-pvc.yaml', 'proxy.yaml', 'grpc-service.yaml']) {
1703
+ const src = `${envManifestPath}/${name}`;
1704
+ if (fs.existsSync(src)) fs.copyFileSync(src, `${rootPath}/${name}`);
1705
+ }
1351
1706
  logger.info('[instance-build-manifest] Production artifacts copied to project root', {
1352
1707
  rootPath,
1353
1708
  dockerfile: `${rootPath}/Dockerfile`,
@@ -1359,6 +1714,25 @@ EOF
1359
1714
  fs.copyFileSync(ciSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.ci.yml`);
1360
1715
  logger.info(`[instance-build-manifest] CI workflow copied`, { src: ciSrc });
1361
1716
  }
1717
+
1718
+ // Ship the development variant alongside production so the instance repo
1719
+ // is self-contained: the dev Dockerfile (built by the -dev CI workflow
1720
+ // into underpost/<runtime>-dev, consumed by the development compose
1721
+ // stack) and its dispatchable workflow. Both are optional — synced only
1722
+ // when the source-of-truth files exist in the engine repo.
1723
+ if (_runtime) {
1724
+ const devDockerfileSrc = `src/runtime/${_runtime}/Dockerfile.dev`;
1725
+ if (fs.existsSync(devDockerfileSrc)) {
1726
+ fs.copyFileSync(devDockerfileSrc, `${rootPath}/Dockerfile.dev`);
1727
+ logger.info('[instance-build-manifest] Dev Dockerfile copied', { src: devDockerfileSrc });
1728
+ }
1729
+ const devCiSrc = `./.github/workflows/docker-image.${_runtime}.dev.ci.yml`;
1730
+ if (fs.existsSync(devCiSrc)) {
1731
+ if (!fs.existsSync(`${rootPath}/.github/workflows`)) fs.mkdirpSync(`${rootPath}/.github/workflows`);
1732
+ fs.copyFileSync(devCiSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.dev.ci.yml`);
1733
+ logger.info(`[instance-build-manifest] Dev CI workflow copied`, { src: devCiSrc });
1734
+ }
1735
+ }
1362
1736
  }
1363
1737
  },
1364
1738
 
@@ -1366,7 +1740,7 @@ EOF
1366
1740
  * @method ls-deployments
1367
1741
  * @description Retrieves and logs a table of Kubernetes deployments using `Underpost.deploy.get`.
1368
1742
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional deployment name filter).
1369
- * @param {Object} options - The default underpost runner options for customizing workflow
1743
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1370
1744
  * @memberof UnderpostRun
1371
1745
  */
1372
1746
  'ls-deployments': async (path, options = DEFAULT_OPTION) => {
@@ -1377,7 +1751,7 @@ EOF
1377
1751
  * @method host-update
1378
1752
  * @description Executes the `rocky-setup.sh` script to update the host system configuration.
1379
1753
  * @param {string} path - The input value, identifier, or path for the operation.
1380
- * @param {Object} options - The default underpost runner options for customizing workflow
1754
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1381
1755
  * @memberof UnderpostRun
1382
1756
  */
1383
1757
  'host-update': async (path, options = DEFAULT_OPTION) => {
@@ -1393,7 +1767,7 @@ EOF
1393
1767
  * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1394
1768
  * so that `crictl` targets the CRI-O socket by default.
1395
1769
  * @param {string} path - Unused.
1396
- * @param {Object} options - The default underpost runner options for customizing workflow.
1770
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
1397
1771
  * @memberof UnderpostRun
1398
1772
  */
1399
1773
  'install-crio': (path, options = DEFAULT_OPTION) => {
@@ -1407,8 +1781,24 @@ gpgcheck=1
1407
1781
  gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
1408
1782
  EOF`);
1409
1783
  shellExec(`sudo dnf -y install cri-o`);
1410
- // crictl is in the kubernetes repo but excluded by default install it explicitly
1411
- shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes`);
1784
+ // Add the Kubernetes repo so cri-tools (crictl CLI) is available.
1785
+ // The repo has exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni, so we
1786
+ // use --disableexcludes=kubernetes to override and install cri-tools.
1787
+ shellExec(`cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
1788
+ [kubernetes]
1789
+ name=Kubernetes
1790
+ baseurl=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/
1791
+ enabled=1
1792
+ gpgcheck=1
1793
+ gpgkey=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/repodata/repomd.xml.key
1794
+ exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
1795
+ EOF`);
1796
+ shellExec(`sudo yum install -y cri-tools --disableexcludes=kubernetes 2>/dev/null || {
1797
+ CRICTL_VERSION="v1.31.0"
1798
+ ARCH="amd64"
1799
+ log "Kubernetes repo not available, downloading crictl binary directly..."
1800
+ curl -sL "https://github.com/kubernetes-sigs/cri-tools/releases/download/\${CRICTL_VERSION}/crictl-\${CRICTL_VERSION}-\${ARCH}.tar.gz" | sudo tar -C /usr/local/bin -xz
1801
+ }`);
1412
1802
  // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1413
1803
  shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1414
1804
  silentOnError: true,
@@ -1428,7 +1818,7 @@ EOF`);
1428
1818
  * @method dd-container
1429
1819
  * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
1430
1820
  * @param {string} path - The input value, identifier, or path for the operation (used as the command to run inside the container).
1431
- * @param {Object} options - The default underpost runner options for customizing workflow
1821
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1432
1822
  * @memberof UnderpostRun
1433
1823
  */
1434
1824
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
@@ -1436,8 +1826,8 @@ EOF`);
1436
1826
  const baseClusterCommand = options.dev ? ' --dev' : '';
1437
1827
  const currentImage = options.imageName
1438
1828
  ? options.imageName
1439
- : Underpost.deploy
1440
- .getCurrentLoadedImages(options.nodeName ? options.nodeName : 'kind-worker', false)
1829
+ : Underpost.image
1830
+ .getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
1441
1831
  .find((o) => o.IMAGE.match('underpost'));
1442
1832
  const podName = options.podName || `underpost-dev-container`;
1443
1833
  const volumeHostPath = options.claimName || '/home/dd';
@@ -1455,7 +1845,11 @@ EOF`);
1455
1845
  }
1456
1846
 
1457
1847
  if (!currentImage)
1458
- shellExec(`${baseCommand} image${baseClusterCommand} --pull-base ${options.dev ? '--kind' : '--kubeadm'}`);
1848
+ shellExec(
1849
+ `${baseCommand} image${baseClusterCommand} --pull-base --build --path ${
1850
+ options.dev ? '.' : options.underpostRoot
1851
+ } ${options.dev ? '--kind' : '--kubeadm'}`,
1852
+ );
1459
1853
  // shellExec(`kubectl delete pod ${podName} --ignore-not-found`);
1460
1854
 
1461
1855
  const payload = {
@@ -1483,7 +1877,7 @@ EOF`);
1483
1877
  * @method ip-info
1484
1878
  * @description Executes the `ip-info.sh` script to display IP-related information for the specified path.
1485
1879
  * @param {string} path - The input value, identifier, or path for the operation (used as an argument to the script).
1486
- * @param {Object} options - The default underpost runner options for customizing workflow
1880
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1487
1881
  * @memberof UnderpostRun
1488
1882
  */
1489
1883
  'ip-info': (path, options = DEFAULT_OPTION) => {
@@ -1496,7 +1890,7 @@ EOF`);
1496
1890
  * @method db-client
1497
1891
  * @description Deploys and exposes the Adminer database client application (using `adminer:4.7.6-standalone` image) on the cluster.
1498
1892
  * @param {string} path - The input value, identifier, or path for the operation.
1499
- * @param {Object} options - The default underpost runner options for customizing workflow
1893
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1500
1894
  * @memberof UnderpostRun
1501
1895
  */
1502
1896
  'db-client': async (path, options = DEFAULT_OPTION) => {
@@ -1523,7 +1917,7 @@ EOF`);
1523
1917
  * @method git-conf
1524
1918
  * @description Configures Git global and local user name and email settings based on the provided `path` (formatted as `username,email`), or defaults to environment variables.
1525
1919
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `username,email`).
1526
- * @param {Object} options - The default underpost runner options for customizing workflow
1920
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1527
1921
  * @memberof UnderpostRun
1528
1922
  */
1529
1923
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
@@ -1571,7 +1965,7 @@ EOF`);
1571
1965
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
1572
1966
  * (cert-manager Certificate resources) for each affected deployment.
1573
1967
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
1574
- * @param {Object} options - The default underpost runner options for customizing workflow
1968
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1575
1969
  * @memberof UnderpostRun
1576
1970
  */
1577
1971
  promote: async (path, options = DEFAULT_OPTION) => {
@@ -1625,10 +2019,31 @@ EOF`);
1625
2019
  * @method metrics
1626
2020
  * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
1627
2021
  * @param {string} path - The input value, identifier, or path for the operation.
1628
- * @param {Object} options - The default underpost runner options for customizing workflow
2022
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1629
2023
  * @memberof UnderpostRun
1630
2024
  */
1631
2025
  metrics: async (path, options = DEFAULT_OPTION) => {
2026
+ if (path === 'server') {
2027
+ shellExec(
2028
+ `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2029
+ );
2030
+ await timer(2000);
2031
+
2032
+ shellExec(`kubectl patch deployment metrics-server -n kube-system \
2033
+ --type='json' \
2034
+ -p='[
2035
+ {
2036
+ "op":"add",
2037
+ "path":"/spec/template/spec/containers/0/args/-",
2038
+ "value":"--kubelet-insecure-tls"
2039
+ }
2040
+ ]'`);
2041
+ shellExec(`kubectl scale deployment metrics-server \
2042
+ -n kube-system \
2043
+ --replicas=1`);
2044
+
2045
+ return;
2046
+ }
1632
2047
  const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
1633
2048
  let hosts = [];
1634
2049
  for (const deployId of deployList) {
@@ -1642,7 +2057,7 @@ EOF`);
1642
2057
  * @method cluster
1643
2058
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB, MariaDB, Valkey, Contour (Ingress), and Cert-Manager, and deploys all services.
1644
2059
  * @param {string} path - The input value, identifier, or path for the operation.
1645
- * @param {Object} options - The default underpost runner options for customizing workflow
2060
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1646
2061
  * @memberof UnderpostRun
1647
2062
  */
1648
2063
  cluster: async (path = '', options = DEFAULT_OPTION) => {
@@ -1704,7 +2119,7 @@ EOF`);
1704
2119
  * @method deploy
1705
2120
  * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
1706
2121
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
1707
- * @param {Object} options - The default underpost runner options for customizing workflow
2122
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1708
2123
  * @memberof UnderpostRun
1709
2124
  */
1710
2125
  deploy: async (path, options = DEFAULT_OPTION) => {
@@ -1714,20 +2129,13 @@ EOF`);
1714
2129
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1715
2130
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1716
2131
  const env = options.dev ? 'development' : 'production';
1717
- const ignorePods = Underpost.deploy
2132
+ const ignorePods = Underpost.kubectl
1718
2133
  .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
1719
2134
  .map((p) => p.NAME);
1720
2135
 
1721
2136
  shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
1722
2137
 
1723
- await Underpost.deploy.monitorReadyRunner(
1724
- deployId,
1725
- env,
1726
- targetTraffic,
1727
- ignorePods,
1728
- options.namespace,
1729
- 'underpost',
1730
- );
2138
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
1731
2139
 
1732
2140
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
1733
2141
  },
@@ -1736,7 +2144,7 @@ EOF`);
1736
2144
  * @method disk-clean
1737
2145
  * @description Executes the `disk-clean-sh` script to perform disk cleanup operations.
1738
2146
  * @param {string} path - The input value, identifier, or path for the operation.
1739
- * @param {Object} options - The default underpost runner options for customizing workflow
2147
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1740
2148
  * @memberof UnderpostRun
1741
2149
  */
1742
2150
  'disk-clean': async (path, options = DEFAULT_OPTION) => {
@@ -1749,7 +2157,7 @@ EOF`);
1749
2157
  * @method disk-devices
1750
2158
  * @description Executes the `disk-devices.sh` script to display information about disk devices.
1751
2159
  * @param {string} path - The input value, identifier, or path for the operation.
1752
- * @param {Object} options - The default underpost runner options for customizing workflow
2160
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1753
2161
  * @memberof UnderpostRun
1754
2162
  */
1755
2163
  'disk-devices': async (path = '/', options = DEFAULT_OPTION) => {
@@ -1762,7 +2170,7 @@ EOF`);
1762
2170
  * @method disk-usage
1763
2171
  * @description Displays disk usage statistics using the `du` command, sorted by size.
1764
2172
  * @param {string} path - The input value, identifier, or path for the operation.
1765
- * @param {Object} options - The default underpost runner options for customizing workflow
2173
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1766
2174
  * @memberof UnderpostRun
1767
2175
  */
1768
2176
  'disk-usage': async (path = '/', options = DEFAULT_OPTION) => {
@@ -1777,7 +2185,7 @@ EOF`);
1777
2185
  * @method dev
1778
2186
  * @description Starts development servers for client, API, and proxy based on provided parameters (deployId, host, path, clientHostPort).
1779
2187
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,subConf,host,path,clientHostPort`).
1780
- * @param {Object} options - The default underpost runner options for customizing workflow
2188
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1781
2189
  * @memberof UnderpostRun
1782
2190
  */
1783
2191
  dev: async (path = '', options = DEFAULT_OPTION) => {
@@ -1819,7 +2227,7 @@ EOF`);
1819
2227
  }`;
1820
2228
  shellExec(cmd, { async: true });
1821
2229
  }
1822
- await awaitDeployMonitor();
2230
+ if ((await awaitDeployMonitor()) !== true) return;
1823
2231
  {
1824
2232
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1825
2233
 
@@ -1827,7 +2235,7 @@ EOF`);
1827
2235
  async: true,
1828
2236
  });
1829
2237
  }
1830
- await awaitDeployMonitor();
2238
+ if ((await awaitDeployMonitor()) !== true) return;
1831
2239
  shellExec(
1832
2240
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1833
2241
  );
@@ -1837,7 +2245,7 @@ EOF`);
1837
2245
  * @method service
1838
2246
  * @description Deploys and exposes specific services (like `mongo-express-service`) on the cluster, updating deployment configurations and monitoring status.
1839
2247
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,serviceId,host,path,replicas,image,node`).
1840
- * @param {Object} options - The default underpost runner options for customizing workflow
2248
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1841
2249
  * @memberof UnderpostRun
1842
2250
  */
1843
2251
  service: async (path = '', options = DEFAULT_OPTION) => {
@@ -1933,7 +2341,7 @@ EOF`);
1933
2341
  * @method etc-hosts
1934
2342
  * @description Generates and logs the contents for the `/etc/hosts` file based on provided hosts or deployment configurations.
1935
2343
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated list of hosts).
1936
- * @param {Object} options - The default underpost runner options for customizing workflow
2344
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1937
2345
  * @memberof UnderpostRun
1938
2346
  */
1939
2347
  'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
@@ -1950,7 +2358,7 @@ EOF`);
1950
2358
  * @method sh
1951
2359
  * @description Enables remote control for the Kitty terminal emulator.
1952
2360
  * @param {string} path - The input value, identifier, or path for the operation.
1953
- * @param {Object} options - The default underpost runner options for customizing workflow
2361
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1954
2362
  * @memberof UnderpostRun
1955
2363
  */
1956
2364
  sh: async (path = '', options = DEFAULT_OPTION) => {
@@ -1970,7 +2378,7 @@ EOF`);
1970
2378
  * @method log
1971
2379
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
1972
2380
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `filePath,keywords,lines`).
1973
- * @param {Object} options - The default underpost runner options for customizing workflow
2381
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1974
2382
  * @memberof UnderpostRun
1975
2383
  */
1976
2384
  log: async (path, options = DEFAULT_OPTION) => {
@@ -1987,7 +2395,7 @@ EOF`);
1987
2395
  * @method ps
1988
2396
  * @description Displays running processes that match a specified path or keyword.
1989
2397
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter processes).
1990
- * @param {Object} options - The default underpost runner options for customizing workflow
2398
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1991
2399
  * @memberof UnderpostRun
1992
2400
  */
1993
2401
  ps: async (path = '', options = DEFAULT_OPTION) => {
@@ -2012,7 +2420,7 @@ EOF`);
2012
2420
  * @method pid-info
2013
2421
  * @description Displays detailed information about a process by PID, including service details, command line, executable path, working directory, environment variables, and parent process tree.
2014
2422
  * @param {string} path - The PID of the process to inspect.
2015
- * @param {Object} options - The default underpost runner options for customizing workflow
2423
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2016
2424
  * @memberof UnderpostRun
2017
2425
  */
2018
2426
  'pid-info': (path, options = DEFAULT_OPTION) => {
@@ -2052,7 +2460,7 @@ EOF`);
2052
2460
  * @method background
2053
2461
  * @description Runs a custom command in the background using nohup, logging output to `/var/log/<id>.log` and saving the PID to `/var/run/<id>.pid`.
2054
2462
  * @param {string} path - The command to run in the background (e.g. 'npm run prod:container dd-cyberia-r3').
2055
- * @param {Object} options - The default underpost runner options for customizing workflow
2463
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2056
2464
  * @memberof UnderpostRun
2057
2465
  */
2058
2466
  background: (path, options = DEFAULT_OPTION) => {
@@ -2072,7 +2480,7 @@ EOF`);
2072
2480
  * @method ports
2073
2481
  * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
2074
2482
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
2075
- * @param {Object} options - The default underpost runner options for customizing workflow
2483
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2076
2484
  * @memberof UnderpostRun
2077
2485
  */
2078
2486
  ports: async (path = '', options = DEFAULT_OPTION) => {
@@ -2084,7 +2492,7 @@ EOF`);
2084
2492
  * @method deploy-test
2085
2493
  * @description Deploys a test deployment (`dd-test`) in either development or production mode, setting up necessary secrets and starting the deployment.
2086
2494
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID).
2087
- * @param {Object} options - The default underpost runner options for customizing workflow
2495
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2088
2496
  * @memberof UnderpostRun
2089
2497
  */
2090
2498
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
@@ -2109,7 +2517,7 @@ EOF`);
2109
2517
  * @method tf-vae-test
2110
2518
  * @description Creates and runs a job pod (`tf-vae-test`) that installs TensorFlow dependencies, clones the TensorFlow docs, and runs the CVAE tutorial script, with a terminal monitor attached.
2111
2519
  * @param {string} path - The input value, identifier, or path for the operation.
2112
- * @param {Object} options - The default underpost runner options for customizing workflow
2520
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2113
2521
  * @memberof UnderpostRun
2114
2522
  */
2115
2523
  'tf-vae-test': async (path, options = DEFAULT_OPTION) => {
@@ -2209,7 +2617,7 @@ EOF`);
2209
2617
  * @method spark-template
2210
2618
  * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
2211
2619
  * @param {string} path - The input value, identifier, or path for the operation.
2212
- * @param {Object} options - The default underpost runner options for customizing workflow
2620
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2213
2621
  * @memberof UnderpostRun
2214
2622
  */
2215
2623
  'spark-template': (path, options = DEFAULT_OPTION) => {
@@ -2237,7 +2645,7 @@ EOF`);
2237
2645
  * @method pull-rocky-image
2238
2646
  * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
2239
2647
  * @param {string} path - The input value, identifier, or path for the operation.
2240
- * @param {Object} options - The default underpost runner options for customizing workflow
2648
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2241
2649
  * @memberof UnderpostRun
2242
2650
  */
2243
2651
  'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
@@ -2247,7 +2655,7 @@ EOF`);
2247
2655
  * @method rmi
2248
2656
  * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
2249
2657
  * @param {string} path - The input value, identifier, or path for the operation.
2250
- * @param {Object} options - The default underpost runner options for customizing workflow
2658
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2251
2659
  * @memberof UnderpostRun
2252
2660
  */
2253
2661
  rmi: (path, options = DEFAULT_OPTION) => {
@@ -2257,7 +2665,7 @@ EOF`);
2257
2665
  * @method kill
2258
2666
  * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
2259
2667
  * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
2260
- * @param {Object} options - The default underpost runner options for customizing workflow
2668
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2261
2669
  * @memberof UnderpostRun
2262
2670
  */
2263
2671
  kill: (path = '', options = DEFAULT_OPTION) => {
@@ -2289,7 +2697,7 @@ EOF`);
2289
2697
  * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
2290
2698
  * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
2291
2699
  * @param {string} path - Optional password length (default: 16).
2292
- * @param {Object} options - The default underpost runner options for customizing workflow.
2700
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2293
2701
  * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
2294
2702
  * @memberof UnderpostRun
2295
2703
  */
@@ -2323,7 +2731,7 @@ EOF`);
2323
2731
  * @method secret
2324
2732
  * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
2325
2733
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
2326
- * @param {Object} options - The default underpost runner options for customizing workflow
2734
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2327
2735
  * @memberof UnderpostRun
2328
2736
  */
2329
2737
  secret: (path, options = DEFAULT_OPTION) => {
@@ -2336,7 +2744,7 @@ EOF`);
2336
2744
  * @method underpost-config
2337
2745
  * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
2338
2746
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
2339
- * @param {Object} options - The default underpost runner options for customizing workflow
2747
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2340
2748
  * @memberof UnderpostRun
2341
2749
  */
2342
2750
  'underpost-config': (path = '', options = DEFAULT_OPTION) => {
@@ -2346,7 +2754,7 @@ EOF`);
2346
2754
  * @method gpu-env
2347
2755
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
2348
2756
  * @param {string} path - The input value, identifier, or path for the operation.
2349
- * @param {Object} options - The default underpost runner options for customizing workflow
2757
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2350
2758
  * @memberof UnderpostRun
2351
2759
  */
2352
2760
  'gpu-env': (path, options = DEFAULT_OPTION) => {
@@ -2359,7 +2767,7 @@ EOF`);
2359
2767
  * @method tf-gpu-test
2360
2768
  * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
2361
2769
  * @param {string} path - The input value, identifier, or path for the operation.
2362
- * @param {Object} options - The default underpost runner options for customizing workflow
2770
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2363
2771
  * @memberof UnderpostRun
2364
2772
  */
2365
2773
  'tf-gpu-test': (path, options = DEFAULT_OPTION) => {
@@ -2373,7 +2781,7 @@ EOF`);
2373
2781
  * @method deploy-job
2374
2782
  * @description Creates and applies a custom Kubernetes Pod manifest (Job) for running arbitrary commands inside a container image (defaulting to a TensorFlow/NVIDIA image).
2375
2783
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional script path or job argument).
2376
- * @param {Object} options - The default underpost runner options for customizing workflow
2784
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2377
2785
  * @memberof UnderpostRun
2378
2786
  */
2379
2787
  'deploy-job': async (path, options = DEFAULT_OPTION) => {
@@ -2504,16 +2912,17 @@ EOF`;
2504
2912
  /**
2505
2913
  * @method push-bundle
2506
2914
  * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
2507
- * Steps: set env, build+split zip, switch to cron env, upload parts to storage.
2915
+ * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
2916
+ * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2508
2917
  * @param {string} path - Optional `fsPath.splitOption` string.
2509
2918
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2510
- * @param {Object} options - The default underpost runner options for customizing workflow.
2919
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2511
2920
  * @param {string} [options.deployId] - Override deploy ID.
2512
2921
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2513
2922
  * @memberof UnderpostRun
2514
2923
  */
2515
2924
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2516
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2925
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2517
2926
  const env = options.dev ? 'development' : 'production';
2518
2927
  const deployId = options.deployId || 'dd-default';
2519
2928
  const pathParts = (path || '').split('.');
@@ -2537,11 +2946,54 @@ EOF`;
2537
2946
  }
2538
2947
  }
2539
2948
 
2949
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2950
+ const confServer = fs.existsSync(confServerPath)
2951
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2952
+ : {};
2953
+ const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
2954
+
2540
2955
  shellExec(`${baseCommand} env ${deployId} ${env}`);
2541
2956
  shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
2542
- shellExec(
2543
- `${baseCommand} fs ${fsPath} --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --force`,
2544
- );
2957
+
2958
+ const pushBundleFiles = (host, routePath) => {
2959
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2960
+ const buildDir = `./${fsPath}`;
2961
+ if (!fs.existsSync(buildDir)) return;
2962
+ const partFiles = fs
2963
+ .readdirSync(buildDir)
2964
+ .filter(
2965
+ (name) =>
2966
+ name.startsWith(`${buildId}.zip.part`) ||
2967
+ name.startsWith(`${buildId}.zip-part`) ||
2968
+ name === `${buildId}.zip`,
2969
+ )
2970
+ .map((name) => `${fsPath}/${name}`);
2971
+ if (partFiles.length === 0) {
2972
+ logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
2973
+ return;
2974
+ }
2975
+ for (const partFile of partFiles) {
2976
+ shellExec(
2977
+ `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
2978
+ );
2979
+ }
2980
+ };
2981
+
2982
+ for (const host of Object.keys(confServer)) {
2983
+ for (const routePath of Object.keys(confServer[host])) {
2984
+ const routeConf = confServer[host][routePath] || {};
2985
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2986
+ if (routeConf.singleReplica) {
2987
+ if (routeConf.replicas) {
2988
+ for (const replica of routeConf.replicas) {
2989
+ pushBundleFiles(host, replica);
2990
+ }
2991
+ }
2992
+ continue;
2993
+ }
2994
+ pushBundleFiles(host, routePath);
2995
+ }
2996
+ }
2545
2997
  },
2546
2998
 
2547
2999
  /**
@@ -2552,17 +3004,19 @@ EOF`;
2552
3004
  * so that multi-path deployments are handled correctly.
2553
3005
  * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
2554
3006
  * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
2555
- * @param {Object} options - The default underpost runner options for customizing workflow.
3007
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2556
3008
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
2557
3009
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2558
3010
  * @memberof UnderpostRun
2559
3011
  */
2560
3012
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
2561
- const baseCommand = options.dev ? 'node bin' : 'underpost';
3013
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2562
3014
  const env = options.dev ? 'development' : 'production';
2563
3015
  const deployId = options.deployId || 'dd-default';
2564
3016
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2565
- const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
3017
+ const confServer = fs.existsSync(confServerPath)
3018
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
3019
+ : {};
2566
3020
  const hostsArg = path
2567
3021
  ? path
2568
3022
  .split(',')
@@ -2581,70 +3035,81 @@ EOF`;
2581
3035
  `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2582
3036
  );
2583
3037
 
3038
+ const pullBundleRoute = (host, routePath) => {
3039
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
3040
+ const zipPath = `build/${buildId}.zip`;
3041
+ const buildDir = './build';
3042
+ const hasZip = fs.existsSync(zipPath);
3043
+ const hasParts =
3044
+ fs.existsSync(buildDir) &&
3045
+ fs
3046
+ .readdirSync(buildDir)
3047
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
3048
+
3049
+ if (!hasZip && !hasParts) {
3050
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
3051
+ return;
3052
+ }
3053
+
3054
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
3055
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
3056
+ shellExec(`sudo rm -rf ${zipPath}`);
3057
+
3058
+ if (fs.existsSync(buildDir)) {
3059
+ fs.readdirSync(buildDir)
3060
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
3061
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
3062
+ }
3063
+
3064
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
3065
+ if (!fs.existsSync(extractedDir)) {
3066
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
3067
+ return;
3068
+ }
3069
+
3070
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
3071
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
3072
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
3073
+ fs.copySync(`${extractedDir}`, `${publicDestPath}`);
3074
+ };
3075
+
2584
3076
  for (const host of hostsArg) {
2585
- // Gather all routes for this host; fall back to root '/' when host is not in confServer
2586
- // (e.g. when hosts were provided explicitly via the path argument).
2587
3077
  const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2588
3078
 
2589
3079
  for (const routePath of routePaths) {
2590
3080
  const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
2591
- // Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
2592
- if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
2593
-
2594
- // buildClient names the zip as "<host>-<path-no-slashes>.zip"
2595
- // e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
2596
- // e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
2597
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2598
- const zipPath = `build/${buildId}.zip`;
2599
- const buildDir = './build';
2600
- const hasZip = fs.existsSync(zipPath);
2601
- const hasParts =
2602
- fs.existsSync(buildDir) &&
2603
- fs
2604
- .readdirSync(buildDir)
2605
- .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2606
-
2607
- if (!hasZip && !hasParts) {
2608
- logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2609
- continue;
2610
- }
2611
-
2612
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2613
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
2614
- shellExec(`sudo rm -rf ${zipPath}`);
2615
-
2616
- // Clean up downloaded part wrapper zips left by --omit-unzip pull
2617
- if (fs.existsSync(buildDir)) {
2618
- fs.readdirSync(buildDir)
2619
- .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2620
- .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2621
- }
2622
-
2623
- // unzipClientBuild extracts to buildId with trailing '-' stripped
2624
- // e.g. "build/underpost.net-" → "build/underpost.net"
2625
- // e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
2626
- const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2627
- if (!fs.existsSync(extractedDir)) {
2628
- logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
3081
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
3082
+ if (routeConf.singleReplica) {
3083
+ if (routeConf.replicas) {
3084
+ for (const replica of routeConf.replicas) {
3085
+ pullBundleRoute(host, replica);
3086
+ }
3087
+ }
2629
3088
  continue;
2630
3089
  }
2631
-
2632
- // Destination mirrors the public directory layout used by the server
2633
- const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2634
- if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2635
- // Ensure parent directory exists for sub-paths
2636
- if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2637
- shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
3090
+ pullBundleRoute(host, routePath);
2638
3091
  }
2639
3092
  }
2640
3093
  },
2641
3094
 
3095
+ /**
3096
+ * @method build-cluster-deployment-manifests
3097
+ * @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.
3098
+ * @param {string} path - Unused.
3099
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3100
+ * @memberof UnderpostRun
3101
+ */
3102
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
3103
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
3104
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
3105
+ },
3106
+
2642
3107
  /**
2643
3108
  * @method monitor-ui
2644
3109
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
2645
3110
  * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
2646
3111
  * @param {string} path - Unused.
2647
- * @param {Object} options - The default underpost runner options for customizing workflow.
3112
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2648
3113
  * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
2649
3114
  * @memberof UnderpostRun
2650
3115
  */
@@ -2668,7 +3133,7 @@ EOF`;
2668
3133
  * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
2669
3134
  * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
2670
3135
  * Customise via the `path` argument or leave empty to use the default.
2671
- * @param {Object} options - The default underpost runner options for customizing workflow.
3136
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2672
3137
  * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
2673
3138
  * @memberof UnderpostRun
2674
3139
  */
@@ -2689,6 +3154,33 @@ EOF`;
2689
3154
 
2690
3155
  logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
2691
3156
  },
3157
+ /**
3158
+ * @method shared-dir-add-user
3159
+ * @description Add a user to an existing shared directory without changing
3160
+ * file owners. Grants recursive group/ACL access so the user can read and
3161
+ * write throughout the shared workspace while preserving existing ownership.
3162
+ *
3163
+ * @param {string} path - Shared directory (defaults to `/home/dd/engine`).
3164
+ * @param {UnderpostRunDefaultOptions} options - Underpost runner options.
3165
+ * Key fields:
3166
+ * - options.user (default: 'admin')
3167
+ * - options.group (default: 'engine-dev')
3168
+ *
3169
+ * @memberof UnderpostRun
3170
+ */ 'shared-dir-add-user': (path = '/home/dd/engine', options = DEFAULT_OPTION) => {
3171
+ const dir = path || '/home/dd/engine';
3172
+ const user = options.user || 'admin';
3173
+
3174
+ logger.info(`[shared-dir-add-user] dir=${dir} user=${user}`);
3175
+
3176
+ // Give the user direct access without changing owners or group ownership.
3177
+ shellExec(`sudo setfacl -R -m u:${user}:rwx ${dir}`);
3178
+
3179
+ // Make future files/directories inherit the same user ACL.
3180
+ shellExec(`sudo find ${dir} -type d -exec setfacl -d -m u:${user}:rwx {} \\;`);
3181
+
3182
+ logger.info(`[shared-dir-add-user] User '${user}' added to shared directory: ${dir}`);
3183
+ },
2692
3184
  };
2693
3185
 
2694
3186
  static API = {
@@ -2717,7 +3209,7 @@ EOF`;
2717
3209
  * @description Executes a specified runner function from the UnderpostRun class with the provided path and options.
2718
3210
  * @param {string} runner - The name of the runner to execute.
2719
3211
  * @param {string} path - The input value, identifier, or path for the operation.
2720
- * @param {Object} options - The default underpost runner options for customizing workflow
3212
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2721
3213
  * @memberof UnderpostRun
2722
3214
  * @returns {Promise<any>} The result of the runner execution.
2723
3215
  */
@@ -2730,7 +3222,7 @@ EOF`;
2730
3222
  * @description Initiates the execution of a specified CLI command (runner) with the given input value (`path`) and processed options.
2731
3223
  * @param {string} runner - The name of the runner to execute.
2732
3224
  * @param {string} path - The input value, identifier, or path for the operation.
2733
- * @param {Object} options - The default underpost runner options for customizing workflow
3225
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2734
3226
  * @memberof UnderpostRun
2735
3227
  * @returns {Promise<any>} The result of the callback execution.
2736
3228
  */