@underpostnet/underpost 3.0.3 → 3.1.1

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 (82) 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 +160 -1
  12. package/CLI-HELP.md +71 -52
  13. package/README.md +2 -2
  14. package/bin/build.js +4 -1
  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 +48 -43
  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 +5 -5
  37. package/src/cli/fs.js +0 -2
  38. package/src/cli/image.js +0 -3
  39. package/src/cli/index.js +27 -13
  40. package/src/cli/monitor.js +5 -6
  41. package/src/cli/repository.js +329 -35
  42. package/src/cli/run.js +118 -69
  43. package/src/cli/secrets.js +1 -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 +20 -6
  51. package/src/client/public/default/sitemap +3 -3
  52. package/src/client/public/test/sitemap +3 -3
  53. package/src/client.build.js +0 -3
  54. package/src/client.dev.js +0 -3
  55. package/src/db/DataBaseProvider.js +17 -2
  56. package/src/db/mariadb/MariaDB.js +14 -9
  57. package/src/db/mongo/MongooseDB.js +17 -1
  58. package/src/index.js +1 -1
  59. package/src/proxy.js +0 -3
  60. package/src/runtime/express/Express.js +7 -1
  61. package/src/runtime/lampp/Lampp.js +6 -13
  62. package/src/server/auth.js +6 -9
  63. package/src/server/backup.js +2 -3
  64. package/src/server/client-build-docs.js +178 -3
  65. package/src/server/client-build-live.js +9 -18
  66. package/src/server/client-build.js +175 -38
  67. package/src/server/client-dev-server.js +14 -13
  68. package/src/server/conf.js +357 -149
  69. package/src/server/cron.js +2 -1
  70. package/src/server/dns.js +28 -12
  71. package/src/server/downloader.js +0 -2
  72. package/src/server/logger.js +27 -9
  73. package/src/server/peer.js +0 -2
  74. package/src/server/process.js +1 -50
  75. package/src/server/proxy.js +4 -8
  76. package/src/server/runtime.js +5 -8
  77. package/src/server/ssr.js +0 -3
  78. package/src/server/start.js +5 -5
  79. package/src/server/tls.js +0 -2
  80. package/src/server.js +0 -4
  81. package/.env.development +0 -43
  82. 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';
@@ -55,7 +56,6 @@ const logger = loggerFactory(import.meta);
55
56
  * @property {string} apiVersion - The API version for the container.
56
57
  * @property {string} claimName - The claim name for the volume.
57
58
  * @property {string} kindType - The kind of resource to create.
58
- * @property {boolean} terminal - Whether to open a terminal.
59
59
  * @property {number} devProxyPortOffset - The port offset for the development proxy.
60
60
  * @property {boolean} hostNetwork - Whether to use host networking.
61
61
  * @property {string} requestsMemory - The memory request for the container.
