@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,588 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawnSync } = require("node:child_process");
5
+ const { createHash } = require("node:crypto");
6
+ const { createRequire } = require("node:module");
7
+
8
+ const APP_REF_ALIASES = {
9
+ "@app.matterhorn.events": "@mh-gg/example-events",
10
+ "@app.matterhorn.kanban": "@mh-gg/example-kanban",
11
+ "@app.matterhorn.wiki": "@mh-gg/example-wiki",
12
+ "@app.matterhorn.polls": "@mh-gg/example-polls",
13
+ "@app.matterhorn.budget": "@mh-gg/example-budget",
14
+ "@app.matterhorn.crm": "@mh-gg/example-crm",
15
+ "@app.matterhorn.react": "@mh-gg/example-react",
16
+ "@app.matterhorn.svelte": "@mh-gg/example-svelte",
17
+ "@app.matterhorn.vue": "@mh-gg/example-vue",
18
+ "@app.matterhorn.lit": "@mh-gg/example-lit",
19
+ "@app.matterhorn.vanilla": "@mh-gg/example-vanilla"
20
+ };
21
+
22
+ function canonicalAppRef(ref) {
23
+ return APP_REF_ALIASES[ref] || ref;
24
+ }
25
+
26
+ function builtInExampleAppRef(ref) {
27
+ return Object.values(APP_REF_ALIASES).includes(ref) || String(ref).startsWith("@mh-gg/example-");
28
+ }
29
+
30
+ function looksLikeLocalRef(ref) {
31
+ return ref === "."
32
+ || ref === ".."
33
+ || ref.startsWith("./")
34
+ || ref.startsWith("../")
35
+ || ref.startsWith(".\\")
36
+ || ref.startsWith("..\\")
37
+ || ref.startsWith("/")
38
+ || ref.startsWith("\\")
39
+ || /^[A-Za-z]:[\\/]/.test(ref);
40
+ }
41
+
42
+ function fileRefPath(ref) {
43
+ if (ref.startsWith("file:")) return ref.slice("file:".length);
44
+ return ref;
45
+ }
46
+
47
+ function candidateEntryFiles(directory) {
48
+ return [
49
+ path.join(directory, "matterhorn.app.json"),
50
+ path.join(directory, "matterhorn.app.cjs"),
51
+ path.join(directory, "matterhorn.app.js"),
52
+ path.join(directory, "index.cjs"),
53
+ path.join(directory, "index.js")
54
+ ];
55
+ }
56
+
57
+ function readJsonFile(filePath) {
58
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
59
+ }
60
+
61
+ function bundleEntryFile(directory) {
62
+ const candidates = fs.readdirSync(directory, { withFileTypes: true })
63
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
64
+ .map((entry) => path.join(directory, entry.name));
65
+ const bundles = [];
66
+ for (const file of candidates) {
67
+ try {
68
+ const value = readJsonFile(file);
69
+ if (value?.kind === "matterhorn.app-bundle" && value.appPack) bundles.push(file);
70
+ } catch {}
71
+ }
72
+ if (bundles.length > 1) throw new Error(`Local Matterhorn app ${directory} exposes multiple app bundle files: ${bundles.map((file) => path.basename(file)).join(", ")}`);
73
+ return bundles[0];
74
+ }
75
+
76
+ function packageEntryFile(directory) {
77
+ const packageFile = path.join(directory, "package.json");
78
+ if (!fs.existsSync(packageFile)) return undefined;
79
+ const manifest = JSON.parse(fs.readFileSync(packageFile, "utf8"));
80
+ const matterhornEntry = typeof manifest.matterhorn === "string"
81
+ ? manifest.matterhorn
82
+ : manifest.matterhorn?.app;
83
+ return path.resolve(directory, matterhornEntry || manifest.main || "index.cjs");
84
+ }
85
+
86
+ function staticPackageEntryFile(directory) {
87
+ const packageFile = path.join(directory, "package.json");
88
+ const manifest = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, "utf8")) : {};
89
+ const matterhornEntry = typeof manifest.matterhorn === "object" && typeof manifest.matterhorn.bundle === "string"
90
+ ? path.resolve(directory, manifest.matterhorn.bundle)
91
+ : undefined;
92
+ if (matterhornEntry && fs.existsSync(matterhornEntry)) return matterhornEntry;
93
+ const direct = path.join(directory, "matterhorn.app.json");
94
+ if (fs.existsSync(direct)) return direct;
95
+ return bundleEntryFile(directory);
96
+ }
97
+
98
+ function localEntryFile(localPath) {
99
+ const resolved = path.resolve(localPath);
100
+ const stat = fs.statSync(resolved);
101
+ if (stat.isFile()) return resolved;
102
+ const bundleEntry = bundleEntryFile(resolved);
103
+ if (bundleEntry) return bundleEntry;
104
+ const packageEntry = packageEntryFile(resolved);
105
+ if (packageEntry && fs.existsSync(packageEntry)) return packageEntry;
106
+ const found = candidateEntryFiles(resolved).find((file) => fs.existsSync(file));
107
+ if (found) return found;
108
+ throw new Error(`Local Matterhorn app ${localPath} does not expose matterhorn.app.json, matterhorn.app.cjs, index.cjs, or package.json`);
109
+ }
110
+
111
+ function nearestPackageRoot(filePath) {
112
+ let current = fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
113
+ while (true) {
114
+ if (fs.existsSync(path.join(current, "package.json"))) return current;
115
+ const parent = path.dirname(current);
116
+ if (parent === current) return path.dirname(filePath);
117
+ current = parent;
118
+ }
119
+ }
120
+
121
+ function gitAppRefsAllowed(options = {}) {
122
+ return options.allowGitAppRefs === true || process.env.MATTERHORN_ALLOW_GIT_APP_REFS === "1";
123
+ }
124
+
125
+ function trustedAppCodeAllowed(options = {}) {
126
+ return options.allowTrustedAppCode === true
127
+ || options.trustedAppCode === true;
128
+ }
129
+
130
+ function localAppCodeTrusted(options = {}) {
131
+ return options.localAppCodeTrusted !== false;
132
+ }
133
+
134
+ function contentHash(filePath) {
135
+ return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
136
+ }
137
+
138
+ function sourceIdentity(source) {
139
+ return `${source.sourceKind}:${source.ref}:${source.sourceRoot}:${source.entryFile}`;
140
+ }
141
+
142
+ function matchingTrustRecord(source, options = {}) {
143
+ const record = options.trustRecord;
144
+ if (!record || typeof record !== "object") return false;
145
+ return record.sourceIdentity === sourceIdentity(source)
146
+ && record.contentHash === contentHash(source.entryFile)
147
+ && record.codeExecution === "trusted-installed";
148
+ }
149
+
150
+ function appTrustForSource(source, options = {}) {
151
+ const metadataMode = source.entryFile.endsWith(".json") ? "static-json" : "executed-js";
152
+ let codeExecution = "none";
153
+ let commandExecution = "none";
154
+ if (metadataMode === "executed-js") {
155
+ if (source.directLocal) {
156
+ codeExecution = "local-dev";
157
+ commandExecution = "trusted-local-dev";
158
+ } else if (matchingTrustRecord(source, options) || trustedAppCodeAllowed(options)) {
159
+ codeExecution = "trusted-installed";
160
+ commandExecution = "allowlisted";
161
+ }
162
+ } else if (source.directLocal) {
163
+ commandExecution = "trusted-local-dev";
164
+ } else if (builtInExampleAppRef(source.ref)) {
165
+ commandExecution = "trusted-local-dev";
166
+ }
167
+ return {
168
+ sourceKind: source.sourceKind,
169
+ metadataMode,
170
+ codeExecution,
171
+ commandExecution,
172
+ sourceIdentity: sourceIdentity(source),
173
+ contentHash: contentHash(source.entryFile),
174
+ ...(source.packageIntegrity ? { packageIntegrity: source.packageIntegrity } : {}),
175
+ ...(source.pinnedCommit ? { pinnedCommit: source.pinnedCommit } : {}),
176
+ ...(source.unsafeDevMode ? { unsafeDevMode: true } : {})
177
+ };
178
+ }
179
+
180
+ function resolveMatterhornAppEntry(ref, options = {}) {
181
+ const cwd = options.cwd || process.cwd();
182
+ const loadRef = canonicalAppRef(ref);
183
+ if (loadRef.startsWith("git+")) {
184
+ if (!gitAppRefsAllowed(options)) {
185
+ throw new Error("Git app refs are disabled by default. Clone the app locally or pass allowGitAppRefs: true for trusted sources.");
186
+ }
187
+ const sourceRoot = gitAppCachePath(loadRef, options);
188
+ const gitMetadata = readGitAppCacheMetadata(sourceRoot) || {};
189
+ const parsedGit = parseGitAppRef(loadRef);
190
+ const gitSource = {
191
+ sourceKind: "git",
192
+ pinnedCommit: gitMetadata.unsafeDevRef ? undefined : gitMetadata.commit,
193
+ unsafeDevMode: gitMetadata.unsafeDevRef === true,
194
+ sourceRef: parsedGit.source
195
+ };
196
+ const staticEntry = staticPackageEntryFile(sourceRoot);
197
+ if (staticEntry) return { ref: loadRef, entryFile: staticEntry, sourceRoot, directLocal: false, ...gitSource };
198
+ const jsSource = { ref: loadRef, entryFile: localEntryFile(sourceRoot), sourceRoot, directLocal: false, ...gitSource };
199
+ if (!trustedAppCodeAllowed(options) && !matchingTrustRecord(jsSource, options)) {
200
+ throw new Error("Git app refs can only load static JSON app bundles by default. Set allowTrustedAppCode: true only for trusted local development.");
201
+ }
202
+ return jsSource;
203
+ }
204
+
205
+ if (loadRef.startsWith("file:") || looksLikeLocalRef(loadRef)) {
206
+ const sourcePath = path.resolve(cwd, fileRefPath(loadRef));
207
+ const entryFile = localEntryFile(sourcePath);
208
+ const sourceRoot = fs.statSync(sourcePath).isDirectory() ? sourcePath : nearestPackageRoot(sourcePath);
209
+ const directLocal = localAppCodeTrusted(options);
210
+ const sourceKind = fs.statSync(sourcePath).isDirectory() ? "local-directory" : "local-file";
211
+ const source = { ref: loadRef, entryFile, sourceRoot, directLocal, sourceKind };
212
+ if (!entryFile.endsWith(".json") && !directLocal && !trustedAppCodeAllowed(options) && !matchingTrustRecord(source, options)) {
213
+ throw new Error("Downloaded app packages can only load static JSON app bundles by default. Set allowTrustedAppCode: true only for trusted package code.");
214
+ }
215
+ return source;
216
+ }
217
+
218
+ const sourceRoot = resolvePackageRoot(loadRef, cwd);
219
+ const staticEntry = staticPackageEntryFile(sourceRoot);
220
+ if (staticEntry) return { ref: loadRef, entryFile: staticEntry, sourceRoot, directLocal: false, sourceKind: "package" };
221
+ const entryFile = resolvePackageEntryFile(loadRef, cwd);
222
+ const source = { ref: loadRef, entryFile, sourceRoot: nearestPackageRoot(entryFile), directLocal: false, sourceKind: "package" };
223
+ if (!trustedAppCodeAllowed(options) && !matchingTrustRecord(source, options)) {
224
+ throw new Error(`Package app ref ${loadRef} can only load static JSON app bundles by default. Set allowTrustedAppCode: true only for trusted local development.`);
225
+ }
226
+ return source;
227
+ }
228
+
229
+ function resolvePackageRoot(ref, cwd) {
230
+ const entryFile = resolvePackageEntryFile(ref, cwd);
231
+ return nearestPackageRoot(entryFile);
232
+ }
233
+
234
+ function resolvePackageEntryFile(ref, cwd) {
235
+ const requireFromCwd = createRequire(path.join(cwd, "matterhorn-app-loader.cjs"));
236
+ try {
237
+ return requireFromCwd.resolve(ref);
238
+ } catch (cwdError) {
239
+ try {
240
+ return require.resolve(ref);
241
+ } catch {
242
+ throw cwdError;
243
+ }
244
+ }
245
+ }
246
+
247
+ function moduleExportsForEntry(entryFile) {
248
+ if (entryFile.endsWith(".json")) return readJsonFile(entryFile);
249
+ delete require.cache[require.resolve(entryFile)];
250
+ return require(entryFile);
251
+ }
252
+
253
+ let basePluginRegistry;
254
+
255
+ function schemaApi() {
256
+ return require("@mh-gg/schema");
257
+ }
258
+
259
+ function configuredBasePluginRegistry() {
260
+ if (!basePluginRegistry) {
261
+ const { createMicroPluginRegistry } = schemaApi();
262
+ const { registryRecords } = require("@mh-gg/base-plugins/composer/registry");
263
+ basePluginRegistry = createMicroPluginRegistry(registryRecords());
264
+ }
265
+ return basePluginRegistry;
266
+ }
267
+
268
+ function schemaModelHostPlugin(app, plugin) {
269
+ const { createSchemaDefinedHostPlugin } = schemaApi();
270
+ const { hashCanonical } = require("@mh-gg/base");
271
+ const composition = plugin.composition || {
272
+ kind: "matterhorn.app-composition.schema",
273
+ schemaVersion: 1,
274
+ app: {
275
+ id: app.id,
276
+ version: app.version || app.appPack?.version || "0.1.0",
277
+ name: app.name
278
+ },
279
+ primaryPlugin: {
280
+ id: plugin.id,
281
+ version: plugin.version || app.version || app.appPack?.version || "0.1.0",
282
+ model: plugin.model
283
+ },
284
+ plugins: plugin.plugins || [],
285
+ actions: plugin.actions || [],
286
+ views: plugin.views || [],
287
+ routes: plugin.routes || []
288
+ };
289
+ const hostPlugin = createSchemaDefinedHostPlugin(composition);
290
+ hostPlugin.stateSchemaHash = hostPlugin.stateSchemaHash || hashCanonical(hostPlugin.stateSchemaDescriptor || hostPlugin.schemas?.state?.descriptor || {});
291
+ hostPlugin.operationSchemaHash = hostPlugin.operationSchemaHash || hashCanonical(hostPlugin.operationSchemaDescriptor || {});
292
+ return hostPlugin;
293
+ }
294
+
295
+ function hydrateJsonHostPlugin(plugin, app) {
296
+ if (plugin?.kind === "matterhorn.schema-model-host-plugin") return schemaModelHostPlugin(app, plugin);
297
+ if (plugin?.kind === "matterhorn.registry-host-plugin") return registryHostPlugin(app, plugin);
298
+ throw new Error("JSON Matterhorn app manifests can only declare schema-model-host-plugin or registry-host-plugin entries; app reducer modules are not loadable from schema manifests");
299
+ }
300
+
301
+ function registryHostPlugin(app, plugin) {
302
+ const { resolveCompositionHostPluginEntries } = schemaApi();
303
+ const composition = app.appPack?.composition || app.compositionSchema;
304
+ const entries = resolveCompositionHostPluginEntries(composition, configuredBasePluginRegistry());
305
+ const entry = entries.find((candidate) => candidate.schema.key === plugin.key || candidate.plugin.id === plugin.id);
306
+ if (!entry) throw new Error(`JSON Matterhorn app bundle references unknown registry plugin ${plugin.key || plugin.id}`);
307
+ if (plugin.config === undefined) return entry.plugin;
308
+ return { ...entry.plugin, config: plugin.config };
309
+ }
310
+
311
+ function normalizeJsonAppPaths(app, source) {
312
+ if (!source.entryFile.endsWith(".json") && app?.kind !== "matterhorn.app-bundle") return app;
313
+ const manifestDir = path.dirname(source.entryFile);
314
+ const frontend = app.frontend ? { ...app.frontend } : undefined;
315
+ if (frontend) frontend.root = path.resolve(manifestDir, frontend.root || ".");
316
+ return {
317
+ ...app,
318
+ ...(frontend ? { frontend } : {}),
319
+ hostPlugins: Array.isArray(app.hostPlugins)
320
+ ? app.hostPlugins.map((plugin) => hydrateJsonHostPlugin(plugin, app))
321
+ : app.hostPlugins
322
+ };
323
+ }
324
+
325
+ function moduleExportsForRef(ref, options = {}) {
326
+ const source = resolveMatterhornAppEntry(ref, options);
327
+ if (!source.entryFile.endsWith(".json") && !source.directLocal && !trustedAppCodeAllowed(options) && !matchingTrustRecord(source, options)) {
328
+ throw new Error("Only direct local file app refs may execute app JavaScript by default. Use static JSON app bundles for package/git refs.");
329
+ }
330
+ return {
331
+ exports: moduleExportsForEntry(source.entryFile),
332
+ source
333
+ };
334
+ }
335
+
336
+ function gitCacheRoot(options = {}) {
337
+ return options.cacheDir || process.env.MATTERHORN_APP_CACHE_DIR || path.join(os.tmpdir(), "matterhorn-apps");
338
+ }
339
+
340
+ const GIT_COMMIT_RE = /^[0-9a-f]{40}$/i;
341
+
342
+ function parseGitAppRef(ref) {
343
+ const raw = ref.slice("git+".length);
344
+ const fragmentIndex = raw.lastIndexOf("#");
345
+ if (fragmentIndex === -1) return { source: raw, refish: "" };
346
+ return {
347
+ source: raw.slice(0, fragmentIndex),
348
+ refish: raw.slice(fragmentIndex + 1)
349
+ };
350
+ }
351
+
352
+ function unsafeGitAppRefsAllowed(options = {}) {
353
+ return options.allowUnsafeGitAppRef === true || process.env.MATTERHORN_UNSAFE_GIT_APP_REF === "1";
354
+ }
355
+
356
+ function gitAppCacheMetadataFile(target) {
357
+ return path.join(target, ".git", "matterhorn-app-cache.json");
358
+ }
359
+
360
+ function readGitAppCacheMetadata(target) {
361
+ const file = gitAppCacheMetadataFile(target);
362
+ if (!fs.existsSync(file)) return undefined;
363
+ return readJsonFile(file);
364
+ }
365
+
366
+ function writeGitAppCacheMetadata(target, metadata) {
367
+ fs.writeFileSync(gitAppCacheMetadataFile(target), `${JSON.stringify(metadata, null, 2)}\n`);
368
+ }
369
+
370
+ function gitHeadCommit(target) {
371
+ return runGit(["-C", target, "rev-parse", "HEAD"], process.cwd()).toLowerCase();
372
+ }
373
+
374
+ function gitWorkingTreeStatus(target) {
375
+ return runGit(["-C", target, "status", "--porcelain", "--untracked-files=all"], process.cwd());
376
+ }
377
+
378
+ function verifyGitAppCache(target, expectedCommit) {
379
+ const metadata = readGitAppCacheMetadata(target);
380
+ const actualCommit = gitHeadCommit(target);
381
+ const normalizedCommit = expectedCommit.toLowerCase();
382
+ if (metadata?.commit !== normalizedCommit || actualCommit !== normalizedCommit) {
383
+ throw new Error(`Git app cache ${target} is not at the pinned commit ${normalizedCommit}; remove the cache and load again`);
384
+ }
385
+ const status = gitWorkingTreeStatus(target);
386
+ if (status.length > 0) {
387
+ throw new Error(`Git app cache ${target} has local modifications; refusing to execute a tampered checkout`);
388
+ }
389
+ }
390
+
391
+ function warnUnsafeGitAppRef(ref, options = {}) {
392
+ const message = `WARNING: ${ref} is an unsafe dev git app ref. Pin git app refs to a commit SHA for deterministic loading.`;
393
+ if (typeof options.io?.warn === "function") options.io.warn(message);
394
+ else if (typeof options.warn === "function") options.warn(message);
395
+ }
396
+
397
+ function gitAppCachePath(ref, options = {}) {
398
+ const parsed = parseGitAppRef(ref);
399
+ const pinnedCommit = parsed.refish && GIT_COMMIT_RE.test(parsed.refish)
400
+ ? parsed.refish.toLowerCase()
401
+ : "";
402
+ if (!pinnedCommit && !unsafeGitAppRefsAllowed(options)) {
403
+ throw new Error("Git app refs must pin a 40-character commit SHA. Set allowUnsafeGitAppRef: true only for local development branch refs.");
404
+ }
405
+ const cacheRoot = gitCacheRoot(options);
406
+ const cacheIdentity = pinnedCommit ? `${parsed.source}#${pinnedCommit}` : `unsafe:${ref}`;
407
+ const cacheKey = createHash("sha256").update(cacheIdentity).digest("hex").slice(0, 24);
408
+ const target = path.join(cacheRoot, cacheKey);
409
+ if (!fs.existsSync(target)) {
410
+ fs.mkdirSync(cacheRoot, { recursive: true });
411
+ if (pinnedCommit) {
412
+ runGit(["clone", "--no-checkout", parsed.source, target], process.cwd());
413
+ runGit(["-C", target, "checkout", "--detach", pinnedCommit], process.cwd());
414
+ } else {
415
+ warnUnsafeGitAppRef(ref, options);
416
+ const args = parsed.refish
417
+ ? ["clone", "--depth", "1", "--branch", parsed.refish, parsed.source, target]
418
+ : ["clone", "--depth", "1", parsed.source, target];
419
+ runGit(args, process.cwd());
420
+ }
421
+ writeGitAppCacheMetadata(target, {
422
+ schemaVersion: 1,
423
+ ref,
424
+ source: parsed.source,
425
+ commit: gitHeadCommit(target),
426
+ unsafeDevRef: pinnedCommit ? false : true
427
+ });
428
+ }
429
+ const metadata = readGitAppCacheMetadata(target);
430
+ const expectedCommit = pinnedCommit || metadata?.commit;
431
+ if (!expectedCommit) {
432
+ throw new Error(`Git app cache ${target} does not record the resolved commit; remove the cache and load again`);
433
+ }
434
+ verifyGitAppCache(target, expectedCommit);
435
+ return target;
436
+ }
437
+
438
+ function runGit(args, cwd) {
439
+ const result = spawnSync("git", args, {
440
+ cwd,
441
+ encoding: "utf8",
442
+ stdio: ["ignore", "pipe", "pipe"],
443
+ windowsHide: true
444
+ });
445
+ if (result.error) throw new Error(`Git app ref load failed: ${result.error.message}`);
446
+ if (result.status === 0) return (result.stdout || "").trim();
447
+ const message = (result.stderr || result.stdout || "").trim();
448
+ throw new Error(`Git app ref load failed: ${message || `git ${args.join(" ")} exited ${result.status}`}`);
449
+ }
450
+
451
+ function defineAppExport(exports) {
452
+ if (exports?.toMatterhornBundle) return exports;
453
+ if (exports?.default?.toMatterhornBundle) return exports.default;
454
+ return undefined;
455
+ }
456
+
457
+ function appDescriptorFromExports(exports) {
458
+ const sdkApp = defineAppExport(exports);
459
+ if (sdkApp) return sdkApp.toMatterhornBundle();
460
+ return exports?.matterhornApp || exports?.default || exports;
461
+ }
462
+
463
+ function normalizeMatterhornApp(ref, exports) {
464
+ const app = appDescriptorFromExports(exports);
465
+ if (!app || typeof app !== "object" || Array.isArray(app)) {
466
+ throw new Error(`Matterhorn app ${ref} did not export an app descriptor`);
467
+ }
468
+ if (!app.id || !app.name) throw new Error(`Matterhorn app ${ref} must export id and name`);
469
+ if (!app.appPack) throw new Error(`Matterhorn app ${ref} must export appPack`);
470
+ return {
471
+ ...app,
472
+ ref
473
+ };
474
+ }
475
+
476
+ function appHostRunner(app) {
477
+ return app.host?.runner || app.deployment?.host?.runner || "matterhorn-example-host";
478
+ }
479
+
480
+ function normalizeLaunch(ref, launch) {
481
+ if (launch === undefined) return undefined;
482
+ if (!launch || typeof launch !== "object" || Array.isArray(launch)) {
483
+ throw new Error(`Matterhorn deployment ${ref} launch must be an object`);
484
+ }
485
+ if (typeof launch.command !== "string" || launch.command.length === 0) {
486
+ throw new Error(`Matterhorn deployment ${ref} launch.command is required`);
487
+ }
488
+ if (launch.args !== undefined && !Array.isArray(launch.args)) {
489
+ throw new Error(`Matterhorn deployment ${ref} launch.args must be an array`);
490
+ }
491
+ return {
492
+ command: launch.command,
493
+ args: (launch.args || []).map((arg) => String(arg))
494
+ };
495
+ }
496
+
497
+ function normalizeMatterhornDeployment(ref, app) {
498
+ if (!app || typeof app !== "object" || Array.isArray(app)) {
499
+ throw new Error(`Matterhorn deployment ${ref} requires an app descriptor`);
500
+ }
501
+ if (!app.id || !app.name) throw new Error(`Matterhorn deployment ${ref} must include id and name`);
502
+ if (!app.appPack) throw new Error(`Matterhorn deployment ${ref} must include appPack`);
503
+ if (!app.hostPack) throw new Error(`Matterhorn deployment ${ref} must include hostPack`);
504
+ if (!Array.isArray(app.hostPlugins) || app.hostPlugins.length === 0) {
505
+ throw new Error(`Matterhorn deployment ${ref} must include at least one host plugin`);
506
+ }
507
+ if (!Array.isArray(app.playerPacks) || app.playerPacks.length === 0) {
508
+ throw new Error(`Matterhorn deployment ${ref} must include at least one player pack`);
509
+ }
510
+
511
+ const declared = app.deployment && typeof app.deployment === "object" && !Array.isArray(app.deployment)
512
+ ? app.deployment
513
+ : {};
514
+ if (declared.kind !== undefined && declared.kind !== "matterhorn.self-contained-app") {
515
+ throw new Error(`Matterhorn deployment ${ref} kind must be matterhorn.self-contained-app`);
516
+ }
517
+ return {
518
+ kind: "matterhorn.self-contained-app",
519
+ id: app.id,
520
+ name: app.name,
521
+ version: app.version || app.appPack.version,
522
+ ref: app.ref || ref,
523
+ appPack: app.appPack,
524
+ hostPack: app.hostPack,
525
+ hostPlugins: app.hostPlugins,
526
+ playerPacks: app.playerPacks,
527
+ frontend: app.frontend,
528
+ host: {
529
+ runner: appHostRunner(app),
530
+ ...(declared.host || {})
531
+ },
532
+ relay: {
533
+ mode: "embedded",
534
+ autoStart: true,
535
+ acceptsPeerRelays: true,
536
+ ...(declared.relay || {})
537
+ },
538
+ frontendDelivery: {
539
+ mode: app.frontend?.bundle ? "relay-chunks" : "external",
540
+ ...(declared.frontendDelivery || declared.frontend || {})
541
+ },
542
+ launch: normalizeLaunch(ref, declared.launch),
543
+ trust: app.trust
544
+ };
545
+ }
546
+
547
+ function loadMatterhornApp(ref, options = {}) {
548
+ if (!ref || typeof ref !== "string") throw new Error("Matterhorn app ref is required");
549
+ const loaded = moduleExportsForRef(ref, options);
550
+ const descriptor = appDescriptorFromExports(loaded.exports);
551
+ const app = normalizeMatterhornApp(ref, normalizeJsonAppPaths(descriptor, loaded.source));
552
+ return {
553
+ ...app,
554
+ sourceEntry: loaded.source.entryFile,
555
+ sourceRoot: loaded.source.sourceRoot,
556
+ sourceKind: defineAppExport(loaded.exports) ? "defineApp" : (loaded.source.entryFile.endsWith(".json") ? "json" : "module"),
557
+ trust: appTrustForSource(loaded.source, options),
558
+ bundleFile: path.join(app.frontend?.root || loaded.source.sourceRoot, `${app.id}.json`)
559
+ };
560
+ }
561
+
562
+ function loadMatterhornDeployment(ref, options = {}) {
563
+ return normalizeMatterhornDeployment(ref, loadMatterhornApp(ref, options));
564
+ }
565
+
566
+ function emitMatterhornBundleForRef(ref, options = {}) {
567
+ if (!ref || typeof ref !== "string") throw new Error("Matterhorn app ref is required");
568
+ const source = resolveMatterhornAppEntry(ref, options);
569
+ const exports = moduleExportsForEntry(source.entryFile);
570
+ const sdkApp = defineAppExport(exports);
571
+ if (!sdkApp?.emitMatterhornBundle) throw new Error(`Matterhorn app ${ref} does not export a defineApp() result`);
572
+ return sdkApp.emitMatterhornBundle({
573
+ ...(options.bundleOptions || {}),
574
+ ...(options.outDir === undefined ? {} : { outDir: options.outDir })
575
+ });
576
+ }
577
+
578
+ module.exports = {
579
+ emitMatterhornBundleForRef,
580
+ loadMatterhornApp,
581
+ loadMatterhornDeployment,
582
+ normalizeMatterhornApp,
583
+ normalizeMatterhornDeployment,
584
+ resolveMatterhornAppEntry,
585
+ canonicalAppRef,
586
+ gitAppRefsAllowed,
587
+ appTrustForSource
588
+ };