Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .mocharc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
recursive: true
timeout: '10000'
reporter: 'spec'
reporter: 'spec'
require: 'test/helpers/setup.js'
125 changes: 116 additions & 9 deletions src/box-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const inquirer = require('./inquirer');
const { stringifyStream } = require('@discoveryjs/json-ext');
const progress = require('cli-progress');
const secureStorage = require('./secure-storage');
const { ProgramStatus } = require('./program-status');

const DEBUG = require('./debug');
const stream = require('node:stream');
Expand Down Expand Up @@ -323,6 +324,11 @@ class BoxCommand extends Command {
this.flags = flags;
this.args = args;
this.settings = await this._loadSettings();
this._programStatus = new ProgramStatus({
settings: this.settings,
label: this.id ? `box ${this.id}` : 'box',
});
this.programStatus.start();
this.client = await this.getClient();
this.tsClient = await this.getTsClient();

Expand Down Expand Up @@ -369,6 +375,9 @@ class BoxCommand extends Command {
stopOnComplete: true,
});
progressBar.start(bulkCalls.length, 0);
const bulkStatusMessage = (count) =>
`Bulk ${this.id}: ${count}/${bulkCalls.length} entries`;
this.programStatus.working(bulkStatusMessage(0), 0);

for (let bulkData of bulkCalls) {
this.argv = [];
Expand Down Expand Up @@ -396,6 +405,19 @@ class BoxCommand extends Command {
}

progressBar.update(bulkEntryIndex);
this.programStatus.working(
bulkStatusMessage(bulkEntryIndex),
(bulkEntryIndex / bulkCalls.length) * 100
);
}
if (this.bulkErrors.length > 0) {
this.programStatus.fail(
`Bulk ${this.id}: ${this.bulkErrors.length} of ${bulkCalls.length} entries failed`
);
} else {
this.programStatus.succeed(
`Bulk ${this.id}: processed ${bulkCalls.length} entries`
);
}
this.isBulk = false;
DEBUG.execute('Leaving bulk mode and writing final output');
Expand Down Expand Up @@ -1586,18 +1608,93 @@ class BoxCommand extends Command {
return true;
}

let answers = await inquirer.prompt([
{
name: 'confirmation',
message: promptText,
type: 'confirm',
default: defaultValue,
},
]);
let answers = await this.prompt(
[
{
name: 'confirmation',
message: promptText,
type: 'confirm',
default: defaultValue,
},
],
{ kind: 'permission' }
);

if (!answers.confirmation && !this.isBulk) {

@mwwoda mwwoda Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output() calls confirm() at the very end, after the result has already been set. In bulk mode, isBulk has also been reset to false by then, so the bulk guard here doesn't apply. cancel() then overwrites whatever outcome was stored.

I could repro it with the following

printf 'fileID\n111\n222\n333\n' > bulk.csv; echo '{}' > out.json
BOX_PROGRAM_STATUS=1 ./bin/run files:get --bulk-file-path bulk.csv --token fake \
    --save-to-file-path out.json 2>status.log

Then answering NO to a prompt

Decoded reports:

  working   100  Bulk files:get: 3/3 entries
  blocked   permission  File .../out.json already exists — overwrite?
  working   100  Bulk files:get: 3/3 entries
  idle      Cancelled box files:get

Answering "Yes" gives

error  Bulk files:get: 3 of 3 entries failed

Which is what I'd expect in both cases, no matter the prompt answer. I think we may consider cancel to keep the outcome already set

this.programStatus.cancel();
}
return answers.confirmation;
}

/**
* Prompt the user, reporting to the terminal that the command is waiting on them
*
* @param {Object[]} questions Inquirer questions
* @param {Object} [options] Status options
* @param {string} [options.kind] permission, question, or auth
* @param {string} [options.msg] What the command is waiting for; defaults to the first question's message
* @returns {Promise<Object>} The answers
*/
async prompt(questions, { kind = 'question', msg } = {}) {
this.programStatus.blocked(kind, msg ?? questions[0]?.message);
try {
return await inquirer.prompt(questions);
} finally {
this.programStatus.resume();
}
}

/**
* Status reporting for the current command run
*
* @returns {ProgramStatus} The status reporter; reports nothing before init()
*/
get programStatus() {
if (!this._programStatus) {
this._programStatus = new ProgramStatus({ enabled: false });
}
return this._programStatus;
}

/**
* Report what the command is working on. Ignored in bulk mode, which reports
* progress across all entries instead.
*
* @param {string} msg What the command is doing
* @param {number} [progress] Percentage complete; indeterminate if omitted
* @returns {void}
*/
reportStatus(msg, progress) {
if (!this.isBulk) {
this.programStatus.working(msg, progress);
}
}

/**
* Set the message reported when the command finishes. Ignored in bulk mode.
*
* @param {string} msg What the command finished
* @returns {void}
*/
setStatusResult(msg) {
if (!this.isBulk) {
this.programStatus.succeed(msg);
}
}

/**
* Mark the command as failed when it reports a failure without throwing.
* Ignored in bulk mode.
*
* @param {string} msg What failed
* @returns {void}
*/
setStatusFailed(msg) {
if (!this.isBulk) {
this.programStatus.fail(msg);
}
}

