underpost 3.2.30 → 3.2.80

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 (46) hide show
  1. package/.github/workflows/ghpkg.ci.yml +87 -0
  2. package/.github/workflows/npmpkg.ci.yml +9 -6
  3. package/.github/workflows/publish.ci.yml +3 -3
  4. package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
  5. package/.github/workflows/release.cd.yml +1 -1
  6. package/CHANGELOG.md +1230 -971
  7. package/CLI-HELP.md +14 -6
  8. package/README.md +3 -3
  9. package/bin/build.js +40 -4
  10. package/bin/deploy.js +2 -2
  11. package/bump.config.js +1 -0
  12. package/docker-compose.yml +26 -26
  13. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/package.json +15 -15
  17. package/scripts/disk-clean.sh +85 -60
  18. package/scripts/test-monitor.sh +1 -1
  19. package/src/api/core/core.controller.js +4 -65
  20. package/src/api/core/core.router.js +8 -14
  21. package/src/api/default/default.controller.js +2 -70
  22. package/src/api/default/default.router.js +7 -17
  23. package/src/api/document/document.controller.js +5 -77
  24. package/src/api/document/document.router.js +9 -13
  25. package/src/api/file/file.controller.js +9 -53
  26. package/src/api/file/file.router.js +14 -6
  27. package/src/api/test/test.controller.js +8 -53
  28. package/src/api/test/test.router.js +1 -4
  29. package/src/cli/cluster.js +104 -11
  30. package/src/cli/db.js +4 -2
  31. package/src/cli/deploy.js +60 -11
  32. package/src/cli/docker-compose.js +212 -1
  33. package/src/cli/fs.js +45 -25
  34. package/src/cli/image.js +147 -51
  35. package/src/cli/index.js +26 -6
  36. package/src/cli/release.js +50 -4
  37. package/src/cli/repository.js +98 -24
  38. package/src/cli/run.js +380 -178
  39. package/src/cli/secrets.js +75 -46
  40. package/src/cli/ssh.js +30 -11
  41. package/src/client/components/core/Modal.js +38 -4
  42. package/src/index.js +1 -1
  43. package/src/server/catalog.js +2 -0
  44. package/src/server/conf.js +163 -3
  45. package/src/server/downloader.js +3 -3
  46. package/src/server/middlewares.js +152 -0
package/src/cli/run.js CHANGED
@@ -14,7 +14,9 @@ import {
14
14
  etcHostFactory,
15
15
  getNpmRootPath,
16
16
  isDeployRunnerContext,
17
+ loadConfInstances,
17
18
  loadConfServerJson,
19
+ selectConfInstances,
18
20
  loadReplicas,
19
21
  writeEnv,
20
22
  } from '../server/conf.js';
@@ -22,7 +24,7 @@ import { actionInitLog, loggerFactory } from '../server/logger.js';
22
24
 
23
25
  import fs from 'fs-extra';
24
26
  import net from 'net';
25
- import { range, setPad, timer } from '../client/components/core/CommonJs.js';
27
+ import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
26
28
 
27
29
  import os from 'os';
28
30
  import Underpost from '../index.js';
@@ -49,9 +51,36 @@ const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 20
49
51
 
50
52
  const logger = loggerFactory(import.meta);
51
53
 
54
+ /**
55
+ * @method instanceProxyRoutesFactory
56
+ * @description Renders the HTTPProxy route block for every instance sharing a host.
57
+ * Routes are emitted longest-prefix first so a specific instance path (`/FOREST`)
58
+ * is never shadowed by the default instance's catch-all (`/`).
59
+ * @param {string} deployId - Parent deployment identifier.
60
+ * @param {Array<object>} instances - Expanded instance entries bound to one host.
61
+ * @param {string} env - `development` | `production`.
62
+ * @param {Object<string,string>} trafficById - Instance id → traffic colour.
63
+ * @returns {string} Concatenated route YAML.
64
+ */
65
+ const instanceProxyRoutesFactory = ({ deployId, instances, env, trafficById }) =>
66
+ [...instances]
67
+ .sort((a, b) => (b.path || '/').length - (a.path || '/').length)
68
+ .map((instance) =>
69
+ Underpost.deploy.deploymentYamlServiceFactory({
70
+ path: instance.path,
71
+ port: env === 'development' && instance.fromDebugPort ? instance.fromDebugPort : instance.fromPort,
72
+ deployId: `${deployId}-${instance.id}`,
73
+ env,
74
+ deploymentVersions: [trafficById[instance.id] || 'blue'],
75
+ pathRewritePolicy: instance.pathRewritePolicy,
76
+ }),
77
+ )
78
+ .join('');
79
+
52
80
  /**
53
81
  * @constant DEFAULT_OPTION
54
82
  * @description Default options for the UnderpostRun class.
83
+ * @typedef {Object} UnderpostRunDefaultOptions
55
84
  * @type {Object}
56
85
  * @property {boolean} dev - Whether to run in development mode.
57
86
  * @property {string} podName - The name of the pod to run.
@@ -122,6 +151,7 @@ const logger = loggerFactory(import.meta);
122
151
  * @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).
123
152
  * @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
124
153
  * @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
154
+ * @property {string} branch - The Git branch to use for operations (e.g., for template-deploy, ssh-deploy).
125
155
  * @memberof UnderpostRun
126
156
  */
