/* 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, }; }