underpost 3.1.2 → 3.2.0

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 (98) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/ghpkg.ci.yml +4 -4
  3. package/.github/workflows/npmpkg.ci.yml +38 -7
  4. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -4
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  6. package/.github/workflows/release.cd.yml +4 -4
  7. package/CHANGELOG.md +365 -1
  8. package/CLI-HELP.md +55 -3
  9. package/README.md +7 -3
  10. package/bin/build.js +18 -12
  11. package/bin/deploy.js +205 -225
  12. package/bin/file.js +3 -0
  13. package/conf.js +4 -10
  14. package/jsdoc.json +1 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +72 -50
  19. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  20. package/manifests/deployment/playwright/deployment.yaml +1 -1
  21. package/nodemon.json +1 -1
  22. package/package.json +21 -14
  23. package/scripts/ports-ls.sh +2 -0
  24. package/scripts/rhel-grpc-setup.sh +56 -0
  25. package/src/api/file/file.ref.json +18 -0
  26. package/src/api/user/user.service.js +8 -7
  27. package/src/cli/cluster.js +7 -7
  28. package/src/cli/db.js +76 -242
  29. package/src/cli/deploy.js +104 -65
  30. package/src/cli/env.js +1 -0
  31. package/src/cli/fs.js +2 -1
  32. package/src/cli/index.js +50 -1
  33. package/src/cli/kubectl.js +211 -0
  34. package/src/cli/release.js +284 -0
  35. package/src/cli/repository.js +328 -112
  36. package/src/cli/run.js +283 -69
  37. package/src/cli/test.js +3 -3
  38. package/src/client/Default.index.js +3 -4
  39. package/src/client/components/core/Alert.js +2 -2
  40. package/src/client/components/core/AppStore.js +69 -0
  41. package/src/client/components/core/CalendarCore.js +2 -2
  42. package/src/client/components/core/Docs.js +9 -2
  43. package/src/client/components/core/DropDown.js +129 -17
  44. package/src/client/components/core/Keyboard.js +2 -2
  45. package/src/client/components/core/LogIn.js +2 -2
  46. package/src/client/components/core/LogOut.js +2 -2
  47. package/src/client/components/core/Modal.js +0 -1
  48. package/src/client/components/core/Panel.js +0 -1
  49. package/src/client/components/core/PanelForm.js +19 -19
  50. package/src/client/components/core/RichText.js +1 -2
  51. package/src/client/components/core/SocketIo.js +82 -29
  52. package/src/client/components/core/SocketIoHandler.js +75 -0
  53. package/src/client/components/core/Stream.js +143 -95
  54. package/src/client/components/core/Webhook.js +40 -7
  55. package/src/client/components/default/AppStoreDefault.js +5 -0
  56. package/src/client/components/default/LogInDefault.js +3 -3
  57. package/src/client/components/default/LogOutDefault.js +2 -2
  58. package/src/client/components/default/MenuDefault.js +5 -5
  59. package/src/client/components/default/SocketIoDefault.js +3 -51
  60. package/src/client/services/core/core.service.js +20 -8
  61. package/src/client/services/user/user.management.js +2 -2
  62. package/src/client/ssr/body/404.js +15 -11
  63. package/src/client/ssr/body/500.js +15 -11
  64. package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
  65. package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
  66. package/src/client/ssr/pages/Test.js +11 -10
  67. package/src/index.js +24 -1
  68. package/src/runtime/express/Express.js +26 -9
  69. package/src/runtime/lampp/Dockerfile +9 -2
  70. package/src/runtime/lampp/Lampp.js +4 -3
  71. package/src/runtime/wp/Dockerfile +64 -0
  72. package/src/runtime/wp/Wp.js +497 -0
  73. package/src/server/auth.js +30 -6
  74. package/src/server/backup.js +19 -1
  75. package/src/server/client-build-docs.js +51 -110
  76. package/src/server/client-build.js +55 -64
  77. package/src/server/client-formatted.js +109 -57
  78. package/src/server/conf.js +19 -15
  79. package/src/server/ipfs-client.js +24 -1
  80. package/src/server/peer.js +8 -0
  81. package/src/server/runtime.js +25 -1
  82. package/src/server/start.js +21 -8
  83. package/src/ws/IoInterface.js +1 -10
  84. package/src/ws/IoServer.js +14 -33
  85. package/src/ws/core/channels/core.ws.chat.js +65 -20
  86. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  87. package/src/ws/core/channels/core.ws.stream.js +90 -31
  88. package/src/ws/core/core.ws.connection.js +12 -33
  89. package/src/ws/core/core.ws.emit.js +10 -26
  90. package/src/ws/core/core.ws.server.js +25 -58
  91. package/src/ws/default/channels/default.ws.main.js +53 -12
  92. package/src/ws/default/default.ws.connection.js +26 -13
  93. package/src/ws/default/default.ws.server.js +30 -12
  94. package/src/client/components/default/ElementsDefault.js +0 -38
  95. package/src/ws/core/management/core.ws.chat.js +0 -8
  96. package/src/ws/core/management/core.ws.mailer.js +0 -16
  97. package/src/ws/core/management/core.ws.stream.js +0 -8
  98. package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/run.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, getTerminalPid, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, getTerminalPid, pbcopy, shellCd, shellExec } from '../server/process.js';
