nitor 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -115,14 +115,19 @@ Once enabled, you can use Tab to autocomplete:
115
115
  ```bash
116
116
  nitor refactor <text>
117
117
  ```
118
- - **Backup:**
119
- ```bash
120
- nitor backup <text>
121
- ```
122
- - **Restore:**
118
+ - **MongoDB Backup & Restore:**
119
+
123
120
  ```bash
124
- nitor restore <text>
121
+ # Backup from Kubernetes pods and restore to local MongoDB
122
+ nitor backup
123
+
124
+ # Backup specific projects only
125
+ nitor backup -project <project1> <project2>
126
+
127
+ # Restore to Docker container (default: local MongoDB)
128
+ nitor backup -docker <boolean>
125
129
  ```
130
+
126
131
  - **Merge:**
127
132
  ```bash
128
133
  nitor merge -source <source branch> -target <target branch>
@@ -191,8 +196,7 @@ Once enabled, you can use Tab to autocomplete:
191
196
  - `create-branch` : Create git branch
192
197
  - `review` : AI review specified merge request
193
198
  - `refactor` : AI refactor specified text
194
- - `backup` : Backup specified projects
195
- - `restore` : Restore specified projects
199
+ - `backup` : Backup MongoDB databases from Kubernetes pods and restore to local/Docker
196
200
  - `merge` : Merge source branch into target branch
197
201
  - `cleanup` : Cleanup local git branches (checkout to master and delete all other branches)
198
202
  - `time-init` : Initialize time entry configuration
@@ -208,6 +212,140 @@ Once enabled, you can use Tab to autocomplete:
208
212
  - `task-stats` : View task statistics with GitLab merge request details
209
213
  - `get-task` : Extract Zoho task IDs from GitLab issue descriptions
210
214
 
215
+ ## MongoDB Backup & Restore
216
+
217
+ ### Overview
218
+
219
+ The MongoDB backup service provides automated backup and restore functionality for MongoDB databases running in Kubernetes pods. It supports:
220
+
221
+ - ✅ Backup from Kubernetes pods using `kubectl`
222
+ - ✅ Restore to Docker containers or local MongoDB instances
223
+ - ✅ Cross-platform support (Windows, macOS, Linux)
224
+ - ✅ Multi-project configuration
225
+ - ✅ Automatic cleanup of temporary files
226
+ - ✅ Error handling with detailed logging
227
+
228
+ ### Configuration
229
+
230
+ Configure your backup settings in the `.env.nu` file using JSON format:
231
+
232
+ #### BACKUP_CONFIG
233
+
234
+ Defines the source databases to backup from Kubernetes pods:
235
+
236
+ ```json
237
+ BACKUP_CONFIG={
238
+ "project1": [
239
+ {
240
+ "pod": "mongodb-pod-name",
241
+ "username": "admin",
242
+ "password": "password123",
243
+ "database": "mydb",
244
+ "backupPath": "/data/backup",
245
+ "localBackupPath": "~/backups/mongo"
246
+ }
247
+ ],
248
+ "project2": [
249
+ {
250
+ "pod": "another-pod",
251
+ "username": "dbuser",
252
+ "password": "dbpass",
253
+ "database": "anotherdb"
254
+ }
255
+ ]
256
+ }
257
+ ```
258
+
259
+ **Configuration Options:**
260
+
261
+ - `pod` (required): Name of the Kubernetes pod
262
+ - `username` (required): MongoDB username
263
+ - `password` (required): MongoDB password
264
+ - `database` (required): Database name
265
+ - `backupPath` (optional): Path inside pod for temporary backup (default: `/data/backup`)
266
+ - `localBackupPath` (optional): Local path to store backups (default: `~/backups/mongo`)
267
+
268
+ #### RESTORE_CONFIG
269
+
270
+ Defines where to restore the backup:
271
+
272
+ ```json
273
+ RESTORE_CONFIG={
274
+ "mongoInDocker": false,
275
+ "containerName": "mongodb",
276
+ "localBackupPath": "~/backups/mongo",
277
+ "containerBackupPath": "/data/backup"
278
+ }
279
+ ```
280
+
281
+ **Configuration Options:**
282
+
283
+ - `mongoInDocker` (optional): Set to `true` to restore to Docker container, `false` for local MongoDB (can be overridden with `-docker` flag)
284
+ - `containerName` (optional): Docker container name (default: `mongodb`)
285
+ - `localBackupPath` (optional): Path where backup is stored (default: `~/backups/mongo`)
286
+ - `containerBackupPath` (optional): Path inside container for temporary files (default: `/data/backup`)
287
+
288
+ ### How It Works
289
+
290
+ 1. **Backup Phase:**
291
+
292
+ - Executes `mongodump` inside the Kubernetes pod
293
+ - Copies backup files to your local machine using `kubectl cp`
294
+ - Cleans up temporary files from the pod
295
+ - Saves to `~/backups/mongo/<database-name>` by default
296
+
297
+ 2. **Restore Phase:**
298
+ - **Docker Mode** (`-docker` flag):
299
+ - Copies backup from local machine to Docker container
300
+ - Executes `mongorestore --drop` inside the container
301
+ - Cleans up temporary files from container
302
+ - **Local Mode** (default):
303
+ - Executes `mongorestore --drop` directly on local machine
304
+ - Connects to `mongodb://localhost:27017/`
305
+
306
+ ### Usage Examples
307
+
308
+ **Backup all configured projects:**
309
+
310
+ ```bash
311
+ nitor backup
312
+ ```
313
+
314
+ **Backup specific projects:**
315
+
316
+ ```bash
317
+ nitor backup -project project1 project2
318
+ ```
319
+
320
+ **Backup and restore to Docker container:**
321
+
322
+ ```bash
323
+ nitor backup -docker
324
+ ```
325
+
326
+ ### Prerequisites
327
+
328
+ - **For Backup:** `kubectl` configured with access to your Kubernetes cluster
329
+ - **For Restore (Docker):** Docker running with MongoDB container
330
+ - **For Restore (Local):** MongoDB installed and running locally on port 27017
331
+ - MongoDB tools (`mongodump`, `mongorestore`) must be available in the pod/container
332
+
333
+ ### Cross-Platform Support
334
+
335
+ The backup service is designed to work on Windows, macOS, and Linux:
336
+
337
+ - Automatically detects the operating system
338
+ - Uses appropriate shell (`cmd.exe` on Windows, `/bin/sh` on Unix)
339
+ - Handles path normalization across platforms
340
+ - Adjusts command flags for Windows compatibility (e.g., removes `-it` flags)
341
+
342
+ ### Error Handling
343
+
344
+ - Non-zero exit codes from `mongorestore` are treated as warnings (common with duplicate keys)
345
+ - Detailed error messages with color-coded output
346
+ - Continues processing remaining projects even if one fails
347
+ - Cleanup operations run even if restore encounters errors
348
+
211
349
  ### Options
