underpost 3.0.2 → 3.1.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 (85) hide show
  1. package/{.env.production → .env.example} +20 -2
  2. package/.github/workflows/ghpkg.ci.yml +1 -1
  3. package/.github/workflows/gitlab.ci.yml +1 -1
  4. package/.github/workflows/npmpkg.ci.yml +22 -7
  5. package/.github/workflows/publish.ci.yml +5 -5
  6. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -3
  7. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  8. package/.github/workflows/release.cd.yml +3 -2
  9. package/.vscode/extensions.json +9 -8
  10. package/.vscode/settings.json +3 -2
  11. package/CHANGELOG.md +468 -290
  12. package/CLI-HELP.md +72 -52
  13. package/README.md +2 -2
  14. package/bin/build.js +4 -2
  15. package/bin/deploy.js +150 -208
  16. package/bin/file.js +2 -1
  17. package/bin/vs.js +3 -3
  18. package/conf.js +30 -13
  19. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  20. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  21. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  22. package/manifests/deployment/dd-test-development/deployment.yaml +52 -52
  23. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  24. package/manifests/pv-pvc-dd.yaml +1 -1
  25. package/package.json +53 -44
  26. package/scripts/k3s-node-setup.sh +1 -1
  27. package/src/api/document/document.service.js +1 -1
  28. package/src/api/file/file.controller.js +3 -1
  29. package/src/api/file/file.service.js +28 -5
  30. package/src/api/user/user.router.js +10 -5
  31. package/src/api/user/user.service.js +7 -7
  32. package/src/cli/baremetal.js +6 -10
  33. package/src/cli/cloud-init.js +0 -3
  34. package/src/cli/db.js +54 -71
  35. package/src/cli/deploy.js +64 -12
  36. package/src/cli/env.js +4 -4
  37. package/src/cli/fs.js +0 -2
  38. package/src/cli/image.js +0 -3
  39. package/src/cli/index.js +33 -13
  40. package/src/cli/monitor.js +5 -6
  41. package/src/cli/repository.js +322 -35
  42. package/src/cli/run.js +148 -71
  43. package/src/cli/secrets.js +0 -3
  44. package/src/cli/ssh.js +1 -1
  45. package/src/client/components/core/AgGrid.js +20 -5
  46. package/src/client/components/core/Content.js +22 -3
  47. package/src/client/components/core/Docs.js +21 -4
  48. package/src/client/components/core/FileExplorer.js +71 -4
  49. package/src/client/components/core/Input.js +1 -1
  50. package/src/client/components/core/Modal.js +22 -6
  51. package/src/client/components/core/PublicProfile.js +3 -3
  52. package/src/client/components/core/Router.js +34 -1
  53. package/src/client/components/core/Worker.js +1 -1
  54. package/src/client/public/default/sitemap +3 -3
  55. package/src/client/public/test/sitemap +3 -3
  56. package/src/client.build.js +0 -3
  57. package/src/client.dev.js +0 -3
  58. package/src/db/DataBaseProvider.js +17 -2
  59. package/src/db/mariadb/MariaDB.js +14 -9
  60. package/src/db/mongo/MongooseDB.js +17 -1
  61. package/src/index.js +1 -1
  62. package/src/proxy.js +0 -3
  63. package/src/runtime/express/Express.js +7 -1
  64. package/src/runtime/lampp/Lampp.js +6 -13
  65. package/src/server/auth.js +6 -9
  66. package/src/server/backup.js +2 -3
  67. package/src/server/client-build-docs.js +178 -3
  68. package/src/server/client-build-live.js +9 -18
  69. package/src/server/client-build.js +175 -38
  70. package/src/server/client-dev-server.js +14 -13
  71. package/src/server/conf.js +357 -149
  72. package/src/server/cron.js +2 -1
  73. package/src/server/dns.js +28 -12
  74. package/src/server/downloader.js +0 -2
  75. package/src/server/logger.js +27 -9
  76. package/src/server/peer.js +0 -2
  77. package/src/server/process.js +1 -50
  78. package/src/server/proxy.js +4 -8
  79. package/src/server/runtime.js +5 -8
  80. package/src/server/ssr.js +0 -3
  81. package/src/server/start.js +5 -5
  82. package/src/server/tls.js +0 -2
  83. package/src/server.js +0 -4
  84. package/.env.development +0 -43
  85. package/.env.test +0 -43
