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-cli2. Install Dependencies
bash
# Install dependencies
pnpm install3. Build and Run
bash
# Build the CLI tool
cd apps/my-cli
pnpm build
# Run the CLI tool
node .output/index.js --helpProject Structure
apps/my-cli/
├── bin/
│ └── cli.js # CLI entry script
├── 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
├── .output/ # Build output directory
├── esbuild.config.js # ESBuild configuration
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configurationCore Features
Command System
Main Entry Point
typescript
// src/index.ts
import { Command } from 'commander';
import { version } from '../package.json';
import languageCommand from './commands/language';
import i18next, { initI18n } from './i18n';
import Logger from './utils/logger';
await initI18n();
const program = new Command();
program.version(version, '-v, --version').description(i18next.t('version.description'));
program
.command('language')
.description(i18next.t('language.description'))
.option('-c, --current', i18next.t('language.options.current'))
.option('-r, --reset', i18next.t('language.options.reset'))
.action(languageCommand);
program.parse();Command Implementation
typescript
// src/commands/language/index.ts
import prompts from 'prompts';
import i18next from '../../i18n';
import Logger from '../../utils/logger';
const LANGUAGES = [
{ title: 'English', value: 'en_US' },
{ title: '中文', value: 'zh_CN' },
];
export default async function languageCommand(options: {
current?: boolean;
reset?: boolean;
}) {
if (options.current) {
Logger.info(`${i18next.t('language.current')}: ${i18next.language}`);
} else if (options.reset) {
const response = await prompts({
type: 'select',
name: 'lang',
message: i18next.t('action.select'),
choices: LANGUAGES,
});
if (response.lang) {
await i18next.changeLanguage(response.lang);
await i18next.reloadResources(response.lang);
Logger.success(`${i18next.t('language.success', { lang: response.lang })}`);
}
} else {
Logger.info(`${i18next.t('language.current')}: ${i18next.language}`);
}
}Interactive Prompts
typescript
// src/commands/language/index.ts (excerpt)
import prompts from 'prompts';
const response = await prompts({
type: 'select',
name: 'lang',
message: i18next.t('action.select'),
choices: [
{ title: 'English', value: 'en_US' },
{ title: '中文', value: 'zh_CN' },
],
});File Operations
typescript
// src/utils/file.ts
import fs from 'fs-extra';
import path from 'node:path';
export default class FileManager {
static async ensureDir(dirPath: string): Promise<void> {
await fs.ensureDir(dirPath);
}
static async writeFile(filePath: string, content: string): Promise<void> {
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf-8');
}
static async readFile(filePath: string): Promise<string> {
return fs.readFile(filePath, 'utf-8');
}
static async exists(filePath: string): Promise<boolean> {
return fs.pathExists(filePath);
}
static async copy(src: string, dest: string): Promise<void> {
await fs.copy(src, dest);
}
static async remove(filePath: string): Promise<void> {
await fs.remove(filePath);
}
static join(...paths: string[]): string {
return path.join(...paths);
}
}Git Operations
typescript
// src/utils/git.ts
import simpleGit, { SimpleGit } from 'simple-git';
import i18next from '../i18n';
import FileManager from './file';
import Logger from './logger';
export interface GitConfig {
url: string;
branch?: string;
depth?: number;
}
export default class GitManager {
private git: SimpleGit;
constructor(workingDir?: string) {
this.git = simpleGit(workingDir);
}
async clone(config: GitConfig, targetPath: string): Promise<void> {
const { url, branch = 'main', depth = 1 } = config;
Logger.step(i18next.t('git.clone.begin'));
const options = ['--branch', branch, '--depth', depth.toString()];
await this.git.clone(url, targetPath, options);
Logger.success(i18next.t('git.clone.success'));
}
async pull(branch: string = 'main'): Promise<void> {
Logger.step(i18next.t('git.pull.begin', { branch }));
await this.git.pull('origin', branch);
Logger.success(i18next.t('git.pull.success'));
}
async removeGitDir(repoPath: string): Promise<void> {
const gitDir = FileManager.join(repoPath, '.git');
if (await FileManager.exists(gitDir)) {
await FileManager.remove(gitDir);
}
}
async cloneAndClean(config: GitConfig, targetPath: string): Promise<void> {
await this.clone(config, targetPath);
await this.removeGitDir(targetPath);
}
}Logging System
typescript
// src/utils/logger.ts
import chalk from 'chalk';
export default class Logger {
static info(message: string) {
console.log(chalk.blue('ℹ', message));
}
static error(message: string) {
console.log(chalk.red('✗', message));
}
static success(message: string) {
console.log(chalk.green('✓', message));
}
static warning(message: string) {
console.log(chalk.yellow('⚠', message));
}
static step(message: string): void {
console.log(chalk.cyan('→', message));
}
}Internationalization
typescript
// src/i18n.ts
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import i18next from 'i18next';
import Backend from 'i18next-fs-backend';
const __dirname = dirname(fileURLToPath(import.meta.url));
export const initI18n = async () => {
const languageMap: Record<string, string> = {
zh: 'zh_CN',
'zh-CN': 'zh_CN',
en: 'en_US',
'en-US': 'en_US',
};
const raw = process.env.LANG || process.env.LANGUAGE || 'zh_CN';
const lng = languageMap[raw.split('.')[0] || raw] || 'zh_CN';
await i18next.use(Backend).init({
lng,
fallbackLng: 'en_US',
ns: ['common'],
defaultNS: 'common',
backend: {
loadPath: join(__dirname, 'locales/{{lng}}/{{ns}}.json'),
},
});
};
export default i18next;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: './.output/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": "./.output",
"rootDir": "."
},
"include": ["src/**/*"],
"ts-node": {
"esm": true
}
}Available Scripts
json
{
"scripts": {
"dev": "NODE_ENV=development tsx src/index.ts",
"build": "NODE_ENV=production node esbuild.config.js",
"build:dev": "NODE_ENV=development node esbuild.config.js --dev",
"build:watch": "NODE_ENV=development node esbuild.config.js --dev --watch",
"start": "node dist/index.js",
"prepublishOnly": "npm run build",
"publish:npm": "npm publish",
"publish:beta": "npm publish --tag beta",
"lint": "eslint src/ --ext .ts,.js",
"lint:fix": "eslint src/ --ext .ts,.js --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss}\""
}
}Build and Publishing
Build for Production
bash
cd apps/my-cli
# Build the CLI tool
pnpm build
# The built files will be in the .output directoryPublish to NPM
bash
# Login to NPM
npm login
# Publish the package
npm publishPackage Configuration
json
// package.json
{
"name": "@your-org/my-cli",
"version": "1.0.0",
"description": "A modern CLI tool built with TypeScript",
"main": "./.output/index.js",
"bin": {
"cli": "./bin/cli.js"
},
"files": ["./.output"],
"engines": {
"node": ">=18.0.0"
}
}