212
350
 
213
351
  - `-project` or `-p` : Project name (`portal`, `gateway`, `phr`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitor",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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>",
@@ -45,12 +45,18 @@ const executeMongoRestore = async (config) => {
45
45
  // Step 2: Execute mongorestore inside the container
46
46
  // Remove -it flag for Windows compatibility (causes issues in non-interactive shells)
47
47
  const restoreCommand = isWindows
48
- ? `docker exec ${containerName} mongorestore ${containerBackupPath}`
49
- : `docker exec -it ${containerName} mongorestore ${containerBackupPath}`;
48
+ ? `docker exec ${containerName} mongorestore --drop --stopOnError=false ${containerBackupPath}`
49
+ : `docker exec -it ${containerName} mongorestore --drop --stopOnError=false ${containerBackupPath}`;
50
50
 
51
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}`);
52
+ try {
53
+ execSync(restoreCommand, { stdio: 'inherit', shell: isWindows ? 'cmd.exe' : '/bin/sh' });
54
+ console.log(`${colors.green}✓ Mongorestore completed successfully${colors.reset}`);
55
+ } catch (error) {
56
+ console.log(
57
+ `${colors.red}⚠ Mongorestore completed with some errors (check output above for details)${colors.reset}`,
58
+ );
59
+ }
54
60
 
55
61
  // Step 3: Remove backup data from container
56
62
  const cleanupCommand = isWindows
@@ -66,12 +72,21 @@ const executeMongoRestore = async (config) => {
66
72
  console.log(`\nStarting MongoDB restore to local machine`);
67
73
 
68
74
  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
+ const localRestoreCommand = `mongorestore --drop --stopOnError=false --uri="mongodb://localhost:27017/" "${normalizedLocalPath}"`;
76
+ try {
77
+ execSync(localRestoreCommand, {
78
+ stdio: 'inherit',
79
+ shell: isWindows ? 'cmd.exe' : '/bin/sh',
80
+ });
81
+ console.log(`${colors.green}✓ Mongorestore completed successfully${colors.reset}`);
82
+ } catch (error) {
83
+ // mongorestore returns non-zero even for partial success (e.g., duplicate keys)
84
+ // Log the warning but don't throw unless it's a critical failure
85
+ console.log(
86
+ `${colors.red}⚠ Mongorestore completed with some errors (this may be expected if data already exists)${colors.reset}`,
87
+ );
88
+ console.log(`Check the output above for details`);
89
+ }
75
90
 
76
91
  console.log(`${colors.green}Restore completed successfully!${colors.reset}\n`);
77
92
  }
@@ -11,7 +11,7 @@ const { merge } = require('./merge');
11
11
  const { cleanup } = require('./cleanup');
12
12
 
13
13
  // Time entry imports
14
- const { removeEmpty } = require('./time-entry/utils');
14
+ const { getSprints } = require('./time-entry/utils');
15
15
  const { getStatus, getTasksByDate } = require('./time-entry/get-report');
16
16
  const { logTaskHoursAndSync } = require('./time-entry/log-task-hours-and-sync');
17
17
  const { addNewTask } = require('./time-entry/add-task');
@@ -199,7 +199,7 @@ Enhance the provided text for improved clarity, conciseness, and professional qu
199
199
  Backup mongodb for specified project and components.
200
200
 
201
201
  project list:
202
- - configCervice
202
+ - configService
203
203
  - medicaCentralAuth
204
204
  - medicaPortal
205
205
  - phr`);
