T-CREATOR

Node.js CLI ツール開発:commander.js で本格コマンドを作る

Node.js CLI ツール開発:commander.js で本格コマンドを作る

コマンドラインインターフェース(CLI)ツールは、開発者の生産性を劇的に向上させる強力な武器です。特に Node.js エコシステムでは、commander.jsというライブラリが CLI ツール開発の標準となっています。

この記事では、commander.jsを使った本格的な CLI ツールの開発方法を、実際のコード例とエラーハンドリングを含めて詳しく解説します。初心者の方でも、記事を読み終える頃には自分の CLI ツールを作れるようになるでしょう。

commander.js とは

commander.jsは、Node.js で CLI ツールを開発するための最も人気のあるライブラリです。Express.js の作者である TJ Holowaychuk 氏によって開発され、直感的で使いやすい API を提供しています。

主な特徴

  • 直感的な API: チェーンメソッドでコマンドを定義
  • 豊富な機能: オプション、フラグ、サブコマンドを簡単に実装
  • 自動ヘルプ生成: コマンドの説明やオプションを自動でヘルプに反映
  • 型安全性: TypeScript との相性が抜群
  • アクティブな開発: 定期的なアップデートとコミュニティサポート

他のライブラリとの比較

ライブラリ学習コスト機能性コミュニティ推奨度
commander.js⭐⭐⭐⭐⭐
yargs⭐⭐⭐⭐
meow⭐⭐⭐
oclif⭐⭐⭐⭐

CLI ツール開発の基本概念

CLI ツールを開発する前に、基本的な概念を理解しておきましょう。

CLI ツールの構造

