Skip to content

Commit

Permalink
feat: show truncated help on some errors (#1004)
Browse files Browse the repository at this point in the history
* feat: show truncated help on some errors

* fix: remove circular deps
  • Loading branch information
mdonnalley authored Mar 12, 2024
1 parent 6894fb9 commit 7133a97
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 61 deletions.
3 changes: 3 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {readFileSync} from 'node:fs'
import {join} from 'node:path'

import {Config} from './config/config'
import {PJSON, Plugin} from './interfaces'

type OclifCoreInfo = {name: string; version: string}

type CacheContents = {
rootPlugin: Plugin
config: Config
exitCodes: PJSON.Plugin['oclif']['exitCodes']
'@oclif/core': OclifCoreInfo
}
Expand All @@ -31,6 +33,7 @@ export default class Cache extends Map<keyof CacheContents, ValueOf<CacheContent
return Cache.instance
}

public get(key: 'config'): Config | undefined
public get(key: '@oclif/core'): OclifCoreInfo
public get(key: 'rootPlugin'): Plugin | undefined
public get(key: 'exitCodes'): PJSON.Plugin['oclif']['exitCodes'] | undefined
Expand Down
2 changes: 1 addition & 1 deletion src/config/ts-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {join, relative as pathRelative, sep} from 'node:path'
import * as TSNode from 'ts-node'

import Cache from '../cache'
import {memoizedWarn} from '../errors'
import {memoizedWarn} from '../errors/warn'
import {Plugin, TSConfig} from '../interfaces'
import {settings} from '../settings'
import {existsSync} from '../util/fs'
Expand Down
29 changes: 29 additions & 0 deletions src/errors/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import write from '../cli-ux/write'
import {OclifError, PrettyPrintableError} from '../interfaces'
import {config} from './config'
import {CLIError, addOclifExitCode} from './errors/cli'
import prettyPrint, {applyPrettyPrintOptions} from './errors/pretty-print'

export function error(input: Error | string, options: {exit: false} & PrettyPrintableError): void
export function error(input: Error | string, options?: {exit?: number} & PrettyPrintableError): never
export function error(input: Error | string, options: {exit?: false | number} & PrettyPrintableError = {}): void {
let err: Error & OclifError

if (typeof input === 'string') {
err = new CLIError(input, options)
} else if (input instanceof Error) {
err = addOclifExitCode(input, options) as Error & OclifError
} else {
throw new TypeError('first argument must be a string or instance of Error')
}

err = applyPrettyPrintOptions(err, options) as Error & OclifError & PrettyPrintableError

if (options.exit === false) {
const message = prettyPrint(err)
if (message) write.stderr(message + '\n')
if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
} else throw err
}

