/** * get-site-config.js * ------------------------------------------------------------ * プリザンターの指定サイトの構成情報(SiteSettings等)を取得し、 * {プロジェクトフォルダ}/configs/{env}/ 配下にタイムスタンプ付きJSONとして保存します。 * ClaudePleasanter直下に置く共通実装。ClaudePleasanter直下から直接実行する * (対象プロジェクトは --project、サーバー環境は --env で指定。詳細は resolve-project.js 参照)。 * * 使い方: * node .claude/js/get-site-config.js --project="IC(東京)【引継依頼】" --env=production * node .claude/js/get-site-config.js --project=IC東京 --env=development * * レイアウト(configs/{env}/の保存構造)は環境変数で切り替え可能: * PLEASANTER_LAYOUT … "flat" | 未設定(既定=hierarchical) * * 事前準備: * 1. リポジトリルート直下の config_production.json / config_development.json に * BaseUrl / ApiKey / ApiVersion(サーバー接続情報)が設定済みであること。 * 2. 対象プロジェクトフォルダ直下の siteid.json に SiteId を設定してください。 * SiteId はカンマ区切りで複数指定可能です(例: "480111,438166,96235")。 * 1件のみの場合は従来通り数値(例: 480111)でも構いません。 * 3. Node.js 18以降(組み込みfetchを使用) * * 参照した公式マニュアル: * - 開発者向け機能:API:サイト操作:サイト更新 * https://pleasanter.org/manual/api-site-update * - 開発者向け機能:API:サイト操作:サイト設定の更新(部分追加/更新/削除) * https://pleasanter.org/ja/manual/api-update-sitesettings * * 注意: * プリザンターのバージョンにより get のレスポンス構造 * (SiteSettingsが Response.Site 配下にあるか等)が異なる場合があります。 * 初回実行時はコンソールに出力されるレスポンスのトップレベルキーを確認し、 * 想定通りの構造か確認してください。 * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { extractSiteConfig } = require("./extract-site-config"); const { rawJsonSaveDir } = require("./site-paths"); const { resolveProjectRoot, loadServerConfig, loadSiteId } = require("./resolve-project"); let _resolved = null; function getResolved() { if (!_resolved) _resolved = resolveProjectRoot(); return _resolved; } function getBaseDir() { return getResolved().baseDir; } function getEnv() { return getResolved().env; } function getLayout() { const v = process.env.PLEASANTER_LAYOUT; return v === "flat" || v === "separate" ? v : "hierarchical"; } // config.json の SiteId(数値 / カンマ区切り文字列 / 配列)を // 個々のSiteId文字列の配列に正規化する。 function parseSiteIds(rawSiteId) { const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(","); return list .map((id) => String(id).trim()) .filter((id) => id.length > 0); } // 1サイト分の取得・保存・抽出処理 async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) { const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`; const body = { ApiVersion: ApiVersion || "1.1", ApiKey: ApiKey, }; console.log(`[INFO] 取得先: ${url}`); console.log(`[INFO] SiteId: ${siteId}`); let response; try { response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } catch (err) { console.error("[エラー] リクエスト送信に失敗しました:", err.message); console.error("BaseUrlの疎通・ネットワーク設定を確認してください。"); return { ok: false }; } const text = await response.text(); if (!response.ok) { console.error(`[エラー] HTTP ${response.status} ${response.statusText}`); console.error(text); return { ok: false }; } let json; try { json = JSON.parse(text); } catch (err) { console.error("[エラー] レスポンスがJSONとして解析できませんでした。"); console.error(text); return { ok: false }; } // レスポンス構造の確認用ログ(初回はここを見て構造を把握してください) console.log("[INFO] レスポンス トップレベルキー:", Object.keys(json)); if (json.Response) { console.log("[INFO] Response配下のキー:", Object.keys(json.Response)); } const baseDir = getBaseDir(); const env = getEnv(); const layout = getLayout(); const configsDir = path.join(baseDir, "configs", env); const title = json?.Response?.Data?.Title; const outDir = rawJsonSaveDir(baseDir, configsDir, siteId, title, layout, env); const timestamp = new Date() .toISOString() .replace(/[:.]/g, "-"); const outFile = path.join(outDir, `site-${siteId}_${timestamp}.json`); fs.writeFileSync(outFile, JSON.stringify(json, null, 2), "utf-8"); // 常に「最新」ファイルも別名で保持(Claudeへのアップロード用に固定名があると便利) const latestFile = path.join(outDir, `site-${siteId}_latest.json`); fs.writeFileSync(latestFile, JSON.stringify(json, null, 2), "utf-8"); console.log(`[OK] 保存しました: ${outFile}`); console.log(`[OK] 最新版としても保存しました: ${latestFile}`); // 取得と同時に Scripts/Styles/ServerScripts/Processes/ガイドHTML も抽出する console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します..."); extractSiteConfig(latestFile); // 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、 // 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。 const inheritPermission = json?.Response?.Data?.InheritPermission; return { ok: true, inheritPermission }; } async function main() { const { baseDir } = getResolved(); const env = getEnv(); const { BaseUrl, ApiKey, ApiVersion } = loadServerConfig(env); const SiteId = loadSiteId(baseDir); const siteIds = parseSiteIds(SiteId); if (siteIds.length === 0) { console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。"); process.exit(1); } // 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を // 指していれば、そのサイトも同じ要領でキューに追加して取得する // (権限の実体は継承元サイド側にあるため)。 const queue = [...siteIds]; const visited = new Set(); const results = []; while (queue.length > 0) { const siteId = String(queue.shift()); if (visited.has(siteId)) continue; visited.add(siteId); console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`); const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }); results.push({ siteId, ok }); if ( ok && inheritPermission != null && String(inheritPermission) !== "0" && String(inheritPermission) !== siteId && !visited.has(String(inheritPermission)) ) { console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`); queue.push(String(inheritPermission)); } } console.log("\n==== 取得結果まとめ ===="); for (const { siteId, ok } of results) { console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`); } console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、"); console.log("configs/site-{SiteId}/ 配下のファイルを直接編集・指示して修正してください。"); if (results.some((r) => !r.ok)) { process.exit(1); } } main();