package/src/cli/run.js CHANGED
@@ -4,13 +4,14 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, getTerminalPid, openTerminal, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, getTerminalPid, shellCd, shellExec } from '../server/process.js';
8
8
  import {
9
9
  awaitDeployMonitor,
10
10
  buildKindPorts,
11
11
  Config,
12
12
  getNpmRootPath,
13
13
  isDeployRunnerContext,
14
+ loadConfServerJson,
14
15
  writeEnv,
15
16
  } from '../server/conf.js';
16
17
  import { actionInitLog, loggerFactory } from '../server/logger.js';
@@ -32,7 +33,6 @@ const logger = loggerFactory(import.meta);
32
33
  * @property {string} podName - The name of the pod to run.
33
34
  * @property {string} nodeName - The name of the node to run.
34
35
  * @property {number} port - Custom port to use.
35
- * @property {boolean} etcHosts - Whether to modify /etc/hosts.
36
36
  * @property {string} volumeHostPath - The host path for the volume.
37
37
  * @property {string} volumeMountPath - The mount path for the volume.
38
38
  * @property {string} imageName - The name of the image to run.
@@ -56,7 +56,6 @@ const logger = loggerFactory(import.meta);
56
56
  * @property {string} apiVersion - The API version for the container.
57
57
  * @property {string} claimName - The claim name for the volume.
58
58
  * @property {string} kindType - The kind of resource to create.
59
- * @property {boolean} terminal - Whether to open a terminal.
60
59
  * @property {number} devProxyPortOffset - The port offset for the development proxy.
61
60
  * @property {boolean} hostNetwork - Whether to use host networking.
62
61
  * @property {string} requestsMemory - The memory request for the container.
@@ -85,9 +84,12 @@ const logger = loggerFactory(import.meta);
85
84
  * @property {string} monitorStatusKindType - The monitor status kind type option.
86
85
  * @property {string} monitorStatusDeltaMs - The monitor status delta in milliseconds.
87
86
  * @property {string} monitorStatusMaxAttempts - The maximum number of attempts for monitor status.
87
+ * @property {boolean} logs - Whether to enable logs.
88
88
  * @property {boolean} dryRun - Whether to perform a dry run.
89
89
  * @property {boolean} createJobNow - Whether to create the job immediately.
90
- * @property {boolean} logs - Whether to enable logs.
90
+ * @property {string|Array<{ip: string, hostnames: string[]}>} hostAliases - Adds entries to the Pod /etc/hosts via Kubernetes hostAliases.
91
+ * 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
+ * As an array (programmatic): objects with `ip` and `hostnames` fields (e.g., [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]).
91
93
  * @memberof UnderpostRun
92
94
  */
93
95
  const DEFAULT_OPTION = {
@@ -118,7 +120,6 @@ const DEFAULT_OPTION = {
118
120
  apiVersion: '',
119
121
  claimName: '',
120
122
  kindType: '',
121
- terminal: false,
122
123
  devProxyPortOffset: 0,
123
124
  hostNetwork: false,
124
125
  requestsMemory: '',
@@ -150,6 +151,7 @@ const DEFAULT_OPTION = {
150
151
  logs: false,
151
152
  dryRun: false,
152
153
  createJobNow: false,
154
+ hostAliases: '',
153
155
  };
154
156
 
155
157
  /**
@@ -192,7 +194,7 @@ class UnderpostRun {
192
194
  }
193
195
 
194
196
  {
195
- // Detect MongoDB primary pod using centralized method
197
+ // Detect MongoDB primary pod using method
196
198
  let primaryMongoHost = 'mongodb-0.mongodb-service';
197
199
  try {
198
200
  const primaryPodName = Underpost.db.getMongoPrimaryPodName({
@@ -352,22 +354,35 @@ class UnderpostRun {
352
354
  },
353
355
  /**
354
356
  * @method template-deploy
355
- * @description Cleans up, pushes `engine-private` and `engine` repositories with a commit tag `ci package-pwa-microservices-template`.
357
+ * @description Pushes `engine-private`, dispatches CI workflow to build `pwa-microservices-template`, and optionally dispatches CD sync workflow.
356
358
  * @param {string} path - The input value, identifier, or path for the operation.
357
359
  * @param {Object} options - The default underpost runner options for customizing workflow
358
360
  * @memberof UnderpostRun
359
361
  */
360
362
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
361
363
  const baseCommand = options.dev ? 'node bin' : 'underpost';
364
+ shellExec(`npm run security:secrets`);
365
+ const reportPath = './gitleaks-report.json';
366
+ if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
367
+ logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
368
+ return;
369
+ }
370
+ shellExec(`${baseCommand} run pull`);
362
371
  const message = shellExec(`node bin cmt --changelog --changelog-no-hash`, { silent: true, stdout: true }).trim();
363
- shellExec(`${baseCommand} run clean`);
364
372
  shellExec(
365
373
  `${baseCommand} push ./engine-private ${options.force ? '-f ' : ''}${
366
374
  process.env.GITHUB_USERNAME
367
375
  }/engine-private`,
368
376
  );
369
377
  shellCd('/home/dd/engine');
370
- shellExec(`git reset`);
378
+
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
+
371
386
  function replaceNthNewline(str, n, replacement = ' ') {
372
387
  let count = 0;
373
388
  return str.replace(/\r\n?|\n/g, (match) => {
@@ -375,35 +390,54 @@ class UnderpostRun {
375
390
  return count === n ? replacement : match;
376
391
  });
377
392
  }
378
- shellExec(
379
- `${baseCommand} cmt . --empty ci package-pwa-microservices-template${
380
- path.startsWith('sync') ? `-${path}` : ''
381
- }${
382
- message
383
- ? ` "${replaceNthNewline(
384
- message.replaceAll('"', '').replaceAll('`', '').replaceAll('#', '').replaceAll('- ', ''),
385
- 2,
386
- )}"`
387
- : ''
388
- }`,
389
- );
393
+ const sanitizedMessage = message
394
+ ? replaceNthNewline(message.replaceAll('"', '').replaceAll('`', '').replaceAll('#', '').replaceAll('- ', ''), 2)
395
+ .replace(/\r\n?|\n/g, ' ')
396
+ .trim()
397
+ : '';
398
+
399
+ // Push engine repo so workflow YAML changes reach GitHub
400
+ shellExec(`git reset`);
390
401
  shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
