nc.cli
What you need for command-line tools: typed arguments with a generated --help, prompts, arrow-key menus, spinners, progress bars, tables and boxes. When nobody's at the keyboard (CI, pipes), prompts read plain lines and animations print only their final state.
const { cli } = require("@ix-xs/node-comfort");
import { table } from "@ix-xs/node-comfort/cli";const { flags } = cli.args({
port: { type: "number", short: "p", default: 3000, description: "Port to listen on" },
});
const name = await cli.prompt("Project name?", { default: "my-app" });
const spin = cli.spinner("Installing").start();
spin.succeed("Installed");nc.cli.Functions
nc.cli.table()
table(rows: Record<string, unknown>[] | unknown[][], options?: TableOptions): stringDraws a table from an array of objects (keys become columns) or an array of arrays. Colors, emoji and CJK characters line up correctly.
Parameters
| Name | Type |
|---|---|
rows | Array<Record<string, unknown>> | unknown[][] |
optionsoptional | TableOptions |
Returns string
Example
console.log(cli.table([
{ name: "Ada", role: "admin", logins: 42 },
{ name: "Bob", role: "user", logins: 7 },
]));
// ╭──────┬───────┬────────╮
// │ name │ role │ logins │
// ├──────┼───────┼────────┤
// │ Ada │ admin │ 42 │
// │ Bob │ user │ 7 │
// ╰──────┴───────┴────────╯nc.cli.box()
box(text: string, options?: BoxOptions): stringDraws a box around text.
Parameters
| Name | Type |
|---|---|
text | string |
optionsoptional | BoxOptions |
Returns string
Example
console.log(cli.box("Server ready\nhttp://localhost:3000", { title: "my-app", borderColor: "green" }));
// ╭─ my-app ──────────────────╮
// │ Server ready │
// │ http://localhost:3000 │
// ╰───────────────────────────╯nc.cli.spinner()
spinner(text?: string, options?: SpinnerOptions): SpinnerA spinner for work in progress. Call .start(), then finish with .succeed(), .fail(), .warn(), .info() or .stop().
Parameters
| Name | Type | Description |
|---|---|---|
textoptional | string | Default: "" |
optionsoptional | SpinnerOptions |
Returns Spinner: Not started yet.
Example
const spin = cli.spinner("Downloading").start();
try {
await download();
spin.succeed("Downloaded 12 files");
} catch (error) {
spin.fail(`Download failed: ${error.message}`);
}nc.cli.progress()
progress(options: ProgressOptions): ProgressBarA progress bar with percentage, ETA and speed. It redraws at most every 50 ms, so calling tick() in a tight loop is fine.
Parameters
| Name | Type |
|---|---|
options | ProgressOptions |
Returns ProgressBar
Example
const bar = cli.progress({ total: files.length, format: "{bar} {percent}% {label}" });
for (const file of files) {
await upload(file);
bar.tick(1, file.name);
}
bar.stop();nc.cli.prompt()
prompt(question: string, options?: PromptOptions): Promise<string>Asks a question and returns the answer. With validate, it keeps asking until the answer is valid.
Parameters
| Name | Type |
|---|---|
question | string |
optionsoptional | PromptOptions |
Returns Promise<string>: Trimmed.
Example
const name = await cli.prompt("Project name?", { default: "my-app" });
const age = await cli.prompt("Age?", {
validate: (answer) => /^\d+$/.test(answer) || "Please enter a number",
});nc.cli.password()
password(question: string, options?: { mask?: string; input?: ReadStream; output?: WriteStream; } | undefined): Promise<string>Asks for a secret without showing it; each character appears as *.
Parameters
| Name | Type | Description |
|---|---|---|
question | string | |
optionsoptional | { mask?: string, input?: NodeJS.ReadStream, output?: NodeJS.WriteStream } | mask: "" shows nothing at all. |
Returns Promise<string>
Example
const token = await cli.password("API token?");nc.cli.confirm()
confirm(question: string, options?: ConfirmOptions): Promise<boolean>Asks a yes/no question. Understands y, yes, o, oui, true, 1 and their opposites; an empty answer gives the default.
Parameters
| Name | Type |
|---|---|
question | string |
optionsoptional | ConfirmOptions |
Returns Promise<boolean>
Example
if (await cli.confirm("Delete 42 files?")) await cleanup();nc.cli.select()
select<V = string>(question: string, choices: Choice<V>[], options?: SelectOptions): Promise<V>Lets the user pick one choice with the arrow keys and Enter. Without an interactive terminal, it prints a numbered list and reads a number.
Parameters
| Name | Type |
|---|---|
question | string |
choices | Array<Choice<V>> |
optionsoptional | SelectOptions |
Returns Promise<V>
Example
const framework = await cli.select("Framework?", ["Express", "Fastify", "Koa"]);
const plan = await cli.select("Plan?", [
{ label: "Free", value: "free", hint: "0 €" },
{ label: "Pro", value: "pro", hint: "9 €/month" },
]);nc.cli.multiselect()
multiselect<V = string>(question: string, choices: Choice<V>[], options?: SelectOptions): Promise<V[]>Lets the user pick several choices: arrows to move, Space to toggle, a for all, Enter to confirm.
Parameters
| Name | Type |
|---|---|
question | string |
choices | Array<Choice<V>> |
optionsoptional | SelectOptions |
Returns Promise<V[]>: In list order.
Example
const features = await cli.multiselect("Features?", ["TypeScript", "ESLint", "Tests", "Docker"], { min: 1 });nc.cli.args()
args<S extends Record<string, FlagSpec>>(schema: S, options?: ArgsOptions): ParsedArgs<S>Parses command-line arguments from a schema, and types the result: flags.port is a number. Handles short aliases, defaults, required flags, allowed values, repeated flags and --no-flag, and generates --help. A typo gets a "Did you mean...?" suggestion.
Parameters
| Name | Type |
|---|---|
schema | S |
optionsoptional | ArgsOptions |
Returns ParsedArgs<S>
Throws TypeError For unknown flags, bad numbers, missing required flags or values not in choices. The message is ready to show.
Example
// node server.js --port 8080 -v --tag api --tag web public/
const { flags, positionals } = cli.args({
port: { type: "number", short: "p", default: 3000, description: "Port to listen on" },
verbose: { type: "boolean", short: "v" },
tag: { type: "string", multiple: true },
env: { type: "string", choices: ["dev", "prod"], default: "dev" },
}, { description: "Start the web server", version: "1.0.0" });
// flags: { port: 8080, verbose: true, tag: ["api", "web"], env: "dev" }nc.cli.isInteractive()
isInteractive(): booleanIs someone at the keyboard? True when stdin and stdout are terminals and we're not in CI.
Returns boolean
Example
const name = cli.isInteractive() ? await cli.prompt("Name?") : "default";nc.cli.size()
size(): { columns: number; rows: number; }The terminal size, or 80x24 when it's unknown.
Returns { columns: number, rows: number }
nc.cli.clear()
clear(): voidClears the screen. Does nothing when stdout isn't a terminal.
Types
Import any of them in TypeScript with import type { ArgsOptions } from "@ix-xs/node-comfort", or in JavaScript with import("@ix-xs/node-comfort").ArgsOptions.
ArgsOptions
Options for args().
| Property | Type | Description |
|---|---|---|
argvoptional | string[] | What to parse. Defaults to process.argv.slice(2). |
nameoptional | string | Program name in the help. Defaults to the script name. |
descriptionoptional | string | Shown at the top of the help. |
usageoptional | string | A usage line, like "<file> [options]". |
helpoptional | boolean | Answer --help and -h by printing the help and exiting. Defaults to true. |
versionoptional | string | Answer --version with this version. |
strictoptional | boolean | Reject unknown flags, with a suggestion. Defaults to true. |
BorderStyle
Border styles for table() and box().
type BorderStyle = "rounded" | "single" | "double" | "heavy" | "ascii" | "none"BoxOptions
Options for box().
| Property | Type | Description |
|---|---|---|
titleoptional | string | Shown in the top border. |
borderoptional | BorderStyle | Defaults to "rounded". |
paddingoptional | number | { x?: number; y?: number; } | Space around the text. Defaults to { x: 2, y: 0 }. |
alignoptional | "left" | "center" | "right" | Defaults to "left". |
borderColoroptional | string | A style name like "green", or a hex code. |
widthoptional | number | Fixed inner width. Defaults to the longest line. |
colorsoptional | boolean | Automatic by default. |
Choice
A choice for select() and multiselect(): a string, or a label with a value of any type.
type Choice = string | { label: string; value: V; hint?: string; disabled?: boolean; }ConfirmOptions
Options for confirm().
| Property | Type | Description |
|---|---|---|
defaultoptional | boolean | The answer when the user just presses Enter. Defaults to false. |
inputoptional | ReadableStream | Defaults to stdin. |
outputoptional | WritableStream | Defaults to stdout. |
FlagSpec
A flag for args().
| Property | Type | Description |
|---|---|---|
type | "string" | "number" | "boolean" | Numbers are checked. Booleans also accept --no-name. |
shortoptional | string | One-letter alias, like "p" for -p. |
defaultoptional | unknown | Used when the flag is missing. |
multipleoptional | boolean | Can be repeated (--tag a --tag b); you get an array. |
requiredoptional | boolean | Fail if it's missing. |
choicesoptional | readonly string[] | Allowed values, for string flags. |
descriptionoptional | string | Shown in --help. |
FlagValue
The type of a flag's value.
type FlagValue = F["type"] extends "number" ? number : F["type"] extends "boolean" ? boolean : F extends { choices: readonly (infer C)[]; } ? C : stringLineReader
One line reader per input stream, shared by every prompt, so piped lines that arrive early are queued instead of lost.
| Property | Type | Description |
|---|---|---|
rl | Interface | |
lines | string[] | |
waiting | { resolve: (line: string) => void; reject: (error: Error) => void; }[] | |
closed | boolean |
ParsedArgs
What args() returns.
| Property | Type | Description |
|---|---|---|
flags | ParsedFlags<S> | |
positionals | string[] | Arguments that aren't flags. |
help | () => string | The generated help text. |
ParsedFlags
The parsed flags, typed from the schema.
type ParsedFlags = { [K in keyof S]: S[K] extends { multiple: true; } ? Array<FlagValue<S[K]>> : S[K] extends { default: any; } | { required: true; } | { type: "boolean"; } ? FlagValue<S[K]> : FlagValue<S[K]> | undefined; }ProgressBar
A progress bar.
| Property | Type | Description |
|---|---|---|
tick | (amount?: number, label?: string) => ProgressBar | Adds to the value, 1 by default. |
update | (value: number, label?: string) => ProgressBar | Sets the value. |
stop | () => ProgressBar | Draws the final state and frees the line. |
value | number | |
total | number | You can change it along the way. |
percent | number | From 0 to 100. |
ProgressOptions
Options for progress().
| Property | Type | Description |
|---|---|---|
total | number | The value that means 100%. |
widthoptional | number | Bar width in characters. Defaults to 30. |
formatoptional | string | Can use {bar}, {percent}, {value}, {total}, {eta}, {elapsed}, {rate} and {label}. |
completeoptional | string | Filled character. Defaults to "█". |
incompleteoptional | string | Empty character. Defaults to "░". |
labeloptional | string | Text for {label}. |
streamoptional | WriteStream | Defaults to stderr. |
PromptOptions
Options for prompt().
| Property | Type | Description |
|---|---|---|
defaultoptional | string | Used when the answer is empty. |
validateoptional | ((answer: string) => string | true | Promise<string | true>) | Return true to accept, or a message to ask again. |
inputoptional | ReadableStream | Defaults to stdin. |
outputoptional | WritableStream | Defaults to stdout. |
SelectOptions
Options for select() and multiselect().
| Property | Type | Description |
|---|---|---|
defaultoptional | number | Index of the choice highlighted at first. Defaults to 0. |
pageSizeoptional | number | How many choices are visible at once. Defaults to 10. |
minoptional | number | multiselect() only: minimum number of choices. |
inputoptional | ReadStream | Defaults to stdin. |
outputoptional | WriteStream | Defaults to stdout. |
Spinner
A spinner. Every method returns it.
| Property | Type | Description |
|---|---|---|
start | (text?: string) => Spinner | Starts spinning, optionally with new text. |
stop | () => Spinner | Stops and clears the line. |
succeed | (text?: string) => Spinner | Stops with a green ✔. |
fail | (text?: string) => Spinner | Stops with a red ✖. |
warn | (text?: string) => Spinner | Stops with a yellow ⚠. |
info | (text?: string) => Spinner | Stops with a blue ℹ. |
text | string | The text next to the spinner. Change it any time. |
isSpinning | boolean |
SpinnerOptions
Options for spinner().
| Property | Type | Description |
|---|---|---|
framesoptional | string[] | Animation frames. Defaults to braille dots. |
intervaloptional | number | Milliseconds per frame. Defaults to 80. |
coloroptional | string | A style name or hex code. Defaults to "cyan". |
streamoptional | WriteStream | Defaults to stderr, which keeps stdout clean for data. |
TableColumn
A column of table().
| Property | Type | Description |
|---|---|---|
key | string | number | Property name for object rows, index for array rows. |
headeroptional | string | Defaults to the key. |
alignoptional | "left" | "center" | "right" | Defaults to right for numbers, left otherwise. |
maxWidthoptional | number | Longer values are cut with …. |
formatoptional | ((value: unknown, row: any) => unknown) | Changes the value before it's shown. |
TableOptions
Options for table().
| Property | Type | Description |
|---|---|---|
columnsoptional | (string | TableColumn)[] | Which columns, in which order. Defaults to every key found. |
borderoptional | BorderStyle | "markdown" | "markdown" gives a Markdown table. Defaults to "rounded". |
headeroptional | boolean | Show the header row. Defaults to true. |
colorsoptional | boolean | Bold header and faint borders. Automatic by default. |
paddingoptional | number | Spaces around cell content. Defaults to 1. |