mesh-tools run: a one-shot entrypoint invocation (ADR 0052 runtime side) #5

Merged
jschoubben merged 1 commits from feat/run-once-entry into main 2026-09-05 22:48:07 +00:00
Showing only changes of commit 991fb6faec - Show all commits
+30 -1
View File
@@ -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<void> {
if (!entrypoint) {
console.error("mesh-tools run <entrypoint> — 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<void> {
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) {