/*
RFC-1065: lagebild.connect command — push 4 LAGEBILD_* secrets to a channel's Worker, health check, record state, merge into .env.
- Do not log the API key value — only the secret name appears in output.
- Do not write LAGEBILD_* to .env.example — that is a separate DNA-40 step.
- Do not block on health check failure — best-effort only.
- RFC-1065: initial lagebild.connect 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.
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import {
readSystemConfigSmart,
readSystemStateSmart,
writeSystemStateSmart,
resolveCacheClonePath,
} from "../sternsystem/registry-io.ts";
import { sourceDotenv, filterEnv } from "../leitstand/adapters/index.ts";
import { runWranglerSecretPut } from "../leitstand/adapters/wrangler-secrets.ts";
import { LAGEBILD_SECRET_NAMES, resolveWorkerName } from "./lagebild-helpers.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
function flagString(input: KernelCommandInput, name: string): string | undefined {
const v = input.flags[name];
return typeof v === "string" ? v : undefined;
}
interface SecretPushResult {
name: string;
pushed: boolean;
error?: string;
}
interface HealthCheckResult {
reachable: boolean;
statusCode?: number;
error?: string;
}
export async function runLagebildConnect(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise<
KernelCommandResult<{
site: string;
channel: string;
workerName: string;
secrets: SecretPushResult[];
healthCheck: HealthCheckResult;
stateRecorded: boolean;
}>
> {
const systemId = flagString(input, "site");
if (!systemId) throw new Error("[lagebild.connect] --site is required");
const apiUrl = flagString(input, "api-url");
if (!apiUrl) throw new Error("[lagebild.connect] --api-url is required");
const apiKey = flagString(input, "api-key");
if (!apiKey) throw new Error("[lagebild.connect] --api-key is required");
if (apiKey.length === 0) {
throw new Error("[lagebild.connect] --api-key must not be empty");
}
const tenantId = flagString(input, "tenant-id");
if (!tenantId) throw new Error("[lagebild.connect] --tenant-id is required");
const sourceSystemId = flagString(input, "source-system-id") ?? `site_${systemId}`;
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,
};
const secretValues: Record = {
LAGEBILD_API_URL: apiUrl,
LAGEBILD_API_KEY: apiKey,
LAGEBILD_TENANT_ID: tenantId,
LAGEBILD_SOURCE_SYSTEM_ID: sourceSystemId,
};
const secretResults: SecretPushResult[] = [];
for (const name of LAGEBILD_SECRET_NAMES) {
const result = await runWranglerSecretPut(
workerName,
name,
secretValues[name],
env,
workspaceRoot,
);
const sr: SecretPushResult = {
name,
pushed: result.exitCode === 0,
};
if (result.exitCode !== 0) {
sr.error = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
}
secretResults.push(sr);
}
// Best-effort health check (non-blocking)
let healthCheck: HealthCheckResult = { reachable: false };
try {
const healthUrl = apiUrl.endsWith("/") ? `${apiUrl}health` : `${apiUrl}/health`;
const response = await fetch(healthUrl, { method: "GET", signal: AbortSignal.timeout(5000) });
healthCheck = {
reachable: response.ok,
statusCode: response.status,
};
} catch (err) {
healthCheck = {
reachable: false,
error: err instanceof Error ? err.message : String(err),
};
}
// Write lagebild block to system-state.yaml
const state = await readSystemStateSmart(workspaceRoot, systemId);
state.lagebild = {
connected: true,
apiUrl,
tenantId,
sourceSystemId,
connectedAt: new Date().toISOString(),
channel,
};
await writeSystemStateSmart(workspaceRoot, systemId, state);
// Merge LAGEBILD_* values into .env file in cache clone
await mergeLagebildIntoEnv(cacheCloneDir, secretValues);
return {
data: {
site: systemId,
channel,
workerName,
secrets: secretResults,
healthCheck,
stateRecorded: true,
},
};
}
async function mergeLagebildIntoEnv(
cacheCloneDir: string,
values: Record,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const envPath = join(cacheCloneDir, ".env");
let lines: string[] = [];
if (await io.exists(envPath)) {
const raw = await io.readFile(envPath);
lines = raw.split("\n");
}
const lagebildKeys = new Set(Object.keys(values));
const filtered = lines.filter((line) => {
const trimmed = line.trim();
if (trimmed.startsWith("#")) return true;
const eqIdx = trimmed.indexOf("=");
if (eqIdx === -1) return true;
const key = trimmed.substring(0, eqIdx).trim();
return !lagebildKeys.has(key);
});
// Remove trailing empty lines
while (filtered.length > 0 && filtered[filtered.length - 1].trim() === "") {
filtered.pop();
}
if (filtered.length > 0) {
filtered.push("");
}
for (const name of LAGEBILD_SECRET_NAMES) {
filtered.push(`${name}=${values[name]}`);
}
filtered.push("");
await io.writeFile(envPath, filtered.join("\n"));
}