@@ -299,27 +299,44 @@ Options:
299
299
  return;
300
300
  }
301
301
 
302
- if (!values.task) {
303
- console.log('Task number is required');
302
+ if (values.sprint) {
303
+ const sprints = await getSprints({ params: {} });
304
+ const taskDetails = await getZohoTasks({
305
+ params: { sprint: sprints.find(({ label }) => label === values.sprint)?.value },
306
+ });
304
307
 
305
- process.exit(1);
308
+ values.task = taskDetails.map(({ taskId }) => taskId);
309
+ } else {
310
+ values.task = values.task.split(' ');
306
311
  }
307
312
 
308
- const list = [];
313
+ // Process all tasks in parallel
314
+ const taskResults = await Promise.all(
315
+ values.task.map(async (task) => {
316
+ const taskDetail = await getTaskStats(task);
309
317
 
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);
318
+ if (!taskDetail?.itemIds) {
319
+ return [];
320
+ }
321
+
322
+ const gitIdDetails = await getGitIdStats(taskDetail.itemIds);
323
+
324
+ if (!gitIdDetails?.length) {
325
+ return [];
326
+ }
327
+
328
+ const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
314
329
 
315
- for (const mrDetail of mrDetails) {
316
- list.push({
330
+ return mrDetails.map((mrDetail) => ({
317
331
  Task: task,
318
332
  Owner: taskDetail.owner,
319
333
  ...mrDetail,
320
- });
321
- }
322
- }
334
+ }));
335
+ }),
336
+ );
337
+
338
+ // Flatten the results
339
+ const list = taskResults.flat();
323
340
 
324
341
  console.table(list);
325
342
 
@@ -370,7 +387,7 @@ Options:
370
387
 
371
388
  // Time entry commands
372
389
  case ACTIONS.TIME_ADD: {
373
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
390
+ const timeValues = value ? value.split(' -') : value;
374
391
 
375
392
  await addNewTask(timeValues);
376
393
 
@@ -378,7 +395,7 @@ Options:
378
395
  }
379
396
 
380
397
  case ACTIONS.TIME_UPDATE: {
381
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
398
+ const timeValues = value ? value.split(' -') : value;
382
399
 
383
400
  await updateTask(timeValues);
384
401
 
@@ -386,7 +403,7 @@ Options:
386
403
  }
387
404
 
388
405
  case ACTIONS.TIME_DELETE: {
389
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
406
+ const timeValues = value ? value.split(' -') : value;
390
407
 
391
408
  console.log(await deleteTask(timeValues));
392
409
 
@@ -394,7 +411,7 @@ Options:
394
411
  }
395
412
 
396
413
  case ACTIONS.TIME_STATUS: {
397
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
414
+ const timeValues = value ? value.split(' -') : value;
398
415
 
399
416
  await getStatus(timeValues);
400
417
 
@@ -402,7 +419,7 @@ Options:
402
419
  }
403
420
 
