bandersnatch
Advanced tools
Comparing version 1.0.0-alpha.3 to 1.0.0-alpha.4
@@ -22,2 +22,3 @@ import { InferredOptionType, Options, PositionalOptions } from 'yargs'; | ||
getDescription(): string | undefined; | ||
getOptions(): OptionOptions | ArgumentOptions; | ||
} |
@@ -25,3 +25,6 @@ "use strict"; | ||
} | ||
getOptions() { | ||
return this.options; | ||
} | ||
} | ||
exports.BaseArg = BaseArg; |
@@ -11,3 +11,3 @@ import { Argv, Arguments as BaseArguments } from 'yargs'; | ||
} | ||
export declare function command<T = {}>(command?: string, description?: string): Command<T>; | ||
export declare function command<T = {}>(command?: string | string[], description?: string): Command<T>; | ||
export declare class Command<T = {}> { | ||
@@ -18,3 +18,3 @@ private command?; | ||
private handler?; | ||
constructor(command?: string, description?: string); | ||
constructor(command?: string | string[] | undefined, description?: string | undefined); | ||
argument<K extends string, O extends ArgumentOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferArgType<O, string>; }>; | ||
@@ -32,2 +32,3 @@ option<K extends string, O extends OptionOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferArgType<O, unknown>; }>; | ||
private getCommands; | ||
private toModule; | ||
/** | ||
@@ -34,0 +35,0 @@ * Calls the command() method on the passed in yargs instance and returns it. |
@@ -32,2 +32,4 @@ "use strict"; | ||
constructor(command, description) { | ||
this.command = command; | ||
this.description = description; | ||
this.args = []; | ||
@@ -40,4 +42,2 @@ /** | ||
}; | ||
this.command = command; | ||
this.description = description; | ||
} | ||
@@ -111,17 +111,20 @@ /* | ||
} | ||
/** | ||
* Calls the command() method on the passed in yargs instance and returns it. | ||
* See https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module | ||
*/ | ||
toYargs(yargs) { | ||
toModule() { | ||
const module = { | ||
command: this.getCommand(), | ||
aliases: [], | ||
describe: this.description, | ||
describe: this.description || '', | ||
builder: this.getBuilder(), | ||
handler: this.getHandler() | ||
}; | ||
return yargs.command(module); | ||
return module; | ||
} | ||
/** | ||
* Calls the command() method on the passed in yargs instance and returns it. | ||
* See https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module | ||
*/ | ||
toYargs(yargs) { | ||
return yargs.command(this.toModule()); | ||
} | ||
/** | ||
* Returns a formatted command which can be used in the command() function | ||
@@ -138,3 +141,5 @@ * of yargs. | ||
if (args !== '') { | ||
return `${this.command} ${args}`; | ||
return Array.isArray(this.command) | ||
? [`${this.command[0]} ${args}`, ...this.command.slice(1)] | ||
: `${this.command} ${args}`; | ||
} | ||
@@ -141,0 +146,0 @@ return this.command; |
@@ -1,4 +0,9 @@ | ||
export * from './program'; | ||
export * from './argument'; | ||
export * from './autocompleter'; | ||
export * from './command'; | ||
export * from './argument'; | ||
export * from './option'; | ||
export * from './printer'; | ||
export * from './program'; | ||
export * from './repl'; | ||
export * from './runner'; | ||
export * from './utils'; |
@@ -6,5 +6,10 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__export(require("./program")); | ||
__export(require("./argument")); | ||
__export(require("./autocompleter")); | ||
__export(require("./command")); | ||
__export(require("./argument")); | ||
__export(require("./option")); | ||
__export(require("./printer")); | ||
__export(require("./program")); | ||
__export(require("./repl")); | ||
__export(require("./runner")); | ||
__export(require("./utils")); |
@@ -5,11 +5,22 @@ import { Argv } from 'yargs'; | ||
import { Runner } from './runner'; | ||
export declare function program(description?: string): Program; | ||
declare type FailFn = (msg: string, err: Error, args: Arguments, usage?: string) => void; | ||
declare type ProgramOptions = { | ||
help?: boolean; | ||
version?: boolean; | ||
fail?: FailFn; | ||
prompt?: string; | ||
exitOnError?: boolean; | ||
}; | ||
export declare function program(description?: string, options?: ProgramOptions): Program; | ||
export declare class Program { | ||
private yargs; | ||
private promptPrefix; | ||
private failFn?; | ||
private description?; | ||
private options; | ||
private commands; | ||
private replInstance?; | ||
private runnerInstance?; | ||
constructor(description?: string); | ||
constructor(description?: string | undefined, options?: ProgramOptions); | ||
/** | ||
* Create a new yargs instance. Not intended for public use. | ||
*/ | ||
createYargsInstance(): Argv<{}>; | ||
add<T>(command: Command<T>): this; | ||
@@ -33,6 +44,2 @@ default<T>(command: Command<T>): this; | ||
repl(): void; | ||
/** | ||
* Allow tweaking the underlaying yargs instance. | ||
*/ | ||
yargsInstance(): Argv<{}>; | ||
private failHandler; | ||
@@ -39,0 +46,0 @@ private defaultFailFn; |
@@ -12,9 +12,11 @@ "use strict"; | ||
const runner_1 = require("./runner"); | ||
function program(description) { | ||
return new Program(description); | ||
function program(description, options = {}) { | ||
return new Program(description, options); | ||
} | ||
exports.program = program; | ||
class Program { | ||
constructor(description) { | ||
this.yargs = yargs_1.default(); | ||
constructor(description, options = {}) { | ||
this.description = description; | ||
this.options = options; | ||
this.commands = []; | ||
this.defaultFailFn = (msg, err, args, usage) => { | ||
@@ -30,37 +32,52 @@ if (msg) { | ||
}; | ||
if (description) { | ||
this.yargs.usage(description); | ||
} | ||
// Some defaults | ||
this.yargs.help(false); | ||
this.yargs.version(false); | ||
this.yargs.recommendCommands(); | ||
this.yargs.strict(); | ||
this.yargs.demandCommand(); | ||
} | ||
/** | ||
* Create a new yargs instance. Not intended for public use. | ||
*/ | ||
createYargsInstance() { | ||
const yargs = yargs_1.default(); | ||
this.description && yargs.usage(this.description); | ||
// Help accepts boolean | ||
yargs.help(!!this.options.help); | ||
// Version must be false or undefined | ||
!!this.options.version ? yargs.version() : yargs.version(false); | ||
// Non-configurable options | ||
yargs.recommendCommands(); | ||
yargs.strict(); | ||
yargs.demandCommand(); | ||
// Hidden completion command | ||
yargs.completion('completion', false); | ||
// Custom fail function. | ||
// TODO current yargs types doesn't include the third parameter. | ||
this.yargs.fail(this.failHandler.bind(this)); | ||
yargs.fail(this.failHandler.bind(this)); | ||
// Exit on errors? | ||
yargs.exitProcess(!!this.options.exitOnError); | ||
// Add commands | ||
this.commands.forEach(command => { | ||
command.toYargs(yargs); | ||
}); | ||
return yargs; | ||
} | ||
add(command) { | ||
command.toYargs(this.yargs); | ||
this.commands.push(command); | ||
return this; | ||
} | ||
default(command) { | ||
command.default().toYargs(this.yargs); | ||
this.commands.push(command.default()); | ||
return this; | ||
} | ||
prompt(prompt) { | ||
this.promptPrefix = prompt; | ||
this.options.prompt = prompt; | ||
return this; | ||
} | ||
withHelp() { | ||
this.yargs.help(true); | ||
this.options.help = true; | ||
return this; | ||
} | ||
withVersion() { | ||
this.yargs.version(); | ||
this.options.version = true; | ||
return this; | ||
} | ||
fail(fn) { | ||
this.failFn = fn; | ||
this.options.fail = fn; | ||
return this; | ||
@@ -76,3 +93,3 @@ } | ||
this.runnerInstance = runner_1.runner((resolve, reject) => { | ||
this.yargs.parse(cmd, {}, (err, argv, output) => { | ||
this.createYargsInstance().parse(cmd, {}, (err, argv, output) => { | ||
// Output is a string for built-in commands like --version and --help | ||
@@ -82,3 +99,3 @@ if (output) { | ||
} | ||
// TODO When is err defined? | ||
// TODO when is err defined? | ||
if (err) { | ||
@@ -93,3 +110,3 @@ console.error(err); | ||
// Resolve with void if promise is not available, which is the case | ||
// e.g. with --version and --help | ||
// with e.g. --version and --help | ||
resolve(); | ||
@@ -111,5 +128,5 @@ } | ||
repl() { | ||
this.replInstance = repl_1.repl(this, this.promptPrefix); | ||
this.replInstance = repl_1.repl(this, this.options.prompt); | ||
// Don't exit on errors. | ||
this.yargs.exitProcess(false); | ||
this.options.exitOnError = false; | ||
// Add exit command | ||
@@ -119,16 +136,11 @@ this.add(command_1.command('exit', 'Exit the application').action(() => { | ||
})); | ||
this.replInstance.loop(); | ||
this.replInstance.start(); | ||
} | ||
/** | ||
* Allow tweaking the underlaying yargs instance. | ||
*/ | ||
yargsInstance() { | ||
return this.yargs; | ||
} | ||
failHandler(msg, err, yargs) { | ||
var _a; | ||
// TODO needs more use-cases: only do something when msg is set, and have | ||
// errors always handled in the runner? | ||
if (this.replInstance) { | ||
// In case we're in a REPL session, we don't want to exit the process | ||
// when an error occurs. | ||
this.replInstance.setError((_a = (msg !== null && msg !== void 0 ? msg : err.stack), (_a !== null && _a !== void 0 ? _a : err.message))); | ||
// In case we're in a REPL session, forward the message which may | ||
// originate from yargs. Errors are handled in the runner. | ||
msg && this.replInstance.setError(msg); | ||
} | ||
@@ -139,5 +151,5 @@ else { | ||
const cb = () => { | ||
if (this.failFn) { | ||
if (this.options.fail) { | ||
// Call custom fail function. | ||
this.failFn(msg, err, args, usage); | ||
this.options.fail(msg, err, args, usage); | ||
} | ||
@@ -144,0 +156,0 @@ else { |
@@ -5,15 +5,11 @@ import { Program } from './program'; | ||
private program; | ||
private prefix; | ||
private prompt; | ||
private server?; | ||
private lastError; | ||
constructor(program: Program, prefix?: string); | ||
loop(): Promise<void>; | ||
private autocompleter; | ||
constructor(program: Program, prompt?: string); | ||
start(): Promise<void>; | ||
setError(err: string): void; | ||
private tick; | ||
/** | ||
* Prompt the user for a command. | ||
*/ | ||
private read; | ||
private completer; | ||
private eval; | ||
private print; | ||
} |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const inquirer_1 = require("inquirer"); | ||
const repl_1 = __importDefault(require("repl")); | ||
const string_argv_1 = require("string-argv"); | ||
const ansi_colors_1 = require("ansi-colors"); | ||
function repl(program, prefix = '>') { | ||
const autocompleter_1 = require("./autocompleter"); | ||
function repl(program, prefix = '> ') { | ||
return new Repl(program, prefix); | ||
@@ -10,48 +15,38 @@ } | ||
class Repl { | ||
constructor(program, prefix = '>') { | ||
constructor(program, prompt = '> ') { | ||
this.program = program; | ||
this.prompt = prompt; | ||
this.lastError = null; | ||
this.program = program; | ||
this.prefix = prefix; | ||
this.prompt = inquirer_1.createPromptModule(); | ||
this.autocompleter = autocompleter_1.autocompleter(program); | ||
} | ||
async loop() { | ||
await this.tick(); | ||
await this.loop(); | ||
async start() { | ||
this.server = repl_1.default.start({ | ||
prompt: this.prompt, | ||
eval: this.eval.bind(this), | ||
completer: this.completer.bind(this), | ||
ignoreUndefined: true | ||
}); | ||
} | ||
setError(err) { | ||
// Only display one error per tick | ||
if (!this.lastError) { | ||
this.lastError = err; | ||
console.error(ansi_colors_1.red(err)); | ||
} | ||
this.lastError = err; | ||
} | ||
async tick() { | ||
const stdin = await this.read(); | ||
async completer(line, cb) { | ||
const argv = string_argv_1.parseArgsStringToArgv(line); | ||
const current = argv.slice(-1).toString(); | ||
const completions = await this.autocompleter.completions(argv); | ||
let hits = completions.filter(completion => completion.startsWith(current)); | ||
// Add trailing space to each hit | ||
hits = hits.map(hit => `${hit} `); | ||
// Show all completions if none found | ||
cb(null, [hits.length ? hits : completions, current]); | ||
} | ||
async eval(line, context, file, cb) { | ||
this.lastError = null; | ||
await this.eval(stdin); | ||
const result = await this.program.run(line.trim()); | ||
if (this.lastError) { | ||
console.error(ansi_colors_1.red(this.lastError)); | ||
} | ||
cb(null, result); | ||
} | ||
/** | ||
* Prompt the user for a command. | ||
*/ | ||
async read() { | ||
// Inquirers default behaviour is to prefix the message with a space. | ||
// See https://github.com/SBoudrias/Inquirer.js/issues/677 | ||
const answers = await this.prompt([ | ||
{ | ||
type: 'input', | ||
name: 'stdin', | ||
message: this.prefix, | ||
prefix: '', | ||
suffix: '' | ||
} | ||
]); | ||
return answers.stdin; | ||
} | ||
async eval(stdin) { | ||
return this.program.run(stdin); | ||
} | ||
async print() { | ||
// Here just for completeness. | ||
} | ||
} | ||
exports.Repl = Repl; |
@@ -19,3 +19,3 @@ import { Printer } from './printer'; | ||
*/ | ||
private promise; | ||
promise(): Promise<any>; | ||
private onfulfilled; | ||
@@ -22,0 +22,0 @@ private onrejected; |
{ | ||
"name": "bandersnatch", | ||
"description": "", | ||
"version": "1.0.0-alpha.3", | ||
"version": "1.0.0-alpha.4", | ||
"main": "lib/index.js", | ||
@@ -23,2 +23,3 @@ "files": [ | ||
"inquirer": "^7.0.0", | ||
"string-argv": "^0.3.1", | ||
"yargs": "^15.0.2" | ||
@@ -25,0 +26,0 @@ }, |
172
README.md
@@ -35,2 +35,6 @@ # bandersnatch | ||
- [Getting started](#getting-started) | ||
- [Installation](#installation) | ||
- [Simple](#simple) | ||
- [REPL](#repl) | ||
- [Prompt](#prompt) | ||
- [API](#api) | ||
@@ -47,3 +51,2 @@ - [`program(description)`](#programdescription) | ||
- [`program.repl()`](#programrepl) | ||
- [`program.yargsInstance()`](#programyargsinstance) | ||
- [`command(name, description)`](#commandname-description) | ||
@@ -56,11 +59,11 @@ - [`command.argument(name, description, options)`](#commandargumentname-description-options) | ||
- [`runner`](#runner) | ||
- [`runner.print(printer)`](#runnerprintprinter) | ||
- [`runner.then(function)`](#runnerthenfunction) | ||
- [`runner.catch(function)`](#runnercatchfunction) | ||
- [`runner.print(printer)`](#runnerprintprinter) | ||
- [`runner.print(printer)`](#runnerprintprinter-1) | ||
- [`printer`](#printer) | ||
- [`printer.write(string)`](#printerwritestring) | ||
- [`printer.error(Error)`](#printererrorerror) | ||
- [`ArgumentOptions`](#argumentoptions) | ||
- [`OptionOptions`](#optionoptions) | ||
- [Bundle](#bundle) | ||
- [Todo](#todo) | ||
- [Contributing](#contributing) | ||
@@ -73,2 +76,4 @@ - [License](#license) | ||
### Installation | ||
```bash | ||
@@ -79,2 +84,4 @@ # Add dependency | ||
### Simple | ||
Now create a simple app `echo.ts`: | ||
@@ -87,6 +94,6 @@ | ||
.argument('words', 'Say some kind words', { variadic: true }) | ||
.action(args => args.words.join(' ')) | ||
.action(args => args.words.map(word => `${word}!`).join(' ')) | ||
program() | ||
.add(echo) | ||
.default(echo) | ||
.run() | ||
@@ -98,3 +105,4 @@ ``` | ||
```bash | ||
ts-node echo.ts | ||
$ ts-node echo.ts Hello world | ||
Hello! world! | ||
``` | ||
@@ -104,2 +112,77 @@ | ||
### REPL | ||
Let's dive right into some more features. This simple app has a single default | ||
command which pretty prints JSON input. When invoked without input, it'll show | ||
an interactive prompt: | ||
```ts | ||
import { program, command } from 'bandersnatch' | ||
const app = program('JSON pretty printer').default( | ||
command() | ||
.argument('json', 'Raw JSON input as string') | ||
.option('color', 'Enables colorized output', { type: 'boolean' }) | ||
.action(async args => { | ||
const json = JSON.parse(args.json) | ||
args.color | ||
? console.dir(json) | ||
: console.log(JSON.stringify(json, undefined, 4)) | ||
}) | ||
) | ||
process.argv.slice(2).length ? app.run() : app.repl() | ||
``` | ||
And run with: | ||
```bash | ||
$ ts-node pretty.ts | ||
> [0,1,1,2,3,5] | ||
[ | ||
0, | ||
1, | ||
1, | ||
2, | ||
3, | ||
5 | ||
] | ||
``` | ||
Now, try typing `[0,1,1,2,3,5] --c` and then hit `TAB`. 😊 | ||
### Prompt | ||
Bandersnatch can also ask a user for input if arguments were not provided on the | ||
command line: | ||
```ts | ||
import { program, command } from 'bandersnatch' | ||
const cmd = command() | ||
.argument('name', "What's your name?", { | ||
prompt: true | ||
}) | ||
.argument('question', "What's your question?", { | ||
prompt: true | ||
}) | ||
.action(args => `Hi ${args.name}, the answer to "${args.question}" is 42.`) | ||
program('Ask me anything') | ||
.default(cmd) | ||
.run() | ||
``` | ||
And run with: | ||
```bash | ||
$ ts-node ama.ts --name Joram | ||
? What's your question? What is everything in ASCII? | ||
Hi Joram, the answer to "What is everything in ASCII?" is 42. | ||
``` | ||
When you omit the `--name` part, the program will also prompt for it. | ||
--- | ||
ℹ More examples in the [examples](https://github.com/hongaar/bandersnatch/tree/alpha/examples) directory. | ||
@@ -180,6 +263,8 @@ | ||
#### `program.yargsInstance()` | ||
```ts | ||
program() | ||
.add(command(...)) | ||
.repl() | ||
``` | ||
Returns internal `yargs` instance. Use with caution. | ||
### `command(name, description)` | ||
@@ -189,4 +274,4 @@ | ||
- Name (string, optional) is used to invoke a command. When | ||
not used as default command, name is required. | ||
- Name (string, optional) is used to invoke a command. When not used as default | ||
command, name is required. | ||
- Description (string, optional) is used in --help output. | ||
@@ -198,6 +283,11 @@ | ||
- Name (string, required) is used to identify the argument. | ||
- Name (string, required) is used to identify the argument. Can also be an array | ||
of strings, in which case subsequent items will be treated as command aliases. | ||
- Description (string, optional) is used in --help output. | ||
- Options (ArgumentOptions) can be provided to change the behaviour of the | ||
argument. | ||
- Options can be provided to change the behaviour of the | ||
argument. Object with any of these keys: | ||
- `optional` (boolean) makes this argument optional. | ||
- `variadic` (boolean) eagerly take all remaining arguments and parse as array. | ||
Only valid for last argument. | ||
- ... | ||
@@ -211,3 +301,5 @@ #### `command.option(name, description, options)` | ||
- Options (OptionOptions) can be provided to change the behaviour of the | ||
option. | ||
option. Object with any of these keys: | ||
- `alias` (string or array of strings) alias(es) for the option key. | ||
- ... | ||
@@ -231,2 +323,26 @@ #### `command.command(command)` | ||
#### `runner.print(printer)` | ||
Prints resolved and rejected command executions to the terminal. Uses the | ||
built-in printer if invoked without arguments. | ||
```ts | ||
const runner = program() | ||
.default( | ||
command().action(() => { | ||
throw new Error('Test customer printer') | ||
}) | ||
) | ||
.eval() | ||
runner.print({ | ||
write(str: any) { | ||
str && console.log(str) | ||
}, | ||
error(error: any) { | ||
console.error(`${red('‼')} ${bgRed(error)}`) | ||
} | ||
}) | ||
``` | ||
#### `runner.then(function)` | ||
@@ -257,16 +373,2 @@ | ||
### `ArgumentOptions` | ||
Object with any of these keys: | ||
- `optional` (boolean) makes this argument optional. | ||
- `variadic` (boolean) eagerly take all remaining arguments and parse as array. | ||
Only valid for last argument. | ||
- ... | ||
### `OptionOptions` | ||
- `alias` (string or array of strings) alias(es) for the option key. | ||
- ... | ||
## Bundle | ||
@@ -425,4 +527,12 @@ | ||
## Todo | ||
- [ ] Better code coverage | ||
- [ ] Choices autocompletion in REPL mode | ||
## Contributing | ||
Contributions are very welcome. Please note this project is in a very early | ||
stage and the roadmap is a bit foggy still... | ||
```bash | ||
@@ -438,2 +548,4 @@ # Clone and install | ||
Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). | ||
## License | ||
@@ -440,0 +552,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
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
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
48018
27
930
542
6
2
+ Addedstring-argv@^0.3.1
+ Addedstring-argv@0.3.2(transitive)