nitor 1.3.2 → 1.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitor",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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>",
@@ -15,15 +15,15 @@ const cleanup = async () => {
15
15
 
16
16
  // Get current branch
17
17
  const currentBranch = execSync('git branch --show-current', execOptions).trim();
18
- console.log(`Current branch: ${currentBranch}`);
19
18
 
20
- // Checkout to master
19
+ console.log(`Current branch: ${currentBranch}`);
21
20
  console.log('\nChecking out to master branch...');
22
21
  try {
23
22
  execSync('git checkout master', execOptionsInherit);
24
23
  } catch (error) {
25
24
  // Try main if master doesn't exist
26
25
  console.log('Master branch not found, trying main...');
26
+
27
27
  execSync('git checkout main', execOptionsInherit);
28
28
  }
29
29
 
@@ -40,28 +40,32 @@ const cleanup = async () => {
40
40
  }
41
41
 
42
42
  console.log(`\nFound ${branches.length} branch(es) to delete:`);
43
-
44
- // Delete each branch
45
43
  console.log('\nDeleting branches...');
44
+
46
45
  let deletedCount = 0;
47
46
  let failedCount = 0;
48
47
 
49
48
  for (const branch of branches) {
50
49
  try {
51
50
  execSync(`git branch -D ${branch}`, execOptions);
52
- console.log(` Deleted: ${branch}`);
51
+ execSync(`rm -rf ~/.git/logs/refs/heads/${branch}`, execOptions);
52
+ execSync(`rm -rf ~/.git/refs/heads/${branch}`, execOptions);
53
+
54
+ console.log(`Deleted: ${branch}`);
55
+
53
56
  deletedCount++;
54
57
  } catch (error) {
55
- console.log(`Failed to delete: ${branch}`);
58
+ console.log(`Failed to delete: ${branch}`);
59
+
56
60
  failedCount++;
57
61
  }
58
62
  }
59
63
 
60
64
  console.log(`\nCleanup complete!`);
61
- console.log(` Deleted: ${deletedCount} branch(es)`);
65
+ console.log(`Deleted: ${deletedCount} branch(es)`);
62
66
 
63
67
  if (failedCount > 0) {
64
- console.log(` Failed: ${failedCount} branch(es)`);
68
+ console.log(`Failed: ${failedCount} branch(es)`);
65
69
  }
66
70
  } catch (error) {
67
71
  console.error('Error during cleanup:', error.message);
@@ -2,7 +2,7 @@ const axios = require('axios');
2
2
  const { build, buildStatus } = require('./build');
3
3
  const { deploy } = require('./deploy');
4
4
  const { ACTIONS } = require('./enums/actions.enum');
5
- const { convertParamsToMap, wait } = require('./utils');
5
+ const { convertParamsToMap, wait, printTable, createSpinner } = require('./utils');
6
6
  const { createBranch } = require('./create-branch');
7
7
  const { mrAIReview } = require('./review');
8
8
  const { refactor } = require('./refactor');
@@ -299,46 +299,59 @@ Options:
299
299
  return;
300
300
  }
301
301
 
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
- });
302
+ const spinner = createSpinner('Fetching task statistics');
303
+ spinner.start();
307
304
 
308
- values.task = taskDetails.map(({ taskId }) => taskId);
309
- } else {
310
- values.task = values.task.split(' ');
311
- }
305
+ try {
306
+ if (values.sprint) {
307
+ spinner.update('Loading sprints');
308
+ const sprints = await getSprints({ params: {} });
309
+ const taskDetails = await getZohoTasks({
310
+ params: { sprint: sprints.find(({ label }) => label.includes(values.sprint))?.value },
311
+ });
312
312
 
313
- // Process all tasks in parallel
314
- const taskResults = await Promise.all(
315
- values.task.map(async (task) => {
316
- const taskDetail = await getTaskStats(task);
313
+ values.task = taskDetails.map(({ taskId }) => taskId);
314
+ } else {
315
+ values.task = values.task.split(' ');
316
+ }
317
317
 
318
- if (!taskDetail?.itemIds) {
319
- return [];
320
- }
318
+ spinner.update(`Processing ${values.task.length} task(s)`);
321
319
 
322
- const gitIdDetails = await getGitIdStats(taskDetail.itemIds);
320
+ // Process all tasks in parallel
321
+ const taskResults = await Promise.all(
322
+ values.task.map(async (task) => {
323
+ const taskDetail = await getTaskStats(task);
323
324
 
324
- if (!gitIdDetails?.length) {
325
- return [];
326
- }
325
+ if (!taskDetail?.itemIds) {
326
+ return [];
327
+ }
327
328
 
328
- const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
329
+ const gitIdDetails = await getGitIdStats(taskDetail.itemIds);
329
330
 
330
- return mrDetails.map((mrDetail) => ({
331
- Task: task,
332
- Owner: taskDetail.owner,
333
- ...mrDetail,
334
- }));
335
- }),
336
- );
331
+ if (!gitIdDetails?.length) {
332
+ return [];
333
+ }
337
334
 
338
- // Flatten the results
339
- const list = taskResults.flat();
335
+ const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
340
336
 
341
- console.table(list);
337
+ return mrDetails.map((mrDetail) => ({
338
+ Task: task,
339
+ Owner: taskDetail.owner,
340
+ ...mrDetail,
341
+ }));
342
+ }),
343
+ );
344
+
345
+ // Flatten the results
346
+ const list = taskResults.flat();
347
+
348
+ spinner.stop(`Found ${list.length} merge request(s)`, list.length > 0);
349
+
350
+ printTable(list);
351
+ } catch (error) {
352
+ spinner.stop('Failed to fetch task statistics', false);
353
+ throw error;
354
+ }
342
355
 
