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

box / boxcli / 22753157565

06 Mar 2026 07:11AM UTC coverage: 85.312% (-0.3%) from 85.585%
22753157565

Pull #632

github

web-flow
Merge 018c464e5 into 11a348b05
Pull Request #632: feat: Unify `keychain` and `keytar` dependencies

1377 of 1828 branches covered (75.33%)

Branch coverage included in aggregate %.

79 of 129 new or added lines in 5 files covered. (61.24%)

2 existing lines in 2 files now uncovered.

4826 of 5443 relevant lines covered (88.66%)

623.32 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 { stringifyStream } = require('@discoveryjs/json-ext');
9✔
37
const progress = require('cli-progress');
9✔
38
let keytar = null;
9✔
39
try {
9✔
40
        keytar = require('keytar');
9✔
41
} catch {
42
        // keytar cannot be imported because the library is not provided for this operating system / architecture
43
}
44

45
const DEBUG = require('./debug');
9✔
46
const stream = require('node:stream');
9✔
47
const pipeline = promisify(stream.pipeline);
9✔
48

49
const { Transform } = require('node:stream');
9✔
50

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

79
const REQUIRED_FIELDS = ['type', 'id'];
9✔
80

81
const SDK_CONFIG = Object.freeze({
9✔
82
        iterators: true,
83
        analyticsClient: {
84
                version: pkg.version,
85
        },
86
        request: {
87
                headers: {
88
                        'User-Agent': `Box CLI v${pkg.version}`,
89
                },
90
        },
91
});
92

93
const CONFIG_FOLDER_PATH = path.join(os.homedir(), '.box');
9✔
94
const SETTINGS_FILE_PATH = path.join(CONFIG_FOLDER_PATH, 'settings.json');
9✔
95
const ENVIRONMENTS_FILE_PATH = path.join(
9✔
96
        CONFIG_FOLDER_PATH,
97
        'box_environments.json'
98
);
99

100
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
9✔
101

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

124
/**
125
 * Removes all the undefined values from the object
126
 *
127
 * @param {Object} obj The object to format for display
128
 * @returns {Object} The formatted object output
129
 */
130
function removeUndefinedValues(obj) {
131
        if (typeof obj !== 'object' || obj === null) {
294,732✔
132
                return obj;
239,553✔
133
        }
134

135
        if (Array.isArray(obj)) {
55,179✔
136
                return obj.map((item) => removeUndefinedValues(item));
14,031✔
137
        }
138

139
        for (const key of Object.keys(obj)) {
48,330✔
140
                if (obj[key] === undefined) {
274,077✔
141
                        delete obj[key];
126✔
142
                } else {
143
                        obj[key] = removeUndefinedValues(obj[key]);
273,951✔
144
                }
145
        }
146

147
        return obj;
48,330✔
148
}
149

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

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

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

215
        // If type is Date, convert to ISO string
216
        if (obj instanceof Date) {
10,305✔
217
                return obj.toISOString();
36✔
218
        }
219

220
        // Don't format metadata objects to avoid mangling keys
221
        if (obj.$type) {
10,269✔
222
                return obj;
90✔
223
        }
224

225
        if (Array.isArray(obj)) {
10,179✔
226
                return obj.map((el) => formatObjectKeys(el));
1,323✔
227
        }
228

229
        let formattedObj = Object.create(null);
9,207✔
230
        for (const key of Object.keys(obj)) {
9,207✔
231
                let formattedKey = formatKey(key);
49,599✔
232
                formattedObj[formattedKey] = formatObjectKeys(obj[key]);
49,599✔
233
        }
234

235
        return formattedObj;
9,207✔
236
}
237

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

249
        // Other objects are formatted as YAML for human-readable output
250
        let yamlString = yaml.dump(outputData, {
2,295✔
251
                indent: 4,
252
                noRefs: true,
253
        });
254

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

262
/**
263
 * Formats the object header, used to separate multiple objects in a collection
264
 *
265
 * @param {Object} obj The object to generate a header for
266
 * @returns {string} The header string
267
 * @private
268
 */
269
function formatObjectHeader(obj) {
270
        if (!obj.type || !obj.id) {
153!
271
                return chalk`{dim ----------}`;
×
272
        }
273
        return chalk`{dim ----- ${formatKey(obj.type)} ${obj.id} -----}`;
153✔
274
}
275

276
/**
277
 * Base class for all Box CLI commands
278
 */
