0.4.0 • Published 6 months ago

@yankeeinlondon/ask v0.4.0

Weekly downloads
-
License
MIT
Repository
-
Last release
6 months ago

ask

strongly typed builder patterns wrapping the popular inquirer CLI utility

Overview

I have turned to inquirer many times over the years when I need to build an interactive dialog with a user via the terminal. It's a great utility but because I'm not a regular user I keep on having to reacquaint myself with it and it's types each time.

The library is so good i'm sure that a regular user would be happy to use "as is" but I find the relearning curve a bit too steep. Admittedly I like my food to fed to me in a small spoon. Anyway, this library wraps the inquirer library and attempts to provide two builders: ask and survey.

Ask Builder

  • allows the lazy creation of questions with a strongly typed builder pattern:

    import { ask } from "@yankeeinlondon/ask";
    
    const name = ask.input("name", "What is your name?");
    const age = ask.number("age", "How old are you?", { min: 1, max: 150 });

    Note: questions can be asked this way only when they don't have any requirements; more on this later.

  • these questions can then be asked directly by simply calling them like this:

    // string
    const name = await name();
  • these questions can also be asked in a manner where they return a key/value dictionary which allows for context to be built up:

    // { name: string }
    const name = await name.ask();
    // { name: string; age: number }
    const age = await name.ask(name);

    If you're using this format, it's likely you should probably just use a survey (see next section).

Survey Builder

The Survey builder is intended to compose several questions together:

const nameAndAge = survey(ask, age);

Above we've configured a pipeline of questions to be asked. To ask them we call .start():

// { name: string; age: number }
const answers = await nameAndAge.start();

Choices

Many of the question types -- such as select, checkbox, rawlist, and expand -- ask that you provide a list of choices for the user to choose from.

For questions which have choices, the third parameter -- after the property name and message/prompt -- will be those choices. You have several options in which you can express these choices so let's review them:

/**
 * a simple array.
 *
 * The elements in the array become both the keys and values of the
 * choices.
 */
const color = ask.select(
  "color",
  "What is your favorite color?",
  ["red", "blue", "green"]
);

// using a simple key/value notation
// -----------------------------------------------------------
// the KEYS are the "names" of the choices, the VALUES are
// the actual value the answer will return.
const color_obj = ask.select(
  "color",
  "What is your favorite color?",
  {
    Red: "red",
    Blue: "blue",
    Green: "green"
  }
);

/**
 *  Using a key/value where value is a tuple:
 *
 * this allows you set both the value AND a description
 */
const color_obj = ask.select(
  "color",
  "What is your favorite color?",
  {
    Red: ["red", "Red like a rose"],
    Blue: ["blue", "Blue like the sky"],
    Green: ["green", "Green like grass"]
  }
);

/**
 * Using the DictProxy shorthand
 *
 * this allows any of the props available in the fully qualified
 * `Choice` type from being expressed:
 *
 * - the "key" is the "name"
 * - you must state the "value"; otherwise all other props
 * are optional
 */

const color_proxy = ask.select(
  "color",
  "What is your favorite color?",
  {
    Red: { value: "red", description: "Red like a rose" },
    Blue: { value: "blue", key: "b" },
    Green: { value: "green", short: "gr" }
  }
);

Any question type which has choices provides the same call signature and variants for representing the choices.

Advanced Features

We support all the core question types that inquirer does along with the options exposed by these various question types. In addition we've added a few advanced features that don't come "out of the box" with inquirer:

withRequirements

Any question can express it's dependencies it expects to be fulfilled prior to be being asked:

const cont = ask
  .withRequirements({ name: "string", age: "number" })
  .confirm("continue", "Continue with installation?");

Unlike the previous questions, this one expects that name (as a string) and age (as a number) will be provided to the question. Attempts to call this question without these parameters (aka, "ask it") will be met with a type error (if you're using TS). However, asking this question is simple enough, even in an "atomic" use case like ask:

// user is prompted if they would like to continue
const shallWeContinue = await cont({ name: "Bob Marley", age: 45 });

This use of "requirements" becomes even more useful in the next section when we look at the survey builder.

abortTimeout and acceptTimeout

TODO

Conditionals / Branching

In addition to being able to compose questions in a simple chain (one which does honor the when clause); there are two types of conditional clauses which can create branching behavior:

  • branchIf() - if a certain boolean condition is met then run another survey before returning to the complete the current one

    Loosely building off our prior examples, let's look at this operator in action:

    import { ask, branchIf, survey } from "@yankeeinlondon/ask";
    
    const why = survey(
      ask.input("why", "Can you tell us why you don't want to install?")
    );
    
    const install = survey(
      name,
      age,
      cont,
      branchIf(a => isDefined(a.continue))
    );

    In this example, if a user says they don't want to continue, we will ask them why.

    This example's simplicity is maybe a shortcoming as we could have easily just included another question at the end with the when option set but by having this conditional expression we're now able to branch out to a set of new questions based on any boolean logic we can express.

    Note: while in this example the condition was the last expression in the survey, this is not required, the branchIf expression can be placed anywhere in the survey and when it complete's this branch it will come back to the original survey and finish it.

  • split() - based on a boolean condition, move to one survey versus another

if(condition, survey) branching

split(test, survey1, survey2) branching

Question types Supported

All core questions from Inquirer:

  • input - text input
  • select - choose one item from a list
  • checkbox - choose multiple items from a list
  • confirm - get a binary yes/no response from the user
  • search -
  • password - masked text input
  • expand - take actions with shortcut keys
  • editor -
  • number - numeric input
  • rawlist -
0.4.0

6 months ago

0.3.0

7 months ago

0.3.2

7 months ago

0.3.1

7 months ago

0.2.12

7 months ago

0.2.11

7 months ago

0.2.10

7 months ago

0.2.9

10 months ago

0.2.8

10 months ago

0.2.7

10 months ago

0.2.6

10 months ago

0.2.5

10 months ago

0.2.4

10 months ago

0.2.3

10 months ago

0.2.2

10 months ago