@@ -120,7 +120,6 @@ const DEFAULT_OPTION = {
120
120
  apiVersion: '',
121
121
  claimName: '',
122
122
  kindType: '',
123
- terminal: false,
124
123
  devProxyPortOffset: 0,
125
124
  hostNetwork: false,
126
125
  requestsMemory: '',
@@ -195,7 +194,7 @@ class UnderpostRun {
195
194
  }
196
195
 
197
196
  {
198
- // Detect MongoDB primary pod using centralized method
197
+ // Detect MongoDB primary pod using method
199
198
  let primaryMongoHost = 'mongodb-0.mongodb-service';
200
199
  try {
201
200
  const primaryPodName = Underpost.db.getMongoPrimaryPodName({
@@ -355,22 +354,35 @@ class UnderpostRun {
355
354
  },
356
355
  /**
357
356
  * @method template-deploy
358
- * @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.
359
358
  * @param {string} path - The input value, identifier, or path for the operation.
360
359
  * @param {Object} options - The default underpost runner options for customizing workflow
361
360
  * @memberof UnderpostRun
362
361
  */
363
362
  'template-deploy': (path = '', options = DEFAULT_OPTION) => {
364
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`);
365
371
  const message = shellExec(`node bin cmt --changelog --changelog-no-hash`, { silent: true, stdout: true }).trim();
366
- shellExec(`${baseCommand} run clean`);
367
372
  shellExec(
368
373
  `${baseCommand} push ./engine-private ${options.force ? '-f ' : ''}${
369
374
  process.env.GITHUB_USERNAME
370
375
  }/engine-private`,
371
376
  );
372
377
  shellCd('/home/dd/engine');
373
- 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
+
374
386
  function replaceNthNewline(str, n, replacement = ' ') {
375
387
  let count = 0;
376
388
  return str.replace(/\r\n?|\n/g, (match) => {
@@ -378,35 +390,54 @@ class UnderpostRun {
378
390
  return count === n ? replacement : match;
379
391
  });
380
392
  }
381
- shellExec(
382
- `${baseCommand} cmt . --empty ci package-pwa-microservices-template${
383
- path.startsWith('sync') ? `-${path}` : ''
384
- }${
385
- message
386
- ? ` "${replaceNthNewline(
387
- message.replaceAll('"', '').replaceAll('`', '').replaceAll('#', '').replaceAll('- ', ''),
388
- 2,
389
- )}"`
390
- : ''
391
- }`,
392
- );
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`);
393
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}`);
394
425
  },
395
426
 
396
427
  /**
397
428
  * @method template-deploy-image
398
- * @description Commits and pushes a Docker image deployment for the `engine` repository.
429
+ * @description Dispatches the Docker image CI workflow for the `engine` repository.
399
430
  * @param {string} path - The input value, identifier, or path for the operation.
400
431
  * @param {Object} options - The default underpost runner options for customizing workflow
401
432
  * @memberof UnderpostRun
402
433
  */
403
434
  'template-deploy-image': (path, options = DEFAULT_OPTION) => {
404
- // const baseCommand = options.dev ? 'node bin' : 'underpost';
405
- shellExec(
406
- `cd /home/dd/engine && git reset && underpost cmt . --empty ci docker-image 'underpost-engine:${
407
- Underpost.version
408
- }' && underpost push . ${options.force ? '-f ' : ''}${process.env.GITHUB_USERNAME}/engine`,
409
- );
435
+ Underpost.repo.dispatchWorkflow({
436
+ repo: `${process.env.GITHUB_USERNAME}/engine`,
437
+ workflowFile: 'docker-image.ci.yml',
438
+ ref: 'master',
439
+ inputs: {},
440
+ });
410
441
  },
411
442
  /**
412
443
  * @method clean
@@ -467,18 +498,30 @@ class UnderpostRun {
467
498
  },
468
499
  /**
469
500
  * @method ssh-deploy
470
- * @description Performs a Git reset, commits with a message `cd ssh-${path}`, and pushes the `engine` repository, likely triggering an SSH-based CD pipeline.
471
- * @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').
472
503
  * @param {Object} options - The default underpost runner options for customizing workflow
473
504
  * @memberof UnderpostRun
474
505
  */
475
506
  'ssh-deploy': (path, options = DEFAULT_OPTION) => {
476
507
  actionInitLog();
477
- const baseCommand = options.dev ? 'node bin' : 'underpost';
478
- shellCd('/home/dd/engine');
479
- shellExec(`git reset`);
480
- shellExec(`${baseCommand} cmt . --empty cd ssh-${path}`);
481
- 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
+ });
482
525
  },
483
526
  /**
484
527
  * @method ide
@@ -488,15 +531,25 @@ class UnderpostRun {
488
531
  * @param {Object} options - The default underpost runner options for customizing workflow
489
532
  * @memberof UnderpostRun
490
533
  */
