ken_nogi/ClaudePleasanter/助っ人クラウド/apply-site-config.js
Kenichiro NOGI 67cc3da6c8 up
2026-07-13 19:02:04 +09:00

157 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* apply-site-config.js
* ------------------------------------------------------------
* ★★★ このスクリプトは常にドライランです。実際のAPI送信は一切行いません。★★★
*
* Claudeが生成した desired_config.json希望構成を読み込み、
* 1. 現状構成configs/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, "configs", `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("--- 送信されるはずのBodyAPIキーはマスク表示 ---");
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();