279
class BoxCommand extends Command {
280
        // @TODO(2018-08-15): Move all fs methods used here to be async
281

282
        /**
283
         * Initialize before the command is run
284
         * @returns {void}
285
         */
286
        async init() {
287
                DEBUG.init('Initializing Box CLI');
7,830✔
288
                let originalArgs, originalFlags;
289
                if (
7,830✔
290
                        this.argv.some((arg) => arg.startsWith('--bulk-file-path')) &&
32,193✔
291
                        Object.keys(this.constructor.flags).includes('bulk-file-path')
292
                ) {
293
                        // Set up the command for bulk run
294
                        DEBUG.init('Preparing for bulk input');
324✔
295
                        this.isBulk = true;
324✔
296
                        // eslint-disable-next-line unicorn/prefer-structured-clone
297
                        originalArgs = _.cloneDeep(this.constructor.args);
324✔
298
                        // eslint-disable-next-line unicorn/prefer-structured-clone
299
                        originalFlags = _.cloneDeep(this.constructor.flags);
324✔
300
                        this.disableRequiredArgsAndFlags();
324✔
301
                }
302

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

305
                this.flags = flags;
7,830✔
306
                this.args = args;
7,830✔
307
                this.settings = await this._loadSettings();
7,830✔
308
                this.client = await this.getClient();
7,830✔
309
                this.tsClient = await this.getTsClient();
7,830✔
310

311
                if (this.isBulk) {
7,830✔
312
                        this.constructor.args = originalArgs;
324✔
313
                        this.constructor.flags = originalFlags;
324✔
314
                        this.bulkOutputList = [];
324✔
315
                        this.bulkErrors = [];
324✔
316
                        this._singleRun = this.run;
324✔
317
                        this.run = this.bulkOutputRun;
324✔
318
                }
319

320
                DEBUG.execute(
7,830✔
321
                        'Starting execution command: %s argv: %O',
322
                        this.id,
323
                        this.argv
324
                );
325
        }
326

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

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

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

380
                        progressBar.update(bulkEntryIndex);
603✔
381
                }
382
                this.isBulk = false;
279✔
383
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
384
                await this.output(this.bulkOutputList);
279✔
385
                this._handleBulkErrors();
279✔
386
        }
387

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

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

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

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

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

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

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

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

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

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

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

728
                if (this.constructor.flags !== undefined) {
324!
729
                        for (const key of Object.keys(this.constructor.flags)) {
324✔
730
                                this.constructor.flags[key].required = false;
8,082✔
731
                        }
732
                }
733
        }
734

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

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

764
                        const { clientId, clientSecret, ccgUser } = environment;
×
765

766
                        if (!clientId || !clientSecret) {
×
767
                                throw new BoxCLIError(
×
768
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
769
                                );
770
                        }
771

772
                        let configObj;
773
                        try {
×
774
                                configObj = JSON.parse(
×
775
                                        fs.readFileSync(environment.boxConfigFilePath)
776
                                );
777
                        } catch (error) {
778
                                throw new BoxCLIError(
×
779
                                        'Could not read environments config file',
780
                                        error
781
                                );
782
                        }
783

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

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

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

869
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
870
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
871

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

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

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

921
                let client;
922
                if (this.flags.token) {
7,830!
923
                        DEBUG.init('Using passed in token %s', this.flags.token);
7,830✔
924
                        let tsSdkAuth = new BoxTSSDK.BoxDeveloperTokenAuth({
7,830✔
925
                                token: this.flags.token,
926
                        });
927
                        client = new BoxTSSDK.BoxClient({
7,830✔
928
                                auth: tsSdkAuth,
929
                        });
930
                        client = this._configureTsSdk(client, SDK_CONFIG);
7,830✔
931
                } else if (authMethod === 'ccg') {
×
932
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
933

934
                        const { clientId, clientSecret, ccgUser } = environment;
×
935

936
                        if (!clientId || !clientSecret) {
×
937
                                throw new BoxCLIError(
×
938
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
939
                                );
940
                        }
941

942
                        let configObj;
943
                        try {
×
944
                                configObj = JSON.parse(
×
945
                                        fs.readFileSync(environment.boxConfigFilePath)
946
                                );
947
                        } catch (error) {
948
                                throw new BoxCLIError(
×
949
                                        'Could not read environments config file',
950
                                        error
951
                                );
952
                        }
953

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

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

1044
                        const jwtConfig = new BoxTSSDK.JwtConfig({
×
1045
                                clientId: configObj.boxAppSettings.clientID,
1046
                                clientSecret: configObj.boxAppSettings.clientSecret,
1047
                                jwtKeyId: configObj.boxAppSettings.appAuth.publicKeyID,
1048
                                privateKey: configObj.boxAppSettings.appAuth.privateKey,
1049
                                privateKeyPassphrase:
1050
                                        configObj.boxAppSettings.appAuth.passphrase,
1051
                                enterpriseId: environment.enterpriseId,
1052
                                tokenStorage: tokenCache,
1053
                        });
1054
                        let jwtAuth = new BoxTSSDK.BoxJwtAuth({ config: jwtConfig });
×
1055
                        client = new BoxTSSDK.BoxClient({ auth: jwtAuth });
×
1056

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

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

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

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

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

1169
                return client;
7,830✔
1170
        }
