@strapi/strapi 4.6.0-beta.1 → 4.6.0

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.
@@ -1,25 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- createLocalFileDestinationProvider,
5
- createLocalStrapiSourceProvider,
6
- createTransferEngine,
7
- // TODO: we need to solve this issue with typescript modules
8
- // eslint-disable-next-line import/no-unresolved, node/no-missing-require
9
- } = require('@strapi/data-transfer');
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
10
  const { isObject, isString, isFinite, toNumber } = require('lodash/fp');
11
11
  const fs = require('fs-extra');
12
12
  const chalk = require('chalk');
13
13
 
14
+ const { TransferEngineTransferError } = require('@strapi/data-transfer/lib/engine/errors');
14
15
  const {
15
16
  getDefaultExportName,
16
17
  buildTransferTable,
17
18
  DEFAULT_IGNORED_CONTENT_TYPES,
18
19
  createStrapiInstance,
20
+ formatDiagnostic,
19
21
  } = require('./utils');
20
22
 
21
23
  /**
22
- * @typedef ImportCommandOptions Options given to the CLI import command
24
+ * @typedef ExportCommandOptions Options given to the CLI import command
23
25
  *
24
26
  * @property {string} [file] The file path to import
25
27
  * @property {boolean} [encrypt] Used to encrypt the final archive
@@ -32,11 +34,11 @@ const logger = console;
32
34
  const BYTES_IN_MB = 1024 * 1024;
33
35
 
34
36
  /**
35
- * Import command.
37
+ * Export command.
36
38
  *
37
- * It transfers data from a local file to a local strapi instance
39
+ * It transfers data from a local Strapi instance to a file
38
40
  *
39
- * @param {ImportCommandOptions} opts
41
+ * @param {ExportCommandOptions} opts
40
42
  */
