@mh-gg/cli 0.1.1-alpha.20260613T085325975Z

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +5 -0
  2. package/bin/matterhorn.cjs +57 -0
  3. package/package.json +49 -0
  4. package/runtime/bin/appFrontend/artifacts.cjs +25 -0
  5. package/runtime/bin/appFrontend/buildServers.cjs +176 -0
  6. package/runtime/bin/appFrontend/commandEnv.cjs +74 -0
  7. package/runtime/bin/appFrontend/commandPolicy.cjs +23 -0
  8. package/runtime/bin/appFrontend/devServers.cjs +150 -0
  9. package/runtime/bin/appFrontend/httpServers.cjs +221 -0
  10. package/runtime/bin/appFrontend/paths.cjs +103 -0
  11. package/runtime/bin/appFrontend/ports.cjs +36 -0
  12. package/runtime/bin/appFrontend/processes.cjs +127 -0
  13. package/runtime/bin/appFrontend.cjs +45 -0
  14. package/runtime/bin/appHostCommand.cjs +381 -0
  15. package/runtime/bin/matterhorn.cjs +501 -0
  16. package/runtime/bin/matterhornAppLoader.cjs +588 -0
  17. package/runtime/bin/matterhornApps.cjs +223 -0
  18. package/runtime/bin/matterhornDeploy.cjs +108 -0
  19. package/runtime/bin/matterhornEmitAppBundle.cjs +20 -0
  20. package/runtime/bin/matterhornInstall.cjs +609 -0
  21. package/runtime/host/callAuth.cjs +76 -0
  22. package/runtime/host/host.cjs +103 -0
  23. package/runtime/host/hostAnnouncement.cjs +70 -0
  24. package/runtime/host/hostClients/constants.cjs +7 -0
  25. package/runtime/host/hostClients/frontendBundleRefresh.cjs +158 -0
  26. package/runtime/host/hostClients/frontendRequests.cjs +166 -0
  27. package/runtime/host/hostClients/index.cjs +68 -0
  28. package/runtime/host/hostClients/rejections.cjs +37 -0
  29. package/runtime/host/hostSession.cjs +160 -0
  30. package/runtime/host/inlineProgressBar.cjs +128 -0
  31. package/runtime/host/localPeerServer.cjs +114 -0
  32. package/runtime/host/localRelayClient.cjs +151 -0
  33. package/runtime/host/matterhornrc.cjs +75 -0
  34. package/runtime/host/memberRootRegistry.cjs +132 -0
  35. package/runtime/host/nodePeer.cjs +127 -0
  36. package/runtime/host/nodePeerRacePatch.cjs +106 -0
  37. package/runtime/host/peerJsConfig.cjs +26 -0
  38. package/runtime/host/pushEgress.cjs +48 -0
  39. package/runtime/host/pushStorage.cjs +233 -0
  40. package/runtime/host/relay/config.cjs +179 -0
  41. package/runtime/host/relay/connectionCleanup.cjs +34 -0
  42. package/runtime/host/relay/connectionDispatcher.cjs +140 -0
  43. package/runtime/host/relay/matterhornOperationEvents.cjs +100 -0
  44. package/runtime/host/relay/matterhornRuntimeEventBridge.cjs +182 -0
  45. package/runtime/host/relay/nostrRelay.cjs +30 -0
  46. package/runtime/host/relay/peerStartup.cjs +81 -0
  47. package/runtime/host/relay.cjs +653 -0
  48. package/runtime/host/relayClientRouting.cjs +1054 -0
  49. package/runtime/host/relayConfig.cjs +156 -0
  50. package/runtime/host/relayHostAuth.cjs +39 -0
  51. package/runtime/host/relayHostMessages.cjs +367 -0
  52. package/runtime/host/relayHttp.cjs +48 -0
  53. package/runtime/host/relayIdentity.cjs +496 -0
  54. package/runtime/host/relayIncomingGate.cjs +153 -0
  55. package/runtime/host/relayMeshEnvelopes.cjs +522 -0
  56. package/runtime/host/relayPeerLifecycle.cjs +96 -0
  57. package/runtime/host/relayPeerSignals.cjs +175 -0
  58. package/runtime/host/relayRoomRuntimePersistence.cjs +129 -0
  59. package/runtime/host/relayStatus.cjs +160 -0
  60. package/runtime/host/sfuRelay.cjs +553 -0
  61. package/runtime/host/sqliteRelayStorage.cjs +352 -0
  62. package/runtime/host/wireValidation/client.cjs +213 -0
  63. package/runtime/host/wireValidation/host.cjs +33 -0
  64. package/runtime/host/wireValidation/index.cjs +13 -0
  65. package/runtime/host/wireValidation/peerSignal.cjs +35 -0
  66. package/runtime/host/wireValidation/presenceEvent.cjs +49 -0
  67. package/runtime/host/wireValidation/push.cjs +49 -0
  68. package/runtime/host/wireValidation/relay.cjs +131 -0
  69. package/runtime/host/wireValidation/shared.cjs +49 -0
  70. package/runtime/scripts/ensureWorkspaceSdkBuild.cjs +148 -0
  71. package/runtime/scripts/killChildTree.cjs +18 -0