CLI ツールは通常、以下の要素で構成されます:

  1. コマンド: 実行したい操作(例:git commit
  2. オプション: コマンドの動作を変更するフラグ(例:--message
  3. 引数: コマンドに渡す値(例:ファイル名)
  4. サブコマンド: メインコマンドの下位コマンド(例:git remote add

一般的なエラーパターン

CLI ツール開発でよく遭遇するエラーとその対処法を理解しておくことが重要です:

bash# よくあるエラー例
Error: Cannot find module 'commander'
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:17)
    at Function.Module._load (node:internal/modules/cjs/loader:778:32)
    at Module.require (node:internal/modules/cjs/helpers:102:12)
    at require (node:internal/modules/cjs/helpers:108:19)

このエラーは、commander.jsがインストールされていない場合に発生します。

commander.js のインストールとセットアップ

まず、新しいプロジェクトを作成し、commander.jsをインストールしましょう。

プロジェクトの初期化

bash# プロジェクトディレクトリを作成
mkdir my-cli-tool
cd my-cli-tool

# package.jsonを初期化
yarn init -y

commander.js のインストール

bash# commander.jsをインストール
yarn add commander

# TypeScriptを使用する場合
yarn add -D typescript @types/node

基本的なセットアップ

最初の CLI ツールを作成してみましょう。index.jsファイルを作成します:

javascript#!/usr/bin/env node

const { Command } = require('commander');

// プログラムの基本情報を設定
const program = new Command();

program
  .name('my-cli')
  .description('My awesome CLI tool')
  .version('1.0.0');

// プログラムを実行
program.parse();

実行権限の設定

CLI ツールとして実行できるように、ファイルに実行権限を付与します:

bash# 実行権限を付与
chmod +x index.js

# テスト実行
node index.js --help

基本的なコマンドの作成

実際に動作するコマンドを作成してみましょう。

シンプルなコマンド

ファイル操作を行う CLI ツールを作成します:

javascript#!/usr/bin/env node

const { Command } = require('commander');
const fs = require('fs');
const path = require('path');

const program = new Command();

program
  .name('file-manager')
  .description('Simple file management CLI tool')
  .version('1.0.0');

// ファイル一覧を表示するコマンド
program
  .command('list')
  .description('List files in current directory')
  .action(() => {
    try {
      const files = fs.readdirSync('.');
      console.log('Files in current directory:');
      files.forEach((file) => {
        const stats = fs.statSync(file);
        const type = stats.isDirectory() ? '📁' : '📄';
        console.log(`${type} ${file}`);
      });
    } catch (error) {
      console.error(
        'Error reading directory:',
        error.message
      );
      process.exit(1);
    }
  });

program.parse();

引数を受け取るコマンド

コマンドに引数を渡せるようにしましょう:

javascript// ファイル内容を表示するコマンド
program
  .command('read <filename>')
  .description('Read and display file content')
  .action((filename) => {
    try {
      if (!fs.existsSync(filename)) {
        console.error(
          `Error: File '${filename}' not found`
        );
        process.exit(1);
      }

      const content = fs.readFileSync(filename, 'utf8');
      console.log(`Content of ${filename}:`);
      console.log('─'.repeat(50));
      console.log(content);
    } catch (error) {
      console.error('Error reading file:', error.message);
      process.exit(1);
    }
  });

オプションとフラグの実装

オプションとフラグを使って、コマンドの動作をカスタマイズできるようにしましょう。

基本的なオプション

javascript// オプション付きのファイル一覧コマンド
program
  .command('list')
  .description('List files in current directory')
  .option('-a, --all', 'Show hidden files')
  .option('-l, --long', 'Show detailed information')
  .action((options) => {
    try {
      const files = fs.readdirSync('.');
      console.log('Files in current directory:');

      files.forEach((file) => {
        // 隠しファイルの処理
        if (!options.all && file.startsWith('.')) {
          return;
        }

        const stats = fs.statSync(file);
        const type = stats.isDirectory() ? '📁' : '📄';

        if (options.long) {
          const size = stats.size;
          const modified = stats.mtime.toLocaleDateString();
          console.log(
            `${type} ${file.padEnd(20)} ${size
              .toString()
              .padStart(10)} bytes ${modified}`
          );
        } else {
          console.log(`${type} ${file}`);
        }
      });
    } catch (error) {
      console.error(
        'Error reading directory:',
        error.message
      );
      process.exit(1);
    }
  });

値を受け取るオプション

javascript// ファイル作成コマンド
program
  .command('create <filename>')
  .description('Create a new file')
  .option('-c, --content <content>', 'Initial file content')
  .option(
    '-t, --type <type>',
    'File type (txt, json, js)',
    'txt'
  )
  .action((filename, options) => {
    try {
      let content = options.content || '';

      // ファイルタイプに応じてデフォルトコンテンツを設定
      if (!options.content) {
        switch (options.type) {
          case 'json':
            content =
              '{\n  "name": "example",\n  "version": "1.0.0"\n}';
            break;
          case 'js':
            content = 'console.log("Hello, World!");';
            break;
          default:
            content =
              '# New File\n\nThis is a new file created by CLI tool.';
        }
      }

      fs.writeFileSync(filename, content);
      console.log(
        `✅ File '${filename}' created successfully`
      );
    } catch (error) {
      console.error('Error creating file:', error.message);
      process.exit(1);
    }
  });

サブコマンドの活用

複雑な CLI ツールでは、サブコマンドを使って機能を整理します。

サブコマンドの実装

javascript// ファイル操作のサブコマンド
const fileCommand = program
  .command('file')
  .description('File operations');

// ファイルのコピー
fileCommand
  .command('copy <source> <destination>')
  .description('Copy a file')
  .option('-f, --force', 'Overwrite existing file')
  .action((source, destination, options) => {
    try {
      if (!fs.existsSync(source)) {
        console.error(
          `Error: Source file '${source}' not found`
        );
        process.exit(1);
      }

      if (fs.existsSync(destination) && !options.force) {
        console.error(
          `Error: Destination file '${destination}' already exists. Use --force to overwrite`
        );
        process.exit(1);
      }

      fs.copyFileSync(source, destination);
      console.log(
        `✅ File copied from '${source}' to '${destination}'`
      );
    } catch (error) {
      console.error('Error copying file:', error.message);
      process.exit(1);
    }
  });

// ファイルの移動
fileCommand
  .command('move <source> <destination>')
  .description('Move a file')
  .action((source, destination) => {
    try {
      if (!fs.existsSync(source)) {
        console.error(
          `Error: Source file '${source}' not found`
        );
        process.exit(1);
      }

      fs.renameSync(source, destination);
      console.log(
        `✅ File moved from '${source}' to '${destination}'`
      );
    } catch (error) {
      console.error('Error moving file:', error.message);
      process.exit(1);
    }
  });

ネストしたサブコマンド

より複雑な構造のサブコマンドも作成できます:

javascript// プロジェクト管理のサブコマンド
const projectCommand = program
  .command('project')
  .description('Project management operations');

const initCommand = projectCommand
  .command('init')
  .description('Initialize a new project');

initCommand
  .command('node')
  .description('Initialize a Node.js project')
  .option('-n, --name <name>', 'Project name')
  .option('-y, --yes', 'Skip prompts')
  .action((options) => {
    try {
      const projectName = options.name || 'my-project';

      if (!options.yes) {
        console.log(
          `Creating Node.js project: ${projectName}`
        );
      }

      // package.jsonの作成
      const packageJson = {
        name: projectName,
        version: '1.0.0',
        description: '',
        main: 'index.js',
        scripts: {
          start: 'node index.js',
          test: 'echo "Error: no test specified" && exit 1',
        },
        keywords: [],
        author: '',
        license: 'ISC',
      };

      fs.writeFileSync(
        'package.json',
        JSON.stringify(packageJson, null, 2)
      );
      console.log('✅ Node.js project initialized');
    } catch (error) {
      console.error(
        'Error initializing project:',
        error.message
      );
      process.exit(1);
    }
  });

エラーハンドリングとバリデーション

堅牢な CLI ツールには、適切なエラーハンドリングとバリデーションが不可欠です。

基本的なエラーハンドリング

javascript// グローバルエラーハンドラー
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error.message);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error(
    'Unhandled Rejection at:',
    promise,
    'reason:',
    reason
  );
  process.exit(1);
});

