cyberia 3.2.22 → 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 (185) 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 +33 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +13 -3
  12. package/.github/workflows/ghpkg.ci.yml +88 -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 +10 -7
  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 +301 -1
  21. package/CLI-HELP.md +71 -6
  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 +31 -0
  28. package/bin/cyberia.js +1080 -184
  29. package/bin/deploy.js +2 -2
  30. package/bin/index.js +1080 -184
  31. package/bump.config.js +1 -0
  32. package/compose.env +131 -0
  33. package/conf.js +18 -1
  34. package/deployment.yaml +2 -2
  35. package/docker-compose.yml +316 -0
  36. package/hardhat/hardhat.config.js +2 -2
  37. package/hardhat/package-lock.json +620 -2041
  38. package/hardhat/package.json +7 -5
  39. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  40. package/hardhat/test/ObjectLayerToken.js +378 -274
  41. package/ipfs/configure-ipfs.sh +13 -0
  42. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  44. package/manifests/deployment/dd-cyberia-development/deployment.yaml +2 -2
  45. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  46. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  47. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  48. package/mongodb/entrypoint.sh +76 -0
  49. package/nginx.conf +87 -0
  50. package/package.json +31 -19
  51. package/pv-pvc.yaml +32 -0
  52. package/scripts/disk-clean.sh +85 -60
  53. package/scripts/kubeadm-node-setup.sh +317 -0
  54. package/scripts/rocky-kickstart.sh +877 -185
  55. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  56. package/scripts/test-monitor.sh +3 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  59. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  60. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  61. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  62. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  63. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  65. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  66. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  67. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  68. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  71. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  72. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  73. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  74. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  75. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  77. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  78. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  79. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  80. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  81. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  82. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  83. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  84. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  85. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -129
  86. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  87. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  88. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  89. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  90. package/src/api/ipfs/ipfs.service.js +28 -15
  91. package/src/api/object-layer/object-layer.router.js +25 -15
  92. package/src/api/object-layer/object-layer.service.js +15 -20
  93. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  94. package/src/cli/baremetal.js +717 -16
  95. package/src/cli/cluster.js +80 -5
  96. package/src/cli/deploy.js +127 -11
  97. package/src/cli/docker-compose.js +648 -0
  98. package/src/cli/env.js +11 -2
  99. package/src/cli/fs.js +75 -30
  100. package/src/cli/image.js +129 -51
  101. package/src/cli/index.js +110 -6
  102. package/src/cli/kickstart.js +142 -20
  103. package/src/cli/release.js +46 -4
  104. package/src/cli/repository.js +238 -33
  105. package/src/cli/run.js +520 -114
  106. package/src/cli/secrets.js +82 -48
  107. package/src/cli/ssh.js +234 -0
  108. package/src/cli/static.js +2 -2
  109. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  110. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  111. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  112. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  113. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  114. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  115. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  116. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  117. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  118. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  119. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +45 -27
  120. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  121. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  122. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  123. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  124. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +8 -1
  125. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  126. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +5 -5
  127. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  128. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  129. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  130. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  131. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  132. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  133. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  134. package/src/client/services/object-layer/object-layer.management.js +6 -6
  135. package/src/{server → client-builder}/client-build-docs.js +15 -5
  136. package/src/{server → client-builder}/client-build-live.js +3 -3
  137. package/src/{server → client-builder}/client-build.js +25 -22
  138. package/src/{server → client-builder}/client-dev-server.js +3 -3
  139. package/src/{server → client-builder}/client-icons.js +2 -2
  140. package/src/{server → client-builder}/ssr.js +5 -5
  141. package/src/client.build.js +1 -3
  142. package/src/client.dev.js +1 -1
  143. package/src/db/mongo/MongoBootstrap.js +12 -12
  144. package/src/grpc/cyberia/grpc-server.js +255 -70
  145. package/src/index.js +12 -1
  146. package/src/mailer/EmailRender.js +1 -1
  147. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  148. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  149. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  150. package/src/projects/cyberia/gemini-client.js +175 -0
  151. package/src/projects/cyberia/generate-saga.js +2107 -0
  152. package/src/{server → projects/cyberia}/ipfs-client.js +2 -2
  153. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  154. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  155. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  156. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  157. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  158. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  159. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  160. package/src/runtime/cyberia-client/Dockerfile +27 -61
  161. package/src/runtime/cyberia-client/Dockerfile.dev +20 -12
  162. package/src/runtime/cyberia-server/Dockerfile +21 -20
  163. package/src/runtime/cyberia-server/Dockerfile.dev +17 -8
  164. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  165. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  166. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  167. package/src/runtime/engine-cyberia/compose.env +131 -0
  168. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  169. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  170. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  171. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  172. package/src/runtime/express/Express.js +2 -2
  173. package/src/runtime/nginx/Nginx.js +250 -0
  174. package/src/server/catalog.js +9 -14
  175. package/src/server/conf.js +3 -6
  176. package/src/server/runtime-status.js +18 -1
  177. package/src/server/start.js +12 -2
  178. package/src/server.js +6 -2
  179. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  180. package/test/deploy-monitor.test.js +26 -10
  181. package/test/shape-generator.test.js +7 -1
  182. package/typedoc.dd-cyberia.json +3 -1
  183. package/typedoc.json +3 -1
  184. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  185. /package/src/{server → client-builder}/client-formatted.js +0 -0
package/src/cli/run.js CHANGED
@@ -22,7 +22,7 @@ import { actionInitLog, loggerFactory } from '../server/logger.js';
22
22
 
23
23
  import fs from 'fs-extra';
24
24
  import net from 'net';
25
- import { range, setPad, timer } from '../client/components/core/CommonJs.js';
25
+ import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
26
26
 
27
27
  import os from 'os';