1171

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1312
                return output;
7,425✔
1313
        }
1314

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1480
                return answers.confirmation;
9✔
1481
        }
1482

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

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

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

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

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

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

1554
                return err;
324✔
1555
        }
1556

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

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

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

1617
                        process.stderr.write(errorMsg, () => {
297✔
1618
                                process.exitCode = 2;
297✔
1619
                        });
1620
                }
1621
        }
1622

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

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

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

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

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

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

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

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

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

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

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

1791
        /**
1792
         * Read the current set of environments from disk
1793
         *
1794
         * @returns {Object} The parsed environment information
1795
         */
1796
        async getEnvironments() {
1797
                try {
15,669✔
1798
                        switch (process.platform) {
15,669!
1799
                                case 'darwin':
1800
                                case 'win32':
1801
                                case 'linux': {
1802
                                        try {
15,669✔
1803
                                                if (!keytar) {
15,669✔
1804
                                                        break;
5,223✔
1805
                                                }
1806
                                                const password = await keytar.getPassword(
10,446✔
1807
                                                        'boxcli' /* service */,
1808
                                                        'Box' /* account */
1809
                                                );
1810
                                                if (password) {
10,446!
1811
                                                        return JSON.parse(password);
10,446✔
1812
                                                }
1813
                                        } catch {
1814
                                                // fallback to env file if keytar fails or secure storage not available
1815
                                        }
1816
                                        break;
×
1817
                                }
1818

1819
                                default:
1820
                        }
1821
                        return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
5,223✔
1822
                } catch (error) {
1823
                        throw new BoxCLIError(
×
1824
                                `Could not read environments config file ${ENVIRONMENTS_FILE_PATH}`,
1825
                                error
1826
                        );
1827
                }
1828
        }
1829

1830
        /**
1831
         * Writes updated environment information to disk
1832
         *
1833
         * @param {Object} updatedEnvironments The environment information to write
1834
         * @param {Object} environments use to override current environment
1835
         * @returns {void}
1836
         */
1837
        async updateEnvironments(updatedEnvironments, environments) {
1838
                if (environments === undefined) {
5,223!
1839
                        environments = await this.getEnvironments();
×
1840
                }
1841
                Object.assign(environments, updatedEnvironments);
5,223✔
1842
                try {
5,223✔
1843
                        let fileContents = JSON.stringify(environments, null, 4);
5,223✔
1844
                        
1845
                        switch (process.platform) {
5,223!
1846
                                case 'darwin':
1847
                                case 'win32':
1848
                                case 'linux': {
1849
                                        if (keytar) {
5,223✔
1850
                                                try {
5,220✔
1851
                                                        await keytar.setPassword(
5,220✔
1852
                                                                'boxcli' /* service */,
1853
                                                                'Box' /* account */,
1854
                                                                JSON.stringify(environments) /* password */
1855
                                                        );
1856
                                                        DEBUG.init(
5,220✔
1857
                                                                'Stored environment configuration in secure storage'
1858
                                                        );
1859
                                                        // Successfully stored in secure storage, remove the file
1860
                                                        if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
5,220!
NEW
1861
                                                                fs.unlinkSync(ENVIRONMENTS_FILE_PATH);
×
NEW
1862
                                                                DEBUG.init(
×
1863
                                                                        'Removed environment configuration file after migrating to secure storage'
1864
                                                                );
1865
                                                        }
1866
                                                        return;
5,220✔
1867
                                                } catch (keytarError) {
1868
                                                        // fallback to file storage if secure storage fails
NEW
1869
                                                        DEBUG.init(
×
1870
                                                                'Could not store credentials in secure storage, falling back to file: %s',
1871
                                                                keytarError.message
1872
                                                        );
1873
                                                }
1874
                                        }
1875
                                        break;
3✔
1876
                                }
1877

1878
                                default:
1879
                        }
1880

1881
                        // Write to file if secure storage failed or not available
1882
                        fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
3✔
1883
                } catch (error) {
1884
                        throw new BoxCLIError(
×
1885
                                `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
1886
                                error
1887
                        );
1888
                }
1889
                return environments;
3✔
1890
        }
1891

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

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

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

1969
                return settings;
7,830✔
1970
        }
1971

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

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

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

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

2077
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