/** * apply-site-config.js * ------------------------------------------------------------ * ★★★ このスクリプトは常にドライランです。実際のAPI送信は一切行いません。★★★ * * Claudeが生成した desired_config.json(希望構成)を読み込み、 * 1. 現状構成(siteSettingJsons/site-{SiteId}_latest.json)との差分表示 * 2. 実際に送信されるはずの HTTPリクエスト(URL / Body)の表示 * 3. そのまま使えるcurlコマンドの出力 * のみを行います。実行(fetch送信)は行いません。 * * 内容を確認した上で、問題なければ、 * - 表示されたcurlコマンドを手動で実行する * - または、このファイル末尾の "実送信ブロック" のコメントを * 自分の判断で外して実行する * のいずれかで反映してください。 * * desired_config.json の形式: * { * "Mode": "partial", // "partial" = updatesitesettings(部分更新) / "full" = updatesite(全体更新) * "SiteId": 12345, // 省略時は config.json の値を使用 * "SiteSettings": { ... }, // 変更したい項目のみ(partialの場合)/ 全体(fullの場合) * "Title": "...", // full更新時のみ必要な場合あり * "ReferenceType": "...", * "ParentId": ..., * "InheritPermission": ... * } * * 参照した公式マニュアル: * - サイト更新(全体): https://pleasanter.org/manual/api-site-update * URL: POST {BaseUrl}/api/items/{SiteId}/updatesite * - サイト設定の部分更新: https://pleasanter.org/ja/manual/api-update-sitesettings * URL: POST {BaseUrl}/api/items/{SiteId}/updatesitesettings * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); function loadJson(p, required = true) { if (!fs.existsSync(p)) { if (required) { console.error(`[エラー] ファイルが見つかりません: ${p}`); process.exit(1); } return null; } return JSON.parse(fs.readFileSync(p, "utf-8")); } // ざっくりした差分表示(キー単位・トップ1階層+SiteSettings1階層まで) // partialモード(updatesitesettings)は desired 側にあるキーだけがサーバーに送られ、 // 記載のないキーは現状のまま変わらない。そのため比較対象は afterKeys のみに限定する // (beforeにしかないキーを「削除される」ように見せるのは誤り)。 // fullモード(updatesite)はSiteSettings全体を丸ごと置き換えるため、beforeKeysも含めて比較する。 function diffTopLevel(before, after, label, mode = "partial") { if (!before) { console.log(` (現状データなし。get-site-config.jsで先に取得しておくと差分表示できます)`); return; } const afterKeys = new Set(Object.keys(after || {})); const allKeys = mode === "full" ? new Set([...Object.keys(before || {}), ...afterKeys]) : afterKeys; let changed = false; for (const key of allKeys) { const b = JSON.stringify(before?.[key]); const a = JSON.stringify(after?.[key]); if (b !== a) { changed = true; console.log(` [変更] ${label}.${key}`); console.log(` - 現状: ${truncate(b)}`); console.log(` + 希望: ${truncate(a)}`); } } if (!changed) console.log(" 差分なし"); } function truncate(str, max = 200) { if (str === undefined) return "(undefined)"; return str.length > max ? str.slice(0, max) + "...(略)" : str; } function maskApiKey(key) { if (!key) return key; return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****"; } async function main() { const baseDir = __dirname; const configPath = path.join(baseDir, "config.json"); const desiredPath = process.argv[2] || path.join(baseDir, "desired_config.json"); const config = loadJson(configPath); const desired = loadJson(desiredPath); const siteId = desired.SiteId || config.SiteId; const mode = desired.Mode || "partial"; const endpoint = mode === "full" ? "updatesite" : "updatesitesettings"; const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/${endpoint}`; const latestConfigPath = path.join(baseDir, "siteSettingJsons", `site-${siteId}_latest.json`); const current = loadJson(latestConfigPath, false); const currentSiteSettings = current?.Response?.Data?.SiteSettings || current?.Response?.Site?.SiteSettings || current?.Response?.SiteSettings || null; console.log("========================================"); console.log(" ドライラン結果(実際の送信は行いません)"); console.log("========================================"); console.log(`Mode : ${mode} (${mode === "full" ? "全体更新" : "部分更新"})`); console.log(`URL : ${url}`); console.log(`SiteId : ${siteId}`); console.log(""); console.log("--- SiteSettings 差分(現状 → 希望) ---"); diffTopLevel(currentSiteSettings, desired.SiteSettings, "SiteSettings", mode); console.log(""); const body = { ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey, ...desired, }; delete body.Mode; // Mode はエンドポイント選択にのみ使用し、送信Bodyには含めない const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) }; console.log("--- 送信されるはずのBody(APIキーはマスク表示) ---"); console.log(JSON.stringify(maskedBody, null, 2)); console.log(""); console.log("--- 手動実行用 curl コマンド(内容確認の上、必要ならご自身で実行してください) ---"); console.log( `curl -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(body)}'` ); console.log(""); console.log("[注意] このスクリプトはAPIへの送信を一切行っていません(ドライラン専用)。"); console.log("内容に問題がなければ、上記curlコマンドを手動実行するか、"); console.log("スクリプト内の「実送信ブロック」を有効化する改修をご自身の判断で行ってください。"); // ------------------------------------------------------------ // 実送信ブロック(デフォルトでは絶対に到達しないようにガードしています) // 意図的に反映を自動化したい場合のみ、下のガードを外して使ってください。 // ------------------------------------------------------------ const ENABLE_ACTUAL_SEND = false; // ← 安全のため常にfalse固定。変更は自己責任で。 if (ENABLE_ACTUAL_SEND) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); console.log(await res.text()); } } main();