343
356
  break;
344
357
  }
@@ -421,7 +434,7 @@ Options:
421
434
  case ACTIONS.TIME_ENTRIES: {
422
435
  const timeValues = value ? value.split(' -') : value;
423
436
 
424
- console.table(await getTasksByDate(timeValues));
437
+ printTable(await getTasksByDate(timeValues));
425
438
 
426
439
  break;
427
440
  }
@@ -485,7 +498,7 @@ Options:
485
498
  }),
486
499
  );
487
500
 
488
- console.table(
501
+ printTable(
489
502
  mergeRequests.map((mr) => ({
490
503
  'Task ID': mr.taskId,
491
504
  'MR IID': mr.gitlabIid,
@@ -562,6 +575,8 @@ Running 'nitor help' will list available subcommands and provide some conceptual
562
575
  return;
563
576
  }
564
577
  }
578
+
579
+ process.exit(1);
565
580
  } catch (error) {
566
581
  console.log(error);
567
582
  process.exit(1);
@@ -84,7 +84,9 @@ const getGitIdStats = async (gitIds) => {
84
84
  const response = await axios.request(config);
85
85
 
86
86
  gitIdStats.push(response.data);
87
- } catch (error) {}
87
+ } catch (error) {
88
+ // Silently skip git IDs that fail to fetch - they may not exist or be inaccessible
89
+ }
88
90
  }
89
91
 
90
92
  return gitIdStats;
@@ -153,8 +155,9 @@ const getGitlabIssueMergeRequests = async (gitDetails) => {
153
155
 
154
156
  // Merge approval data with MR data
155
157
  return {
156
- IssueID: gitDetail.iid,
158
+ GitID: gitDetail.iid,
157
159
  MRID: mrData.iid,
160
+ MRStatus: mrData.state,
158
161
  MRAssignedTo: mrData.assignee?.name,
159
162
  MRTarget: mrData.target_branch,
160
163
  Repo: mr.reference?.split('!')?.[0],
@@ -162,7 +165,7 @@ const getGitlabIssueMergeRequests = async (gitDetails) => {
162
165
  (approvalData.approved_by || [])
163
166
  .map((approvedBy) => approvedBy?.user?.name || '')
164
167
  ?.join(', ') || 'Not Approved',
165
- ApprovedCount: approvalData.approved_by?.length || 0,
168
+ Approvals: approvalData.approved_by?.length || 0,
166
169
  MergedBy: mrData.merged_by?.name || 'Not Merged',
167
170
  MergedOn: mrData.merged_at?.split('T')?.[0] || 'Not Merged',
168
171
  };
package/services/utils.js CHANGED
@@ -353,6 +353,253 @@ const convertParamsToMap = async (item, type) => {
353
353
  }, {});
354
354
  };
355
355
 
356
+ // ANSI color codes for table
357
+ const colors = {
358
+ reset: '\x1b[0m',
359
+ bold: '\x1b[1m',
360
+ dim: '\x1b[2m',
361
+ cyan: '\x1b[36m',
362
+ green: '\x1b[32m',
363
+ yellow: '\x1b[33m',
364
+ magenta: '\x1b[35m',
365
+ blue: '\x1b[34m',
366
+ white: '\x1b[37m',
367
+ bgBlue: '\x1b[44m',
368
+ bgCyan: '\x1b[46m',
369
+ };
370
+
371
+ // Unicode box-drawing characters
372
+ const box = {
373
+ topLeft: '╭',
374
+ topRight: '╮',
375
+ bottomLeft: '╰',
376
+ bottomRight: '╯',
377
+ horizontal: '─',
378
+ vertical: '│',
379
+ teeDown: '┬',
380
+ teeUp: '┴',
381
+ teeRight: '├',
382
+ teeLeft: '┤',
383
+ cross: '┼',
384
+ };
385
+
386
+ // Loading spinner frames
387
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
388
+
389
+ /**
390
+ * Create a loading spinner for async operations
391
+ * @param {string} message - Loading message to display
392
+ * @returns {Object} Spinner controller with start() and stop(finalMessage) methods
393
+ */
394
+ const createSpinner = (message = 'Loading') => {
395
+ let frameIndex = 0;
396
+ let interval = null;
397
+ let isSpinning = false;
398
+
399
+ const start = () => {
400
+ if (isSpinning) return;
401
+ isSpinning = true;
402
+ process.stdout.write('\x1b[?25l'); // Hide cursor
403
+
404
+ interval = setInterval(() => {
405
+ const frame = spinnerFrames[frameIndex];
406
+ process.stdout.write(`\r${colors.cyan}${frame}${colors.reset} ${message}...`);
407
+ frameIndex = (frameIndex + 1) % spinnerFrames.length;
408
+ }, 80);
409
+ };
410
+
411
+ const stop = (finalMessage = null, success = true) => {
412
+ if (!isSpinning) return;
413
+ isSpinning = false;
414
+
415
+ if (interval) {
416
+ clearInterval(interval);
417
+ interval = null;
418
+ }
419
+
420
+ process.stdout.write('\x1b[?25h'); // Show cursor
421
+ process.stdout.write('\r\x1b[K'); // Clear line
422
+
423
+ if (finalMessage) {
424
+ const icon = success
425
+ ? `${colors.green}✔${colors.reset}`
426
+ : `${colors.yellow}✖${colors.reset}`;
427
+ console.log(`${icon} ${finalMessage}`);
428
+ }
429
+ };
430
+
431
+ const update = (newMessage) => {
432
+ message = newMessage;
433
+ };
434
+
435
+ return { start, stop, update };
436
+ };
437
+
438
+ /**
439
+ * Print a beautiful CLI table
440
+ * @param {Array<Object>} data - Array of objects to display
441
+ * @param {Object} options - Configuration options
442
+ * @param {number} options.maxColWidth - Maximum column width (default: 20)
443
+ * @param {Array<string>} options.columns - Specific columns to display (default: all)
444
+ * @param {boolean} options.sort - Whether to sort by first column (default: true)
445
+ * @param {string} options.sortBy - Column to sort by (default: first column)
446
+ * @param {string} options.sortOrder - 'asc' or 'desc' (default: 'asc')
447
+ */
448
+ const printTable = (data, options = {}) => {
449
+ if (!data || !Array.isArray(data) || data.length === 0) {
450
+ console.log(`${colors.yellow}No data to display${colors.reset}`);
451
+ return;
452
+ }
453
+
454
+ // Filter out undefined/null entries
455
+ let filteredData = data.filter((item) => item != null);
456
+
457
+ if (filteredData.length === 0) {
458
+ console.log(`${colors.yellow}No data to display${colors.reset}`);
459
+ return;
460
+ }
461
+
462
+ const { maxColWidth = 20, sort = true, sortOrder = 'asc' } = options;
463
+
464
+ // Get all columns from the data
465
+ const allColumns = [...new Set(filteredData.flatMap((row) => Object.keys(row)))];
466
+ const columns = options.columns || allColumns;
467
+
468
+ // Sort data by first column (or specified column)
469
+ const sortBy = options.sortBy || columns[0];
470
+ if (sort && sortBy) {
471
+ filteredData = [...filteredData].sort((a, b) => {
472
+ const valA = a[sortBy] ?? '';
473
+ const valB = b[sortBy] ?? '';
474
+
475
+ // Try numeric comparison first
476
+ const numA = Number(valA);
477
+ const numB = Number(valB);
478
+ if (!isNaN(numA) && !isNaN(numB)) {
479
+ return sortOrder === 'asc' ? numA - numB : numB - numA;
480
+ }
481
+
482
+ // Fall back to string comparison
483
+ const strA = String(valA).toLowerCase();
484
+ const strB = String(valB).toLowerCase();
485
+ if (sortOrder === 'asc') {
486
+ return strA.localeCompare(strB);
487
+ }
488
+ return strB.localeCompare(strA);
489
+ });
490
+ }
491
+
492
+ // Calculate column widths
493
+ const colWidths = {};
494
+ columns.forEach((col) => {
495
+ const headerLen = col.length;
496
+ const maxDataLen = Math.max(
497
+ ...filteredData.map((row) => String(row[col] ?? '').length),
498
+ headerLen,
499
+ );
500
+ colWidths[col] = Math.min(maxDataLen, maxColWidth);
501
+ });
502
+
503
+ // Helper to truncate and pad text
504
+ const formatCell = (text, width) => {
505
+ const str = String(text ?? '');
506
+ if (str.length > width) {
507
+ return str.substring(0, width - 2) + '..';
508
+ }
509
+ return str.padEnd(width);
510
+ };
511
+
512
+ // Top border
513
+ let topBorder = colors.cyan + box.topLeft;
514
+ columns.forEach((col, i) => {
515
+ topBorder += box.horizontal.repeat(colWidths[col] + 2);
516
+ topBorder += i < columns.length - 1 ? box.teeDown : box.topRight;
517
+ });
518
+ topBorder += colors.reset;
519
+
520
+ // Header row
521
+ let headerRow = colors.cyan + box.vertical + colors.reset;
522
+ columns.forEach((col) => {
523
+ const sortIndicator = sort && col === sortBy ? (sortOrder === 'asc' ? ' ↑' : ' ↓') : '';
524
+ headerRow +=
525
+ ' ' +
526
+ colors.bold +
527
+ colors.yellow +
528
+ formatCell(col + sortIndicator, colWidths[col]) +
529
+ colors.reset +
530
+ ' ' +
531
+ colors.cyan +
532
+ box.vertical +
533
+ colors.reset;
534
+ });
535
+
536
+ // Header separator
537
+ let headerSep = colors.cyan + box.teeRight;
538
+ columns.forEach((col, i) => {
539
+ headerSep += box.horizontal.repeat(colWidths[col] + 2);
540
+ headerSep += i < columns.length - 1 ? box.cross : box.teeLeft;
541
+ });
542
+ headerSep += colors.reset;
543
+
544
+ // Data rows
545
+ const dataRows = filteredData.map((row) => {
546
+ let dataRow = colors.cyan + box.vertical + colors.reset;
547
+ columns.forEach((col) => {
548
+ const value = row[col];
549
+ let cellColor = colors.white;
550
+
551
+ // Apply colors based on content
552
+ if (col === 'Approvals') {
553
+ cellColor = value >= 2 ? colors.green : value >= 1 ? colors.yellow : colors.dim;
554
+ } else if (col === 'MergedBy' || col === 'MergedOn') {
555
+ cellColor = value === 'Not Merged' ? colors.dim : colors.green;
556
+ } else if (col === 'ApprovedBy') {
557
+ cellColor = value === 'Not Approved' ? colors.dim : colors.green;
558
+ } else if (col === 'MRStatus') {
559
+ cellColor =
560
+ value === 'merged' ? colors.green : value === 'opened' ? colors.yellow : colors.dim;
561
+ } else if (col === 'Task' || col === 'MRID' || col === 'GitID') {
562
+ cellColor = colors.cyan;
563
+ } else if (col === 'Owner' || col === 'MRAssignedTo') {
564
+ cellColor = colors.magenta;
565
+ } else if (col === 'Repo') {
566
+ cellColor = colors.blue;
567
+ }
568
+
569
+ dataRow +=
570
+ ' ' +
571
+ cellColor +
572
+ formatCell(value, colWidths[col]) +
573
+ colors.reset +
574
+ ' ' +
575
+ colors.cyan +
576
+ box.vertical +
577
+ colors.reset;
578
+ });
579
+ return dataRow;
580
+ });
581
+
582
+ // Bottom border
583
+ let bottomBorder = colors.cyan + box.bottomLeft;
584
+ columns.forEach((col, i) => {
585
+ bottomBorder += box.horizontal.repeat(colWidths[col] + 2);
586
+ bottomBorder += i < columns.length - 1 ? box.teeUp : box.bottomRight;
587
+ });
588
+ bottomBorder += colors.reset;
589
+
590
+ // Print the table
591
+ console.log('');
592
+ console.log(topBorder);
593
+ console.log(headerRow);
594
+ console.log(headerSep);
595
+ dataRows.forEach((row) => console.log(row));
596
+ console.log(bottomBorder);
597
+ console.log(
598
+ `${colors.dim} ${filteredData.length} row${filteredData.length !== 1 ? 's' : ''} total${colors.reset}`,
599
+ );
600
+ console.log('');
601
+ };
602
+
356
603
  module.exports = {
357
604
  convertParamsToMap,
358
605
  generateBuildConfigs,
@@ -369,4 +616,6 @@ module.exports = {
369
616
  restoreConfig,
370
617
  zohoConfig,
371
618
  gitlabConfig,
619
+ printTable,
620
+ createSpinner,
372
621
  };