// カスタムエラークラス
class CLIError extends Error {
  constructor(message, code = 1) {
    super(message);
    this.name = 'CLIError';
    this.code = code;
  }
}

// バリデーション関数
function validateFileExists(filename) {
  if (!fs.existsSync(filename)) {
    throw new CLIError(`File '${filename}' not found`, 2);
  }
}

function validateDirectory(path) {
  if (!fs.existsSync(path)) {
    throw new CLIError(`Directory '${path}' not found`, 3);
  }

  const stats = fs.statSync(path);
  if (!stats.isDirectory()) {
    throw new CLIError(`'${path}' is not a directory`, 4);
  }
}

コマンドでのエラーハンドリング

javascript// エラーハンドリング付きのコマンド
program
  .command('validate <path>')
  .description('Validate file or directory')
  .option(
    '-t, --type <type>',
    'Type to validate (file|directory)',
    'file'
  )
  .action((path, options) => {
    try {
      if (options.type === 'file') {
        validateFileExists(path);
        console.log(
          `✅ File '${path}' exists and is valid`
        );
      } else if (options.type === 'directory') {
        validateDirectory(path);
        console.log(
          `✅ Directory '${path}' exists and is valid`
        );
      } else {
        throw new CLIError(
          `Invalid type: ${options.type}`,
          5
        );
      }
    } catch (error) {
      if (error instanceof CLIError) {
        console.error(`Error: ${error.message}`);
        process.exit(error.code);
      } else {
        console.error('Unexpected error:', error.message);
        process.exit(1);
      }
    }
  });

入力バリデーション

javascript// 高度なバリデーション
function validateEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new CLIError('Invalid email format', 6);
  }
}

function validateNumber(value, min = 0, max = Infinity) {
  const num = Number(value);
  if (isNaN(num)) {
    throw new CLIError('Value must be a number', 7);
  }
  if (num < min || num > max) {
    throw new CLIError(
      `Value must be between ${min} and ${max}`,
      8
    );
  }
  return num;
}

