/*
RFC-0221 ยง4 steps 6-8: materialize a handoff bundle into the current ecosystem โ
apply the migrator chain to the authored set, inject it into apps/<target>, then delegate
regeneration + validation to the standard build pipeline. Never round-trips generated files.
- Do not regenerate derived artifacts here โ that is delegated to site-kernel-codegen via the pipeline.
- Do not git-merge; injection overwrites authored files only, git is the rollback net.
- RFC-0221: materialization write-path (inject + migrators + delegated regen).
*/
import path from "node:path";
import type { HandoffManifest } from "@warpgogol/werkstatt-engine/schemas";
import type { AuthoredSet } from "./types.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const TEXT_EXT = new Set([
".md",
".mdx",
".ts",
".tsx",
".js",
".mjs",
".cjs",
".json",
".jsonc",
".css",
".yaml",
".yml",
".astro",
".txt",
".html",
".svg",
]);
function isTextPath(p: string): boolean {
return TEXT_EXT.has(path.extname(p).toLowerCase());
}
/** Strip the leading "site/" segment from a bundle-relative manifest path โ app-relative path. */
export function toAppRelative(bundlePath: string): string | null {
const norm = bundlePath.replace(/\\/g, "/");
if (!norm.startsWith("site/")) return null;
return norm.slice("site/".length);
}
export interface LoadedBundle {
/** App-relative text files (eligible for migrator transforms). */
authored: AuthoredSet;
/** App-relative binary files copied verbatim (bundle-absolute source path). */
binaries: Map;
}
/** Load the manifest's authored entries: text into memory, binaries tracked by source path. */
export async function loadAuthoredSet(
bundleDir: string,
manifest: HandoffManifest,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const authored: AuthoredSet = new Map();
const binaries = new Map();
for (const entry of manifest.entries) {
if (entry.kind !== "authored") continue;
const appRel = toAppRelative(entry.path);
if (!appRel) continue;
const abs = path.join(bundleDir, entry.path);
if (isTextPath(appRel)) {
authored.set(appRel, await io.readFile(abs));
} else {
binaries.set(appRel, abs);
}
}
return { authored, binaries };
}
/** Write the (migrated) authored set + binaries into the target app directory. */
export async function injectAuthoredSet(
targetDir: string,
loaded: LoadedBundle,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
let written = 0;
for (const [appRel, content] of loaded.authored) {
const dest = path.join(targetDir, appRel);
await io.mkdir(path.dirname(dest));
await io.writeFile(dest, content);
written++;
}
for (const [appRel, srcAbs] of loaded.binaries) {
const dest = path.join(targetDir, appRel);
await io.mkdir(path.dirname(dest));
await io.copyFile(srcAbs, dest);
written++;
}
return written;
}
async function runKernel(
workspaceRoot: string,
args: string[],
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const bin = path.join(workspaceRoot, "packages", "os", "site-kernel", "bin", "werkstatt.mjs");
try {
const result = await io.exec(process.execPath, [bin, ...args], { cwd: workspaceRoot });
return result.exitCode ?? 1;
} catch {
return 1;
}
}
async function runPnpm(
workspaceRoot: string,
args: string[],
io: WorkspaceIO = createDefaultIO().io,
): Promise {
try {
const result = await io.exec("pnpm", args, { cwd: workspaceRoot });
return result.exitCode ?? 1;
} catch {
return 1;
}
}
export interface RegenerationResult {
installCode: number;
prepareCode: number;
checkCode: number;
}
/**
* Delegate regeneration + validation to the standard pipelines for the app. When `install` is set
* (a brand-new app that is not yet a resolved workspace member), run `pnpm install` first so the
* app's dependencies and node_modules exist before regeneration/build.
*/
export async function runRegeneration(
workspaceRoot: string,
siteId: string,
options: { install?: boolean } = {},
): Promise {
const installCode = options.install ? await runPnpm(workspaceRoot, ["install"]) : 0;
if (installCode !== 0) return { installCode, prepareCode: -1, checkCode: -1 };
const prepareCode = await runKernel(workspaceRoot, [
"pipeline",
"build.prepare",
"--site",
siteId,
]);
if (prepareCode !== 0) return { installCode, prepareCode, checkCode: -1 };
const checkCode = await runKernel(workspaceRoot, ["pipeline", "build.check", "--site", siteId]);
return { installCode, prepareCode, checkCode };
}