Build beautiful CLIs with TypeScript
In 2026, Command Line Interfaces (CLIs) don't have to be boring black-and-white text streams. Developers expect polished, interactive, and informative tools that rival the UX of web applications.
This guide covers the complete EasyCLI API with examples.
defineCLI(config)The main entry point for creating a CLI application.
import { defineCLI } from "easycli-core";
const cli = defineCLI({
name: "my-app",
version: "1.0.0",
description: "My awesome CLI",
commands: {
deploy: {
description: "Deploy the application",
args: {
env: ["prod", "staging", "dev"] // Typed enum argument
},
flags: {
force: { type: "boolean", alias: "f", description: "Force deploy" },
region: { type: "string", default: "us-east-1" },
replicas: { type: "number", default: 3 }
},
run({ env, force, region, replicas }) {
// All args and flags are fully typed!
console.log(`Deploying to ${env} in ${region} with ${replicas} replicas`);
}
}
}
});
cli.run();
| Type | Example | Result |
|---|---|---|
"string" |
args: { name: "string" } |
name: string |
"number" |
args: { count: "number" } |
count: number |
string[] |
args: { env: ["prod", "dev"] } |
env: "prod" | "dev" |
ArgDef |
args: { name: { type: "string", optional: true } } |
name: string | undefined |
flags: {
verbose: "boolean", // Shorthand
port: { type: "number", default: 3000 }, // With default
env: { type: "string", alias: "e" }, // With alias
debug: { type: "boolean", required: true } // Required
}
const cli = defineCLI({
name: "docker",
commands: {
container: {
description: "Manage containers",
commands: {
ls: {
description: "List containers",
run() { /* ... */ }
},
rm: {
description: "Remove container",
args: { id: "string" },
run({ id }) { /* ... */ }
}
}
}
}
});
Zero-dependency ANSI colors with automatic TTY detection.
import { colors } from "easycli-ui";
| Method | Description |
|---|---|
colors.bold(text) |
Bold text |
colors.dim(text) |
Dimmed/faded text |
colors.italic(text) |
Italic text |
colors.underline(text) |
Underlined text |
colors.inverse(text) |
Inverted colors |
colors.strikethrough(text) |
Strikethrough text |
| Method | Method | Method |
|---|---|---|
colors.black() |
colors.red() |
colors.green() |
colors.yellow() |
colors.blue() |
colors.magenta() |
colors.cyan() |
colors.white() |
colors.gray() |
| Method | Method | Method |
|---|---|---|
colors.redBright() |
colors.greenBright() |
colors.yellowBright() |
colors.blueBright() |
colors.magentaBright() |
colors.cyanBright() |
| Method | Method | Method |
|---|---|---|
colors.bgRed() |
colors.bgGreen() |
colors.bgYellow() |
colors.bgBlue() |
colors.bgMagenta() |
colors.bgCyan() |
colors.bgBlack() |
colors.bgWhite() |
// Combine styles and colors
console.log(colors.bold(colors.red("Error!")));
console.log(colors.bgYellow(colors.black(" WARNING ")));
console.log(colors.dim(colors.italic("Hint: use --help")));
// Semantic color usage
const success = (msg: string) => colors.green(`✔ ${msg}`);
const error = (msg: string) => colors.red(`✖ ${msg}`);
const info = (msg: string) => colors.cyan(`ℹ ${msg}`);
const warn = (msg: string) => colors.yellow(`⚠ ${msg}`);
Animated spinners for async operations.
import { spinner } from "easycli-ui";
| Method | Description |
|---|---|
spinner(message) |
Create a spinner |
.start() |
Start the animation |
.success(msg?) |
Stop with ✔ green checkmark |
.fail(msg?) |
Stop with ✖ red X |
.stop() |
Stop without message |
const s = spinner("Deploying to production...");
s.start();
try {
await deployApp();
s.success("Deployed successfully!");
} catch (err) {
s.fail("Deployment failed");
throw err;
}
Output:
⠋ Deploying to production...
✔ Deployed successfully!
Progress bars for deterministic tasks.
import { progress } from "easycli-ui";
| Method | Description |
|---|---|
progress(total, width?) |
Create a progress bar (default width: 30) |
.update(value) |
Update current progress |
.complete() |
Mark as 100% complete |
const bar = progress(100);
for (let i = 0; i <= 100; i += 10) {
await downloadChunk();
bar.update(i);
}
bar.complete();
Output:
████████████████████░░░░░░░░░░ 70%
████████████████████████████████ 100%
Beautiful tables with automatic column sizing.
import { table } from "easycli-ui";
table([
{ ID: "srv-123", Name: "api-server", Status: "Running" },
{ ID: "srv-124", Name: "worker-01", Status: "Stopped" },
{ ID: "srv-125", Name: "db-primary", Status: "Maintenance" }
]);
Output:
ID NAME STATUS
srv-123 api-server Running (green)
srv-124 worker-01 Stopped (red)
srv-125 db-primary Maintenance (yellow)
The table automatically colors status values:
Bordered boxes for announcements and notices.
import { box } from "easycli-ui";
| Option | Type | Default | Description |
|---|---|---|---|
borderStyle |
"single" | "double" | "rounded" | "bold" |
"rounded" |
Border characters |
borderColor |
ColorName |
- | Border color |
padding |
number |
1 |
Inner padding |
margin |
number |
0 |
Outer margin |
title |
string |
- | Title in top border |
titlePosition |
"left" | "center" | "right" |
"left" |
Title position |
textAlign |
"left" | "center" | "right" |
"left" |
Content alignment |
dimBorder |
boolean |
false |
Dim the border |
// Simple announcement
console.log(box("Update available: 1.0.0 → 2.0.0", {
borderStyle: "rounded",
borderColor: "yellow",
padding: 1
}));
Output:
╭─────────────────────────────────╮
│ │
│ Update available: 1.0.0 → 2.0.0 │
│ │
╰─────────────────────────────────╯
// Multi-line with title
console.log(box([
"New features:",
"• Box component",
"• Better prompts",
"• Scaffolder"
], {
title: " What's New ",
borderStyle: "double",
borderColor: "cyan"
}));
Output:
╔═ What's New ═════╗
║ New features: ║
║ • Box component ║
║ • Better prompts ║
║ • Scaffolder ║
╚══════════════════╝
single: ┌───┐ double: ╔═══╗
│ │ ║ ║
└───┘ ╚═══╝
rounded: ╭───╮ bold: ┏━━━┓
│ │ ┃ ┃
╰───╯ ┗━━━┛
Interactive prompts with CLI flag/config fallback.
// Available via ctx.ask in command handlers
run(args, ctx) {
const name = await ctx.ask.text("Project name");
}
| Method | Description |
|---|---|
ask.text(message, default?) |
Text input |
ask.password(message) |
Hidden input |
ask.confirm(message) |
Yes/No confirmation |
ask.select(message, options) |
Single selection |
ask.multiselect(message, options) |
Multiple selection |
const name = await ctx.ask.text("Project name", "my-app");
// ? Project name (my-app): _
const token = await ctx.ask.password("API Token");
// ? API Token: ****
const proceed = await ctx.ask.confirm("Deploy to production?");
// ? Deploy to production? (y/n): _
const env = await ctx.ask.select("Environment", ["dev", "staging", "prod"]);
// ? Environment:
// 1. dev
// 2. staging
// 3. prod
// Select: _
const features = await ctx.ask.multiselect("Features", [
"auth", "database", "analytics", "realtime"
]);
// ? Features
// > [x] auth
// [ ] database
// [x] analytics
// [ ] realtime
// (Use ↑↓, space to select, enter to continue)
Chain prompts together for setup wizards:
const [name, region, ci] = await ctx.flow([
() => ctx.ask.text("Project name"),
() => ctx.ask.select("Region", ["us-east", "eu-west", "ap-south"]),
() => ctx.ask.confirm("Enable CI?")
]);
const cli = defineCLI({
name: "my-cli",
commands: { /* ... */ },
hooks: {
onInit() {
console.log("CLI initialized");
},
onBeforeCommand({ command, args, flags }) {
console.log(`Running: ${command}`);
},
onAfterCommand({ command }) {
console.log(`Completed: ${command}`);
},
onError(error, ctx) {
console.error(`Error in ${ctx.command}:`, error.message);
},
onExit(code) {
console.log(`Exiting with code ${code}`);
}
}
});
Create reusable hook bundles:
import { definePlugin } from "easycli-plugins";
const analyticsPlugin = definePlugin({
name: "analytics",
hooks: {
onBeforeCommand({ command }) {
trackEvent("command_start", { command });
},
onAfterCommand({ command }) {
trackEvent("command_complete", { command });
}
}
});
const cli = defineCLI({
plugins: [analyticsPlugin],
// ...
});
EasyCLI automatically handles SIGINT (Ctrl+C) and SIGTERM for graceful shutdown.
Signal handlers are installed automatically when you call cli.run(). The CLI will:
Register cleanup functions to run before exit:
import { onCleanup } from "easycli-core";
onCleanup(async () => {
await database.disconnect();
await cache.flush();
console.log("Cleanup complete");
});
For advanced use cases:
import { installSignalHandlers, resetTerminal } from "easycli-core";
installSignalHandlers();
onCleanup(resetTerminal);
Protect your CLI from injection attacks with built-in sanitization.
import { sanitize, sanitizeWithLimit, isValidPath } from "easycli-prompts";
| Method | Description |
|---|---|
sanitize(input) |
Remove control chars and ANSI escape codes |
sanitizeWithLimit(input, max) |
Sanitize and enforce max length |
validatePattern(input, regex) |
Validate against a pattern |
isValidPath(input) |
Check for path traversal attacks |
const userInput = await ctx.ask.text("Enter filename");
if (!isValidPath(userInput)) {
throw ctx.error("Invalid path", { hint: "Paths cannot contain .." });
}
const clean = sanitize(userInput);
All prompt inputs are automatically sanitized:
const name = await ctx.ask.text("Name");
Arguments can be marked as optional:
commands: {
greet: {
args: {
name: { type: "string", optional: true }
},
run({ name }) {
if (name) {
console.log(`Hello, ${name}!`);
} else {
console.log("Hello, stranger!");
}
}
}
}
Collect multiple flag values into an array:
commands: {
build: {
flags: {
file: { type: "string", array: true, alias: "f" },
exclude: { type: "string", array: true }
},
run({ file, exclude }) {
console.log("Files:", file);
console.log("Excluded:", exclude);
}
}
}
Usage:
my-cli build --file a.ts --file b.ts --file c.ts
my-cli build -f one.ts -f two.ts
| Property | Type | Description |
|---|---|---|
type |
"string" | "boolean" | "number" |
Value type |
alias |
string |
Short flag (e.g., -f) |
default |
any |
Default value |
required |
boolean |
Fail if not provided |
array |
boolean |
Collect multiple values |
description |
string |
Help text |
import { defineCLI } from "easycli-core";
import { colors, spinner, box, table, progress } from "easycli-ui";
const cli = defineCLI({
name: "deploy-cli",
version: "1.0.0",
commands: {
deploy: {
description: "Deploy application",
args: { env: ["prod", "staging", "dev"] },
async run({ env }, ctx) {
console.log(box(`Deploying to ${colors.bold(env)}`, {
borderStyle: "rounded",
borderColor: "cyan"
}));
const proceed = await ctx.ask.confirm("Proceed with deployment?");
if (!proceed) return;
const s = spinner("Building...");
s.start();
await build();
s.success("Build complete");
const bar = progress(100);
for (let i = 0; i <= 100; i += 20) {
await uploadChunk();
bar.update(i);
}
bar.complete();
table([
{ Service: "api", Status: "Running", Replicas: 3 },
{ Service: "worker", Status: "Running", Replicas: 2 }
]);
console.log(colors.green("\n✔ Deployment complete!"));
}
}
}
});
cli.run();
Check out the packages/ui source code to see how these components are built!