// バリデーション付きのユーザー作成コマンド
program
  .command('user')
  .description('User management')
  .command('create')
  .description('Create a new user')
  .requiredOption('-n, --name <name>', 'User name')
  .requiredOption('-e, --email <email>', 'User email')
  .option('-a, --age <age>', 'User age')
  .action((options) => {
    try {
      // バリデーション
      if (options.name.length < 2) {
        throw new CLIError(
          'Name must be at least 2 characters long',
          9
        );
      }

      validateEmail(options.email);

      let age = null;
      if (options.age) {
        age = validateNumber(options.age, 0, 150);
      }

      // ユーザーオブジェクトの作成
      const user = {
        name: options.name,
        email: options.email,
        age: age,
        createdAt: new Date().toISOString(),
      };

      console.log('✅ User created successfully:');
      console.log(JSON.stringify(user, null, 2));
    } catch (error) {
      if (error instanceof CLIError) {
        console.error(`Error: ${error.message}`);
        process.exit(error.code);
      } else {
        console.error('Unexpected error:', error.message);
        process.exit(1);
      }
    }
  });

実際のプロジェクトでの活用例

実際のプロジェクトで使える実用的な CLI ツールを作成してみましょう。

プロジェクトテンプレート生成ツール

javascript#!/usr/bin/env node

const { Command } = require('commander');
const fs = require('fs');
const path = require('path');

const program = new Command();

program
  .name('project-generator')
  .description('Generate project templates')
  .version('1.0.0');

// テンプレートの定義
const templates = {
  'node-api': {
    description: 'Node.js API project with Express',
    files: {
      'package.json': JSON.stringify(
        {
          name: '{{name}}',
          version: '1.0.0',
          description: '{{description}}',
          main: 'src/index.js',
          scripts: {
            start: 'node src/index.js',
            dev: 'nodemon src/index.js',
            test: 'jest',
          },
          dependencies: {
            express: '^4.18.2',
            cors: '^2.8.5',
          },
          devDependencies: {
            nodemon: '^2.0.22',
            jest: '^29.5.0',
          },
        },
        null,
        2
      ),
      'src/index.js': `const express = require('express');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Hello from {{name}}!' });
});

app.listen(PORT, () => {
  console.log(\`Server running on port \${PORT}\`);
});`,
      'README.md': `# {{name}}

{{description}}

# Installation

\`\`\`bash
yarn install
\`\`\`

# Usage

\`\`\`bash
yarn start
\`\`\`

# Development

\`\`\`bash
yarn dev
\`\`\`
`,
    },
  },
  'react-app': {
    description: 'React application with Vite',
    files: {
      'package.json': JSON.stringify(
        {
          name: '{{name}}',
          version: '1.0.0',
          description: '{{description}}',
          scripts: {
            dev: 'vite',
            build: 'vite build',
            preview: 'vite preview',
          },
          dependencies: {
            react: '^18.2.0',
            'react-dom': '^18.2.0',
          },
          devDependencies: {
            '@vitejs/plugin-react': '^4.0.0',
            vite: '^4.3.9',
          },
        },
        null,
        2
      ),
      'vite.config.js': `import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
});`,
      'index.html': `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{name}}</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>`,
      'src/main.jsx': `import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);`,
      'src/App.jsx': `import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <h1>{{name}}</h1>
      <p>{{description}}</p>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
    </div>
  );
}

export default App;`,
    },
  },
};

