Initial commit — SvelteKit fleet manager for T5 Plutonium servers

Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app
that manages a fleet of T5 Plutonium servers running on Komodo. Same push
and beta workflow as the shell script, with a live SSE pipeline view, a
!rt-token-gated auth modal that hard-pauses deployments on stale tokens,
per-file audit diffs, and a Komodo-container deployment target.

- Push/beta engines port the exact filter, held-by-beta, conflict, and
  stale-backup rules from the shell script
- SQLite (better-sqlite3) replaces script_metadata.json; a one-shot
  importer moves legacy state over on first boot
- 16 vitest tests cover push add/update/skip, beta open→rollback,
  open→promote, conflict guards, sweep frozen/manual branches, and IW4M
  login/status parsing
- docker/ + komodo/stack.toml deploy as a managed container on the same
  host as the game servers, /root/zombies bind-mounted at the same path
  the shell script used
This commit is contained in:
2026-09-12 00:23:51 -04:00
commit d99711563e
67 changed files with 9415 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
import { createHash } from 'node:crypto';
import { readdir, readFile, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, posix, relative, sep } from 'node:path';
import { betaSrc, config } from '../config.js';
// Ported from push_scripts.sh:414-464. These are the exact filter rules —
// change one and the parity check against the shell script will fail.
const EXCLUDE_NAMES_EXACT = new Set(['.git']);
const EXCLUDE_SUFFIXES = ['.swp', '.OLD'];
const EXCLUDE_TRAILING = ['~'];
function isExcluded(name: string): boolean {
if (EXCLUDE_NAMES_EXACT.has(name)) return true;
if (name.startsWith('.')) return false; // dotfiles allowed except .git
for (const s of EXCLUDE_SUFFIXES) if (name.endsWith(s)) return true;
for (const t of EXCLUDE_TRAILING) if (name.endsWith(t)) return true;
// *-beta.* — script line 417 excludes any file with "-beta." in its name
if (/-beta\./.test(name)) return true;
return false;
}
function toPosix(p: string): string {
return p.split(sep).join(posix.sep);
}
async function walk(root: string, prune: (rel: string) => boolean): Promise<string[]> {
const out: string[] = [];
async function recur(dir: string): Promise<void> {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const abs = join(dir, e.name);
const rel = toPosix(relative(root, abs));
if (prune(rel)) continue;
if (e.isDirectory()) {
await recur(abs);
} else if (e.isFile()) {
if (isExcluded(e.name)) continue;
out.push(rel);
}
}
}
await recur(root);
return out.sort();
}
/**
* Every file under SHARED_SCRIPTS except the betas/ tree.
* Mirrors `list_source_files` in push_scripts.sh:414.
*/
export async function listSourceFiles(): Promise<string[]> {
return walk(config.sharedScripts, (rel) => rel === 'betas' || rel.startsWith('betas/'));
}
/**
* Every file directly in betas/ (one level deep, ignore subfolders — those
* are open betas). Mirrors `staged_files` in push_scripts.sh:420.
*/
export async function stagedFiles(): Promise<string[]> {
if (!existsSync(betaSrc())) return [];
let entries;
try {
entries = await readdir(betaSrc(), { withFileTypes: true });
} catch {
return [];
}
return entries
.filter((e) => e.isFile() && !isExcluded(e.name) && !e.name.startsWith('.'))
.map((e) => e.name)
.sort();
}
/**
* Find every source file whose basename matches `base`.
* Mirrors `find_targets` in push_scripts.sh:426.
*/
export async function findTargets(base: string): Promise<string[]> {
const all = await listSourceFiles();
return all.filter((rel) => posix.basename(rel) === base);
}
/**
* `foo/bar.gsc` -> `foo/bar-beta.gsc`
* Mirrors `beta_file` in push_scripts.sh:450.
*/
export function betaFile(rel: string): string {
const dir = posix.dirname(rel);
const base = posix.basename(rel);
const dot = base.lastIndexOf('.');
const stem = dot === -1 ? base : base.slice(0, dot);
const ext = dot === -1 ? '' : base.slice(dot);
const named = `${stem}-beta${ext}`;
return dir === '.' ? named : `${dir}/${named}`;
}
/**
* `foo/bar.gsc` -> `foo/bar.OLD`
* Mirrors `old_file` in push_scripts.sh:458.
*/
export function oldFile(rel: string): string {
const dir = posix.dirname(rel);
const base = posix.basename(rel);
const dot = base.lastIndexOf('.');
const stem = dot === -1 ? base : base.slice(0, dot);
const named = `${stem}.OLD`;
return dir === '.' ? named : `${dir}/${named}`;
}
/**
* Slugify a beta name the same way the shell script does.
* Mirrors `slugify` in push_scripts.sh:399.
*/
export function slugify(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function sha256(path: string): Promise<string> {
const buf = await readFile(path);
return createHash('sha256').update(buf).digest('hex');
}
export async function sha256OrNull(path: string): Promise<string | null> {
try {
await stat(path);
} catch {
return null;
}
return sha256(path);
}