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

box / boxcli / 19504729128

19 Nov 2025 02:27PM UTC coverage: 85.591% (+0.01%) from 85.581%
19504729128

Pull #603

github

web-flow
Merge 7c8937cd2 into b65d3b937
Pull Request #603: feat: support auto update using Github releases

1329 of 1751 branches covered (75.9%)

Branch coverage included in aggregate %.

142 of 163 new or added lines in 4 files covered. (87.12%)

1 existing line in 1 file now uncovered.

4736 of 5335 relevant lines covered (88.77%)

616.34 hits per line

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

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

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

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

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

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

56
const { Transform } = require('node:stream');
9✔
57

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

86
const REQUIRED_FIELDS = ['type', 'id'];
9✔
87

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

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

107
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
9✔
108

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

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

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

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

154
        return obj;
48,330✔
155
}
156

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

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

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

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

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

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

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

242
        return formattedObj;
9,207✔
243
}
244

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

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

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

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

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

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

308
                let { flags, args } = await this.parse(this.constructor);
7,830✔
309

310
                this.flags = flags;
7,830✔
311
                this.args = args;
7,830✔
312
                this.settings = await this._loadSettings();
7,830✔
313
                this.client = await this.getClient();
7,830✔
314
                this.tsClient = await this.getTsClient();
7,830✔
315

316
                if (this.isBulk) {
7,830✔
317
                        this.constructor.args = originalArgs;
324✔
318
                        this.constructor.flags = originalFlags;
324✔
319
                        this.bulkOutputList = [];
324✔
320
                        this.bulkErrors = [];
324✔
321
                        this._singleRun = this.run;
324✔
322
                        this.run = this.bulkOutputRun;
324✔
323
                }
324

325
                DEBUG.execute(
7,830✔
326
                        'Starting execution command: %s argv: %O',
327
                        this.id,
328
                        this.argv
329
                );
330
        }
331

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

360
                for (let bulkData of bulkCalls) {
279✔
361
                        this.argv = [];
603✔
362
                        bulkEntryIndex += 1;
603✔
363
                        this._getArgsForBulkInput(allPossibleArgs, bulkData);
603✔
364
                        this._setFlagsForBulkInput(bulkData);
603✔
365
                        await this._handleAsUserSettings(bulkData);
603✔
366
                        DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
603✔
367
                        // @TODO(2018-08-29): Convert this to a promise queue to improve performance
368

369
                        try {
603✔
370
                                await this._singleRun();
603✔
371
                        } catch (error) {
372
                                // In bulk mode, we don't want to write directly to console and kill the command
373
                                // Instead, we should buffer the error output so subsequent commands might be able to succeed
374
                                DEBUG.execute(
27✔
375
                                        'Caught error from bulk input entry %d',
376
                                        bulkEntryIndex
377
                                );
378
                                this.bulkErrors.push({
27✔
379
                                        index: bulkEntryIndex,
380
                                        data: bulkData,
381
                                        error: this.wrapError(error),
382
                                });
383
                        }
384

385
                        progressBar.update(bulkEntryIndex);
603✔
386
                }
387
                this.isBulk = false;
279✔
388
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
389
                await this.output(this.bulkOutputList);
279✔
390
                this._handleBulkErrors();
279✔
391
        }
392

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

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

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

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

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

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

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

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

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

772
                        const { clientId, clientSecret, ccgUser } = environment;
×
773

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

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

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

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

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

877
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
878
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
879

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

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

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

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

942
                        const { clientId, clientSecret, ccgUser } = environment;
×
943

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

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

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

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

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

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

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

1126
                if (Object.keys(clientSettings).length > 0) {
7,830!
1127
                        DEBUG.init('SDK client settings %s', clientSettings);
7,830✔
1128
                        sdk.configure(clientSettings);
7,830✔
1129
                }
1130
        }
1131

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

1160
                if (this.settings.numMaxRetries) {
7,830!
1161
                        // Not supported in TS SDK
1162
                }
1163
                if (this.settings.retryIntervalMS) {
7,830!
1164
                        // Not supported in TS SDK
1165
                }
1166
                if (this.settings.uploadRequestTimeoutMS) {
7,830!
1167
                        // Not supported in TS SDK
1168
                }
1169
                additionalHeaders['X-Box-UA'] =
7,830✔
1170
                        this.settings.enableAnalyticsClient &&
15,660!
1171
                        this.settings.analyticsClient.name
1172
                                ? `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`
1173
                                : DEFAULT_ANALYTICS_CLIENT_NAME;
1174
                client = client.withExtraHeaders(additionalHeaders);