/**
* Writes output to stderr — this should be used for informational output. For example, a message
* stating that an item has been deleted.
Expand Down Expand Up @@ -1705,6 +1802,15 @@ class BoxCommand extends Command {
DEBUG.execute('Error parsing BoxSdkError message: %s', error);
}
}
if (err?.code !== 'EEXIT') {
const statusMessage =
typeof err?.message === 'string'
? this.wrapError(err).message
: String(err);
this.programStatus.fail(
`${this.programStatus.label} failed: ${statusMessage}`
);
}
try {
// Let the oclif default handler run first, since it handles the help and version flags there
/* eslint-disable promise/no-promise-in-callback */
Expand Down Expand Up @@ -1755,8 +1861,9 @@ class BoxCommand extends Command {
* @param {Error} [err] An error, if one occurred
* @returns {void}
*/
async finally(/* err */) {
async finally(err) {
// called after run and catch regardless of whether or not the command errored
this.programStatus.finish(err);
}

/**
Expand Down
20 changes: 11 additions & 9 deletions src/commands/configure/environments/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const { Args } = require('@oclif/core');
const BoxCommand = require('../../../box-command');
const BoxCLIError = require('../../../cli-error');
const inquirer = require('../../../inquirer');

class EnvironmentsDeleteCommand extends BoxCommand {
async run() {
Expand All @@ -16,14 +15,17 @@ class EnvironmentsDeleteCommand extends BoxCommand {
if (choices.length === 0) {
throw new BoxCLIError('No environments to delete');
}
let answers = await inquirer.prompt([
{
type: 'list',
name: 'environment',
message: 'Which environment?',
choices: Object.keys(environmentsObject.environments),
},
]);
let answers = await this.prompt(
[
{
type: 'list',
name: 'environment',
message: 'Which environment?',
choices: Object.keys(environmentsObject.environments),
},
],
{ msg: 'Choose the Box environment to delete' }
);
name = answers.environment;
}

Expand Down
20 changes: 11 additions & 9 deletions src/commands/configure/environments/set-current.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const { Args } = require('@oclif/core');
const BoxCommand = require('../../../box-command');
const inquirer = require('../../../inquirer');

class EnvironmentsSetCurrentCommand extends BoxCommand {
async run() {
Expand All @@ -11,14 +10,17 @@ class EnvironmentsSetCurrentCommand extends BoxCommand {
let name = args.id;

if (!name) {
let answers = await inquirer.prompt([
{
type: 'list',
name: 'environment',
message: 'Which environment?',
choices: Object.keys(environmentsObject.environments),
},
]);
let answers = await this.prompt(
[
{
type: 'list',
name: 'environment',
message: 'Which environment?',
choices: Object.keys(environmentsObject.environments),
},
],
{ msg: 'Choose the Box environment to use' }
);
name = answers.environment;
}

Expand Down
8 changes: 8 additions & 0 deletions src/commands/files/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ class FilesDownloadCommand extends BoxCommand {
progressBar.start(file.size, 0, { speed: 'N/A' });

let downloadedByte = 0;
const reportDownloadStatus = () =>
this.reportStatus(
`Downloading ${fileName}`,
file.size > 0 ? (downloadedByte / file.size) * 100 : undefined
);
reportDownloadStatus();
stream.on('data', (chunk) => {
downloadedByte += chunk.length;
});
Expand All @@ -79,6 +85,7 @@ class FilesDownloadCommand extends BoxCommand {
downloadedByte / (Date.now() - startTime) / 1000
),
});
reportDownloadStatus();
}, 1000);

// We need to await the end of the stream to avoid a race condition here
Expand All @@ -89,6 +96,7 @@ class FilesDownloadCommand extends BoxCommand {
clearInterval(intervalUpdate);
progressBar.update(downloadedByte);
this.info(`Downloaded file ${fileName}`);
this.setStatusResult(`Downloaded ${fileName}`);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/commands/files/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class FilesUploadCommand extends BoxCommand {
fileAttributes.content_modified_at = flags['content-modified-at'];
}

const statusMessage = `Uploading ${name}`;
const onProgress = (percentage) =>
this.reportStatus(statusMessage, percentage);
this.reportStatus(statusMessage);

let file;
try {
file = await uploadFile(this.client, {
Expand All @@ -42,6 +47,7 @@ class FilesUploadCommand extends BoxCommand {
stream,
size,
fileAttributes,
onProgress,
});
} catch (error) {
const { statusCode, response } = error;
Expand Down Expand Up @@ -88,9 +94,11 @@ class FilesUploadCommand extends BoxCommand {
stream: versionStream,
size,
fileAttributes,
onProgress,
});
}

this.setStatusResult(`Uploaded ${name}`);
await this.output(file.entries[0]);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/commands/files/versions/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ class FilesVersionsDownloadCommand extends BoxCommand {
}

// @TODO(2018-09-18): Add progress bar for large downloads
this.reportStatus(`Downloading ${fileName}`);

// We need to await the end of the stream to avoid a race condition here
await new Promise((resolve, reject) => {
output.on('close', resolve);
stream.on('error', reject);
});
this.info(`Downloaded file ${fileName}`);
this.setStatusResult(`Downloaded ${fileName}`);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/commands/files/versions/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ class FilesUploadVersionsCommand extends BoxCommand {
fileAttributes.name = flags.name;
}

const statusMessage = `Uploading a new version of file ${args.fileID}`;
this.reportStatus(statusMessage);
const file = await uploadNewFileVersion(this.client, {
fileID: args.fileID,
stream,
size,
fileAttributes,
onProgress: (percentage) =>
this.reportStatus(statusMessage, percentage),
});
this.setStatusResult(`Uploaded a new version of file ${args.fileID}`);
await this.output(file.entries[0]);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/commands/files/zip.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class FilesZipCommand extends BoxCommand {

let downloadStatus;
let outputFinished = false;
this.reportStatus(`Downloading zip ${fileName}`);

await new Promise((resolve, reject) => {
/* eslint-disable promise/always-return */
Expand All @@ -69,6 +70,7 @@ class FilesZipCommand extends BoxCommand {
});
output.on('error', reject);
});
this.setStatusResult(`Downloaded zip ${fileName}`);
await this.output(downloadStatus);
}
}
Expand Down
Loading
Loading