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

box / boxcli / 19065258536

04 Nov 2025 10:15AM UTC coverage: 82.696% (-2.9%) from 85.581%
19065258536

Pull #603

github

web-flow
Merge d3a13f3d8 into 6a32aa52c
Pull Request #603: feat: POC auto update CLI

1261 of 1757 branches covered (71.77%)

Branch coverage included in aggregate %.

13 of 166 new or added lines in 2 files covered. (7.83%)

146 existing lines in 3 files now uncovered.

4622 of 5357 relevant lines covered (86.28%)

611.73 hits per line

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

75.69
/src/box-command.js
1
 
2
'use strict';
3

4
const originalEmitWarning = process.emitWarning;
9✔
5
process.emitWarning = (warning, ...args) => {
9✔
6
        const message =
7
                typeof warning === 'string' ? warning : warning?.message || '';
7,839!
8

9
        if (message.includes('DEPRECATION WARNING')) {
7,839✔
10
                return;
7,830✔
11
        }
12
        // If not the BoxClient deprecation warning, call the original emitWarning function
13
        originalEmitWarning.call(process, warning, ...args);
9✔
14
};
15

16
const { Command, Flags } = require('@oclif/core');
9✔
17
const chalk = require('chalk');
9✔
18
const { promisify } = require('node:util');
9✔
19
const _ = require('lodash');
9✔
20
const fs = require('node:fs');
9✔
21
const { mkdirp } = require('mkdirp');
9✔
22
const os = require('node:os');
9✔
23
const path = require('node:path');
9✔
24
const yaml = require('js-yaml');
9✔
25
const csv = require('csv');
9✔
26
const csvParse = promisify(csv.parse);
9✔
27
const csvStringify = promisify(csv.stringify);
9✔
28
const dateTime = require('date-fns');
9✔
29
const BoxSDK = require('box-node-sdk').default;
9✔
30
const BoxTSSDK = require('box-node-sdk/sdk-gen');
9✔
31
const BoxTsErrors = require('box-node-sdk/sdk-gen/box/errors');
9✔
32
const BoxCLIError = require('./cli-error');
9✔
33
const CLITokenCache = require('./token-cache');
9✔
34
const utils = require('./util');
9✔
35
const pkg = require('../package.json');
9✔
36
const inquirer = require('inquirer');
9✔
37
const darwinKeychain = require('keychain');
9✔
38
const { stringifyStream } = require('@discoveryjs/json-ext');
9✔
39
const progress = require('cli-progress');
9✔
40
const darwinKeychainSetPassword = promisify(
9✔
41
        darwinKeychain.setPassword.bind(darwinKeychain)
42
);
43
const darwinKeychainGetPassword = promisify(
9✔
44
        darwinKeychain.getPassword.bind(darwinKeychain)
45
);
46
let keytar = null;
9✔
47
try {
9✔
48
         
49
        keytar = require('keytar');
9✔
50
} catch {
51
        // keytar cannot be imported because the library is not provided for this operating system / architecture
52
}
53

54
const DEBUG = require('./debug');
9✔
55
const stream = require('node:stream');
9✔
56
const pipeline = promisify(stream.pipeline);
9✔
57

58
const { Transform } = require('node:stream');
9✔
59

60
const KEY_MAPPINGS = {
9✔
61
        url: 'URL',
62
        id: 'ID',
63
        etag: 'ETag',
64
        sha1: 'SHA1',
65
        templateKey: 'Template Key',
66
        displayName: 'Display Name',
67
        tos: 'ToS',
68
        statusCode: 'Status Code',
69
        boxReportsFolderPath: 'Box Reports Folder Path',
70
        boxReportsFolderName: 'Box Reports Folder Name (Deprecated)',
71
        boxReportsFileFormat: 'Box Reports File Format',
72
        boxDownloadsFolderPath: 'Box Downloads Folder Path',
73
        boxDownloadsFolderName: 'Box Downloads Folder Name (Deprecated)',
74
        outputJson: 'Output JSON',
75
        clientId: 'Client ID',
76
        enterpriseId: 'Enterprise ID',
77
        boxConfigFilePath: 'Box Config File Path',
78
        hasInLinePrivateKey: 'Has Inline Private Key',
79
        privateKeyPath: 'Private Key Path',
80
        defaultAsUserId: 'Default As-User ID',
81
        useDefaultAsUser: 'Use Default As-User',
82
        cacheTokens: 'Cache Tokens',
83
        ip: 'IP',
84
        operationParams: 'Operation Params',
85
        copyInstanceOnItemCopy: 'Copy Instance On Item Copy',
86
};
87

88
const REQUIRED_FIELDS = ['type', 'id'];
9✔
89

90
const SDK_CONFIG = Object.freeze({
9✔
91
        iterators: true,
92
        analyticsClient: {
93
                version: pkg.version,
94
        },
95
        request: {
96
                headers: {
97
                        'User-Agent': `Box CLI v${pkg.version}`,
98
                },
99
        },
100
});
101

102
const CONFIG_FOLDER_PATH = path.join(os.homedir(), '.box');
9✔
103
const SETTINGS_FILE_PATH = path.join(CONFIG_FOLDER_PATH, 'settings.json');
9✔
104
const ENVIRONMENTS_FILE_PATH = path.join(
9✔
105
        CONFIG_FOLDER_PATH,
106
        'box_environments.json'
107
);
108

109
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
9✔
110

111
/**
112
 * Parse a string value from CSV into the correct boolean value
113
 * @param {string|boolean} value The value to parse
114
 * @returns {boolean} The parsed value
115
 * @private
116
 */
117
function getBooleanFlagValue(value) {
118
        let trueValues = ['yes', 'y', 'true', '1', 't', 'on'];
1,593✔
119
        let falseValues = ['no', 'n', 'false', '0', 'f', 'off'];
1,593✔
120
        if (typeof value === 'boolean') {
1,593✔
121
                return value;
1,467✔
122
        } else if (trueValues.includes(value.toLowerCase())) {
126✔
123
                return true;
81✔
124
        } else if (falseValues.includes(value.toLowerCase())) {
45!
125
                return false;
45✔
126
        }
UNCOV
127
        let possibleValues = [...trueValues, ...falseValues].join(', ');
×
UNCOV
128
        throw new Error(
×
129
                `Incorrect boolean value "${value}" passed. Possible values are ${possibleValues}`
130
        );
131
}
132

133
/**
134
 * Removes all the undefined values from the object
135
 *
136
 * @param {Object} obj The object to format for display
137
 * @returns {Object} The formatted object output
138
 */
139
function removeUndefinedValues(obj) {
140
        if (typeof obj !== 'object' || obj === null) {
294,732✔
141
                return obj;
239,553✔
142
        }
143

144
        if (Array.isArray(obj)) {
55,179✔
145
                return obj.map((item) => removeUndefinedValues(item));
14,031✔
146
        }
147

148
        for (const key of Object.keys(obj)) {
48,330✔
149
                if (obj[key] === undefined) {
274,077✔
150
                        delete obj[key];
126✔
151
                } else {
152
                        obj[key] = removeUndefinedValues(obj[key]);
273,951✔
153
                }
154
        }
155

156
        return obj;
48,330✔
157
}
158

159
/**
160
 * Add or subtract a given offset from a date
161
 *
162
 * @param {Date} date The date to offset
163
 * @param {int} timeLength The number of time units to offset by
164
 * @param {string} timeUnit The unit of time to offset by, in single-character shorthand
165
 * @returns {Date} The date with offset applied
166
 */
167
function offsetDate(date, timeLength, timeUnit) {
168
        switch (timeUnit) {
234!
169
                case 's': {
170
                        return dateTime.addSeconds(date, timeLength);
36✔
171
                }
172
                case 'm': {
173
                        return dateTime.addMinutes(date, timeLength);
27✔
174
                }
175
                case 'h': {
176
                        return dateTime.addHours(date, timeLength);
36✔
177
                }
178
                case 'd': {
179
                        return dateTime.addDays(date, timeLength);
54✔
180
                }
181
                case 'w': {
182
                        return dateTime.addWeeks(date, timeLength);
27✔
183
                }
184
                case 'M': {
185
                        return dateTime.addMonths(date, timeLength);
27✔
186
                }
187
                case 'y': {
188
                        return dateTime.addYears(date, timeLength);
27✔
189
                }
190
                default: {
UNCOV
191
                        throw new Error(`Invalid time unit: ${timeUnit}`);
×
192
                }
193
        }
194
}
195

196
/**
197
 * Formats an API key (e.g. field name) for human-readable display
198
 *
199
 * @param {string} key The key to format
200
 * @returns {string} The formatted key
201
 * @private
202
 */
