/** * extract-site-config.js * ------------------------------------------------------------ * 取得済みのサイト構成 (configs/site-{SiteId}_latest.json 等) から、 * Scripts / Styles / ServerScripts / Processes / ガイドHTML を * 個別ファイルとして configs/site-{SiteId}_{サイト名}/ 配下に書き出します。 * ClaudePleasanter直下に置く共通実装。全プロジェクトの薄いラッパーから require される。 * * 呼び出し元プロジェクトのルート・レイアウトは環境変数で受け取る: * PLEASANTER_PROJECT_ROOT … 必須。プロジェクトルートの絶対パス * PLEASANTER_LAYOUT … "flat" | 未設定(既定=hierarchical) * 抽出先(configs/site-{SiteId}_{サイト名}/scripts 等)はlayoutによらず共通。 * layoutは生JSON(latest.json)の探索元パスの解決にのみ影響する。 * * 使い方: * node extract-site-config.js * node extract-site-config.js ./configs/site-214812_latest.json * * 出力: * configs/site-{SiteId}_{サイト名}/ * manifest.json ← 抽出内容の一覧(build-desired-config.jsでの再構成に使用) * scripts/{Id}_{Title}.js * styles/{Id}_{Title}.css * serverscripts/{Id}_{Title}.js * guide/{GuideName}.html ← GridGuide等、ガイド(サイトデータ直下、値が空でないもののみ) * html/{ColumnName}.html ← 項目ごとの補足HTML(SiteSettings.Columns[].ExtendedHtmlAfterControl) * processes.json ← プロセス設定(SiteSettings.Processes をそのまま保存) * * 注意: * 「guide」(GridGuide等)と「html」(ExtendedHtmlAfterControl)は別物。 * Pleasanter用語としての「Html」はColumns配下の項目ごとの補足HTMLを指し、 * GridGuide等の画面ガイド文言とは異なるフィールド。旧実装ではガイドHTMLの保存先が * 誤って"html"フォルダになっていたため、guide/htmlに分離した(2026-08-16修正)。 * ガイド・項目ごとの補足HTMLはいずれもSiteSettingsではなく、サイトデータ直下 * (ガイド)/Columns配下(Html)の項目です。反映時はサイト設定の部分更新 * (updatesitesettings)ではなく、サイト全体更新(updatesite / Mode:"full")が必要になります。 * Scripts/Styles/ServerScripts/Processesは部分更新(Mode:"partial")で反映できます。 * 詳しくは build-desired-config.js の実行結果メッセージを参照してください。 * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { sanitize, ensureSiteDir, findLatestJsonPath } = require("./site-paths"); const { resolveProjectRoot, loadSiteId } = require("./resolve-project"); function getBaseDir() { return resolveProjectRoot().baseDir; } function getEnv() { return resolveProjectRoot().env; } function getLayout() { const v = process.env.PLEASANTER_LAYOUT; return v === "flat" || v === "separate" ? v : "hierarchical"; } function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function extractList(items, outDir, ext, bodyKey = "Body") { const manifestEntries = []; if (!items || items.length === 0) return manifestEntries; ensureDir(outDir); for (const item of items) { const fileName = `${item.Id}_${sanitize(item.Title || item.Name)}${ext}`; fs.writeFileSync(path.join(outDir, fileName), item[bodyKey] || "", "utf-8"); const { [bodyKey]: _omit, ...meta } = item; manifestEntries.push({ ...meta, File: fileName }); } return manifestEntries; } const GUIDE_FIELDS = [ "GridGuide", "EditorGuide", "CalendarGuide", "CrosstabGuide", "GanttGuide", "BurnDownGuide", "TimeSeriesGuide", "AnalyGuide", "KambanGuide", "ImageLibGuide", ]; // get-site-config.js からも呼び出せるよう関数化。 // CLI実行時(直接 `node extract-site-config.js`)は末尾のブロックが引数解決を行う。 function extractSiteConfig(latestPath) { const baseDir = getBaseDir(); if (!fs.existsSync(latestPath)) { console.error(`[エラー] ファイルが見つかりません: ${latestPath}`); console.error("先に node get-site-config.js を実行してください。"); process.exit(1); } const json = JSON.parse(fs.readFileSync(latestPath, "utf-8")); const data = json?.Response?.Data; if (!data) { console.error("[エラー] Response.Data が見つかりません。getsiteのレスポンス形式を確認してください。"); process.exit(1); } const siteId = data.SiteId; const settings = data.SiteSettings || {}; const configsDir = path.join(baseDir, "configs", getEnv()); const outRoot = ensureSiteDir(configsDir, siteId, data.Title); const manifest = { SiteId: siteId, Title: data.Title, ExtractedAt: new Date().toISOString(), SourceFile: path.relative(baseDir, latestPath).replace(/\\/g, "/"), }; manifest.Scripts = extractList(settings.Scripts, path.join(outRoot, "scripts"), ".js"); manifest.Styles = extractList(settings.Styles, path.join(outRoot, "styles"), ".css"); manifest.ServerScripts = extractList(settings.ServerScripts, path.join(outRoot, "serverscripts"), ".js"); // ガイド(サイトデータ直下。SiteSettingsではない点に注意) const guideDir = path.join(outRoot, "guide"); const guideEntries = []; for (const field of GUIDE_FIELDS) { const value = data[field]; if (value) { ensureDir(guideDir); const fileName = `${field}.html`; fs.writeFileSync(path.join(guideDir, fileName), value, "utf-8"); guideEntries.push({ Field: field, File: fileName, TopLevel: true }); } } manifest.Guide = guideEntries; // Html(項目ごとの補足HTML。SiteSettings.Columns[].ExtendedHtmlAfterControl) const htmlDir = path.join(outRoot, "html"); const htmlEntries = []; for (const col of settings.Columns || []) { const value = col.ExtendedHtmlAfterControl; if (value) { ensureDir(htmlDir); const fileName = `${sanitize(col.ColumnName)}.html`; fs.writeFileSync(path.join(htmlDir, fileName), value, "utf-8"); htmlEntries.push({ ColumnName: col.ColumnName, File: fileName }); } } manifest.Html = htmlEntries; // プロセス設定 … 個別分割せず1つの定義ファイルとして保存 fs.writeFileSync( path.join(outRoot, "processes.json"), JSON.stringify(settings.Processes || [], null, 2), "utf-8" ); manifest.ProcessesFile = "processes.json"; manifest.ProcessesCount = (settings.Processes || []).length; fs.writeFileSync(path.join(outRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8"); console.log(`[OK] 抽出完了: ${outRoot}`); console.log(` Scripts : ${manifest.Scripts.length}件`); console.log(` Styles : ${manifest.Styles.length}件`); console.log(` ServerScripts : ${manifest.ServerScripts.length}件`); console.log(` Guide : ${manifest.Guide.length}件`); console.log(` Html : ${manifest.Html.length}件`); console.log(` Processes : ${manifest.ProcessesCount}件 → processes.json`); console.log("\n次のステップ: 各ファイルを直接編集するか、Claudeに修正内容を指示してください。"); console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${path.basename(outRoot)}`); return manifest; } // CLIとして直接実行された場合のみ、引数からlatestPathを解決して実行する if (require.main === module) { const { baseDir, env, argv } = resolveProjectRoot(); let latestPath = argv[0]; if (!latestPath) { const siteId = loadSiteId(baseDir); const layout = getLayout(); const configsDir = path.join(baseDir, "configs", env); const resolved = findLatestJsonPath(baseDir, configsDir, siteId, layout, env); latestPath = resolved || path.join(configsDir, `site-${siteId}_latest.json`); } extractSiteConfig(latestPath); } module.exports = { extractSiteConfig };