@strapi/strapi 4.6.0-beta.0 → 4.6.0-beta.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
@@ -80,14 +80,18 @@ Complete installation requirements can be found in the documentation under <a hr
80
80
  - CentOS/RHEL 8
81
81
  - macOS Mojave
82
82
  - Windows 10
83
- - Docker - [Docker-Repo](https://github.com/strapi/strapi-docker)
83
+ - Docker
84
84
 
85
85
  (Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)
86
86
 
87
87
  **Node:**
88
88
 
89
- - NodeJS >= 14 <= 18
90
- - NPM >= 6.x
89
+ Strapi only supports maintenance and LTS versions of Node.js. Please refer to the <a href="https://nodejs.org/en/about/releases/">Node.js release schedule</a> for more information. NPM versions installed by default with Node.js are supported. Generally it's recommended to use yarn over npm where possible.
90
+
91
+ | Strapi Version | Recommended | Minimum |
92
+ | -------------- | ----------- | ------- |
93
+ | 4.3.9 and up | 18.x | 14.x |
94
+ | 4.0.x to 4.3.8 | 16.x | 14.x |
91
95
 
92
96
  **Database:**
93
97
 
@@ -98,7 +102,7 @@ Complete installation requirements can be found in the documentation under <a hr
98
102
  | PostgreSQL | 11.0 | 14.0 |
99
103
  | SQLite | 3 | 3 |
100
104
 
101
- **We recommend always using the latest version of Strapi to start your new projects**.
105
+ **We recommend always using the latest version of Strapi stable to start your new projects**.
102
106
 
103
107
  ## Features
104
108
 
package/bin/strapi.js CHANGED
@@ -5,13 +5,27 @@
5
5
  // FIXME
6
6
  /* eslint-disable import/extensions */
7
7
  const _ = require('lodash');
8
+ const path = require('path');
8
9
  const resolveCwd = require('resolve-cwd');
9
10
  const { yellow } = require('chalk');
10
- const { Command } = require('commander');
11
+ const { Command, Option } = require('commander');
12
+ const inquirer = require('inquirer');
11
13
 
12
14
  const program = new Command();
13
15
 
14
16
  const packageJSON = require('../package.json');
17
+ const {
18
+ promptEncryptionKey,
19
+ confirmMessage,
20
+ parseURL,
21
+ forceOption,
22
+ } = require('../lib/commands/utils/commander');
23
+ const { ifOptions, assertUrlHasProtocol, exitWith } = require('../lib/commands/utils/helpers');
24
+ const {
25
+ excludeOption,
26
+ onlyOption,
27
+ validateExcludeOnly,
28
+ } = require('../lib/commands/transfer/utils');
15
29
 
16
30
  const checkCwdIsStrapiApp = (name) => {
17
31
  const logErrorAndExit = () => {
@@ -255,4 +269,128 @@ program
255
269
  .option('-s, --silent', `Run the generation silently, without any output`, false)
256
270
  .action(getLocalScript('ts/generate-types'));
257
271
 
272
+ if (process.env.STRAPI_EXPERIMENTAL === 'true') {
273
+ // `$ strapi transfer`
274
+ program
275
+ .command('transfer')
276
+ .description('Transfer data from one source to another')
277
+ .allowExcessArguments(false)
278
+ .addOption(
279
+ new Option(
280
+ '--from <sourceURL>',
281
+ `URL of the remote Strapi instance to get data from`
282
+ ).argParser(parseURL)
283
+ )
284
+ .addOption(
285
+ new Option(
286
+ '--to <destinationURL>',
287
+ `URL of the remote Strapi instance to send data to`
288
+ ).argParser(parseURL)
289
+ )
290
+ .addOption(forceOption)
291
+ // Validate URLs
292
+ .hook(
293
+ 'preAction',
294
+ ifOptions(
295
+ (opts) => opts.from,
296
+ (thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
297
+ )
298
+ )
299
+ .hook(
300
+ 'preAction',
301
+ ifOptions(
302
+ (opts) => opts.to,
303
+ (thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
304
+ )
305
+ )
306
+ .hook(
307
+ 'preAction',
308
+ ifOptions(
309
+ (opts) => !opts.from && !opts.to,
310
+ () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
311
+ )
312
+ )
313
+ .addOption(forceOption)
314
+ .addOption(excludeOption)
315
+ .addOption(onlyOption)
316
+ .hook('preAction', validateExcludeOnly)
317
+ .hook(
318
+ 'preAction',
319
+ confirmMessage(
320
+ 'The import will delete all data in the remote database. Are you sure you want to proceed?'
321
+ )
322
+ )
323
+ .action(getLocalScript('transfer/transfer'));
324
+ }
325
+
326
+ // `$ strapi export`
327
+ program
328
+ .command('export')
329
+ .description('Export data from Strapi to file')
330
+ .allowExcessArguments(false)
331
+ .addOption(
332
+ new Option('--no-encrypt', `Disables 'aes-128-ecb' encryption of the output file`).default(true)
333
+ )
334
+ .addOption(new Option('--no-compress', 'Disables gzip compression of output file').default(true))
335
+ .addOption(
336
+ new Option(
337
+ '-k, --key <string>',
338
+ 'Provide encryption key in command instead of using the prompt'
339
+ )
340
+ )
341
+ .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
342
+ .addOption(excludeOption)
343
+ .addOption(onlyOption)
344
+ .hook('preAction', validateExcludeOnly)
345
+ .hook('preAction', promptEncryptionKey)
346
+ .action(getLocalScript('transfer/export'));
347
+
348
+ // `$ strapi import`
349
+ program
350
+ .command('import')
351
+ .description('Import data from file to Strapi')
352
+ .allowExcessArguments(false)
353
+ .requiredOption(
354
+ '-f, --file <file>',
355
+ 'path and filename for the Strapi export file you want to import'
356
+ )
357
+ .addOption(
358
+ new Option(
359
+ '-k, --key <string>',
360
+ 'Provide encryption key in command instead of using the prompt'
361
+ )
362
+ )
363
+ .addOption(forceOption)
364
+ .addOption(excludeOption)
365
+ .addOption(onlyOption)
366
+ .hook('preAction', validateExcludeOnly)
367
+ .hook('preAction', async (thisCommand) => {
368
+ const opts = thisCommand.opts();
369
+ const ext = path.extname(String(opts.file));
370
+
371
+ // check extension to guess if we should prompt for key
372
+ if (ext === '.enc') {
373
+ if (!opts.key) {
374
+ const answers = await inquirer.prompt([
375
+ {
376
+ type: 'password',
377
+ message: 'Please enter your decryption key',
378
+ name: 'key',
379
+ },
380
+ ]);
381
+ if (!answers.key?.length) {
382
+ exitWith(0, 'No key entered, aborting import.');
383
+ }
384
+ opts.key = answers.key;
385
+ }
386
+ }
387
+ })
388
+ .hook(
389
+ 'preAction',
390
+ confirmMessage(
391
+ 'The import will delete all data in your database. Are you sure you want to proceed?'
392
+ )
393
+ )
394
+ .action(getLocalScript('transfer/import'));
395
+
258
396
  program.parseAsync(process.argv);
package/lib/Strapi.js CHANGED
@@ -225,7 +225,7 @@ class Strapi {
225
225
 
226
226
  await this.runLifecyclesFunctions(LIFECYCLES.DESTROY);
227
227
 
228
- this.eventHub.removeAllListeners();
228
+ this.eventHub.destroy();
229
229
 
230
230
  if (_.has(this, 'db')) {
231
231
  await this.db.destroy();
@@ -242,11 +242,14 @@ class Strapi {
242
242
  sendStartupTelemetry() {
243
243
  // Emit started event.
244
244
  // do not await to avoid slower startup
245
+ // This event is anonymous
245
246
  this.telemetry.send('didStartServer', {
246
- database: strapi.config.get('database.connection.client'),
247
- plugins: Object.keys(strapi.plugins),
248
- // TODO: to add back
249
- // providers: this.config.installedProviders,
247
+ groupProperties: {
248
+ database: strapi.config.get('database.connection.client'),
249
+ plugins: Object.keys(strapi.plugins),
250
+ // TODO: to add back
251
+ // providers: this.config.installedProviders,
252
+ },
250
253
  });
251
254
  }
252
255
 
@@ -53,12 +53,12 @@ const generateNewPackageJSON = (packageObj) => {
53
53
 
54
54
  const sendEvent = async (uuid) => {
55
55
  try {
56
- await fetch('https://analytics.strapi.io/track', {
56
+ await fetch('https://analytics.strapi.io/api/v2/track', {
57
57
  method: 'POST',
58
58
  body: JSON.stringify({
59
59
  event: 'didOptInTelemetry',
60
- uuid,
61
60
  deviceId: machineID(),
61
+ groupProperties: { projectId: uuid },
62
62
  }),
63
63
  headers: { 'Content-Type': 'application/json' },
64
64
  });
@@ -28,12 +28,12 @@ const writePackageJSON = async (path, file, spacing) => {
28
28
 
29
29
  const sendEvent = async (uuid) => {
30
30
  try {
31
- await fetch('https://analytics.strapi.io/track', {
31
+ await fetch('https://analytics.strapi.io/api/v2/track', {
32
32
  method: 'POST',
33
33
  body: JSON.stringify({
34
34
  event: 'didOptOutTelemetry',
35
- uuid,
36
35
  deviceId: machineID(),
36
+ groupProperties: { projectId: uuid },
37
37
  }),
38
38
  headers: { 'Content-Type': 'application/json' },
39
39
  });
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ providers: { createLocalFileDestinationProvider },
5
+ } = require('@strapi/data-transfer/lib/file');
6
+ const {
7
+ providers: { createLocalStrapiSourceProvider },
8
+ } = require('@strapi/data-transfer/lib/strapi');
9
+ const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
10
+ const { isObject, isString, isFinite, toNumber } = require('lodash/fp');
11
+ const fs = require('fs-extra');
12
+ const chalk = require('chalk');
13
+
14
+ const {
15
+ getDefaultExportName,
16
+ buildTransferTable,
17
+ DEFAULT_IGNORED_CONTENT_TYPES,
18
+ createStrapiInstance,
19
+ } = require('./utils');
20
+
21
+ /**
22
+ * @typedef ImportCommandOptions Options given to the CLI import command
23
+ *
24
+ * @property {string} [file] The file path to import
25
+ * @property {boolean} [encrypt] Used to encrypt the final archive
26
+ * @property {string} [key] Encryption key, only useful when encryption is enabled
27
+ * @property {boolean} [compress] Used to compress the final archive
28
+ */
29
+
30
+ const logger = console;
31
+
32
+ const BYTES_IN_MB = 1024 * 1024;
33
+
34
+ /**
35
+ * Import command.
36
+ *
37
+ * It transfers data from a local file to a local strapi instance
38
+ *
39
+ * @param {ImportCommandOptions} opts
40
+ */
41
+ module.exports = async (opts) => {
42
+ // Validate inputs from Commander
43
+ if (!isObject(opts)) {
44
+ logger.error('Could not parse command arguments');
45
+ process.exit(1);
46
+ }
47
+
48
+ const strapi = await createStrapiInstance();
49
+
50
+ const source = createSourceProvider(strapi);
51
+ const destination = createDestinationProvider(opts);
52
+
53
+ const engine = createTransferEngine(source, destination, {
54
+ versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
55
+ schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
56
+ exclude: opts.exclude,
57
+ only: opts.only,
58
+ transforms: {
59
+ links: [
60
+ {
61
+ filter(link) {
62
+ return (
63
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
64
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
65
+ );
66
+ },
67
+ },
68
+ ],
69
+ entities: [
70
+ {
71
+ filter(entity) {
72
+ return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ });
78
+
79
+ const progress = engine.progress.stream;
80
+
81
+ const getTelemetryPayload = (/* payload */) => {
82
+ return {
83
+ eventProperties: {
84
+ source: engine.sourceProvider.name,
85
+ destination: engine.destinationProvider.name,
86
+ },
87
+ };
88
+ };
89
+
90
+ progress.on('transfer::start', async () => {
91
+ logger.log(`Starting export...`);
92
+ await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
93
+ });
94
+
95
+ try {
96
+ const results = await engine.transfer();
97
+ const outFile = results.destination.file.path;
98
+
99
+ const table = buildTransferTable(results.engine);
100
+ logger.log(table.toString());
101
+
102
+ const outFileExists = await fs.pathExists(outFile);
103
+ if (!outFileExists) {
104
+ throw new Error(`Export file not created "${outFile}"`);
105
+ }
106
+
107
+ logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
108
+ logger.log(`Export archive is in ${chalk.green(outFile)}`);
109
+ } catch (e) {
110
+ await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
111
+ logger.error('Export process failed unexpectedly:', e.toString());
112
+ process.exit(1);
113
+ }
114
+
115
+ // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
116
+ await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
117
+ process.exit(0);
118
+ };
119
+
120
+ /**
121
+ * It creates a local strapi destination provider
122
+ */
123
+ const createSourceProvider = (strapi) => {
124
+ return createLocalStrapiSourceProvider({
125
+ async getStrapi() {
126
+ return strapi;
127
+ },
128
+ });
129
+ };
130
+
131
+ /**
132
+ * It creates a local file destination provider based on the given options
133
+ *
134
+ * @param {ImportCommandOptions} opts
135
+ */
136
+ const createDestinationProvider = (opts) => {
137
+ const { file, compress, encrypt, key, maxSizeJsonl } = opts;
138
+
139
+ const filepath = isString(file) && file.length > 0 ? file : getDefaultExportName();
140
+
141
+ const maxSizeJsonlInMb = isFinite(toNumber(maxSizeJsonl))
142
+ ? toNumber(maxSizeJsonl) * BYTES_IN_MB
143
+ : undefined;
144
+
145
+ return createLocalFileDestinationProvider({
146
+ file: {
147
+ path: filepath,
148
+ maxSizeJsonl: maxSizeJsonlInMb,
149
+ },
150
+ encryption: {
151
+ enabled: encrypt,
152
+ key: encrypt ? key : undefined,
153
+ },
154
+ compression: {
155
+ enabled: compress,
156
+ },
157
+ });
158
+ };
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ providers: { createLocalFileSourceProvider },
5
+ } = require('@strapi/data-transfer/lib/file');
6
+ const {
7
+ providers: { createLocalStrapiDestinationProvider, DEFAULT_CONFLICT_STRATEGY },
8
+ } = require('@strapi/data-transfer/lib/strapi');
9
+ const {
10
+ createTransferEngine,
11
+ DEFAULT_VERSION_STRATEGY,
12
+ DEFAULT_SCHEMA_STRATEGY,
13
+ } = require('@strapi/data-transfer/lib/engine');
14
+
15
+ const { isObject } = require('lodash/fp');
16
+ const path = require('path');
17
+
18
+ const strapi = require('../../index');
19
+ const { buildTransferTable, DEFAULT_IGNORED_CONTENT_TYPES } = require('./utils');
20
+
21
+ /**
22
+ * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
23
+ */
24
+
25
+ const logger = console;
26
+
27
+ module.exports = async (opts) => {
28
+ // validate inputs from Commander
29
+ if (!isObject(opts)) {
30
+ logger.error('Could not parse arguments');
31
+ process.exit(1);
32
+ }
33
+
34
+ /**
35
+ * From strapi backup file
36
+ */
37
+ const sourceOptions = getLocalFileSourceOptions(opts);
38
+
39
+ const source = createLocalFileSourceProvider(sourceOptions);
40
+
41
+ /**
42
+ * To local Strapi instance
43
+ */
44
+ const strapiInstance = await strapi(await strapi.compile()).load();
45
+
46
+ const destinationOptions = {
47
+ async getStrapi() {
48
+ return strapiInstance;
49
+ },
50
+ autoDestroy: false,
51
+ strategy: opts.conflictStrategy || DEFAULT_CONFLICT_STRATEGY,
52
+ restore: {
53
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
54
+ },
55
+ };
56
+ const destination = createLocalStrapiDestinationProvider(destinationOptions);
57
+
58
+ /**
59
+ * Configure and run the transfer engine
60
+ */
61
+ const engineOptions = {
62
+ versionStrategy: opts.versionStrategy || DEFAULT_VERSION_STRATEGY,
63
+ schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
64
+ exclude: opts.exclude,
65
+ only: opts.only,
66
+ rules: {
67
+ links: [
68
+ {
69
+ filter(link) {
70
+ return (
71
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
72
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
73
+ );
74
+ },
75
+ },
76
+ ],
77
+ entities: [
78
+ {
79
+ filter: (entity) => !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type),
80
+ },
81
+ ],
82
+ },
83
+ };
84
+
85
+ const engine = createTransferEngine(source, destination, engineOptions);
86
+
87
+ const progress = engine.progress.stream;
88
+ const getTelemetryPayload = () => {
89
+ return {
90
+ eventProperties: {
91
+ source: engine.sourceProvider.name,
92
+ destination: engine.destinationProvider.name,
93
+ },
94
+ };
95
+ };
96
+
97
+ progress.on('transfer::start', async () => {
98
+ logger.info('Starting import...');
99
+ await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
100
+ });
101
+
102
+ try {
103
+ const results = await engine.transfer();
104
+ const table = buildTransferTable(results.engine);
105
+ logger.info(table.toString());
106
+
107
+ logger.info('Import process has been completed successfully!');
108
+ } catch (e) {
109
+ await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
110
+ logger.error('Import process failed unexpectedly:');
111
+ logger.error(e);
112
+ process.exit(1);
113
+ }
114
+
115
+ // Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
116
+ await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
117
+ await strapiInstance.destroy();
118
+
119
+ process.exit(0);
120
+ };
121
+
122
+ /**
123
+ * Infer local file source provider options based on a given filename
124
+ *
125
+ * @param {{ file: string; key?: string }} opts
126
+ *
127
+ * @return {ILocalFileSourceProviderOptions}
128
+ */
129
+ const getLocalFileSourceOptions = (opts) => {
130
+ /**
131
+ * @type {ILocalFileSourceProviderOptions}
132
+ */
133
+ const options = {
134
+ file: { path: opts.file },
135
+ compression: { enabled: false },
136
+ encryption: { enabled: false },
137
+ };
138
+
139
+ const { extname, parse } = path;
140
+
141
+ let file = options.file.path;
142
+
143
+ if (extname(file) === '.enc') {
144
+ file = parse(file).name;
145
+ options.encryption = { enabled: true, key: opts.key };
146
+ }
147
+
148
+ if (extname(file) === '.gz') {
149
+ file = parse(file).name;
150
+ options.compression = { enabled: true };
151
+ }
152
+
153
+ return options;
154
+ };
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const { createTransferEngine } = require('@strapi/data-transfer/lib/engine');
4
+ const {
5
+ providers: {
6
+ createRemoteStrapiDestinationProvider,
7
+ createLocalStrapiSourceProvider,
8
+ createLocalStrapiDestinationProvider,
9
+ },
10
+ } = require('@strapi/data-transfer/lib/strapi');
11
+ const { isObject } = require('lodash/fp');
12
+ const chalk = require('chalk');
13
+
14
+ const {
15
+ buildTransferTable,
16
+ createStrapiInstance,
17
+ DEFAULT_IGNORED_CONTENT_TYPES,
18
+ } = require('./utils');
19
+
20
+ const logger = console;
21
+
22
+ /**
23
+ * @typedef TransferCommandOptions Options given to the CLI transfer command
24
+ *
25
+ * @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
26
+ * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
27
+ */
28
+
29
+ /**
30
+ * Transfer command.
31
+ *
32
+ * It transfers data from a local file to a local strapi instance
33
+ *
34
+ * @param {TransferCommandOptions} opts
35
+ */
36
+ module.exports = async (opts) => {
37
+ // Validate inputs from Commander
38
+ if (!isObject(opts)) {
39
+ logger.error('Could not parse command arguments');
40
+ process.exit(1);
41
+ }
42
+
43
+ const strapi = await createStrapiInstance();
44
+
45
+ let source;
46
+ let destination;
47
+
48
+ if (!opts.from && !opts.to) {
49
+ logger.error('At least one source (from) or destination (to) option must be provided');
50
+ process.exit(1);
51
+ }
52
+
53
+ // if no URL provided, use local Strapi
54
+ if (!opts.from) {
55
+ source = createLocalStrapiSourceProvider({
56
+ getStrapi: () => strapi,
57
+ });
58
+ }
59
+ // if URL provided, set up a remote source provider
60
+ else {
61
+ logger.error(`Remote Strapi source provider not yet implemented`);
62
+ process.exit(1);
63
+ }
64
+
65
+ // if no URL provided, use local Strapi
66
+ if (!opts.to) {
67
+ destination = createLocalStrapiDestinationProvider({
68
+ getStrapi: () => strapi,
69
+ });
70
+ }
71
+ // if URL provided, set up a remote destination provider
72
+ else {
73
+ destination = createRemoteStrapiDestinationProvider({
74
+ url: opts.to,
75
+ auth: false,
76
+ strategy: 'restore',
77
+ restore: {
78
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
79
+ },
80
+ });
81
+ }
82
+
83
+ if (!source || !destination) {
84
+ logger.error('Could not create providers');
85
+ process.exit(1);
86
+ }
87
+
88
+ const engine = createTransferEngine(source, destination, {
89
+ versionStrategy: 'strict',
90
+ schemaStrategy: 'strict',
91
+ transforms: {
92
+ links: [
93
+ {
94
+ filter(link) {
95
+ return (
96
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
97
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
98
+ );
99
+ },
100
+ },
101
+ ],
102
+ entities: [
103
+ {
104
+ filter(entity) {
105
+ return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
106
+ },
107
+ },
108
+ ],
109
+ },
110
+ });
111
+
112
+ try {
113
+ logger.log(`Starting transfer...`);
114
+
115
+ const results = await engine.transfer();
116
+
117
+ const table = buildTransferTable(results.engine);
118
+ logger.log(table.toString());
119
+
120
+ logger.log(`${chalk.bold('Transfer process has been completed successfully!')}`);
121
+ process.exit(0);
122
+ } catch (e) {
123
+ logger.error('Transfer process failed unexpectedly');
124
+ logger.error(e);
125
+ process.exit(1);
126
+ }
127
+ };