404
421
  case ACTIONS.TIME_ENTRIES: {
405
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
422
+ const timeValues = value ? value.split(' -') : value;
406
423
 
407
424
  console.table(await getTasksByDate(timeValues));
408
425
 
@@ -416,7 +433,7 @@ Options:
416
433
  }
417
434
 
418
435
  case ACTIONS.TIME_GITLAB: {
419
- const timeValues = value ? removeEmpty(value?.split(' -')) : value;
436
+ const timeValues = value ? value.split(' -') : value;
420
437
 
421
438
  await getGitlabActivities(timeValues);
422
439
 
@@ -488,6 +505,7 @@ Options:
488
505
  \t[${ACTIONS.BUILD}] [${ACTIONS.DEPLOY}] [${ACTIONS.BUILD_DEPLOY}]
489
506
  \t[${ACTIONS.CREATE_BRANCH}] [${ACTIONS.REVIEW}] [${ACTIONS.MERGE}]
490
507
  \t[${ACTIONS.CLEANUP}] [${ACTIONS.BACKUP}] [${ACTIONS.REFACTOR}]
508
+ \t[${ACTIONS.TASK_STATS}] [${ACTIONS.GET_TASK}]
491
509
  \t[${ACTIONS.TIME_INIT}] [${ACTIONS.TIME_ADD}] [${ACTIONS.TIME_STATUS}]\n
492
510
  Available commands:\n
493
511
  backup : Backup MongoDB databases
@@ -497,9 +515,11 @@ Available commands:\n
497
515
  completion : Setup shell autocomplete
498
516
  create-branch : Create git branch
499
517
  deploy : Deploy specified components
518
+ get-task : Get task details from GitLab merge requests
500
519
  merge : Merge source branch into target branch
501
520
  refactor : Refactor the provided text for improved clarity, conciseness, and professional quality
502
521
  review : AI Review specified merge request
522
+ task-stats : View task statistics and merge request details
503
523
  version : Show version info
504
524
  help : Show help
505
525
 
@@ -510,7 +530,7 @@ Time Entry commands:
510
530
  time-entries : View time entries by date
511
531
  time-gitlab : Get GitLab activities
512
532
  time-merge : View merge request status for tasks
513
- time-stats : View daily status of time entries
533
+ time-stats : View daily status of time entries
514
534
  time-switch : Switch between default projects
515
535
  time-update : Update existing time entry
516
536
  time-zoho : Sync time entries to Zoho
@@ -523,9 +543,11 @@ Example usage:\n
523
543
  nitor cleanup
524
544
  nitor create-branch -task <task number> -type <feat|fix> -description <description> -project <project short name>
525
545
  nitor deploy -project <project> -components <components> -instance <instance>
546
+ nitor get-task -task <task numbers with space>
526
547
  nitor merge -source <source branch> -target <target branch>
527
548
  nitor refactor <text>
528
549
  nitor review -project <project short name> -mergeId <merge id> -repository <repository name>
550
+ nitor task-stats -task <task number>
529
551
  nitor time-init
530
552
  nitor time-add
531
553
  nitor time-stats
@@ -542,9 +564,8 @@ Running 'nitor help' will list available subcommands and provide some conceptual
542
564
  }
543
565
  } catch (error) {
544
566
  console.log(error);
567
+ process.exit(1);
545
568
  }
546
-
547
- process.exit(1);
548
569
  };
549
570
 
550
571
  module.exports = { processArgs };
