[Pkg-javascript-commits] [node-inquirer] 01/03: Import Upstream version 2.0.0

Paolo Greppi paolog-guest at moszumanska.debian.org
Fri Dec 16 17:50:16 UTC 2016


This is an automated email from the git hooks/post-receive script.

paolog-guest pushed a commit to branch master
in repository node-inquirer.

commit a9baed0c5ade0972c277d1652fb65e1bed50f0fe
Author: Paolo Greppi <paolo.greppi at libpf.com>
Date:   Fri Dec 16 17:42:07 2016 +0000

    Import Upstream version 2.0.0
---
 .editorconfig                    |  11 +
 .gitattributes                   |   1 +
 .gitignore                       |   2 +
 .travis.yml                      |   6 +
 LICENSE-MIT                      |  22 ++
 README.md                        | 353 +++++++++++++++++++++++
 assets/inquirer_large.png        | Bin 0 -> 6614 bytes
 assets/inquirer_med.png          | Bin 0 -> 3920 bytes
 assets/inquirer_readme.png       | Bin 0 -> 3996 bytes
 assets/inquirer_small.png        | Bin 0 -> 3038 bytes
 examples/bottom-bar.js           |  24 ++
 examples/checkbox.js             |  66 +++++
 examples/editor.js               |  25 ++
 examples/expand.js               |  39 +++
 examples/hierarchical.js         |  85 ++++++
 examples/input.js                |  39 +++
 examples/list.js                 |  36 +++
 examples/long-list.js            |  35 +++
 examples/nested-call.js          |  20 ++
 examples/password.js             |  16 ++
 examples/pizza.js                |  98 +++++++
 examples/rawlist.js              |  32 +++
 examples/recursive.js            |  35 +++
 examples/rx-observable-array.js  |  45 +++
 examples/rx-observable-create.js |  38 +++
 examples/when.js                 |  46 +++
 gulpfile.js                      |  62 ++++
 lib/inquirer.js                  |  84 ++++++
 lib/objects/choice.js            |  35 +++
 lib/objects/choices.js           | 112 ++++++++
 lib/objects/separator.js         |  34 +++
 lib/prompts/base.js              | 129 +++++++++
 lib/prompts/checkbox.js          | 235 +++++++++++++++
 lib/prompts/confirm.js           | 106 +++++++
 lib/prompts/editor.js            | 111 +++++++
 lib/prompts/expand.js            | 260 +++++++++++++++++
 lib/prompts/input.js             | 104 +++++++
 lib/prompts/list.js              | 187 ++++++++++++
 lib/prompts/password.js          | 115 ++++++++
 lib/prompts/rawlist.js           | 179 ++++++++++++
 lib/ui/baseUI.js                 |  69 +++++
 lib/ui/bottom-bar.js             | 101 +++++++
 lib/ui/prompt.js                 | 116 ++++++++
 lib/utils/events.js              |  45 +++
 lib/utils/paginator.js           |  37 +++
 lib/utils/readline.js            |  51 ++++
 lib/utils/screen-manager.js      | 129 +++++++++
 lib/utils/utils.js               |  26 ++
 package.json                     |  77 +++++
 test/before.js                   |  10 +
 test/bin/write.js                |   9 +
 test/helpers/events.js           |  13 +
 test/helpers/fixtures.js         |  59 ++++
 test/helpers/readline.js         |  36 +++
 test/specs/api.js                | 348 ++++++++++++++++++++++
 test/specs/inquirer.js           | 603 +++++++++++++++++++++++++++++++++++++++
 test/specs/objects/choice.js     |  43 +++
 test/specs/objects/choices.js    |  75 +++++
 test/specs/objects/separator.js  |  36 +++
 test/specs/prompts/base.js       |  25 ++
 test/specs/prompts/checkbox.js   | 258 +++++++++++++++++
 test/specs/prompts/confirm.js    |  90 ++++++
 test/specs/prompts/editor.js     |  31 ++
 test/specs/prompts/expand.js     | 146 ++++++++++
 test/specs/prompts/input.js      |  38 +++
 test/specs/prompts/list.js       | 172 +++++++++++
 test/specs/prompts/password.js   |  24 ++
 test/specs/prompts/rawlist.js    |  75 +++++
 68 files changed, 5569 insertions(+)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..beffa30
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..176a458
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba2a97b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+coverage
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..68aff2a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+sudo: false
+language: node_js
+node_js:
+  - 4
+  - 6
+  - node
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..8aae090
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Simon Boudrias
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..68c25e6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,353 @@
+Inquirer.js
+===========
+
+[![npm](https://badge.fury.io/js/inquirer.svg)](http://badge.fury.io/js/inquirer) [![tests](https://travis-ci.org/SBoudrias/Inquirer.js.svg?branch=master)](http://travis-ci.org/SBoudrias/Inquirer.js) [![Coverage Status](https://coveralls.io/repos/yeoman/generator/badge.svg)](https://coveralls.io/r/SBoudrias/Inquirer.js) [![dependencies](https://david-dm.org/SBoudrias/Inquirer.js.svg?theme=shields.io)](https://david-dm.org/SBoudrias/Inquirer.js)
+
+A collection of common interactive command line user interfaces.
+
+## Table of Contents
+
+  1. [Documentation](#documentation)
+    1. [Installation](#installation)
+    2. [Examples](#examples)
+    3. [Methods](#methods)
+    4. [Objects](#objects)
+      1. [Questions](#questions)
+      2. [Answers](#answers)
+      3. [Separator](#separator)
+    4. [Prompt Types](#prompt)
+  2. [User Interfaces and Layouts](#layouts)
+    1. [Reactive Interface](#reactive)
+  3. [Support](#support)
+  4. [News](#news)
+  5. [Contributing](#contributing)
+  6. [License](#license)
+
+
+## Goal and Philosophy
+
+<img align="right" alt="Inquirer Logo" src="/assets/inquirer_readme.png" title="Inquirer.js"/>
+
+**`Inquirer.js`** strives to be an easily embeddable and beautiful command line interface for [Node.js](https://nodejs.org/) (and perhaps the "CLI [Xanadu](https://en.wikipedia.org/wiki/Citizen_Kane)").
+
+**`Inquirer.js`** should ease the process of
+- providing *error feedback*
+- *asking questions*
+- *parsing* input
+- *validating* answers
+- managing *hierarchical prompts*
+
+> **Note:** **`Inquirer.js`** provides the user interface and the inquiry session flow. If you're searching for a full blown command line program utility, then check out [commander](https://github.com/visionmedia/commander.js), [vorpal](https://github.com/dthree/vorpal) or [args](https://github.com/leo/args).
+
+
+## [Documentation](#documentation)
+<a name="documentation"></a>
+
+### Installation
+<a name="installation"></a>
+
+``` shell
+npm install inquirer
+```
+
+```javascript
+var inquirer = require('inquirer');
+inquirer.prompt([/* Pass your questions in here */]).then(function (answers) {
+	// Use user feedback for... whatever!!
+});
+```
+
+<a name="examples"></a>
+### Examples (Run it and see it)
+Check out the `examples/` folder for code and interface examples.
+
+``` shell
+node examples/pizza.js
+node examples/checkbox.js
+# etc...
+```
+
+
+### Methods
+<a name="methods"></a>
+#### `inquirer.prompt(questions) -> promise`
+
+Launch the prompt interface (inquiry session)
+
+- **questions** (Array) containing [Question Object](#question) (using the [reactive interface](#reactive-interface), you can also pass a `Rx.Observable` instance)
+- returns a **Promise**
+
+#### `inquirer.registerPrompt(name, prompt)`
+
+Register prompt plugins under `name`.
+
+- **name** (string) name of the this new prompt. (used for question `type`)
+- **prompt** (object) the prompt object itself (the plugin)
+
+#### `inquirer.createPromptModule() -> prompt function`
+
+Create a self contained inquirer module. If don't want to affect other libraries that also rely on inquirer when you overwrite or add new prompt types.
+
+```js
+var prompt = inquirer.createPromptModule();
+
+prompt(questions).then(/* ... */);
+```
+
+### Objects
+<a name="objects"></a>
+
+#### Question
+<a name="questions"></a>
+A question object is a `hash` containing question related values:
+
+- **type**: (String) Type of the prompt. Defaults: `input` - Possible values: `input`, `confirm`,
+`list`, `rawlist`, `expand`, `checkbox`, `password`, `editor`
+- **name**: (String) The name to use when storing the answer in the answers hash. If the name contains periods, it will define a path in the answers hash.
+- **message**: (String|Function) The question to print. If defined as a function, the first parameter will be the current inquirer session answers.
+- **default**: (String|Number|Array|Function) Default value(s) to use if nothing is entered, or a function that returns the default value(s). If defined as a function, the first parameter will be the current inquirer session answers.
+- **choices**: (Array|Function) Choices array or a function returning a choices array. If defined as a function, the first parameter will be the current inquirer session answers.
+Array values can be simple `strings`, or `objects` containing a `name` (to display in list), a `value` (to save in the answers hash) and a `short` (to display after selection) properties. The choices array can also contain [a `Separator`](#separator).
+- **validate**: (Function) Receive the user input and should return `true` if the value is valid, and an error message (`String`) otherwise. If `false` is returned, a default error message is provided.
+- **filter**: (Function) Receive the user input and return the filtered value to be used inside the program. The value returned will be added to the _Answers_ hash.
+- **when**: (Function, Boolean) Receive the current user answers hash and should return `true` or `false` depending on whether or not this question should be asked. The value can also be a simple boolean.
+
+`default`, `choices`(if defined as functions), `validate`, `filter` and `when` functions can be called asynchronous. Either return a promise or use `this.async()` to get a callback you'll call with the final value.
+
+``` javascript
+{
+  /* Preferred way: with promise */
+  filter: function () {
+    return new Promise(/* etc... */);
+  },
+
+  /* Legacy way: with this.async */
+  validate: function (input) {
+    // Declare function as asynchronous, and save the done callback
+    var done = this.async();
+
+    // Do async stuff
+    setTimeout(function () {
+      if (typeof input !== 'number') {
+        // Pass the return value in the done callback
+        done('You need to provide a number');
+        return;
+      }
+      // Pass the return value in the done callback
+      done(null, true);
+    }, 3000);
+  }
+}
+```
+
+### Answers
+<a name="answers"></a>
+A key/value hash containing the client answers in each prompt.
+
+- **Key** The `name` property of the _question_ object
+- **Value** (Depends on the prompt)
+  - `confirm`: (Boolean)
+  - `input` : User input (filtered if `filter` is defined) (String)
+  - `rawlist`, `list` : Selected choice value (or name if no value specified) (String)
+
+### Separator
+<a name="separator"></a>
+A separator can be added to any `choices` array:
+
+```
+// In the question object
+choices: [ "Choice A", new inquirer.Separator(), "choice B" ]
+
+// Which'll be displayed this way
+[?] What do you want to do?
+ > Order a pizza
+   Make a reservation
+   --------
+   Ask opening hours
+   Talk to the receptionist
+```
+
+The constructor takes a facultative `String` value that'll be use as the separator. If omitted, the separator will be `--------`.
+
+Separator instances have a property `type` equal to `separator`. This should allow tools façading Inquirer interface from detecting separator types in lists.
+
+<a name="prompt"></a>
+### Prompt types
+---------------------
+
+> **Note:**: _allowed options written inside square brackets (`[]`) are optional. Others are required._
+
+#### List - `{type: 'list'}`
+
+Take `type`, `name`, `message`, `choices`[, `default`, `filter`] properties. (Note that
+default must be the choice `index` in the array or a choice `value`)
+
+![List prompt](https://dl.dropboxusercontent.com/u/59696254/inquirer/list-prompt.png)
+
+---
+
+#### Raw List - `{type: 'rawlist'}`
+
+Take `type`, `name`, `message`, `choices`[, `default`, `filter`] properties. (Note that
+default must the choice `index` in the array)
+
+![Raw list prompt](https://i.cloudup.com/LcRGpXI0CX-3000x3000.png)
+
+---
+
+#### Expand - `{type: 'expand'}`
+
+Take `type`, `name`, `message`, `choices`[, `default`] properties. (Note that
+default must be the choice `index` in the array. If `default` key not provided, then `help` will be used as default choice)
+
+Note that the `choices` object will take an extra parameter called `key` for the `expand` prompt. This parameter must be a single (lowercased) character. The `h` option is added by the prompt and shouldn't be defined by the user.
+
+See `examples/expand.js` for a running example.
+
+![Expand prompt closed](https://dl.dropboxusercontent.com/u/59696254/inquirer/expand-prompt-1.png)
+![Expand prompt expanded](https://dl.dropboxusercontent.com/u/59696254/inquirer/expand-prompt-2.png)
+
+---
+
+#### Checkbox - `{type: 'checkbox'}`
+
+Take `type`, `name`, `message`, `choices`[, `filter`, `validate`, `default`] properties. `default` is expected to be an Array of the checked choices value.
+
+Choices marked as `{checked: true}` will be checked by default.
+
+Choices whose property `disabled` is truthy will be unselectable. If `disabled` is a string, then the string will be outputted next to the disabled choice, otherwise it'll default to `"Disabled"`. The `disabled` property can also be a synchronous function receiving the current answers as argument and returning a boolean or a string.
+
+![Checkbox prompt](https://dl.dropboxusercontent.com/u/59696254/inquirer/checkbox-prompt.png)
+
+---
+
+#### Confirm - `{type: 'confirm'}`
+
+Take `type`, `name`, `message`[, `default`] properties. `default` is expected to be a boolean if used.
+
+![Confirm prompt](https://dl.dropboxusercontent.com/u/59696254/inquirer/confirm-prompt.png)
+
+---
+
+#### Input - `{type: 'input'}`
+
+Take `type`, `name`, `message`[, `default`, `filter`, `validate`] properties.
+
+![Input prompt](https://dl.dropboxusercontent.com/u/59696254/inquirer/input-prompt.png)
+
+---
+
+#### Password - `{type: 'password'}`
+
+Take `type`, `name`, `message`[, `default`, `filter`, `validate`] properties.
+
+![Password prompt](https://dl.dropboxusercontent.com/u/59696254/inquirer/password-prompt.png)
+
+---
+
+#### Editor - `{type: 'editor'}`
+
+Take `type`, `name`, `message`[, `default`, `filter`, `validate`] properties
+
+Launches an instance of the users preferred editor on a temporary file. Once the user exits their editor, the contents of the temporary file are read in as the result. The editor to use is determined by reading the $VISUAL or $EDITOR environment variables. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.
+
+<a name="layouts"></a>
+## User Interfaces and layouts
+
+
+Along with the prompts, Inquirer offers some basic text UI.
+
+#### Bottom Bar - `inquirer.ui.BottomBar`
+
+This UI present a fixed text at the bottom of a free text zone. This is useful to keep a message to the bottom of the screen while outputting command outputs on the higher section.
+
+```javascript
+var ui = new inquirer.ui.BottomBar();
+
+// pipe a Stream to the log zone
+outputStream.pipe(ui.log);
+
+// Or simply write output
+ui.log.write('something just happened.');
+ui.log.write('Almost over, standby!');
+
+// During processing, update the bottom bar content to display a loader
+// or output a progress bar, etc
+ui.updateBottomBar('new bottom bar content');
+```
+
+<a name="reactive"></a>
+## Reactive interface
+
+
+Internally, Inquirer uses the [JS reactive extension](https://github.com/Reactive-Extensions/RxJS) to handle events and async flows.
+
+This mean you can take advantage of this feature to provide more advanced flows. For example, you can dynamically add questions to be asked:
+
+```js
+var prompts = new Rx.Subject();
+inquirer.prompt(prompts);
+
+// At some point in the future, push new questions
+prompts.onNext({ /* question... */ });
+prompts.onNext({ /* question... */ });
+
+// When you're done
+prompts.onCompleted();
+```
+
+And using the return value `process` property, you can access more fine grained callbacks:
+
+```js
+inquirer.prompt(prompts).ui.process.subscribe(
+  onEachAnswer,
+  onError,
+  onComplete
+);
+```
+
+## Support (OS Terminals)
+<a name="support"></a>
+
+You should expect mostly good support for the CLI below. This does not mean we won't
+look at issues found on other command line - feel free to report any!
+
+- **Mac OS**:
+  - Terminal.app
+  - iTerm
+- **Windows**:
+  - [ConEmu](https://conemu.github.io/)
+  - cmd.exe
+  - Powershell
+  - Cygwin
+- **Linux (Ubuntu, openSUSE, Arch Linux, etc)**:
+  - gnome-terminal (Terminal GNOME)
+  - konsole
+
+
+## News on the march (Release notes)
+<a name="news"></a>
+
+
+Please refer to the [Github releases section for the changelog](https://github.com/SBoudrias/Inquirer.js/releases)
+
+
+## Contributing
+<a name="contributing"></a>
+
+**Unit test**
+Unit test are written in [Mocha](https://mochajs.org/). Please add a unit test for every new feature or bug fix. `npm test` to run the test suite.
+
+**Documentation**
+Add documentation for every API change. Feel free to send typo fixes and better docs!
+
+We're looking to offer good support for multiple prompts and environments. If you want to
+help, we'd like to keep a list of testers for each terminal/OS so we can contact you and
+get feedback before release. Let us know if you want to be added to the list (just tweet
+to [@vaxilart](https://twitter.com/Vaxilart)) or just add your name to [the wiki](https://github.com/SBoudrias/Inquirer.js/wiki/Testers)
+
+## License
+<a name="license"></a>
+
+Copyright (c) 2016 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))
+Licensed under the MIT license.
diff --git a/assets/inquirer_large.png b/assets/inquirer_large.png
new file mode 100644
index 0000000..0e5388e
Binary files /dev/null and b/assets/inquirer_large.png differ
diff --git a/assets/inquirer_med.png b/assets/inquirer_med.png
new file mode 100644
index 0000000..f676cee
Binary files /dev/null and b/assets/inquirer_med.png differ
diff --git a/assets/inquirer_readme.png b/assets/inquirer_readme.png
new file mode 100644
index 0000000..37478f1
Binary files /dev/null and b/assets/inquirer_readme.png differ
diff --git a/assets/inquirer_small.png b/assets/inquirer_small.png
new file mode 100644
index 0000000..da77f05
Binary files /dev/null and b/assets/inquirer_small.png differ
diff --git a/examples/bottom-bar.js b/examples/bottom-bar.js
new file mode 100644
index 0000000..1432268
--- /dev/null
+++ b/examples/bottom-bar.js
@@ -0,0 +1,24 @@
+var BottomBar = require('../lib/ui/bottom-bar');
+var cmdify = require('cmdify');
+
+var loader = [
+  '/ Installing',
+  '| Installing',
+  '\\ Installing',
+  '- Installing'
+];
+var i = 4;
+var ui = new BottomBar({bottomBar: loader[i % 4]});
+
+setInterval(function () {
+  ui.updateBottomBar(loader[i++ % 4]);
+}, 300);
+
+var spawn = require('child_process').spawn;
+
+var cmd = spawn(cmdify('npm'), ['-g', 'install', 'inquirer'], {stdio: 'pipe'});
+cmd.stdout.pipe(ui.log);
+cmd.on('close', function () {
+  ui.updateBottomBar('Installation done!\n');
+  process.exit();
+});
diff --git a/examples/checkbox.js b/examples/checkbox.js
new file mode 100644
index 0000000..6bd814e
--- /dev/null
+++ b/examples/checkbox.js
@@ -0,0 +1,66 @@
+/**
+ * Checkbox list examples
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt([
+  {
+    type: 'checkbox',
+    message: 'Select toppings',
+    name: 'toppings',
+    choices: [
+      new inquirer.Separator(' = The Meats = '),
+      {
+        name: 'Pepperoni'
+      },
+      {
+        name: 'Ham'
+      },
+      {
+        name: 'Ground Meat'
+      },
+      {
+        name: 'Bacon'
+      },
+      new inquirer.Separator(' = The Cheeses = '),
+      {
+        name: 'Mozzarella',
+        checked: true
+      },
+      {
+        name: 'Cheddar'
+      },
+      {
+        name: 'Parmesan'
+      },
+      new inquirer.Separator(' = The usual ='),
+      {
+        name: 'Mushroom'
+      },
+      {
+        name: 'Tomato'
+      },
+      new inquirer.Separator(' = The extras = '),
+      {
+        name: 'Pineapple'
+      },
+      {
+        name: 'Olives',
+        disabled: 'out of stock'
+      },
+      {
+        name: 'Extra cheese'
+      }
+    ],
+    validate: function (answer) {
+      if (answer.length < 1) {
+        return 'You must choose at least one topping.';
+      }
+      return true;
+    }
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/editor.js b/examples/editor.js
new file mode 100644
index 0000000..6848668
--- /dev/null
+++ b/examples/editor.js
@@ -0,0 +1,25 @@
+/**
+ * Editor prompt example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+var questions = [
+  {
+    type: 'editor',
+    name: 'bio',
+    message: 'Please write a short bio of at least 3 lines.',
+    validate: function (text) {
+      if (text.split('\n').length < 3) {
+        return 'Must be at least 3 lines.';
+      }
+
+      return true;
+    }
+  }
+];
+
+inquirer.prompt(questions).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/expand.js b/examples/expand.js
new file mode 100644
index 0000000..c2bd6f2
--- /dev/null
+++ b/examples/expand.js
@@ -0,0 +1,39 @@
+/**
+ * Expand list examples
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt([
+  {
+    type: 'expand',
+    message: 'Conflict on `file.js`: ',
+    name: 'overwrite',
+    choices: [
+      {
+        key: 'y',
+        name: 'Overwrite',
+        value: 'overwrite'
+      },
+      {
+        key: 'a',
+        name: 'Overwrite this one and all next',
+        value: 'overwrite_all'
+      },
+      {
+        key: 'd',
+        name: 'Show diff',
+        value: 'diff'
+      },
+      new inquirer.Separator(),
+      {
+        key: 'x',
+        name: 'Abort',
+        value: 'abort'
+      }
+    ]
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/hierarchical.js b/examples/hierarchical.js
new file mode 100644
index 0000000..84d3ac2
--- /dev/null
+++ b/examples/hierarchical.js
@@ -0,0 +1,85 @@
+/**
+ * Heirarchical conversation example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+var directionsPrompt = {
+  type: 'list',
+  name: 'direction',
+  message: 'Which direction would you like to go?',
+  choices: ['Forward', 'Right', 'Left', 'Back']
+};
+
+function main() {
+  console.log('You find youself in a small room, there is a door in front of you.');
+  exitHouse();
+}
+
+function exitHouse() {
+  inquirer.prompt(directionsPrompt).then(function (answers) {
+    if (answers.direction === 'Forward') {
+      console.log('You find yourself in a forest');
+      console.log('There is a wolf in front of you; a friendly looking dwarf to the right and an impasse to the left.');
+      encounter1();
+    } else {
+      console.log('You cannot go that way. Try again');
+      exitHouse();
+    }
+  });
+}
+
+function encounter1() {
+  inquirer.prompt(directionsPrompt).then(function (answers) {
+    var direction = answers.direction;
+    if (direction === 'Forward') {
+      console.log('You attempt to fight the wolf');
+      console.log('Theres a stick and some stones lying around you could use as a weapon');
+      encounter2b();
+    } else if (direction === 'Right') {
+      console.log('You befriend the dwarf');
+      console.log('He helps you kill the wolf. You can now move forward');
+      encounter2a();
+    } else {
+      console.log('You cannot go that way');
+      encounter1();
+    }
+  });
+}
+
+function encounter2a() {
+  inquirer.prompt(directionsPrompt).then(function (answers) {
+    var direction = answers.direction;
+    if (direction === 'Forward') {
+      var output = 'You find a painted wooden sign that says:';
+      output += ' \n';
+      output += ' ____  _____  ____  _____ \n';
+      output += '(_  _)(  _  )(  _ \\(  _  ) \n';
+      output += '  )(   )(_)(  )(_) ))(_)(  \n';
+      output += ' (__) (_____)(____/(_____) \n';
+      console.log(output);
+    } else {
+      console.log('You cannot go that way');
+      encounter2a();
+    }
+  });
+}
+
+function encounter2b() {
+  inquirer.prompt({
+    type: 'list',
+    name: 'weapon',
+    message: 'Pick one',
+    choices: [
+      'Use the stick',
+      'Grab a large rock',
+      'Try and make a run for it',
+      'Attack the wolf unarmed'
+    ]
+  }).then(function () {
+    console.log('The wolf mauls you. You die. The end.');
+  });
+}
+
+main();
diff --git a/examples/input.js b/examples/input.js
new file mode 100644
index 0000000..5324f2c
--- /dev/null
+++ b/examples/input.js
@@ -0,0 +1,39 @@
+/**
+ * Input prompt example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+var questions = [
+  {
+    type: 'input',
+    name: 'first_name',
+    message: 'What\'s your first name'
+  },
+  {
+    type: 'input',
+    name: 'last_name',
+    message: 'What\'s your last name',
+    default: function () {
+      return 'Doe';
+    }
+  },
+  {
+    type: 'input',
+    name: 'phone',
+    message: 'What\'s your phone number',
+    validate: function (value) {
+      var pass = value.match(/^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i);
+      if (pass) {
+        return true;
+      }
+
+      return 'Please enter a valid phone number';
+    }
+  }
+];
+
+inquirer.prompt(questions).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/list.js b/examples/list.js
new file mode 100644
index 0000000..31042a8
--- /dev/null
+++ b/examples/list.js
@@ -0,0 +1,36 @@
+/**
+ * List prompt example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt([
+  {
+    type: 'list',
+    name: 'theme',
+    message: 'What do you want to do?',
+    choices: [
+      'Order a pizza',
+      'Make a reservation',
+      new inquirer.Separator(),
+      'Ask for opening hours',
+      {
+        name: 'Contact support',
+        disabled: 'Unavailable at this time'
+      },
+      'Talk to the receptionist'
+    ]
+  },
+  {
+    type: 'list',
+    name: 'size',
+    message: 'What size do you need?',
+    choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'],
+    filter: function (val) {
+      return val.toLowerCase();
+    }
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/long-list.js b/examples/long-list.js
new file mode 100644
index 0000000..b48841d
--- /dev/null
+++ b/examples/long-list.js
@@ -0,0 +1,35 @@
+/**
+ * Paginated list
+ */
+
+'use strict';
+var inquirer = require('..');
+
+var choices = Array.apply(0, new Array(26)).map(function (x, y) {
+  return String.fromCharCode(y + 65);
+});
+choices.push('Multiline option \n  super cool feature');
+choices.push({
+  name: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.',
+  value: 'foo',
+  short: 'The long option'
+});
+
+inquirer.prompt([
+  {
+    type: 'list',
+    name: 'letter',
+    message: 'What\'s your favorite letter?',
+    paginated: true,
+    choices: choices
+  },
+  {
+    type: 'checkbox',
+    name: 'name',
+    message: 'Select the letter contained in your name:',
+    paginated: true,
+    choices: choices
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/nested-call.js b/examples/nested-call.js
new file mode 100644
index 0000000..f3f74c0
--- /dev/null
+++ b/examples/nested-call.js
@@ -0,0 +1,20 @@
+/**
+ * Nested Inquirer call
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt({
+  type: 'list',
+  name: 'chocolate',
+  message: 'What\'s your favorite chocolate?',
+  choices: ['Mars', 'Oh Henry', 'Hershey']
+}).then(function () {
+  inquirer.prompt({
+    type: 'list',
+    name: 'beverage',
+    message: 'And your favorite beverage?',
+    choices: ['Pepsi', 'Coke', '7up', 'Mountain Dew', 'Red Bull']
+  });
+});
diff --git a/examples/password.js b/examples/password.js
new file mode 100644
index 0000000..68ed4bf
--- /dev/null
+++ b/examples/password.js
@@ -0,0 +1,16 @@
+/**
+ * Password prompt example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt([
+  {
+    type: 'password',
+    message: 'Enter your git password',
+    name: 'password'
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/pizza.js b/examples/pizza.js
new file mode 100644
index 0000000..8fb6e30
--- /dev/null
+++ b/examples/pizza.js
@@ -0,0 +1,98 @@
+/**
+ * Pizza delivery prompt example
+ * run example by writing `node pizza.js` in your console
+ */
+
+'use strict';
+var inquirer = require('..');
+
+console.log('Hi, welcome to Node Pizza');
+
+var questions = [
+  {
+    type: 'confirm',
+    name: 'toBeDelivered',
+    message: 'Is this for delivery?',
+    default: false
+  },
+  {
+    type: 'input',
+    name: 'phone',
+    message: 'What\'s your phone number?',
+    validate: function (value) {
+      var pass = value.match(/^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i);
+      if (pass) {
+        return true;
+      }
+
+      return 'Please enter a valid phone number';
+    }
+  },
+  {
+    type: 'list',
+    name: 'size',
+    message: 'What size do you need?',
+    choices: ['Large', 'Medium', 'Small'],
+    filter: function (val) {
+      return val.toLowerCase();
+    }
+  },
+  {
+    type: 'input',
+    name: 'quantity',
+    message: 'How many do you need?',
+    validate: function (value) {
+      var valid = !isNaN(parseFloat(value));
+      return valid || 'Please enter a number';
+    },
+    filter: Number
+  },
+  {
+    type: 'expand',
+    name: 'toppings',
+    message: 'What about the toppings?',
+    choices: [
+      {
+        key: 'p',
+        name: 'Pepperoni and cheese',
+        value: 'PepperoniCheese'
+      },
+      {
+        key: 'a',
+        name: 'All dressed',
+        value: 'alldressed'
+      },
+      {
+        key: 'w',
+        name: 'Hawaiian',
+        value: 'hawaiian'
+      }
+    ]
+  },
+  {
+    type: 'rawlist',
+    name: 'beverage',
+    message: 'You also get a free 2L beverage',
+    choices: ['Pepsi', '7up', 'Coke']
+  },
+  {
+    type: 'input',
+    name: 'comments',
+    message: 'Any comments on your purchase experience?',
+    default: 'Nope, all good!'
+  },
+  {
+    type: 'list',
+    name: 'prize',
+    message: 'For leaving a comment, you get a freebie',
+    choices: ['cake', 'fries'],
+    when: function (answers) {
+      return answers.comments !== 'Nope, all good!';
+    }
+  }
+];
+
+inquirer.prompt(questions).then(function (answers) {
+  console.log('\nOrder receipt:');
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/rawlist.js b/examples/rawlist.js
new file mode 100644
index 0000000..1034c4c
--- /dev/null
+++ b/examples/rawlist.js
@@ -0,0 +1,32 @@
+/**
+ * Raw List prompt example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+inquirer.prompt([
+  {
+    type: 'rawlist',
+    name: 'theme',
+    message: 'What do you want to do?',
+    choices: [
+      'Order a pizza',
+      'Make a reservation',
+      new inquirer.Separator(),
+      'Ask opening hours',
+      'Talk to the receptionist'
+    ]
+  },
+  {
+    type: 'rawlist',
+    name: 'size',
+    message: 'What size do you need',
+    choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'],
+    filter: function (val) {
+      return val.toLowerCase();
+    }
+  }
+]).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/recursive.js b/examples/recursive.js
new file mode 100644
index 0000000..a58682c
--- /dev/null
+++ b/examples/recursive.js
@@ -0,0 +1,35 @@
+/**
+ * Recursive prompt example
+ * Allows user to choose when to exit prompt
+ */
+
+'use strict';
+var inquirer = require('..');
+var output = [];
+
+var questions = [
+  {
+    type: 'input',
+    name: 'tvShow',
+    message: 'What\'s your favorite TV show?'
+  },
+  {
+    type: 'confirm',
+    name: 'askAgain',
+    message: 'Want to enter another TV show favorite (just hit enter for YES)?',
+    default: true
+  }
+];
+
+function ask() {
+  inquirer.prompt(questions).then(function (answers) {
+    output.push(answers.tvShow);
+    if (answers.askAgain) {
+      ask();
+    } else {
+      console.log('Your favorite TV Shows:', output.join(', '));
+    }
+  });
+}
+
+ask();
diff --git a/examples/rx-observable-array.js b/examples/rx-observable-array.js
new file mode 100644
index 0000000..cb33198
--- /dev/null
+++ b/examples/rx-observable-array.js
@@ -0,0 +1,45 @@
+var inquirer = require('..');
+var Rx = require('rx');
+
+var questions = [
+  {
+    type: 'input',
+    name: 'first_name',
+    message: 'What\'s your first name'
+  },
+  {
+    type: 'input',
+    name: 'last_name',
+    message: 'What\'s your last name',
+    default: function () {
+      return 'Doe';
+    }
+  },
+  {
+    type: 'input',
+    name: 'phone',
+    message: 'What\'s your phone number',
+    validate: function (value) {
+      var pass = value.match(/^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i);
+      if (pass) {
+        return true;
+      }
+
+      return 'Please enter a valid phone number';
+    }
+  }
+];
+
+var observable = Rx.Observable.fromArray(questions);
+
+inquirer.prompt(observable).ui.process.subscribe(
+  function (ans) {
+    console.log('Answer is: ', ans);
+  },
+  function (err) {
+    console.log('Error: ', err);
+  },
+  function () {
+    console.log('Completed');
+  }
+);
diff --git a/examples/rx-observable-create.js b/examples/rx-observable-create.js
new file mode 100644
index 0000000..e37e9b5
--- /dev/null
+++ b/examples/rx-observable-create.js
@@ -0,0 +1,38 @@
+var inquirer = require('..');
+var Rx = require('rx');
+
+var observe = Rx.Observable.create(function (obs) {
+  obs.onNext({
+    type: 'input',
+    name: 'first_name',
+    message: 'What\'s your first name'
+  });
+
+  obs.onNext({
+    type: 'input',
+    name: 'last_name',
+    message: 'What\'s your last name',
+    default: function () {
+      return 'Doe';
+    }
+  });
+
+  obs.onNext({
+    type: 'input',
+    name: 'phone',
+    message: 'What\'s your phone number',
+    validate: function (value) {
+      var pass = value.match(/^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i);
+      if (pass) {
+        return true;
+      }
+
+      return 'Please enter a valid phone number';
+    }
+  });
+  obs.onCompleted();
+});
+
+inquirer.prompt(observe).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/examples/when.js b/examples/when.js
new file mode 100644
index 0000000..dcbdc2e
--- /dev/null
+++ b/examples/when.js
@@ -0,0 +1,46 @@
+/**
+ * When example
+ */
+
+'use strict';
+var inquirer = require('..');
+
+var questions = [
+  {
+    type: 'confirm',
+    name: 'bacon',
+    message: 'Do you like bacon?'
+  },
+  {
+    type: 'input',
+    name: 'favorite',
+    message: 'Bacon lover, what is your favorite type of bacon?',
+    when: function (answers) {
+      return answers.bacon;
+    }
+  },
+  {
+    type: 'confirm',
+    name: 'pizza',
+    message: 'Ok... Do you like pizza?',
+    when: function (answers) {
+      return !likesFood('bacon')(answers);
+    }
+  },
+  {
+    type: 'input',
+    name: 'favorite',
+    message: 'Whew! What is your favorite type of pizza?',
+    when: likesFood('pizza')
+  }
+];
+
+function likesFood(aFood) {
+  return function (answers) {
+    return answers[aFood];
+  };
+}
+
+inquirer.prompt(questions).then(function (answers) {
+  console.log(JSON.stringify(answers, null, '  '));
+});
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..4a1de7f
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,62 @@
+'use strict';
+var path = require('path');
+var gulp = require('gulp');
+var eslint = require('gulp-eslint');
+var excludeGitignore = require('gulp-exclude-gitignore');
+var mocha = require('gulp-mocha');
+var istanbul = require('gulp-istanbul');
+var nsp = require('gulp-nsp');
+var plumber = require('gulp-plumber');
+var coveralls = require('gulp-coveralls');
+
+gulp.task('static', function () {
+  return gulp.src('**/*.js')
+    .pipe(excludeGitignore())
+    .pipe(eslint())
+    .pipe(eslint.format())
+    .pipe(eslint.failAfterError());
+});
+
+gulp.task('nsp', function (cb) {
+  nsp({package: path.resolve('package.json')}, cb);
+});
+
+gulp.task('pre-test', function () {
+  return gulp.src('lib/**/*.js')
+    .pipe(excludeGitignore())
+    .pipe(istanbul({
+      includeUntested: true
+    }))
+    .pipe(istanbul.hookRequire());
+});
+
+gulp.task('test', ['pre-test'], function (cb) {
+  var mochaErr;
+
+  gulp.src('test/**/*.js')
+    .pipe(plumber())
+    .pipe(mocha({reporter: 'spec'}))
+    .on('error', function (err) {
+      mochaErr = err;
+    })
+    .pipe(istanbul.writeReports())
+    .on('end', function () {
+      cb(mochaErr);
+    });
+});
+
+gulp.task('watch', function () {
+  gulp.watch(['lib/**/*.js', 'test/**'], ['test']);
+});
+
+gulp.task('coveralls', ['test'], function () {
+  if (!process.env.CI) {
+    return;
+  }
+
+  return gulp.src(path.join(__dirname, 'coverage/lcov.info'))
+    .pipe(coveralls());
+});
+
+gulp.task('prepublish', ['nsp']);
+gulp.task('default', ['static', 'test', 'coveralls']);
diff --git a/lib/inquirer.js b/lib/inquirer.js
new file mode 100644
index 0000000..5bbd801
--- /dev/null
+++ b/lib/inquirer.js
@@ -0,0 +1,84 @@
+/**
+ * Inquirer.js
+ * A collection of common interactive command line user interfaces.
+ */
+
+var inquirer = module.exports;
+
+/**
+ * Client interfaces
+ */
+
+inquirer.prompts = {};
+
+inquirer.Separator = require('./objects/separator');
+
+inquirer.ui = {
+  BottomBar: require('./ui/bottom-bar'),
+  Prompt: require('./ui/prompt')
+};
+
+/**
+ * Create a new self-contained prompt module.
+ */
+inquirer.createPromptModule = function (opt) {
+  var promptModule = function (questions) {
+    var ui = new inquirer.ui.Prompt(promptModule.prompts, opt);
+    var promise = ui.run(questions);
+
+    // Monkey patch the UI on the promise object so
+    // that it remains publicly accessible.
+    promise.ui = ui;
+
+    return promise;
+  };
+  promptModule.prompts = {};
+
+  /**
+   * Register a prompt type
+   * @param {String} name     Prompt type name
+   * @param {Function} prompt Prompt constructor
+   * @return {inquirer}
+   */
+
+  promptModule.registerPrompt = function (name, prompt) {
+    promptModule.prompts[name] = prompt;
+    return this;
+  };
+
+  /**
+   * Register the defaults provider prompts
+   */
+
+  promptModule.restoreDefaultPrompts = function () {
+    this.registerPrompt('list', require('./prompts/list'));
+    this.registerPrompt('input', require('./prompts/input'));
+    this.registerPrompt('confirm', require('./prompts/confirm'));
+    this.registerPrompt('rawlist', require('./prompts/rawlist'));
+    this.registerPrompt('expand', require('./prompts/expand'));
+    this.registerPrompt('checkbox', require('./prompts/checkbox'));
+    this.registerPrompt('password', require('./prompts/password'));
+    this.registerPrompt('editor', require('./prompts/editor'));
+  };
+
+  promptModule.restoreDefaultPrompts();
+
+  return promptModule;
+};
+
+/**
+ * Public CLI helper interface
+ * @param  {Array|Object|rx.Observable} questions - Questions settings array
+ * @param  {Function} cb - Callback being passed the user answers
+ * @return {inquirer.ui.Prompt}
+ */
+
+inquirer.prompt = inquirer.createPromptModule();
+
+// Expose helper functions on the top level for easiest usage by common users
+inquirer.registerPrompt = function (name, prompt) {
+  inquirer.prompt.registerPrompt(name, prompt);
+};
+inquirer.restoreDefaultPrompts = function () {
+  inquirer.prompt.restoreDefaultPrompts();
+};
diff --git a/lib/objects/choice.js b/lib/objects/choice.js
new file mode 100644
index 0000000..51affac
--- /dev/null
+++ b/lib/objects/choice.js
@@ -0,0 +1,35 @@
+'use strict';
+var _ = require('lodash');
+
+/**
+ * Choice object
+ * Normalize input as choice object
+ * @constructor
+ * @param {String|Object} val  Choice value. If an object is passed, it should contains
+ *                             at least one of `value` or `name` property
+ */
+
+var Choice = module.exports = function (val, answers) {
+  // Don't process Choice and Separator object
+  if (val instanceof Choice || val.type === 'separator') {
+    return val;
+  }
+
+  if (_.isString(val)) {
+    this.name = val;
+    this.value = val;
+    this.short = val;
+  } else {
+    _.extend(this, val, {
+      name: val.name || val.value,
+      value: 'value' in val ? val.value : val.name,
+      short: val.short || val.name || val.value
+    });
+  }
+
+  if (_.isFunction(val.disabled)) {
+    this.disabled = val.disabled(answers);
+  } else {
+    this.disabled = val.disabled;
+  }
+};
diff --git a/lib/objects/choices.js b/lib/objects/choices.js
new file mode 100644
index 0000000..41a3629
--- /dev/null
+++ b/lib/objects/choices.js
@@ -0,0 +1,112 @@
+'use strict';
+var assert = require('assert');
+var _ = require('lodash');
+var Separator = require('./separator');
+var Choice = require('./choice');
+
+/**
+ * Choices collection
+ * Collection of multiple `choice` object
+ * @constructor
+ * @param {Array} choices  All `choice` to keep in the collection
+ */
+
+var Choices = module.exports = function (choices, answers) {
+  this.choices = choices.map(function (val) {
+    if (val.type === 'separator') {
+      if (!(val instanceof Separator)) {
+        val = new Separator(val.line);
+      }
+      return val;
+    }
+    return new Choice(val, answers);
+  });
+
+  this.realChoices = this.choices
+    .filter(Separator.exclude)
+    .filter(function (item) {
+      return !item.disabled;
+    });
+
+  Object.defineProperty(this, 'length', {
+    get: function () {
+      return this.choices.length;
+    },
+    set: function (val) {
+      this.choices.length = val;
+    }
+  });
+
+  Object.defineProperty(this, 'realLength', {
+    get: function () {
+      return this.realChoices.length;
+    },
+    set: function () {
+      throw new Error('Cannot set `realLength` of a Choices collection');
+    }
+  });
+};
+
+/**
+ * Get a valid choice from the collection
+ * @param  {Number} selector  The selected choice index
+ * @return {Choice|Undefined} Return the matched choice or undefined
+ */
+
+Choices.prototype.getChoice = function (selector) {
+  assert(_.isNumber(selector));
+  return this.realChoices[selector];
+};
+
+/**
+ * Get a raw element from the collection
+ * @param  {Number} selector  The selected index value
+ * @return {Choice|Undefined} Return the matched choice or undefined
+ */
+
+Choices.prototype.get = function (selector) {
+  assert(_.isNumber(selector));
+  return this.choices[selector];
+};
+
+/**
+ * Match the valid choices against a where clause
+ * @param  {Object} whereClause Lodash `where` clause
+ * @return {Array}              Matching choices or empty array
+ */
+
+Choices.prototype.where = function (whereClause) {
+  return _.filter(this.realChoices, whereClause);
+};
+
+/**
+ * Pluck a particular key from the choices
+ * @param  {String} propertyName Property name to select
+ * @return {Array}               Selected properties
+ */
+
+Choices.prototype.pluck = function (propertyName) {
+  return _.map(this.realChoices, propertyName);
+};
+
+// Expose usual Array methods
+Choices.prototype.indexOf = function () {
+  return this.choices.indexOf.apply(this.choices, arguments);
+};
+Choices.prototype.forEach = function () {
+  return this.choices.forEach.apply(this.choices, arguments);
+};
+Choices.prototype.filter = function () {
+  return this.choices.filter.apply(this.choices, arguments);
+};
+Choices.prototype.find = function (func) {
+  return _.find(this.choices, func);
+};
+Choices.prototype.push = function () {
+  var objs = _.map(arguments, function (val) {
+    return new Choice(val);
+  });
+  this.choices.push.apply(this.choices, objs);
+  this.realChoices = this.choices.filter(Separator.exclude);
+  return this.choices;
+};
diff --git a/lib/objects/separator.js b/lib/objects/separator.js
new file mode 100644
index 0000000..abfaef1
--- /dev/null
+++ b/lib/objects/separator.js
@@ -0,0 +1,34 @@
+'use strict';
+var chalk = require('chalk');
+var figures = require('figures');
+
+/**
+ * Separator object
+ * Used to space/separate choices group
+ * @constructor
+ * @param {String} line   Separation line content (facultative)
+ */
+
+var Separator = module.exports = function (line) {
+  this.type = 'separator';
+  this.line = chalk.dim(line || new Array(15).join(figures.line));
+};
+
+/**
+ * Helper function returning false if object is a separator
+ * @param  {Object} obj object to test against
+ * @return {Boolean}    `false` if object is a separator
+ */
+
+Separator.exclude = function (obj) {
+  return obj.type !== 'separator';
+};
+
+/**
+ * Stringify separator
+ * @return {String} the separator display string
+ */
+
+Separator.prototype.toString = function () {
+  return this.line;
+};
diff --git a/lib/prompts/base.js b/lib/prompts/base.js
new file mode 100644
index 0000000..f20d365
--- /dev/null
+++ b/lib/prompts/base.js
@@ -0,0 +1,129 @@
+/**
+ * Base prompt implementation
+ * Should be extended by prompt types.
+ */
+
+var _ = require('lodash');
+var chalk = require('chalk');
+var runAsync = require('run-async');
+var Choices = require('../objects/choices');
+var ScreenManager = require('../utils/screen-manager');
+var Promise = require('pinkie-promise');
+
+var Prompt = module.exports = function (question, rl, answers) {
+  // Setup instance defaults property
+  _.assign(this, {
+    answers: answers,
+    status: 'pending'
+  });
+
+  // Set defaults prompt options
+  this.opt = _.defaults(_.clone(question), {
+    validate: function () {
+      return true;
+    },
+    filter: function (val) {
+      return val;
+    },
+    when: function () {
+      return true;
+    }
+  });
+
+  // Check to make sure prompt requirements are there
+  if (!this.opt.message) {
+    this.throwParamError('message');
+  }
+  if (!this.opt.name) {
+    this.throwParamError('name');
+  }
+
+  // Normalize choices
+  if (Array.isArray(this.opt.choices)) {
+    this.opt.choices = new Choices(this.opt.choices, answers);
+  }
+
+  this.rl = rl;
+  this.screen = new ScreenManager(this.rl);
+};
+
+/**
+ * Start the Inquiry session and manage output value filtering
+ * @return {Promise}
+ */
+
+Prompt.prototype.run = function () {
+  return new Promise(function (resolve) {
+    this._run(function (value) {
+      resolve(value);
+    });
+  }.bind(this));
+};
+
+// default noop (this one should be overwritten in prompts)
+Prompt.prototype._run = function (cb) {
+  cb();
+};
+
+/**
+ * Throw an error telling a required parameter is missing
+ * @param  {String} name Name of the missing param
+ * @return {Throw Error}
+ */
+
+Prompt.prototype.throwParamError = function (name) {
+  throw new Error('You must provide a `' + name + '` parameter');
+};
+
+/**
+ * Run the provided validation method each time a submit event occur.
+ * @param  {Rx.Observable} submit - submit event flow
+ * @return {Object}        Object containing two observables: `success` and `error`
+ */
+Prompt.prototype.handleSubmitEvents = function (submit) {
+  var self = this;
+  var validate = runAsync(this.opt.validate);
+  var filter = runAsync(this.opt.filter);
+  var validation = submit.flatMap(function (value) {
+    return filter(value).then(function (filteredValue) {
+      return validate(filteredValue, self.answers).then(function (isValid) {
+        return {isValid: isValid, value: filteredValue};
+      });
+    }, function (err) {
+      return {isValid: err};
+    });
+  }).share();
+
+  var success = validation
+    .filter(function (state) {
+      return state.isValid === true;
+    })
+    .take(1);
+
+  var error = validation
+    .filter(function (state) {
+      return state.isValid !== true;
+    })
+    .takeUntil(success);
+
+  return {
+    success: success,
+    error: error
+  };
+};
+
+/**
+ * Generate the prompt question string
+ * @return {String} prompt question string
+ */
+
+Prompt.prototype.getQuestion = function () {
+  var message = chalk.green('?') + ' ' + chalk.bold(this.opt.message) + ' ';
+
+  // Append the default if available, and if question isn't answered
+  if (this.opt.default != null && this.status !== 'answered') {
+    message += chalk.dim('(' + this.opt.default + ') ');
+  }
+
+  return message;
+};
diff --git a/lib/prompts/checkbox.js b/lib/prompts/checkbox.js
new file mode 100644
index 0000000..460f50c
--- /dev/null
+++ b/lib/prompts/checkbox.js
@@ -0,0 +1,235 @@
+/**
+ * `list` type prompt
+ */
+
+var _ = require('lodash');
+var util = require('util');
+var chalk = require('chalk');
+var cliCursor = require('cli-cursor');
+var figures = require('figures');
+var Base = require('./base');
+var observe = require('../utils/events');
+var Paginator = require('../utils/paginator');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  Base.apply(this, arguments);
+
+  if (!this.opt.choices) {
+    this.throwParamError('choices');
+  }
+
+  if (_.isArray(this.opt.default)) {
+    this.opt.choices.forEach(function (choice) {
+      if (this.opt.default.indexOf(choice.value) >= 0) {
+        choice.checked = true;
+      }
+    }, this);
+  }
+
+  this.pointer = 0;
+
+  // Make sure no default is set (so it won't be printed)
+  this.opt.default = null;
+
+  this.paginator = new Paginator();
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  var events = observe(this.rl);
+
+  var validation = this.handleSubmitEvents(
+    events.line.map(this.getCurrentValue.bind(this))
+  );
+  validation.success.forEach(this.onEnd.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+
+  events.normalizedUpKey.takeUntil(validation.success).forEach(this.onUpKey.bind(this));
+  events.normalizedDownKey.takeUntil(validation.success).forEach(this.onDownKey.bind(this));
+  events.numberKey.takeUntil(validation.success).forEach(this.onNumberKey.bind(this));
+  events.spaceKey.takeUntil(validation.success).forEach(this.onSpaceKey.bind(this));
+  events.aKey.takeUntil(validation.success).forEach(this.onAllKey.bind(this));
+  events.iKey.takeUntil(validation.success).forEach(this.onInverseKey.bind(this));
+
+  // Init the prompt
+  cliCursor.hide();
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error) {
+  // Render question
+  var message = this.getQuestion();
+  var bottomContent = '';
+
+  if (!this.spaceKeyPressed) {
+    message += '(Press ' + chalk.cyan.bold('<space>') + ' to select, ' + chalk.cyan.bold('<a>') + ' to toggle all, ' + chalk.cyan.bold('<i>') + ' to inverse selection)';
+  }
+
+  // Render choices or answer depending on the state
+  if (this.status === 'answered') {
+    message += chalk.cyan(this.selection.join(', '));
+  } else {
+    var choicesStr = renderChoices(this.opt.choices, this.pointer);
+    var indexPosition = this.opt.choices.indexOf(this.opt.choices.getChoice(this.pointer));
+    message += '\n' + this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
+  }
+
+  if (error) {
+    bottomContent = chalk.red('>> ') + error;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.onEnd = function (state) {
+  this.status = 'answered';
+
+  // Rerender prompt (and clean subline error)
+  this.render();
+
+  this.screen.done();
+  cliCursor.show();
+  this.done(state.value);
+};
+
+Prompt.prototype.onError = function (state) {
+  this.render(state.isValid);
+};
+
+Prompt.prototype.getCurrentValue = function () {
+  var choices = this.opt.choices.filter(function (choice) {
+    return Boolean(choice.checked) && !choice.disabled;
+  });
+
+  this.selection = _.map(choices, 'short');
+  return _.map(choices, 'value');
+};
+
+Prompt.prototype.onUpKey = function () {
+  var len = this.opt.choices.realLength;
+  this.pointer = (this.pointer > 0) ? this.pointer - 1 : len - 1;
+  this.render();
+};
+
+Prompt.prototype.onDownKey = function () {
+  var len = this.opt.choices.realLength;
+  this.pointer = (this.pointer < len - 1) ? this.pointer + 1 : 0;
+  this.render();
+};
+
+Prompt.prototype.onNumberKey = function (input) {
+  if (input <= this.opt.choices.realLength) {
+    this.pointer = input - 1;
+    this.toggleChoice(this.pointer);
+  }
+  this.render();
+};
+
+Prompt.prototype.onSpaceKey = function () {
+  this.spaceKeyPressed = true;
+  this.toggleChoice(this.pointer);
+  this.render();
+};
+
+Prompt.prototype.onAllKey = function () {
+  var shouldBeChecked = Boolean(this.opt.choices.find(function (choice) {
+    return choice.type !== 'separator' && !choice.checked;
+  }));
+
+  this.opt.choices.forEach(function (choice) {
+    if (choice.type !== 'separator') {
+      choice.checked = shouldBeChecked;
+    }
+  });
+
+  this.render();
+};
+
+Prompt.prototype.onInverseKey = function () {
+  this.opt.choices.forEach(function (choice) {
+    if (choice.type !== 'separator') {
+      choice.checked = !choice.checked;
+    }
+  });
+
+  this.render();
+};
+
+Prompt.prototype.toggleChoice = function (index) {
+  var item = this.opt.choices.getChoice(index);
+  if (item !== undefined) {
+    this.opt.choices.getChoice(index).checked = !item.checked;
+  }
+};
+
+/**
+ * Function for rendering checkbox choices
+ * @param  {Number} pointer Position of the pointer
+ * @return {String}         Rendered content
+ */
+
+function renderChoices(choices, pointer) {
+  var output = '';
+  var separatorOffset = 0;
+
+  choices.forEach(function (choice, i) {
+    if (choice.type === 'separator') {
+      separatorOffset++;
+      output += ' ' + choice + '\n';
+      return;
+    }
+
+    if (choice.disabled) {
+      separatorOffset++;
+      output += ' - ' + choice.name;
+      output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')';
+    } else {
+      var isSelected = (i - separatorOffset === pointer);
+      output += isSelected ? chalk.cyan(figures.pointer) : ' ';
+      output += getCheckbox(choice.checked) + ' ' + choice.name;
+    }
+
+    output += '\n';
+  });
+
+  return output.replace(/\n$/, '');
+}
+
+/**
+ * Get the checkbox
+ * @param  {Boolean} checked - add a X or not to the checkbox
+ * @return {String} Composited checkbox string
+ */
+
+function getCheckbox(checked) {
+  return checked ? chalk.green(figures.radioOn) : figures.radioOff;
+}
diff --git a/lib/prompts/confirm.js b/lib/prompts/confirm.js
new file mode 100644
index 0000000..542d120
--- /dev/null
+++ b/lib/prompts/confirm.js
@@ -0,0 +1,106 @@
+/**
+ * `confirm` type prompt
+ */
+
+var _ = require('lodash');
+var util = require('util');
+var chalk = require('chalk');
+var Base = require('./base');
+var observe = require('../utils/events');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  Base.apply(this, arguments);
+
+  var rawDefault = true;
+
+  _.extend(this.opt, {
+    filter: function (input) {
+      var value = rawDefault;
+      if (input != null && input !== '') {
+        value = /^y(es)?/i.test(input);
+      }
+      return value;
+    }
+  });
+
+  if (_.isBoolean(this.opt.default)) {
+    rawDefault = this.opt.default;
+  }
+
+  this.opt.default = rawDefault ? 'Y/n' : 'y/N';
+
+  return this;
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb   Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  // Once user confirm (enter key)
+  var events = observe(this.rl);
+  events.keypress.takeUntil(events.line).forEach(this.onKeypress.bind(this));
+
+  events.line.take(1).forEach(this.onEnd.bind(this));
+
+  // Init
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (answer) {
+  var message = this.getQuestion();
+
+  if (typeof answer === 'boolean') {
+    message += chalk.cyan(answer ? 'Yes' : 'No');
+  } else {
+    message += this.rl.line;
+  }
+
+  this.screen.render(message);
+
+  return this;
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.onEnd = function (input) {
+  this.status = 'answered';
+
+  var output = this.opt.filter(input);
+  this.render(output);
+
+  this.screen.done();
+  this.done(output);
+};
+
+/**
+ * When user press a key
+ */
+
+Prompt.prototype.onKeypress = function () {
+  this.render();
+};
diff --git a/lib/prompts/editor.js b/lib/prompts/editor.js
new file mode 100644
index 0000000..31d0e3e
--- /dev/null
+++ b/lib/prompts/editor.js
@@ -0,0 +1,111 @@
+/**
+ * `editor` type prompt
+ */
+
+var util = require('util');
+var chalk = require('chalk');
+var ExternalEditor = require('external-editor');
+var Base = require('./base');
+var observe = require('../utils/events');
+var rx = require('rx');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  return Base.apply(this, arguments);
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  this.editorResult = new rx.Subject();
+
+  // Open Editor on "line" (Enter Key)
+  var events = observe(this.rl);
+  this.lineSubscription = events.line.forEach(this.startExternalEditor.bind(this));
+
+  // Trigger Validation when editor closes
+  var validation = this.handleSubmitEvents(this.editorResult);
+  validation.success.forEach(this.onEnd.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+
+  // Prevents default from being printed on screen (can look weird with multiple lines)
+  this.currentText = this.opt.default;
+  this.opt.default = null;
+
+  // Init
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error) {
+  var bottomContent = '';
+  var message = this.getQuestion();
+
+  if (this.status === 'answered') {
+    message += chalk.dim('Received');
+  } else {
+    message += chalk.dim('Press <enter> to launch your preferred editor.');
+  }
+
+  if (error) {
+    bottomContent = chalk.red('>> ') + error;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+/**
+ * Launch $EDITOR on user press enter
+ */
+
+Prompt.prototype.startExternalEditor = function () {
+  // Pause Readline to prevent stdin and stdout from being modified while the editor is showing
+  this.rl.pause();
+  ExternalEditor.editAsync(this.currentText, this.endExternalEditor.bind(this));
+};
+
+Prompt.prototype.endExternalEditor = function (error, result) {
+  this.rl.resume();
+  if (error) {
+    this.editorResult.onError(error);
+  } else {
+    this.editorResult.onNext(result);
+  }
+};
+
+Prompt.prototype.onEnd = function (state) {
+  this.editorResult.dispose();
+  this.lineSubscription.dispose();
+  this.answer = state.value;
+  this.status = 'answered';
+  // Re-render prompt
+  this.render();
+  this.screen.done();
+  this.done(this.answer);
+};
+
+Prompt.prototype.onError = function (state) {
+  this.render(state.isValid);
+};
diff --git a/lib/prompts/expand.js b/lib/prompts/expand.js
new file mode 100644
index 0000000..336d98c
--- /dev/null
+++ b/lib/prompts/expand.js
@@ -0,0 +1,260 @@
+/**
+ * `rawlist` type prompt
+ */
+
+var _ = require('lodash');
+var util = require('util');
+var chalk = require('chalk');
+var Base = require('./base');
+var Separator = require('../objects/separator');
+var observe = require('../utils/events');
+var Paginator = require('../utils/paginator');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  Base.apply(this, arguments);
+
+  if (!this.opt.choices) {
+    this.throwParamError('choices');
+  }
+
+  this.validateChoices(this.opt.choices);
+
+  // Add the default `help` (/expand) option
+  this.opt.choices.push({
+    key: 'h',
+    name: 'Help, list all options',
+    value: 'help'
+  });
+
+  this.opt.validate = function (choice) {
+    if (choice == null) {
+      return 'Please enter a valid command';
+    }
+
+    return choice !== 'help';
+  };
+
+  // Setup the default string (capitalize the default key)
+  this.opt.default = this.generateChoicesString(this.opt.choices, this.opt.default);
+
+  this.paginator = new Paginator();
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  // Save user answer and update prompt to show selected option.
+  var events = observe(this.rl);
+  var validation = this.handleSubmitEvents(
+    events.line.map(this.getCurrentValue.bind(this))
+  );
+  validation.success.forEach(this.onSubmit.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+  this.keypressObs = events.keypress.takeUntil(validation.success)
+    .forEach(this.onKeypress.bind(this));
+
+  // Init the prompt
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error, hint) {
+  var message = this.getQuestion();
+  var bottomContent = '';
+
+  if (this.status === 'answered') {
+    message += chalk.cyan(this.answer);
+  } else if (this.status === 'expanded') {
+    var choicesStr = renderChoices(this.opt.choices, this.selectedKey);
+    message += this.paginator.paginate(choicesStr, this.selectedKey, this.opt.pageSize);
+    message += '\n  Answer: ';
+  }
+
+  message += this.rl.line;
+
+  if (error) {
+    bottomContent = chalk.red('>> ') + error;
+  }
+
+  if (hint) {
+    bottomContent = chalk.cyan('>> ') + hint;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+Prompt.prototype.getCurrentValue = function (input) {
+  if (!input) {
+    input = this.rawDefault;
+  }
+  var selected = this.opt.choices.where({key: input.toLowerCase().trim()})[0];
+  if (!selected) {
+    return null;
+  }
+
+  return selected.value;
+};
+
+/**
+ * Generate the prompt choices string
+ * @return {String}  Choices string
+ */
+
+Prompt.prototype.getChoices = function () {
+  var output = '';
+
+  this.opt.choices.forEach(function (choice) {
+    output += '\n  ';
+
+    if (choice.type === 'separator') {
+      output += ' ' + choice;
+      return;
+    }
+
+    var choiceStr = choice.key + ') ' + choice.name;
+    if (this.selectedKey === choice.key) {
+      choiceStr = chalk.cyan(choiceStr);
+    }
+    output += choiceStr;
+  }.bind(this));
+
+  return output;
+};
+
+Prompt.prototype.onError = function (state) {
+  if (state.value === 'help') {
+    this.selectedKey = '';
+    this.status = 'expanded';
+    this.render();
+    return;
+  }
+  this.render(state.isValid);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.onSubmit = function (state) {
+  this.status = 'answered';
+  var choice = this.opt.choices.where({value: state.value})[0];
+  this.answer = choice.short || choice.name;
+
+  // Re-render prompt
+  this.render();
+  this.screen.done();
+  this.done(state.value);
+};
+
+/**
+ * When user press a key
+ */
+
+Prompt.prototype.onKeypress = function () {
+  this.selectedKey = this.rl.line.toLowerCase();
+  var selected = this.opt.choices.where({key: this.selectedKey})[0];
+  if (this.status === 'expanded') {
+    this.render();
+  } else {
+    this.render(null, selected ? selected.name : null);
+  }
+};
+
+/**
+ * Validate the choices
+ * @param {Array} choices
+ */
+
+Prompt.prototype.validateChoices = function (choices) {
+  var formatError;
+  var errors = [];
+  var keymap = {};
+  choices.filter(Separator.exclude).forEach(function (choice) {
+    if (!choice.key || choice.key.length !== 1) {
+      formatError = true;
+    }
+    if (keymap[choice.key]) {
+      errors.push(choice.key);
+    }
+    keymap[choice.key] = true;
+    choice.key = String(choice.key).toLowerCase();
+  });
+
+  if (formatError) {
+    throw new Error('Format error: `key` param must be a single letter and is required.');
+  }
+  if (keymap.h) {
+    throw new Error('Reserved key error: `key` param cannot be `h` - this value is reserved.');
+  }
+  if (errors.length) {
+    throw new Error('Duplicate key error: `key` param must be unique. Duplicates: ' +
+        _.uniq(errors).join(', '));
+  }
+};
+
+/**
+ * Generate a string out of the choices keys
+ * @param  {Array}  choices
+ * @param  {Number} defaultIndex - the choice index to capitalize
+ * @return {String} The rendered choices key string
+ */
+Prompt.prototype.generateChoicesString = function (choices, defaultIndex) {
+  var defIndex = choices.realLength - 1;
+  if (_.isNumber(defaultIndex) && this.opt.choices.getChoice(defaultIndex)) {
+    defIndex = defaultIndex;
+  }
+  var defStr = this.opt.choices.pluck('key');
+  this.rawDefault = defStr[defIndex];
+  defStr[defIndex] = String(defStr[defIndex]).toUpperCase();
+  return defStr.join('');
+};
+
+/**
+ * Function for rendering checkbox choices
+ * @param  {String} pointer Selected key
+ * @return {String}         Rendered content
+ */
+
+function renderChoices(choices, pointer) {
+  var output = '';
+
+  choices.forEach(function (choice) {
+    output += '\n  ';
+
+    if (choice.type === 'separator') {
+      output += ' ' + choice;
+      return;
+    }
+
+    var choiceStr = choice.key + ') ' + choice.name;
+    if (pointer === choice.key) {
+      choiceStr = chalk.cyan(choiceStr);
+    }
+    output += choiceStr;
+  });
+
+  return output;
+}
diff --git a/lib/prompts/input.js b/lib/prompts/input.js
new file mode 100644
index 0000000..a1c7be2
--- /dev/null
+++ b/lib/prompts/input.js
@@ -0,0 +1,104 @@
+/**
+ * `input` type prompt
+ */
+
+var util = require('util');
+var chalk = require('chalk');
+var Base = require('./base');
+var observe = require('../utils/events');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  return Base.apply(this, arguments);
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  // Once user confirm (enter key)
+  var events = observe(this.rl);
+  var submit = events.line.map(this.filterInput.bind(this));
+
+  var validation = this.handleSubmitEvents(submit);
+  validation.success.forEach(this.onEnd.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+
+  events.keypress.takeUntil(validation.success).forEach(this.onKeypress.bind(this));
+
+  // Init
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error) {
+  var bottomContent = '';
+  var message = this.getQuestion();
+
+  if (this.status === 'answered') {
+    message += chalk.cyan(this.answer);
+  } else {
+    message += this.rl.line;
+  }
+
+  if (error) {
+    bottomContent = chalk.red('>> ') + error;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.filterInput = function (input) {
+  if (!input) {
+    return this.opt.default == null ? '' : this.opt.default;
+  }
+  return input;
+};
+
+Prompt.prototype.onEnd = function (state) {
+  this.answer = state.value;
+  this.status = 'answered';
+
+  // Re-render prompt
+  this.render();
+
+  this.screen.done();
+  this.done(state.value);
+};
+
+Prompt.prototype.onError = function (state) {
+  this.render(state.isValid);
+};
+
+/**
+ * When user press a key
+ */
+
+Prompt.prototype.onKeypress = function () {
+  this.render();
+};
diff --git a/lib/prompts/list.js b/lib/prompts/list.js
new file mode 100644
index 0000000..b15ad2e
--- /dev/null
+++ b/lib/prompts/list.js
@@ -0,0 +1,187 @@
+/**
+ * `list` type prompt
+ */
+
+var _ = require('lodash');
+var util = require('util');
+var chalk = require('chalk');
+var figures = require('figures');
+var cliCursor = require('cli-cursor');
+var runAsync = require('run-async');
+var Base = require('./base');
+var observe = require('../utils/events');
+var Paginator = require('../utils/paginator');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  Base.apply(this, arguments);
+
+  if (!this.opt.choices) {
+    this.throwParamError('choices');
+  }
+
+  this.firstRender = true;
+  this.selected = 0;
+
+  var def = this.opt.default;
+
+  // Default being a Number
+  if (_.isNumber(def) && def >= 0 && def < this.opt.choices.realLength) {
+    this.selected = def;
+  }
+
+  // Default being a String
+  if (_.isString(def)) {
+    this.selected = this.opt.choices.pluck('value').indexOf(def);
+  }
+
+  // Make sure no default is set (so it won't be printed)
+  this.opt.default = null;
+
+  this.paginator = new Paginator();
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  var self = this;
+
+  var events = observe(this.rl);
+  events.normalizedUpKey.takeUntil(events.line).forEach(this.onUpKey.bind(this));
+  events.normalizedDownKey.takeUntil(events.line).forEach(this.onDownKey.bind(this));
+  events.numberKey.takeUntil(events.line).forEach(this.onNumberKey.bind(this));
+  events.line
+    .take(1)
+    .map(this.getCurrentValue.bind(this))
+    .flatMap(function (value) {
+      return runAsync(self.opt.filter)(value).catch(function (err) {
+        return err;
+      });
+    })
+    .forEach(this.onSubmit.bind(this));
+
+  // Init the prompt
+  cliCursor.hide();
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function () {
+  // Render question
+  var message = this.getQuestion();
+
+  if (this.firstRender) {
+    message += chalk.dim('(Use arrow keys)');
+  }
+
+  // Render choices or answer depending on the state
+  if (this.status === 'answered') {
+    message += chalk.cyan(this.opt.choices.getChoice(this.selected).short);
+  } else {
+    var choicesStr = listRender(this.opt.choices, this.selected);
+    var indexPosition = this.opt.choices.indexOf(this.opt.choices.getChoice(this.selected));
+    message += '\n' + this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize);
+  }
+
+  this.firstRender = false;
+
+  this.screen.render(message);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.onSubmit = function (value) {
+  this.status = 'answered';
+
+  // Rerender prompt
+  this.render();
+
+  this.screen.done();
+  cliCursor.show();
+  this.done(value);
+};
+
+Prompt.prototype.getCurrentValue = function () {
+  return this.opt.choices.getChoice(this.selected).value;
+};
+
+/**
+ * When user press a key
+ */
+Prompt.prototype.onUpKey = function () {
+  var len = this.opt.choices.realLength;
+  this.selected = (this.selected > 0) ? this.selected - 1 : len - 1;
+  this.render();
+};
+
+Prompt.prototype.onDownKey = function () {
+  var len = this.opt.choices.realLength;
+  this.selected = (this.selected < len - 1) ? this.selected + 1 : 0;
+  this.render();
+};
+
+Prompt.prototype.onNumberKey = function (input) {
+  if (input <= this.opt.choices.realLength) {
+    this.selected = input - 1;
+  }
+  this.render();
+};
+
+/**
+ * Function for rendering list choices
+ * @param  {Number} pointer Position of the pointer
+ * @return {String}         Rendered content
+ */
+function listRender(choices, pointer) {
+  var output = '';
+  var separatorOffset = 0;
+
+  choices.forEach(function (choice, i) {
+    if (choice.type === 'separator') {
+      separatorOffset++;
+      output += '  ' + choice + '\n';
+      return;
+    }
+
+    if (choice.disabled) {
+      separatorOffset++;
+      output += '  - ' + choice.name;
+      output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')';
+      output += '\n';
+      return;
+    }
+
+    var isSelected = (i - separatorOffset === pointer);
+    var line = (isSelected ? figures.pointer + ' ' : '  ') + choice.name;
+    if (isSelected) {
+      line = chalk.cyan(line);
+    }
+    output += line + ' \n';
+  });
+
+  return output.replace(/\n$/, '');
+}
diff --git a/lib/prompts/password.js b/lib/prompts/password.js
new file mode 100644
index 0000000..aedbc91
--- /dev/null
+++ b/lib/prompts/password.js
@@ -0,0 +1,115 @@
+/**
+ * `password` type prompt
+ */
+
+var util = require('util');
+var chalk = require('chalk');
+var Base = require('./base');
+var observe = require('../utils/events');
+
+function mask(input) {
+  input = String(input);
+  if (input.length === 0) {
+    return '';
+  }
+
+  return new Array(input.length + 1).join('*');
+}
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  return Base.apply(this, arguments);
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  var events = observe(this.rl);
+
+  // Once user confirm (enter key)
+  var submit = events.line.map(this.filterInput.bind(this));
+
+  var validation = this.handleSubmitEvents(submit);
+  validation.success.forEach(this.onEnd.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+
+  events.keypress.takeUntil(validation.success).forEach(this.onKeypress.bind(this));
+
+  // Init
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error) {
+  var message = this.getQuestion();
+  var bottomContent = '';
+
+  if (this.status === 'answered') {
+    message += chalk.cyan(mask(this.answer));
+  } else {
+    message += mask(this.rl.line || '');
+  }
+
+  if (error) {
+    bottomContent = '\n' + chalk.red('>> ') + error;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.filterInput = function (input) {
+  if (!input) {
+    return this.opt.default == null ? '' : this.opt.default;
+  }
+  return input;
+};
+
+Prompt.prototype.onEnd = function (state) {
+  this.status = 'answered';
+  this.answer = state.value;
+
+  // Re-render prompt
+  this.render();
+
+  this.screen.done();
+  this.done(state.value);
+};
+
+Prompt.prototype.onError = function (state) {
+  this.render(state.isValid);
+  this.rl.output.unmute();
+};
+
+/**
+ * When user type
+ */
+
+Prompt.prototype.onKeypress = function () {
+  this.render();
+};
diff --git a/lib/prompts/rawlist.js b/lib/prompts/rawlist.js
new file mode 100644
index 0000000..15e9911
--- /dev/null
+++ b/lib/prompts/rawlist.js
@@ -0,0 +1,179 @@
+/**
+ * `rawlist` type prompt
+ */
+
+var _ = require('lodash');
+var util = require('util');
+var chalk = require('chalk');
+var Base = require('./base');
+var Separator = require('../objects/separator');
+var observe = require('../utils/events');
+var Paginator = require('../utils/paginator');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt() {
+  Base.apply(this, arguments);
+
+  if (!this.opt.choices) {
+    this.throwParamError('choices');
+  }
+
+  this.opt.validChoices = this.opt.choices.filter(Separator.exclude);
+
+  this.selected = 0;
+  this.rawDefault = 0;
+
+  _.extend(this.opt, {
+    validate: function (val) {
+      return val != null;
+    }
+  });
+
+  var def = this.opt.default;
+  if (_.isNumber(def) && def >= 0 && def < this.opt.choices.realLength) {
+    this.selected = this.rawDefault = def;
+  }
+
+  // Make sure no default is set (so it won't be printed)
+  this.opt.default = null;
+
+  this.paginator = new Paginator();
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Start the Inquiry session
+ * @param  {Function} cb      Callback when prompt is done
+ * @return {this}
+ */
+
+Prompt.prototype._run = function (cb) {
+  this.done = cb;
+
+  // Once user confirm (enter key)
+  var events = observe(this.rl);
+  var submit = events.line.map(this.getCurrentValue.bind(this));
+
+  var validation = this.handleSubmitEvents(submit);
+  validation.success.forEach(this.onEnd.bind(this));
+  validation.error.forEach(this.onError.bind(this));
+
+  events.keypress.takeUntil(validation.success).forEach(this.onKeypress.bind(this));
+
+  // Init the prompt
+  this.render();
+
+  return this;
+};
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function (error) {
+  // Render question
+  var message = this.getQuestion();
+  var bottomContent = '';
+
+  if (this.status === 'answered') {
+    message += chalk.cyan(this.answer);
+  } else {
+    var choicesStr = renderChoices(this.opt.choices, this.selected);
+    message += this.paginator.paginate(choicesStr, this.selected, this.opt.pageSize);
+    message += '\n  Answer: ';
+  }
+
+  message += this.rl.line;
+
+  if (error) {
+    bottomContent = '\n' + chalk.red('>> ') + error;
+  }
+
+  this.screen.render(message, bottomContent);
+};
+
+/**
+ * When user press `enter` key
+ */
+
+Prompt.prototype.getCurrentValue = function (index) {
+  if (index == null || index === '') {
+    index = this.rawDefault;
+  } else {
+    index -= 1;
+  }
+
+  var choice = this.opt.choices.getChoice(index);
+  return choice ? choice.value : null;
+};
+
+Prompt.prototype.onEnd = function (state) {
+  this.status = 'answered';
+  this.answer = state.value;
+
+  // Re-render prompt
+  this.render();
+
+  this.screen.done();
+  this.done(state.value);
+};
+
+Prompt.prototype.onError = function () {
+  this.render('Please enter a valid index');
+};
+
+/**
+ * When user press a key
+ */
+
+Prompt.prototype.onKeypress = function () {
+  var index = this.rl.line.length ? Number(this.rl.line) - 1 : 0;
+
+  if (this.opt.choices.getChoice(index)) {
+    this.selected = index;
+  } else {
+    this.selected = undefined;
+  }
+
+  this.render();
+};
+
+/**
+ * Function for rendering list choices
+ * @param  {Number} pointer Position of the pointer
+ * @return {String}         Rendered content
+ */
+
+function renderChoices(choices, pointer) {
+  var output = '';
+  var separatorOffset = 0;
+
+  choices.forEach(function (choice, i) {
+    output += '\n  ';
+
+    if (choice.type === 'separator') {
+      separatorOffset++;
+      output += ' ' + choice;
+      return;
+    }
+
+    var index = i - separatorOffset;
+    var display = (index + 1) + ') ' + choice.name;
+    if (index === pointer) {
+      display = chalk.cyan(display);
+    }
+    output += display;
+  });
+
+  return output;
+}
diff --git a/lib/ui/baseUI.js b/lib/ui/baseUI.js
new file mode 100644
index 0000000..fc4bcd9
--- /dev/null
+++ b/lib/ui/baseUI.js
@@ -0,0 +1,69 @@
+'use strict';
+var _ = require('lodash');
+var MuteStream = require('mute-stream');
+var readline = require('readline');
+
+/**
+ * Base interface class other can inherits from
+ */
+
+var UI = module.exports = function (opt) {
+  // Instantiate the Readline interface
+  // @Note: Don't reassign if already present (allow test to override the Stream)
+  if (!this.rl) {
+    this.rl = readline.createInterface(setupReadlineOptions(opt));
+  }
+  this.rl.resume();
+
+  this.onForceClose = this.onForceClose.bind(this);
+
+  // Make sure new prompt start on a newline when closing
+  this.rl.on('SIGINT', this.onForceClose);
+  process.on('exit', this.onForceClose);
+};
+
+/**
+ * Handle the ^C exit
+ * @return {null}
+ */
+
+UI.prototype.onForceClose = function () {
+  this.close();
+  console.log('\n'); // Line return
+};
+
+/**
+ * Close the interface and cleanup listeners
+ */
+
+UI.prototype.close = function () {
+  // Remove events listeners
+  this.rl.removeListener('SIGINT', this.onForceClose);
+  process.removeListener('exit', this.onForceClose);
+
+  // Restore prompt functionnalities
+  this.rl.output.unmute();
+
+  // Close the readline
+  this.rl.output.end();
+  this.rl.pause();
+  this.rl.close();
+};
+
+function setupReadlineOptions(opt) {
+  opt = opt || {};
+
+  // Default `input` to stdin
+  var input = opt.input || process.stdin;
+
+  // Add mute capabilities to the output
+  var ms = new MuteStream();
+  ms.pipe(opt.output || process.stdout);
+  var output = ms;
+
+  return _.extend({
+    terminal: true,
+    input: input,
+    output: output
+  }, _.omit(opt, ['input', 'output']));
+}
diff --git a/lib/ui/bottom-bar.js b/lib/ui/bottom-bar.js
new file mode 100644
index 0000000..890a6cf
--- /dev/null
+++ b/lib/ui/bottom-bar.js
@@ -0,0 +1,101 @@
+/**
+ * Sticky bottom bar user interface
+ */
+
+var util = require('util');
+var through = require('through');
+var Base = require('./baseUI');
+var rlUtils = require('../utils/readline');
+var _ = require('lodash');
+
+/**
+ * Module exports
+ */
+
+module.exports = Prompt;
+
+/**
+ * Constructor
+ */
+
+function Prompt(opt) {
+  opt || (opt = {});
+
+  Base.apply(this, arguments);
+
+  this.log = through(this.writeLog.bind(this));
+  this.bottomBar = opt.bottomBar || '';
+  this.render();
+}
+util.inherits(Prompt, Base);
+
+/**
+ * Render the prompt to screen
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.render = function () {
+  this.write(this.bottomBar);
+  return this;
+};
+
+Prompt.prototype.clean = function () {
+  rlUtils.clearLine(this.rl, this.bottomBar.split('\n').length);
+  return this;
+};
+
+/**
+ * Update the bottom bar content and rerender
+ * @param  {String} bottomBar Bottom bar content
+ * @return {Prompt}           self
+ */
+
+Prompt.prototype.updateBottomBar = function (bottomBar) {
+  this.bottomBar = bottomBar;
+  rlUtils.clearLine(this.rl, 1);
+  this.rl.output.unmute();
+  this.clean().render();
+  this.rl.output.mute();
+  return this;
+};
+
+/**
+ * Rerender the prompt
+ * @return {Prompt} self
+ */
+
+Prompt.prototype.writeLog = function (data) {
+  rlUtils.clearLine(this.rl, 1);
+  this.rl.output.write(this.enforceLF(data.toString()));
+  return this.render();
+};
+
+/**
+ * Make sure line end on a line feed
+ * @param  {String} str Input string
+ * @return {String}     The input string with a final line feed
+ */
+
+Prompt.prototype.enforceLF = function (str) {
+  return str.match(/[\r\n]$/) ? str : str + '\n';
+};
+
+/**
+ * Helper for writing message in Prompt
+ * @param {Prompt} prompt  - The Prompt object that extends tty
+ * @param {String} message - The message to be output
+ */
+Prompt.prototype.write = function (message) {
+  var msgLines = message.split(/\n/);
+  this.height = msgLines.length;
+
+  // Write message to screen and setPrompt to control backspace
+  this.rl.setPrompt(_.last(msgLines));
+
+  if (this.rl.output.rows === 0 && this.rl.output.columns === 0) {
+    /* When it's a tty through serial port there's no terminal info and the render will malfunction,
+       so we need enforce the cursor to locate to the leftmost position for rendering. */
+    rlUtils.left(this.rl, message.length + this.rl.line.length);
+  }
+  this.rl.output.write(message);
+};
diff --git a/lib/ui/prompt.js b/lib/ui/prompt.js
new file mode 100644
index 0000000..574ea7b
--- /dev/null
+++ b/lib/ui/prompt.js
@@ -0,0 +1,116 @@
+'use strict';
+var _ = require('lodash');
+var rx = require('rx');
+var util = require('util');
+var runAsync = require('run-async');
+var utils = require('../utils/utils');
+var Base = require('./baseUI');
+var Promise = require('pinkie-promise');
+
+/**
+ * Base interface class other can inherits from
+ */
+
+var PromptUI = module.exports = function (prompts, opt) {
+  Base.call(this, opt);
+  this.prompts = prompts;
+};
+util.inherits(PromptUI, Base);
+
+PromptUI.prototype.run = function (questions) {
+  // Keep global reference to the answers
+  this.answers = {};
+
+  // Make sure questions is an array.
+  if (_.isPlainObject(questions)) {
+    questions = [questions];
+  }
+
+  // Create an observable, unless we received one as parameter.
+  // Note: As this is a public interface, we cannot do an instanceof check as we won't
+  // be using the exact same object in memory.
+  var obs = _.isArray(questions) ? rx.Observable.from(questions) : questions;
+
+  this.process = obs
+    .concatMap(this.processQuestion.bind(this))
+    // `publish` creates a hot Observable. It prevents duplicating prompts.
+    .publish();
+
+  this.process.connect();
+
+  return this.process
+    .reduce(function (answers, answer) {
+      _.set(this.answers, answer.name, answer.answer);
+      return this.answers;
+    }.bind(this), {})
+    .toPromise(Promise)
+    .then(this.onCompletion.bind(this));
+};
+
+/**
+ * Once all prompt are over
+ */
+
+PromptUI.prototype.onCompletion = function (answers) {
+  this.close();
+
+  return answers;
+};
+
+PromptUI.prototype.processQuestion = function (question) {
+  question = _.clone(question);
+  return rx.Observable.defer(function () {
+    var obs = rx.Observable.of(question);
+
+    return obs
+      .concatMap(this.setDefaultType.bind(this))
+      .concatMap(this.filterIfRunnable.bind(this))
+      .concatMap(utils.fetchAsyncQuestionProperty.bind(null, question, 'message', this.answers))
+      .concatMap(utils.fetchAsyncQuestionProperty.bind(null, question, 'default', this.answers))
+      .concatMap(utils.fetchAsyncQuestionProperty.bind(null, question, 'choices', this.answers))
+      .concatMap(this.fetchAnswer.bind(this));
+  }.bind(this));
+};
+
+PromptUI.prototype.fetchAnswer = function (question) {
+  var Prompt = this.prompts[question.type];
+  var prompt = new Prompt(question, this.rl, this.answers);
+  return rx.Observable.defer(function () {
+    return rx.Observable.fromPromise(prompt.run().then(function (answer) {
+      return {name: question.name, answer: answer};
+    }));
+  });
+};
+
+PromptUI.prototype.setDefaultType = function (question) {
+  // Default type to input
+  if (!this.prompts[question.type]) {
+    question.type = 'input';
+  }
+  return rx.Observable.defer(function () {
+    return rx.Observable.return(question);
+  });
+};
+
+PromptUI.prototype.filterIfRunnable = function (question) {
+  if (question.when === false) {
+    return rx.Observable.empty();
+  }
+
+  if (!_.isFunction(question.when)) {
+    return rx.Observable.return(question);
+  }
+
+  var answers = this.answers;
+  return rx.Observable.defer(function () {
+    return rx.Observable.fromPromise(
+      runAsync(question.when)(answers).then(function (shouldRun) {
+        if (shouldRun) {
+          return question;
+        }
+      })
+    ).filter(function (val) {
+      return val != null;
+    });
+  });
+};
diff --git a/lib/utils/events.js b/lib/utils/events.js
new file mode 100644
index 0000000..fc2b8bd
--- /dev/null
+++ b/lib/utils/events.js
@@ -0,0 +1,45 @@
+'use strict';
+var rx = require('rx');
+
+function normalizeKeypressEvents(value, key) {
+  return {value: value, key: key || {}};
+}
+
+module.exports = function (rl) {
+  var keypress = rx.Observable.fromEvent(rl.input, 'keypress', normalizeKeypressEvents)
+    .filter(function (e) {
+      // Ignore `enter` key. On the readline, we only care about the `line` event.
+      return e.key.name !== 'enter' && e.key.name !== 'return';
+    });
+
+  return {
+    line: rx.Observable.fromEvent(rl, 'line'),
+    keypress: keypress,
+
+    normalizedUpKey: keypress.filter(function (e) {
+      return e.key.name === 'up' || e.key.name === 'k' || (e.key.name === 'p' && e.key.ctrl);
+    }).share(),
+
+    normalizedDownKey: keypress.filter(function (e) {
+      return e.key.name === 'down' || e.key.name === 'j' || (e.key.name === 'n' && e.key.ctrl);
+    }).share(),
+
+    numberKey: keypress.filter(function (e) {
+      return e.value && '123456789'.indexOf(e.value) >= 0;
+    }).map(function (e) {
+      return Number(e.value);
+    }).share(),
+
+    spaceKey: keypress.filter(function (e) {
+      return e.key && e.key.name === 'space';
+    }).share(),
+
+    aKey: keypress.filter(function (e) {
+      return e.key && e.key.name === 'a';
+    }).share(),
+
+    iKey: keypress.filter(function (e) {
+      return e.key && e.key.name === 'i';
+    }).share()
+  };
+};
diff --git a/lib/utils/paginator.js b/lib/utils/paginator.js
new file mode 100644
index 0000000..0e9b055
--- /dev/null
+++ b/lib/utils/paginator.js
@@ -0,0 +1,37 @@
+'use strict';
+
+var _ = require('lodash');
+var chalk = require('chalk');
+
+/**
+ * The paginator keep trakcs of a pointer index in a list and return
+ * a subset of the choices if the list is too long.
+ */
+
+var Paginator = module.exports = function () {
+  this.pointer = 0;
+  this.lastIndex = 0;
+};
+
+Paginator.prototype.paginate = function (output, active, pageSize) {
+  pageSize = pageSize || 7;
+  var lines = output.split('\n');
+
+  // Make sure there's enough lines to paginate
+  if (lines.length <= pageSize + 2) {
+    return output;
+  }
+
+  // Move the pointer only when the user go down and limit it to 3
+  if (this.pointer < 3 && this.lastIndex < active && active - this.lastIndex < 9) {
+    this.pointer = Math.min(3, this.pointer + active - this.lastIndex);
+  }
+  this.lastIndex = active;
+
+  // Duplicate the lines so it give an infinite list look
+  var infinite = _.flatten([lines, lines, lines]);
+  var topIndex = Math.max(0, active + lines.length - this.pointer);
+
+  var section = infinite.splice(topIndex, pageSize).join('\n');
+  return section + '\n' + chalk.dim('(Move up and down to reveal more choices)');
+};
diff --git a/lib/utils/readline.js b/lib/utils/readline.js
new file mode 100644
index 0000000..092059b
--- /dev/null
+++ b/lib/utils/readline.js
@@ -0,0 +1,51 @@
+'use strict';
+var ansiEscapes = require('ansi-escapes');
+
+/**
+ * Move cursor left by `x`
+ * @param  {Readline} rl - Readline instance
+ * @param  {Number}   x  - How far to go left (default to 1)
+ */
+
+exports.left = function (rl, x) {
+  rl.output.write(ansiEscapes.cursorBackward(x));
+};
+
+/**
+ * Move cursor right by `x`
+ * @param  {Readline} rl - Readline instance
+ * @param  {Number}   x  - How far to go left (default to 1)
+ */
+
+exports.right = function (rl, x) {
+  rl.output.write(ansiEscapes.cursorForward(x));
+};
+
+/**
+ * Move cursor up by `x`
+ * @param  {Readline} rl - Readline instance
+ * @param  {Number}   x  - How far to go up (default to 1)
+ */
+
+exports.up = function (rl, x) {
+  rl.output.write(ansiEscapes.cursorUp(x));
+};
+
+/**
+ * Move cursor down by `x`
+ * @param  {Readline} rl - Readline instance
+ * @param  {Number}   x  - How far to go down (default to 1)
+ */
+
+exports.down = function (rl, x) {
+  rl.output.write(ansiEscapes.cursorDown(x));
+};
+
+/**
+ * Clear current line
+ * @param  {Readline} rl  - Readline instance
+ * @param  {Number}   len - number of line to delete
+ */
+exports.clearLine = function (rl, len) {
+  rl.output.write(ansiEscapes.eraseLines(len));
+};
diff --git a/lib/utils/screen-manager.js b/lib/utils/screen-manager.js
new file mode 100644
index 0000000..25cc8a0
--- /dev/null
+++ b/lib/utils/screen-manager.js
@@ -0,0 +1,129 @@
+'use strict';
+var _ = require('lodash');
+var util = require('./readline');
+var cliWidth = require('cli-width');
+var stripAnsi = require('strip-ansi');
+var stringWidth = require('string-width');
+
+function height(content) {
+  return content.split('\n').length;
+}
+
+function lastLine(content) {
+  return _.last(content.split('\n'));
+}
+
+var ScreenManager = module.exports = function (rl) {
+  // These variables are keeping information to allow correct prompt re-rendering
+  this.height = 0;
+  this.extraLinesUnderPrompt = 0;
+
+  this.rl = rl;
+};
+
+ScreenManager.prototype.render = function (content, bottomContent) {
+  this.rl.output.unmute();
+  this.clean(this.extraLinesUnderPrompt);
+
+  /**
+   * Write message to screen and setPrompt to control backspace
+   */
+
+  var promptLine = lastLine(content);
+  var rawPromptLine = stripAnsi(promptLine);
+
+  // Remove the rl.line from our prompt. We can't rely on the content of
+  // rl.line (mainly because of the password prompt), so just rely on it's
+  // length.
+  var prompt = promptLine;
+  if (this.rl.line.length) {
+    prompt = prompt.slice(0, -this.rl.line.length);
+  }
+  this.rl.setPrompt(prompt);
+
+  // setPrompt will change cursor position, now we can get correct value
+  var cursorPos = this.rl._getCursorPos();
+  var width = this.normalizedCliWidth();
+
+  content = forceLineReturn(content, width);
+  if (bottomContent) {
+    bottomContent = forceLineReturn(bottomContent, width);
+  }
+  // Manually insert an extra line if we're at the end of the line.
+  // This prevent the cursor from appearing at the beginning of the
+  // current line.
+  if (rawPromptLine.length % width === 0) {
+    content += '\n';
+  }
+  var fullContent = content + (bottomContent ? '\n' + bottomContent : '');
+  this.rl.output.write(fullContent);
+
+  /**
+   * Re-adjust the cursor at the correct position.
+   */
+
+  // We need to consider parts of the prompt under the cursor as part of the bottom
+  // content in order to correctly cleanup and re-render.
+  var promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows;
+  var bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);
+  if (bottomContentHeight > 0) {
+    util.up(this.rl, bottomContentHeight);
+  }
+
+  // Reset cursor at the beginning of the line
+  util.left(this.rl, stringWidth(lastLine(fullContent)));
+
+  // Adjust cursor on the right
+  util.right(this.rl, cursorPos.cols);
+
+  /**
+   * Set up state for next re-rendering
+   */
+  this.extraLinesUnderPrompt = bottomContentHeight;
+  this.height = height(fullContent);
+
+  this.rl.output.mute();
+};
+
+ScreenManager.prototype.clean = function (extraLines) {
+  if (extraLines > 0) {
+    util.down(this.rl, extraLines);
+  }
+  util.clearLine(this.rl, this.height);
+};
+
+ScreenManager.prototype.done = function () {
+  this.rl.setPrompt('');
+  this.rl.output.unmute();
+  this.rl.output.write('\n');
+};
+
+ScreenManager.prototype.normalizedCliWidth = function () {
+  var width = cliWidth({
+    defaultWidth: 80,
+    output: this.rl.output
+  });
+  if (process.platform === 'win32') {
+    return width - 1;
+  }
+  return width;
+};
+
+function breakLines(lines, width) {
+  // Break lines who're longuer than the cli width so we can normalize the natural line
+  // returns behavior accross terminals.
+  var regex = new RegExp(
+    '(?:(?:\\033[[0-9;]*m)*.?){1,' + width + '}',
+    'g'
+  );
+  return lines.map(function (line) {
+    var chunk = line.match(regex);
+    // last match is always empty
+    chunk.pop();
+    return chunk || '';
+  });
+}
+
+function forceLineReturn(content, width) {
+  return _.flatten(breakLines(content.split('\n'), width)).join('\n');
+}
diff --git a/lib/utils/utils.js b/lib/utils/utils.js
new file mode 100644
index 0000000..85f652a
--- /dev/null
+++ b/lib/utils/utils.js
@@ -0,0 +1,26 @@
+'use strict';
+var _ = require('lodash');
+var rx = require('rx');
+var runAsync = require('run-async');
+
+/**
+ * Resolve a question property value if it is passed as a function.
+ * This method will overwrite the property on the question object with the received value.
+ * @param  {Object} question - Question object
+ * @param  {String} prop     - Property to fetch name
+ * @param  {Object} answers  - Answers object
+ * @return {rx.Obsersable}   - Observable emitting once value is known
+ */
+
+exports.fetchAsyncQuestionProperty = function (question, prop, answers) {
+  if (!_.isFunction(question[prop])) {
+    return rx.Observable.return(question);
+  }
+
+  return rx.Observable.fromPromise(runAsync(question[prop])(answers)
+    .then(function (value) {
+      question[prop] = value;
+      return question;
+    })
+  );
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..035bead
--- /dev/null
+++ b/package.json
@@ -0,0 +1,77 @@
+{
+  "name": "inquirer",
+  "version": "2.0.0",
+  "description": "A collection of common interactive command line user interfaces.",
+  "author": "Simon Boudrias <admin at simonboudrias.com>",
+  "files": [
+    "lib"
+  ],
+  "main": "lib/inquirer.js",
+  "keywords": [
+    "command",
+    "prompt",
+    "stdin",
+    "cli",
+    "tty",
+    "menu"
+  ],
+  "scripts": {
+    "test": "gulp",
+    "prepublish": "gulp prepublish"
+  },
+  "repository": "SBoudrias/Inquirer.js",
+  "license": "MIT",
+  "dependencies": {
+    "ansi-escapes": "^1.1.0",
+    "chalk": "^1.0.0",
+    "cli-cursor": "^1.0.1",
+    "cli-width": "^2.0.0",
+    "external-editor": "^1.1.0",
+    "figures": "^2.0.0",
+    "lodash": "^4.3.0",
+    "mute-stream": "0.0.6",
+    "pinkie-promise": "^2.0.0",
+    "run-async": "^2.2.0",
+    "rx": "^4.1.0",
+    "string-width": "^2.0.0",
+    "strip-ansi": "^3.0.0",
+    "through": "^2.3.6"
+  },
+  "devDependencies": {
+    "chai": "^3.0.0",
+    "cmdify": "^0.0.4",
+    "eslint": "^3.10.2",
+    "eslint-config-xo-space": "^0.15.0",
+    "gulp": "^3.9.0",
+    "gulp-coveralls": "^0.1.0",
+    "gulp-eslint": "^3.0.1",
+    "gulp-exclude-gitignore": "^1.0.0",
+    "gulp-istanbul": "^1.1.1",
+    "gulp-line-ending-corrector": "^1.0.1",
+    "gulp-mocha": "^3.0.1",
+    "gulp-nsp": "^2.1.0",
+    "gulp-plumber": "^1.0.0",
+    "mocha": "^3.1.2",
+    "mockery": "^2.0.0",
+    "sinon": "^1.12.1"
+  },
+  "eslintConfig": {
+    "extends": "xo-space",
+    "env": {
+      "mocha": true
+    },
+    "rules": {
+      "quotes": [
+        "error",
+        "single"
+      ],
+      "no-unused-expressions": "off",
+      "handle-callback-err": "off",
+      "no-eq-null": "off",
+      "eqeqeq": [
+        "error",
+        "allow-null"
+      ]
+    }
+  }
+}
diff --git a/test/before.js b/test/before.js
new file mode 100644
index 0000000..dee6836
--- /dev/null
+++ b/test/before.js
@@ -0,0 +1,10 @@
+var mockery = require('mockery');
+var ReadlineStub = require('./helpers/readline');
+
+mockery.enable();
+mockery.warnOnUnregistered(false);
+mockery.registerMock('readline', {
+  createInterface: function () {
+    return new ReadlineStub();
+  }
+});
diff --git a/test/bin/write.js b/test/bin/write.js
new file mode 100644
index 0000000..d949b1e
--- /dev/null
+++ b/test/bin/write.js
@@ -0,0 +1,9 @@
+/**
+ * Simple script to write an argument to a file
+ */
+
+var fs = require('fs');
+
+if (process.argv.length === 4) {
+  fs.writeFileSync(process.argv[3], process.argv[2]);
+}
diff --git a/test/helpers/events.js b/test/helpers/events.js
new file mode 100644
index 0000000..8ce735c
--- /dev/null
+++ b/test/helpers/events.js
@@ -0,0 +1,13 @@
+/**
+ * Automatically trigger a line event on the readline on each prompt
+ */
+exports.autosubmit = function (ui) {
+  ui.process.subscribe(function () {
+    // Use setTimeout because async properties on the following question object will still
+    // be processed when we receive the subscribe event.
+    setTimeout(function () {
+      ui.rl.emit('line');
+    }, 5);
+  });
+  ui.rl.emit('line');
+};
diff --git a/test/helpers/fixtures.js b/test/helpers/fixtures.js
new file mode 100644
index 0000000..8762cf5
--- /dev/null
+++ b/test/helpers/fixtures.js
@@ -0,0 +1,59 @@
+var inquirer = require('../../lib/inquirer');
+
+module.exports = {
+  input: {
+    message: 'message',
+    name: 'name'
+  },
+
+  confirm: {
+    message: 'message',
+    name: 'name'
+  },
+
+  password: {
+    message: 'message',
+    name: 'name'
+  },
+
+  list: {
+    message: 'message',
+    name: 'name',
+    choices: ['foo', new inquirer.Separator(), 'bar', 'bum']
+  },
+
+  rawlist: {
+    message: 'message',
+    name: 'name',
+    choices: ['foo', 'bar', new inquirer.Separator(), 'bum']
+  },
+
+  expand: {
+    message: 'message',
+    name: 'name',
+    choices: [
+      {key: 'a', name: 'acab'},
+      new inquirer.Separator(),
+      {key: 'b', name: 'bar'},
+      {key: 'c', name: 'chile'},
+      {key: 'd', name: 'd', value: false}
+    ]
+  },
+
+  checkbox: {
+    message: 'message',
+    name: 'name',
+    choices: [
+      'choice 1',
+      new inquirer.Separator(),
+      'choice 2',
+      'choice 3'
+    ]
+  },
+
+  editor: {
+    message: 'message',
+    name: 'name',
+    default: 'Inquirer'
+  }
+};
diff --git a/test/helpers/readline.js b/test/helpers/readline.js
new file mode 100644
index 0000000..2278b49
--- /dev/null
+++ b/test/helpers/readline.js
@@ -0,0 +1,36 @@
+var EventEmitter = require('events').EventEmitter;
+var sinon = require('sinon');
+var util = require('util');
+var _ = require('lodash');
+
+var stub = {};
+
+_.extend(stub, {
+  write: sinon.stub().returns(stub),
+  moveCursor: sinon.stub().returns(stub),
+  setPrompt: sinon.stub().returns(stub),
+  close: sinon.stub().returns(stub),
+  pause: sinon.stub().returns(stub),
+  resume: sinon.stub().returns(stub),
+  _getCursorPos: sinon.stub().returns({cols: 0, rows: 0}),
+  output: {
+    end: sinon.stub(),
+    mute: sinon.stub(),
+    unmute: sinon.stub(),
+    __raw__: '',
+    write: function (str) {
+      this.__raw__ += str;
+    }
+  }
+});
+
+var ReadlineStub = function () {
+  this.line = '';
+  this.input = new EventEmitter();
+  EventEmitter.apply(this, arguments);
+};
+
+util.inherits(ReadlineStub, EventEmitter);
+_.assign(ReadlineStub.prototype, stub);
+
+module.exports = ReadlineStub;
diff --git a/test/specs/api.js b/test/specs/api.js
new file mode 100644
index 0000000..279cad8
--- /dev/null
+++ b/test/specs/api.js
@@ -0,0 +1,348 @@
+/**
+ * Test Prompt public APIs
+ */
+
+var expect = require('chai').expect;
+var _ = require('lodash');
+var fixtures = require('../helpers/fixtures');
+var ReadlineStub = require('../helpers/readline');
+var inquirer = require('../../lib/inquirer');
+var autosubmit = require('../helpers/events').autosubmit;
+
+// Define prompts and their public API
+var prompts = [
+  {
+    name: 'input',
+    apis: [
+      'filter',
+      'validate',
+      'default',
+      'message',
+      'requiredValues'
+    ]
+  },
+  {
+    name: 'confirm',
+    apis: [
+      'message',
+      'requiredValues'
+    ]
+  },
+  {
+    name: 'rawlist',
+    apis: [
+      'filter',
+      'message',
+      'choices',
+      'requiredValues'
+    ]
+  },
+  {
+    name: 'list',
+    apis: [
+      'filter',
+      'message',
+      'choices',
+      'requiredValues'
+    ]
+  },
+  {
+    name: 'expand',
+    apis: [
+      'requiredValues',
+      'message'
+    ]
+  },
+  {
+    name: 'checkbox',
+    apis: [
+      'requiredValues',
+      'message',
+      'choices',
+      'filter',
+      'validate'
+    ]
+  },
+  {
+    name: 'password',
+    apis: [
+      'requiredValues',
+      'message',
+      'filter',
+      'validate',
+      'default'
+    ]
+  }
+];
+
+// Define tests
+var tests = {
+  filter: function () {
+    describe('filter API', function () {
+      it('should filter the user input', function (done) {
+        this.fixture.filter = function () {
+          return 'pass';
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run().then(function (answer) {
+          expect(answer).to.equal('pass');
+          done();
+        });
+
+        this.rl.emit('line', '');
+      });
+
+      it('should allow filter function to be asynchronous', function (done) {
+        this.fixture.filter = function () {
+          var done = this.async();
+          setTimeout(function () {
+            done(null, 'pass');
+          }, 0);
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run().then(function (answer) {
+          expect(answer).to.equal('pass');
+          done();
+        });
+
+        this.rl.emit('line', '');
+      });
+
+      it('should handle errors produced in async filters', function () {
+        var called = 0;
+        var rl = this.rl;
+
+        this.fixture.filter = function () {
+          called++;
+          var cb = this.async();
+
+          if (called === 2) {
+            return cb(null, 'pass');
+          }
+
+          rl.emit('line');
+          return cb(new Error('fail'));
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        var promise = prompt.run();
+
+        this.rl.emit('line');
+        return promise;
+      });
+    });
+  },
+
+  validate: function () {
+    describe('validate API', function () {
+      it('should reject input if boolean false is returned', function () {
+        var called = 0;
+
+        this.fixture.validate = function () {
+          called++;
+          // Make sure returning false won't continue
+          if (called === 2) {
+            return true;
+          }
+
+          this.rl.emit('line');
+          return false;
+        }.bind(this);
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        var promise = prompt.run();
+
+        this.rl.emit('line');
+        return promise;
+      });
+
+      it('should reject input if a string is returned', function (done) {
+        var self = this;
+        var called = 0;
+        var errorMessage = 'uh oh, error!';
+
+        this.fixture.validate = function () {
+          called++;
+          // Make sure returning false won't continue
+          if (called === 2) {
+            done();
+            return;
+          }
+
+          self.rl.emit('line');
+          return errorMessage;
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run();
+
+        this.rl.emit('line');
+      });
+
+      it('should accept input if boolean true is returned', function () {
+        var called = 0;
+
+        this.fixture.validate = function () {
+          called++;
+          return true;
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        var promise = prompt.run().then(function () {
+          expect(called).to.equal(1);
+        });
+
+        this.rl.emit('line');
+        return promise;
+      });
+
+      it('should allow validate function to be asynchronous', function () {
+        var self = this;
+        var called = 0;
+
+        this.fixture.validate = function () {
+          var done = this.async();
+          setTimeout(function () {
+            called++;
+            // Make sure returning false won't continue
+            if (called === 2) {
+              done(null, true);
+            } else {
+              self.rl.emit('line');
+            }
+            done(false);
+          }, 0);
+        };
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        var promise = prompt.run();
+
+        this.rl.emit('line');
+        return promise;
+      });
+
+      it('should pass previous answers to the prompt validation function', function () {
+        var prompt = inquirer.createPromptModule();
+        var questions = [{
+          type: 'confirm',
+          name: 'q1',
+          message: 'message'
+        }, {
+          type: 'confirm',
+          name: 'q2',
+          message: 'message',
+          validate: function (input, answers) {
+            expect(answers.q1).to.be.true;
+            return true;
+          },
+          default: false
+        }];
+
+        var promise = prompt(questions);
+        autosubmit(promise.ui);
+
+        return promise.then(function (answers) {
+          expect(answers.q1).to.be.true;
+          expect(answers.q2).to.be.false;
+        });
+      });
+    });
+  },
+
+  default: function () {
+    describe('default API', function () {
+      it('should allow a default value', function (done) {
+        this.fixture.default = 'pass';
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run().then(function (answer) {
+          expect(this.rl.output.__raw__).to.contain('(pass)');
+          expect(answer).to.equal('pass');
+          done();
+        }.bind(this));
+
+        this.rl.emit('line', '');
+      });
+
+      it('should allow a falsy default value', function (done) {
+        this.fixture.default = 0;
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run().then(function (answer) {
+          expect(this.rl.output.__raw__).to.contain('(0)');
+          expect(answer).to.equal(0);
+          done();
+        }.bind(this));
+
+        this.rl.emit('line', '');
+      });
+    });
+  },
+
+  message: function () {
+    describe('message API', function () {
+      it('should print message on screen', function () {
+        this.fixture.message = 'Foo bar bar foo bar';
+
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        prompt.run();
+
+        expect(this.rl.output.__raw__).to.contain(this.fixture.message);
+      });
+    });
+  },
+
+  choices: function () {
+    describe('choices API', function () {
+      it('should print choices to screen', function () {
+        var prompt = new this.Prompt(this.fixture, this.rl);
+        var choices = prompt.opt.choices;
+
+        prompt.run();
+
+        _.each(choices.filter(inquirer.Separator.exclude), function (choice) {
+          expect(this.rl.output.__raw__).to.contain(choice.name);
+        }.bind(this));
+      });
+    });
+  },
+
+  requiredValues: function () {
+    describe('Missing value', function () {
+      it('`message` should throw', function () {
+        var mkPrompt = function () {
+          delete this.fixture.message;
+          return new this.Prompt(this.fixture, this.rl);
+        }.bind(this);
+        expect(mkPrompt).to.throw(/message/);
+      });
+
+      it('`name` should throw', function () {
+        var mkPrompt = function () {
+          delete this.fixture.name;
+          return new this.Prompt(this.fixture, this.rl);
+        }.bind(this);
+        expect(mkPrompt).to.throw(/name/);
+      });
+    });
+  }
+};
+
+// Run tests
+describe('Prompt public APIs', function () {
+  _.each(prompts, function (detail) {
+    describe('on ' + detail.name + ' prompt', function () {
+      beforeEach(function () {
+        this.fixture = _.clone(fixtures[detail.name]);
+        this.Prompt = inquirer.prompt.prompts[detail.name];
+        this.rl = new ReadlineStub();
+      });
+
+      _.each(detail.apis, function (apiName) {
+        tests[apiName](detail.name);
+      });
+    });
+  });
+});
diff --git a/test/specs/inquirer.js b/test/specs/inquirer.js
new file mode 100644
index 0000000..6a82dea
--- /dev/null
+++ b/test/specs/inquirer.js
@@ -0,0 +1,603 @@
+/**
+ * Inquirer public API test
+ */
+
+var expect = require('chai').expect;
+var sinon = require('sinon');
+var _ = require('lodash');
+var rx = require('rx');
+var Promise = require('pinkie-promise');
+var inquirer = require('../../lib/inquirer');
+var autosubmit = require('../helpers/events').autosubmit;
+
+describe('inquirer.prompt', function () {
+  beforeEach(function () {
+    this.prompt = inquirer.createPromptModule();
+  });
+
+  it('should close and create a new readline instances each time it\'s called', function () {
+    var ctx = this;
+    var rl1;
+
+    var promise = this.prompt({
+      type: 'confirm',
+      name: 'q1',
+      message: 'message'
+    });
+
+    rl1 = promise.ui.rl;
+    rl1.emit('line');
+
+    return promise.then(function () {
+      expect(rl1.close.called).to.be.true;
+      expect(rl1.output.end.called).to.be.true;
+
+      var rl2;
+      var promise2 = ctx.prompt({
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      });
+
+      rl2 = promise2.ui.rl;
+      rl2.emit('line');
+
+      return promise2.then(function () {
+        expect(rl2.close.called).to.be.true;
+        expect(rl2.output.end.called).to.be.true;
+
+        expect(rl1).to.not.equal(rl2);
+      });
+    });
+  });
+
+  it('should take a prompts array and return answers', function () {
+    var prompts = [{
+      type: 'confirm',
+      name: 'q1',
+      message: 'message'
+    }, {
+      type: 'confirm',
+      name: 'q2',
+      message: 'message',
+      default: false
+    }];
+
+    var promise = this.prompt(prompts);
+    autosubmit(promise.ui);
+
+    return promise.then(function (answers) {
+      expect(answers.q1).to.be.true;
+      expect(answers.q2).to.be.false;
+    });
+  });
+
+  it('should take a prompts array with nested names', function () {
+    var prompts = [{
+      type: 'confirm',
+      name: 'foo.bar.q1',
+      message: 'message'
+    }, {
+      type: 'confirm',
+      name: 'foo.q2',
+      message: 'message',
+      default: false
+    }];
+
+    var promise = this.prompt(prompts);
+    autosubmit(promise.ui);
+
+    return promise.then(function (answers) {
+      expect(answers).to.deep.equal({
+        foo: {
+          bar: {
+            q1: true
+          },
+          q2: false
+        }
+      });
+    });
+  });
+
+  it('should take a single prompt and return answer', function () {
+    var prompt = {
+      type: 'input',
+      name: 'q1',
+      message: 'message',
+      default: 'bar'
+    };
+
+    var promise = this.prompt(prompt);
+
+    promise.ui.rl.emit('line');
+    return promise.then(function (answers) {
+      expect(answers.q1).to.equal('bar');
+    });
+  });
+
+  it('should parse `message` if passed as a function', function () {
+    var stubMessage = 'foo';
+    this.prompt.registerPrompt('stub', function (params) {
+      this.opt = {
+        when: function () {
+          return true;
+        }
+      };
+      this.run = sinon.stub().returns(Promise.resolve());
+      expect(params.message).to.equal(stubMessage);
+    });
+
+    var msgFunc = function (answers) {
+      expect(answers.name1).to.equal('bar');
+      return stubMessage;
+    };
+
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'stub',
+      name: 'name',
+      message: msgFunc
+    }];
+
+    var promise = this.prompt(prompts);
+    promise.ui.rl.emit('line');
+    promise.ui.rl.emit('line');
+    return promise.then(function () {
+      // Ensure we're not overwriting original prompt values.
+      expect(prompts[1].message).to.equal(msgFunc);
+    });
+  });
+
+  it('should run asynchronous `message`', function (done) {
+    var stubMessage = 'foo';
+    this.prompt.registerPrompt('stub', function (params) {
+      this.opt = {
+        when: function () {
+          return true;
+        }
+      };
+      this.run = sinon.stub().returns(Promise.resolve());
+      expect(params.message).to.equal(stubMessage);
+      done();
+    });
+
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'stub',
+      name: 'name',
+      message: function (answers) {
+        expect(answers.name1).to.equal('bar');
+        var goOn = this.async();
+        setTimeout(function () {
+          goOn(null, stubMessage);
+        }, 0);
+      }
+    }];
+
+    var promise = this.prompt(prompts, function () {});
+    promise.ui.rl.emit('line');
+  });
+
+  it('should parse `default` if passed as a function', function (done) {
+    var stubDefault = 'foo';
+    this.prompt.registerPrompt('stub', function (params) {
+      this.opt = {
+        when: function () {
+          return true;
+        }
+      };
+      this.run = sinon.stub().returns(Promise.resolve());
+      expect(params.default).to.equal(stubDefault);
+      done();
+    });
+
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'stub',
+      name: 'name',
+      message: 'message',
+      default: function (answers) {
+        expect(answers.name1).to.equal('bar');
+        return stubDefault;
+      }
+    }];
+
+    var promise = this.prompt(prompts, function () {});
+    promise.ui.rl.emit('line');
+  });
+
+  it('should run asynchronous `default`', function () {
+    var goesInDefault = false;
+    var input2Default = 'foo';
+    var promise;
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'input2',
+      name: 'q2',
+      message: 'message',
+      default: function (answers) {
+        goesInDefault = true;
+        expect(answers.name1).to.equal('bar');
+        var goOn = this.async();
+        setTimeout(function () {
+          goOn(null, input2Default);
+        }, 0);
+        setTimeout(function () {
+          promise.ui.rl.emit('line');
+        }, 10);
+      }
+    }];
+
+    promise = this.prompt(prompts);
+    promise.ui.rl.emit('line');
+
+    return promise.then(function (answers) {
+      expect(goesInDefault).to.be.true;
+      expect(answers.q2).to.equal(input2Default);
+    });
+  });
+
+  it('should pass previous answers to the prompt constructor', function (done) {
+    this.prompt.registerPrompt('stub', function (params, rl, answers) {
+      this.run = sinon.stub().returns(Promise.resolve());
+      expect(answers.name1).to.equal('bar');
+      done();
+    });
+
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'stub',
+      name: 'name',
+      message: 'message'
+    }];
+
+    var promise = this.prompt(prompts);
+    promise.ui.rl.emit('line');
+  });
+
+  it('should parse `choices` if passed as a function', function (done) {
+    var stubChoices = ['foo', 'bar'];
+    this.prompt.registerPrompt('stub', function (params) {
+      this.run = sinon.stub().returns(Promise.resolve());
+      this.opt = {
+        when: function () {
+          return true;
+        }
+      };
+      expect(params.choices).to.equal(stubChoices);
+      done();
+    });
+
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'stub',
+      name: 'name',
+      message: 'message',
+      choices: function (answers) {
+        expect(answers.name1).to.equal('bar');
+        return stubChoices;
+      }
+    }];
+
+    var promise = this.prompt(prompts, function () {});
+    promise.ui.rl.emit('line');
+  });
+
+  it('should returns a promise', function (done) {
+    var prompt = {
+      type: 'input',
+      name: 'q1',
+      message: 'message',
+      default: 'bar'
+    };
+
+    var promise = this.prompt(prompt);
+    promise.then(function (answers) {
+      expect(answers.q1).to.equal('bar');
+      done();
+    });
+
+    promise.ui.rl.emit('line');
+  });
+
+  it('should expose the Reactive interface', function (done) {
+    var prompts = [{
+      type: 'input',
+      name: 'name1',
+      message: 'message',
+      default: 'bar'
+    }, {
+      type: 'input',
+      name: 'name',
+      message: 'message',
+      default: 'doe'
+    }];
+
+    var promise = this.prompt(prompts);
+    var spy = sinon.spy();
+    promise.ui.process.subscribe(spy, function () {}, function () {
+      sinon.assert.calledWith(spy, {name: 'name1', answer: 'bar'});
+      sinon.assert.calledWith(spy, {name: 'name', answer: 'doe'});
+      done();
+    });
+
+    autosubmit(promise.ui);
+  });
+
+  it('should expose the UI', function (done) {
+    var promise = this.prompt([], function () {});
+    expect(promise.ui.answers).to.be.an('object');
+    done();
+  });
+
+  it('takes an Observable as question', function () {
+    var promise;
+    var prompts = rx.Observable.create(function (obs) {
+      obs.onNext({
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      });
+      setTimeout(function () {
+        obs.onNext({
+          type: 'confirm',
+          name: 'q2',
+          message: 'message',
+          default: false
+        });
+        obs.onCompleted();
+        promise.ui.rl.emit('line');
+      }, 30);
+    });
+
+    promise = this.prompt(prompts);
+    promise.ui.rl.emit('line');
+
+    return promise.then(function (answers) {
+      expect(answers.q1).to.be.true;
+      expect(answers.q2).to.be.false;
+    });
+  });
+
+  describe('hierarchical mode (`when`)', function () {
+    it('should pass current answers to `when`', function () {
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        name: 'q2',
+        message: 'message',
+        when: function (answers) {
+          expect(answers).to.be.an('object');
+          expect(answers.q1).to.be.true;
+        }
+      }];
+
+      var promise = this.prompt(prompts);
+
+      autosubmit(promise.ui);
+      return promise;
+    });
+
+    it('should run prompt if `when` returns true', function () {
+      var goesInWhen = false;
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        type: 'input',
+        name: 'q2',
+        message: 'message',
+        default: 'bar-var',
+        when: function () {
+          goesInWhen = true;
+          return true;
+        }
+      }];
+
+      var promise = this.prompt(prompts);
+      autosubmit(promise.ui);
+
+      return promise.then(function (answers) {
+        expect(goesInWhen).to.be.true;
+        expect(answers.q2).to.equal('bar-var');
+      });
+    });
+
+    it('should run prompt if `when` is true', function () {
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        type: 'input',
+        name: 'q2',
+        message: 'message',
+        default: 'bar-var',
+        when: true
+      }];
+
+      var promise = this.prompt(prompts);
+      autosubmit(promise.ui);
+
+      return promise.then(function (answers) {
+        expect(answers.q2).to.equal('bar-var');
+      });
+    });
+
+    it('should not run prompt if `when` returns false', function () {
+      var goesInWhen = false;
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        type: 'confirm',
+        name: 'q2',
+        message: 'message',
+        when: function () {
+          goesInWhen = true;
+          return false;
+        }
+      }, {
+        type: 'input',
+        name: 'q3',
+        message: 'message',
+        default: 'foo'
+      }];
+
+      var promise = this.prompt(prompts);
+      autosubmit(promise.ui);
+
+      return promise.then(function (answers) {
+        expect(goesInWhen).to.be.true;
+        expect(answers.q2).to.not.exist;
+        expect(answers.q3).to.equal('foo');
+        expect(answers.q1).to.be.true;
+      });
+    });
+
+    it('should not run prompt if `when` is false', function () {
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        type: 'confirm',
+        name: 'q2',
+        message: 'message',
+        when: false
+      }, {
+        type: 'input',
+        name: 'q3',
+        message: 'message',
+        default: 'foo'
+      }];
+
+      var promise = this.prompt(prompts);
+      autosubmit(promise.ui);
+
+      return promise.then(function (answers) {
+        expect(answers.q2).to.not.exist;
+        expect(answers.q3).to.equal('foo');
+        expect(answers.q1).to.be.true;
+      });
+    });
+
+    it('should run asynchronous `when`', function () {
+      var promise;
+      var goesInWhen = false;
+      var prompts = [{
+        type: 'confirm',
+        name: 'q1',
+        message: 'message'
+      }, {
+        type: 'input',
+        name: 'q2',
+        message: 'message',
+        default: 'foo-bar',
+        when: function () {
+          goesInWhen = true;
+          var goOn = this.async();
+          setTimeout(function () {
+            goOn(null, true);
+          }, 0);
+          setTimeout(function () {
+            promise.ui.rl.emit('line');
+          }, 10);
+        }
+      }];
+
+      promise = this.prompt(prompts);
+      autosubmit(promise.ui);
+
+      return promise.then(function (answers) {
+        expect(goesInWhen).to.be.true;
+        expect(answers.q2).to.equal('foo-bar');
+      });
+    });
+  });
+
+  describe('#registerPrompt()', function () {
+    it('register new prompt types', function (done) {
+      var questions = [{type: 'foo', message: 'something'}];
+      inquirer.registerPrompt('foo', function (question, rl, answers) {
+        expect(question).to.eql(questions[0]);
+        expect(answers).to.eql({});
+        this.run = sinon.stub().returns(Promise.resolve());
+        done();
+      });
+
+      inquirer.prompt(questions, _.noop);
+    });
+
+    it('overwrite default prompt types', function (done) {
+      var questions = [{type: 'confirm', message: 'something'}];
+      inquirer.registerPrompt('confirm', function () {
+        this.run = sinon.stub().returns(Promise.resolve());
+        done();
+      });
+
+      inquirer.prompt(questions, _.noop);
+      inquirer.restoreDefaultPrompts();
+    });
+  });
+
+  describe('#restoreDefaultPrompts()', function () {
+    it('restore default prompts', function () {
+      var ConfirmPrompt = inquirer.prompt.prompts.confirm;
+      inquirer.registerPrompt('confirm', _.noop);
+      inquirer.restoreDefaultPrompts();
+      expect(ConfirmPrompt).to.equal(inquirer.prompt.prompts.confirm);
+    });
+  });
+
+  // see: https://github.com/SBoudrias/Inquirer.js/pull/326
+  it('does not throw exception if cli-width reports width of 0', function () {
+    var original = process.stdout.getWindowSize;
+    process.stdout.getWindowSize = function () {
+      return [0];
+    };
+    var prompt = inquirer.createPromptModule();
+
+    var prompts = [{
+      type: 'confirm',
+      name: 'q1',
+      message: 'message'
+    }];
+
+    var promise = prompt(prompts);
+    promise.ui.rl.emit('line');
+
+    return promise.then(function (answers) {
+      process.stdout.getWindowSize = original;
+      expect(answers.q1).to.equal(true);
+    });
+  });
+});
diff --git a/test/specs/objects/choice.js b/test/specs/objects/choice.js
new file mode 100644
index 0000000..055d572
--- /dev/null
+++ b/test/specs/objects/choice.js
@@ -0,0 +1,43 @@
+var expect = require('chai').expect;
+
+var Choice = require('../../../lib/objects/choice');
+var Separator = require('../../../lib/objects/separator');
+
+describe('Choice object', function () {
+  it('should normalize accept String as value', function () {
+    var choice = new Choice('foo');
+    expect(choice.name).to.equal('foo');
+    expect(choice.value).to.equal('foo');
+  });
+
+  it('should use value|name as default if default property is missing', function () {
+    var onlyName = new Choice({name: 'foo'});
+    var onlyVal = new Choice({value: 'bar'});
+
+    expect(onlyName.name).to.equal('foo');
+    expect(onlyName.value).to.equal('foo');
+    expect(onlyName.short).to.equal('foo');
+    expect(onlyVal.name).to.equal('bar');
+    expect(onlyVal.value).to.equal('bar');
+    expect(onlyVal.short).to.equal('bar');
+  });
+
+  it('should keep extra keys', function () {
+    var choice = new Choice({name: 'foo', extra: '1'});
+
+    expect(choice.extra).to.equal('1');
+    expect(choice.name).to.equal('foo');
+    expect(choice.value).to.equal('foo');
+  });
+
+  it('shouldn\'t process Separator object', function () {
+    var sep = new Choice(new Separator());
+    expect(sep).to.be.instanceOf(Separator);
+  });
+
+  it('shouldn\'t process object with property type=separator', function () {
+    var obj = {type: 'separator'};
+    var sep = new Choice(obj);
+    expect(sep).to.equal(obj);
+  });
+});
diff --git a/test/specs/objects/choices.js b/test/specs/objects/choices.js
new file mode 100644
index 0000000..bbc247a
--- /dev/null
+++ b/test/specs/objects/choices.js
@@ -0,0 +1,75 @@
+var expect = require('chai').expect;
+
+var inquirer = require('../../../lib/inquirer');
+var Choices = require('../../../lib/objects/choices');
+var Choice = require('../../../lib/objects/choice');
+
+describe('Choices collection', function () {
+  it('should create Choice object from array member', function () {
+    var choices = new Choices(['bar', {name: 'foo'}]);
+    expect(choices.getChoice(0)).to.be.instanceOf(Choice);
+    expect(choices.getChoice(1)).to.be.instanceOf(Choice);
+  });
+
+  it('should not process Separator object', function () {
+    var sep = new inquirer.Separator();
+    var choices = new Choices(['Bar', sep]);
+    expect(choices.get(0).name).to.equal('Bar');
+    expect(choices.get(1)).to.equal(sep);
+  });
+
+  it('should provide access to length information', function () {
+    var choices = new Choices(['Bar', new inquirer.Separator(), 'foo']);
+    expect(choices.length).to.equal(3);
+    expect(choices.realLength).to.equal(2);
+
+    choices.length = 1;
+    expect(choices.length).to.equal(1);
+    expect(choices.get(1)).to.not.exist;
+    expect(function () {
+      choices.realLength = 0;
+    }).to.throw;
+  });
+
+  it('should allow plucking choice content', function () {
+    var choices = new Choices([{name: 'n', key: 'foo'}, {name: 'a', key: 'lab'}]);
+    expect(choices.pluck('key')).to.eql(['foo', 'lab']);
+  });
+
+  it('should allow filtering value with where', function () {
+    var choices = new Choices([{name: 'n', key: 'foo'}, {name: 'a', key: 'lab'}]);
+    expect(choices.where({key: 'lab'})).to.eql([{
+      name: 'a',
+      value: 'a',
+      short: 'a',
+      key: 'lab',
+      disabled: undefined
+    }]);
+  });
+
+  it('should façade forEach', function () {
+    var raw = ['a', 'b', 'c'];
+    var choices = new Choices(raw);
+    choices.forEach(function (val, i) {
+      expect(val.name).to.equal(raw[i]);
+    });
+  });
+
+  it('should façade filter', function () {
+    var choices = new Choices(['a', 'b', 'c']);
+    var filtered = choices.filter(function (val) {
+      return val.name === 'a';
+    });
+    expect(filtered.length).to.equal(1);
+    expect(filtered[0].name).to.equal('a');
+  });
+
+  it('should façade push and update the realChoices internally', function () {
+    var choices = new Choices(['a']);
+    choices.push('b', new inquirer.Separator());
+    expect(choices.length).to.equal(3);
+    expect(choices.realLength).to.equal(2);
+    expect(choices.getChoice(1)).to.be.instanceOf(Choice);
+    expect(choices.get(2)).to.be.instanceOf(inquirer.Separator);
+  });
+});
diff --git a/test/specs/objects/separator.js b/test/specs/objects/separator.js
new file mode 100644
index 0000000..99dcfb8
--- /dev/null
+++ b/test/specs/objects/separator.js
@@ -0,0 +1,36 @@
+var expect = require('chai').expect;
+var chalk = require('chalk');
+
+var Separator = require('../../../lib/objects/separator');
+var Inquirer = require('../../../lib/inquirer');
+
+describe('Separator constructor', function () {
+  it('should set a default', function () {
+    var sep = new Separator();
+    expect(chalk.stripColor(sep.toString())).to.equal('──────────────');
+  });
+
+  it('should set user input as separator', function () {
+    var sep = new Separator('foo bar');
+    expect(chalk.stripColor(sep.toString())).to.equal('foo bar');
+  });
+
+  it('instances should be stringified when appended to a string', function () {
+    var sep = new Separator('foo bar');
+    expect(chalk.stripColor(String(sep))).to.equal('foo bar');
+  });
+
+  it('should be exposed on Inquirer object', function () {
+    expect(Inquirer.Separator).to.equal(Separator);
+  });
+
+  it('should expose a helper function to check for separator', function () {
+    expect(Separator.exclude({})).to.be.true;
+    expect(Separator.exclude(new Separator())).to.be.false;
+  });
+
+  it('give the type \'separator\' to its object', function () {
+    var sep = new Separator();
+    expect(sep.type).to.equal('separator');
+  });
+});
diff --git a/test/specs/prompts/base.js b/test/specs/prompts/base.js
new file mode 100644
index 0000000..f74aecd
--- /dev/null
+++ b/test/specs/prompts/base.js
@@ -0,0 +1,25 @@
+var expect = require('chai').expect;
+var ReadlineStub = require('../../helpers/readline');
+
+var Base = require('../../../lib/prompts/base');
+
+describe('`base` prompt (e.g. prompt helpers)', function () {
+  beforeEach(function () {
+    this.rl = new ReadlineStub();
+    this.base = new Base({
+      message: 'foo bar',
+      name: 'name'
+    }, this.rl);
+  });
+
+  it('should not point by reference to the entry `question` object', function () {
+    var question = {
+      message: 'foo bar',
+      name: 'name'
+    };
+    var base = new Base(question, this.rl);
+    expect(question).to.not.equal(base.opt);
+    expect(question.name).to.equal(base.opt.name);
+    expect(question.message).to.equal(base.opt.message);
+  });
+});
diff --git a/test/specs/prompts/checkbox.js b/test/specs/prompts/checkbox.js
new file mode 100644
index 0000000..7d4f72d
--- /dev/null
+++ b/test/specs/prompts/checkbox.js
@@ -0,0 +1,258 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Checkbox = require('../../../lib/prompts/checkbox');
+
+describe('`checkbox` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.checkbox);
+    this.rl = new ReadlineStub();
+    this.checkbox = new Checkbox(this.fixture, this.rl);
+  });
+
+  it('should return a single selected choice in an array', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer).to.be.an('array');
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 1');
+      done();
+    });
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should return multiples selected choices in an array', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer).to.be.an('array');
+      expect(answer.length).to.equal(2);
+      expect(answer[0]).to.equal('choice 1');
+      expect(answer[1]).to.equal('choice 2');
+      done();
+    });
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.input.emit('keypress', null, {name: 'down'});
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should check defaults choices', function (done) {
+    this.fixture.choices = [
+      {name: '1', checked: true},
+      {name: '2', checked: false},
+      {name: '3', checked: false}
+    ];
+    this.checkbox = new Checkbox(this.fixture, this.rl);
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('1');
+      done();
+    });
+    this.rl.emit('line');
+  });
+
+  it('provide an array of checked choice to validate', function () {
+    this.fixture.choices = [
+      {name: '1', checked: true},
+      {name: '2', checked: 1},
+      {name: '3', checked: false}
+    ];
+    this.fixture.validate = function (answer) {
+      expect(answer).to.eql(['1', '2']);
+      return true;
+    };
+    this.checkbox = new Checkbox(this.fixture, this.rl);
+    var promise = this.checkbox.run();
+    this.rl.emit('line');
+    return promise;
+  });
+
+  it('should check defaults choices if given as array of values', function (done) {
+    this.fixture.choices = [
+      {name: '1'},
+      {name: '2'},
+      {name: '3'}
+    ];
+    this.fixture.default = ['1', '3'];
+    this.checkbox = new Checkbox(this.fixture, this.rl);
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(2);
+      expect(answer[0]).to.equal('1');
+      expect(answer[1]).to.equal('3');
+      done();
+    });
+    this.rl.emit('line');
+  });
+
+  it('should toggle choice when hitting space', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 1');
+      done();
+    });
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.input.emit('keypress', null, {name: 'down'});
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for arrow navigation', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 2');
+      done();
+    });
+
+    this.rl.input.emit('keypress', null, {name: 'down'});
+    this.rl.input.emit('keypress', null, {name: 'down'});
+    this.rl.input.emit('keypress', null, {name: 'up'});
+
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for vi-style navigation', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 2');
+      done();
+    });
+
+    this.rl.input.emit('keypress', 'j', {name: 'j'});
+    this.rl.input.emit('keypress', 'j', {name: 'j'});
+    this.rl.input.emit('keypress', 'k', {name: 'k'});
+
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for emacs-style navigation', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 2');
+      done();
+    });
+
+    this.rl.input.emit('keypress', 'n', {name: 'n', ctrl: true});
+    this.rl.input.emit('keypress', 'n', {name: 'n', ctrl: true});
+    this.rl.input.emit('keypress', 'p', {name: 'p', ctrl: true});
+
+    this.rl.input.emit('keypress', ' ', {name: 'space'});
+    this.rl.emit('line');
+  });
+
+  it('should allow 1-9 shortcut key', function (done) {
+    this.checkbox.run().then(function (answer) {
+      expect(answer.length).to.equal(1);
+      expect(answer[0]).to.equal('choice 2');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '2');
+    this.rl.emit('line');
+  });
+
+  it('should select all answers if <a> is pressed', function () {
+    var promise = this.checkbox.run();
+
+    this.rl.input.emit('keypress', 'a', {name: 'a'});
+    this.rl.emit('line');
+
+    return promise.then(function (answer) {
+      expect(answer.length).to.equal(3);
+    });
+  });
+
+  it('should select no answers if <a> is pressed a second time', function () {
+    var promise = this.checkbox.run();
+
+    this.rl.input.emit('keypress', 'a', {name: 'a'});
+    this.rl.input.emit('keypress', 'a', {name: 'a'});
+    this.rl.emit('line');
+
+    return promise.then(function (answer) {
+      expect(answer.length).to.equal(0);
+    });
+  });
+
+  it('should select the inverse of the current selection when <i> is pressed', function () {
+    var promise = this.checkbox.run();
+
+    this.rl.input.emit('keypress', 'i', {name: 'i'});
+    this.rl.emit('line');
+
+    return promise.then(function (answer) {
+      expect(answer.length).to.equal(3);
+    });
+  });
+
+  describe('with disabled choices', function () {
+    beforeEach(function () {
+      this.fixture.choices.push({
+        name: 'dis1',
+        disabled: true
+      });
+      this.fixture.choices.push({
+        name: 'dis2',
+        disabled: 'uh oh'
+      });
+      this.checkbox = new Checkbox(this.fixture, this.rl);
+    });
+
+    it('output disabled choices and custom messages', function () {
+      var promise = this.checkbox.run();
+      this.rl.emit('line');
+      return promise.then(function () {
+        expect(this.rl.output.__raw__).to.contain('- dis1 (Disabled)');
+        expect(this.rl.output.__raw__).to.contain('- dis2 (uh oh)');
+      }.bind(this));
+    });
+
+    it('skip disabled choices', function (done) {
+      this.checkbox.run().then(function (answer) {
+        expect(answer[0]).to.equal('choice 1');
+        done();
+      });
+      this.rl.input.emit('keypress', null, {name: 'down'});
+      this.rl.input.emit('keypress', null, {name: 'down'});
+      this.rl.input.emit('keypress', null, {name: 'down'});
+
+      this.rl.input.emit('keypress', ' ', {name: 'space'});
+      this.rl.emit('line');
+    });
+
+    it('uncheck defaults choices who\'re disabled', function (done) {
+      this.fixture.choices = [
+        {name: '1', checked: true, disabled: true},
+        {name: '2'}
+      ];
+      this.checkbox = new Checkbox(this.fixture, this.rl);
+      this.checkbox.run().then(function (answer) {
+        expect(answer.length).to.equal(0);
+        done();
+      });
+      this.rl.emit('line');
+    });
+
+    it('disabled can be a function', function () {
+      this.fixture.choices = [
+        {
+          name: 'dis1',
+          disabled: function (answers) {
+            expect(answers.foo).to.equal('foo');
+            return true;
+          }
+        }
+      ];
+      this.checkbox = new Checkbox(this.fixture, this.rl, {foo: 'foo'});
+      var promise = this.checkbox.run();
+      this.rl.emit('line');
+
+      promise.then(function () {
+        expect(this.rl.output.__raw__).to.contain('- dis1 (Disabled)');
+      }.bind(this));
+    });
+  });
+});
diff --git a/test/specs/prompts/confirm.js b/test/specs/prompts/confirm.js
new file mode 100644
index 0000000..e812ffc
--- /dev/null
+++ b/test/specs/prompts/confirm.js
@@ -0,0 +1,90 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Confirm = require('../../../lib/prompts/confirm');
+
+describe('`confirm` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.confirm);
+    this.rl = new ReadlineStub();
+    this.confirm = new Confirm(this.fixture, this.rl);
+  });
+
+  afterEach(function () {
+    Confirm.prototype.write = this._write;
+  });
+
+  it('should default to true', function (done) {
+    this.confirm.run().then(function (answer) {
+      expect(this.rl.output.__raw__).to.contain('Y/n');
+      expect(answer).to.be.true;
+      done();
+    }.bind(this));
+
+    this.rl.emit('line', '');
+  });
+
+  it('should allow a default `false` value', function (done) {
+    this.fixture.default = false;
+    var falseConfirm = new Confirm(this.fixture, this.rl);
+
+    falseConfirm.run().then(function (answer) {
+      expect(this.rl.output.__raw__).to.contain('y/N');
+      expect(answer).to.be.false;
+      done();
+    }.bind(this));
+
+    this.rl.emit('line', '');
+  });
+
+  it('should allow a default `true` value', function (done) {
+    this.fixture.default = true;
+    var falseConfirm = new Confirm(this.fixture, this.rl);
+
+    falseConfirm.run().then(function (answer) {
+      expect(this.rl.output.__raw__).to.contain('Y/n');
+      expect(answer).to.be.true;
+      done();
+    }.bind(this));
+
+    this.rl.emit('line', '');
+  });
+
+  it('should parse \'Y\' value to boolean true', function (done) {
+    this.confirm.run().then(function (answer) {
+      expect(answer).to.be.true;
+      done();
+    });
+
+    this.rl.emit('line', 'Y');
+  });
+
+  it('should parse \'Yes\' value to boolean true', function (done) {
+    this.confirm.run().then(function (answer) {
+      expect(answer).to.be.true;
+      done();
+    });
+
+    this.rl.emit('line', 'Yes');
+  });
+
+  it('should parse \'No\' value to boolean false', function (done) {
+    this.confirm.run().then(function (answer) {
+      expect(answer).to.be.false;
+      done();
+    });
+
+    this.rl.emit('line', 'No');
+  });
+
+  it('should parse every other string value to boolean false', function (done) {
+    this.confirm.run().then(function (answer) {
+      expect(answer).to.be.false;
+      done();
+    });
+
+    this.rl.emit('line', 'bla bla foo');
+  });
+});
diff --git a/test/specs/prompts/editor.js b/test/specs/prompts/editor.js
new file mode 100644
index 0000000..0ea1e04
--- /dev/null
+++ b/test/specs/prompts/editor.js
@@ -0,0 +1,31 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Editor = require('../../../lib/prompts/editor');
+
+describe('`editor` prompt', function () {
+  beforeEach(function () {
+    this.previousVisual = process.env.VISUAL;
+    // Writes the word "testing" to the file
+    process.env.VISUAL = 'node ./test/bin/write.js testing';
+    this.fixture = _.clone(fixtures.editor);
+    this.rl = new ReadlineStub();
+  });
+
+  afterEach(function () {
+    process.env.VISUAL = this.previousVisual;
+  });
+
+  it('should retrieve temporary files contents', function () {
+    var prompt = new Editor(this.fixture, this.rl);
+
+    var promise = prompt.run();
+    this.rl.emit('line', '');
+
+    return promise.then(function (answer) {
+      return expect(answer).to.equal('testing');
+    });
+  });
+});
diff --git a/test/specs/prompts/expand.js b/test/specs/prompts/expand.js
new file mode 100644
index 0000000..5c0f9f4
--- /dev/null
+++ b/test/specs/prompts/expand.js
@@ -0,0 +1,146 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Expand = require('../../../lib/prompts/expand');
+
+describe('`expand` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.expand);
+    this.rl = new ReadlineStub();
+    this.expand = new Expand(this.fixture, this.rl);
+  });
+
+  it('should throw if `key` is missing', function () {
+    var mkPrompt = function () {
+      this.fixture.choices = ['a', 'a'];
+      return new Expand(this.fixture, this.rl);
+    }.bind(this);
+
+    expect(mkPrompt).to.throw(/Format error/);
+  });
+
+  it('should throw if `key` is duplicate', function () {
+    var mkPrompt = function () {
+      this.fixture.choices = [
+        {key: 'a', name: 'foo'},
+        {key: 'a', name: 'foo'}
+      ];
+      return new Expand(this.fixture, this.rl);
+    }.bind(this);
+
+    expect(mkPrompt).to.throw(/Duplicate key error/);
+  });
+
+  it('should throw if `key` is `h`', function () {
+    var mkPrompt = function () {
+      this.fixture.choices = [
+        {key: 'h', name: 'foo'}
+      ];
+      return new Expand(this.fixture, this.rl);
+    }.bind(this);
+
+    expect(mkPrompt).to.throw(/Reserved key error/);
+  });
+
+  it('should allow false as a value', function () {
+    var promise = this.expand.run();
+
+    this.rl.emit('line', 'd');
+    return promise.then(function (answer) {
+      expect(answer).to.equal(false);
+    });
+  });
+
+  it('pass the value as answer, and display short on the prompt', function () {
+    this.fixture.choices = [
+      {key: 'a', name: 'A Name', value: 'a value', short: 'ShortA'},
+      {key: 'b', name: 'B Name', value: 'b value', short: 'ShortB'}
+    ];
+    var prompt = new Expand(this.fixture, this.rl);
+    var promise = prompt.run();
+    this.rl.emit('line', 'b');
+
+    return promise.then(function (answer) {
+      expect(answer).to.equal('b value');
+      expect(this.rl.output.__raw__).to.match(/ShortB/);
+    }.bind(this));
+  });
+
+  it('should use the `default` argument value', function (done) {
+    this.fixture.default = 1;
+    this.expand = new Expand(this.fixture, this.rl);
+
+    this.expand.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+    this.rl.emit('line');
+  });
+
+  it('should return the user input', function (done) {
+    this.expand.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+    this.rl.emit('line', 'b');
+  });
+
+  it('should strip the user input', function (done) {
+    this.expand.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+    this.rl.emit('line', ' b ');
+  });
+
+  it('should have help option', function (done) {
+    this.expand.run().then(function (answer) {
+      expect(this.rl.output.__raw__).to.match(/a\) acab/);
+      expect(this.rl.output.__raw__).to.match(/b\) bar/);
+      expect(answer).to.equal('chile');
+      done();
+    }.bind(this));
+    this.rl.emit('line', 'h');
+    this.rl.emit('line', 'c');
+  });
+
+  it('should not allow invalid command', function () {
+    var self = this;
+    var promise = this.expand.run();
+
+    this.rl.emit('line', 'blah');
+    setTimeout(function () {
+      self.rl.emit('line', 'a');
+    }, 10);
+    return promise;
+  });
+
+  it('should display and capitalize the default choice `key`', function () {
+    this.fixture.default = 1;
+    this.expand = new Expand(this.fixture, this.rl);
+
+    this.expand.run();
+    expect(this.rl.output.__raw__).to.contain('(aBcdh)');
+  });
+
+  it('should display and capitalize the default choice H (Help) `key` if none provided', function () {
+    delete this.fixture.default;
+    this.expand = new Expand(this.fixture, this.rl);
+    this.expand.run();
+
+    expect(this.rl.output.__raw__).to.contain('(abcdH)');
+  });
+
+  it('should \'autocomplete\' the user input', function (done) {
+    this.expand = new Expand(this.fixture, this.rl);
+    this.expand.run();
+    this.rl.line = 'a';
+    this.rl.emit('keypress');
+    setTimeout(function () {
+      expect(this.rl.output.__raw__).to.contain('acab');
+      done();
+    }.bind(this), 10);
+  });
+});
diff --git a/test/specs/prompts/input.js b/test/specs/prompts/input.js
new file mode 100644
index 0000000..12ab505
--- /dev/null
+++ b/test/specs/prompts/input.js
@@ -0,0 +1,38 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Input = require('../../../lib/prompts/input');
+
+describe('`input` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.input);
+    this.rl = new ReadlineStub();
+  });
+
+  it('should use raw value from the user', function (done) {
+    var input = new Input(this.fixture, this.rl);
+
+    input.run().then(function (answer) {
+      expect(answer).to.equal('Inquirer');
+      done();
+    });
+
+    this.rl.emit('line', 'Inquirer');
+  });
+
+  it('should output filtered value', function () {
+    this.fixture.filter = function () {
+      return 'pass';
+    };
+
+    var prompt = new Input(this.fixture, this.rl);
+    var promise = prompt.run();
+    this.rl.emit('line', '');
+
+    return promise.then(function () {
+      expect(this.rl.output.__raw__).to.contain('pass');
+    }.bind(this));
+  });
+});
diff --git a/test/specs/prompts/list.js b/test/specs/prompts/list.js
new file mode 100644
index 0000000..72b402b
--- /dev/null
+++ b/test/specs/prompts/list.js
@@ -0,0 +1,172 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var List = require('../../../lib/prompts/list');
+
+describe('`list` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.list);
+    this.rl = new ReadlineStub();
+    this.list = new List(this.fixture, this.rl);
+  });
+
+  it('should default to first choice', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('foo');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('should move selected cursor on keypress', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '', {name: 'down'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for arrow navigation', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '', {name: 'down'});
+    this.rl.input.emit('keypress', '', {name: 'down'});
+    this.rl.input.emit('keypress', '', {name: 'up'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for vi-style navigation', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.input.emit('keypress', 'j', {name: 'j'});
+    this.rl.input.emit('keypress', 'j', {name: 'j'});
+    this.rl.input.emit('keypress', 'k', {name: 'k'});
+    this.rl.emit('line');
+  });
+
+  it('should allow for emacs-style navigation', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.input.emit('keypress', 'n', {name: 'n', ctrl: true});
+    this.rl.input.emit('keypress', 'n', {name: 'n', ctrl: true});
+    this.rl.input.emit('keypress', 'p', {name: 'p', ctrl: true});
+    this.rl.emit('line');
+  });
+
+  it('should loop the choices when going out of boundaries', function () {
+    var promise1 = this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+    });
+
+    this.rl.input.emit('keypress', '', {name: 'up'});
+    this.rl.input.emit('keypress', '', {name: 'up'});
+    this.rl.emit('line');
+
+    return promise1
+      .then(function () {
+        this.list.selected = 0; // reset
+        var promise2 = this.list.run().then(function (answer) {
+          expect(answer).to.equal('foo');
+        });
+
+        this.rl.input.emit('keypress', '', {name: 'down'});
+        this.rl.input.emit('keypress', '', {name: 'down'});
+        this.rl.input.emit('keypress', '', {name: 'down'});
+        this.rl.emit('line');
+        return promise2;
+      }.bind(this));
+  });
+
+  it('should require a choices array', function () {
+    var mkPrompt = function () {
+      return new List({name: 'foo', message: 'bar'});
+    };
+    expect(mkPrompt).to.throw(/choices/);
+  });
+
+  it('should allow a numeric default', function (done) {
+    this.fixture.default = 1;
+    var list = new List(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('should work from a numeric default being the index', function (done) {
+    this.fixture.default = 1;
+    var list = new List(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('bum');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '', {name: 'down'});
+    this.rl.emit('line');
+  });
+
+  it('should allow a string default being the value', function (done) {
+    this.fixture.default = 'bar';
+    var list = new List(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('should work from a string default', function (done) {
+    this.fixture.default = 'bar';
+    var list = new List(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('bum');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '', {name: 'down'});
+    this.rl.emit('line');
+  });
+
+  it('shouldn\'t allow an invalid index as default', function (done) {
+    this.fixture.default = 4;
+    var list = new List(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('foo');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('should allow 1-9 shortcut key', function (done) {
+    this.list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.input.emit('keypress', '2');
+    this.rl.emit('line');
+  });
+});
diff --git a/test/specs/prompts/password.js b/test/specs/prompts/password.js
new file mode 100644
index 0000000..7714074
--- /dev/null
+++ b/test/specs/prompts/password.js
@@ -0,0 +1,24 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Password = require('../../../lib/prompts/password');
+
+describe('`password` prompt', function () {
+  beforeEach(function () {
+    this.fixture = _.clone(fixtures.password);
+    this.rl = new ReadlineStub();
+  });
+
+  it('should use raw value from the user', function (done) {
+    var password = new Password(this.fixture, this.rl);
+
+    password.run().then(function (answer) {
+      expect(answer).to.equal('Inquirer');
+      done();
+    });
+
+    this.rl.emit('line', 'Inquirer');
+  });
+});
diff --git a/test/specs/prompts/rawlist.js b/test/specs/prompts/rawlist.js
new file mode 100644
index 0000000..5ea3525
--- /dev/null
+++ b/test/specs/prompts/rawlist.js
@@ -0,0 +1,75 @@
+var expect = require('chai').expect;
+var _ = require('lodash');
+var ReadlineStub = require('../../helpers/readline');
+var fixtures = require('../../helpers/fixtures');
+
+var Rawlist = require('../../../lib/prompts/rawlist');
+
+describe('`rawlist` prompt', function () {
+  beforeEach(function () {
+    this.rl = new ReadlineStub();
+    this.fixture = _.clone(fixtures.rawlist);
+    this.rawlist = new Rawlist(this.fixture, this.rl);
+  });
+
+  it('should default to first choice', function (done) {
+    this.rawlist.run().then(function (answer) {
+      expect(answer).to.equal('foo');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('should select given index', function (done) {
+    this.rawlist.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.emit('line', '2');
+  });
+
+  it('should not allow invalid index', function () {
+    var self = this;
+    var promise = this.rawlist.run();
+
+    this.rl.emit('line', 'blah');
+    setTimeout(function () {
+      self.rl.emit('line', '1');
+    }, 10);
+
+    return promise;
+  });
+
+  it('should require a choices array', function () {
+    var mkPrompt = function () {
+      return new Rawlist({name: 'foo', message: 'bar'});
+    };
+    expect(mkPrompt).to.throw(/choices/);
+  });
+
+  it('should allow a default index', function (done) {
+    this.fixture.default = 1;
+    var list = new Rawlist(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('bar');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+
+  it('shouldn\'t allow an invalid index as default', function (done) {
+    this.fixture.default = 4;
+    var list = new Rawlist(this.fixture, this.rl);
+
+    list.run().then(function (answer) {
+      expect(answer).to.equal('foo');
+      done();
+    });
+
+    this.rl.emit('line');
+  });
+});

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-inquirer.git



More information about the Pkg-javascript-commits mailing list