CLI 工具模板
CLI 工具模板提供了现代化的命令行工具开发环境,基于 TypeScript,完美适用于构建功能强大的 CLI 应用程序,提供出色的开发体验。
技术栈
- Node.js - JavaScript 运行时
- TypeScript - 类型安全的开发
- ESBuild - 快速的 JavaScript 打包工具
- Commander.js - 命令行界面框架
- Chalk - 终端字符串样式
- Prompts - 交互式提示
- fs-extra - 增强的文件系统操作
- simple-git - Git 操作
- i18next - 国际化
快速开始
创建项目
bash
# 初始化项目
vup init my-cli-project
# 进入项目目录
cd my-cli-project
# 添加 CLI 模板
vup add my-cli安装依赖
bash
# 安装依赖
pnpm install构建和运行
bash
# 构建 CLI 工具
cd apps/my-cli
pnpm build
# 运行 CLI 工具
node dist/index.js --help项目结构
apps/my-cli/
├── src/
│ ├── commands/ # 命令实现
│ │ └── language/ # 语言命令
│ │ └── index.ts # 语言命令实现
│ ├── utils/ # 工具函数
│ │ ├── file.ts # 文件操作
│ │ ├── git.ts # Git 操作
│ │ └── logger.ts # 日志工具
│ ├── locales/ # 国际化
│ │ ├── en_US/ # 英文翻译
│ │ │ └── common.json
│ │ └── zh_CN/ # 中文翻译
│ │ └── common.json
│ ├── i18n.ts # i18n 配置
│ └── index.ts # 主入口文件
├── esbuild.config.js # ESBuild 配置
├── package.json # 依赖和脚本
└── tsconfig.json # TypeScript 配置核心特性
命令系统
主入口点
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();
// 设置国际化
setupI18n();
// 配置程序
program
.name('my-cli')
.description('使用 TypeScript 构建的现代化 CLI 工具')
.version('1.0.0');
// 添加命令
program.addCommand(languageCommand);
// 处理错误
program.on('command:*', () => {
logger.error('未知命令: %s', program.args.join(' '));
process.exit(1);
});
// 解析参数
program.parse();命令实现
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('管理语言设置')
.option('-l, --list', '列出可用语言')
.option('-s, --set <lang>', '设置语言')
.action(async (options) => {
try {
if (options.list) {
await listLanguages();
} else if (options.set) {
await setLanguage(options.set);
} else {
logger.info('当前语言: %s', i18n.language);
logger.info('使用 --help 查看更多选项');
}
} catch (error) {
logger.error('命令执行失败: %s', error.message);
process.exit(1);
}
});
async function listLanguages() {
const languages = ['en', 'zh'];
logger.info('可用语言:');
languages.forEach((lang) => {
logger.info(' - %s', lang);
});
}
async function setLanguage(lang: string) {
// 设置语言的实现
logger.info('语言已设置为: %s', lang);
}交互式提示
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('操作已取消');
process.exit(0);
}
return response[options.name];
} catch (error) {
logger.error('提示失败: %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;
}文件操作
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('读取文件失败 %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('文件已写入: %s', filePath);
} catch (error) {
logger.error('写入文件失败 %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('文件已复制: %s -> %s', src, dest);
} catch (error) {
logger.error('复制文件失败 %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('目录已创建: %s', dirPath);
} catch (error) {
logger.error('创建目录失败 %s: %s', dirPath, error.message);
throw error;
}
}
}Git 操作
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 仓库已初始化');
} catch (error) {
logger.error('初始化 Git 仓库失败: %s', error.message);
throw error;
}
}
async add(files: string[]): Promise<void> {
try {
await this.git.add(files);
logger.info('文件已添加到 git: %s', files.join(', '));
} catch (error) {
logger.error('添加文件到 git 失败: %s', error.message);
throw error;
}
}
async commit(message: string): Promise<void> {
try {
await this.git.commit(message);
logger.info('变更已提交: %s', message);
} catch (error) {
logger.error('提交变更失败: %s', error.message);
throw error;
}
}
async status(): Promise<any> {
try {
return await this.git.status();
} catch (error) {
logger.error('获取 git 状态失败: %s', error.message);
throw error;
}
}
async isRepo(): Promise<boolean> {
try {
await this.git.status();
return true;
} catch {
return false;
}
}
}日志系统
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();国际化
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 };开发工具
ESBuild 配置
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: [
// 将依赖标记为外部
],
banner: {
js: '#!/usr/bin/env node',
},
define: {
'process.env.NODE_ENV': '"production"',
},
};
build(buildConfig).catch(() => process.exit(1));TypeScript 配置
json
// tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../../_shared/*"]
}
},
"include": ["src/**/*", "src/**/*.ts"]
}可用脚本
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}\""
}
}最佳实践
错误处理
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 错误 [%s]: %s', error.code, error.message);
} else if (error instanceof Error) {
logger.error('意外错误: %s', error.message);
} else {
logger.error('未知错误: %s', String(error));
}
process.exit(1);
}配置管理
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('保存配置失败: %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);
}
}命令验证
typescript
// src/utils/validation.ts
export function validateRequired(value: any, name: string): void {
if (value === undefined || value === null || value === '') {
throw new CLIError(`${name} 是必需的`, 'VALIDATION_ERROR');
}
}
export function validateFileExists(filePath: string): void {
if (!fs.existsSync(filePath)) {
throw new CLIError(`文件未找到: ${filePath}`, 'FILE_NOT_FOUND');
}
}
export function validateDirectory(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
throw new CLIError(`目录未找到: ${dirPath}`, 'DIRECTORY_NOT_FOUND');
}
if (!fs.statSync(dirPath).isDirectory()) {
throw new CLIError(`路径不是目录: ${dirPath}`, 'NOT_A_DIRECTORY');
}
}构建和发布
构建生产版本
bash
# 构建 CLI 工具
pnpm build
# 构建的文件将在 dist 目录中发布到 NPM
bash
# 登录 NPM
npm login
# 发布包
npm publish包配置
json
// package.json
{
"name": "@your-org/my-cli",
"version": "1.0.0",
"description": "使用 TypeScript 构建的现代化 CLI 工具",
"main": "dist/index.js",
"bin": {
"my-cli": "dist/index.js"
},
"files": ["dist"],
"engines": {
"node": ">=18.0.0"
}
}