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

box / boxcli / 26750576945

01 Jun 2026 10:54AM UTC coverage: 84.967%. Remained the same
26750576945

push

github

web-flow
chore: release 4.9.0 (#689)

1577 of 2151 branches covered (73.31%)

Branch coverage included in aggregate %.

12 of 14 new or added lines in 11 files covered. (85.71%)

1 existing line in 1 file now uncovered.

5392 of 6051 relevant lines covered (89.11%)

643.35 hits per line

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

75.74
/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 || '';
8,082!
7

8
        if (message.includes('DEPRECATION WARNING')) {
8,082✔
9
                return;
8,073✔
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 PaginationUtilities = require('./pagination-utils');
9✔
34
const utils = require('./util');
9✔
35
const pkg = require('../package.json');
9✔
36
const inquirer = require('./inquirer');
9✔
37
const { stringifyStream } = require('@discoveryjs/json-ext');
9✔
38
const progress = require('cli-progress');
9✔
39
const secureStorage = require('./secure-storage');
9✔
40

41
const DEBUG = require('./debug');
9✔
42
const stream = require('node:stream');
9✔
43
const pipeline = promisify(stream.pipeline);
9✔
44

45
const { Transform } = require('node:stream');
9✔
46

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

75
const REQUIRED_FIELDS = ['type', 'id'];
9✔
76

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

89
const CONFIG_FOLDER_PATH = path.join(os.homedir(), '.box');
9✔
90
const SETTINGS_FILE_PATH = path.join(CONFIG_FOLDER_PATH, 'settings.json');
9✔
91
const ENVIRONMENTS_FILE_PATH = path.join(
9✔
92
        CONFIG_FOLDER_PATH,
93
        'box_environments.json'
94
);
95
const ENVIRONMENTS_KEYCHAIN_SERVICE = 'boxcli';
9✔
96
const ENVIRONMENTS_KEYCHAIN_ACCOUNT = 'Box';
9✔
97

98
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
9✔
99

100
/**
101
 * Convert error objects to a stable debug-safe shape.
102
 *
103
 * @param {unknown} error A caught error object
104
 * @returns {Object} A reduced object for DEBUG logging
105
 */
106
function getDebugErrorDetails(error) {
107
        if (!error || typeof error !== 'object') {
×
108
                return { message: String(error) };
×
109
        }
110
        return {
×
111
                name: error.name || 'Error',
×
112
                code: error.code,
113
                message: error.message || String(error),
×
114
                stack: error.stack,
115
        };
116
}
117

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

140
/**
141
 * Removes all the undefined values from the object
142
 *
143
 * @param {Object} obj The object to format for display
144
 * @returns {Object} The formatted object output
145
 */
146
function removeUndefinedValues(obj) {
147
        if (typeof obj !== 'object' || obj === null) {
325,278✔
148
                return obj;
260,298✔
149
        }
150

151
        if (Array.isArray(obj)) {
64,980✔
152
                return obj.map((item) => removeUndefinedValues(item));
23,220✔
153
        }
154

155
        for (const key of Object.keys(obj)) {
57,978✔
156
                if (obj[key] === undefined) {
295,245✔
157
                        delete obj[key];
126✔
158
                } else {
159
                        obj[key] = removeUndefinedValues(obj[key]);
295,119✔
160
                }
161
        }
162

163
        return obj;
57,978✔
164
}
165

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

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

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

231
        // If type is Date, convert to ISO string
232
        if (obj instanceof Date) {
10,305✔
233
                return obj.toISOString();
36✔
234
        }
235

236
        // Don't format metadata objects to avoid mangling keys
237
        if (obj.$type) {
10,269✔
238
                return obj;
90✔
239
        }
240

241
        if (Array.isArray(obj)) {
10,179✔
242
                return obj.map((el) => formatObjectKeys(el));
1,323✔
243
        }
244

245
        let formattedObj = Object.create(null);
9,207✔
246
        for (const key of Object.keys(obj)) {
9,207✔
247
                let formattedKey = formatKey(key);
49,599✔
248
                formattedObj[formattedKey] = formatObjectKeys(obj[key]);
49,599✔
249
        }
250

251
        return formattedObj;
9,207✔
252
}
253

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

265
        // Other objects are formatted as YAML for human-readable output
266
        let yamlString = yaml.dump(outputData, {
2,295✔
267
                indent: 4,
268
                noRefs: true,
269
        });
270

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

278
/**
279
 * Formats the object header, used to separate multiple objects in a collection
280
 *
281
 * @param {Object} obj The object to generate a header for
282
 * @returns {string} The header string
283
 * @private
284
 */
285
function formatObjectHeader(obj) {
286
        if (!obj.type || !obj.id) {
153!
287
                return chalk`{dim ----------}`;
×
288
        }
289
        return chalk`{dim ----- ${formatKey(obj.type)} ${obj.id} -----}`;
153✔
290
}
291

292
/**
293
 * Base class for all Box CLI commands
294
 */
295
class BoxCommand extends Command {
296
        // @TODO(2018-08-15): Move all fs methods used here to be async
297

298
        /**
299
         * Initialize before the command is run
300
         * @returns {void}
301
         */
302
        async init() {
303
                DEBUG.init('Initializing Box CLI');
8,100✔
304
                let originalArgs, originalFlags;
305
                if (
8,100✔
306
                        this.argv.some((arg) => arg.startsWith('--bulk-file-path')) &&
33,381✔
307
                        Object.keys(this.constructor.flags).includes('bulk-file-path')
308
                ) {
309
                        // Set up the command for bulk run
310
                        DEBUG.init('Preparing for bulk input');
324✔
311
                        this.isBulk = true;
324✔
312
                        // eslint-disable-next-line unicorn/prefer-structured-clone
313
                        originalArgs = _.cloneDeep(this.constructor.args);
324✔
314
                        // eslint-disable-next-line unicorn/prefer-structured-clone
315
                        originalFlags = _.cloneDeep(this.constructor.flags);
324✔
316
                        this.disableRequiredArgsAndFlags();
324✔
317
                }
318

319
                this.supportsSecureStorage = secureStorage.available;
8,100✔
320

321
                let { flags, args } = await this.parse(this.constructor);
8,100✔
322

323
                this.flags = flags;
8,100✔
324
                this.args = args;
8,100✔
325
                this.settings = await this._loadSettings();
8,100✔
326
                this.client = await this.getClient();
8,100✔
327
                this.tsClient = await this.getTsClient();
8,100✔
328

329
                if (this.isBulk) {
8,100✔
330
                        this.constructor.args = originalArgs;
324✔
331
                        this.constructor.flags = originalFlags;
324✔
332
                        this.bulkOutputList = [];
324✔
333
                        this.bulkErrors = [];
324✔
334
                        this._singleRun = this.run;
324✔
335
                        this.run = this.bulkOutputRun;
324✔
336
                }
337

338
                DEBUG.execute(
8,100✔
339
                        'Starting execution command: %s argv: %O',
340
                        this.id,
341
                        this.argv
342
                );
343
        }
344

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

373
                for (let bulkData of bulkCalls) {
279✔
374
                        this.argv = [];
603✔
375
                        bulkEntryIndex += 1;
603✔
376
                        this._getArgsForBulkInput(allPossibleArgs, bulkData);
603✔
377
                        this._setFlagsForBulkInput(bulkData);
603✔
378
                        await this._handleAsUserSettings(bulkData);
603✔
379
                        DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
603✔
380
                        // @TODO(2018-08-29): Convert this to a promise queue to improve performance
381

382
                        try {
603✔
383
                                await this._singleRun();
603✔
384
                        } catch (error) {
385
                                // In bulk mode, we don't want to write directly to console and kill the command
386
                                // Instead, we should buffer the error output so subsequent commands might be able to succeed
387
                                DEBUG.execute(
27✔
388
                                        'Caught error from bulk input entry %d',
389
                                        bulkEntryIndex
390
                                );
391
                                this.bulkErrors.push({
27✔
392
                                        index: bulkEntryIndex,
393
                                        data: bulkData,
394
                                        error: this.wrapError(error),
395
                                });
396
                        }
397

398
                        progressBar.update(bulkEntryIndex);
603✔
399
                }
400
                this.isBulk = false;
279✔
401
                DEBUG.execute('Leaving bulk mode and writing final output');
279✔
402
                await this.output(this.bulkOutputList);
279✔
403
                this._handleBulkErrors();
279✔
404
        }
405

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

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

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

527
        /**
528
         * For each possible arg, find the correct value between bulk input and values given on the command line.
529
         * @param {Array} allPossibleArgs All possible args
530
         * @param {Array} bulkData Bulk data
531
         * @returns {void}
532
         * @private
533
         */
534
        _getArgsForBulkInput(allPossibleArgs, bulkData) {
535
                for (let arg of allPossibleArgs) {
603✔
536
                        let bulkArg = bulkData.find((o) => o.fieldKey === arg) || {};
1,422✔
537
                        if (!_.isNil(bulkArg.value)) {
927✔
538
                                // Use value from bulk input file when available
539
                                this.argv.push(bulkArg.value);
756✔
540
                        } else if (this.args[arg]) {
171✔
541
                                // Fall back to value from command line
542
                                this.argv.push(this.args[arg]);
135✔
543
                        }
544
                }
545
        }
546

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

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

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

691
        /**
692
         * Returns bulk file contents
693
         * @param {String} filePath Path to bulk file
694
         * @returns {Buffer} Bulk file contents
695
         * @private
696
         */
697
        _readBulkFile(filePath) {
698
                try {
324✔
699
                        const fileContents = fs.readFileSync(filePath);
324✔
700
                        DEBUG.execute('Read bulk input file at %s', filePath);
324✔
701
                        return fileContents;
324✔
702
                } catch (error) {
703
                        throw new BoxCLIError(
×
704
                                `Could not open input file ${filePath}`,
705
                                error
706
                        );
707
                }
708
        }
709

710
        /**
711
         * Writes a given flag value to the command's argv array
712
         *
713
         * @param {string} flag The flag name
714
         * @param {*} flagValue The flag value
715
         * @returns {void}
716
         * @private
717
         */
718
        _addFlagToArgv(flag, flagValue) {
719
                if (_.isNil(flagValue)) {
3,177✔
720
                        return;
108✔
721
                }
722

723
                if (this.constructor.flags[flag].type === 'boolean') {
3,069✔
724
                        if (getBooleanFlagValue(flagValue)) {
1,593✔
725
                                this.argv.push(`--${flag}`);
1,494✔
726
                        } else {
727
                                this.argv.push(`--no-${flag}`);
99✔
728
                        }
729
                } else {
730
                        this.argv.push(`--${flag}=${flagValue}`);
1,476✔
731
                }
732
        }
733

734
        /**
735
         * Ensure that all args and flags for the command are not marked as required,
736
         * to avoid issues when filling in required values from the input file.
737
         * @returns {void}
738
         */
739
        disableRequiredArgsAndFlags() {
740
                if (this.constructor.args !== undefined) {
324!
741
                        for (const key of Object.keys(this.constructor.args)) {
324✔
742
                                this.constructor.args[key].required = false;
486✔
743
                        }
744
                }
745

746
                if (this.constructor.flags !== undefined) {
324!
747
                        for (const key of Object.keys(this.constructor.flags)) {
324✔
748
                                this.constructor.flags[key].required = false;
8,082✔
749
                        }
750
                }
751
        }
752

753
        /**
754
         * Instantiate the SDK client for making API calls
755
         *
756
         * @returns {BoxClient} The client for making API calls in the command
757
         */
758
        async getClient() {
759
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
760
                if (this.constructor.noClient) {
8,100✔
761
                        return null;
27✔
762
                }
763
                let environmentsObj = await this.getEnvironments();
8,073✔
764
                const environment =
765
                        environmentsObj.environments[environmentsObj.default] || {};
8,073✔
766
                const { authMethod } = environment;
8,073✔
767

768
                let client;
769
                if (this.flags.token) {
8,073!
770
                        DEBUG.init('Using passed in token %s', this.flags.token);
8,073✔
771
                        let sdk = new BoxSDK({
8,073✔
772
                                clientID: '',
773
                                clientSecret: '',
774
                                ...SDK_CONFIG,
775
                        });
776
                        this._configureSdk(sdk, { ...SDK_CONFIG });
8,073✔
777
                        this.sdk = sdk;
8,073✔
778
                        client = sdk.getBasicClient(this.flags.token);
8,073✔
779
                } else if (authMethod === 'ccg') {
×
780
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
781

782
                        const { clientId, clientSecret, ccgUser } = environment;
×
783

784
                        if (!clientId || !clientSecret) {
×
785
                                throw new BoxCLIError(
×
786
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
787
                                );
788
                        }
789

790
                        let configObj;
791
                        try {
×
792
                                configObj = JSON.parse(
×
793
                                        fs.readFileSync(environment.boxConfigFilePath)
794
                                );
795
                        } catch (error) {
796
                                throw new BoxCLIError(
×
797
                                        'Could not read environments config file',
798
                                        error
799
                                );
800
                        }
801

802
                        const { enterpriseID } = configObj;
×
803
                        const sdk = new BoxSDK({
×
804
                                clientID: clientId,
805
                                clientSecret,
806
                                enterpriseID,
807
                                ...SDK_CONFIG,
808
                        });
809
                        this._configureSdk(sdk, { ...SDK_CONFIG });
×
810
                        this.sdk = sdk;
×
811
                        client = ccgUser
×
812
                                ? sdk.getCCGClientForUser(ccgUser)
813
                                : sdk.getAnonymousClient();
814
                } else if (
×
815
                        environmentsObj.default &&
×
816
                        environmentsObj.environments[environmentsObj.default].authMethod ===
817
                                'oauth20'
818
                ) {
819
                        try {
×
820
                                DEBUG.init(
×
821
                                        'Using environment %s %O',
822
                                        environmentsObj.default,
823
                                        environment
824
                                );
825
                                let tokenCache = new CLITokenCache(environmentsObj.default);
×
826

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

871
                        if (!environment.hasInLinePrivateKey) {
×
872
                                try {
×
873
                                        configObj.boxAppSettings.appAuth.privateKey =
×
874
                                                fs.readFileSync(environment.privateKeyPath, 'utf8');
875
                                        DEBUG.init(
×
876
                                                'Loaded JWT private key from %s',
877
                                                environment.privateKeyPath
878
                                        );
879
                                } catch (error) {
880
                                        throw new BoxCLIError(
×
881
                                                `Could not read private key file ${environment.privateKeyPath}`,
882
                                                error
883
                                        );
884
                                }
885
                        }
886

887
                        this.sdk = BoxSDK.getPreconfiguredInstance(configObj);
×
888
                        this._configureSdk(this.sdk, { ...SDK_CONFIG });
×
889

890
                        client = this.sdk.getAppAuthClient(
×
891
                                'enterprise',
892
                                environment.enterpriseId,
893
                                tokenCache
894
                        );
895
                        DEBUG.init('Initialized client from environment config');
×
896
                } else {
897
                        // No environments set up yet!
898
                        throw new BoxCLIError(
×
899
                                `No default environment found.
900
                                It looks like you haven't configured the Box CLI yet.
901
                                See this command for help adding an environment: box configure:environments:add --help
902
                                Or, supply a token with your command with --token.`.replaceAll(/^\s+/gmu, '')
903
                        );
904
                }
905

906
                // Using the as-user flag should have precedence over the environment setting
907
                if (this.flags['as-user']) {
8,073✔
908
                        client.asUser(this.flags['as-user']);
9✔
909
                        DEBUG.init(
9✔
910
                                'Impersonating user ID %s using the ID provided via the --as-user flag',
911
                                this.flags['as-user']
912
                        );
913
                } else if (!this.flags.token && environment.useDefaultAsUser) {
8,064!
914
                        // We don't want to use any environment settings if a token is passed in the command
915
                        client.asUser(environment.defaultAsUserId);
×
916
                        DEBUG.init(
×
917
                                'Impersonating default user ID %s using environment configuration',
918
                                environment.defaultAsUserId
919
                        );
920
                }
921
                return client;
8,073✔
922
        }
923

924
        /**
925
         * Instantiate the TypeScript SDK client for making API calls
926
         *
927
         * @returns {BoxTSSDK.BoxClient} The TypeScript SDK client for making API calls in the command
928
         */
929
        async getTsClient() {
930
                // Allow some commands (e.g. configure:environments:add, login) to skip client setup so they can run
931
                if (this.constructor.noClient) {
8,100✔
932
                        return null;
27✔
933
                }
934
                let environmentsObj = await this.getEnvironments();
8,073✔
935
                const environment =
936
                        environmentsObj.environments[environmentsObj.default] || {};
8,073✔
937
                const { authMethod } = environment;
8,073✔
938

939
                let client;
940
                if (this.flags.token) {
8,073!
941
                        DEBUG.init('Using passed in token %s', this.flags.token);
8,073✔
942
                        let tsSdkAuth = new BoxTSSDK.BoxDeveloperTokenAuth({
8,073✔
943
                                token: this.flags.token,
944
                        });
945
                        client = new BoxTSSDK.BoxClient({
8,073✔
946
                                auth: tsSdkAuth,
947
                        });
948
                        client = this._configureTsSdk(client, SDK_CONFIG);
8,073✔
949
                } else if (authMethod === 'ccg') {
×
950
                        DEBUG.init('Using Client Credentials Grant Authentication');
×
951

952
                        const { clientId, clientSecret, ccgUser } = environment;
×
953

954
                        if (!clientId || !clientSecret) {
×
955
                                throw new BoxCLIError(
×
956
                                        'You need to have a default environment with clientId and clientSecret in order to use CCG'
957
                                );
958
                        }
959

960
                        let configObj;
961
                        try {
×
962
                                configObj = JSON.parse(
×
963
                                        fs.readFileSync(environment.boxConfigFilePath)
964
                                );
965
                        } catch (error) {
966
                                throw new BoxCLIError(
×
967
                                        'Could not read environments config file',
968
                                        error
969
                                );
970
                        }
971

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

1046
                        if (!environment.hasInLinePrivateKey) {
×
1047
                                try {
×
1048
                                        configObj.boxAppSettings.appAuth.privateKey =
×
1049
                                                fs.readFileSync(environment.privateKeyPath, 'utf8');
1050
                                        DEBUG.init(
×
1051
                                                'Loaded JWT private key from %s',
1052
                                                environment.privateKeyPath
1053
                                        );
1054
                                } catch (error) {
1055
                                        throw new BoxCLIError(
×
1056
                                                `Could not read private key file ${environment.privateKeyPath}`,
1057
                                                error
1058
                                        );
1059
                                }
1060
                        }
1061

1062
                        const jwtConfig = new BoxTSSDK.JwtConfig({
×
1063
                                clientId: configObj.boxAppSettings.clientID,
1064
                                clientSecret: configObj.boxAppSettings.clientSecret,
1065
                                jwtKeyId: configObj.boxAppSettings.appAuth.publicKeyID,
1066
                                privateKey: configObj.boxAppSettings.appAuth.privateKey,
1067
                                privateKeyPassphrase:
1068
                                        configObj.boxAppSettings.appAuth.passphrase,
1069
                                enterpriseId: environment.enterpriseId,
1070
                                tokenStorage: tokenCache,
1071
                        });
1072
                        let jwtAuth = new BoxTSSDK.BoxJwtAuth({ config: jwtConfig });
×
1073
                        client = new BoxTSSDK.BoxClient({ auth: jwtAuth });
×
1074

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

1100
        /**
1101
         * Configures SDK by using values from settings.json file
1102
         * @param {*} sdk to configure
1103
         * @param {*} config Additional options to use while building configuration
1104
         * @returns {void}
1105
         */
1106
        _configureSdk(sdk, config = {}) {
×
1107
                const clientSettings = { ...config };
8,100✔
1108
                if (this.settings.enableProxy) {
8,100!
1109
                        clientSettings.proxy = this.settings.proxy;
×
1110
                }
1111
                if (this.settings.apiRootURL) {
8,100!
1112
                        clientSettings.apiRootURL = this.settings.apiRootURL;
×
1113
                }
1114
                if (this.settings.uploadAPIRootURL) {
8,100!
1115
                        clientSettings.uploadAPIRootURL = this.settings.uploadAPIRootURL;
×
1116
                }
1117
                if (this.settings.authorizeRootURL) {
8,100!
1118
                        clientSettings.authorizeRootURL = this.settings.authorizeRootURL;
×
1119
                }
1120
                if (this.settings.numMaxRetries) {
8,100!
1121
                        clientSettings.numMaxRetries = this.settings.numMaxRetries;
×
1122
                }
1123
                if (this.settings.retryIntervalMS) {
8,100!
1124
                        clientSettings.retryIntervalMS = this.settings.retryIntervalMS;
×
1125
                }
1126
                if (this.settings.uploadRequestTimeoutMS) {
8,100!
1127
                        clientSettings.uploadRequestTimeoutMS =
×
1128
                                this.settings.uploadRequestTimeoutMS;
1129
                }
1130
                clientSettings.analyticsClient.name =
8,100✔
1131
                        this.settings.enableAnalyticsClient &&
16,200!
1132
                        this.settings.analyticsClient.name
1133
                                ? `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`
1134
                                : DEFAULT_ANALYTICS_CLIENT_NAME;
1135

1136
                if (Object.keys(clientSettings).length > 0) {
8,100!
1137
                        DEBUG.init('SDK client settings %s', clientSettings);
8,100✔
1138
                        sdk.configure(clientSettings);
8,100✔
1139
                }
1140
        }
1141

1142
        /**
1143
         * Configures TS SDK by using values from settings.json file
1144
         *
1145
         * @param {BoxTSSDK.BoxClient} client to configure
1146
         * @param {Object} config Additional options to use while building configuration
1147
         * @returns {BoxTSSDK.BoxClient} The configured client
1148
         */
1149
        _configureTsSdk(client, config) {
1150
                let additionalHeaders = config.request.headers;
8,073✔
1151
                let customBaseURL = {
8,073✔
1152
                        baseUrl: 'https://api.box.com',
1153
                        uploadUrl: 'https://upload.box.com/api',
1154
                        oauth2Url: 'https://account.box.com/api/oauth2',
1155
                };
1156
                if (this.settings.enableProxy) {
8,073!
1157
                        client = client.withProxy(this.settings.proxy);
×
1158
                }
1159
                if (this.settings.apiRootURL) {
8,073!
1160
                        customBaseURL.baseUrl = this.settings.apiRootURL;
×
1161
                }
1162
                if (this.settings.uploadAPIRootURL) {
8,073!
1163
                        customBaseURL.uploadUrl = this.settings.uploadAPIRootURL;
×
1164
                }
1165
                if (this.settings.authorizeRootURL) {
8,073!
1166
                        customBaseURL.oauth2Url = this.settings.authorizeRootURL;
×
1167
                }
1168
                client = client.withCustomBaseUrls(customBaseURL);
8,073✔
1169

1170
                if (this.settings.numMaxRetries) {
8,073!
1171
                        // Not supported in TS SDK
1172
                }
1173
                if (this.settings.retryIntervalMS) {
8,073!
1174
                        // Not supported in TS SDK
1175
                }
1176
                if (this.settings.uploadRequestTimeoutMS) {
8,073!
1177
                        // Not supported in TS SDK
1178
                }
1179
                additionalHeaders['X-Box-UA'] =
8,073✔
1180
                        this.settings.enableAnalyticsClient &&
16,146!
1181
                        this.settings.analyticsClient.name
1182
                                ? `${DEFAULT_ANALYTICS_CLIENT_NAME} ${this.settings.analyticsClient.name}`
1183
                                : DEFAULT_ANALYTICS_CLIENT_NAME;
1184
                client = client.withExtraHeaders(additionalHeaders);
8,073✔
1185
                DEBUG.init('TS SDK configured with settings from settings.json');
8,073✔
1186

1187
                return client;
8,073✔
1188
        }
1189

1190
        /**
1191
         * Returns true when raw API JSON output was requested.
1192
         *
1193
         * @returns {boolean} True if raw JSON output should be used
1194
         * @private
1195
         */
1196
        _wantsRawJsonOutput() {
1197
                return Boolean(this.flags && this.flags['raw-json']);
5,139✔
1198
        }
1199

1200
        /**
1201
         * Preserves default output behavior while adding raw JSON support.
1202
         *
1203
         * The generated TypeScript client can expose response objects with normalized
1204
         * field names that do not match the original API schema. That behavior was
1205
         * introduced as newer commands moved to `tsClient`. To fix the JSON output
1206
         * shape without a breaking change, commands that support `--raw-json` use this
1207
         * wrapper on the top-level response object before calling `output()`.
1208
         *
1209
         * @param {*} content The content to potentially replace with rawData
1210
         * @returns {*} The content to send to output formatting
1211
         */
1212
        getOutputContentWithRawJsonSupport(content) {
1213
                if (typeof content === 'object' && content !== null) {
126!
1214
                        if (this._wantsRawJsonOutput()) {
126✔
1215
                                return content.rawData ?? content;
36!
1216
                        }
1217

1218
                        if (Object.hasOwn(content, 'rawData')) {
90!
1219
                                const output = { ...content };
90✔
1220
                                delete output.rawData;
90✔
1221
                                return output;
90✔
1222
                        }
1223
                }
1224

1225
                return content;
×
1226
        }
1227

1228
        /**
1229
         * Format data for output to stdout
1230
         * @param {*} content The content to output
1231
         * @returns {Promise<void>} A promise resolving when output is handled
1232
         */
1233
        async output(content) {
1234
                if (this.isBulk) {
7,515✔
1235
                        this.bulkOutputList.push(content);
576✔
1236
                        DEBUG.output(
576✔
1237
                                'Added command output to bulk list total: %d',
1238
                                this.bulkOutputList.length
1239
                        );
1240
                        return;
576✔
1241
                }
1242

1243
                let formattedOutputData;
1244
                if (Array.isArray(content)) {
6,939✔
1245
                        // Format each object individually and then flatten in case this an array of arrays,
1246
                        // which happens when a command that outputs a collection gets run in bulk
1247
                        const formattedOutputResults = await Promise.all(
468✔
1248
                                content.map((o) => this._formatOutputObject(o))
10,170✔
1249
                        );
1250
                        formattedOutputData = formattedOutputResults.flat();
468✔
1251
                        DEBUG.output(
468✔
1252
                                'Formatted %d output entries for display',
1253
                                content.length
1254
                        );
1255
                } else {
1256
                        formattedOutputData = await this._formatOutputObject(content);
6,471✔
1257
                        DEBUG.output('Formatted output content for display');
6,471✔
1258
                }
1259
                let outputFormat = this._getOutputFormat();
6,939✔
1260
                DEBUG.output('Using %s output format', outputFormat);
6,939✔
1261
                DEBUG.output(formattedOutputData);
6,939✔
1262

1263
                let writeFunc;
1264
                let logFunc;
1265
                let stringifiedOutput;
1266

1267
                // remove all the undefined values from the object
1268
                formattedOutputData = removeUndefinedValues(formattedOutputData);
6,939✔
1269

1270
                if (outputFormat === 'json') {
6,939✔
1271
                        stringifiedOutput = stringifyStream(formattedOutputData, null, 4);
4,455✔
1272

1273
                        let appendNewLineTransform = new Transform({
4,455✔
1274
                                transform(chunk, encoding, callback) {
1275
                                        callback(null, chunk);
36✔
1276
                                },
1277
                                flush(callback) {
1278
                                        this.push(os.EOL);
36✔
1279
                                        callback();
36✔
1280
                                },
1281
                        });
1282

1283
                        writeFunc = async (savePath) => {
4,455✔
1284
                                await pipeline(
36✔
1285
                                        stringifiedOutput,
1286
                                        appendNewLineTransform,
1287
                                        fs.createWriteStream(savePath, { encoding: 'utf8' })
1288
                                );
1289
                        };
1290

1291
                        logFunc = async () => {
4,455✔
1292
                                await this.logStream(stringifiedOutput);
4,419✔
1293
                        };
1294
                } else {
1295
                        stringifiedOutput =
2,484✔
1296
                                await this._stringifyOutput(formattedOutputData);
1297

1298
                        writeFunc = async (savePath) => {
2,484✔
1299
                                await utils.writeFileAsync(
9✔
1300
                                        savePath,
1301
                                        stringifiedOutput + os.EOL,
1302
                                        {
1303
                                                encoding: 'utf8',
1304
                                        }
1305
                                );
1306
                        };
1307

1308
                        logFunc = () => this.log(stringifiedOutput);
2,484✔
1309
                }
1310
                return this._writeOutput(writeFunc, logFunc);
6,939✔
1311
        }
1312

1313
        /**
1314
         * Check if max-items has been reached.
1315
         *
1316
         * @param {number} maxItems Total number of items to return
1317
         * @param {number} itemsCount Current number of items
1318
         * @returns {boolean} True if limit has been reached, otherwise false
1319
         * @private
1320
         */
1321
        maxItemsReached(maxItems, itemsCount) {
1322
                return maxItems && itemsCount >= maxItems;
6,777✔
1323
        }
1324

1325
        /**
1326
         * Fetch all marker-based pages from a TypeScript SDK endpoint.
1327
         *
1328
         * @param {Function} fetchPage Callback that receives query params and returns a paged response
1329
         * @param {Object} queryParams Base query params for each request
1330
         * @param {number} [maxItemsOverride] Optional max items override for advanced use
1331
         * @returns {Promise<Object[]>} Aggregated marker-based response entries
1332
         */
1333
        async markerPagination(fetchPage, queryParams, maxItemsOverride) {
1334
                const normalizedQueryParams = queryParams || {};
63!
1335
                const paginationFlags = {
63✔
1336
                        'max-items':
1337
                                maxItemsOverride === undefined
63!
1338
                                        ? this.flags['max-items']
1339
                                        : maxItemsOverride,
1340
                };
1341
                const paginationOptions =
1342
                        PaginationUtilities.handlePagination(paginationFlags);
63✔
1343
                const maxItems =
1344
                        paginationFlags['max-items'] === undefined
63!
1345
                                ? paginationOptions.limit
1346
                                : paginationFlags['max-items'];
1347

1348
                let remaining = maxItems;
63✔
1349
                const entries = [];
63✔
1350
                const pageLimit = paginationOptions.limit;
63✔
1351
                let marker;
1352

1353
                while (remaining > 0) {
63✔
1354
                        const pageQueryParams = {
72✔
1355
                                ...normalizedQueryParams,
1356
                                limit: Math.min(pageLimit, remaining),
1357
                        };
1358

1359
                        if (marker) {
72✔
1360
                                pageQueryParams.marker = marker;
9✔
1361
                        }
1362

1363
                        const page = await fetchPage(pageQueryParams);
72✔
1364
                        const rawPage =
1365
                                typeof page?.rawData === 'object' && page.rawData !== null
72!
1366
                                        ? page.rawData
1367
                                        : page;
1368
                        const pageEntries = rawPage.entries || page.entries || [];
72!
1369
                        entries.push(...pageEntries);
72✔
1370
                        remaining -= pageEntries.length;
72✔
1371
                        marker = page.nextMarker || page.next_marker;
72✔
1372

1373
                        if (!marker || pageEntries.length === 0) {
72✔
1374
                                break;
63✔
1375
                        }
1376
                }
1377

1378
                return entries;
63✔
1379
        }
1380

1381
        /**
1382
         * Prepare the output data by:
1383
         *   1) Unrolling an iterator into an array
1384
         *   2) Filtering out unwanted object fields
1385
         *
1386
         * @param {*} obj The raw object containing output data
1387
         * @returns {*} The formatted output data
1388
         * @private
1389
         */
1390
        async _formatOutputObject(obj) {
1391
                let output = obj;
16,641✔
1392

1393
                // Pass primitive content types through
1394
                if (typeof output !== 'object' || output === null) {
16,641!
1395
                        return output;
×
1396
                }
1397

1398
                // Unroll iterator into array
1399
                if (typeof obj.next === 'function') {
16,641✔
1400
                        output = [];
1,494✔
1401
                        let entry = await obj.next();
1,494✔
1402
                        while (!entry.done) {
1,494✔
1403
                                output.push(entry.value);
6,777✔
1404

1405
                                if (
6,777✔
1406
                                        this.maxItemsReached(this.flags['max-items'], output.length)
1407
                                ) {
1408
                                        break;
45✔
1409
                                }
1410

1411
                                entry = await obj.next();
6,732✔
1412
                        }
1413
                        DEBUG.output('Unrolled iterable into %d entries', output.length);
1,494✔
1414
                }
1415

1416
                if (this.flags['id-only']) {
16,641✔
1417
                        output = Array.isArray(output)
270!
1418
                                ? this.filterOutput(output, 'id')
1419
                                : output.id;
1420
                } else {
1421
                        output = this.filterOutput(output, this.flags.fields);
16,371✔
1422
                }
1423

1424
                return output;
16,641✔
1425
        }
1426

1427
        /**
1428
         * Get the output format (and file extension) based on the settings and flags set
1429
         *
1430
         * @returns {string} The file extension/format to use for output
1431
         * @private
1432
         */
1433
        _getOutputFormat() {
1434
                if (this.flags.json) {
9,441✔
1435
                        return 'json';
4,428✔
1436
                }
1437

1438
                // Raw API payloads are only intended for JSON output, so `--raw-json`
1439
                // implicitly promotes the format to JSON without changing default behavior.
1440
                if (this._wantsRawJsonOutput()) {
5,013✔
1441
                        return 'json';
36✔
1442
                }
1443

1444
                if (this.flags.csv) {
4,977✔
1445
                        return 'csv';
54✔
1446
                }
1447

1448
                if (this.flags.save || this.flags['save-to-file-path']) {
4,923✔
1449
                        return this.settings.boxReportsFileFormat || 'txt';
27!
1450
                }
1451

1452
                if (this.settings.outputJson) {
4,896!
1453
                        return 'json';
×
1454
                }
1455

1456
                return 'txt';
4,896✔
1457
        }
1458

1459
        /**
1460
         * Converts output data to a string based on the type of content and flags the user
1461
         * has specified regarding output format
1462
         *
1463
         * @param {*} outputData The data to output
1464
         * @returns {string} Promise resolving to the output data as a string
1465
         * @private
1466
         */
1467
        async _stringifyOutput(outputData) {
1468
                let outputFormat = this._getOutputFormat();
2,484✔
1469

1470
                if (typeof outputData !== 'object') {
2,484✔
1471
                        DEBUG.output('Primitive output cast to string');
270✔
1472
                        return String(outputData);
270✔
1473
                } else if (outputFormat === 'csv') {
2,214✔
1474
                        let csvString = await csvStringify(
27✔
1475
                                this.formatForTableAndCSVOutput(outputData)
1476
                        );
1477
                        // The CSV library puts a trailing newline at the end of the string, which is
1478
                        // redundant with the automatic newline added by oclif when writing to stdout
1479
                        DEBUG.output('Processed output as CSV');
27✔
1480
                        return csvString.replace(/\r?\n$/u, '');
27✔
1481
                } else if (Array.isArray(outputData)) {
2,187✔
1482
                        let str = outputData
63✔
1483
                                .map(
1484
                                        (o) => `${formatObjectHeader(o)}${os.EOL}${formatObject(o)}`
153✔
1485
                                )
1486
                                .join(os.EOL.repeat(2));
1487
                        DEBUG.output('Processed collection into human-readable output');
63✔
1488
                        return str;
63✔
1489
                }
1490

1491
                let str = formatObject(outputData);
2,124✔
1492
                DEBUG.output('Processed human-readable output');
2,124✔
1493
                return str;
2,124✔
1494
        }
1495

1496
        /**
1497
         * Generate an appropriate default filename for writing
1498
         * the output of this command to disk.
1499
         *
1500
         * @returns {string} The output file name
1501
         * @private
1502
         */
1503
        _getOutputFileName() {
1504
                let extension = this._getOutputFormat();
18✔
1505
                return `${this.id.replaceAll(':', '-')}-${dateTime.format(
18✔
1506
                        new Date(),
1507
                        'YYYY-MM-DD_HH_mm_ss_SSS'
1508
                )}.${extension}`;
1509
        }
1510

1511
        /**
1512
         * Write output to its final destination, either a file or stdout
1513
         * @param {Function} writeFunc Function used to save output to a file
1514
         * @param {Function} logFunc Function used to print output to stdout
1515
         * @returns {Promise<void>} A promise resolving when output is written
1516
         * @private
1517
         */
1518
        async _writeOutput(writeFunc, logFunc) {
1519
                if (this.flags.save) {
6,939✔
1520
                        DEBUG.output('Writing output to default location on disk');
9✔
1521
                        let filePath = path.join(
9✔
1522
                                this.settings.boxReportsFolderPath,
1523
                                this._getOutputFileName()
1524
                        );
1525
                        try {
9✔
1526
                                await writeFunc(filePath);
9✔
1527
                        } catch (error) {
1528
                                throw new BoxCLIError(
×
1529
                                        `Could not write output to file at ${filePath}`,
1530
                                        error
1531
                                );
1532
                        }
1533
                        this.info(chalk`{green Output written to ${filePath}}`);
9✔
1534
                } else if (this.flags['save-to-file-path']) {
6,930✔
1535
                        let savePath = this.flags['save-to-file-path'];
36✔
1536
                        if (fs.existsSync(savePath)) {
36!
1537
                                if (fs.statSync(savePath).isDirectory()) {
36✔
1538
                                        // Append default file name and write into the provided directory
1539
                                        savePath = path.join(savePath, this._getOutputFileName());
9✔
1540
                                        DEBUG.output(
9✔
1541
                                                'Output path is a directory, will write to %s',
1542
                                                savePath
1543
                                        );
1544
                                } else {
1545
                                        DEBUG.output('File already exists at %s', savePath);
27✔
1546
                                        // Ask if the user want to overwrite the file
1547
                                        let shouldOverwrite = await this.confirm(
27✔
1548
                                                `File ${savePath} already exists — overwrite?`
1549
                                        );
1550

1551
                                        if (!shouldOverwrite) {
27!
1552
                                                return;
×
1553
                                        }
1554
                                }
1555
                        }
1556
                        try {
36✔
1557
                                DEBUG.output(
36✔
1558
                                        'Writing output to specified location on disk: %s',
1559
                                        savePath
1560
                                );
1561
                                await writeFunc(savePath);
36✔
1562
                        } catch (error) {
1563
                                throw new BoxCLIError(
×
1564
                                        `Could not write output to file at ${savePath}`,
1565
                                        error
1566
                                );
1567
                        }
1568
                        this.info(chalk`{green Output written to ${savePath}}`);
36✔
1569
                } else {
1570
                        DEBUG.output('Writing output to terminal');
6,894✔
1571
                        await logFunc();
6,894✔
1572
                }
1573

1574
                DEBUG.output('Finished writing output');
6,939✔
1575
        }
1576

1577
        /**
1578
         * Ask a user to confirm something, respecting the default --yes flag
1579
         *
1580
         * @param {string} promptText The text of the prompt to the user
1581
         * @param {boolean} defaultValue The default value of the prompt
1582
         * @returns {Promise<boolean>} A promise resolving to a boolean that is true iff the user confirmed
1583
         */
1584
        async confirm(promptText, defaultValue = false) {
27✔
1585
                if (this.flags.yes) {
27✔
1586
                        return true;
18✔
1587
                }
1588

1589
                let answers = await inquirer.prompt([
9✔
1590
                        {
1591
                                name: 'confirmation',
1592
                                message: promptText,
1593
                                type: 'confirm',
1594
                                default: defaultValue,
1595
                        },
1596
                ]);
1597

1598
                return answers.confirmation;
9✔
1599
        }
1600

1601
        /**
1602
         * Writes output to stderr — this should be used for informational output.  For example, a message
1603
         * stating that an item has been deleted.
1604
         *
1605
         * @param {string} content The message to output
1606
         * @returns {void}
1607
         */
1608
        info(content) {
1609
                if (!this.flags.quiet) {
1,152✔
1610
                        process.stderr.write(`${content}${os.EOL}`);
1,143✔
1611
                }
1612
        }
1613

1614
        /**
1615
         * Writes output to stderr — this should be used for informational output.  For example, a message
1616
         * stating that an item has been deleted.
1617
         *
1618
         * @param {string} content The message to output
1619
         * @returns {void}
1620
         */
1621
        log(content) {
1622
                if (!this.flags.quiet) {
2,475✔
1623
                        process.stdout.write(`${content}${os.EOL}`);
2,457✔
1624
                }
1625
        }
1626

1627
        /**
1628
         * Writes stream output to stderr — this should be used for informational output.  For example, a message
1629
         * stating that an item has been deleted.
1630
         *
1631
         * @param {ReadableStream} content The message to output
1632
         * @returns {void}
1633
         */
1634
        async logStream(content) {
1635
                if (!this.flags.quiet) {
4,419!
1636
                        // For Node 12 when process.stdout is in pipeline it's not emitting end event correctly and it freezes.
1637
                        // See - https://github.com/nodejs/node/issues/34059
1638
                        // Using promise for now.
1639
                        content.pipe(process.stdout);
4,419✔
1640

1641
                        await new Promise((resolve, reject) => {
4,419✔
1642
                                content
4,419✔
1643
                                        .on('end', () => {
1644
                                                process.stdout.write(os.EOL);
4,419✔
1645
                                                resolve();
4,419✔
1646
                                        })
1647
                                        .on('error', (err) => {
1648
                                                reject(err);
×
1649
                                        });
1650
                        });
1651
                }
1652
        }
1653

1654
        /**
1655
         * Wraps filtered error in an error with a user-friendly description
1656
         *
1657
         * @param {Error} err  The thrown error
1658
         * @returns {Error} Error wrapped in an error with user friendly description
1659
         */
1660
        wrapError(err) {
1661
                let messageMap = {
369✔
1662
                        'invalid_grant - Refresh token has expired':
1663
                                '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.',
1664
                        'Expired Auth: Auth code or refresh token has expired':
1665
                                'Authentication failed: token is invalid or expired. OAuth: run "box login --reauthorize". JWT/CCG: tokens are refreshed automatically, so this usually means app credentials or environment configuration must be fixed. You can also provide a fresh token with --token.',
1666
                };
1667

1668
                for (const key in messageMap) {
369✔
1669
                        if (err.message.includes(key)) {
738!
1670
                                return new BoxCLIError(messageMap[key], err);
×
1671
                        }
1672
                }
1673

1674
                return err;
369✔
1675
        }
1676

1677
        /**
1678
         * Handles an error thrown within a command
1679
         *
1680
         * @param {Error} err  The thrown error
1681
         * @returns {void}
1682
         */
1683
        async catch(err) {
1684
                const AUTH_FAILED_HINT =
1685
                        'Authentication failed: token is invalid or expired. OAuth: run "box login --reauthorize". JWT/CCG: tokens are refreshed automatically, so a 401 usually means app credentials or environment configuration must be fixed. You can also provide a fresh token with --token.';
342✔
1686
                if (
342!
1687
                        err instanceof BoxTsErrors.BoxApiError &&
342!
1688
                        err.responseInfo &&
1689
                        err.responseInfo.body
1690
                ) {
1691
                        const responseInfo = err.responseInfo;
×
1692
                        let errorMessage = `Unexpected API Response [${responseInfo.body.status} ${responseInfo.body.message} | ${responseInfo.body.request_id}] ${responseInfo.body.code} - ${responseInfo.body.message}`;
×
1693
                        if (responseInfo.body.status === 401) {
×
1694
                                errorMessage += `\n${AUTH_FAILED_HINT}`;
×
1695
                        }
1696
                        err = new BoxCLIError(errorMessage, err);
×
1697
                }
1698
                if (err instanceof BoxTsErrors.BoxSdkError) {
342!
1699
                        try {
×
1700
                                let errorObj = JSON.parse(err.message);
×
1701
                                if (errorObj.message) {
×
1702
                                        err = new BoxCLIError(errorObj.message, err);
×
1703
                                }
1704
                        } catch (error) {
1705
                                DEBUG.execute('Error parsing BoxSdkError message: %s', error);
×
1706
                        }
1707
                }
1708
                try {
342✔
1709
                        // Let the oclif default handler run first, since it handles the help and version flags there
1710
                        /* eslint-disable promise/no-promise-in-callback */
1711
                        DEBUG.execute('Running framework error handler');
342✔
1712
                        await super.catch(this.wrapError(err));
342✔
1713
                } catch (error) {
1714
                        // The oclif default catch handler rethrows most errors; handle those here
1715
                        DEBUG.execute('Handling re-thrown error in base command handler');
342✔
1716

1717
                        if (error.code === 'EEXIT') {
342!
1718
                                // oclif throws this when it handled the error itself and wants to exit, so just let it do that
1719
                                DEBUG.execute('Got EEXIT code, exiting immediately');
×
1720
                                return;
×
1721
                        }
1722
                        let contextInfo;
1723
                        if (
342✔
1724
                                error.response &&
540✔
1725
                                error.response.body &&
1726
                                error.response.body.context_info
1727
                        ) {
1728
                                contextInfo = formatObject(error.response.body.context_info);
9✔
1729
                                // Remove color codes from context info
1730
                                // eslint-disable-next-line no-control-regex
1731
                                contextInfo = contextInfo.replaceAll(/\u001B\[\d+m/gu, '');
9✔
1732
                                // Remove \n with os.EOL
1733
                                contextInfo = contextInfo.replaceAll('\n', os.EOL);
9✔
1734
                        }
1735
                        let statusHint = '';
342✔
1736
                        const statusCode = error.statusCode || error.response?.statusCode;
342✔
1737
                        if (statusCode === 401) {
342!
1738
                                statusHint = AUTH_FAILED_HINT;
×
1739
                        }
1740
                        let errorMsg = chalk`{redBright ${
342✔
1741
                                this.flags && this.flags.verbose ? error.stack : error.message
1,026✔
1742
                        }${os.EOL}${contextInfo ? contextInfo + os.EOL : ''}${statusHint ? statusHint + os.EOL : ''}}`;
684!
1743

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

1747
                        process.stderr.write(errorMsg, () => {
342✔
1748
                                process.exitCode = 2;
342✔
1749
                        });
1750
                }
1751
        }
1752

1753
        /**
1754
         * Final hook that executes for all commands, regardless of if an error occurred
1755
         * @param {Error} [err] An error, if one occurred
1756
         * @returns {void}
1757
         */
1758
        async finally(/* err */) {
1759
                // called after run and catch regardless of whether or not the command errored
1760
        }
1761

1762
        /**
1763
         * Filter out unwanted fields from the output object(s)
1764
         *
1765
         * @param {Object|Object[]} output The output object(s)
1766
         * @param {string} [fields] Comma-separated list of fields to include
1767
         * @returns {Object|Object[]} The filtered object(s) for output
1768
         */
1769
        filterOutput(output, fields) {
1770
                if (!fields) {
16,371✔
1771
                        return output;
15,795✔
1772
                }
1773
                fields = [
576✔
1774
                        ...REQUIRED_FIELDS,
1775
                        ...fields.split(',').filter((f) => !REQUIRED_FIELDS.includes(f)),
711✔
1776
                ];
1777
                DEBUG.output('Filtering output with fields: %O', fields);
576✔
1778
                if (Array.isArray(output)) {
576✔
1779
                        output = output.map((o) =>
342✔
1780
                                typeof o === 'object' ? _.pick(o, fields) : o
1,404!
1781
                        );
1782
                } else if (typeof output === 'object') {
234!
1783
                        output = _.pick(output, fields);
234✔
1784
                }
1785
                return output;
576✔
1786
        }
1787

1788
        /**
1789
         * Flatten nested objects for output to a table/CSV
1790
         *
1791
         * @param {Object[]} objectArray The objects that will be output
1792
         * @returns {Array[]} The formatted output
1793
         */
1794
        formatForTableAndCSVOutput(objectArray) {
1795
                let formattedData = [];
27✔
1796
                if (!Array.isArray(objectArray)) {
27!
1797
                        objectArray = [objectArray];
×
1798
                        DEBUG.output('Creating tabular output from single object');
×
1799
                }
1800

1801
                let keyPaths = [];
27✔
1802
                for (let object of objectArray) {
27✔
1803
                        keyPaths = _.union(keyPaths, this.getNestedKeys(object));
126✔
1804
                }
1805

1806
                DEBUG.output('Found %d keys for tabular output', keyPaths.length);
27✔
1807
                formattedData.push(keyPaths);
27✔
1808
                for (let object of objectArray) {
27✔
1809
                        let row = [];
126✔
1810
                        if (typeof object === 'object') {
126!
1811
                                for (let keyPath of keyPaths) {
126✔
1812
                                        let value = _.get(object, keyPath);
1,584✔
1813
                                        if (value === null || value === undefined) {
1,584✔
1814
                                                row.push('');
180✔
1815
                                        } else {
1816
                                                row.push(value);
1,404✔
1817
                                        }
1818
                                }
1819
                        } else {
1820
                                row.push(object);
×
1821
                        }
1822
                        DEBUG.output('Processed row with %d values', row.length);
126✔
1823
                        formattedData.push(row);
126✔
1824
                }
1825
                DEBUG.output(
27✔
1826
                        'Processed %d rows of tabular output',
1827
                        formattedData.length - 1
1828
                );
1829
                return formattedData;
27✔
1830
        }
1831

1832
        /**
1833
         * Extracts all keys from an object and flattens them
1834
         *
1835
         * @param {Object} object The object to extract flattened keys from
1836
         * @returns {string[]} The array of flattened keys
1837
         */
1838
        getNestedKeys(object) {
1839
                let keys = [];
405✔
1840
                if (typeof object === 'object') {
405!
1841
                        for (let key in object) {
405✔
1842
                                if (
1,683✔
1843
                                        typeof object[key] === 'object' &&
1,962✔
1844
                                        !Array.isArray(object[key])
1845
                                ) {
1846
                                        let subKeys = this.getNestedKeys(object[key]);
279✔
1847
                                        subKeys = subKeys.map((x) => `${key}.${x}`);
1,026✔
1848
                                        keys = [...keys, ...subKeys];
279✔
1849
                                } else {
1850
                                        keys.push(key);
1,404✔
1851
                                }
1852
                        }
1853
                }
1854
                return keys;
405✔
1855
        }
1856

1857
        /**
1858
         * Converts time interval shorthand like 5w, -3d, etc to timestamps. It also ensures any timestamp
1859
         * passed in is properly formatted for API calls.
1860
         *
1861
         * @param {string} time The command lint input string for the datetime
1862
         * @returns {string} The full RFC3339-formatted datetime string in UTC
1863
         */
1864
        static normalizeDateString(time) {
1865
                // Attempt to parse date as timestamp or string
1866
                let newDate = /^\d+$/u.test(time)
1,152✔
1867
                        ? dateTime.parse(Number.parseInt(time, 10) * 1000)
1868
                        : dateTime.parse(time);
1869
                if (!dateTime.isValid(newDate)) {
1,152✔
1870
                        let parsedOffset = time.match(/^(-?)((?:\d+[smhdwMy])+)$/u);
261✔
1871
                        if (parsedOffset) {
261✔
1872
                                let sign = parsedOffset[1] === '-' ? -1 : 1,
216✔
1873
                                        offset = parsedOffset[2];
216✔
1874

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

1881
                                // Successively apply the offsets to the current time
1882
                                newDate = new Date();
216✔
1883
                                for (const args of argPairs) {
216✔
1884
                                        newDate = offsetDate(newDate, ...args);
234✔
1885
                                }
1886
                        } else if (time === 'now') {
45!
1887
                                newDate = new Date();
45✔
1888
                        } else {
1889
                                throw new BoxCLIError(`Cannot parse date format "${time}"`);
×
1890
                        }
1891
                }
1892

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

1898
        /**
1899
         * Writes updated settings to disk
1900
         *
1901
         * @param {Object} updatedSettings The settings object to write
1902
         * @returns {void}
1903
         */
1904
        updateSettings(updatedSettings) {
1905
                this.settings = Object.assign(this.settings, updatedSettings);
×
1906
                try {
×
1907
                        fs.writeFileSync(
×
1908
                                SETTINGS_FILE_PATH,
1909
                                JSON.stringify(this.settings, null, 4),
1910
                                'utf8'
1911
                        );
1912
                } catch (error) {
1913
                        throw new BoxCLIError(
×
1914
                                `Could not write settings file ${SETTINGS_FILE_PATH}`,
1915
                                error
1916
                        );
1917
                }
1918
                return this.settings;
×
1919
        }
1920

1921
        /**
1922
         * Read the current set of environments from disk
1923
         *
1924
         * @returns {Object} The parsed environment information
1925
         */
1926
        async getEnvironments() {
1927
                if (this.supportsSecureStorage) {
24,228✔
1928
                        DEBUG.init(
16,152✔
1929
                                'Attempting secure storage read via %s service="%s" account="%s"',
1930
                                secureStorage.backend,
1931
                                ENVIRONMENTS_KEYCHAIN_SERVICE,
1932
                                ENVIRONMENTS_KEYCHAIN_ACCOUNT
1933
                        );
1934
                        try {
16,152✔
1935
                                const password = await secureStorage.getPassword(
16,152✔
1936
                                        ENVIRONMENTS_KEYCHAIN_SERVICE,
1937
                                        ENVIRONMENTS_KEYCHAIN_ACCOUNT
1938
                                );
1939
                                if (password) {
16,152✔
1940
                                        DEBUG.init(
16,146✔
1941
                                                'Successfully loaded environments from secure storage (%s)',
1942
                                                secureStorage.backend
1943
                                        );
1944
                                        return JSON.parse(password);
16,146✔
1945
                                }
1946
                                DEBUG.init(
6✔
1947
                                        'Secure storage returned empty result for service="%s" account="%s"',
1948
                                        ENVIRONMENTS_KEYCHAIN_SERVICE,
1949
                                        ENVIRONMENTS_KEYCHAIN_ACCOUNT
1950
                                );
1951
                        } catch (error) {
1952
                                DEBUG.init(
×
1953
                                        'Failed to read from secure storage (%s), falling back to file: %O',
1954
                                        secureStorage.backend,
1955
                                        getDebugErrorDetails(error)
1956
                                );
1957
                        }
1958
                } else {
1959
                        DEBUG.init(
8,076✔
1960
                                'Skipping secure storage read: platform=%s available=%s',
1961
                                process.platform,
1962
                                secureStorage.available
1963
                        );
1964
                }
1965

1966
                // Try to read from file (fallback or no secure storage)
1967
                try {
8,082✔
1968
                        if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
8,082✔
1969
                                DEBUG.init(
8,073✔
1970
                                        'Attempting environments fallback file read at %s',
1971
                                        ENVIRONMENTS_FILE_PATH
1972
                                );
1973
                                return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
8,073✔
1974
                        }
1975
                        DEBUG.init(
9✔
1976
                                'Environments fallback file does not exist at %s',
1977
                                ENVIRONMENTS_FILE_PATH
1978
                        );
1979
                } catch (error) {
1980
                        DEBUG.init(
×
1981
                                'Failed to read environments from file: %O',
1982
                                getDebugErrorDetails(error)
1983
                        );
1984
                }
1985

1986
                // No environments found in either location
1987
                throw new BoxCLIError(
9✔
1988
                        `Could not read environments. No environments found in secure storage or file ${ENVIRONMENTS_FILE_PATH}`
1989
                );
1990
        }
1991

1992
        /**
1993
         * Writes updated environment information to disk
1994
         *
1995
         * @param {Object} updatedEnvironments The environment information to write
1996
         * @param {Object} environments use to override current environment
1997
         * @returns {void}
1998
         */
1999
        async updateEnvironments(updatedEnvironments, environments) {
2000
                if (environments === undefined) {
8,073!
2001
                        environments = await this.getEnvironments();
×
2002
                }
2003
                Object.assign(environments, updatedEnvironments);
8,073✔
2004

2005
                let storedInSecureStorage = false;
8,073✔
2006

2007
                if (this.supportsSecureStorage) {
8,073✔
2008
                        DEBUG.init(
5,382✔
2009
                                'Attempting secure storage write via %s service="%s" account="%s"',
2010
                                secureStorage.backend,
2011
                                ENVIRONMENTS_KEYCHAIN_SERVICE,
2012
                                ENVIRONMENTS_KEYCHAIN_ACCOUNT
2013
                        );
2014
                        try {
5,382✔
2015
                                await secureStorage.setPassword(
5,382✔
2016
                                        ENVIRONMENTS_KEYCHAIN_SERVICE,
2017
                                        ENVIRONMENTS_KEYCHAIN_ACCOUNT,
2018
                                        JSON.stringify(environments)
2019
                                );
2020
                                storedInSecureStorage = true;
5,382✔
2021
                                DEBUG.init(
5,382✔
2022
                                        'Stored environment configuration in secure storage (%s)',
2023
                                        secureStorage.backend
2024
                                );
2025
                                if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
5,382!
2026
                                        fs.unlinkSync(ENVIRONMENTS_FILE_PATH);
×
2027
                                        DEBUG.init(
×
2028
                                                'Removed environment configuration file after migrating to secure storage'
2029
                                        );
2030
                                }
2031
                        } catch (error) {
2032
                                DEBUG.init(
×
2033
                                        'Could not store credentials in secure storage (%s), falling back to file: %O',
2034
                                        secureStorage.backend,
2035
                                        getDebugErrorDetails(error)
2036
                                );
2037
                        }
2038
                } else {
2039
                        DEBUG.init(
2,691✔
2040
                                'Skipping secure storage write: platform=%s available=%s',
2041
                                process.platform,
2042
                                secureStorage.available
2043
                        );
2044
                }
2045

2046
                // Write to file if secure storage failed or not available
2047
                if (!storedInSecureStorage) {
8,073✔
2048
                        try {
2,691✔
2049
                                let fileContents = JSON.stringify(environments, null, 4);
2,691✔
2050
                                fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');
2,691✔
2051

2052
                                if (
2,691!
2053
                                        process.platform === 'linux' &&
5,382✔
2054
                                        this.supportsSecureStorage
2055
                                ) {
UNCOV
2056
                                        this.info(
×
2057
                                                'Could not store credentials in secure storage, falling back to file.' +
2058
                                                        ' To enable secure storage on Linux, install libsecret-1-dev package.'
2059
                                        );
2060
                                }
2061
                        } catch (error) {
2062
                                throw new BoxCLIError(
×
2063
                                        `Could not write environments config file ${ENVIRONMENTS_FILE_PATH}`,
2064
                                        error
2065
                                );
2066
                        }
2067
                }
2068

2069
                return environments;
8,073✔
2070
        }
2071

2072
        /**
2073
         * Initialize the CLI by creating the necessary configuration files on disk
2074
         * in the users' home directory, then read and parse the CLI settings file.
2075
         *
2076
         * @returns {Object} The parsed settings
2077
         * @private
2078
         */
2079
        async _loadSettings() {
2080
                try {
8,073✔
2081
                        if (!fs.existsSync(CONFIG_FOLDER_PATH)) {
8,073✔
2082
                                mkdirp.sync(CONFIG_FOLDER_PATH);
9✔
2083
                                DEBUG.init('Created config folder at %s', CONFIG_FOLDER_PATH);
9✔
2084
                        }
2085

2086
                        // Check if environments exist (in secure storage or file)
2087
                        let environmentsExist = false;
8,073✔
2088
                        try {
8,073✔
2089
                                const environments = await this.getEnvironments();
8,073✔
2090
                                // Check if there are any environments configured
2091
                                if (
8,064!
2092
                                        environments &&
24,192✔
2093
                                        environments.environments &&
2094
                                        Object.keys(environments.environments).length > 0
2095
                                ) {
2096
                                        environmentsExist = true;
×
2097
                                        DEBUG.init('Found existing environments in storage');
×
2098
                                }
2099
                        } catch (error) {
2100
                                // No environments found, need to create defaults
2101
                                DEBUG.init('No existing environments found: %s', error.message);
9✔
2102
                        }
2103

2104
                        if (!environmentsExist) {
8,073!
2105
                                // Create default environments (will be stored in secure storage if available)
2106
                                await this.updateEnvironments(
8,073✔
2107
                                        {},
2108
                                        this._getDefaultEnvironments()
2109
                                );
2110
                                DEBUG.init('Created default environments configuration');
8,073✔
2111
                        }
2112

2113
                        if (!fs.existsSync(SETTINGS_FILE_PATH)) {
8,073✔
2114
                                let settingsJSON = JSON.stringify(
9✔
2115
                                        this._getDefaultSettings(),
2116
                                        null,
2117
                                        4
2118
                                );
2119
                                fs.writeFileSync(SETTINGS_FILE_PATH, settingsJSON, 'utf8');
9✔
2120
                                DEBUG.init(
9✔
2121
                                        'Created settings file at %s %O',
2122
                                        SETTINGS_FILE_PATH,
2123
                                        settingsJSON
2124
                                );
2125
                        }
2126
                } catch (error) {
2127
                        throw new BoxCLIError(
×
2128
                                'Could not initialize CLI home directory',
2129
                                error
2130
                        );
2131
                }
2132

2133
                let settings;
2134
                try {
8,073✔
2135
                        settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
8,073✔
2136
                        settings = Object.assign(this._getDefaultSettings(), settings);
8,073✔
2137
                        DEBUG.init('Loaded settings %O', settings);
8,073✔
2138
                } catch (error) {
2139
                        throw new BoxCLIError(
×
2140
                                `Could not read CLI settings file at ${SETTINGS_FILE_PATH}`,
2141
                                error
2142
                        );
2143
                }
2144

2145
                try {
8,073✔
2146
                        if (!fs.existsSync(settings.boxReportsFolderPath)) {
8,073✔
2147
                                mkdirp.sync(settings.boxReportsFolderPath);
9✔
2148
                                DEBUG.init(
9✔
2149
                                        'Created reports folder at %s',
2150
                                        settings.boxReportsFolderPath
2151
                                );
2152
                        }
2153
                        if (!fs.existsSync(settings.boxDownloadsFolderPath)) {
8,073✔
2154
                                mkdirp.sync(settings.boxDownloadsFolderPath);
9✔
2155
                                DEBUG.init(
9✔
2156
                                        'Created downloads folder at %s',
2157
                                        settings.boxDownloadsFolderPath
2158
                                );
2159
                        }
2160
                } catch (error) {
2161
                        throw new BoxCLIError(
×
2162
                                'Failed creating CLI working directory',
2163
                                error
2164
                        );
2165
                }
2166

2167
                return settings;
8,073✔
2168
        }
2169

2170
        /**
2171
         * Get the default settings object
2172
         *
2173
         * @returns {Object} The default settings object
2174
         * @private
2175
         */
2176
        _getDefaultSettings() {
2177
                return {
8,082✔
2178
                        boxReportsFolderPath: path.join(
2179
                                os.homedir(),
2180
                                'Documents/Box-Reports'
2181
                        ),
2182
                        boxReportsFileFormat: 'txt',
2183
                        boxDownloadsFolderPath: path.join(
2184
                                os.homedir(),
2185
                                'Downloads/Box-Downloads'
2186
                        ),
2187
                        outputJson: false,
2188
                        enableProxy: false,
2189
                        proxy: {
2190
                                url: null,
2191
                                username: null,
2192
                                password: null,
2193
                        },
2194
                        enableAnalyticsClient: false,
2195
                        analyticsClient: {
2196
                                name: null,
2197
                        },
2198
                };
2199
        }
2200

2201
        /**
2202
         * Get the default environments object
2203
         *
2204
         * @returns {Object} The default environments object
2205
         * @private
2206
         */
2207
        _getDefaultEnvironments() {
2208
                return {
8,073✔
2209
                        default: null,
2210
                        environments: {},
2211
                };
2212
        }
2213
}
2214

2215
BoxCommand.flags = {
9✔
2216
        token: Flags.string({
2217
                char: 't',
2218
                description: 'Provide a token to perform this call',
2219
        }),
2220
        'as-user': Flags.string({ description: 'Provide an ID for a user' }),
2221
        // @NOTE: This flag is not read anywhere directly; the chalk library automatically turns off color when it's passed
2222
        'no-color': Flags.boolean({
2223
                description: 'Turn off colors for logging',
2224
        }),
2225
        json: Flags.boolean({
2226
                description: 'Output formatted JSON',
2227
                exclusive: ['csv'],
2228
        }),
2229
        csv: Flags.boolean({
2230
                description: 'Output formatted CSV',
2231
                exclusive: ['json'],
2232
        }),
2233
        save: Flags.boolean({
2234
                char: 's',
2235
                description: 'Save report to default reports folder on disk',
2236
                exclusive: ['save-to-file-path'],
2237
        }),
2238
        'save-to-file-path': Flags.string({
2239
                description: 'Override default file path to save report',
2240
                exclusive: ['save'],
2241
                parse: utils.parsePath,
2242
        }),
2243
        fields: Flags.string({
2244
                description: 'Comma separated list of fields to show',
2245
        }),
2246
        'bulk-file-path': Flags.string({
2247
                description: 'File path to bulk .csv or .json objects',
2248
                parse: utils.parsePath,
2249
        }),
2250
        help: Flags.help({
2251
                char: 'h',
2252
                description: 'Show CLI help',
2253
        }),
2254
        verbose: Flags.boolean({
2255
                char: 'v',
2256
                description: 'Show verbose output, which can be helpful for debugging',
2257
        }),
2258
        yes: Flags.boolean({
2259
                char: 'y',
2260
                description: 'Automatically respond yes to all confirmation prompts',
2261
        }),
2262
        quiet: Flags.boolean({
2263
                char: 'q',
2264
                description: 'Suppress any non-error output to stderr',
2265
        }),
2266
};
2267

2268
BoxCommand.rawJsonFlags = Object.freeze({
9✔
2269
        'raw-json': Flags.boolean({
2270
                description:
2271
                        'Output the raw API JSON response instead of the tsClient-normalized object fields. Added as a non-breaking compatibility flag for users who need JSON field names to match the API schema exactly. Implies --json.',
2272
                exclusive: ['csv'],
2273
        }),
2274
});
2275

2276
BoxCommand.minFlags = _.pick(BoxCommand.flags, [
9✔
2277
        'no-color',
2278
        'help',
2279
        'verbose',
2280
        'quiet',
2281
]);
2282

2283
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