7,830✔
1175
                DEBUG.init('TS SDK configured with settings from settings.json');
7,830✔
1176

1177
                return client;
7,830✔
1178
        }
1179

1180
        /**
1181
         * Format data for output to stdout
1182
         * @param {*} content The content to output
1183
         * @returns {Promise<void>} A promise resolving when output is handled
1184
         */
1185
        async output(content) {
1186
                if (this.isBulk) {
7,326✔
1187
                        this.bulkOutputList.push(content);
576✔
1188
                        DEBUG.output(
576✔
1189
                                'Added command output to bulk list total: %d',
1190
                                this.bulkOutputList.length
1191
                        );
1192
                        return;
576✔
1193
                }
1194

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

1216
                let writeFunc;
1217
                let logFunc;
1218
                let stringifiedOutput;
1219

1220
                // remove all the undefined values from the object
1221
                formattedOutputData = removeUndefinedValues(formattedOutputData);
6,750✔
1222

1223
                if (outputFormat === 'json') {
6,750✔
1224
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,266✔
1225

1226
                        let appendNewLineTransform = new Transform({
4,266✔
1227
                                transform(chunk, encoding, callback) {
1228
                                        callback(null, chunk);
36✔
1229
                                },
1230
                                flush(callback) {
1231
                                        this.push(os.EOL);
36✔
1232
                                        callback();
36✔
1233
                                },
1234
                        });
1235

1236
                        writeFunc = async (savePath) => {
4,266✔
1237
                                await pipeline(
36✔
1238
                                        stringifiedOutput,
1239
                                        appendNewLineTransform,
1240
                                        fs.createWriteStream(savePath, { encoding: 'utf8' })
1241
                                );
1242
                        };
1243

1244
                        logFunc = async () => {
4,266✔
1245
                                await this.logStream(stringifiedOutput);
4,230✔
1246
                        };
1247
                } else {
1248
                        stringifiedOutput =
2,484✔
1249
                                await this._stringifyOutput(formattedOutputData);
1250

1251
                        writeFunc = async (savePath) => {
2,484✔
1252
                                await utils.writeFileAsync(
9✔
1253
                                        savePath,
1254
                                        stringifiedOutput + os.EOL,
1255
                                        {
1256
                                                encoding: 'utf8',
1257
                                        }
1258
                                );
1259
                        };
1260

1261
                        logFunc = () => this.log(stringifiedOutput);
2,484✔
1262
                }
1263
                return this._writeOutput(writeFunc, logFunc);
6,750✔
1264
        }
1265

1266
        /**
1267
         * Check if max-items has been reached.
1268
         *
1269
         * @param {number} maxItems Total number of items to return
1270
         * @param {number} itemsCount Current number of items
1271
         * @returns {boolean} True if limit has been reached, otherwise false
1272
         * @private
1273
         */
1274
        maxItemsReached(maxItems, itemsCount) {
1275
                return maxItems && itemsCount >= maxItems;
6,777✔
1276
        }
1277

1278
        /**
1279
         * Prepare the output data by:
1280
         *   1) Unrolling an iterator into an array
1281
         *   2) Filtering out unwanted object fields
1282
         *
1283
         * @param {*} obj The raw object containing output data
1284
         * @returns {*} The formatted output data
1285
         * @private
1286
         */
1287
        async _formatOutputObject(obj) {
1288
                let output = obj;
7,425✔
1289

1290
                // Pass primitive content types through
1291
                if (typeof output !== 'object' || output === null) {
7,425!
1292
                        return output;
×
1293
                }
1294

1295
                // Unroll iterator into array
1296
                if (typeof obj.next === 'function') {
7,425✔
1297
                        output = [];
1,494✔
1298
                        let entry = await obj.next();
1,494✔
1299
                        while (!entry.done) {
1,494✔
1300
                                output.push(entry.value);
6,777✔
1301

1302
                                if (
6,777✔
1303
                                        this.maxItemsReached(this.flags['max-items'], output.length)
1304
                                ) {
1305
                                        break;
45✔
1306
                                }
1307

1308
                                entry = await obj.next();
6,732✔
1309
                        }
1310
                        DEBUG.output('Unrolled iterable into %d entries', output.length);
1,494✔
1311
                }
1312

1313
                if (this.flags['id-only']) {
7,425✔
1314
                        output = Array.isArray(output)
270!
1315
                                ? this.filterOutput(output, 'id')
1316
                                : output.id;
1317
                } else {
1318
                        output = this.filterOutput(output, this.flags.fields);
7,155✔
1319
                }
1320

1321
                return output;
7,425✔
1322
        }
