@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
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @mh-gg/cli
2
+
3
+ Public Matterhorn CLI package used by the `matterhorn` npm bin.
4
+
5
+ The package keeps local development on the monorepo CLI source and prepares a packaged runtime copy during `prepack`; release checks use `pack:verify`.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require("node:child_process");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ function localWorkspaceCli() {
8
+ const candidate = path.resolve(__dirname, "..", "..", "..", "bin", "matterhorn.cjs");
9
+ return fs.existsSync(candidate) ? candidate : "";
10
+ }
11
+
12
+ function packagedCli() {
13
+ const candidate = path.resolve(__dirname, "..", "runtime", "bin", "matterhorn.cjs");
14
+ return fs.existsSync(candidate) ? candidate : "";
15
+ }
16
+
17
+ function resolveCliPath() {
18
+ return localWorkspaceCli() || packagedCli();
19
+ }
20
+
21
+ function runNodeScript(script, args) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(process.execPath, [script, ...args], {
24
+ stdio: "inherit",
25
+ windowsHide: true
26
+ });
27
+ child.on("exit", (code, signal) => {
28
+ if (signal) {
29
+ process.kill(process.pid, signal);
30
+ return;
31
+ }
32
+ resolve(code ?? 0);
33
+ });
34
+ child.on("error", reject);
35
+ });
36
+ }
37
+
38
+ async function main(args = process.argv.slice(2)) {
39
+ const cliPath = resolveCliPath();
40
+ if (!cliPath) {
41
+ throw new Error("Matterhorn CLI runtime is missing. Reinstall @mh-gg/cli or run pnpm --dir packages/matterhorn-cli run prepare:files.");
42
+ }
43
+ const code = await runNodeScript(cliPath, args);
44
+ process.exitCode = code;
45
+ }
46
+
47
+ if (require.main === module) {
48
+ main().catch((error) => {
49
+ console.error(error.message || error);
50
+ process.exit(1);
51
+ });
52
+ }
53
+
54
+ module.exports = {
55
+ main,
56
+ resolveCliPath
57
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@mh-gg/cli",
3
+ "version": "0.1.1-alpha.20260613T085325975Z",
4
+ "description": "Matterhorn command-line interface for hosting, deploying, and launching Matterhorn apps.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "matterhorn": "./bin/matterhorn.cjs"
8
+ },
9
+ "exports": {
10
+ "./bin/matterhorn.cjs": "./bin/matterhorn.cjs",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "runtime",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@roamhq/wrtc": "^0.10.0",
20
+ "nostr-tools": "^2.23.5",
21
+ "peer": "^1.0.2",
22
+ "peerjs": "^1.5.5",
23
+ "qrcode-terminal": "^0.12.0",
24
+ "ws": "^8.18.0",
25
+ "xhr2": "^0.2.1",
26
+ "@mh-gg/host-config": "^0.1.1-alpha.20260613T085325975Z",
27
+ "@mh-gg/base-plugins": "^0.1.1-alpha.20260613T085325975Z",
28
+ "@mh-gg/base": "^0.1.1-alpha.20260613T085325975Z",
29
+ "@mh-gg/host-store": "^0.1.1-alpha.20260613T085325975Z",
30
+ "@mh-gg/event": "^0.1.1-alpha.20260613T085325975Z",
31
+ "@mh-gg/protocol": "^0.1.1-alpha.20260613T085325975Z",
32
+ "@mh-gg/host-runtime": "^0.1.1-alpha.20260613T085325975Z",
33
+ "@mh-gg/host-ipc": "^0.1.1-alpha.20260613T085325975Z",
34
+ "@mh-gg/schema": "^0.1.1-alpha.20260613T085325975Z",
35
+ "@mh-gg/relay-mesh": "^0.1.1-alpha.20260613T085325975Z",
36
+ "@mh-gg/relay-core": "^0.1.1-alpha.20260613T085325975Z",
37
+ "@mh-gg/room-security": "^0.1.1-alpha.20260613T085325975Z",
38
+ "@mh-gg/relay-runtime": "^0.1.1-alpha.20260613T085325975Z",
39
+ "@mh-gg/room-link": "^0.1.1-alpha.20260613T085325975Z"
40
+ },
41
+ "engines": {
42
+ "node": ">=22.12"
43
+ },
44
+ "scripts": {
45
+ "prepare:files": "node scripts/prepare-files.cjs",
46
+ "test:package": "node scripts/smoke-package.cjs",
47
+ "pack:verify": "pnpm run prepare:files && pnpm run test:package"
48
+ }
49
+ }
@@ -0,0 +1,25 @@
1
+ const fs = require("node:fs");
2
+ const { createHash } = require("node:crypto");
3
+ const { bundleDistDir, bundleEntryFile, bundleEntryName, bundleMountPath } = require("./paths.cjs");
4
+
5
+ function fileIntegrity(filePath) {
6
+ return `sha256-${createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
7
+ }
8
+
9
+ function appBundleArtifact(frontend) {
10
+ const file = bundleEntryFile(frontend);
11
+ const stat = fs.statSync(file);
12
+ if (!stat.isFile()) throw new Error(`Built app bundle is missing ${file}`);
13
+ return {
14
+ file,
15
+ entry: bundleEntryName(frontend),
16
+ distDir: bundleDistDir(frontend),
17
+ mountPath: bundleMountPath(frontend),
18
+ byteLength: stat.size,
19
+ integrity: fileIntegrity(file)
20
+ };
21
+ }
22
+
23
+ module.exports = {
24
+ appBundleArtifact
25
+ };
@@ -0,0 +1,176 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { appBundleArtifact } = require("./artifacts.cjs");
4
+ const { bundleConfig, bundleDistDir, bundleEntryFile, bundleMountPath, frontendRoot, launcherBuildCommand, repoRoot } = require("./paths.cjs");
5
+ const { commandSpec, runCommand } = require("./processes.cjs");
6
+ const { assertAppCommandAllowed, commandEnvTrustForPolicy } = require("./commandPolicy.cjs");
7
+ const { selectFrontendPort } = require("./ports.cjs");
8
+ const { startBundleStaticFrontend, startBundledStaticFrontend, startStaticFrontend, waitForHttpOk } = require("./httpServers.cjs");
9
+ const { ensureWorkspaceSdkBuild } = require("../../scripts/ensureWorkspaceSdkBuild.cjs");
10
+
11
+ const DEFAULT_FRONTEND_BUILD_TIMEOUT_MS = 120000;
12
+
13
+ function isIgnoredSourcePath(candidate, ignoredRoots) {
14
+ const base = path.basename(candidate);
15
+ if (base === "node_modules" || base === ".git" || base === ".matterhorn-live" || base === ".matterhorn-live-objects" || base === "coverage") return true;
16
+ const resolved = path.resolve(candidate);
17
+ return ignoredRoots.some((ignoredRoot) => {
18
+ const relative = path.relative(ignoredRoot, resolved);
19
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
20
+ });
21
+ }
22
+
23
+ function latestSourceMtime(root, ignoredRoots = []) {
24
+ if (!root || !fs.existsSync(root)) return 0;
25
+ const resolvedIgnoredRoots = ignoredRoots.filter(Boolean).map((item) => path.resolve(item));
26
+ const pending = [path.resolve(root)];
27
+ let latest = 0;
28
+
29
+ while (pending.length > 0) {
30
+ const current = pending.pop();
31
+ if (!current || isIgnoredSourcePath(current, resolvedIgnoredRoots)) continue;
32
+ let stat;
33
+ try {
34
+ stat = fs.statSync(current);
35
+ } catch {
36
+ continue;
37
+ }
38
+ if (!stat.isDirectory()) {
39
+ if (stat.isFile()) latest = Math.max(latest, stat.mtimeMs);
40
+ continue;
41
+ }
42
+ let entries;
43
+ try {
44
+ entries = fs.readdirSync(current, { withFileTypes: true });
45
+ } catch {
46
+ continue;
47
+ }
48
+ for (const entry of entries) {
49
+ const entryPath = path.join(current, entry.name);
50
+ if (isIgnoredSourcePath(entryPath, resolvedIgnoredRoots)) continue;
51
+ if (entry.isDirectory()) {
52
+ pending.push(entryPath);
53
+ } else if (entry.isFile()) {
54
+ try {
55
+ latest = Math.max(latest, fs.statSync(entryPath).mtimeMs);
56
+ } catch {}
57
+ }
58
+ }
59
+ }
60
+
61
+ return latest;
62
+ }
63
+
64
+ function workspaceSdkBuildInputs() {
65
+ const sdkRoot = path.join(repoRoot(), "packages", "matterhorn-sdk");
66
+ return [
67
+ path.join(sdkRoot, "dist"),
68
+ path.join(sdkRoot, "package.json")
69
+ ].filter((target) => fs.existsSync(target));
70
+ }
71
+
72
+ function appBundleBuildRequired(frontend) {
73
+ const bundleFile = bundleEntryFile(frontend);
74
+ if (!fs.existsSync(bundleFile)) return true;
75
+ let bundleMtime = 0;
76
+ try {
77
+ const stat = fs.statSync(bundleFile);
78
+ if (!stat.isFile()) return true;
79
+ bundleMtime = stat.mtimeMs;
80
+ } catch {
81
+ return true;
82
+ }
83
+
84
+ const ignoredRoots = [bundleDistDir(frontend)];
85
+ const inputs = [frontendRoot(frontend), ...workspaceSdkBuildInputs()];
86
+ return inputs.some((input) => latestSourceMtime(input, ignoredRoots) > bundleMtime);
87
+ }
88
+
89
+
90
+ async function startBuiltFrontend(frontend, options = {}) {
91
+ const root = frontendRoot(frontend);
92
+ const port = await selectFrontendPort(options.port, frontend.defaultPort);
93
+ if (frontend.build) {
94
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn frontend build");
95
+ await runCommand(commandSpec(frontend.build, "build"), {
96
+ cwd: root,
97
+ label: "matterhorn frontend build",
98
+ logger: options.logger,
99
+ port,
100
+ trust: commandEnvTrustForPolicy(policy),
101
+ timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
102
+ });
103
+ }
104
+ const server = await startStaticFrontend(frontend, { port });
105
+ await waitForHttpOk(server.appUrl, options.timeoutMs);
106
+ return server;
107
+ }
108
+
109
+ async function startBundledBuiltFrontend(frontend, options = {}) {
110
+ const root = frontendRoot(frontend);
111
+ const port = await selectFrontendPort(options.port, frontend.defaultPort);
112
+ const bundle = bundleConfig(frontend);
113
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn bundled frontend build");
114
+ await ensureWorkspaceSdkBuild({ logger: options.logger, timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS });
115
+ await runCommand(commandSpec(bundle.build, "bundle.build"), {
116
+ cwd: root,
117
+ label: "matterhorn app bundle build",
118
+ logger: options.logger,
119
+ port,
120
+ trust: commandEnvTrustForPolicy(policy),
121
+ timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
122
+ });
123
+ await runCommand(commandSpec(frontend.launcher?.build || launcherBuildCommand(), "launcher.build"), {
124
+ cwd: repoRoot(),
125
+ label: "matterhorn launcher build",
126
+ logger: options.logger,
127
+ port,
128
+ trust: commandEnvTrustForPolicy(policy),
129
+ timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
130
+ });
131
+ const server = await startBundledStaticFrontend(frontend, { port });
132
+ await waitForHttpOk(server.appUrl, options.timeoutMs);
133
+ await waitForHttpOk(server.bundleUrl, options.timeoutMs);
134
+ return server;
135
+ }
136
+
137
+ async function buildAppBundleFrontend(appOrFrontend, options = {}) {
138
+ const frontend = appOrFrontend?.frontend || appOrFrontend;
139
+ if (!frontend?.bundle) throw new Error("App does not declare a bundled frontend");
140
+ const root = frontendRoot(frontend);
141
+ const port = options.port ?? options.bundlePort ?? frontend.bundle.defaultPort ?? 0;
142
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust || appOrFrontend?.trust, "matterhorn app bundle build");
143
+ await ensureWorkspaceSdkBuild({ logger: options.logger, timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS });
144
+ if (!options.force && !appBundleBuildRequired(frontend)) {
145
+ options.logger?.log?.(`matterhorn app bundle build skipped: ${bundleEntryFile(frontend)} is current`);
146
+ return appBundleArtifact(frontend);
147
+ }
148
+ await runCommand(commandSpec(frontend.bundle.build, "bundle.build"), {
149
+ cwd: root,
150
+ label: "matterhorn app bundle build",
151
+ logger: options.logger,
152
+ port,
153
+ bundlePort: options.bundlePort ?? port,
154
+ mountPath: bundleMountPath(frontend),
155
+ trust: commandEnvTrustForPolicy(policy),
156
+ timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
157
+ });
158
+ return appBundleArtifact(frontend);
159
+ }
160
+
161
+ async function startBundleBuiltFrontend(frontend, options = {}) {
162
+ const bundle = bundleConfig(frontend);
163
+ const port = await selectFrontendPort(options.port ?? options.bundlePort, bundle.defaultPort);
164
+ await buildAppBundleFrontend(frontend, { ...options, port, bundlePort: port });
165
+ const server = await startBundleStaticFrontend(frontend, { port });
166
+ await waitForHttpOk(server.bundleUrl, options.timeoutMs);
167
+ return server;
168
+ }
169
+
170
+ module.exports = {
171
+ appBundleBuildRequired,
172
+ buildAppBundleFrontend,
173
+ startBundleBuiltFrontend,
174
+ startBuiltFrontend,
175
+ startBundledBuiltFrontend
176
+ };
@@ -0,0 +1,74 @@
1
+ const MATTERHORN_PUBLIC_KEYS = new Set([
2
+ "MATTERHORN_APP_BUNDLE_MOUNT",
3
+ "MATTERHORN_APP_BUNDLE_TARGET",
4
+ "MATTERHORN_BUNDLE_BASE",
5
+ "MATTERHORN_BUNDLE_PORT",
6
+ "MATTERHORN_FRONTEND_PORT"
7
+ ]);
8
+
9
+ function baseProcessEnv() {
10
+ const env = {};
11
+ for (const key of ["PATH", "Path", "ComSpec", "SystemRoot", "WINDIR", "TEMP", "TMP", "HOME", "USERPROFILE"]) {
12
+ if (process.env[key] !== undefined) env[key] = process.env[key];
13
+ }
14
+ return env;
15
+ }
16
+
17
+ function sensitiveEnvKey(key) {
18
+ const upper = String(key).toUpperCase();
19
+ if (MATTERHORN_PUBLIC_KEYS.has(upper)) return false;
20
+ if (upper === "NODE_OPTIONS" || upper === "SSH_AUTH_SOCK") return true;
21
+ if (upper.startsWith("AWS_") || upper.startsWith("GOOGLE_") || upper.startsWith("AZURE_")) return true;
22
+ return /(?:^|_)(?:TOKEN|SECRET|PASSWORD|KEY)$/.test(upper);
23
+ }
24
+
25
+ function validateCommandEnv(commandEnv = {}, options = {}) {
26
+ for (const key of Object.keys(commandEnv || {})) {
27
+ if (!sensitiveEnvKey(key)) continue;
28
+ if (options.trust === "trusted-local-dev" && options.allowSensitiveCommandEnv === true) continue;
29
+ throw new Error(`App command env key ${key} is not allowed by Matterhorn command env policy`);
30
+ }
31
+ }
32
+
33
+ function commandPortEnv(input = {}) {
34
+ const env = {};
35
+ if (input.port !== undefined) {
36
+ env.PORT = String(input.port);
37
+ env.MATTERHORN_FRONTEND_PORT = String(input.port);
38
+ }
39
+ if (input.bundlePort !== undefined) env.MATTERHORN_BUNDLE_PORT = String(input.bundlePort);
40
+ if (input.mountPath !== undefined) env.MATTERHORN_BUNDLE_BASE = String(input.mountPath || "/");
41
+ return env;
42
+ }
43
+
44
+ function canUnsafeInherit(input) {
45
+ if (input.trust !== "trusted-local-dev") return false;
46
+ return input.unsafeInheritEnv === true || process.env.MATTERHORN_UNSAFE_APP_FULL_ENV === "1";
47
+ }
48
+
49
+ function buildMatterhornCommandEnv(input = {}) {
50
+ const trust = input.trust || "untrusted-metadata";
51
+ const commandEnv = input.commandEnv || {};
52
+ validateCommandEnv(commandEnv, {
53
+ trust,
54
+ allowSensitiveCommandEnv: input.allowSensitiveCommandEnv
55
+ });
56
+ return {
57
+ ...(canUnsafeInherit({ ...input, trust }) ? process.env : baseProcessEnv()),
58
+ ...commandPortEnv(input),
59
+ ...commandEnv
60
+ };
61
+ }
62
+
63
+ const SECRET_VALUE_PATTERN = /(?:npm|github|ghp|aws|secret|token|password|key)[A-Za-z0-9_./+=:-]{8,}/gi;
64
+
65
+ function redactCommandOutput(value) {
66
+ return String(value).replace(SECRET_VALUE_PATTERN, "[REDACTED]");
67
+ }
68
+
69
+ module.exports = {
70
+ buildMatterhornCommandEnv,
71
+ redactCommandOutput,
72
+ sensitiveEnvKey,
73
+ validateCommandEnv
74
+ };
@@ -0,0 +1,23 @@
1
+ const RUNNABLE_COMMAND_POLICIES = new Set(["allowlisted", "sandboxed", "trusted-local-dev"]);
2
+
3
+ function commandPolicyFromTrust(trust) {
4
+ return trust?.commandExecution || "trusted-local-dev";
5
+ }
6
+
7
+ function commandEnvTrustForPolicy(policy) {
8
+ if (policy === "trusted-local-dev") return "trusted-local-dev";
9
+ if (policy === "allowlisted" || policy === "sandboxed") return "trusted-installed";
10
+ return "untrusted-metadata";
11
+ }
12
+
13
+ function assertAppCommandAllowed(trust, label) {
14
+ const policy = commandPolicyFromTrust(trust);
15
+ if (RUNNABLE_COMMAND_POLICIES.has(policy)) return policy;
16
+ throw new Error(`${label} requires app command execution trust; metadata-only app sources cannot run frontend commands`);
17
+ }
18
+
19
+ module.exports = {
20
+ assertAppCommandAllowed,
21
+ commandEnvTrustForPolicy,
22
+ commandPolicyFromTrust
23
+ };
@@ -0,0 +1,150 @@
1
+ const { bundleConfig, bundleMountPath, bundleUrl, frontendRoot, frontendUrl, launcherDevCommand, repoRoot } = require("./paths.cjs");
2
+ const { closeChildProcess, commandSpec, spawnCommand } = require("./processes.cjs");
3
+ const { assertAppCommandAllowed, commandEnvTrustForPolicy } = require("./commandPolicy.cjs");
4
+ const { selectFrontendPort } = require("./ports.cjs");
5
+ const { waitForHttpOk } = require("./httpServers.cjs");
6
+
7
+ async function startDevFrontend(frontend, options = {}) {
8
+ const root = frontendRoot(frontend);
9
+ const port = await selectFrontendPort(options.port, frontend.defaultPort);
10
+ const spec = commandSpec(frontend.dev, "dev");
11
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn frontend dev");
12
+ const child = spawnCommand(spec, {
13
+ cwd: root,
14
+ label: "matterhorn frontend dev",
15
+ logger: options.logger,
16
+ port,
17
+ trust: commandEnvTrustForPolicy(policy)
18
+ });
19
+ const appUrl = frontendUrl(port, frontend.devBasePath || frontend.basePath || "/");
20
+ try {
21
+ await waitForHttpOk(new URL(frontend.dev?.healthPath || "/", appUrl).toString(), options.timeoutMs);
22
+ } catch (error) {
23
+ await closeChildProcess(child);
24
+ throw error;
25
+ }
26
+ let closed = false;
27
+ return {
28
+ appUrl,
29
+ mode: "dev",
30
+ port,
31
+ process: child,
32
+ close: async () => {
33
+ if (closed) return;
34
+ closed = true;
35
+ await closeChildProcess(child);
36
+ }
37
+ };
38
+ }
39
+
40
+ async function startBundleDevFrontend(frontend, options = {}) {
41
+ const root = frontendRoot(frontend);
42
+ const bundle = bundleConfig(frontend);
43
+ const bundlePort = await selectFrontendPort(options.port ?? options.bundlePort, bundle.defaultPort);
44
+ const mountPath = bundleMountPath(frontend);
45
+ const bundleSpec = commandSpec(bundle.dev, "bundle.dev");
46
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn app bundle dev");
47
+ const bundleChild = spawnCommand(bundleSpec, {
48
+ cwd: root,
49
+ label: "matterhorn app bundle dev",
50
+ logger: options.logger,
51
+ port: bundlePort,
52
+ bundlePort,
53
+ mountPath,
54
+ trust: commandEnvTrustForPolicy(policy)
55
+ });
56
+ const appUrl = frontendUrl(bundlePort, "/");
57
+ try {
58
+ const healthPath = bundle.healthPath || bundle.devEntry || "";
59
+ await waitForHttpOk(new URL(`${mountPath}${healthPath}`, appUrl).toString(), options.timeoutMs);
60
+ } catch (error) {
61
+ await closeChildProcess(bundleChild);
62
+ throw error;
63
+ }
64
+ let closed = false;
65
+ return {
66
+ appUrl,
67
+ bundleUrl: bundleUrl(appUrl, mountPath, bundle.devEntry || "src/index.tsx"),
68
+ mode: "dev",
69
+ port: bundlePort,
70
+ bundlePort,
71
+ process: bundleChild,
72
+ close: async () => {
73
+ if (closed) return;
74
+ closed = true;
75
+ await closeChildProcess(bundleChild);
76
+ }
77
+ };
78
+ }
79
+
80
+ async function startBundledDevFrontend(frontend, options = {}) {
81
+ const root = frontendRoot(frontend);
82
+ const launcherPort = await selectFrontendPort(options.port, frontend.defaultPort);
83
+ const appBundlePort = await selectFrontendPort(options.bundlePort, frontend.bundle?.defaultPort);
84
+ const bundle = bundleConfig(frontend);
85
+ const mountPath = bundleMountPath(frontend);
86
+ const bundleSpec = commandSpec(bundle.dev, "bundle.dev");
87
+ const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn bundled frontend dev");
88
+ const bundleChild = spawnCommand(bundleSpec, {
89
+ cwd: root,
90
+ label: "matterhorn app bundle dev",
91
+ logger: options.logger,
92
+ port: launcherPort,
93
+ bundlePort: appBundlePort,
94
+ mountPath,
95
+ trust: commandEnvTrustForPolicy(policy)
96
+ });
97
+ const bundleBaseUrl = frontendUrl(appBundlePort, "/");
98
+
99
+ try {
100
+ const healthPath = bundle.healthPath || bundle.devEntry || "";
101
+ await waitForHttpOk(new URL(`${mountPath}${healthPath}`, bundleBaseUrl).toString(), options.timeoutMs);
102
+ } catch (error) {
103
+ await closeChildProcess(bundleChild);
104
+ throw error;
105
+ }
106
+
107
+ const launcherSpec = commandSpec(frontend.launcher?.dev || launcherDevCommand(), "launcher.dev");
108
+ const launcherChild = spawnCommand(launcherSpec, {
109
+ cwd: repoRoot(),
110
+ label: "matterhorn launcher dev",
111
+ logger: options.logger,
112
+ port: launcherPort,
113
+ bundlePort: appBundlePort,
114
+ mountPath,
115
+ env: {
116
+ MATTERHORN_APP_BUNDLE_TARGET: bundleBaseUrl,
117
+ MATTERHORN_APP_BUNDLE_MOUNT: mountPath
118
+ },
119
+ trust: commandEnvTrustForPolicy(policy)
120
+ });
121
+ const appUrl = frontendUrl(launcherPort, frontend.launcher?.devBasePath || "/");
122
+ try {
123
+ await waitForHttpOk(new URL(frontend.launcher?.healthPath || "/", appUrl).toString(), options.timeoutMs);
124
+ } catch (error) {
125
+ await closeChildProcess(launcherChild);
126
+ await closeChildProcess(bundleChild);
127
+ throw error;
128
+ }
129
+
130
+ let closed = false;
131
+ return {
132
+ appUrl,
133
+ bundleUrl: bundleUrl(appUrl, mountPath, bundle.devEntry || "src/index.tsx"),
134
+ mode: "dev",
135
+ port: launcherPort,
136
+ bundlePort: appBundlePort,
137
+ close: async () => {
138
+ if (closed) return;
139
+ closed = true;
140
+ await closeChildProcess(launcherChild);
141
+ await closeChildProcess(bundleChild);
142
+ }
143
+ };
144
+ }
145
+
146
+ module.exports = {
147
+ startBundleDevFrontend,
148
+ startBundledDevFrontend,
149
+ startDevFrontend
150
+ };