/*
Facilitates discovery and configuration loading for kernel applications within a workspace.
- Do not handle application runtime or execution logic.
- Do not parse raw content from configuration files.
- Do not manage application dependencies or orchestration.
- Workspace discovery must remain read-only and avoid app-specific path assumptions outside explicit adapters.
- 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 path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { tsImport } from "tsx/esm/api";
import type { DiscoveredSiteWorkspace, KernelAppConfig } from "@warpgogol/werkstatt-shared/kernel";
import { discoverSiteWorkspaces as discoverSitesFromResolver } from "./site-workspace-resolver.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
// @ai-invariant: Workspace discovery must remain read-only and avoid app-specific path assumptions outside explicit adapters.
function unwrapConfig(moduleNamespace: Record): unknown {
let candidate: unknown = moduleNamespace.default ?? moduleNamespace.config;
for (let depth = 0; depth < 8; depth++) {
if (
candidate &&
typeof candidate === "object" &&
"default" in (candidate as Record)
) {
const inner = (candidate as Record).default;
if (inner && typeof inner === "object") {
candidate = inner;
continue;
}
}
break;
}
return candidate;
}
const APP_CONFIG_FILENAMES = [
"kernel.config.ts",
"kernel.config.mts",
"kernel.config.js",
"kernel.config.mjs",
] as const;
async function resolveAppConfigPath(
toolsDirectory: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
for (const filename of APP_CONFIG_FILENAMES) {
const filePath = path.join(toolsDirectory, filename);
if (await io.exists(filePath)) {
return filePath;
}
}
return undefined;
}
/**
* tsx's CJS interop can fail under nested loader environments (vitest): a `.js`
* dep's `require("node:fs")` gets a `?tsx-namespace=` specifier that the CJS
* loader then opens as a path (ENOENT), or `import.meta.url` arrives with a
* non-file scheme. Plain `import()` goes through the host module runner
* (vitest) or Node type-stripping and succeeds where tsx breaks.
*/
function isLoaderInteropError(err: unknown): boolean {
const msg = err instanceof Error ? err.message : String(err);
return msg.includes("tsx-namespace") || msg.includes("must be of scheme file");
}
async function importConfigModule(url: string): Promise> {
// Under vitest, prefer the host module runner: a tsx-loaded config yields
// tsx-bound loader closures whose internal dynamic imports hit the
// CJS-namespace bug later in buildRegistry — catching tsImport's own throw
// is not enough.
if (process.env.VITEST) {
try {
return (await import(url)) as Record;
} catch {
// vite-node cannot reach config files outside the workspace root
// (e.g. /tmp fixture workspaces) — fall back to tsx for those.
return (await tsImport(url, import.meta.url)) as Record;
}
}
try {
return (await tsImport(url, import.meta.url)) as Record;
} catch (err) {
if (!isLoaderInteropError(err)) throw err;
return (await import(url)) as Record;
}
}
export async function discoverSiteWorkspaces(
workspaceRoot: string,
): Promise {
return discoverSitesFromResolver(workspaceRoot);
}
export interface KernelConfigLoadOptions {
/** RFC-1309: suppress the `[config]` preamble lines on stderr. */
quiet?: boolean;
}
export async function loadKernelAppConfig(
site: DiscoveredSiteWorkspace,
options?: KernelConfigLoadOptions,
): Promise {
if (!site.configPath) {
throw new Error(`No kernel config found for site \`${site.name}\`.`);
}
if (!options?.quiet) process.stderr.write(` [config] loading ${site.configPath} …\n`);
let moduleNamespace: Record;
try {
moduleNamespace = await importConfigModule(pathToFileURL(site.configPath).href);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to load kernel config \`${site.configPath}\` for site \`${site.name}\`: ${detail}`,
{ cause: error },
);
}
if (!options?.quiet) process.stderr.write(` [config] loaded ${site.configPath}\n`);
const candidate = unwrapConfig(moduleNamespace);
if (!candidate || typeof candidate !== "object") {
throw new Error(`Kernel config \`${site.configPath}\` does not export a valid config object.`);
}
const config = candidate as KernelAppConfig;
if (!config.modules && !config.moduleLoaders) {
throw new Error(
`Kernel config \`${site.configPath}\` must provide either a modules array or moduleLoaders record.`,
);
}
return config;
}
export async function loadWorkspaceConfig(
workspaceRoot: string,
options?: KernelConfigLoadOptions,
): Promise {
const toolsDirectory = path.join(workspaceRoot, "tools");
const configPath = await resolveAppConfigPath(toolsDirectory);
if (!configPath) {
return undefined;
}
if (!options?.quiet) process.stderr.write(` [config] loading ${configPath} …\n`);
let moduleNamespace: Record;
try {
moduleNamespace = await importConfigModule(pathToFileURL(configPath).href);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to load workspace kernel config \`${configPath}\`: ${detail}`, {
cause: error,
});
}
if (!options?.quiet) process.stderr.write(` [config] loaded ${configPath}\n`);
const candidate = unwrapConfig(moduleNamespace);
if (!candidate || typeof candidate !== "object") {
throw new Error(
`Workspace kernel config \`${configPath}\` does not export a valid config object.`,
);
}
const config = candidate as KernelAppConfig;
if (!config.modules && !config.moduleLoaders) {
throw new Error(
`Workspace kernel config \`${configPath}\` must provide either a modules array or moduleLoaders record.`,
);
}
return config;
}
export async function findWorkspaceRoot(
startDirectory: string = process.cwd(),
io: WorkspaceIO = createDefaultIO().io,
): Promise {
let currentDirectory = path.resolve(startDirectory);
while (true) {
const packageJsonPath = path.join(currentDirectory, "package.json");
const workspacePath = path.join(currentDirectory, "pnpm-workspace.yaml");
if ((await io.exists(packageJsonPath)) && (await io.exists(workspacePath))) {
return currentDirectory;
}
{
const parentDirectory = path.dirname(currentDirectory);
if (parentDirectory === currentDirectory) {
throw new Error(`Unable to locate a pnpm workspace root from ${startDirectory}.`);
}
currentDirectory = parentDirectory;
}
}
}