259 lines
7.0 KiB
JavaScript
259 lines
7.0 KiB
JavaScript
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
|
|
};
|