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

box / boxcli / 15348520616

30 May 2025 02:03PM UTC coverage: 85.368% (-0.03%) from 85.395%
15348520616

Pull #577

github

web-flow
Merge 4c3a97d85 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 %.

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${
388
                                numErrors > 1 ? 'ies' : 'y'
18✔
389
                        } failed!}`,
390
                );
391
                this.bulkErrors.forEach((errorInfo) => {
18✔
392
                        this.info(chalk`{dim ----------}`);
27✔
393
                        let entryData = errorInfo.data
27✔
394
                                .map((o) => `    ${o.fieldKey}=${o.value}`)
18✔
395
                                .join(os.EOL);
396
                        this.info(
27✔
397
                                chalk`{redBright Entry ${errorInfo.index} (${
398
                                        os.EOL + entryData + os.EOL
399
                                }) failed with error:}`,
400
                        );
401
                        let err = errorInfo.error;
27✔
402
                        let contextInfo;
403
                        if (err.response && err.response.body && err.response.body.context_info) {
27✔
404
                                contextInfo = formatObject(err.response.body.context_info);
9✔
405
                                // Remove color codes from context info
406
                                // eslint-disable-next-line no-control-regex
407
                                contextInfo = contextInfo.replace(/\u001b\[\d+m/gu, '');
9✔
408
                                // Remove \n with os.EOL
409
                                contextInfo = contextInfo.replace(/\n/gu, os.EOL);
9✔
410
                        }
411
                        let errMsg = chalk`{redBright ${
27✔
412
                                this.flags && this.flags.verbose ? err.stack : err.message
81!
413
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
27✔
414
                        this.info(errMsg);
27✔
415
                });
416
        }
417

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

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

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

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

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

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

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

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

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

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

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

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

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

736
                        const { clientId, clientSecret, ccgUser } = environment;
×
737

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

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

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

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

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

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

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

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

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

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

899
                        const { clientId, clientSecret, ccgUser } = environment;
×
900

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

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

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

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

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

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

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

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

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

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

1133
                return client;
7,722✔
1134
        }
1135

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

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

1167
                let writeFunc;
1168
                let logFunc;
1169
                let stringifiedOutput;
1170

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1267
                return output;
7,326✔
1268
        }
1269

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

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

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

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

1293
                return 'txt';
4,878✔
1294
        }
1295

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

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

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

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

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

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

1409
                DEBUG.output('Finished writing output');
6,651✔
1410
        }
1411

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

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

1433
                return answers.confirmation;
9✔
1434
        }
1435

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

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

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

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

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

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

1507
                return err;
324✔
1508
        }
1509

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1826
                                default:
1827
                        }
1828

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

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

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

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

1900
                return settings;
7,722✔
1901
        }
1902

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

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

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

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

2005
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