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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user