402
+
403
+ // Dispatch CI workflow instead of empty commit + push
404
+ const repo = `${process.env.GITHUB_USERNAME}/engine`;
405
+ Underpost.repo.dispatchWorkflow({
406
+ repo,
407
+ workflowFile: 'npmpkg.ci.yml',
408
+ ref: 'master',
409
+ inputs: sanitizedMessage ? { message: sanitizedMessage } : {},
410
+ });
411
+
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
+ });
421
+ }
422
+
423
+ // Store deploy boundary for changelog
424
+ shellExec(`${baseCommand} config set LAST_CI_DEPLOY_HASH ${deployBoundaryHash}`);
391
425
  },
392
426
 
393
427
  /**
394
428
  * @method template-deploy-image
395
- * @description Commits and pushes a Docker image deployment for the `engine` repository.
429
+ * @description Dispatches the Docker image CI workflow for the `engine` repository.
396
430
  * @param {string} path - The input value, identifier, or path for the operation.
397
431
  * @param {Object} options - The default underpost runner options for customizing workflow
398
432
  * @memberof UnderpostRun
399
433
  */
400
434
  'template-deploy-image': (path, options = DEFAULT_OPTION) => {
401
- // const baseCommand = options.dev ? 'node bin' : 'underpost';
402
- shellExec(
403
- `cd /home/dd/engine && git reset && underpost cmt . --empty ci docker-image 'underpost-engine:${
404
- Underpost.version
405
- }' && underpost push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`,
406
- );
435
+ Underpost.repo.dispatchWorkflow({
436
+ repo: `${process.env.GITHUB_USERNAME}/engine`,
437
+ workflowFile: 'docker-image.ci.yml',
438
+ ref: 'master',
439
+ inputs: {},
440
+ });
407
441
  },
408
442
  /**
409
443
  * @method clean
@@ -464,18 +498,30 @@ class UnderpostRun {
464
498
  },
465
499
  /**
466
500
  * @method ssh-deploy
467
- * @description Performs a Git reset, commits with a message `cd ssh-${path}`, and pushes the `engine` repository, likely triggering an SSH-based CD pipeline.
468
- * @param {string} path - The input value, identifier, or path for the operation (used as the deployment identifier for the commit message).
501
+ * @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
502
+ * @param {string} path - The deployment identifier (e.g., 'engine-core', 'sync-engine-core', 'init-engine-core').
469
503
  * @param {Object} options - The default underpost runner options for customizing workflow
470
504
  * @memberof UnderpostRun
471
505
  */
