Skip to content

Commit

Permalink
feat: prompter (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
hongaar committed Jul 4, 2020
1 parent 8fb3303 commit 5bc9595
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 61 deletions.
15 changes: 14 additions & 1 deletion examples/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ const cmd = command()
description: 'Your question',
prompt: true,
})
.action((args) => `Hi ${args.name}, the answer to "${args.question}" is 42.`)
.option('greeting', {
description: 'Use this greeting',
choices: ['Hi', 'Hey', 'Hiya'],
default: 'Hi',
prompt: true,
})
.option('save', {
description: 'Save the message',
type: 'boolean',
prompt: true,
})
.action((args) => {
return `${args.greeting} ${args.name}, the answer to "${args.question}" is 42.`
})

program().description('Ask me anything').default(cmd).run().then(console.log)
51 changes: 36 additions & 15 deletions src/baseArg.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InferredOptionType, Options, PositionalOptions } from 'yargs'
import { ArgumentOptions } from './argument'
import { OptionOptions } from './option'
import type { InferredOptionType, Options, PositionalOptions } from 'yargs'
import type { ArgumentOptions } from './argument'
import type { OptionOptions } from './option'

export interface BaseArgOptions {
prompt?: true | string
Expand All @@ -15,22 +15,22 @@ export type InferArgType<O extends Options | PositionalOptions, F = unknown> =
type: 'number'
}
? Array<number>
: /**
* Add support for string variadic arguments
*/
O extends {
/**
* Add support for string variadic arguments
*/
: O extends {
variadic: true
}
? Array<string>
: /**
* Prefer choices over default
*/
O extends { choices: ReadonlyArray<infer C> }
/**
* Prefer choices over default
*/
: O extends { choices: ReadonlyArray<infer C> }
? C
: /**
* Allow fallback type
*/
unknown extends InferredOptionType<O>
/**
* Allow fallback type
*/
: unknown extends InferredOptionType<O>
? F
: InferredOptionType<O>

Expand Down Expand Up @@ -68,6 +68,27 @@ export class BaseArg {
: this.name
}

/**
* Get default value, if specified.
*/
getDefault() {
return this.options.default
}

/**
* Get possible values, is specified.
*/
getChoices() {
return this.options.choices
}

/**
* Get type, is specified.
*/
getType() {
return this.options.type
}

/**
* Returns the argument/option identifier.
*/
Expand Down
56 changes: 11 additions & 45 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Argv, CommandModule, Arguments as BaseArguments } from 'yargs'
import { prompt, Question } from 'inquirer'
import { Argument, ArgumentOptions } from './argument'
import { Option, OptionOptions } from './option'
import { InferArgType } from './baseArg'
import { prompter } from './prompter'

export type Arguments<T = {}> = T &
BaseArguments<T> & {
Expand Down Expand Up @@ -247,20 +247,21 @@ export class Command<T = {}> {
* Takes command runner.
*/
private getHandler(commandRunner: CommandRunner) {
return (argv: Arguments<T>) => {
return async (argv: Arguments<T>) => {
const { _, $0, ...rest } = argv
const questions = this.getQuestions(rest)
let chain = Promise.resolve(rest)
const prompterInstance = prompter(
[...this.getArguments(), ...this.getOptions()],
rest
)

if (questions.length) {
chain = chain.then(this.prompt(questions))
}
let promise = prompterInstance.prompt()

chain = chain.then((args) => {
promise = promise.then((args) => {
if (this.handler) {
return this.handler(args)
}

// Display help this command contains sub-commands
if (this.getCommands().length) {
return commandRunner(`${this.getFqn()} --help`)
}
Expand All @@ -270,44 +271,9 @@ export class Command<T = {}> {

// Save promise chain on argv instance, so we can access it in parse
// callback.
argv.__promise = chain
argv.__promise = promise

return chain
return promise
}
}

/**
* Returns an array of arguments and options which should be prompted, because
* they are promptable (`isPromptable()` returned true) and they are not
* provided in the args passed in to this function.
*/
private getQuestions<T = {}>(args: T) {
// If we need to prompt for things, fill questions array
return [...this.getArguments(), ...this.getOptions()].reduce(
(questions, arg) => {
const name = arg.getName()
const presentInArgs = Object.constructor.hasOwnProperty.call(args, name)
// @todo How can we force prompting when default was used?
if (!presentInArgs && arg.isPromptable()) {
questions.push({
name,
message: arg.getPrompt(),
})
}

return questions
},
[] as Question[]
)
}

/**
* Ask questions and merge with passed in args.
*/
private prompt = <Q = Question[]>(questions: Q) => <T = {}>(args: T) => {
return prompt<{}>(questions).then((answers) => ({
...args,
...answers,
}))
}
}
103 changes: 103 additions & 0 deletions src/prompter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { prompt, Question as BaseQuestion, ListQuestion } from 'inquirer'
import { Argument } from './argument'
import { Option } from './option'

type PromptType =
| 'input'
| 'number'
| 'confirm'
| 'list'
| 'rawlist'
| 'expand'
| 'checkbox'
| 'password'
| 'editor'

type Question = BaseQuestion | ListQuestion

/**
* Creates a new command, which can be added to a program.
*/
export function prompter<T = {}>(baseArgs: Array<Argument | Option>, args: T) {
return new Prompter(baseArgs, args)
}

export class Prompter<T = {}> {
constructor(private baseArgs: Array<Argument | Option>, private args: T) {}

public async prompt() {
const questions = this.getQuestions(this.args)

// Short circuit if there are no questions to ask.
if (!questions.length) {
return this.args
}

// Ask questions and merge with passed in args.
const answers = await prompt(questions)

return {
...this.args,
...answers,
}
}

/**
* Returns an array of arguments and options which should be prompted, because
* they are promptable (`isPromptable()` returned true) and they are not
* provided in the args passed in to this function.
*/
private getQuestions(args: T) {
// If we need to prompt for things, fill questions array
return this.baseArgs.reduce((questions, arg) => {
const name = arg.getName()
const presentInArgs = Object.constructor.hasOwnProperty.call(args, name)

// We're going to assume that if an argument/option still has its default
// value and it is promptable, it should have a question.
const defaultInArgs =
presentInArgs &&
arg.getDefault() &&
arg.getDefault() == (args as any)[name]

if (arg.isPromptable() && (!presentInArgs || defaultInArgs)) {
// Detect the type of question we need to ask
if (arg.getChoices()) {
// Use list question type
questions.push({
name,
type: 'list',
message: arg.getPrompt(),
choices: arg.getChoices() as string[],
})
} else if (arg.getType() == 'boolean') {
// Use list question type
questions.push({
name,
type: 'list',
message: arg.getPrompt(),
choices: [
{
value: true,
name: 'Yes',
},
{
value: false,
name: 'No',
},
],
})
} else {
// Default question type is input
questions.push({
name,
type: '',
message: arg.getPrompt(),
})
}
}

return questions
}, [] as Question[])
}
}

0 comments on commit 5bc9595

Please sign in to comment.