/** * site-paths.js * ------------------------------------------------------------ * configs/ 配下のフォルダ構成を一箇所で管理する共有ヘルパー。 * ClaudePleasanter直下に置く共通実装。全プロジェクトの .claude/js/ 配下 * (またはプロジェクト直下)にある薄いラッパーから require される。 * * baseDir(プロジェクトルート)は各スクリプトの main() が * process.env.PLEASANTER_PROJECT_ROOT(ラッパーが設定)から受け取る。 * このファイル自体はbaseDir解決を行わず、呼び出し側から渡されたconfigsDirを * そのまま使う(layout非依存の純粋なパス組み立てヘルパー)。 * * レイアウトは2種類: * hierarchical(既定): configs/site-{SiteId}_{サイト名}/sitesettings/ に生JSONを保存 * flat : configs/ 直下に site-{SiteId}_latest.json をフラットに保存 * (抽出済みファイル configs/site-{SiteId}_{サイト名}/scripts/ 等は * レイアウトによらず共通) * layoutは各プロジェクトのラッパーが PLEASANTER_LAYOUT 環境変数("flat"|未設定=hierarchical) * で明示指定する。既存configsフォルダの実際の保存構造を変えないための切り替えであり、 * 自動判定はしない。 * * 構成(hierarchicalの場合): * configs/ * master/ … ユーザー/組織/グループのマスターデータ(get-master-data.js) * site-{SiteId}_{サイト名}/ * sitesettings/ … getsiteの生JSON(site-{SiteId}_latest.json / _{timestamp}.json) * scripts/ styles/ serverscripts/ html/ processes.json / manifest.json * … extract-site-config.js による分割済みファイル(従来通り) * docs/ … 仕様書(.md)や改善要望プラン等、sitesettings以外の関連ドキュメント * modify/{request}/ … 修正依頼ごとに新規作成。変更前後のconfigファイルや差分・結果を保管 * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); function sanitize(name) { return ( String(name || "untitled") .replace(/[\\/:*?"<>|]/g, "_") .trim() .slice(0, 60) || "untitled" ); } function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); return dir; } // configs/ 配下から "site-{siteId}_*"(前方一致)または "site-{siteId}"(完全一致)の // フォルダを探す。見つからなければ null。(抽出済みファイルの置き場所はlayoutによらず共通) function findSiteDir(configsDir, siteId) { if (!fs.existsSync(configsDir)) return null; const bare = `site-${siteId}`; const prefix = `site-${siteId}_`; const candidates = fs.readdirSync(configsDir).filter((name) => { const full = path.join(configsDir, name); return fs.statSync(full).isDirectory() && (name === bare || name.startsWith(prefix)); }); const suffixed = candidates.filter((n) => n !== bare); if (suffixed.length >= 1) return path.join(configsDir, suffixed[0]); if (candidates.includes(bare)) return path.join(configsDir, bare); return null; } // 既存フォルダがあればそれを使い、無ければ site-{siteId}_{sanitize(title)} を新規作成する function ensureSiteDir(configsDir, siteId, title) { const existing = findSiteDir(configsDir, siteId); if (existing) return existing; const dir = path.join(configsDir, `site-${siteId}_${sanitize(title)}`); return ensureDir(dir); } function sitesettingsDir(siteDir) { return ensureDir(path.join(siteDir, "sitesettings")); } function docsDir(siteDir) { return ensureDir(path.join(siteDir, "docs")); } // 修正依頼用の新規フォルダを作成する(labelを省略した場合はタイムスタンプを使う) function newModifyDir(siteDir, label) { const name = label || new Date().toISOString().replace(/[:.]/g, "-"); return ensureDir(path.join(siteDir, "modify", name)); } // 既存のmodifyフォルダの中で最も新しいもの(フォルダ名の降順ソート)を返す。無ければnull。 function latestModifyDir(siteDir) { const modifyRoot = path.join(siteDir, "modify"); if (!fs.existsSync(modifyRoot)) return null; const dirs = fs .readdirSync(modifyRoot) .filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory()) .sort(); if (dirs.length === 0) return null; return path.join(modifyRoot, dirs[dirs.length - 1]); } // サイトの最新getsiteダンプ(site-{siteId}_latest.json)のパスを返す(hierarchicalレイアウト用)。 // フォルダ自体が無い場合は null。 function latestJsonPath(configsDir, siteId) { const siteDir = findSiteDir(configsDir, siteId); if (!siteDir) return null; return path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`); } // 生JSON(getsiteダンプ)の置き場所は、プロジェクトごとに3パターンある // (既存configsフォルダの実際の保存構造を変えないための切り替え。自動判定はしない): // hierarchical(既定): configs/site-{siteId}_{title}/sitesettings/ // flat : configs/ 直下にフラット保存(サイト名フォルダを作らない) // separate : baseDir/siteSettingJsons/ 直下にフラット保存(configs/とは別のトップレベルフォルダ) // 抽出済みファイル(scripts/styles/等)の置き場所 configs/site-{siteId}_{title}/ 自体は // どのlayoutでも共通(extract-site-config.js等はconfigsDirをそのまま使えばよい)。 // 生JSON保存先ディレクトリを解決する(保存前。フォルダが無ければ作成する)。 // baseDir: プロジェクトルート。configsDir: baseDir/configs/{env}(呼び出し元でenvを織り込み済み)。 // env: "production"|"development"。hierarchical/flatはconfigsDir側で分離済みのため未使用、 // separateのみ baseDir/siteSettingJsons/{env}/ として直接envを使う(configsDirを経由しないため)。 function rawJsonSaveDir(baseDir, configsDir, siteId, title, layout, env) { if (layout === "flat") { return ensureDir(configsDir); } if (layout === "separate") { return ensureDir(path.join(baseDir, "siteSettingJsons", env)); } const siteDir = ensureSiteDir(configsDir, siteId, title); return sitesettingsDir(siteDir); } // 既存サイトの生JSON latestファイルパスを layout に応じて解決する(参照用、無ければnull)。 function findLatestJsonPath(baseDir, configsDir, siteId, layout, env) { if (layout === "flat") { const p = path.join(configsDir, `site-${siteId}_latest.json`); return fs.existsSync(p) ? p : null; } if (layout === "separate") { const p = path.join(baseDir, "siteSettingJsons", env, `site-${siteId}_latest.json`); return fs.existsSync(p) ? p : null; } return latestJsonPath(configsDir, siteId); } module.exports = { sanitize, ensureDir, findSiteDir, ensureSiteDir, sitesettingsDir, docsDir, newModifyDir, latestModifyDir, latestJsonPath, rawJsonSaveDir, findLatestJsonPath, };