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 +1 -1
- package/services/cleanup.js +12 -8
- package/services/process-commands.js +49 -34
- package/services/task-stats.js +6 -3
- package/services/utils.js +249 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitor",
|
|
3
|
-
"version": "1.3.
|
|
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>",
|
package/services/cleanup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(`
|
|
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(`
|
|
65
|
+
console.log(`Deleted: ${deletedCount} branch(es)`);
|
|
62
66
|
|
|
63
67
|
if (failedCount > 0) {
|
|
64
|
-
console.log(`
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
values.task = taskDetails.map(({ taskId }) => taskId);
|
|
314
|
+
} else {
|
|
315
|
+
values.task = values.task.split(' ');
|
|
316
|
+
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
318
|
+
spinner.update(`Processing ${values.task.length} task(s)`);
|
|
321
319
|
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
if (!taskDetail?.itemIds) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
327
328
|
|
|
328
|
-
|
|
329
|
+
const gitIdDetails = await getGitIdStats(taskDetail.itemIds);
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
...mrDetail,
|
|
334
|
-
}));
|
|
335
|
-
}),
|
|
336
|
-
);
|
|
331
|
+
if (!gitIdDetails?.length) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
337
334
|
|
|
338
|
-
|
|
339
|
-
const list = taskResults.flat();
|
|
335
|
+
const mrDetails = await getGitlabIssueMergeRequests(gitIdDetails);
|
|
340
336
|
|
|
341
|
-
|
|
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
|
-
|
|
437
|
+
printTable(await getTasksByDate(timeValues));
|
|
425
438
|
|
|
426
439
|
break;
|
|
427
440
|
}
|
|
@@ -485,7 +498,7 @@ Options:
|
|
|
485
498
|
}),
|
|
486
499
|
);
|
|
487
500
|
|
|
488
|
-
|
|
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);
|
package/services/task-stats.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
};
|