• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / boxcli / 13208703545

07 Feb 2025 09:58PM UTC coverage: 84.603% (-0.7%) from 85.332%
13208703545

Pull #564

github

web-flow
Merge bf3d0e97e into 3d3b19f65
Pull Request #564: feat: add real API integration tests for users

1192 of 1607 branches covered (74.18%)

Branch coverage included in aggregate %.

4374 of 4972 relevant lines covered (87.97%)

374.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.15
/src/box-command.js
1
/* eslint-disable promise/prefer-await-to-callbacks,promise/avoid-new,class-methods-use-this  */
2
'use strict';
3

4
const { Command, Flags } = require('@oclif/core');
9✔
5
const chalk = require('chalk');
9✔
6
const util = require('util');
9✔
7
const _ = require('lodash');
9✔
8
const fs = require('fs');
9✔
9
const { mkdirp } = require('mkdirp');
9✔
10
const os = require('os');
9✔
11
const path = require('path');
9✔
12
const yaml = require('js-yaml');
9✔
13
const csv = require('csv');
9✔
14
const csvParse = util.promisify(csv.parse);
9✔
15
const csvStringify = util.promisify(csv.stringify);
9✔
16
const dateTime = require('date-fns');
9✔
17
const BoxSDK = require('box-node-sdk');
9✔
18
const BoxTSSDK = require('box-typescript-sdk-gen');
9✔
19
const BoxTsErrors = require('box-typescript-sdk-gen/lib/box/errors');
9✔
20
const BoxCLIError = require('./cli-error');
9✔
21
const CLITokenCache = require('./token-cache');
9✔
22
const utils = require('./util');
9✔
23
const pkg = require('../package.json');
9✔
24
const inquirer = require('inquirer');
9✔
25
const darwinKeychain = require('keychain');
9✔
26
const { stringifyStream } = require('@discoveryjs/json-ext');
9✔
27
const progress = require('cli-progress');
9✔
28
const darwinKeychainSetPassword = util.promisify(
9✔
29
        darwinKeychain.setPassword.bind(darwinKeychain)
30
);
31
const darwinKeychainGetPassword = util.promisify(
9✔
32
        darwinKeychain.getPassword.bind(darwinKeychain)
33
);
34
let keytar = null;
9✔
35
try {
9✔
36
        /* eslint-disable-next-line global-require */
37
        keytar = require('keytar');
9✔
38
} catch (ex) {
39
        // keytar cannot be imported because the library is not provided for this operating system / architecture
40
}
41

42
const DEBUG = require('./debug');
9✔
43
const stream = require('stream');
9✔
44
const pipeline = util.promisify(stream.pipeline);
9✔
45

46
const { Transform } = require('stream');
9✔
47

48
const KEY_MAPPINGS = {
9✔
49
        url: 'URL',
50
        id: 'ID',
51
        etag: 'ETag',
52
        sha1: 'SHA1',
53
        templateKey: 'Template Key',
54
        displayName: 'Display Name',
55
        tos: 'ToS',
56
        statusCode: 'Status Code',
57
        boxReportsFolderPath: 'Box Reports Folder Path',
58
        boxReportsFolderName: 'Box Reports Folder Name (Deprecated)',
59
        boxReportsFileFormat: 'Box Reports File Format',
60
        boxDownloadsFolderPath: 'Box Downloads Folder Path',
61
        boxDownloadsFolderName: 'Box Downloads Folder Name (Deprecated)',
62
        outputJson: 'Output JSON',
63
        clientId: 'Client ID',
64
        enterpriseId: 'Enterprise ID',
65
        boxConfigFilePath: 'Box Config File Path',
66
        hasInLinePrivateKey: 'Has Inline Private Key',
67
        privateKeyPath: 'Private Key Path',
68
        defaultAsUserId: 'Default As-User ID',
69
        useDefaultAsUser: 'Use Default As-User',
70
        cacheTokens: 'Cache Tokens',
71
        ip: 'IP',
72
        operationParams: 'Operation Params',
73
        copyInstanceOnItemCopy: 'Copy Instance On Item Copy',
74
};
75

76
const REQUIRED_FIELDS = ['type', 'id'];
9✔
77

78
const SDK_CONFIG = Object.freeze({
9✔
79
        iterators: true,
80
        analyticsClient: {
81
                version: pkg.version,
82
        },
83
        request: {
84
                headers: {
85
                        'User-Agent': `Box CLI v${pkg.version}`,
86
                },
87
        },
88
});
89

90
const CONFIG_FOLDER_PATH = path.join(os.homedir(), '.box');
9✔
91
const SETTINGS_FILE_PATH = path.join(CONFIG_FOLDER_PATH, 'settings.json');
9✔
92
const ENVIRONMENTS_FILE_PATH = path.join(
9✔
93
        CONFIG_FOLDER_PATH,
94
        'box_environments.json'
95
);
96

97
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
9✔
98

99
/**
100
 * Parse a string value from CSV into the correct boolean value
101
 * @param {string|boolean} value The value to parse
102
 * @returns {boolean} The parsed value
103
 * @private
104
 */
105
function getBooleanFlagValue(value) {
106
        let trueValues = ['yes', 'y', 'true', '1', 't', 'on'];
1,593✔
107
        let falseValues = ['no', 'n', 'false', '0', 'f', 'off'];
1,593✔
108
        if (typeof value === 'boolean') {
1,593✔
109
                return value;
1,467✔
110
        } else if (trueValues.includes(value.toLowerCase())) {
126✔
111
                return true;
81✔
112
        } else if (falseValues.includes(value.toLowerCase())) {
45!
113
                return false;
45✔
114
        }
115
        let possibleValues = trueValues.concat(falseValues).join(', ');
×
116
        throw new Error(
×
117
                `Incorrect boolean value "${value}" passed. Possible values are ${possibleValues}`
118
        );
119
}
120

121
/**
122
 * Add or subtract a given offset from a date
123
 *
124
 * @param {Date} date The date to offset
125
 * @param {int} timeLength The number of time units to offset by
126
 * @param {string} timeUnit The unit of time to offset by, in single-character shorthand
127
 * @returns {Date} The date with offset applied
128
 */
129
function offsetDate(date, timeLength, timeUnit) {
130
        switch (timeUnit) {
18!
131
                case 's':
132
                        return dateTime.addSeconds(date, timeLength);
×
133
                case 'm':
134
                        return dateTime.addMinutes(date, timeLength);
×
135
                case 'h':
136
                        return dateTime.addHours(date, timeLength);
×
137
                case 'd':
138
                        return dateTime.addDays(date, timeLength);
18✔
139
                case 'w':
140
                        return dateTime.addWeeks(date, timeLength);
×
141
                case 'M':
142
                        return dateTime.addMonths(date, timeLength);
×
143
                case 'y':
144
                        return dateTime.addYears(date, timeLength);
×
145
                default:
146
                        throw new Error(`Invalid time unit: ${timeUnit}`);
×
147
        }
148
}
149

150
/**
151
 * Formats an API key (e.g. field name) for human-readable display
152
 *
153
 * @param {string} key The key to format
154
 * @returns {string} The formatted key
155
 * @private
156
 */
157
function formatKey(key) {
158
        // Converting camel case to snake case and then to title case
159
        return key
49,185✔
160
                .replace(/[A-Z]/gu, letter => `_${letter.toLowerCase()}`)
234✔
161
                .split('_')
162
                .map((s) => KEY_MAPPINGS[s] || _.capitalize(s))
66,411✔
163
                .join(' ');
164
}
165

166
/**
167
 * Formats an object's keys for human-readable output
168
 * @param {*} obj The thing to format
169
 * @returns {*} The formatted thing
170
 * @private
171
 */
172
function formatObjectKeys(obj) {
173
        // No need to process primitive values
174
        if (typeof obj !== 'object' || obj === null) {
52,497✔
175
                return obj;
42,570✔
176
        }
177

178
        // If type is Date, convert to ISO string
179
        if (obj instanceof Date) {
9,927!
180
                return obj.toISOString();
×
181
        }
182

183
        // Don't format metadata objects to avoid mangling keys
184
        if (obj.$type) {
9,927✔
185
                return obj;
90✔
186
        }
187

188
        if (Array.isArray(obj)) {
9,837✔
189
                return obj.map((el) => formatObjectKeys(el));
1,260✔
190
        }
191

192
        let formattedObj = Object.create(null);
8,928✔
193
        Object.keys(obj).forEach((key) => {
8,928✔
194
                let formattedKey = formatKey(key);
49,032✔
195
                formattedObj[formattedKey] = formatObjectKeys(obj[key]);
49,032✔
196
        });
197

198
        return formattedObj;
8,928✔
199
}
200

201
/**
202
 * Formats an object for output by prettifying its keys
203
 * and rendering it in a more human-readable form (i.e. YAML)
204
 *
205
 * @param {Object} obj The object to format for display
206
 * @returns {string} The formatted object output
207
 * @private
208
 */
209
function formatObject(obj) {
210
        let outputData = formatObjectKeys(obj);
2,205✔
211

212
        // Other objects are formatted as YAML for human-readable output
213
        let yamlString = yaml.safeDump(outputData, {
2,205✔
214
                indent: 4,
215
                noRefs: true,
216
        });
217

218
        // The YAML library puts a trailing newline at the end of the string, which is
219
        // redundant with the automatic newline added by oclif when writing to stdout
220
        return yamlString
2,205✔
221
                .replace(/\r?\n$/u, '')
222
                .replace(/^([^:]+:)/gmu, (match, key) => chalk.cyan(key));
49,851✔
223
}
224

225
/**
226
 * Formats the object header, used to separate multiple objects in a collection
227
 *
228
 * @param {Object} obj The object to generate a header for
229
 * @returns {string} The header string
230
 * @private
231
 */
232
function formatObjectHeader(obj) {
233
        if (!obj.type || !obj.id) {
153!
234
                return chalk`{dim ----------}`;
×
235
        }
236
        return chalk`{dim ----- ${formatKey(obj.type)} ${obj.id} -----}`;
153✔
237
}
238

