Skip to content

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 .output/index.js --help

Project 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 configuration

Core 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 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": "./.output/index.js",
  "bin": {
    "cli": "./bin/cli.js"
  },
  "files": ["./.output"],
  "engines": {
    "node": ">=18.0.0"
  }
}