Drift now covers all three destinations

This commit is contained in:
2026-09-12 19:16:14 -04:00
parent 8c40bbeff1
commit 6ae9004ed2
3 changed files with 85 additions and 22 deletions
+32 -20
View File
@@ -1,17 +1,22 @@
import { readdir, readFile, rm, stat } from 'node:fs/promises';
import { join, posix, relative, sep } from 'node:path';
import { destOf } from '../config.js';
import { destOfKind, type ArtifactKind } from '../config.js';
import { db } from '../db/index.js';
import { emit } from '../pipeline/bus.js';
import { sha256Buffer } from './files.js';
import { listServerScripts, removeScript } from './inventory.js';
import { allServers, listServers } from './servers.js';
// Destinations drift scans, in a stable order so the UI shows scripts first.
// Baseline shares the config destination — one physical dir, two logical kinds.
const DRIFT_DESTS: ReadonlyArray<ArtifactKind> = ['script', 'config', 'plugin'];
export interface DriftEntry {
server: string;
relPath: string;
size: number;
sha256: string;
kind: ArtifactKind;
}
function toPosix(p: string): string {
@@ -38,28 +43,35 @@ async function walkFiles(root: string): Promise<string[]> {
}
/**
* Scan one server's sp/ tree and return every file that isn't tracked in
* server_scripts. This includes user-placed scripts and leftovers from
* before Electric Cherry started tracking inventory. Beta artifacts
* (-beta.gsc, .OLD) are tracked while a beta is active so they don't
* appear as drift.
* Scan every managed destination tree on a server (scripts, configs, plugins)
* and return every file that isn't tracked in server_scripts. Beta artifacts
* (-beta.gsc, .OLD, -beta.dll, composed dedicated_sp.cfg) are tracked while
* a beta is active so they don't appear as drift.
*
* The inventory table (server_scripts) keys on (server, rel_path) with no
* destination discriminator. Rel_paths across destinations don't collide in
* practice (`.gsc` for scripts, `.cfg` for configs, `.dll` for plugins), so
* a shared tracked-set is safe.
*/
export async function scanServerDrift(server: string): Promise<DriftEntry[]> {
const dest = destOf(server);
const onDisk = await walkFiles(dest);
if (onDisk.length === 0) return [];
const tracked = new Set(listServerScripts(server).map((r) => r.rel_path));
const drift: DriftEntry[] = [];
for (const relPath of onDisk) {
if (tracked.has(relPath)) continue;
const abs = join(dest, relPath);
try {
const st = await stat(abs);
const buf = await readFile(abs);
drift.push({ server, relPath, size: st.size, sha256: sha256Buffer(buf) });
} catch {
/* file disappeared between walk and stat — skip */
for (const kind of DRIFT_DESTS) {
const dest = destOfKind(server, kind);
const onDisk = await walkFiles(dest);
if (onDisk.length === 0) continue;
for (const relPath of onDisk) {
if (tracked.has(relPath)) continue;
const abs = join(dest, relPath);
try {
const st = await stat(abs);
const buf = await readFile(abs);
drift.push({ server, relPath, size: st.size, sha256: sha256Buffer(buf), kind });
} catch {
/* file disappeared between walk and stat — skip */
}
}
}
return drift;
@@ -157,8 +169,8 @@ export async function runDriftDeployment(input: RunDriftInput = {}): Promise<Run
const affected: string[] = [];
for (const s of perServer) {
if (s.entries.length === 0) continue;
const dest = destOf(s.server);
for (const entry of s.entries) {
const dest = destOfKind(s.server, entry.kind);
const abs = join(dest, entry.relPath);
try {
const content = await readFile(abs);
+2 -2
View File
@@ -5,10 +5,10 @@ import { kick } from '$lib/server/pipeline/worker.js';
// GET returns the current drift plan for the whole fleet without touching disk.
export async function GET() {
const entries = await scanFleetDrift();
const byServer = new Map<string, Array<{ relPath: string; size: number; sha256: string }>>();
const byServer = new Map<string, Array<{ relPath: string; size: number; sha256: string; kind: string }>>();
for (const e of entries) {
const arr = byServer.get(e.server) ?? [];
arr.push({ relPath: e.relPath, size: e.size, sha256: e.sha256 });
arr.push({ relPath: e.relPath, size: e.size, sha256: e.sha256, kind: e.kind });
byServer.set(e.server, arr);
}
return json({