@@ -7,6 +7,7 @@ const executeMergeRequestReview = async (values) => {
7
7
  maxBodyLength: Infinity,
8
8
  url: `${mrApiUri}/${projectIdMap[values.project]}/mergeid/${values.mergeId}`,
9
9
  headers: { 'Content-Type': 'application/json' },
10
+ timeout: 10 * 60 * 1000, // 10 minutes
10
11
  data: JSON.stringify({
11
12
  language: mrLang,
12
13
  gitlabToken: gitlabConfig.token,
@@ -84,9 +84,7 @@ const getGitIdStats = async (gitIds) => {
84
84
  const response = await axios.request(config);
85
85
 
86
86
  gitIdStats.push(response.data);
87
- } catch (error) {
88
- console.log(error);
89
- }
87
+ } catch (error) {}
90
88
  }
91
89
 
92
90
  return gitIdStats;
@@ -124,6 +122,8 @@ const getGitMergeRequestDetails = async (issueIid, projectId) => {
124
122
  };
125
123
 
126
124
  const getGitlabIssueMergeRequests = async (gitDetails) => {
125
+ const skippedMrBranches = ['dev-qa-testing', 'qa-testing'];
126
+
127
127
  for (const gitDetail of gitDetails) {
128
128
  try {
129
129
  const mergeRequests = await getGitMergeRequestDetails(
@@ -139,6 +139,10 @@ const getGitlabIssueMergeRequests = async (gitDetails) => {
139
139
  const detailResponse = await axios.request(detailConfig);
140
140
  const mrData = detailResponse.data;
141
141
 
142
+ if (skippedMrBranches.some((branch) => mrData.target_branch.includes(branch))) {
143
+ return;
144
+ }
145
+
142
146
  // Fetch approval details separately
143
147
  try {
144
148
  const approvalConfig = getGitlabConfig(
@@ -152,6 +156,7 @@ const getGitlabIssueMergeRequests = async (gitDetails) => {
152
156
  IssueID: gitDetail.iid,
153
157
  MRID: mrData.iid,
154
158
  MRAssignedTo: mrData.assignee?.name,
159
+ MRTarget: mrData.target_branch,
155
160
  Repo: mr.reference?.split('!')?.[0],
156
161
  ApprovedBy:
157
162
  (approvalData.approved_by || [])
@@ -4,7 +4,6 @@ const {
4
4
  path,
5
5
  readFileSync,
6
6
  userHomeDir,
7
- removeEmpty,
8
7
  } = require('./utils');
9
8
  const getTasksByDate = async (filters) => {
10
9
  try {
@@ -29,7 +28,10 @@ const getTasksByDate = async (filters) => {
29
28
  const item = readFileSync(path.join(userHomeDir, `.${stringDate}`));
30
29
 
31
30
  if (item) {
32
- const timeEntries = removeEmpty(item.split(contentTableSeparator)[1].split('\n'));
31
+ const timeEntries = item
32
+ .split(contentTableSeparator)[1]
33
+ .split('\n')
34
+ .filter((entry) => entry.trim() !== '');
33
35
 
34
36
  data = data.concat(
35
37
  timeEntries.map((entry) =>
@@ -78,4 +78,4 @@ const checkSprintId = async (id, req) => {
78
78
  return sprints.some(({ value }) => value === id);
79
79
  };
80
80
 
81
- module.exports = { getZohoTasks };
81
+ module.exports = { getZohoTasks, getTasksBySprint };
@@ -228,29 +228,6 @@ const readFileSync = (path) => {
228
228
  // console.log(error);
229
229
  }
230
230
  };
231
- const removeEmpty = (obj) => {
232
- for (let [key, val] of Object.entries(obj)) {
233
- if (val && typeof val === 'object') {
234
- this.removeEmpty(val);
235
-
236
- if (!(Object.keys(val).length || val instanceof Date)) {
237
- delete obj[key];
238
- }
239
- } else {
240
- if (typeof val === 'string') {
241
- val = val.trim();
242
- }
243
-
244
- if (val === null || val === undefined || val === '') {
245
- delete obj[key];
246
- } else {
247
- obj[key] = val;
248
- }
249
- }
250
- }
251
-
252
- return obj;
253
- };
254
231
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
255
232
  const userConfig = async (jsonData) => {
256
233
  return new Promise((resolve, reject) => {
@@ -293,7 +270,6 @@ module.exports = {
293
270
  mkdirSync,
294
271
  path,
295
272
  readFileSync,
296
- removeEmpty,
297
273
  rl,
298
274
  userConfig,
299
275
  userHomeDir,
package/services/utils.js CHANGED
@@ -22,7 +22,7 @@ const gitlabConfig = {
22
22
  const zohoConfig = {
23
23
  cookie: process.env.ZOHO_COOKIE,
24
24
  customViewId: process.env.ZOHO_CUSTOMVIEW_ID,
25
- defaultProjectId: process.env.ZOHO_DEFAULt_PROJECT_ID,
25
+ defaultProjectId: process.env.ZOHO_DEFAULT_PROJECT_ID,
26
26
  portalId: process.env.ZOHO_PORTAL_ID,
27
27
  projects: process.env.ZOHO_PROJECTS,
28
28
  sessionId: process.env.ZOHO_SESSION_ID,
@@ -59,6 +59,8 @@ const keyMap = {
59
59
  ta: 'target',
60
60
  docker: 'docker',
61
61
  do: 'docker',
62
+ sprint: 'sprint',
63
+ s: 'sprint',
62
64
  };
63
65
  const projectMap = {
64
66
  portal: 'medica-portal',