1323

1324
        /**
1325
         * Get the output format (and file extension) based on the settings and flags set
1326
         *
1327
         * @returns {string} The file extension/format to use for output
1328
         * @private
1329
         */
1330
        _getOutputFormat() {
1331
                if (this.flags.json) {
9,252✔
1332
                        return 'json';
4,275✔
1333
                }
1334

1335
                if (this.flags.csv) {
4,977✔
1336
                        return 'csv';
54✔
1337
                }
1338

1339
                if (this.flags.save || this.flags['save-to-file-path']) {
4,923✔
1340
                        return this.settings.boxReportsFileFormat || 'txt';
27!
1341
                }
1342

1343
                if (this.settings.outputJson) {
4,896!
1344
                        return 'json';
×
1345
                }
1346

1347
                return 'txt';
4,896✔
1348
        }
1349

1350
        /**
1351
         * Converts output data to a string based on the type of content and flags the user
1352
         * has specified regarding output format
1353
         *
1354
         * @param {*} outputData The data to output
1355
         * @returns {string} Promise resolving to the output data as a string
1356
         * @private
1357
         */
1358
        async _stringifyOutput(outputData) {
1359
                let outputFormat = this._getOutputFormat();
2,484✔
1360

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

1382
                let str = formatObject(outputData);
2,124✔
1383
                DEBUG.output('Processed human-readable output');
2,124✔
1384
                return str;
2,124✔
1385
        }
1386

1387
        /**
1388
         * Generate an appropriate default filename for writing
1389
         * the output of this command to disk.
1390
         *
1391
         * @returns {string} The output file name
1392
         * @private
1393
         */
1394
        _getOutputFileName() {
1395
                let extension = this._getOutputFormat();
18✔
1396
                return `${this.id.replaceAll(':', '-')}-${dateTime.format(
18✔
1397
                        new Date(),
1398
                        'YYYY-MM-DD_HH_mm_ss_SSS'
1399
                )}.${extension}`;
1400
        }
1401

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

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

1465
                DEBUG.output('Finished writing output');
6,750✔
1466
        }
1467

1468
        /**
1469
         * Ask a user to confirm something, respecting the default --yes flag
1470
         *
1471
         * @param {string} promptText The text of the prompt to the user
1472
         * @param {boolean} defaultValue The default value of the prompt
1473
         * @returns {Promise<boolean>} A promise resolving to a boolean that is true iff the user confirmed
1474
         */
1475
        async confirm(promptText, defaultValue = false) {
27✔
1476
                if (this.flags.yes) {
27✔
1477
                        return true;
18✔
1478
                }
1479

1480
                let answers = await inquirer.prompt([
9✔
1481
                        {
1482
                                name: 'confirmation',
1483
                                message: promptText,
1484
                                type: 'confirm',
1485
                                default: defaultValue,
1486
                        },
1487
                ]);
1488

1489
                return answers.confirmation;
9✔
1490
        }
1491

1492
        /**
1493
         * Writes output to stderr — this should be used for informational output.  For example, a message
1494
         * stating that an item has been deleted.
1495
         *
1496
         * @param {string} content The message to output
1497
         * @returns {void}
1498
         */
1499
        info(content) {
1500
                if (!this.flags.quiet) {
1,098✔
1501
                        process.stderr.write(`${content}${os.EOL}`);
1,089✔
1502
                }
1503
        }
1504

1505
        /**
1506
         * Writes output to stderr — this should be used for informational output.  For example, a message
1507
         * stating that an item has been deleted.
1508
         *
1509
         * @param {string} content The message to output
1510
         * @returns {void}
1511
         */
1512
        log(content) {
1513
                if (!this.flags.quiet) {
2,475✔
1514
                        process.stdout.write(`${content}${os.EOL}`);
2,457✔
1515
                }
1516
        }
1517

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

1532
                        await new Promise((resolve, reject) => {
4,230✔
1533
                                content
4,230✔
1534
                                        .on('end', () => {
1535
                                                process.stdout.write(os.EOL);
4,230✔
1536
                                                resolve();
4,230✔
1537
                                        })
1538
                                        .on('error', (err) => {
1539
                                                reject(err);
×
1540
                                        });
1541
                        });
1542
                }
1543
        }
1544

1545
        /**
1546
         * Wraps filtered error in an error with a user-friendly description
1547
         *
1548
         * @param {Error} err  The thrown error
1549
         * @returns {Error} Error wrapped in an error with user friendly description
1550
         */