8
+ import crypto from 'crypto';
8
9
  import {
9
10
  awaitDeployMonitor,
10
11
  buildKindPorts,
@@ -17,12 +18,31 @@ import {
17
18
  import { actionInitLog, loggerFactory } from '../server/logger.js';
18
19
 
19
20
  import fs from 'fs-extra';
21
+ import net from 'net';
20
22
  import { range, setPad, timer } from '../client/components/core/CommonJs.js';
21
23
 
22
24
  import os from 'os';
23
25
  import Underpost from '../index.js';
24
26
  import dotenv from 'dotenv';
25
27
 
28
+ const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
29
+ new Promise((resolve, reject) => {
30
+ let attempts = 0;
31
+ const tryConnect = () => {
32
+ attempts++;
33
+ const socket = net.createConnection({ port, host }, () => {
34
+ socket.destroy();
35
+ resolve();
36
+ });
37
+ socket.on('error', () => {
38
+ socket.destroy();
39
+ if (attempts >= maxAttempts) return reject(new Error(`Port ${port} not ready after ${maxAttempts} attempts`));
40
+ setTimeout(tryConnect, interval);
41
+ });
42
+ };
43
+ tryConnect();
44
+ });
45
+
26
46
  const logger = loggerFactory(import.meta);
27
47
 
28
48
  /**
@@ -87,6 +107,7 @@ const logger = loggerFactory(import.meta);
87
107
  * @property {boolean} logs - Whether to enable logs.
88
108
  * @property {boolean} dryRun - Whether to perform a dry run.
89
109
  * @property {boolean} createJobNow - Whether to create the job immediately.
110
+ * @property {number} fromNCommit - Number of commits back to use for message propagation (default: 1, last commit only).
90
111
  * @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
91
112
  * As a string (CLI): semicolon-separated entries of "ip=hostname1,hostname2" (e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote").
92
113
  * As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
@@ -151,7 +172,10 @@ const DEFAULT_OPTION = {
151
172
  logs: false,
152
173
  dryRun: false,
153
174
  createJobNow: false,
175
+ fromNCommit: 0,
154
176
  hostAliases: '',
177
+ gitClean: false,
178
+ copy: false,
155
179
  };
156
180
 
157
181
  /**
@@ -244,8 +268,15 @@ class UnderpostRun {
244
268
  const ports = '6379,27017';
245
269
  shellExec(`node bin run kill '${ports}'`);
246
270
  shellExec(`node bin run dev-cluster --dev --expose --namespace ${options.namespace}`, { async: true });
247
- console.log('Loading fordward services...');
248
- await timer(5000);
271
+ logger.info('Waiting for port-forward services to be ready...');
272
+ try {
273
+ await Promise.all([waitForPort(27017), waitForPort(6379)]);
274
+ logger.info('Port-forward services are ready');
275
+ } catch (err) {
276
+ logger.error('Port-forward services failed to become ready', { error: err.message });
277
+ shellExec(`node bin run kill '${ports}'`);
278
+ throw err;
279
+ }
249
280
  shellExec(`node bin metadata --generate ${path}`);
250
281
  shellExec(`node bin db --dev --clean-fs-collection dd`);
251
282
  shellExec(`node bin run kill '${ports}'`);
@@ -354,8 +385,10 @@ class UnderpostRun {
354
385
  },
355
386
  /**
356
387
  * @method template-deploy
357
- * @description Pushes `engine-private`, dispatches CI workflow to build `pwa-microservices-template`, and optionally dispatches CD sync workflow.
358
- * @param {string} path - The input value, identifier, or path for the operation.
388
+ * @description Pushes `engine-private`, dispatches CI workflow to build `pwa-microservices-template`,
389
+ * and optionally triggers engine-<conf-id> CI with sync/init which in turn dispatches the CD workflow
390
+ * after the build chain completes (template → ghpkg → engine-<conf-id> → CD).
391
+ * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
359
392
  * @param {Object} options - The default underpost runner options for customizing workflow
360
393
  * @memberof UnderpostRun
361
394
  */
@@ -368,7 +401,18 @@ class UnderpostRun {
368
401
  return;
369
402
  }
370
403
  shellExec(`${baseCommand} run pull`);
371
- const message = shellExec(`node bin cmt --changelog --changelog-no-hash`, { silent: true, stdout: true }).trim();
404
+
405
+ // Capture last N commit messages for propagation.
406
+ // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
407
+ const fromN =
408
+ options.fromNCommit && parseInt(options.fromNCommit) > 0
409
+ ? parseInt(options.fromNCommit)
410
+ : Underpost.repo.getUnpushedCount('.').count;
411
+ const message = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
412
+ silent: true,
413
+ stdout: true,
414
+ }).trim();
415
+
372
416
  shellExec(
373
417
  `${baseCommand} push ./engine-private ${options.force ? '-f ' : ''}${
374
418
  process.env.GITHUB_USERNAME
@@ -376,54 +420,76 @@ class UnderpostRun {
376
420
  );
377
421
  shellCd('/home/dd/engine');
378
422
 
379
- // Store deploy boundary hash for changelog before dispatch
380
- const deployBoundaryHash = shellExec('git rev-parse HEAD', {
381
- stdout: true,
382
- silent: true,
383
- disableLog: true,
384
- }).trim();
385
-
386
- function replaceNthNewline(str, n, replacement = ' ') {
387
- let count = 0;
388
- return str.replace(/\r\n?|\n/g, (match) => {
389
- count++;
390
- return count === n ? replacement : match;
391
- });
392
- }
393
- const sanitizedMessage = message
394
- ? replaceNthNewline(message.replaceAll('"', '').replaceAll('`', '').replaceAll('#', '').replaceAll('- ', ''), 2)
395
- .replace(/\r\n?|\n/g, ' ')
396
- .trim()
397
- : '';
423
+ const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(message);
398
424
 
399
425
  // Push engine repo so workflow YAML changes reach GitHub
400
426
  shellExec(`git reset`);
401
427
  shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
402
428
 
403
- // Dispatch CI workflow instead of empty commit + push
429
+ // Determine deploy conf and type from path (sync-engine-core, init-engine-core, etc.)
430
+ let deployConfId = '';
431
+ let deployType = '';
432
+ if (path.startsWith('sync-')) {
433
+ deployConfId = path.replace(/^sync-/, '');
434
+ deployType = 'sync-and-deploy';
435
+ } else if (path.startsWith('init-')) {
436
+ deployConfId = path.replace(/^init-/, '');
437
+ deployType = 'init';
438
+ }
439
+
440
+ // Dispatch npmpkg CI workflow — this builds pwa-microservices-template first.
441
+ // If deployConfId is set, npmpkg.ci.yml will dispatch the engine-<conf-id> CI
442
+ // with sync=true after template build completes. The engine CI then dispatches
443
+ // the CD workflow after the engine repo build finishes — ensuring correct sequence:
444
+ // npmpkg.ci → engine-<id>.ci → engine-<id>.cd
404
445
  const repo = `${process.env.GITHUB_USERNAME}/engine`;
446
+ const inputs = {};
447
+ if (sanitizedMessage) inputs.message = sanitizedMessage;
448
+ if (deployConfId) inputs.deploy_conf_id = deployConfId;
449
+ if (deployType) inputs.deploy_type = deployType;
450
+
405
451
  Underpost.repo.dispatchWorkflow({
406
452
  repo,
407
453
  workflowFile: 'npmpkg.ci.yml',
408
454
  ref: 'master',
409
- inputs: sanitizedMessage ? { message: sanitizedMessage } : {},
455
+ inputs,
410
456
  });
457
+ },
411
458
 