41
43
  module.exports = async (opts) => {
42
44
  // Validate inputs from Commander
@@ -53,6 +55,8 @@ module.exports = async (opts) => {
53
55
  const engine = createTransferEngine(source, destination, {
54
56
  versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
55
57
  schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
58
+ exclude: opts.exclude,
59
+ only: opts.only,
56
60
  transforms: {
57
61
  links: [
58
62
  {
@@ -74,32 +78,25 @@ module.exports = async (opts) => {
74
78
  },
75
79
  });
76
80
 
77
- try {
78
- logger.log(`Starting export...`);
81
+ engine.diagnostics.onDiagnostic(formatDiagnostic('export'));
79
82
 
80
- const progress = engine.progress.stream;
83
+ const progress = engine.progress.stream;
81
84
 
82
- const telemetryPayload = (/* payload */) => {
83
- return {
84
- eventProperties: {
85
- source: engine.sourceProvider.name,
86
- destination: engine.destinationProvider.name,
87
- },
88
- };
85
+ const getTelemetryPayload = (/* payload */) => {
86
+ return {
87
+ eventProperties: {
88
+ source: engine.sourceProvider.name,
89
+ destination: engine.destinationProvider.name,
90
+ },
89
91
  };
92
+ };
90
93
 
91
- progress.on('transfer::start', (payload) => {
92
- strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
93
- });
94
-
95
- progress.on('transfer::finish', (payload) => {
96
- strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
97
- });
98
-
99
- progress.on('transfer::error', (payload) => {
100
- strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
101
- });
94
+ progress.on('transfer::start', async () => {
95
+ logger.log(`Starting export...`);
96
+ await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
97
+ });
102
98
 
99
+ try {
103
100
  const results = await engine.transfer();
104
101
  const outFile = results.destination.file.path;
105
102
 
@@ -108,16 +105,20 @@ module.exports = async (opts) => {
108
105
 
109
106
  const outFileExists = await fs.pathExists(outFile);
110
107
  if (!outFileExists) {
111
- throw new Error(`Export file not created "${outFile}"`);
108
+ throw new TransferEngineTransferError(`Export file not created "${outFile}"`);
112
109
  }
113
110
 
114
111
  logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
115
112
  logger.log(`Export archive is in ${chalk.green(outFile)}`);
116
- process.exit(0);
117
- } catch (e) {
118
- logger.error('Export process failed unexpectedly:', e.toString());
113
+ } catch {
114
+ await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
115
+ logger.error('Export process failed.');
119
116
  process.exit(1);
120
117
  }
118
+
119
+ // 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
120
+ await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
121
+ process.exit(0);
121
122
  };
122
123
 
123
124
  /**
@@ -134,7 +135,7 @@ const createSourceProvider = (strapi) => {
134
135
  /**
135
136
  * It creates a local file destination provider based on the given options
136
137
  *
137
- * @param {ImportCommandOptions} opts
138
+ * @param {ExportCommandOptions} opts
138
139
  */
139
140
  const createDestinationProvider = (opts) => {
140
141
  const { file, compress, encrypt, key, maxSizeJsonl } = opts;
@@ -1,20 +1,25 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- createLocalFileSourceProvider,
5
- createLocalStrapiDestinationProvider,
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 {
6
10
  createTransferEngine,
7
11
  DEFAULT_VERSION_STRATEGY,
8
12
  DEFAULT_SCHEMA_STRATEGY,
9
- DEFAULT_CONFLICT_STRATEGY,
10
- // TODO: we need to solve this issue with typescript modules
11
- // eslint-disable-next-line import/no-unresolved, node/no-missing-require
12
- } = require('@strapi/data-transfer');
13
+ } = require('@strapi/data-transfer/lib/engine');
14
+
13
15
  const { isObject } = require('lodash/fp');
14
- const path = require('path');
15
16
 
16
- const strapi = require('../../index');
17
- const { buildTransferTable, DEFAULT_IGNORED_CONTENT_TYPES } = require('./utils');
17
+ const {
18
+ buildTransferTable,
19
+ DEFAULT_IGNORED_CONTENT_TYPES,
20
+ createStrapiInstance,
21
+ formatDiagnostic,
22
+ } = require('./utils');
18
23
 
19
24
  /**
20
25
  * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
@@ -39,12 +44,13 @@ module.exports = async (opts) => {
39
44
  /**
40
45
  * To local Strapi instance
41
46
  */
42
- const strapiInstance = await strapi(await strapi.compile()).load();
47
+ const strapiInstance = await createStrapiInstance();
43
48
 
44
49
  const destinationOptions = {
45
50
  async getStrapi() {
46
51
  return strapiInstance;
47
52
  },
53
+ autoDestroy: false,
48
54
  strategy: opts.conflictStrategy || DEFAULT_CONFLICT_STRATEGY,
49
55
  restore: {
50
56
  entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
@@ -59,6 +65,7 @@ module.exports = async (opts) => {
59
65
  versionStrategy: opts.versionStrategy || DEFAULT_VERSION_STRATEGY,
60
66
  schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
61
67
  exclude: opts.exclude,
68
+ only: opts.only,
62
69
  rules: {
63
70
  links: [
64
71
  {
@@ -77,44 +84,43 @@ module.exports = async (opts) => {
77
84
  ],
78
85
  },
79
86
  };
87
+
80
88
  const engine = createTransferEngine(source, destination, engineOptions);
81
89
 
82
- try {
83
- logger.info('Starting import...');
90
+ engine.diagnostics.onDiagnostic(formatDiagnostic('import'));
84
91
 
85
- const progress = engine.progress.stream;
86
- const telemetryPayload = (/* payload */) => {
87
- return {
88
- eventProperties: {
89
- source: engine.sourceProvider.name,
90
- destination: engine.destinationProvider.name,
91
- },
92
- };
92
+ const progress = engine.progress.stream;
93
+ const getTelemetryPayload = () => {
94
+ return {
95
+ eventProperties: {
96
+ source: engine.sourceProvider.name,
97
+ destination: engine.destinationProvider.name,
98
+ },
93
99
  };
100
+ };
94
101
 
95
- progress.on('transfer::start', (payload) => {
96
- strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
97
- });
98
-
99
- progress.on('transfer::finish', (payload) => {
100
- strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
101
- });
102
-
103
- progress.on('transfer::error', (payload) => {
104
- strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
105
- });
102
+ progress.on('transfer::start', async () => {
103
+ logger.info('Starting import...');
104
+ await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
105
+ });
106
106
 
107
+ try {
107
108
  const results = await engine.transfer();
108
109
  const table = buildTransferTable(results.engine);
109
110
  logger.info(table.toString());
110
111
 
111
112
  logger.info('Import process has been completed successfully!');
112
- process.exit(0);
113
113
  } catch (e) {
114
- logger.error('Import process failed unexpectedly:');
115
- logger.error(e);
114
+ await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
115
+ logger.error('Import process failed.');
116
116
  process.exit(1);
117
117
  }
118
+
119
+ // 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
120
+ await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
121
+ await strapiInstance.destroy();
122
+
123
+ process.exit(0);
118
124
  };
119
125
 
120
126
  /**
@@ -130,23 +136,9 @@ const getLocalFileSourceOptions = (opts) => {
130
136
  */
131
137
  const options = {
132
138
  file: { path: opts.file },
133
- compression: { enabled: false },
134
- encryption: { enabled: false },
139
+ compression: { enabled: !!opts.decompress },
140
+ encryption: { enabled: !!opts.decrypt, key: opts.key },
135
141
  };
136
142
 
137
- const { extname, parse } = path;
138
-
139
- let file = options.file.path;
140
-
141
- if (extname(file) === '.enc') {
142
- file = parse(file).name;
143
- options.encryption = { enabled: true, key: opts.key };
144
- }
145
-
146
- if (extname(file) === '.gz') {
147
- file = parse(file).name;
148
- options.compression = { enabled: true };
149
- }
150
-
151
143
  return options;
152
144
  };
@@ -0,0 +1,129 @@
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
+ formatDiagnostic,
19
+ } = require('./utils');
20
+
21
+ const logger = console;
22
+
23
+ /**
24
+ * @typedef TransferCommandOptions Options given to the CLI transfer command
25
+ *
26
+ * @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
27
+ * @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
28
+ */
29
+
30
+ /**
31
+ * Transfer command.
32
+ *
33
+ * It transfers data from a local file to a local strapi instance
34
+ *
35
+ * @param {TransferCommandOptions} opts
36
+ */
37
+ module.exports = async (opts) => {
38
+ // Validate inputs from Commander
39
+ if (!isObject(opts)) {
40
+ logger.error('Could not parse command arguments');
41
+ process.exit(1);
42
+ }
43
+
44
+ const strapi = await createStrapiInstance();
45
+
46
+ let source;
47
+ let destination;
48
+
49
+ if (!opts.from && !opts.to) {
50
+ logger.error('At least one source (from) or destination (to) option must be provided');
51
+ process.exit(1);
52
+ }
53
+
54
+ // if no URL provided, use local Strapi
55
+ if (!opts.from) {
56
+ source = createLocalStrapiSourceProvider({
57
+ getStrapi: () => strapi,
58
+ });
59
+ }
60
+ // if URL provided, set up a remote source provider
61
+ else {
62
+ logger.error(`Remote Strapi source provider not yet implemented`);
63
+ process.exit(1);
64
+ }
65
+
66
+ // if no URL provided, use local Strapi
67
+ if (!opts.to) {
68
+ destination = createLocalStrapiDestinationProvider({
69
+ getStrapi: () => strapi,
70
+ });
71
+ }
72
+ // if URL provided, set up a remote destination provider
73
+ else {
74
+ destination = createRemoteStrapiDestinationProvider({
75
+ url: opts.to,
76
+ auth: false,
77
+ strategy: 'restore',
78
+ restore: {
79
+ entities: { exclude: DEFAULT_IGNORED_CONTENT_TYPES },
80
+ },
81
+ });
82
+ }
83
+
84
+ if (!source || !destination) {
85
+ logger.error('Could not create providers');
86
+ process.exit(1);
87
+ }
88
+
89
+ const engine = createTransferEngine(source, destination, {
90
+ versionStrategy: 'strict',
91
+ schemaStrategy: 'strict',
92
+ transforms: {
93
+ links: [
94
+ {
95
+ filter(link) {
96
+ return (
97
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
98
+ !DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
99
+ );
100
+ },
101
+ },
102
+ ],
103
+ entities: [
104
+ {
105
+ filter(entity) {
106
+ return !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type);
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ });
112
+
113
+ engine.diagnostics.onDiagnostic(formatDiagnostic('transfer'));
114
+
115
+ try {
116
+ logger.log(`Starting transfer...`);
117
+
118
+ const results = await engine.transfer();
119
+
120
+ const table = buildTransferTable(results.engine);
121
+ logger.log(table.toString());
122
+
123
+ logger.log(`${chalk.bold('Transfer process has been completed successfully!')}`);
124
+ process.exit(0);
125
+ } catch (e) {
126
+ logger.error('Transfer process failed.');
127
+ process.exit(1);
128
+ }
129
+ };
@@ -2,8 +2,16 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const Table = require('cli-table3');
5
- const { readableBytes } = require('../utils');
5
+ const { Option } = require('commander');
6
+ const { TransferGroupPresets } = require('@strapi/data-transfer/lib/engine');
7
+
8
+ const {
9
+ configs: { createOutputFileConfiguration },
10
+ createLogger,
11
+ } = require('@strapi/logger');
12
+ const { readableBytes, exitWith } = require('../utils/helpers');
6
13
  const strapi = require('../../index');
14
+ const { getParseListWithChoices } = require('../utils/commander');
7
15
 
8
16
  const pad = (n) => {
9
17
  return (n < 10 ? '0' : '') + String(n);
@@ -74,21 +82,100 @@ const DEFAULT_IGNORED_CONTENT_TYPES = [
74
82
  'admin::role',
75
83
  'admin::api-token',
76
84
  'admin::api-token-permission',
85
+ 'admin::audit-log',
77
86
  ];
78
87
 
79
88
  const createStrapiInstance = async (logLevel = 'error') => {
80
- const appContext = await strapi.compile();
81
- const app = strapi(appContext);
89
+ try {
90
+ const appContext = await strapi.compile();
91
+ const app = strapi(appContext);
92
+
93
+ app.log.level = logLevel;
94
+ return await app.load();
95
+ } catch (err) {
96
+ if (err.code === 'ECONNREFUSED') {
97
+ throw new Error('Process failed. Check the database connection with your Strapi project.');
98
+ }
99
+ throw err;
100
+ }
101
+ };
102
+
103
+ const transferDataTypes = Object.keys(TransferGroupPresets);
104
+
105
+ const excludeOption = new Option(
106
+ '--exclude <comma-separated data types>',
107
+ `Exclude data using comma-separated types. Available types: ${transferDataTypes.join(',')}`
108
+ ).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "exclude"'));
109
+
110
+ const onlyOption = new Option(
111
+ '--only <command-separated data types>',
112
+ `Include only these types of data (plus schemas). Available types: ${transferDataTypes.join(',')}`
113
+ ).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "only"'));
82
114
 
83
- app.log.level = logLevel;
115
+ const validateExcludeOnly = (command) => {
116
+ const { exclude, only } = command.opts();
117
+ if (!only || !exclude) {
118
+ return;
119
+ }
84
120
 
85
- return app.load();
121
+ const choicesInBoth = only.filter((n) => {
122
+ return exclude.indexOf(n) !== -1;
123
+ });
124
+ if (choicesInBoth.length > 0) {
125
+ exitWith(
126
+ 1,
127
+ `Data types may not be used in both "exclude" and "only" in the same command. Found in both: ${choicesInBoth.join(
128
+ ','
129
+ )}`
130
+ );
131
+ }
132
+ };
133
+
134
+ const errorColors = {
135
+ fatal: chalk.red,
136
+ error: chalk.red,
137
+ silly: chalk.yellow,
86
138
  };
87
139
 
140
+ const formatDiagnostic =
141
+ (operation) =>
142
+ ({ details, kind }) => {
143
+ const logger = createLogger(
144
+ createOutputFileConfiguration(`${operation}_error_log_${Date.now()}.log`)
145
+ );
146
+ try {
147
+ if (kind === 'error') {
148
+ const { message, severity = 'fatal' } = details;
149
+
150
+ const colorizeError = errorColors[severity];
151
+ const errorMessage = colorizeError(`[${severity.toUpperCase()}] ${message}`);
152
+
153
+ logger.error(errorMessage);
154
+ }
155
+ if (kind === 'info') {
156
+ const { message, params } = details;
157
+
158
+ const msg = `${message}\n${params ? JSON.stringify(params, null, 2) : ''}`;
159
+
160
+ logger.info(msg);
161
+ }
162
+ if (kind === 'warning') {
163
+ const { origin, message } = details;
164
+
165
+ logger.warn(`(${origin ?? 'transfer'}) ${message}`);
166
+ }
167
+ } catch (err) {
168
+ logger.error(err);
169
+ }
170
+ };
171
+
88
172
  module.exports = {
89
173
  buildTransferTable,
90
174
  getDefaultExportName,
91
- yyyymmddHHMMSS,
92
175
  DEFAULT_IGNORED_CONTENT_TYPES,
93
176
  createStrapiInstance,
177
+ excludeOption,
178
+ onlyOption,
179
+ validateExcludeOnly,
180
+ formatDiagnostic,
94
181
  };
@@ -1,12 +1,59 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * This file includes hooks to use for commander.hook and argParsers for commander.argParser
5
+ */
6
+
3
7
  const inquirer = require('inquirer');
8
+ const { InvalidOptionArgumentError, Option } = require('commander');
9
+ const { bold, green, cyan } = require('chalk');
10
+ const { exitWith } = require('./helpers');
11
+
12
+ /**
13
+ * argParser: Parse a comma-delimited string as an array
14
+ */
15
+ const parseList = (value) => {
16
+ let list;
17
+ try {
18
+ list = value.split(',').map((item) => item.trim()); // trim shouldn't be necessary but might help catch unexpected whitespace characters
19
+ } catch (e) {
20
+ exitWith(1, `Unrecognized input: ${value}`);
21
+ }
22
+ return list;
23
+ };
24
+
25
+ /**
26
+ * Returns an argParser that returns a list
27
+ */
28
+ const getParseListWithChoices = (choices, errorMessage = 'Invalid options:') => {
29
+ return (value) => {
30
+ const list = parseList(value);
31
+ const invalid = list.filter((item) => {
32
+ return !choices.includes(item);
33
+ });
34
+
35
+ if (invalid.length > 0) {
36
+ exitWith(1, `${errorMessage}: ${invalid.join(',')}`);
37
+ }
38
+
39
+ return list;
40
+ };
41
+ };
4
42
 
5
43
  /**
6
- * argsParser: Parse a comma-delimited string as an array
44
+ * argParser: Parse a string as a URL object
7
45
  */
8
- const parseInputList = (value) => {
9
- return value.split(',');
46
+ const parseURL = (value) => {
47
+ try {
48
+ const url = new URL(value);
49
+ if (!url.host) {
50
+ throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
51
+ }
52
+
53
+ return url;
54
+ } catch (e) {
55
+ throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
56
+ }
10
57
  };
11
58
 
12
59
  /**
@@ -16,8 +63,7 @@ const promptEncryptionKey = async (thisCommand) => {
16
63
  const opts = thisCommand.opts();
17
64
 
18
65
  if (!opts.encrypt && opts.key) {
19
- console.error('Key may not be present unless encryption is used');
20
- process.exit(1);
66
+ return exitWith(1, 'Key may not be present unless encryption is used');
21
67
  }
22
68
 
23
69
  // if encrypt==true but we have no key, prompt for it
@@ -37,21 +83,30 @@ const promptEncryptionKey = async (thisCommand) => {
37
83
  ]);
38
84
  opts.key = answers.key;
39
85
  } catch (e) {
40
- console.error('Failed to get encryption key');
41
- process.exit(1);
86
+ return exitWith(1, 'Failed to get encryption key');
42
87
  }
43
88
  if (!opts.key) {
44
- console.error('Failed to get encryption key');
45
- process.exit(1);
89
+ return exitWith(1, 'Failed to get encryption key');
46
90
  }
47
91
  }
48
92
  };
49
93
 
50
94
  /**
51
- * hook: require a confirmation message to be accepted
95
+ * hook: require a confirmation message to be accepted unless forceOption (-f,--force) is used
96
+ *
97
+ * @param {string} message The message to confirm with user
98
+ * @param {object} options Additional options
52
99
  */
53
100
  const confirmMessage = (message) => {
54
- return async () => {
101
+ return async (command) => {
102
+ // if we have a force option, assume yes
103
+ const opts = command.opts();
104
+ if (opts?.force === true) {
105
+ // attempt to mimic the inquirer prompt exactly
106
+ console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
107
+ return;
108
+ }
109
+
55
110
  const answers = await inquirer.prompt([
56
111
  {
57
112
  type: 'confirm',
@@ -61,13 +116,21 @@ const confirmMessage = (message) => {
61
116
  },
62
117
  ]);
63
118
  if (!answers.confirm) {
64
- process.exit(0);
119
+ exitWith(0);
65
120
  }
66
121
  };
67
122
  };
68
123
 
124
+ const forceOption = new Option(
125
+ '--force',
126
+ `Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.`
127
+ );
128
+
69
129
  module.exports = {
70
- parseInputList,
130
+ getParseListWithChoices,
131
+ parseList,
132
+ parseURL,
71
133
  promptEncryptionKey,
72
134
  confirmMessage,
135
+ forceOption,
73
136
  };