// テンプレート生成コマンド
program
  .command('create <template> <name>')
  .description('Create a new project from template')
  .option(
    '-d, --description <description>',
    'Project description',
    'A new project'
  )
  .option('-f, --force', 'Overwrite existing directory')
  .action((template, name, options) => {
    try {
      // テンプレートの存在確認
      if (!templates[template]) {
        console.error(
          `Error: Template '${template}' not found`
        );
        console.log('Available templates:');
        Object.keys(templates).forEach((t) => {
          console.log(
            `  - ${t}: ${templates[t].description}`
          );
        });
        process.exit(1);
      }

      const projectPath = path.join(process.cwd(), name);

      // ディレクトリの存在確認
      if (fs.existsSync(projectPath) && !options.force) {
        console.error(
          `Error: Directory '${name}' already exists. Use --force to overwrite`
        );
        process.exit(1);
      }

      // ディレクトリの作成
      if (!fs.existsSync(projectPath)) {
        fs.mkdirSync(projectPath, { recursive: true });
      }

      const templateData = templates[template];

      // ファイルの作成
      Object.entries(templateData.files).forEach(
        ([filename, content]) => {
          const filePath = path.join(projectPath, filename);
          const dir = path.dirname(filePath);

          // ディレクトリの作成
          if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
          }

          // テンプレート変数の置換
          let processedContent = content
            .replace(/\{\{name\}\}/g, name)
            .replace(
              /\{\{description\}\}/g,
              options.description
            );

          fs.writeFileSync(filePath, processedContent);
          console.log(`✅ Created: ${filename}`);
        }
      );

      console.log(
        `\n🎉 Project '${name}' created successfully!`
      );
      console.log(`\nNext steps:`);
      console.log(`  cd ${name}`);
      console.log(`  yarn install`);

      if (template === 'node-api') {
        console.log(`  yarn dev`);
      } else if (template === 'react-app') {
        console.log(`  yarn dev`);
      }
    } catch (error) {
      console.error(
        'Error creating project:',
        error.message
      );
      process.exit(1);
    }
  });

// 利用可能なテンプレート一覧
program
  .command('list')
  .description('List available templates')
  .action(() => {
    console.log('Available templates:\n');
    Object.entries(templates).forEach(
      ([name, template]) => {
        console.log(
          `${name.padEnd(15)} ${template.description}`
        );
      }
    );
  });

program.parse();

デバッグとテスト

CLI ツールの品質を保つために、適切なデバッグとテストの仕組みを導入しましょう。

デバッグ機能の実装

javascript// デバッグモードの実装
program
  .option('-D, --debug', 'Enable debug mode')
  .hook('preAction', (thisCommand) => {
    if (thisCommand.opts().debug) {
      console.log('🐛 Debug mode enabled');
      console.log('Command:', thisCommand.name());
      console.log('Options:', thisCommand.opts());
      console.log('Arguments:', thisCommand.args);
    }
  });

// ログ機能
class Logger {
  constructor(debug = false) {
    this.debug = debug;
  }

  info(message) {
    console.log(`ℹ️  ${message}`);
  }

  success(message) {
    console.log(`✅ ${message}`);
  }

  warning(message) {
    console.log(`⚠️  ${message}`);
  }

  error(message) {
    console.error(`❌ ${message}`);
  }

  debug(message) {
    if (this.debug) {
      console.log(`🐛 ${message}`);
    }
  }
}

// ログ機能付きのコマンド
program
  .command('process <input>')
  .description('Process input with logging')
  .option('-o, --output <output>', 'Output file')
  .option('-v, --verbose', 'Verbose output')
  .action((input, options) => {
    const logger = new Logger(options.verbose);

    logger.info(`Processing input: ${input}`);
    logger.debug(`Options: ${JSON.stringify(options)}`);

    try {
      // 処理のシミュレーション
      const result = input.toUpperCase();

      if (options.output) {
        fs.writeFileSync(options.output, result);
        logger.success(
          `Result written to: ${options.output}`
        );
      } else {
        console.log(result);
      }

      logger.success('Processing completed');
    } catch (error) {
      logger.error(`Processing failed: ${error.message}`);
      process.exit(1);
    }
  });

テストの実装

javascript// テスト用のユーティリティ
function runCommand(command, args = []) {
  return new Promise((resolve, reject) => {
    const { spawn } = require('child_process');
    const child = spawn('node', [
      'index.js',
      command,
      ...args,
    ]);

    let stdout = '';
    let stderr = '';

    child.stdout.on('data', (data) => {
      stdout += data.toString();
    });

    child.stderr.on('data', (data) => {
      stderr += data.toString();
    });

    child.on('close', (code) => {
      resolve({ code, stdout, stderr });
    });

    child.on('error', (error) => {
      reject(error);
    });
  });
}

