Skip to content

01 — Work

Selected
work

By day I lead enterprise Shopify builds at fusefabric — solution design, architecture and delivery for fashion and lifestyle brands. Before that I led front-end engineering at MISSOMA and Dr. Martens, and agency teams in London.

Outside client work I build developer tools: Shopify theme asset compressors, Node CLI and config libraries, AI agent skills, and the occasional game.

Brands I've built for

  • MISSOMA
  • Topshop
  • Dr. Martens
  • Samsung
  • Spotify
  • Tesco
  • Cafédirect
  • ExxonMobil
  • Hitachi
  • London Real

Node.js

CLI Geniev2.0.0MIT

Easily create interactive command-line interfaces (CLIs) with a genie-style interface

$ npm i cli-genie
Last 30 days
174 downloads
Published
3 Nov 2026
Unpacked
175.2 KB
Commands
cli-genie
Dependencies
1
  1. ![CLI Genie logo](https://i.ibb.co/pbdjT80/cli-genie.png)
  2. # CLI Genie
  3. [![npm](https://img.shields.io/npm/v/cli-genie)](https://www.npmjs.com/package/cli-genie)
  4. [![CI](https://github.com/bentoms-dev/cli-genie/actions/workflows/ci.yml/badge.svg)](https://github.com/bentoms-dev/cli-genie/actions/workflows/ci.yml)
  5. [![license](https://img.shields.io/npm/l/cli-genie)](./LICENSE)
  6. Build guided, genie-style command-line wizards in a few lines. CLI Genie wraps
  7. [Inquirer](https://github.com/SBoudrias/Inquirer.js) with what most wizards need:
  8. - 🧞 10 prompt types: text, password, number, confirm, select, rawlist, checkbox, expand, search, editor
  9. - 🔀 Conditional and dynamic steps: when, message, default and choices can depend on earlier answers
  10. - 📋 Review screen where users can check and change any answer before finishing
  11. - 🤖 Non-interactive mode for CI and scripts: answers from flags, env vars or code, with type coercion and validation
  12. - 🪝 Hooks: transform, onAnswer, onComplete, onCancel
  13. - 📄 JSON wizards and a cli-genie command, so no code is needed
  14. - 🎨 Genie theme, progress counter, clean Ctrl+C handling
  15. - 🟦 Written in TypeScript, ships ESM + CommonJS with type definitions
  16. Requires Node.js 22.13+ or 24+.
  17. ## Installation
  18. ```bash
  19. npm install cli-genie
  20. ```
  21. ## Quick start
  22. ```js
  23. import { CliGenie, confirm, select, text } from 'cli-genie';
  24. // or: const { CliGenie, confirm, select, text } = require('cli-genie');
  25. const genie = new CliGenie({
  26. title: 'Welcome to CLI Genie',
  27. summary: true,
  28. steps: [
  29. text({ name: 'name', message: 'What is your name?', required: true }),
  30. confirm({ name: 'chooseColor', message: 'Do you want to choose a colour?' }),
  31. select({
  32. name: 'color',
  33. message: (answers) => `Pick a colour, ${answers.name}:`,
  34. choices: ['Red', 'Green', 'Blue'],
  35. when: (answers) => answers.chooseColor,
  36. }),
  37. ],
  38. });
  39. const answers = await genie.run();
  40. // { name: 'Ben', chooseColor: true, color: 'Green' }
  41. ```
  42. There are more examples in [examples/](./examples).
  43. ## Steps
  44. Each step builder takes the matching [Inquirer prompt options](https://github.com/SBoudrias/Inquirer.js/tree/main/packages/prompts#prompts)
  45. plus these CLI Genie options:
  46. | Option | Type | Description |
  47. | ----------- | ------------------------------------ | ------------------------------------------------------------ |
  48. | `name` | `string` | **Required.** Key the answer is stored under. |
  49. | `message` | `string \| (answers) => string` | **Required.** The question. |
  50. | `default` | `value \| (answers) => value` | Default answer (also used in non-interactive mode). |
  51. | `when` | `(answers) => boolean` | Only ask when this returns `true`. |
  52. | `validate` | `(value, answers) => true \| string` | Return `true` or an error message. Receives earlier answers. |
  53. | `transform` | `(value, answers) => any` | Change the answer before it is stored. |
  54. | `label` | `string` | Label for the review screen (defaults to the message). |
  55. | `theme` | `object` | Per-step Inquirer theme overrides. |
  56. | Builder | Answer type | Notes |
  57. | ------------ | ----------- | ------------------------------------------------------------------------ |
  58. | `text()` | `string` | `required: true` rejects empty input. |
  59. | `password()` | `string` | `mask: true` shows `*` characters. |
  60. | `number()` | `number` | `min`, `max`, `step`. |
  61. | `confirm()` | `boolean` | |
  62. | `select()` | choice | `choices` may be a function of the answers. Supports `Separator`. |
  63. | `rawlist()` | choice | Numbered list. |
  64. | `checkbox()` | `array` | `default` is the list of pre-checked values. `validate` gets the values. |
  65. | `expand()` | choice | Choices have a `key`; `default` is a key. |
  66. | `search()` | choice | `source(term, { signal, answers })` returns choices. |
  67. | `editor()` | `string` | Opens `$EDITOR`. |
  68. ## Options
  69. ```ts
  70. new CliGenie({
  71. steps, // Step[]
  72. title, // banner shown before the first question
  73. progress: true, // show "(2/5)" before each question
  74. summary: false, // true | { title?, allowEdit?, format?(value, step) }
  75. theme: 'genie', // 'genie' | 'plain' | custom Inquirer theme object
  76. initialAnswers: {}, // pre-filled answers; matching steps are skipped
  77. interactive: 'auto', // true | false | 'auto' (prompt only when stdin is a TTY)
  78. hooks: { onAnswer, onComplete, onCancel },
  79. context: { input, output, signal }, // passed to every prompt
  80. });
  81. ```
  82. ### Review screen
  83. With summary: true, the genie lists every answer at the end. The user can confirm,
  84. change any answer or start over. When an answer changes, steps that depend on it
  85. are asked or removed automatically.
  86. ### Non-interactive mode and pre-filled answers
  87. ```js
  88. import { CliGenie, fromArgv, fromEnv } from 'cli-genie';
  89. const genie = new CliGenie({
  90. steps,
  91. // `--first-name=Ben`, `--no-git`, `GENIE_FIRST_NAME=Ben` all match a step named `firstName`
  92. initialAnswers: { ...fromEnv('GENIE_'), ...fromArgv() },
  93. });
  94. ```
  95. Supplied answers are converted to the step's type ("42" → 42, "yes" → true,
  96. "a,b" → ['a', 'b'], and a choice's name or value → its value) and then validated.
  97. When there's no TTY (CI, piped input) or interactive: false is set, any step without
  98. a supplied answer uses its default. If it has no default, a MissingAnswerError is thrown.
  99. ### Cancelling
  100. Pressing Ctrl+C (or aborting context.signal) rejects run() with a GenieCancelledError
  101. that contains the answers collected so far:
  102. ```js
  103. import { isCancelled } from 'cli-genie';
  104. try {
  105. await genie.run();
  106. } catch (error) {
  107. if (isCancelled(error)) process.exit(130);
  108. throw error;
  109. }
  110. ```
  111. Other errors: MissingAnswerError and InvalidAnswerError, both subclasses of GenieError.
  112. ### TypeScript
  113. ```ts
  114. interface Order {
  115. size: 'small' | 'large';
  116. extras: string[];
  117. }
  118. const order = await genie.run<Order>();
  119. ```
  120. ## JSON wizards and the `cli-genie` command
  121. Define a wizard as JSON. Functions are replaced by declarative options:
  122. ```json
  123. {
  124. "title": "Pizza order",
  125. "summary": true,
  126. "steps": [
  127. {
  128. "type": "text",
  129. "name": "email",
  130. "message": "Email",
  131. "pattern": "@",
  132. "patternMessage": "Invalid email"
  133. },
  134. { "type": "select", "name": "size", "message": "Size", "choices": ["small", "large"] },
  135. { "type": "confirm", "name": "delivery", "message": "Delivery?" },
  136. { "type": "text", "name": "address", "message": "Address", "when": { "delivery": true } }
  137. ]
  138. }
  139. ```
  140. when matches when every listed answer is equal; an array value means "any of".
  141. search steps aren't available in JSON because they need a function.
  142. Run it from code with await loadWizard('wizard.json') or defineWizard(object), or from the shell:
  143. ```bash
  144. npx cli-genie wizard.json # prompts on stderr, answers as JSON on stdout
  145. npx cli-genie wizard.json --size=large # pre-fill answers
  146. npx cli-genie wizard.json --non-interactive # never prompt (CI)
  147. ORDER=$(npx cli-genie wizard.json) # capture the answers
  148. ```
  149. ## Migrating from v1
  150. v1 code still works, with deprecation warnings in your editor:
  151. ```js
  152. const { CliGenie, textInputPrompt, listPrompt } = require('cli-genie');
  153. new CliGenie([textInputPrompt('Name?', 'name'), listPrompt('Colour?', 'color', ['Red'])]).run();
  154. ```
  155. Breaking changes in 2.0:
  156. - Node.js 22.13+ is required.
  157. - run() resolves with the answers and no longer prints them.
  158. - Errors are thrown (as GenieError subclasses) instead of being logged with process.exitCode = 1.
  159. - when now receives the earlier answers. In v1 it always received {}, so conditional questions never appeared.
  160. - styling is merged into the Inquirer theme. promptLibrary is ignored.
  161. ## Contributing
  162. See [CONTRIBUTING.md](./CONTRIBUTING.md).
  163. ## License
  164. MIT, see [LICENSE](./LICENSE).
5 itemsgithub.com/bentoms-dev ↗