239
/**
240
 * Base class for all Box CLI commands
241
 */
242
class BoxCommand extends Command {
243
        // @TODO(2018-08-15): Move all fs methods used here to be async
244
        /* eslint-disable no-sync */
245

246
        /**
247
         * Initialize before the command is run
248
         * @returns {void}
249
         */
250
        async init() {
251
                DEBUG.init('Initializing Box CLI');
7,560✔
252
                let originalArgs, originalFlags;
253
                if (
7,560✔
254
                        this.argv.some((arg) => arg.startsWith('--bulk-file-path')) &&
30,861✔
255
                        Object.keys(this.constructor.flags).includes('bulk-file-path')
256
                ) {
257
                        // Set up the command for bulk run
258
                        DEBUG.init('Preparing for bulk input');
324✔
259
                        this.isBulk = true;
324✔
260
                        originalArgs = _.cloneDeep(this.constructor.args);
324✔
261
                        originalFlags = _.cloneDeep(this.constructor.flags);
324✔
262
                        this.disableRequiredArgsAndFlags();
324✔
263
                }
264

265
                /* eslint-disable no-shadow */
266
                let { flags, args } = await this.parse(this.constructor);
7,560✔
267
                /* eslint-enable no-shadow */
268
                this.flags = flags;
7,560✔
269
                this.args = args;
7,560✔
270
                this.settings = await this._loadSettings();
7,560✔
271
                this.client = await this.getClient();
7,560✔
272
                this.tsClient = await this.getTsClient();
7,560✔
273

274
                if (this.isBulk) {
7,560✔
275
                        this.constructor.args = originalArgs;
324✔
276
                        this.constructor.flags = originalFlags;
324✔
277
                        this.bulkOutputList = [];
324✔
278
                        this.bulkErrors = [];
324✔
279
                        this._singleRun = this.run;
324✔
280
                        this.run = this.bulkOutputRun;
324✔
281
                }
282

283
                DEBUG.execute(
7,560✔
284
                        'Starting execution command: %s argv: %O',
285
                        this.id,
286
                        this.argv
287
                );
288
        }
289

290
        /**
291
         * Read in the input file and run the command once for each set of inputs
292
         * @returns {void}
293
         */
294
        async bulkOutputRun() {
295
                const allPossibleArgs = Object.keys(this.constructor.args || {});
324!
296
                const allPossibleFlags = Object.keys(this.constructor.flags || {});
324!
297
                // Map from matchKey (arg/flag name in all lower-case characters) => {type, fieldKey}
298
                let fieldMapping = Object.assign(
324✔
299
                        {},
300
                        ...allPossibleArgs.map((arg) => ({
486✔
301
                                [arg.toLowerCase()]: { type: 'arg', fieldKey: arg },
302
                        })),
303
                        ...allPossibleFlags.map((flag) => ({
8,082✔
304
                                [flag.replace(/-/gu, '')]: { type: 'flag', fieldKey: flag },
305
                        }))
306
                );
307
                let bulkCalls = await this._parseBulkFile(
324✔
308
                        this.flags['bulk-file-path'],
309
                        fieldMapping
310
                );
311
                let bulkEntryIndex = 0;
279✔
312
                let progressBar = new progress.Bar({
279✔
313
                        format: '[{bar}] {percentage}% | {value}/{total}',
314
                        stopOnComplete: true,
315
                });
316
                progressBar.start(bulkCalls.length, 0);
279✔
317

318
                for (let bulkData of bulkCalls) {
279✔
319
                        /* eslint-disable no-await-in-loop */
320
                        this.argv = [];
603✔
321
                        bulkEntryIndex += 1;
603✔
322
                        this._getArgsForBulkInput(allPossibleArgs, bulkData);
603✔
323
                        this._setFlagsForBulkInput(bulkData);
603✔
324
                        await this._handleAsUserSettings(bulkData);
603✔
325
                        DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
603✔
326
                        // @TODO(2018-08-29): Convert this to a promise queue to improve performance
327
                        /* eslint-disable no-await-in-loop */
328
                        try {
603✔
329
                                await this._singleRun();
603✔
330
                        } catch (err) {
331
                                // In bulk mode, we don't want to write directly to console and kill the command
332
                                // Instead, we should buffer the error output so subsequent commands might be able to succeed
333
                                DEBUG.execute('Caught error from bulk input entry %d', bulkEntryIndex);
27✔
334
                                this.bulkErrors.push({
27✔
335
                                        index: bulkEntryIndex,
336
                                        data: bulkData,
337
                                        error: this.wrapError(err),
338
                                });
339
                        }
340
                        /* eslint-enable no-await-in-loop */
341
                        progressBar.update(bulkEntryIndex);
603✔
342
                }
343
                this.isBulk = false;
279✔
344
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
345
                await this.output(this.bulkOutputList);
279✔
346
                this._handleBulkErrors();
279✔
347
        }
348

349
        /**
350
         * Logs bulk processing errors if any occured.
351
         * @returns {void}
352
         * @private
353
         */
354
        _handleBulkErrors() {
355
                const numErrors = this.bulkErrors.length;
279✔
356
                if (numErrors === 0) {
279✔
357
                        this.info(chalk`{green All bulk input entries processed successfully.}`);
261✔
358
                        return;
261✔
359
                }
360
                this.info(
18✔
361
                        chalk`{redBright ${numErrors} entr${numErrors > 1 ? 'ies' : 'y'} failed!}`
18✔
362
                );
363
                this.bulkErrors.forEach((errorInfo) => {
18✔
364
                        this.info(chalk`{dim ----------}`);
27✔
365
                        let entryData = errorInfo.data
27✔
366
                                .map((o) => `    ${o.fieldKey}=${o.value}`)
18✔
367
                                .join(os.EOL);
368
                        this.info(
27✔
369
                                chalk`{redBright Entry ${errorInfo.index} (${
370
                                        os.EOL + entryData + os.EOL
371
                                }) failed with error:}`
372
                        );
373
                        let err = errorInfo.error;
27✔
374
                        let contextInfo;
375
                        if (err.response && err.response.body && err.response.body.context_info) {
27✔
376
                                contextInfo = formatObject(err.response.body.context_info);
9✔
377
                                // Remove color codes from context info
378
                                // eslint-disable-next-line no-control-regex
379
                                contextInfo = contextInfo.replace(/\u001b\[\d+m/gu, '');
9✔
380
                                // Remove \n with os.EOL
381
                                contextInfo = contextInfo.replace(/\n/gu, os.EOL);
9✔
382
                        }
383
                        let errMsg = chalk`{redBright ${
27✔
384
                                this.flags && this.flags.verbose ? err.stack : err.message
81!
385
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
27✔
386
                        this.info(errMsg);
27✔
387
                });
388
        }
389

390
        /**
391
         * Set as-user header from the bulk file or use the default one.
392
         * @param {Array} bulkData Bulk data
393
         * @returns {Promise<void>} Returns nothing
394
         * @private
395
         */
396
        async _handleAsUserSettings(bulkData) {
397
                let asUser = bulkData.find((o) => o.fieldKey === 'as-user') || {};
1,647✔
398
                if (!_.isEmpty(asUser)) {
603✔
399
                        if (_.isNil(asUser.value)) {
27✔
400
                                let environmentsObj = await this.getEnvironments(); // eslint-disable-line no-await-in-loop
9✔
401
                                if (environmentsObj.default) {
9!
402
                                        let environment =
403
                                                environmentsObj.environments[environmentsObj.default];
×
404
                                        DEBUG.init(
×
405
                                                'Using environment %s %O',
406
                                                environmentsObj.default,
407
                                                environment
408
                                        );
409
                                        if (environment.useDefaultAsUser) {
×
410
                                                this.client.asUser(environment.defaultAsUserId);
×
411
                                                DEBUG.init(
×
412
                                                        'Impersonating default user ID %s',
413
                                                        environment.defaultAsUserId
414
                                                );
415
                                        } else {
416
                                                this.client.asSelf();
×
417
                                        }
418
                                } else {
419
                                        this.client.asSelf();
9✔
420
                                }
421
                        } else {
422
                                this.client.asUser(asUser.value);
18✔
423
                                DEBUG.init('Impersonating user ID %s', asUser.value);
18✔
424
                        }
425
                }
426
        }
427

428
        /**
429
         * Include flag values from command line first,
430
         * they'll automatically be overwritten/combined with later values by the oclif parser.
431
         * @param {Array} bulkData Bulk data
432
         * @returns {void}
433
         * @private
434
         */
435
        _setFlagsForBulkInput(bulkData) {
436
                const bulkDataFlags = bulkData.filter((o) => o.type === 'flag' && !_.isNil(o.value)).map((o) => o.fieldKey);
1,647✔
437
                Object.keys(this.flags)
603✔
438
                        .filter((flag) => flag !== 'bulk-file-path') // Remove the bulk file path flag so we don't recurse!
3,330✔
439
                        .filter((flag) => !bulkDataFlags.includes(flag))
2,727✔
440
                        .forEach((flag) => {
441
                                // Some flags can be specified multiple times in a single command. For these flags, their value is an array of user inputted values.
442
                                // For these flags, we iterate through their values and add each one as a separate flag to comply with oclif
443
                                if (Array.isArray(this.flags[flag])) {
2,322✔
444
                                        this.flags[flag].forEach((value) => {
9✔
445
                                                this._addFlagToArgv(flag, value);
18✔
446
                                        });
447
                                } else {
448
                                        this._addFlagToArgv(flag, this.flags[flag]);
2,313✔
449
                                }
450
                        });
451
                // Include all flag values from bulk input, which will override earlier ones
452
                // from the command line
453
                bulkData
603✔
454
                        // Remove the bulk file path flag so we don't recurse!
455
                        .filter((o) => o.type === 'flag' && o.fieldKey !== 'bulk-file-path')
1,647✔
456
                        .forEach((o) => this._addFlagToArgv(o.fieldKey, o.value));
846✔
457
        }
458

459
        /**
460
         * For each possible arg, find the correct value between bulk input and values given on the command line.
461
         * @param {Array} allPossibleArgs All possible args
462
         * @param {Array} bulkData Bulk data
463
         * @returns {void}
464
         * @private
465
         */
466
        _getArgsForBulkInput(allPossibleArgs, bulkData) {
467
                for (let arg of allPossibleArgs) {
603✔
468
                        let bulkArg = bulkData.find((o) => o.fieldKey === arg) || {};
1,422✔
469
                        if (!_.isNil(bulkArg.value)) {
927✔
470
                                // Use value from bulk input file when available
471
                                this.argv.push(bulkArg.value);
756✔
472
                        } else if (this.args[arg]) {
171✔
473
                                // Fall back to value from command line
474
                                this.argv.push(this.args[arg]);
135✔
475
                        }
476
                }
477
        }
478

479
        /**
480
         * Parses file wilk bulk commands
481
         * @param {String} filePath Path to file with bulk commands
482
         * @param {Array} fieldMapping Data to parse
483
         * @returns {Promise<*>} Returns parsed data
484
         * @private
485
         */
486
        async _parseBulkFile(filePath, fieldMapping) {
487
                const fileExtension = path.extname(filePath);
324✔
488
                const fileContents = this._readBulkFile(filePath);
324✔
489
                let bulkCalls;
490
                if (fileExtension === '.json') {
324✔
491
                        bulkCalls = this._handleJsonFile(fileContents, fieldMapping);
144✔
492
                } else if (fileExtension === '.csv') {
180✔
493
                        bulkCalls = await this._handleCsvFile(fileContents, fieldMapping);
171✔
494
                } else {
495
                        throw new Error(
9✔
496
                                `Input file had extension "${fileExtension}", but only .json and .csv are supported`
497
                        );
498
                }
499
                // Filter out any undefined values, which can arise when the input file contains extraneous keys
500
                bulkCalls = bulkCalls.map((args) => args.filter((o) => o !== undefined));
1,737✔
501
                DEBUG.execute(
279✔
502
                        'Read %d entries from bulk file %s',
503
                        bulkCalls.length,
504
                        this.flags['bulk-file-path']
505
                );
506
                return bulkCalls;
279✔
507
        }
508

509
        /**
510
         * Parses CSV file
511
         * @param {Object} fileContents File content to parse
512
         * @param {Array} fieldMapping Field mapings
513
         * @returns {Promise<string|null|*>} Returns parsed data
514
         * @private
515
         */
516
        async _handleCsvFile(fileContents, fieldMapping) {
517
                let parsedData = await csvParse(fileContents, {
171✔
518
                        bom: true,
519
                        delimiter: ',',
520
                        cast(value, context) {
521
                                if (value.length === 0) {
1,584✔
522
                                        // Regard unquoted empty values as null
523
                                        return context.quoting ? '' : null;
162✔
524
                                }
525
                                return value;
1,422✔
526
                        },
527
                });
528
                if (parsedData.length < 2) {
171✔
529
                        throw new Error(
9✔
530
                                'CSV input file should contain the headers row and at least on data row'
531
                        );
532
                }
533
                // @NOTE: We don't parse the CSV into an aray of Objects
534
                // and instead mainatain a separate array of headers, in
535
                // order to ensure that ordering is maintained in the keys
536
                let headers = parsedData.shift().map((key) => {
162✔
537
                        let keyParts = key.match(/(.*)_\d+$/u);
522✔
538
                        let someKey = keyParts ? keyParts[1] : key;
522✔
539
                        return someKey.toLowerCase().replace(/[-_]/gu, '');
522✔
540
                });
541
                return parsedData.map((values) =>
162✔
542
                        values.map((value, index) => {
324✔
543
                                let key = headers[index];
1,044✔
544
                                let field = fieldMapping[key];
1,044✔
545
                                return field ? { ...field, value } : undefined;
1,044✔
546
                        })
547
                );
548
        }
549

550
        /**
551
         * Parses JSON file
552
         * @param {Object} fileContents File content to parse
553
         * @param {Array} fieldMapping Field mapings
554
         * @returns {*} Returns parsed data
555
         * @private
556
         */
557
        _handleJsonFile(fileContents, fieldMapping) {
558
                let parsedData;
559
                try {
144✔
560
                        let jsonFile = JSON.parse(fileContents);
144✔
561
                        parsedData = jsonFile.hasOwnProperty('entries')
126✔
562
                                ? jsonFile.entries
563
                                : jsonFile;
564
                } catch (e) {
565
                        throw new BoxCLIError(
18✔
566
                                `Could not parse JSON input file ${this.flags['bulk-file-path']}`,
567
                                e
568
                        );
569
                }
570
                if (!Array.isArray(parsedData)) {
126✔
571
                        throw new TypeError(
9✔
572
                                'Expected input file to contain an array of input objects, but none found'
573
                        );
574
                }
575
                // Translate each row object to an array of {type, fieldKey, value}, to be handled below
576
                return parsedData.map(function flattenObjectToArgs(obj) {
117✔
577
                        // One top-level object key can map to multiple args/flags, so we need to deeply flatten after mapping
578
                        return _.flatMapDeep(obj, (value, key) => {
315✔
579
                                let matchKey = key.toLowerCase().replace(/[-_]/gu, '');
693✔
580
                                let field = fieldMapping[matchKey];
693✔
581
                                if (_.isPlainObject(value)) {
693✔
582
                                        // Map e.g. { item: { id: 12345, type: folder } } => { item: 12345, itemtype: folder }
583
                                        // @NOTE: For now, we only support nesting keys this way one level deep
584
                                        return Object.keys(value).map((nestedKey) => {
18✔
585
                                                let nestedMatchKey =
586
                                                        matchKey + nestedKey.toLowerCase().replace(/[-_]/gu, '');
27✔
587
                                                let nestedField = fieldMapping[nestedMatchKey];
27✔
588
                                                return nestedField
27✔
589
                                                        ? { ...nestedField, value: value[nestedKey] }
590
                                                        : undefined;
591
                                        });
592
                                } else if (Array.isArray(value)) {
675✔
593
                                        // Arrays can be one of two things: an array of values for a single key,
594
                                        // or an array of grouped flags/args as objects
595
                                        // First, check if everything in the array is either all object or all non-object
596
                                        let types = value.reduce((acc, t) => acc.concat(typeof t), []);
63✔
597
                                        if (
27!
598
                                                types.some((t) => t !== 'object') &&
54✔
599
                                                types.some((t) => t === 'object')
27✔
600
                                        ) {
601
                                                throw new BoxCLIError(
×
602
                                                        'Mixed types in bulk input JSON array; use strings or Objects'
603
                                                );
604
                                        }
605
                                        // If everything in the array is objects, handle each one as a group of flags and args
606
                                        // by recursively parsing that object into args
607
                                        if (types[0] === 'object') {
27✔
608
                                                return value.map((o) => flattenObjectToArgs(o));
36✔
609
                                        }
610
                                        // If the array is of values for this field, just return those
611
                                        return field ? value.map((v) => ({ ...field, value: v })) : [];
18✔
612
                                }
613
                                return field ? { ...field, value } : undefined;
648✔
614
                        });
615
                });
616
        }
617

618
        /**
619
         * Returns bulk file contents
620
         * @param {String} filePath Path to bulk file
621
         * @returns {Buffer} Bulk file contents
622
         * @private
623
         */
624
        _readBulkFile(filePath) {
625
                try {
324✔
626
                        const fileContents = fs.readFileSync(filePath);
324✔
627
                        DEBUG.execute('Read bulk input file at %s', filePath);
324✔
628
                        return fileContents;
324✔
629
                } catch (ex) {
630
                        throw new BoxCLIError(`Could not open input file ${filePath}`, ex);
×
631
                }
632
        }
633

634
        /**
635
         * Writes a given flag value to the command's argv array
636
         *
637
         * @param {string} flag The flag name
638
         * @param {*} flagValue The flag value
639
         * @returns {void}
640
         * @private
641
         */
642
        _addFlagToArgv(flag, flagValue) {
643
                if (_.isNil(flagValue)) {
3,177✔
644
                        return;
108✔
645
                }
646

647
                if (this.constructor.flags[flag].type === 'boolean') {
3,069✔
648
                        if (getBooleanFlagValue(flagValue)) {
1,593✔
649
                                this.argv.push(`--${flag}`);
1,494✔
650
                        } else {
651
                                this.argv.push(`--no-${flag}`);
99✔
652
                        }
653
                } else {
654
                        this.argv.push(`--${flag}=${flagValue}`);
1,476✔
655
                }
656
        }
657

658
        /**
659
         * Ensure that all args and flags for the command are not marked as required,
660
         * to avoid issues when filling in required values from the input file.
661
         * @returns {void}
662
         */
663
        disableRequiredArgsAndFlags() {
664
                if (this.constructor.args !== undefined) {
324!
665
                        Object.keys(this.constructor.args).forEach((key) => {
324✔
666
                                this.constructor.args[key].required = false;
486✔
667
                        });
668
                }
669

670
                if (this.constructor.flags !== undefined) {
324!
671
                        Object.keys(this.constructor.flags).forEach((key) => {
324✔
672
                                this.constructor.flags[key].required = false;
8,082✔
673
                        });
674
                }
675
        }
676

677
        /**
678
         * Instantiate the SDK client for making API calls
679
         *
680
         * @returns {BoxClient} The client for making API calls in the command
681
         */
682
        async getClient() {
683
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
684
                if (this.constructor.noClient) {
7,560!
685
                        return null;
×
686
                }
687
                let environmentsObj = await this.getEnvironments();
7,560✔
688
                const environment =
689
                        environmentsObj.environments[environmentsObj.default] || {};
7,560✔
690
                const { authMethod } = environment;
7,560✔
691

692
                let client;
693
                if (this.flags.token) {
7,560!
694
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,560✔
695
                        let sdk = new BoxSDK({
7,560✔
696
                                clientID: '',
697
                                clientSecret: '',
698
                                ...SDK_CONFIG,
699
                        });
700
                        this._configureSdk(sdk, { ...SDK_CONFIG });
7,560✔
701
                        this.sdk = sdk;
7,560✔
702
                        client = sdk.getBasicClient(this.flags.token);
7,560✔
703
                } else if (authMethod === 'ccg') {
×
704
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
705

706
                        const { clientId, clientSecret, ccgUser } = environment;
×
707

708
                        if (!clientId || !clientSecret) {
×
709
                                throw new BoxCLIError(
×
710
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
711
                                );
712
                        }
713

714
                        let configObj;
715
                        try {
×
716
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
717
                        } catch (ex) {
718
                                throw new BoxCLIError('Could not read environments config file', ex);
×
719
                        }
720

721
                        const { enterpriseID } = configObj;
×
722
                        const sdk = new BoxSDK({
×
723
                                clientID: clientId,
724
                                clientSecret,
725
                                enterpriseID,
726
                                ...SDK_CONFIG,
727
                        });
728
                        this._configureSdk(sdk, { ...SDK_CONFIG });
×
729
                        this.sdk = sdk;
×
730
                        client = ccgUser
×
731
                                ? sdk.getCCGClientForUser(ccgUser)
732
                                : sdk.getAnonymousClient();
733
                } else if (
×
734
                        environmentsObj.default &&
×
735
                        environmentsObj.environments[environmentsObj.default].authMethod ===
736
                                'oauth20'
737
                ) {
738
                        try {
×
739
                                DEBUG.init(
×
740
                                        'Using environment %s %O',
741
                                        environmentsObj.default,
742
                                        environment
743
                                );
744
                                let tokenCache = new CLITokenCache(environmentsObj.default);
×
745

746
                                let sdk = new BoxSDK({
×
747
                                        clientID: environment.clientId,
748
                                        clientSecret: environment.clientSecret,
749
                                        ...SDK_CONFIG,
750
                                });
751
                                this._configureSdk(sdk, { ...SDK_CONFIG });
×
752
                                this.sdk = sdk;
×
753
                                let tokenInfo = await new Promise((resolve, reject) => {
×
754
                                        // eslint-disable-line promise/avoid-new
755
                                        tokenCache.read((error, localTokenInfo) => {
×
756
                                                if (error) {
×
757
                                                        reject(error);
×
758
                                                } else {
759
                                                        resolve(localTokenInfo);
×
760
                                                }
761
                                        });
762
                                });
763
                                client = sdk.getPersistentClient(tokenInfo, tokenCache);
×
764
                        } catch (err) {
765
                                throw new BoxCLIError(
×
766
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
767
                                );
768
                        }
769
                } else if (environmentsObj.default) {
×
770
                        DEBUG.init(
×
771
                                'Using environment %s %O',
772
                                environmentsObj.default,
773
                                environment
774
                        );
775
                        let tokenCache =
776
                                environment.cacheTokens === false
×
777
                                        ? null
778
                                        : new CLITokenCache(environmentsObj.default);
779
                        let configObj;
780
                        try {
×
781
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
782
                        } catch (ex) {
783
                                throw new BoxCLIError('Could not read environments config file', ex);
×
784
                        }
785

786
                        if (!environment.hasInLinePrivateKey) {
×
787
                                try {
×
788
                                        configObj.boxAppSettings.appAuth.privateKey = fs.readFileSync(
×
789
                                                environment.privateKeyPath,
790
                                                'utf8'
791
                                        );
792
                                        DEBUG.init(
×
793
                                                'Loaded JWT private key from %s',
794
                                                environment.privateKeyPath
795
                                        );
796
                                } catch (ex) {
797
                                        throw new BoxCLIError(
×
798
                                                `Could not read private key file ${environment.privateKeyPath}`,
799
                                                ex
800
                                        );
801
                                }
802
                        }
803

804
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
805
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
806

807
                        client = this.sdk.getAppAuthClient(
×
808
                                'enterprise',
809
                                environment.enterpriseId,
810
                                tokenCache
811
                        );
812
                        DEBUG.init('Initialized client from environment config');
×
813

814
                } else {
815
                        // No environments set up yet!
816
                        throw new BoxCLIError(
×
817
                                `No default environment found.
818
                                It looks like you haven't configured the Box CLI yet.
819
                                See this command for help adding an environment: box configure:environments:add --help
820
                                Or, supply a token with your command with --token.`.replace(/^\s+/gmu, '')
821
                        );
822
                }
823

824
                // Using the as-user flag should have precedence over the environment setting
825
                if (this.flags['as-user']) {
7,560!
826
                        client.asUser(this.flags['as-user']);
×
827
                        DEBUG.init('Impersonating user ID %s using the ID provided via the --as-user flag', this.flags['as-user']);
×
828
                } else if (!this.flags.token && environment.useDefaultAsUser) { // We don't want to use any environment settings if a token is passed in the command
7,560!
829
                        client.asUser(environment.defaultAsUserId);
×
830
                        DEBUG.init(
×
831
                                'Impersonating default user ID %s using environment configuration',
832
                                environment.defaultAsUserId
833
                        );
834
                }
835
                return client;
7,560✔
836
        }
837

838
        /**
839
         * Instantiate the TypeScript SDK client for making API calls
840
         *
841
         * @returns {BoxTSSDK.BoxClient} The TypeScript SDK client for making API calls in the command
842
         */
843
        async getTsClient() {
844
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
845
                if (this.constructor.noClient) {
7,560!
846
                        return null;
×
847
                }
848
                let environmentsObj = await this.getEnvironments();
7,560✔
849
                const environment =
850
                        environmentsObj.environments[environmentsObj.default] || {};
7,560✔
851
                const { authMethod } = environment;
7,560✔
852

853
                let client;
854
                if (this.flags.token) {
7,560!
855
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,560✔
856
                        let tsSdkAuth = new BoxTSSDK.BoxDeveloperTokenAuth({
7,560✔
857
                                token: this.flags.token,
858
                        });
859
                        client = new BoxTSSDK.BoxClient({
7,560✔
860
                                auth: tsSdkAuth,
861
                        });
862
                        client = this._configureTsSdk(client, SDK_CONFIG);
7,560✔
863
                } else if (authMethod === 'ccg') {
×
864
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
865

866
                        const { clientId, clientSecret, ccgUser } = environment;
×
867

868
                        if (!clientId || !clientSecret) {
×
869
                                throw new BoxCLIError(
×
870
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
871
                                );
872
                        }
873

874
                        let configObj;
875
                        try {
×
876
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
877
                        } catch (ex) {
878
                                throw new BoxCLIError('Could not read environments config file', ex);
×
879
                        }
880

881
                        const { enterpriseID } = configObj;
×
882
                        const tokenCache = environment.cacheTokens === false
×
883
                                ? null
884
                                : new CLITokenCache(environmentsObj.default);
885
                        let ccgConfig = new BoxTSSDK.CcgConfig(ccgUser ? {
×
886
                                clientId,
887
                                clientSecret,
888
                                userId: ccgUser,
889
                                tokenStorage: tokenCache,
890
                        } : {
891
                                clientId,
892
                                clientSecret,
893
                                enterpriseId: enterpriseID,
894
                                tokenStorage: tokenCache,
895
                        });
896
                        let ccgAuth = new BoxTSSDK.BoxCcgAuth({config: ccgConfig});
×
897
                        client = new BoxTSSDK.BoxClient({
×
898
                                auth: ccgAuth,
899
                        });
900
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
901
                } else if (
×
902
                        environmentsObj.default &&
×
903
                        environmentsObj.environments[environmentsObj.default].authMethod ===
904
                                'oauth20'
905
                ) {
906
                        try {
×
907
                                DEBUG.init(
×
908
                                        'Using environment %s %O',
909
                                        environmentsObj.default,
910
                                        environment
911
                                );
912
                                const tokenCache = new CLITokenCache(environmentsObj.default);
×
913
                                const oauthConfig = new BoxTSSDK.OAuthConfig({
×
914
                                        clientId: environment.clientId,
915
                                        clientSecret: environment.clientSecret,
916
                                        tokenStorage: tokenCache,
917
                                });
918
                                const oauthAuth = new BoxTSSDK.BoxOAuth({
×
919
                                        config: oauthConfig,
920
                                });
921
                                client = new BoxTSSDK.BoxClient({auth: oauthAuth});
×
922
                                client = this._configureTsSdk(client, SDK_CONFIG);
×
923
                        } catch (err) {
924
                                throw new BoxCLIError(
×
925
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
926
                                );
927
                        }
928
                } else if (environmentsObj.default) {
×
929
                        DEBUG.init(
×
930
                                'Using environment %s %O',
931
                                environmentsObj.default,
932
                                environment
933
                        );
934
                        let tokenCache =
935
                                environment.cacheTokens === false
×
936
                                        ? null
937
                                        : new CLITokenCache(environmentsObj.default);
938
                        let configObj;
939
                        try {
×
940
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
941
                        } catch (ex) {
942
                                throw new BoxCLIError('Could not read environments config file', ex);
×
943
                        }
944

945
                        if (!environment.hasInLinePrivateKey) {
×
946
                                try {
×
947
                                        configObj.boxAppSettings.appAuth.privateKey = fs.readFileSync(
×
948
                                                environment.privateKeyPath,
949
                                                'utf8'
950
                                        );
951
                                        DEBUG.init(
×
952
                                                'Loaded JWT private key from %s',
953
                                                environment.privateKeyPath
954
                                        );
955
                                } catch (ex) {
956
                                        throw new BoxCLIError(
×
957
                                                `Could not read private key file ${environment.privateKeyPath}`,
958
                                                ex
959
                                        );
960
                                }
961
                        }
962

963
                        const jwtConfig = new BoxTSSDK.JwtConfig({
×
964
                                clientId: configObj.boxAppSettings.clientID,
965
                                clientSecret: configObj.boxAppSettings.clientSecret,
966
                                jwtKeyId: configObj.boxAppSettings.appAuth.publicKeyID,
967
                                privateKey: configObj.boxAppSettings.appAuth.privateKey,
968
                                privateKeyPassphrase: configObj.boxAppSettings.appAuth.passphrase,
969
                                enterpriseId: environment.enterpriseId,
970
                                tokenStorage: tokenCache,
971
                        });
972
                        let jwtAuth = new BoxTSSDK.BoxJwtAuth({config: jwtConfig});
×
973
                        client = new BoxTSSDK.BoxClient({auth: jwtAuth});
×
974

975
                        DEBUG.init('Initialized client from environment config');
×
976
                        if (environment.useDefaultAsUser) {
×
977
                                client = client.withAsUserHeader(environment.defaultAsUserId);
×
978
                                DEBUG.init(
×
979
                                        'Impersonating default user ID %s',
980
                                        environment.defaultAsUserId
981
                                );
982
                        }
983
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
984
                } else {
985
                        // No environments set up yet!
986
                        throw new BoxCLIError(
×
987
                                `No default environment found.
988
                                It looks like you haven't configured the Box CLI yet.
989
                                See this command for help adding an environment: box configure:environments:add --help
990
                                Or, supply a token with your command with --token.`.replace(/^\s+/gmu, '')
991
                        );
992
                }
993
                if (this.flags['as-user']) {
7,560!
994
                        client = client.withAsUserHeader(this.flags['as-user']);
×
995
                        DEBUG.init('Impersonating user ID %s', this.flags['as-user']);
×
996
                }
997
                return client;
7,560✔
998
        }
999

1000
        /**
1001
         * Configures SDK by using values from settings.json file
1002
         * @param {*} sdk to configure
1003
         * @param {*} config Additional options to use while building configuration
1004
         * @returns {void}
1005
         */
1006
        _configureSdk(sdk, config = {}) {
×
1007
                const clientSettings = { ...config };
7,560✔
1008
                if (this.settings.enableProxy) {
7,560!
1009
                        clientSettings.proxy = this.settings.proxy;
×
1010
                }
1011
                if (this.settings.apiRootURL) {
7,560!
1012
                        clientSettings.apiRootURL = this.settings.apiRootURL;
×
1013
                }
1014
                if (this.settings.uploadAPIRootURL) {
7,560!
1015
                        clientSettings.uploadAPIRootURL = this.settings.uploadAPIRootURL;
×
1016
                }
1017
                if (this.settings.authorizeRootURL) {
7,560!
1018
                        clientSettings.authorizeRootURL = this.settings.authorizeRootURL;
×
1019
                }
1020
                if (this.settings.numMaxRetries) {
7,560!
1021
                        clientSettings.numMaxRetries = this.settings.numMaxRetries;
×
1022
                }
1023
                if (this.settings.retryIntervalMS) {
7,560!
1024
                        clientSettings.retryIntervalMS = this.settings.retryIntervalMS;
×
1025
                }
1026
                if (this.settings.uploadRequestTimeoutMS) {
7,560!
1027
                        clientSettings.uploadRequestTimeoutMS =
×
1028
                                this.settings.uploadRequestTimeoutMS;
1029
                }
1030
                if (
7,560!
1031
                        this.settings.enableAnalyticsClient &&
7,560!
1032
                        this.settings.analyticsClient.name
1033
                ) {
1034
                        clientSettings.analyticsClient.name = `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`;
×
1035
                } else {
1036
                        clientSettings.analyticsClient.name = DEFAULT_ANALYTICS_CLIENT_NAME;
7,560✔
1037
                }
1038

1039
                if (Object.keys(clientSettings).length > 0) {
7,560!
1040
                        DEBUG.init('SDK client settings %s', clientSettings);
7,560✔
1041
                        sdk.configure(clientSettings);
7,560✔
1042
                }
1043
        }
1044

1045
        /**
1046
         * Configures TS SDK by using values from settings.json file
1047
         *
1048
         * @param {BoxTSSDK.BoxClient} client to configure
1049
         * @param {Object} config Additional options to use while building configuration
1050
         * @returns {BoxTSSDK.BoxClient} The configured client
1051
         */
1052
        _configureTsSdk(client, config) {
1053
                let additionalHeaders = config.request.headers;
7,560✔
1054
                let customBaseURL = {
7,560✔
1055
                        baseUrl: 'https://api.box.com',
1056
                        uploadUrl: 'https://upload.box.com/api',
1057
                        oauth2Url: 'https://account.box.com/api/oauth2',
1058
                };
1059
                if (this.settings.enableProxy) {
7,560!
1060
                        // Not supported in TS SDK
1061
                }
1062
                if (this.settings.apiRootURL) {
7,560!
1063
                        customBaseURL.baseUrl = this.settings.apiRootURL;
×
1064
                }
1065
                if (this.settings.uploadAPIRootURL) {
7,560!
1066
                        customBaseURL.uploadUrl = this.settings.uploadAPIRootURL;
×
1067
                }
1068
                if (this.settings.authorizeRootURL) {
7,560!
1069
                        customBaseURL.oauth2Url = this.settings.authorizeRootURL;
×
1070
                }
1071
                client = client.withCustomBaseUrls(customBaseURL);
7,560✔
1072

1073
                if (this.settings.numMaxRetries) {
7,560!
1074
                        // Not supported in TS SDK
1075
                }
1076
                if (this.settings.retryIntervalMS) {
7,560!
1077
                        // Not supported in TS SDK
1078
                }
1079
                if (this.settings.uploadRequestTimeoutMS) {
7,560!
1080
                        // Not supported in TS SDK
1081
                }
1082
                if (
7,560!
1083
                        this.settings.enableAnalyticsClient &&
7,560!
1084
                        this.settings.analyticsClient.name
1085
                ) {
1086
                        additionalHeaders['X-Box-UA'] = `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`;
×
1087
                } else {
1088
                        additionalHeaders['X-Box-UA'] = DEFAULT_ANALYTICS_CLIENT_NAME;
7,560✔
1089
                }
1090
                client = client.withExtraHeaders(additionalHeaders);
7,560✔
1091
                DEBUG.init('TS SDK configured with settings from settings.json');
7,560✔
1092

1093
                return client;
7,560✔
1094
        }
1095

1096
        /**
1097
         * Format data for output to stdout
1098
         * @param {*} content The content to output
1099
         * @returns {Promise<void>} A promise resolving when output is handled
1100
         */
1101
        async output(content) {
1102
                if (this.isBulk) {
7,065✔
1103
                        this.bulkOutputList.push(content);
576✔
1104
                        DEBUG.output(
576✔
1105
                                'Added command output to bulk list total: %d',
1106
                                this.bulkOutputList.length
1107
                        );
1108
                        return undefined;
576✔
1109
                }
1110

1111
                let formattedOutputData;
1112
                if (Array.isArray(content)) {
6,489✔
1113
                        // Format each object individually and then flatten in case this an array of arrays,
1114
                        // which happens when a command that outputs a collection gets run in bulk
1115
                        formattedOutputData = _.flatten(
405✔
1116
                                await Promise.all(content.map((o) => this._formatOutputObject(o)))
1,080✔
1117
                        );
1118
                        DEBUG.output('Formatted %d output entries for display', content.length);
405✔
1119
                } else {
1120
                        formattedOutputData = await this._formatOutputObject(content);
6,084✔
1121
                        DEBUG.output('Formatted output content for display');
6,084✔
1122
                }
1123
                let outputFormat = this._getOutputFormat();
6,489✔
1124
                DEBUG.output('Using %s output format', outputFormat);
6,489✔
1125
                DEBUG.output(formattedOutputData);
6,489✔
1126

1127
                let writeFunc;
1128
                let logFunc;
1129
                let stringifiedOutput;
1130

1131
                if (outputFormat === 'json') {
6,489✔
1132
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,095✔
1133

1134
                        let appendNewLineTransform = new Transform({
4,095✔
1135
                                transform(chunk, encoding, callback) {
1136
                                        callback(null, chunk);
36✔
1137
                                },
1138
                                flush(callback) {
1139
                                        this.push(os.EOL);
36✔
1140
                                        callback();
36✔
1141
                                },
1142
                        });
1143

1144
                        writeFunc = async(savePath) => {
4,095✔
1145
                                await pipeline(
36✔
1146
                                        stringifiedOutput,
1147
                                        appendNewLineTransform,
1148
                                        fs.createWriteStream(savePath, { encoding: 'utf8' })
1149
                                );
1150
                        };
1151

1152
                        logFunc = async() => {
4,095✔
1153
                                await this.logStream(stringifiedOutput);
4,059✔
1154
                        };
1155
                } else {
1156
                        stringifiedOutput = await this._stringifyOutput(formattedOutputData);
2,394✔
1157

1158
                        writeFunc = async(savePath) => {
2,394✔
1159
                                await utils.writeFileAsync(savePath, stringifiedOutput + os.EOL, {
×
1160
                                        encoding: 'utf8',
1161
                                });
1162
                        };
1163

1164
                        logFunc = () => this.log(stringifiedOutput);
2,394✔
1165
                }
1166
                return this._writeOutput(writeFunc, logFunc);
6,489✔
1167
        }
1168

1169
        /**
1170
         * Check if max-items has been reached.
1171
         *
1172
         * @param {number} maxItems Total number of items to return
1173
         * @param {number} itemsCount Current number of items
1174
         * @returns {boolean} True if limit has been reached, otherwise false
1175
         * @private
1176
         */
1177
        maxItemsReached(maxItems, itemsCount) {
1178
                return maxItems && itemsCount >= maxItems;
6,714✔
1179
        }
1180

1181
        /**
1182
         * Prepare the output data by:
1183
         *   1) Unrolling an iterator into an array
1184
         *   2) Filtering out unwanted object fields
1185
         *
1186
         * @param {*} obj The raw object containing output data
1187
         * @returns {*} The formatted output data
1188
         * @private
1189
         */
1190
        async _formatOutputObject(obj) {
1191
                let output = obj;
7,164✔
1192

1193
                // Pass primitive content types through
1194
                if (typeof output !== 'object' || output === null) {
7,164!
1195
                        return output;
×
1196
                }
1197

1198
                // Unroll iterator into array
1199
                if (typeof obj.next === 'function') {
7,164✔
1200
                        output = [];
1,458✔
1201
                        let entry = await obj.next();
1,458✔
1202
                        while (!entry.done) {
1,458✔
1203
                                output.push(entry.value);
6,714✔
1204

1205
                                if (this.maxItemsReached(this.flags['max-items'], output.length)) {
6,714✔
1206
                                        break;
18✔
1207
                                }
1208

1209
                                /* eslint-disable no-await-in-loop */
1210
                                entry = await obj.next();
6,696✔
1211
                                /* eslint-enable no-await-in-loop */
1212
                        }
1213
                        DEBUG.output('Unrolled iterable into %d entries', output.length);
1,458✔
1214
                }
1215

1216
                if (this.flags['id-only']) {
7,164✔
1217
                        output = Array.isArray(output)
270!
1218
                                ? this.filterOutput(output, 'id')
1219
                                : output.id;
1220
                } else {
1221
                        output = this.filterOutput(output, this.flags.fields);
6,894✔
1222
                }
1223

1224
                return output;
7,164✔
1225
        }
1226

1227
        /**
1228
         * Get the output format (and file extension) based on the settings and flags set
1229
         *
1230
         * @returns {string} The file extension/format to use for output
1231
         * @private
1232
         */
1233
        _getOutputFormat() {
1234
                if (this.flags.json) {
8,892✔
1235
                        return 'json';
4,104✔
1236
                }
1237

1238
                if (this.flags.csv) {
4,788✔
1239
                        return 'csv';
54✔
1240
                }
1241

1242
                if (this.flags.save || this.flags['save-to-file-path']) {
4,734!
1243
                        return this.settings.boxReportsFileFormat || 'txt';
×
1244
                }
1245

1246
                if (this.settings.outputJson) {
4,734!
1247
                        return 'json';
×
1248
                }
1249

1250
                return 'txt';
4,734✔
1251
        }
1252

1253
        /**
1254
         * Converts output data to a string based on the type of content and flags the user
1255
         * has specified regarding output format
1256
         *
1257
         * @param {*} outputData The data to output
1258
         * @returns {string} Promise resolving to the output data as a string
1259
         * @private
1260
         */
1261
        async _stringifyOutput(outputData) {
1262
                let outputFormat = this._getOutputFormat();
2,394✔
1263

1264
                if (typeof outputData !== 'object') {
2,394✔
1265
                        DEBUG.output('Primitive output cast to string');
270✔
1266
                        return String(outputData);
270✔
1267
                } else if (outputFormat === 'csv') {
2,124✔
1268
                        let csvString = await csvStringify(
27✔
1269
                                this.formatForTableAndCSVOutput(outputData)
1270
                        );
1271
                        // The CSV library puts a trailing newline at the end of the string, which is
1272
                        // redundant with the automatic newline added by oclif when writing to stdout
1273
                        DEBUG.output('Processed output as CSV');
27✔
1274
                        return csvString.replace(/\r?\n$/u, '');
27✔
1275
                } else if (Array.isArray(outputData)) {
2,097✔
1276
                        let str = outputData
63✔
1277
                                .map((o) => `${formatObjectHeader(o)}${os.EOL}${formatObject(o)}`)
153✔
1278
                                .join(os.EOL.repeat(2));
1279
                        DEBUG.output('Processed collection into human-readable output');
63✔
1280
                        return str;
63✔
1281
                }
1282

1283
                let str = formatObject(outputData);
2,034✔
1284
                DEBUG.output('Processed human-readable output');
2,034✔
1285
                return str;
2,034✔
1286
        }
1287

1288
        /**
1289
         * Generate an appropriate default filename for writing
1290
         * the output of this command to disk.
1291
         *
1292
         * @returns {string} The output file name
1293
         * @private
1294
         */
1295
        _getOutputFileName() {
1296
                let extension = this._getOutputFormat();
9✔
1297
                return `${this.id.replace(/:/gu, '-')}-${dateTime.format(
9✔
1298
                        new Date(),
1299
                        'YYYY-MM-DD_HH_mm_ss_SSS'
1300
                )}.${extension}`;
1301
        }
1302

1303
        /**
1304
         * Write output to its final destination, either a file or stdout
1305
         * @param {Function} writeFunc Function used to save output to a file
1306
         * @param {Function} logFunc Function used to print output to stdout
1307
         * @returns {Promise<void>} A promise resolving when output is written
1308
         * @private
1309
         */
1310
        async _writeOutput(writeFunc, logFunc) {
1311
                if (this.flags.save) {
6,489!
1312
                        DEBUG.output('Writing output to default location on disk');
×
1313
                        let filePath = path.join(
×
1314
                                this.settings.boxReportsFolderPath,
1315
                                this._getOutputFileName()
1316
                        );
1317
                        try {
×
1318
                                await writeFunc(filePath);
×
1319
                        } catch (ex) {
1320
                                throw new BoxCLIError(
×
1321
                                        `Could not write output to file at ${filePath}`,
1322
                                        ex
1323
                                );
1324
                        }
1325
                        this.info(chalk`{green Output written to ${filePath}}`);
×
1326
                } else if (this.flags['save-to-file-path']) {
6,489✔
1327
                        let savePath = this.flags['save-to-file-path'];
36✔
1328
                        if (fs.existsSync(savePath)) {
36!
1329
                                if (fs.statSync(savePath).isDirectory()) {
36✔
1330
                                        // Append default file name and write into the provided directory
1331
                                        savePath = path.join(savePath, this._getOutputFileName());
9✔
1332
                                        DEBUG.output(
9✔
1333
                                                'Output path is a directory, will write to %s',
1334
                                                savePath
1335
                                        );
1336
                                } else {
1337
                                        DEBUG.output('File already exists at %s', savePath);
27✔
1338
                                        // Ask if the user want to overwrite the file
1339
                                        let shouldOverwrite = await this.confirm(
27✔
1340
                                                `File ${savePath} already exists — overwrite?`
1341
                                        );
1342

1343
                                        if (!shouldOverwrite) {
27!
1344
                                                return;
×
1345
                                        }
1346
                                }
1347
                        }
1348
                        try {
36✔
1349
                                DEBUG.output(
36✔
1350
                                        'Writing output to specified location on disk: %s',
1351
                                        savePath
1352
                                );
1353
                                await writeFunc(savePath);
36✔
1354
                        } catch (ex) {
1355
                                throw new BoxCLIError(
×
1356
                                        `Could not write output to file at ${savePath}`,
1357
                                        ex
1358
                                );
1359
                        }
1360
                        this.info(chalk`{green Output written to ${savePath}}`);
36✔
1361
                } else {
1362
                        DEBUG.output('Writing output to terminal');
6,453✔
1363
                        await logFunc();
6,453✔
1364
                }
1365

1366
                DEBUG.output('Finished writing output');
6,489✔
1367
        }
1368

1369
        /**
1370
         * Ask a user to confirm something, respecting the default --yes flag
1371
         *
1372
         * @param {string} promptText The text of the prompt to the user
1373
         * @param {boolean} defaultValue The default value of the prompt
1374
         * @returns {Promise<boolean>} A promise resolving to a boolean that is true iff the user confirmed
1375
         */
1376
        async confirm(promptText, defaultValue = false) {
27✔
1377
                if (this.flags.yes) {
27✔
1378
                        return true;
18✔
1379
                }
1380

1381
                let answers = await inquirer.prompt([
9✔
1382
                        {
1383
                                name: 'confirmation',
1384
                                message: promptText,
1385
                                type: 'confirm',
1386
                                default: defaultValue,
1387
                        },
1388
                ]);
1389

1390
                return answers.confirmation;
9✔
1391
        }
1392

1393
        /**
1394
         * Writes output to stderr — this should be used for informational output.  For example, a message
1395
         * stating that an item has been deleted.
1396
         *
1397
         * @param {string} content The message to output
1398
         * @returns {void}
1399
         */
1400
        info(content) {
1401
                if (!this.flags.quiet) {
1,080✔
1402
                        process.stderr.write(`${content}${os.EOL}`);
1,071✔
1403
                }
1404
        }
1405

1406
        /**
1407
         * Writes output to stderr — this should be used for informational output.  For example, a message
1408
         * stating that an item has been deleted.
1409
         *
1410
         * @param {string} content The message to output
1411
         * @returns {void}
1412
         */
1413
        log(content) {
1414
                if (!this.flags.quiet) {
2,394✔
1415
                        process.stdout.write(`${content}${os.EOL}`);
2,376✔
1416
                }
1417
        }
1418

1419
        /**
1420
         * Writes stream output to stderr — this should be used for informational output.  For example, a message
1421
         * stating that an item has been deleted.
1422
         *
1423
         * @param {ReadableStream} content The message to output
1424
         * @returns {void}
1425
         */
1426
        async logStream(content) {
1427
                if (!this.flags.quiet) {
4,059!
1428
                        // For Node 12 when process.stdout is in pipeline it's not emitting end event correctly and it freezes.
1429
                        // See - https://github.com/nodejs/node/issues/34059
1430
                        // Using promise for now.
1431
                        content.pipe(process.stdout);
4,059✔
1432

1433
                        await new Promise((resolve, reject) => {
4,059✔
1434
                                content
4,059✔
1435
                                        .on('end', () => {
1436
                                                process.stdout.write(os.EOL);
4,059✔
1437
                                                resolve();
4,059✔
1438
                                        })
1439
                                        .on('error', (err) => {
1440
                                                reject(err);
×
1441
                                        });
1442
                        });
1443
                }
1444
        }
1445

1446
        /**
1447
         * Wraps filtered error in an error with a user-friendly description
1448
         *
1449
         * @param {Error} err  The thrown error
1450
         * @returns {Error} Error wrapped in an error with user friendly description
1451
         */
1452
        wrapError(err) {
1453
                let messageMap = {
324✔
1454
                        'invalid_grant - Refresh token has expired':
1455
                                'Your refresh token has expired. \nPlease run this command "box login --name <ENVIRONMENT_NAME> --reauthorize" to reauthorize selected environment and then run your command again.',
1456
                };
1457

1458
                for (const key in messageMap) {
324✔
1459
                        if (err.message.includes(key)) {
324!
1460
                                return new BoxCLIError(messageMap[key], err);
×
1461
                        }
1462
                }
1463

1464
                return err;
324✔
1465
        }
1466

1467
        /**
1468
         * Handles an error thrown within a command
1469
         *
1470
         * @param {Error} err  The thrown error
1471
         * @returns {void}
1472
         */
1473
        async catch(err) {
1474
                if (err instanceof BoxTsErrors.BoxApiError && err.responseInfo && err.responseInfo.body) {
297!
1475
                        const responseInfo = err.responseInfo;
×
1476
                        let errorMessage = `Unexpected API Response [${responseInfo.body.status} ${responseInfo.body.message} | ${responseInfo.body.request_id}] ${responseInfo.body.code} - ${responseInfo.body.message}`;
×
1477
                        err = new BoxCLIError(errorMessage, err);
×
1478
                }
1479
                if (err instanceof BoxTsErrors.BoxSdkError) {
297!
1480
                        try {
×
1481
                                let errorObj = JSON.parse(err.message);
×
1482
                                if (errorObj.message) {
×
1483
                                        err = new BoxCLIError(errorObj.message, err);
×
1484
                                }
1485
                        } catch (ex) {
1486
                                // eslint-disable-next-line no-empty
1487
                        }
1488
                }
1489
                try {
297✔
1490
                        // Let the oclif default handler run first, since it handles the help and version flags there
1491
                        /* eslint-disable promise/no-promise-in-callback */
1492
                        DEBUG.execute('Running framework error handler');
297✔
1493
                        await super.catch(this.wrapError(err));
297✔
1494
                        /* eslint-disable no-shadow,no-catch-shadow */
1495
                } catch (err) {
1496
                        // The oclif default catch handler rethrows most errors; handle those here
1497
                        DEBUG.execute('Handling re-thrown error in base command handler');
297✔
1498

1499
                        if (err.code === 'EEXIT') {
297!
1500
                                // oclif throws this when it handled the error itself and wants to exit, so just let it do that
1501
                                DEBUG.execute('Got EEXIT code, exiting immediately');
×
1502
                                return;
×
1503
                        }
1504
                        let contextInfo;
1505
                        if (err.response && err.response.body && err.response.body.context_info) {
297✔
1506
                                contextInfo = formatObject(err.response.body.context_info);
9✔
1507
                                // Remove color codes from context info
1508
                                // eslint-disable-next-line no-control-regex
1509
                                contextInfo = contextInfo.replace(/\u001b\[\d+m/gu, '');
9✔
1510
                                // Remove \n with os.EOL
1511
                                contextInfo = contextInfo.replace(/\n/gu, os.EOL);
9✔
1512
                        }
1513
                        let errorMsg = chalk`{redBright ${
297✔
1514
                                this.flags && this.flags.verbose ? err.stack : err.message
891✔
1515
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
297✔
1516

1517
                        // Write the error message but let the process exit gracefully with error code so stderr gets written out
1518
                        // @NOTE: Exiting the process in the callback enables tests to mock out stderr and run to completion!
1519
                        /* eslint-disable no-process-exit,unicorn/no-process-exit */
1520
                        process.stderr.setEncoding('utf8');
297✔
1521
                        process.stderr.write(errorMsg, () => {
297✔
1522
                                process.exitCode = 2;
297✔
1523
                        });
1524
                        /* eslint-enable no-process-exit,unicorn/no-process-exit */
1525
                }
1526
        }
1527

1528
        /**
1529
         * Final hook that executes for all commands, regardless of if an error occurred
1530
         * @param {Error} [err] An error, if one occurred
1531
         * @returns {void}
1532
         */
1533
        async finally(/* err */) {
1534
                // called after run and catch regardless of whether or not the command errored
1535
        }
1536

1537
        /**
1538
         * Filter out unwanted fields from the output object(s)
1539
         *
1540
         * @param {Object|Object[]} output The output object(s)
1541
         * @param {string} [fields] Comma-separated list of fields to include
1542
         * @returns {Object|Object[]} The filtered object(s) for output
1543
         */
1544
        filterOutput(output, fields) {
1545
                if (!fields) {
6,894✔
1546
                        return output;
6,318✔
1547
                }
1548
                fields = REQUIRED_FIELDS.concat(
576✔
1549
                        fields.split(',').filter((f) => !REQUIRED_FIELDS.includes(f))
711✔
1550
                );
1551
                DEBUG.output('Filtering output with fields: %O', fields);
576✔
1552
                if (Array.isArray(output)) {
576✔
1553
                        output = output.map((o) =>
342✔
1554
                                typeof o === 'object' ? _.pick(o, fields) : o
1,404!
1555
                        );
1556
                } else if (typeof output === 'object') {
234!
1557
                        output = _.pick(output, fields);
234✔
1558
                }
1559
                return output;
576✔
1560
        }
1561

1562
        /**
1563
         * Flatten nested objects for output to a table/CSV
1564
         *
1565
         * @param {Object[]} objectArray The objects that will be output
1566
         * @returns {Array[]} The formatted output
1567
         */
1568
        formatForTableAndCSVOutput(objectArray) {
1569
                let formattedData = [];
27✔
1570
                if (!Array.isArray(objectArray)) {
27!
1571
                        objectArray = [objectArray];
×
1572
                        DEBUG.output('Creating tabular output from single object');
×
1573
                }
1574

1575
                let keyPaths = [];
27✔
1576
                for (let object of objectArray) {
27✔
1577
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1578
                }
1579

1580
                DEBUG.output('Found %d keys for tabular output', keyPaths.length);
27✔
1581
                formattedData.push(keyPaths);
27✔
1582
                for (let object of objectArray) {
27✔
1583
                        let row = [];
126✔
1584
                        if (typeof object === 'object') {
126!
1585
                                for (let keyPath of keyPaths) {
126✔
1586
                                        let value = _.get(object, keyPath);
1,584✔
1587
                                        if (value === null || value === undefined) {
1,584✔
1588
                                                row.push('');
180✔
1589
                                        } else {
1590
                                                row.push(value);
1,404✔
1591
                                        }
1592
                                }
1593
                        } else {
1594
                                row.push(object);
×
1595
                        }
1596
                        DEBUG.output('Processed row with %d values', row.length);
126✔
1597
                        formattedData.push(row);
126✔
1598
                }
1599
                DEBUG.output(
27✔
1600
                        'Processed %d rows of tabular output',
1601
                        formattedData.length - 1
1602
                );
1603
                return formattedData;
27✔
1604
        }
1605

1606
        /**
1607
         * Extracts all keys from an object and flattens them
1608
         *
1609
         * @param {Object} object The object to extract flattened keys from
1610
         * @returns {string[]} The array of flattened keys
1611
         */
1612
        getNestedKeys(object) {
1613
                let keys = [];
405✔
1614
                if (typeof object === 'object') {
405!
1615
                        for (let key in object) {
405✔
1616
                                if (typeof object[key] === 'object' && !Array.isArray(object[key])) {
1,683✔
1617
                                        let subKeys = this.getNestedKeys(object[key]);
279✔
1618
                                        subKeys = subKeys.map((x) => `${key}.${x}`);
1,026✔
1619
                                        keys = keys.concat(subKeys);
279✔
1620
                                } else {
1621
                                        keys.push(key);
1,404✔
1622
                                }
1623
                        }
1624
                }
1625
                return keys;
405✔
1626
        }
1627

1628
        /**
1629
         * Converts time interval shorthand like 5w, -3d, etc to timestamps. It also ensures any timestamp
1630
         * passed in is properly formatted for API calls.
1631
         *
1632
         * @param {string} time The command lint input string for the datetime
1633
         * @returns {string} The full RFC3339-formatted datetime string in UTC
1634
         */
1635
        static normalizeDateString(time) {
1636
                // Attempt to parse date as timestamp or string
1637
                let newDate = time.match(/^\d+$/u)
900!
1638
                        ? dateTime.parse(parseInt(time, 10) * 1000)
1639
                        : dateTime.parse(time);
1640
                if (!dateTime.isValid(newDate)) {
900✔
1641
                        let parsedOffset = time.match(/^(-?)((?:\d+[smhdwMy])+)$/u);
54✔
1642
                        if (parsedOffset) {
54✔
1643
                                let sign = parsedOffset[1] === '-' ? -1 : 1,
18!
1644
                                        offset = parsedOffset[2];
18✔
1645

1646
                                // Transform a string like "-1d2h3m" into an array of arg arrays, e.g.:
1647
                                // [ [-1, "d"], [-2, "h"], [-3, "m"] ]
1648
                                let argPairs = _.chunk(offset.split(/(\d+)/u).slice(1), 2).map(
18✔
1649
                                        (pair) => [sign * parseInt(pair[0], 10), pair[1]]
18✔
1650
                                );
1651

1652
                                // Successively apply the offsets to the current time
1653
                                newDate = argPairs.reduce(
18✔
1654
                                        (d, args) => offsetDate(d, ...args),
18✔
1655
                                        new Date()
1656
                                );
1657
                        } else if (time === 'now') {
36!
1658
                                newDate = new Date();
36✔
1659
                        } else {
1660
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1661
                        }
1662
                }
1663

1664
                // Format the timezone to RFC3339 format for the Box API
1665
                // Also always use UTC timezone for consistency in tests
1666
                return newDate.toISOString().replace(/\.\d{3}Z$/u, '+00:00');
900✔
1667
        }
1668

1669
        /**
1670
         * Writes updated settings to disk
1671
         *
1672
         * @param {Object} updatedSettings The settings object to write
1673
         * @returns {void}
1674
         */
1675
        updateSettings(updatedSettings) {
1676
                this.settings = Object.assign(this.settings, updatedSettings);
×
1677
                try {
×
1678
                        fs.writeFileSync(
×
1679
                                SETTINGS_FILE_PATH,
1680
                                JSON.stringify(this.settings, null, 4),
1681
                                'utf8'
1682
                        );
1683
                } catch (ex) {
1684
                        throw new BoxCLIError(
×
1685
                                `Could not write settings file ${SETTINGS_FILE_PATH}`,
1686
                                ex
1687
                        );
1688
                }
1689
                return this.settings;
×
1690
        }
1691

1692
        /**
1693
         * Read the current set of environments from disk
1694
         *
1695
         * @returns {Object} The parsed environment information
1696
         */
1697
        async getEnvironments() {
1698
                try {
15,129✔
1699
                        switch (process.platform) {
15,129✔
1700
                                case 'darwin': {
1701
                                        try {
5,043✔
1702
                                                const password = await darwinKeychainGetPassword({
5,043✔
1703
                                                        account: 'Box',
1704
                                                        service: 'boxcli',
1705
                                                });
1706
                                                return JSON.parse(password);
5,043✔
1707
                                        } catch (e) {
1708
                                                // fallback to env file if not found
1709
                                        }
1710
                                        break;
×
1711
                                }
1712

1713
                                case 'win32': {
1714
                                        try {
5,043✔
1715
                                                if (!keytar) {
5,043!
1716
                                                        break;
×
1717
                                                }
1718
                                                const password = await keytar.getPassword(
5,043✔
1719
                                                        'boxcli' /* service */,
1720
                                                        'Box' /* account */
1721
                                                );
1722
                                                if (password) {
5,043!
1723
                                                        return JSON.parse(password);
5,043✔
1724
                                                }
1725
                                        } catch (e) {
1726
                                                // fallback to env file if not found
1727
                                        }
1728
                                        break;
×
1729
                                }
1730

1731
                                default:
1732
                        }
1733
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,043✔
1734
                } catch (ex) {
1735
                        throw new BoxCLIError(
×
1736
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1737
                                ex
1738
                        );
1739
                }
1740
        }
1741

1742
        /**
1743
         * Writes updated environment information to disk
1744
         *
1745
         * @param {Object} updatedEnvironments The environment information to write
1746
         * @param {Object} environments use to override current environment
1747
         * @returns {void}
1748
         */
1749
        async updateEnvironments(updatedEnvironments, environments) {
1750
                if (typeof environments === 'undefined') {
9!
1751
                        environments = await this.getEnvironments();
×
1752
                }
1753
                Object.assign(environments, updatedEnvironments);
9✔
1754
                try {
9✔
1755
                        let fileContents = JSON.stringify(environments, null, 4);
9✔
1756
                        switch (process.platform) {
9✔
1757
                                case 'darwin': {
1758
                                        await darwinKeychainSetPassword({
3✔
1759
                                                account: 'Box',
1760
                                                service: 'boxcli',
1761
                                                password: JSON.stringify(environments),
1762
                                        });
1763
                                        fileContents = '';
3✔
1764
                                        break;
3✔
1765
                                }
1766

1767
                                case 'win32': {
1768
                                        if (!keytar) {
3!
1769
                                                break;
×
1770
                                        }
1771
                                        await keytar.setPassword(
3✔
1772
                                                'boxcli' /* service */,
1773
                                                'Box' /* account */,
1774
                                                JSON.stringify(environments) /* password */
1775
                                        );
1776
                                        fileContents = '';
3✔
1777
                                        break;
3✔
1778
                                }
1779

1780
                                default:
1781
                        }
1782

1783
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
9✔
1784
                } catch (ex) {
1785
                        throw new BoxCLIError(
×
1786
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1787
                                ex
1788
                        );
1789
                }
1790
                return environments;
9✔
1791
        }
1792

1793
        /**
1794
         * Initialize the CLI by creating the necessary configuration files on disk
1795
         * in the users' home directory, then read and parse the CLI settings file.
1796
         *
1797
         * @returns {Object} The parsed settings
1798
         * @private
1799
         */
1800
        async _loadSettings() {
1801
                try {
7,560✔
1802
                        if (!fs.existsSync(CONFIG_FOLDER_PATH)) {
7,560✔
1803
                                mkdirp.sync(CONFIG_FOLDER_PATH);
9✔
1804
                                DEBUG.init('Created config folder at %s', CONFIG_FOLDER_PATH);
9✔
1805
                        }
1806
                        if (!fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
7,560✔
1807
                                await this.updateEnvironments({}, this._getDefaultEnvironments());
9✔
1808
                                DEBUG.init('Created environments config at %s', ENVIRONMENTS_FILE_PATH);
9✔
1809
                        }
1810
                        if (!fs.existsSync(SETTINGS_FILE_PATH)) {
7,560✔
1811
                                let settingsJSON = JSON.stringify(this._getDefaultSettings(), null, 4);
9✔
1812
                                fs.writeFileSync(SETTINGS_FILE_PATH, settingsJSON, 'utf8');
9✔
1813
                                DEBUG.init(
9✔
1814
                                        'Created settings file at %s %O',
1815
                                        SETTINGS_FILE_PATH,
1816
                                        settingsJSON
1817
                                );
1818
                        }
1819
                } catch (ex) {
1820
                        throw new BoxCLIError('Could not initialize CLI home directory', ex);
×
1821
                }
1822

1823
                let settings;
1824
                try {
7,560✔
1825
                        settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
7,560✔
1826
                        settings = Object.assign(this._getDefaultSettings(), settings);
7,560✔
1827
                        DEBUG.init('Loaded settings %O', settings);
7,560✔
1828
                } catch (ex) {
1829
                        throw new BoxCLIError(
×
1830
                                `Could not read CLI settings file at ${SETTINGS_FILE_PATH}`,
1831
                                ex
1832
                        );
1833
                }
1834

1835
                try {
7,560✔
1836
                        if (!fs.existsSync(settings.boxReportsFolderPath)) {
7,560✔
1837
                                mkdirp.sync(settings.boxReportsFolderPath);
9✔
1838
                                DEBUG.init(
9✔
1839
                                        'Created reports folder at %s',
1840
                                        settings.boxReportsFolderPath
1841
                                );
1842
                        }
1843
                        if (!fs.existsSync(settings.boxDownloadsFolderPath)) {
7,560✔
1844
                                mkdirp.sync(settings.boxDownloadsFolderPath);
9✔
1845
                                DEBUG.init(
9✔
1846
                                        'Created downloads folder at %s',
1847
                                        settings.boxDownloadsFolderPath
1848
                                );
1849
                        }
1850
                } catch (ex) {
1851
                        throw new BoxCLIError('Failed creating CLI working directory', ex);
×
1852
                }
1853

1854
                return settings;
7,560✔
1855
        }
1856

1857
        /**
1858
         * Get the default settings object
1859
         *
1860
         * @returns {Object} The default settings object
1861
         * @private
1862
         */
1863
        _getDefaultSettings() {
1864
                return {
7,569✔
1865
                        boxReportsFolderPath: path.join(os.homedir(), 'Documents/Box-Reports'),
1866
                        boxReportsFileFormat: 'txt',
1867
                        boxDownloadsFolderPath: path.join(
1868
                                os.homedir(),
1869
                                'Downloads/Box-Downloads'
1870
                        ),
1871
                        outputJson: false,
1872
                        enableProxy: false,
1873
                        proxy: {
1874
                                url: null,
1875
                                username: null,
1876
                                password: null,
1877
                        },
1878
                        enableAnalyticsClient: false,
1879
                        analyticsClient: {
1880
                                name: null,
1881
                        },
1882
                };
1883
        }
1884

1885
        /**
1886
         * Get the default environments object
1887
         *
1888
         * @returns {Object} The default environments object
1889
         * @private
1890
         */
1891
        _getDefaultEnvironments() {
1892
                return {
9✔
1893
                        default: null,
1894
                        environments: {},
1895
                };
1896
        }
1897
}
1898

1899
BoxCommand.flags = {
9✔
1900
        token: Flags.string({
1901
                char: 't',
1902
                description: 'Provide a token to perform this call',
1903
        }),
1904
        'as-user': Flags.string({ description: 'Provide an ID for a user' }),
1905
        // @NOTE: This flag is not read anywhere directly; the chalk library automatically turns off color when it's passed
1906
        'no-color': Flags.boolean({
1907
                description: 'Turn off colors for logging',
1908
        }),
1909
        json: Flags.boolean({
1910
                description: 'Output formatted JSON',
1911
                exclusive: ['csv'],
1912
        }),
1913
        csv: Flags.boolean({
1914
                description: 'Output formatted CSV',
1915
                exclusive: ['json'],
1916
        }),
1917
        save: Flags.boolean({
1918
                char: 's',
1919
                description: 'Save report to default reports folder on disk',
1920
                exclusive: ['save-to-file-path'],
1921
        }),
1922
        'save-to-file-path': Flags.string({
1923
                description: 'Override default file path to save report',
1924
                exclusive: ['save'],
1925
                parse: utils.parsePath,
1926
        }),
1927
        fields: Flags.string({
1928
                description: 'Comma separated list of fields to show',
1929
        }),
1930
        'bulk-file-path': Flags.string({
1931
                description: 'File path to bulk .csv or .json objects',
1932
                parse: utils.parsePath,
1933
        }),
1934
        help: Flags.help({
1935
                char: 'h',
1936
                description: 'Show CLI help',
1937
        }),
1938
        verbose: Flags.boolean({
1939
                char: 'v',
1940
                description: 'Show verbose output, which can be helpful for debugging',
1941
        }),
1942
        yes: Flags.boolean({
1943
                char: 'y',
1944
                description: 'Automatically respond yes to all confirmation prompts',
1945
        }),
1946
        quiet: Flags.boolean({
1947
                char: 'q',
1948
                description: 'Suppress any non-error output to stderr',
1949
        }),
1950
};
1951

1952
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
1953
        'no-color',
1954
        'help',
1955
        'verbose',
1956
        'quiet',
1957
]);
1958

1959
module.exports = BoxCommand;
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc