🛠️

Typescriptの便利なCLI作成ツール 〜DevOpsを見据えて〜

2024/08/14に公開

今回は便利なCLI作成ツールでTypescript製ライブラリOclifについて、DevOpsの文脈を絡めながらまとめてみたいと思います。

DevOpsの考え方

DevOpsとは

そのそもDevOpsというものは、特定のツールや言語に影響されるものではなく、考え方や文化といったものだと考えられます。
https://www.oreilly.co.jp/books/9784873118352/

Effective DevOpsによると下記のように記載があります。

devopsは、アジャイルシステム管理や開発と運用の協力を支持する実践者たちによって生まれたものだが、実際にどのようなことをしたのかは環境ごとに異なっている。

また、アンチパターンの章では次のようにも述べられています。

ツールは効果的だが、devopsは特定のツールを使わなければいけないものではない。
~ 中略 ~
devopsは文化運動である。あなたの環境において今使っているツールは、あなたの文化の一部である。ツールの変更を決める前に、既存の文化の一部だったツールには何があるのか、それらのツールを使っている人たちがどんな体験をしたのかを理解し、それぞれの体験の類似点や相違点を検討すべきだろう。

このあとでoclifについて紹介しますが、別にoclifである必要もなく、使いやすい言語のclickのようなCLIライブラリでもいいし、単純にShellなどで実装してしまってもいいと思います。また、大規模になってくると市販のJob管理ツールを入れてもいいなどチームの改善の中で、最適な技術を入れていけばいいと考えます。

DevOpsでCLIを作る意図

(開発と)運用といえば、一般的(WF)には監視の仕組みやツールを導入し、何かしらも問題が発生した場合は、運用設計時に定義したOperationを実行するものだと思います。
しかしスクラムやXPの文脈で登場するDevOpsでは、運用チームと開発チームが極めて近かったり、開発者が運用の一部を受け入れているケースもあります。
特にPJの初期〜中盤フェーズでは、複雑なツールを導入するコストの方が簡単なCLIを実装するよりも、低いことが多いです。

また、チームが楽できることも重要です。反復作業をプログラム化するだけでも十分に意味があると考えます(特にフロー効率重視で、デプロイメントなどの同じ作業を繰り返すことが多いならば余計に効果が出てきます)

Oclifの紹介

紹介

oclifはSalesforce社が管理するTypescript製のCLIツールです。Star数は9k(2024/08/14 投稿時点)となっており、Salesforceやheroku, Shopifyがoclif製のCLIのようです。
node製のCLIライブラリでは、commander.jsがありますが、私が慣れ親しんでいないため下記の機能が提供されていることや、クラスベースで実装の見通しがしやすいため、今回はoclifの紹介です。
oclifが提供するCLIによって、CLIの追加がしやすくなっていたり、HookなどでCLIの実行前後に処理を差し込める他、CLIのヘルプ自動作成やReadme自動作成、ビルドなしでのインスタントな実行機能などCLIの作成に必要な様々な周辺機能が提供されており、Developer Friendlyなツールです。

初期設定

oclifのCLI機能を使いたい時は、globalでinstallします。

your console
npm install --global oclif

oclifの提供するCLIで、初期設定のCLIは2つあります。
基本的にはoclif generateで十分でしょう。

your console
# 1から初期設定する場合に利用する
oclif generate <CLI package name>

# 既存のプロジェクトで事前にコマンドが実装されている場合に利用する
# (bin/dev.jsなどの設定や依存関係の追加などを実行)
oclif init

CLIを管理・公開することを見越して、githubのrepositoryやexport時の名付けなどを聞いてきます。
ほかは基本的な設定なので好きなものを入れておくといいでしょう。

your console
$ oclif generate <cli name>
? Select a module type (ECM or CommonJS)
? NPM package name (your package name)
? Command bin name the CLI will export (your export package name)
? Description (A new CLI generated with oclif)
? Author (git name)
? License (MIT)
? Who is the GitHub owner of repository (https://github.com/OWNER/repo)
(directory name)
? What is the GitHub name of repository (https://github.com/owner/REPO) (cli name)
? Select a package manager (npm, yarn or pnpm)
Creating <cli name>/.eslintignore
Creating <cli name>/.eslintrc.json
Creating <cli name>/.gitignore
Creating <cli name>/.prettierrc.json
Creating <cli name>/README.md
Creating <cli name>/package.json
Creating <cli name>/test/tsconfig.json
Creating <cli name>/test/commands/hello/index.test.ts
Creating <cli name>/test/commands/hello/world.test.ts
Creating <cli name>/src/index.ts
Creating <cli name>/src/commands/hello/index.ts
Creating <cli name>/src/commands/hello/world.ts
Creating <cli name>/.github/workflows/onPushToMain.yml
Creating <cli name>/.github/workflows/onRelease.yml
Creating <cli name>/.github/workflows/test.yml
Creating <cli name>/.mocharc.json
Creating <cli name>/tsconfig.json
Creating <cli name>/bin/dev.cmd
Creating <cli name>/bin/dev.js
Creating <cli name>/bin/run.cmd
Creating <cli name>/bin/run.js
Creating <cli name>/.vscode/launch.json

ここまで済めばあとはひたすらコマンドを設定していけばOKです。
oclifの美味しいところですが、簡単にnest構造のcliにすることもできます。

また基本的に新しいコマンドを設定する時はgenerateコマンドを利用して作成することが望ましいでしょう。
testのscaffoldを作成してくれたり、tsconfig.tsの設定やpackage.jsonへの設定追加など必要なConfig設定を同時にしてくれるため、基本的にはコマンドから作成しましょう。

your console
# 構文
oclif generate command <your command name>

# ex1: src/command配下にsample.tsを作成する
oclif generate command sample

# ex2: src/command配下にfoo/bar.tsを作成する
# 続けて、fooに追加のCLIのコマンドを追加する
oclif generate command foo:bar
oclif generate command foo/add-command # 区切り文字は(/)でも問題なし

ちなみに oclif generate hook <hook name> でhookも同様の生成が可能です。

デバッグの仕方

開発モードだと bin/dev.js (windowsだとbin/dev.cmd)、本番モードだと bin/run.js (windowsだとbin/run.cmd) でコマンドを実行できます。
本番モードの場合は事前にnpm run buildを実行し、Buildをしておく必要があります。

動作の実装

まずCLIへのアクセスですが、ファイル名によって識別しています。
下記の場合だと、bin/dev.js sampleでアクセス可能です。
このコマンド実行を意識してか、ファイル名はケバブケースを推奨しており、基本的にこれに乗っとった方が良さそうです。

sample.ts
import {Args, Command, Flags} from '@oclif/core'

export default class Sample extends Command {
  static override args = {
    file: Args.string({description: 'file to read'}),
  }

  static override description = 'describe the command here'

  static override examples = [
    '<%= config.bin %> <%= command.id %>',
  ]

  static override flags = {
    // flag with no value (-f, --force)
    force: Flags.boolean({char: 'f'}),
    // flag with a value (-n, --name=VALUE)
    name: Flags.string({char: 'n', description: 'name to print'}),
  }

  public async run(): Promise<void> {
    const {args, flags} = await this.parse(Sample)

    const name = flags.name ?? 'world'
    this.log(`hello ${name} from /workspaces/hogehoge/src/commands/group1/sample.ts`)
    if (args.file && flags.force) {
      this.log(`you input --force and --file: ${args.file}`)
    }
  }
}

Args

  static override args = {
    file: Args.string({description: 'file to read'}),
  }

CLIにおける名無しの引数を示しています。
呼び出し方としてはbin/dev.js sample <args>となっており、argsの部分に当たるものです。
※余談ですが、defaultで出てくるこのArgsのfileという命名はわかりづらいですね...

引数は自由にカスタマイズでき、integerbooleanなどいろいろ使えるほか、argsの中に複数設定して、同時に2つ以上のArgsも受けることができます。

  static args = {
    firstArg: Args.string(),
    secondArg: Args.integer(),
  }

https://oclif.io/docs/args

Flag

おそらくCLI作成においてこれが一番使われるものだと思います。
フラグ付きの引数の設定です。

  static override flags = {
    // flag with no value (-f, --force)
    force: Flags.boolean({char: 'f'}),
    // flag with a value (-n, --name=VALUE)
    name: Flags.string({char: 'n', description: 'name to print'}),
  }

呼び出し方はbin/dev.js sample --force --name <something string>です。
また短縮文字の設定も可能です(サンプルの通り)

基本的には、型の指定や増やし方はArgsと同じですが、Flagsの引数がArgsと違うことと、flagsに指定しているプロパティ名がそのままflag名になる点です。

https://oclif.io/docs/flags

Run(Command)

Commandが実行される時の動作を実装しています。

  public async run(): Promise<void> {
    const {args, flags} = await this.parse(Sample)

    const name = flags.name ?? 'world'
    this.log(`hello ${name} from /workspaces/hogehoge/src/sample.ts`)
    if (args.file && flags.force) {
      this.log(`you input --force and --file: ${args.file}`)
    }
  }

デフォルトのTimeoutは10秒です。10秒を超える実装をする場合は、async/awaitで処理の一部を逃すことで実装可能なので、見通しの良さも含めて簡潔な実装を心得ましょう。

Commandの実装についてはリファレンスからご確認ください。
コマンドの説明、コマンドグループへのヘルプの追加、実行モードやアウトプット形式など、様々なカスタマイズについて説明があります。

https://oclif.io/docs/commands

プロジェクトへの適用

開発チームが運用を兼任している場合は、深く考えずにCLIを実装していけば良いかなと思います。
運用チームとの連携がある場合、開発ツールとしてのCLIと運用チームとしてのCLIということできちんと認識できることが必要になってきます。
例として次のような構成が考えられます。

├── _components # 実行用の関数群
│   └── utils.ts
├── development # 開発者向け
│   ├── deploy
│   │   ├── prepare-components.ts
│   │   └── start-delivery.ts
│   └── utils
│       ├── start-bastion.ts
│       └── stop-bastion.ts
└── maintainance # 運用者向け
    ├── data
    │   ├── add-temp-user.ts
    │   └── recovery-something.ts
    └── infra
        └── add-capacity.ts

また、危険回避のための--dry-runモードの実装や、本番実行モードの--executeの実装、そもそものインフラレベルでの権限管理(AWS IAMなど)など、様々な実装が考えられます。

チームの文化を鑑み、最適な構成をご検討ください。

最後に

ここで取り上げているのはoclifの機能のほんの一部です。
Configの細かい設定やnpmやAWS S3への公開など様々な機能があり、快適にCLIを実装させてくれます。
Developerたちの開発/運用体験の向上に役立てていただければ良いかなと思います。
(DevOpsの文脈ですが、ツールの選定、導入、実装はチームの文化に合わせて選んでいただければと思います)

Discussion