203
function formatKey(key) {
204
        // Converting camel case to snake case and then to title case
205
        return key
49,752✔
206
                .replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`)
432✔
207
                .split('_')
208
                .map((s) => KEY_MAPPINGS[s] || _.capitalize(s))
67,266✔
209
                .join(' ');
210
}
211

212
/**
213
 * Formats an object's keys for human-readable output
214
 * @param {*} obj The thing to format
215
 * @returns {*} The formatted thing
216
 * @private
217
 */
218
function formatObjectKeys(obj) {
219
        // No need to process primitive values
220
        if (typeof obj !== 'object' || obj === null) {
53,217✔
221
                return obj;
42,912✔
222
        }
223

224
        // If type is Date, convert to ISO string
225
        if (obj instanceof Date) {
10,305✔
226
                return obj.toISOString();
36✔
227
        }
228

229
        // Don't format metadata objects to avoid mangling keys
230
        if (obj.$type) {
10,269✔
231
                return obj;
90✔
232
        }
233

234
        if (Array.isArray(obj)) {
10,179✔
235
                return obj.map((el) => formatObjectKeys(el));
1,323✔
236
        }
237

238
        let formattedObj = Object.create(null);
9,207✔
239
        for (const key of Object.keys(obj)) {
9,207✔
240
                let formattedKey = formatKey(key);
49,599✔
241
                formattedObj[formattedKey] = formatObjectKeys(obj[key]);
49,599✔
242
        }
243

244
        return formattedObj;
9,207✔
245
}
246

247
/**
248
 * Formats an object for output by prettifying its keys
249
 * and rendering it in a more human-readable form (i.e. YAML)
250
 *
251
 * @param {Object} obj The object to format for display
252
 * @returns {string} The formatted object output
253
 * @private
254
 */
255
function formatObject(obj) {
256
        let outputData = formatObjectKeys(obj);
2,295✔
257

258
        // Other objects are formatted as YAML for human-readable output
259
        let yamlString = yaml.safeDump(outputData, {
2,295✔
260
                indent: 4,
261
                noRefs: true,
262
        });
263

264
        // The YAML library puts a trailing newline at the end of the string, which is
265
        // redundant with the automatic newline added by oclif when writing to stdout
266
        return yamlString
2,295✔
267
                .replace(/\r?\n$/u, '')
268
                .replaceAll(/^([^:]+:)/gmu, (match, key) => chalk.cyan(key));
50,454✔
269
}
270

271
/**
272
 * Formats the object header, used to separate multiple objects in a collection
273
 *
274
 * @param {Object} obj The object to generate a header for
275
 * @returns {string} The header string
276
 * @private
277
 */
278
function formatObjectHeader(obj) {
279
        if (!obj.type || !obj.id) {
153!
UNCOV
280
                return chalk`{dim ----------}`;
×
281
        }
282
        return chalk`{dim ----- ${formatKey(obj.type)} ${obj.id} -----}`;
153✔
283
}
284

285
/**
286
 * Base class for all Box CLI commands
287
 */
288
class BoxCommand extends Command {
289
        // @TODO(2018-08-15): Move all fs methods used here to be async
290
         
291

292
        /**
293
         * Initialize before the command is run
294
         * @returns {void}
295
         */
296
        async init() {
297
                DEBUG.init('Initializing Box CLI');
7,830✔
298
                let originalArgs, originalFlags;
299
                if (
7,830✔
300
                        this.argv.some((arg) => arg.startsWith('--bulk-file-path')) &&
32,193✔
301
                        Object.keys(this.constructor.flags).includes('bulk-file-path')
302
                ) {
303
                        // Set up the command for bulk run
304
                        DEBUG.init('Preparing for bulk input');
324✔
305
                        this.isBulk = true;
324✔
306
                        originalArgs = _.cloneDeep(this.constructor.args);
324✔
307
                        originalFlags = _.cloneDeep(this.constructor.flags);
324✔
308
                        this.disableRequiredArgsAndFlags();
324✔
309
                }
310

311
                 
312
                let { flags, args } = await this.parse(this.constructor);
7,830✔
313
                 
314
                this.flags = flags;
7,830✔
315
                this.args = args;
7,830✔
316
                this.settings = await this._loadSettings();
7,830✔
317
                this.client = await this.getClient();
7,830✔
318
                this.tsClient = await this.getTsClient();
7,830✔
319

320
                if (this.isBulk) {
7,830✔
321
                        this.constructor.args = originalArgs;
324✔
322
                        this.constructor.flags = originalFlags;
324✔
323
                        this.bulkOutputList = [];
324✔
324
                        this.bulkErrors = [];
324✔
325
                        this._singleRun = this.run;
324✔
326
                        this.run = this.bulkOutputRun;
324✔
327
                }
328

329
                DEBUG.execute(
7,830✔
330
                        'Starting execution command: %s argv: %O',
331
                        this.id,
332
                        this.argv
333
                );
334
        }
335

336
        /**
337
         * Read in the input file and run the command once for each set of inputs
338
         * @returns {void}
339
         */
340
        async bulkOutputRun() {
341
                const allPossibleArgs = Object.keys(this.constructor.args || {});
324!
342
                const allPossibleFlags = Object.keys(this.constructor.flags || {});
324!
343
                // Map from matchKey (arg/flag name in all lower-case characters) => {type, fieldKey}
344
                let fieldMapping = Object.assign(
324✔
345
                        {},
346
                        ...allPossibleArgs.map((arg) => ({
486✔
347
                                [arg.toLowerCase()]: { type: 'arg', fieldKey: arg },
348
                        })),
349
                        ...allPossibleFlags.map((flag) => ({
8,082✔
350
                                [flag.replaceAll('-', '')]: { type: 'flag', fieldKey: flag },
351
                        }))
352
                );
353
                let bulkCalls = await this._parseBulkFile(
324✔
354
                        this.flags['bulk-file-path'],
355
                        fieldMapping
356
                );
357
                let bulkEntryIndex = 0;
279✔
358
                let progressBar = new progress.Bar({
279✔
359
                        format: '[{bar}] {percentage}% | {value}/{total}',
360
                        stopOnComplete: true,
361
                });
362
                progressBar.start(bulkCalls.length, 0);
279✔
363

364
                for (let bulkData of bulkCalls) {
279✔
365
                         
366
                        this.argv = [];
603✔
367
                        bulkEntryIndex += 1;
603✔
368
                        this._getArgsForBulkInput(allPossibleArgs, bulkData);
603✔
369
                        this._setFlagsForBulkInput(bulkData);
603✔
370
                        await this._handleAsUserSettings(bulkData);
603✔
371
                        DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
603✔
372
                        // @TODO(2018-08-29): Convert this to a promise queue to improve performance
373
                         
374
                        try {
603✔
375
                                await this._singleRun();
603✔
376
                        } catch (error) {
377
                                // In bulk mode, we don't want to write directly to console and kill the command
378
                                // Instead, we should buffer the error output so subsequent commands might be able to succeed
379
                                DEBUG.execute(
27✔
380
                                        'Caught error from bulk input entry %d',
381
                                        bulkEntryIndex
382
                                );
383
                                this.bulkErrors.push({
27✔
384
                                        index: bulkEntryIndex,
385
                                        data: bulkData,
386
                                        error: this.wrapError(error),
387
                                });
388
                        }
389
                         
390
                        progressBar.update(bulkEntryIndex);
603✔
391
                }
392
                this.isBulk = false;
279✔
393
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
394
                await this.output(this.bulkOutputList);
279✔
395
                this._handleBulkErrors();
279✔
396
        }
397

398
        /**
399
         * Logs bulk processing errors if any occured.
400
         * @returns {void}
401
         * @private
402
         */
403
        _handleBulkErrors() {
404
                const numErrors = this.bulkErrors.length;
279✔
405
                if (numErrors === 0) {
279✔
406
                        this.info(
261✔
407
                                chalk`{green All bulk input entries processed successfully.}`
408
                        );
409
                        return;
261✔
410
                }
411
                this.info(
18✔
412
                        chalk`{redBright ${numErrors} entr${numErrors > 1 ? 'ies' : 'y'} failed!}`
18✔
413
                );
414
                for (const errorInfo of this.bulkErrors) {
18✔
415
                        this.info(chalk`{dim ----------}`);
27✔
416
                        let entryData = errorInfo.data
27✔
417
                                .map((o) => `    ${o.fieldKey}=${o.value}`)
18✔
418
                                .join(os.EOL);
419
                        this.info(
27✔
420
                                chalk`{redBright Entry ${errorInfo.index} (${
421
                                        os.EOL + entryData + os.EOL
422
                                }) failed with error:}`
423
                        );
424
                        let err = errorInfo.error;
27✔
425
                        let contextInfo;
426
                        if (
27✔
427
                                err.response &&
45✔
428
                                err.response.body &&
429
                                err.response.body.context_info
430
                        ) {
431
                                contextInfo = formatObject(err.response.body.context_info);
9✔
432
                                // Remove color codes from context info
433
                                // eslint-disable-next-line no-control-regex
434
                                contextInfo = contextInfo.replaceAll(/\u001B\[\d+m/gu, '');
9✔
435
                                // Remove \n with os.EOL
436
                                contextInfo = contextInfo.replaceAll('\n', os.EOL);
9✔
437
                        }
438
                        let errMsg = chalk`{redBright ${
27✔
439
                                this.flags && this.flags.verbose ? err.stack : err.message
81!
440
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
27✔
441
                        this.info(errMsg);
27✔
442
                }
443
        }
444

445
        /**
446
         * Set as-user header from the bulk file or use the default one.
447
         * @param {Array} bulkData Bulk data
448
         * @returns {Promise<void>} Returns nothing
449
         * @private
450
         */
451
        async _handleAsUserSettings(bulkData) {
452
                let asUser = bulkData.find((o) => o.fieldKey === 'as-user') || {};
1,647✔
453
                if (!_.isEmpty(asUser)) {
603✔
454
                        if (_.isNil(asUser.value)) {
27✔
455
                                let environmentsObj = await this.getEnvironments();  
9✔
456
                                if (environmentsObj.default) {
9!
457
                                        let environment =
UNCOV
458
                                                environmentsObj.environments[environmentsObj.default];
×
459
                                        DEBUG.init(
×
460
                                                'Using environment %s %O',
461
                                                environmentsObj.default,
462
                                                environment
463
                                        );
UNCOV
464
                                        if (environment.useDefaultAsUser) {
×
UNCOV
465
                                                this.client.asUser(environment.defaultAsUserId);
×
466
                                                DEBUG.init(
×
467
                                                        'Impersonating default user ID %s',
468
                                                        environment.defaultAsUserId
469
                                                );
470
                                        } else {
UNCOV
471
                                                this.client.asSelf();
×
472
                                        }
473
                                } else {
474
                                        this.client.asSelf();
9✔
475
                                }
476
                        } else {
477
                                this.client.asUser(asUser.value);
18✔
478
                                DEBUG.init('Impersonating user ID %s', asUser.value);
18✔
479
                        }
480
                }
481
        }
482

483
        /**
484
         * Include flag values from command line first,
485
         * they'll automatically be overwritten/combined with later values by the oclif parser.
486
         * @param {Array} bulkData Bulk data
487
         * @returns {void}
488
         * @private
489
         */
490
        _setFlagsForBulkInput(bulkData) {
491
                const bulkDataFlags = new Set(bulkData
603✔
492
                        .filter((o) => o.type === 'flag' && !_.isNil(o.value))
1,647✔
493
                        .map((o) => o.fieldKey));
738✔
494
                for (const flag of Object.keys(this.flags)
603✔
495
                        .filter((flag) => flag !== 'bulk-file-path') // Remove the bulk file path flag so we don't recurse!
3,330✔
496
                        .filter((flag) => !bulkDataFlags.has(flag))) {
2,727✔
497
                                // Some flags can be specified multiple times in a single command. For these flags, their value is an array of user inputted values.
498
                                // For these flags, we iterate through their values and add each one as a separate flag to comply with oclif
499
                                if (Array.isArray(this.flags[flag])) {
2,322✔
500
                                        for (const value of this.flags[flag]) {
9✔
501
                                                this._addFlagToArgv(flag, value);
18✔
502
                                        }
503
                                } else {
504
                                        this._addFlagToArgv(flag, this.flags[flag]);
2,313✔
505
                                }
506
                        }
507
                // Include all flag values from bulk input, which will override earlier ones
508
                // from the command line
509
                for (const o of bulkData
603✔
510
                        // Remove the bulk file path flag so we don't recurse!
511
                        .filter((o) => o.type === 'flag' && o.fieldKey !== 'bulk-file-path')) this._addFlagToArgv(o.fieldKey, o.value);
1,647✔
512
        }
513

514
        /**
515
         * For each possible arg, find the correct value between bulk input and values given on the command line.
516
         * @param {Array} allPossibleArgs All possible args
517
         * @param {Array} bulkData Bulk data
518
         * @returns {void}
519
         * @private
520
         */
521
        _getArgsForBulkInput(allPossibleArgs, bulkData) {
522
                for (let arg of allPossibleArgs) {
603✔
523
                        let bulkArg = bulkData.find((o) => o.fieldKey === arg) || {};
1,422✔
524
                        if (!_.isNil(bulkArg.value)) {
927✔
525
                                // Use value from bulk input file when available
526
                                this.argv.push(bulkArg.value);
756✔
527
                        } else if (this.args[arg]) {
171✔
528
                                // Fall back to value from command line
529
                                this.argv.push(this.args[arg]);
135✔
530
                        }
531
                }
532
        }
533

534
        /**
535
         * Parses file wilk bulk commands
536
         * @param {String} filePath Path to file with bulk commands
537
         * @param {Array} fieldMapping Data to parse
538
         * @returns {Promise<*>} Returns parsed data
539
         * @private
540
         */
541
        async _parseBulkFile(filePath, fieldMapping) {
542
                const fileExtension = path.extname(filePath);
324✔
543
                const fileContents = this._readBulkFile(filePath);
324✔
544
                let bulkCalls;
545
                if (fileExtension === '.json') {
324✔
546
                        bulkCalls = this._handleJsonFile(fileContents, fieldMapping);
144✔
547
                } else if (fileExtension === '.csv') {
180✔
548
                        bulkCalls = await this._handleCsvFile(fileContents, fieldMapping);
171✔
549
                } else {
550
                        throw new Error(
9✔
551
                                `Input file had extension "${fileExtension}", but only .json and .csv are supported`
552
                        );
553
                }
554
                // Filter out any undefined values, which can arise when the input file contains extraneous keys
555
                bulkCalls = bulkCalls.map((args) =>
279✔
556
                        args.filter((o) => o !== undefined)
1,737✔
557
                );
558
                DEBUG.execute(
279✔
559
                        'Read %d entries from bulk file %s',
560
                        bulkCalls.length,
561
                        this.flags['bulk-file-path']
562
                );
563
                return bulkCalls;
279✔
564
        }
565

566
        /**
567
         * Parses CSV file
568
         * @param {Object} fileContents File content to parse
569
         * @param {Array} fieldMapping Field mapings
570
         * @returns {Promise<string|null|*>} Returns parsed data
571
         * @private
572
         */
573
        async _handleCsvFile(fileContents, fieldMapping) {
574
                let parsedData = await csvParse(fileContents, {
171✔
575
                        bom: true,
576
                        delimiter: ',',
577
                        cast(value, context) {
578
                                if (value.length === 0) {
1,584✔
579
                                        // Regard unquoted empty values as null
580
                                        return context.quoting ? '' : null;
162✔
581
                                }
582
                                return value;
1,422✔
583
                        },
584
                });
585
                if (parsedData.length < 2) {
171✔
586
                        throw new Error(
9✔
587
                                'CSV input file should contain the headers row and at least on data row'
588
                        );
589
                }
590
                // @NOTE: We don't parse the CSV into an aray of Objects
591
                // and instead mainatain a separate array of headers, in
592
                // order to ensure that ordering is maintained in the keys
593
                let headers = parsedData.shift().map((key) => {
162✔
594
                        let keyParts = key.match(/(.*)_\d+$/u);
522✔
595
                        let someKey = keyParts ? keyParts[1] : key;
522✔
596
                        return someKey.toLowerCase().replaceAll(/[-_]/gu, '');
522✔
597
                });
598
                return parsedData.map((values) =>
162✔
599
                        values.map((value, index) => {
324✔
600
                                let key = headers[index];
1,044✔
601
                                let field = fieldMapping[key];
1,044✔
602
                                return field ? { ...field, value } : undefined;
1,044✔
603
                        })
604
                );
605
        }
606

607
        /**
608
         * Parses JSON file
609
         * @param {Object} fileContents File content to parse
610
         * @param {Array} fieldMapping Field mapings
611
         * @returns {*} Returns parsed data
612
         * @private
613
         */
614
        _handleJsonFile(fileContents, fieldMapping) {
615
                let parsedData;
616
                try {
144✔
617
                        let jsonFile = JSON.parse(fileContents);
144✔
618
                        parsedData = jsonFile.hasOwnProperty('entries')
126✔
619
                                ? jsonFile.entries
620
                                : jsonFile;
621
                } catch (error) {
622
                        throw new BoxCLIError(
18✔
623
                                `Could not parse JSON input file ${this.flags['bulk-file-path']}`,
624
                                error
625
                        );
626
                }
627
                if (!Array.isArray(parsedData)) {
126✔
628
                        throw new TypeError(
9✔
629
                                'Expected input file to contain an array of input objects, but none found'
630
                        );
631
                }
632
                // Translate each row object to an array of {type, fieldKey, value}, to be handled below
633
                return parsedData.map(function flattenObjectToArgs(obj) {
117✔
634
                        // One top-level object key can map to multiple args/flags, so we need to deeply flatten after mapping
635
                        return _.flatMapDeep(obj, (value, key) => {
315✔
636
                                let matchKey = key.toLowerCase().replaceAll(/[-_]/gu, '');
693✔
637
                                let field = fieldMapping[matchKey];
693✔
638
                                if (_.isPlainObject(value)) {
693✔
639
                                        // Map e.g. { item: { id: 12345, type: folder } } => { item: 12345, itemtype: folder }
640
                                        // @NOTE: For now, we only support nesting keys this way one level deep
641
                                        return Object.keys(value).map((nestedKey) => {
18✔
642
                                                let nestedMatchKey =
643
                                                        matchKey +
27✔
644
                                                        nestedKey.toLowerCase().replaceAll(/[-_]/gu, '');
645
                                                let nestedField = fieldMapping[nestedMatchKey];
27✔
646
                                                return nestedField
27✔
647
                                                        ? { ...nestedField, value: value[nestedKey] }
648
                                                        : undefined;
649
                                        });
650
                                } else if (Array.isArray(value)) {
675✔
651
                                        // Arrays can be one of two things: an array of values for a single key,
652
                                        // or an array of grouped flags/args as objects
653
                                        // First, check if everything in the array is either all object or all non-object
654
                                        let types = value.reduce(
27✔
655
                                                (acc, t) => acc.concat(typeof t),
63✔
656
                                                []
657
                                        );
658
                                        if (
27!
659
                                                types.some((t) => t !== 'object') &&
54✔
660
                                                types.includes('object')
661
                                        ) {
662
                                                throw new BoxCLIError(
×
663
                                                        'Mixed types in bulk input JSON array; use strings or Objects'
664
                                                );
665
                                        }
666
                                        // If everything in the array is objects, handle each one as a group of flags and args
667
                                        // by recursively parsing that object into args
668
                                        if (types[0] === 'object') {
27✔
669
                                                return value.map((o) => flattenObjectToArgs(o));
36✔
670
                                        }
671
                                        // If the array is of values for this field, just return those
672
                                        return field
18✔
673
                                                ? value.map((v) => ({ ...field, value: v }))
18✔
674
                                                : [];
675
                                }
676
                                return field ? { ...field, value } : undefined;
648✔
677
                        });
678
                });
679
        }
680

681
        /**
682
         * Returns bulk file contents
683
         * @param {String} filePath Path to bulk file
684
         * @returns {Buffer} Bulk file contents
685
         * @private
686
         */
687
        _readBulkFile(filePath) {
688
                try {
324✔
689
                        const fileContents = fs.readFileSync(filePath);
324✔
690
                        DEBUG.execute('Read bulk input file at %s', filePath);
324✔
691
                        return fileContents;
324✔
692
                } catch (error) {
693
                        throw new BoxCLIError(`Could not open input file ${filePath}`, error);
×
694
                }
695
        }
696

697
        /**
698
         * Writes a given flag value to the command's argv array
699
         *
700
         * @param {string} flag The flag name
701
         * @param {*} flagValue The flag value
702
         * @returns {void}
703
         * @private
704
         */
705
        _addFlagToArgv(flag, flagValue) {
706
                if (_.isNil(flagValue)) {
3,177✔
707
                        return;
108✔
708
                }
709

710
                if (this.constructor.flags[flag].type === 'boolean') {
3,069✔
711
                        if (getBooleanFlagValue(flagValue)) {
1,593✔
712
                                this.argv.push(`--${flag}`);
1,494✔
713
                        } else {
714
                                this.argv.push(`--no-${flag}`);
99✔
715
                        }
716
                } else {
717
                        this.argv.push(`--${flag}=${flagValue}`);
1,476✔
718
                }
719
        }
720

721
        /**
722
         * Ensure that all args and flags for the command are not marked as required,
723
         * to avoid issues when filling in required values from the input file.
724
         * @returns {void}
725
         */
726
        disableRequiredArgsAndFlags() {
727
                if (this.constructor.args !== undefined) {
324!
728
                        for (const key of Object.keys(this.constructor.args)) {
324✔
729
                                this.constructor.args[key].required = false;
486✔
730
                        }
731
                }
732

733
                if (this.constructor.flags !== undefined) {
324!
734
                        for (const key of Object.keys(this.constructor.flags)) {
324✔
735
                                this.constructor.flags[key].required = false;
8,082✔
736
                        }
737
                }
738
        }
739

740
        /**
741
         * Instantiate the SDK client for making API calls
742
         *
743
         * @returns {BoxClient} The client for making API calls in the command
744
         */
745
        async getClient() {
746
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
747
                if (this.constructor.noClient) {
7,830!
UNCOV
748
                        return null;
×
749
                }
750
                let environmentsObj = await this.getEnvironments();
7,830✔
751
                const environment =
752
                        environmentsObj.environments[environmentsObj.default] || {};
7,830✔
753
                const { authMethod } = environment;
7,830✔
754

755
                let client;
756
                if (this.flags.token) {
7,830!
757
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,830✔
758
                        let sdk = new BoxSDK({
7,830✔
759
                                clientID: '',
760
                                clientSecret: '',
761
                                ...SDK_CONFIG,
762
                        });
763
                        this._configureSdk(sdk, { ...SDK_CONFIG });
7,830✔
764
                        this.sdk = sdk;
7,830✔
765
                        client = sdk.getBasicClient(this.flags.token);
7,830✔
UNCOV
766
                } else if (authMethod === 'ccg') {
×
UNCOV
767
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
768

769
                        const { clientId, clientSecret, ccgUser } = environment;
×
770

UNCOV
771
                        if (!clientId || !clientSecret) {
×
772
                                throw new BoxCLIError(
×
773
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
774
                                );
775
                        }
776

777
                        let configObj;
UNCOV
778
                        try {
×
UNCOV
779
                                configObj = JSON.parse(
×
780
                                        fs.readFileSync(environment.boxConfigFilePath)
781
                                );
782
                        } catch (error) {
UNCOV
783
                                throw new BoxCLIError(
×
784
                                        'Could not read environments config file',
785
                                        error
786
                                );
787
                        }
788

UNCOV
789
                        const { enterpriseID } = configObj;
×
UNCOV
790
                        const sdk = new BoxSDK({
×
791
                                clientID: clientId,
792
                                clientSecret,
793
                                enterpriseID,
794
                                ...SDK_CONFIG,
795
                        });
UNCOV
796
                        this._configureSdk(sdk, { ...SDK_CONFIG });
×
UNCOV
797
                        this.sdk = sdk;
×
UNCOV
798
                        client = ccgUser
×
799
                                ? sdk.getCCGClientForUser(ccgUser)
800
                                : sdk.getAnonymousClient();
801
                } else if (
×
802
                        environmentsObj.default &&
×
803
                        environmentsObj.environments[environmentsObj.default].authMethod ===
804
                                'oauth20'
805
                ) {
UNCOV
806
                        try {
×
UNCOV
807
                                DEBUG.init(
×
808
                                        'Using environment %s %O',
809
                                        environmentsObj.default,
810
                                        environment
811
                                );
UNCOV
812
                                let tokenCache = new CLITokenCache(environmentsObj.default);
×
813

UNCOV
814
                                let sdk = new BoxSDK({
×
815
                                        clientID: environment.clientId,
816
                                        clientSecret: environment.clientSecret,
817
                                        ...SDK_CONFIG,
818
                                });
UNCOV
819
                                this._configureSdk(sdk, { ...SDK_CONFIG });
×
UNCOV
820
                                this.sdk = sdk;
×
UNCOV
821
                                let tokenInfo = await new Promise((resolve, reject) => {
×
822
                                         
823
                                        tokenCache.read((error, localTokenInfo) => {
×
824
                                                if (error) {
×
UNCOV
825
                                                        reject(error);
×
826
                                                } else {
827
                                                        resolve(localTokenInfo);
×
828
                                                }
829
                                        });
830
                                });
UNCOV
831
                                client = sdk.getPersistentClient(tokenInfo, tokenCache);
×
832
                        } catch {
833
                                throw new BoxCLIError(
×
834
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
835
                                );
836
                        }
UNCOV
837
                } else if (environmentsObj.default) {
×
UNCOV
838
                        DEBUG.init(
×
839
                                'Using environment %s %O',
840
                                environmentsObj.default,
841
                                environment
842
                        );
843
                        let tokenCache =
UNCOV
844
                                environment.cacheTokens === false
×
845
                                        ? null
846
                                        : new CLITokenCache(environmentsObj.default);
847
                        let configObj;
UNCOV
848
                        try {
×
UNCOV
849
                                configObj = JSON.parse(
×
850
                                        fs.readFileSync(environment.boxConfigFilePath)
851
                                );
852
                        } catch (error) {
UNCOV
853
                                throw new BoxCLIError(
×
854
                                        'Could not read environments config file',
855
                                        error
856
                                );
857
                        }
858

UNCOV
859
                        if (!environment.hasInLinePrivateKey) {
×
UNCOV
860
                                try {
×
861
                                        configObj.boxAppSettings.appAuth.privateKey =
×
862
                                                fs.readFileSync(environment.privateKeyPath, 'utf8');
863
                                        DEBUG.init(
×
864
                                                'Loaded JWT private key from %s',
865
                                                environment.privateKeyPath
866
                                        );
867
                                } catch (error) {
UNCOV
868
                                        throw new BoxCLIError(
×
869
                                                `Could not read private key file ${environment.privateKeyPath}`,
870
                                                error
871
                                        );
872
                                }
873
                        }
874

UNCOV
875
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
UNCOV
876
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
877

878
                        client = this.sdk.getAppAuthClient(
×
879
                                'enterprise',
880
                                environment.enterpriseId,
881
                                tokenCache
882
                        );
UNCOV
883
                        DEBUG.init('Initialized client from environment config');
×
884
                } else {
885
                        // No environments set up yet!
UNCOV
886
                        throw new BoxCLIError(
×
887
                                `No default environment found.
888
                                It looks like you haven't configured the Box CLI yet.
889
                                See this command for help adding an environment: box configure:environments:add --help
890
                                Or, supply a token with your command with --token.`.replaceAll(/^\s+/gmu, '')
891
                        );
892
                }
893

894
                // Using the as-user flag should have precedence over the environment setting
895
                if (this.flags['as-user']) {
7,830✔
896
                        client.asUser(this.flags['as-user']);
9✔
897
                        DEBUG.init(
9✔
898
                                'Impersonating user ID %s using the ID provided via the --as-user flag',
899
                                this.flags['as-user']
900
                        );
901
                } else if (!this.flags.token && environment.useDefaultAsUser) {
7,821!
902
                        // We don't want to use any environment settings if a token is passed in the command
UNCOV
903
                        client.asUser(environment.defaultAsUserId);
×
UNCOV
904
                        DEBUG.init(
×
905
                                'Impersonating default user ID %s using environment configuration',
906
                                environment.defaultAsUserId
907
                        );
908
                }
909
                return client;
7,830✔
910
        }
911

912
        /**
913
         * Instantiate the TypeScript SDK client for making API calls
914
         *
915
         * @returns {BoxTSSDK.BoxClient} The TypeScript SDK client for making API calls in the command
916
         */
917
        async getTsClient() {
918
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
919
                if (this.constructor.noClient) {
7,830!
UNCOV
920
                        return null;
×
921
                }
922
                let environmentsObj = await this.getEnvironments();
7,830✔
923
                const environment =
924
                        environmentsObj.environments[environmentsObj.default] || {};
7,830✔
925
                const { authMethod } = environment;
7,830✔
926

927
                let client;
928
                if (this.flags.token) {
7,830!
929
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,830✔
930
                        let tsSdkAuth = new BoxTSSDK.BoxDeveloperTokenAuth({
7,830✔
931
                                token: this.flags.token,
932
                        });
933
                        client = new BoxTSSDK.BoxClient({
7,830✔
934
                                auth: tsSdkAuth,
935
                        });
936
                        client = this._configureTsSdk(client, SDK_CONFIG);
7,830✔
UNCOV
937
                } else if (authMethod === 'ccg') {
×
UNCOV
938
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
939

940
                        const { clientId, clientSecret, ccgUser } = environment;
×
941

942
                        if (!clientId || !clientSecret) {
×
UNCOV
943
                                throw new BoxCLIError(
×
944
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
945
                                );
946
                        }
947

948
                        let configObj;
UNCOV
949
                        try {
×
UNCOV
950
                                configObj = JSON.parse(
×
951
                                        fs.readFileSync(environment.boxConfigFilePath)
952
                                );
953
                        } catch (error) {
UNCOV
954
                                throw new BoxCLIError(
×
955
                                        'Could not read environments config file',
956
                                        error
957
                                );
958
                        }
959

UNCOV
960
                        const { enterpriseID } = configObj;
×
961
                        const tokenCache =
962
                                environment.cacheTokens === false
×
963
                                        ? null
964
                                        : new CLITokenCache(environmentsObj.default);
UNCOV
965
                        let ccgConfig = new BoxTSSDK.CcgConfig(
×
966
                                ccgUser
×
967
                                        ? {
968
                                                        clientId,
969
                                                        clientSecret,
970
                                                        userId: ccgUser,
971
                                                        tokenStorage: tokenCache,
972
                                                }
973
                                        : {
974
                                                        clientId,
975
                                                        clientSecret,
976
                                                        enterpriseId: enterpriseID,
977
                                                        tokenStorage: tokenCache,
978
                                                }
979
                        );
UNCOV
980
                        let ccgAuth = new BoxTSSDK.BoxCcgAuth({ config: ccgConfig });
×
UNCOV
981
                        client = new BoxTSSDK.BoxClient({
×
982
                                auth: ccgAuth,
983
                        });
UNCOV
984
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
UNCOV
985
                } else if (
×
986
                        environmentsObj.default &&
×
987
                        environmentsObj.environments[environmentsObj.default].authMethod ===
988
                                'oauth20'
989
                ) {
UNCOV
990
                        try {
×
UNCOV
991
                                DEBUG.init(
×
992
                                        'Using environment %s %O',
993
                                        environmentsObj.default,
994
                                        environment
995
                                );
UNCOV
996
                                const tokenCache = new CLITokenCache(environmentsObj.default);
×
UNCOV
997
                                const oauthConfig = new BoxTSSDK.OAuthConfig({
×
998
                                        clientId: environment.clientId,
999
                                        clientSecret: environment.clientSecret,
1000
                                        tokenStorage: tokenCache,
1001
                                });
UNCOV
1002
                                const oauthAuth = new BoxTSSDK.BoxOAuth({
×
1003
                                        config: oauthConfig,
1004
                                });
UNCOV
1005
                                client = new BoxTSSDK.BoxClient({ auth: oauthAuth });
×
UNCOV
1006
                                client = this._configureTsSdk(client, SDK_CONFIG);
×
1007
                        } catch {
1008
                                throw new BoxCLIError(
×
1009
                                        `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.`
1010
                                );
1011
                        }
UNCOV
1012
                } else if (environmentsObj.default) {
×
UNCOV
1013
                        DEBUG.init(
×
1014
                                'Using environment %s %O',
1015
                                environmentsObj.default,
1016
                                environment
1017
                        );
1018
                        let tokenCache =
UNCOV
1019
                                environment.cacheTokens === false
×
1020
                                        ? null
1021
                                        : new CLITokenCache(environmentsObj.default);
1022
                        let configObj;
UNCOV
1023
                        try {
×
UNCOV
1024
                                configObj = JSON.parse(
×
1025
                                        fs.readFileSync(environment.boxConfigFilePath)
1026
                                );
1027
                        } catch (error) {
UNCOV
1028
                                throw new BoxCLIError(
×
1029
                                        'Could not read environments config file',
1030
                                        error
1031
                                );
1032
                        }
1033

UNCOV
1034
                        if (!environment.hasInLinePrivateKey) {
×
UNCOV
1035
                                try {
×
1036
                                        configObj.boxAppSettings.appAuth.privateKey =
×
1037
                                                fs.readFileSync(environment.privateKeyPath, 'utf8');
1038
                                        DEBUG.init(
×
1039
                                                'Loaded JWT private key from %s',
1040
                                                environment.privateKeyPath
1041
                                        );
1042
                                } catch (error) {
UNCOV
1043
                                        throw new BoxCLIError(
×
1044
                                                `Could not read private key file ${environment.privateKeyPath}`,
1045
                                                error
1046
                                        );
1047
                                }
1048
                        }
1049

UNCOV
1050
                        const jwtConfig = new BoxTSSDK.JwtConfig({
×
1051
                                clientId: configObj.boxAppSettings.clientID,
1052
                                clientSecret: configObj.boxAppSettings.clientSecret,
1053
                                jwtKeyId: configObj.boxAppSettings.appAuth.publicKeyID,
1054
                                privateKey: configObj.boxAppSettings.appAuth.privateKey,
1055
                                privateKeyPassphrase:
1056
                                        configObj.boxAppSettings.appAuth.passphrase,
1057
                                enterpriseId: environment.enterpriseId,
1058
                                tokenStorage: tokenCache,
1059
                        });
UNCOV
1060
                        let jwtAuth = new BoxTSSDK.BoxJwtAuth({ config: jwtConfig });
×
UNCOV
1061
                        client = new BoxTSSDK.BoxClient({ auth: jwtAuth });
×
1062

1063
                        DEBUG.init('Initialized client from environment config');
×
UNCOV
1064
                        if (environment.useDefaultAsUser) {
×
1065
                                client = client.withAsUserHeader(environment.defaultAsUserId);
×
1066
                                DEBUG.init(
×
1067
                                        'Impersonating default user ID %s',
1068
                                        environment.defaultAsUserId
1069
                                );
1070
                        }
UNCOV
1071
                        client = this._configureTsSdk(client, SDK_CONFIG);
×
1072
                } else {
1073
                        // No environments set up yet!
UNCOV
1074
                        throw new BoxCLIError(
×
1075
                                `No default environment found.
1076
                                It looks like you haven't configured the Box CLI yet.
1077
                                See this command for help adding an environment: box configure:environments:add --help
1078
                                Or, supply a token with your command with --token.`.replaceAll(/^\s+/gmu, '')
1079
                        );
1080
                }
1081
                if (this.flags['as-user']) {
7,830✔
1082
                        client = client.withAsUserHeader(this.flags['as-user']);
9✔
1083
                        DEBUG.init('Impersonating user ID %s', this.flags['as-user']);
9✔
1084
                }
1085
                return client;
7,830✔
1086
        }
1087

1088
        /**
1089
         * Configures SDK by using values from settings.json file
1090
         * @param {*} sdk to configure
1091
         * @param {*} config Additional options to use while building configuration
1092
         * @returns {void}
1093
         */
1094
        _configureSdk(sdk, config = {}) {
×
1095
                const clientSettings = { ...config };
7,830✔
1096
                if (this.settings.enableProxy) {
7,830!
UNCOV
1097
                        clientSettings.proxy = this.settings.proxy;
×
1098
                }
1099
                if (this.settings.apiRootURL) {
7,830!
UNCOV
1100
                        clientSettings.apiRootURL = this.settings.apiRootURL;
×
1101
                }
1102
                if (this.settings.uploadAPIRootURL) {
7,830!
UNCOV
1103
                        clientSettings.uploadAPIRootURL = this.settings.uploadAPIRootURL;
×
1104
                }
1105
                if (this.settings.authorizeRootURL) {
7,830!
UNCOV
1106
                        clientSettings.authorizeRootURL = this.settings.authorizeRootURL;
×
1107
                }
1108
                if (this.settings.numMaxRetries) {
7,830!
UNCOV
1109
                        clientSettings.numMaxRetries = this.settings.numMaxRetries;
×
1110
                }
1111
                if (this.settings.retryIntervalMS) {
7,830!
UNCOV
1112
                        clientSettings.retryIntervalMS = this.settings.retryIntervalMS;
×
1113
                }
1114
                if (this.settings.uploadRequestTimeoutMS) {
7,830!
UNCOV
1115
                        clientSettings.uploadRequestTimeoutMS =
×
1116
                                this.settings.uploadRequestTimeoutMS;
1117
                }
1118
                clientSettings.analyticsClient.name = this.settings.enableAnalyticsClient &&
7,830!
1119
                        this.settings.analyticsClient.name ? `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}` : DEFAULT_ANALYTICS_CLIENT_NAME;
1120

1121
                if (Object.keys(clientSettings).length > 0) {
7,830!
1122
                        DEBUG.init('SDK client settings %s', clientSettings);
7,830✔
1123
                        sdk.configure(clientSettings);
7,830✔
1124
                }
1125
        }
1126

1127
        /**
1128
         * Configures TS SDK by using values from settings.json file
1129
         *
1130
         * @param {BoxTSSDK.BoxClient} client to configure
1131
         * @param {Object} config Additional options to use while building configuration
1132
         * @returns {BoxTSSDK.BoxClient} The configured client
1133
         */
1134
        _configureTsSdk(client, config) {
1135
                let additionalHeaders = config.request.headers;
7,830✔
1136
                let customBaseURL = {
7,830✔
1137
                        baseUrl: 'https://api.box.com',
1138
                        uploadUrl: 'https://upload.box.com/api',
1139
                        oauth2Url: 'https://account.box.com/api/oauth2',
1140
                };
1141
                if (this.settings.enableProxy) {
7,830!
UNCOV
1142
                        client = client.withProxy(this.settings.proxy);
×
1143
                }
1144
                if (this.settings.apiRootURL) {
7,830!
UNCOV
1145
                        customBaseURL.baseUrl = this.settings.apiRootURL;
×
1146
                }
1147
                if (this.settings.uploadAPIRootURL) {
7,830!
UNCOV
1148
                        customBaseURL.uploadUrl = this.settings.uploadAPIRootURL;
×
1149
                }
1150
                if (this.settings.authorizeRootURL) {
7,830!
UNCOV
1151
                        customBaseURL.oauth2Url = this.settings.authorizeRootURL;
×
1152
                }
1153
                client = client.withCustomBaseUrls(customBaseURL);
7,830✔
1154

1155
                if (this.settings.numMaxRetries) {
7,830!
1156
                        // Not supported in TS SDK
1157
                }
1158
                if (this.settings.retryIntervalMS) {
7,830!
1159
                        // Not supported in TS SDK
1160
                }
1161
                if (this.settings.uploadRequestTimeoutMS) {
7,830!
1162
                        // Not supported in TS SDK
1163
                }
1164
                additionalHeaders['X-Box-UA'] = this.settings.enableAnalyticsClient &&
7,830!
1165
                        this.settings.analyticsClient.name ? `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}` : DEFAULT_ANALYTICS_CLIENT_NAME;
1166
                client = client.withExtraHeaders(additionalHeaders);
7,830✔
1167
                DEBUG.init('TS SDK configured with settings from settings.json');
7,830✔
1168

1169
                return client;
7,830✔
1170
        }
1171

1172
        /**
1173
         * Format data for output to stdout
1174
         * @param {*} content The content to output
1175
         * @returns {Promise<void>} A promise resolving when output is handled
1176
         */
1177
        async output(content) {
1178
                if (this.isBulk) {
7,326✔
1179
                        this.bulkOutputList.push(content);
576✔
1180
                        DEBUG.output(
576✔
1181
                                'Added command output to bulk list total: %d',
1182
                                this.bulkOutputList.length
1183
                        );
1184
                        return;
576✔
1185
                }
1186

1187
                let formattedOutputData;
1188
                if (Array.isArray(content)) {
6,750✔
1189
                        // Format each object individually and then flatten in case this an array of arrays,
1190
                        // which happens when a command that outputs a collection gets run in bulk
1191
                        formattedOutputData = (await Promise.all(
405✔
1192
                                        content.map((o) => this._formatOutputObject(o))
1,080✔
1193
                                )).flat();
1194
                        DEBUG.output(
405✔
1195
                                'Formatted %d output entries for display',
1196
                                content.length
1197
                        );
1198
                } else {
1199
                        formattedOutputData = await this._formatOutputObject(content);
6,345✔
1200
                        DEBUG.output('Formatted output content for display');
6,345✔
1201
                }
1202
                let outputFormat = this._getOutputFormat();
6,750✔
1203
                DEBUG.output('Using %s output format', outputFormat);
6,750✔
1204
                DEBUG.output(formattedOutputData);
6,750✔
1205

1206
                let writeFunc;
1207
                let logFunc;
1208
                let stringifiedOutput;
1209

1210
                // remove all the undefined values from the object
1211
                formattedOutputData = removeUndefinedValues(formattedOutputData);
6,750✔
1212

1213
                if (outputFormat === 'json') {
6,750✔
1214
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,266✔
1215

1216
                        let appendNewLineTransform = new Transform({
4,266✔
1217
                                transform(chunk, encoding, callback) {
1218
                                        callback(null, chunk);
36✔
1219
                                },
1220
                                flush(callback) {
1221
                                        this.push(os.EOL);
36✔
1222
                                        callback();
36✔
1223
                                },
1224
                        });
1225

1226
                        writeFunc = async (savePath) => {
4,266✔
1227
                                await pipeline(
36✔
1228
                                        stringifiedOutput,
1229
                                        appendNewLineTransform,
1230
                                        fs.createWriteStream(savePath, { encoding: 'utf8' })
1231
                                );
1232
                        };
1233

1234
                        logFunc = async () => {
4,266✔
1235
                                await this.logStream(stringifiedOutput);
4,230✔
1236
                        };
1237
                } else {
1238
                        stringifiedOutput =
2,484✔
1239
                                await this._stringifyOutput(formattedOutputData);
1240

1241
                        writeFunc = async (savePath) => {
2,484✔
1242
                                await utils.writeFileAsync(
9✔
1243
                                        savePath,
1244
                                        stringifiedOutput + os.EOL,
1245
                                        {
1246
                                                encoding: 'utf8',
1247
                                        }
1248
                                );
1249
                        };
1250

1251
                        logFunc = () => this.log(stringifiedOutput);
2,484✔
1252
                }
1253
                return this._writeOutput(writeFunc, logFunc);
6,750✔
1254
        }
1255

1256
        /**
1257
         * Check if max-items has been reached.
1258
         *
1259
         * @param {number} maxItems Total number of items to return
1260
         * @param {number} itemsCount Current number of items
1261
         * @returns {boolean} True if limit has been reached, otherwise false
1262
         * @private
1263
         */
1264
        maxItemsReached(maxItems, itemsCount) {
1265
                return maxItems && itemsCount >= maxItems;
6,777✔
1266
        }
1267

1268
        /**
1269
         * Prepare the output data by:
1270
         *   1) Unrolling an iterator into an array
1271
         *   2) Filtering out unwanted object fields
1272
         *
1273
         * @param {*} obj The raw object containing output data
1274
         * @returns {*} The formatted output data
1275
         * @private
1276
         */
1277
        async _formatOutputObject(obj) {
1278
                let output = obj;
7,425✔
1279

1280
                // Pass primitive content types through
1281
                if (typeof output !== 'object' || output === null) {
7,425!
UNCOV
1282
                        return output;
×
1283
                }
1284

1285
                // Unroll iterator into array
1286
                if (typeof obj.next === 'function') {
7,425✔
1287
                        output = [];
1,494✔
1288
                        let entry = await obj.next();
1,494✔
1289
                        while (!entry.done) {
1,494✔
1290
                                output.push(entry.value);
6,777✔
1291

1292
                                if (
6,777✔
1293
                                        this.maxItemsReached(this.flags['max-items'], output.length)
1294
                                ) {
1295
                                        break;
45✔
1296
                                }
1297

1298
                                 
1299
                                entry = await obj.next();
6,732✔
1300
                                 
1301
                        }
1302
                        DEBUG.output('Unrolled iterable into %d entries', output.length);
1,494✔
1303
                }
1304

1305
                if (this.flags['id-only']) {
7,425✔
1306
                        output = Array.isArray(output)
270!
1307
                                ? this.filterOutput(output, 'id')
1308
                                : output.id;
1309
                } else {
1310
                        output = this.filterOutput(output, this.flags.fields);
7,155✔
1311
                }
1312

1313
                return output;
7,425✔
1314
        }
1315

1316
        /**
1317
         * Get the output format (and file extension) based on the settings and flags set
1318
         *
1319
         * @returns {string} The file extension/format to use for output
1320
         * @private
1321
         */
1322
        _getOutputFormat() {
1323
                if (this.flags.json) {
9,252✔
1324
                        return 'json';
4,275✔
1325
                }
1326

1327
                if (this.flags.csv) {
4,977✔
1328
                        return 'csv';
54✔
1329
                }
1330

1331
                if (this.flags.save || this.flags['save-to-file-path']) {
4,923✔
1332
                        return this.settings.boxReportsFileFormat || 'txt';
27!
1333
                }
1334

1335
                if (this.settings.outputJson) {
4,896!
UNCOV
1336
                        return 'json';
×
1337
                }
1338

1339
                return 'txt';
4,896✔
1340
        }
1341

1342
        /**
1343
         * Converts output data to a string based on the type of content and flags the user
1344
         * has specified regarding output format
1345
         *
1346
         * @param {*} outputData The data to output
1347
         * @returns {string} Promise resolving to the output data as a string
1348
         * @private
1349
         */
1350
        async _stringifyOutput(outputData) {
1351
                let outputFormat = this._getOutputFormat();
2,484✔
1352

1353
                if (typeof outputData !== 'object') {
2,484✔
1354
                        DEBUG.output('Primitive output cast to string');
270✔
1355
                        return String(outputData);
270✔
1356
                } else if (outputFormat === 'csv') {
2,214✔
1357
                        let csvString = await csvStringify(
27✔
1358
                                this.formatForTableAndCSVOutput(outputData)
1359
                        );
1360
                        // The CSV library puts a trailing newline at the end of the string, which is
1361
                        // redundant with the automatic newline added by oclif when writing to stdout
1362
                        DEBUG.output('Processed output as CSV');
27✔
1363
                        return csvString.replace(/\r?\n$/u, '');
27✔
1364
                } else if (Array.isArray(outputData)) {
2,187✔
1365
                        let str = outputData
63✔
1366
                                .map(
1367
                                        (o) => `${formatObjectHeader(o)}${os.EOL}${formatObject(o)}`
153✔
1368
                                )
1369
                                .join(os.EOL.repeat(2));
1370
                        DEBUG.output('Processed collection into human-readable output');
63✔
1371
                        return str;
63✔
1372
                }
1373

1374
                let str = formatObject(outputData);
2,124✔
1375
                DEBUG.output('Processed human-readable output');
2,124✔
1376
                return str;
2,124✔
1377
        }
1378

1379
        /**
1380
         * Generate an appropriate default filename for writing
1381
         * the output of this command to disk.
1382
         *
1383
         * @returns {string} The output file name
1384
         * @private
1385
         */
1386
        _getOutputFileName() {
1387
                let extension = this._getOutputFormat();
18✔
1388
                return `${this.id.replaceAll(':', '-')}-${dateTime.format(
18✔
1389
                        new Date(),
1390
                        'YYYY-MM-DD_HH_mm_ss_SSS'
1391
                )}.${extension}`;
1392
        }
1393

1394
        /**
1395
         * Write output to its final destination, either a file or stdout
1396
         * @param {Function} writeFunc Function used to save output to a file
1397
         * @param {Function} logFunc Function used to print output to stdout
1398
         * @returns {Promise<void>} A promise resolving when output is written
1399
         * @private
1400
         */
1401
        async _writeOutput(writeFunc, logFunc) {
1402
                if (this.flags.save) {
6,750✔
1403
                        DEBUG.output('Writing output to default location on disk');
9✔
1404
                        let filePath = path.join(
9✔
1405
                                this.settings.boxReportsFolderPath,
1406
                                this._getOutputFileName()
1407
                        );
1408
                        try {
9✔
1409
                                await writeFunc(filePath);
9✔
1410
                        } catch (error) {
UNCOV
1411
                                throw new BoxCLIError(
×
1412
                                        `Could not write output to file at ${filePath}`,
1413
                                        error
1414
                                );
1415
                        }
1416
                        this.info(chalk`{green Output written to ${filePath}}`);
9✔
1417
                } else if (this.flags['save-to-file-path']) {
6,741✔
1418
                        let savePath = this.flags['save-to-file-path'];
36✔
1419
                        if (fs.existsSync(savePath)) {
36!
1420
                                if (fs.statSync(savePath).isDirectory()) {
36✔
1421
                                        // Append default file name and write into the provided directory
1422
                                        savePath = path.join(savePath, this._getOutputFileName());
9✔
1423
                                        DEBUG.output(
9✔
1424
                                                'Output path is a directory, will write to %s',
1425
                                                savePath
1426
                                        );
1427
                                } else {
1428
                                        DEBUG.output('File already exists at %s', savePath);
27✔
1429
                                        // Ask if the user want to overwrite the file
1430
                                        let shouldOverwrite = await this.confirm(
27✔
1431
                                                `File ${savePath} already exists — overwrite?`
1432
                                        );
1433

1434
                                        if (!shouldOverwrite) {
27!
UNCOV
1435
                                                return;
×
1436
                                        }
1437
                                }
1438
                        }
1439
                        try {
36✔
1440
                                DEBUG.output(
36✔
1441
                                        'Writing output to specified location on disk: %s',
1442
                                        savePath
1443
                                );
1444
                                await writeFunc(savePath);
36✔
1445
                        } catch (error) {
UNCOV
1446
                                throw new BoxCLIError(
×
1447
                                        `Could not write output to file at ${savePath}`,
1448
                                        error
1449
                                );
1450
                        }
1451
                        this.info(chalk`{green Output written to ${savePath}}`);
36✔
1452
                } else {
1453
                        DEBUG.output('Writing output to terminal');
6,705✔
1454
                        await logFunc();
6,705✔
1455
                }
1456

1457
                DEBUG.output('Finished writing output');
6,750✔
1458
        }
1459

1460
        /**
1461
         * Ask a user to confirm something, respecting the default --yes flag
1462
         *
1463
         * @param {string} promptText The text of the prompt to the user
1464
         * @param {boolean} defaultValue The default value of the prompt
1465
         * @returns {Promise<boolean>} A promise resolving to a boolean that is true iff the user confirmed
1466
         */
1467
        async confirm(promptText, defaultValue = false) {
27✔
1468
                if (this.flags.yes) {
27✔
1469
                        return true;
18✔
1470
                }
1471

1472
                let answers = await inquirer.prompt([
9✔
1473
                        {
1474
                                name: 'confirmation',
1475
                                message: promptText,
1476
                                type: 'confirm',
1477
                                default: defaultValue,
1478
                        },
1479
                ]);
1480

1481
                return answers.confirmation;
9✔
1482
        }
1483

1484
        /**
1485
         * Writes output to stderr — this should be used for informational output.  For example, a message
1486
         * stating that an item has been deleted.
1487
         *
1488
         * @param {string} content The message to output
1489
         * @returns {void}
1490
         */
1491
        info(content) {
1492
                if (!this.flags.quiet) {
1,098✔
1493
                        process.stderr.write(`${content}${os.EOL}`);
1,089✔
1494
                }
1495
        }
1496

1497
        /**
1498
         * Writes output to stderr — this should be used for informational output.  For example, a message
1499
         * stating that an item has been deleted.
1500
         *
1501
         * @param {string} content The message to output
1502
         * @returns {void}
1503
         */
1504
        log(content) {
1505
                if (!this.flags.quiet) {
2,475✔
1506
                        process.stdout.write(`${content}${os.EOL}`);
2,457✔
1507
                }
1508
        }
1509

1510
        /**
1511
         * Writes stream output to stderr — this should be used for informational output.  For example, a message
1512
         * stating that an item has been deleted.
1513
         *
1514
         * @param {ReadableStream} content The message to output
1515
         * @returns {void}
1516
         */
1517
        async logStream(content) {
1518
                if (!this.flags.quiet) {
4,230!
1519
                        // For Node 12 when process.stdout is in pipeline it's not emitting end event correctly and it freezes.
1520
                        // See - https://github.com/nodejs/node/issues/34059
1521
                        // Using promise for now.
1522
                        content.pipe(process.stdout);
4,230✔
1523

1524
                        await new Promise((resolve, reject) => {
4,230✔
1525
                                content
4,230✔
1526
                                        .on('end', () => {
1527
                                                process.stdout.write(os.EOL);
4,230✔
1528
                                                resolve();
4,230✔
1529
                                        })
1530
                                        .on('error', (err) => {
UNCOV
1531
                                                reject(err);
×
1532
                                        });
1533
                        });
1534
                }
1535
        }
1536

1537
        /**
1538
         * Wraps filtered error in an error with a user-friendly description
1539
         *
1540
         * @param {Error} err  The thrown error
1541
         * @returns {Error} Error wrapped in an error with user friendly description
1542
         */
1543
        wrapError(err) {
1544
                let messageMap = {
324✔
1545
                        'invalid_grant - Refresh token has expired':
1546
                                '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.',
1547
                };
1548

1549
                for (const key in messageMap) {
324✔
1550
                        if (err.message.includes(key)) {
324!
UNCOV
1551
                                return new BoxCLIError(messageMap[key], err);
×
1552
                        }
1553
                }
1554

1555
                return err;
324✔
1556
        }
1557

1558
        /**
1559
         * Handles an error thrown within a command
1560
         *
1561
         * @param {Error} err  The thrown error
1562
         * @returns {void}
1563
         */
1564
        async catch(err) {
1565
                if (
297!
1566
                        err instanceof BoxTsErrors.BoxApiError &&
297!
1567
                        err.responseInfo &&
1568
                        err.responseInfo.body
1569
                ) {
UNCOV
1570
                        const responseInfo = err.responseInfo;
×
UNCOV
1571
                        let errorMessage = `Unexpected API Response [${responseInfo.body.status} ${responseInfo.body.message} | ${responseInfo.body.request_id}] ${responseInfo.body.code} - ${responseInfo.body.message}`;
×
UNCOV
1572
                        err = new BoxCLIError(errorMessage, err);
×
1573
                }
1574
                if (err instanceof BoxTsErrors.BoxSdkError) {
297!
UNCOV
1575
                        try {
×
UNCOV
1576
                                let errorObj = JSON.parse(err.message);
×
UNCOV
1577
                                if (errorObj.message) {
×
1578
                                        err = new BoxCLIError(errorObj.message, err);
×
1579
                                }
1580
                        } catch (error) {
UNCOV
1581
                                DEBUG.execute('Error parsing BoxSdkError message: %s', error);
×
1582
                        }
1583
                }
1584
                try {
297✔
1585
                        // Let the oclif default handler run first, since it handles the help and version flags there
1586
                        /* eslint-disable promise/no-promise-in-callback */
1587
                        DEBUG.execute('Running framework error handler');
297✔
1588
                        await super.catch(this.wrapError(err));
297✔
1589
                         
1590
                } catch (error) {
1591
                        // The oclif default catch handler rethrows most errors; handle those here
1592
                        DEBUG.execute('Handling re-thrown error in base command handler');
297✔
1593

1594
                        if (error.code === 'EEXIT') {
297!
1595
                                // oclif throws this when it handled the error itself and wants to exit, so just let it do that
UNCOV
1596
                                DEBUG.execute('Got EEXIT code, exiting immediately');
×
UNCOV
1597
                                return;
×
1598
                        }
1599
                        let contextInfo;
1600
                        if (
297✔
1601
                                error.response &&
477✔
1602
                                error.response.body &&
1603
                                error.response.body.context_info
1604
                        ) {
1605
                                contextInfo = formatObject(error.response.body.context_info);
9✔
1606
                                // Remove color codes from context info
1607
                                // eslint-disable-next-line no-control-regex
1608
                                contextInfo = contextInfo.replaceAll(/\u001B\[\d+m/gu, '');
9✔
1609
                                // Remove \n with os.EOL
1610
                                contextInfo = contextInfo.replaceAll('\n', os.EOL);
9✔
1611
                        }
1612
                        let errorMsg = chalk`{redBright ${
297✔
1613
                                this.flags && this.flags.verbose ? error.stack : error.message
891✔
1614
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}}`;
297✔
1615

1616
                        // Write the error message but let the process exit gracefully with error code so stderr gets written out
1617
                        // @NOTE: Exiting the process in the callback enables tests to mock out stderr and run to completion!
1618
                         
1619
                        process.stderr.write(errorMsg, () => {
297✔
1620
                                process.exitCode = 2;
297✔
1621
                        });
1622
                         
1623
                }
1624
        }
1625

1626
        /**
1627
         * Final hook that executes for all commands, regardless of if an error occurred
1628
         * @param {Error} [err] An error, if one occurred
1629
         * @returns {void}
1630
         */
1631
        async finally(/* err */) {
1632
                // called after run and catch regardless of whether or not the command errored
1633
        }
1634

1635
        /**
1636
         * Filter out unwanted fields from the output object(s)
1637
         *
1638
         * @param {Object|Object[]} output The output object(s)
1639
         * @param {string} [fields] Comma-separated list of fields to include
1640
         * @returns {Object|Object[]} The filtered object(s) for output
1641
         */
1642
        filterOutput(output, fields) {
1643
                if (!fields) {
7,155✔
1644
                        return output;
6,579✔
1645
                }
1646
                fields = REQUIRED_FIELDS.concat(
576✔
1647
                        fields.split(',').filter((f) => !REQUIRED_FIELDS.includes(f))
711✔
1648
                );
1649
                DEBUG.output('Filtering output with fields: %O', fields);
576✔
1650
                if (Array.isArray(output)) {
576✔
1651
                        output = output.map((o) =>
342✔
1652
                                typeof o === 'object' ? _.pick(o, fields) : o
1,404!
1653
                        );
1654
                } else if (typeof output === 'object') {
234!
1655
                        output = _.pick(output, fields);
234✔
1656
                }
1657
                return output;
576✔
1658
        }
1659

1660
        /**
1661
         * Flatten nested objects for output to a table/CSV
1662
         *
1663
         * @param {Object[]} objectArray The objects that will be output
1664
         * @returns {Array[]} The formatted output
1665
         */
1666
        formatForTableAndCSVOutput(objectArray) {
1667
                let formattedData = [];
27✔
1668
                if (!Array.isArray(objectArray)) {
27!
UNCOV
1669
                        objectArray = [objectArray];
×
UNCOV
1670
                        DEBUG.output('Creating tabular output from single object');
×
1671
                }
1672

1673
                let keyPaths = [];
27✔
1674
                for (let object of objectArray) {
27✔
1675
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1676
                }
1677

1678
                DEBUG.output('Found %d keys for tabular output', keyPaths.length);
27✔
1679
                formattedData.push(keyPaths);
27✔
1680
                for (let object of objectArray) {
27✔
1681
                        let row = [];
126✔
1682
                        if (typeof object === 'object') {
126!
1683
                                for (let keyPath of keyPaths) {
126✔
1684
                                        let value = _.get(object, keyPath);
1,584✔
1685
                                        if (value === null || value === undefined) {
1,584✔
1686
                                                row.push('');
180✔
1687
                                        } else {
1688
                                                row.push(value);
1,404✔
1689
                                        }
1690
                                }
1691
                        } else {
UNCOV
1692
                                row.push(object);
×
1693
                        }
1694
                        DEBUG.output('Processed row with %d values', row.length);
126✔
1695
                        formattedData.push(row);
126✔
1696
                }
1697
                DEBUG.output(
27✔
1698
                        'Processed %d rows of tabular output',
1699
                        formattedData.length - 1
1700
                );
1701
                return formattedData;
27✔
1702
        }
1703

1704
        /**
1705
         * Extracts all keys from an object and flattens them
1706
         *
1707
         * @param {Object} object The object to extract flattened keys from
1708
         * @returns {string[]} The array of flattened keys
1709
         */
1710
        getNestedKeys(object) {
1711
                let keys = [];
405✔
1712
                if (typeof object === 'object') {
405!
1713
                        for (let key in object) {
405✔
1714
                                if (
1,683✔
1715
                                        typeof object[key] === 'object' &&
1,962✔
1716
                                        !Array.isArray(object[key])
1717
                                ) {
1718
                                        let subKeys = this.getNestedKeys(object[key]);
279✔
1719
                                        subKeys = subKeys.map((x) => `${key}.${x}`);
1,026✔
1720
                                        keys = keys.concat(subKeys);
279✔
1721
                                } else {
1722
                                        keys.push(key);
1,404✔
1723
                                }
1724
                        }
1725
                }
1726
                return keys;
405✔
1727
        }
1728

1729
        /**
1730
         * Converts time interval shorthand like 5w, -3d, etc to timestamps. It also ensures any timestamp
1731
         * passed in is properly formatted for API calls.
1732
         *
1733
         * @param {string} time The command lint input string for the datetime
1734
         * @returns {string} The full RFC3339-formatted datetime string in UTC
1735
         */
1736
        static normalizeDateString(time) {
1737
                // Attempt to parse date as timestamp or string
1738
                let newDate = /^\d+$/u.test(time)
1,152✔
1739
                        ? dateTime.parse(Number.parseInt(time, 10) * 1000)
1740
                        : dateTime.parse(time);
1741
                if (!dateTime.isValid(newDate)) {
1,152✔
1742
                        let parsedOffset = time.match(/^(-?)((?:\d+[smhdwMy])+)$/u);
261✔
1743
                        if (parsedOffset) {
261✔
1744
                                let sign = parsedOffset[1] === '-' ? -1 : 1,
216✔
1745
                                        offset = parsedOffset[2];
216✔
1746

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

1753
                                // Successively apply the offsets to the current time
1754
                                newDate = argPairs.reduce(
216✔
1755
                                        (d, args) => offsetDate(d, ...args),
234✔
1756
                                        new Date()
1757
                                );
1758
                        } else if (time === 'now') {
45!
1759
                                newDate = new Date();
45✔
1760
                        } else {
UNCOV
1761
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1762
                        }
1763
                }
1764

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

1770
        /**
1771
         * Writes updated settings to disk
1772
         *
1773
         * @param {Object} updatedSettings The settings object to write
1774
         * @returns {void}
1775
         */
1776
        updateSettings(updatedSettings) {
UNCOV
1777
                this.settings = Object.assign(this.settings, updatedSettings);
×
UNCOV
1778
                try {
×
UNCOV
1779
                        fs.writeFileSync(
×
1780
                                SETTINGS_FILE_PATH,
1781
                                JSON.stringify(this.settings, null, 4),
1782
                                'utf8'
1783
                        );
1784
                } catch (error) {
1785
                        throw new BoxCLIError(
×
1786
                                `Could not write settings file ${SETTINGS_FILE_PATH}`,
1787
                                error
1788
                        );
1789
                }
UNCOV
1790
                return this.settings;
×
1791
        }
1792

1793
        /**
1794
         * Read the current set of environments from disk
1795
         *
1796
         * @returns {Object} The parsed environment information
1797
         */
1798
        async getEnvironments() {
1799
                try {
15,669✔
1800
                        switch (process.platform) {
15,669✔
1801
                                case 'darwin': {
1802
                                        try {
5,223✔
1803
                                                const password = await darwinKeychainGetPassword({
5,223✔
1804
                                                        account: 'Box',
1805
                                                        service: 'boxcli',
1806
                                                });
1807
                                                return JSON.parse(password);
5,223✔
1808
                                        } catch {
1809
                                                // fallback to env file if not found
1810
                                        }
UNCOV
1811
                                        break;
×
1812
                                }
1813

1814
                                case 'win32': {
1815
                                        try {
5,223✔
1816
                                                if (!keytar) {
5,223!
1817
                                                        break;
×
1818
                                                }
1819
                                                const password = await keytar.getPassword(
5,223✔
1820
                                                        'boxcli' /* service */,
1821
                                                        'Box' /* account */
1822
                                                );
1823
                                                if (password) {
5,223!
1824
                                                        return JSON.parse(password);
5,223✔
1825
                                                }
1826
                                        } catch {
1827
                                                // fallback to env file if not found
1828
                                        }
UNCOV
1829
                                        break;
×
1830
                                }
1831

1832
                                default:
1833
                        }
1834
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,223✔
1835
                } catch (error) {
UNCOV
1836
                        throw new BoxCLIError(
×
1837
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1838
                                error
1839
                        );
1840
                }
1841
        }
1842

1843
        /**
1844
         * Writes updated environment information to disk
1845
         *
1846
         * @param {Object} updatedEnvironments The environment information to write
1847
         * @param {Object} environments use to override current environment
1848
         * @returns {void}
1849
         */
1850
        async updateEnvironments(updatedEnvironments, environments) {
1851
                if (environments === undefined) {
9!
UNCOV
1852
                        environments = await this.getEnvironments();
×
1853
                }
1854
                Object.assign(environments, updatedEnvironments);
9✔
1855
                try {
9✔
1856
                        let fileContents = JSON.stringify(environments, null, 4);
9✔
1857
                        switch (process.platform) {
9✔
1858
                                case 'darwin': {
1859
                                        await darwinKeychainSetPassword({
3✔
1860
                                                account: 'Box',
1861
                                                service: 'boxcli',
1862
                                                password: JSON.stringify(environments),
1863
                                        });
1864
                                        fileContents = '';
3✔
1865
                                        break;
3✔
1866
                                }
1867

1868
                                case 'win32': {
1869
                                        if (!keytar) {
3!
UNCOV
1870
                                                break;
×
1871
                                        }
1872
                                        await keytar.setPassword(
3✔
1873
                                                'boxcli' /* service */,
1874
                                                'Box' /* account */,
1875
                                                JSON.stringify(environments) /* password */
1876
                                        );
1877
                                        fileContents = '';
3✔
1878
                                        break;
3✔
1879
                                }
1880

1881
                                default:
1882
                        }
1883

1884
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
9✔
1885
                } catch (error) {
UNCOV
1886
                        throw new BoxCLIError(
×
1887
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1888
                                error
1889
                        );
1890
                }
1891
                return environments;
9✔
1892
        }
1893

1894
        /**
1895
         * Initialize the CLI by creating the necessary configuration files on disk
1896
         * in the users' home directory, then read and parse the CLI settings file.
1897
         *
1898
         * @returns {Object} The parsed settings
1899
         * @private
1900
         */
1901
        async _loadSettings() {
1902
                try {
7,830✔
1903
                        if (!fs.existsSync(CONFIG_FOLDER_PATH)) {
7,830✔
1904
                                mkdirp.sync(CONFIG_FOLDER_PATH);
9✔
1905
                                DEBUG.init('Created config folder at %s', CONFIG_FOLDER_PATH);
9✔
1906
                        }
1907
                        if (!fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
7,830✔
1908
                                await this.updateEnvironments(
9✔
1909
                                        {},
1910
                                        this._getDefaultEnvironments()
1911
                                );
1912
                                DEBUG.init(
9✔
1913
                                        'Created environments config at %s',
1914
                                        ENVIRONMENTS_FILE_PATH
1915
                                );
1916
                        }
1917
                        if (!fs.existsSync(SETTINGS_FILE_PATH)) {
7,830✔
1918
                                let settingsJSON = JSON.stringify(
9✔
1919
                                        this._getDefaultSettings(),
1920
                                        null,
1921
                                        4
1922
                                );
1923
                                fs.writeFileSync(SETTINGS_FILE_PATH, settingsJSON, 'utf8');
9✔
1924
                                DEBUG.init(
9✔
1925
                                        'Created settings file at %s %O',
1926
                                        SETTINGS_FILE_PATH,
1927
                                        settingsJSON
1928
                                );
1929
                        }
1930
                } catch (error) {
UNCOV
1931
                        throw new BoxCLIError(
×
1932
                                'Could not initialize CLI home directory',
1933
                                error
1934
                        );
1935
                }
1936

1937
                let settings;
1938
                try {
7,830✔
1939
                        settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
7,830✔
1940
                        settings = Object.assign(this._getDefaultSettings(), settings);
7,830✔
1941
                        DEBUG.init('Loaded settings %O', settings);
7,830✔
1942
                } catch (error) {
UNCOV
1943
                        throw new BoxCLIError(
×
1944
                                `Could not read CLI settings file at ${SETTINGS_FILE_PATH}`,
1945
                                error
1946
                        );
1947
                }
1948

1949
                try {
7,830✔
1950
                        if (!fs.existsSync(settings.boxReportsFolderPath)) {
7,830✔
1951
                                mkdirp.sync(settings.boxReportsFolderPath);
9✔
1952
                                DEBUG.init(
9✔
1953
                                        'Created reports folder at %s',
1954
                                        settings.boxReportsFolderPath
1955
                                );
1956
                        }
1957
                        if (!fs.existsSync(settings.boxDownloadsFolderPath)) {
7,830✔
1958
                                mkdirp.sync(settings.boxDownloadsFolderPath);
9✔
1959
                                DEBUG.init(
9✔
1960
                                        'Created downloads folder at %s',
1961
                                        settings.boxDownloadsFolderPath
1962
                                );
1963
                        }
1964
                } catch (error) {
UNCOV
1965
                        throw new BoxCLIError('Failed creating CLI working directory', error);
×
1966
                }
1967

1968
                return settings;
7,830✔
1969
        }
1970

1971
        /**
1972
         * Get the default settings object
1973
         *
1974
         * @returns {Object} The default settings object
1975
         * @private
1976
         */
1977
        _getDefaultSettings() {
1978
                return {
7,839✔
1979
                        boxReportsFolderPath: path.join(
1980
                                os.homedir(),
1981
                                'Documents/Box-Reports'
1982
                        ),
1983
                        boxReportsFileFormat: 'txt',
1984
                        boxDownloadsFolderPath: path.join(
1985
                                os.homedir(),
1986
                                'Downloads/Box-Downloads'
1987
                        ),
1988
                        outputJson: false,
1989
                        enableProxy: false,
1990
                        proxy: {
1991
                                url: null,
1992
                                username: null,
1993
                                password: null,
1994
                        },
1995
                        enableAnalyticsClient: false,
1996
                        analyticsClient: {
1997
                                name: null,
1998
                        },
1999
                };
2000
        }
2001

2002
        /**
2003
         * Get the default environments object
2004
         *
2005
         * @returns {Object} The default environments object
2006
         * @private
2007
         */
2008
        _getDefaultEnvironments() {
2009
                return {
9✔
2010
                        default: null,
2011
                        environments: {},
2012
                };
2013
        }
2014
}
2015

2016
BoxCommand.flags = {
9✔
2017
        token: Flags.string({
2018
                char: 't',
2019
                description: 'Provide a token to perform this call',
2020
        }),
2021
        'as-user': Flags.string({ description: 'Provide an ID for a user' }),
2022
        // @NOTE: This flag is not read anywhere directly; the chalk library automatically turns off color when it's passed
2023
        'no-color': Flags.boolean({
2024
                description: 'Turn off colors for logging',
2025
        }),
2026
        json: Flags.boolean({
2027
                description: 'Output formatted JSON',
2028
                exclusive: ['csv'],
2029
        }),
2030
        csv: Flags.boolean({
2031
                description: 'Output formatted CSV',
2032
                exclusive: ['json'],
2033
        }),
2034
        save: Flags.boolean({
2035
                char: 's',
2036
                description: 'Save report to default reports folder on disk',
2037
                exclusive: ['save-to-file-path'],
2038
        }),
2039
        'save-to-file-path': Flags.string({
2040
                description: 'Override default file path to save report',
2041
                exclusive: ['save'],
2042
                parse: utils.parsePath,
2043
        }),
2044
        fields: Flags.string({
2045
                description: 'Comma separated list of fields to show',
2046
        }),
2047
        'bulk-file-path': Flags.string({
2048
                description: 'File path to bulk .csv or .json objects',
2049
                parse: utils.parsePath,
2050
        }),
2051
        help: Flags.help({
2052
                char: 'h',
2053
                description: 'Show CLI help',
2054
        }),
2055
        verbose: Flags.boolean({
2056
                char: 'v',
2057
                description: 'Show verbose output, which can be helpful for debugging',
2058
        }),
2059
        yes: Flags.boolean({
2060
                char: 'y',
2061
                description: 'Automatically respond yes to all confirmation prompts',
2062
        }),
2063
        quiet: Flags.boolean({
2064
                char: 'q',
2065
                description: 'Suppress any non-error output to stderr',
2066
        }),
2067
};
2068

2069
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
2070
        'no-color',
2071
        'help',
2072
        'verbose',
2073
        'quiet',
2074
]);
2075

2076
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