期待の新星@clack/promptsで作る対話型CLI

## 概要

2023/2/13に[@clack/prompts](https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme)がリリースされました。

https://twitter.com/n_moore/status/1624951793829519362

筆者は今まで対話型CLIで有名な[prompts](https://www.npmjs.com/package/prompts)を使ってきましたが、@clack/promptsは評判も上々(1.9k Stars)なので実際に触ってみたいと思います。

## @clack/promptsとは
- 対話型CLIを作るためのAPI
- [prompts](https://www.npmjs.com/package/prompts)より機能は少ないが **80%** 小さいサイズ
- UIが少し**カッコいい**✨

## できること
- テキスト入力
- テキストのバリデーション
- 確認分岐
- 単一選択
- 複数選択
- スピナーの表示
- イントロ、アウトロの表示
- キャンセル時のCallback

## 実際に使ってみる

### インストール
```bash
npm install @clack/prompts
```

公式にデモが用意されているので、デモのソースをベースに構成してみました。
```js
import { intro, outro, confirm, select, multiselect, spinner, isCancel, cancel, text } from '@clack/prompts';
import { setTimeout as sleep } from 'node:timers/promises';

async function main() {
  intro('ようこそ！');

  // テキスト
  const name = await text({
    message: 'お名前を教えてください',
    placeholder: 'ひみつ', // デフォルト値
    validate(value) {
      // バリデーション
      if (value.length < 2) return 'お名前が間違っています';
    },
  });

  // textがキャンセルされた
  if (isCancel(name)) {
    cancel('バイバイ');
    return process.exit(0);
  }

  // 確認（Yes / No）
  const shouldContinue = await confirm({
    message: `お名前は【${name}】でよろしいですか？`,
  });

  // confirmがキャンセルされた
  if (isCancel(shouldContinue)) {
    cancel('バイバイ');
    return process.exit(0);
  }

  // 単一選択
  const animals = await select({
    message: '好きな動物は？.',
    options: [
      { value: 'cats', label: '猫' },
      { value: 'dogs', label: '犬' },
      { value: 'bees', label: 'ハチ', hint: 'これは虫だよ' },
    ],
  });

  // selectがキャンセルされた
  if (isCancel(animals)) {
    cancel('バイバイ');
    return process.exit(0);
  }

  // 複数選択
  const weathers = await multiselect({
    message: '好きな天気は？（いくつでも）',
    options: [
      { value: 'fine', label: '晴れ' },
      { value: 'cloudy', label: '曇り' },
      { value: 'rain', label: '雨' },
      { value: 'snow', label: '雪' },
      initialValue: 'snow', // 初期カーソル位置
    ],
  });

  // selectがキャンセルされた
  if (isCancel(weathers)) {
    cancel('バイバイ');
    return process.exit(0);
  }

  // スピナー
  const s = spinner();
  s.start('考え中です...');

  await sleep(3000);

  s.stop('終わりました！');

  outro('さようなら👋');
}

main().catch(console.error);
```
次にそれぞれの機能を解説していきます。

### イントロ表示
プロンプトのイントロを表示します。
```js
  intro('ようこそ！');
```
#### 実行イメージ
![1__.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/4873067c-36ce-0d32-e98f-ef3e9ba8b1cd.png)

公式デモのように、[picocolors](https://www.npmjs.com/package/picocolors)などを使用して文字を修飾することもできます。
```js
intro(color.inverse('ようこそ！'));
```

#### 実行イメージ
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/2dcf8e67-f6a1-d61d-2a86-e1f6663f2894.png)


### テキスト入力
テキスト入力を促すプロンプトを実行します。設問文の他にデフォルトとバリデーションが設定できます。

返り値には入力値が格納されているので、後ほど確認プロンプトで設問文に挿入していきます。
```js
// テキスト
const name = await text({
  message: 'お名前を教えてください',
  placeholder: 'ひみつ', // デフォルト値
  validate(value) {
    // バリデーション
    if (value.length < 2) return 'お名前が間違っています';
  },
});
```
#### 実行イメージ
![1___.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/8204eed6-e277-2985-f92f-f04bef699854.png)

### キャンセル
プロンプトの返り値を引数にしてisCancelで判定することで、キャンセル（```CTRL + C```）時のハンドリングが可能です。
```js
// textがキャンセルされた
if (isCancel(name)) {
  cancel('バイバイ');
  return process.exit(0);
}
```

#### 実行イメージ
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/b124a5de-0f26-5d19-b5a3-ae62498974a6.png)

### 確認
YesかNoで回答できる確認プロンプトを実行します。

設問文に上で入力したテキストの値を表示してみました。
```js
// 確認（Yes / No）
const shouldContinue = await confirm({
  message: `お名前は【${name}】でよろしいですか？`,
});
```

#### 実行イメージ
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/9bf01bf3-e682-d13f-78d6-a692554b620a.png)

### 単一選択
単一選択ではvalueやlabelの他に、選択肢がアクティブな際に表示される注釈も設定できます。
```js
// 単一選択
const animals = await select({
  message: '好きな動物は？.',
  options: [
    { value: 'cats', label: '猫' },
    { value: 'dogs', label: '犬' },
    { value: 'bees', label: 'ハチ', hint: 'これは虫だよ' },
  ],
});
```

#### 実行イメージ
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/f02828a2-4319-522d-1328-95c166b87a55.png)

### 複数選択
Spaceキーで選択肢を複数個選択できるプロンプトです。
初期カーソル位置を指定することもできます。
```js
// 複数選択
const weathers = await multiselect({
  message: '好きな天気は？（いくつでも）',
  options: [
    { value: 'fine', label: '晴れ' },
    { value: 'cloudy', label: '曇り' },
    { value: 'rain', label: '雨' },
    { value: 'snow', label: '雪' },
  ],
  initialValue: 'snow', // 初期カーソル位置
});
```

#### 実行イメージ
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/ad59b19c-b9a7-e5a0-8d39-9e9140231166.png)

### スピナー
別途[ora](https://github.com/sindresorhus/ora)などを用意しなくとも、処理中にくるくる回るアレを簡単に表示することができます。
promptsにはない機能です。
```js
// スピナー
const s = spinner();
s.start('考え中です...');

// ... なにかの処理

s.stop('終わりました！');
```

#### 実行イメージ
![7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/237b638d-0706-44b1-94fd-2e21eadea0f8.gif)


### アウトロ
イントロ同様に最終的に表示されるメッセージを指定することができます。
```js
outro('さようなら👋');
```
#### 実行イメージ
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/4401ea14-cb93-3e0f-54ba-c38790edec4e.png)


## まとめ
老舗のpromptsと比べて入力文字のマスキング、オートコンプリート、日付指定などはまだできませんが、単純な対話型CLIなら十分対応できると思いました。何よりサイズが軽量なのとちょっとばかりオシャレなUIが魅力です。

ぜひ対話型CLIを活用して、自分やチーム内で使うちょっとしたツールを開発してみてはいかがでしょうか。

## 参考
https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme

https://www.npmjs.com/package/prompts

https://www.npmjs.com/package/picocolors

https://github.com/sindresorhus/ora