A comprehensive, modular CLI menu system for Node.js with full TypeScript support. Zero dependencies, pure Node.js implementation with advanced features including i18n, wizards, and command handling.
- ✅ RadioMenu - Single-select vertical menu with arrow/number/letter navigation
- ✅ CheckboxMenu - Multi-select with checkboxes, select all, and invert
- ✅ BooleanMenu - Yes/No selection (horizontal and vertical)
- ✅ TextInput - Single-line text with validation and constraints
- ✅ NumberInput - Numeric input with min/max validation
- ✅ LanguageSelector - Specialized language picker
- ✅ ModifyField - Composite field modification prompt
- ✅ Headers - Simple and ASCII art headers with borders
- ✅ Progress - Step indicators, stage headers, separators
- ✅ Messages - Success/Error/Warning/Info/Question with icons
- ✅ Summary - Bordered tables with sections and key-value pairs
- ✅ Wizard System - Multi-step configuration flows with progress tracking
- ✅ i18n Support - Chinese and English translations (extensible)
- ✅ Command Handling - Built-in commands (/quit, /help, /clear, /back)
- ✅ Layout System - Flexible component composition
- ✅ Color System - Single colors and two-color gradients
- ✅ Unified API - Simple, consistent interface for all components
- ✅ Zero dependencies - Pure Node.js
- ✅ Fully typed - Complete TypeScript support
- ✅ Modular architecture - All files under 300 lines
- ✅ Component-based - Reusable, composable components
- ✅ Type-safe - Strict TypeScript with full type definitions
npm install cli-menu-kitCLI Menu Kit is highly customizable with sensible defaults. Configure colors, language, and UI elements to match your application's style.
Customize all UI colors globally:
import { setUIColors, colors } from 'cli-menu-kit';
// Override specific colors (all optional)
setUIColors({
primary: colors.blue, // Main interactive elements, highlights
textSecondary: colors.dim, // Descriptions, hints
error: colors.red, // Errors, exit options
border: colors.magenta, // Borders, frames
separator: colors.dim, // Section separators
// ... see full list in documentation
});
// Reset to defaults
import { resetUIColors } from 'cli-menu-kit';
resetUIColors();Switch between English and Chinese (or add custom languages):
import { setLanguage } from 'cli-menu-kit';
setLanguage('en'); // English (default: 'zh')Three header modes for different contexts:
import { renderHeader, renderSectionHeader, renderSimpleHeader } from 'cli-menu-kit';
// Full header (main menu, initialization)
renderHeader({
asciiArt: ['...'], // Optional
title: 'Product Name', // Optional
description: '...', // Optional
version: '1.0.0', // Optional - omit to hide
url: 'https://...', // Optional - omit to hide
menuTitle: 'Select option:' // Optional - omit to hide
});
// Section header (sub-menus)
renderSectionHeader('Section Title', 50); // Width configurable
// Simple header (quick prompts)
renderSimpleHeader('Simple Title');All menu options are configurable:
menu.radio({
options: [
// Optional grouping with separators
{ type: 'separator', label: 'Setup' },
'1. Option 1',
'2. Option 2',
{ type: 'separator', label: 'Advanced' },
'3. Option 3'
],
title: 'Menu Title', // Optional
hints: ['↑↓ Navigate'], // Optional - omit or pass [] to hide
separatorWidth: 40, // Optional - default: 30
allowNumberKeys: true, // Optional - default: true
allowLetterKeys: false // Optional - default: false
});import { menu, input, wizard } from 'cli-menu-kit';
// Radio menu (single-select)
const result = await menu.radio({
title: 'Select Framework',
options: ['React', 'Vue', 'Angular', 'Svelte']
});
console.log(`Selected: ${result.value}`);
// Checkbox menu (multi-select)
const features = await menu.checkbox({
options: ['TypeScript', 'ESLint', 'Prettier', 'Testing'],
minSelections: 1
});
console.log(`Selected: ${features.values.join(', ')}`);
// Boolean menu (yes/no)
const confirmed = await menu.booleanH('Continue?', true);
console.log(`Confirmed: ${confirmed}`);
// Text input
const name = await input.text({
prompt: 'Enter your name',
defaultValue: 'User',
minLength: 2
});
// Number input
const age = await input.number({
prompt: 'Enter your age',
min: 1,
max: 120
});
// Language selector
const lang = await input.language({
languages: [
{ code: 'zh', name: 'Chinese', nativeName: '简体中文' },
{ code: 'en', name: 'English' }
]
});
// Wizard (multi-step flow)
const result = await wizard.run({
steps: [
{
name: 'language',
title: 'Select Language',
component: 'language-selector',
config: { /* ... */ }
},
{
name: 'projectName',
title: 'Project Name',
component: 'text-input',
config: { prompt: 'Enter project name' }
}
]
});import {
createSimpleHeader,
createAsciiHeader,
createProgressIndicator,
showSuccess,
showError,
showWarning,
showInfo,
createSummaryTable
} from 'cli-menu-kit';
// Simple header
createSimpleHeader('My Application', '\x1b[36m');
// ASCII header
createAsciiHeader(asciiArt, {
subtitle: 'Version 1.0.0',
url: 'https://github.com/user/repo'
});
// Progress indicator
createProgressIndicator(['Step 1', 'Step 2', 'Step 3'], 1);
// Messages
showSuccess('Operation completed!');
showError('Something went wrong');
showWarning('Please check your input');
showInfo('Press Ctrl+C to exit');
// Summary table
createSummaryTable('Session Summary', [
{
header: 'Statistics',
items: [
{ key: 'Total', value: '100' },
{ key: 'Success', value: '95' }
]
}
]);import { setLanguage, t } from 'cli-menu-kit';
// Set language
setLanguage('en'); // or 'zh'
// Get translations
const prompt = t('menus.selectPrompt');
const goodbye = t('messages.goodbye');import { registerCommand, handleCommand } from 'cli-menu-kit';
// Register custom command
registerCommand('test', () => {
console.log('Test command executed!');
return false; // Continue (don't exit)
}, 'Run test command');
// Handle command input
const result = handleCommand('/test');
// Built-in commands: /quit, /help, /clear, /backSingle-select vertical menu.
interface RadioMenuConfig {
title?: string;
options: MenuOption[];
prompt?: string;
hints?: string[];
layout?: MenuLayout;
defaultIndex?: number;
allowNumberKeys?: boolean;
allowLetterKeys?: boolean;
onExit?: () => void;
}
// Returns: { index: number, value: string }Multi-select vertical menu.
interface CheckboxMenuConfig {
title?: string;
options: MenuOption[];
prompt?: string;
hints?: string[];
layout?: MenuLayout;
defaultSelected?: number[];
minSelections?: number;
maxSelections?: number;
allowSelectAll?: boolean;
allowInvert?: boolean;
onExit?: () => void;
}
// Returns: { indices: number[], values: string[] }Yes/No selection menu.
interface BooleanMenuConfig {
question: string;
defaultValue?: boolean;
yesText?: string;
noText?: string;
orientation?: 'horizontal' | 'vertical';
onExit?: () => void;
}
// Returns: booleanText input with validation.
interface TextInputConfig {
prompt: string;
defaultValue?: string;
placeholder?: string;
maxLength?: number;
minLength?: number;
allowEmpty?: boolean;
validate?: (value: string) => boolean | string;
errorMessage?: string;
onExit?: () => void;
}
// Returns: stringNumber input with constraints.
interface NumberInputConfig {
prompt: string;
defaultValue?: number;
min?: number;
max?: number;
allowDecimals?: boolean;
allowNegative?: boolean;
validate?: (value: string) => boolean | string;
errorMessage?: string;
onExit?: () => void;
}
// Returns: numberLanguage selector.
interface LanguageSelectorConfig {
languages: Array<{
code: string;
name: string;
nativeName?: string;
}>;
defaultLanguage?: string;
prompt?: string;
onExit?: () => void;
}
// Returns: string (language code)Run a multi-step wizard.
interface WizardConfig {
title?: string;
steps: WizardStep[];
showProgress?: boolean;
onComplete?: (results: Record<string, any>) => void;
onCancel?: () => void;
}
interface WizardStep {
name: string;
title: string;
component: 'radio-menu' | 'checkbox-menu' | 'boolean-menu' |
'text-input' | 'number-input' | 'language-selector';
config: any;
required?: boolean;
validate?: (value: any) => boolean | string;
skip?: (results: Record<string, any>) => boolean;
}
// Returns: { completed: boolean, results: Record<string, any> }The library is organized into a modular architecture:
src/
├── types/ # Type definitions
│ ├── layout.types.ts
│ ├── menu.types.ts
│ ├── input.types.ts
│ └── display.types.ts
├── core/ # Core utilities
│ ├── terminal.ts
│ ├── keyboard.ts
│ ├── renderer.ts
│ └── colors.ts
├── components/ # UI components
│ ├── menus/
│ ├── inputs/
│ └── display/
├── features/ # Advanced features
│ ├── wizard.ts
│ └── commands.ts
├── i18n/ # Internationalization
│ ├── types.ts
│ ├── registry.ts
│ └── languages/
├── api.ts # Unified API
└── index.ts # Main entry point
See ARCHITECTURE.md for detailed documentation.
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
node test/phase2-test.js # Menu components
node test/phase3-test.js # Input components
node test/phase4-test.js # Display components
node test/phase5-test.js # Advanced features- Component-Based: Each UI element is a separate, reusable component
- Layout System: Components can be composed in different orders
- Type Safety: Full TypeScript support with strict typing
- Zero Dependencies: Pure Node.js implementation
- i18n Support: Multi-language support with mapping system
- Maintainability: All files kept under 300 lines
MIT
Contributions are welcome! Please ensure:
- Files stay under 300 lines
- TypeScript types are properly defined
- Code follows existing patterns
- Tests are included for new features
- All comments in English
npm install cli-menu-kitconst { menu } = require('cli-menu-kit');
// Single select
const choice = await menu.select(
['Option 1', 'Option 2', 'Option 3'],
{ title: 'Choose one', lang: 'en' }
);
// Multi select
const choices = await menu.multiSelect(
['Feature A', 'Feature B', 'Feature C'],
{ lang: 'en' }
);
// Yes/No confirmation
const confirmed = await menu.confirm('Continue?', { lang: 'en' });
// Text input
const name = await menu.input('Enter your name', {
defaultValue: 'User',
validator: (input) => input.length > 0 || 'Name cannot be empty'
});
// Number input
const age = await menu.number('Enter your age', {
min: 1,
max: 120
});const {
selectMenu,
selectMultiMenu,
askYesNo,
askInput,
askNumber
} = require('cli-menu-kit');
const choice = await selectMenu(['A', 'B', 'C'], { lang: 'zh' });
const choices = await selectMultiMenu(['1', '2', '3'], { lang: 'zh' });
const confirmed = await askYesNo('确认吗?', { lang: 'zh' });Single-select menu with vertical navigation.
Parameters:
options: Array of strings orMenuOptionobjectsconfig: Configuration objectlang: 'zh' | 'en' (default: 'zh')type: 'main' | 'sub' | 'firstRun' (default: 'main')title: Optional header titleshowPrompt: Show input prompt (default: true for main)showHints: Show operation hints (default: true)
Returns: Selected index (0-based)
Keyboard shortcuts:
- ↑/↓: Navigate
- 1-9: Quick select by number
- A-Z: Quick select by letter (for labeled options)
- Enter: Confirm
- Ctrl+C: Exit
Multi-select menu with checkboxes.
Parameters:
options: Array of stringsconfig: Configuration objectlang: 'zh' | 'en' (default: 'zh')defaultSelected: Array of pre-selected indices
Returns: Array of selected indices
Keyboard shortcuts:
- ↑/↓: Navigate
- Space: Toggle selection
- A: Select all
- I: Invert selection
- Enter: Confirm
- Ctrl+C: Exit
Yes/No confirmation with horizontal selection.
Parameters:
prompt: Question to askoptions: Configuration objectlang: 'zh' | 'en' (default: 'zh')defaultYes: Default to Yes (default: true)
Returns: Boolean (true for Yes, false for No)
Keyboard shortcuts:
- ←/→: Navigate
- Y/N: Quick select
- Enter: Confirm
- Ctrl+C: Exit
Text input with validation.
Parameters:
prompt: Input prompt textoptions: Configuration objectlang: 'zh' | 'en' (default: 'zh')defaultValue: Default valuevalidator: Validation function(input: string) => boolean | string
Returns: User input string
Number input with constraints.
Parameters:
prompt: Input prompt textoptions: Configuration objectlang: 'zh' | 'en' (default: 'zh')min: Minimum valuemax: Maximum valuedefaultValue: Default value
Returns: User input number
Parent-child menu relationship.
Parameters:
parentOptions: Parent menu optionsgetChildOptions: Function(parentIndex: number) => string[]config: Configuration objectparentConfig: Parent menu configurationchildConfig: Child menu configuration
Returns: { parentIndex: number, childIndices: number[] }
const { menu } = require('cli-menu-kit');
const options = [
'1. Create new project - Start a new project',
'2. Open existing - Open an existing project',
'3. Settings - Configure settings',
'4. Exit - Exit the application'
];
const choice = await menu.select(options, {
title: 'Main Menu',
lang: 'en'
});
console.log(`You selected: ${choice}`);const { menu } = require('cli-menu-kit');
const features = ['Dark Mode', 'Auto Save', 'Notifications', 'Analytics'];
const selected = await menu.multiSelect(features, {
lang: 'en',
defaultSelected: [0, 1] // Pre-select first two options
});
console.log(`Selected features: ${selected.map(i => features[i]).join(', ')}`);const { menu } = require('cli-menu-kit');
const email = await menu.input('Enter your email', {
lang: 'en',
validator: (input) => {
if (!input.includes('@')) {
return 'Invalid email format';
}
return true;
}
});
console.log(`Email: ${email}`);const { selectWithChildren } = require('cli-menu-kit');
const categories = ['Electronics', 'Clothing', 'Books'];
const result = await selectWithChildren(
categories,
(parentIndex) => {
if (parentIndex === 0) return ['Phones', 'Laptops', 'Tablets'];
if (parentIndex === 1) return ['Shirts', 'Pants', 'Shoes'];
return ['Fiction', 'Non-Fiction', 'Comics'];
},
{
parentConfig: { title: 'Select Category', lang: 'en' },
childConfig: { lang: 'en' }
}
);
console.log(`Category: ${categories[result.parentIndex]}`);
console.log(`Items: ${result.childIndices.join(', ')}`);const {
showInfo,
showSuccess,
showError,
showWarning,
printHeader
} = require('cli-menu-kit');
showInfo('Processing...', 'zh');
showSuccess('Operation completed!', 'en');
showError('Something went wrong', 'en');
showWarning('Please check your input', 'zh');
printHeader({
asciiArt: [' ███╗ ███╗', ' ████╗ ████║', ' ██╔████╔██║'],
title: 'My App',
subtitle: 'v1.0.0'
});See ARCHITECTURE.md for detailed architecture documentation.
src/
├── types.ts # Type definitions (58 lines)
├── components.ts # Colors, themes, symbols (187 lines)
├── menu-core.ts # Shared utilities (213 lines)
├── menu-single.ts # Single-select menu (163 lines)
├── menu-multi.ts # Multi-select menu (151 lines)
├── input.ts # Input components (246 lines)
├── menu.ts # Unified API wrapper (90 lines)
└── index.ts # Main entry point (12 lines)
All files are kept under 300 lines for maintainability.
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
node test/simple-test.js
node test/input-test.js
node test/unified-api-test.js- Modularity: Each menu type is in its own file
- Decoupling: Common utilities extracted to menu-core.ts
- File Size: Each file kept under 200-300 lines
- Zero Dependencies: Pure Node.js implementation
- Type Safety: Full TypeScript support
- Flexibility: Configurable layouts, prompts, hints
MIT
Contributions are welcome! Please ensure:
- Files stay under 300 lines
- TypeScript types are properly defined
- Code follows existing patterns
- Tests are included for new features