kintone プラグイン一括登録・更新

kintone プラグインを一括登録・更新する処理です。
最新版のプラグインをまとめて登録・更新するのに便利です。

# 概要

[kintone プラグインアップローダー互換対応](https://qiita.com/rex0220/items/aab91d4ee35baf113082)を順番に呼び出すだけですが、ドメインやユーザーなどを指定して汎用的に使えるようにします。
開発環境・運用環境・米国・中国など複数環境のプラグイン登録・更新に便利です。

plugin-uploader.js を起動するリストがあれば、それを単純にシェルに流し込めば実行できますが、エラー処理や対象ドメインを指定できるようにします。

- ドメイン・ユーザー・パスワード指定
- 待機時間指定
    - プラグイン登録・更新は、サーバー側でそれなりの処理時間がかかると思われるので、規定値で3秒待ちにした
- エラー時に続行するか応答確認

# プラグイン一括登録・更新の実行例

指定ファイルには、プラグインファイル・プラグインIDを指定します。
プラグイン名は、コメントで入れています。
プラグインの開発環境では、[kintone プラグイン一括登録・更新用リスト作成](https://qiita.com/rex0220/items/dafcae04a045e6292281)を使って、作ると簡単です。

```batchList.txt
node scripts/plugin-uploader.js -f aggregate/dist/aggregate-plugin56.zip  --id dnfmecefagpmheminfphakcfaoephohi  # アプリ集計
node scripts/plugin-uploader.js -f autocomplete/dist/autocomplete-plugin2.zip  --id oekapkklfjhakkcfefjpldkbpkialkof  # 自動補完
node scripts/plugin-uploader.js -f autolookup/dist/autolookup-plugin8.zip  --id ioagfjojflfaoomnphdpkfgjeghohebj  # ルックアップ自動取得
```
- 正常時のログ例

```実行ログ.txt
plugin > node .\scripts\bulk-reg-plugins.js -f .\scripts\batchList.txt -d {domain} -u {user} -p {password}
 #1: node scripts/plugin-uploader.js -f aggregate/dist/aggregate-plugin56.zip  --id dnfmecefagpmheminfphakcfaoephohi  # アプリ集計
Uploading file...
File uploaded successfully. fileKey: 6e2b4db7-9d59-4927-ac2d-d0f1307b8c50
Plugin added successfully. { id: 'dnfmecefagpmheminfphakcfaoephohi', version: '56' }

 #2: node scripts/plugin-uploader.js -f autocomplete/dist/autocomplete-plugin2.zip  --id oekapkklfjhakkcfefjpldkbpkialkof  # 自動補完
Uploading file...
File uploaded successfully. fileKey: a9becc07-d3a5-4166-877a-6e1589468d55
Plugin added successfully. { id: 'oekapkklfjhakkcfefjpldkbpkialkof', version: '2' }

 #3: node scripts/plugin-uploader.js -f autolookup/dist/autolookup-plugin8.zip  --id ioagfjojflfaoomnphdpkfgjeghohebj  # ルックアップ自動取得
Uploading file...
File uploaded successfully. fileKey: 1ba0b9bd-b99c-4355-92c2-7e7af5177a83
Plugin added successfully. { id: 'ioagfjojflfaoomnphdpkfgjeghohebj', version: '8' }

```

- エラー時のログ例

![2024-07-20_16h06_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100572/5674559a-8c78-cbea-5a86-0b8d914f0a3d.png)


# bulk-reg-plugins.js オプション

help オプションで表示

```.log
> node .\scripts\bulk-reg-plugins.js -h
オプション:
      --version   バージョンを表示                                        [真偽]
  -f, --file      Specify the file containing commands           [文字列] [必須]
  -d, --domain    kintone domain                                        [文字列]
  -u, --username  kintone username                                      [文字列]
  -p, --password  kintone password                                      [文字列]
      --dir       Specify the directory                [文字列] [デフォルト: ""]
  -t, --waittime  Specify the wait time in ms          [数値] [デフォルト: 3000]
  -s, --skip      Skip errors.                        [真偽] [デフォルト: false]
  -h, --help      ヘルプを表示                                            [真偽]
```

# bulk-reg-plugins.js

```bulk-reg-plugins.js
/**
 * プラグイン一括登録更新スクリプト
 * 
 * このスクリプトは、指定されたファイルからコマンドを読み込み、
 * 各コマンドに指定されたディレクトリと待機時間のオプションを追加して実行します。
 * コマンド実行中にエラーが発生した場合、ユーザーに継続するか中止するかを尋ねます。
 */

const fs = require('fs').promises; // ファイル
const { exec } = require('child_process'); // 実行
const util = require('util'); // ユーティリティ
const yargs = require('yargs'); // 引数解析
const prompts = require('prompts'); // プロンプト
const kleur = require('kleur'); // 色

// `exec` をプロミスに変換
const execPromise = util.promisify(exec);

// yargs を使用してコマンドライン引数を解析
const argv = yargs
    .option('file', {
        alias: 'f',
        description: 'Specify the file containing commands',
        type: 'string',
        demandOption: true
    })
    .option('domain', {
        alias: 'd',
        description: 'kintone domain',
        type: 'string'
    })
    .option('username', {
        alias: 'u',
        description: 'kintone username',
        type: 'string'
    })
    .option('password', {
        alias: 'p',
        description: 'kintone password',
        type: 'string'
    })
    .option('dir', {
        description: 'Specify the directory',
        type: 'string',
        default: ''
    })
    .option('waittime', {
        alias: 't',
        description: 'Specify the wait time in ms',
        type: 'number',
        default: 3000 // デフォルト値を設定
    })
    .option('skip', {
        alias: 's',
        description: 'Skip errors.',
        type: 'boolean',
        default: false
    })
    .help()
    .alias('help', 'h')
    .argv;

// オプション 
const domain = argv.domain;
const username = argv.username;
const password = argv.password;
const waitTime = argv.waittime;
const targetDir = argv.dir;
const skipErrors = argv.skip;

// ファイルを読み込む
const readFile = async (filePath) => {
    try {
        let data = await fs.readFile(filePath, 'utf8');
        if (targetDir) {
            data = data.replace(/(node scripts\/plugin-uploader\.js\s+--?f(?:ile)?\s+)([\w\/-]+\.zip)/g, '$1' + targetDir +'$2');
        }
        return data.replace(/\r/g, '').split('\n').filter(line => line.trim() !== '');
    } catch (err) {
        throw new Error(`Error reading file: ${err.message}`);
    }
};

// エラー確認
const errPrompt = async () => {
    if (skipErrors) return;
    const response = await prompts({
        type: 'confirm',
        name: 'continue',
        message: kleur.red('An error occurred. Do you want to continue?'),
        initial: false
    });
    if (!response.continue) {
        process.exit(1);
    }
};

// コマンドを実行
const runCommand = async (command) => {
    try {
        const { stdout, stderr } = await execPromise(command);
        console.log(stdout);
        if (stderr) {
            console.error(kleur.red(stderr));
            await errPrompt();
        }
    } catch (error) {
        console.error(kleur.red(error.message));
        await errPrompt();
    }
};

// メイン関数
const main = async () => {
    try {
        const commands = await readFile(argv.file);
        let option = '';
        if (domain) option += ' -d ' + domain;
        if (username) option += ' -u ' + username;
        if (password) option += ' -p ' + password;
        if (waitTime) option += ' -t ' + waitTime;
        for (let index = 0; index < commands.length; index++) {
            const cmd = commands[index];
            console.log(' #'+(index+1)+':', cmd);
            const command = cmd + option;
            await runCommand(command);
        }
    } catch (error) {
        console.error(kleur.red(error.message));
    }
};

main(); // 開始
```