Skip to content

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"
  }
}

相关资源