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

box / boxcli / 13012176007

28 Jan 2025 02:04PM UTC coverage: 85.226% (-1.5%) from 86.684%
13012176007

push

github

web-flow
feat!: Drop support old Node version and integrate typescript SDK (#548)

1213 of 1603 branches covered (75.67%)

Branch coverage included in aggregate %.

38 of 106 new or added lines in 2 files covered. (35.85%)

2 existing lines in 2 files now uncovered.

4319 of 4888 relevant lines covered (88.36%)

395.71 hits per line

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

75.12
/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/command');
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,791✔
107
        let falseValues = ['no', 'n', 'false', '0', 'f', 'off'];
1,791✔
108
        if (typeof value === 'boolean') {
1,791✔
109
                return value;
1,665✔
110
        } else if (trueValues.includes(value.toLowerCase())) {
126✔
111
                return true;
81✔
112
        } else if (falseValues.includes(value.toLowerCase())) {
45!
113
                return false;
45✔
114
        }
115
        let possibleValues = trueValues.concat(falseValues).join(', ');
×
116
        throw new Error(
×
117
                `Incorrect boolean value "${value}" passed. Possible values are ${possibleValues}`
118
        );
119
}
120

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

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

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

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

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

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

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

198
        return formattedObj;
8,955✔
199
}
200

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

645
                if (this.constructor.flags[flag].type === 'boolean') {
3,474✔
646
                        if (getBooleanFlagValue(flagValue)) {
1,791✔
647
                                this.argv.push(`--${flag}`);
1,692✔
648
                        } else {
649
                                this.argv.push(`--no-${flag}`);
99✔
650
                        }
651
                } else {
652
                        this.argv.push(`--${flag}=${flagValue}`);
1,683✔
653
                }
654
        }
655

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

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

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

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

704
                        const { clientId, clientSecret, ccgUser } = environment;
×
705

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

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

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

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

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

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

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

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

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

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

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

NEW
864
                        const { clientId, clientSecret, ccgUser } = environment;
×
865

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

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

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

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

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

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

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

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

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

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

1091
                return client;
7,623✔
1092
        }
1093

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

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

1125
                let writeFunc;
1126
                let logFunc;
1127
                let stringifiedOutput;
1128

1129
                if (outputFormat === 'json') {
6,552✔
1130
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,131✔
1131

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

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

1150
                        logFunc = async() => {
4,131✔
1151
                                await this.logStream(stringifiedOutput);
4,095✔
1152
                        };
1153
                } else {
1154
                        stringifiedOutput = await this._stringifyOutput(formattedOutputData);
2,421✔
1155

1156
                        writeFunc = async(savePath) => {
2,421✔
1157
                                await utils.writeFileAsync(savePath, stringifiedOutput + os.EOL, {
9✔
1158
                                        encoding: 'utf8',
1159
                                });
1160
                        };
1161

1162
                        logFunc = () => this.log(stringifiedOutput);
2,421✔
1163
                }
1164
                return this._writeOutput(writeFunc, logFunc);
6,552✔
1165
        }
1166

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

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

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

1196
                // Unroll iterator into array
1197
                if (typeof obj.next === 'function') {
7,227✔
1198
                        output = [];
1,494✔
1199
                        let entry = await obj.next();
1,494✔
1200
                        while (!entry.done) {
1,494✔
1201
                                output.push(entry.value);
6,777✔
1202

1203
                                if (this.maxItemsReached(this.flags['max-items'], output.length)) {
6,777✔
1204
                                        break;
45✔
1205
                                }
1206

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

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

1222
                return output;
7,227✔
1223
        }
1224

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

1236
                if (this.flags.csv) {
4,851✔
1237
                        return 'csv';
54✔
1238
                }
1239

1240
                if (this.flags.save || this.flags['save-to-file-path']) {
4,797✔
1241
                        return this.settings.boxReportsFileFormat || 'txt';
27!
1242
                }
1243

1244
                if (this.settings.outputJson) {
4,770!
1245
                        return 'json';
×
1246
                }
1247

1248
                return 'txt';
4,770✔
1249
        }
1250

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

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

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

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

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

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

1364
                DEBUG.output('Finished writing output');
6,552✔
1365
        }
1366

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

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

1388
                return answers.confirmation;
9✔
1389
        }
1390

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

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

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

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

1444
        /**
1445
         * Wraps filtered error in an error with a user-friendly description
1446
         *
1447
         * @param {Error} err  The thrown error
1448
         * @returns {Error} Error wrapped in an error with user friendly description
1449
         */
1450
        wrapError(err) {
1451
                let messageMap = {
324✔
1452
                        'invalid_grant - Refresh token has expired':
1453
                                '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.',
1454
                };
1455

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

1462
                return err;
324✔
1463
        }
1464

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

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

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

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

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

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

1570
                let keyPaths = [];
27✔
1571
                for (let object of objectArray) {
27✔
1572
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1573
                }
1574

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

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

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

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

1647
                                // Successively apply the offsets to the current time
1648
                                newDate = argPairs.reduce(
216✔
1649
                                        (d, args) => offsetDate(d, ...args),
234✔
1650
                                        new Date()
1651
                                );
1652
                        } else if (time === 'now') {
45!
1653
                                newDate = new Date();
45✔
1654
                        } else {
1655
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1656
                        }
1657
                }
1658

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

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

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

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

1726
                                default:
1727
                        }
1728
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,085✔
1729
                } catch (ex) {
1730
                        throw new BoxCLIError(
×
1731
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1732
                                ex
1733
                        );
1734
                }
1735
        }
1736

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

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

1775
                                default:
1776
                        }
1777

1778
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
9✔
1779
                } catch (ex) {
1780
                        throw new BoxCLIError(
×
1781
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1782
                                ex
1783
                        );
1784
                }
1785
                return environments;
9✔
1786
        }
1787

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

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

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

1849
                return settings;
7,623✔
1850
        }
1851

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

1880
        /**
1881
         * Get the default environments object
1882
         *
1883
         * @returns {Object} The default environments object
1884
         * @private
1885
         */
1886
        _getDefaultEnvironments() {
1887
                return {
9✔
1888
                        default: null,
1889
                        environments: {},
1890
                };
1891
        }
1892
}
1893

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

1947
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
1948
        'no-color',
1949
        'help',
1950
        'verbose',
1951
        'quiet',
1952
]);
1953

1954
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