// テストファイル (test.js)
async function runTests() {
  console.log('🧪 Running CLI tests...\n');

  try {
    // ヘルプコマンドのテスト
    console.log('Testing help command...');
    const helpResult = await runCommand('--help');
    if (
      helpResult.code === 0 &&
      helpResult.stdout.includes('Usage:')
    ) {
      console.log('✅ Help command works correctly');
    } else {
      console.log('❌ Help command failed');
    }

    // バージョンコマンドのテスト
    console.log('Testing version command...');
    const versionResult = await runCommand('--version');
    if (
      versionResult.code === 0 &&
      versionResult.stdout.includes('1.0.0')
    ) {
      console.log('✅ Version command works correctly');
    } else {
      console.log('❌ Version command failed');
    }

    // エラーハンドリングのテスト
    console.log('Testing error handling...');
    const errorResult = await runCommand(
      'nonexistent-command'
    );
    if (errorResult.code !== 0) {
      console.log('✅ Error handling works correctly');
    } else {
      console.log('❌ Error handling failed');
    }

    console.log('\n🎉 All tests completed!');
  } catch (error) {
    console.error('Test execution failed:', error.message);
    process.exit(1);
  }
}

// テストの実行
if (require.main === module) {
  runTests();
}

パッケージングと配布

作成した CLI ツールを他の人と共有するために、パッケージングと配布の方法を学びましょう。

package.json の設定

json{
  "name": "my-awesome-cli",
  "version": "1.0.0",
  "description": "A powerful CLI tool built with commander.js",
  "main": "index.js",
  "bin": {
    "my-cli": "./index.js"
  },
  "scripts": {
    "start": "node index.js",
    "test": "node test.js",
    "build": "echo 'No build step required'",
    "prepublishOnly": "npm test"
  },
  "keywords": ["cli", "commander", "nodejs", "tools"],
  "author": "Your Name <your.email@example.com>",
  "license": "MIT",
  "dependencies": {
    "commander": "^11.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  },
  "engines": {
    "node": ">=14.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/my-awesome-cli.git"
  },
  "bugs": {
    "url": "https://github.com/yourusername/my-awesome-cli/issues"
  },
  "homepage": "https://github.com/yourusername/my-awesome-cli#readme"
}

グローバルインストール用の設定

bash# グローバルインストール
yarn global add .

# または、npmを使用する場合
npm install -g .

配布用の README 作成

markdown# My Awesome CLI

A powerful command-line interface tool built with Node.js and commander.js.

# Installation

```bash
# Using yarn
yarn global add my-awesome-cli

# Using npm
npm install -g my-awesome-cli
```

Usage

bash# Show help
my-cli --help

# List files
my-cli list

# Create a new file
my-cli create myfile.txt --content "Hello, World!"

# Process files
my-cli process input.txt --output result.txt

Features

  • 📁 File management operations
  • 🔧 Project template generation
  • ✅ Robust error handling
  • 📝 Comprehensive logging
  • 🧪 Built-in testing

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature​/​amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature​/​amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

csharp
## パッケージの公開

```bash
# npmにログイン
npm login

# パッケージを公開
npm publish

# または、スコープ付きパッケージの場合
npm publish --access public

まとめ

この記事では、commander.jsを使った本格的な CLI ツール開発について詳しく解説しました。

学んだこと

  • commander.js の基本: 直感的で強力な CLI ツール開発ライブラリ
  • コマンド設計: 引数、オプション、サブコマンドの効果的な活用
  • エラーハンドリング: 堅牢なエラー処理とバリデーション
  • 実践的な開発: 実際のプロジェクトで使える CLI ツールの作成
  • テストとデバッグ: 品質を保つための仕組み
  • 配布と共有: 他の開発者とツールを共有する方法

次のステップ

CLI ツール開発の世界は奥深く、まだまだ学ぶことがたくさんあります。以下のような発展的なトピックに挑戦してみてください:

  • インタラクティブ機能: inquirer.jsを使った対話的な CLI
  • カラフルな出力: chalkを使った美しいターミナル表示
  • プログレスバー: oracli-progressを使った進捗表示
  • 設定管理: 設定ファイルの読み書きと管理
  • プラグインシステム: 拡張可能な CLI ツールの設計

CLI ツール開発は、開発者の生産性を劇的に向上させる強力なスキルです。この記事で学んだことを基に、自分だけの便利なツールを作成してみてください。きっと、開発の楽しさを再発見できるはずです。

関連リンク