Skip to content

Commit

Permalink
feat: repl
Browse files Browse the repository at this point in the history
  • Loading branch information
hongaar committed Dec 30, 2019
1 parent 2bde7e0 commit 6ee5b10
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 143 deletions.
100 changes: 89 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,22 @@ intuitive to work with.
- [`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.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)
- [Bundle](#bundle)
- [Todo](#todo)
- [Contributing](#contributing)
- [License](#license)

Expand All @@ -77,21 +78,61 @@ import { program, command } from 'bandersnatch'

const echo = command('echo', 'Echo something in the terminal')
.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()
```

And run with:

```bash
ts-node echo.ts
$ ts-node echo.ts Hello world
Hello! world!
```

_👆 Assuming you have `ts-node` installed._

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 '../src'

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`. 😊

ℹ More examples in the [examples](https://github.com/hongaar/bandersnatch/tree/alpha/examples) directory.

## API
Expand Down Expand Up @@ -167,23 +208,26 @@ program()

Start a read-eval-print loop.

#### `program.yargsInstance()`

Returns internal `yargs` instance. Use with caution.
```ts
program()
.add(command(...))
.repl()
```

### `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.
- 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.
- 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 can be provided to change the behaviour of the
argument. Object with any of these keys:
Expand Down Expand Up @@ -220,6 +264,30 @@ object containing key/value pairs of parsed arguments and options.

Returned from `program().eval()`, can't be invoked directly.

#### `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)`

Function is invoked when command handler resolves.
Expand Down Expand Up @@ -398,8 +466,16 @@ Run `yarn bundle` and then `./echo --help`. 💪

Optionally deploy to GitHub, S3, etc. using your preferred CD method if needed.

## 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
# Clone and install
git clone [email protected]:hongaar/bandersnatch.git
Expand All @@ -410,6 +486,8 @@ yarn
yarn start examples/simple.ts
```

Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).

## License

Copyright (c) 2019 Joram van den Boezem. Licensed under the MIT license.
Expand Down
15 changes: 15 additions & 0 deletions examples/pretty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { program, command } from '../src'

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()
4 changes: 2 additions & 2 deletions examples/printer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { program, command } from '../src'
import { blue, bgMagenta } from 'ansi-colors'
import { red, bgRed } from 'ansi-colors'

// All failures vanish into the void
const failWithMeaning = () => process.exit(42)
Expand All @@ -10,7 +10,7 @@ const printer = {
str && console.log(str)
},
error(error: any) {
const str = `${bgMagenta('Oh noes!')}\n${blue(error)}`
const str = `${red('‼')} ${bgRed(error)}`
console.error(str)
}
}
Expand Down
22 changes: 12 additions & 10 deletions examples/repl.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { program, command } from '../src'
import { blue, red, dim } from 'ansi-colors'

const say = command('say', 'Say something to the terminal')
.argument('word', 'The word to say')
.argument('any', 'Maybe another', { optional: true })
.argument('some', 'Say some words', { variadic: true, type: 'number' })
.option('cache', 'Use cache', { type: 'boolean', demandOption: true })
const echo = command(['echo', 'say'], 'Echo something to the terminal')
.argument('words', { variadic: true })
.option('blue', { type: 'boolean' })
.option('red', { type: 'boolean' })
.action(async function(args) {
console.log('Executing with', { args })
const str = args.words.join(' ')
console.log(args.blue ? blue(str) : args.red ? red(str) : str)
})

program('simple cli app')
.add(say)
const app = program('simple repl app')
.add(echo)
.withHelp()
.withVersion()
.prompt('command:')
.repl()
.prompt(`${dim('command')} ${blue('$')} `)

app.repl()
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@types/yargs": "^13.0.3",
"ansi-colors": "^4.1.1",
"inquirer": "^7.0.0",
"string-argv": "^0.3.1",
"yargs": "^15.0.2"
},
"devDependencies": {
Expand Down
30 changes: 30 additions & 0 deletions src/autocompleter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Program } from './program'

export function autocompleter(program: Program) {
return new Autocompleter(program)
}

export class Autocompleter {
constructor(private program: Program) {}

completions(argv: string[]) {
return this.yargsCompletions(argv)
}

private yargsCompletions(argv: string[]) {
return new Promise<string[]>((resolve, reject) => {
const yargs = this.program.createYargsInstance()

// yargs.getCompletion() doesn't work for our use case.
yargs.parse(
['$0', '--get-yargs-completions', '$0', ...argv],
{},
(err, argv, output) => {
if (argv.getYargsCompletions) {
resolve(output ? output.split('\n') : [])
}
}
)
})
}
}
19 changes: 11 additions & 8 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,21 @@ function isCommand(obj: Argument | Option | Command): obj is Command {
return obj.constructor.name === 'Command'
}

export function command<T = {}>(command?: string, description?: string) {
export function command<T = {}>(
command?: string | string[],
description?: string
) {
return new Command<T>(command, description)
}

export class Command<T = {}> {
private command?: string
private description?: string
private args: (Argument | Option | Command)[] = []
private handler?: HandlerFn<T>

constructor(command?: string, description?: string) {
this.command = command
this.description = description
}
constructor(
private command?: string | string[],
private description?: string
) {}

/*
* This is shorthand for .add(argument(...))
Expand Down Expand Up @@ -162,7 +163,9 @@ export class Command<T = {}> {
.join(' ')

if (args !== '') {
return `${this.command} ${args}`
return Array.isArray(this.command)
? [`${this.command[0]} ${args}`, ...this.command.slice(1)]
: `${this.command} ${args}`
}

return this.command
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './argument'
export * from './autocompleter'
export * from './command'
export * from './option'
export * from './printer'
Expand Down
Loading

0 comments on commit 6ee5b10

Please sign in to comment.