1551
        wrapError(err) {
1552
                let messageMap = {
324✔
1553
                        'invalid_grant - Refresh token has expired':
1554
                                '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.',
1555
                };
1556

1557
                for (const key in messageMap) {
324✔
1558
                        if (err.message.includes(key)) {
324!
1559
                                return new BoxCLIError(messageMap[key], err);
×
1560
                        }
1561
                }
1562

1563
                return err;
324✔
1564
        }
1565

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

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

1623
                        // Write the error message but let the process exit gracefully with error code so stderr gets written out
1624
                        // @NOTE: Exiting the process in the callback enables tests to mock out stderr and run to completion!
1625

1626
                        process.stderr.write(errorMsg, () => {
297✔
1627
                                process.exitCode = 2;
297✔
1628
                        });
1629
                }
1630
        }
1631

1632
        /**
1633
         * Final hook that executes for all commands, regardless of if an error occurred
1634
         * @param {Error} [err] An error, if one occurred
1635
         * @returns {void}
1636
         */
1637
        async finally(/* err */) {
1638
                // called after run and catch regardless of whether or not the command errored
1639
        }
1640

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

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

1679
                let keyPaths = [];
27✔
1680
                for (let object of objectArray) {
27✔
1681
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1682
                }
1683

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

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

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

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

1759
                                // Successively apply the offsets to the current time
1760
                                newDate = argPairs.reduce(
216✔
1761
                                        (d, args) => offsetDate(d, ...args),
234✔
1762
                                        new Date()
1763
                                );
1764
                        } else if (time === 'now') {
45!
1765
                                newDate = new Date();
45✔
1766
                        } else {
1767
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1768
                        }
1769
                }
1770

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

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

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

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

1838
                                default:
1839
                        }
1840
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,223✔
1841
                } catch (error) {
1842
                        throw new BoxCLIError(
×
1843
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1844
                                error
1845
                        );
1846
                }
1847
        }
1848

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

1874
                                case 'win32': {
1875
                                        if (!keytar) {
3!
1876
                                                break;
×
1877
                                        }
1878
                                        await keytar.setPassword(
3✔
1879
                                                'boxcli' /* service */,
1880
                                                'Box' /* account */,
1881
                                                JSON.stringify(environments) /* password */
1882
                                        );
1883
                                        fileContents = '';
3✔
1884
                                        break;
3✔
1885
                                }
1886

1887
                                default:
1888
                        }
1889

1890
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
9✔
1891
                } catch (error) {
1892
                        throw new BoxCLIError(
×
1893
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1894
                                error
1895
                        );
1896
                }
1897
                return environments;
9✔
1898
        }
1899

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

1943
                let settings;
1944
                try {
7,830✔
1945
                        settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
7,830✔
1946
                        settings = Object.assign(this._getDefaultSettings(), settings);
7,830✔
1947
                        DEBUG.init('Loaded settings %O', settings);
7,830✔
1948
                } catch (error) {
1949
                        throw new BoxCLIError(
×
1950
                                `Could not read CLI settings file at ${SETTINGS_FILE_PATH}`,
1951
                                error
1952
                        );
1953
                }
1954

1955
                try {
7,830✔
1956
                        if (!fs.existsSync(settings.boxReportsFolderPath)) {
7,830✔
1957
                                mkdirp.sync(settings.boxReportsFolderPath);
9✔
1958
                                DEBUG.init(
9✔
1959
                                        'Created reports folder at %s',
1960
                                        settings.boxReportsFolderPath
1961
                                );
1962
                        }
1963
                        if (!fs.existsSync(settings.boxDownloadsFolderPath)) {
7,830✔
1964
                                mkdirp.sync(settings.boxDownloadsFolderPath);
9✔
1965
                                DEBUG.init(
9✔
1966
                                        'Created downloads folder at %s',
1967
                                        settings.boxDownloadsFolderPath
1968
                                );
1969
                        }
1970
                } catch (error) {
NEW
1971
                        throw new BoxCLIError(
×
1972
                                'Failed creating CLI working directory',
1973
                                error
1974
                        );
1975
                }
1976

1977
                return settings;
7,830✔
1978
        }
1979

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

2011
        /**
2012
         * Get the default environments object
2013
         *
2014
         * @returns {Object} The default environments object
2015
         * @private
2016
         */
2017
        _getDefaultEnvironments() {
2018
                return {
9✔
2019
                        default: null,
2020
                        environments: {},
2021
                };
2022
        }
2023
}
2024

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

2078
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
2079
        'no-color',
2080
        'help',
2081
        'verbose',
2082
        'quiet',
2083
]);
2084

2085
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