@@ -0,0 +1,160 @@
1
+ const WebSocket = require("ws");
2
+ const { announceHost } = require("./hostAnnouncement.cjs");
3
+ const { createHostClients } = require("./hostClients/index.cjs");
4
+ const { relayIpcUrl } = require("./localRelayClient.cjs");
5
+ const { createRelayMessageReceiver, parseRelayMessage, sendToRelay } = require("@mh-gg/host-ipc");
6
+
7
+ function createHostSession(options) {
8
+ const {
9
+ store,
10
+ args,
11
+ logger = console
12
+ } = options;
13
+ const ipcSecret = options.localRelay?.config?.ipcSecret;
14
+ const reconnectRelay = options.reconnectRelay === true;
15
+ const relayReconnectMs = Number.isFinite(options.relayReconnectMs) ? Math.max(100, options.relayReconnectMs) : 1000;
16
+ let activeRelaySocket;
17
+ let reconnectTimer;
18
+ const clients = createHostClients({
19
+ store,
20
+ logger,
21
+ frontendBundle: options.frontendBundle,
22
+ frontendProgress: options.frontendProgress,
23
+ onStoreChange: syncStoreToRelay
24
+ });
25
+ let announced = false;
26
+
27
+ function debugHost(message, ...args) {
28
+ if (process.env.MATTERHORN_DEBUG === "1") logger.log(message, ...args);
29
+ }
30
+
31
+ function announceRegisteredRelay() {
32
+ if (announced) return;
33
+ announced = true;
34
+ announceHost(options);
35
+ }
36
+
37
+ function roomStoreSnapshot() {
38
+ return JSON.parse(JSON.stringify(store));
39
+ }
40
+
41
+ function relayAuth() {
42
+ if (!ipcSecret) return undefined;
43
+ return {
44
+ type: "relay-ipc",
45
+ secret: ipcSecret
46
+ };
47
+ }
48
+
49
+ function hostIpcMessage(message) {
50
+ const auth = relayAuth();
51
+ return auth ? { ...message, auth } : message;
52
+ }
53
+
54
+ function syncStoreToRelay() {
55
+ if (!activeRelaySocket) return;
56
+ sendToRelay(activeRelaySocket, hostIpcMessage({
57
+ type: "host.snapshot",
58
+ roomName: store.roomName,
59
+ store: roomStoreSnapshot(),
60
+ appRef: options.appRef,
61
+ appCwd: options.appCwd
62
+ }));
63
+ }
64
+
65
+ function createReconnectSocket() {
66
+ if (typeof options.createRelaySocket === "function") return options.createRelaySocket();
67
+ if (!options.localRelay?.config) return undefined;
68
+ return new WebSocket(relayIpcUrl(options.localRelay.config));
69
+ }
70
+
71
+ function scheduleRelayReconnect() {
72
+ if (!reconnectRelay || reconnectTimer || activeRelaySocket) return;
73
+ reconnectTimer = setTimeout(() => {
74
+ reconnectTimer = undefined;
75
+ let relaySocket;
76
+ try {
77
+ relaySocket = createReconnectSocket();
78
+ } catch (error) {
79
+ logger.error("Local relay IPC reconnect failed:", error);
80
+ scheduleRelayReconnect();
81
+ return;
82
+ }
83
+ if (relaySocket) attachRelaySocket(relaySocket);
84
+ else scheduleRelayReconnect();
85
+ }, relayReconnectMs);
86
+ reconnectTimer.unref?.();
87
+ }
88
+
89
+ function handleRelayMessage(socket, data) {
90
+ const message = typeof data === "object" && !Buffer.isBuffer(data) ? data : parseRelayMessage(data);
91
+ if (!message) return;
92
+
93
+ if (message.type === "host.register.ok") {
94
+ announceRegisteredRelay();
95
+ return;
96
+ }
97
+
98
+ if (message.type === "host.register.error") {
99
+ logger.error(message.message || "Relay rejected host registration.");
100
+ return;
101
+ }
102
+
103
+ if (message.type === "relay/client-message") {
104
+ clients.handleClientMessage(socket, message.peerId, message.message);
105
+ return;
106
+ }
107
+
108
+ if (message.type === "relay/client-close") {
109
+ debugHost("Relay client connection closed", message.peerId);
110
+ clients.removeConnection(message.peerId);
111
+ }
112
+ }
113
+
114
+ function attachRelaySocket(relaySocket) {
115
+ const receiveRelayMessage = createRelayMessageReceiver();
116
+ relaySocket.on("open", () => {
117
+ if (reconnectTimer) {
118
+ clearTimeout(reconnectTimer);
119
+ reconnectTimer = undefined;
120
+ }
121
+ activeRelaySocket = relaySocket;
122
+ sendToRelay(relaySocket, hostIpcMessage({
123
+ type: "host.register",
124
+ roomName: store.roomName,
125
+ store: roomStoreSnapshot(),
126
+ appRef: options.appRef,
127
+ appCwd: options.appCwd,
128
+ relayHints: [store.relayAddress, ...args.relayPeers].filter(Boolean)
129
+ }));
130
+ });
131
+ relaySocket.on("message", (data) => {
132
+ const message = receiveRelayMessage(data);
133
+ if (message) handleRelayMessage(relaySocket, message);
134
+ });
135
+ relaySocket.on("close", () => {
136
+ if (activeRelaySocket === relaySocket) activeRelaySocket = undefined;
137
+ if (reconnectRelay) {
138
+ logger.error("Local relay IPC closed. Reconnecting when the relay is available.");
139
+ scheduleRelayReconnect();
140
+ } else {
141
+ logger.error("Local relay IPC closed. Stop this host or restart it after the relay is available.");
142
+ }
143
+ });
144
+ relaySocket.on("error", (error) => {
145
+ logger.error("Local relay IPC error:", error);
146
+ });
147
+ }
148
+
149
+ return {
150
+ attachRelaySocket,
151
+ connectionRoles: clients.connectionRoles,
152
+ handleClientMessage: clients.handleClientMessage,
153
+ handleRelayMessage,
154
+ pendingJoinConnections: clients.pendingJoinConnections
155
+ };
156
+ }
157
+
158
+ module.exports = {
159
+ createHostSession
160
+ };
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ function sanitizeProgressText(value) {
4
+ return String(value || "").replace(/[\r\n\t]+/g, " ").slice(0, 80);
5
+ }
6
+
7
+ function normalizeCount(value, fallback = 0) {
8
+ return Number.isFinite(value) ? value : fallback;
9
+ }
10
+
11
+ function normalizeProgressState(update = {}, previous = {}) {
12
+ const total = normalizeCount(update.total, normalizeCount(previous.total, 0));
13
+ const rawCompleted = Object.prototype.hasOwnProperty.call(update, "completed")
14
+ ? update.completed
15
+ : update.progress;
16
+ const completed = normalizeCount(rawCompleted, normalizeCount(previous.completed, 0));
17
+ const currentItem = typeof update.currentItem === "string"
18
+ ? sanitizeProgressText(update.currentItem)
19
+ : (previous.currentItem || "");
20
+
21
+ return {
22
+ completed: Math.max(0, total > 0 ? Math.min(completed, total) : completed),
23
+ total: Math.max(0, total),
24
+ currentItem
25
+ };
26
+ }
27
+
28
+ function progressRatio(done, total) {
29
+ if (!Number.isFinite(total) || total <= 0) return 0;
30
+ const safeDone = Math.max(0, Math.min(done, total));
31
+ return safeDone / total;
32
+ }
33
+
34
+ function formatProgressPercent(done, total) {
35
+ if (!Number.isFinite(total) || total <= 0) return "";
36
+ const percent = progressRatio(done, total) * 100;
37
+ return Number.isInteger(percent) ? `${percent}%` : `${percent.toFixed(1)}%`;
38
+ }
39
+
40
+ function renderProgressBar(done, total, width = 24) {
41
+ if (!Number.isFinite(total) || total <= 0) return "";
42
+ const safeWidth = Math.max(0, Math.floor(Number.isFinite(width) ? width : 24));
43
+ if (safeWidth <= 0) return "[]";
44
+
45
+ const filled = Math.max(0, Math.min(safeWidth, Math.floor(progressRatio(done, total) * safeWidth)));
46
+ const percentText = formatProgressPercent(done, total);
47
+ const label = percentText.length > safeWidth ? percentText.slice(0, safeWidth) : percentText;
48
+ const labelStart = Math.max(0, Math.floor((safeWidth - label.length) / 2));
49
+ let content = "";
50
+
51
+ for (let index = 0; index < safeWidth; index += 1) {
52
+ const labelIndex = index - labelStart;
53
+ if (labelIndex >= 0 && labelIndex < label.length) {
54
+ content += label[labelIndex];
55
+ } else {
56
+ content += index < filled ? "=" : " ";
57
+ }
58
+ }
59
+
60
+ return `[${content}]`;
61
+ }
62
+
63
+ function renderProgressLine(message, state, options = {}) {
64
+ const progress = normalizeProgressState(state);
65
+ const width = Number.isFinite(options.width) ? options.width : 24;
66
+ const bar = renderProgressBar(progress.completed, progress.total, width);
67
+ const count = progress.total > 0
68
+ ? `${Math.min(progress.completed, progress.total)}/${progress.total}`
69
+ : String(progress.completed);
70
+ const item = progress.currentItem ? ` ${progress.currentItem}` : "";
71
+ return `${message} ${bar ? `${bar} ` : ""}${count}${item}`;
72
+ }
73
+
74
+ function createNoopProgressLoader() {
75
+ return {
76
+ start() {},
77
+ update() {},
78
+ stop() {}
79
+ };
80
+ }
81
+
82
+ function createInlineProgressLoader(message, options = {}) {
83
+ const stream = options.stream || process.stderr;
84
+ const enabled = options.enabled !== undefined
85
+ ? options.enabled
86
+ : Boolean(stream?.isTTY);
87
+
88
+ if (!enabled || !stream || typeof stream.write !== "function") {
89
+ return createNoopProgressLoader();
90
+ }
91
+
92
+ const width = Number.isFinite(options.width) ? options.width : 24;
93
+ let state = normalizeProgressState();
94
+ let lastLine = "";
95
+ const clearOnStop = options.clearOnStop !== false;
96
+ const newlineOnStop = options.newlineOnStop === true;
97
+
98
+ function draw() {
99
+ const line = renderProgressLine(message, state, { width });
100
+ const clearLength = Math.max(0, lastLine.length - line.length);
101
+ stream.write(`\r${line}${" ".repeat(clearLength)}`);
102
+ lastLine = line;
103
+ }
104
+
105
+ return {
106
+ start() {
107
+ draw();
108
+ },
109
+ update(update) {
110
+ state = normalizeProgressState(update, state);
111
+ draw();
112
+ },
113
+ stop() {
114
+ if (clearOnStop && lastLine) stream.write("\r\x1b[2K");
115
+ if (newlineOnStop) stream.write("\n");
116
+ lastLine = "";
117
+ }
118
+ };
119
+ }
120
+
121
+ module.exports = {
122
+ createInlineProgressLoader,
123
+ formatProgressPercent,
124
+ normalizeProgressState,
125
+ renderProgressBar,
126
+ renderProgressLine,
127
+ sanitizeProgressText
128
+ };
@@ -0,0 +1,114 @@
1
+ const { setTimeout: delay } = require("node:timers/promises");
2
+
3
+ const DEFAULT_LOCAL_PEERJS_HOST = "127.0.0.1";
4
+ const DEFAULT_LOCAL_PEERJS_PORT = 9000;
5
+ const DEFAULT_LOCAL_PEERJS_PATH = "/peerjs";
6
+
7
+ function localPeerServerConfig(options = {}) {
8
+ return {
9
+ host: options.host || process.env.MATTERHORN_PEERJS_LOCAL_HOST || DEFAULT_LOCAL_PEERJS_HOST,
10
+ port: Number(options.port || process.env.MATTERHORN_PEERJS_LOCAL_PORT || DEFAULT_LOCAL_PEERJS_PORT),
11
+ path: options.path || process.env.MATTERHORN_PEERJS_LOCAL_PATH || DEFAULT_LOCAL_PEERJS_PATH,
12
+ secure: false
13
+ };
14
+ }
15
+
16
+ function applyPeerServerEnv(peerServer) {
17
+ process.env.MATTERHORN_PEERJS_HOST = peerServer.host;
18
+ process.env.MATTERHORN_PEERJS_PORT = String(peerServer.port);
19
+ process.env.MATTERHORN_PEERJS_PATH = peerServer.path;
20
+ process.env.MATTERHORN_PEERJS_SECURE = String(peerServer.secure);
21
+ }
22
+
23
+ function peerServerUrl(config) {
24
+ return `http://${config.host}:${config.port}${config.path}`;
25
+ }
26
+
27
+ function peerServerIdUrl(config) {
28
+ const base = peerServerUrl(config).replace(/\/$/, "");
29
+ return `${base}/peerjs/id`;
30
+ }
31
+
32
+ async function waitForPeerServer(config, options = {}) {
33
+ const timeoutMs = Number(options.timeoutMs || 5000);
34
+ const deadline = Date.now() + timeoutMs;
35
+ let lastError;
36
+ while (Date.now() < deadline) {
37
+ try {
38
+ const response = await fetch(peerServerIdUrl(config));
39
+ if (response.ok) return true;
40
+ lastError = new Error(`PeerJS health returned ${response.status}`);
41
+ } catch (error) {
42
+ lastError = error;
43
+ }
44
+ await delay(100);
45
+ }
46
+ throw lastError || new Error("Timed out waiting for local PeerJS server");
47
+ }
48
+
49
+ function closePeerServer(server) {
50
+ const target = server?.close ? server : server?._server;
51
+ if (!target?.close) return Promise.resolve();
52
+ return new Promise((resolve) => target.close(() => resolve()));
53
+ }
54
+
55
+ async function startLocalPeerServer(options = {}) {
56
+ const config = localPeerServerConfig(options);
57
+ if (options.reuseExisting !== false) {
58
+ try {
59
+ await waitForPeerServer(config, { timeoutMs: options.reuseTimeoutMs || 300 });
60
+ applyPeerServerEnv(config);
61
+ return {
62
+ ...config,
63
+ url: peerServerUrl(config),
64
+ reused: true,
65
+ server: undefined,
66
+ close: () => Promise.resolve()
67
+ };
68
+ } catch {}
69
+ }
70
+
71
+ let PeerServer;
72
+ try {
73
+ ({ PeerServer } = require("peer"));
74
+ } catch (error) {
75
+ throw new Error(`The local PeerJS server requires the peer package. Run pnpm install once at the Matterhorn workspace root. ${error.message}`);
76
+ }
77
+ if (typeof PeerServer !== "function") throw new Error("The peer package did not export PeerServer.");
78
+ const server = PeerServer({
79
+ host: config.host,
80
+ port: config.port,
81
+ path: config.path,
82
+ proxied: false
83
+ });
84
+ let startupError;
85
+ server?.once?.("error", (error) => {
86
+ startupError = error;
87
+ });
88
+ applyPeerServerEnv(config);
89
+ try {
90
+ await waitForPeerServer(config, options);
91
+ } catch (error) {
92
+ await closePeerServer(server).catch(() => undefined);
93
+ throw startupError || error;
94
+ }
95
+ return {
96
+ ...config,
97
+ url: peerServerUrl(config),
98
+ reused: false,
99
+ server,
100
+ close: () => closePeerServer(server)
101
+ };
102
+ }
103
+
104
+ module.exports = {
105
+ DEFAULT_LOCAL_PEERJS_HOST,
106
+ DEFAULT_LOCAL_PEERJS_PATH,
107
+ DEFAULT_LOCAL_PEERJS_PORT,
108
+ applyPeerServerEnv,
109
+ localPeerServerConfig,
110
+ peerServerIdUrl,
111
+ peerServerUrl,
112
+ startLocalPeerServer,
113
+ waitForPeerServer
114
+ };
@@ -0,0 +1,151 @@
1
+ const { spawn } = require("node:child_process");
2
+ const path = require("node:path");
3
+ const { ensureRelayConfig } = require("./relayConfig.cjs");
4
+ const { startLocalPeerServer } = require("./localPeerServer.cjs");
5
+
6
+ const DEFAULT_RELAY_START_TIMEOUT_MS = 6000;
7
+
8
+ function delay(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ function relayIpcUrl(config) {
13
+ return `ws://${config.ipcHost}:${config.ipcPort}/room`;
14
+ }
15
+
16
+ function relayHealthUrl(config) {
17
+ return `http://${config.ipcHost}:${config.ipcPort}/health`;
18
+ }
19
+
20
+ async function fetchRelayInfo(config, fetcher = fetch) {
21
+ const response = await fetcher(relayHealthUrl(config));
22
+ if (!response.ok) throw new Error(`Relay health returned ${response.status}`);
23
+ const info = await response.json();
24
+ if (info.protocol !== "matterhorn-peer-relay") throw new Error("Local process is not a matterhorn relay");
25
+ if (info.roomPeerOpen === false) throw new Error("Local relay room peer is not live");
26
+ return info;
27
+ }
28
+
29
+ async function waitForRelay(config, timeoutMs = DEFAULT_RELAY_START_TIMEOUT_MS) {
30
+ const deadline = Date.now() + timeoutMs;
31
+ let lastError;
32
+
33
+ while (Date.now() < deadline) {
34
+ try {
35
+ return await fetchRelayInfo(config);
36
+ } catch (error) {
37
+ lastError = error;
38
+ await delay(250);
39
+ }
40
+ }
41
+
42
+ throw lastError || new Error("Timed out waiting for local relay");
43
+ }
44
+
45
+ function spawnRelayProcess(config, options = {}) {
46
+ const relayPath = path.join(__dirname, "relay.cjs");
47
+ const args = [
48
+ relayPath,
49
+ "--ipc-host",
50
+ config.ipcHost,
51
+ "--ipc-port",
52
+ String(config.ipcPort),
53
+ "--relay-peer-id",
54
+ config.relayPeerId
55
+ ];
56
+
57
+ for (const relayPeer of options.relayPeers || []) {
58
+ args.push("--relay-peer", relayPeer);
59
+ }
60
+ if (options.iceServers) {
61
+ args.push("--ice", options.iceServers);
62
+ }
63
+
64
+ const env = { ...process.env };
65
+ if (options.dataDir) env.MATTERHORN_HOME = options.dataDir;
66
+ if (options.localPeerjs) env.MATTERHORN_PEERJS_LOCAL = "1";
67
+
68
+ const child = spawn(process.execPath, args, {
69
+ detached: true,
70
+ stdio: "ignore",
71
+ windowsHide: true,
72
+ env
73
+ });
74
+ child.unref();
75
+ return child;
76
+ }
77
+
78
+ function parseIceServers(value) {
79
+ if (Array.isArray(value)) return value.length > 0 ? value : undefined;
80
+ if (!value) return undefined;
81
+ return String(value || "")
82
+ .split(",")
83
+ .map((item) => item.trim())
84
+ .filter(Boolean)
85
+ .map((urls) => ({ urls }));
86
+ }
87
+
88
+ async function startEmbeddedRelay(config, options = {}) {
89
+ const { createMatterhornRelay } = require("./relay.cjs");
90
+ const relay = createMatterhornRelay({
91
+ dataDir: options.dataDir,
92
+ ipcHost: config.ipcHost,
93
+ ipcPort: config.ipcPort,
94
+ storagePath: options.storagePath,
95
+ relayPeerId: config.relayPeerId,
96
+ relayPeers: options.relayPeers || [],
97
+ iceServers: parseIceServers(options.iceServers),
98
+ startPeer: options.startPeer,
99
+ authenticateActor: options.authenticateActor,
100
+ peerJsSignaling: options.peerJsSignaling
101
+ });
102
+ await relay.listen();
103
+ config.ipcPort = relay.config.ipcPort;
104
+ const info = await fetchRelayInfo(config);
105
+ return { relay, info };
106
+ }
107
+
108
+ async function ensureLocalRelay(options = {}) {
109
+ let localPeerServer;
110
+ if (options.localPeerjs) {
111
+ localPeerServer = await startLocalPeerServer(options.localPeerjs === true ? {} : options.localPeerjs);
112
+ options = {
113
+ ...options,
114
+ peerJsSignaling: {
115
+ host: localPeerServer.host,
116
+ port: localPeerServer.port,
117
+ path: localPeerServer.path,
118
+ secure: localPeerServer.secure
119
+ }
120
+ };
121
+ }
122
+
123
+ const config = ensureRelayConfig({
124
+ ...options,
125
+ forcePeerJsSignaling: Boolean(options.localPeerjs),
126
+ ipcHost: options.ipcHost || process.env.MATTERHORN_RELAY_IPC_HOST,
127
+ ipcPort: options.ipcPort ?? (process.env.MATTERHORN_RELAY_IPC_PORT ? Number(process.env.MATTERHORN_RELAY_IPC_PORT) : undefined)
128
+ });
129
+ try {
130
+ const info = await fetchRelayInfo(config);
131
+ return { config, info, localPeerServer, spawned: false };
132
+ } catch (error) {
133
+ try {
134
+ const embedded = await startEmbeddedRelay(config, options);
135
+ return { config, info: embedded.info, relay: embedded.relay, localPeerServer, spawned: true };
136
+ } catch (startError) {
137
+ await localPeerServer?.close?.().catch(() => undefined);
138
+ throw startError || error;
139
+ }
140
+ }
141
+ }
142
+
143
+ module.exports = {
144
+ ensureLocalRelay,
145
+ fetchRelayInfo,
146
+ relayHealthUrl,
147
+ relayIpcUrl,
148
+ spawnRelayProcess,
149
+ startEmbeddedRelay,
150
+ waitForRelay
151
+ };
@@ -0,0 +1,75 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+
5
+ function userDataDir() {
6
+ return process.env.MATTERHORN_HOME || path.join(os.homedir(), ".matterhorn");
7
+ }
8
+
9
+ function usermatterhornrcFile() {
10
+ return path.join(userDataDir(), ".matterhornrc");
11
+ }
12
+
13
+ function parseKeyValueText(text) {
14
+ const values = {};
15
+ for (const rawLine of text.split(/\r?\n/)) {
16
+ const line = rawLine.trim();
17
+ if (!line || line.startsWith("#")) continue;
18
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.*)$/.exec(line);
19
+ if (!match) continue;
20
+ values[match[1]] = match[2].trim().replace(/^["']|["']$/g, "");
21
+ }
22
+ return values;
23
+ }
24
+
25
+ function normalizematterhornrc(values) {
26
+ const host = values.host || values.HOST_URL || values.appUrl || values.app_url;
27
+ const normalized = host ? { host: String(host).trim() } : {};
28
+ const players = Array.isArray(values.players) ? values.players : values.playerPacks;
29
+ if (Array.isArray(players)) {
30
+ normalized.players = players
31
+ .filter((player) => player && typeof player === "object" && typeof player.url === "string")
32
+ .map((player) => ({
33
+ url: player.url,
34
+ ...(player.integrity === undefined ? {} : { integrity: String(player.integrity) }),
35
+ ...(player.id === undefined ? {} : { id: String(player.id) }),
36
+ ...(player.name === undefined ? {} : { name: String(player.name) }),
37
+ ...(player.version === undefined ? {} : { version: String(player.version) }),
38
+ ...(player.addedAt === undefined ? {} : { addedAt: String(player.addedAt) })
39
+ }));
40
+ }
41
+ return normalized;
42
+ }
43
+
44
+ function parsematterhornrc(text) {
45
+ try {
46
+ return normalizematterhornrc(JSON.parse(text));
47
+ } catch {
48
+ return normalizematterhornrc(parseKeyValueText(text));
49
+ }
50
+ }
51
+
52
+ function readmatterhornrc(file = usermatterhornrcFile()) {
53
+ if (!fs.existsSync(file)) return {};
54
+ return parsematterhornrc(fs.readFileSync(file, "utf8"));
55
+ }
56
+
57
+ function writematterhornrc(values, file = usermatterhornrcFile()) {
58
+ fs.mkdirSync(path.dirname(file), { recursive: true });
59
+ fs.writeFileSync(file, `${JSON.stringify(values, null, 2)}\n`);
60
+ }
61
+
62
+ function applymatterhornrcDefaults(target = process.env, file = usermatterhornrcFile()) {
63
+ const values = readmatterhornrc(file);
64
+ if (values.host && target.APP_URL === undefined && target.HOST_URL === undefined) {
65
+ target.HOST_URL = values.host;
66
+ }
67
+ }
68
+
69
+ module.exports = {
70
+ applymatterhornrcDefaults,
71
+ readmatterhornrc,
72
+ userDataDir,
73
+ usermatterhornrcFile,
74
+ writematterhornrc
75
+ };