@ibm-cloud/cd-tools 1.13.0 → 1.13.1
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/cmd/direct-transfer.js +142 -114
- package/package.json +1 -1
package/cmd/direct-transfer.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import axios from 'axios';
|
|
12
|
-
import readline from 'readline/promises';
|
|
13
12
|
import { writeFile } from 'fs/promises';
|
|
14
13
|
import { COPY_PROJECT_GROUP_DESC, SOURCE_REGIONS } from '../config.js';
|
|
15
14
|
import { getWithRetry } from './utils/requests.js';
|
|
15
|
+
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
16
|
+
import { promptUserYesNo } from './utils/utils.js';
|
|
16
17
|
|
|
17
18
|
const HTTP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default
|
|
18
19
|
|
|
@@ -39,8 +40,9 @@ class GitLabClient {
|
|
|
39
40
|
const toVisit = [groupId];
|
|
40
41
|
const visited = new Set();
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
`
|
|
43
|
+
logger.debug(
|
|
44
|
+
`Starting BFS project listing from group ${groupId} (maxProjects=${maxProjects}, maxRequests=${maxRequests})`,
|
|
45
|
+
LOG_STAGES.setup
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
while (toVisit.length > 0) {
|
|
@@ -48,7 +50,7 @@ class GitLabClient {
|
|
|
48
50
|
if (visited.has(currentGroupId)) continue;
|
|
49
51
|
visited.add(currentGroupId);
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
logger.debug(`Visiting group ${currentGroupId}. Remaining groups in queue: ${toVisit.length}`, LOG_STAGES.setup);
|
|
52
54
|
|
|
53
55
|
// List projects for THIS group (no include_subgroups!)
|
|
54
56
|
let projPage = 1;
|
|
@@ -56,7 +58,7 @@ class GitLabClient {
|
|
|
56
58
|
|
|
57
59
|
while (hasMoreProjects) {
|
|
58
60
|
if (requestCount >= maxRequests || projects.length >= maxProjects) {
|
|
59
|
-
|
|
61
|
+
logger.warn(`Stopping project traversal early: requestCount=${requestCount}, projects=${projects.length}`, LOG_STAGES.setup);
|
|
60
62
|
return projects;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -82,9 +84,7 @@ class GitLabClient {
|
|
|
82
84
|
|
|
83
85
|
while (hasMoreSubgroups) {
|
|
84
86
|
if (requestCount >= maxRequests) {
|
|
85
|
-
|
|
86
|
-
`[WARN] Stopping subgroup traversal: requestCount=${requestCount}`
|
|
87
|
-
);
|
|
87
|
+
logger.warn(`Stopping subgroup traversal early: requestCount=${requestCount}`, LOG_STAGES.setup);
|
|
88
88
|
return projects;
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -110,7 +110,7 @@ class GitLabClient {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
logger.debug(`Finished BFS project listing. Total projects=${projects.length}, total requests=${requestCount}`, LOG_STAGES.setup);
|
|
114
114
|
return projects;
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -271,23 +271,6 @@ class GitLabClient {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
async function promptUser(name) {
|
|
275
|
-
const rl = readline.createInterface({
|
|
276
|
-
input: process.stdin,
|
|
277
|
-
output: process.stdout,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const answer = await rl.question(`Your new group name is ${name}. Are you sure? (Yes/No)`);
|
|
281
|
-
|
|
282
|
-
rl.close();
|
|
283
|
-
|
|
284
|
-
if (answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') {
|
|
285
|
-
console.log("Proceeding...");
|
|
286
|
-
} else {
|
|
287
|
-
process.exit(0);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
274
|
function validateAndConvertRegion(region) {
|
|
292
275
|
if (!SOURCE_REGIONS.includes(region)) {
|
|
293
276
|
throw new Error(
|
|
@@ -322,8 +305,9 @@ async function generateUrlMappingFile({ destUrl, sourceGroup, destinationGroupPa
|
|
|
322
305
|
encoding: 'utf8',
|
|
323
306
|
});
|
|
324
307
|
|
|
325
|
-
|
|
326
|
-
|
|
308
|
+
logger.print();
|
|
309
|
+
logger.info(`Created file mapping old project urls to new urls at: ${mappingFile}`, LOG_STAGES.info);
|
|
310
|
+
logger.info(`Total mapped projects: ${sourceProjects.length}`, LOG_STAGES.info);
|
|
327
311
|
}
|
|
328
312
|
|
|
329
313
|
function buildGroupImportHistoryUrl(destUrl) {
|
|
@@ -386,10 +370,12 @@ function summarizeBulkImportProgress(entities = []) {
|
|
|
386
370
|
|
|
387
371
|
return {
|
|
388
372
|
entityTotal,
|
|
373
|
+
entityFinished,
|
|
389
374
|
entityDone,
|
|
390
375
|
entityFailed,
|
|
391
376
|
entityPct,
|
|
392
377
|
projectTotal,
|
|
378
|
+
projectFinished,
|
|
393
379
|
projectDone,
|
|
394
380
|
projectFailed,
|
|
395
381
|
projectPct,
|
|
@@ -435,9 +421,10 @@ async function handleBulkImportConflict({ destination, destUrl, sourceGroupFullP
|
|
|
435
421
|
const historyUrl = buildGroupImportHistoryUrl(destUrl);
|
|
436
422
|
const groupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
437
423
|
const fallback = () => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (
|
|
424
|
+
logger.print();
|
|
425
|
+
logger.warn(`Destination group already exists.`, LOG_STAGES.import);
|
|
426
|
+
if (groupUrl) logger.info(`Group: ${groupUrl}`, LOG_STAGES.import);
|
|
427
|
+
if (historyUrl) logger.info(`Group import history: ${historyUrl}`, LOG_STAGES.import);
|
|
441
428
|
process.exit(0);
|
|
442
429
|
};
|
|
443
430
|
|
|
@@ -472,18 +459,21 @@ async function handleBulkImportConflict({ destination, destUrl, sourceGroupFullP
|
|
|
472
459
|
if (!matchesThisGroup) continue;
|
|
473
460
|
|
|
474
461
|
if (status === 'created' || status === 'started') {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (
|
|
462
|
+
logger.print();
|
|
463
|
+
logger.warn(`Group is already in migration...`, LOG_STAGES.import);
|
|
464
|
+
logger.info(`Bulk import ID: ${bi.id}`, LOG_STAGES.import);
|
|
465
|
+
if (groupUrl) logger.info(`Group URL: ${groupUrl}`, LOG_STAGES.import);
|
|
466
|
+
if (historyUrl) logger.info(`Group import history: ${historyUrl}`, LOG_STAGES.import);
|
|
479
467
|
process.exit(0);
|
|
480
468
|
}
|
|
481
469
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
470
|
+
logger.print();
|
|
471
|
+
logger.warn(`Conflict detected: ${importResErr}`, LOG_STAGES.import);
|
|
472
|
+
logger.info(`Tip: specify a new group name using -n, --new-group-slug <slug> and try again.`, LOG_STAGES.import);
|
|
473
|
+
logger.print();
|
|
474
|
+
logger.info(`Group already migrated.`, LOG_STAGES.import);
|
|
475
|
+
if (groupUrl) logger.info(`Group URL: ${groupUrl}`, LOG_STAGES.import);
|
|
476
|
+
if (historyUrl) logger.info(`Group import history: ${historyUrl}`, LOG_STAGES.import);
|
|
487
477
|
process.exit(0);
|
|
488
478
|
}
|
|
489
479
|
|
|
@@ -504,20 +494,20 @@ async function directTransfer(options) {
|
|
|
504
494
|
const destination = new GitLabClient(destUrl, options.destToken);
|
|
505
495
|
|
|
506
496
|
try {
|
|
507
|
-
|
|
497
|
+
logger.info(`Fetching source group from ID: ${options.groupId}...`, LOG_STAGES.setup);
|
|
508
498
|
let sourceGroup;
|
|
509
499
|
try {
|
|
510
500
|
sourceGroup = await source.getGroup(options.groupId);
|
|
511
501
|
} catch (err) {
|
|
512
502
|
if (err?.response?.status === 404) {
|
|
513
|
-
|
|
503
|
+
logger.error(
|
|
514
504
|
`Error: group "${options.groupId}" not found in source region "${options.sourceRegion}".\n` +
|
|
515
505
|
`Tip: -g accepts numeric ID or full group path like "parent/subgroup".`
|
|
516
506
|
);
|
|
517
507
|
return 1;
|
|
518
508
|
}
|
|
519
509
|
|
|
520
|
-
|
|
510
|
+
logger.error(`Error: failed to fetch group "${options.groupId}": ${err?.message || err}`, LOG_STAGES.setup);
|
|
521
511
|
return 1;
|
|
522
512
|
}
|
|
523
513
|
|
|
@@ -531,18 +521,20 @@ async function directTransfer(options) {
|
|
|
531
521
|
maxProjects: 10000,
|
|
532
522
|
});
|
|
533
523
|
} catch (e) {
|
|
534
|
-
|
|
524
|
+
logger.warn(`GraphQL listing failed. Falling back to REST project listing...`, LOG_STAGES.setup);
|
|
525
|
+
logger.debug(`GraphQL error: ${e.message}`, LOG_STAGES.setup);
|
|
535
526
|
sourceProjects = await source.getGroupProjects(sourceGroup.id);
|
|
536
527
|
}
|
|
537
528
|
|
|
538
|
-
|
|
529
|
+
logger.info(`Found ${sourceProjects.length} projects in source group`, LOG_STAGES.setup);
|
|
539
530
|
if (sourceProjects.length > 0) {
|
|
540
|
-
|
|
541
|
-
sourceProjects.forEach(p =>
|
|
531
|
+
logger.info('Projects to be migrated:', LOG_STAGES.setup);
|
|
532
|
+
sourceProjects.forEach(p => logger.print(p.name_with_namespace || p.nameWithNamespace || p.fullPath));
|
|
542
533
|
}
|
|
543
534
|
|
|
544
535
|
if (options.newGroupSlug) {
|
|
545
|
-
await
|
|
536
|
+
const ok = await promptUserYesNo(`Your new group slug is "${options.newGroupSlug}". Proceed?`);
|
|
537
|
+
if (!ok) return 0;
|
|
546
538
|
}
|
|
547
539
|
|
|
548
540
|
// Generate URL mapping JSON before starting the migration
|
|
@@ -571,11 +563,12 @@ async function directTransfer(options) {
|
|
|
571
563
|
let importRes = null;
|
|
572
564
|
|
|
573
565
|
try {
|
|
566
|
+
logger.print();
|
|
567
|
+
logger.info(`Requesting bulk import request in '${options.destRegion}'...`, LOG_STAGES.request);
|
|
574
568
|
importRes = await destination.bulkImport(requestPayload);
|
|
575
569
|
if (importRes.success) {
|
|
576
570
|
bulkImport = importRes.data;
|
|
577
|
-
|
|
578
|
-
console.log(`Bulk import initiated successfully (ID: ${importRes.data?.id})`);
|
|
571
|
+
logger.success(`✔ Bulk import initiated successfully (ID: ${importRes.data?.id})`, LOG_STAGES.request);
|
|
579
572
|
} else if (importRes.conflict) {
|
|
580
573
|
await handleBulkImportConflict({
|
|
581
574
|
destination,
|
|
@@ -586,11 +579,17 @@ async function directTransfer(options) {
|
|
|
586
579
|
});
|
|
587
580
|
}
|
|
588
581
|
} catch (error) {
|
|
589
|
-
|
|
582
|
+
logger.error(`✖ Bulk import request failed - ${error.message}`, LOG_STAGES.request);
|
|
590
583
|
process.exit(0);
|
|
591
584
|
}
|
|
585
|
+
|
|
586
|
+
logger.print();
|
|
587
|
+
const spinnerOff = process.env.DISABLE_SPINNER === 'true';
|
|
588
|
+
if (spinnerOff) {
|
|
589
|
+
logger.info('Waiting for bulk project import to complete...', LOG_STAGES.import);
|
|
590
|
+
logger.info('This may take time depending on the number and size of projects.', LOG_STAGES.import);
|
|
591
|
+
}
|
|
592
592
|
|
|
593
|
-
console.log('\nPolling bulk import status (adaptive: 1m→2m→3m→4m→5m, max 60 checks)...');
|
|
594
593
|
const MAX_ATTEMPTS = 60;
|
|
595
594
|
const POLLS_PER_STEP = 5;
|
|
596
595
|
const MIN_INTERVAL_MIN = 1;
|
|
@@ -598,94 +597,121 @@ async function directTransfer(options) {
|
|
|
598
597
|
|
|
599
598
|
let importStatus = 'created';
|
|
600
599
|
let attempts = 0;
|
|
600
|
+
let entitiesAll = [];
|
|
601
601
|
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
602
|
+
const emit = (msg) => {
|
|
603
|
+
if (spinnerOff) logger.info(msg, LOG_STAGES.import);
|
|
604
|
+
else logger.updateSpinnerMsg(msg);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const waitStep = async () => {
|
|
608
|
+
const step = Math.floor(attempts / POLLS_PER_STEP);
|
|
609
|
+
const waitMin = Math.min(MIN_INTERVAL_MIN + step, MAX_INTERVAL_MIN);
|
|
610
|
+
|
|
611
|
+
if (options.verbose) emit(`Waiting ${waitMin} minute before next status check...`);
|
|
612
|
+
await new Promise(r => setTimeout(r, waitMin * 60000));
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const pollBulkImport = async () => {
|
|
616
|
+
while (!['finished', 'failed', 'timeout'].includes(importStatus) && attempts < MAX_ATTEMPTS) {
|
|
617
|
+
if (attempts > 0) await waitStep();
|
|
606
618
|
|
|
607
|
-
console.log(`Waiting ${waitMin} minute before next status check...`);
|
|
608
|
-
await new Promise(resolve => setTimeout(resolve, waitMin * 60000));
|
|
609
|
-
}
|
|
610
|
-
try {
|
|
611
619
|
const importDetails = await destination.getBulkImport(bulkImport.id);
|
|
612
620
|
importStatus = importDetails.status;
|
|
621
|
+
|
|
613
622
|
let progressLine;
|
|
614
623
|
try {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
progressLine = formatBulkImportProgressLine(importStatus, summary);
|
|
624
|
+
entitiesAll = await destination.getBulkImportEntitiesAll(bulkImport.id);
|
|
625
|
+
progressLine = formatBulkImportProgressLine(importStatus, summarizeBulkImportProgress(entitiesAll));
|
|
618
626
|
} catch {
|
|
619
627
|
progressLine = `Import status: ${importStatus} | Progress: (unable to fetch entity details)`;
|
|
620
628
|
}
|
|
621
629
|
|
|
622
|
-
|
|
630
|
+
emit(progressLine);
|
|
623
631
|
|
|
624
|
-
if (importStatus === 'finished') {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
console.log('Bulk import failed!');
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
} catch (e) {
|
|
632
|
-
console.error(`Error checking import status: ${e.message}`);
|
|
633
|
-
if (e.response?.status === 404) {
|
|
634
|
-
throw new Error('Bulk import not found - it may have been deleted');
|
|
635
|
-
}
|
|
632
|
+
if (importStatus === 'finished') return { importStatus, entitiesAll };
|
|
633
|
+
if (importStatus === 'failed') throw new Error('GitLab bulk import failed');
|
|
634
|
+
|
|
635
|
+
attempts++;
|
|
636
636
|
}
|
|
637
|
-
attempts++;
|
|
638
|
-
}
|
|
639
637
|
|
|
640
|
-
|
|
641
|
-
|
|
638
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
639
|
+
const err = new Error('POLLING_TIMEOUT');
|
|
640
|
+
err.code = 'POLLING_TIMEOUT';
|
|
641
|
+
err.importStatus = importStatus;
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
642
644
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
console.error(`Last reported status for bulk import ${bulkImport.id}: ${importStatus}`);
|
|
645
|
+
return { importStatus, entitiesAll };
|
|
646
|
+
};
|
|
646
647
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
648
|
+
let pollResult;
|
|
649
|
+
try {
|
|
650
|
+
pollResult = await logger.withSpinner(
|
|
651
|
+
pollBulkImport,
|
|
652
|
+
'Waiting for bulk project import to complete... (may take some time)',
|
|
653
|
+
'Bulk import completed successfully!',
|
|
654
|
+
LOG_STAGES.import
|
|
655
|
+
);
|
|
655
656
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
657
|
+
if (spinnerOff) logger.success('Bulk import completed successfully!', LOG_STAGES.import);
|
|
658
|
+
importStatus = pollResult.importStatus;
|
|
659
|
+
entitiesAll = pollResult.entitiesAll?.length ? pollResult.entitiesAll : entitiesAll;
|
|
660
|
+
} catch (e) {
|
|
661
|
+
logger.failSpinner('✖ Bulk import did not complete');
|
|
662
|
+
logger.resetSpinner();
|
|
663
|
+
|
|
664
|
+
if (e?.code === 'POLLING_TIMEOUT') {
|
|
665
|
+
const historyUrl = buildGroupImportHistoryUrl(destUrl);
|
|
666
|
+
|
|
667
|
+
logger.print();
|
|
668
|
+
logger.error('The CLI has stopped polling for the GitLab bulk import.', LOG_STAGES.import);
|
|
669
|
+
logger.error('The migration itself may still be running inside GitLab — the CLI only waits for a limited time.', LOG_STAGES.import);
|
|
670
|
+
logger.error(`Last reported status for bulk import ${bulkImport.id}: ${e.importStatus}`, LOG_STAGES.import);
|
|
671
|
+
|
|
672
|
+
logger.print();
|
|
673
|
+
if (historyUrl) {
|
|
674
|
+
logger.info('You can continue monitoring this migration in the GitLab UI:', LOG_STAGES.import);
|
|
675
|
+
logger.info(`Group import history: ${historyUrl}`, LOG_STAGES.import);
|
|
676
|
+
} else {
|
|
677
|
+
logger.info('You can continue monitoring this migration from the Group import history page in the GitLab UI.', LOG_STAGES.import);
|
|
678
|
+
}
|
|
679
|
+
process.exit(0);
|
|
680
|
+
}
|
|
659
681
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
console.log(`Migration Results:`);
|
|
663
|
-
console.log(`Successfully migrated: ${finishedEntities.length} entities`);
|
|
664
|
-
console.log(`Failed: ${failedEntities.length} entities`);
|
|
682
|
+
throw e;
|
|
683
|
+
}
|
|
665
684
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
685
|
+
const summary = summarizeBulkImportProgress(entitiesAll);
|
|
686
|
+
|
|
687
|
+
if (importStatus === 'finished' && summary.entityFinished > 0) {
|
|
688
|
+
const newGroupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
689
|
+
|
|
690
|
+
logger.print();
|
|
691
|
+
logger.success('✔ Project group copy completed successfully.', LOG_STAGES.import);
|
|
692
|
+
logger.info('Summary:', LOG_STAGES.import);
|
|
693
|
+
logger.info(`${sourceProjects.length} projects copied successfully`, LOG_STAGES.import);
|
|
694
|
+
logger.info(`${summary.entityFinished} entities copied successfully`, LOG_STAGES.import);
|
|
695
|
+
logger.info(`${summary.entityFailed} entities failed to copy`, LOG_STAGES.import);
|
|
696
|
+
if (newGroupUrl) logger.info(`New group URL: ${newGroupUrl}`, LOG_STAGES.import);
|
|
697
|
+
|
|
698
|
+
// show failed list only in verbose (or if failures exist)
|
|
699
|
+
if (summary.entityFailed > 0) {
|
|
700
|
+
logger.print();
|
|
701
|
+
logger.warn('Failed entities:', LOG_STAGES.import);
|
|
702
|
+
entitiesAll.filter(e => e.status === 'failed').forEach(e => {
|
|
703
|
+
logger.print(`- ${e.source_type}: ${e.source_full_path} (${e.status})`);
|
|
670
704
|
});
|
|
671
705
|
}
|
|
672
|
-
const migratedGroupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
673
|
-
if (migratedGroupUrl) console.log(`\nMigrated group: ${migratedGroupUrl}`);
|
|
674
|
-
|
|
675
706
|
return 0;
|
|
676
707
|
} else {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
console.error('Failed entities:');
|
|
680
|
-
failedEntities.forEach(e => {
|
|
681
|
-
console.error(`${e.source_type}: ${e.source_full_path} (${e.status})`);
|
|
682
|
-
});
|
|
683
|
-
}
|
|
708
|
+
logger.print();
|
|
709
|
+
logger.error('✖ Bulk import failed!', LOG_STAGES.import);
|
|
684
710
|
throw new Error('GitLab bulk import failed');
|
|
685
711
|
}
|
|
686
712
|
|
|
687
713
|
} catch (error) {
|
|
688
|
-
|
|
714
|
+
logger.error(`Project group copy failed: ${error.message}`, LOG_STAGES.import);
|
|
689
715
|
throw error;
|
|
690
716
|
}
|
|
691
717
|
}
|
|
@@ -699,9 +725,11 @@ const command = new Command('copy-project-group')
|
|
|
699
725
|
.requiredOption('--dt, --dest-token <token>', 'A Git Repos and Issue Tracking personal access token from the target region. The api scope is required on the token.')
|
|
700
726
|
.requiredOption('-g, --group-id <id>', 'The id of the group to copy from the source region (e.g. "1796019"), or the group name (e.g. "mygroup") for top-level groups. For sub-groups, a path is also allowed, e.g. "mygroup/subgroup"')
|
|
701
727
|
.option('-n, --new-group-slug <slug>', '(Optional) Destination group URL slug (single path segment, e.g. "mygroup-copy"). Must be unique. Group display name remains the same as source.')
|
|
728
|
+
.option('-v, --verbose', 'Enable verbose output (debug logs + wait details)')
|
|
702
729
|
.showHelpAfterError()
|
|
703
730
|
.hook('preAction', cmd => cmd.showHelpAfterError(false)) // only show help during validation
|
|
704
731
|
.action(async (options) => {
|
|
732
|
+
logger.setVerbosity(options.verbose ? 2 : 1);
|
|
705
733
|
await directTransfer(options);
|
|
706
734
|
});
|
|
707
735
|
|