28
28
  import Underpost from '../index.js';
@@ -52,10 +52,12 @@ const logger = loggerFactory(import.meta);
52
52
  /**
53
53
  * @constant DEFAULT_OPTION
54
54
  * @description Default options for the UnderpostRun class.
55
+ * @typedef {Object} UnderpostRunDefaultOptions
55
56
  * @type {Object}
56
57
  * @property {boolean} dev - Whether to run in development mode.
57
58
  * @property {string} podName - The name of the pod to run.
58
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.
59
61
  * @property {number} port - Custom port to use.
60
62
  * @property {string} volumeHostPath - The host path for the volume.
61
63
  * @property {string} volumeMountPath - The mount path for the volume.
@@ -127,6 +129,7 @@ const DEFAULT_OPTION = {
127
129
  dev: false,
128
130
  podName: '',
129
131
  nodeName: '',
132
+ sshKeyPath: '',
130
133
  port: 0,
131
134
  volumeHostPath: '',
132
135
  volumeMountPath: '',
@@ -213,7 +216,7 @@ class UnderpostRun {
213
216
  * @method dev-cluster
214
217
  * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
215
218
  * @param {string} path - The input value, identifier, or path for the operation.
216
- * @param {Object} options - The default underpost runner options for customizing workflow
219
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
217
220
  * @memberof UnderpostRun
218
221
  */