472
506
  'ssh-deploy': (path, options = DEFAULT_OPTION) => {
473
507
  actionInitLog();
474
- const baseCommand = options.dev ? 'node bin' : 'underpost';
475
- shellCd('/home/dd/engine');
476
- shellExec(`git reset`);
477
- shellExec(`${baseCommand} cmt . --empty cd ssh-${path}`);
478
- shellExec(`${baseCommand} push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`);
508
+
509
+ let job = 'deploy';
510
+ let confId = path;
511
+ if (path.startsWith('sync-')) {
512
+ job = 'sync-and-deploy';
513
+ confId = path.replace(/^sync-/, '');
514
+ } else if (path.startsWith('init-')) {
515
+ job = 'init';
516
+ confId = path.replace(/^init-/, '');
517
+ }
518
+
519
+ Underpost.repo.dispatchWorkflow({
520
+ repo: `${process.env.GITHUB_USERNAME}/engine`,
521
+ workflowFile: `${confId}.cd.yml`,
522
+ ref: 'master',
523
+ inputs: { job },
524
+ });
479
525
  },
480
526
  /**
481
527
  * @method ide
@@ -485,15 +531,25 @@ class UnderpostRun {
485
531
  * @param {Object} options - The default underpost runner options for customizing workflow
486
532
  * @memberof UnderpostRun
487
533
  */