export default error
2 changes: 1 addition & 1 deletion src/errors/errors/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export function addOclifExitCode(error: Record<string, any>, options?: {exit?: f

export class CLIError extends Error implements OclifError {
code?: string

oclif: OclifError['oclif'] = {}
skipOclifErrorHandling?: boolean
suggestions?: string[]

constructor(error: Error | string, options: {exit?: false | number} & PrettyPrintableError = {}) {
Expand Down
20 changes: 19 additions & 1 deletion src/errors/handle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import clean from 'clean-stack'

import Cache from '../cache'
import {Help} from '../help/index'
import {OclifError, PrettyPrintableError} from '../interfaces'
import {CLIParseError} from '../parser/errors'
import {config} from './config'
import {CLIError} from './errors/cli'
import {ExitError} from './errors/exit'
Expand All @@ -18,7 +21,11 @@ export const Exit = {
},
}

type ErrorToHandle = Error & Partial<PrettyPrintableError> & Partial<OclifError> & {skipOclifErrorHandling?: boolean}
type ErrorToHandle = Error &
Partial<PrettyPrintableError> &
Partial<OclifError> &
Partial<CLIError> &
Partial<CLIParseError>

export async function handle(err: ErrorToHandle): Promise<void> {
try {
Expand All @@ -31,6 +38,17 @@ export async function handle(err: ErrorToHandle): Promise<void> {

if (shouldPrint) {
console.error(pretty ?? stack)
const config = Cache.getInstance().get('config')
if (err.showHelp && err.parse?.input?.argv && config) {
const options = {
...(config.pjson.oclif.helpOptions ?? config.pjson.helpOptions),
sections: ['flags', 'usage', 'arguments'],
sendToStderr: true,
}
const help = new Help(config, options)
console.error()
await help.showHelp(process.argv.slice(2))
}
}

const exitCode = err.oclif?.exit ?? 1
Expand Down
55 changes: 3 additions & 52 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,15 @@
import write from '../cli-ux/write'
import {OclifError, PrettyPrintableError} from '../interfaces'
import {config} from './config'
import {CLIError, addOclifExitCode} from './errors/cli'
import {ExitError} from './errors/exit'
import prettyPrint, {applyPrettyPrintOptions} from './errors/pretty-print'

export {PrettyPrintableError} from '../interfaces'
export {config} from './config'
export {error} from './error'
export {CLIError} from './errors/cli'
export {ExitError} from './errors/exit'
export {ModuleLoadError} from './errors/module-load'
export {handle} from './handle'

export {Logger} from './logger'
export function exit(code = 0): never {
throw new ExitError(code)
}

export function error(input: Error | string, options: {exit: false} & PrettyPrintableError): void
export function error(input: Error | string, options?: {exit?: number} & PrettyPrintableError): never
export function error(input: Error | string, options: {exit?: false | number} & PrettyPrintableError = {}): void {
let err: Error & OclifError

if (typeof input === 'string') {
err = new CLIError(input, options)
} else if (input instanceof Error) {
err = addOclifExitCode(input, options) as Error & OclifError
} else {
throw new TypeError('first argument must be a string or instance of Error')
}

err = applyPrettyPrintOptions(err, options) as Error & OclifError & PrettyPrintableError

if (options.exit === false) {
const message = prettyPrint(err)
if (message) write.stderr(message + '\n')
if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
} else throw err
}

export function warn(input: Error | string): void {
let err: Error & OclifError

if (typeof input === 'string') {
err = new CLIError.Warn(input)
} else if (input instanceof Error) {
err = addOclifExitCode(input) as Error & OclifError
} else {
throw new TypeError('first argument must be a string or instance of Error')
}

const message = prettyPrint(err)
if (message) write.stderr(message + '\n')
if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
}

const WARNINGS = new Set<Error | string>()
export function memoizedWarn(input: Error | string): void {
if (!WARNINGS.has(input)) warn(input)

WARNINGS.add(input)
}

export {Logger} from './logger'
export {warn} from './warn'
30 changes: 30 additions & 0 deletions src/errors/warn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import write from '../cli-ux/write'
import {OclifError} from '../interfaces'
import {config} from './config'
import {CLIError, addOclifExitCode} from './errors/cli'
import prettyPrint from './errors/pretty-print'

export function warn(input: Error | string): void {
let err: Error & OclifError

if (typeof input === 'string') {
err = new CLIError.Warn(input)
} else if (input instanceof Error) {
err = addOclifExitCode(input) as Error & OclifError
} else {
throw new TypeError('first argument must be a string or instance of Error')
}

const message = prettyPrint(err)
if (message) write.stderr(message + '\n')
if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
}

const WARNINGS = new Set<Error | string>()
export function memoizedWarn(input: Error | string): void {
if (!WARNINGS.has(input)) warn(input)

WARNINGS.add(input)
}

export default warn
6 changes: 5 additions & 1 deletion src/help/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export class CommandHelp extends HelpFormatter {
}

protected sections(): Array<{generate: HelpSectionRenderer; header: string}> {
return [
const sections: Array<{generate: HelpSectionRenderer; header: string}> = [
{
generate: () => this.usage(),
header: this.opts.usageHeader || 'USAGE',
Expand Down Expand Up @@ -328,6 +328,10 @@ export class CommandHelp extends HelpFormatter {
header: 'FLAG DESCRIPTIONS',
},
]

const allowedSections = this.opts.sections?.map((s) => s.toLowerCase())

return sections.filter(({header}) => !allowedSections || allowedSections.includes(header.toLowerCase()))
}

protected usage(): string {
Expand Down
7 changes: 5 additions & 2 deletions src/help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import stripAnsi from 'strip-ansi'
import {colorize} from '../cli-ux/theme'
import write from '../cli-ux/write'
import {Command} from '../command'
import {error} from '../errors'
import {error} from '../errors/error'
import * as Interfaces from '../interfaces'
import {load} from '../module-loader'
import {SINGLE_COMMAND_CLI_SYMBOL} from '../symbols'
Expand Down Expand Up @@ -173,7 +173,9 @@ export class Help extends HelpBase {
}

protected log(...args: string[]) {
write.stdout(format.apply(this, args) + '\n')
this.opts.sendToStderr
? write.stderr(format.apply(this, args) + '\n')
: write.stdout(format.apply(this, args) + '\n')
}

public async showCommandHelp(command: Command.Loadable): Promise<void> {
Expand Down Expand Up @@ -358,6 +360,7 @@ export class Help extends HelpBase {
}

protected summary(c: Command.Loadable): string | undefined {
if (this.opts.sections && !this.opts.sections.map((s) => s.toLowerCase()).includes('summary')) return
if (c.summary) return colorize(this.config?.theme?.commandSummary, this.render(c.summary.split('\n')[0]))
return c.description && colorize(this.config?.theme?.commandSummary, this.render(c.description).split('\n')[0])
}
Expand Down
8 changes: 8 additions & 0 deletions src/interfaces/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export interface HelpOptions {
*/
hideCommandSummaryInDescription?: boolean
maxWidth: number
/**
* Only show the help for the specified sections. Defaults to all sections.
*/
sections?: string[]
/**
* By default, the help output is sent to stdout. If this is true, it will be sent to stderr.
*/
sendToStderr?: boolean
/**
* By default, titles show flag values as `<value>`. Some CLI developers may prefer titles
* to show the flag name as the value. i.e. `--myflag=myflag` instead of `--myflag=<value>`.
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {URL, fileURLToPath} from 'node:url'

import Cache from './cache'
import {ux} from './cli-ux'
import {Config} from './config'
import {getHelpFlagAdditions, loadHelpClass, normalizeArgv} from './help'
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr
}

const config = await Config.load(options ?? require.main?.filename ?? __dirname)

Cache.getInstance().set('config', config)
// If this is a single command CLI, then insert the SINGLE_COMMAND_CLI_SYMBOL into the argv array to serve as the command id.
if (config.isSingleCommandCLI) {
argv = [SINGLE_COMMAND_CLI_SYMBOL, ...argv]
Expand Down
2 changes: 1 addition & 1 deletion src/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {pathToFileURL} from 'node:url'

import {Command} from './command'
import {tsPath} from './config/ts-path'
import {ModuleLoadError} from './errors'
import {ModuleLoadError} from './errors/errors/module-load'
import {Config as IConfig, Plugin as IPlugin} from './interfaces'
import {existsSync} from './util/fs'

Expand Down
4 changes: 4 additions & 0 deletions src/parser/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Validation = {

export class CLIParseError extends CLIError {
public parse: CLIParseErrorOptions['parse']
public showHelp = false

constructor(options: CLIParseErrorOptions & {message: string}) {
options.message += '\nSee more help with --help'
Expand Down Expand Up @@ -73,6 +74,7 @@ export class RequiredArgsError extends CLIParseError {

super({exit: Cache.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse})
this.args = args
this.showHelp = true
}
}

Expand All @@ -83,6 +85,7 @@ export class UnexpectedArgsError extends CLIParseError {
const message = `Unexpected argument${args.length === 1 ? '' : 's'}: ${args.join(', ')}`
super({exit: Cache.getInstance().get('exitCodes')?.unexpectedArgs ?? exit, message, parse})
this.args = args
this.showHelp = true
}
}

Expand All @@ -93,6 +96,7 @@ export class NonExistentFlagsError extends CLIParseError {
const message = `Nonexistent flag${flags.length === 1 ? '' : 's'}: ${flags.join(', ')}`
super({exit: Cache.getInstance().get('exitCodes')?.nonExistentFlag ?? exit, message, parse})
this.flags = flags
this.showHelp = true
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/util/read-tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import makeDebug from 'debug'
import {readFile, readdir} from 'node:fs/promises'
import {dirname, join} from 'node:path'

import {memoizedWarn} from '../errors'
import {memoizedWarn} from '../errors/warn'
import {TSConfig} from '../interfaces'
import {mergeNestedObjects} from './util'

Expand Down

0 comments on commit 7133a97

Please sign in to comment.