/*
RFC-0844: workpiece.config.presence.check command handler — pre-build gate
that verifies all OPERATOR_CONFIG_FILES entries are present in the active
mission's workpiece directory before the build pipeline starts. Returns
pass/fail status with restore commands for missing files.
- Do not validate file contents — only presence (existsSync).
- Do not make the check fatal if the command itself throws in mission.validate — follow non-fatal pattern from RFC-0813.
- Do not add new files to OPERATOR_CONFIG_FILES — that requires a superseding RFC.
- RFC-0844: initial workpiece.config.presence.check command handler.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1254: drop unused io-migration leftovers
*/
import path from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { OPERATOR_CONFIG_FILES } from "./operator-config-files.ts";
import { resolveMissionDir } from "./mission-io.ts";
export interface WorkpieceConfigPresenceResult {
command: "workpiece.config.presence.check";
status: "pass" | "fail";
missionId: string;
missing: Array<{
file: string;
restoreCommand: string;
}>;
present: string[];
}
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
function resolveSystemId(missionId: string): string {
const match = missionId.match(/^(.+)-m\d+$/);
return match ? match[1] : missionId;
}
function buildRestoreCommand(entry: string, systemId: string, missionId: string): string {
const cachePath = `../systems-cache/${systemId}/${entry}`;
const workpiecePath = `missions/${missionId}/workpiece/${entry}`;
const targetDir = path.dirname(`missions/${missionId}/workpiece/${entry}`);
if (entry.includes("/")) {
return `mkdir -p ${targetDir} && cp ${cachePath} ${workpiecePath}`;
}
return `cp ${cachePath} ${workpiecePath}`;
}
export async function runWorkpieceConfigPresenceCheck(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const missionId = flagString(input, "mission");
if (!missionId) throw new Error("[workpiece.config.presence.check] --mission is required");
const missionDir = await resolveMissionDir(workspaceRoot, missionId, context.io);
const workpieceDir = path.join(missionDir, "workpiece");
if (!(await context.io.exists(workpieceDir))) {
return {
exitCode: 1,
summary: `[workpiece.config.presence.check] Workpiece directory not found: missions/${missionId}/workpiece/`,
nextSteps: [
{
action: `[workpiece.config.presence.check] Workpiece directory not found: missions/${missionId}/workpiece/`,
kind: "required" as const,
},
],
data: {
command: "workpiece.config.presence.check",
status: "fail",
missionId,
missing: [],
present: [],
},
};
}
const systemId = resolveSystemId(missionId);
const missing: Array<{ file: string; restoreCommand: string }> = [];
const present: string[] = [];
for (const entry of OPERATOR_CONFIG_FILES) {
const fullPath = path.join(workpieceDir, entry);
if (await context.io.exists(fullPath)) {
present.push(entry);
} else {
missing.push({
file: entry,
restoreCommand: buildRestoreCommand(entry, systemId, missionId),
});
}
}
const status: WorkpieceConfigPresenceResult["status"] = missing.length > 0 ? "fail" : "pass";
return {
data: {
command: "workpiece.config.presence.check",
status,
missionId,
missing,
present,
},
exitCode: missing.length > 0 ? 1 : 0,
summary:
missing.length > 0
? `[workpiece.config.presence.check] Missing operator config files: ${missing.map((m) => m.file).join(", ")}`
: "[workpiece.config.presence.check] All operator config files present",
};
}