underpost 2.95.3 → 2.95.8

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.
package/src/cli/cron.js CHANGED
@@ -4,12 +4,10 @@
4
4
  * @namespace UnderpostCron
5
5
  */
6
6
 
7
- import { DataBaseProvider } from '../db/DataBaseProvider.js';
8
7
  import BackUp from '../server/backup.js';
9
8
  import { Cmd } from '../server/conf.js';
10
9
  import Dns from '../server/dns.js';
11
10
  import { loggerFactory } from '../server/logger.js';
12
-
13
11
  import { shellExec } from '../server/process.js';
14
12
  import fs from 'fs-extra';
15
13
 
@@ -37,57 +35,191 @@ class UnderpostCron {
37
35
  */
38
36
  backup: BackUp,
39
37
  };
38
+
40
39
  static API = {
41
40
  /**
42
41
  * Run the cron jobs
43
42
  * @static
44
43
  * @param {String} deployList - Comma separated deploy ids
45
44
  * @param {String} jobList - Comma separated job ids
45
+ * @param {Object} options - Options for cron execution
46
46
  * @return {void}
47
47
  * @memberof UnderpostCron
48
48
  */
49
49
  callback: async function (
50
50
  deployList = 'default',
51
- jobList = Object.keys(UnderpostCron.JOB),
52
- options = { itc: false, init: false, git: false },
51
+ jobList = Object.keys(UnderpostCron.JOB).join(','),
52
+ options = { initPm2Cronjobs: false, git: false, updatePackageScripts: false },
53
53
  ) {
54
- if (options.init === true) {
55
- const jobDeployId = fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
56
- deployList = fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').trim();
57
- const confCronConfig = JSON.parse(fs.readFileSync(`./engine-private/conf/${jobDeployId}/conf.cron.json`));
58
- if (confCronConfig.jobs && Object.keys(confCronConfig.jobs).length > 0) {
59
- for (const job of Object.keys(confCronConfig.jobs)) {
60
- const name = `${jobDeployId}-${job}`;
61
- let deployId;
62
- shellExec(Cmd.delete(name));
63
- deployId = UnderpostCron.API.getRelatedDeployId(job);
64
- shellExec(Cmd.cron(deployId, job, name, confCronConfig.jobs[job].expression, options));
65
- }
66
- }
54
+ if (options.updatePackageScripts === true) {
55
+ await UnderpostCron.API.updatePackageScripts(deployList);
67
56
  return;
68
57
  }
58
+
59
+ if (options.initPm2Cronjobs === true) {
60
+ await UnderpostCron.API.initCronJobs(options);
61
+ return;
62
+ }
63
+
64
+ // Execute the requested jobs
69
65
  for (const _jobId of jobList.split(',')) {
70
66
  const jobId = _jobId.trim();
71
- if (UnderpostCron.JOB[jobId]) await UnderpostCron.JOB[jobId].callback(deployList, options);
67
+ if (UnderpostCron.JOB[jobId]) {
68
+ logger.info(`Executing cron job: ${jobId}`);
69
+ await UnderpostCron.JOB[jobId].callback(deployList, options);
70
+ } else {
71
+ logger.warn(`Unknown cron job: ${jobId}`);
72
+ }
73
+ }
74
+ },
75
+
76
+ /**
77
+ * Initialize PM2 cron jobs from configuration
78
+ * @static
79
+ * @param {Object} options - Initialization options
80
+ * @memberof UnderpostCron
81
+ */
82
+ initCronJobs: async function (options = { git: false }) {
83
+ logger.info('Initializing PM2 cron jobs');
84
+
85
+ // Read cron job deployment ID from dd.cron file (e.g., "dd-cron")
86
+ const jobDeployId = fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
87
+ const confCronPath = `./engine-private/conf/${jobDeployId}/conf.cron.json`;
88
+
89
+ if (!fs.existsSync(confCronPath)) {
90
+ logger.warn(`Cron configuration not found: ${confCronPath}`);
91
+ return;
92
+ }
93
+
94
+ const confCronConfig = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
95
+
96
+ if (!confCronConfig.jobs || Object.keys(confCronConfig.jobs).length === 0) {
97
+ logger.info('No cron jobs configured');
98
+ return;
99
+ }
100
+
101
+ // Delete all existing cron jobs
102
+ for (const job of Object.keys(confCronConfig.jobs)) {
103
+ const name = `${jobDeployId}-${job}`;
104
+ logger.info(`Removing existing PM2 process: ${name}`);
105
+ shellExec(Cmd.delete(name));
106
+ }
107
+
108
+ // Create PM2 cron jobs for each configured job
109
+ for (const job of Object.keys(confCronConfig.jobs)) {
110
+ const jobConfig = confCronConfig.jobs[job];
111
+
112
+ if (jobConfig.enabled === false) {
113
+ logger.info(`Skipping disabled job: ${job}`);
114
+ continue;
115
+ }
116
+
117
+ const name = `${jobDeployId}-${job}`;
118
+ const deployIdList = UnderpostCron.API.getRelatedDeployIdList(job);
119
+ const expression = jobConfig.expression || '0 0 * * *'; // Default: daily at midnight
120
+ const instances = jobConfig.instances || 1; // Default: 1 instance
121
+
122
+ logger.info(`Creating PM2 cron job: ${name} with expression: ${expression}, instances: ${instances}`);
123
+ shellExec(Cmd.cron(deployIdList, job, name, expression, options, instances));
72
124
  }
125
+
126
+ logger.info('PM2 cron jobs initialization completed');
73
127
  },
74
128
 
75
129
  /**
76
- * Get the related deploy id for the given job id
130
+ * Update package.json start scripts for specified deploy-ids
77
131
  * @static
78
- * @param {String} jobId - The job id
79
- * @return {String} The related deploy id
132
+ * @param {String} deployList - Comma separated deploy ids
80
133
  * @memberof UnderpostCron
81
134
  */
82
- getRelatedDeployId(jobId) {
83
- switch (jobId) {
84
- case 'dns':
85
- return fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
86
- case 'backup':
87
- return fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').trim();
88
- default:
89
- return fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
135
+ updatePackageScripts: async function (deployList = 'default') {
136
+ logger.info('Updating package.json start scripts for deploy-id configurations');
137
+
138
+ // Resolve deploy list
139
+ if ((!deployList || deployList === 'dd') && fs.existsSync(`./engine-private/deploy/dd.router`)) {
140
+ deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim();
141
+ }
142
+
143
+ const confDir = './engine-private/conf';
144
+ if (!fs.existsSync(confDir)) {
145
+ logger.warn(`Configuration directory not found: ${confDir}`);
146
+ return;
90
147
  }
148
+
149
+ // Parse deploy list into array
150
+ const deployIds = deployList
151
+ .split(',')
152
+ .map((id) => id.trim())
153
+ .filter((id) => id);
154
+
155
+ for (const deployId of deployIds) {
156
+ const packageJsonPath = `${confDir}/${deployId}/package.json`;
157
+ const confCronPath = `${confDir}/${deployId}/conf.cron.json`;
158
+
159
+ // Only update if both package.json and conf.cron.json exist
160
+ if (!fs.existsSync(packageJsonPath)) {
161
+ logger.info(`Skipping ${deployId}: package.json not found`);
162
+ continue;
163
+ }
164
+
165
+ if (!fs.existsSync(confCronPath)) {
166
+ logger.info(`Skipping ${deployId}: conf.cron.json not found`);
167
+ continue;
168
+ }
169
+
170
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
171
+ const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
172
+
173
+ // Build start script based on cron jobs configuration
174
+ if (confCron.jobs && Object.keys(confCron.jobs).length > 0) {
175
+ const hasEnabledJobs = Object.values(confCron.jobs).some((job) => job.enabled !== false);
176
+
177
+ if (hasEnabledJobs) {
178
+ // Update start script with PM2 cron jobs initialization
179
+ const startScript = 'pm2 flush && pm2 reloadLogs && node bin cron --init-pm2-cronjobs --git';
180
+
181
+ if (!packageJson.scripts) {
182
+ packageJson.scripts = {};
183
+ }
184
+
185
+ packageJson.scripts.start = startScript;
186
+
187
+ // Write updated package.json
188
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n', 'utf8');
189
+ logger.info(`Updated package.json for ${deployId} with cron start script`);
190
+ } else {
191
+ logger.info(`Skipping ${deployId}: no enabled cron jobs`);
192
+ }
193
+ } else {
194
+ logger.info(`Skipping ${deployId}: no cron jobs configured`);
195
+ }
196
+ }
197
+
198
+ logger.info('Package.json start scripts update completed');
199
+ },
200
+
201
+ /**
202
+ * Get the related deploy id list for the given job id
203
+ * @static
204
+ * @param {String} jobId - The job id (e.g., 'dns', 'backup')
205
+ * @return {String} Comma-separated list of deploy ids to process
206
+ * @memberof UnderpostCron
207
+ */
208
+ getRelatedDeployIdList(jobId) {
209
+ // Backup job uses dd.router file (contains multiple deploy-ids)
210
+ // Other jobs use dd.cron file (contains single deploy-id)
211
+ const deployFilePath =
212
+ jobId === 'backup' ? './engine-private/deploy/dd.router' : './engine-private/deploy/dd.cron';
213
+
214
+ if (!fs.existsSync(deployFilePath)) {
215
+ logger.warn(`Deploy file not found: ${deployFilePath}, using default`);
216
+ return fs.existsSync('./engine-private/deploy/dd.cron')
217
+ ? fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim()
218
+ : 'dd-cron';
219
+ }
220
+
221
+ // Return the deploy-id list from the file (may be single or comma-separated)
222
+ return fs.readFileSync(deployFilePath, 'utf8').trim();
91
223
  },
92
224
  };
93
225
  }
package/src/cli/db.js CHANGED
@@ -31,7 +31,6 @@ const MAX_BACKUP_RETENTION = 5;
31
31
  * @property {boolean} [import=false] - Flag to import data from a backup
32
32
  * @property {boolean} [export=false] - Flag to export data to a backup
33
33
  * @property {string} [podName=''] - Comma-separated list of pod names or patterns
34
- * @property {string} [nodeName=''] - Comma-separated list of node names for pod filtering
35
34
  * @property {string} [ns='default'] - Kubernetes namespace
36
35
  * @property {string} [collections=''] - Comma-separated list of collections to include
37
36
  * @property {string} [outPath=''] - Output path for backup files
@@ -40,7 +39,6 @@ const MAX_BACKUP_RETENTION = 5;
40
39
  * @property {boolean} [git=false] - Flag to enable Git integration
41
40
  * @property {string} [hosts=''] - Comma-separated list of hosts to include
42
41
  * @property {string} [paths=''] - Comma-separated list of paths to include
43
- * @property {string} [labelSelector=''] - Kubernetes label selector for pods
44
42
  * @property {boolean} [allPods=false] - Flag to target all matching pods
45
43
  * @property {boolean} [primaryPod=false] - Flag to automatically detect and use MongoDB primary pod
46
44
  * @property {boolean} [stats=false] - Flag to display collection/table statistics
@@ -78,43 +76,17 @@ const MAX_BACKUP_RETENTION = 5;
78
76
  */
79
77
  class UnderpostDB {
80
78
  static API = {
81
- /**
82
- * Helper: Validates namespace name
83
- * @private
84
- * @param {string} namespace - Namespace to validate
85
- * @returns {boolean} True if valid
86
- */
87
- _validateNamespace(namespace) {
88
- if (!namespace || typeof namespace !== 'string') return false;
89
- // Kubernetes namespace naming rules: lowercase alphanumeric, -, max 63 chars
90
- return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) && namespace.length <= 63;
91
- },
92
-
93
- /**
94
- * Helper: Validates pod name
95
- * @private
96
- * @param {string} podName - Pod name to validate
97
- * @returns {boolean} True if valid
98
- */
99
- _validatePodName(podName) {
100
- if (!podName || typeof podName !== 'string') return false;
101
- // Kubernetes pod naming rules: lowercase alphanumeric, -, max 253 chars
102
- return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(podName) && podName.length <= 253;
103
- },
104
-
105
79
  /**
106
80
  * Helper: Gets filtered pods based on criteria
107
81
  * @private
108
82
  * @param {Object} criteria - Filter criteria
109
83
  * @param {string} [criteria.podNames] - Comma-separated pod name patterns
110
- * @param {string} [criteria.nodeNames] - Comma-separated node names
111
84
  * @param {string} [criteria.namespace='default'] - Kubernetes namespace
112
- * @param {string} [criteria.labelSelector] - Label selector
113
85
  * @param {string} [criteria.deployId] - Deployment ID pattern
114
86
  * @returns {Array<PodInfo>} Filtered pod list
115
87
  */
116
88
  _getFilteredPods(criteria = {}) {
117
- const { podNames, nodeNames, namespace = 'default', labelSelector, deployId } = criteria;
89
+ const { podNames, namespace = 'default', deployId } = criteria;
118
90
 
119
91
  try {
120
92
  // Get all pods using UnderpostDeploy.API.get
@@ -132,25 +104,6 @@ class UnderpostDB {
132
104
  });
133
105
  }
134
106
 
135
- // Filter by node names if specified (only if NODE is not '<none>')
136
- if (nodeNames) {
137
- const nodes = nodeNames.split(',').map((n) => n.trim());
138
- pods = pods.filter((pod) => {
139
- // Skip filtering if NODE is '<none>' or undefined
140
- if (!pod.NODE || pod.NODE === '<none>') {
141
- return true;
142
- }
143
- return nodes.includes(pod.NODE);
144
- });
145
- }
146
-
147
- // Filter by label selector if specified
148
- if (labelSelector) {
149
- // Note: UnderpostDeploy.API.get doesn't support label selectors directly
150
- // This would require a separate kubectl command
151
- logger.warn('Label selector filtering requires additional implementation');
152
- }
153
-
154
107
  logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
155
108
  return pods;
156
109
  } catch (error) {
@@ -717,9 +670,6 @@ class UnderpostDB {
717
670
  * @param {string} [options.podName='mongodb-0'] - Initial pod name to query replica set status
718
671
  * @returns {string|null} Primary pod name or null if not found
719
672
  * @memberof UnderpostDB
720
- * @example
721
- * const primaryPod = UnderpostDB.API.getMongoPrimaryPodName({ namespace: 'production' });
722
- * console.log(primaryPod); // 'mongodb-1'
723
673
  */
724
674
  getMongoPrimaryPodName(options = { namespace: 'default', podName: 'mongodb-0' }) {
725
675
  const { namespace = 'default', podName = 'mongodb-0' } = options;
@@ -765,7 +715,6 @@ class UnderpostDB {
765
715
  * @param {boolean} [options.import=false] - Whether to perform import operation
766
716
  * @param {boolean} [options.export=false] - Whether to perform export operation
767
717
  * @param {string} [options.podName=''] - Comma-separated pod name patterns to target
768
- * @param {string} [options.nodeName=''] - Comma-separated node names to target
769
718
  * @param {string} [options.ns='default'] - Kubernetes namespace
770
719
  * @param {string} [options.collections=''] - Comma-separated MongoDB collections for export
771
720
  * @param {string} [options.outPath=''] - Output path for backups
@@ -774,7 +723,6 @@ class UnderpostDB {
774
723
  * @param {boolean} [options.git=false] - Whether to use Git for backup versioning
775
724
  * @param {string} [options.hosts=''] - Comma-separated list of hosts to filter databases
776
725
  * @param {string} [options.paths=''] - Comma-separated list of paths to filter databases
777
- * @param {string} [options.labelSelector=''] - Label selector for pod filtering
778
726
  * @param {boolean} [options.allPods=false] - Whether to target all pods in deployment
779
727
  * @param {boolean} [options.primaryPod=false] - Whether to target MongoDB primary pod only
780
728
  * @param {boolean} [options.stats=false] - Whether to display database statistics
@@ -788,7 +736,6 @@ class UnderpostDB {
788
736
  import: false,
789
737
  export: false,
790
738
  podName: '',
791
- nodeName: '',
792
739
  ns: 'default',
793
740
  collections: '',
794
741
  outPath: '',
@@ -797,7 +744,6 @@ class UnderpostDB {
797
744
  git: false,
798
745
  hosts: '',
799
746
  paths: '',
800
- labelSelector: '',
801
747
  allPods: false,
802
748
  primaryPod: false,
803
749
  stats: false,
@@ -808,12 +754,6 @@ class UnderpostDB {
808
754
  const newBackupTimestamp = new Date().getTime();
809
755
  const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
810
756
 
811
- // Validate namespace
812
- if (!UnderpostDB.API._validateNamespace(namespace)) {
813
- logger.error('Invalid namespace format', { namespace });
814
- throw new Error(`Invalid namespace: ${namespace}`);
815
- }
816
-
817
757
  if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
818
758
 
819
759
  logger.info('Starting database operation', {
@@ -823,6 +763,11 @@ class UnderpostDB {
823
763
  export: options.export,
824
764
  });
825
765
 
766
+ // Track processed repositories to avoid duplicate Git operations
767
+ const processedRepos = new Set();
768
+ // Track processed host+path combinations to avoid duplicates
769
+ const processedHostPaths = new Set();
770
+
826
771
  for (const _deployId of deployList.split(',')) {
827
772
  const deployId = _deployId.trim();
828
773
  if (!deployId) continue;
@@ -831,7 +776,7 @@ class UnderpostDB {
831
776
 
832
777
  /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
833
778
  const dbs = {};
834
- const repoName = `engine-${deployId.split('dd-')[1]}-cron-backups`;
779
+ const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
835
780
 
836
781
  // Load server configuration
837
782
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
@@ -863,31 +808,42 @@ class UnderpostDB {
863
808
  }
864
809
  }
865
810
 
866
- // Handle Git operations
867
- if (options.git === true) {
868
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
869
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
870
- }
811
+ // Handle Git operations - execute only once per repository
812
+ if (!processedRepos.has(repoName)) {
813
+ logger.info('Processing Git operations for repository', { repoName, deployId });
814
+ if (options.git === true) {
815
+ UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
816
+ UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
817
+ }
818
+
819
+ if (options.macroRollbackExport) {
820
+ // Only clone if not already done by git option above
821
+ if (options.git !== true) {
822
+ UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
823
+ UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
824
+ }
871
825
 
872
- if (options.macroRollbackExport) {
873
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
874
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
875
-
876
- const nCommits = parseInt(options.macroRollbackExport);
877
- const repoPath = `../${repoName}`;
878
- const username = process.env.GITHUB_USERNAME;
879
-
880
- if (fs.existsSync(repoPath) && username) {
881
- logger.info('Executing macro rollback export', { repoName, nCommits });
882
- shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
883
- shellExec(`cd ${repoPath} && git reset`);
884
- shellExec(`cd ${repoPath} && git checkout .`);
885
- shellExec(`cd ${repoPath} && git clean -f -d`);
886
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
887
- } else {
888
- if (!username) logger.error('GITHUB_USERNAME environment variable not set');
889
- logger.warn('Repository not found for macro rollback', { repoPath });
826
+ const nCommits = parseInt(options.macroRollbackExport);
827
+ const repoPath = `../${repoName}`;
828
+ const username = process.env.GITHUB_USERNAME;
829
+
830
+ if (fs.existsSync(repoPath) && username) {
831
+ logger.info('Executing macro rollback export', { repoName, nCommits });
832
+ shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
833
+ shellExec(`cd ${repoPath} && git reset`);
834
+ shellExec(`cd ${repoPath} && git checkout .`);
835
+ shellExec(`cd ${repoPath} && git clean -f -d`);
836
+ shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
837
+ } else {
838
+ if (!username) logger.error('GITHUB_USERNAME environment variable not set');
839
+ logger.warn('Repository not found for macro rollback', { repoPath });
840
+ }
890
841
  }
842
+
843
+ processedRepos.add(repoName);
844
+ logger.info('Repository marked as processed', { repoName });
845
+ } else {
846
+ logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
891
847
  }
892
848
 
893
849
  // Process each database provider
@@ -895,6 +851,15 @@ class UnderpostDB {
895
851
  for (const dbName of Object.keys(dbs[provider])) {
896
852
  const { hostFolder, user, password, host, path } = dbs[provider][dbName];
897
853
 
854
+ // Create unique identifier for host+path combination
855
+ const hostPathKey = `${deployId}:${host}:${path}`;
856
+
857
+ // Skip if this host+path combination was already processed
858
+ if (processedHostPaths.has(hostPathKey)) {
859
+ logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
860
+ continue;
861
+ }
862
+
898
863
  // Filter by hosts and paths if specified
899
864
  if (
900
865
  (options.hosts &&
@@ -917,7 +882,7 @@ class UnderpostDB {
917
882
  continue;
918
883
  }
919
884
 
920
- logger.info('Processing database', { hostFolder, provider, dbName });
885
+ logger.info('Processing database', { hostFolder, provider, dbName, deployId });
921
886
 
922
887
  const backUpPath = `../${repoName}/${hostFolder}`;
923
888
  const backupInfo = UnderpostDB.API._manageBackupTimestamps(
@@ -949,16 +914,14 @@ class UnderpostDB {
949
914
  let targetPods = [];
950
915
  const podCriteria = {
951
916
  podNames: options.podName,
952
- nodeNames: options.nodeName,
953
917
  namespace,
954
- labelSelector: options.labelSelector,
955
918
  deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
956
919
  };
957
920
 
958
921
  targetPods = UnderpostDB.API._getFilteredPods(podCriteria);
959
922
 
960
923
  // Fallback to default if no custom pods specified
961
- if (targetPods.length === 0 && !options.podName && !options.nodeName) {
924
+ if (targetPods.length === 0 && !options.podName) {
962
925
  const defaultPods = UnderpostDeploy.API.get(
963
926
  provider === 'mariadb' ? 'mariadb' : 'mongo',
964
927
  'pods',
@@ -1094,16 +1057,20 @@ class UnderpostDB {
1094
1057
  break;
1095
1058
  }
1096
1059
  }
1060
+
1061
+ // Mark this host+path combination as processed
1062
+ processedHostPaths.add(hostPathKey);
1097
1063
  }
1098
1064
  }
1099
1065
 
1100
- // Commit and push to Git if enabled
1101
- if (options.export === true && options.git === true) {
1066
+ // Commit and push to Git if enabled - execute only once per repository
1067
+ if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
1102
1068
  const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
1103
1069
  newBackupTimestamp,
1104
1070
  ).toLocaleTimeString()}`;
1105
1071
  UnderpostDB.API._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
1106
1072
  UnderpostDB.API._manageGitRepo({ repoName, operation: 'push' });
1073
+ processedRepos.add(`${repoName}-committed`);
1107
1074
  }
1108
1075
  }
1109
1076
 
@@ -1267,7 +1234,7 @@ class UnderpostDB {
1267
1234
  for (const jobId of Object.keys(confCron.jobs)) {
1268
1235
  const body = {
1269
1236
  jobId,
1270
- deployId: UnderpostCron.API.getRelatedDeployId(jobId),
1237
+ deployId: UnderpostCron.API.getRelatedDeployIdList(jobId),
1271
1238
  expression: confCron.jobs[jobId].expression,
1272
1239
  enabled: confCron.jobs[jobId].enabled,
1273
1240
  };
package/src/cli/index.js CHANGED
@@ -172,59 +172,7 @@ program
172
172
  .option('--dev', 'Sets the development cli context')
173
173
 
174
174
  .description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
175
- .action((options) => {
176
- // Handle config template generation
177
- if (options.generateConfig) {
178
- const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
179
- return UnderpostStatic.API.generateConfigTemplate(configPath);
180
- }
181
-
182
- // Parse comma-separated options
183
- if (options.keywords) {
184
- options.keywords = options.keywords.split(',').map((k) => k.trim());
185
- }
186
- if (options.headScripts) {
187
- options.scripts = options.scripts || {};
188
- options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
189
- }
190
- if (options.bodyScripts) {
191
- options.scripts = options.scripts || {};
192
- options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
193
- }
194
- if (options.styles) {
195
- options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
196
- }
197
- if (options.headComponents) {
198
- options.headComponents = options.headComponents.split(',').map((c) => c.trim());
199
- }
200
- if (options.bodyComponents) {
201
- options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
202
- }
203
-
204
- // Build metadata object from individual options
205
- options.metadata = {
206
- ...(options.title && { title: options.title }),
207
- ...(options.description && { description: options.description }),
208
- ...(options.keywords && { keywords: options.keywords }),
209
- ...(options.author && { author: options.author }),
210
- ...(options.themeColor && { themeColor: options.themeColor }),
211
- ...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
212
- ...(options.thumbnail && { thumbnail: options.thumbnail }),
213
- ...(options.locale && { locale: options.locale }),
214
- ...(options.siteName && { siteName: options.siteName }),
215
- };
216
-
217
- // Build icons object
218
- if (options.favicon || options.appleTouchIcon || options.manifest) {
219
- options.icons = {
220
- ...(options.favicon && { favicon: options.favicon }),
221
- ...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
222
- ...(options.manifest && { manifest: options.manifest }),
223
- };
224
- }
225
-
226
- return UnderpostStatic.API.callback(options);
227
- });
175
+ .action(UnderpostStatic.API.callback);
228
176
 
229
177
  // 'config' command: Manage Underpost configurations
230
178
  program
@@ -417,8 +365,6 @@ program
417
365
  '--pod-name <pod-name>',
418
366
  'Comma-separated list of pod names or patterns (supports wildcards like "mariadb-*").',
419
367
  )
420
- .option('--node-name <node-name>', 'Comma-separated list of node names to filter pods by their node placement.')
421
- .option('--label-selector <selector>', 'Kubernetes label selector for filtering pods (e.g., "app=mariadb").')
422
368
  .option('--all-pods', 'Target all matching pods instead of just the first one.')
423
369
  .option('--primary-pod', 'Automatically detect and use MongoDB primary pod (MongoDB only).')
424
370
  .option('--stats', 'Display database statistics (collection/table names with document/row counts).')
@@ -479,9 +425,9 @@ program
479
425
  ', ',
480
426
  )}. Defaults to all available jobs.`,
481
427
  )
482
- .option('--itc', 'Executes cron jobs within the container execution context.')
483
- .option('--init', 'Initializes cron jobs for the default deployment ID.')
428
+ .option('--init-pm2-cronjobs', 'Initializes PM2 cron jobs from configuration for the specified deployment IDs.')
484
429
  .option('--git', 'Uploads cron job configurations to GitHub.')
430
+ .option('--update-package-scripts', 'Updates package.json start scripts for each deploy-id configuration.')
485
431
  .description('Manages cron jobs, including initialization, execution, and configuration updates.')
486
432
  .action(Underpost.cron.callback);
487
433
 
@@ -670,7 +616,6 @@ program
670
616
  .option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
671
617
  .option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
672
618
  .option('--cloud-init-update', 'Updates cloud init for a workflow id config architecture.')
673
- .option('--cloud-init-reset', 'Resets cloud init for a workflow id config architecture.')
674
619
  .option('--logs <log-id>', 'Displays logs for log id: dhcp, cloud, machine, cloud-config.')
675
620
  .option('--dev', 'Sets the development context environment for baremetal operations.')
676
621
  .option('--ls', 'Lists available boot resources and machines.')
@@ -587,6 +587,20 @@ Prevent build private config repo.`,
587
587
  fs.writeFileSync(targetConfPath, confRawPaths.join(sepRender), 'utf8');
588
588
  shellExec(`prettier --write ${targetConfPath}`);
589
589
  },
590
+
591
+ /**
592
+ * Cleans the specified paths in the repository by resetting, checking out, and cleaning untracked files.
593
+ * @param {object} [options={ paths: [''] }] - The options for cleaning.
594
+ * @param {string[]} [options.paths=['']] - The paths to clean.
595
+ * @memberof UnderpostRepository
596
+ */
597
+ clean(options = { paths: [''] }) {
598
+ for (const path of options.paths) {
599
+ shellExec(`cd ${path} && git reset`, { silent: true });
600
+ shellExec(`cd ${path} && git checkout .`, { silent: true });
601
+ shellExec(`cd ${path} && git clean -f -d`, { silent: true });
602
+ }
603
+ },
590
604
  };
591
605
  }
592
606
 
package/src/cli/run.js CHANGED
@@ -446,8 +446,7 @@ class UnderpostRun {
446
446
  * @memberof UnderpostRun
447
447
  */
448
448
  clean: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
449
- shellCd(path ? path : `/home/dd/engine`);
450
- shellExec(`node bin/deploy clean-core-repo`);
449
+ Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
451
450
  },
452
451
  /**
453
452
  * @method pull
@@ -1187,7 +1186,7 @@ EOF
1187
1186
  }
1188
1187
  await timer(5000);
1189
1188
  for (const deployId of deployList) {
1190
- shellExec(`${baseCommand} db ${deployId} --import --git`);
1189
+ shellExec(`${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod`);
1191
1190
  }
1192
1191
  await timer(5000);
1193
1192
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --valkey`);