491
- ide: (path, options = DEFAULT_OPTION) => {
492
- const { underpostRoot } = options;
493
- if (path === 'install') {
494
- shellExec(`sudo curl -f https://zed.dev/install.sh | sh`);
495
- shellExec(
496
- `sudo dnf config-manager --add-repo https://download.sublimetext.com/rpm/stable/x86_64/sublime-text.repo`,
497
- );
498
- shellExec(`sudo dnf install -y sublime-text`);
499
- } 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}`);
500
553
  },
501
554
  /**
502
555
  * @method crypto-policy
@@ -525,7 +578,7 @@ class UnderpostRun {
525
578
  options.replicas,
526
579
  ``,
527
580
  ``,
528
- options.dev || !isDeployRunnerContext(path, options) ? 'kind-control-plane' : os.hostname(),
581
+ !options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname(),
529
582
  ];
530
583
  let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
531
584
  deployId = deployId ? deployId : defaultPath[0];
@@ -1008,9 +1061,7 @@ EOF
1008
1061
  volumeMountPath: volumeHostPath,
1009
1062
  ...(options.dev ? { volumeHostPath } : { claimName }),
1010
1063
  on: {
1011
- init: async () => {
1012
- // openTerminal(`kubectl logs -f ${podName}`);
1013
- },
1064
+ init: async () => {},
1014
1065
  },
1015
1066
  args: [daemonProcess(path ? path : `cd /home/dd/engine && npm install && npm run test`)],
1016
1067
  };
@@ -1145,7 +1196,7 @@ EOF
1145
1196
  const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
1146
1197
  let hosts = [];
1147
1198
  for (const deployId of deployList) {
1148
- 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`);
1149
1200
  hosts = hosts.concat(Object.keys(confServer));
1150
1201
  }
1151
1202
  shellExec(`node bin cluster --prom ${hosts.join(',')}`);
@@ -1318,34 +1369,31 @@ EOF
1318
1369
  if (!subConf) subConf = 'local';
1319
1370
  if (options.reset && fs.existsSync(`./engine-private/conf/${deployId}`))
1320
1371
  fs.removeSync(`./engine-private/conf/${deployId}`);
1321
- if (!fs.existsSync(`./engine-private/conf/${deployId}`)) Config.deployIdFactory(deployId, { subConf });
1322
1372
  if (options.devProxyPortOffset) {
1323
1373
  const envPath = `./engine-private/conf/${deployId}/.env.development`;
1324
1374
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
1325
1375
  envObj.DEV_PROXY_PORT_OFFSET = options.devProxyPortOffset;
1326
1376
  writeEnv(envPath, envObj);
1327
1377
  }
1378
+ dotenv.config({ path: `./engine-private/conf/${deployId}/.env.development`, override: true });
1328
1379
  shellExec(`node bin run dev-cluster --expose --namespace ${options.namespace}`, { async: true });
1329
1380
  {
1330
- const cmd = `npm run dev-api ${deployId} ${subConf} ${host} ${_path} ${clientHostPort}${
1381
+ const cmd = `npm run dev:api ${deployId} ${subConf} ${host} ${_path} ${clientHostPort} proxy${
1331
1382
  options.tls ? ' tls' : ''
1332
1383
  }`;
1333
- options.terminal ? openTerminal(cmd) : shellExec(cmd, { async: true });
1384
+ shellExec(cmd, { async: true });
1334
1385
  }
1335
1386
  await awaitDeployMonitor(true);
1336
1387
  {
1337
- const cmd = `npm run dev-client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1338
- options.terminal
1339
- ? openTerminal(cmd)
1340
- : shellExec(cmd, {
1341
- async: true,
1342
- });
1388
+ const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1389
+
1390
+ shellExec(cmd, {
1391
+ async: true,
1392
+ });
1343
1393
  }
1344
1394
  await awaitDeployMonitor(true);