219
222
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
@@ -295,7 +298,7 @@ class UnderpostRun {
295
298
  * @method metadata
296
299
  * @description Generates metadata for the specified path after exposing the development cluster.
297
300
  * @param {string} path - The input value, identifier, or path for the operation.
298
- * @param {Object} options - The default underpost runner options for customizing workflow
301
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
299
302
  * @memberof UnderpostRun
300
303
  */
301
304
  metadata: async (path, options = DEFAULT_OPTION) => {
@@ -320,7 +323,7 @@ class UnderpostRun {
320
323
  * @method svc-ls
321
324
  * @description Lists systemd services and installed packages, optionally filtering by the provided path.
322
325
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional filter for services and packages).
323
- * @param {Object} options - The default underpost runner options for customizing workflow
326
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
324
327
  * @memberof UnderpostRun
325
328
  */
326
329
  'svc-ls': (path, options = DEFAULT_OPTION) => {
@@ -340,7 +343,7 @@ class UnderpostRun {
340
343
  * @method svc-rm
341
344
  * @description Removes a systemd service by stopping it, disabling it, uninstalling the package, and deleting related files.
342
345
  * @param {string} path - The input value, identifier, or path for the operation (used as the service name).
343
- * @param {Object} options - The default underpost runner options for customizing workflow
346
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
344
347
  * @memberof UnderpostRun
345
348
  */
346
349
  'svc-rm': (path, options = DEFAULT_OPTION) => {
@@ -355,7 +358,7 @@ class UnderpostRun {
355
358
  * @method ssh-deploy-info
356
359
  * @description Retrieves deployment status and pod information from a remote server via SSH.
357
360
  * @param {string} path - The input value, identifier, or path for the operation.
358
- * @param {Object} options - The default underpost runner options for customizing workflow
361
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
359
362
  * @memberof UnderpostRun
360
363
  */
361
364
  'ssh-deploy-info': async (path = '', options = DEFAULT_OPTION) => {
@@ -373,11 +376,233 @@ class UnderpostRun {
373
376
  );
374
377
  },
375
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
+
376
601
  /**
377
602
  * @method dev-hosts-expose
378
603
  * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
379
604
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
380
- * @param {Object} options - The default underpost runner options for customizing workflow
605
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
381
606
  * @memberof UnderpostRun
382
607
  */
383
608
  'dev-hosts-expose': (path, options = DEFAULT_OPTION) => {
@@ -390,7 +615,7 @@ class UnderpostRun {
390
615
  * @method dev-hosts-restore
391
616
  * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
392
617
  * @param {string} path - The input value, identifier, or path for the operation.
393
- * @param {Object} options - The default underpost runner options for customizing workflow
618
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
394
619
  * @memberof UnderpostRun
395
620
  */
396
621
  'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
@@ -401,11 +626,13 @@ class UnderpostRun {
401
626
  * @method cluster-build
402
627
  * @description Build configuration for cluster deployment.
403
628
  * @param {string} path - The input value, identifier, or path for the operation.
404
- * @param {Object} options - The default underpost runner options for customizing workflow
629
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
405
630
  * @memberof UnderpostRun
406
631
  */
407
632
  'cluster-build': (path, options = DEFAULT_OPTION) => {
408
- 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}` : '');
409
636
  shellExec(`node bin run clean`);
410
637
  shellExec(`node bin run --dev sync-replica template-deploy${nodeOptions}`);
411
638
  shellExec(`node bin run sync-replica template-deploy${nodeOptions}`);
@@ -423,7 +650,7 @@ class UnderpostRun {
423
650
  * and optionally triggers engine-<conf-id> CI with sync/init which in turn dispatches the CD workflow
424
651
  * after the build chain completes (template → ghpkg → engine-<conf-id> → CD).
425
652
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
426
- * @param {Object} options - The default underpost runner options for customizing workflow
653
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
427
654
  * @memberof UnderpostRun
428
655
  */
429
656
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
@@ -437,13 +664,10 @@ class UnderpostRun {
437
664
  shellExec(`${baseCommand} run pull`);
438
665
  shellExec(`${baseCommand} run shared-dir`);
439
666
 
440
- // Capture last N commit messages for propagation.
441
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
442
- const fromN =
443
- options.fromNCommit && parseInt(options.fromNCommit) > 0
444
- ? parseInt(options.fromNCommit)
445
- : Underpost.repo.getUnpushedCount('.').count;
446
- 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`, {
447
671
  silent: true,
448
672
  stdout: true,
449
673
  }).trim();
@@ -455,8 +679,6 @@ class UnderpostRun {
455
679
  );
456
680
  shellCd('/home/dd/engine');
457
681
 
458
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(message);
459
-
460
682
  // Push engine repo so workflow YAML changes reach GitHub
461
683
  shellExec(`git reset`);
462
684
  shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
@@ -504,7 +726,7 @@ class UnderpostRun {
504
726
  * @method template-deploy-local
505
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.
506
728
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
507
- * @param {Object} options - The default underpost runner options for customizing workflow
729
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
508
730
  * @memberof UnderpostRun
509
731
  */
510
732
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
@@ -518,17 +740,12 @@ class UnderpostRun {
518
740
  shellExec(`${baseCommand} run pull`);
519
741
  shellExec(`${baseCommand} run shared-dir`);
520
742
 
521
- // Capture last N commit messages from the engine repo.
522
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
523
- const fromN =
524
- options.fromNCommit && parseInt(options.fromNCommit) > 0
525
- ? parseInt(options.fromNCommit)
526
- : Underpost.repo.getUnpushedCount('.').count;
527
- 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`, {
528
746
  silent: true,
529
747
  stdout: true,
530
748
  }).trim();
531
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
532
749
 
533
750
  const { triggerCmd } = path
534
751
  ? await Underpost.release.ci(path, sanitizedMessage, options)
@@ -540,15 +757,14 @@ class UnderpostRun {
540
757
  * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
541
758
  * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
542
759
  * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
543
- * @param {Object} options - The default underpost runner options for customizing workflow
760
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
544
761
  * @memberof UnderpostRun
545
762
  */
546
763
  'docker-image': (path, options = DEFAULT_OPTION) => {
547
- const repo = Underpost.repo.resolveInstanceRepo(path);
764
+ const repo = Underpost.repo.resolveInstanceRepo(path, options.dev);
548
765
  Underpost.repo.dispatchWorkflow({
549
766
  repo,
550
- workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
551
- ref: 'master',
767
+ workflowFile: `docker-image${path ? `.${path}` : ''}${options.dev ? '.dev' : ''}.ci.yml`,
552
768
  inputs: {},
553
769
  });
554
770
  },
@@ -556,7 +772,7 @@ class UnderpostRun {
556
772
  * @method clean
557
773
  * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
558
774
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional directory path).
559
- * @param {Object} options - The default underpost runner options for customizing workflow
775
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
560
776
  * @memberof UnderpostRun
561
777
  */
562
778
  clean: (path = '', options = DEFAULT_OPTION) => {
@@ -567,7 +783,7 @@ class UnderpostRun {
567
783
  * @method pull
568
784
  * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
569
785
  * @param {string} path - The input value, identifier, or path for the operation.
570
- * @param {Object} options - The default underpost runner options for customizing workflow
786
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
571
787
  * @memberof UnderpostRun
572
788
  */
573
789
  pull: (path, options = DEFAULT_OPTION) => {
@@ -594,7 +810,7 @@ class UnderpostRun {
594
810
  * @method release-deploy
595
811
  * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
596
812
  * @param {string} path - The input value, identifier, or path for the operation.
597
- * @param {Object} options - The default underpost runner options for customizing workflow
813
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
598
814
  * @memberof UnderpostRun
599
815
  */
600
816
  'release-deploy': (path, options = DEFAULT_OPTION) => {
@@ -610,7 +826,7 @@ class UnderpostRun {
610
826
  * @method ssh-deploy
611
827
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
612
828
  * @param {string} path - The deployment identifier (e.g., 'engine-core', 'sync-engine-core', 'init-engine-core').
613
- * @param {Object} options - The default underpost runner options for customizing workflow
829
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
614
830
  * @memberof UnderpostRun
615
831
  */
616
832
  'ssh-deploy': (path, options = DEFAULT_OPTION) => {
@@ -625,9 +841,9 @@ class UnderpostRun {
625
841
  job = 'init';
626
842
  confId = path.replace(/^init-/, '');
627
843
  }
628
-
844
+ const repo = Underpost.repo.resolveInstanceRepo(confId, options.dev);
629
845
  Underpost.repo.dispatchWorkflow({
630
- repo: `${process.env.GITHUB_USERNAME}/engine`,
846
+ repo,
631
847
  workflowFile: `${confId}.cd.yml`,
632
848
  ref: 'master',
633
849
  inputs: { job },
@@ -638,7 +854,7 @@ class UnderpostRun {
638
854
  * @description Opens a Visual Studio Code (VS Code) session for the specified path using `node ${underpostRoot}/bin/zed ${path}`,
639
855
  * or installs Zed and sublime-text IDE if `path` is 'install'.
640
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).
641
- * @param {Object} options - The default underpost runner options for customizing workflow
857
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
642
858
  * @memberof UnderpostRun
643
859
  */
644
860
  ide: (path = '', options = DEFAULT_OPTION) => {
@@ -665,7 +881,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
665
881
  * @method crypto-policy
666
882
  * @description Sets the system's crypto policies to `DEFAULT:SHA1` using `update-crypto-policies` command.
667
883
  * @param {string} path - The input value, identifier, or path for the operation.
668
- * @param {Object} options - The default underpost runner options for customizing workflow
884
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
669
885
  * @memberof UnderpostRun
670
886
  */
671
887
  'crypto-policy': (path, options = DEFAULT_OPTION) => {
@@ -680,7 +896,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
680
896
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
681
897
  * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
682
898
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
683
- * @param {Object} options - The default underpost runner options for customizing workflow
899
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
684
900
  * @memberof UnderpostRun
685
901
  */
686
902
  sync: async (path, options = DEFAULT_OPTION) => {
@@ -701,7 +917,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
701
917
  replicas = replicas ? replicas : defaultPath[1];
702
918
  versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
703
919
  image = image ? image : defaultPath[3];
704
- node = node ? node : defaultPath[4];
920
+ node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
705
921
  shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
706
922
 
707
923
  if (image && !image.startsWith('localhost'))
@@ -742,13 +958,14 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
742
958
  const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
743
959
  const pullBundleFlag = options.pullBundle ? ' --pull-bundle' : '';
744
960
  const imagePullPolicyFlag = options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : '';
961
+ const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
745
962
 
746
963
  shellExec(
747
964
  `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
748
965
  image ? ` --image ${image}` : ''
749
966
  }${versions ? ` --versions ${versions}` : ''}${
750
967
  options.namespace ? ` --namespace ${options.namespace}` : ''
751
- }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag} ${deployId} ${env}`,
968
+ }${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag} ${deployId} ${env}`,
752
969
  );
753
970
 
754
971
  if (isDeployRunnerContext(path, options)) {
@@ -757,9 +974,9 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
757
974
  `${baseCommand} db ${deployId} ${clusterFlag}${baseClusterCommand} --repo-backup --primary-pod --git --force-clone --preserveUUID ${options.namespace ? ` --ns ${options.namespace}` : ''}`,
758
975
  );
759
976
  shellExec(
760
- `${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}${
761
978
  options.namespace ? ` --namespace ${options.namespace}` : ''
762
- }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}`,
979
+ }${timeoutFlags}${gitCleanFlag}${imagePullPolicyFlag}${sshKeyPathFlag}`,
763
980
  );
764
981
  if (!targetTraffic)
765
982
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -773,7 +990,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
773
990
  * @method stop
774
991
  * @description Stops a deployment by deleting the corresponding Kubernetes deployment and service resources.
775
992
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
776
- * @param {Object} options - The default underpost runner options for customizing workflow
993
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
777
994
  * @memberof UnderpostRun
778
995
  */
779
996
  stop: async (path = '', options = DEFAULT_OPTION) => {
@@ -797,7 +1014,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
797
1014
  * @method ssh-deploy-stop
798
1015
  * @description Stops a remote deployment via SSH by executing the appropriate Underpost command on the remote server.
799
1016
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
800
- * @param {Object} options - The default underpost runner options for customizing workflow
1017
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
801
1018
  * @memberof UnderpostRun
802
1019
  */
803
1020
  'ssh-deploy-stop': async (path, options = DEFAULT_OPTION) => {
@@ -824,7 +1041,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
824
1041
  * @method ssh-deploy-db-rollback
825
1042
  * @description Performs a database rollback on remote deployment via SSH.
826
1043
  * @param {string} path - Comma-separated deployId and optional number of commits to reset (format: "deployId,nCommits")
827
- * @param {Object} options - The default underpost runner options for customizing workflow
1044
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
828
1045
  * @param {string} options.deployId - The deployment identifier
829
1046
  * @param {string} options.user - The SSH user for credential lookup
830
1047
  * @param {boolean} options.dev - Development mode flag
@@ -851,7 +1068,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
851
1068
  * @method ssh-deploy-db
852
1069
  * @description Imports/restores a database on remote deployment via SSH.
853
1070
  * @param {string} path - The deployment ID for database import
854
- * @param {Object} options - The default underpost runner options for customizing workflow
1071
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
855
1072
  * @param {string} options.deployId - The deployment identifier
856
1073
  * @param {string} options.user - The SSH user for credential lookup
857
1074
  * @param {boolean} options.dev - Development mode flag
@@ -876,7 +1093,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
876
1093
  * @method ssh-deploy-db-status
877
1094
  * @description Retrieves database status/stats for a deployment (or all deployments from dd.router) via SSH.
878
1095
  * @param {string} path - Comma-separated deployId(s) or 'dd' to use the dd.router list.
879
- * @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).
880
1097
  * @param {string} options.deployId - Deployment identifier used for SSH config lookup.
881
1098
  * @param {string} options.user - SSH user for credential lookup.
882
1099
  * @param {boolean} options.dev - Development mode flag.
@@ -922,7 +1139,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
922
1139
  * @method tz
923
1140
  * @description Sets the system timezone using `timedatectl set-timezone` command.
924
1141
  * @param {string} path - The input value, identifier, or path for the operation (used as the timezone string).
925
- * @param {Object} options - The default underpost runner options for customizing workflow
1142
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
926
1143
  * @memberof UnderpostRun
927
1144
  */
928
1145
  tz: (path, options = DEFAULT_OPTION) => {
@@ -943,7 +1160,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
943
1160
  * @method get-proxy
944
1161
  * @description Retrieves and logs the HTTPProxy resources in the specified namespace using `kubectl get HTTPProxy`.
945
1162
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional filter for the HTTPProxy resources).
946
- * @param {Object} options - The default underpost runner options for customizing workflow
1163
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
947
1164
  * @memberof UnderpostRun
948
1165
  */
949
1166
  'get-proxy': async (path = '', options = DEFAULT_OPTION) => {
@@ -1050,7 +1267,7 @@ EOF
1050
1267
  /**
1051
1268
  * @method instance
1052
1269
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1053
- * @param {Object} options - The default underpost runner options for customizing workflow
1270
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1054
1271
  * @memberof UnderpostRun
1055
1272
  */
1056
1273
  instance: async (path = '', options = DEFAULT_OPTION) => {
@@ -1119,9 +1336,16 @@ EOF
1119
1336
  deployId: _deployId,
1120
1337
  env,
1121
1338
  version: targetTraffic,
1122
- 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
+ }),
1123
1346
  clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
1124
1347
  gitClean: options.gitClean || false,
1348
+ sshKeyPath: options.sshKeyPath || '',
1125
1349
  });
1126
1350
  // Regenerate the parent deploy's gRPC ClusterIP service pointing to the
1127
1351
  // parent's current traffic colour and apply it before the instance pod starts so
@@ -1215,12 +1439,39 @@ EOF
1215
1439
  }
1216
1440
  },
1217
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
+
1218
1468
  /**
1219
1469
  * @method instance-build-manifest
1220
1470
  * @description Builds a Kubernetes Deployment + Service manifest for a specific instance entry
1221
- * from `conf.instances.json` and writes it to a file.
1222
- * Traffic colour is automatically chosen as the opposite of the current live colour (blue/green),
1223
- * 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`).
1224
1475
  *
1225
1476
  * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
1226
1477
  * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
@@ -1231,7 +1482,7 @@ EOF
1231
1482
  * `<projectPath>/manifests/<env>/deployment.yaml`.
1232
1483
  * In production, files are also copied to `<projectPath>/Dockerfile` and
1233
1484
  * `<projectPath>/deployment.yaml`.
1234
- * @param {Object} options - The default underpost runner options for customizing workflow
1485
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1235
1486
  * @memberof UnderpostRun
1236
1487
  */
1237
1488
  'instance-build-manifest': (path, options = DEFAULT_OPTION) => {
@@ -1310,7 +1561,7 @@ EOF
1310
1561
  path: projectPath,
1311
1562
  imageName: _image,
1312
1563
  podmanSave: true,
1313
- imagePath: projectPath,
1564
+ imageOutPath: projectPath,
1314
1565
  kind: isKind,
1315
1566
  kubeadm: !!options.kubeadm,
1316
1567
  k3s: !!options.k3s,
@@ -1323,15 +1574,8 @@ EOF
1323
1574
  });
1324
1575
  }
1325
1576
 
1326
- // Determine target traffic: opposite of current, or 'blue' if nothing is running yet.
1327
- const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1328
- hostTest: _host,
1329
- namespace: options.namespace,
1330
- });
1331
- const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1332
-
1333
- // Resolve {{grpc-service-dns}} using the parent deploy's current (or default) traffic.
1334
- const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1577
+ const targetTraffic = options.traffic || 'blue';
1578
+ const parentTraffic = targetTraffic;
1335
1579
  const resolvedCmd = _cmd[env].map((c) =>
1336
1580
  c.replaceAll(
1337
1581
  '{{grpc-service-dns}}',
@@ -1379,11 +1623,86 @@ EOF
1379
1623
  image: _image,
1380
1624
  });
1381
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
+
1382
1696
  if (env === 'production') {
1383
1697
  if (fs.existsSync(dockerfileManifestPath)) {
1384
1698
  fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
1385
1699
  }
1386
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
+ }
1387
1706
  logger.info('[instance-build-manifest] Production artifacts copied to project root', {
1388
1707
  rootPath,
1389
1708
  dockerfile: `${rootPath}/Dockerfile`,
@@ -1395,6 +1714,25 @@ EOF
1395
1714
  fs.copyFileSync(ciSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.ci.yml`);
1396
1715
  logger.info(`[instance-build-manifest] CI workflow copied`, { src: ciSrc });
1397
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
+ }
1398
1736
  }
1399
1737
  },
1400
1738
 
@@ -1402,7 +1740,7 @@ EOF
1402
1740
  * @method ls-deployments
1403
1741
  * @description Retrieves and logs a table of Kubernetes deployments using `Underpost.deploy.get`.
1404
1742
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional deployment name filter).
1405
- * @param {Object} options - The default underpost runner options for customizing workflow
1743
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1406
1744
  * @memberof UnderpostRun
1407
1745
  */
1408
1746
  'ls-deployments': async (path, options = DEFAULT_OPTION) => {
@@ -1413,7 +1751,7 @@ EOF
1413
1751
  * @method host-update
1414
1752
  * @description Executes the `rocky-setup.sh` script to update the host system configuration.
1415
1753
  * @param {string} path - The input value, identifier, or path for the operation.
1416
- * @param {Object} options - The default underpost runner options for customizing workflow
1754
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1417
1755
  * @memberof UnderpostRun
1418
1756
  */
1419
1757
  'host-update': async (path, options = DEFAULT_OPTION) => {
@@ -1429,7 +1767,7 @@ EOF
1429
1767
  * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1430
1768
  * so that `crictl` targets the CRI-O socket by default.
1431
1769
  * @param {string} path - Unused.
1432
- * @param {Object} options - The default underpost runner options for customizing workflow.
1770
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
1433
1771
  * @memberof UnderpostRun
1434
1772
  */
1435
1773
  'install-crio': (path, options = DEFAULT_OPTION) => {
@@ -1443,8 +1781,24 @@ gpgcheck=1
1443
1781
  gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/v1.33/rpm/repodata/repomd.xml.key
1444
1782
  EOF`);
1445
1783
  shellExec(`sudo dnf -y install cri-o`);
1446
- // crictl is in the kubernetes repo but excluded by default install it explicitly
1447
- 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
+ }`);
1448
1802
  // Ensure CRI-O uses systemd cgroup driver (matches kubelet default)
1449
1803
  shellExec(`sudo sed -i 's/^#\?cgroup_manager =.*/cgroup_manager = "systemd"/' /etc/crio/crio.conf`, {
1450
1804
  silentOnError: true,
@@ -1464,7 +1818,7 @@ EOF`);
1464
1818
  * @method dd-container
1465
1819
  * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
1466
1820
  * @param {string} path - The input value, identifier, or path for the operation (used as the command to run inside the container).
1467
- * @param {Object} options - The default underpost runner options for customizing workflow
1821
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1468
1822
  * @memberof UnderpostRun
1469
1823
  */
1470
1824
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
@@ -1491,7 +1845,11 @@ EOF`);
1491
1845
  }
1492
1846
 
1493
1847
  if (!currentImage)
1494
- 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
+ );
1495
1853
  // shellExec(`kubectl delete pod ${podName} --ignore-not-found`);
1496
1854
 
1497
1855
  const payload = {
@@ -1519,7 +1877,7 @@ EOF`);
1519
1877
  * @method ip-info
1520
1878
  * @description Executes the `ip-info.sh` script to display IP-related information for the specified path.
1521
1879
  * @param {string} path - The input value, identifier, or path for the operation (used as an argument to the script).
1522
- * @param {Object} options - The default underpost runner options for customizing workflow
1880
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1523
1881
  * @memberof UnderpostRun
1524
1882
  */
1525
1883
  'ip-info': (path, options = DEFAULT_OPTION) => {
@@ -1532,7 +1890,7 @@ EOF`);
1532
1890
  * @method db-client
1533
1891
  * @description Deploys and exposes the Adminer database client application (using `adminer:4.7.6-standalone` image) on the cluster.
1534
1892
  * @param {string} path - The input value, identifier, or path for the operation.
1535
- * @param {Object} options - The default underpost runner options for customizing workflow
1893
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1536
1894
  * @memberof UnderpostRun
1537
1895
  */
1538
1896
  'db-client': async (path, options = DEFAULT_OPTION) => {
@@ -1559,7 +1917,7 @@ EOF`);
1559
1917
  * @method git-conf
1560
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.
1561
1919
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `username,email`).
1562
- * @param {Object} options - The default underpost runner options for customizing workflow
1920
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1563
1921
  * @memberof UnderpostRun
