const fs = require("fs"); const path = require("path"); const { spawnSync } = require("child_process"); const repoRoot = path.join(__dirname, "..", ".."); const dataDir = path.join(repoRoot, "data"); const updateCacheDir = path.join(dataDir, "update-cache"); const managedRepoDir = path.join(updateCacheDir, "repo"); const updateStatePath = path.join(dataDir, "update-state.json"); const DEFAULT_REPOSITORY = "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi"; const PRESERVED_PATHS = [ "data", "config", "storage", "uploads", "logs", "database", "databases", "plugins", "knowledge/community", "knowledge/corrections", ".env", ".env.local", ".env.production", ".secrets", "codex-guidelines" ]; function runGit(args, options = {}) { const result = spawnSync("git", args, { cwd: options.cwd || repoRoot, encoding: "utf8", timeout: options.timeout || 120000 }); if (result.status !== 0) { throw new Error((result.stderr || result.stdout || "Git command failed.").trim()); } return result.stdout.trim(); } function tryGit(args, options = {}, fallback = "") { try { return runGit(args, options); } catch { return fallback; } } function isRepositoryUrl(value) { return /^(?:https?:\/\/|ssh:\/\/|git@)/i.test(String(value || "")); } function normalizeRepositoryTarget(value) { const target = String(value || DEFAULT_REPOSITORY).trim() || DEFAULT_REPOSITORY; if (isRepositoryUrl(target)) { return target.replace(/\.git\/?$/i, "").replace(/\/+$/, ""); } return target; } function isGitRepository(directory = repoRoot) { return tryGit(["rev-parse", "--is-inside-work-tree"], { cwd: directory }, "") === "true"; } function readUpdateState() { try { const state = JSON.parse(fs.readFileSync(updateStatePath, "utf8")); return state && typeof state === "object" ? state : {}; } catch { return {}; } } function writeUpdateState(values = {}) { fs.mkdirSync(dataDir, { recursive: true }); const current = readUpdateState(); const next = { schema_version: 1, preserve_paths: PRESERVED_PATHS, managed_repo_path: managedRepoDir, updated_at: new Date().toISOString(), ...current, ...values }; fs.writeFileSync(updateStatePath, JSON.stringify(next, null, 2), "utf8"); return next; } function resolveRepositoryRemote(value) { const target = normalizeRepositoryTarget(value); if (isRepositoryUrl(target)) { return target; } const state = readUpdateState(); if (isRepositoryUrl(state.remote)) { return normalizeRepositoryTarget(state.remote); } if (isGitRepository(managedRepoDir)) { const cached = tryGit(["remote", "get-url", target], { cwd: managedRepoDir }, ""); if (cached) return normalizeRepositoryTarget(cached); const origin = tryGit(["remote", "get-url", "origin"], { cwd: managedRepoDir }, ""); if (origin) return normalizeRepositoryTarget(origin); } if (isGitRepository(repoRoot)) { const live = tryGit(["remote", "get-url", target], { cwd: repoRoot }, ""); if (live) return normalizeRepositoryTarget(live); } if (target === "origin") { return DEFAULT_REPOSITORY; } throw new Error("Configure Git remote / repository target as a repository URL for ZIP-origin installs."); } function resolveSourceBranch(remote, requested = "stable") { const repository = resolveRepositoryRemote(remote); if (requested === "experimental") { const refs = runGit(["ls-remote", "--heads", repository, "experimental-*"], { timeout: 60000 }); const branches = refs .split(/\r?\n/) .map((line) => line.trim().split(/\s+/)[1] || "") .map((ref) => ref.replace(/^refs\/heads\//, "")) .filter(Boolean) .sort((a, b) => b.localeCompare(a)); if (branches[0]) return branches[0]; return "experimental"; } if (requested && requested !== "stable" && requested !== "main") { return String(requested).replace(/^origin\//, ""); } return "main"; } function migrateZipEraLayout(remote) { fs.mkdirSync(updateCacheDir, { recursive: true }); const state = readUpdateState(); if (state.migrated_zip_layout_at) { return state; } return writeUpdateState({ migrated_zip_layout_at: new Date().toISOString(), migration: "zip-era-layout", remote: resolveRepositoryRemote(remote), live_install_git_repository: isGitRepository(repoRoot) }); } function resetManagedRepo(repository) { fs.rmSync(managedRepoDir, { recursive: true, force: true }); fs.mkdirSync(updateCacheDir, { recursive: true }); runGit(["clone", repository, managedRepoDir], { cwd: updateCacheDir, timeout: 300000 }); } function ensureManagedRepo(remote, branch) { const repository = resolveRepositoryRemote(remote); migrateZipEraLayout(repository); fs.mkdirSync(updateCacheDir, { recursive: true }); if (!isGitRepository(managedRepoDir)) { resetManagedRepo(repository); } else { const currentRemote = normalizeRepositoryTarget( tryGit(["remote", "get-url", "origin"], { cwd: managedRepoDir }, "") ); if (currentRemote && currentRemote !== repository) { runGit(["remote", "set-url", "origin", repository], { cwd: managedRepoDir }); } else if (!currentRemote) { resetManagedRepo(repository); } } runGit(["fetch", "--prune", "origin"], { cwd: managedRepoDir, timeout: 300000 }); runGit(["checkout", "-B", branch, `origin/${branch}`], { cwd: managedRepoDir, timeout: 120000 }); runGit(["reset", "--hard", `origin/${branch}`], { cwd: managedRepoDir, timeout: 120000 }); runGit(["clean", "-fdx"], { cwd: managedRepoDir, timeout: 120000 }); writeUpdateState({ remote: repository, branch, managed_repo_path: managedRepoDir, last_fetch_at: new Date().toISOString(), live_install_git_repository: isGitRepository(repoRoot) }); return { repository, branch, path: managedRepoDir }; } function createMetadataReader(remote, branch) { const managed = ensureManagedRepo(remote, branch); return { repository: managed.repository, branch: managed.branch, root: managed.path, readFile(filePath) { const target = path.resolve(managed.path, filePath); if (!target.startsWith(managed.path + path.sep) && target !== managed.path) { return null; } try { return fs.readFileSync(target, "utf8"); } catch { return null; } }, listPluginDirs() { const pluginsPath = path.join(managed.path, "plugins"); try { return fs.readdirSync(pluginsPath, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name); } catch { return []; } }, cleanup() {} }; } module.exports = { DEFAULT_REPOSITORY, PRESERVED_PATHS, updateCacheDir, managedRepoDir, updateStatePath, runGit, isRepositoryUrl, isGitRepository, normalizeRepositoryTarget, resolveRepositoryRemote, resolveSourceBranch, migrateZipEraLayout, ensureManagedRepo, createMetadataReader, readUpdateState, writeUpdateState };