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 app add my-cli安装依赖
bash
# 安装依赖
pnpm install构建和运行
bash
# 构建 CLI 工具
cd apps/my-cli
pnpm build
# 运行 CLI 工具
node .output/index.js --help项目结构
apps/my-cli/
├── bin/
│ └── cli.js # 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 # 主入口文件
├── .output/ # 构建输出目录
├── esbuild.config.js # ESBuild 配置
├── package.json # 依赖和脚本
└── tsconfig.json # TypeScript 配置核心特性
命令系统
主入口点
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
.command('version')
.description(i18next.t('version.description'))
.action(() => {
console.log(version);
});
const languageCommandRoot = program
.command('language')
.description(i18next.t('language.description'))
.action(languageCommand);
languageCommandRoot
.command('reset')
.description(i18next.t('language.reset.description'))
.action(() => languageCommand({ reset: true }));
program.parse();命令实现
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: { reset?: boolean } = {}
) {
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}`);
}
}交互式提示
typescript
// src/commands/language/index.ts(片段)
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' },
],
});文件操作
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 操作
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);
}
}日志系统
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));
}
}国际化
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;开发工具
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: './.output/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": "./.output",
"rootDir": "."
},
"include": ["src/**/*"],
"ts-node": {
"esm": true
}
}可用脚本
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}\""
}
}构建和发布
构建生产版本
bash
cd apps/my-cli
# 构建 CLI 工具
pnpm build
# 构建的文件将在 .output 目录中发布到 NPM
bash
# 登录 NPM
npm login
# 发布包
npm publish包配置
json
// package.json
{
"name": "@your-org/my-cli",
"version": "1.0.0",
"description": "使用 TypeScript 构建的现代化 CLI 工具",
"main": "./.output/index.js",
"bin": {
"cli": "./bin/cli.js"
},
"files": ["./.output"],
"engines": {
"node": ">=18.0.0"
}
}