/** * mx run — package-first standalone runner. * * Preferred mode: * mx run . --env dev * * Actor-file mode: * mx run ./MyActor.ts --mount my-svc --root MY-ROOT */ import { pathToFileURL } from 'node:url'; import * as path from 'node:path'; import * as fs from 'node:fs'; import * as net from 'node:net'; import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; import type { NatsConnection, Subscription } from 'nats'; import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js'; import { buildMatrixHttpAssetMount, MatrixHttpAssetEndpointActor, } from '@open-matrix/system-gateway-http'; import { HostAuthStateStore, HostCredentialStore } from '@open-matrix/system-auth'; import { serviceCommand, startServiceCommand, type IServiceCommandOptions, type IStartedServiceCommand, } from './service.js'; import { loadPackageEnvironment, loadPackageEnvironmentFromPath } from '../utils/package-environment.js'; import { createRunnerRuntimeHandle, formatRunnerRuntimeClosed, type IRunnerRuntimeHandle, } from '../utils/runner-runtime.js'; import { resolveNatsBinary } from '../utils/runner-transport.js'; import { resolveRunnerTransportPlan, type IResolvedRunnerTransportPlan } from '../utils/runner-transport.js'; import { startStandaloneWebappServer, type IStandaloneWebappServerHandle } from '../utils/standalone-webapp-server.js'; import { loadMatrixWebappManifest } from '../utils/webapp-manifest.js'; import { deregisterRunnerRuntime, heartbeatRunnerRuntime, registerRunnerRuntime, type IRunnerRuntimeRegistration, type IRunnerWebappRouteMetadata, } from '../utils/runner-control-plane.js'; import { packageDeclaresStandaloneFactory, type ILoadedMatrixServiceManifest, } from '../utils/service-manifest.js'; import { defaultRunnerControlMount, defaultRunnerRuntimeMount, mountRunnerRuntimeControl, type RunnerRuntimeLifecycleState, } from '../utils/runner-runtime-control.js'; import type { ILoadedMatrixWebappManifest } from '../utils/webapp-manifest.js'; export interface IRunCommandOptions { readonly check?: boolean; readonly json?: boolean; readonly env?: string; readonly envFile?: string; readonly serve?: boolean; readonly entry?: string; readonly mount?: string; readonly root?: string; readonly export?: string; readonly config?: string; readonly secrets?: string; readonly overrides?: string; readonly broker?: boolean; readonly brokerPort?: number; readonly federation?: boolean; readonly backboneUrl?: string; readonly props?: string; } type ActorConstructor = { new (): MatrixActor; readonly accepts?: Record; readonly name: string; }; type BrokerLike = { publish(topic: string, payload: unknown): void; }; type BrokerConstructor = new () => BrokerLike; type TransportConstructor = new (broker: BrokerLike, options: { readonly name: string }) => unknown; type RuntimeConstructor = new () => unknown; type MatrixContextFactory = { create(mount: string, transport: unknown, runtime: unknown): Parameters[0]; }; type FederationPeerConstructor = new (options: { readonly localRoot: string; readonly localBus: unknown; readonly instanceResolver: unknown; }) => { setBackbone(transport: unknown): Promise; start(): Promise; }; type ExactInstanceResolverConstructor = new () => unknown; type CreateNatsTransport = (client: NatsConnection) => unknown; interface IActorFileRunCommandOptions { readonly mount: string; readonly root?: string; readonly export?: string; readonly broker?: boolean; readonly brokerPort?: number; readonly federation?: boolean; readonly backboneUrl?: string; readonly props?: string; } interface IRunningNatsServer { readonly port: number; stop(): Promise; } // P1.65 — set process.title for ps/tasklist visibility. Sourcing order: // MATRIX_RUNTIME_KEY env → packageRef shortname → packageDir basename // → MATRIX_RUNTIME_ID stripped → default 'runtime'. function setRuntimeProcessTitle(input: { readonly runtimeKey?: string; readonly runtimeId?: string; readonly packageDir?: string; }): void { const fromKey = readTrimmed(input.runtimeKey); const fromPackageRef = readPackageRefShortName(input.packageDir); const fromDir = readPackageDirBasename(input.packageDir); const fromRuntimeId = readRuntimeIdShortName(input.runtimeId); const name = fromKey ?? fromPackageRef ?? fromDir ?? fromRuntimeId ?? 'runtime'; process.title = `hivecast-runtime:${normalizeRuntimeTitleName(name)}`; } function readTrimmed(value: string | undefined): string | undefined { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function readPackageRefShortName(packageDir: string | undefined): string | undefined { if (!packageDir) return undefined; try { const manifestPath = path.join(packageDir, 'package.json'); if (!fs.existsSync(manifestPath)) return undefined; const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { name?: unknown }; const name = typeof parsed.name === 'string' ? parsed.name : undefined; if (!name) return undefined; return name.split('/').pop(); } catch { return undefined; } } function readPackageDirBasename(packageDir: string | undefined): string | undefined { if (!packageDir) return undefined; const base = path.basename(packageDir); return base.length > 0 ? base : undefined; } function readRuntimeIdShortName(runtimeId: string | undefined): string | undefined { const value = readTrimmed(runtimeId); if (!value) return undefined; return value .replace(/^RUNTIME-/i, '') .replace(/^HOST-/i, '') .replace(/^LOCAL-/i, ''); } function normalizeRuntimeTitleName(value: string): string { return value .trim() .toLowerCase() .replace(/^@[^/]+\//, '') .replace(/@[^@/]+$/, '') .replace(/[^a-z0-9_-]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') || 'runtime'; } async function waitForPort( port: number, timeoutMs: number, child: ChildProcessWithoutNullStreams, getLogs: () => string, getSpawnError?: () => Error | null, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const spawnError = getSpawnError?.(); if (spawnError) { throw new Error(`Failed to spawn nats-server: ${spawnError.message}`); } if (child.exitCode != null) { throw new Error(`nats-server exited early (${child.exitCode}): ${getLogs()}`); } const listening = await new Promise((resolve) => { const socket = net.connect({ host: '127.0.0.1', port }); socket.once('connect', () => { socket.destroy(); resolve(true); }); socket.once('error', () => { socket.destroy(); resolve(false); }); socket.setTimeout(500, () => { socket.destroy(); resolve(false); }); }); if (listening) { return; } await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error(`Timed out waiting for nats-server on port ${port}: ${getLogs()}`); } async function stopChildProcess(child: ChildProcessWithoutNullStreams): Promise { if (child.exitCode != null) { return; } await new Promise((resolve) => { const timeout = setTimeout(() => { if (child.exitCode == null) { child.kill('SIGKILL'); } }, 2000); child.once('exit', () => { clearTimeout(timeout); resolve(); }); child.kill('SIGTERM'); }); } async function startNatsServer(port: number, baseDir: string): Promise { const binaryPath = resolveNatsBinary(baseDir); const child = spawn(binaryPath, ['-a', '127.0.0.1', '-p', String(port), '-js'], { stdio: 'pipe', env: process.env, windowsHide: true, }); let spawnError: Error | null = null; let stderr = ''; child.once('error', (error: Error) => { spawnError = error; }); child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); }); await waitForPort(port, 10_000, child, () => { if (spawnError) { return `${spawnError.message}${stderr ? ` | ${stderr.trim()}` : ''}`; } return stderr.trim(); }, () => spawnError); return { port, async stop(): Promise { await stopChildProcess(child); }, }; } function localTopicToNatsSubject(topic: string, root: string, mount: string): string | null { if (topic === `${mount}/$events`) { return `${root}.${mount}.$events`; } if (topic.startsWith('$replies.')) { return `${root}.$reply.${topic.slice('$replies.'.length)}`; } if (topic.startsWith(`${root}.$reply.`)) { return topic; } return null; } function decodeNatsPayload( data: Uint8Array, jsonCodec: { decode(payload: Uint8Array): unknown }, stringCodec: { decode(payload: Uint8Array): string }, ): unknown { try { return jsonCodec.decode(data); } catch { const text = stringCodec.decode(data); try { return JSON.parse(text); } catch { return text; } } } function pumpSubscription( subscription: Subscription, label: string, onMessage: (subject: string, payload: unknown) => void, jsonCodec: { decode(payload: Uint8Array): unknown }, stringCodec: { decode(payload: Uint8Array): string }, ): void { void (async () => { for await (const message of subscription) { const payload = decodeNatsPayload(message.data, jsonCodec, stringCodec); onMessage(message.subject, payload); } })().catch((error) => { console.error(`[mx run] ${label} subscription failed: ${error}`); }); } export async function runCommand( targetPath: string, options: IRunCommandOptions, ): Promise { const resolvedPath = path.resolve(targetPath); if (!fs.existsSync(resolvedPath)) { throw new Error(`Run target not found: ${resolvedPath}`); } const targetStats = fs.statSync(resolvedPath); if (targetStats.isDirectory()) { await runPackageCommand(resolvedPath, options); return; } await runActorFileCommand(resolvedPath, assertActorFileRunOptions(options)); } async function runPackageCommand( packageDir: string, options: IRunCommandOptions, ): Promise { // P1.65 — name the runtime process for ps/tasklist. Sourcing order: // MATRIX_RUNTIME_KEY env → packageRef shortname → packageDir basename // → MATRIX_RUNTIME_ID → 'runtime'. setRuntimeProcessTitle({ runtimeKey: process.env.MATRIX_RUNTIME_KEY, runtimeId: process.env.MATRIX_RUNTIME_ID, packageDir, }); const hasServiceManifest = packageDeclaresStandaloneFactory(packageDir); const webapp = loadMatrixWebappManifest(packageDir); if (!options.serve && !hasServiceManifest && !webapp) { throw new Error( `Package run target ${packageDir} does not declare a matrix.json runtime.factory. ` + 'Use a package directory with runtime.factory metadata, or pass an actor file path with --mount.', ); } if (options.serve || (!hasServiceManifest && webapp)) { await runStandaloneWebappPackage(packageDir, options, { hasServiceManifest, webapp, serve: options.serve === true, }); return; } const result = await serviceCommand(packageDir, { check: options.check, json: options.json, env: options.env, envFile: options.envFile, entry: options.entry, export: options.export, root: options.root, mount: options.mount, config: options.config, secrets: options.secrets, overrides: options.overrides, } satisfies IServiceCommandOptions); if (!options.check) { return; } if (options.json) { console.log(JSON.stringify(result, null, 2)); return; } console.log(`Validated run-in-place bootstrap for ${result.packageName}`); console.log(`manifest: ${result.manifestPath}`); console.log(`entry: ${result.entryPath}`); console.log(`export: ${result.exportName}`); } async function runStandaloneWebappPackage( packageDir: string, options: IRunCommandOptions, state: { readonly hasServiceManifest: boolean; readonly webapp: ReturnType; readonly serve: boolean; }, ): Promise { if (!state.webapp) { throw new Error(`Package run target ${packageDir} does not declare matrix.json webapp metadata`); } const envName = options.env?.trim(); const envFile = options.envFile?.trim(); if (!envName) { throw new Error('Package webapp runtime mode requires --env '); } const loadedEnvironment = envFile ? loadPackageEnvironmentFromPath(packageDir, envFile, envName) : loadPackageEnvironment(packageDir, envName); const root = options.root?.trim() || loadedEnvironment.environment.runtime.root; const runnerTransport = resolveRunnerTransportPlan(packageDir, loadedEnvironment, root); let serviceHandle: IStartedServiceCommand | undefined; let runnerRuntimeHandle: IRunnerRuntimeHandle | undefined; let webappServer: IStandaloneWebappServerHandle | undefined; let runtimeRegistration: IRunnerRuntimeRegistration | undefined; let webappAssetMount: string | undefined; let heartbeatTimer: NodeJS.Timeout | undefined; let runtimeReclaimAttempts = 0; let lifecycleState: RunnerRuntimeLifecycleState = 'starting'; let requestRuntimeShutdown: (reason: string) => void = () => undefined; const shutdownRequested = new Promise((resolve) => { requestRuntimeShutdown = resolve; }); try { if (state.hasServiceManifest) { serviceHandle = await startServiceCommand(packageDir, { env: envName, envFile, entry: options.entry, export: options.export, root: options.root, mount: options.mount, config: options.config, secrets: options.secrets, overrides: options.overrides, } satisfies IServiceCommandOptions); } else { runnerRuntimeHandle = await createRunnerRuntimeHandle(root, runnerTransport); registerHostHomeService(runnerRuntimeHandle.runtime, loadedEnvironment.environment.host?.matrixDir); const runtimeId = resolveWebappRuntimeId(state.webapp, loadedEnvironment); const runtimeMount = loadedEnvironment.environment.runtime.runtimeMount ?? process.env.MATRIX_RUNTIME_MOUNT ?? defaultRunnerRuntimeMount(runtimeId); const controlMount = loadedEnvironment.environment.runtime.controlMount ?? process.env.MATRIX_RUNTIME_CONTROL_MOUNT ?? defaultRunnerControlMount(runtimeId); await mountRunnerRuntimeControl({ runtime: runnerRuntimeHandle.runtime, runtimeId, runtimeMount, controlMount, startedAt: new Date().toISOString(), getState: () => lifecycleState, requestShutdown: (reason) => requestRuntimeShutdown(reason), loadedPackage: { packageRef: state.webapp.packageName, }, }); if (state.serve) { webappServer = await startStandaloneWebappServer({ loadedEnvironment, webapp: state.webapp, runnerTransport, }); } webappAssetMount = await mountMatrixHttpAssetEndpoint(runnerRuntimeHandle.runtime, state.webapp, runtimeId); runtimeRegistration = await registerRunnerRuntime( runnerRuntimeHandle.runtime, buildWebappRuntimeManifest(state.webapp), loadedEnvironment, runnerTransport, undefined, runtimeMount, controlMount, buildRunnerWebappRouteMetadata(state.webapp, webappAssetMount, webappServer), ); if (!options.check) { const heartbeatRuntime = runnerRuntimeHandle.runtime; heartbeatTimer = setInterval(() => { void (async () => { const heartbeat = await heartbeatRunnerRuntime(heartbeatRuntime, runtimeRegistration); if (!heartbeat || heartbeat.known) { runtimeReclaimAttempts = 0; return; } runtimeReclaimAttempts += 1; if (runtimeReclaimAttempts === 1 || runtimeReclaimAttempts % 10 === 0) { console.error( `[mx run] registry forgot runtime claim for ${heartbeat.runtimeId} (renewed=0); reclaiming`, ); } const reclaimed = await runtimeRegistration?.reclaim(); if (reclaimed) { console.error( `[mx run] runtime reclaim recovered for ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempt(s): ${reclaimed.inventoryClaimCount} inventory claims re-issued`, ); runtimeReclaimAttempts = 0; } })().catch((error: unknown) => { if (runtimeReclaimAttempts === 1 || runtimeReclaimAttempts % 10 === 0) { console.error( `[mx run] runtime reclaim failed: ${error instanceof Error ? error.message : String(error)}`, ); } }); }, 10_000); } } if (state.serve && serviceHandle) { webappServer = await startStandaloneWebappServer({ loadedEnvironment, webapp: state.webapp, runnerTransport: serviceHandle?.result.runnerTransport ?? runnerTransport, }); } lifecycleState = 'live'; const buildServeResult = () => ({ packageName: state.webapp?.packageName, packageDir, environmentName: loadedEnvironment.envName, environmentPath: loadedEnvironment.environmentPath, root, mode: state.serve ? 'standalone-webapp' : 'webapp-assets', ...(webappAssetMount ? { assetMount: webappAssetMount } : {}), ...(webappServer ? { baseUrl: webappServer.baseUrl, httpPort: webappServer.port, bootstrapPath: webappServer.bootstrapPath, natsWsPath: webappServer.natsWsPath, } : {}), runnerTransport: serviceHandle?.result.runnerTransport ?? runnerTransport, ...(serviceHandle ? { service: serviceHandle.result } : {}), }); const shutdownAll = async (): Promise => { lifecycleState = 'stopping'; if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = undefined; } if (webappServer) { const current = webappServer; webappServer = undefined; await current.shutdown(); } if (serviceHandle) { const current = serviceHandle; serviceHandle = undefined; await current.shutdown(); } if (runnerRuntimeHandle) { const current = runnerRuntimeHandle; runnerRuntimeHandle = undefined; await deregisterRunnerRuntime(current.runtime, runtimeRegistration); await current.shutdown(); } lifecycleState = 'stopped'; }; if (options.check) { const result = buildServeResult(); await shutdownAll(); if (options.json) { console.log(JSON.stringify(result, null, 2)); } else if (state.serve) { console.log(`Validated standalone webapp serve for ${state.webapp.packageName}`); console.log(`base: ${result.baseUrl}`); } else { console.log(`Validated webapp asset runtime for ${state.webapp.packageName}`); console.log(`asset mount: ${result.assetMount ?? '(service runtime)'}`); } return; } if (options.json) { console.log(JSON.stringify(buildServeResult(), null, 2)); } else if (state.serve && webappServer) { console.log(`[mx run] serving ${state.webapp.packageName} at ${webappServer.baseUrl}`); console.log(`[mx run] bootstrap: ${webappServer.baseUrl}${webappServer.bootstrapPath}`); console.log(`[mx run] nats-ws: ${webappServer.baseUrl}${webappServer.natsWsPath}`); } else { console.log(`[mx run] running ${state.webapp.packageName} web assets at ${webappAssetMount ?? '(service runtime)'}`); } const stopTriggers: Promise[] = [ waitForShutdownSignal(shutdownAll), shutdownRequested.then(async () => { await shutdownAll(); }), ]; if (serviceHandle) { stopTriggers.push(serviceHandle.shutdownRequested.then(async () => { await shutdownAll(); })); if (serviceHandle.transportClosed) { stopTriggers.push(serviceHandle.transportClosed.then((closed) => { throw new Error(formatRunnerRuntimeClosed(closed)); })); } } if (runnerRuntimeHandle?.closed) { stopTriggers.push(runnerRuntimeHandle.closed.then((closed) => { throw new Error(formatRunnerRuntimeClosed(closed)); })); } await Promise.race(stopTriggers); return; } catch (error) { lifecycleState = 'failed'; if (webappServer) { await webappServer.shutdown().catch(() => undefined); } if (serviceHandle) { await serviceHandle.shutdown().catch(() => undefined); } if (runnerRuntimeHandle) { await deregisterRunnerRuntime(runnerRuntimeHandle.runtime, runtimeRegistration).catch(() => undefined); await runnerRuntimeHandle.shutdown().catch(() => undefined); } throw error; } } function registerHostHomeService( runtime: IRunnerRuntimeHandle['runtime'], matrixDir: string | undefined, ): void { const hostHome = matrixDir?.trim(); if (hostHome) { runtime.registerService('matrix-host-home', hostHome); runtime.registerService('credential-store', new HostCredentialStore(new HostAuthStateStore(hostHome))); } } async function mountMatrixHttpAssetEndpoint( runtime: IRunnerRuntimeHandle['runtime'], webapp: ILoadedMatrixWebappManifest, runtimeId?: string, ): Promise { const assetMount = buildMatrixHttpAssetMount(webapp.appName, runtimeId); if (!runtime.hasComponent(assetMount)) { await runtime.createSupervised(MatrixHttpAssetEndpointActor as unknown as new () => MatrixActor, assetMount, { assetEndpoint: { packageName: webapp.packageName, appName: webapp.appName, distDir: webapp.distDir, entryFile: webapp.entryFile, }, }); } return assetMount; } function buildRunnerWebappRouteMetadata( webapp: ILoadedMatrixWebappManifest, assetMount: string, webappServer?: IStandaloneWebappServerHandle, ): IRunnerWebappRouteMetadata { return { appName: webapp.appName, routePrefix: `/apps/${webapp.appName}/`, ...(webapp.displayName ? { displayName: webapp.displayName } : {}), ...(webapp.icon ? { icon: webapp.icon } : {}), ...(typeof webapp.navOrder === 'number' ? { navOrder: webapp.navOrder } : {}), ...(webapp.description ? { description: webapp.description } : {}), ...(webapp.shells && webapp.shells.length > 0 ? { shells: webapp.shells } : {}), assetMount, ...(webappServer ? { origin: webappServer.baseUrl, port: webappServer.port, } : {}), }; } function buildWebappRuntimeManifest( webapp: NonNullable>, ): ILoadedMatrixServiceManifest { return { packageDir: webapp.packageDir, packageName: webapp.packageName, manifestPath: webapp.manifestPath, entryPath: path.join(webapp.distDir, webapp.entryFile), exportName: 'webapp', kind: 'factory', overrides: {}, packageComponents: [], serviceInstance: { id: webapp.packageName, packageName: webapp.packageName, class: 'webapp', rootKind: 'package-root', autoStart: true, registrationSource: 'matrix-service', }, }; } function resolveWebappRuntimeId( webapp: NonNullable>, loadedEnvironment: ReturnType, ): string { const runtimeId = loadedEnvironment.environment.runtime.runtimeId?.trim(); if (runtimeId) { return runtimeId; } return webapp.packageName; } function actorConstructorFromExport(value: unknown): ActorConstructor | null { if (typeof value !== 'function') { return null; } return value as ActorConstructor; } function requireFunctionExport( moduleExports: Record, exportName: string, ): T { const value = moduleExports[exportName]; if (typeof value !== 'function') { throw new Error(`Expected function export ${exportName}`); } return value as T; } function requireObjectExport( moduleExports: Record, exportName: string, ): T { const value = moduleExports[exportName]; if (!value || typeof value !== 'object') { throw new Error(`Expected object export ${exportName}`); } return value as T; } function assertActorFileRunOptions(options: IRunCommandOptions): IActorFileRunCommandOptions { const mount = options.mount?.trim(); if (!mount) { throw new Error( 'Actor-file mode requires --mount. ' + 'For package execution use: mx run . --env ', ); } return { mount, root: options.root, export: options.export, broker: options.broker, brokerPort: options.brokerPort, federation: options.federation, backboneUrl: options.backboneUrl, props: options.props, }; } async function waitForShutdownSignal(onShutdown: () => Promise): Promise { await new Promise((resolve, reject) => { let settled = false; const complete = (error?: unknown) => { if (settled) { return; } settled = true; process.off('SIGINT', handleSignal); process.off('SIGTERM', handleSignal); if (error) { reject(error); return; } resolve(); }; const handleSignal = () => { void onShutdown() .then(() => complete()) .catch((error) => complete(error)); }; process.on('SIGINT', handleSignal); process.on('SIGTERM', handleSignal); }); } async function runActorFileCommand( actorPath: string, options: IActorFileRunCommandOptions, ): Promise { const root = options.root ?? 'standalone'; const mount = options.mount; const exportName = options.export ?? 'default'; const props = options.props ? JSON.parse(options.props) : {}; // ── Step 1: Import actor class ────────────────────────────────────── const moduleUrl = pathToFileURL(actorPath).href; const mod = await import(moduleUrl) as Record; // Try named export, then default, then first export that looks like a class let ActorClass = actorConstructorFromExport(mod[exportName]); if (!ActorClass && exportName === 'default') { // Try first exported class for (const [key, val] of Object.entries(mod)) { const candidate = actorConstructorFromExport(val); if (candidate && key !== 'default') { ActorClass = candidate; console.log(`[mx run] Using export: ${key}`); break; } } } if (!ActorClass) { const available = Object.keys(mod).filter(k => actorConstructorFromExport(mod[k])); throw new Error( `No actor class found as export '${exportName}'. Available exports: ${available.join(', ') || '(none)'}` ); } // ── Step 2: Set up transport ──────────────────────────────────────── // // Three modes: // 1. InMemory only (default) — local actors talk, no external access // 2. +broker — starts embedded NATS NATS on a port, CLI can connect // 3. +federation — connects to NATS hub backbone, world can reach us // Dynamic imports — import individual modules to avoid barrel hangs const cwd = process.cwd(); async function importModule(relativePath: string): Promise> { const full = path.resolve(cwd, relativePath); return await import(pathToFileURL(full).href) as Record; } const [brokerMod, transportMod, contextMod, runtimeMod] = await Promise.all([ importModule('src/transport/InMemoryBroker.ts'), importModule('src/transport/InMemoryTransport.ts'), importModule('src/engine/core/MatrixContext.ts'), importModule('src/core/MockRuntime.ts'), ]); const InMemoryBroker = requireFunctionExport(brokerMod, 'InMemoryBroker'); const InMemoryTransport = requireFunctionExport(transportMod, 'InMemoryTransport'); const MatrixContext = requireObjectExport(contextMod, 'MatrixContext'); const MockRuntime = requireFunctionExport(runtimeMod, 'MockRuntime'); const broker = new InMemoryBroker(); const transport = new InMemoryTransport(broker, { name: 'mx-run' }); const runtime = new MockRuntime(); // ── Step 3: Optional embedded NATS NATS server ────────────────────────────── let natsServer: IRunningNatsServer | null = null; let natsClient: NatsConnection | null = null; if (options.broker || options.federation) { const port = options.brokerPort ?? 4222; try { const { connect, JSONCodec, StringCodec } = await import('nats'); const jsonCodec = JSONCodec(); const stringCodec = StringCodec(); natsServer = await startNatsServer(port, cwd); console.log(`[mx run] NATS server listening on nats://127.0.0.1:${port}`); // Bridge InMemoryBroker ↔ local NATS so actors are reachable via NATS natsClient = await connect({ servers: `nats://127.0.0.1:${port}`, name: `mx-run-${process.pid}`, timeout: 5000, }); const actorInbox = `${root}.${mount}.$inbox`; pumpSubscription( natsClient.subscribe(actorInbox), actorInbox, (_subject, payload) => { broker.publish(`${mount}/$inbox`, payload); }, jsonCodec, stringCodec, ); const rootInbox = `${root}.$inbox`; pumpSubscription( natsClient.subscribe(rootInbox), rootInbox, (_subject, payload) => { broker.publish('$inbox', payload); }, jsonCodec, stringCodec, ); // Hook actor replies — the actor publishes to the replyTo topic const origPublish = broker.publish.bind(broker); broker.publish = (topic: string, payload: unknown) => { origPublish(topic, payload); const subject = localTopicToNatsSubject(topic, root, mount); if (subject) { natsClient?.publish(subject, jsonCodec.encode(payload)); } }; } catch (err) { throw new Error(`Failed to start NATS server for mx run: ${String(err)}`); } } // ── Step 4: Optional Federation ───────────────────────────────────── if (options.federation) { const backboneUrl = options.backboneUrl ?? 'nats://hub.example.com:4222'; try { const fedPath = path.resolve(cwd, 'packages', 'federation', 'dist', 'src', 'index.js'); const fedMod = await import(pathToFileURL(fedPath).href) as Record; const FederationPeer = requireFunctionExport(fedMod, 'FederationPeer'); const ExactInstanceResolver = requireFunctionExport(fedMod, 'ExactInstanceResolver'); const createNatsTransport = requireFunctionExport(fedMod, 'createNatsTransport'); const { connect } = await import('nats'); // Connect to backbone const backboneClient = await connect({ servers: backboneUrl, name: `mx-run-fed-${root}-${process.pid}`, timeout: 10000, }); console.log(`[mx run] Federation backbone connected: ${backboneUrl}`); const backboneTransport = createNatsTransport(backboneClient); if (!natsServer) { throw new Error('Federation mode requires the embedded NATS server to be running'); } const localNats = await connect({ servers: `nats://127.0.0.1:${options.brokerPort ?? 4222}`, name: `mx-run-fed-local-${process.pid}`, timeout: 3000, }); const localBus = createNatsTransport(localNats); const peer = new FederationPeer({ localRoot: root, localBus, instanceResolver: new ExactInstanceResolver(), }); await peer.setBackbone(backboneTransport); await peer.start(); console.log(`[mx run] FederationPeer active for root: ${root}`); console.log(`[mx run] Actor reachable at: ${root}/${mount}`); } catch (err) { throw new Error(`Federation setup failed for mx run: ${String(err)}`); } } // ── Step 5: Boot the actor ────────────────────────────────────────── const context = MatrixContext.create(mount, transport, runtime); const actor = new ActorClass(); await actor.initialize(context, ActorClass.name || 'Actor'); // Assign actor properties requested by the CLI. for (const [key, value] of Object.entries(props)) { Object.defineProperty(actor, key, { value, writable: true, enumerable: true, configurable: true, }); } const accepts = ActorClass.accepts ? Object.keys(ActorClass.accepts).join(', ') : 'N/A'; console.log(''); console.log('╔════════════════════════════════════════════════╗'); console.log('║ mx run — Actor Live ║'); console.log('╠════════════════════════════════════════════════╣'); console.log(`║ Actor: ${ActorClass.name}`.padEnd(49) + '║'); console.log(`║ Mount: ${mount}`.padEnd(49) + '║'); console.log(`║ Root: ${root}`.padEnd(49) + '║'); console.log(`║ Accepts: ${accepts}`.padEnd(49) + '║'); if (options.broker || options.federation) { console.log(`║ Broker: nats://127.0.0.1:${options.brokerPort ?? 4222}`.padEnd(49) + '║'); } if (options.federation) { console.log(`║ Federation: CONNECTED`.padEnd(49) + '║'); console.log(`║ URI: ${root}/${mount}`.padEnd(49) + '║'); } console.log('╚════════════════════════════════════════════════╝'); console.log(''); if (options.broker) { console.log(`Talk to this actor:`); console.log(` matrix send ${mount} $introspect -r ${root}`); console.log(''); } if (options.federation) { console.log(`From FlowPad or a federated client:`); console.log(` flow().fromValue({}).viaRemote("${root}/${mount}", "introspect")`); console.log(''); } console.log('Press Ctrl+C to stop.'); // Keep process alive await new Promise(() => { const shutdown = async () => { console.log('\n[mx run] Shutting down...'); try { if (natsClient && !natsClient.isClosed()) { await natsClient.drain(); } } catch { // Best-effort shutdown only. } try { await natsServer?.stop(); } catch { // Best-effort shutdown only. } process.exit(0); }; process.on('SIGINT', () => { void shutdown(); }); }); }