1564
1922
  */
1565
1923
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
@@ -1607,7 +1965,7 @@ EOF`);
1607
1965
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
1608
1966
  * (cert-manager Certificate resources) for each affected deployment.
1609
1967
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
1610
- * @param {Object} options - The default underpost runner options for customizing workflow
1968
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1611
1969
  * @memberof UnderpostRun
1612
1970
  */
1613
1971
  promote: async (path, options = DEFAULT_OPTION) => {
@@ -1661,10 +2019,31 @@ EOF`);
1661
2019
  * @method metrics
1662
2020
  * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
1663
2021
  * @param {string} path - The input value, identifier, or path for the operation.
1664
- * @param {Object} options - The default underpost runner options for customizing workflow
2022
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1665
2023
  * @memberof UnderpostRun
1666
2024
  */
1667
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
+ }
1668
2047
  const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
1669
2048
  let hosts = [];
1670
2049
  for (const deployId of deployList) {
@@ -1678,7 +2057,7 @@ EOF`);
1678
2057
  * @method cluster
1679
2058
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB, MariaDB, Valkey, Contour (Ingress), and Cert-Manager, and deploys all services.
1680
2059
  * @param {string} path - The input value, identifier, or path for the operation.
1681
- * @param {Object} options - The default underpost runner options for customizing workflow
2060
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1682
2061
  * @memberof UnderpostRun
1683
2062
  */
1684
2063
  cluster: async (path = '', options = DEFAULT_OPTION) => {
@@ -1740,7 +2119,7 @@ EOF`);
1740
2119
  * @method deploy
