Guides

Building CLI tool development patterns with TypeScript an...

This guide provides a structured approach to building a CLI tool with TypeScript, focusing on cross-platform compatibility, error handling, and integration patterns. Follow these steps to implement a production-ready developer tool.

3-5 hours6 steps
1

Initialize project structure

Create a new project directory and configure TypeScript with a tsconfig.json file. Install required dependencies including commander and cross-env for platform-specific commands.

setup.sh
mkdir devtool-cli && cd $_
npm init -y
tsconfig.json
npm install --save commander cross-env

⚠ Common Pitfalls

  • Forgetting to add 'type': 'module' in package.json for ES modules
  • Incorrect TypeScript target configuration causing runtime errors
2

Define CLI command structure

Create a main.ts file that initializes the CLI with commander. Define base commands and subcommands with appropriate options and descriptions.

import { Command } from 'commander';
const program = new Command();
program.command('init <project-name>').description('Initialize new project').action((name) => {
  console.log(`Initializing project: ${name}`);
});
program.parse(process.argv);

⚠ Common Pitfalls

  • Not using .parse() to trigger command execution
  • Missing required options in subcommands
3

Implement platform-specific logic

Use the os and path modules to handle platform-specific file paths and command execution. Add environment checks for Windows/MacOS/Linux.

import * as os from 'os';
import * as path from 'path';
const configPath = os.platform() === 'win32' 
  ? path.join(process.env.APPDATA || '', 'devtool')
  : path.join(os.homedir(), '.devtool');

⚠ Common Pitfalls

  • Hardcoding file paths instead of using platform-aware modules
  • Ignoring environment variable differences between OSes
4

Add error handling patterns

Implement centralized error handling with try/catch blocks. Create custom error classes for different failure scenarios.

class CLIError extends Error {
  constructor(public code: number, message: string) {
    super(message);
  }
}
try {
  // command logic
} catch (err) {
  console.error(new CLIError(1, err.message));
  process.exit(1);
}

⚠ Common Pitfalls

  • Not differentiating between user errors and system errors
  • Ignoring unhandled promise rejections
5

Create test suite

Write unit tests for core commands using Jest. Test command parsing, error conditions, and platform-specific behavior.

import { execSync } from 'child_process';
test('version command', () => {
  const output = execSync('devtool --version').toString();
  expect(output).toMatch(/v\d+\.\d+\.\d+/);
});

⚠ Common Pitfalls

  • Not mocking process.exit() in tests
  • Missing edge case testing for invalid inputs
6

Package for distribution

Configure npm scripts for building and publishing. Create a .npmignore file to exclude development files.

{
  "scripts": {
    "build": "tsc",
    "publish": "npm publish --access public"
  },
  "files": ["dist"]
}

⚠ Common Pitfalls

  • Including source files in the published package
  • Forgetting to update the version field before publishing

What you built

By following these steps, you've created a CLI tool with robust error handling, cross-platform support, and test coverage. Focus on maintaining clear command hierarchies and consistent error messaging to improve developer experience.