412
- // Dispatch CD sync-and-deploy if path starts with 'sync'
413
- if (path.startsWith('sync')) {
414
- const confId = path.replace(/^sync-/, '');
415
- Underpost.repo.dispatchWorkflow({
416
- repo,
417
- workflowFile: `${confId}.cd.yml`,
418
- ref: 'master',
419
- inputs: { job: 'sync-and-deploy' },
420
- });
459
+ /**
460
+ * @method template-deploy-local
461
+ * @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.
462
+ * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
463
+ * @param {Object} options - The default underpost runner options for customizing workflow
464
+ * @memberof UnderpostRun
465
+ */
466
+ 'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
467
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
468
+ shellExec(`npm run security:secrets`);
469
+ const reportPath = './gitleaks-report.json';
470
+ if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
471
+ logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
472
+ return;
421
473
  }
474
+ shellExec(`${baseCommand} run pull`);
422
475
 
423
- // Store deploy boundary for changelog
424
- shellExec(`${baseCommand} config set LAST_CI_DEPLOY_HASH ${deployBoundaryHash}`);
425
- },
476
+ // Capture last N commit messages from the engine repo.
477
+ // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
478
+ const fromN =
479
+ options.fromNCommit && parseInt(options.fromNCommit) > 0
480
+ ? parseInt(options.fromNCommit)
481
+ : Underpost.repo.getUnpushedCount('.').count;
482
+ const rawMessage = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
483
+ silent: true,
484
+ stdout: true,
485
+ }).trim();
486
+ const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
426
487
 