1741
2120
  * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
1742
2121
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
1743
- * @param {Object} options - The default underpost runner options for customizing workflow
2122
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1744
2123
  * @memberof UnderpostRun
1745
2124
  */
1746
2125
  deploy: async (path, options = DEFAULT_OPTION) => {
@@ -1765,7 +2144,7 @@ EOF`);
1765
2144
  * @method disk-clean
1766
2145
  * @description Executes the `disk-clean-sh` script to perform disk cleanup operations.
1767
2146
  * @param {string} path - The input value, identifier, or path for the operation.
1768
- * @param {Object} options - The default underpost runner options for customizing workflow
2147
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1769
2148
  * @memberof UnderpostRun
1770
2149
  */
1771
2150
  'disk-clean': async (path, options = DEFAULT_OPTION) => {
@@ -1778,7 +2157,7 @@ EOF`);
1778
2157
  * @method disk-devices
1779
2158
  * @description Executes the `disk-devices.sh` script to display information about disk devices.
1780
2159
  * @param {string} path - The input value, identifier, or path for the operation.
1781
- * @param {Object} options - The default underpost runner options for customizing workflow
2160
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1782
2161
  * @memberof UnderpostRun
1783
2162
  */
1784
2163
  'disk-devices': async (path = '/', options = DEFAULT_OPTION) => {
@@ -1791,7 +2170,7 @@ EOF`);
1791
2170
  * @method disk-usage
1792
2171
  * @description Displays disk usage statistics using the `du` command, sorted by size.
1793
2172
  * @param {string} path - The input value, identifier, or path for the operation.
1794
- * @param {Object} options - The default underpost runner options for customizing workflow
2173
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1795
2174
  * @memberof UnderpostRun
1796
2175
  */
1797
2176
  'disk-usage': async (path = '/', options = DEFAULT_OPTION) => {
@@ -1806,7 +2185,7 @@ EOF`);
1806
2185
  * @method dev
