bandersnatch
Advanced tools
Comparing version 1.0.0-alpha.2 to 1.0.0-alpha.3
@@ -0,1 +1,2 @@ | ||
import { InferredOptionType, Options, PositionalOptions } from 'yargs'; | ||
import { ArgumentOptions } from './argument'; | ||
@@ -6,2 +7,8 @@ import { OptionOptions } from './option'; | ||
} | ||
export declare type InferArgType<O extends Options | PositionalOptions, D = unknown> = O extends { | ||
variadic: true; | ||
type: 'number'; | ||
} ? Array<number> : O extends { | ||
variadic: true; | ||
} ? Array<string> : unknown extends InferredOptionType<O> ? D : InferredOptionType<O>; | ||
export declare class BaseArg { | ||
@@ -8,0 +15,0 @@ protected name: string; |
@@ -1,26 +0,20 @@ | ||
import { Argv, InferredOptionType, Arguments as BaseArguments, Options, PositionalOptions } from 'yargs'; | ||
import { Argv, Arguments as BaseArguments } from 'yargs'; | ||
import { Argument, ArgumentOptions } from './argument'; | ||
import { Option, OptionOptions } from './option'; | ||
declare type InferT<O extends Options | PositionalOptions, D = unknown> = O extends { | ||
variadic: true; | ||
type: 'number'; | ||
} ? Array<number> : O extends { | ||
variadic: true; | ||
type: 'string'; | ||
} ? Array<string> : unknown extends InferredOptionType<O> ? D : InferredOptionType<O>; | ||
import { InferArgType } from './baseArg'; | ||
export declare type Arguments<T = {}> = T & BaseArguments<T> & { | ||
__promise?: Promise<any>; | ||
}; | ||
export interface HandlerFn<T = {}> { | ||
export interface HandlerFn<T> { | ||
(args: Omit<T, '_' | '$0'>): Promise<any> | any; | ||
} | ||
export declare function command<T = {}>(command: string, description?: string): Command<T>; | ||
export declare function command<T = {}>(command?: string, description?: string): Command<T>; | ||
export declare class Command<T = {}> { | ||
private command; | ||
private command?; | ||
private description?; | ||
private args; | ||
private handler?; | ||
constructor(command: string, description?: string); | ||
argument<K extends string, O extends ArgumentOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferT<O, string>; }>; | ||
option<K extends string, O extends OptionOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferT<O, unknown>; }>; | ||
constructor(command?: string, description?: string); | ||
argument<K extends string, O extends ArgumentOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferArgType<O, string>; }>; | ||
option<K extends string, O extends OptionOptions>(name: K, descriptionOrOptions?: string | O, options?: O): Command<T & { [key in K]: InferArgType<O, unknown>; }>; | ||
/** | ||
@@ -33,5 +27,5 @@ * This is the base method for adding arguments and options, but it doesn't provide | ||
action(fn: HandlerFn<T>): this; | ||
getArguments(): Argument[]; | ||
getOptions(): Option[]; | ||
getCommands(): Command<{}>[]; | ||
private getArguments; | ||
private getOptions; | ||
private getCommands; | ||
/** | ||
@@ -61,2 +55,1 @@ * Calls the command() method on the passed in yargs instance and returns it. | ||
} | ||
export {}; |
@@ -128,2 +128,5 @@ "use strict"; | ||
getCommand() { | ||
if (!this.command) { | ||
throw new Error('Command name must be set'); | ||
} | ||
const args = this.getArguments() | ||
@@ -160,3 +163,3 @@ .map(arg => arg.toCommand()) | ||
} | ||
chain = chain.then(args => { | ||
chain = chain.then(async (args) => { | ||
if (!this.handler) { | ||
@@ -163,0 +166,0 @@ throw new Error('No handler defined for this command.'); |
import { Argv } from 'yargs'; | ||
import { Command } from '.'; | ||
import { Command } from './command'; | ||
import { Arguments } from './command'; | ||
import { Runner } from './runner'; | ||
export declare function program(description?: string): Program; | ||
declare type FailFn = (msg: string, err: Error, yargs: Argv) => void; | ||
declare type FailFn = (msg: string, err: Error, args: Arguments, usage?: string) => void; | ||
export declare class Program { | ||
@@ -10,2 +12,3 @@ private yargs; | ||
private replInstance?; | ||
private runnerInstance?; | ||
constructor(description?: string); | ||
@@ -19,6 +22,10 @@ add<T>(command: Command<T>): this; | ||
/** | ||
* If invoked with a command, this is used instead of process.argv. | ||
* Evaluate command (or process.argv) and return runner instance. | ||
*/ | ||
run(command?: string | ReadonlyArray<string>): Promise<unknown>; | ||
eval(command?: string | ReadonlyArray<string>): Runner<unknown>; | ||
/** | ||
* Run a command (or process.argv) and print output. | ||
*/ | ||
run(command?: string | ReadonlyArray<string>): Runner<unknown>; | ||
/** | ||
* Run event loop which reads command from stdin. | ||
@@ -32,3 +39,4 @@ */ | ||
private failHandler; | ||
private defaultFailFn; | ||
} | ||
export {}; |
@@ -8,5 +8,6 @@ "use strict"; | ||
const ansi_colors_1 = require("ansi-colors"); | ||
const command_1 = require("./command"); | ||
const repl_1 = require("./repl"); | ||
const command_1 = require("./command"); | ||
const utils_1 = require("./utils"); | ||
const runner_1 = require("./runner"); | ||
function program(description) { | ||
@@ -18,3 +19,13 @@ return new Program(description); | ||
constructor(description) { | ||
this.yargs = yargs_1.default(process.argv.slice(2)); | ||
this.yargs = yargs_1.default(); | ||
this.defaultFailFn = (msg, err, args, usage) => { | ||
if (msg) { | ||
console.error(ansi_colors_1.yellow(msg)); | ||
} | ||
if (usage) { | ||
console.error(''); | ||
console.error(usage); | ||
} | ||
process.exit(1); | ||
}; | ||
if (description) { | ||
@@ -58,16 +69,25 @@ this.yargs.usage(description); | ||
/** | ||
* If invoked with a command, this is used instead of process.argv. | ||
* Evaluate command (or process.argv) and return runner instance. | ||
*/ | ||
run(command) { | ||
eval(command) { | ||
const cmd = command || process.argv.slice(2); | ||
// Return promise resolving to the return value of the command handler. | ||
return new Promise((resolve, reject) => { | ||
// Set executor to promise resolving to the return value of the command | ||
// handler. | ||
this.runnerInstance = runner_1.runner((resolve, reject) => { | ||
this.yargs.parse(cmd, {}, (err, argv, output) => { | ||
// Output is a string for built-in commands like --version and --help | ||
if (output) { | ||
console.log(output); | ||
} | ||
// TODO When is err defined? | ||
if (err) { | ||
console.error(err); | ||
} | ||
if (utils_1.isPromise(argv.__promise)) { | ||
argv.__promise.then(resolve); | ||
// Delegate resolve/reject to promise returned from handler | ||
argv.__promise.then(resolve).catch(reject); | ||
} | ||
else { | ||
// Resolve with void if promise is not available, which is the case | ||
// e.g. with --version and --help | ||
resolve(); | ||
@@ -77,8 +97,15 @@ } | ||
}); | ||
return this.runnerInstance.eval(); | ||
} | ||
/** | ||
* Run a command (or process.argv) and print output. | ||
*/ | ||
run(command) { | ||
return this.eval(command).print(); | ||
} | ||
/** | ||
* Run event loop which reads command from stdin. | ||
*/ | ||
repl() { | ||
this.replInstance = new repl_1.Repl(this, this.promptPrefix); | ||
this.replInstance = repl_1.repl(this, this.promptPrefix); | ||
// Don't exit on errors. | ||
@@ -99,24 +126,26 @@ this.yargs.exitProcess(false); | ||
failHandler(msg, err, yargs) { | ||
var _a, _b; | ||
if (this.failFn) { | ||
this.failFn(msg, err, yargs); | ||
var _a; | ||
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))); | ||
} | ||
else if (this.replInstance) { | ||
if (msg) { | ||
this.replInstance.setError(msg); | ||
} | ||
else if (err) { | ||
this.replInstance.setError((_a = err.stack, (_a !== null && _a !== void 0 ? _a : err.message))); | ||
} | ||
} | ||
else { | ||
if (msg) { | ||
console.error(ansi_colors_1.red(msg)); | ||
} | ||
else if (err) { | ||
console.error(ansi_colors_1.red((_b = err.stack, (_b !== null && _b !== void 0 ? _b : err.message)))); | ||
} | ||
console.error(''); | ||
console.error(yargs.help()); | ||
process.exit(1); | ||
const args = yargs.argv; | ||
const usage = yargs.help(); | ||
const cb = () => { | ||
if (this.failFn) { | ||
// Call custom fail function. | ||
this.failFn(msg, err, args, usage); | ||
} | ||
else { | ||
// Call default fail function. | ||
this.defaultFailFn(msg, err, args, usage); | ||
} | ||
}; | ||
// We call the fail function in the runner chain if available, to give | ||
// async printer a chance to complete first. | ||
this.runnerInstance | ||
? this.runnerInstance.then(cb) | ||
: Promise.resolve().then(cb); | ||
} | ||
@@ -123,0 +152,0 @@ } |
import { Program } from './program'; | ||
export declare function repl(program: Program, prefix?: string): Repl; | ||
export declare class Repl { | ||
@@ -3,0 +4,0 @@ private program; |
@@ -5,2 +5,6 @@ "use strict"; | ||
const ansi_colors_1 = require("ansi-colors"); | ||
function repl(program, prefix = '>') { | ||
return new Repl(program, prefix); | ||
} | ||
exports.repl = repl; | ||
class Repl { | ||
@@ -7,0 +11,0 @@ constructor(program, prefix = '>') { |
{ | ||
"name": "bandersnatch", | ||
"description": "", | ||
"version": "1.0.0-alpha.2", | ||
"version": "1.0.0-alpha.3", | ||
"main": "lib/index.js", | ||
@@ -28,8 +28,17 @@ "files": [ | ||
"@types/node": "^12.12.17", | ||
"doctoc": "^1.4.0", | ||
"husky": "^3.1.0", | ||
"jest": "^24.9.0", | ||
"mock-argv": "^1.1.2", | ||
"prettier": "^1.19.1", | ||
"promise.prototype.finally": "^3.1.2", | ||
"ts-jest": "^24.2.0", | ||
"ts-node": "^8.5.4", | ||
"typescript": "^3.7.3" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "yarn doctoc . && git add README.md" | ||
} | ||
} | ||
} |
391
README.md
@@ -11,11 +11,11 @@ # bandersnatch | ||
**🚧 but not quite yet** | ||
**🚧 alpha version** | ||
## Features | ||
- Built-in REPL | ||
- Prompts for missing arguments | ||
- Autocompletes arguments, options and values | ||
- Fully typed | ||
- Uses the power of `yargs` & `inquirer` | ||
- ➰ Built-in REPL | ||
- 💬 Prompts for missing arguments | ||
- ➡ Autocompletes arguments, options and values | ||
- 🤯 Fully typed | ||
- ⚡ Uses the power of `yargs` & `inquirer` | ||
@@ -29,2 +29,42 @@ It's built in TypeScript and while it's of course possible to write your app | ||
## Table of contents | ||
<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
- [Getting started](#getting-started) | ||
- [API](#api) | ||
- [`program(description)`](#programdescription) | ||
- [`program.add(command)`](#programaddcommand) | ||
- [`program.default(command)`](#programdefaultcommand) | ||
- [`program.prompt(prompt)`](#programpromptprompt) | ||
- [`program.withHelp()`](#programwithhelp) | ||
- [`program.withVersion()`](#programwithversion) | ||
- [`program.fail(function)`](#programfailfunction) | ||
- [`program.eval(command)`](#programevalcommand) | ||
- [`program.run(command)`](#programruncommand) | ||
- [`program.repl()`](#programrepl) | ||
- [`program.yargsInstance()`](#programyargsinstance) | ||
- [`command(name, description)`](#commandname-description) | ||
- [`command.argument(name, description, options)`](#commandargumentname-description-options) | ||
- [`command.option(name, description, options)`](#commandoptionname-description-options) | ||
- [`command.command(command)`](#commandcommandcommand) | ||
- [`command.default()`](#commanddefault) | ||
- [`command.action(function)`](#commandactionfunction) | ||
- [`runner`](#runner) | ||
- [`runner.then(function)`](#runnerthenfunction) | ||
- [`runner.catch(function)`](#runnercatchfunction) | ||
- [`runner.print(printer)`](#runnerprintprinter) | ||
- [`printer`](#printer) | ||
- [`printer.write(string)`](#printerwritestring) | ||
- [`printer.error(Error)`](#printererrorerror) | ||
- [`ArgumentOptions`](#argumentoptions) | ||
- [`OptionOptions`](#optionoptions) | ||
- [Bundle](#bundle) | ||
- [Contributing](#contributing) | ||
- [License](#license) | ||
<!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
## Getting started | ||
@@ -44,5 +84,3 @@ | ||
.argument('words', 'Say some kind words', { variadic: true }) | ||
.action(function(args) { | ||
console.log(args.words.join(' ')) | ||
}) | ||
.action(args => args.words.join(' ')) | ||
@@ -54,7 +92,327 @@ program() | ||
More examples in https://github.com/hongaar/bandersnatch/tree/master/examples | ||
And run with: | ||
## Development | ||
```bash | ||
ts-node echo.ts | ||
``` | ||
_👆 Assuming you have `ts-node` installed._ | ||
ℹ More examples in the [examples](https://github.com/hongaar/bandersnatch/tree/alpha/examples) directory. | ||
## API | ||
All methods are chainable unless the docs mention otherwise. | ||
### `program(description)` | ||
Creates a new program. | ||
- Description (string, optional) is used in --help output. | ||
#### `program.add(command)` | ||
Adds a command to the program. | ||
```ts | ||
program().add(command(...)) | ||
``` | ||
#### `program.default(command)` | ||
Adds a default command to the program. Shorthand for: | ||
```ts | ||
program().add(command(...).default()) | ||
``` | ||
#### `program.prompt(prompt)` | ||
Use this prompt prefix (string, required) when in REPL mode. | ||
#### `program.withHelp()` | ||
Adds `help` and `--help` to program which displays program usage information. | ||
#### `program.withVersion()` | ||
Adds `version` and `--version` to program which displays program version from | ||
package.json. | ||
#### `program.fail(function)` | ||
Use custom error handler. Function will be called with 4 arguments: | ||
- Message (string) will contain internal message about e.g. missing arguments | ||
- Error (Error) is only set when an error was explicitly thrown | ||
- Args (array) contains program arguments | ||
- Usage (string) contains usage information from --help | ||
#### `program.eval(command)` | ||
Uses process.argv or passed in command (string, optional) to match and execute | ||
command. Returns runner instance. | ||
```ts | ||
program() | ||
.add(command(...)) | ||
.eval() | ||
``` | ||
#### `program.run(command)` | ||
Shorthand for `eval().print()`. | ||
```ts | ||
program() | ||
.add(command(...)) | ||
.run() | ||
``` | ||
#### `program.repl()` | ||
Start a read-eval-print loop. | ||
#### `program.yargsInstance()` | ||
Returns internal `yargs` instance. Use with caution. | ||
### `command(name, description)` | ||
Creates a new command. | ||
- 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. | ||
#### `command.argument(name, description, options)` | ||
Adds a positional argument to the command. | ||
- Name (string, required) is used to identify the argument. | ||
- Description (string, optional) is used in --help output. | ||
- Options (ArgumentOptions) can be provided to change the behaviour of the | ||
argument. | ||
#### `command.option(name, description, options)` | ||
Adds an option to the command. | ||
- Name (string, required) is used to identify the option. | ||
- Description (string, optional) is used in --help output. | ||
- Options (OptionOptions) can be provided to change the behaviour of the | ||
option. | ||
#### `command.command(command)` | ||
Adds a sub-command to the command. | ||
#### `command.default()` | ||
Mark command as default. Default commands are executed immediately and don't require a name. | ||
#### `command.action(function)` | ||
Function to execute when command is invoked. Is called with one argument: an | ||
object containing key/value pairs of parsed arguments and options. | ||
### `runner` | ||
Returned from `program().eval()`, can't be invoked directly. | ||
#### `runner.then(function)` | ||
Function is invoked when command handler resolves. | ||
#### `runner.catch(function)` | ||
Function is invoked when command handler rejects. | ||
#### `runner.print(printer)` | ||
Attaches a printer to the runner. Uses a default printer unless called with a | ||
custom printer argument. | ||
### `printer` | ||
Used by runner, can't be invoked directly. | ||
#### `printer.write(string)` | ||
Handles output. Prints to stdout by default. | ||
#### `printer.error(Error)` | ||
Handles errors. Prints stack trace to stderr by default. | ||
### `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 | ||
There are many options to bundle your application for distribution. We'll | ||
discuss a common pattern. | ||
ℹ An example can be found in the [examples/bundle](https://github.com/hongaar/bandersnatch/tree/alpha/examples/bundle) directory. | ||
Init a `package.json` if needed: | ||
```bash | ||
mkdir echo && cd echo | ||
yarn init | ||
``` | ||
Install dependencies: | ||
```bash | ||
yarn add bandersnatch | ||
yarn add typescript pkg --dev | ||
``` | ||
And create an example app in `src/cli.ts`: | ||
```ts | ||
import { program, command } from 'bandersnatch' | ||
export default program() | ||
.withHelp() | ||
.default( | ||
command('echo', 'Echo something in the terminal') | ||
.argument('words', 'Say some kind words', { variadic: true }) | ||
.action(console.log) | ||
) | ||
``` | ||
Building your app with TypeScript is very powerful, but runtime compilation is | ||
slow so we compile the code ahead of time. | ||
Add a `tsconfig.json`, similar to: | ||
```json | ||
{ | ||
"include": ["./src"], | ||
"compilerOptions": { | ||
"target": "es2017", | ||
"module": "commonjs", | ||
"lib": ["es2017"], | ||
"declaration": true, | ||
"outDir": "lib", | ||
"rootDir": "src", | ||
"strict": true, | ||
"allowSyntheticDefaultImports": true, | ||
"esModuleInterop": true, | ||
"moduleResolution": "node" | ||
} | ||
} | ||
``` | ||
Add these scripts to your `package.json`: | ||
```diff | ||
{ | ||
"name": "echo", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
+ "scripts": { | ||
+ "prepublishOnly": "yarn build", | ||
+ "build": "tsc", | ||
+ }, | ||
"dependencies": { | ||
"bandersnatch": "^1.0.0-alpha.2" | ||
}, | ||
"devDependencies": { | ||
"pkg": "^4.4.2", | ||
"typescript": "^3.7.3" | ||
} | ||
} | ||
``` | ||
And compile now by running `yarn build`. | ||
Next, we need to create a simple entrypoint `echo.js`, which can be run with | ||
node: | ||
```bash | ||
#!/usr/bin/env node | ||
require('./lib/cli').default.run() | ||
``` | ||
To run your app, users may want to run `yarn global add echo`. For this to | ||
work, we need to make a small adjustment to `package.json`: | ||
```diff | ||
{ | ||
"name": "echo", | ||
"version": "1.0.0", | ||
- "main": "index.js", | ||
+ "bin": "echo.js", | ||
+ "files": [ | ||
+ "lib" | ||
+ ], | ||
"license": "MIT", | ||
"scripts": { | ||
"prepublishOnly": "yarn build", | ||
"build": "tsc", | ||
}, | ||
"dependencies": { | ||
"bandersnatch": "^1.0.0-alpha.2" | ||
}, | ||
"devDependencies": { | ||
"pkg": "^4.4.2", | ||
"typescript": "^3.7.3" | ||
} | ||
} | ||
``` | ||
You can now `npm publish`. | ||
To create a binary (your app with Node.js bundled), add this script to | ||
`package.json`: | ||
```diff | ||
{ | ||
"name": "echo", | ||
"version": "1.0.0", | ||
"bin": "echo.js", | ||
"files": [ | ||
"lib" | ||
], | ||
"license": "MIT", | ||
"scripts": { | ||
"prepublishOnly": "yarn build", | ||
"build": "tsc", | ||
+ "bundle": "yarn build && pkg -t host ." | ||
}, | ||
"dependencies": { | ||
"bandersnatch": "^1.0.0-alpha.2" | ||
}, | ||
"devDependencies": { | ||
"pkg": "^4.4.2", | ||
"typescript": "^3.7.3" | ||
} | ||
} | ||
``` | ||
_👆 Omit `-t host` to create binaries for all platforms._ | ||
Run `yarn bundle` and then `./echo --help`. 💪 | ||
Optionally deploy to GitHub, S3, etc. using your preferred CD method if needed. | ||
## Contributing | ||
```bash | ||
# Clone and install | ||
@@ -69,13 +427,8 @@ git clone git@github.com:hongaar/bandersnatch.git | ||
## API | ||
## License | ||
_Work in progress_ | ||
Copyright (c) 2019 Joram van den Boezem. Licensed under the MIT license. | ||
## Todo | ||
- [ ] Autocomplete in repl mode | ||
- [ ] Remove \$0 from help when in repl mode | ||
--- | ||
Inspired by Vorpal | ||
Inspired by [vorpal](https://vorpal.js.org/) |
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
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
42464
25
866
430
0
11