127
157
  const DEFAULT_OPTION = {
@@ -192,6 +222,7 @@ const DEFAULT_OPTION = {
192
222
  pullBundle: false,
193
223
  remove: false,
194
224
  test: false,
225
+ branch: '',
195
226
  };
196
227
 
197
228
  /**
@@ -215,7 +246,7 @@ class UnderpostRun {
215
246
  * @method dev-cluster
216
247
  * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
217
248
  * @param {string} path - The input value, identifier, or path for the operation.
218
- * @param {Object} options - The default underpost runner options for customizing workflow
249
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
219
250
  * @memberof UnderpostRun
220
251
  */
221
252
  'dev-cluster': (path, options = DEFAULT_OPTION) => {
@@ -297,7 +328,7 @@ class UnderpostRun {
297
328
  * @method metadata
298
329
  * @description Generates metadata for the specified path after exposing the development cluster.
299
330
  * @param {string} path - The input value, identifier, or path for the operation.
300
- * @param {Object} options - The default underpost runner options for customizing workflow
331
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
301
332
  * @memberof UnderpostRun
302
333
  */
303
334
  metadata: async (path, options = DEFAULT_OPTION) => {
@@ -322,7 +353,7 @@ class UnderpostRun {
322
353
  * @method svc-ls
323
354
  * @description Lists systemd services and installed packages, optionally filtering by the provided path.
324
355
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional filter for services and packages).
325
- * @param {Object} options - The default underpost runner options for customizing workflow
356
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
326
357
  * @memberof UnderpostRun
327
358
  */
328
359
  'svc-ls': (path, options = DEFAULT_OPTION) => {
@@ -342,7 +373,7 @@ class UnderpostRun {
342
373
  * @method svc-rm
343
374
  * @description Removes a systemd service by stopping it, disabling it, uninstalling the package, and deleting related files.
344
375
  * @param {string} path - The input value, identifier, or path for the operation (used as the service name).
345
- * @param {Object} options - The default underpost runner options for customizing workflow
376
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
346
377
  * @memberof UnderpostRun
347
378
  */
348
379
  'svc-rm': (path, options = DEFAULT_OPTION) => {
@@ -357,7 +388,7 @@ class UnderpostRun {
357
388
  * @method ssh-deploy-info
358
389
  * @description Retrieves deployment status and pod information from a remote server via SSH.
359
390
  * @param {string} path - The input value, identifier, or path for the operation.
360
- * @param {Object} options - The default underpost runner options for customizing workflow
391
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
361
392
  * @memberof UnderpostRun
362
393
  */
363
394
  'ssh-deploy-info': async (path = '', options = DEFAULT_OPTION) => {
@@ -401,7 +432,7 @@ class UnderpostRun {
401
432
  * skipped (move the owning controller). StatefulSets bound to node-local PVs may stay
402
433
  * Pending after a move until their volume is available on the target node.
403
434
  * @param {string} path - Resource selector (`kind/name`, `kind`, or empty).
404
- * @param {Object} options - The default underpost runner options for customizing workflow
435
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
405
436
  * @memberof UnderpostRun
406
437
  * @returns {Array<{ref:string,kind:string,status:string,node?:string}>} Per-resource outcome.
407
438
  */
@@ -601,7 +632,7 @@ class UnderpostRun {
601
632
  * @method dev-hosts-expose
602
633
  * @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
603
634
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
604
- * @param {Object} options - The default underpost runner options for customizing workflow
635
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
605
636
  * @memberof UnderpostRun
606
637
  */
607
638
  'dev-hosts-expose': (path, options = DEFAULT_OPTION) => {
@@ -614,7 +645,7 @@ class UnderpostRun {
614
645
  * @method dev-hosts-restore
615
646
  * @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
616
647
  * @param {string} path - The input value, identifier, or path for the operation.
617
- * @param {Object} options - The default underpost runner options for customizing workflow
648
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
618
649
  * @memberof UnderpostRun
619
650
  */
620
651
  'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
@@ -625,7 +656,7 @@ class UnderpostRun {
625
656
  * @method cluster-build
626
657
  * @description Build configuration for cluster deployment.
627
658
  * @param {string} path - The input value, identifier, or path for the operation.
628
- * @param {Object} options - The default underpost runner options for customizing workflow
659
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
629
660
  * @memberof UnderpostRun
630
661
  */
631
662
  'cluster-build': (path, options = DEFAULT_OPTION) => {
@@ -649,7 +680,7 @@ class UnderpostRun {
649
680
  * and optionally triggers engine-<conf-id> CI with sync/init which in turn dispatches the CD workflow
650
681
  * after the build chain completes (template → ghpkg → engine-<conf-id> → CD).
651
682
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
652
- * @param {Object} options - The default underpost runner options for customizing workflow
683
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
653
684
  * @memberof UnderpostRun
654
685
  */
655
686
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
@@ -663,13 +694,10 @@ class UnderpostRun {
663
694
  shellExec(`${baseCommand} run pull`);
664
695
  shellExec(`${baseCommand} run shared-dir`);
665
696
 
666
- // Capture last N commit messages for propagation.
667
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
668
- const fromN =
669
- options.fromNCommit && parseInt(options.fromNCommit) > 0
670
- ? parseInt(options.fromNCommit)
671
- : Underpost.repo.getUnpushedCount('.').count;
672
- const message = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
697
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
698
+ // propagation to pwa-microservices-template and every engine-* repo.
699
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
700
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
673
701
  silent: true,
674
702
  stdout: true,
675
703
  }).trim();
@@ -681,8 +709,6 @@ class UnderpostRun {
681
709
  );
682
710
  shellCd('/home/dd/engine');
683
711
 
684
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(message);
685
-
686
712
  // Push engine repo so workflow YAML changes reach GitHub
687
713
  shellExec(`git reset`);
688
714
  shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
@@ -718,10 +744,11 @@ class UnderpostRun {
718
744
  if (deployConfId) inputs.deploy_conf_id = deployConfId;
719
745
  if (deployType) inputs.deploy_type = deployType;
720
746
 
747
+ // Omit `ref` so dispatchWorkflow auto-detects the repo's default branch
748
+ // (a fork may default to `main` rather than the monorepo's `master`).
721
749
  Underpost.repo.dispatchWorkflow({
722
750
  repo,
723
751
  workflowFile: 'npmpkg.ci.yml',
724
- ref: 'master',
725
752
  inputs,
726
753
  });
727
754
  },
@@ -730,7 +757,7 @@ class UnderpostRun {
730
757
  * @method template-deploy-local
731
758
  * @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.
732
759
  * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
733
- * @param {Object} options - The default underpost runner options for customizing workflow
760
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
734
761
  * @memberof UnderpostRun
735
762
  */
736
763
  'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
@@ -744,17 +771,12 @@ class UnderpostRun {
744
771
  shellExec(`${baseCommand} run pull`);
745
772
  shellExec(`${baseCommand} run shared-dir`);
746
773
 
747
- // Capture last N commit messages from the engine repo.
748
- // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
749
- const fromN =
750
- options.fromNCommit && parseInt(options.fromNCommit) > 0
751
- ? parseInt(options.fromNCommit)
752
- : Underpost.repo.getUnpushedCount('.').count;
753
- const rawMessage = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
774
+ // Capture the sanitized message from the last N commits (--from-n-commit, default 1).
775
+ const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
776
+ const sanitizedMessage = shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
754
777
  silent: true,
755
778
  stdout: true,
756
779
  }).trim();
757
- const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
758
780
 
759
781
  const { triggerCmd } = path
760
782
  ? await Underpost.release.ci(path, sanitizedMessage, options)
@@ -766,15 +788,14 @@ class UnderpostRun {
766
788
  * @description Dispatches the Docker image CI workflow (`docker-image[.<runtime>].ci.yml`) via `workflow_dispatch`.
767
789
  * Repository resolution is delegated to `Underpost.repo.resolveInstanceRepo(path)`.
768
790
  * @param {string} path - Optional runtime / workflow suffix (e.g. `cyberia-server`, `cyberia-client`).
769
- * @param {Object} options - The default underpost runner options for customizing workflow
791
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
770
792
  * @memberof UnderpostRun
771
793
  */
772
794
  'docker-image': (path, options = DEFAULT_OPTION) => {
773
- const repo = Underpost.repo.resolveInstanceRepo(path);
795
+ const repo = Underpost.repo.resolveInstanceRepo(path, !options.test);
774
796
  Underpost.repo.dispatchWorkflow({
775
797
  repo,
776
- workflowFile: `docker-image${path ? `.${path}` : ''}.ci.yml`,
777
- ref: 'master',
798
+ workflowFile: `docker-image${path ? `.${path}` : ''}${options.dev ? '.dev' : ''}.ci.yml`,
778
799
  inputs: {},
779
800
  });
780
801
  },
@@ -782,7 +803,7 @@ class UnderpostRun {
782
803
  * @method clean
783
804
  * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
784
805
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional directory path).
785
- * @param {Object} options - The default underpost runner options for customizing workflow
806
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
786
807
  * @memberof UnderpostRun
787
808
  */
788
809
  clean: (path = '', options = DEFAULT_OPTION) => {
@@ -793,7 +814,7 @@ class UnderpostRun {
793
814
  * @method pull
794
815
  * @description Clones or pulls updates for the `engine` and `engine-private` repositories into `/home/dd/engine` and `/home/dd/engine/engine-private`.
795
816
  * @param {string} path - The input value, identifier, or path for the operation.
796
- * @param {Object} options - The default underpost runner options for customizing workflow
817
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
797
818
  * @memberof UnderpostRun
798
819
  */
799
820
  pull: (path, options = DEFAULT_OPTION) => {
@@ -820,7 +841,7 @@ class UnderpostRun {
820
841
  * @method release-deploy
821
842
  * @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
822
843
  * @param {string} path - The input value, identifier, or path for the operation.
823
- * @param {Object} options - The default underpost runner options for customizing workflow
844
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
824
845
  * @memberof UnderpostRun
825
846
  */
826
847
  'release-deploy': (path, options = DEFAULT_OPTION) => {
@@ -836,7 +857,7 @@ class UnderpostRun {
836
857
  * @method ssh-deploy
837
858
  * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
838
859
  * @param {string} path - The deployment identifier (e.g., 'engine-core', 'sync-engine-core', 'init-engine-core').
839
- * @param {Object} options - The default underpost runner options for customizing workflow
860
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
840
861
  * @memberof UnderpostRun
841
862
  */
842
863
  'ssh-deploy': (path, options = DEFAULT_OPTION) => {
@@ -851,11 +872,13 @@ class UnderpostRun {
851
872
  job = 'init';
852
873
  confId = path.replace(/^init-/, '');
853
874
  }
854
-
875
+ const repo = Underpost.repo.resolveInstanceRepo(confId, !options.test);
876
+ // Omit `ref` so dispatchWorkflow auto-detects the target repo's default
877
+ // branch (getDefaultBranch): the monorepo is `master` but instance repos
878
+ // like engine-cyberia default to `main` — hardcoding either 422s.
855
879
  Underpost.repo.dispatchWorkflow({
856
- repo: `${process.env.GITHUB_USERNAME}/engine`,
880
+ repo,
857
881
  workflowFile: `${confId}.cd.yml`,
858
- ref: 'master',
859
882
  inputs: { job },
860
883
  });
861
884
  },
@@ -864,7 +887,7 @@ class UnderpostRun {
864
887
  * @description Opens a Visual Studio Code (VS Code) session for the specified path using `node ${underpostRoot}/bin/zed ${path}`,
865
888
  * or installs Zed and sublime-text IDE if `path` is 'install'.
866
889
  * @param {string} path - The input value, identifier, or path for the operation (used as the path to the directory to open in the IDE).
867
- * @param {Object} options - The default underpost runner options for customizing workflow
890
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
868
891
  * @memberof UnderpostRun
869
892
  */
870
893
  ide: (path = '', options = DEFAULT_OPTION) => {
@@ -891,7 +914,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
891
914
  * @method crypto-policy
892
915
  * @description Sets the system's crypto policies to `DEFAULT:SHA1` using `update-crypto-policies` command.
893
916
  * @param {string} path - The input value, identifier, or path for the operation.
894
- * @param {Object} options - The default underpost runner options for customizing workflow
917
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
895
918
  * @memberof UnderpostRun
896
919
  */
897
920
  'crypto-policy': (path, options = DEFAULT_OPTION) => {
@@ -906,7 +929,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
906
929
  * `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
907
930
  * `node bin run sync dd-core --kubeadm --image-pull-policy Always`
908
931
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing deploy parameters).
909
- * @param {Object} options - The default underpost runner options for customizing workflow
932
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
910
933
  * @memberof UnderpostRun
911
934
  */
912
935
  sync: async (path, options = DEFAULT_OPTION) => {
@@ -1000,7 +1023,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1000
1023
  * @method stop
1001
1024
  * @description Stops a deployment by deleting the corresponding Kubernetes deployment and service resources.
1002
1025
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
1003
- * @param {Object} options - The default underpost runner options for customizing workflow
1026
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1004
1027
  * @memberof UnderpostRun
1005
1028
  */
1006
1029
  stop: async (path = '', options = DEFAULT_OPTION) => {
@@ -1024,7 +1047,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1024
1047
  * @method ssh-deploy-stop
1025
1048
  * @description Stops a remote deployment via SSH by executing the appropriate Underpost command on the remote server.
1026
1049
  * @param {string} path - The input value, identifier, or path for the operation (used to determine which traffic to stop).
1027
- * @param {Object} options - The default underpost runner options for customizing workflow
1050
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1028
1051
  * @memberof UnderpostRun
1029
1052
  */
1030
1053
  'ssh-deploy-stop': async (path, options = DEFAULT_OPTION) => {
@@ -1051,7 +1074,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1051
1074
  * @method ssh-deploy-db-rollback
1052
1075
  * @description Performs a database rollback on remote deployment via SSH.
1053
1076
  * @param {string} path - Comma-separated deployId and optional number of commits to reset (format: "deployId,nCommits")
1054
- * @param {Object} options - The default underpost runner options for customizing workflow
1077
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1055
1078
  * @param {string} options.deployId - The deployment identifier
1056
1079
  * @param {string} options.user - The SSH user for credential lookup
1057
1080
  * @param {boolean} options.dev - Development mode flag
@@ -1078,7 +1101,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1078
1101
  * @method ssh-deploy-db
1079
1102
  * @description Imports/restores a database on remote deployment via SSH.
1080
1103
  * @param {string} path - The deployment ID for database import
1081
- * @param {Object} options - The default underpost runner options for customizing workflow
1104
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1082
1105
  * @param {string} options.deployId - The deployment identifier
1083
1106
  * @param {string} options.user - The SSH user for credential lookup
1084
1107
  * @param {boolean} options.dev - Development mode flag
@@ -1103,7 +1126,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1103
1126
  * @method ssh-deploy-db-status
1104
1127
  * @description Retrieves database status/stats for a deployment (or all deployments from dd.router) via SSH.
1105
1128
  * @param {string} path - Comma-separated deployId(s) or 'dd' to use the dd.router list.
1106
- * @param {Object} options - Runner options (uses options.deployId for SSH host lookup).
1129
+ * @param {UnderpostRunDefaultOptions} options - Runner options (uses options.deployId for SSH host lookup).
1107
1130
  * @param {string} options.deployId - Deployment identifier used for SSH config lookup.
1108
1131
  * @param {string} options.user - SSH user for credential lookup.
1109
1132
  * @param {boolean} options.dev - Development mode flag.
@@ -1149,7 +1172,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1149
1172
  * @method tz
1150
1173
  * @description Sets the system timezone using `timedatectl set-timezone` command.
1151
1174
  * @param {string} path - The input value, identifier, or path for the operation (used as the timezone string).
1152
- * @param {Object} options - The default underpost runner options for customizing workflow
1175
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1153
1176
  * @memberof UnderpostRun
1154
1177
  */
1155
1178
  tz: (path, options = DEFAULT_OPTION) => {
@@ -1170,7 +1193,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1170
1193
  * @method get-proxy
1171
1194
  * @description Retrieves and logs the HTTPProxy resources in the specified namespace using `kubectl get HTTPProxy`.
1172
1195
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional filter for the HTTPProxy resources).
1173
- * @param {Object} options - The default underpost runner options for customizing workflow
1196
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1174
1197
  * @memberof UnderpostRun
1175
1198
  */
1176
1199
  'get-proxy': async (path = '', options = DEFAULT_OPTION) => {
@@ -1192,66 +1215,54 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
1192
1215
 
1193
1216
  'instance-promote': async (path, options = DEFAULT_OPTION) => {
1194
1217
  const env = options.dev ? 'development' : 'production';
1195
- const baseCommand = options.dev ? 'node bin' : 'underpost';
1196
- const baseClusterCommand = options.dev ? ' --dev' : '';
1197
1218
  let [deployId, id] = path.split(',');
1198
- const confInstances = JSON.parse(
1199
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
1200
- );
1219
+ const confInstances = loadConfInstances(deployId);
1220
+ const promoted = selectConfInstances(confInstances, id);
1201
1221
  let promotedTraffic = '';
1202
- for (const instance of confInstances) {
1203
- let {
1204
- id: _id,
1205
- host: _host,
1206
- path: _path,
1207
- image: _image,
1208
- fromPort: _fromPort,
1209
- toPort: _toPort,
1210
- fromDebugPort: _fromDebugPort,
1211
- toDebugPort: _toDebugPort,
1212
- cmd: _cmd,
1213
- volumes: _volumes,
1214
- metadata: _metadata,
1215
- } = instance;
1216
- if (id !== _id) continue;
1217
- const _deployId = `${deployId}-${_id}`;
1218
- // Use debug ports in development when defined, fall back to production ports.
1219
- if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
1220
- if (env === 'development' && _toDebugPort) _toPort = _toDebugPort;
1221
- const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1222
- hostTest: _host,
1222
+
1223
+ // A Contour HTTPProxy is named after its host, so every instance sharing a
1224
+ // host shares one object. Rebuilding it from the promoted instance alone
1225
+ // would drop its siblings' routes, so each host is rendered from the full
1226
+ // set: promoted instances flip colour, the rest keep their live colour.
1227
+ const promotedIds = new Set(promoted.map((instance) => instance.id));
1228
+ const hosts = [...new Set(promoted.map((instance) => instance.host))];
1229
+ const affected = confInstances.filter((instance) => hosts.includes(instance.host));
1230
+ const trafficById = {};
1231
+ for (const instance of affected) {
1232
+ const currentTraffic = Underpost.deploy.getCurrentTraffic(`${deployId}-${instance.id}`, {
1233
+ hostTest: instance.host,
1223
1234
  namespace: options.namespace,
1235
+ env,
1224
1236
  });
1225
- const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1226
- promotedTraffic = targetTraffic;
1237
+ if (!promotedIds.has(instance.id)) {
1238
+ trafficById[instance.id] = currentTraffic || 'blue';
1239
+ continue;
1240
+ }
1241
+ trafficById[instance.id] = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1242
+ promotedTraffic = trafficById[instance.id];
1243
+ }
1244
+
1245
+ for (const host of hosts) {
1246
+ const hostInstances = affected.filter((instance) => instance.host === host);
1227
1247
  let proxyYaml =
1228
- Underpost.deploy.baseProxyYamlFactory({ host: _host, env: options.tls ? 'production' : env, options }) +
1229
- Underpost.deploy.deploymentYamlServiceFactory({
1230
- path: _path,
1231
- port: _fromPort,
1232
- // serviceId: deployId,
1233
- deployId: _deployId,
1234
- env,
1235
- deploymentVersions: [targetTraffic],
1236
- // pathRewritePolicy,
1237
- });
1248
+ Underpost.deploy.baseProxyYamlFactory({ host, env: options.tls ? 'production' : env, options }) +
1249
+ instanceProxyRoutesFactory({ deployId, instances: hostInstances, env, trafficById });
1238
1250
  if (options.tls) {
1239
1251
  if (options.test) {
1240
- const sslDir = `./engine-private/ssl/${_host}`;
1241
- const nameSafe = _host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1252
+ const sslDir = `./engine-private/ssl/${host}`;
1253
+ const nameSafe = host.replace(/[^a-zA-Z0-9_.-]/g, '_');
1242
1254
  fs.mkdirpSync(sslDir);
1243
- shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${_host}"`);
1244
- shellExec(`kubectl delete secret ${_host} -n ${options.namespace} --ignore-not-found`);
1255
+ shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${host}"`);
1256
+ shellExec(`kubectl delete secret ${host} -n ${options.namespace} --ignore-not-found`);
1245
1257
  shellExec(
1246
- `kubectl create secret tls ${_host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1258
+ `kubectl create secret tls ${host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
1247
1259
  );
1248
1260
  } else {
1249
- shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
1250
- proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
1261
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${options.namespace} --ignore-not-found`);
1262
+ proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host });
1251
1263
  }
1252
1264
  }
1253
- // console.log(proxyYaml);
1254
- shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
1265
+ shellExec(`kubectl delete HTTPProxy ${host} --namespace ${options.namespace} --ignore-not-found`);
1255
1266
  shellExec(
1256
1267
  `kubectl apply -f - -n ${options.namespace} <<EOF
1257
1268
  ${proxyYaml}
@@ -1277,7 +1288,7 @@ EOF
1277
1288
  /**
1278
1289
  * @method instance
1279
1290
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string containing workflow parameters).
1280
- * @param {Object} options - The default underpost runner options for customizing workflow
1291
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1281
1292
  * @memberof UnderpostRun
1282
1293
  */
1283
1294
  instance: async (path = '', options = DEFAULT_OPTION) => {
@@ -1286,9 +1297,7 @@ EOF
1286
1297
  const baseClusterCommand = options.dev ? ' --dev' : '';
1287
1298
  let [deployId, id, replicas] = path.split(',');
1288
1299
  if (!replicas) replicas = options.replicas;
1289
- const confInstances = JSON.parse(
1290
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
1291
- );
1300
+ const confInstances = selectConfInstances(loadConfInstances(deployId), id);
1292
1301
  const etcHosts = [];
1293
1302
  for (const instance of confInstances) {
1294
1303
  let {
@@ -1307,7 +1316,6 @@ EOF
1307
1316
  readinessProbe: _readinessProbe,
1308
1317
  livenessProbe: _livenessProbe,
1309
1318
  } = instance;
1310
- if (id !== _id) continue;
1311
1319
  const _deployId = `${deployId}-${_id}`;
1312
1320
  // Use debug ports in development when defined, fall back to production ports.
1313
1321
  if (env === 'development' && _fromDebugPort) _fromPort = _fromDebugPort;
@@ -1331,6 +1339,7 @@ EOF
1331
1339
  const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1332
1340
  hostTest: _host,
1333
1341
  namespace: options.namespace,
1342
+ env,
1334
1343
  });
1335
1344
 
1336
1345
  const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
@@ -1436,25 +1445,54 @@ EOF
1436
1445
  logger.error(`Deployment ${deployId} did not become ready in time.`);
1437
1446
  return;
1438
1447
  }
1439
- shellExec(
1440
- `${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
1441
- `${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
1442
- `${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
1443
- ` instance-promote '${path}'`,
1444
- );
1445
1448
  }
1449
+ // Promote (switch the HTTPProxy to the new colour) ONLY after EVERY
1450
+ // instance in the family is deployed and ready — never per-instance inside
1451
+ // the loop. A per-variant switch would flip server.cyberiaonline.com to the
1452
+ // default's new deployment while -forest/-test are still being created,
1453
+ // pointing routes at services that don't exist yet. instance-promote
1454
+ // rebuilds the whole host proxy (all variant routes) in one shot, so a
1455
+ // single call with the family id promotes the family atomically.
1456
+ if (!options.expose) await UnderpostRun.RUNNERS['instance-promote'](`${deployId},${id}`, options);
1446
1457
  if (options.etcHosts) {
1447
1458
  const hostListenResult = etcHostFactory(etcHosts);
1448
1459
  logger.info(hostListenResult.renderHosts);
1449
1460
  }
1450
1461
  },
1451
1462
 
1463
+ /**
1464
+ * @method deploy-key
1465
+ * @description Copies the deploy key for a specific user and deployId to a temporary location on the local machine.
1466
+ * @param {string} path - The input value, identifier, or path for the operation (not used in this method).
1467
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1468
+ * @param {string} options.user - The user for which to copy the deploy key.
1469
+ * @param {string} options.deployId - The deployment identifier associated with the deploy key.
1470
+ * @memberof UnderpostRun
1471
+ */
1472
+ 'deploy-key': (path, options = DEFAULT_OPTION) => {
1473
+ const prefix = 'dd-key';
1474
+ if (options.reset) {
1475
+ shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
1476
+ return;
1477
+ }
1478
+ if (!options.user || !options.deployId) {
1479
+ logger.error('Both --user and --deploy-id options are required to copy the deploy key.');
1480
+ return;
1481
+ }
1482
+ const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
1483
+ fs.mkdirSync('/home/dd/tmp', { recursive: true });
1484
+ fs.copyFileSync(`./engine-private/conf/${options.deployId}/users/${options.user}/id_rsa`, targetPath);
1485
+ logger.info(`Copied deploy key to ${targetPath}`);
1486
+ if (options.copy) pbcopy(targetPath);
1487
+ },
1488
+
1452
1489
  /**
1453
1490
  * @method instance-build-manifest
1454
1491
  * @description Builds a Kubernetes Deployment + Service manifest for a specific instance entry
1455
- * from `conf.instances.json` and writes it to a file.
1456
- * Traffic colour is automatically chosen as the opposite of the current live colour (blue/green),
1457
- * defaulting to `blue` when no deployment is running yet.
1492
+ * from `conf.instances.json` and writes it to a file. This is a purely local
1493
+ * artifact generator: it never probes a live cluster. Traffic colour defaults
1494
+ * to the canonical initial `blue` and can be overridden with `--traffic`; the
1495
+ * real blue/green swap is resolved at deploy time (`deploy --sync`).
1458
1496
  *
1459
1497
  * If `--build` is supplied the image is built from the project Dockerfile and loaded into the
1460
1498
  * cluster before the manifest is written (kind by default; `--kubeadm` / `--k3s` override).
@@ -1465,33 +1503,49 @@ EOF
1465
1503
  * `<projectPath>/manifests/<env>/deployment.yaml`.
1466
1504
  * In production, files are also copied to `<projectPath>/Dockerfile` and
1467
1505
  * `<projectPath>/deployment.yaml`.
1468
- * @param {Object} options - The default underpost runner options for customizing workflow
1506
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1469
1507
  * @memberof UnderpostRun
1470
1508
  */
1471
1509
  'instance-build-manifest': (path, options = DEFAULT_OPTION) => {
1472
1510
  const env = options.dev ? 'development' : 'production';
1473
1511
  let [deployId, id, projectPath] = path.split(',');
1474
1512
  const rootPath = projectPath ? projectPath : '.';
1513
+
1514
+ const confInstances = loadConfInstances(deployId);
1515
+ // Targeting a template id builds every world in the family. The fan-out
1516
+ // re-enters this runner with `instanceOnly` because the default world
1517
+ // keeps the template id verbatim — without it, selecting `mmo-server`
1518
+ // would match the family again and recurse forever. Only that default
1519
+ // world publishes to the project root, so the repo keeps exactly one
1520
+ // canonical Dockerfile/deployment.yaml pair.
1521
+ const selected = options.instanceOnly
1522
+ ? confInstances.filter((instance) => instance.id === id)
1523
+ : selectConfInstances(confInstances, id);
1524
+ if (selected.length === 0) {
1525
+ logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
1526
+ return;
1527
+ }
1528
+ if (!options.instanceOnly && (selected.length > 1 || selected[0].id !== id)) {
1529
+ for (const instance of selected)
1530
+ UnderpostRun.RUNNERS['instance-build-manifest'](
1531
+ [deployId, instance.id, projectPath].filter((v) => v !== undefined).join(','),
1532
+ { ...options, instanceOnly: true },
1533
+ );
1534
+ return;
1535
+ }
1536
+
1475
1537
  const envManifestPath = `${rootPath}/manifests/deployments/${id}-${env}`;
1476
1538
  const outputPath = `${envManifestPath}/deployment.yaml`;
1477
1539
  const dockerfileManifestPath = `${envManifestPath}/Dockerfile`;
1478
1540
 
1479
1541
  fs.mkdirpSync(envManifestPath);
1480
1542
 
1481
- const confInstances = JSON.parse(
1482
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
1483
- );
1484
-
1485
- const instance = confInstances.find((i) => i.id === id);
1486
- if (!instance) {
1487
- logger.error(`Instance with id '${id}' not found in conf.instances.json for deployId '${deployId}'`);
1488
- return;
1489
- }
1543
+ const instance = selected[0];
1544
+ const isDefaultInstance = instance.id === instance.templateId || !instance.templateId;
1490
1545
 
1491
1546
  let {
1492
1547
  id: _id,
1493
1548
  host: _host,
1494
- path: _path,
1495
1549
  image: _image,
1496
1550
  fromPort: _fromPort,
1497
1551
  toPort: _toPort,
@@ -1544,7 +1598,7 @@ EOF
1544
1598
  path: projectPath,
1545
1599
  imageName: _image,
1546
1600
  podmanSave: true,
1547
- imagePath: projectPath,
1601
+ imageOutPath: projectPath,
1548
1602
  kind: isKind,
1549
1603
  kubeadm: !!options.kubeadm,
1550
1604
  k3s: !!options.k3s,
@@ -1557,15 +1611,8 @@ EOF
1557
1611
  });
1558
1612
  }
1559
1613
 
1560
- // Determine target traffic: opposite of current, or 'blue' if nothing is running yet.
1561
- const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
1562
- hostTest: _host,
1563
- namespace: options.namespace,
1564
- });
1565
- const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
1566
-
1567
- // Resolve {{grpc-service-dns}} using the parent deploy's current (or default) traffic.
1568
- const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1614
+ const targetTraffic = options.traffic || 'blue';
1615
+ const parentTraffic = targetTraffic;
1569
1616
  const resolvedCmd = _cmd[env].map((c) =>
1570
1617
  c.replaceAll(
1571
1618
  '{{grpc-service-dns}}',
@@ -1613,11 +1660,122 @@ EOF
1613
1660
  image: _image,
1614
1661
  });
1615
1662
 
1616
- if (env === 'production') {
1663
+ // --- Sibling manifests (pv-pvc, proxy, grpc-service) ------------------
1664
+ // Emit the same apply-able set the parent deploy writes to build/<env>,
1665
+ // scoped to this instance so the project repo and engine-private ship
1666
+ // more than just deployment.yaml. Content matches what `run instance`
1667
+ // creates dynamically at deploy time (deployVolume / instance-promote /
1668
+ // the parent gRPC ClusterIP), so a static `kubectl apply` is equivalent.
1669
+ const pvDataNode = Underpost.deploy.resolveDeployNode({
1670
+ node: options.nodeName,
1671
+ kind: options.kind,
1672
+ kubeadm: options.kubeadm,
1673
+ k3s: options.k3s,
1674
+ env,
1675
+ });
1676
+
1677
+ // pv-pvc.yaml — one PV+PVC per instance volume; names mirror deployVolume.
1678
+ let pvPvcYaml = '';
1679
+ for (const volume of _volumes || []) {
1680
+ if (!volume.claimName) continue;
1681
+ const pvcId = `${volume.claimName}-${_deployId}-${env}-${targetTraffic}`;
1682
+ const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${_deployId}-${env}-${targetTraffic}`;
1683
+ pvPvcYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
1684
+ pvcId,
1685
+ namespace: options.namespace,
1686
+ hostPath: `/home/dd/engine/volume/${pvId}`,
1687
+ nodeName: pvDataNode,
1688
+ })}\n`;
1689
+ }
1690
+
1691
+ // proxy.yaml — this instance's OWN route only (its sub-path → its own
1692
+ // service), so each variant's build dir carries a distinct, instance-scoped
1693
+ // fragment rather than an identical copy of the whole host proxy. The
1694
+ // complete host HTTPProxy — every variant's route aggregated onto the one
1695
+ // fqdn, each pointing at its live colour — is assembled and applied by
1696
+ // `instance-promote` at deploy time, and only once EVERY variant is ready.
1697
+ const proxyYaml =
1698
+ Underpost.deploy.baseProxyYamlFactory({ host: _host, env, options }) +
1699
+ instanceProxyRoutesFactory({
1700
+ deployId,
1701
+ instances: [instance],
1702
+ env,
1703
+ trafficById: { [instance.id]: targetTraffic },
1704
+ });
1705
+
1706
+ // grpc-service.yaml — the parent deploy's gRPC ClusterIP (shared; the
1707
+ // instance cmd resolves {{grpc-service-dns}} to it). Reuse the parent's
1708
+ // generated manifest when present rather than regenerating it here.
1709
+ const parentGrpcServicePath = `./engine-private/conf/${deployId}/build/${env}/grpc-service.yaml`;
1710
+ const grpcServiceYaml = fs.existsSync(parentGrpcServicePath)
1711
+ ? fs.readFileSync(parentGrpcServicePath, 'utf8')
1712
+ : '';
1713
+
1714
+ // Write the sibling set next to deployment.yaml (project) and into the
1715
+ // engine-private per-instance build dir (mirrors instances/<id>/ layout).
1716
+ const instanceBuildDir = `./engine-private/conf/${deployId}/instances/${_id}/build/${env}`;
1717
+ fs.mkdirpSync(instanceBuildDir);
1718
+ fs.writeFileSync(`${instanceBuildDir}/deployment.yaml`, deploymentYaml, 'utf8');
1719
+ const siblingManifests = {
1720
+ 'pv-pvc.yaml': pvPvcYaml,
1721
+ 'proxy.yaml': proxyYaml,
1722
+ 'grpc-service.yaml': grpcServiceYaml,
1723
+ };
1724
+ for (const [name, content] of Object.entries(siblingManifests)) {
1725
+ if (!content) continue;
1726
+ fs.writeFileSync(`${envManifestPath}/${name}`, content, 'utf8');
1727
+ fs.writeFileSync(`${instanceBuildDir}/${name}`, content, 'utf8');
1728
+ }
1729
+ logger.info('[instance-build-manifest] Sibling manifests written', {
1730
+ project: envManifestPath,
1731
+ enginePrivate: instanceBuildDir,
1732
+ pvPvc: !!pvPvcYaml,
1733
+ proxy: !!proxyYaml,
1734
+ grpcService: !!grpcServiceYaml,
1735
+ });
1736
+
1737
+ // --- Per-instance env files -----------------------------------------
1738
+ // Each env file is seeded from the template instance's env file for the
1739
+ // same mode so operator-owned keys (API keys, memory limits, service URLs)
1740
+ // are inherited verbatim; only the keys the instance declares under
1741
+ // `multiInstance.env` — plus the derived container id — are (re)written.
1742
+ //
1743
+ // A derived instance's env dir is generated in full: both development.env
1744
+ // and production.env are written on every build, so a deploy in either
1745
+ // environment always finds the env file its `cmd` sources, no matter which
1746
+ // mode this build ran. The default/template instance owns the committed
1747
+ // source files, so only its current-mode file is idempotently refreshed.
1748
+ if (instance.templateId) {
1749
+ const instanceEnvDir = `./engine-private/conf/${deployId}/instances/${_id}/env`;
1750
+ fs.mkdirpSync(instanceEnvDir);
1751
+ const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
1752
+ for (const targetEnv of envsToWrite) {
1753
+ const templateEnvPath = `./engine-private/conf/${deployId}/instances/${instance.templateId}/env/${targetEnv}.env`;
1754
+ const baseEnv = fs.existsSync(templateEnvPath) ? dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8')) : {};
1755
+ writeEnv(`${instanceEnvDir}/${targetEnv}.env`, {
1756
+ ...baseEnv,
1757
+ ...(instance.instanceEnv?.[targetEnv] || {}),
1758
+ CONTAINER_DEPLOY_ID: `${_deployId}-${targetEnv}`,
1759
+ });
1760
+ }
1761
+ logger.info('[instance-build-manifest] Instance env written', {
1762
+ dir: instanceEnvDir,
1763
+ instanceCode: instance.instanceCode,
1764
+ envs: envsToWrite,
1765
+ keys: Object.keys(instance.instanceEnv?.[env] || {}),
1766
+ });
1767
+ }
1768
+
1769
+ if (env === 'production' && isDefaultInstance) {
1617
1770
  if (fs.existsSync(dockerfileManifestPath)) {
1618
1771
  fs.copyFileSync(dockerfileManifestPath, `${rootPath}/Dockerfile`);
1619
1772
  }
1620
1773
  fs.copyFileSync(outputPath, `${rootPath}/deployment.yaml`);
1774
+ // Sibling manifests alongside deployment.yaml at the project root.
1775
+ for (const name of ['pv-pvc.yaml', 'proxy.yaml', 'grpc-service.yaml']) {
1776
+ const src = `${envManifestPath}/${name}`;
1777
+ if (fs.existsSync(src)) fs.copyFileSync(src, `${rootPath}/${name}`);
1778
+ }
1621
1779
  logger.info('[instance-build-manifest] Production artifacts copied to project root', {
1622
1780
  rootPath,
1623
1781
  dockerfile: `${rootPath}/Dockerfile`,
@@ -1629,6 +1787,25 @@ EOF
1629
1787
  fs.copyFileSync(ciSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.ci.yml`);
1630
1788
  logger.info(`[instance-build-manifest] CI workflow copied`, { src: ciSrc });
1631
1789
  }
1790
+
1791
+ // Ship the development variant alongside production so the instance repo
1792
+ // is self-contained: the dev Dockerfile (built by the -dev CI workflow
1793
+ // into underpost/<runtime>-dev, consumed by the development compose
1794
+ // stack) and its dispatchable workflow. Both are optional — synced only
1795
+ // when the source-of-truth files exist in the engine repo.
1796
+ if (_runtime) {
1797
+ const devDockerfileSrc = `src/runtime/${_runtime}/Dockerfile.dev`;
1798
+ if (fs.existsSync(devDockerfileSrc)) {
1799
+ fs.copyFileSync(devDockerfileSrc, `${rootPath}/Dockerfile.dev`);
1800
+ logger.info('[instance-build-manifest] Dev Dockerfile copied', { src: devDockerfileSrc });
1801
+ }
1802
+ const devCiSrc = `./.github/workflows/docker-image.${_runtime}.dev.ci.yml`;
1803
+ if (fs.existsSync(devCiSrc)) {
1804
+ if (!fs.existsSync(`${rootPath}/.github/workflows`)) fs.mkdirpSync(`${rootPath}/.github/workflows`);
1805
+ fs.copyFileSync(devCiSrc, `${rootPath}/.github/workflows/docker-image.${_runtime}.dev.ci.yml`);
1806
+ logger.info(`[instance-build-manifest] Dev CI workflow copied`, { src: devCiSrc });
1807
+ }
1808
+ }
1632
1809
  }
1633
1810
  },
1634
1811
 
@@ -1636,7 +1813,7 @@ EOF
1636
1813
  * @method ls-deployments
1637
1814
  * @description Retrieves and logs a table of Kubernetes deployments using `Underpost.deploy.get`.
1638
1815
  * @param {string} path - The input value, identifier, or path for the operation (used as an optional deployment name filter).
1639
- * @param {Object} options - The default underpost runner options for customizing workflow
1816
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1640
1817
  * @memberof UnderpostRun
1641
1818
  */
1642
1819
  'ls-deployments': async (path, options = DEFAULT_OPTION) => {
@@ -1647,7 +1824,7 @@ EOF
1647
1824
  * @method host-update
1648
1825
  * @description Executes the `rocky-setup.sh` script to update the host system configuration.
1649
1826
  * @param {string} path - The input value, identifier, or path for the operation.
1650
- * @param {Object} options - The default underpost runner options for customizing workflow
1827
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1651
1828
  * @memberof UnderpostRun
1652
1829
  */
1653
1830
  'host-update': async (path, options = DEFAULT_OPTION) => {
@@ -1663,7 +1840,7 @@ EOF
1663
1840
  * the systemd cgroup driver, enables the `crio` service, and writes `/etc/crictl.yaml`
1664
1841
  * so that `crictl` targets the CRI-O socket by default.
1665
1842
  * @param {string} path - Unused.
1666
- * @param {Object} options - The default underpost runner options for customizing workflow.
1843
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
1667
1844
  * @memberof UnderpostRun
1668
1845
  */
1669
1846
  'install-crio': (path, options = DEFAULT_OPTION) => {
@@ -1714,7 +1891,7 @@ EOF`);
1714
1891
  * @method dd-container
1715
1892
  * @description Deploys a development or debug container tasks jobs, setting up necessary volumes and images, and running specified commands within the container.
1716
1893
  * @param {string} path - The input value, identifier, or path for the operation (used as the command to run inside the container).
1717
- * @param {Object} options - The default underpost runner options for customizing workflow
1894
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1718
1895
  * @memberof UnderpostRun
1719
1896
  */
1720
1897
  'dd-container': async (path = '', options = DEFAULT_OPTION) => {
@@ -1741,7 +1918,11 @@ EOF`);
1741
1918
  }
1742
1919
 
1743
1920
  if (!currentImage)
1744
- shellExec(`${baseCommand} image${baseClusterCommand} --pull-base ${options.dev ? '--kind' : '--kubeadm'}`);
1921
+ shellExec(
1922
+ `${baseCommand} image${baseClusterCommand} --pull-base --build --path ${
1923
+ options.dev ? '.' : options.underpostRoot
1924
+ } ${options.dev ? '--kind' : '--kubeadm'}`,
1925
+ );
1745
1926
  // shellExec(`kubectl delete pod ${podName} --ignore-not-found`);
1746
1927
 
1747
1928
  const payload = {
@@ -1769,7 +1950,7 @@ EOF`);
1769
1950
  * @method ip-info
1770
1951
  * @description Executes the `ip-info.sh` script to display IP-related information for the specified path.
1771
1952
  * @param {string} path - The input value, identifier, or path for the operation (used as an argument to the script).
1772
- * @param {Object} options - The default underpost runner options for customizing workflow
1953
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1773
1954
  * @memberof UnderpostRun
1774
1955
  */
1775
1956
  'ip-info': (path, options = DEFAULT_OPTION) => {
@@ -1782,7 +1963,7 @@ EOF`);
1782
1963
  * @method db-client
1783
1964
  * @description Deploys and exposes the Adminer database client application (using `adminer:4.7.6-standalone` image) on the cluster.
1784
1965
  * @param {string} path - The input value, identifier, or path for the operation.
1785
- * @param {Object} options - The default underpost runner options for customizing workflow
1966
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1786
1967
  * @memberof UnderpostRun
1787
1968
  */
1788
1969
  'db-client': async (path, options = DEFAULT_OPTION) => {
@@ -1809,7 +1990,7 @@ EOF`);
1809
1990
  * @method git-conf
1810
1991
  * @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.
1811
1992
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `username,email`).
1812
- * @param {Object} options - The default underpost runner options for customizing workflow
1993
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1813
1994
  * @memberof UnderpostRun
1814
1995
  */
1815
1996
  'git-conf': (path = '', options = DEFAULT_OPTION) => {
@@ -1857,7 +2038,7 @@ EOF`);
1857
2038
  * TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
1858
2039
  * (cert-manager Certificate resources) for each affected deployment.
1859
2040
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated string: `deployId,env,replicas`).
1860
- * @param {Object} options - The default underpost runner options for customizing workflow
2041
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1861
2042
  * @memberof UnderpostRun
1862
2043
  */
1863
2044
  promote: async (path, options = DEFAULT_OPTION) => {
@@ -1911,10 +2092,31 @@ EOF`);
1911
2092
  * @method metrics
1912
2093
  * @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
1913
2094
  * @param {string} path - The input value, identifier, or path for the operation.
1914
- * @param {Object} options - The default underpost runner options for customizing workflow
2095
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1915
2096
  * @memberof UnderpostRun
1916
2097
  */
1917
2098
  metrics: async (path, options = DEFAULT_OPTION) => {
2099
+ if (path === 'server') {
2100
+ shellExec(
2101
+ `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
2102
+ );
2103
+ await timer(2000);
2104
+
2105
+ shellExec(`kubectl patch deployment metrics-server -n kube-system \
2106
+ --type='json' \
2107
+ -p='[
2108
+ {
2109
+ "op":"add",
2110
+ "path":"/spec/template/spec/containers/0/args/-",
2111
+ "value":"--kubelet-insecure-tls"
2112
+ }
2113
+ ]'`);
2114
+ shellExec(`kubectl scale deployment metrics-server \
2115
+ -n kube-system \
2116
+ --replicas=1`);
2117
+
2118
+ return;
2119
+ }
1918
2120
  const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
1919
2121
  let hosts = [];
1920
2122
  for (const deployId of deployList) {
@@ -1928,7 +2130,7 @@ EOF`);
1928
2130
  * @method cluster
1929
2131
  * @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB, MariaDB, Valkey, Contour (Ingress), and Cert-Manager, and deploys all services.
1930
2132
  * @param {string} path - The input value, identifier, or path for the operation.
1931
- * @param {Object} options - The default underpost runner options for customizing workflow
2133
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1932
2134
  * @memberof UnderpostRun
1933
2135
  */
1934
2136
  cluster: async (path = '', options = DEFAULT_OPTION) => {
@@ -1990,7 +2192,7 @@ EOF`);
1990
2192
  * @method deploy
1991
2193
  * @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
1992
2194
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
1993
- * @param {Object} options - The default underpost runner options for customizing workflow
2195
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
1994
2196
  * @memberof UnderpostRun
1995
2197
  */
1996
2198
  deploy: async (path, options = DEFAULT_OPTION) => {
@@ -2015,7 +2217,7 @@ EOF`);
2015
2217
  * @method disk-clean
2016
2218
  * @description Executes the `disk-clean-sh` script to perform disk cleanup operations.
2017
2219
  * @param {string} path - The input value, identifier, or path for the operation.
2018
- * @param {Object} options - The default underpost runner options for customizing workflow
2220
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2019
2221
  * @memberof UnderpostRun
2020
2222
  */
2021
2223
  'disk-clean': async (path, options = DEFAULT_OPTION) => {
@@ -2028,7 +2230,7 @@ EOF`);
2028
2230
  * @method disk-devices
2029
2231
  * @description Executes the `disk-devices.sh` script to display information about disk devices.
2030
2232
  * @param {string} path - The input value, identifier, or path for the operation.
2031
- * @param {Object} options - The default underpost runner options for customizing workflow
2233
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2032
2234
  * @memberof UnderpostRun
2033
2235
  */
2034
2236
  'disk-devices': async (path = '/', options = DEFAULT_OPTION) => {
@@ -2041,7 +2243,7 @@ EOF`);
2041
2243
  * @method disk-usage
2042
2244
  * @description Displays disk usage statistics using the `du` command, sorted by size.
2043
2245
  * @param {string} path - The input value, identifier, or path for the operation.
2044
- * @param {Object} options - The default underpost runner options for customizing workflow
2246
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2045
2247
  * @memberof UnderpostRun
2046
2248
  */
2047
2249
  'disk-usage': async (path = '/', options = DEFAULT_OPTION) => {
@@ -2056,7 +2258,7 @@ EOF`);
2056
2258
  * @method dev
2057
2259
  * @description Starts development servers for client, API, and proxy based on provided parameters (deployId, host, path, clientHostPort).
2058
2260
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,subConf,host,path,clientHostPort`).
2059
- * @param {Object} options - The default underpost runner options for customizing workflow
2261
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2060
2262
  * @memberof UnderpostRun
2061
2263
  */
2062
2264
  dev: async (path = '', options = DEFAULT_OPTION) => {
@@ -2116,7 +2318,7 @@ EOF`);
2116
2318
  * @method service
2117
2319
  * @description Deploys and exposes specific services (like `mongo-express-service`) on the cluster, updating deployment configurations and monitoring status.
2118
2320
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `deployId,serviceId,host,path,replicas,image,node`).
2119
- * @param {Object} options - The default underpost runner options for customizing workflow
2321
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2120
2322
  * @memberof UnderpostRun
2121
2323
  */
2122
2324
  service: async (path = '', options = DEFAULT_OPTION) => {
@@ -2212,7 +2414,7 @@ EOF`);
2212
2414
  * @method etc-hosts
2213
2415
  * @description Generates and logs the contents for the `/etc/hosts` file based on provided hosts or deployment configurations.
2214
2416
  * @param {string} path - The input value, identifier, or path for the operation (used as a comma-separated list of hosts).
2215
- * @param {Object} options - The default underpost runner options for customizing workflow
2417
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2216
2418
  * @memberof UnderpostRun
2217
2419
  */
2218
2420
  'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
@@ -2229,7 +2431,7 @@ EOF`);
2229
2431
  * @method sh
2230
2432
  * @description Enables remote control for the Kitty terminal emulator.
2231
2433
  * @param {string} path - The input value, identifier, or path for the operation.
2232
- * @param {Object} options - The default underpost runner options for customizing workflow
2434
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2233
2435
  * @memberof UnderpostRun
2234
2436
  */
2235
2437
  sh: async (path = '', options = DEFAULT_OPTION) => {
@@ -2249,7 +2451,7 @@ EOF`);
2249
2451
  * @method log
2250
2452
  * @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
2251
2453
  * @param {string} path - The input value, identifier, or path for the operation (formatted as `filePath,keywords,lines`).
2252
- * @param {Object} options - The default underpost runner options for customizing workflow
2454
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2253
2455
  * @memberof UnderpostRun
2254
2456
  */
2255
2457
  log: async (path, options = DEFAULT_OPTION) => {
@@ -2266,7 +2468,7 @@ EOF`);
2266
2468
  * @method ps
2267
2469
  * @description Displays running processes that match a specified path or keyword.
2268
2470
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter processes).
2269
- * @param {Object} options - The default underpost runner options for customizing workflow
2471
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2270
2472
  * @memberof UnderpostRun
2271
2473
  */
2272
2474
  ps: async (path = '', options = DEFAULT_OPTION) => {
@@ -2291,7 +2493,7 @@ EOF`);
2291
2493
  * @method pid-info
2292
2494
  * @description Displays detailed information about a process by PID, including service details, command line, executable path, working directory, environment variables, and parent process tree.
2293
2495
  * @param {string} path - The PID of the process to inspect.
2294
- * @param {Object} options - The default underpost runner options for customizing workflow
2496
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2295
2497
  * @memberof UnderpostRun
2296
2498
  */
2297
2499
  'pid-info': (path, options = DEFAULT_OPTION) => {
@@ -2331,7 +2533,7 @@ EOF`);
2331
2533
  * @method background
2332
2534
  * @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`.
2333
2535
  * @param {string} path - The command to run in the background (e.g. 'npm run prod:container dd-cyberia-r3').
2334
- * @param {Object} options - The default underpost runner options for customizing workflow
2536
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2335
2537
  * @memberof UnderpostRun
2336
2538
  */
2337
2539
  background: (path, options = DEFAULT_OPTION) => {
@@ -2351,7 +2553,7 @@ EOF`);
2351
2553
  * @method ports
2352
2554
  * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
2353
2555
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
2354
- * @param {Object} options - The default underpost runner options for customizing workflow
2556
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2355
2557
  * @memberof UnderpostRun
2356
2558
  */
2357
2559
  ports: async (path = '', options = DEFAULT_OPTION) => {
@@ -2363,7 +2565,7 @@ EOF`);
2363
2565
  * @method deploy-test
2364
2566
  * @description Deploys a test deployment (`dd-test`) in either development or production mode, setting up necessary secrets and starting the deployment.
2365
2567
  * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID).
2366
- * @param {Object} options - The default underpost runner options for customizing workflow
2568
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2367
2569
  * @memberof UnderpostRun
2368
2570
  */
2369
2571
  'deploy-test': async (path, options = DEFAULT_OPTION) => {
@@ -2388,7 +2590,7 @@ EOF`);
2388
2590
  * @method tf-vae-test
2389
2591
  * @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.
2390
2592
  * @param {string} path - The input value, identifier, or path for the operation.
2391
- * @param {Object} options - The default underpost runner options for customizing workflow
2593
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2392
2594
  * @memberof UnderpostRun
2393
2595
  */
2394
2596
  'tf-vae-test': async (path, options = DEFAULT_OPTION) => {
@@ -2488,7 +2690,7 @@ EOF`);
2488
2690
  * @method spark-template
2489
2691
  * @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`.
2490
2692
  * @param {string} path - The input value, identifier, or path for the operation.
2491
- * @param {Object} options - The default underpost runner options for customizing workflow
2693
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2492
2694
  * @memberof UnderpostRun
2493
2695
  */
2494
2696
  'spark-template': (path, options = DEFAULT_OPTION) => {
@@ -2516,7 +2718,7 @@ EOF`);
2516
2718
  * @method pull-rocky-image
2517
2719
  * @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
2518
2720
  * @param {string} path - The input value, identifier, or path for the operation.
2519
- * @param {Object} options - The default underpost runner options for customizing workflow
2721
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2520
2722
  * @memberof UnderpostRun
2521
2723
  */
2522
2724
  'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
@@ -2526,7 +2728,7 @@ EOF`);
2526
2728
  * @method rmi
2527
2729
  * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
2528
2730
  * @param {string} path - The input value, identifier, or path for the operation.
2529
- * @param {Object} options - The default underpost runner options for customizing workflow
2731
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2530
2732
  * @memberof UnderpostRun
2531
2733
  */
2532
2734
  rmi: (path, options = DEFAULT_OPTION) => {
@@ -2536,7 +2738,7 @@ EOF`);
2536
2738
  * @method kill
2537
2739
  * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
2538
2740
  * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
2539
- * @param {Object} options - The default underpost runner options for customizing workflow
2741
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2540
2742
  * @memberof UnderpostRun
2541
2743
  */
2542
2744
  kill: (path = '', options = DEFAULT_OPTION) => {
@@ -2568,7 +2770,7 @@ EOF`);
2568
2770
  * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
2569
2771
  * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
2570
2772
  * @param {string} path - Optional password length (default: 16).
2571
- * @param {Object} options - The default underpost runner options for customizing workflow.
2773
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2572
2774
  * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
2573
2775
  * @memberof UnderpostRun
2574
2776
  */
@@ -2602,7 +2804,7 @@ EOF`);
2602
2804
  * @method secret
2603
2805
  * @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.
2604
2806
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
2605
- * @param {Object} options - The default underpost runner options for customizing workflow
2807
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2606
2808
  * @memberof UnderpostRun
2607
2809
  */
2608
2810
  secret: (path, options = DEFAULT_OPTION) => {
@@ -2615,7 +2817,7 @@ EOF`);
2615
2817
  * @method underpost-config
2616
2818
  * @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
2617
2819
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
2618
- * @param {Object} options - The default underpost runner options for customizing workflow
2820
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2619
2821
  * @memberof UnderpostRun
2620
2822
  */
2621
2823
  'underpost-config': (path = '', options = DEFAULT_OPTION) => {
@@ -2625,7 +2827,7 @@ EOF`);
2625
2827
  * @method gpu-env
2626
2828
  * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
2627
2829
  * @param {string} path - The input value, identifier, or path for the operation.
2628
- * @param {Object} options - The default underpost runner options for customizing workflow
2830
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2629
2831
  * @memberof UnderpostRun
2630
2832
  */
2631
2833
  'gpu-env': (path, options = DEFAULT_OPTION) => {
@@ -2638,7 +2840,7 @@ EOF`);
2638
2840
  * @method tf-gpu-test
2639
2841
  * @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`.
2640
2842
  * @param {string} path - The input value, identifier, or path for the operation.
2641
- * @param {Object} options - The default underpost runner options for customizing workflow
2843
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2642
2844
  * @memberof UnderpostRun
2643
2845
  */
2644
2846
  'tf-gpu-test': (path, options = DEFAULT_OPTION) => {
@@ -2652,7 +2854,7 @@ EOF`);
2652
2854
  * @method deploy-job
2653
2855
  * @description Creates and applies a custom Kubernetes Pod manifest (Job) for running arbitrary commands inside a container image (defaulting to a TensorFlow/NVIDIA image).
2654
2856
  * @param {string} path - The input value, identifier, or path for the operation (used as the optional script path or job argument).
2655
- * @param {Object} options - The default underpost runner options for customizing workflow
2857
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
2656
2858
  * @memberof UnderpostRun
2657
2859
  */
2658
2860
  'deploy-job': async (path, options = DEFAULT_OPTION) => {
@@ -2787,7 +2989,7 @@ EOF`;
2787
2989
  * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2788
2990
  * @param {string} path - Optional `fsPath.splitOption` string.
2789
2991
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2790
- * @param {Object} options - The default underpost runner options for customizing workflow.
2992
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2791
2993
  * @param {string} [options.deployId] - Override deploy ID.
2792
2994
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2793
2995
  * @memberof UnderpostRun
@@ -2875,7 +3077,7 @@ EOF`;
2875
3077
  * so that multi-path deployments are handled correctly.
2876
3078
  * @param {string} path - Optional comma-separated host name(s) to restrict processing (e.g. 'underpost.net' or 'a.com,b.com').
2877
3079
  * If omitted, all hosts from `engine-private/conf/<deployId>/conf.server.json` are used.
2878
- * @param {Object} options - The default underpost runner options for customizing workflow.
3080
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2879
3081
  * @param {string} [options.deployId] - Deploy ID for storage lookup (defaults to 'dd-default').
2880
3082
  * @param {boolean} [options.dev] - Use development environment; defaults to production.
2881
3083
  * @memberof UnderpostRun
@@ -2967,7 +3169,7 @@ EOF`;
2967
3169
  * @method build-cluster-deployment-manifests
2968
3170
  * @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.
2969
3171
  * @param {string} path - Unused.
2970
- * @param {Object} options - The default underpost runner options for customizing workflow.
3172
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2971
3173
  * @memberof UnderpostRun
2972
3174
  */
2973
3175
  'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
@@ -2980,7 +3182,7 @@ EOF`;
2980
3182
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
2981
3183
  * and opens the cockpit firewall service. With `--remove`, closes the firewall service instead.
2982
3184
  * @param {string} path - Unused.
2983
- * @param {Object} options - The default underpost runner options for customizing workflow.
3185
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
2984
3186
  * `options.remove` — when true, removes the cockpit firewall rule instead of adding it.
2985
3187
  * @memberof UnderpostRun
2986
3188
  */
@@ -3004,7 +3206,7 @@ EOF`;
3004
3206
  * Use `reload-shared-dir` for subsequent permission repairs without recreating the group.
3005
3207
  * @param {string} path - Target directory to set up (defaults to `/home/dd/engine`).
3006
3208
  * Customise via the `path` argument or leave empty to use the default.
3007
- * @param {Object} options - The default underpost runner options for customizing workflow.
3209
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
3008
3210
  * Key fields: `options.user` (default `'admin'`), `options.group` (default `'engine-dev'`).
3009
3211
  * @memberof UnderpostRun
3010
3212
  */
@@ -3032,7 +3234,7 @@ EOF`;
3032
3234
  * write throughout the shared workspace while preserving existing ownership.
3033
3235
  *
3034
3236
  * @param {string} path - Shared directory (defaults to `/home/dd/engine`).
3035
- * @param {Object} options - Underpost runner options.
3237
+ * @param {UnderpostRunDefaultOptions} options - Underpost runner options.
3036
3238
  * Key fields:
3037
3239
  * - options.user (default: 'admin')
3038
3240
  * - options.group (default: 'engine-dev')
@@ -3080,7 +3282,7 @@ EOF`;
3080
3282
  * @description Executes a specified runner function from the UnderpostRun class with the provided path and options.
3081
3283
  * @param {string} runner - The name of the runner to execute.
3082
3284
  * @param {string} path - The input value, identifier, or path for the operation.
3083
- * @param {Object} options - The default underpost runner options for customizing workflow
3285
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3084
3286
  * @memberof UnderpostRun
3085
3287
  * @returns {Promise<any>} The result of the runner execution.
3086
3288
  */
@@ -3093,7 +3295,7 @@ EOF`;
3093
3295
  * @description Initiates the execution of a specified CLI command (runner) with the given input value (`path`) and processed options.
3094
3296
  * @param {string} runner - The name of the runner to execute.
3095
3297
  * @param {string} path - The input value, identifier, or path for the operation.
3096
- * @param {Object} options - The default underpost runner options for customizing workflow
3298
+ * @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
3097
3299
  * @memberof UnderpostRun
3098
3300
  * @returns {Promise<any>} The result of the callback execution.
3099
3301
  */