488
- ide: (path, options = DEFAULT_OPTION) => {
489
- const { underpostRoot } = options;
490
- if (path === 'install') {
491
- shellExec(`sudo curl -f https://zed.dev/install.sh | sh`);
492
- shellExec(
493
- `sudo dnf config-manager --add-repo https://download.sublimetext.com/rpm/stable/x86_64/sublime-text.repo`,
494
- );
495
- shellExec(`sudo dnf install -y sublime-text`);
496
- } else shellExec(`node ${underpostRoot}/bin/zed ${path}`);
534
+ ide: (path = '', options = DEFAULT_OPTION) => {
535
+ const underpostRoot = options.dev ? '.' : options.underpostRoot;
536
+ const [projectPath, customIde] = path.split(',');
537
+ if (projectPath === 'install') {
538
+ if (customIde === 'zed') shellExec(`sudo curl -f https://zed.dev/install.sh | sh`);
539
+ else if (customIde === 'subl') {
540
+ shellExec(
541
+ `sudo dnf config-manager --add-repo https://download.sublimetext.com/rpm/stable/x86_64/sublime-text.repo`,
542
+ );
543
+ shellExec(`sudo dnf install -y sublime-text`);
544
+ } else {
545
+ shellExec(`sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc &&
546
+ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\nautorefresh=1\ntype=rpm-md\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" | sudo tee /etc/yum.repos.d/vscode.repo > /dev/null`);
547
+ shellExec(`sudo dnf install -y code`);
548
+ }
549
+ return;
550
+ }
551
+ if (customIde === 'zed') shellExec(`node ${underpostRoot}/bin/zed ${projectPath}`);
552
+ else shellExec(`node ${underpostRoot}/bin/vs ${projectPath}`);
497
553
  },
498
554
  /**
499
555
  * @method crypto-policy
@@ -522,7 +578,7 @@ class UnderpostRun {
522
578
  options.replicas,
523
579
  ``,
524
580
  ``,
525
- options.dev || !isDeployRunnerContext(path, options) ? 'kind-control-plane' : os.hostname(),
581
+ !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
526
582
  ];
527
583
  let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
528
584
  deployId = deployId ? deployId : defaultPath[0];
@@ -1005,9 +1061,7 @@ EOF
1005
1061
  volumeMountPath: volumeHostPath,
1006
1062
  ...(options.dev ? { volumeHostPath } : { claimName }),
1007
1063
  on: {
1008
- init: async () => {
1009
- // openTerminal(`kubectl logs -f ${podName}`);
1010
- },
1064
+ init: async () => {},
1011
1065
  },
1012
1066
  args: [daemonProcess(path ? path : `cd /home/dd/engine && npm install && npm run test`)],
1013
1067
  };
@@ -1142,7 +1196,7 @@ EOF
1142
1196
  const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
1143
1197
  let hosts = [];
1144
1198
  for (const deployId of deployList) {
1145
- const confServer = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'));
1199
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
1146
1200
  hosts = hosts.concat(Object.keys(confServer));
1147
1201
  }
1148
1202
  shellExec(`node bin cluster --prom ${hosts.join(',')}`);
@@ -1315,34 +1369,31 @@ EOF
1315
1369
  if (!subConf) subConf = 'local';
1316
1370
  if (options.reset && fs.existsSync(`./engine-private/conf/${deployId}`))
1317
1371
  fs.removeSync(`./engine-private/conf/${deployId}`);
1318
- if (!fs.existsSync(`./engine-private/conf/${deployId}`)) Config.deployIdFactory(deployId, { subConf });
1319
1372
  if (options.devProxyPortOffset) {
1320
1373
  const envPath = `./engine-private/conf/${deployId}/.env.development`;
1321
1374
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
1322
1375
  envObj.DEV_PROXY_PORT_OFFSET = options.devProxyPortOffset;
1323
1376
  writeEnv(envPath, envObj);
1324
1377
  }
1378
+ dotenv.config({ path: `./engine-private/conf/${deployId}/.env.development`, override: true });
1325
1379
  shellExec(`node bin run dev-cluster --expose --namespace ${options.namespace}`, { async: true });
1326
1380
  {
1327
- const cmd = `npm run dev-api ${deployId} ${subConf} ${host} ${_path} ${clientHostPort}${
1381
+ const cmd = `npm run dev:api ${deployId} ${subConf} ${host} ${_path} ${clientHostPort} proxy${
1328
1382
  options.tls ? ' tls' : ''
1329
1383
  }`;
1330
- options.terminal ? openTerminal(cmd) : shellExec(cmd, { async: true });
1384
+ shellExec(cmd, { async: true });
1331
1385
  }
1332
1386
  await awaitDeployMonitor(true);
1333
1387
  {
1334
- const cmd = `npm run dev-client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1335
- options.terminal
1336
- ? openTerminal(cmd)
1337
- : shellExec(cmd, {
1338
- async: true,
1339
- });
1388
+ const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1389
+
1390
+ shellExec(cmd, {
1391
+ async: true,
1392
+ });
1340
1393
  }
1341
1394
  await awaitDeployMonitor(true);
1342
1395
  shellExec(
1343
- `./node_modules/.bin/env-cmd -f .env.development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${
1344
- options.tls ? ' tls' : ''
1345
- }`,
1396
+ `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1346
1397
  );
1347
1398
  },
1348
1399
 
@@ -1361,7 +1412,7 @@ EOF
1361
1412
  let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
1362
1413
  if (!replicas) replicas = options.replicas;
1363
1414
  // const confClient = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.client.json`, 'utf8'));
1364
- const confServer = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'));
1415
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
1365
1416
  // const confSSR = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.ssr.json`, 'utf8'));
1366
1417
  // const packageData = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/package.json`, 'utf8'));
1367
1418
  const services = fs.existsSync(`./engine-private/deploy/${deployId}/conf.services.json`)
@@ -1452,9 +1503,7 @@ EOF
1452
1503
  'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
1453
1504
  const hosts = path ? path.split(',') : [];
1454
1505
  if (options.deployId) {
1455
- const confServer = JSON.parse(
1456
- fs.readFileSync(`./engine-private/conf/${options.deployId}/conf.server.json`, 'utf8'),
1457
- );
1506
+ const confServer = loadConfServerJson(`./engine-private/conf/${options.deployId}/conf.server.json`);
1458
1507
  hosts.push(...Object.keys(confServer));
1459
1508
  }
1460
1509
  const hostListenResult = Underpost.deploy.etcHostFactory(hosts);
@@ -1569,7 +1618,7 @@ EOF
1569
1618
  `npm install -g npm@11.2.0`,
1570
1619
  `npm install -g underpost`,
1571
1620
  `${baseCommand} secret underpost --create-from-file /etc/config/.env.${env}`,
1572
- `${baseCommand} start --build --run ${deployId} ${env} --underpost-quickly-install`,
1621
+ `${baseCommand} start --build --run ${deployId} ${env}`,
1573
1622
  ];
1574
1623
  shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
1575
1624
  },
@@ -1588,19 +1637,22 @@ EOF
1588
1637
  for (let deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1589
1638
  deployId = deployId.trim();
1590
1639
  const _path = '/single-replica';
1591
- const confServer = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'));
1640
+ const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
1592
1641
  shellExec(`${baseCommand} env ${deployId} ${env}`);
1593
1642
  for (const host of Object.keys(confServer))
1594
- if (_path in confServer[host]) shellExec(`node bin/deploy build-single-replica ${deployId} ${host} ${_path}`);
1643
+ if (_path in confServer[host])
1644
+ await Underpost.repo.client(deployId, '', host, _path, {
1645
+ singleReplica: true,
1646
+ });
1595
1647
  const node = options.nodeName
1596
1648
  ? options.nodeName
1597
- : options.dev || !isDeployRunnerContext(path, options)
1649
+ : !options.kubeadm && !options.k3s
1598
1650
  ? 'kind-control-plane'
1599
1651
  : os.hostname();
1600
1652
  // deployId, replicas, versions, image, node
1601
1653
  let defaultPath = [deployId, 1, ``, ``, node];
1602
1654
  shellExec(`${baseCommand} run${options.dev === true ? ' --dev' : ''} --build sync ${defaultPath}`);
1603
- shellExec(`node bin/deploy build-full-client ${deployId}`);
1655
+ await Underpost.repo.client(deployId);
1604
1656
  }
1605
1657
  if (isDeployRunnerContext(path, options)) shellExec(`${baseCommand} run promote ${path} production`);
1606
1658
  },
@@ -1677,7 +1729,7 @@ EOF
1677
1729
  shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${fromPath} ${toPath}`);
1678
1730
  }
1679
1731
 
1680
- openTerminal(`firefox ${outsPaths.join(' ')}`, { single: true });
1732
+ shellExec(`firefox ${outsPaths.join(' ')}`);
1681
1733
  process.exit(0);
1682
1734
  }
1683
1735
  })();
@@ -1840,6 +1892,30 @@ EOF
1840
1892
  const imagePullPolicy = options.imagePullPolicy || 'IfNotPresent';
1841
1893
  const hostNetwork = options.hostNetwork ? options.hostNetwork : '';
1842
1894
  const apiVersion = options.apiVersion || 'v1';
1895
+ // Parse hostAliases option:
1896
+ // - string from CLI: "ip1=host1,host2;ip2=host3,host4"
1897
+ // - array from programmatic callers: [{ ip: "127.0.0.1", hostnames: ["foo.local"] }]
1898
+ const hostAliases = options.hostAliases
1899
+ ? Array.isArray(options.hostAliases)
1900
+ ? options.hostAliases
1901
+ : options.hostAliases
1902
+ .split(';')
1903
+ .filter((entry) => entry.trim())
1904
+ .map((entry) => {
1905
+ const [ip, hostnamesStr] = entry.split('=');
1906
+ const hostnames = hostnamesStr ? hostnamesStr.split(',').map((h) => h.trim()) : [];
1907
+ return { ip: ip.trim(), hostnames };
1908
+ })
1909
+ : [];
1910
+ const hostAliasesYaml =
1911
+ hostAliases.length > 0
1912
+ ? ` hostAliases:\n${hostAliases
1913
+ .map(
1914
+ (alias) =>
1915
+ ` - ip: "${alias.ip}"\n hostnames:\n${alias.hostnames.map((h) => ` - "${h}"`).join('\n')}`,
1916
+ )
1917
+ .join('\n')}`
1918
+ : '';
1843
1919
  const labels = options.labels
1844
1920
  ? options.labels
1845
1921
  .split(',')
@@ -1870,6 +1946,7 @@ spec:
1870
1946
  restartPolicy: ${restartPolicy}
1871
1947
  ${runtimeClassName ? ` runtimeClassName: ${runtimeClassName}` : ''}
1872
1948
  ${hostNetwork ? ` hostNetwork: ${hostNetwork}` : ''}
1949
+ ${hostAliasesYaml}
1873
1950
  containers:
1874
1951
  - name: ${containerName}
1875
1952
  image: ${imageName}
@@ -4,13 +4,10 @@
4
4
  * @namespace UnderpostSecret
5
5
  */
6
6
 
7
- import dotenv from 'dotenv';
8
7
  import { shellExec } from '../server/process.js';
9
8
  import fs from 'fs-extra';
10
9
  import Underpost from '../index.js';
11
10
 
12
- dotenv.config();
13
-
14
11
  /**
15
12
  * @class UnderpostSecret
16
13
  * @description Manages the secrets of the application.
package/src/cli/ssh.js CHANGED
@@ -497,7 +497,7 @@ EOF`);
497
497
  },
498
498
 
499
499
  /**
500
- * Generic SSH remote command runner that centralizes SSH execution logic.
500
+ * Generic SSH remote command runner that SSH execution logic.
501
501
  * Executes arbitrary shell commands on a remote server via SSH with proper credential handling.
502
502
  * @async
503
503
  * @function sshRemoteRunner
@@ -13,20 +13,32 @@ const AgGrid = {
13
13
  Render: async function (options) {
14
14
  let { id, paginationOptions } = options;
15
15
  setTimeout(() => {
16
+ // Normalize rowSelection from deprecated string form to object form (AG Grid v32.2.1+)
17
+ let gridOptionsOverrides = { ...(options.gridOptions || {}) };
18
+ if (typeof gridOptionsOverrides.rowSelection === 'string') {
19
+ const mode = gridOptionsOverrides.rowSelection; // 'single' or 'multiple'
20
+ gridOptionsOverrides.rowSelection = {
21
+ mode: mode === 'multiple' ? 'multiRow' : 'singleRow',
22
+ };
23
+ }
24
+
16
25
  // Grid Options: Contains all of the grid configurations
17
26
  const gridOptions = {
27
+ // Use legacy CSS theme mode to avoid conflict with Theming API (AG Grid v33+)
28
+ theme: 'legacy',
18
29
  // Row Data: The data to be displayed.
19
30
  pagination: false, // Disabled by default, will be handled by the management view
20
31
  // paginationPageSize: 100,
21
32
  // suppressPaginationPanel: true, // We are using our own custom pagination component
22
33
  // rowHeight: 60,
23
- enableCellChangeFlash: true,
34
+ // enableCellChangeFlash was removed in v35; use enableCellChangeFlash on defaultColDef instead
24
35
  defaultColDef: {
25
36
  editable: false,
26
37
  flex: 1,
27
38
  minWidth: 50,
28
39
  filter: true,
29
40
  autoHeight: true,
41
+ enableCellChangeFlash: true,
30
42
  },
31
43
  rowClassRules: {
32
44
  'row-new-highlight': (params) => {
@@ -76,7 +88,7 @@ const AgGrid = {
76
88
  return { field };
77
89
  })
78
90
  : [],
79
- ...options.gridOptions,
91
+ ...gridOptionsOverrides,
80
92
  };
81
93
 
82
94
  // Your Javascript code to create the grid
@@ -86,8 +98,11 @@ const AgGrid = {
86
98
  // myGridElement.style.setProperty('width', '100%');
87
99
  ThemeEvents[id] = () => {
88
100
  if (s(`.${id}`)) {
89
- s(`.${id}`).classList.remove(darkTheme ? this.theme : this.theme + '-dark');
90
- s(`.${id}`).classList.add(!darkTheme ? this.theme : this.theme + '-dark');
101
+ // darkTheme has already been updated by Css.js when this event fires
102
+ // If darkTheme is true: remove light class, add dark class
103
+ // If darkTheme is false: remove dark class, add light class
104
+ s(`.${id}`).classList.remove(this.theme, this.theme + '-dark');
105
+ s(`.${id}`).classList.add(darkTheme ? this.theme + '-dark' : this.theme);
91
106
  } else {
92
107
  // console.warn('change theme: grid not found');
93
108
  delete ThemeEvents[id];
@@ -112,7 +127,7 @@ const AgGrid = {
112
127
  : '';
113
128
  return html`
114
129
  <div
115
- class="${id} ${this.theme}${options?.darkTheme ? `-dark` : ''}"
130
+ class="${id} ${darkTheme ? this.theme + '-dark' : this.theme}"
116
131
  style="${options?.style
117
132
  ? Object.keys(options.style).map((styleKey) => `${styleKey}: ${options.style[styleKey]}; `)
118
133
  : ''}"
@@ -56,7 +56,7 @@ const attachMarkdownLinkHandlers = (containerSelector) => {
56
56
  };
57
57
 
58
58
  const Content = {
59
- Render: async function (options = { idModal: '' }) {
59
+ Render: async function (options = { idModal: '', titleIcon: '' }) {
60
60
  const { idModal } = options;
61
61
  setTimeout(async () => {
62
62
  try {
@@ -111,11 +111,30 @@ const Content = {
111
111
  throw new Error(`no-preview-available`);
112
112
  }
113
113
 
114
+ // Use custom titleIcon from options, or extract from the modal's original title HTML, or fall back to default
115
+ const titleIcon = options.titleIcon
116
+ ? options.titleIcon
117
+ : Modal.Data[idModal] &&
118
+ Modal.Data[idModal].options &&
119
+ Modal.Data[idModal].options.title &&
120
+ Modal.Data[idModal].options.title.includes &&
121
+ Modal.Data[idModal].options.title.includes('<img')
122
+ ? Modal.Data[idModal].options.title.match(/<img[^>]*>/)?.[0] || html`<i class="inl far fa-file"></i>`
123
+ : html`<i class="inl far fa-file"></i>`;
124
+
125
+ // Preserve the original text wrapper class if present in the modal's stored title
126
+ const originalTitle = Modal.Data[idModal]?.options?.title || '';
127
+ const hasCustomTextClass = originalTitle.includes && originalTitle.includes('underpost-text-title-modal');
128
+ const docTitle = documentObj.title ? documentObj.title : documentObj.location;
129
+ const titleText = hasCustomTextClass
130
+ ? `<span class='inl underpost-text-title-modal'>${docTitle}</span>`
131
+ : docTitle;
132
+
114
133
  htmls(
115
134
  `.title-modal-${idModal}`,
116
135
  html`${renderViewTitle({
117
- icon: html`<i class="inl far fa-file"></i>`,
118
- text: `${documentObj.title ? documentObj.title : documentObj.location}`,
136
+ icon: titleIcon,
137
+ text: titleText,
119
138
  })} `,
120
139
  );
121
140
  htmls(`.content-render-${idModal}`, ``);
@@ -12,6 +12,10 @@ const Docs = {
12
12
  const docData = this.Data.find((d) => d.type === type);
13
13
  const ModalId = `modal-docs-${docData.type}`;
14
14
  const { barConfig } = await Themes[Css.currentTheme]();
15
+ const parentBarMode =
16
+ Modal.Data['modal-docs'] && Modal.Data['modal-docs'].options.barMode
17
+ ? Modal.Data['modal-docs'].options.barMode
18
+ : undefined;
15
19
 
16
20
  await Modal.Render({
17
21
  barConfig,
@@ -34,7 +38,7 @@ const Docs = {
34
38
  route: 'docs',
35
39
  slideMenu: 'modal-menu',
36
40
  observer: true,
37
- barMode: 'top-bottom-bar',
41
+ barMode: parentBarMode,
38
42
  query: true,
39
43
  RouterInstance: Modal.Data['modal-docs'].options.RouterInstance,
40
44
  });
@@ -166,6 +170,8 @@ const Docs = {
166
170
  icon: html`<i class="fab fa-github"></i>`,
167
171
  text: `Last Release`,
168
172
  url: function () {
173
+ const tokenOpts = Docs.Tokens['modal-docs'];
174
+ if (tokenOpts && tokenOpts.lastReleaseUrl) return tokenOpts.lastReleaseUrl();
169
175
  return `https://github.com/underpostnet/pwa-microservices-template-ghpkg/`;
170
176
  },
171
177
  },
@@ -180,6 +186,8 @@ const Docs = {
180
186
  </svg>`,
181
187
  text: html`Demo`,
182
188
  url: function () {
189
+ const tokenOpts = Docs.Tokens['modal-docs'];
190
+ if (tokenOpts && tokenOpts.demoUrl) return tokenOpts.demoUrl();
183
191
  return `https://underpostnet.github.io/pwa-microservices-template-ghpkg/`;
184
192
  },
185
193
  },
@@ -204,6 +212,8 @@ const Docs = {
204
212
  icon: html`<img height="20" width="20" class="doc-icon-coverage" />`,
205
213
  text: `Coverage report`,
206
214
  url: function () {
215
+ const tokenOpts = Docs.Tokens['modal-docs'];
216
+ if (tokenOpts && tokenOpts.coverageUrl) return tokenOpts.coverageUrl();
207
217
  return `${getProxyPath()}docs/coverage`;
208
218
  },
209
219
  themeEvent: () => {
@@ -223,7 +233,7 @@ const Docs = {
223
233
  },
224
234
  ],
225
235
  Tokens: {},
226
- Init: async function (options) {
236
+ Init: async function (options = {}) {
227
237
  const { idModal } = options;
228
238
  this.Tokens[idModal] = options;
229
239
  setTimeout(() => {
@@ -284,10 +294,14 @@ const Docs = {
284
294
  break;
285
295
  }
286
296
  tabHref = docData.url();
297
+ const subMenuIcon =
298
+ options.subMenuIcon && typeof options.subMenuIcon === 'function'
299
+ ? options.subMenuIcon(docData.type)
300
+ : docData.icon;
287
301
  docMenuRender += html`
288
302
  ${await BtnIcon.Render({
289
303
  class: `in wfa main-btn-menu submenu-btn btn-docs btn-docs-${docData.type}`,
290
- label: html`<span class="inl menu-btn-icon">${docData.icon}</span
304
+ label: html`<span class="inl menu-btn-icon">${subMenuIcon}</span
291
305
  ><span class="menu-label-text menu-label-text-docs"> ${docData.text} </span>`,
292
306
  tabHref,
293
307
  tooltipHtml: await Badge.Render(buildBadgeToolTipMenuOption(docData.text, 'right')),
@@ -447,7 +461,10 @@ const Docs = {
447
461
  if (s(`.docs-card-container-${item.id}`)) {
448
462
  s(`.docs-card-container-${item.id}`).onclick = () => {
449
463
  if (item.id.match('demo')) {
450
- location.href = 'https://underpostnet.github.io/pwa-microservices-template-ghpkg/';
464
+ const demoData = Docs.Data.find((d) => d.type === 'demo');
465
+ location.href = demoData
466
+ ? demoData.url()
467
+ : 'https://underpostnet.github.io/pwa-microservices-template-ghpkg/';
451
468
  } else if (item.id.match('api')) {
452
469
  if (s(`.btn-docs-api`)) s(`.btn-docs-api`).click();
453
470
  } else {