Initial import

This commit is contained in:
Andraxion 2026-06-26 18:18:14 -04:00
commit ab891a315c
773 changed files with 257255 additions and 0 deletions

134
scripts/clean-workspace.mjs Normal file
View file

@ -0,0 +1,134 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const removableDirs = [
"dist",
"Current",
"tmp",
".codex-logs",
];
const removableFiles = [
".codex-api.err.log",
".codex-api.log",
".codex-api.out.log",
".codex-server.err.log",
".codex-server.out.log",
".codex-vite.err.log",
".codex-vite.out.log",
".codex-web.log",
".tmp-map-editor-api.err.log",
".tmp-map-editor-api.log",
".tmp-map-editor-vite.err.log",
".tmp-map-editor-vite.log",
];
const backupRetentionByPrefix = new Map([
["maps", 5],
["npcs", 5],
["tiles", 8],
["sprites", 5],
]);
function toAbsolute(relativePath) {
return path.join(projectRoot, relativePath);
}
function safeRemoveDir(relativePath, removed) {
const absolutePath = toAbsolute(relativePath);
if (!fs.existsSync(absolutePath)) {
return;
}
try {
fs.rmSync(absolutePath, { recursive: true, force: true });
removed.push(relativePath);
} catch {
// Ignore locked runtime folders so one open handle does not block the rest of cleanup.
}
}
function safeRemoveFile(relativePath, removed) {
const absolutePath = toAbsolute(relativePath);
if (!fs.existsSync(absolutePath)) {
return;
}
try {
fs.rmSync(absolutePath, { force: true });
removed.push(relativePath);
} catch {
// Ignore locked runtime files so cleanup can continue.
}
}
function parseBackupGroup(fileName) {
const match = /^([a-z_]+)-\d{4}-\d{2}-\d{2}T.*\.json$/i.exec(fileName);
return match ? match[1].toLowerCase() : null;
}
function pruneBackups() {
const backupsDir = toAbsolute("backups");
const removed = [];
if (!fs.existsSync(backupsDir)) {
return removed;
}
const files = fs.readdirSync(backupsDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
.map((entry) => ({
name: entry.name,
path: path.join(backupsDir, entry.name),
group: parseBackupGroup(entry.name),
stat: fs.statSync(path.join(backupsDir, entry.name)),
}));
const grouped = new Map();
files.forEach((file) => {
if (!file.group) {
return;
}
if (!grouped.has(file.group)) {
grouped.set(file.group, []);
}
grouped.get(file.group).push(file);
});
grouped.forEach((groupFiles, group) => {
const keepCount = backupRetentionByPrefix.get(group) ?? 3;
const sorted = groupFiles.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
sorted.slice(keepCount).forEach((file) => {
try {
fs.rmSync(file.path, { force: true });
removed.push(path.join("backups", file.name));
} catch {
// Ignore any locked backup files.
}
});
});
return removed;
}
function main() {
const removedDirs = [];
const removedFiles = [];
removableDirs.forEach((dir) => safeRemoveDir(dir, removedDirs));
removableFiles.forEach((file) => safeRemoveFile(file, removedFiles));
const removedBackups = pruneBackups();
const summary = {
removedDirs,
removedFiles,
removedBackups,
};
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
}
main();

31
scripts/deploy-vps.ps1 Normal file
View file

@ -0,0 +1,31 @@
param(
[string]$Remote = "vps",
[string]$Branch = ""
)
$ErrorActionPreference = "Stop"
function Step($message) {
Write-Host ""
Write-Host "==> $message" -ForegroundColor Cyan
}
if (-not $Branch) {
$Branch = (git branch --show-current).Trim()
}
if (-not $Branch) {
throw "Could not determine the current git branch. Pass -Branch explicitly."
}
Step "Validating content"
npm run validate:content
Step "Building project"
npm run build
Step "Pushing $Branch to $Remote"
git push $Remote $Branch
Write-Host ""
Write-Host "Deploy push complete." -ForegroundColor Green

View file

@ -0,0 +1,141 @@
import fs from "fs";
import path from "path";
import { createRequire } from "module";
import { fileURLToPath } from "url";
const require = createRequire(import.meta.url);
const Ajv = require("ajv");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const contentRoot = path.join(repoRoot, "content");
const schemaRoot = path.join(contentRoot, "schema");
const ajv = new Ajv({
allErrors: true,
schemaId: "auto",
jsonPointers: true,
});
function normalizePath(filePath) {
return filePath.split(path.sep).join("/");
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function listFilesRecursive(dirPath) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...listFilesRecursive(fullPath));
} else {
files.push(fullPath);
}
}
return files;
}
const jobs = [
{
schema: "abilities.schema.json",
files: [path.join(contentRoot, "abilities.json")],
},
{
schema: "dialogues.schema.json",
files: [path.join(contentRoot, "dialogues.json")],
},
{
schema: "factions.schema.json",
files: [path.join(contentRoot, "factions.json")],
},
{
schema: "images.schema.json",
files: [path.join(contentRoot, "images.json")],
},
{
schema: "items.schema.json",
files: [path.join(contentRoot, "items.json")],
},
{
schema: "loot_tables.schema.json",
files: [path.join(contentRoot, "loot_tables.json")],
},
{
schema: "monsters.schema.json",
files: [path.join(contentRoot, "monsters.json")],
},
{
schema: "npc_templates.schema.json",
files: [path.join(contentRoot, "npc_templates.json")],
},
{
schema: "quests.schema.json",
files: [path.join(contentRoot, "quests.json")],
},
{
schema: "worlds.schema.json",
files: [path.join(contentRoot, "worlds.json")],
},
{
schema: "world.schema.json",
files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => path.basename(filePath) === "world.json"),
},
{
schema: "world-bookmarks.schema.json",
files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => path.basename(filePath) === "bookmarks.json"),
},
{
schema: "world-chunk.schema.json",
files: listFilesRecursive(path.join(contentRoot, "worlds")).filter((filePath) => /^-?\d+_-?\d+\.json$/i.test(path.basename(filePath))),
},
{
schema: "dev_config.schema.json",
files: [path.join(contentRoot, "dev_config.json")],
},
{
schema: "npcs.schema.json",
files: fs.existsSync(path.join(contentRoot, "npcs.json")) ? [path.join(contentRoot, "npcs.json")] : [],
},
];
let hasErrors = false;
let validatedCount = 0;
for (const job of jobs) {
const schemaPath = path.join(schemaRoot, job.schema);
const schema = readJson(schemaPath);
let validate;
try {
validate = ajv.compile(schema);
} catch (error) {
hasErrors = true;
console.error(`Schema compile failed: ${normalizePath(path.relative(repoRoot, schemaPath))}`);
console.error(String(error));
continue;
}
for (const filePath of job.files) {
const payload = readJson(filePath);
const valid = validate(payload);
validatedCount += 1;
if (!valid) {
hasErrors = true;
console.error(`Validation failed: ${normalizePath(path.relative(repoRoot, filePath))}`);
for (const error of validate.errors || []) {
const instancePath = error.dataPath || "/";
console.error(` ${instancePath} ${error.message}`);
}
}
}
}
if (hasErrors) {
process.exitCode = 1;
} else {
console.log(`Validated ${validatedCount} content file(s) successfully.`);
}

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/srv/content-editor-v2/app"
GIT_DIR="/srv/content-editor-v2/repo.git"
CONTENT_ROOT="/srv/content-editor-v2/shared/content"
PORT="5180"
APP_NAME="content-editor-v2"
echo "[deploy] checkout"
git --work-tree="$APP_DIR" --git-dir="$GIT_DIR" checkout -f
cd "$APP_DIR"
echo "[deploy] install"
npm install
echo "[deploy] validate"
npm run validate:content
echo "[deploy] build"
npm run build
echo "[deploy] restart"
CONTENT_ROOT="$CONTENT_ROOT" PORT="$PORT" pm2 restart "$APP_NAME" || \
CONTENT_ROOT="$CONTENT_ROOT" PORT="$PORT" pm2 start server.js --name "$APP_NAME"