nitor 1.2.2 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -30,3 +30,11 @@ For a complete list of changes and discussion, see [Issue #1](https://github.com
30
30
  - Added time entry commands.
31
31
 
32
32
  - [Issue #5](https://github.com/codebynithin/nitor/issues/5)
33
+
34
+ ## [1.3.0] - 2025-11-29
35
+
36
+ ### New Features
37
+
38
+ - Added support for task stats and get task commands.
39
+
40
+ - [Issue #7](https://github.com/codebynithin/nitor/issues/7)
package/README.md CHANGED
@@ -26,6 +26,9 @@ A CLI utility toolkit for automating and managing build, deploy, and status oper
26
26
  - Cleanup local git branches
27
27
  - Time entry management with Zoho integration
28
28
  - GitLab activity tracking
29
+ - Task statistics with merge request details
30
+ - Extract Zoho task IDs from GitLab issue descriptions
31
+ - Merge request status tracking for active tasks
29
32
 
30
33
  ## Requirements
31
34
 
@@ -36,6 +39,7 @@ A CLI utility toolkit for automating and managing build, deploy, and status oper
36
39
  - `COOKIE` - Cookie for Gitlab (Copy from browser)
37
40
  - `GITLAB_URI` - GitLab API URL, eg: `https://gitlab.com/`
38
41
  - `GITLAB_TOKEN` - Gitlab token
42
+ - `GITLAB_DEFAULT_PROJECT_ID` - Default GitLab project ID for issue lookups
39
43
  - `MR_PROMPT` - Merge request prompt
40
44
  - `MR_LANG` - Merge request language
41
45
  - `AI_API_KEY` - AI API key
@@ -155,6 +159,26 @@ Once enabled, you can use Tab to autocomplete:
155
159
  ```bash
156
160
  nitor time-zoho
157
161
  ```
162
+ - **Time Entry - GitLab Activities:**
163
+ ```bash
164
+ nitor time-gitlab -from <YYYY-MM-DD> -to <YYYY-MM-DD>
165
+ ```
166
+ - **Time Entry - Merge Request Status:**
167
+ ```bash
168
+ nitor time-merge
169
+ ```
170
+ - **Time Entry - Switch Project:**
171
+ ```bash
172
+ nitor time-switch
173
+ ```
174
+ - **Task Stats:**
175
+ ```bash
176
+ nitor task-stats -task <task numbers with space>
177
+ ```
178
+ - **Get Task (Extract Zoho Task IDs):**
179
+ ```bash
180
+ nitor get-task -task <task numbers with space>
181
+ ```
158
182
 
159
183
  ### Command Reference
160
184
 
@@ -178,6 +202,11 @@ Once enabled, you can use Tab to autocomplete:
178
202
  - `time-entries` : View time entries by date range
179
203
  - `time-stats` : View daily statistics of time entries
180
204
  - `time-zoho` : Sync time entries to Zoho
205
+ - `time-gitlab` : Get GitLab activities for a date range
206
+ - `time-merge` : View merge request status for active tasks
207
+ - `time-switch` : Switch between default projects
208
+ - `task-stats` : View task statistics with GitLab merge request details
209
+ - `get-task` : Extract Zoho task IDs from GitLab issue descriptions
181
210
 
182
211
  ### Options
183
212
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitor",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "A comprehensive CLI toolkit for automating GitLab operations, AI-powered code review, build/deploy automation, MongoDB backup/restore, and developer productivity tools",
5
5
  "main": "index.js",
6
6
  "author": "Nithin V <mails2nithin@gmail.com>",
@@ -1,26 +1,35 @@
1
1
  const { execSync } = require('child_process');
2
+ const os = require('os');
3
+ const isWindows = os.platform() === 'win32';
2
4
 
3
5
  const cleanup = async () => {
4
6
  try {
5
7
  console.log('Starting cleanup process...\n');
6
8
 
9
+ const execOptions = { encoding: 'utf-8', shell: isWindows ? 'cmd.exe' : '/bin/sh' };
10
+ const execOptionsInherit = {
11
+ encoding: 'utf-8',
12
+ stdio: 'inherit',
13
+ shell: isWindows ? 'cmd.exe' : '/bin/sh',
14
+ };
15
+
7
16
  // Get current branch
8
- const currentBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
17
+ const currentBranch = execSync('git branch --show-current', execOptions).trim();
9
18
  console.log(`Current branch: ${currentBranch}`);
10
19
 
11
20
  // Checkout to master
12
21
  console.log('\nChecking out to master branch...');
13
22
  try {
14
- execSync('git checkout master', { encoding: 'utf-8', stdio: 'inherit' });
23
+ execSync('git checkout master', execOptionsInherit);
15
24
  } catch (error) {
16
25
  // Try main if master doesn't exist
17
26
  console.log('Master branch not found, trying main...');
18
- execSync('git checkout main', { encoding: 'utf-8', stdio: 'inherit' });
27
+ execSync('git checkout main', execOptionsInherit);
19
28
  }
20
29
 
21
30
  // Get all local branches except master/main
22
31
  console.log('\nFetching list of local branches...');
23
- const branches = execSync('git branch', { encoding: 'utf-8' })
32
+ const branches = execSync('git branch', execOptions)
24
33
  .split('\n')
25
34
  .map((branch) => branch.trim().replace('* ', ''))
26
35
  .filter((branch) => branch && branch !== 'master' && branch !== 'main');
@@ -39,7 +48,7 @@ const cleanup = async () => {
39
48
 
40
49
  for (const branch of branches) {
41
50
  try {
42
- execSync(`git branch -D ${branch}`, { encoding: 'utf-8' });
51
+ execSync(`git branch -D ${branch}`, execOptions);
43
52
  console.log(` ✓ Deleted: ${branch}`);
44
53
  deletedCount++;
45
54
  } catch (error) {
@@ -1,4 +1,6 @@
1
1
  const { execSync } = require('child_process');
2
+ const os = require('os');
3
+ const isWindows = os.platform() === 'win32';
2
4
 
3
5
  const createBranch = async (values) => {
4
6
  if (!values.task || !values.type || !values.description) {
@@ -20,7 +22,10 @@ const createBranch = async (values) => {
20
22
  const branchName = `${values.project}-${values.task}-${values.type}-${values.description.toLowerCase().replace(/\s/g, '-')}`;
21
23
 
22
24
  try {
23
- execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
25
+ execSync(`git checkout -b ${branchName}`, {
26
+ stdio: 'inherit',
27
+ shell: isWindows ? 'cmd.exe' : '/bin/sh',
28
+ });
24
29
  } catch (error) {
25
30
  console.error('Error creating branch:', error.message);
26
31
  }
@@ -11,6 +11,8 @@ module.exports = {
11
11
  REFACTOR: 'refactor',
12
12
  REVIEW: 'review',
13
13
  VERSION: 'version',
14
+ TASK_STATS: 'task-stats',
15
+ GET_TASK: 'get-task',
14
16
 
15
17
  // Time entry commands
16
18
  TIME_INIT: 'time-init',
package/services/merge.js CHANGED
@@ -1,4 +1,6 @@
1
1
  const { execSync } = require('child_process');
2
+ const os = require('os');
3
+ const isWindows = os.platform() === 'win32';
2
4
 
3
5
  const merge = async (values) => {
4
6
  const { source, target } = values;
@@ -12,16 +14,18 @@ const merge = async (values) => {
12
14
  try {
13
15
  console.log('Starting merge process...\n');
14
16
 
15
- execSync('git checkout master', { stdio: 'inherit' });
16
- execSync('git pull', { stdio: 'inherit' });
17
- execSync(`git checkout ${source}`, { stdio: 'inherit' });
18
- execSync('git pull', { stdio: 'inherit' });
17
+ const execOptions = { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' };
18
+
19
+ execSync('git checkout master', execOptions);
20
+ execSync('git pull', execOptions);
21
+ execSync(`git checkout ${source}`, execOptions);
22
+ execSync('git pull', execOptions);
19
23
 
20
24
  for (const branch of target.split(' ')) {
21
- execSync(`git checkout ${branch}`, { stdio: 'inherit' });
22
- execSync('git pull', { stdio: 'inherit' });
23
- execSync(`git merge ${source} --no-edit`, { stdio: 'inherit' });
24
- execSync('git push', { stdio: 'inherit' });
25
+ execSync(`git checkout ${branch}`, execOptions);
26
+ execSync('git pull', execOptions);
27
+ execSync(`git merge ${source} --no-edit`, execOptions);
28
+ execSync('git push', execOptions);
25
29
  }
26
30
 
27
31
  console.log('\n✓ Merge completed successfully!');
@@ -1,7 +1,8 @@
1
1
  const { execSync } = require('child_process');
2
2
  const os = require('os');
3
+ const path = require('path');
3
4
  const { backupConfig, restoreConfig } = require('./utils');
4
-
5
+ const isWindows = os.platform() === 'win32';
5
6
  // ANSI color codes
6
7
  const colors = {
7
8
  red: '\x1b[31m',
@@ -12,6 +13,7 @@ const colors = {
12
13
  /**
13
14
  * Execute MongoDB restore from local backup to Docker container
14
15
  * @param {Object} config - Restore configuration
16
+ * @param {boolean} config.mongoInDocker - If mongodb running outside of docker
15
17
  * @param {string} config.containerName - Name of the Docker container (default: 'mongodb')
16
18
  * @param {string} config.localBackupPath - Local path where backup is stored (default: '~/backups/mongo')
17
19
  * @param {string} config.containerBackupPath - Path inside container for backup (default: '/data/backup')
@@ -19,37 +21,60 @@ const colors = {
19
21
  const executeMongoRestore = async (config) => {
20
22
  const {
21
23
  containerName = 'mongodb',
22
- localBackupPath = os.homedir() + '/backups/mongo',
24
+ localBackupPath = path.join(os.homedir(), 'backups', 'mongo'),
23
25
  containerBackupPath = '/data/backup',
24
26
  } = config;
25
27
 
26
28
  try {
27
- const expandedLocalPath = localBackupPath.replace('~', os.homedir());
28
-
29
- console.log(`\nStarting MongoDB restore to container: ${containerName}`);
30
-
31
- // Step 1: Copy backup from local to Docker container
32
- const copyCommand = `docker cp ${expandedLocalPath} ${containerName}:${containerBackupPath}`;
33
-
34
- console.log(`Copying backup to container...`);
35
- execSync(copyCommand, { stdio: 'inherit' });
36
- console.log(`${colors.green}✓ Backup copied to container${colors.reset}`);
37
-
38
- // Step 2: Execute mongorestore inside the container
39
- const restoreCommand = `docker exec -it ${containerName} mongorestore ${containerBackupPath}`;
40
-
41
- console.log('Executing mongorestore...');
42
- execSync(restoreCommand, { stdio: 'inherit' });
43
- console.log(`${colors.green}✓ Mongorestore completed successfully${colors.reset}`);
44
-
45
- // Step 3: Remove backup data from container
46
- const cleanupCommand = `docker exec -it ${containerName} rm -rf ${containerBackupPath}`;
47
-
48
- console.log('Cleaning up backup data from container...');
49
- execSync(cleanupCommand, { stdio: 'inherit' });
50
- console.log(`${colors.green}✓ Backup data removed from container${colors.reset}`);
51
-
52
- console.log(`${colors.green}Restore completed successfully!${colors.reset}\n`);
29
+ const expandedLocalPath = localBackupPath.startsWith('~')
30
+ ? localBackupPath.replace('~', os.homedir())
31
+ : localBackupPath;
32
+ const normalizedLocalPath = path.normalize(expandedLocalPath);
33
+
34
+ if (config.mongoInDocker) {
35
+ console.log(`\nStarting MongoDB restore to container: ${containerName}`);
36
+
37
+ // Step 1: Copy backup from local to Docker container
38
+ // Use normalized path for Windows compatibility
39
+ const copyCommand = `docker cp "${normalizedLocalPath}" ${containerName}:${containerBackupPath}`;
40
+
41
+ console.log(`Copying backup to container...`);
42
+ execSync(copyCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
43
+ console.log(`${colors.green}✓ Backup copied to container${colors.reset}`);
44
+
45
+ // Step 2: Execute mongorestore inside the container
46
+ // Remove -it flag for Windows compatibility (causes issues in non-interactive shells)
47
+ const restoreCommand = isWindows
48
+ ? `docker exec ${containerName} mongorestore ${containerBackupPath}`
49
+ : `docker exec -it ${containerName} mongorestore ${containerBackupPath}`;
50
+
51
+ console.log('Executing mongorestore...');
52
+ execSync(restoreCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
53
+ console.log(`${colors.green}✓ Mongorestore completed successfully${colors.reset}`);
54
+
55
+ // Step 3: Remove backup data from container
56
+ const cleanupCommand = isWindows
57
+ ? `docker exec ${containerName} rm -rf ${containerBackupPath}`
58
+ : `docker exec -it ${containerName} rm -rf ${containerBackupPath}`;
59
+
60
+ console.log('Cleaning up backup data from container...');
61
+ execSync(cleanupCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
62
+ console.log(`${colors.green}✓ Backup data removed from container${colors.reset}`);
63
+
64
+ console.log(`${colors.green}Restore completed successfully!${colors.reset}\n`);
65
+ } else {
66
+ console.log(`\nStarting MongoDB restore to local machine`);
67
+
68
+ console.log('Executing mongorestore...');
69
+ const localRestoreCommand = `mongorestore --uri="mongodb://localhost:27017/" "${normalizedLocalPath}"`;
70
+ execSync(localRestoreCommand, {
71
+ stdio: 'inherit',
72
+ shell: isWindows ? 'cmd.exe' : '/bin/sh',
73
+ });
74
+ console.log(`${colors.green}✓ Mongorestore completed successfully${colors.reset}`);
75
+
76
+ console.log(`${colors.green}Restore completed successfully!${colors.reset}\n`);
77
+ }
53
78
 
54
79
  return { success: true };
55
80
  } catch (error) {
@@ -75,29 +100,40 @@ const executeMongoBackup = async (config) => {
75
100
  password,
76
101
  database,
77
102
  backupPath = '/data/backup',
78
- localBackupPath = '~/backups/mongo/',
103
+ localBackupPath = path.join(os.homedir(), 'backups', 'mongo'),
79
104
  } = config;
80
105
 
81
106
  try {
82
107
  console.log(`\nStarting MongoDB backup for pod: ${pod}, database: ${database}`);
83
108
 
84
- const mongodumpCommand = `kubectl exec -it ${pod} -- mongodump --username ${username} --password ${password} --authenticationDatabase ${database} --db ${database} --out ${backupPath}`;
109
+ // Remove -it flag for Windows compatibility
110
+ const mongodumpCommand = isWindows
111
+ ? `kubectl exec ${pod} -- mongodump --username ${username} --password ${password} --authenticationDatabase ${database} --db ${database} --out ${backupPath}`
112
+ : `kubectl exec -it ${pod} -- mongodump --username ${username} --password ${password} --authenticationDatabase ${database} --db ${database} --out ${backupPath}`;
85
113
 
86
114
  console.log('Executing mongodump...');
87
- execSync(mongodumpCommand, { stdio: 'inherit' });
115
+ execSync(mongodumpCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
88
116
  console.log(`${colors.green}✓ Mongodump completed successfully${colors.reset}`);
89
117
 
90
- const expandedLocalPath = localBackupPath.replace('~', os.homedir());
91
- const destinationPath = `${expandedLocalPath}${database}`;
92
- const copyCommand = `kubectl cp ${pod}:${backupPath}/${database} ${destinationPath}`;
93
- const cleanupCommand = `kubectl exec -it ${pod} -- rm -rf ${backupPath}`;
118
+ // Expand and normalize paths for cross-platform compatibility
119
+ const expandedLocalPath = localBackupPath.startsWith('~')
120
+ ? localBackupPath.replace('~', os.homedir())
121
+ : localBackupPath;
122
+ const normalizedBasePath = path.normalize(expandedLocalPath);
123
+ const destinationPath = path.join(normalizedBasePath, database);
124
+
125
+ // Use quotes for paths with spaces on Windows
126
+ const copyCommand = `kubectl cp ${pod}:${backupPath}/${database} "${destinationPath}"`;
127
+ const cleanupCommand = isWindows
128
+ ? `kubectl exec ${pod} -- rm -rf ${backupPath}`
129
+ : `kubectl exec -it ${pod} -- rm -rf ${backupPath}`;
94
130
 
95
131
  console.log(`Copying backup to ${destinationPath}...`);
96
- execSync(copyCommand, { stdio: 'inherit' });
132
+ execSync(copyCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
97
133
  console.log(`${colors.green}✓ Backup copied successfully${colors.reset}`);
98
134
 
99
135
  console.log('Cleaning up backup data from pod...');
100
- execSync(cleanupCommand, { stdio: 'inherit' });
136
+ execSync(cleanupCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
101
137
  console.log(`${colors.green}✓ Backup data removed from pod${colors.reset}`);
102
138
 
103
139
  console.log(
@@ -159,6 +195,8 @@ const backup = async (values) => {
159
195
  console.log(`Starting restore process for projects: ${projects.join(', ')}`);
160
196
  console.log(`========================================`);
161
197
 
198
+ restoreConfig.mongoInDocker = !!values?.docker;
199
+
162
200
  await executeMongoRestore(restoreConfig);
163
201
 
164
202
  console.log('\n========================================');
@@ -18,25 +18,19 @@ const { addNewTask } = require('./time-entry/add-task');
18
18
  const { updateTask, deleteTask } = require('./time-entry/update-delete-task');
19
19
  const { getGitlabActivities } = require('./time-entry/get-gitlab-activities');
20
20
  const { getZohoTasks } = require('./time-entry/get-zoho-tasks');
21
+ const {
22
+ getTaskStats,
23
+ getGitIdStats,
24
+ getGitlabIssueMergeRequests,
25
+ getGitMergeRequestDetails,
26
+ } = require('./task-stats');
21
27
 
22
28
  const processArgs = async (type, value) => {
23
29
  try {
24
30
  let values;
25
31
 
26
32
  // Skip convertParamsToMap for time commands and other special commands
27
- const skipConversion = [
28
- ACTIONS.HELP,
29
- ACTIONS.VERSION,
30
- ACTIONS.CLEANUP,
31
- ACTIONS.TIME_ADD,
32
- ACTIONS.TIME_UPDATE,
33
- ACTIONS.TIME_DELETE,
34
- ACTIONS.TIME_STATUS,
35
- ACTIONS.TIME_ENTRIES,
36
- ACTIONS.TIME_ZOHO,
37
- ACTIONS.TIME_GITLAB,
38
- ACTIONS.TIME_MERGE,
39
- ];
33
+ const skipConversion = [ACTIONS.HELP, ACTIONS.VERSION, ACTIONS.CLEANUP];
40
34
 
41
35
  if (!skipConversion.includes(type) && !value?.includes('--h') && !value?.includes('-help')) {
42
36
  values = await convertParamsToMap(value, type);
@@ -292,6 +286,88 @@ ${cyan}${bold}╔═════════════════════
292
286
  break;
293
287
  }
294
288
 
289
+ case ACTIONS.TASK_STATS: {
290
+ if (value === '-help' || value === '--h') {
291
+ console.log(`usage: \tnu task-stats [-task <task number>]
292
+ \tnu task-stats [-t <task number>]
293
+
294
+ View task stats
295
+
296
+ Options:
297
+ -t, --task <number> task number`);
298
+
299
+ return;
300
+ }
301
+
302
+ if (!values.task) {
303
+ console.log('Task number is required');
304
+
305
+ process.exit(1);
306
+ }
307
+
308
+ const list = [];
309
+
310
+ for (const task of values.task.split(' ')) {
311
+ const taskDetail = await getTaskStats(task);
312
+ const gitIdDetails = await getGitIdStats(taskDetail.itemIds);
313
+ const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
314
+
315
+ for (const mrDetail of mrDetails) {
316
+ list.push({
317
+ Task: task,
318
+ Owner: taskDetail.owner,
319
+ ...mrDetail,
320
+ });
321
+ }
322
+ }
323
+
324
+ console.table(list);
325
+
326
+ break;
327
+ }
328
+
329
+ case ACTIONS.GET_TASK: {
330
+ if (value === '-help' || value === '--h') {
331
+ console.log(`usage: \tnu get-task [-task <task numbers with space>]
332
+ \tnu get-task [-t <task numbers with space>]
333
+
334
+ Get task details
335
+
336
+ Options:
337
+ -t, --task <numbers with space> task numbers with space`);
338
+
339
+ return;
340
+ }
341
+
342
+ if (!values.task) {
343
+ console.log('Task number is required');
344
+
345
+ process.exit(1);
346
+ }
347
+
348
+ const tasks = {};
349
+
350
+ for (const task of values.task.split(' ')) {
351
+ const taskDetails = await getGitMergeRequestDetails(task);
352
+
353
+ if (taskDetails.error) {
354
+ console.log(`No merge requests found for task ${task}`);
355
+
356
+ continue;
357
+ }
358
+
359
+ for (const taskDetail of taskDetails) {
360
+ const zohoTaskMatch = taskDetail.description?.match(/itemdetails\/(I\d+)\)/);
361
+
362
+ tasks[task] = zohoTaskMatch ? zohoTaskMatch[1] : 'Not found';
363
+ }
364
+ }
365
+
366
+ console.log(tasks);
367
+
368
+ break;
369
+ }
370
+
295
371
  // Time entry commands
296
372
  case ACTIONS.TIME_ADD: {
297
373
  const timeValues = value ? removeEmpty(value?.split(' -')) : value;
@@ -0,0 +1,197 @@
1
+ const axios = require('axios');
2
+ const { zohoConfig, gitlabConfig } = require('./utils');
3
+
4
+ const getZohoUrl = (type, taskNumber) => {
5
+ switch (type) {
6
+ case 'task':
7
+ return `${zohoConfig.url}/zsapi/team/${zohoConfig.team}/projects/${zohoConfig.defaultProjectId}/item/no-${taskNumber}/?action=details`;
8
+
9
+ case 'gitId':
10
+ return `${zohoConfig.url}/zsapi/team/${zohoConfig.team}/projects/${zohoConfig.defaultProjectId}/item/${taskNumber}/scm/3/issuedetails/?action=scmissuedetails`;
11
+ }
12
+ };
13
+
14
+ const getZohoConfig = (type, taskNumber) => {
15
+ return {
16
+ method: 'get',
17
+ maxBodyLength: Infinity,
18
+ url: getZohoUrl(type, taskNumber),
19
+ headers: {
20
+ 'User-Agent':
21
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0',
22
+ Accept: '*/*',
23
+ 'Accept-Language': 'en-GB,en;q=0.5',
24
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
25
+ Referer: `${zohoConfig.url}/workspace/4medica/client/wmoku`,
26
+ 'X-ZA-REQSIZE': 'large',
27
+ 'X-Requested-With': 'XMLHttpRequest',
28
+ 'X-ZA-SOURCE': zohoConfig.source,
29
+ 'X-ZA-UI-VERSION': 'v2',
30
+ 'X-ZCSRF-TOKEN': zohoConfig.token,
31
+ 'X-ZA-CLIENTPORTALID': zohoConfig.portalId,
32
+ 'X-ZA-SESSIONID': zohoConfig.sessionId,
33
+ Connection: 'keep-alive',
34
+ Cookie: zohoConfig.cookie,
35
+ 'Sec-Fetch-Dest': 'empty',
36
+ 'Sec-Fetch-Mode': 'cors',
37
+ 'Sec-Fetch-Site': 'same-origin',
38
+ Priority: 'u=0',
39
+ Pragma: 'no-cache',
40
+ 'Cache-Control': 'no-cache',
41
+ TE: 'trailers',
42
+ },
43
+ };
44
+ };
45
+
46
+ const getGitlabConfig = (url) => {
47
+ return {
48
+ method: 'get',
49
+ maxBodyLength: Infinity,
50
+ url,
51
+ headers: {
52
+ 'User-Agent':
53
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0',
54
+ Accept: 'application/json',
55
+ 'Accept-Language': 'en-GB,en;q=0.5',
56
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
57
+ 'PRIVATE-TOKEN': gitlabConfig.token,
58
+ Connection: 'keep-alive',
59
+ Cookie: gitlabConfig.cookie,
60
+ },
61
+ };
62
+ };
63
+
64
+ const getTaskStats = async (task) => {
65
+ try {
66
+ const config = getZohoConfig('task', task);
67
+ const response = await axios.request(config);
68
+
69
+ return {
70
+ itemIds: response.data.itemIds,
71
+ owner: Object.values(response.data.userDisplayName).join(', '),
72
+ };
73
+ } catch (error) {
74
+ console.log(error);
75
+ }
76
+ };
77
+
78
+ const getGitIdStats = async (gitIds) => {
79
+ const gitIdStats = [];
80
+
81
+ for (const gitId of gitIds) {
82
+ try {
83
+ const config = getZohoConfig('gitId', gitId);
84
+ const response = await axios.request(config);
85
+
86
+ gitIdStats.push(response.data);
87
+ } catch (error) {
88
+ console.log(error);
89
+ }
90
+ }
91
+
92
+ return gitIdStats;
93
+ };
94
+
95
+ const getGitlabIssueDetails = async (projectId, issueIid) => {
96
+ try {
97
+ const config = getGitlabConfig(`${projectId}/issues/${issueIid}`);
98
+ const response = await axios.request(config);
99
+
100
+ return response.data;
101
+ } catch (error) {
102
+ console.error('Error fetching GitLab issue details:', error.message);
103
+ if (error.response) {
104
+ console.error('Response status:', error.response.status);
105
+ console.error('Response data:', error.response.data);
106
+ }
107
+ throw error;
108
+ }
109
+ };
110
+
111
+ const getGitMergeRequestDetails = async (issueIid, projectId) => {
112
+ try {
113
+ if (!projectId) {
114
+ projectId = `${gitlabConfig.url}/api/v4/projects/${gitlabConfig.defaultProjectId}`;
115
+ }
116
+
117
+ const config = getGitlabConfig(`${projectId}/issues/${issueIid}/related_merge_requests`);
118
+ const response = await axios.request(config);
119
+
120
+ return response.data;
121
+ } catch (error) {
122
+ return { error };
123
+ }
124
+ };
125
+
126
+ const getGitlabIssueMergeRequests = async (gitDetails) => {
127
+ for (const gitDetail of gitDetails) {
128
+ try {
129
+ const mergeRequests = await getGitMergeRequestDetails(
130
+ gitDetail.iid,
131
+ gitDetail._links.project,
132
+ );
133
+ const detailedMRs = await Promise.all(
134
+ mergeRequests.map(async (mr) => {
135
+ try {
136
+ const detailConfig = getGitlabConfig(
137
+ `${gitlabConfig.url}/api/v4/projects/${mr.project_id}/merge_requests/${mr.iid}`,
138
+ );
139
+ const detailResponse = await axios.request(detailConfig);
140
+ const mrData = detailResponse.data;
141
+
142
+ // Fetch approval details separately
143
+ try {
144
+ const approvalConfig = getGitlabConfig(
145
+ `${gitlabConfig.url}/api/v4/projects/${mr.project_id}/merge_requests/${mr.iid}/approvals`,
146
+ );
147
+ const approvalResponse = await axios.request(approvalConfig);
148
+ const approvalData = approvalResponse.data;
149
+
150
+ // Merge approval data with MR data
151
+ return {
152
+ IssueID: gitDetail.iid,
153
+ MRID: mrData.iid,
154
+ MRAssignedTo: mrData.assignee?.name,
155
+ Repo: mr.reference?.split('!')?.[0],
156
+ ApprovedBy:
157
+ (approvalData.approved_by || [])
158
+ .map((approvedBy) => approvedBy?.user?.name || '')
159
+ ?.join(', ') || 'Not Approved',
160
+ ApprovedCount: approvalData.approved_by?.length || 0,
161
+ MergedBy: mrData.merged_by?.name || 'Not Merged',
162
+ MergedOn: mrData.merged_at?.split('T')?.[0] || 'Not Merged',
163
+ };
164
+ } catch (approvalError) {
165
+ console.error(`Error fetching approvals for MR ${mr.iid}:`, approvalError.message);
166
+ // Return MR data without approval details if approval fetch fails
167
+ return mrData;
168
+ }
169
+ } catch (error) {
170
+ console.error(`Error fetching details for MR ${mr.iid}:`, error.message);
171
+ // Return the basic MR data if detailed fetch fails
172
+ return mr;
173
+ }
174
+ }),
175
+ );
176
+
177
+ return detailedMRs;
178
+ } catch (error) {
179
+ console.error('Error fetching GitLab issue merge requests:', error.message);
180
+
181
+ if (error.response) {
182
+ console.error('Response status:', error.response.status);
183
+ console.error('Response data:', error.response.data);
184
+ }
185
+
186
+ throw error;
187
+ }
188
+ }
189
+ };
190
+
191
+ module.exports = {
192
+ getTaskStats,
193
+ getGitIdStats,
194
+ getGitlabIssueDetails,
195
+ getGitlabIssueMergeRequests,
196
+ getGitMergeRequestDetails,
197
+ };
@@ -2,25 +2,61 @@ const axios = require('axios');
2
2
  const cheerio = require('cheerio');
3
3
  const { gitlabConfig } = require('../utils');
4
4
 
5
+ /**
6
+ * Fetches GitLab activities for a date range
7
+ * Defaults to last one week if no values are provided
8
+ * @param {Array} values - Optional array of filter values (e.g., ['-from 2024-11-21', '-to 2024-11-28'])
9
+ * @returns {Object} Object with dates as keys and activities grouped by action
10
+ */
5
11
  const getGitlabActivities = async (values) => {
6
12
  const resp = {};
13
+ const res = [];
7
14
  const filters = getFilters(values);
8
15
  const startDate = new Date(filters.from);
9
16
 
17
+ // Iterate through each date in the range
10
18
  while (startDate <= new Date(filters.to)) {
11
19
  const date = startDate.toISOString().split('T')[0];
12
20
  const activities = await getGitlabActivitiesByDate(date);
13
21
 
14
- if (activities?.length) {
22
+ /* if (activities?.length) {
15
23
  resp[date] = arrayToObjectByKey(activities, 'action');
16
- }
24
+ } */
25
+ res.push(...activities);
17
26
 
18
27
  startDate.setDate(startDate.getDate() + 1);
19
28
  }
20
29
 
21
- console.log(resp);
30
+ // Format and display each entry in a single line with ellipses
31
+ const truncate = (str, maxLen = 50) => {
32
+ if (!str) return '';
33
+ return str.length > maxLen ? str.substring(0, maxLen - 3) + '...' : str;
34
+ };
35
+
36
+ console.log('\n📊 GitLab Activities (Last Week)\n');
37
+ console.log(
38
+ `\n${'index'.toString().padStart(8)} | ${'date'.toString().padStart(10)} ${'time'.toString().padEnd(8) || ''.padEnd(8)} | ${'action'.toString().padEnd(18)} | ${'text'.toString().padEnd(100)} | ${'project'.toString().padEnd(25)}`,
39
+ );
40
+ res.forEach((activity, index) => {
41
+ const { date, time, action, text, project } = activity;
42
+ const formattedText = truncate(text, 100);
43
+ const formattedAction = action?.padEnd(18) || ''.padEnd(18);
44
+ const formattedProject = truncate(project, 25);
45
+
46
+ console.log(
47
+ `${(index + 1).toString().padStart(8)} | ${date} ${time?.padEnd(8) || ''.padEnd(8)} | ${formattedAction} | ${formattedText.padEnd(100)} | ${formattedProject}`,
48
+ );
49
+ });
50
+ console.log(`\n✅ Total: ${res.length} activities\n`);
51
+
22
52
  return resp;
23
53
  };
54
+
55
+ /**
56
+ * Parses filter values or sets default to last one week
57
+ * @param {Array} values - Array of filter strings
58
+ * @returns {Object} Object with 'from' and 'to' date strings
59
+ */
24
60
  const getFilters = (values) => {
25
61
  const filters = {};
26
62
  const keyMap = {
@@ -31,10 +67,12 @@ const getFilters = (values) => {
31
67
  };
32
68
 
33
69
  if (values?.length) {
70
+ // Parse provided filter values
34
71
  for (const item of values) {
35
72
  let [key, ...itemValues] = item.split(' ');
36
73
  const itemValue = itemValues.join(' ');
37
74
 
75
+ // Remove leading dash if present
38
76
  if (key.charAt(0) === '-') {
39
77
  key = key.substring(1);
40
78
  }
@@ -46,6 +84,7 @@ const getFilters = (values) => {
46
84
  }
47
85
  }
48
86
  } else {
87
+ // Default: last one week
49
88
  filters.from = new Date(new Date().setDate(new Date().getDate() - 7))
50
89
  .toISOString()
51
90
  .split('T')[0];
@@ -54,8 +93,13 @@ const getFilters = (values) => {
54
93
 
55
94
  return filters;
56
95
  };
96
+
97
+ /**
98
+ * Fetches GitLab activities for a specific date
99
+ * @param {string} date - Date in YYYY-MM-DD format
100
+ * @returns {Array} Array of activity objects
101
+ */
57
102
  const getGitlabActivitiesByDate = async (date) => {
58
- const { removeEmpty } = require('./utils');
59
103
  const gitlabUrl = `${gitlabConfig.url}/users/${gitlabConfig.userId}/calendar_activities?date=${date}`;
60
104
  const headers = {
61
105
  accept: 'application/json, text/plain, */*',
@@ -71,7 +115,6 @@ const getGitlabActivitiesByDate = async (date) => {
71
115
  'x-csrf-token': gitlabConfig.xCsrfToken,
72
116
  'x-requested-with': 'XMLHttpRequest',
73
117
  };
74
- // let config = { method: 'get', maxBodyLength: Infinity, url: `${gitlabUrl}2024-5-10`, headers };
75
118
 
76
119
  try {
77
120
  const response = await axios.get(gitlabUrl, { headers });
@@ -79,45 +122,39 @@ const getGitlabActivitiesByDate = async (date) => {
79
122
  const $ = cheerio.load(response.data);
80
123
 
81
124
  // Initialize an empty array to store the JSON objects
82
- let contributions = [];
125
+ const contributions = [];
83
126
 
84
127
  // Iterate over each list item
85
- $('ul.bordered-list li').each((i, element) => {
128
+ $('.event-item').each((i, element) => {
86
129
  const $element = $(element);
87
- const time = $element.find('span.js-localtime').text().trim();
88
- const action = $element
89
- .contents()
90
- .filter(function () {
91
- return this.nodeType === 3; // Text nodes
92
- })
93
- .text()
94
- .trim()
95
- .split(/\n/g)[0]
96
- .trim();
97
- const detailsArray = $element
98
- .find('a')
99
- .map((i, el) => {
100
- return {
101
- title: $(el).attr('title'),
102
- href: $(el).attr('href').split(/\/-\//g)[1],
103
- text: $(el).text().trim().replace(/!/g, ''),
104
- };
105
- })
106
- .get();
107
- const details = detailsArray.reduce((acc, item) => {
108
- if (item.title === 'medica-portal-client') {
109
- acc.repo = item.title;
110
- } else {
111
- acc = removeEmpty(item);
112
- }
113
-
114
- return acc;
115
- }, {});
130
+ const time = $element.find('.event-item-timestamp').text()?.trim();
131
+ const action = $element.find('.event-title-block .event-type').text()?.trim();
132
+ const project = $element.find('.event-title-block .project-name').text()?.trim();
133
+ const $refLink = $element.find('.event-title-block a.ref-name');
134
+ const $targetLink = $element.find('.event-title-block a.event-target-link');
135
+
136
+ let href, text;
137
+
138
+ if ($refLink.length > 0) {
139
+ href = $refLink.attr('href');
140
+ text = $refLink.text()?.trim();
141
+ } else if ($targetLink.length > 0) {
142
+ href = $targetLink.attr('href');
143
+ text = $targetLink.text()?.trim();
144
+ } else {
145
+ const $eventTypeSpan = $element.find('.event-title-block .event-type');
146
+ const nextSpan = $eventTypeSpan.next('span');
147
+
148
+ text = nextSpan.text()?.trim();
149
+ href = undefined;
150
+ }
116
151
 
117
152
  contributions.push({
118
153
  time,
119
154
  action,
120
- ...details,
155
+ text,
156
+ project,
157
+ date,
121
158
  });
122
159
  });
123
160
 
@@ -126,12 +163,5 @@ const getGitlabActivitiesByDate = async (date) => {
126
163
  console.log(error.message);
127
164
  }
128
165
  };
129
- const arrayToObjectByKey = (arr, key) => {
130
- return arr.reduce((acc, obj) => {
131
- acc[obj[key]] = obj;
132
-
133
- return acc;
134
- }, {});
135
- };
136
166
 
137
167
  module.exports = { getGitlabActivities };
package/services/utils.js CHANGED
@@ -17,6 +17,7 @@ const gitlabConfig = {
17
17
  xCsrfToken: process.env.GITLAB_XCSRF_TOKEN,
18
18
  userId: process.env.GITLAB_USERID,
19
19
  token: process.env.GITLAB_TOKEN,
20
+ defaultProjectId: process.env.GITLAB_DEFAULT_PROJECT_ID,
20
21
  };
21
22
  const zohoConfig = {
22
23
  cookie: process.env.ZOHO_COOKIE,
@@ -56,6 +57,8 @@ const keyMap = {
56
57
  so: 'source',
57
58
  target: 'target',
58
59
  ta: 'target',
60
+ docker: 'docker',
61
+ do: 'docker',
59
62
  };
60
63
  const projectMap = {
61
64
  portal: 'medica-portal',
@@ -297,18 +300,16 @@ const generateDeployConfigs = (values = {}) => {
297
300
  return { configs: removeEmpty(configs, true) };
298
301
  };
299
302
  const convertParamsToMap = async (item, type) => {
300
- const itemsToSkipCheck = [
301
- ACTIONS.CREATE_BRANCH,
302
- ACTIONS.HELP,
303
- ACTIONS.VERSION,
304
- ACTIONS.REFACTOR,
305
- ACTIONS.BACKUP,
303
+ const itemsToCheckLiveness = [
304
+ ACTIONS.BUILD_DEPLOY,
305
+ ACTIONS.BUILD,
306
+ ACTIONS.DEPLOY,
306
307
  ACTIONS.MERGE,
308
+ ACTIONS.REVIEW,
307
309
  ];
308
- const skipCheck = itemsToSkipCheck.includes(type);
309
310
  let live = false;
310
311
 
311
- if (!skipCheck) {
312
+ if (itemsToCheckLiveness.includes(type)) {
312
313
  if (!gitlabConfig.token) {
313
314
  console.log('Configurations are missing...!');
314
315
  return null;
@@ -1,16 +0,0 @@
1
- module.exports = {
2
- ACTIONS: {
3
- INIT: 'init',
4
- SWITCH: 'switch',
5
- ADD: 'add',
6
- UPDATE: 'update',
7
- DELETE: 'delete',
8
- STATUS: 'status',
9
- ENTRIES: 'entries',
10
- ZOHO: 'zoho',
11
- GITLAB: 'gitlab',
12
- MERGE: 'merge',
13
- VERSION: 'version',
14
- HELP: 'help',
15
- },
16
- };