From 98a2c0635db750e86c6f7d980b2ee3a04677a560 Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Fri, 1 May 2020 14:38:53 +0200 Subject: [PATCH] Improve presentation of errors, help Change-type: patch Resolves: #1779 #1757 Signed-off-by: Scott Lowe --- lib/actions/help.js | 9 ++++- lib/errors.ts | 77 ++++++++++++++++++++++++------------- lib/utils/messages.ts | 15 +++----- tests/commands/help.spec.ts | 40 +++++++++---------- 4 files changed, 84 insertions(+), 57 deletions(-) diff --git a/lib/actions/help.js b/lib/actions/help.js index c8358bc7..9b18bace 100644 --- a/lib/actions/help.js +++ b/lib/actions/help.js @@ -73,8 +73,8 @@ const manuallySortedPrimaryCommands = [ const general = function(_params, options, done) { console.log('Usage: balena [COMMAND] [OPTIONS]\n'); - console.log(messages.reachingOut); - console.log('\nPrimary commands:\n'); + + console.log('Primary commands:\n'); // We do not want the wildcard command // to be printed in the help screen. @@ -123,6 +123,9 @@ const general = function(_params, options, done) { console.log('\nGlobal Options:\n'); print(parse(capitano.state.globalOptions).sort()); } + console.log(indent('--debug\n')); + + console.log(messages.help); return done(); }) @@ -152,6 +155,8 @@ const commandHelp = (params, _options, done) => print(parse(command.options).sort()); } + console.log(); + return done(); }); diff --git a/lib/errors.ts b/lib/errors.ts index c35dedff..72e0677e 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -28,7 +28,7 @@ export class NotLoggedInError extends ExpectedError {} export class InsufficientPrivilegesError extends ExpectedError {} /** - * instanceOf is a more reliable implemention of the plain `instanceof` + * instanceOf is a more reliable implementation of the plain `instanceof` * typescript operator, for use with TypedError errors when the error * classes may be defined in external packages/dependencies. * Sample usage: @@ -126,56 +126,81 @@ const messages: { `, BalenaExpiredToken: () => stripIndent` - Looks like your session token is expired. + Looks like your session token has expired. Please try logging in again with: $ balena login`, }; +const EXPECTED_ERROR_REGEXES = [ + /^BalenaApplicationNotFound:/, // balena-sdk + /^BalenaDeviceNotFound:/, // balena-sdk + /^Missing \w+$/, // Capitano, oclif parser: RequiredArgsError, RequiredFlagError + /^Unexpected arguments?:/, // oclif parser: UnexpectedArgsError + /to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError +]; + export async function handleError(error: any) { + // Set appropriate exitCode process.exitCode = error.exitCode === 0 ? 0 : parseInt(error.exitCode, 10) || process.exitCode || 1; + // Handle non-Error objects (probably strings) if (!(error instanceof Error)) { printErrorMessage(String(error)); return; } + // Prepare message const message = [interpret(error)]; - if (process.env.DEBUG && error.stack) { - message.push('\n' + error.stack); + if (error.stack) { + if (process.env.DEBUG) { + message.push('\n' + error.stack); + } else { + // Include first line of stacktrace + message.push('\n' + error.stack.split(`\n`)[0]); + } } - printErrorMessage(message.join('\n')); - const expectedErrorREs = [ - /^BalenaApplicationNotFound:/, // balena-sdk - /^BalenaDeviceNotFound:/, // balena-sdk - /^Missing \w+$/, // Capitano's command line parsing error - /^Unexpected arguments?:/, // oclif's command line parsing error - ]; - - if ( + // Expected? + const isExpectedError = error instanceof ExpectedError || - expectedErrorREs.some(re => re.test(message[0])) - ) { - return; - } + EXPECTED_ERROR_REGEXES.some(re => re.test(message[0])); - // Report "unexpected" errors via Sentry.io - const Sentry = await import('@sentry/node'); - Sentry.captureException(error); - await Sentry.close(1000); - // Unhandled/unexpected error: ensure that the process terminates. - // The exit error code was set above through `process.exitCode`. - process.exit(); + // Output/report error + if (isExpectedError) { + printExpectedErrorMessage(message.join('\n')); + } else { + printErrorMessage(message.join('\n')); + + // Report "unexpected" errors via Sentry.io + const Sentry = await import('@sentry/node'); + Sentry.captureException(error); + await Sentry.close(1000); + // Unhandled/unexpected error: ensure that the process terminates. + // The exit error code was set above through `process.exitCode`. + process.exit(); + } } export function printErrorMessage(message: string) { const chalk = getChalk(); - console.error(chalk.red(message)); - console.error(chalk.red(`\n${getHelp}\n`)); + + // Only first line should be red + const messageLines = message.split('\n'); + console.error(chalk.red(messageLines.shift())); + + messageLines.forEach(line => { + console.error(line); + }); + + console.error(`\n${getHelp}\n`); +} + +export function printExpectedErrorMessage(message: string) { + console.error(`${message}\n`); } /** diff --git a/lib/utils/messages.ts b/lib/utils/messages.ts index a22939e5..ae909026 100644 --- a/lib/utils/messages.ts +++ b/lib/utils/messages.ts @@ -9,19 +9,16 @@ create a new one at: https://github.com/balena-io/balena-cli/issues/\ `; const debugHint = `\ -Additional information may be available by setting a DEBUG=1 environment -variable: "set DEBUG=1" on a Windows command prompt, "$env:DEBUG = 1" on -powershell, or "export DEBUG=1" on Linux or macOS.\n +Additional information may be available with the \`--debug\` flag. `; -export const getHelp = `${DEBUG_MODE ? '' : debugHint}\ -If you need help, don't hesitate in contacting our support forums at -https://forums.balena.io - -For CLI bug reports or feature requests, have a look at the GitHub issues or -create a new one at: https://github.com/balena-io/balena-cli/issues/\ +export const help = `\ +For help, visit our support forums: https://forums.balena.io +For bug reports or feature requests, see: https://github.com/balena-io/balena-cli/issues/ `; +export const getHelp = (DEBUG_MODE ? '' : debugHint) + help; + export const balenaAsciiArt = `\ _ _ | |__ __ _ | | ____ _ __ __ _ diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index b89aa063..f232aa94 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -4,12 +4,6 @@ import { cleanOutput, runCommand } from '../helpers'; const SIMPLE_HELP = ` Usage: balena [COMMAND] [OPTIONS] -If you need help, or just want to say hi, don't hesitate in reaching out -through our discussion and support forums at https://forums.balena.io - -For bug reports or feature requests, have a look at the GitHub issues or -create a new one at: https://github.com/balena-io/balena-cli/issues/ - Primary commands: help [command...] show help @@ -84,33 +78,33 @@ Additional commands: `; +const LIST_ADDITIONAL = ` +Run \`balena help --verbose\` to list additional commands +`; + const GLOBAL_OPTIONS = ` Global Options: --help, -h --version, -v + --debug +`; + +const ONLINE_RESOURCES = ` + For help, visit our support forums: https://forums.balena.io + For bug reports or feature requests, see: https://github.com/balena-io/balena-cli/issues/ `; describe('balena help', function() { it('should list primary command summaries', async () => { const { out, err } = await runCommand('help'); - console.log('ONE'); - console.log(cleanOutput(out)); - console.log( - cleanOutput([ - SIMPLE_HELP, - 'Run `balena help --verbose` to list additional commands', - GLOBAL_OPTIONS, - ]), - ); - console.log(); - expect(cleanOutput(out)).to.deep.equal( cleanOutput([ SIMPLE_HELP, - 'Run `balena help --verbose` to list additional commands', + LIST_ADDITIONAL, GLOBAL_OPTIONS, + ONLINE_RESOURCES, ]), ); @@ -121,7 +115,12 @@ describe('balena help', function() { const { out, err } = await runCommand('help -v'); expect(cleanOutput(out)).to.deep.equal( - cleanOutput([SIMPLE_HELP, ADDITIONAL_HELP, GLOBAL_OPTIONS]), + cleanOutput([ + SIMPLE_HELP, + ADDITIONAL_HELP, + GLOBAL_OPTIONS, + ONLINE_RESOURCES, + ]), ); expect(err.join('')).to.equal(''); @@ -135,8 +134,9 @@ describe('balena help', function() { expect(cleanOutput(out)).to.deep.equal( cleanOutput([ SIMPLE_HELP, - 'Run `balena help --verbose` to list additional commands', + LIST_ADDITIONAL, GLOBAL_OPTIONS, + ONLINE_RESOURCES, ]), );