488
+ const { triggerCmd } = path
489
+ ? await Underpost.release.ci(path, sanitizedMessage, options)
490
+ : await Underpost.release.pwa(sanitizedMessage, options);
491
+ pbcopy(triggerCmd + ' && cd /home/dd/engine');
492
+ },
427
493
  /**
428
494
  * @method template-deploy-image
429
495
  * @description Dispatches the Docker image CI workflow for the `engine` repository.
@@ -439,6 +505,21 @@ class UnderpostRun {
439
505
  inputs: {},
440
506
  });
441
507
  },
508
+ /**
509
+ * @method docker-image
510
+ * @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
511
+ * @param {string} path - The input value, identifier, or path for the operation.
512
+ * @param {Object} options - The default underpost runner options for customizing workflow
513
+ * @memberof UnderpostRun
514
+ */
515
+ 'docker-image': (path, options = DEFAULT_OPTION) => {
516
+ Underpost.repo.dispatchWorkflow({
517
+ repo: `${process.env.GITHUB_USERNAME}/engine`,
518
+ workflowFile: 'docker-image.ci.yml',
519
+ ref: 'master',
520
+ inputs: {},
521
+ });
522
+ },
442
523
  /**
443
524
  * @method clean
444
525
  * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
@@ -606,20 +687,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
606
687
  const cmdString = options.cmd
607
688
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
608
689
  : '';
690
+ const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
691
+ const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
609
692
 
610
693
  shellExec(
611
- `${baseCommand} deploy --kubeadm --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
694
+ `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
612
695
  image ? ` --image ${image}` : ''
613
696
  }${versions ? ` --versions ${versions}` : ''}${
614
697
  options.namespace ? ` --namespace ${options.namespace}` : ''
615
- }${timeoutFlags}${cmdString} ${deployId} ${env}`,
698
+ }${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
616
699
  );
617
700
 
618
701
  if (isDeployRunnerContext(path, options)) {
619
702
  shellExec(
620
- `${baseCommand} deploy --kubeadm${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
703
+ `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
621
704
  options.namespace ? ` --namespace ${options.namespace}` : ''
622
- }${timeoutFlags}`,
705
+ }${timeoutFlags}${gitCleanFlag}`,
623
706
  );
624
707
  if (!targetTraffic)
625
708
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -831,6 +914,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
831
914
  const confInstances = JSON.parse(
832
915
  fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
833
916
  );
917
+ let promotedTraffic = '';
834
918
  for (const instance of confInstances) {
835
919
  let {
836
920
  id: _id,
@@ -850,6 +934,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
850
934
  namespace: options.namespace,
851
935
  });
852
936
  const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
937
+ promotedTraffic = targetTraffic;
853
938
  let proxyYaml =
854
939
  Underpost.deploy.baseProxyYamlFactory({ host: _host, env: options.tls ? 'production' : env, options }) +
855
940
  Underpost.deploy.deploymentYamlServiceFactory({
@@ -875,6 +960,18 @@ EOF
875
960
  { disableLog: true },
876
961
  );
877
962
  }
963
+ // Refresh the gRPC service to ensure it points to the parent deploy's current traffic.
964
+ if (promotedTraffic) {
965
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
966
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
967
+ deployId,
968
+ env,
969
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
970
+ namespace: options.namespace,
971
+ traffic: [parentTraffic],
972
+ });
973
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
974
+ }
878
975
  },
879
976
 
880
977
  /**
@@ -914,12 +1011,12 @@ EOF
914
1011
  // `localhost/rockylinux9-underpost:${Underpost.version}`
915
1012
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
916
1013
 
917
- if (options.nodeName) {
918
- shellExec(`sudo crictl pull ${_image}`);
919
- } else {
920
- shellExec(`docker pull ${_image}`);
921
- shellExec(`sudo kind load docker-image ${_image}`);
922
- }
1014
+ Underpost.image.pullDockerHubImage({
1015
+ dockerhubImage: _image,
1016
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
1017
+ kubeadm: options.nodeName || options.kubeadm,
1018
+ k3s: options.k3s,
1019
+ });
923
1020
 
924
1021
  const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
925
1022
  hostTest: _host,
@@ -928,7 +1025,7 @@ EOF
928
1025
 
929
1026
  const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
930
1027
  const podId = `${_deployId}-${env}-${targetTraffic}`;
931
- const ignorePods = Underpost.deploy.get(podId, 'pods', options.namespace).map((p) => p.NAME);
1028
+ const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
932
1029
  Underpost.deploy.configMap(env, options.namespace);
933
1030
  shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
934
1031
  shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
@@ -940,7 +1037,30 @@ EOF
940
1037
  env,
941
1038
  version: targetTraffic,
942
1039
  nodeName: options.nodeName,
1040
+ clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
1041
+ gitClean: options.gitClean || false,
943
1042
  });
1043
+ // Regenerate the parent deploy's gRPC ClusterIP service pointing to the
1044
+ // parent's current traffic colour and apply it before the instance pod starts so
1045
+ // DNS is resolvable the moment the pod boots.
1046
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1047
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
1048
+ deployId,
1049
+ env,
1050
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
1051
+ namespace: options.namespace,
1052
+ traffic: [targetTraffic],
1053
+ host: _host,
1054
+ });
1055
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
1056
+
1057
+ const resolvedCmd = _cmd[env].map((c) =>
1058
+ c.replaceAll(
1059
+ '{{grpc-service-dns}}',
1060
+ `${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
1061
+ ),
1062
+ );
1063
+
944
1064
  let deploymentYaml = `---
945
1065
  ${Underpost.deploy
946
1066
  .deploymentYamlPartsFactory({
@@ -952,7 +1072,7 @@ ${Underpost.deploy
952
1072
  image: _image,
953
1073
  namespace: options.namespace,
954
1074
  volumes: _volumes,
955
- cmd: _cmd[env],
1075
+ cmd: resolvedCmd,
956
1076
  })
957
1077
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
958
1078
  `;
@@ -998,7 +1118,7 @@ EOF
998
1118
  * @memberof UnderpostRun
999
1119
  */
