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

box / boxcli / 15347981644

30 May 2025 01:34PM UTC coverage: 85.368% (-0.03%) from 85.395%
15347981644

Pull #577

github

web-flow
Merge 8ce916b0a into 474890c35
Pull Request #577: ci: Setup proxy for box cli with ts sdk

1263 of 1667 branches covered (75.76%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

4507 of 5092 relevant lines covered (88.51%)

633.61 hits per line

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

75.64
/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
 * Removes all the undefined values from the object
123
 *
124
 * @param {Object} obj The object to format for display
125
 * @returns {Object} The formatted object output
126
 */
127
function removeUndefinedValues(obj) {
128
        if (typeof obj !== 'object' || obj === null) {
291,366✔
129
                return obj;
237,024✔
130
        }
131

132
        if (Array.isArray(obj)) {
54,342✔
133
                return obj.map((item) => removeUndefinedValues(item));
13,932✔
134
        }
135

136
        Object.keys(obj).forEach((key) => {
47,592✔
137
                if (obj[key] === undefined) {
270,837✔
138
                        delete obj[key];
54✔
139
                } else {
140
                        obj[key] = removeUndefinedValues(obj[key]);
270,783✔
141
                }
142
        });
143

144
        return obj;
47,592✔
145
}
146

147
/**
148
 * Add or subtract a given offset from a date
149
 *
150
 * @param {Date} date The date to offset
151
 * @param {int} timeLength The number of time units to offset by
152
 * @param {string} timeUnit The unit of time to offset by, in single-character shorthand
153
 * @returns {Date} The date with offset applied
154
 */
155
function offsetDate(date, timeLength, timeUnit) {
156
        switch (timeUnit) {
234!
157
                case 's':
158
                        return dateTime.addSeconds(date, timeLength);
36✔
159
                case 'm':
160
                        return dateTime.addMinutes(date, timeLength);
27✔
161
                case 'h':
162
                        return dateTime.addHours(date, timeLength);
36✔
163
                case 'd':
164
                        return dateTime.addDays(date, timeLength);
54✔
165
                case 'w':
166
                        return dateTime.addWeeks(date, timeLength);
27✔
167
                case 'M':
168
                        return dateTime.addMonths(date, timeLength);
27✔
169
                case 'y':
170
                        return dateTime.addYears(date, timeLength);
27✔
171
                default:
172
                        throw new Error(`Invalid time unit: ${timeUnit}`);
×
173
        }
174
}
175

176
/**
177
 * Formats an API key (e.g. field name) for human-readable display
178
 *
179
 * @param {string} key The key to format
180
 * @returns {string} The formatted key
181
 * @private
182
 */
183
function formatKey(key) {
184
        // Converting camel case to snake case and then to title case
185
        return key
49,617✔
186
                .replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`)
360✔
187
                .split('_')
188
                .map((s) => KEY_MAPPINGS[s] || _.capitalize(s))
67,077✔
189
                .join(' ');
190
}
191

192
/**
193
 * Formats an object's keys for human-readable output
194
 * @param {*} obj The thing to format
195
 * @returns {*} The formatted thing
196
 * @private
197
 */
198
function formatObjectKeys(obj) {
199
        // No need to process primitive values
200
        if (typeof obj !== 'object' || obj === null) {
53,055✔
201
                return obj;
42,849✔
202
        }
203

204
        // If type is Date, convert to ISO string
205
        if (obj instanceof Date) {
10,206✔
206
                return obj.toISOString();
18✔
207
        }
208

209
        // Don't format metadata objects to avoid mangling keys
210
        if (obj.$type) {
10,188✔
211
                return obj;
90✔
212
        }
213

214
        if (Array.isArray(obj)) {
10,098✔
215
                return obj.map((el) => formatObjectKeys(el));
1,305✔
216
        }
217

218
        let formattedObj = Object.create(null);
9,144✔
219
        Object.keys(obj).forEach((key) => {
9,144✔
220
                let formattedKey = formatKey(key);
49,464✔
221
                formattedObj[formattedKey] = formatObjectKeys(obj[key]);
49,464✔
222
        });
223

224
        return formattedObj;
9,144✔
225
}
226

227
/**
228
 * Formats an object for output by prettifying its keys
229
 * and rendering it in a more human-readable form (i.e. YAML)
230
 *
231
 * @param {Object} obj The object to format for display
232
 * @returns {string} The formatted object output
233
 * @private
234
 */
235
function formatObject(obj) {
236
        let outputData = formatObjectKeys(obj);
2,286✔
237

238
        // Other objects are formatted as YAML for human-readable output
239
        let yamlString = yaml.safeDump(outputData, {
2,286✔
240
                indent: 4,
241
                noRefs: true,
242
        });
243

244
        // The YAML library puts a trailing newline at the end of the string, which is
245
        // redundant with the automatic newline added by oclif when writing to stdout
246
        return yamlString
2,286✔
247
                .replace(/\r?\n$/u, '')
248
                .replace(/^([^:]+:)/gmu, (match, key) => chalk.cyan(key));
50,319✔
249
}
250

251
/**
252
 * Formats the object header, used to separate multiple objects in a collection
253
 *
254
 * @param {Object} obj The object to generate a header for
255
 * @returns {string} The header string
256
 * @private
257
 */
258
function formatObjectHeader(obj) {
259
        if (!obj.type || !obj.id) {
153!
260
                return chalk`{dim ----------}`;
×
261
        }
262
        return chalk`{dim ----- ${formatKey(obj.type)} ${obj.id} -----}`;
153✔
263
}
264

265
/**
266
 * Base class for all Box CLI commands
267
 */
268
class BoxCommand extends Command {
269
        // @TODO(2018-08-15): Move all fs methods used here to be async
270
        /* eslint-disable no-sync */
271

272
        /**
273
         * Initialize before the command is run
274
         * @returns {void}
275
         */
276
        async init() {
277
                DEBUG.init('Initializing Box CLI');
7,722✔
278
                let originalArgs, originalFlags;
279
                if (
7,722✔
280
                        this.argv.some((arg) => arg.startsWith('--bulk-file-path')) &&
31,536✔
281
                        Object.keys(this.constructor.flags).includes('bulk-file-path')
282
                ) {
283
                        // Set up the command for bulk run
284
                        DEBUG.init('Preparing for bulk input');
324✔
285
                        this.isBulk = true;
324✔
286
                        originalArgs = _.cloneDeep(this.constructor.args);
324✔
287
                        originalFlags = _.cloneDeep(this.constructor.flags);
324✔
288
                        this.disableRequiredArgsAndFlags();
324✔
289
                }
290

291
                /* eslint-disable no-shadow */
292
                let { flags, args } = await this.parse(this.constructor);
7,722✔
293
                /* eslint-enable no-shadow */
294
                this.flags = flags;
7,722✔
295
                this.args = args;
7,722✔
296
                this.settings = await this._loadSettings();
7,722✔
297
                this.client = await this.getClient();
7,722✔
298
                this.tsClient = await this.getTsClient();
7,722✔
299

300
                if (this.isBulk) {
7,722✔
301
                        this.constructor.args = originalArgs;
324✔
302
                        this.constructor.flags = originalFlags;
324✔
303
                        this.bulkOutputList = [];
324✔
304
                        this.bulkErrors = [];
324✔
305
                        this._singleRun = this.run;
324✔
306
                        this.run = this.bulkOutputRun;
324✔
307
                }
308

309
                DEBUG.execute(
7,722✔
310
                        'Starting execution command: %s argv: %O',
311
                        this.id,
312
                        this.argv
313
                );
314
        }
315

316
        /**
317
         * Read in the input file and run the command once for each set of inputs
318
         * @returns {void}
319
         */
320
        async bulkOutputRun() {
321
                const allPossibleArgs = Object.keys(this.constructor.args || {});
324!
322
                const allPossibleFlags = Object.keys(this.constructor.flags || {});
324!
323
                // Map from matchKey (arg/flag name in all lower-case characters) => {type, fieldKey}
324
                let fieldMapping = Object.assign(
324✔
325
                        {},
326
                        ...allPossibleArgs.map((arg) => ({
486✔
327
                                [arg.toLowerCase()]: { type: 'arg', fieldKey: arg },
328
                        })),
329
                        ...allPossibleFlags.map((flag) => ({
8,082✔
330
                                [flag.replace(/-/gu, '')]: { type: 'flag', fieldKey: flag },
331
                        }))
332
                );
333
                let bulkCalls = await this._parseBulkFile(
324✔
334
                        this.flags['bulk-file-path'],
335
                        fieldMapping
336
                );
337
                let bulkEntryIndex = 0;
279✔
338
                let progressBar = new progress.Bar({
279✔
339
                        format: '[{bar}] {percentage}% | {value}/{total}',
340
                        stopOnComplete: true,
341
                });
342
                progressBar.start(bulkCalls.length, 0);
279✔
343

344
                for (let bulkData of bulkCalls) {
279✔
345
                        /* eslint-disable no-await-in-loop */
346
                        this.argv = [];
603✔
347
                        bulkEntryIndex += 1;
603✔
348
                        this._getArgsForBulkInput(allPossibleArgs, bulkData);
603✔
349
                        this._setFlagsForBulkInput(bulkData);
603✔
350
                        await this._handleAsUserSettings(bulkData);
603✔
351
                        DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
603✔
352
                        // @TODO(2018-08-29): Convert this to a promise queue to improve performance
353
                        /* eslint-disable no-await-in-loop */
354
                        try {
603✔
355
                                await this._singleRun();
603✔
356
                        } catch (err) {
357
                                // In bulk mode, we don't want to write directly to console and kill the command
358
                                // Instead, we should buffer the error output so subsequent commands might be able to succeed
359
                                DEBUG.execute('Caught error from bulk input entry %d', bulkEntryIndex);
27✔
360
                                this.bulkErrors.push({
27✔
361
                                        index: bulkEntryIndex,
362
                                        data: bulkData,
363
                                        error: this.wrapError(err),
364
                                });
365
                        }
366
                        /* eslint-enable no-await-in-loop */
367
                        progressBar.update(bulkEntryIndex);
603✔
368
                }
369
                this.isBulk = false;
279✔
370
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
371
                await this.output(this.bulkOutputList);
279✔
372
                this._handleBulkErrors();
279✔
373
        }
374

375
        /**
376
         * Logs bulk processing errors if any occured.
377
         * @returns {void}
378
         * @private
379
         */
380
        _handleBulkErrors() {
381
                const numErrors = this.bulkErrors.length;
279✔
382
                if (numErrors === 0) {
279✔
383
                        this.info(chalk`{green All bulk input entries processed successfully.}`);
261✔
384
                        return;
261✔
385
                }
386
                this.info(
18✔
387
                        chalk`{redBright ${numErrors} entr${numErrors > 1 ? 'ies' : 'y'} failed!}`
18✔
388
                );
389
                this.bulkErrors.forEach((errorInfo) => {
18✔
390
                        this.info(chalk`{dim ----------}`);
27✔
391
                        let entryData = errorInfo.data
27✔
392
                                .map((o) => `    ${o.fieldKey}=${o.value}`)
18✔
393
                                .join(os.EOL);
394
                        this.info(
27✔
395
                                chalk`{redBright Entry ${errorInfo.index} (${
396
                                        os.EOL + entryData + os.EOL
397
                                }) failed with error:}`
398
                        );
399
                        let err = errorInfo.error;
27✔
400
                        let contextInfo;
401
                        if (err.response && err.response.body && err.response.body.context_info) {
27✔
402
                                contextInfo = formatObject(err.response.body.context_info);
9✔
403
                                // Remove color codes from context info
404
                                // eslint-disable-next-line no-control-regex
405
                                contextInfo = contextInfo.replace(/\u001b\[\d+m/gu, '');
9✔
406
                                // Remove \n with os.EOL
407
                                contextInfo = contextInfo.replace(/\n/gu, os.EOL);
9✔
408
                        }
409
                        let errMsg = chalk`{redBright ${
27✔
410
                                this.flags && this.flags.verbose ? err.stack : err.message
81!
411
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
27✔
412
                        this.info(errMsg);
27✔
413
                });
414
        }
415

416
        /**
417
         * Set as-user header from the bulk file or use the default one.
418
         * @param {Array} bulkData Bulk data
419
         * @returns {Promise<void>} Returns nothing
420
         * @private
421
         */
422
        async _handleAsUserSettings(bulkData) {
423
                let asUser = bulkData.find((o) => o.fieldKey === 'as-user') || {};
1,647✔
424
                if (!_.isEmpty(asUser)) {
603✔
425
                        if (_.isNil(asUser.value)) {
27✔
426
                                let environmentsObj = await this.getEnvironments(); // eslint-disable-line no-await-in-loop
9✔
427
                                if (environmentsObj.default) {
9!
428
                                        let environment =
429
                                                environmentsObj.environments[environmentsObj.default];
×
430
                                        DEBUG.init(
×
431
                                                'Using environment %s %O',
432
                                                environmentsObj.default,
433
                                                environment
434
                                        );
435
                                        if (environment.useDefaultAsUser) {
×
436
                                                this.client.asUser(environment.defaultAsUserId);
×
437
                                                DEBUG.init(
×
438
                                                        'Impersonating default user ID %s',
439
                                                        environment.defaultAsUserId
440
                                                );
441
                                        } else {
442
                                                this.client.asSelf();
×
443
                                        }
444
                                } else {
445
                                        this.client.asSelf();
9✔
446
                                }
447
                        } else {
448
                                this.client.asUser(asUser.value);
18✔
449
                                DEBUG.init('Impersonating user ID %s', asUser.value);
18✔
450
                        }
451
                }
452
        }
453

454
        /**
455
         * Include flag values from command line first,
456
         * they'll automatically be overwritten/combined with later values by the oclif parser.
457
         * @param {Array} bulkData Bulk data
458
         * @returns {void}
459
         * @private
460
         */
461
        _setFlagsForBulkInput(bulkData) {
462
                const bulkDataFlags = bulkData
603✔
463
                        .filter((o) => o.type === 'flag' && !_.isNil(o.value))
1,647✔
464
                        .map((o) => o.fieldKey);
738✔
465
                Object.keys(this.flags)
603✔
466
                        .filter((flag) => flag !== 'bulk-file-path') // Remove the bulk file path flag so we don't recurse!
3,330✔
467
                        .filter((flag) => !bulkDataFlags.includes(flag))
2,727✔
468
                        .forEach((flag) => {
469
                                // Some flags can be specified multiple times in a single command. For these flags, their value is an array of user inputted values.
470
                                // For these flags, we iterate through their values and add each one as a separate flag to comply with oclif
471
                                if (Array.isArray(this.flags[flag])) {
2,322✔
472
                                        this.flags[flag].forEach((value) => {
9✔
473
                                                this._addFlagToArgv(flag, value);
18✔
474
                                        });
475
                                } else {
476
                                        this._addFlagToArgv(flag, this.flags[flag]);
2,313✔
477
                                }
478
                        });
479
                // Include all flag values from bulk input, which will override earlier ones
480
                // from the command line
481
                bulkData
603✔
482
                        // Remove the bulk file path flag so we don't recurse!
483
                        .filter((o) => o.type === 'flag' && o.fieldKey !== 'bulk-file-path')
1,647✔
484
                        .forEach((o) => this._addFlagToArgv(o.fieldKey, o.value));
846✔
485
        }
486

487
        /**
488
         * For each possible arg, find the correct value between bulk input and values given on the command line.
489
         * @param {Array} allPossibleArgs All possible args
490
         * @param {Array} bulkData Bulk data
491
         * @returns {void}
492
         * @private
493
         */
494
        _getArgsForBulkInput(allPossibleArgs, bulkData) {
495
                for (let arg of allPossibleArgs) {
603✔
496
                        let bulkArg = bulkData.find((o) => o.fieldKey === arg) || {};
1,422✔
497
                        if (!_.isNil(bulkArg.value)) {
927✔
498
                                // Use value from bulk input file when available
499
                                this.argv.push(bulkArg.value);
756✔
500
                        } else if (this.args[arg]) {
171✔
501
                                // Fall back to value from command line
502
                                this.argv.push(this.args[arg]);
135✔
503
                        }
504
                }
505
        }
506

507
        /**
508
         * Parses file wilk bulk commands
509
         * @param {String} filePath Path to file with bulk commands
510
         * @param {Array} fieldMapping Data to parse
511
         * @returns {Promise<*>} Returns parsed data
512
         * @private
513
         */
514
        async _parseBulkFile(filePath, fieldMapping) {
515
                const fileExtension = path.extname(filePath);
324✔
516
                const fileContents = this._readBulkFile(filePath);
324✔
517
                let bulkCalls;
518
                if (fileExtension === '.json') {
324✔
519
                        bulkCalls = this._handleJsonFile(fileContents, fieldMapping);
144✔
520
                } else if (fileExtension === '.csv') {
180✔
521
                        bulkCalls = await this._handleCsvFile(fileContents, fieldMapping);
171✔
522
                } else {
523
                        throw new Error(
9✔
524
                                `Input file had extension "${fileExtension}", but only .json and .csv are supported`
525
                        );
526
                }
527
                // Filter out any undefined values, which can arise when the input file contains extraneous keys
528
                bulkCalls = bulkCalls.map((args) => args.filter((o) => o !== undefined));
1,737✔
529
                DEBUG.execute(
279✔
530
                        'Read %d entries from bulk file %s',
531
                        bulkCalls.length,
532
                        this.flags['bulk-file-path']
533
                );
534
                return bulkCalls;
279✔
535
        }
536

537
        /**
538
         * Parses CSV file
539
         * @param {Object} fileContents File content to parse
540
         * @param {Array} fieldMapping Field mapings
541
         * @returns {Promise<string|null|*>} Returns parsed data
542
         * @private
543
         */
544
        async _handleCsvFile(fileContents, fieldMapping) {
545
                let parsedData = await csvParse(fileContents, {
171✔
546
                        bom: true,
547
                        delimiter: ',',
548
                        cast(value, context) {
549
                                if (value.length === 0) {
1,584✔
550
                                        // Regard unquoted empty values as null
551
                                        return context.quoting ? '' : null;
162✔
552
                                }
553
                                return value;
1,422✔
554
                        },
555
                });
556
                if (parsedData.length < 2) {
171✔
557
                        throw new Error(
9✔
558
                                'CSV input file should contain the headers row and at least on data row'
559
                        );
560
                }
561
                // @NOTE: We don't parse the CSV into an aray of Objects
562
                // and instead mainatain a separate array of headers, in
563
                // order to ensure that ordering is maintained in the keys
564
                let headers = parsedData.shift().map((key) => {
162✔
565
                        let keyParts = key.match(/(.*)_\d+$/u);
522✔
566
                        let someKey = keyParts ? keyParts[1] : key;
522✔
567
                        return someKey.toLowerCase().replace(/[-_]/gu, '');
522✔
568
                });
569
                return parsedData.map((values) =>
162✔
570
                        values.map((value, index) => {
324✔
571
                                let key = headers[index];
1,044✔
572
                                let field = fieldMapping[key];
1,044✔
573
                                return field ? { ...field, value } : undefined;
1,044✔
574
                        })
575
                );
576
        }
577

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

646
        /**
647
         * Returns bulk file contents
648
         * @param {String} filePath Path to bulk file
649
         * @returns {Buffer} Bulk file contents
650
         * @private
651
         */
652
        _readBulkFile(filePath) {
653
                try {
324✔
654
                        const fileContents = fs.readFileSync(filePath);
324✔
655
                        DEBUG.execute('Read bulk input file at %s', filePath);
324✔
656
                        return fileContents;
324✔
657
                } catch (ex) {
658
                        throw new BoxCLIError(`Could not open input file ${filePath}`, ex);
×
659
                }
660
        }
661

662
        /**
663
         * Writes a given flag value to the command's argv array
664
         *
665
         * @param {string} flag The flag name
666
         * @param {*} flagValue The flag value
667
         * @returns {void}
668
         * @private
669
         */
670
        _addFlagToArgv(flag, flagValue) {
671
                if (_.isNil(flagValue)) {
3,177✔
672
                        return;
108✔
673
                }
674

675
                if (this.constructor.flags[flag].type === 'boolean') {
3,069✔
676
                        if (getBooleanFlagValue(flagValue)) {
1,593✔
677
                                this.argv.push(`--${flag}`);
1,494✔
678
                        } else {
679
                                this.argv.push(`--no-${flag}`);
99✔
680
                        }
681
                } else {
682
                        this.argv.push(`--${flag}=${flagValue}`);
1,476✔
683
                }
684
        }
685

686
        /**
687
         * Ensure that all args and flags for the command are not marked as required,
688
         * to avoid issues when filling in required values from the input file.
689
         * @returns {void}
690
         */
691
        disableRequiredArgsAndFlags() {
692
                if (this.constructor.args !== undefined) {
324!
693
                        Object.keys(this.constructor.args).forEach((key) => {
324✔
694
                                this.constructor.args[key].required = false;
486✔
695
                        });
696
                }
697

698
                if (this.constructor.flags !== undefined) {
324!
699
                        Object.keys(this.constructor.flags).forEach((key) => {
324✔
700
                                this.constructor.flags[key].required = false;
8,082✔
701
                        });
702
                }
703
        }
704

705
        /**
706
         * Instantiate the SDK client for making API calls
707
         *
708
         * @returns {BoxClient} The client for making API calls in the command
709
         */
710
        async getClient() {
711
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
712
                if (this.constructor.noClient) {
7,722!
713
                        return null;
×
714
                }
715
                let environmentsObj = await this.getEnvironments();
7,722✔
716
                const environment =
717
                        environmentsObj.environments[environmentsObj.default] || {};
7,722✔
718
                const { authMethod } = environment;
7,722✔
719

720
                let client;
721
                if (this.flags.token) {
7,722!
722
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,722✔
723
                        let sdk = new BoxSDK({
7,722✔
724
                                clientID: '',
725
                                clientSecret: '',
726
                                ...SDK_CONFIG,
727
                        });
728
                        this._configureSdk(sdk, { ...SDK_CONFIG });
7,722✔
729
                        this.sdk = sdk;
7,722✔
730
                        client = sdk.getBasicClient(this.flags.token);
7,722✔
731
                } else if (authMethod === 'ccg') {
×
732
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
733

734
                        const { clientId, clientSecret, ccgUser } = environment;
×
735

736
                        if (!clientId || !clientSecret) {
×
737
                                throw new BoxCLIError(
×
738
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
739
                                );
740
                        }
741

742
                        let configObj;
743
                        try {
×
744
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
745
                        } catch (ex) {
746
                                throw new BoxCLIError('Could not read environments config file', ex);
×
747
                        }
748

749
                        const { enterpriseID } = configObj;
×
750
                        const sdk = new BoxSDK({
×
751
                                clientID: clientId,
752
                                clientSecret,
753
                                enterpriseID,
754
                                ...SDK_CONFIG,
755
                        });
756
                        this._configureSdk(sdk, { ...SDK_CONFIG });
×
757
                        this.sdk = sdk;
×
758
                        client = ccgUser
×
759
                                ? sdk.getCCGClientForUser(ccgUser)
760
                                : sdk.getAnonymousClient();
761
                } else if (
×
762
                        environmentsObj.default &&
×
763
                        environmentsObj.environments[environmentsObj.default].authMethod ===
764
                                'oauth20'
765
                ) {
766
                        try {
×
767
                                DEBUG.init(
×
768
                                        'Using environment %s %O',
769
                                        environmentsObj.default,
770
                                        environment
771
                                );
772
                                let tokenCache = new CLITokenCache(environmentsObj.default);
×
773

774
                                let sdk = new BoxSDK({
×
775
                                        clientID: environment.clientId,
776
                                        clientSecret: environment.clientSecret,
777
                                        ...SDK_CONFIG,
778
                                });
779
                                this._configureSdk(sdk, { ...SDK_CONFIG });
×
780
                                this.sdk = sdk;
×
781
                                let tokenInfo = await new Promise((resolve, reject) => {
×
782
                                        // eslint-disable-line promise/avoid-new
783
                                        tokenCache.read((error, localTokenInfo) => {
×
784
                                                if (error) {
×
785
                                                        reject(error);
×
786
                                                } else {
787
                                                        resolve(localTokenInfo);
×
788
                                                }
789
                                        });
790
                                });
791
                                client = sdk.getPersistentClient(tokenInfo, tokenCache);
×
792
                        } catch (err) {
793
                                throw new BoxCLIError(
×
794
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
795
                                );
796
                        }
797
                } else if (environmentsObj.default) {
×
798
                        DEBUG.init(
×
799
                                'Using environment %s %O',
800
                                environmentsObj.default,
801
                                environment
802
                        );
803
                        let tokenCache =
804
                                environment.cacheTokens === false
×
805
                                        ? null
806
                                        : new CLITokenCache(environmentsObj.default);
807
                        let configObj;
808
                        try {
×
809
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
810
                        } catch (ex) {
811
                                throw new BoxCLIError('Could not read environments config file', ex);
×
812
                        }
813

814
                        if (!environment.hasInLinePrivateKey) {
×
815
                                try {
×
816
                                        configObj.boxAppSettings.appAuth.privateKey = fs.readFileSync(
×
817
                                                environment.privateKeyPath,
818
                                                'utf8'
819
                                        );
820
                                        DEBUG.init(
×
821
                                                'Loaded JWT private key from %s',
822
                                                environment.privateKeyPath
823
                                        );
824
                                } catch (ex) {
825
                                        throw new BoxCLIError(
×
826
                                                `Could not read private key file ${environment.privateKeyPath}`,
827
                                                ex
828
                                        );
829
                                }
830
                        }
831

832
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
833
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
834

835
                        client = this.sdk.getAppAuthClient(
×
836
                                'enterprise',
837
                                environment.enterpriseId,
838
                                tokenCache
839
                        );
840
                        DEBUG.init('Initialized client from environment config');
×
841
                } else {
842
                        // No environments set up yet!
843
                        throw new BoxCLIError(
×
844
                                `No default environment found.
845
                                It looks like you haven't configured the Box CLI yet.
846
                                See this command for help adding an environment: box configure:environments:add --help
847
                                Or, supply a token with your command with --token.`.replace(/^\s+/gmu, '')
848
                        );
849
                }
850

851
                // Using the as-user flag should have precedence over the environment setting
852
                if (this.flags['as-user']) {
7,722✔
853
                        client.asUser(this.flags['as-user']);
9✔
854
                        DEBUG.init(
9✔
855
                                'Impersonating user ID %s using the ID provided via the --as-user flag',
856
                                this.flags['as-user']
857
                        );
858
                } else if (!this.flags.token && environment.useDefaultAsUser) {
7,713!
859
                        // We don't want to use any environment settings if a token is passed in the command
860
                        client.asUser(environment.defaultAsUserId);
×
861
                        DEBUG.init(
×
862
                                'Impersonating default user ID %s using environment configuration',
863
                                environment.defaultAsUserId
864
                        );
865
                }
866
                return client;
7,722✔
867
        }
868

869
        /**
870
         * Instantiate the TypeScript SDK client for making API calls
871
         *
872
         * @returns {BoxTSSDK.BoxClient} The TypeScript SDK client for making API calls in the command
873
         */
874
        async getTsClient() {
875
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
876
                if (this.constructor.noClient) {
7,722!
877
                        return null;
×
878
                }
879
                let environmentsObj = await this.getEnvironments();
7,722✔
880
                const environment =
881
                        environmentsObj.environments[environmentsObj.default] || {};
7,722✔
882
                const { authMethod } = environment;
7,722✔
883

884
                let client;
885
                if (this.flags.token) {
7,722!
886
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,722✔
887
                        let tsSdkAuth = new BoxTSSDK.BoxDeveloperTokenAuth({
7,722✔
888
                                token: this.flags.token,
889
                        });
890
                        client = new BoxTSSDK.BoxClient({
7,722✔
891
                                auth: tsSdkAuth,
892
                        });
893
                        client = this._configureTsSdk(client, SDK_CONFIG);
7,722✔
894
                } else if (authMethod === 'ccg') {
×
895
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
896

897
                        const { clientId, clientSecret, ccgUser } = environment;
×
898

899
                        if (!clientId || !clientSecret) {
×
900
                                throw new BoxCLIError(
×
901
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
902
                                );
903
                        }
904

905
                        let configObj;
906
                        try {
×
907
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
908
                        } catch (ex) {
909
                                throw new BoxCLIError('Could not read environments config file', ex);
×
910
                        }
911

912
                        const { enterpriseID } = configObj;
×
913
                        const tokenCache =
914
                                environment.cacheTokens === false
×
915
                                        ? null
916
                                        : new CLITokenCache(environmentsObj.default);
917
                        let ccgConfig = new BoxTSSDK.CcgConfig(
×
918
                                ccgUser
×
919
                                        ? {
920
                                                        clientId,
921
                                                        clientSecret,
922
                                                        userId: ccgUser,
923
                                                        tokenStorage: tokenCache,
924
                                          }
925
                                        : {
926
                                                        clientId,
927
                                                        clientSecret,
928
                                                        enterpriseId: enterpriseID,
929
                                                        tokenStorage: tokenCache,
930
                                          }
931
                        );
932
                        let ccgAuth = new BoxTSSDK.BoxCcgAuth({ config: ccgConfig });
×
933
                        client = new BoxTSSDK.BoxClient({
×
934
                                auth: ccgAuth,
935
                        });
936
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
937
                } else if (
×
938
                        environmentsObj.default &&
×
939
                        environmentsObj.environments[environmentsObj.default].authMethod ===
940
                                'oauth20'
941
                ) {
942
                        try {
×
943
                                DEBUG.init(
×
944
                                        'Using environment %s %O',
945
                                        environmentsObj.default,
946
                                        environment
947
                                );
948
                                const tokenCache = new CLITokenCache(environmentsObj.default);
×
949
                                const oauthConfig = new BoxTSSDK.OAuthConfig({
×
950
                                        clientId: environment.clientId,
951
                                        clientSecret: environment.clientSecret,
952
                                        tokenStorage: tokenCache,
953
                                });
954
                                const oauthAuth = new BoxTSSDK.BoxOAuth({
×
955
                                        config: oauthConfig,
956
                                });
957
                                client = new BoxTSSDK.BoxClient({ auth: oauthAuth });
×
958
                                client = this._configureTsSdk(client, SDK_CONFIG);
×
959
                        } catch (err) {
960
                                throw new BoxCLIError(
×
961
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
962
                                );
963
                        }
964
                } else if (environmentsObj.default) {
×
965
                        DEBUG.init(
×
966
                                'Using environment %s %O',
967
                                environmentsObj.default,
968
                                environment
969
                        );
970
                        let tokenCache =
971
                                environment.cacheTokens === false
×
972
                                        ? null
973
                                        : new CLITokenCache(environmentsObj.default);
974
                        let configObj;
975
                        try {
×
976
                                configObj = JSON.parse(fs.readFileSync(environment.boxConfigFilePath));
×
977
                        } catch (ex) {
978
                                throw new BoxCLIError('Could not read environments config file', ex);
×
979
                        }
980

981
                        if (!environment.hasInLinePrivateKey) {
×
982
                                try {
×
983
                                        configObj.boxAppSettings.appAuth.privateKey = fs.readFileSync(
×
984
                                                environment.privateKeyPath,
985
                                                'utf8'
986
                                        );
987
                                        DEBUG.init(
×
988
                                                'Loaded JWT private key from %s',
989
                                                environment.privateKeyPath
990
                                        );
991
                                } catch (ex) {
992
                                        throw new BoxCLIError(
×
993
                                                `Could not read private key file ${environment.privateKeyPath}`,
994
                                                ex
995
                                        );
996
                                }
997
                        }
998

999
                        const jwtConfig = new BoxTSSDK.JwtConfig({
×
1000
                                clientId: configObj.boxAppSettings.clientID,
1001
                                clientSecret: configObj.boxAppSettings.clientSecret,
1002
                                jwtKeyId: configObj.boxAppSettings.appAuth.publicKeyID,
1003
                                privateKey: configObj.boxAppSettings.appAuth.privateKey,
1004
                                privateKeyPassphrase: configObj.boxAppSettings.appAuth.passphrase,
1005
                                enterpriseId: environment.enterpriseId,
1006
                                tokenStorage: tokenCache,
1007
                        });
1008
                        let jwtAuth = new BoxTSSDK.BoxJwtAuth({ config: jwtConfig });
×
1009
                        client = new BoxTSSDK.BoxClient({ auth: jwtAuth });
×
1010

1011
                        DEBUG.init('Initialized client from environment config');
×
1012
                        if (environment.useDefaultAsUser) {
×
1013
                                client = client.withAsUserHeader(environment.defaultAsUserId);
×
1014
                                DEBUG.init(
×
1015
                                        'Impersonating default user ID %s',
1016
                                        environment.defaultAsUserId
1017
                                );
1018
                        }
1019
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
1020
                } else {
1021
                        // No environments set up yet!
1022
                        throw new BoxCLIError(
×
1023
                                `No default environment found.
1024
                                It looks like you haven't configured the Box CLI yet.
1025
                                See this command for help adding an environment: box configure:environments:add --help
1026
                                Or, supply a token with your command with --token.`.replace(/^\s+/gmu, '')
1027
                        );
1028
                }
1029
                if (this.flags['as-user']) {
7,722✔
1030
                        client = client.withAsUserHeader(this.flags['as-user']);
9✔
1031
                        DEBUG.init('Impersonating user ID %s', this.flags['as-user']);
9✔
1032
                }
1033
                return client;
7,722✔
1034
        }
1035

1036
        /**
1037
         * Configures SDK by using values from settings.json file
1038
         * @param {*} sdk to configure
1039
         * @param {*} config Additional options to use while building configuration
1040
         * @returns {void}
1041
         */
1042
        _configureSdk(sdk, config = {}) {
×
1043
                const clientSettings = { ...config };
7,722✔
1044
                if (this.settings.enableProxy) {
7,722!
1045
                        clientSettings.proxy = this.settings.proxy;
×
1046
                }
1047
                if (this.settings.apiRootURL) {
7,722!
1048
                        clientSettings.apiRootURL = this.settings.apiRootURL;
×
1049
                }
1050
                if (this.settings.uploadAPIRootURL) {
7,722!
1051
                        clientSettings.uploadAPIRootURL = this.settings.uploadAPIRootURL;
×
1052
                }
1053
                if (this.settings.authorizeRootURL) {
7,722!
1054
                        clientSettings.authorizeRootURL = this.settings.authorizeRootURL;
×
1055
                }
1056
                if (this.settings.numMaxRetries) {
7,722!
1057
                        clientSettings.numMaxRetries = this.settings.numMaxRetries;
×
1058
                }
1059
                if (this.settings.retryIntervalMS) {
7,722!
1060
                        clientSettings.retryIntervalMS = this.settings.retryIntervalMS;
×
1061
                }
1062
                if (this.settings.uploadRequestTimeoutMS) {
7,722!
1063
                        clientSettings.uploadRequestTimeoutMS =
×
1064
                                this.settings.uploadRequestTimeoutMS;
1065
                }
1066
                if (
7,722!
1067
                        this.settings.enableAnalyticsClient &&
7,722!
1068
                        this.settings.analyticsClient.name
1069
                ) {
1070
                        clientSettings.analyticsClient.name = `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`;
×
1071
                } else {
1072
                        clientSettings.analyticsClient.name = DEFAULT_ANALYTICS_CLIENT_NAME;
7,722✔
1073
                }
1074

1075
                if (Object.keys(clientSettings).length > 0) {
7,722!
1076
                        DEBUG.init('SDK client settings %s', clientSettings);
7,722✔
1077
                        sdk.configure(clientSettings);
7,722✔
1078
                }
1079
        }
1080

1081
        /**
1082
         * Configures TS SDK by using values from settings.json file
1083
         *
1084
         * @param {BoxTSSDK.BoxClient} client to configure
1085
         * @param {Object} config Additional options to use while building configuration
1086
         * @returns {BoxTSSDK.BoxClient} The configured client
1087
         */
1088
        _configureTsSdk(client, config) {
1089
                let additionalHeaders = config.request.headers;
7,722✔
1090
                let customBaseURL = {
7,722✔
1091
                        baseUrl: 'https://api.box.com',
1092
                        uploadUrl: 'https://upload.box.com/api',
1093
                        oauth2Url: 'https://account.box.com/api/oauth2',
1094
                };
1095
                if (this.settings.enableProxy) {
7,722!
NEW
1096
                        client = client.withProxy(this.settings.proxy);
×
1097
                }
1098
                if (this.settings.apiRootURL) {
7,722!
1099
                        customBaseURL.baseUrl = this.settings.apiRootURL;
×
1100
                }
1101
                if (this.settings.uploadAPIRootURL) {
7,722!
1102
                        customBaseURL.uploadUrl = this.settings.uploadAPIRootURL;
×
1103
                }
1104
                if (this.settings.authorizeRootURL) {
7,722!
1105
                        customBaseURL.oauth2Url = this.settings.authorizeRootURL;
×
1106
                }
1107
                client = client.withCustomBaseUrls(customBaseURL);
7,722✔
1108

1109
                if (this.settings.numMaxRetries) {
7,722!
1110
                        // Not supported in TS SDK
1111
                }
1112
                if (this.settings.retryIntervalMS) {
7,722!
1113
                        // Not supported in TS SDK
1114
                }
1115
                if (this.settings.uploadRequestTimeoutMS) {
7,722!
1116
                        // Not supported in TS SDK
1117
                }
1118
                if (
7,722!
1119
                        this.settings.enableAnalyticsClient &&
7,722!
1120
                        this.settings.analyticsClient.name
1121
                ) {
1122
                        additionalHeaders[
×
1123
                                'X-Box-UA'
1124
                        ] = `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`;
1125
                } else {
1126
                        additionalHeaders['X-Box-UA'] = DEFAULT_ANALYTICS_CLIENT_NAME;
7,722✔
1127
                }
1128
                client = client.withExtraHeaders(additionalHeaders);
7,722✔
1129
                DEBUG.init('TS SDK configured with settings from settings.json');
7,722✔
1130

1131
                return client;
7,722✔
1132
        }
1133
        
1134
        /**
1135
         * Format data for output to stdout
1136
         * @param {*} content The content to output
1137
         * @returns {Promise<void>} A promise resolving when output is handled
1138
         */
1139
        async output(content) {
1140
                if (this.isBulk) {
7,227✔
1141
                        this.bulkOutputList.push(content);
576✔
1142
                        DEBUG.output(
576✔
1143
                                'Added command output to bulk list total: %d',
1144
                                this.bulkOutputList.length
1145
                        );
1146
                        return undefined;
576✔
1147
                }
1148

1149
                let formattedOutputData;
1150
                if (Array.isArray(content)) {
6,651✔
1151
                        // Format each object individually and then flatten in case this an array of arrays,
1152
                        // which happens when a command that outputs a collection gets run in bulk
1153
                        formattedOutputData = _.flatten(
405✔
1154
                                await Promise.all(content.map((o) => this._formatOutputObject(o)))
1,080✔
1155
                        );
1156
                        DEBUG.output('Formatted %d output entries for display', content.length);
405✔
1157
                } else {
1158
                        formattedOutputData = await this._formatOutputObject(content);
6,246✔
1159
                        DEBUG.output('Formatted output content for display');
6,246✔
1160
                }
1161
                let outputFormat = this._getOutputFormat();
6,651✔
1162
                DEBUG.output('Using %s output format', outputFormat);
6,651✔
1163
                DEBUG.output(formattedOutputData);
6,651✔
1164

1165
                let writeFunc;
1166
                let logFunc;
1167
                let stringifiedOutput;
1168

1169
                // remove all the undefined values from the object
1170
                formattedOutputData = removeUndefinedValues(formattedOutputData);
6,651✔
1171

1172
                if (outputFormat === 'json') {
6,651✔
1173
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,176✔
1174

1175
                        let appendNewLineTransform = new Transform({
4,176✔
1176
                                transform(chunk, encoding, callback) {
1177
                                        callback(null, chunk);
36✔
1178
                                },
1179
                                flush(callback) {
1180
                                        this.push(os.EOL);
36✔
1181
                                        callback();
36✔
1182
                                },
1183
                        });
1184

1185
                        writeFunc = async (savePath) => {
4,176✔
1186
                                await pipeline(
36✔
1187
                                        stringifiedOutput,
1188
                                        appendNewLineTransform,
1189
                                        fs.createWriteStream(savePath, { encoding: 'utf8' })
1190
                                );
1191
                        };
1192

1193
                        logFunc = async () => {
4,176✔
1194
                                await this.logStream(stringifiedOutput);
4,140✔
1195
                        };
1196
                } else {
1197
                        stringifiedOutput = await this._stringifyOutput(formattedOutputData);
2,475✔
1198

1199
                        writeFunc = async (savePath) => {
2,475✔
1200
                                await utils.writeFileAsync(savePath, stringifiedOutput + os.EOL, {
9✔
1201
                                        encoding: 'utf8',
1202
                                });
1203
                        };
1204

1205
                        logFunc = () => this.log(stringifiedOutput);
2,475✔
1206
                }
1207
                return this._writeOutput(writeFunc, logFunc);
6,651✔
1208
        }
1209

1210
        /**
1211
         * Check if max-items has been reached.
1212
         *
1213
         * @param {number} maxItems Total number of items to return
1214
         * @param {number} itemsCount Current number of items
1215
         * @returns {boolean} True if limit has been reached, otherwise false
1216
         * @private
1217
         */
1218
        maxItemsReached(maxItems, itemsCount) {
1219
                return maxItems && itemsCount >= maxItems;
6,777✔
1220
        }
1221

1222
        /**
1223
         * Prepare the output data by:
1224
         *   1) Unrolling an iterator into an array
1225
         *   2) Filtering out unwanted object fields
1226
         *
1227
         * @param {*} obj The raw object containing output data
1228
         * @returns {*} The formatted output data
1229
         * @private
1230
         */
1231
        async _formatOutputObject(obj) {
1232
                let output = obj;
7,326✔
1233

1234
                // Pass primitive content types through
1235
                if (typeof output !== 'object' || output === null) {
7,326!
1236
                        return output;
×
1237
                }
1238

1239
                // Unroll iterator into array
1240
                if (typeof obj.next === 'function') {
7,326✔
1241
                        output = [];
1,494✔
1242
                        let entry = await obj.next();
1,494✔
1243
                        while (!entry.done) {
1,494✔
1244
                                output.push(entry.value);
6,777✔
1245

1246
                                if (this.maxItemsReached(this.flags['max-items'], output.length)) {
6,777✔
1247
                                        break;
45✔
1248
                                }
1249

1250
                                /* eslint-disable no-await-in-loop */
1251
                                entry = await obj.next();
6,732✔
1252
                                /* eslint-enable no-await-in-loop */
1253
                        }
1254
                        DEBUG.output('Unrolled iterable into %d entries', output.length);
1,494✔
1255
                }
1256

1257
                if (this.flags['id-only']) {
7,326✔
1258
                        output = Array.isArray(output)
270!
1259
                                ? this.filterOutput(output, 'id')
1260
                                : output.id;
1261
                } else {
1262
                        output = this.filterOutput(output, this.flags.fields);
7,056✔
1263
                }
1264

1265
                return output;
7,326✔
1266
        }
1267

1268
        /**
1269
         * Get the output format (and file extension) based on the settings and flags set
1270
         *
1271
         * @returns {string} The file extension/format to use for output
1272
         * @private
1273
         */
1274
        _getOutputFormat() {
1275
                if (this.flags.json) {
9,144✔
1276
                        return 'json';
4,185✔
1277
                }
1278

1279
                if (this.flags.csv) {
4,959✔
1280
                        return 'csv';
54✔
1281
                }
1282

1283
                if (this.flags.save || this.flags['save-to-file-path']) {
4,905✔
1284
                        return this.settings.boxReportsFileFormat || 'txt';
27!
1285
                }
1286

1287
                if (this.settings.outputJson) {
4,878!
1288
                        return 'json';
×
1289
                }
1290

1291
                return 'txt';
4,878✔
1292
        }
1293

1294
        /**
1295
         * Converts output data to a string based on the type of content and flags the user
1296
         * has specified regarding output format
1297
         *
1298
         * @param {*} outputData The data to output
1299
         * @returns {string} Promise resolving to the output data as a string
1300
         * @private
1301
         */
1302
        async _stringifyOutput(outputData) {
1303
                let outputFormat = this._getOutputFormat();
2,475✔
1304

1305
                if (typeof outputData !== 'object') {
2,475✔
1306
                        DEBUG.output('Primitive output cast to string');
270✔
1307
                        return String(outputData);
270✔
1308
                } else if (outputFormat === 'csv') {
2,205✔
1309
                        let csvString = await csvStringify(
27✔
1310
                                this.formatForTableAndCSVOutput(outputData)
1311
                        );
1312
                        // The CSV library puts a trailing newline at the end of the string, which is
1313
                        // redundant with the automatic newline added by oclif when writing to stdout
1314
                        DEBUG.output('Processed output as CSV');
27✔
1315
                        return csvString.replace(/\r?\n$/u, '');
27✔
1316
                } else if (Array.isArray(outputData)) {
2,178✔
1317
                        let str = outputData
63✔
1318
                                .map((o) => `${formatObjectHeader(o)}${os.EOL}${formatObject(o)}`)
153✔
1319
                                .join(os.EOL.repeat(2));
1320
                        DEBUG.output('Processed collection into human-readable output');
63✔
1321
                        return str;
63✔
1322
                }
1323

1324
                let str = formatObject(outputData);
2,115✔
1325
                DEBUG.output('Processed human-readable output');
2,115✔
1326
                return str;
2,115✔
1327
        }
1328

1329
        /**
1330
         * Generate an appropriate default filename for writing
1331
         * the output of this command to disk.
1332
         *
1333
         * @returns {string} The output file name
1334
         * @private
1335
         */
1336
        _getOutputFileName() {
1337
                let extension = this._getOutputFormat();
18✔
1338
                return `${this.id.replace(/:/gu, '-')}-${dateTime.format(
18✔
1339
                        new Date(),
1340
                        'YYYY-MM-DD_HH_mm_ss_SSS'
1341
                )}.${extension}`;
1342
        }
1343

1344
        /**
1345
         * Write output to its final destination, either a file or stdout
1346
         * @param {Function} writeFunc Function used to save output to a file
1347
         * @param {Function} logFunc Function used to print output to stdout
1348
         * @returns {Promise<void>} A promise resolving when output is written
1349
         * @private
1350
         */
1351
        async _writeOutput(writeFunc, logFunc) {
1352
                if (this.flags.save) {
6,651✔
1353
                        DEBUG.output('Writing output to default location on disk');
9✔
1354
                        let filePath = path.join(
9✔
1355
                                this.settings.boxReportsFolderPath,
1356
                                this._getOutputFileName()
1357
                        );
1358
                        try {
9✔
1359
                                await writeFunc(filePath);
9✔
1360
                        } catch (ex) {
1361
                                throw new BoxCLIError(
×
1362
                                        `Could not write output to file at ${filePath}`,
1363
                                        ex
1364
                                );
1365
                        }
1366
                        this.info(chalk`{green Output written to ${filePath}}`);
9✔
1367
                } else if (this.flags['save-to-file-path']) {
6,642✔
1368
                        let savePath = this.flags['save-to-file-path'];
36✔
1369
                        if (fs.existsSync(savePath)) {
36!
1370
                                if (fs.statSync(savePath).isDirectory()) {
36✔
1371
                                        // Append default file name and write into the provided directory
1372
                                        savePath = path.join(savePath, this._getOutputFileName());
9✔
1373
                                        DEBUG.output(
9✔
1374
                                                'Output path is a directory, will write to %s',
1375
                                                savePath
1376
                                        );
1377
                                } else {
1378
                                        DEBUG.output('File already exists at %s', savePath);
27✔
1379
                                        // Ask if the user want to overwrite the file
1380
                                        let shouldOverwrite = await this.confirm(
27✔
1381
                                                `File ${savePath} already exists — overwrite?`
1382
                                        );
1383

1384
                                        if (!shouldOverwrite) {
27!
1385
                                                return;
×
1386
                                        }
1387
                                }
1388
                        }
1389
                        try {
36✔
1390
                                DEBUG.output(
36✔
1391
                                        'Writing output to specified location on disk: %s',
1392
                                        savePath
1393
                                );
1394
                                await writeFunc(savePath);
36✔
1395
                        } catch (ex) {
1396
                                throw new BoxCLIError(
×
1397
                                        `Could not write output to file at ${savePath}`,
1398
                                        ex
1399
                                );
1400
                        }
1401
                        this.info(chalk`{green Output written to ${savePath}}`);
36✔
1402
                } else {
1403
                        DEBUG.output('Writing output to terminal');
6,606✔
1404
                        await logFunc();
6,606✔
1405
                }
1406

1407
                DEBUG.output('Finished writing output');
6,651✔
1408
        }
1409

1410
        /**
1411
         * Ask a user to confirm something, respecting the default --yes flag
1412
         *
1413
         * @param {string} promptText The text of the prompt to the user
1414
         * @param {boolean} defaultValue The default value of the prompt
1415
         * @returns {Promise<boolean>} A promise resolving to a boolean that is true iff the user confirmed
1416
         */
1417
        async confirm(promptText, defaultValue = false) {
27✔
1418
                if (this.flags.yes) {
27✔
1419
                        return true;
18✔
1420
                }
1421

1422
                let answers = await inquirer.prompt([
9✔
1423
                        {
1424
                                name: 'confirmation',
1425
                                message: promptText,
1426
                                type: 'confirm',
1427
                                default: defaultValue,
1428
                        },
1429
                ]);
1430

1431
                return answers.confirmation;
9✔
1432
        }
1433

1434
        /**
1435
         * Writes output to stderr — this should be used for informational output.  For example, a message
1436
         * stating that an item has been deleted.
1437
         *
1438
         * @param {string} content The message to output
1439
         * @returns {void}
1440
         */
1441
        info(content) {
1442
                if (!this.flags.quiet) {
1,089✔
1443
                        process.stderr.write(`${content}${os.EOL}`);
1,080✔
1444
                }
1445
        }
1446

1447
        /**
1448
         * Writes output to stderr — this should be used for informational output.  For example, a message
1449
         * stating that an item has been deleted.
1450
         *
1451
         * @param {string} content The message to output
1452
         * @returns {void}
1453
         */
1454
        log(content) {
1455
                if (!this.flags.quiet) {
2,466✔
1456
                        process.stdout.write(`${content}${os.EOL}`);
2,448✔
1457
                }
1458
        }
1459

1460
        /**
1461
         * Writes stream output to stderr — this should be used for informational output.  For example, a message
1462
         * stating that an item has been deleted.
1463
         *
1464
         * @param {ReadableStream} content The message to output
1465
         * @returns {void}
1466
         */
1467
        async logStream(content) {
1468
                if (!this.flags.quiet) {
4,140!
1469
                        // For Node 12 when process.stdout is in pipeline it's not emitting end event correctly and it freezes.
1470
                        // See - https://github.com/nodejs/node/issues/34059
1471
                        // Using promise for now.
1472
                        content.pipe(process.stdout);
4,140✔
1473

1474
                        await new Promise((resolve, reject) => {
4,140✔
1475
                                content
4,140✔
1476
                                        .on('end', () => {
1477
                                                process.stdout.write(os.EOL);
4,140✔
1478
                                                resolve();
4,140✔
1479
                                        })
1480
                                        .on('error', (err) => {
1481
                                                reject(err);
×
1482
                                        });
1483
                        });
1484
                }
1485
        }
1486

1487
        /**
1488
         * Wraps filtered error in an error with a user-friendly description
1489
         *
1490
         * @param {Error} err  The thrown error
1491
         * @returns {Error} Error wrapped in an error with user friendly description
1492
         */
1493
        wrapError(err) {
1494
                let messageMap = {
324✔
1495
                        'invalid_grant - Refresh token has expired':
1496
                                '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.',
1497
                };
1498

1499
                for (const key in messageMap) {
324✔
1500
                        if (err.message.includes(key)) {
324!
1501
                                return new BoxCLIError(messageMap[key], err);
×
1502
                        }
1503
                }
1504

1505
                return err;
324✔
1506
        }
1507

1508
        /**
1509
         * Handles an error thrown within a command
1510
         *
1511
         * @param {Error} err  The thrown error
1512
         * @returns {void}
1513
         */
1514
        async catch(err) {
1515
                if (
297!
1516
                        err instanceof BoxTsErrors.BoxApiError &&
297!
1517
                        err.responseInfo &&
1518
                        err.responseInfo.body
1519
                ) {
1520
                        const responseInfo = err.responseInfo;
×
1521
                        let errorMessage = `Unexpected API Response [${responseInfo.body.status} ${responseInfo.body.message} | ${responseInfo.body.request_id}] ${responseInfo.body.code} - ${responseInfo.body.message}`;
×
1522
                        err = new BoxCLIError(errorMessage, err);
×
1523
                }
1524
                if (err instanceof BoxTsErrors.BoxSdkError) {
297!
1525
                        try {
×
1526
                                let errorObj = JSON.parse(err.message);
×
1527
                                if (errorObj.message) {
×
1528
                                        err = new BoxCLIError(errorObj.message, err);
×
1529
                                }
1530
                        } catch (ex) {
1531
                                // eslint-disable-next-line no-empty
1532
                        }
1533
                }
1534
                try {
297✔
1535
                        // Let the oclif default handler run first, since it handles the help and version flags there
1536
                        /* eslint-disable promise/no-promise-in-callback */
1537
                        DEBUG.execute('Running framework error handler');
297✔
1538
                        await super.catch(this.wrapError(err));
297✔
1539
                        /* eslint-disable no-shadow,no-catch-shadow */
1540
                } catch (err) {
1541
                        // The oclif default catch handler rethrows most errors; handle those here
1542
                        DEBUG.execute('Handling re-thrown error in base command handler');
297✔
1543

1544
                        if (err.code === 'EEXIT') {
297!
1545
                                // oclif throws this when it handled the error itself and wants to exit, so just let it do that
1546
                                DEBUG.execute('Got EEXIT code, exiting immediately');
×
1547
                                return;
×
1548
                        }
1549
                        let contextInfo;
1550
                        if (err.response && err.response.body && err.response.body.context_info) {
297✔
1551
                                contextInfo = formatObject(err.response.body.context_info);
9✔
1552
                                // Remove color codes from context info
1553
                                // eslint-disable-next-line no-control-regex
1554
                                contextInfo = contextInfo.replace(/\u001b\[\d+m/gu, '');
9✔
1555
                                // Remove \n with os.EOL
1556
                                contextInfo = contextInfo.replace(/\n/gu, os.EOL);
9✔
1557
                        }
1558
                        let errorMsg = chalk`{redBright ${
297✔
1559
                                this.flags && this.flags.verbose ? err.stack : err.message
891✔
1560
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
297✔
1561

1562
                        // Write the error message but let the process exit gracefully with error code so stderr gets written out
1563
                        // @NOTE: Exiting the process in the callback enables tests to mock out stderr and run to completion!
1564
                        /* eslint-disable no-process-exit,unicorn/no-process-exit */
1565
                        process.stderr.write(errorMsg, () => {
297✔
1566
                                process.exitCode = 2;
297✔
1567
                        });
1568
                        /* eslint-enable no-process-exit,unicorn/no-process-exit */
1569
                }
1570
        }
1571

1572
        /**
1573
         * Final hook that executes for all commands, regardless of if an error occurred
1574
         * @param {Error} [err] An error, if one occurred
1575
         * @returns {void}
1576
         */
1577
        async finally(/* err */) {
1578
                // called after run and catch regardless of whether or not the command errored
1579
        }
1580

1581
        /**
1582
         * Filter out unwanted fields from the output object(s)
1583
         *
1584
         * @param {Object|Object[]} output The output object(s)
1585
         * @param {string} [fields] Comma-separated list of fields to include
1586
         * @returns {Object|Object[]} The filtered object(s) for output
1587
         */
1588
        filterOutput(output, fields) {
1589
                if (!fields) {
7,056✔
1590
                        return output;
6,480✔
1591
                }
1592
                fields = REQUIRED_FIELDS.concat(
576✔
1593
                        fields.split(',').filter((f) => !REQUIRED_FIELDS.includes(f))
711✔
1594
                );
1595
                DEBUG.output('Filtering output with fields: %O', fields);
576✔
1596
                if (Array.isArray(output)) {
576✔
1597
                        output = output.map((o) =>
342✔
1598
                                typeof o === 'object' ? _.pick(o, fields) : o
1,404!
1599
                        );
1600
                } else if (typeof output === 'object') {
234!
1601
                        output = _.pick(output, fields);
234✔
1602
                }
1603
                return output;
576✔
1604
        }
1605

1606
        /**
1607
         * Flatten nested objects for output to a table/CSV
1608
         *
1609
         * @param {Object[]} objectArray The objects that will be output
1610
         * @returns {Array[]} The formatted output
1611
         */
1612
        formatForTableAndCSVOutput(objectArray) {
1613
                let formattedData = [];
27✔
1614
                if (!Array.isArray(objectArray)) {
27!
1615
                        objectArray = [objectArray];
×
1616
                        DEBUG.output('Creating tabular output from single object');
×
1617
                }
1618

1619
                let keyPaths = [];
27✔
1620
                for (let object of objectArray) {
27✔
1621
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1622
                }
1623

1624
                DEBUG.output('Found %d keys for tabular output', keyPaths.length);
27✔
1625
                formattedData.push(keyPaths);
27✔
1626
                for (let object of objectArray) {
27✔
1627
                        let row = [];
126✔
1628
                        if (typeof object === 'object') {
126!
1629
                                for (let keyPath of keyPaths) {
126✔
1630
                                        let value = _.get(object, keyPath);
1,584✔
1631
                                        if (value === null || value === undefined) {
1,584✔
1632
                                                row.push('');
180✔
1633
                                        } else {
1634
                                                row.push(value);
1,404✔
1635
                                        }
1636
                                }
1637
                        } else {
1638
                                row.push(object);
×
1639
                        }
1640
                        DEBUG.output('Processed row with %d values', row.length);
126✔
1641
                        formattedData.push(row);
126✔
1642
                }
1643
                DEBUG.output(
27✔
1644
                        'Processed %d rows of tabular output',
1645
                        formattedData.length - 1
1646
                );
1647
                return formattedData;
27✔
1648
        }
1649

1650
        /**
1651
         * Extracts all keys from an object and flattens them
1652
         *
1653
         * @param {Object} object The object to extract flattened keys from
1654
         * @returns {string[]} The array of flattened keys
1655
         */
1656
        getNestedKeys(object) {
1657
                let keys = [];
405✔
1658
                if (typeof object === 'object') {
405!
1659
                        for (let key in object) {
405✔
1660
                                if (typeof object[key] === 'object' && !Array.isArray(object[key])) {
1,683✔
1661
                                        let subKeys = this.getNestedKeys(object[key]);
279✔
1662
                                        subKeys = subKeys.map((x) => `${key}.${x}`);
1,026✔
1663
                                        keys = keys.concat(subKeys);
279✔
1664
                                } else {
1665
                                        keys.push(key);
1,404✔
1666
                                }
1667
                        }
1668
                }
1669
                return keys;
405✔
1670
        }
1671

1672
        /**
1673
         * Converts time interval shorthand like 5w, -3d, etc to timestamps. It also ensures any timestamp
1674
         * passed in is properly formatted for API calls.
1675
         *
1676
         * @param {string} time The command lint input string for the datetime
1677
         * @returns {string} The full RFC3339-formatted datetime string in UTC
1678
         */
1679
        static normalizeDateString(time) {
1680
                // Attempt to parse date as timestamp or string
1681
                let newDate = time.match(/^\d+$/u)
1,152✔
1682
                        ? dateTime.parse(parseInt(time, 10) * 1000)
1683
                        : dateTime.parse(time);
1684
                if (!dateTime.isValid(newDate)) {
1,152✔
1685
                        let parsedOffset = time.match(/^(-?)((?:\d+[smhdwMy])+)$/u);
261✔
1686
                        if (parsedOffset) {
261✔
1687
                                let sign = parsedOffset[1] === '-' ? -1 : 1,
216✔
1688
                                        offset = parsedOffset[2];
216✔
1689

1690
                                // Transform a string like "-1d2h3m" into an array of arg arrays, e.g.:
1691
                                // [ [-1, "d"], [-2, "h"], [-3, "m"] ]
1692
                                let argPairs = _.chunk(offset.split(/(\d+)/u).slice(1), 2).map(
216✔
1693
                                        (pair) => [sign * parseInt(pair[0], 10), pair[1]]
234✔
1694
                                );
1695

1696
                                // Successively apply the offsets to the current time
1697
                                newDate = argPairs.reduce(
216✔
1698
                                        (d, args) => offsetDate(d, ...args),
234✔
1699
                                        new Date()
1700
                                );
1701
                        } else if (time === 'now') {
45!
1702
                                newDate = new Date();
45✔
1703
                        } else {
1704
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1705
                        }
1706
                }
1707

1708
                // Format the timezone to RFC3339 format for the Box API
1709
                // Also always use UTC timezone for consistency in tests
1710
                return newDate.toISOString().replace(/\.\d{3}Z$/u, '+00:00');
1,152✔
1711
        }
1712

1713
        /**
1714
         * Writes updated settings to disk
1715
         *
1716
         * @param {Object} updatedSettings The settings object to write
1717
         * @returns {void}
1718
         */
1719
        updateSettings(updatedSettings) {
1720
                this.settings = Object.assign(this.settings, updatedSettings);
×
1721
                try {
×
1722
                        fs.writeFileSync(
×
1723
                                SETTINGS_FILE_PATH,
1724
                                JSON.stringify(this.settings, null, 4),
1725
                                'utf8'
1726
                        );
1727
                } catch (ex) {
1728
                        throw new BoxCLIError(
×
1729
                                `Could not write settings file ${SETTINGS_FILE_PATH}`,
1730
                                ex
1731
                        );
1732
                }
1733
                return this.settings;
×
1734
        }
1735

1736
        /**
1737
         * Read the current set of environments from disk
1738
         *
1739
         * @returns {Object} The parsed environment information
1740
         */
1741
        async getEnvironments() {
1742
                try {
15,453✔
1743
                        switch (process.platform) {
15,453✔
1744
                                case 'darwin': {
1745
                                        try {
5,151✔
1746
                                                const password = await darwinKeychainGetPassword({
5,151✔
1747
                                                        account: 'Box',
1748
                                                        service: 'boxcli',
1749
                                                });
1750
                                                return JSON.parse(password);
5,151✔
1751
                                        } catch (e) {
1752
                                                // fallback to env file if not found
1753
                                        }
1754
                                        break;
×
1755
                                }
1756

1757
                                case 'win32': {
1758
                                        try {
5,151✔
1759
                                                if (!keytar) {
5,151!
1760
                                                        break;
×
1761
                                                }
1762
                                                const password = await keytar.getPassword(
5,151✔
1763
                                                        'boxcli' /* service */,
1764
                                                        'Box' /* account */
1765
                                                );
1766
                                                if (password) {
5,151!
1767
                                                        return JSON.parse(password);
5,151✔
1768
                                                }
1769
                                        } catch (e) {
1770
                                                // fallback to env file if not found
1771
                                        }
1772
                                        break;
×
1773
                                }
1774

1775
                                default:
1776
                        }
1777
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,151✔
1778
                } catch (ex) {
1779
                        throw new BoxCLIError(
×
1780
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1781
                                ex
1782
                        );
1783
                }
1784
        }
1785

1786
        /**
1787
         * Writes updated environment information to disk
1788
         *
1789
         * @param {Object} updatedEnvironments The environment information to write
1790
         * @param {Object} environments use to override current environment
1791
         * @returns {void}
1792
         */
1793
        async updateEnvironments(updatedEnvironments, environments) {
1794
                if (typeof environments === 'undefined') {
9!
1795
                        environments = await this.getEnvironments();
×
1796
                }
1797
                Object.assign(environments, updatedEnvironments);
9✔
1798
                try {
9✔
1799
                        let fileContents = JSON.stringify(environments, null, 4);
9✔
1800
                        switch (process.platform) {
9✔
1801
                                case 'darwin': {
1802
                                        await darwinKeychainSetPassword({
3✔
1803
                                                account: 'Box',
1804
                                                service: 'boxcli',
1805
                                                password: JSON.stringify(environments),
1806
                                        });
1807
                                        fileContents = '';
3✔
1808
                                        break;
3✔
1809
                                }
1810

1811
                                case 'win32': {
1812
                                        if (!keytar) {
3!
1813
                                                break;
×
1814
                                        }
1815
                                        await keytar.setPassword(
3✔
1816
                                                'boxcli' /* service */,
1817
                                                'Box' /* account */,
1818
                                                JSON.stringify(environments) /* password */
1819
                                        );
1820
                                        fileContents = '';
3✔
1821
                                        break;
3✔
1822
                                }
1823

1824
                                default:
1825
                        }
1826

1827
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
9✔
1828
                } catch (ex) {
1829
                        throw new BoxCLIError(
×
1830
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1831
                                ex
1832
                        );
1833
                }
1834
                return environments;
9✔
1835
        }
1836

1837
        /**
1838
         * Initialize the CLI by creating the necessary configuration files on disk
1839
         * in the users' home directory, then read and parse the CLI settings file.
1840
         *
1841
         * @returns {Object} The parsed settings
1842
         * @private
1843
         */
1844
        async _loadSettings() {
1845
                try {
7,722✔
1846
                        if (!fs.existsSync(CONFIG_FOLDER_PATH)) {
7,722✔
1847
                                mkdirp.sync(CONFIG_FOLDER_PATH);
9✔
1848
                                DEBUG.init('Created config folder at %s', CONFIG_FOLDER_PATH);
9✔
1849
                        }
1850
                        if (!fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
7,722✔
1851
                                await this.updateEnvironments({}, this._getDefaultEnvironments());
9✔
1852
                                DEBUG.init('Created environments config at %s', ENVIRONMENTS_FILE_PATH);
9✔
1853
                        }
1854
                        if (!fs.existsSync(SETTINGS_FILE_PATH)) {
7,722✔
1855
                                let settingsJSON = JSON.stringify(this._getDefaultSettings(), null, 4);
9✔
1856
                                fs.writeFileSync(SETTINGS_FILE_PATH, settingsJSON, 'utf8');
9✔
1857
                                DEBUG.init(
9✔
1858
                                        'Created settings file at %s %O',
1859
                                        SETTINGS_FILE_PATH,
1860
                                        settingsJSON
1861
                                );
1862
                        }
1863
                } catch (ex) {
1864
                        throw new BoxCLIError('Could not initialize CLI home directory', ex);
×
1865
                }
1866

1867
                let settings;
1868
                try {
7,722✔
1869
                        settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
7,722✔
1870
                        settings = Object.assign(this._getDefaultSettings(), settings);
7,722✔
1871
                        DEBUG.init('Loaded settings %O', settings);
7,722✔
1872
                } catch (ex) {
1873
                        throw new BoxCLIError(
×
1874
                                `Could not read CLI settings file at ${SETTINGS_FILE_PATH}`,
1875
                                ex
1876
                        );
1877
                }
1878

1879
                try {
7,722✔
1880
                        if (!fs.existsSync(settings.boxReportsFolderPath)) {
7,722✔
1881
                                mkdirp.sync(settings.boxReportsFolderPath);
9✔
1882
                                DEBUG.init(
9✔
1883
                                        'Created reports folder at %s',
1884
                                        settings.boxReportsFolderPath
1885
                                );
1886
                        }
1887
                        if (!fs.existsSync(settings.boxDownloadsFolderPath)) {
7,722✔
1888
                                mkdirp.sync(settings.boxDownloadsFolderPath);
9✔
1889
                                DEBUG.init(
9✔
1890
                                        'Created downloads folder at %s',
1891
                                        settings.boxDownloadsFolderPath
1892
                                );
1893
                        }
1894
                } catch (ex) {
1895
                        throw new BoxCLIError('Failed creating CLI working directory', ex);
×
1896
                }
1897

1898
                return settings;
7,722✔
1899
        }
1900

1901
        /**
1902
         * Get the default settings object
1903
         *
1904
         * @returns {Object} The default settings object
1905
         * @private
1906
         */
1907
        _getDefaultSettings() {
1908
                return {
7,731✔
1909
                        boxReportsFolderPath: path.join(os.homedir(), 'Documents/Box-Reports'),
1910
                        boxReportsFileFormat: 'txt',
1911
                        boxDownloadsFolderPath: path.join(
1912
                                os.homedir(),
1913
                                'Downloads/Box-Downloads'
1914
                        ),
1915
                        outputJson: false,
1916
                        enableProxy: false,
1917
                        proxy: {
1918
                                url: null,
1919
                                username: null,
1920
                                password: null,
1921
                        },
1922
                        enableAnalyticsClient: false,
1923
                        analyticsClient: {
1924
                                name: null,
1925
                        },
1926
                };
1927
        }
1928

1929
        /**
1930
         * Get the default environments object
1931
         *
1932
         * @returns {Object} The default environments object
1933
         * @private
1934
         */
1935
        _getDefaultEnvironments() {
1936
                return {
9✔
1937
                        default: null,
1938
                        environments: {},
1939
                };
1940
        }
1941
}
1942

1943
BoxCommand.flags = {
9✔
1944
        token: Flags.string({
1945
                char: 't',
1946
                description: 'Provide a token to perform this call',
1947
        }),
1948
        'as-user': Flags.string({ description: 'Provide an ID for a user' }),
1949
        // @NOTE: This flag is not read anywhere directly; the chalk library automatically turns off color when it's passed
1950
        'no-color': Flags.boolean({
1951
                description: 'Turn off colors for logging',
1952
        }),
1953
        json: Flags.boolean({
1954
                description: 'Output formatted JSON',
1955
                exclusive: ['csv'],
1956
        }),
1957
        csv: Flags.boolean({
1958
                description: 'Output formatted CSV',
1959
                exclusive: ['json'],
1960
        }),
1961
        save: Flags.boolean({
1962
                char: 's',
1963
                description: 'Save report to default reports folder on disk',
1964
                exclusive: ['save-to-file-path'],
1965
        }),
1966
        'save-to-file-path': Flags.string({
1967
                description: 'Override default file path to save report',
1968
                exclusive: ['save'],
1969
                parse: utils.parsePath,
1970
        }),
1971
        fields: Flags.string({
1972
                description: 'Comma separated list of fields to show',
1973
        }),
1974
        'bulk-file-path': Flags.string({
1975
                description: 'File path to bulk .csv or .json objects',
1976
                parse: utils.parsePath,
1977
        }),
1978
        help: Flags.help({
1979
                char: 'h',
1980
                description: 'Show CLI help',
1981
        }),
1982
        verbose: Flags.boolean({
1983
                char: 'v',
1984
                description: 'Show verbose output, which can be helpful for debugging',
1985
        }),
1986
        yes: Flags.boolean({
1987
                char: 'y',
1988
                description: 'Automatically respond yes to all confirmation prompts',
1989
        }),
1990
        quiet: Flags.boolean({
1991
                char: 'q',
1992
                description: 'Suppress any non-error output to stderr',
1993
        }),
1994
};
1995

1996
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
1997
        'no-color',
1998
        'help',
1999
        'verbose',
2000
        'quiet',
2001
]);
2002

2003
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