1807
2186
  * @description Starts development servers for client, API, and proxy based on provided parameters (deployId, host, path, clientHostPort).
1808
2187
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,subConf,host,path,clientHostPort`).
1809
- * @param {Object} options - The default underpost runner options for customizing workflow
2188
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1810
2189
  * @memberof UnderpostRun
1811
2190
  */
1812
2191
  dev: async (path = '', options = DEFAULT_OPTION) => {
@@ -1866,7 +2245,7 @@ EOF`);
1866
2245
  * @method service
1867
2246
  * @description Deploys and exposes specific services (like `mongo-express-service`) on the cluster, updating deployment configurations and monitoring status.
1868
2247
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,serviceId,host,path,replicas,image,node`).
1869
- * @param {Object} options - The default underpost runner options for customizing workflow
2248
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1870
2249
  * @memberof UnderpostRun
1871
2250
  */
1872
2251
  service: async (path = '', options = DEFAULT_OPTION) => {
@@ -1962,7 +2341,7 @@ EOF`);
1962
2341
  * @method etc-hosts
1963
2342
  * @description Generates and logs the contents for the `/etc/hosts` file based on provided hosts or deployment configurations.
1964
2343
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated list of hosts).
1965
- * @param {Object} options - The default underpost runner options for customizing workflow
2344
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1966
2345
  * @memberof UnderpostRun
1967
2346
  */
1968
2347
  'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
@@ -1979,7 +2358,7 @@ EOF`);
1979
2358
  * @method sh
1980
2359
  * @description Enables remote control for the Kitty terminal emulator.
1981
2360
  * @param {string} path - The input value, identifier, or path for the operation.
1982
- * @param {Object} options - The default underpost runner options for customizing workflow
2361
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1983
2362
  * @memberof UnderpostRun
1984
2363
  */
1985
2364
  sh: async (path = '', options = DEFAULT_OPTION) => {
@@ -1999,7 +2378,7 @@ EOF`);
1999
2378
  * @method log
2000
2379
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
2001
2380
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `filePath,keywords,lines`).
2002
- * @param {Object} options - The default underpost runner options for customizing workflow
2381
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2003
2382
  * @memberof UnderpostRun
2004
2383
  */
2005
2384
  log: async (path, options = DEFAULT_OPTION) => {
@@ -2016,7 +2395,7 @@ EOF`);
2016
2395
  * @method ps
2017
2396
  * @description Displays running processes that match a specified path or keyword.
2018
2397
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter processes).
2019
- * @param {Object} options - The default underpost runner options for customizing workflow
2398
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2020
2399
  * @memberof UnderpostRun
2021
2400
  */
2022
2401
  ps: async (path = '', options = DEFAULT_OPTION) => {
@@ -2041,7 +2420,7 @@ EOF`);
2041
2420
  * @method pid-info
2042
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.
2043
2422
  * @param {string} path - The PID of the process to inspect.
2044
- * @param {Object} options - The default underpost runner options for customizing workflow
2423
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2045
2424
  * @memberof UnderpostRun
2046
2425
  */
2047
2426
  'pid-info': (path, options = DEFAULT_OPTION) => {
@@ -2081,7 +2460,7 @@ EOF`);
2081
2460
  * @method background
2082
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`.
2083
2462
  * @param {string} path - The command to run in the background (e.g. 'npm run prod:container dd-cyberia-r3').
2084
- * @param {Object} options - The default underpost runner options for customizing workflow
2463
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2085
2464
  * @memberof UnderpostRun
2086
2465
  */
2087
2466
  background: (path, options = DEFAULT_OPTION) => {
@@ -2101,7 +2480,7 @@ EOF`);
2101
2480
  * @method ports
2102
2481
  * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
2103
2482
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
2104
- * @param {Object} options - The default underpost runner options for customizing workflow
2483
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2105
2484
  * @memberof UnderpostRun
2106
2485
  */
2107
2486
  ports: async (path = '', options = DEFAULT_OPTION) => {
@@ -2113,7 +2492,7 @@ EOF`);
2113
2492
  * @method deploy-test
2114
2493
  * @description Deploys a test deployment (`dd-test`) in either development or production mode, setting up necessary secrets and starting the deployment.
2115
2494
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID).
2116
- * @param {Object} options - The default underpost runner options for customizing workflow
2495
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2117
2496
  * @memberof UnderpostRun
2118
2497
  */
2119
2498
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
@@ -2138,7 +2517,7 @@ EOF`);
2138
2517
  * @method tf-vae-test
2139
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.
2140
2519
  * @param {string} path - The input value, identifier, or path for the operation.
2141
- * @param {Object} options - The default underpost runner options for customizing workflow
2520
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2142
2521
  * @memberof UnderpostRun
2143
2522
  */
2144
2523
  'tf-vae-test': async (path, options = DEFAULT_OPTION) => {
@@ -2238,7 +2617,7 @@ EOF`);
2238
2617
  * @method spark-template
2239
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`.
2240
2619
  * @param {string} path - The input value, identifier, or path for the operation.
2241
- * @param {Object} options - The default underpost runner options for customizing workflow
2620
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2242
2621
  * @memberof UnderpostRun
2243
2622
  */
2244
2623
  'spark-template': (path, options = DEFAULT_OPTION) => {
@@ -2266,7 +2645,7 @@ EOF`);
2266
2645
  * @method pull-rocky-image
2267
2646
  * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
2268
2647
  * @param {string} path - The input value, identifier, or path for the operation.
2269
- * @param {Object} options - The default underpost runner options for customizing workflow
2648
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2270
2649
  * @memberof UnderpostRun
2271
2650
  */
2272
2651
  'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
@@ -2276,7 +2655,7 @@ EOF`);
2276
2655
  * @method rmi
2277
2656
  * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
2278
2657
  * @param {string} path - The input value, identifier, or path for the operation.
2279
- * @param {Object} options - The default underpost runner options for customizing workflow
2658
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2280
2659
  * @memberof UnderpostRun
2281
2660
  */
2282
2661
  rmi: (path, options = DEFAULT_OPTION) => {
@@ -2286,7 +2665,7 @@ EOF`);
2286
2665
  * @method kill
2287
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.
2288
2667
  * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
