Category: guide
Node.js CLI Tools — commander, inquirer, chalk, ora
สร้าง CLI tool ด้วย Node.js: parse args ด้วย commander, prompts ด้วย inquirer, colors ด้วย chalk, spinner ด้วย ora
สารบัญ
Setup Project
mkdir my-cli && cd my-cli
npm init -y
# TypeScript + Execution
npm install --save-dev typescript tsx @types/node
npm install commander chalk ora inquirer
// package.json
{
"name": "my-cli",
"bin": { "my-cli": "./dist/index.js" },
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Commander.js — Argument Parsing
import { program } from 'commander';
program
.name('my-cli')
.description('CLI tool description')
.version('1.0.0');
// Command
program
.command('convert <input>') // required arg
.description('Convert a file')
.option('-o, --output <path>', 'Output path', './output')
.option('-f, --format <type>', 'Format: html|pdf|md', 'html')
.option('-v, --verbose', 'Show verbose output')
.action(async (input, options) => {
console.log('Input:', input);
console.log('Output:', options.output);
console.log('Format:', options.format);
console.log('Verbose:', options.verbose);
});
// Sub-command
program
.command('init [directory]') // optional arg
.description('Initialize project in directory')
.option('--no-git', 'Skip git init')
.action(async (directory = '.', options) => {
const dir = directory ?? '.';
// ...
});
program.parse();
Chalk — Terminal Colors
import chalk from 'chalk';
// Colors
console.log(chalk.green('✓ Success'));
console.log(chalk.red('✗ Error'));
console.log(chalk.yellow('⚠ Warning'));
console.log(chalk.blue('ℹ Info'));
// Styles
console.log(chalk.bold('Bold text'));
console.log(chalk.dim('Dimmed text'));
console.log(chalk.underline('Underlined'));
// Combinations
console.log(chalk.bold.green('Bold green'));
console.log(chalk.bgRed.white('White on red'));
// Template literal
console.log(chalk`{bold.blue INFO} {gray Processing...}`);
// Hex color
console.log(chalk.hex('#ff6b6b')('Custom color'));
Helper Functions
import chalk from 'chalk';
const log = {
success: (msg: string) => console.log(`${chalk.green('✓')} ${msg}`),
error: (msg: string) => console.error(`${chalk.red('✗')} ${chalk.red(msg)}`),
warn: (msg: string) => console.log(`${chalk.yellow('⚠')} ${chalk.yellow(msg)}`),
info: (msg: string) => console.log(`${chalk.blue('ℹ')} ${msg}`),
dim: (msg: string) => console.log(chalk.dim(msg)),
};
log.success('File converted successfully');
log.error('Permission denied');
log.warn('Output directory already exists');
Ora — Spinners
import ora from 'ora';
// Basic spinner
const spinner = ora('Converting file...').start();
try {
await doLongTask();
spinner.succeed('Conversion complete');
} catch (err) {
spinner.fail('Conversion failed');
}
// Change text during task
const spinner = ora('Downloading...').start();
for (let i = 0; i <= 100; i += 10) {
spinner.text = `Downloading... ${i}%`;
await delay(100);
}
spinner.succeed('Downloaded!');
// Custom colors and symbols
const spinner = ora({
text: 'Processing...',
color: 'cyan',
spinner: 'dots2', // or 'line', 'star', 'bouncingBar', etc.
}).start();
Inquirer — Interactive Prompts
import inquirer from 'inquirer';
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'Project name:',
default: 'my-project',
validate: (input) => input.length > 0 || 'Project name cannot be empty',
},
{
type: 'list',
name: 'format',
message: 'Output format:',
choices: ['html', 'pdf', 'markdown'],
default: 'html',
},
{
type: 'checkbox',
name: 'features',
message: 'Select features:',
choices: [
{ name: 'TypeScript', value: 'ts', checked: true },
{ name: 'ESLint', value: 'lint' },
{ name: 'Prettier', value: 'format' },
{ name: 'Tests (Vitest)', value: 'test' },
],
},
{
type: 'confirm',
name: 'init',
message: 'Run npm install?',
default: true,
},
{
type: 'password',
name: 'apiKey',
message: 'API key:',
mask: '*',
},
]);
console.log(answers.projectName); // string
console.log(answers.format); // 'html' | 'pdf' | 'markdown'
console.log(answers.features); // string[]
console.log(answers.init); // boolean
Exit Codes
process.exit(0); // success
process.exit(1); // general error
process.exit(2); // misuse of CLI
Commander ออก exit 1 อัตโนมัติเมื่อ parse error
การอ่าน stdin
import { createInterface } from 'node:readline';
// อ่านทีละบรรทัด
const rl = createInterface({ input: process.stdin });
for await (const line of rl) {
console.log('Line:', line);
}
// อ่านทั้งหมดเป็น string
async function readStdin(): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf-8');
}
// ใช้:
const input = await readStdin();
Full Example: File Converter CLI
#!/usr/bin/env node
// src/index.ts
import { program } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
program
.name('converter')
.description('Convert markdown files to HTML or PDF')
.version('1.0.0');
program
.command('convert <input>')
.option('-o, --output <dir>', 'Output directory', 'output')
.option('-f, --format <type>', 'Output format (html|pdf)', 'html')
.action(async (input: string, options: { output: string; format: string }) => {
if (!existsSync(input)) {
console.error(chalk.red(`✗ File not found: ${input}`));
process.exit(1);
}
await mkdir(options.output, { recursive: true });
const spinner = ora(`Converting ${input}...`).start();
try {
const content = await readFile(input, 'utf-8');
const outputName = path.basename(input, '.md') + `.${options.format}`;
const outputPath = path.join(options.output, outputName);
await writeFile(outputPath, `<html>${content}</html>`); // simplified
spinner.succeed(chalk.green(`Converted → ${outputPath}`));
} catch (err) {
spinner.fail(chalk.red(`Failed: ${(err as Error).message}`));
process.exit(1);
}
});
program.parse();
Distribute ด้วย npm link (local) หรือ npm publish
# ทดสอบ local
npm run build
npm link
# ใช้งาน global
my-cli convert file.md
# unlink
npm unlink my-cli
# publish
npm publish --access public