1000
1120
  'ls-deployments': async (path, options = DEFAULT_OPTION) => {
1001
- console.table(await Underpost.deploy.get(path, 'deployments', options.namespace));
1121
+ console.table(await Underpost.kubectl.get(path, 'deployments', options.namespace));
1002
1122
  },
1003
1123
 
1004
1124
  /**
@@ -1092,15 +1212,13 @@ EOF
1092
1212
  'db-client': async (path, options = DEFAULT_OPTION) => {
1093
1213
  const { underpostRoot } = options;
1094
1214
 
1095
- const image = 'adminer:4.7.6-standalone';
1096
-
1097
- if (!options.kubeadm && !options.k3s) {
1098
- // Only load if not kubeadm/k3s (Kind needs it)
1099
- shellExec(`docker pull ${image}`);
1100
- shellExec(`sudo kind load docker-image ${image}`);
1101
- } else if (options.kubeadm || options.k3s)
1102
- // For kubeadm/k3s, ensure it's available for containerd
1103
- shellExec(`sudo crictl pull ${image}`);
1215
+ Underpost.image.pullDockerHubImage({
1216
+ dockerhubImage: 'adminer',
1217
+ version: '4.7.6-standalone',
1218
+ kind: options.kind,
1219
+ kubeadm: options.kubeadm,
1220
+ k3s: options.k3s,
1221
+ });
1104
1222
 
1105
1223
  shellExec(`kubectl delete deployment adminer -n ${options.namespace} --ignore-not-found`);
1106
1224
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/deployment/adminer/. -n ${options.namespace}`);
@@ -1573,13 +1691,73 @@ EOF
1573
1691
  },
1574
1692
 
1575
1693
  /**
1576
- * @method ptls
1694
+ * @method pid-info
1695
+ * @description Displays detailed information about a process by PID, including service details, command line, executable path, working directory, environment variables, and parent process tree.
1696
+ * @param {string} path - The PID of the process to inspect.
1697
+ * @param {Object} options - The default underpost runner options for customizing workflow
1698
+ * @memberof UnderpostRun
1699
+ */
1700
+ 'pid-info': (path, options = DEFAULT_OPTION) => {
1701
+ const pid = path;
1702
+ if (!pid) {
1703
+ logger.error('PID is required. Usage: underpost run pid-info <pid>');
1704
+ return;
1705
+ }
1706
+
1707
+ // Services
1708
+ logger.info('Process info');
1709
+ shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`);
1710
+ logger.info('Command line');
1711
+ shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`);
1712
+ logger.info('Executable path');
1713
+ shellExec(`sudo readlink -f /proc/${pid}/exe`);
1714
+ logger.info('Working directory');
1715
+ shellExec(`sudo readlink -f /proc/${pid}/cwd`);
1716
+ logger.info('Environment variables (first 200)');
1717
+ shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`);
1718
+
1719
+ // Parent
1720
+ logger.info('Parent process');
1721
+ const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, { stdout: true, silent: true });
1722
+ console.log(parentInfo);
1723
+ const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
1724
+ if (ppidMatch) {
1725
+ const ppid = ppidMatch.trim().split(/\s+/)[1];
1726
+ logger.info(`Parent PID: ${ppid}`);
1727
+ shellExec(`ps -fp ${ppid}`);
1728
+ }
1729
+ logger.info('Process tree');
1730
+ shellExec(`pstree -s ${pid}`);
1731
+ },
1732
+
1733
+ /**
1734
+ * @method background
1735
+ * @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`.
1736
+ * @param {string} path - The command to run in the background (e.g. 'npm run prod:container dd-cyberia-r3').
1737
+ * @param {Object} options - The default underpost runner options for customizing workflow
1738
+ * @memberof UnderpostRun
1739
+ */
1740
+ background: (path, options = DEFAULT_OPTION) => {
1741
+ if (!path) {
1742
+ logger.error('Command is required. Usage: underpost run background <command>');
1743
+ return;
1744
+ }
1745
+ const id = path.split(/\s+/).pop();
1746
+ const logFile = `/var/log/${id}.log`;
1747
+ const pidFile = `/var/run/${id}.pid`;
1748
+ logger.info(`Starting background process`, { id, logFile, pidFile });
1749
+ shellExec(`nohup ${path} > ${logFile} 2>&1 & pid=$!; echo $pid > ${pidFile}; disown`);
1750
+ logger.info(`Background process started for '${id}'`);
1751
+ },
1752
+
1753
+ /**
1754
+ * @method ports
1577
1755
  * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
1578
1756
  * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
1579
1757
  * @param {Object} options - The default underpost runner options for customizing workflow
1580
1758
  * @memberof UnderpostRun
1581
1759
  */