2289
- * @param {Object} options - The default underpost runner options for customizing workflow
2668
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2290
2669
  * @memberof UnderpostRun
2291
2670
  */
2292
2671
  kill: (path = '', options = DEFAULT_OPTION) => {
@@ -2318,7 +2697,7 @@ EOF`);
2318
2697
  * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
2319
2698
  * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
2320
2699
  * @param {string} path - Optional password length (default: 16).
2321
- * @param {Object} options - The default underpost runner options for customizing workflow.
2700
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2322
2701
  * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
2323
2702
  * @memberof UnderpostRun
2324
2703
  */
@@ -2352,7 +2731,7 @@ EOF`);
2352
2731
  * @method secret
2353
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.
2354
2733
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
2355
- * @param {Object} options - The default underpost runner options for customizing workflow
2734
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2356
2735
  * @memberof UnderpostRun
2357
2736
  */
2358
2737
  secret: (path, options = DEFAULT_OPTION) => {
@@ -2365,7 +2744,7 @@ EOF`);
2365
2744
  * @method underpost-config
2366
2745
  * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
2367
2746
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
2368
- * @param {Object} options - The default underpost runner options for customizing workflow
2747
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2369
2748
  * @memberof UnderpostRun
2370
2749
  */
2371
2750
  'underpost-config': (path = '', options = DEFAULT_OPTION) => {
@@ -2375,7 +2754,7 @@ EOF`);
2375
2754
  * @method gpu-env
2376
2755
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
2377
2756
  * @param {string} path - The input value, identifier, or path for the operation.
2378
- * @param {Object} options - The default underpost runner options for customizing workflow
2757
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2379
2758
  * @memberof UnderpostRun
2380
2759
  */
2381
2760
  'gpu-env': (path, options = DEFAULT_OPTION) => {
@@ -2388,7 +2767,7 @@ EOF`);
2388
2767
  * @method tf-gpu-test
2389
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`.
2390
2769
  * @param {string} path - The input value, identifier, or path for the operation.
2391
- * @param {Object} options - The default underpost runner options for customizing workflow
2770
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2392
2771
  * @memberof UnderpostRun
2393
2772
  */
2394
2773
  'tf-gpu-test': (path, options = DEFAULT_OPTION) => {
@@ -2402,7 +2781,7 @@ EOF`);
2402
2781
  * @method deploy-job
2403
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).
2404
2783
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional script path or job argument).
2405
- * @param {Object} options - The default underpost runner options for customizing workflow
2784
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2406
2785
  * @memberof UnderpostRun
2407
2786
  */
2408
2787
  'deploy-job': async (path, options = DEFAULT_OPTION) => {
@@ -2537,7 +2916,7 @@ EOF`;
2537
2916
  * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2538
2917
  * @param {string} path - Optional `fsPath.splitOption` string.
2539
2918
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2540
- * @param {Object} options - The default underpost runner options for customizing workflow.
2919
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2541
2920
  * @param {string} [options.deployId] - Override deploy ID.
2542
2921
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2543
2922
  * @memberof UnderpostRun
@@ -2625,7 +3004,7 @@ EOF`;
2625
3004
  * so that multi-path deployments are handled correctly.
2626
3005
  * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
2627
3006
  * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
2628
- * @param {Object} options - The default underpost runner options for customizing workflow.
3007
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2629
3008
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
2630
3009
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2631
3010
  * @memberof UnderpostRun
@@ -2717,7 +3096,7 @@ EOF`;
2717
3096
  * @method build-cluster-deployment-manifests
2718
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.
2719
3098
  * @param {string} path - Unused.
2720
- * @param {Object} options - The default underpost runner options for customizing workflow.
3099
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2721
3100
  * @memberof UnderpostRun
2722
3101
  */
2723
3102
  'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
@@ -2730,7 +3109,7 @@ EOF`;
2730
3109
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
2731
3110
  * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
2732
3111
  * @param {string} path - Unused.
2733
- * @param {Object} options - The default underpost runner options for customizing workflow.
3112
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2734
3113
  * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
2735
3114
  * @memberof UnderpostRun
2736
3115
  */
@@ -2754,7 +3133,7 @@ EOF`;
2754
3133
  * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
2755
3134
  * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
2756
3135
  * Customise via the `path` argument or leave empty to use the default.
2757
- * @param {Object} options - The default underpost runner options for customizing workflow.
3136
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2758
3137
  * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
2759
3138
  * @memberof UnderpostRun
2760
3139
  */
@@ -2775,6 +3154,33 @@ EOF`;
2775
3154
 
2776
3155
  logger.info(`[setup-shared-dir] Shared directory setup complete: ${dir}`);
2777
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
+ },
2778
3184
  };
2779
3185
 
2780
3186
  static API = {
@@ -2803,7 +3209,7 @@ EOF`;
2803
3209
  * @description Executes a specified runner function from the UnderpostRun class with the provided path and options.
2804
3210
  * @param {string} runner - The name of the runner to execute.
2805
3211
  * @param {string} path - The input value, identifier, or path for the operation.
2806
- * @param {Object} options - The default underpost runner options for customizing workflow
3212
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2807
3213
  * @memberof UnderpostRun
2808
3214
  * @returns {Promise<any>} The result of the runner execution.
2809
3215
  */
@@ -2816,7 +3222,7 @@ EOF`;
2816
3222
  * @description Initiates the execution of a specified CLI command (runner) with the given input value (`path`) and processed options.
2817
3223
  * @param {string} runner - The name of the runner to execute.
2818
3224
  * @param {string} path - The input value, identifier, or path for the operation.
2819
- * @param {Object} options - The default underpost runner options for customizing workflow
3225
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2820
3226
  * @memberof UnderpostRun
2821
3227
  * @returns {Promise<any>} The result of the callback execution.
2822
3228
  */