Commander.jsはJS/TS向けのCLI作成キットだ。
こういう感じでCLIに必要な要素をガリガリと定義していける。
const { program } = require('commander'); program .option('--first') .option('-s, --separator <char>') .argument('<string>'); program.parse(); const options = program.opts(); const limit = options.first ? 1 : undefined; console.log(program.args[0].split(options.separator, limit));
なんでCommander.jsに目がいったのか?
最近Hono CLIをずっと見ているから。
Hono CLIはその名の通りHonoのCLIで、内部的にはCommander.jsをガッツリ活用している。例えばhono requestコマンドだとこんな感じ。
export function requestCommand(program: Command) { program .command('request') .description('Send request to Hono app using app.request()') .argument('[file]', 'Path to the Hono app file') .option('-P, --path <path>', 'Request path', '/') .option('-X, --method <method>', 'HTTP method', 'GET') .option('-d, --data <data>', 'Request body data') .option('-w, --watch', 'Watch for changes and resend request', false) .option('-e, --exclude', 'Exclude protocol response headers in the output', false) .option( '-H, --header <header>', 'Custom headers', (value: string, previous: string[]) => { return previous ? [...previous, value] : [value] }, [] as string[] ) .action(async (file: string | undefined, options: RequestOptions) => { // 以下略
なんでESLintのことを思い出したのか?
最近 ESLintとPrettierのコードリーディングでASTベースの静的解析を理解する | TypeScriptでCLIを作って学ぶAST | Think IT(シンクイット) という素晴らしい記事を拝読してみた結果、理解を深めるついでに自分でもESLintの何か作ってみたくなったから。
だいたいこのへんがわかると、なんとなーくESLintがどうやって解析してるのかを理解できる。
あとはHono CLIにコントリビュートする過程でyusukebeさんと会話するうちに、僕も「自分があると便利そ〜」「これよくない??」っていうものを作って公開してみたくなったから。
何をlintしたかったのか?
上記のコードのoptionメソッドが気になっていた。
optionメソッドの第一引数flagsのJSDoc上の型はstringとなっているので、自由に文字列を渡すことが理論上可能

短縮と正式の順序が逆になってても怒られない

最後は好みだけど、オプションはアルファベット順の方がわかりやすくない?

ってことで作ったのがこれ
https://www.npmjs.com/package/eslint-plugin-commander-option-flags
実装の簡単な説明はREADMEに書いてるのでそっちを参照されたし。
デモ
アルファベット順に並んでなかったりするとwarnで怒ってくれて、eslint --fixすると修正までしてくれる。
Getting Start
npm i eslint --save-dev
eslint.config.mjsに以下のように修正を入れる。
{ plugins: { 'commander-option-flags': commanderOptionFlags, }, rules: { 'commander-option-flags/commander-option-flags': ['warn', { order: 'long' }], }, },
{ order: 'long' }の部分で、アルファベットのソート基準を-pのほうか、--pathの方かを決められる。これは、-X, --method <HTTP Method>のように違うエイリアスが割り当てられる場合が考えられるから。
おわりに
Commander.jsで要領をつかめたので、次は自分が一番好きなCLIツールのcitty向けにもっとガッツリ作ってみようと思う。
2025/12/15 追記
citty向けのeslint-pluginも作成し、リリースしました。