CLI Tool Template
The CLI Tool template provides a modern command-line tool development environment with TypeScript, perfect for building powerful CLI applications with excellent developer experience.
Technical Stack
- Node.js - JavaScript runtime
- TypeScript - Type-safe development
- ESBuild - Fast JavaScript bundler
- Commander.js - Command-line interface framework
- Chalk - Terminal string styling
- Prompts - Interactive prompts
- fs-extra - Enhanced file system operations
- simple-git - Git operations
- i18next - Internationalization
Quick Start
1. Create Project
bash
# Initialize project
vup init my-cli-project
cd my-cli-project
# Add CLI template
vup add my-cli
2. Install Dependencies
bash
# Install dependencies
pnpm install
3. Build and Run
bash
# Build the CLI tool
cd apps/my-cli
pnpm build
# Run the CLI tool
node dist/index.js --help
Project Structure
apps/my-cli/
├── src/
│ ├── commands/ # Command implementations
│ │ └── language/ # Language command
│ │ └── index.ts # Language command implementation
│ ├── utils/ # Utility functions
│ │ ├── file.ts # File operations
│ │ ├── git.ts # Git operations
│ │ └── logger.ts # Logging utilities
│ ├── locales/ # Internationalization
│ │ ├── en_US/ # English translations
│ │ │ └── common.json
│ │ └── zh_CN/ # Chinese translations
│ │ └── common.json
│ ├── i18n.ts # i18n configuration
│ └── index.ts # Main entry point
├── esbuild.config.js # ESBuild configuration
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configuration
Core Features
Command System
Main Entry Point
typescript
// src/index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import { logger } from './utils/logger';
import { setupI18n } from './i18n';
import { languageCommand } from './commands/language';
const program = new Command();
// Set up internationalization
setupI18n();
// Configure the program
program
.name('my-cli')
.description('A modern CLI tool built with TypeScript')
.version('1.0.0');
// Add commands
program.addCommand(languageCommand);
// Handle errors
program.on('command:*', () => {
logger.error('Unknown command: %s', program.args.join(' '));
process.exit(1);
});
// Parse arguments
program.parse();
Command Implementation
typescript
// src/commands/language/index.ts
import { Command } from 'commander';
import { logger } from '../../utils/logger';
import { i18n } from '../../i18n';
export const languageCommand = new Command('language')
.description('Manage language settings')
.option('-l, --list', 'List available languages')
.option('-s, --set <lang>', 'Set language')
.action(async (options) => {
try {
if (options.list) {
await listLanguages();
} else if (options.set) {
await setLanguage(options.set);
} else {
logger.info('Current language: %s', i18n.language);
logger.info('Use --help for more options');
}
} catch (error) {
logger.error('Command failed: %s', error.message);
process.exit(1);
}
});
async function listLanguages() {
const languages = ['en', 'zh'];
logger.info('Available languages:');
languages.forEach((lang) => {
logger.info(' - %s', lang);
});
}
async function setLanguage(lang: string) {
// Implementation for setting language
logger.info('Language set to: %s', lang);
}
Interactive Prompts
typescript
// src/utils/prompts.ts
import prompts from 'prompts';
import { logger } from './logger';
export interface PromptOptions {
type: 'text' | 'select' | 'confirm' | 'multiselect';
name: string;
message: string;
choices?: Array<{ title: string; value: any }>;
initial?: any;
validate?: (value: any) => boolean | string;
}
export async function prompt(options: PromptOptions): Promise<any> {
try {
const response = await prompts({
type: options.type,
name: options.name,
message: options.message,
choices: options.choices,
initial: options.initial,
validate: options.validate,
});
if (response[options.name] === undefined) {
logger.warn('Operation cancelled');
process.exit(0);
}
return response[options.name];
} catch (error) {
logger.error('Prompt failed: %s', error.message);
process.exit(1);
}
}
export async function confirm(message: string): Promise<boolean> {
const response = await prompt({
type: 'confirm',
name: 'value',
message,
});
return response;
}
export async function select<T>(
message: string,
choices: Array<{ title: string; value: T }>
): Promise<T> {
const response = await prompt({
type: 'select',
name: 'value',
message,
choices,
});
return response;
}
File Operations
typescript
// src/utils/file.ts
import fs from 'fs-extra';
import path from 'path';
import { logger } from './logger';
export class FileService {
static async readFile(filePath: string): Promise<string> {
try {
return await fs.readFile(filePath, 'utf-8');
} catch (error) {
logger.error('Failed to read file %s: %s', filePath, error.message);
throw error;
}
}
static async writeFile(filePath: string, content: string): Promise<void> {
try {
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf-8');
logger.info('File written: %s', filePath);
} catch (error) {
logger.error('Failed to write file %s: %s', filePath, error.message);
throw error;
}
}
static async copyFile(src: string, dest: string): Promise<void> {
try {
await fs.ensureDir(path.dirname(dest));
await fs.copy(src, dest);
logger.info('File copied: %s -> %s', src, dest);
} catch (error) {
logger.error('Failed to copy file %s: %s', src, error.message);
throw error;
}
}
static async exists(filePath: string): Promise<boolean> {
return await fs.pathExists(filePath);
}
static async mkdir(dirPath: string): Promise<void> {
try {
await fs.ensureDir(dirPath);
logger.info('Directory created: %s', dirPath);
} catch (error) {
logger.error('Failed to create directory %s: %s', dirPath, error.message);
throw error;
}
}
}
Git Operations
typescript
// src/utils/git.ts
import simpleGit, { SimpleGit } from 'simple-git';
import { logger } from './logger';
export class GitService {
private git: SimpleGit;
constructor(workingDir: string = process.cwd()) {
this.git = simpleGit(workingDir);
}
async init(): Promise<void> {
try {
await this.git.init();
logger.info('Git repository initialized');
} catch (error) {
logger.error('Failed to initialize git repository: %s', error.message);
throw error;
}
}
async add(files: string[]): Promise<void> {
try {
await this.git.add(files);
logger.info('Files added to git: %s', files.join(', '));
} catch (error) {
logger.error('Failed to add files to git: %s', error.message);
throw error;
}
}
async commit(message: string): Promise<void> {
try {
await this.git.commit(message);
logger.info('Changes committed: %s', message);
} catch (error) {
logger.error('Failed to commit changes: %s', error.message);
throw error;
}
}
async status(): Promise<any> {
try {
return await this.git.status();
} catch (error) {
logger.error('Failed to get git status: %s', error.message);
throw error;
}
}
async isRepo(): Promise<boolean> {
try {
await this.git.status();
return true;
} catch {
return false;
}
}
}
Logging System
typescript
// src/utils/logger.ts
import chalk from 'chalk';
export class Logger {
private static instance: Logger;
private verbose: boolean = false;
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setVerbose(verbose: boolean): void {
this.verbose = verbose;
}
info(message: string, ...args: any[]): void {
console.log(chalk.blue('ℹ'), message, ...args);
}
success(message: string, ...args: any[]): void {
console.log(chalk.green('✓'), message, ...args);
}
warn(message: string, ...args: any[]): void {
console.log(chalk.yellow('⚠'), message, ...args);
}
error(message: string, ...args: any[]): void {
console.error(chalk.red('✗'), message, ...args);
}
debug(message: string, ...args: any[]): void {
if (this.verbose) {
console.log(chalk.gray('🐛'), message, ...args);
}
}
}
export const logger = Logger.getInstance();
Internationalization
typescript
// src/i18n.ts
import i18next from 'i18next';
import { readFileSync } from 'fs';
import { join } from 'path';
let i18n: typeof i18next;
export function setupI18n(language: string = 'en'): typeof i18next {
if (i18n) {
return i18n;
}
i18n = i18next.init({
lng: language,
fallbackLng: 'en',
resources: {
en: {
translation: JSON.parse(
readFileSync(join(__dirname, 'locales/en_US/common.json'), 'utf-8')
),
},
zh: {
translation: JSON.parse(
readFileSync(join(__dirname, 'locales/zh_CN/common.json'), 'utf-8')
),
},
},
});
return i18n;
}
export { i18n };
Development Tools
ESBuild Configuration
javascript
// esbuild.config.js
import { build } from 'esbuild';
import { resolve } from 'path';
const buildConfig = {
entryPoints: ['src/index.ts'],
bundle: true,
platform: 'node',
target: 'node18',
outfile: 'dist/index.js',
format: 'cjs',
sourcemap: true,
minify: true,
external: [
// Mark dependencies as external
],
banner: {
js: '#!/usr/bin/env node',
},
define: {
'process.env.NODE_ENV': '"production"',
},
};
build(buildConfig).catch(() => process.exit(1));
TypeScript Configuration
json
// tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../../_shared/*"]
}
},
"include": ["src/**/*", "src/**/*.ts"]
}
Available Scripts
json
{
"scripts": {
"dev": "tsc --watch",
"build": "tsc && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --banner:js='#!/usr/bin/env node'",
"start": "node dist/index.js",
"lint": "eslint src/ --ext .ts",
"lint:fix": "eslint src/ --ext .ts --fix",
"format": "prettier --write \"src/**/*.{ts,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,json}\""
}
}
Best Practices
Error Handling
typescript
// src/utils/error.ts
export class CLIError extends Error {
constructor(
message: string,
public code: string = 'CLI_ERROR'
) {
super(message);
this.name = 'CLIError';
}
}
export function handleError(error: unknown): void {
if (error instanceof CLIError) {
logger.error('CLI Error [%s]: %s', error.code, error.message);
} else if (error instanceof Error) {
logger.error('Unexpected Error: %s', error.message);
} else {
logger.error('Unknown Error: %s', String(error));
}
process.exit(1);
}
Configuration Management
typescript
// src/utils/config.ts
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
export interface Config {
language: string;
verbose: boolean;
[key: string]: any;
}
export class ConfigService {
private static configPath = join(homedir(), '.my-cli', 'config.json');
private static config: Config;
static load(): Config {
try {
const configData = readFileSync(this.configPath, 'utf-8');
this.config = JSON.parse(configData);
} catch {
this.config = {
language: 'en',
verbose: false,
};
}
return this.config;
}
static save(config: Config): void {
try {
writeFileSync(this.configPath, JSON.stringify(config, null, 2));
this.config = config;
} catch (error) {
logger.error('Failed to save config: %s', error.message);
}
}
static get(key: string): any {
return this.config[key];
}
static set(key: string, value: any): void {
this.config[key] = value;
this.save(this.config);
}
}
Command Validation
typescript
// src/utils/validation.ts
export function validateRequired(value: any, name: string): void {
if (value === undefined || value === null || value === '') {
throw new CLIError(`${name} is required`, 'VALIDATION_ERROR');
}
}
export function validateFileExists(filePath: string): void {
if (!fs.existsSync(filePath)) {
throw new CLIError(`File not found: ${filePath}`, 'FILE_NOT_FOUND');
}
}
export function validateDirectory(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
throw new CLIError(
`Directory not found: ${dirPath}`,
'DIRECTORY_NOT_FOUND'
);
}
if (!fs.statSync(dirPath).isDirectory()) {
throw new CLIError(
`Path is not a directory: ${dirPath}`,
'NOT_A_DIRECTORY'
);
}
}
Build and Publishing
Build for Production
bash
# Build the CLI tool
pnpm build
# The built files will be in the dist directory
Publish to NPM
bash
# Login to NPM
npm login
# Publish the package
npm publish
Package Configuration
json
// package.json
{
"name": "@your-org/my-cli",
"version": "1.0.0",
"description": "A modern CLI tool built with TypeScript",
"main": "dist/index.js",
"bin": {
"my-cli": "dist/index.js"
},
"files": ["dist"],
"engines": {
"node": ">=18.0.0"
}
}