/*
RFC-1065: lagebild.validate command — check 4 LAGEBILD_* secrets via wrangler secret list, verify .env key presence, best-effort GET /health.
- Do not use the API key for authenticated requests — GET /health only.
- Do not block on health check failure — best-effort, non-blocking.
- Do not modify system-state.yaml.
- RFC-1065: initial lagebild.validate command handler.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import {
readSystemConfigSmart,
readSystemStateSmart,
resolveCacheClonePath,
} from "../sternsystem/registry-io.ts";
import { filterEnv, sourceDotenv } from "../leitstand/adapters/index.ts";
import { LAGEBILD_SECRET_NAMES, resolveWorkerName } from "./lagebild-helpers.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { failResult } from "@warpgogol/werkstatt-shared/checks/result-helpers";
function flagString(input: KernelCommandInput, name: string): string | undefined {
const v = input.flags[name];
return typeof v === "string" ? v : undefined;
}
interface SecretCheckResult {
name: string;
present: boolean;
}
interface HealthCheckResult {
reachable: boolean;
statusCode?: number;
error?: string;
}
async function runWranglerSecretList(
workerName: string,
env: Record,
cwd: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
try {
const envFiltered = Object.fromEntries(
Object.entries(env).filter((e): e is [string, string] => e[1] !== undefined),
);
const result = await io.exec(
"npx",
["--yes", "wrangler", "secret", "list", "--name", workerName],
{ cwd, env: envFiltered },
);
const listed: { exitCode: number; stdout: string; stderr: string } = {
exitCode: result.exitCode ?? 1,
stdout: result.stdout,
stderr: result.stderr,
};
return listed;
} catch {
const failed: { exitCode: number; stdout: string; stderr: string } = {
exitCode: 1,
stdout: "",
stderr: "Failed to spawn wrangler",
};
return failed;
}
}
export async function runLagebildValidate(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise<
KernelCommandResult<{
site: string;
channel: string;
workerName: string;
secrets: SecretCheckResult[];
allSecretsPresent: boolean;
apiKeyInEnv: boolean;
apiReachable: HealthCheckResult;
state: { connected: boolean; apiUrl?: string; channel?: string };
}>
> {
const systemId = flagString(input, "site");
if (!systemId) throw new Error("[lagebild.validate] --site is required");
const channel = flagString(input, "channel") ?? "main";
const workspaceRoot = context.workspaceRoot;
let systemConfig;
try {
systemConfig = await readSystemConfigSmart(workspaceRoot, systemId);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes("ENOENT") || msg.includes("no such file")) {
throw new Error(`Site '${systemId}' not found`);
}
throw err;
}
const workerName = resolveWorkerName(systemConfig, channel);
const cacheCloneDir = resolveCacheClonePath(workspaceRoot, systemId);
const envPath = join(cacheCloneDir, ".env");
const secretsEnv = (await context.io.exists(envPath)) ? await sourceDotenv(envPath) : {};
const env: Record = {
...filterEnv(process.env as Record),
...secretsEnv,
};
// Spawn wrangler secret list
const listResult = await runWranglerSecretList(workerName, env, workspaceRoot);
// Parse secret names from wrangler output
// wrangler secret list outputs lines like: "SECRET_NAME 2024-01-01T00:00:00Z"
const presentSecrets = new Set();
if (listResult.exitCode === 0) {
for (const line of listResult.stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
// First column is the secret name
const name = trimmed.split(/\s+/)[0];
if (name) presentSecrets.add(name);
}
}
const secretChecks: SecretCheckResult[] = LAGEBILD_SECRET_NAMES.map((name) => ({
name,
present: presentSecrets.has(name),
}));
const allSecretsPresent = secretChecks.every((s) => s.present);
// Check .env for LAGEBILD_API_KEY presence
let apiKeyInEnv = false;
if (await context.io.exists(envPath)) {
const rawEnv = await context.io.readFile(envPath);
for (const line of rawEnv.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("LAGEBILD_API_KEY=") && trimmed.length > "LAGEBILD_API_KEY=".length) {
apiKeyInEnv = true;
break;
}
}
}
// Read state for API URL
const state = await readSystemStateSmart(workspaceRoot, systemId);
const lagebildState = state.lagebild;
// Best-effort GET /health (non-blocking)
let apiReachable: HealthCheckResult = { reachable: false };
if (lagebildState?.apiUrl) {
try {
const healthUrl = lagebildState.apiUrl.endsWith("/")
? `${lagebildState.apiUrl}health`
: `${lagebildState.apiUrl}/health`;
const response = await fetch(healthUrl, {
method: "GET",
signal: AbortSignal.timeout(5000),
});
apiReachable = {
reachable: response.ok,
statusCode: response.status,
};
} catch (err) {
apiReachable = {
reachable: false,
error: err instanceof Error ? err.message : String(err),
};
}
}
if (!allSecretsPresent) {
return {
...failResult("lagebild.validate", [
`Missing secrets: ${secretChecks
.filter((s) => !s.present)
.map((s) => s.name)
.join(", ")}`,
]),
data: {
site: systemId,
channel,
workerName,
secrets: secretChecks,
allSecretsPresent: false,
apiKeyInEnv,
apiReachable,
state: {
connected: lagebildState?.connected ?? false,
apiUrl: lagebildState?.apiUrl,
channel: lagebildState?.channel,
},
},
};
}
return {
data: {
site: systemId,
channel,
workerName,
secrets: secretChecks,
allSecretsPresent: true,
apiKeyInEnv,
apiReachable,
state: {
connected: lagebildState?.connected ?? false,
apiUrl: lagebildState?.apiUrl,
channel: lagebildState?.channel,
},
},
};
}