Comparing version 2.3.1 to 2.4.0
/// <reference types="node" /> | ||
import { Readable, Writable } from 'stream'; | ||
import { CommandClass, Command } from './Command'; | ||
import { CommandClass, Command, Definition } from './Command'; | ||
/** | ||
* The base context of the CLI. | ||
* | ||
* All Contexts have to extend it. | ||
*/ | ||
export declare type BaseContext = { | ||
/** | ||
* The input stream of the CLI. | ||
* | ||
* @default | ||
* process.stdin | ||
*/ | ||
stdin: Readable; | ||
/** | ||
* The output stream of the CLI. | ||
* | ||
* @default | ||
* process.stdout | ||
*/ | ||
stdout: Writable; | ||
/** | ||
* The error stream of the CLI. | ||
* | ||
* @default | ||
* process.stderr | ||
*/ | ||
stderr: Writable; | ||
@@ -13,8 +36,57 @@ }; | ||
export declare type MiniCli<Context extends BaseContext> = { | ||
definitions(): Object; | ||
/** | ||
* The label of the binary. | ||
* | ||
* Shown at the top of the usage information. | ||
*/ | ||
readonly binaryLabel?: string; | ||
/** | ||
* The name of the binary. | ||
* | ||
* Included in the path and the examples of the definitions. | ||
*/ | ||
readonly binaryName: string; | ||
/** | ||
* The version of the binary. | ||
* | ||
* Shown at the top of the usage information. | ||
*/ | ||
readonly binaryVersion?: string; | ||
/** | ||
* Returns an Array representing the definitions of all registered commands. | ||
*/ | ||
definitions(): Definition[]; | ||
/** | ||
* Formats errors using colors. | ||
* | ||
* @param error The error to format. If `error.name` is `'Error'`, it is replaced with `'Internal Error'`. | ||
* @param opts.command The command whose usage will be included in the formatted error. | ||
*/ | ||
error(error: Error, opts?: { | ||
command?: Command<Context> | null; | ||
}): string; | ||
/** | ||
* Compiles a command and its arguments using the `CommandBuilder`. | ||
* | ||
* @param input An array containing the name of the command and its arguments | ||
* | ||
* @returns The compiled `Command`, with its properties populated with the arguments. | ||
*/ | ||
process(input: string[]): Command<Context>; | ||
/** | ||
* Runs a command. | ||
* | ||
* @param input An array containing the name of the command and its arguments | ||
* @param context Overrides the Context of the main `Cli` instance | ||
* | ||
* @returns The exit code of the command | ||
*/ | ||
run(input: string[], context?: Partial<Context>): Promise<number>; | ||
/** | ||
* Returns the usage of a command. | ||
* | ||
* @param command The `Command` whose usage will be returned or `null` to return the usage of all commands. | ||
* @param opts.detailed If `true`, the usage of a command will also include its description, details, and examples. Doesn't have any effect if `command` is `null` or doesn't have a `usage` property. | ||
* @param opts.prefix The prefix displayed before each command. Defaults to `$`. | ||
*/ | ||
usage(command?: CommandClass<Context> | Command<Context> | null, opts?: { | ||
@@ -25,3 +97,11 @@ detailed?: boolean; | ||
}; | ||
/** | ||
* @template Context The context shared by all commands. Contexts are a set of values, defined when calling the `run`/`runExit` functions from the CLI instance, that will be made available to the commands via `this.context`. | ||
*/ | ||
export declare class Cli<Context extends BaseContext = BaseContext> implements MiniCli<Context> { | ||
/** | ||
* The default context of the CLI. | ||
* | ||
* Contains the stdio of the current `process`. | ||
*/ | ||
static defaultContext: { | ||
@@ -37,26 +117,42 @@ stdin: NodeJS.ReadStream; | ||
readonly binaryVersion?: string; | ||
readonly enableColors: boolean; | ||
/** | ||
* Creates a new Cli and registers all commands passed as parameters. | ||
* | ||
* @param commandClasses The Commands to register | ||
* @returns The created `Cli` instance | ||
*/ | ||
static from<Context extends BaseContext = BaseContext>(commandClasses: CommandClass<Context>[]): Cli<Context>; | ||
constructor({ binaryLabel, binaryName, binaryVersion }?: { | ||
constructor({ binaryLabel, binaryName, binaryVersion, enableColors }?: { | ||
binaryLabel?: string; | ||
binaryName?: string; | ||
binaryVersion?: string; | ||
enableColors?: boolean; | ||
}); | ||
/** | ||
* Registers a command inside the CLI. | ||
*/ | ||
register(commandClass: CommandClass<Context>): void; | ||
process(input: string[]): Command<Context>; | ||
run(input: Command<Context> | string[], context: Context): Promise<number>; | ||
/** | ||
* Runs a command and exits the current `process` with the exit code returned by the command. | ||
* | ||
* @param input An array containing the name of the command and its arguments. | ||
* | ||
* @example | ||
* cli.runExit(process.argv.slice(2), Cli.defaultContext) | ||
*/ | ||
runExit(input: Command<Context> | string[], context: Context): Promise<void>; | ||
suggest(input: string[], partial: boolean): string[][]; | ||
definitions(): { | ||
path: string; | ||
usage: string; | ||
category: string | undefined; | ||
description: string | undefined; | ||
details: string | undefined; | ||
examples: string[][] | undefined; | ||
}[]; | ||
usage(command?: CommandClass<Context> | Command<Context> | null, { detailed, prefix }?: { | ||
definitions({ colored }?: { | ||
colored?: boolean; | ||
}): Definition[]; | ||
usage(command?: CommandClass<Context> | Command<Context> | null, { colored, detailed, prefix }?: { | ||
colored?: boolean; | ||
detailed?: boolean; | ||
prefix?: string; | ||
}): string; | ||
error(error: Error, { command }?: { | ||
error(error: Error | any, { colored, command }?: { | ||
colored?: boolean; | ||
command?: Command<Context> | null; | ||
@@ -66,2 +162,3 @@ }): string; | ||
private getUsageByIndex; | ||
private format; | ||
} |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const chalk_1 = __importDefault(require("chalk")); | ||
exports.Cli = void 0; | ||
const core_1 = require("../core"); | ||
@@ -11,247 +8,293 @@ const core_2 = require("../core"); | ||
const HelpCommand_1 = require("./HelpCommand"); | ||
class Cli { | ||
constructor({ binaryLabel, binaryName = `...`, binaryVersion } = {}) { | ||
this.registrations = new Map(); | ||
this.builder = new core_2.CliBuilder({ binaryName }); | ||
this.binaryLabel = binaryLabel; | ||
this.binaryName = binaryName; | ||
this.binaryVersion = binaryVersion; | ||
} | ||
static from(commandClasses) { | ||
const cli = new Cli(); | ||
for (const commandClass of commandClasses) | ||
cli.register(commandClass); | ||
return cli; | ||
} | ||
register(commandClass) { | ||
const commandBuilder = this.builder.command(); | ||
this.registrations.set(commandClass, commandBuilder.cliIndex); | ||
const { definitions } = commandClass.resolveMeta(commandClass.prototype); | ||
for (const definition of definitions) | ||
definition(commandBuilder); | ||
commandBuilder.setContext({ | ||
commandClass, | ||
}); | ||
} | ||
process(input) { | ||
const { contexts, process } = this.builder.compile(); | ||
const state = process(input); | ||
switch (state.selectedIndex) { | ||
case core_1.HELP_COMMAND_INDEX: | ||
{ | ||
return HelpCommand_1.HelpCommand.from(state, this, contexts); | ||
function getDefaultColorSettings() { | ||
if (process.env.FORCE_COLOR === `0`) | ||
return false; | ||
if (process.env.FORCE_COLOR === `1`) | ||
return true; | ||
if (typeof process.stdout !== `undefined` && process.stdout.isTTY) | ||
return true; | ||
return false; | ||
} | ||
/** | ||
* @template Context The context shared by all commands. Contexts are a set of values, defined when calling the `run`/`runExit` functions from the CLI instance, that will be made available to the commands via `this.context`. | ||
*/ | ||
let Cli = /** @class */ (() => { | ||
class Cli { | ||
constructor({ binaryLabel, binaryName = `...`, binaryVersion, enableColors = getDefaultColorSettings() } = {}) { | ||
this.registrations = new Map(); | ||
this.builder = new core_2.CliBuilder({ binaryName }); | ||
this.binaryLabel = binaryLabel; | ||
this.binaryName = binaryName; | ||
this.binaryVersion = binaryVersion; | ||
this.enableColors = enableColors; | ||
} | ||
/** | ||
* Creates a new Cli and registers all commands passed as parameters. | ||
* | ||
* @param commandClasses The Commands to register | ||
* @returns The created `Cli` instance | ||
*/ | ||
static from(commandClasses) { | ||
const cli = new Cli(); | ||
for (const commandClass of commandClasses) | ||
cli.register(commandClass); | ||
return cli; | ||
} | ||
/** | ||
* Registers a command inside the CLI. | ||
*/ | ||
register(commandClass) { | ||
const commandBuilder = this.builder.command(); | ||
this.registrations.set(commandClass, commandBuilder.cliIndex); | ||
const { definitions } = commandClass.resolveMeta(commandClass.prototype); | ||
for (const definition of definitions) | ||
definition(commandBuilder); | ||
commandBuilder.setContext({ | ||
commandClass, | ||
}); | ||
} | ||
process(input) { | ||
const { contexts, process } = this.builder.compile(); | ||
const state = process(input); | ||
switch (state.selectedIndex) { | ||
case core_1.HELP_COMMAND_INDEX: | ||
{ | ||
return HelpCommand_1.HelpCommand.from(state, this, contexts); | ||
} | ||
break; | ||
default: | ||
{ | ||
const { commandClass } = contexts[state.selectedIndex]; | ||
const command = new commandClass(); | ||
command.path = state.path; | ||
const { transformers } = commandClass.resolveMeta(commandClass.prototype); | ||
for (const transformer of transformers) | ||
transformer(state, command); | ||
return command; | ||
} | ||
break; | ||
} | ||
} | ||
async run(input, context) { | ||
let command; | ||
if (!Array.isArray(input)) { | ||
command = input; | ||
} | ||
else { | ||
try { | ||
command = this.process(input); | ||
} | ||
break; | ||
default: | ||
{ | ||
const { commandClass } = contexts[state.selectedIndex]; | ||
const command = new commandClass(); | ||
command.path = state.path; | ||
const { transformers } = commandClass.resolveMeta(commandClass.prototype); | ||
for (const transformer of transformers) | ||
transformer(state, command); | ||
return command; | ||
catch (error) { | ||
context.stdout.write(this.error(error)); | ||
return 1; | ||
} | ||
break; | ||
} | ||
} | ||
async run(input, context) { | ||
let command; | ||
if (!Array.isArray(input)) { | ||
command = input; | ||
} | ||
else { | ||
} | ||
if (command.help) { | ||
context.stdout.write(this.usage(command, { detailed: true })); | ||
return 0; | ||
} | ||
command.context = context; | ||
command.cli = { | ||
binaryLabel: this.binaryLabel, | ||
binaryName: this.binaryName, | ||
binaryVersion: this.binaryVersion, | ||
definitions: () => this.definitions(), | ||
error: (error, opts) => this.error(error, opts), | ||
process: input => this.process(input), | ||
run: (input, subContext) => this.run(input, Object.assign(Object.assign({}, context), subContext)), | ||
usage: (command, opts) => this.usage(command, opts), | ||
}; | ||
let exitCode; | ||
try { | ||
command = this.process(input); | ||
exitCode = await command.validateAndExecute(); | ||
} | ||
catch (error) { | ||
context.stdout.write(this.error(error)); | ||
context.stdout.write(this.error(error, { command })); | ||
return 1; | ||
} | ||
return exitCode; | ||
} | ||
if (command.help) { | ||
context.stdout.write(this.usage(command, { detailed: true })); | ||
return 0; | ||
/** | ||
* Runs a command and exits the current `process` with the exit code returned by the command. | ||
* | ||
* @param input An array containing the name of the command and its arguments. | ||
* | ||
* @example | ||
* cli.runExit(process.argv.slice(2), Cli.defaultContext) | ||
*/ | ||
async runExit(input, context) { | ||
process.exitCode = await this.run(input, context); | ||
} | ||
command.context = context; | ||
command.cli = { | ||
definitions: () => this.definitions(), | ||
error: (error, opts) => this.error(error, opts), | ||
process: input => this.process(input), | ||
run: (input, subContext) => this.run(input, Object.assign(Object.assign({}, context), subContext)), | ||
usage: (command, opts) => this.usage(command, opts), | ||
}; | ||
let exitCode; | ||
try { | ||
exitCode = await command.validateAndExecute(); | ||
suggest(input, partial) { | ||
const { contexts, process, suggest } = this.builder.compile(); | ||
return suggest(input, partial); | ||
} | ||
catch (error) { | ||
context.stdout.write(this.error(error, { command })); | ||
return 1; | ||
} | ||
return exitCode; | ||
} | ||
async runExit(input, context) { | ||
process.exitCode = await this.run(input, context); | ||
} | ||
suggest(input, partial) { | ||
const { contexts, process, suggest } = this.builder.compile(); | ||
return suggest(input, partial); | ||
} | ||
definitions() { | ||
const data = []; | ||
for (const [commandClass, number] of this.registrations) { | ||
if (typeof commandClass.usage === `undefined`) | ||
continue; | ||
const path = this.getUsageByIndex(number, { detailed: false }); | ||
const usage = this.getUsageByIndex(number, { detailed: true }); | ||
const category = typeof commandClass.usage.category !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.category, false) | ||
: undefined; | ||
const description = typeof commandClass.usage.description !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.description, false) | ||
: undefined; | ||
const details = typeof commandClass.usage.details !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.details, true) | ||
: undefined; | ||
const examples = typeof commandClass.usage.examples !== `undefined` | ||
? commandClass.usage.examples.map(([label, cli]) => [format_1.formatMarkdownish(label, false), cli.replace(/\$0/g, this.binaryName)]) | ||
: undefined; | ||
data.push({ path, usage, category, description, details, examples }); | ||
} | ||
return data; | ||
} | ||
usage(command = null, { detailed = false, prefix = `$ ` } = {}) { | ||
// @ts-ignore | ||
const commandClass = command !== null && typeof command.getMeta === `undefined` | ||
? command.constructor | ||
: command; | ||
let result = ``; | ||
if (!commandClass) { | ||
const commandsByCategories = new Map(); | ||
for (const [commandClass, number] of this.registrations.entries()) { | ||
definitions({ colored = false } = {}) { | ||
const data = []; | ||
for (const [commandClass, number] of this.registrations) { | ||
if (typeof commandClass.usage === `undefined`) | ||
continue; | ||
const path = this.getUsageByIndex(number, { detailed: false }); | ||
const usage = this.getUsageByIndex(number, { detailed: true }); | ||
const category = typeof commandClass.usage.category !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.category, false) | ||
: null; | ||
let categoryCommands = commandsByCategories.get(category); | ||
if (typeof categoryCommands === `undefined`) | ||
commandsByCategories.set(category, categoryCommands = []); | ||
const usage = this.getUsageByIndex(number); | ||
categoryCommands.push({ commandClass, usage }); | ||
? format_1.formatMarkdownish(commandClass.usage.category, { format: this.format(colored), paragraphs: false }) | ||
: undefined; | ||
const description = typeof commandClass.usage.description !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.description, { format: this.format(colored), paragraphs: false }) | ||
: undefined; | ||
const details = typeof commandClass.usage.details !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.details, { format: this.format(colored), paragraphs: true }) | ||
: undefined; | ||
const examples = typeof commandClass.usage.examples !== `undefined` | ||
? commandClass.usage.examples.map(([label, cli]) => [format_1.formatMarkdownish(label, { format: this.format(colored), paragraphs: false }), cli.replace(/\$0/g, this.binaryName)]) | ||
: undefined; | ||
data.push({ path, usage, category, description, details, examples }); | ||
} | ||
const categoryNames = Array.from(commandsByCategories.keys()).sort((a, b) => { | ||
if (a === null) | ||
return -1; | ||
if (b === null) | ||
return +1; | ||
return a.localeCompare(b, `en`, { usage: `sort`, caseFirst: `upper` }); | ||
}); | ||
const hasLabel = typeof this.binaryLabel !== `undefined`; | ||
const hasVersion = typeof this.binaryVersion !== `undefined`; | ||
if (hasLabel || hasVersion) { | ||
if (hasLabel && hasVersion) | ||
result += `${chalk_1.default.bold(`${this.binaryLabel} - ${this.binaryVersion}`)}\n\n`; | ||
else if (hasLabel) | ||
result += `${chalk_1.default.bold(`${this.binaryLabel}`)}\n`; | ||
else | ||
result += `${chalk_1.default.bold(`${this.binaryVersion}`)}\n`; | ||
result += ` ${chalk_1.default.bold(prefix)}${this.binaryName} <command>\n`; | ||
} | ||
else { | ||
result += `${chalk_1.default.bold(prefix)}${this.binaryName} <command>\n`; | ||
} | ||
for (let categoryName of categoryNames) { | ||
const commands = commandsByCategories.get(categoryName).slice().sort((a, b) => { | ||
return a.usage.localeCompare(b.usage, `en`, { usage: `sort`, caseFirst: `upper` }); | ||
return data; | ||
} | ||
usage(command = null, { colored, detailed = false, prefix = `$ ` } = {}) { | ||
// @ts-ignore | ||
const commandClass = command !== null && typeof command.getMeta === `undefined` | ||
? command.constructor | ||
: command; | ||
let result = ``; | ||
if (!commandClass) { | ||
const commandsByCategories = new Map(); | ||
for (const [commandClass, number] of this.registrations.entries()) { | ||
if (typeof commandClass.usage === `undefined`) | ||
continue; | ||
const category = typeof commandClass.usage.category !== `undefined` | ||
? format_1.formatMarkdownish(commandClass.usage.category, { format: this.format(colored), paragraphs: false }) | ||
: null; | ||
let categoryCommands = commandsByCategories.get(category); | ||
if (typeof categoryCommands === `undefined`) | ||
commandsByCategories.set(category, categoryCommands = []); | ||
const usage = this.getUsageByIndex(number); | ||
categoryCommands.push({ commandClass, usage }); | ||
} | ||
const categoryNames = Array.from(commandsByCategories.keys()).sort((a, b) => { | ||
if (a === null) | ||
return -1; | ||
if (b === null) | ||
return +1; | ||
return a.localeCompare(b, `en`, { usage: `sort`, caseFirst: `upper` }); | ||
}); | ||
const header = categoryName !== null | ||
? categoryName.trim() | ||
: `Where <command> is one of`; | ||
result += `\n`; | ||
result += `${chalk_1.default.bold(`${header}:`)}\n`; | ||
for (let { commandClass, usage } of commands) { | ||
const doc = commandClass.usage.description || `undocumented`; | ||
const hasLabel = typeof this.binaryLabel !== `undefined`; | ||
const hasVersion = typeof this.binaryVersion !== `undefined`; | ||
if (hasLabel || hasVersion) { | ||
if (hasLabel && hasVersion) | ||
result += `${this.format(colored).bold(`${this.binaryLabel} - ${this.binaryVersion}`)}\n\n`; | ||
else if (hasLabel) | ||
result += `${this.format(colored).bold(`${this.binaryLabel}`)}\n`; | ||
else | ||
result += `${this.format(colored).bold(`${this.binaryVersion}`)}\n`; | ||
result += ` ${this.format(colored).bold(prefix)}${this.binaryName} <command>\n`; | ||
} | ||
else { | ||
result += `${this.format(colored).bold(prefix)}${this.binaryName} <command>\n`; | ||
} | ||
for (let categoryName of categoryNames) { | ||
const commands = commandsByCategories.get(categoryName).slice().sort((a, b) => { | ||
return a.usage.localeCompare(b.usage, `en`, { usage: `sort`, caseFirst: `upper` }); | ||
}); | ||
const header = categoryName !== null | ||
? categoryName.trim() | ||
: `Where <command> is one of`; | ||
result += `\n`; | ||
result += ` ${chalk_1.default.bold(usage)}\n`; | ||
result += ` ${format_1.formatMarkdownish(doc, false)}`; | ||
result += `${this.format(colored).bold(`${header}:`)}\n`; | ||
for (let { commandClass, usage } of commands) { | ||
const doc = commandClass.usage.description || `undocumented`; | ||
result += `\n`; | ||
result += ` ${this.format(colored).bold(usage)}\n`; | ||
result += ` ${format_1.formatMarkdownish(doc, { format: this.format(colored), paragraphs: false })}`; | ||
} | ||
} | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(`You can also print more details about any of these commands by calling them after adding the \`-h,--help\` flag right after the command name.`, { format: this.format(colored), paragraphs: true }); | ||
} | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(`You can also print more details about any of these commands by calling them after adding the \`-h,--help\` flag right after the command name.`, true); | ||
} | ||
else { | ||
if (!detailed) { | ||
result += `${chalk_1.default.bold(prefix)}${this.getUsageByRegistration(commandClass)}\n`; | ||
} | ||
else { | ||
const { description = ``, details = ``, examples = [], } = commandClass.usage || {}; | ||
if (description !== ``) { | ||
result += format_1.formatMarkdownish(description, false).replace(/^./, $0 => $0.toUpperCase()); | ||
result += `\n`; | ||
if (!detailed) { | ||
result += `${this.format(colored).bold(prefix)}${this.getUsageByRegistration(commandClass)}\n`; | ||
} | ||
if (details !== `` || examples.length > 0) { | ||
result += `${chalk_1.default.bold(`Usage:`)}\n`; | ||
result += `\n`; | ||
} | ||
result += `${chalk_1.default.bold(prefix)}${this.getUsageByRegistration(commandClass)}\n`; | ||
if (details !== ``) { | ||
result += `\n`; | ||
result += `${chalk_1.default.bold(`Details:`)}\n`; | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(details, true); | ||
} | ||
if (examples.length > 0) { | ||
result += `\n`; | ||
result += `${chalk_1.default.bold(`Examples:`)}\n`; | ||
for (let [description, example] of examples) { | ||
else { | ||
const { description = ``, details = ``, examples = [], } = commandClass.usage || {}; | ||
if (description !== ``) { | ||
result += format_1.formatMarkdownish(description, { format: this.format(colored), paragraphs: false }).replace(/^./, $0 => $0.toUpperCase()); | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(description, false); | ||
result += example | ||
.replace(/^/m, ` ${chalk_1.default.bold(prefix)}`) | ||
.replace(/\$0/g, this.binaryName) | ||
+ `\n`; | ||
} | ||
if (details !== `` || examples.length > 0) { | ||
result += `${this.format(colored).bold(`Usage:`)}\n`; | ||
result += `\n`; | ||
} | ||
result += `${this.format(colored).bold(prefix)}${this.getUsageByRegistration(commandClass)}\n`; | ||
if (details !== ``) { | ||
result += `\n`; | ||
result += `${this.format(colored).bold(`Details:`)}\n`; | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(details, { format: this.format(colored), paragraphs: true }); | ||
} | ||
if (examples.length > 0) { | ||
result += `\n`; | ||
result += `${this.format(colored).bold(`Examples:`)}\n`; | ||
for (let [description, example] of examples) { | ||
result += `\n`; | ||
result += format_1.formatMarkdownish(description, { format: this.format(colored), paragraphs: false }); | ||
result += example | ||
.replace(/^/m, ` ${this.format(colored).bold(prefix)}`) | ||
.replace(/\$0/g, this.binaryName) | ||
+ `\n`; | ||
} | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
return result; | ||
} | ||
error(error, { command = null } = {}) { | ||
let result = ``; | ||
let name = error.name.replace(/([a-z])([A-Z])/g, `$1 $2`); | ||
if (name === `Error`) | ||
name = `Internal Error`; | ||
result += `${chalk_1.default.red.bold(name)}: ${error.message}\n`; | ||
// @ts-ignore | ||
const meta = error.clipanion; | ||
if (typeof meta !== `undefined`) { | ||
if (meta.type === `usage`) { | ||
result += `\n`; | ||
result += this.usage(command); | ||
error(error, { colored, command = null } = {}) { | ||
if (!(error instanceof Error)) | ||
error = new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(error)})`); | ||
let result = ``; | ||
let name = error.name.replace(/([a-z])([A-Z])/g, `$1 $2`); | ||
if (name === `Error`) | ||
name = `Internal Error`; | ||
result += `${this.format(colored).error(name)}: ${error.message}\n`; | ||
// @ts-ignore | ||
const meta = error.clipanion; | ||
if (typeof meta !== `undefined`) { | ||
if (meta.type === `usage`) { | ||
result += `\n`; | ||
result += this.usage(command); | ||
} | ||
} | ||
} | ||
else { | ||
if (error.stack) { | ||
result += `${error.stack.replace(/^.*\n/, ``)}\n`; | ||
else { | ||
if (error.stack) { | ||
result += `${error.stack.replace(/^.*\n/, ``)}\n`; | ||
} | ||
} | ||
return result; | ||
} | ||
return result; | ||
getUsageByRegistration(klass, opts) { | ||
const index = this.registrations.get(klass); | ||
if (typeof index === `undefined`) | ||
throw new Error(`Assertion failed: Unregistered command`); | ||
return this.getUsageByIndex(index, opts); | ||
} | ||
getUsageByIndex(n, opts) { | ||
return this.builder.getBuilderByIndex(n).usage(opts); | ||
} | ||
format(colored = this.enableColors) { | ||
return colored ? format_1.richFormat : format_1.textFormat; | ||
} | ||
} | ||
getUsageByRegistration(klass, opts) { | ||
const index = this.registrations.get(klass); | ||
if (typeof index === `undefined`) | ||
throw new Error(`Assertion failed: Unregistered command`); | ||
return this.getUsageByIndex(index, opts); | ||
} | ||
getUsageByIndex(n, opts) { | ||
return this.builder.getBuilderByIndex(n).usage(opts); | ||
} | ||
} | ||
/** | ||
* The default context of the CLI. | ||
* | ||
* Contains the stdio of the current `process`. | ||
*/ | ||
Cli.defaultContext = { | ||
stdin: process.stdin, | ||
stdout: process.stdout, | ||
stderr: process.stderr, | ||
}; | ||
return Cli; | ||
})(); | ||
exports.Cli = Cli; | ||
Cli.defaultContext = { | ||
stdin: process.stdin, | ||
stdout: process.stdout, | ||
stderr: process.stderr, | ||
}; |
import { CommandBuilder, RunState } from '../core'; | ||
import { BaseContext, CliContext, MiniCli } from './Cli'; | ||
import type { HelpCommand } from './entries/help'; | ||
import type { VersionCommand } from './entries/version'; | ||
export declare type Meta<Context extends BaseContext> = { | ||
@@ -7,14 +9,68 @@ definitions: ((command: CommandBuilder<CliContext<Context>>) => void)[]; | ||
}; | ||
/** | ||
* The usage of a Command. | ||
*/ | ||
export declare type Usage = { | ||
/** | ||
* The category of the command. | ||
* | ||
* Included in the detailed usage. | ||
*/ | ||
category?: string; | ||
/** | ||
* The short description of the command, formatted as Markdown. | ||
* | ||
* Included in the detailed usage. | ||
*/ | ||
description?: string; | ||
/** | ||
* The extended details of the command, formatted as Markdown. | ||
* | ||
* Included in the detailed usage. | ||
*/ | ||
details?: string; | ||
/** | ||
* Examples of the command represented as an Array of tuples. | ||
* | ||
* The first element of the tuple represents the description of the example. | ||
* | ||
* The second element of the tuple represents the command of the example. | ||
* If present, the leading `$0` is replaced with `cli.binaryName`. | ||
*/ | ||
examples?: [string, string][]; | ||
}; | ||
/** | ||
* The definition of a Command. | ||
*/ | ||
export declare type Definition = Usage & { | ||
/** | ||
* The path of the command, starting with `cli.binaryName`. | ||
*/ | ||
path: string; | ||
/** | ||
* The detailed usage of the command. | ||
*/ | ||
usage: string; | ||
}; | ||
/** | ||
* The schema used to validate the Command instance. | ||
* | ||
* The easiest way to validate it is by using the [Yup](https://github.com/jquense/yup) library. | ||
* | ||
* @example | ||
* yup.object().shape({ | ||
* a: yup.number().integer(), | ||
* b: yup.number().integer(), | ||
* }) | ||
*/ | ||
export declare type Schema<C extends Command<any>> = { | ||
/** | ||
* A function that takes the `Command` instance as a parameter and validates it, throwing an Error if the validation fails. | ||
*/ | ||
validate: (object: C) => void; | ||
}; | ||
export declare type CommandClass<Context extends BaseContext = BaseContext> = { | ||
new (): Command<Context>; | ||
resolveMeta(prototype: Command<Context>): Meta<Context>; | ||
schema?: { | ||
validate: (object: any) => void; | ||
}; | ||
schema?: Schema<any>; | ||
usage?: Usage; | ||
@@ -49,2 +105,3 @@ }; | ||
static String(descriptor: string, opts?: { | ||
tolerateBoolean?: boolean; | ||
hidden?: boolean; | ||
@@ -96,2 +153,11 @@ }): PropertyDecorator; | ||
/** | ||
* Defines the schema for the given command. | ||
* @param schema | ||
*/ | ||
static Schema<C extends Command<any> = Command<BaseContext>>(schema: Schema<C>): Schema<C>; | ||
/** | ||
* The schema used to validate the Command instance. | ||
*/ | ||
static schema?: Schema<any>; | ||
/** | ||
* Standard command that'll get executed by `Cli#run` and `Cli#runExit`. Expected to return an exit code or nothing (which Clipanion will treat as if 0 had been returned). | ||
@@ -118,1 +184,26 @@ */ | ||
} | ||
export declare namespace Command { | ||
/** | ||
* A list of useful semi-opinionated command entries that have to be registered manually. | ||
* | ||
* They cover the basic needs of most CLIs (e.g. help command, version command). | ||
* | ||
* @example | ||
* cli.register(Command.Entries.Help); | ||
* cli.register(Command.Entries.Version); | ||
*/ | ||
const Entries: { | ||
/** | ||
* A command that prints the usage of all commands. | ||
* | ||
* Paths: `-h`, `--help` | ||
*/ | ||
Help: typeof HelpCommand; | ||
/** | ||
* A command that prints the version of the binary (`cli.binaryVersion`). | ||
* | ||
* Paths: `-v`, `--version` | ||
*/ | ||
Version: typeof VersionCommand; | ||
}; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Command = void 0; | ||
class Command { | ||
@@ -74,3 +75,3 @@ constructor() { | ||
this.registerDefinition(prototype, command => { | ||
command.addOption({ names: optNames, arity: 0, hidden }); | ||
command.addOption({ names: optNames, arity: 0, hidden, allowBinding: false }); | ||
}); | ||
@@ -87,3 +88,3 @@ this.registerTransformer(prototype, (state, command) => { | ||
} | ||
static String(descriptor = { required: true }, { hidden = false } = {}) { | ||
static String(descriptor = { required: true }, { tolerateBoolean = false, hidden = false } = {}) { | ||
return (prototype, propertyName) => { | ||
@@ -93,3 +94,5 @@ if (typeof descriptor === `string`) { | ||
this.registerDefinition(prototype, command => { | ||
command.addOption({ names: optNames, arity: 1, hidden }); | ||
// If tolerateBoolean is specified, the command will only accept a string value | ||
// using the bind syntax and will otherwise act like a boolean option | ||
command.addOption({ names: optNames, arity: tolerateBoolean ? 0 : 1, hidden }); | ||
}); | ||
@@ -172,2 +175,9 @@ this.registerTransformer(prototype, (state, command) => { | ||
} | ||
/** | ||
* Defines the schema for the given command. | ||
* @param schema | ||
*/ | ||
static Schema(schema) { | ||
return schema; | ||
} | ||
async validateAndExecute() { | ||
@@ -196,1 +206,26 @@ const commandClass = this.constructor; | ||
exports.Command = Command; | ||
(function (Command) { | ||
/** | ||
* A list of useful semi-opinionated command entries that have to be registered manually. | ||
* | ||
* They cover the basic needs of most CLIs (e.g. help command, version command). | ||
* | ||
* @example | ||
* cli.register(Command.Entries.Help); | ||
* cli.register(Command.Entries.Version); | ||
*/ | ||
Command.Entries = { | ||
/** | ||
* A command that prints the usage of all commands. | ||
* | ||
* Paths: `-h`, `--help` | ||
*/ | ||
Help: require(`./entries/help`).HelpCommand, | ||
/** | ||
* A command that prints the version of the binary (`cli.binaryVersion`). | ||
* | ||
* Paths: `-v`, `--version` | ||
*/ | ||
Version: require(`./entries/version`).VersionCommand, | ||
}; | ||
})(Command = exports.Command || (exports.Command = {})); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.HelpCommand = void 0; | ||
const Command_1 = require("./Command"); | ||
@@ -4,0 +5,0 @@ class HelpCommand extends Command_1.Command { |
export { BaseContext, Cli } from './Cli'; | ||
export { CommandClass, Command, Usage } from './Command'; | ||
export { CommandClass, Command, Usage, Definition, Schema } from './Command'; | ||
export { UsageError } from '../errors'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var Cli_1 = require("./Cli"); | ||
exports.Cli = Cli_1.Cli; | ||
Object.defineProperty(exports, "Cli", { enumerable: true, get: function () { return Cli_1.Cli; } }); | ||
var Command_1 = require("./Command"); | ||
exports.Command = Command_1.Command; | ||
Object.defineProperty(exports, "Command", { enumerable: true, get: function () { return Command_1.Command; } }); | ||
var errors_1 = require("../errors"); | ||
exports.UsageError = errors_1.UsageError; | ||
Object.defineProperty(exports, "UsageError", { enumerable: true, get: function () { return errors_1.UsageError; } }); |
@@ -84,3 +84,3 @@ export declare const NODE_INITIAL = 0; | ||
isBatchOption: (state: RunState, segment: string, names: string[]) => boolean; | ||
isBoundOption: (state: RunState, segment: string, names: string[]) => boolean; | ||
isBoundOption: (state: RunState, segment: string, names: string[], options: OptDefinition[]) => boolean; | ||
isNegatedOption: (state: RunState, segment: string, name: string) => boolean; | ||
@@ -328,2 +328,3 @@ isHelp: (state: RunState, segment: string) => boolean; | ||
hidden: boolean; | ||
allowBinding: boolean; | ||
}; | ||
@@ -353,3 +354,3 @@ export declare class CommandBuilder<Context> { | ||
}): void; | ||
addOption({ names, arity, hidden }: Partial<OptDefinition> & { | ||
addOption({ names, arity, hidden, allowBinding }: Partial<OptDefinition> & { | ||
names: string[]; | ||
@@ -356,0 +357,0 @@ }): void; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; | ||
result["default"] = mod; | ||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CliBuilder = exports.CommandBuilder = exports.NoLimits = exports.reducers = exports.tests = exports.suggest = exports.execute = exports.registerStatic = exports.registerShortcut = exports.registerDynamic = exports.cloneNode = exports.cloneTransition = exports.isTerminalNode = exports.makeNode = exports.aggregateHelpStates = exports.selectBestState = exports.trimSmallerBranches = exports.runMachineInternal = exports.debugMachine = exports.simplifyMachine = exports.injectNode = exports.makeAnyOfMachine = exports.makeStateMachine = exports.debug = exports.DEBUG = exports.BINDING_REGEX = exports.BATCH_REGEX = exports.OPTION_REGEX = exports.HELP_REGEX = exports.HELP_COMMAND_INDEX = exports.END_OF_INPUT = exports.START_OF_INPUT = exports.NODE_ERRORED = exports.NODE_SUCCESS = exports.NODE_INITIAL = void 0; | ||
const errors = __importStar(require("./errors")); | ||
@@ -428,5 +441,7 @@ exports.NODE_INITIAL = 0; | ||
}, | ||
isBoundOption: (state, segment, names) => { | ||
isBoundOption: (state, segment, names, options) => { | ||
const optionParsing = segment.match(exports.BINDING_REGEX); | ||
return !state.ignoreOptions && !!optionParsing && exports.OPTION_REGEX.test(optionParsing[1]) && names.includes(optionParsing[1]); | ||
return !state.ignoreOptions && !!optionParsing && exports.OPTION_REGEX.test(optionParsing[1]) && names.includes(optionParsing[1]) | ||
// Disallow bound options with no arguments (i.e. booleans) | ||
&& options.filter(opt => opt.names.includes(optionParsing[1])).every(opt => opt.allowBinding); | ||
}, | ||
@@ -551,5 +566,5 @@ isNegatedOption: (state, segment, name) => { | ||
} | ||
addOption({ names, arity = 0, hidden = false }) { | ||
addOption({ names, arity = 0, hidden = false, allowBinding = true }) { | ||
this.allOptionNames.push(...names); | ||
this.options.push({ names, arity, hidden }); | ||
this.options.push({ names, arity, hidden, allowBinding }); | ||
} | ||
@@ -675,3 +690,3 @@ setContext(context) { | ||
registerDynamic(machine, node, [`isBatchOption`, this.allOptionNames], node, `pushBatch`); | ||
registerDynamic(machine, node, [`isBoundOption`, this.allOptionNames], node, `pushBound`); | ||
registerDynamic(machine, node, [`isBoundOption`, this.allOptionNames, this.options], node, `pushBound`); | ||
registerDynamic(machine, node, [`isUnsupportedOption`, this.allOptionNames], exports.NODE_ERRORED, [`setError`, `Unsupported option name`]); | ||
@@ -678,0 +693,0 @@ registerDynamic(machine, node, [`isInvalidOption`], exports.NODE_ERRORED, [`setError`, `Invalid option name`]); |
@@ -6,2 +6,7 @@ export declare type ErrorMeta = { | ||
}; | ||
/** | ||
* A generic usage error with the name `UsageError`. | ||
* | ||
* It should be used over `Error` only when it's the user's fault. | ||
*/ | ||
export declare class UsageError extends Error { | ||
@@ -8,0 +13,0 @@ clipanion: ErrorMeta; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.AmbiguousSyntaxError = exports.UnknownSyntaxError = exports.UsageError = void 0; | ||
const core_1 = require("./core"); | ||
/** | ||
* A generic usage error with the name `UsageError`. | ||
* | ||
* It should be used over `Error` only when it's the user's fault. | ||
*/ | ||
class UsageError extends Error { | ||
@@ -5,0 +11,0 @@ constructor(message) { |
@@ -1,1 +0,11 @@ | ||
export declare function formatMarkdownish(text: string, paragraphs: boolean): string; | ||
export interface ColorFormat { | ||
bold(str: string): string; | ||
error(str: string): string; | ||
code(str: string): string; | ||
} | ||
export declare const richFormat: ColorFormat; | ||
export declare const textFormat: ColorFormat; | ||
export declare function formatMarkdownish(text: string, { format, paragraphs }: { | ||
format: ColorFormat; | ||
paragraphs: boolean; | ||
}): string; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.formatMarkdownish = exports.textFormat = exports.richFormat = void 0; | ||
; | ||
exports.richFormat = { | ||
bold: str => `\x1b[1m${str}\x1b[22m`, | ||
error: str => `\x1b[31m\x1b[1m${str}\x1b[22m\x1b[39m`, | ||
code: str => `\x1b[36m${str}\x1b[39m`, | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const chalk_1 = __importDefault(require("chalk")); | ||
function formatMarkdownish(text, paragraphs) { | ||
exports.textFormat = { | ||
bold: str => str, | ||
error: str => str, | ||
code: str => str, | ||
}; | ||
function formatMarkdownish(text, { format, paragraphs }) { | ||
// Enforce \n as newline character | ||
@@ -33,3 +41,3 @@ text = text.replace(/\r\n?/g, `\n`); | ||
text = text.replace(/(`+)((?:.|[\n])*?)\1/g, function ($0, $1, $2) { | ||
return chalk_1.default.cyan($1 + $2 + $1); | ||
return format.code($1 + $2 + $1); | ||
}); | ||
@@ -36,0 +44,0 @@ return text ? text + `\n` : ``; |
{ | ||
"name": "clipanion", | ||
"version": "2.3.1", | ||
"version": "2.4.0", | ||
"main": "lib/advanced", | ||
"license": "MIT", | ||
"dependencies": { | ||
"chalk": "^2.4.2" | ||
}, | ||
"devDependencies": { | ||
"@berry/pnpify": "^0.0.6", | ||
"@types/chai": "^4.1.7", | ||
"@types/chai-as-promised": "^7.1.0", | ||
"@types/chalk": "^2.2.0", | ||
"@types/mocha": "^5.2.7", | ||
"@types/node": "^12.6.2", | ||
"@types/yup": "^0.26.21", | ||
"@types/chai": "^4.2.11", | ||
"@types/chai-as-promised": "^7.1.2", | ||
"@types/mocha": "^7.0.2", | ||
"@types/node": "^14.0.1", | ||
"@types/yup": "^0.28.3", | ||
"chai": "^4.2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"get-stream": "^5.1.0", | ||
"mocha": "^6.1.4", | ||
"ts-node": "^8.3.0", | ||
"typescript": "^3.5.3", | ||
"yup": "^0.27.0" | ||
"mocha": "^7.1.2", | ||
"ts-node": "^8.10.1", | ||
"typescript": "^3.9.2", | ||
"yup": "^0.28.5" | ||
}, | ||
"scripts": { | ||
"prepack": "rm -rf lib && yarn pnpify tsc", | ||
"prepack": "rm -rf lib && yarn tsc", | ||
"postpack": "rm -rf lib", | ||
"test": "yarn pnpify mocha --require ts-node/register --extension ts tests" | ||
"test": "FORCE_COLOR=1 mocha --require ts-node/register --extension ts tests" | ||
}, | ||
@@ -30,0 +25,0 @@ "publishConfig": { |
@@ -22,2 +22,3 @@ # <img src="./logo.svg" height="25" /> Clipanion | ||
- Clipanion generates good-looking help pages out of the box | ||
- Clipanion offers common optional command entries out-of-the-box (e.g. version command, help command) | ||
@@ -124,6 +125,25 @@ Clipanion is used in [Yarn](https://github.com/yarnpkg/berry) with great success. | ||
#### `@Command.String({required?: boolean})` | ||
#### `@Command.String({required?: boolean, tolerateBoolean?: boolean})` | ||
Specifies that the command accepts a positional argument. By default it will be required, but this can be toggled off. | ||
`tolerateBoolean` specifies that the command will act like a boolean flag if it doesn't have a value. With this option on, an argument value can only be specified using `=`. It is off by default. | ||
```ts | ||
class RunCommand extends Command { | ||
@Command.String(`--inspect`, { tolerateBoolean: true }) | ||
public debug: boolean | string = false; | ||
// ... | ||
} | ||
run --inspect | ||
=> debug = true | ||
run --inspect=1234 | ||
=> debug = "1234" | ||
run --inspect 1234 | ||
=> invalid | ||
``` | ||
#### `@Command.String(optionNames: string)` | ||
@@ -169,20 +189,26 @@ | ||
## General Help Page | ||
## Optional Built-in Command Entries | ||
In order to support using the `-h` option to list the commands available to the application, just register a new command as such: | ||
Clipanion offers common optional command entries out-of-the-box, under the `Command.Entries` namespace. | ||
They have to be manually registered: | ||
```ts | ||
class HelpCommand extends Command { | ||
@Command.Path(`--help`) | ||
@Command.Path(`-h`) | ||
async execute() { | ||
this.context.stdout.write(this.cli.usage(null)); | ||
} | ||
} | ||
cli.register(Command.Entries.Help); | ||
cli.register(Command.Entries.Version); | ||
``` | ||
This will print a block similar to the following: | ||
### Help Command - General Help Page | ||
> Paths: `-h`, `--help` | ||
The `Command.Entries.Help` command displays the list of commands available to the application, printing a block similar to the following. | ||
![](assets/example-general-help.png) | ||
### Version Command | ||
> Paths: `-v`, `--version` | ||
The `Command.Entries.Version` command displays the version of the binary provided under `binaryVersion` when creating the CLI. | ||
## Composition | ||
@@ -189,0 +215,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
107598
0
12
20
2325
269
6
- Removedchalk@^2.4.2
- Removedansi-styles@3.2.1(transitive)
- Removedchalk@2.4.2(transitive)
- Removedcolor-convert@1.9.3(transitive)
- Removedcolor-name@1.1.3(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedhas-flag@3.0.0(transitive)
- Removedsupports-color@5.5.0(transitive)