kintone プラグインアップローダー互換対応

[kintone プラグインアップロード処理](https://qiita.com/rex0220/items/ea321d0573b83607ab2c) の動作が問題ないので、プラグインアップローダー互換対応してみました。
アップロードまでの待ち時間短縮とアップロード失敗が無くなり、作業中のストレスがかなり減ります。

# 概要

プラグインアップローダーの代わりに、REST API でプラグインアップロードするように変更。

- watch オプション追加
    - プラグインパッケージ処理が実行するたびに、プラグインアップロードする
プラグインファイル変更の監視処理追加
- waittime オプション追加
    - 1回目のプラグインパッケージ処理が完了するまでの待ち時間

最新版は、[@rex0220/kintone-plugin-uploader](https://github.com/rex0220/kintone-plugin-uploader)

※npm パッケージ公開しました。

https://www.npmjs.com/package/@rex0220/kintone-plugin-uploader

## 問題点

- プラグインのインストールと変更をプラグインID の有無で判断しているため、複数の kintone 環境に対応できない

REST API で、プラグインID無しで更新ができると解決するのだが...
暫定対応としては、「とりあえず更新処理してエラーになったらインストール処理する」にした。

## 実行ログ

最初のビルド・アップロードと2回目のビルド・アップロードまで。

```.log
> npm start

> narrow-down@0.1.0 start
> node scripts/npm-start.js


> narrow-down@0.1.0 develop-prod
> npm run build-prod -- --watch


> narrow-down@0.1.0 upload-prod
> node scripts/plugin-uploader.js --file dist/narrow-down-plugin24.zip --watch --waittime 3000


> narrow-down@0.1.0 build-prod
> kintone-plugin-packer --ppk private.ppk --out dist/narrow-down-plugin24.zip prod --watch

Succeeded: dist/narrow-down-plugin24.zip
Uploading file...
File uploaded successfully. fileKey: 8704efb1-5cfd-477d-bdd8-aa4e81653084
Plugin updated successfully. { id: 'gbjnadbngbniopdeibcjihdkhkcinljf', version: '24' }

Succeeded: dist/narrow-down-plugin24.zip
Uploading file...
File uploaded successfully. fileKey: 58bb630f-95ce-4ba3-8781-670752e0e6cb
Plugin updated successfully. { id: 'gbjnadbngbniopdeibcjihdkhkcinljf', version: '24' }

```

# package.json

オリジナルのプラグインアップローダーからREST API版のプラグインアップローダーに変更

```package.json
"scripts": {
    // オリジナルのプラグインアップローダー
    "upload-prod": "kintone-plugin-uploader dist/narrow-down-plugin24.zip --watch --waiting-dialog-ms 3000",

    // REST API版のプラグインアップローダー
    "upload-prod": "node scripts/plugin-uploader.js --file dist/narrow-down-plugin24.zip --watch --waittime 3000",
    }
```

# plugin-uploader.js オプション

help オプションで表示

```
> node scripts/plugin-uploader.js -h                                                                          
オプション:
      --version   バージョンを表示                                        [真偽]
  -d, --domain    kintone domain                                        [文字列]
  -u, --username  kintone username                                      [文字列]
  -p, --password  kintone password                                      [文字列]
  -f, --file      The path of the plugin file.                   [文字列] [必須]
  -w, --watch     Watch the plugin file.              [真偽] [デフォルト: false]
  -t, --waittime  waittime[ms].                           [数値] [デフォルト: 0]
  -i, --id        plugin ID.                           [文字列] [デフォルト: ""]
  -h, --help      ヘルプを表示                                            [真偽]
```

# 環境変数

- KINTONE_DOMAIN: kintone domain
- KINTONE_USERNAME: kintone username
- KINTONE_PASSWORD: kintone password

# plugin-uploader.js

```plugin-uploader.js
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
const path = require('path');
const yargs = require('yargs');
const { pid } = require('process');

// プラグインIDファイルパス
const pluginIdPath = 'pluginId.txt';

// 環境変数を取得します
const envDomain = process.env.KINTONE_DOMAIN;
const envUsername = process.env.KINTONE_USERNAME;
const envPassword = process.env.KINTONE_PASSWORD;

// 実行時パラメータを取得します
const argv = yargs
    .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('file', {
        alias: 'f',
        description: 'The path of the plugin file.',
        type: 'string',
        demandOption: true
    })
    .option('watch', {
        alias: 'w',
        description: 'Watch the plugin file.',
        type: 'boolean',
        default: false
    })
    .option('waittime', {
        alias: 't',
        description: 'waittime[ms].',
        type: 'number',
        default: 0
    })
    .option('id', {
        alias: 'i',
        description: 'plugin ID.',
        type: 'string',
        default: ''
    })
    .help()
    .alias('help', 'h')
    .argv;

// パラメータを変数に格納します
const subdomain = argv.domain || envDomain;
const username = argv.username || envUsername;
const password = argv.password || envPassword;
const pluginFilePath = argv.file;
const watchMode = argv.watch;
const waitTime = argv.waittime;
const pPluginId = argv.id;

// 環境変数またはコマンドライン引数が不足している場合のチェック
if (!subdomain || !username || !password || !pluginFilePath) {
    console.error('Domain, username, password, and file are required.');
    process.exit(1);
}

// プラグインIDの書き込み
function writePluginID(content) {
    try {
        const rawData = fs.writeFileSync(pluginIdPath, content, 'utf8');
    } catch (error) {
        console.error('Could not write pluginId file:', error.message);
        return '';
    }
}

// プラグインIDをロード
function loadPluginID() {
    try {
        const rawData = fs.readFileSync(pluginIdPath, 'utf8');
        return rawData;
    } catch (error) {
        console.error('Could not load pluginId file:', error.message);
        return '';
    }
}

// プラグインをアップロードする非同期関数
async function uploadPlugin() {
    try {
        // プラグインファイルを読み込み、FormDataオブジェクトを作成します
        const form = new FormData();
        form.append('file', fs.createReadStream(pluginFilePath));


        // KintoneのファイルアップロードAPIエンドポイント
        const fileUploadUrl = `https://${subdomain}/k/v1/file.json`;

        // ヘッダーに基本認証とFormDataのヘッダーを設定します
        const fileUploadHeaders = {
            'X-Cybozu-Authorization': Buffer.from(`${username}:${password}`).toString('base64'),
            ...form.getHeaders(),
        };

        const pluginId = pPluginId || loadPluginID();

        // ファイルをアップロードしてfileKeyを取得します
        console.log('Uploading file...');
        const fileUploadResponse = await axios.post(fileUploadUrl, form, { headers: fileUploadHeaders });
        const fileKey = fileUploadResponse.data.fileKey;
        console.log('File uploaded successfully. fileKey:', fileKey);

        // Kintoneのプラグイン読み込みAPIエンドポイント
        const pluginUploadUrl = `https://${subdomain}/k/v1/plugin.json`;

        // ヘッダーに基本認証を設定します
        const pluginUploadHeaders = {
            'X-Cybozu-Authorization': Buffer.from(`${username}:${password}`).toString('base64'),
            'Content-Type': 'application/json'
        };

        // プラグイン読み込みリクエストのペイロード
        const payload = {
            fileKey: fileKey
        };

        if (pluginId) {
            // プラグインの更新
            try {
                payload.id = pluginId;
                const respUpload = await axios.put(pluginUploadUrl, payload, { headers: pluginUploadHeaders });
                console.log('Plugin updated successfully.', respUpload.data);
                console.log('');
                return;
            }
            catch(error) {
                // console.error('An error has occurred.');
                // console.error(error.response ? error.response.data : error.message);
                // 未インストールの場合、インストール処理へ
            }
        }

        // プラグインのインストール
        const respUpload = await axios.post(pluginUploadUrl, payload, { headers: pluginUploadHeaders });
        console.log('Plugin added successfully.', respUpload.data);
        if (!pPluginId) writePluginID(respUpload.data.id);
        console.log('');

    } catch (error) {
        console.error('An error has occurred.');
        console.error(error.response ? error.response.data : error.message);
    }
}

// ファイルの変更を監視する関数
function watchFile(file) {
    fs.watchFile(file, (curr, prev) => {
        // プラグインをアップロード
        uploadPlugin();
    });
}

// 待ち時間
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// プラグインをアップロード
async function main() {
    if (waitTime > 0) {
        await delay(waitTime);
    }

    await uploadPlugin();
    if (watchMode) {
        watchFile(pluginFilePath);
    }
}

main();
```