// What the host and the human commands read before they touch the bus: // the system config (through scripts/mosaic-config.mjs, as `mosaic business` // does), the business file, and the boot message for the broker process. // Everything here reads and refuses; nothing writes. import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { BusinessError, configDir, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs"; import { busBusiness } from "../../bus/src/business.mjs"; import { BusError } from "../../bus/src/broker.mjs"; import { CliError } from "./errors.mjs"; export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); // The broker's socket. One broker per data root: the bus store takes // `/bus/writer.lock` (packages/bus/src/store.mjs). export const socketPath = (dataRoot) => join(dataRoot, "bus", "broker.sock"); export function loadSystem({ env = process.env } = {}) { const proc = spawnSync(process.execPath, [join(REPO, "scripts", "mosaic-config.mjs"), "validate"], { encoding: "utf8", maxBuffer: 1024 * 1024, env, }); if (proc.status !== 0) throw new CliError(`system config problem (mosaic-config exit ${proc.status}): ${proc.stderr.trim()}`, 3); try { return JSON.parse(proc.stdout); } catch { throw new CliError("system config: mosaic-config printed something that isn't JSON", 3); } } export function rolesDir(env = process.env) { return resolve(env.MOSAIC_ROLES_DIR || join(REPO, "roles")); } // A business-package or adapter refusal is a config problem: exit 3. function business(fn) { try { return fn(); } catch (error) { if (error instanceof BusinessError) throw new CliError(error.message, 3); if (error instanceof BusError) throw new CliError(`business adapter refused: ${error.code}`, 3); throw error; } } // config.trackers[] (lead decision 70): plain data from the // tracker.* variables. tracker.project is a project-layer variable, so the // entry comes from the one declared project whose file sets it. No // tracker.baseUrl, or no project naming a tracker project, means no entry // and no task verbs; two projects naming one each refuse, since the boot // shape holds one tracker project per business. function trackerFor(system, b, warn) { const first = Object.keys(b.roles)[0]; const base = resolveInstance({ system, business: b, instance: first }).vars; if (base["tracker.baseUrl"] === undefined) return null; const named = []; for (const [id, entry] of Object.entries(b.projects)) { if (!existsSync(projectFilePath(entry.root))) continue; const project = loadProject(entry.root); if (project.id !== id) throw new CliError(`project file ${project.file} has id ${project.id}, but business ${b.id} declares it as ${id}`, 3); const vars = resolveInstance({ system, business: b, project, instance: first }).vars; if (vars["tracker.project"] !== undefined) named.push({ id, vars }); } if (named.length > 1) { throw new CliError(`business ${b.id}: projects ${named.map((n) => n.id).join(", ")} each set tracker.project; the broker takes one tracker project per business`, 3); } if (named.length === 0) { warn(`business ${b.id} sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs`); return null; } const v = named[0].vars; return { baseUrl: v["tracker.baseUrl"], project: v["tracker.project"], pollSeconds: v["tracker.pollSeconds"], reconcileMinutes: v["tracker.reconcileMinutes"], }; } // The `{op: 'boot'}` config for packages/bus/src/process.mjs, for one // business. The host holds a reader capability for the notifier and binds // launches later through bindLaunch, so `launches` starts empty. export function bootConfig({ system, businessId, env = process.env, warn = () => {} }) { return business(() => { const vars = systemVars(system); const b = loadBusiness(businessId, { dir: configDir(env), rolesDir: rolesDir(env) }); const resolved = {}; for (const instance of Object.keys(b.roles)) resolved[instance] = resolveInstance({ system: vars, business: b, instance }); const config = { dataRoot: system.dataRoot, businesses: { [b.id]: busBusiness(b, resolved) }, launches: [], readers: [b.id], }; const tracker = trackerFor(vars, b, warn); if (tracker) config.trackers = { [b.id]: tracker }; return config; }); }