1345
1395
  shellExec(
1346
- `./node_modules/.bin/env-cmd -f .env.development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${
1347
- options.tls ? ' tls' : ''
1348
- }`,
1396
+ `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1349
1397
  );
1350
1398
  },
1351
1399
 
@@ -1364,7 +1412,7 @@ EOF
1364
1412
  let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
1365
1413
  if (!replicas) replicas = options.replicas;
1366
1414
  // const confClient = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.client.json`, 'utf8'));
1367
- 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`);
1368
1416
  // const confSSR = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.ssr.json`, 'utf8'));
1369
1417
  // const packageData = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/package.json`, 'utf8'));
1370
1418
  const services = fs.existsSync(`./engine-private/deploy/${deployId}/conf.services.json`)
@@ -1455,9 +1503,7 @@ EOF
1455
1503
  'etc-hosts': async (path = '', options = DEFAULT_OPTION) => {
1456
1504
  const hosts = path ? path.split(',') : [];
1457
1505
  if (options.deployId) {
1458
- const confServer = JSON.parse(
1459
- fs.readFileSync(`./engine-private/conf/${options.deployId}/conf.server.json`, 'utf8'),
1460
- );
1506
+ const confServer = loadConfServerJson(`./engine-private/conf/${options.deployId}/conf.server.json`);
1461
1507
  hosts.push(...Object.keys(confServer));
1462
1508
  }
1463
1509
  const hostListenResult = Underpost.deploy.etcHostFactory(hosts);
@@ -1572,7 +1618,7 @@ EOF
1572
1618
  `npm install -g npm@11.2.0`,
1573
1619
  `npm install -g underpost`,
1574
1620
  `${baseCommand} secret underpost --create-from-file /etc/config/.env.${env}`,
1575
- `${baseCommand} start --build --run ${deployId} ${env} --underpost-quickly-install`,
1621
+ `${baseCommand} start --build --run ${deployId} ${env}`,
1576
1622
  ];
1577
1623
  shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
1578
1624
  },
@@ -1591,19 +1637,22 @@ EOF
1591
1637
  for (let deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
1592
1638
  deployId = deployId.trim();
1593
1639
  const _path = '/single-replica';
1594
- 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`);
1595
1641
  shellExec(`${baseCommand} env ${deployId} ${env}`);
1596
1642
  for (const host of Object.keys(confServer))
1597
- 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
+ });
1598
1647
  const node = options.nodeName
1599
1648
  ? options.nodeName
1600
- : options.dev || !isDeployRunnerContext(path, options)
1649
+ : !options.kubeadm && !options.k3s
1601
1650
  ? 'kind-control-plane'
1602
1651
  : os.hostname();
1603
1652
  // deployId, replicas, versions, image, node
1604
1653
  let defaultPath = [deployId, 1, ``, ``, node];
1605
1654
  shellExec(`${baseCommand} run${options.dev === true ? ' --dev' : ''} --build sync ${defaultPath}`);
1606
- shellExec(`node bin/deploy build-full-client ${deployId}`);
1655
+ await Underpost.repo.client(deployId);
1607
1656
  }
1608
1657
  if (isDeployRunnerContext(path, options)) shellExec(`${baseCommand} run promote ${path} production`);
1609
1658
  },
@@ -1680,7 +1729,7 @@ EOF
1680
1729
  shellExec(`sudo kubectl cp ${nameSpace}/${podName}:${basePath}/docs${fromPath} ${toPath}`);
1681
1730
  }
1682
1731
 
1683
- openTerminal(`firefox ${outsPaths.join(' ')}`, { single: true });
1732
+ shellExec(`firefox ${outsPaths.join(' ')}`);
1684
1733
  process.exit(0);
1685
1734
  }
1686
1735
  })();
@@ -4,13 +4,11 @@
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';
9
+ import dotenv from 'dotenv';
10
10
  import Underpost from '../index.js';
11
11
 
12
- dotenv.config();
13
-
14
12
  /**
15
13
  * @class UnderpostSecret
16
14
  * @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 {