diff --git a/src/main.ts b/src/main.ts index f1aea23..d0d46d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,17 @@ -// The runnable entrypoint. Two modes: +// The runnable entrypoint. Three modes: // // mesh-tools serve — bind the broker and serve the assigned modules until // stopped. A module entrypoint that subscribes to events (on("#")) // starts consuming as it is imported, so this also runs consumers. // mesh-tools emit TYPE [JSON] emit one event onto the mesh and exit — an operable primitive, // and what an events test uses to put a message on the wire. +// mesh-tools run ENTRYPOINT run one compiled module entrypoint to completion and exit — the +// runtime side of a run-once step (novox/hq ADR 0052). It imports +// the given entrypoint, whose top-level code does its work — seed a +// store, migrate, health-gate — and awaits it. It does NOT connect +// to the broker: a first-boot step runs offline, before the module +// has anything to talk to, and the host gates the container that +// depends on it on this process exiting 0. // // The broker, in order of preference: // MESH_BROKER_FILE a sealed {url, fingerprint} the mesh delivered (novox/hq ADR 0043) — an @@ -14,6 +21,7 @@ // MESH_MODULE / MESH_NODE the identity stamped onto emitted events (ADR 0042) import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; import { connectAmqp } from "./broker-amqp.js"; import type { Credential } from "./broker-amqp.js"; import { runTools } from "./runtime.js"; @@ -109,8 +117,29 @@ async function invokeOnce(module: string, tool: string, argsJson: string): Promi await broker.close(); } +/** + * Run one compiled module entrypoint to completion — the runtime side of a run-once step + * (novox/hq ADR 0052). Importing it runs its top-level code and awaits any top-level await, so this + * returns only once the step's own code has finished; a step that throws rejects here and the + * process exits non-zero, which is how the host knows the step did not complete and must not start + * the container it gates. No broker is connected — a first-boot seed or migration runs offline. + */ +async function runEntry(entrypoint: string): Promise { + if (!entrypoint) { + console.error("mesh-tools run — a compiled module entrypoint path is required"); + process.exit(1); + } + // A file URL, not a bare path: dynamic import of an absolute path is not portable, and the + // entrypoint the manifest names is an absolute path inside the image. + await import(pathToFileURL(entrypoint).href); +} + async function main(): Promise { const [command, ...rest] = process.argv.slice(2); + if (command === "run") { + await runEntry(rest[0] ?? ""); + return; + } if (command === "invoke") { const [module, tool] = rest; if (!module || !tool) {