/*
RFC-0964: fleet.sites.generate — generates fleet/fleet.sites.yaml from
discoverSystems output (RFC-0790 convention). The registry is a truthful
generated projection of systems-cache/{id}/system-config.yaml — no second
source of truth (DNA-45). Moved from packages/werkstatt-site/src/checks/
to packages/werkstatt-engine/src/fleet/ so fleet.apply can call it for
self-healing without engine→site imports (DNA-64).
- Do not read or modify fleet status, plan, or killswitch state.
- Do not hardcode site names — derive entirely from convention-based discovery.
- RFC-0378: initial implementation of fleet.sites.generate command (in werkstatt-site).
- RFC-0964: moved from werkstatt-site to werkstatt-engine, repointed from discoverSiteWorkspaces to discoverSystems, enriched output with platformVersion, channels, mirrors, activeMission, canary, path fields.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1284: fallback system state literal gains accessPinSuspended: null
*/
import { dirname, join, relative } from "node:path";
import { stringify as yamlStringify } from "yaml";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-shared/kernel";
import { buildGeneratedHeader, stripGeneratedMarker } from "../kernel/generated-marker.ts";
import { writeFileAtomic } from "@warpgogol/werkstatt-shared/kernel";
import {
discoverSystems,
resolveCacheClonePath,
readSystemState,
} from "../sternsystem/registry-io.ts";
import type { SystemConfig } from "@warpgogol/werkstatt-shared/ontology/operations";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const FLEET_SITES_FILE = "fleet/fleet.sites.yaml";
export interface FleetSiteRecord {
id: string;
path: string;
platformVersion: string;
channels: Record<"dev" | "alt" | "main", { releaseId: string | null }>;
mirrors: { count: number; protocols: string[] };
activeMission: string | null;
canary: boolean;
}
interface FleetSitesFile {
sites: FleetSiteRecord[];
}
function extractMirrorProtocols(config: SystemConfig): string[] {
const protocols = new Set();
for (const mirror of config.mirrors) {
let proto = "file";
if (mirror.path.includes("://")) {
proto = new URL(mirror.path).protocol.replace(":", "");
} else if (/^[^\s/]+@[^\s/]+:/.test(mirror.path)) {
// SCP-style remote (git@github.com:org/repo.git) — SSH transport, not
// parseable by new URL().
proto = "ssh";
}
protocols.add(proto);
}
return [...protocols].sort();
}
async function buildFleetSiteRecord(
workspaceRoot: string,
config: SystemConfig,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const cacheClonePath = resolveCacheClonePath(workspaceRoot, config.id);
const relPath = relative(workspaceRoot, cacheClonePath).replace(/\\/g, "/");
let platformVersion = "unknown";
try {
const pinRaw = await io.readFile(join(cacheClonePath, "system.pin.json"));
const pin = JSON.parse(pinRaw) as { platform?: { version?: string } };
platformVersion = pin.platform?.version ?? "unknown";
} catch {
// pin may not exist yet for registered-but-not-pinned systems
}
let state: Awaited>;
try {
state = await readSystemState(workspaceRoot, config.id);
} catch {
state = {
schemaVersion: "1",
systemId: config.id,
currentMission: null,
lastRelease: null,
lastPropagated: {},
accessPin: null,
accessPinSuspended: null,
passportRequired: false,
ownershipRequired: false,
};
}
const channels: FleetSiteRecord["channels"] = {
dev: { releaseId: state.lastPropagated.dev?.releaseId ?? null },
alt: { releaseId: state.lastPropagated.alt?.releaseId ?? null },
main: { releaseId: state.lastPropagated.main?.releaseId ?? null },
};
return {
id: config.id,
path: relPath,
platformVersion,
channels,
mirrors: {
count: config.mirrors.length,
protocols: extractMirrorProtocols(config),
},
activeMission: state.currentMission,
canary: config.fleet?.canary ?? false,
};
}
export async function runFleetSitesGenerate(
_input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise {
const { workspaceRoot } = context;
const { systems, errors } = await discoverSystems(workspaceRoot);
const records: FleetSiteRecord[] = [];
for (const config of systems) {
records.push(await buildFleetSiteRecord(workspaceRoot, config));
}
records.sort((a, b) => a.id.localeCompare(b.id));
const fleetSites: FleetSitesFile = { sites: records };
const outputPath = join(workspaceRoot, FLEET_SITES_FILE);
await context.io.mkdir(dirname(outputPath));
const header = buildGeneratedHeader({
filePath: FLEET_SITES_FILE,
ownerCommand: "fleet.sites.generate",
});
const body = yamlStringify(fleetSites);
const content = `${header}\n${body}`;
await writeFileAtomic(outputPath, content);
return {
exitCode: 0,
summary: `[fleet.sites.generate] wrote ${records.length} site(s) to ${FLEET_SITES_FILE}`,
data: {
command: "fleet.sites.generate",
sites: records,
errors,
written: FLEET_SITES_FILE,
},
};
}
export async function validateFleetSitesDrift(
workspaceRoot: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise<{ drifted: boolean; expected: string; actual: string }> {
const { systems } = await discoverSystems(workspaceRoot);
const records: FleetSiteRecord[] = [];
for (const config of systems) {
records.push(await buildFleetSiteRecord(workspaceRoot, config));
}
records.sort((a, b) => a.id.localeCompare(b.id));
const expectedContent = yamlStringify({ sites: records });
const filePath = join(workspaceRoot, FLEET_SITES_FILE);
if (!(await io.exists(filePath))) {
return { drifted: true, expected: expectedContent, actual: "" };
}
const raw = await io.readFile(filePath);
const { content: actualContent } = stripGeneratedMarker(raw);
return {
drifted: actualContent.trim() !== expectedContent.trim(),
expected: expectedContent,
actual: actualContent,
};
}