1582
- ptls: async (path = '', options = DEFAULT_OPTION) => {
1760
+ ports: async (path = '', options = DEFAULT_OPTION) => {
1583
1761
  shellExec(`chmod +x ${options.underpostRoot}/scripts/ports-ls.sh`);
1584
1762
  shellExec(`${options.underpostRoot}/scripts/ports-ls.sh`);
1585
1763
  },
@@ -1634,7 +1812,7 @@ EOF
1634
1812
 
1635
1813
  const { close } = await (async () => {
1636
1814
  const checkAwaitPath = '/await';
1637
- while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1815
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
1638
1816
  logger.info('monitor', checkAwaitPath);
1639
1817
  await timer(1000);
1640
1818
  }
@@ -1661,7 +1839,7 @@ EOF
1661
1839
  logger.info('monitor', checkPath);
1662
1840
  {
1663
1841
  const checkAwaitPath = `/home/dd/docs${checkPath}`;
1664
- while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1842
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
1665
1843
  logger.info('waiting for', checkAwaitPath);
1666
1844
  await timer(1000);
1667
1845
  }
@@ -1727,7 +1905,8 @@ EOF
1727
1905
 
1728
1906
  shellCd(dir);
1729
1907
 
1730
- shellExec(`git init && git add . && git commit -m "Base implementation"`);
1908
+ Underpost.repo.initLocalRepo({ path: dir });
1909
+ shellExec(`git add . && git commit -m "Base implementation"`);
1731
1910
  shellExec(`chmod +x ./replace_params.sh`);
1732
1911
  shellExec(`chmod +x ./build.sh`);
1733
1912
 
@@ -1772,11 +1951,46 @@ EOF
1772
1951
  * @param {Object} options - The default underpost runner options for customizing workflow
1773
1952
  * @memberof UnderpostRun
1774
1953
  */
1954
+ /**
1955
+ * @method generate-pass
1956
+ * @description Generates a cryptographically secure random password that satisfies all validatePassword
1957
+ * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
1958
+ * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
1959
+ * @param {string} path - Optional password length (default: 16).
1960
+ * @param {Object} options - The default underpost runner options for customizing workflow.
1961
+ * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
1962
+ * @memberof UnderpostRun
1963
+ */
1964
+ 'generate-pass': (path, options = DEFAULT_OPTION) => {
1965
+ const length = path && parseInt(path) > 0 ? parseInt(path) : 16;
1966
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
1967
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
1968
+ const digits = '0123456789';
1969
+ const special = '@#$%^&*()_+';
1970
+ const all = lower + upper + digits + special;
1971
+ const buf = crypto.randomBytes(length + 4);
1972
+ // Guarantee at least one character from each required class
1973
+ const chars = [
1974
+ lower[buf[0] % lower.length],
1975
+ upper[buf[1] % upper.length],
1976
+ digits[buf[2] % digits.length],
1977
+ special[buf[3] % special.length],
1978
+ ];
1979
+ for (let i = 4; i < length; i++) chars.push(all[buf[i] % all.length]);
1980
+ // Fisher-Yates shuffle using an independent random buffer
1981
+ const shuf = crypto.randomBytes(length);
1982
+ for (let i = chars.length - 1; i > 0; i--) {
1983
+ const j = shuf[i % shuf.length] % (i + 1);
1984
+ [chars[i], chars[j]] = [chars[j], chars[i]];
1985
+ }
1986
+ const password = chars.join('');
1987
+ if (options.copy) pbcopy(password);
1988
+ else console.log(password);
1989
+ },
1990
+
1775
1991
  secret: (path, options = DEFAULT_OPTION) => {
1776
1992
  const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
1777
- const command = options.dev
1778
- ? `node bin secret underpost --create-from-file ${secretPath}`
1779
- : `underpost secret underpost --create-from-file ${secretPath}`;
1993
+ const command = `node bin secret underpost --create-from-file ${secretPath}`;
1780
1994
  shellExec(command);
1781
1995
  },
1782
1996
  /**
package/src/cli/test.js CHANGED
@@ -81,7 +81,7 @@ class UnderpostTest {
81
81
  return await Underpost.test.statusMonitor(options.podName, options.podStatus, options.kindType);
82
82
 
83
83
  if (options.sh === true || options.logs === true) {
84
- const [pod] = Underpost.deploy.get(deployList);
84
+ const [pod] = Underpost.kubectl.get(deployList);
85
85
  if (pod) {
86
86
  if (options.sh) return pbcopy(`sudo kubectl exec -it ${pod.NAME} -- sh`);
87
87
  if (options.logs) return shellExec(`sudo kubectl logs -f ${pod.NAME}`);
@@ -115,7 +115,7 @@ class UnderpostTest {
115
115
  break;
116
116
  }
117
117
  else {
118
- const pods = Underpost.deploy.get(deployId);
118
+ const pods = Underpost.kubectl.get(deployId);
119
119
  if (pods.length > 0)
120
120
  for (const deployData of pods) {
121
121
  const { NAME } = deployData;
@@ -145,7 +145,7 @@ class UnderpostTest {
145
145
  logger.info(`Loading instance`, { podName, status, kindType, deltaMs, maxAttempts });
146
146
  const _monitor = async () => {
147
147
  await timer(deltaMs);
148
- const pods = Underpost.deploy.get(podName, kindType);
148
+ const pods = Underpost.kubectl.get(podName, kindType);
149
149
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
150
150
  logger.info(
151
151
  `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
@@ -11,10 +11,9 @@ import { RouterDefault } from './components/default/RoutesDefault.js';
11
11
  import { TranslateDefault } from './components/default/TranslateDefault.js';
12
12
  import { Worker } from './components/core/Worker.js';
13
13
  import { Keyboard } from './components/core/Keyboard.js';
14
- import { DefaultParams } from './components/default/CommonDefault.js';
15
14
  import { SocketIo } from './components/core/SocketIo.js';
16
15
  import { SocketIoDefault } from './components/default/SocketIoDefault.js';
17
- import { ElementsDefault } from './components/default/ElementsDefault.js';
16
+ import { AppStoreDefault } from './components/default/AppStoreDefault.js';
18
17
  import { CssDefaultDark, CssDefaultLight } from './components/default/CssDefault.js';
19
18
  import { EventsUI } from './components/core/EventsUI.js';
20
19
  import { Modal } from './components/core/Modal.js';
@@ -71,14 +70,14 @@ window.onload = () =>
71
70
  await Responsive.Init();
72
71
  await MenuDefault.Render({ htmlMainBody });
73
72
  await SocketIo.Init({
74
- channels: ElementsDefault.Data,
73
+ channels: AppStoreDefault.Data,
75
74
  path: `/`,
76
75
  });
77
76
  await SocketIoDefault.Init();
78
77
  await LogInDefault();
79
78
  await LogOutDefault();
80
79
  await SignUpDefault();
81
- await Keyboard.Init({ callBackTime: DefaultParams.EVENT_CALLBACK_TIME });
80
+ await Keyboard.Init();
82
81
  await Modal.RenderSeoSanitizer();
83
82
  },
84
83
  });
@@ -49,7 +49,7 @@ const e404 = async () => {
49
49
  <br />
50
50
  <br />${Translate.Render('page-not-found')} <br />
51
51
  <br />
52
- <a href="${location.origin}">${Translate.Render('back')}</a>
52
+ <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
53
53
  </div>`;
54
54
  };
55
55
 
@@ -68,7 +68,7 @@ const e500 = async () => {
68
68
  <br />
69
69
  <br />${Translate.Render('page-broken')} <br />
70
70
  <br />
71
- <a href="${location.origin}">${Translate.Render('back')}</a>
71
+ <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
72
72
  </div>`;
73
73
  };
74
74