cli-chain
Version:
chainable cli creation, minimal, interactive, powerful.
354 lines (301 loc) • 8.97 kB
JavaScript
/**
* @file core of cli-chain
* @exports Core
* @requires flipchain/ChainedMap
* @requires flipchain/ChainedSet
* @requires fliplog
* @requires ./Steps
* @requires ./Program
* @requires ./Question
* @requires ./Choice
*/
const {ChainedMap, ChainedSet, log} = require('./deps')
const Program = require('./Program')
const Stepper = require('./Stepper')
const Steps = require('./Steps')
const Question = require('./Question')
const Choice = require('./Choice')
/**
* @example
* story (name, is usable for multiple stories to run)
* step (name, type = checkboxes)
* group
* input (type = checkbox, input, confirm)
* input
* group
* input
* step (this is where it gets tricky eh)
* group
* input
*
* step
* group...
*
* @prop {ChainedSet<string, Function>} answerCallbacks callbacks to call for each answer
* @prop {Stepper} stepper steps through the steps 1 by 1
* @prop {answers} answers from stepper.data, by reference
* @prop {Steps} steps steps to use in stepper
* @prop {Question} current last step accessed, for easier chaining
* @type {ChainedMap}
*/
class Core extends ChainedMap {
/**
* @see Core.program
* @return {Core}
*/
static program(...args) {
return Core.init().program(...args)
}
/**
* @param {Core | StepChild | any} [parent]
* @return {Core}
*/
static init(parent) {
return new Core(parent)
}
/**
* @param {Core | StepChild | any} [parent]
*/
constructor(parent) {
super(parent).handleParent(parent)
this.answerCallbacks = new ChainedSet(this)
this.stepper = new Stepper(this)
this.answers = this.stepper.data
this.steps = new Steps(this)
}
/**
* @protected
* @desc binds parent methods to child for easy chaining back up
* @param {any} [parent]
*/
handleParent(parent) {
if (!this.parent || !this.parent.delimiter) return
this.delimiter = this.parent.delimiter.bind(this.parent)
this.parse = this.parent.parse.bind(this.parent)
this.show = this.parent.show.bind(this.parent)
this.command = this.parent.command.bind(this.parent)
this.alias = this.parent.alias.bind(this.parent)
this.action = this.parent.action.bind(this.parent)
}
/**
* @protected
* @desc decorates the latest step for easy chaining
* @return {Core} @chainable
*/
decorateCurrent() {
this.current.step = this.step.bind(this)
// this.current.confirm = this.confirm.bind(this)
// this.current.input = this.input.bind(this)
// this.current.list = this.list.bind(this)
// this.current.stepChild = this.stepChild.bind(this)
this.current.stepChild = this.stepChild.bind(this.current)
this.current.child = this.child.bind(this)
this.current.then = this.then.bind(this)
if (this.parent && this.parent.delimiter) {
this.current.delimiter = this.parent.delimiter.bind(this.parent)
this.current.parse = this.parent.parse.bind(this.parent)
this.current.show = this.parent.show.bind(this.parent)
this.current.command = this.parent.command.bind(this.parent)
}
return this
}
// --- program ---
/**
* @param {VorpalArgs} args args to pass to Vorpal
* @return {Program}
*/
program(...args) {
return new Program(this).program(...args)
}
/**
* @desc function to call each time a user inputs an answer
* @param {Function} [cb=Function.prototype]
* @return {Core} @chainable
*/
onAnswers(cb = Function.prototype) {
this.answerCallbacks.add(cb)
return this
}
/**
* @desc after all answers are done,
* since we chain the promises
* it is a delayed promise resolve
*
* @param {Function} cb
* @return {Core} @chainable
*/
then(cb) {
return this.set('then', cb)
}
/**
* @desc calls toConfig,
* passes in .then to stepper when there is one,
* starts the stepper
*
* @return {Promise}
*/
run() {
const steps = this.toConfig().steps
this.thenner = this.stepper.start(steps)
if (this.has('then') === true) {
this.thenner.then(this.get('then'))
}
return this.thenner
}
/**
* @alias prompt
* @alias choice
*
* @desc add a step, which is a new Question
* @param {string} name key for the step, ends up on answers
* @param {string} [type='checkbox']
* @param {string} [msg=null] defaults to `name`, text that is shown
* @return {StepChild} @chainable
*/
step(name, type = 'checkbox', msg = null) {
if (msg === null) msg = name
this.current = new Question(this).name(name).type(type).message(msg)
this.decorateCurrent()
this.steps.add(this.current)
return this.current
}
/**
* @TODO: abstract this and stepChild
* @TODO: pass in condition (when) here optionally
* @desc similar to stepChild, but adds to THIS.steps, not PARENT.steps
* @see this.stepChild, StepChild
* @param {string} name key for the step, ends up on answers
* @param {string} type any valid vorpal `type`
* @param {string} [msg=null] defaults to `name`, text that is shown
* @return {StepChild} @chainable
*/
child(name, type, msg = null) {
// eslint-disable-next-line
const current = new StepChild(this)
// improve... maybe need to loop... only 2 levels
// store ref
// .data.values().pop()
const parent = this.current.get('name')
let currentChoice = this.current
if (currentChoice.current) currentChoice = currentChoice.current
currentChoice = currentChoice.get('name')
const step = current.step(name, type, msg)
step.when(answers => {
// parent.childName
let key = parent + '.' + currentChoice
// get answer
let parentAnswer = this.answers[parent]
let result
// checkboxes
if (Array.isArray(parentAnswer)) {
result = parentAnswer.includes(key)
}
else {
// input
result = !!parentAnswer[key]
}
// fallback
if (!result) {
key = parent
parentAnswer = this.answers[parent]
// if it was a confirm y/n
if (parentAnswer === true) {
result = parentAnswer
}
else if (Array.isArray(parentAnswer)) {
// checkboxes
result = parentAnswer.includes(key)
}
else {
// input?
result = parentAnswer[key]
}
}
return result
})
this.steps.add(current)
return step
}
/**
* @desc add a child step that has an auto-setup .when condition to be used
* @param {string} name key for the step, ends up on answers
* @param {string} [type='list'] any valid vorpal `type`
* @param {string} [msg=null] defaults to `name`, text that is shown
* @return {StepChild} @chainable
*/
stepChild(name, type = 'list', msg = null) {
// eslint-disable-next-line
const current = new StepChild(this)
const parent = this.current.get('name')
let currentChoice = this.current
if (currentChoice.current) currentChoice = currentChoice.current
currentChoice = currentChoice.get('name')
const step = current.step(name, type, msg)
step.when(answers => {
// parent.childName
let key = parent + '.' + currentChoice
// get answer
let parentAnswer = this.answers[parent]
let result
// checkboxes
if (Array.isArray(parentAnswer)) {
result = parentAnswer.includes(key)
}
else {
// input
result = !!parentAnswer[key]
}
// fallback
if (!result) {
key = parent
parentAnswer = this.answers[parent]
// if it was a confirm y/n
if (parentAnswer === true) {
result = parentAnswer
}
else if (Array.isArray(parentAnswer)) {
// checkboxes
result = parentAnswer.includes(key)
}
else {
// input?
result = parentAnswer[key]
}
}
return result
})
this.parent.steps.add(current)
return step
}
toConfig() {
return this.steps.toConfig()
}
// --- todo ---
// story() {}
// but it is optional :s
// group is section / QYUESTIONS...
// group() {}
// goToStep
// getStep
// steps() {}
// checkbox(name, msg) { return this.step(name, msg, 'checkbox') }
// confirm(name, msg) { return this.step(name, msg, 'confirm') }
// input(name, msg) { return this.step(name, msg, 'input') }
// list(name, msg) { return this.step(name, msg, 'list') }
}
class StepChild extends Core {
// step(name, type = 'checkbox', msg = null) {
// return this.end().step(name, type, msg)
// }
/**
* @desc runs toConfig, then returns only the first step
* @return {Step}
*/
toConfig() {
const steps = this.steps.toConfig().steps
const first = steps.shift()
return first
}
}
Core.StepChild = StepChild
module.exports = Core