overmux 0.0.4 → 0.0.5

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 (141) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/dist/bin.js +140 -127
  4. package/dist/bin.js.map +1 -1
  5. package/dist/docs/000-index.md +2 -4
  6. package/dist/docs/100-introduction/200-how-overmux-works.md +126 -2
  7. package/dist/docs/100-introduction/{300-why-overmux.md → 300-why-i-built-overmux.md} +1 -1
  8. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  9. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  10. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  11. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  12. package/dist/docs/400-reference/100-project-structure.md +83 -0
  13. package/dist/docs/400-reference/200-configuration.md +257 -0
  14. package/dist/docs/400-reference/300-storage-locations.md +46 -0
  15. package/dist/docs/400-reference/400-authentication-and-security.md +37 -0
  16. package/dist/docs/400-reference/500-server/000-index.md +13 -0
  17. package/dist/docs/400-reference/500-server/100-resources.md +191 -0
  18. package/dist/docs/400-reference/500-server/200-operations.md +111 -0
  19. package/dist/docs/400-reference/500-server/300-streams.md +140 -0
  20. package/dist/docs/400-reference/500-server/400-notifications.md +32 -0
  21. package/dist/docs/400-reference/500-server/500-api.md +118 -0
  22. package/dist/docs/400-reference/600-client/000-index.md +9 -0
  23. package/dist/docs/400-reference/600-client/100-api.md +370 -0
  24. package/dist/docs/400-reference/600-client/200-theming.md +94 -0
  25. package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  26. package/dist/docs/400-reference/{400-cli → 700-cli}/050-init.md +2 -4
  27. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/100-serve.md +9 -4
  28. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/200-auth.md +1 -4
  29. package/dist/docs/400-reference/{400-cli → 700-cli}/300-call.md +1 -4
  30. package/dist/docs/400-reference/{400-cli → 700-cli}/350-instance.md +1 -4
  31. package/dist/docs/400-reference/700-cli/600-docs.md +128 -0
  32. package/dist/docs/400-reference/{400-cli → 700-cli}/700-desktop.md +1 -3
  33. package/dist/docs/500-hosted-pages.md +0 -1
  34. package/dist/exports/client.d.ts +5 -14
  35. package/dist/exports/client.d.ts.map +1 -1
  36. package/dist/exports/client.js +141 -46
  37. package/dist/exports/client.js.map +1 -1
  38. package/dist/exports/{index-DS70rKzo.d.ts → index-Cz3xkCa4.d.ts} +50 -36
  39. package/dist/exports/index-Cz3xkCa4.d.ts.map +1 -0
  40. package/dist/exports/index.d.ts +1 -1
  41. package/dist/exports/index.js +19 -5
  42. package/dist/exports/index.js.map +1 -1
  43. package/dist/exports/{notifications-av0FK0yZ.js → notifications-BFAD3QQl.js} +29 -4
  44. package/dist/exports/notifications-BFAD3QQl.js.map +1 -0
  45. package/dist/exports/server.d.ts +1 -44
  46. package/dist/exports/server.d.ts.map +1 -1
  47. package/dist/exports/server.js +9 -1061
  48. package/dist/exports/server.js.map +1 -1
  49. package/dist/internal/server/coordinator/server-child.js +47 -43
  50. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  51. package/docs/000-index.md +2 -4
  52. package/docs/100-introduction/200-how-overmux-works.md +126 -2
  53. package/docs/100-introduction/{300-why-overmux.md → 300-why-i-built-overmux.md} +1 -1
  54. package/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  55. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  56. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  57. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  58. package/docs/400-reference/100-project-structure.md +83 -0
  59. package/docs/400-reference/200-configuration.md +257 -0
  60. package/docs/400-reference/300-storage-locations.md +46 -0
  61. package/docs/400-reference/400-authentication-and-security.md +37 -0
  62. package/docs/400-reference/500-server/000-index.md +13 -0
  63. package/docs/400-reference/500-server/100-resources.md +191 -0
  64. package/docs/400-reference/500-server/200-operations.md +111 -0
  65. package/docs/400-reference/500-server/300-streams.md +140 -0
  66. package/docs/400-reference/500-server/400-notifications.md +32 -0
  67. package/docs/400-reference/500-server/500-api.md +118 -0
  68. package/docs/400-reference/600-client/000-index.md +9 -0
  69. package/docs/400-reference/600-client/100-api.md +370 -0
  70. package/docs/400-reference/600-client/200-theming.md +94 -0
  71. package/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  72. package/docs/400-reference/{400-cli → 700-cli}/050-init.md +2 -4
  73. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/100-serve.md +9 -4
  74. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/200-auth.md +1 -4
  75. package/docs/400-reference/{400-cli → 700-cli}/300-call.md +1 -4
  76. package/docs/400-reference/{400-cli → 700-cli}/350-instance.md +1 -4
  77. package/docs/400-reference/700-cli/600-docs.md +128 -0
  78. package/docs/400-reference/{400-cli → 700-cli}/700-desktop.md +1 -3
  79. package/docs/500-hosted-pages.md +0 -1
  80. package/package.json +3 -2
  81. package/src/internal/cli/app.ts +3 -5
  82. package/src/internal/cli/commands/docs-ai-context.ts +33 -0
  83. package/src/internal/cli/commands/docs.ts +19 -2
  84. package/src/internal/cli/commands/init-template.ts +1 -1
  85. package/src/internal/cli/commands/serve.ts +3 -0
  86. package/src/internal/cli/login.ts +9 -9
  87. package/src/internal/client/client-definition.ts +6 -8
  88. package/src/internal/client/host/deep-link-navigation.ts +75 -0
  89. package/src/internal/client/host/overmux-host.tsx +9 -0
  90. package/src/internal/client/index.ts +0 -6
  91. package/src/internal/client/overmux-react.ts +71 -40
  92. package/src/internal/server/auth/auth-service.ts +2 -2
  93. package/src/internal/server/auth/instance-control.ts +65 -14
  94. package/src/internal/server/coordinator/ipc-protocol.ts +0 -1
  95. package/src/internal/server/runtime/create-runtime.ts +5 -3
  96. package/src/internal/server/runtime/runtime-instance.ts +5 -4
  97. package/src/internal/server/runtime/runtime-operations.ts +9 -3
  98. package/src/internal/server/runtime/runtime-resources.ts +12 -32
  99. package/src/internal/server/runtime/runtime-streams.ts +5 -6
  100. package/src/internal/server/server-logger.ts +5 -10
  101. package/src/internal/server/server-startup-options.ts +5 -10
  102. package/src/internal/server/start-application-server.ts +4 -7
  103. package/src/public/ai-context.ts +7 -29
  104. package/src/public/client.ts +0 -6
  105. package/src/public/config.ts +156 -31
  106. package/src/public/server.ts +1 -16
  107. package/dist/docs/100-introduction/100-what-is-overmux.md +0 -7
  108. package/dist/docs/300-fundamentals/100-project-structure.md +0 -23
  109. package/dist/docs/300-fundamentals/200-configuration.md +0 -3
  110. package/dist/docs/300-fundamentals/300-theming.md +0 -54
  111. package/dist/docs/300-fundamentals/400-server.md +0 -3
  112. package/dist/docs/300-fundamentals/500-client.md +0 -3
  113. package/dist/docs/300-fundamentals/600-operations.md +0 -3
  114. package/dist/docs/300-fundamentals/700-resources.md +0 -3
  115. package/dist/docs/300-fundamentals/800-streams.md +0 -3
  116. package/dist/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  117. package/dist/docs/400-reference/100-configuration.md +0 -23
  118. package/dist/docs/400-reference/200-server-api.md +0 -21
  119. package/dist/docs/400-reference/300-client-api.md +0 -39
  120. package/dist/docs/400-reference/400-cli/400-check.md +0 -20
  121. package/dist/docs/400-reference/400-cli/500-ai-context.md +0 -102
  122. package/dist/docs/400-reference/400-cli/600-docs.md +0 -23
  123. package/dist/exports/index-DS70rKzo.d.ts.map +0 -1
  124. package/dist/exports/notifications-av0FK0yZ.js.map +0 -1
  125. package/docs/100-introduction/100-what-is-overmux.md +0 -7
  126. package/docs/300-fundamentals/100-project-structure.md +0 -23
  127. package/docs/300-fundamentals/200-configuration.md +0 -3
  128. package/docs/300-fundamentals/300-theming.md +0 -54
  129. package/docs/300-fundamentals/400-server.md +0 -3
  130. package/docs/300-fundamentals/500-client.md +0 -3
  131. package/docs/300-fundamentals/600-operations.md +0 -3
  132. package/docs/300-fundamentals/700-resources.md +0 -3
  133. package/docs/300-fundamentals/800-streams.md +0 -3
  134. package/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  135. package/docs/400-reference/100-configuration.md +0 -23
  136. package/docs/400-reference/200-server-api.md +0 -21
  137. package/docs/400-reference/300-client-api.md +0 -39
  138. package/docs/400-reference/400-cli/400-check.md +0 -20
  139. package/docs/400-reference/400-cli/500-ai-context.md +0 -102
  140. package/docs/400-reference/400-cli/600-docs.md +0 -23
  141. package/src/internal/cli/commands/ai.ts +0 -44
@@ -1,14 +1,8 @@
1
- import { realpath } from "node:fs/promises";
2
1
  import { isIP } from "node:net";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
- import { createJiti } from "jiti";
5
- import { ZodError, z } from "zod";
6
- import { homedir, hostname } from "node:os";
7
- import ts from "typescript";
8
- import { isDeepStrictEqual } from "node:util";
9
- import { fork } from "node:child_process";
10
- import { fileURLToPath } from "node:url";
11
- import { watch } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import "jiti";
4
+ import { z } from "zod";
5
+ import { homedir } from "node:os";
12
6
  //#region ../shared/dist/node/index.js
13
7
  const durationPattern = /^([1-9]\d*)(ms|s|m|h|d)$/;
14
8
  const millisecondsByUnit = {
@@ -26,7 +20,7 @@ const durationToMilliseconds = (duration) => {
26
20
  return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
27
21
  };
28
22
  const aiContextSnippets = ["package-source", "tech-stack-recommendations"];
29
- const instanceIdSchema$1 = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?![\s\S])/, "Expected a lowercase instance ID with only ASCII letters, digits, internal dots and hyphens");
23
+ const instanceIdSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?![\s\S])/, "Expected a lowercase instance ID with only ASCII letters, digits, internal dots and hyphens");
30
24
  const zodTypeSchema = z.custom((value) => value instanceof z.ZodType, "Expected a Zod schema");
31
25
  const functionSchema = z.custom((value) => typeof value === "function", "Expected a function");
32
26
  const contractRuntimeSchema = z.object({
@@ -75,12 +69,11 @@ const loopbackPeerSchema = z.string().refine((value) => isIP(value) !== 0 && (va
75
69
  const authConfigRuntimeSchema = z.object({
76
70
  mode: z.literal("cli-login"),
77
71
  origins: z.array(exactOriginSchema).min(1).refine((origins) => new Set(origins).size === origins.length, "Expected unique origins").optional(),
78
- sessionLifetime: z.union([z.literal("never"), authDurationSchema]).default("never"),
72
+ sessionLifetime: z.union([z.literal("forever"), authDurationSchema]).default("forever"),
79
73
  trustedProxyPeer: loopbackPeerSchema.optional()
80
74
  }).strict();
81
75
  const serverConfigRuntimeSchema = z.object({
82
76
  host: z.string().min(1).optional(),
83
- logFile: z.string().min(1).optional(),
84
77
  port: z.number().int().min(1).max(65535).optional(),
85
78
  productionWebAssetsDir: z.string().min(1).optional(),
86
79
  watch: z.boolean().optional()
@@ -90,52 +83,14 @@ const serverDefinitionRuntimeSchema = z.object({
90
83
  resources: z.record(z.string(), resourceDefinitionRuntimeSchema),
91
84
  streams: z.record(z.string(), streamHandlerRuntimeSchema).optional()
92
85
  });
93
- const configDefinitionRuntimeSchema = serverConfigRuntimeSchema.extend({
86
+ serverConfigRuntimeSchema.extend({
94
87
  aiContextSnippets: z.array(z.enum(aiContextSnippets)).optional(),
95
88
  auth: authConfigRuntimeSchema,
96
89
  debug: z.boolean().optional(),
97
- instanceId: z.union([instanceIdSchema$1, z.custom((value) => typeof value === "function", "Expected an instance ID or a function of the bound port")]).optional(),
90
+ instanceId: z.union([instanceIdSchema, z.custom((value) => typeof value === "function", "Expected an instance ID or a function of the bound port")]).optional(),
98
91
  server: serverDefinitionRuntimeSchema,
99
92
  vite: z.string().min(1).optional()
100
93
  });
101
- const resolveLogicalPath = async (path) => {
102
- const logicalPath = resolve(path);
103
- return {
104
- logicalPath,
105
- realPath: await realpath(logicalPath)
106
- };
107
- };
108
- const resolveOvermuxConfigPaths = async (configPath) => {
109
- const { logicalPath, realPath } = await resolveLogicalPath(configPath);
110
- return {
111
- logicalApplicationRoot: dirname(logicalPath),
112
- logicalConfigPath: logicalPath,
113
- realApplicationRoot: dirname(realPath),
114
- realConfigPath: realPath
115
- };
116
- };
117
- const unwrapDefaultExport = (module) => {
118
- if (typeof module === "object" && module !== null && "default" in module) return module.default;
119
- return module;
120
- };
121
- const loadOvermuxConfig = async ({ aliases = {}, configPath }) => {
122
- const paths = await resolveOvermuxConfigPaths(configPath);
123
- const module = await createJiti(paths.logicalConfigPath, {
124
- alias: aliases,
125
- fsCache: true,
126
- jsx: {
127
- importSource: "react",
128
- runtime: "automatic"
129
- },
130
- moduleCache: false,
131
- sourceMaps: true,
132
- tsconfigPaths: true
133
- }).import(paths.logicalConfigPath);
134
- return {
135
- config: configDefinitionRuntimeSchema.parse(unwrapDefaultExport(module)),
136
- paths
137
- };
138
- };
139
94
  const xdgDirectory = (configured, fallback) => configured && isAbsolute(configured) ? configured : fallback;
140
95
  const overmuxDirectory = (root) => join(root, "overmux");
141
96
  const getOvermuxPaths = ({ environment = process.env, homeDir = homedir(), xdgCacheHome = environment.XDG_CACHE_HOME, xdgConfigHome = environment.XDG_CONFIG_HOME, xdgDataHome = environment.XDG_DATA_HOME, xdgRuntimeDir = environment.XDG_RUNTIME_DIR, xdgStateHome = environment.XDG_STATE_HOME } = {}) => {
@@ -150,1013 +105,6 @@ const getOvermuxPaths = ({ environment = process.env, homeDir = homedir(), xdgCa
150
105
  };
151
106
  };
152
107
  //#endregion
153
- //#region src/internal/shared/background-notifications.ts
154
- const deliveryKeySchema = z.string().min(1).max(1024).regex(/^[\w-]+$/u);
155
- const backgroundNotificationSubscriptionSchema = z.object({
156
- endpoint: z.url().max(4096).refine((endpoint) => {
157
- const url = new URL(endpoint);
158
- return url.protocol === "https:" && !url.username && !url.password;
159
- }),
160
- expirationTime: z.number().int().nonnegative().nullable(),
161
- keys: z.object({
162
- auth: deliveryKeySchema,
163
- p256dh: deliveryKeySchema
164
- }).strict()
165
- }).strict();
166
- z.object({ publicKey: deliveryKeySchema }).strict();
167
- z.object({ endpoint: backgroundNotificationSubscriptionSchema.shape.endpoint }).strict();
168
- const safeRelativeNotificationLink = (link) => {
169
- let decoded;
170
- try {
171
- decoded = decodeURIComponent(link);
172
- } catch {
173
- return false;
174
- }
175
- return !decoded.startsWith("//") && !decoded.includes("\\") && ![...decoded].some((character) => {
176
- const codePoint = character.codePointAt(0) ?? 0;
177
- return codePoint < 32 || codePoint === 127;
178
- });
179
- };
180
- const notificationLinkSchema = z.union([z.string().max(4096).startsWith("/").refine(safeRelativeNotificationLink), z.string().max(4096).url().refine((link) => {
181
- if (!URL.canParse(link)) return false;
182
- const url = new URL(link);
183
- return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
184
- })]);
185
- const notificationSchema = z.object({
186
- body: z.string().max(4096).optional(),
187
- open: z.object({ link: notificationLinkSchema }).strict().optional(),
188
- title: z.string().min(1).max(4096)
189
- }).strict();
190
- const instanceIdSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?![\s\S])/, "Expected a lowercase instance ID with only ASCII letters, digits, internal dots and hyphens");
191
- const createInstanceIdentity = (instanceId) => ({
192
- instanceId: instanceIdSchema.parse(instanceId),
193
- deepLinkPrefix: `overmux://${instanceId}`
194
- });
195
- z.object({
196
- debug: z.boolean(),
197
- operations: z.array(z.string().min(1)),
198
- protocolVersion: z.literal(10),
199
- resources: z.array(z.string().min(1)),
200
- streams: z.array(z.string().min(1))
201
- });
202
- const updateAvailableEventSchema = z.object({ type: z.literal("update-available") });
203
- const restartingEventSchema = z.object({ type: z.literal("restarting") });
204
- z.discriminatedUnion("type", [updateAvailableEventSchema, restartingEventSchema]);
205
- const notificationEventSchema = z.object({
206
- notification: notificationSchema,
207
- type: z.literal("notification")
208
- }).strict();
209
- const operationIdSchema = z.string().min(1).max(256);
210
- const registrationNameSchema = z.string().min(1).max(256);
211
- const resourceReadMessageSchema = z.object({
212
- input: z.unknown().optional(),
213
- operationId: operationIdSchema,
214
- resourceName: registrationNameSchema,
215
- type: z.literal("resource-read")
216
- });
217
- const resourceSubscribeMessageSchema = z.object({
218
- input: z.unknown().optional(),
219
- operationId: operationIdSchema,
220
- resourceName: registrationNameSchema,
221
- subscriptionId: operationIdSchema,
222
- type: z.literal("resource-subscribe")
223
- });
224
- const resourceUnsubscribeMessageSchema = z.object({
225
- subscriptionId: operationIdSchema,
226
- type: z.literal("resource-unsubscribe")
227
- });
228
- const streamOpenMessageSchema = z.object({
229
- input: z.unknown().optional(),
230
- operationId: operationIdSchema,
231
- streamId: operationIdSchema,
232
- streamName: registrationNameSchema,
233
- type: z.literal("stream-open")
234
- });
235
- const streamMessageSchema = z.object({
236
- message: z.unknown(),
237
- streamId: operationIdSchema,
238
- type: z.literal("stream-message")
239
- });
240
- const streamCloseMessageSchema = z.object({
241
- streamId: operationIdSchema,
242
- type: z.literal("stream-close")
243
- });
244
- const clientDiagnosticEventSchema = z.object({
245
- arguments: z.string().max(16100),
246
- level: z.enum([
247
- "log",
248
- "info",
249
- "warn",
250
- "error",
251
- "debug"
252
- ]),
253
- message: z.string().max(4100),
254
- stack: z.string().max(8100).optional(),
255
- timestamp: z.iso.datetime(),
256
- type: z.literal("client-diagnostic"),
257
- url: z.string().max(2100).optional()
258
- });
259
- z.discriminatedUnion("type", [
260
- resourceReadMessageSchema,
261
- resourceSubscribeMessageSchema,
262
- resourceUnsubscribeMessageSchema,
263
- streamOpenMessageSchema,
264
- streamMessageSchema,
265
- streamCloseMessageSchema,
266
- clientDiagnosticEventSchema
267
- ]);
268
- const resourceResultMessageSchema = z.object({
269
- operationId: operationIdSchema,
270
- output: z.unknown(),
271
- type: z.literal("resource-result")
272
- });
273
- const resourceInvalidatedMessageSchema = z.object({
274
- subscriptionId: operationIdSchema,
275
- type: z.literal("resource-invalidated")
276
- });
277
- const streamOpenedMessageSchema = z.object({
278
- operationId: operationIdSchema,
279
- streamId: operationIdSchema,
280
- type: z.literal("stream-opened")
281
- });
282
- const streamOutputMessageSchema = z.object({
283
- message: z.unknown(),
284
- streamId: operationIdSchema,
285
- type: z.literal("stream-output")
286
- });
287
- const streamClosedMessageSchema = z.object({
288
- streamId: operationIdSchema,
289
- type: z.literal("stream-closed")
290
- });
291
- const protocolErrorMessageSchema = z.object({
292
- code: z.enum([
293
- "bad-request",
294
- "conflict",
295
- "internal",
296
- "not-found"
297
- ]),
298
- message: z.string(),
299
- operationId: operationIdSchema.optional(),
300
- streamId: operationIdSchema.optional(),
301
- subscriptionId: operationIdSchema.optional(),
302
- type: z.literal("error")
303
- });
304
- const serverInfoMessageSchema = z.object({
305
- type: z.literal("server-info"),
306
- instanceId: instanceIdSchema
307
- }).strict();
308
- z.discriminatedUnion("type", [
309
- serverInfoMessageSchema,
310
- resourceResultMessageSchema,
311
- resourceInvalidatedMessageSchema,
312
- streamOpenedMessageSchema,
313
- streamOutputMessageSchema,
314
- streamClosedMessageSchema,
315
- protocolErrorMessageSchema,
316
- updateAvailableEventSchema,
317
- restartingEventSchema,
318
- notificationEventSchema
319
- ]);
320
- //#endregion
321
- //#region src/internal/server/runtime/runtime-instance.ts
322
- const createRuntimeInstance = (configured) => {
323
- let identity;
324
- const get = () => {
325
- if (!identity) throw new Error("Overmux instance identity is not ready");
326
- return identity;
327
- };
328
- return {
329
- context: {
330
- getInstanceId: () => get().instanceId,
331
- getDeepLinkPrefix: () => get().deepLinkPrefix
332
- },
333
- establish: (port) => {
334
- if (identity) throw new Error("Overmux instance identity is already established");
335
- try {
336
- identity = createInstanceIdentity(typeof configured === "function" ? configured({ port }) : configured ?? `${hostname()}-${port}`);
337
- } catch (cause) {
338
- throw new Error("Invalid Overmux instance ID; configure instanceId with a canonical lowercase ID", { cause });
339
- }
340
- return identity;
341
- }
342
- };
343
- };
344
- //#endregion
345
- //#region src/internal/server/runtime/runtime-lifecycle.ts
346
- const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
347
- const disposeAll = async (disposers) => {
348
- const errors = (await Promise.allSettled([...disposers].map(async (dispose) => dispose()))).flatMap((result) => result.status === "rejected" ? [asError(result.reason)] : []);
349
- if (errors.length) throw new AggregateError(errors, "Runtime disposal failed");
350
- };
351
- const createRuntimeLifecycle = (preparationSignal) => {
352
- const controller = new AbortController();
353
- const activeDisposers = /* @__PURE__ */ new Set();
354
- let disposal;
355
- const abortPreparation = () => controller.abort(preparationSignal?.reason);
356
- preparationSignal?.addEventListener("abort", abortPreparation, { once: true });
357
- if (preparationSignal?.aborted) abortPreparation();
358
- const completePreparation = () => {
359
- preparationSignal?.removeEventListener("abort", abortPreparation);
360
- };
361
- const requestSignal = (signal) => signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
362
- const track = (dispose, signal) => {
363
- let pending;
364
- const disposeOnce = () => {
365
- if (pending) return pending;
366
- controller.signal.removeEventListener("abort", onAbort);
367
- signal?.removeEventListener("abort", onAbort);
368
- activeDisposers.delete(disposeOnce);
369
- pending = Promise.resolve().then(dispose);
370
- return pending;
371
- };
372
- const onAbort = () => {
373
- disposeOnce().catch(() => void 0);
374
- };
375
- activeDisposers.add(disposeOnce);
376
- controller.signal.addEventListener("abort", onAbort, { once: true });
377
- signal?.addEventListener("abort", onAbort, { once: true });
378
- if (controller.signal.aborted || signal?.aborted) onAbort();
379
- return disposeOnce;
380
- };
381
- const dispose = (reason = /* @__PURE__ */ new Error("Runtime disposed")) => {
382
- if (disposal) return disposal;
383
- completePreparation();
384
- const pending = [...activeDisposers];
385
- activeDisposers.clear();
386
- controller.abort(reason);
387
- disposal = disposeAll(pending);
388
- return disposal;
389
- };
390
- return {
391
- completePreparation,
392
- dispose,
393
- disposeAll,
394
- requestSignal,
395
- signal: controller.signal,
396
- track
397
- };
398
- };
399
- //#endregion
400
- //#region src/internal/server/runtime/runtime-operations.ts
401
- var OperationValidationError = class extends Error {
402
- phase;
403
- constructor(phase, cause) {
404
- super(cause.message, { cause });
405
- this.phase = phase;
406
- }
407
- };
408
- const createRuntimeOperations = ({ context, definitions, notifications }) => {
409
- const operations = /* @__PURE__ */ new Map();
410
- Object.entries(definitions).forEach(([name, definition]) => {
411
- if (!name) throw new Error("Operation names must not be empty");
412
- operations.set(name, {
413
- execute: async (rawInput, signal) => {
414
- const operationContext = context(signal);
415
- operationContext.signal.throwIfAborted();
416
- const input = definition.input.safeParse(rawInput);
417
- if (!input.success) throw new OperationValidationError("input", input.error);
418
- const rawOutput = await definition.handle(input.data, {
419
- ...operationContext,
420
- notifications
421
- });
422
- operationContext.signal.throwIfAborted();
423
- const output = definition.output.safeParse(rawOutput);
424
- if (!output.success) throw new OperationValidationError("output", output.error);
425
- return output.data;
426
- },
427
- returnsVoid: definition.output instanceof z.ZodVoid
428
- });
429
- });
430
- return {
431
- get: (name) => operations.get(name),
432
- names: [...operations.keys()]
433
- };
434
- };
435
- z.void();
436
- const jsonValueSchema = z.lazy(() => z.union([
437
- z.string(),
438
- z.number().finite(),
439
- z.boolean(),
440
- z.null(),
441
- z.array(jsonValueSchema),
442
- z.record(z.string(), jsonValueSchema)
443
- ]));
444
- //#endregion
445
- //#region src/internal/server/runtime/resource-dependency-graph.ts
446
- const emptyGraphFor = (resourceIds) => new Map(resourceIds.map((resourceId) => [resourceId, /* @__PURE__ */ new Set()]));
447
- const visitDependencies = ({ dependencies, path, resourceId, visited, visiting }) => {
448
- if (visiting.has(resourceId)) throw new Error(`Resource dependency cycle: ${[...path, resourceId].join(" -> ")}`);
449
- if (visited.has(resourceId)) return;
450
- visiting.add(resourceId);
451
- dependencies.get(resourceId)?.forEach((dependencyId) => {
452
- visitDependencies({
453
- dependencies,
454
- path: [...path, resourceId],
455
- resourceId: dependencyId,
456
- visited,
457
- visiting
458
- });
459
- });
460
- visiting.delete(resourceId);
461
- visited.add(resourceId);
462
- };
463
- const assertAcyclic = (dependencies) => {
464
- const visited = /* @__PURE__ */ new Set();
465
- const visiting = /* @__PURE__ */ new Set();
466
- dependencies.forEach((_, resourceId) => {
467
- visitDependencies({
468
- dependencies,
469
- path: [],
470
- resourceId,
471
- visited,
472
- visiting
473
- });
474
- });
475
- };
476
- const createResourceDependencyGraph = (resources) => {
477
- const resourceIds = Object.keys(resources);
478
- if (resourceIds.some((resourceId) => !resourceId)) throw new Error("Resource IDs must not be empty");
479
- const dependencies = emptyGraphFor(resourceIds);
480
- const dependants = emptyGraphFor(resourceIds);
481
- Object.entries(resources).forEach(([resourceId, resource]) => {
482
- if (resource.kind !== "derived") return;
483
- Object.entries(resource.dependencies).forEach(([dependencyName, dependencyId]) => {
484
- if (!dependencyName) throw new Error(`Resource ${resourceId} has an empty dependency name`);
485
- if (!dependencyId) throw new Error(`Resource ${resourceId} has an empty dependency ID`);
486
- if (!dependencies.has(dependencyId)) throw new Error(`Unknown resource dependency: ${dependencyId}`);
487
- dependencies.get(resourceId)?.add(dependencyId);
488
- dependants.get(dependencyId)?.add(resourceId);
489
- });
490
- });
491
- assertAcyclic(dependencies);
492
- return Object.freeze({
493
- dependencies,
494
- dependants
495
- });
496
- };
497
- const collectInvalidatedResourceIds = (graph, resourceId, invalidated) => {
498
- if (invalidated.has(resourceId)) return;
499
- invalidated.add(resourceId);
500
- graph.dependants.get(resourceId)?.forEach((dependantId) => {
501
- collectInvalidatedResourceIds(graph, dependantId, invalidated);
502
- });
503
- };
504
- const getInvalidatedResourceIds = (graph, resourceId) => {
505
- const invalidated = /* @__PURE__ */ new Set();
506
- collectInvalidatedResourceIds(graph, resourceId, invalidated);
507
- return invalidated;
508
- };
509
- //#endregion
510
- //#region src/internal/server/runtime/runtime-resources.ts
511
- const createRuntimeResources = ({ definitions, instance, lifecycle }) => {
512
- const graph = createResourceDependencyGraph(definitions);
513
- const invalidationListeners = /* @__PURE__ */ new Set();
514
- const contracts = /* @__PURE__ */ new Map();
515
- const resources = /* @__PURE__ */ new Map();
516
- Object.entries(definitions).forEach(([id, definition]) => {
517
- contracts.set(definition.contract, [...contracts.get(definition.contract) ?? [], id]);
518
- });
519
- const invalidateResource = (id, input, allInputs = false) => {
520
- if (lifecycle.signal.aborted) return;
521
- const ids = getInvalidatedResourceIds(graph, id);
522
- invalidationListeners.forEach((entry) => {
523
- if (ids.has(entry.resourceId) && (allInputs || isDeepStrictEqual(entry.input, input))) entry.listener();
524
- });
525
- };
526
- const invalidate = (contract, input) => {
527
- const ids = contracts.get(contract) ?? Object.entries(definitions).filter(([, definition]) => definition.contract.input === contract.input && definition.contract.output === contract.output).map(([id]) => id);
528
- if (!ids.length) throw new Error("Cannot invalidate a resource outside this runtime");
529
- ids.forEach((id) => {
530
- const definition = definitions[id];
531
- const parsedInput = input === void 0 ? void 0 : definition.contract.input.parse(input);
532
- invalidateResource(id, parsedInput, input === void 0);
533
- });
534
- };
535
- const context = (signal) => ({
536
- instance,
537
- invalidate,
538
- signal: lifecycle.requestSignal(signal)
539
- });
540
- const read = async (id, rawInput, signal) => {
541
- const definition = definitions[id];
542
- const operationSignal = lifecycle.requestSignal(signal);
543
- operationSignal.throwIfAborted();
544
- const input = definition.contract.input.parse(rawInput);
545
- const rawOutput = definition.kind === "derived" ? definition.combine(Object.fromEntries(await Promise.all(Object.entries(definition.dependencies).map(async ([name, dependencyId]) => [name, await read(dependencyId, rawInput, signal)])))) : await definition.read(input, context(signal));
546
- operationSignal.throwIfAborted();
547
- return definition.contract.output.parse(rawOutput);
548
- };
549
- const activateWatchers = (id, rawInput, signal, activated, disposers) => {
550
- if (activated.has(id)) return;
551
- activated.add(id);
552
- const definition = definitions[id];
553
- const input = definition.contract.input.parse(rawInput);
554
- if (definition.kind === "subscription") {
555
- const dispose = definition.subscribe(input, () => invalidateResource(id, input), context(signal));
556
- if (typeof dispose !== "function") throw new Error(`Resource ${id} subscribe must return a disposer`);
557
- disposers.push(dispose);
558
- return;
559
- }
560
- if (definition.kind === "derived") graph.dependencies.get(id)?.forEach((dependencyId) => {
561
- activateWatchers(dependencyId, rawInput, signal, activated, disposers);
562
- });
563
- };
564
- Object.keys(definitions).forEach((name) => {
565
- resources.set(name, {
566
- read: (input, signal) => read(name, input, signal),
567
- subscribe: (rawInput, listener, signal) => {
568
- lifecycle.requestSignal(signal).throwIfAborted();
569
- const entry = {
570
- input: definitions[name].contract.input.parse(rawInput),
571
- listener,
572
- resourceId: name
573
- };
574
- const watcherDisposers = [];
575
- invalidationListeners.add(entry);
576
- try {
577
- activateWatchers(name, rawInput, signal, /* @__PURE__ */ new Set(), watcherDisposers);
578
- } catch (cause) {
579
- invalidationListeners.delete(entry);
580
- lifecycle.disposeAll(watcherDisposers).catch(() => void 0);
581
- throw cause;
582
- }
583
- return lifecycle.track(async () => {
584
- invalidationListeners.delete(entry);
585
- await lifecycle.disposeAll(watcherDisposers);
586
- }, signal);
587
- }
588
- });
589
- });
590
- return {
591
- context,
592
- get: (name) => resources.get(name),
593
- invalidate,
594
- names: [...resources.keys()]
595
- };
596
- };
597
- //#endregion
598
- //#region src/internal/server/runtime/runtime-streams.ts
599
- const createRuntimeStreams = ({ context, definitions, lifecycle }) => {
600
- const streams = /* @__PURE__ */ new Map();
601
- Object.entries(definitions).forEach(([name, definition]) => {
602
- if (!name) throw new Error("Stream names must not be empty");
603
- streams.set(name, { open: async (rawInput, emit, requestSignal, onError) => {
604
- const sessionController = new AbortController();
605
- const sessionSignal = requestSignal ? AbortSignal.any([requestSignal, sessionController.signal]) : sessionController.signal;
606
- const operationSignal = lifecycle.requestSignal(sessionSignal);
607
- operationSignal.throwIfAborted();
608
- const input = definition.contract.input.parse(rawInput);
609
- let acceptingMessages = true;
610
- let failed = false;
611
- let failure;
612
- let opened = false;
613
- const fail = (cause) => {
614
- if (failed || operationSignal.aborted) return;
615
- failed = true;
616
- failure = cause;
617
- acceptingMessages = false;
618
- sessionController.abort(cause);
619
- if (opened) onError?.(cause);
620
- };
621
- const session = await definition.open(input, {
622
- ...context(sessionSignal),
623
- emit: (message) => {
624
- if (operationSignal.aborted) return;
625
- try {
626
- emit(definition.contract.serverMessage.parse(message));
627
- } catch (cause) {
628
- fail(cause);
629
- }
630
- },
631
- fail,
632
- signal: operationSignal
633
- });
634
- opened = true;
635
- if (failed || operationSignal.aborted) {
636
- await session.dispose?.();
637
- operationSignal.throwIfAborted();
638
- throw failure;
639
- }
640
- let messages = Promise.resolve();
641
- const dispose = lifecycle.track(async () => {
642
- acceptingMessages = false;
643
- sessionController.abort(/* @__PURE__ */ new Error("Stream session disposed"));
644
- await messages.catch(() => void 0);
645
- await session.dispose?.();
646
- }, requestSignal);
647
- return {
648
- dispose: async () => dispose(),
649
- send: async (rawMessage) => {
650
- if (!acceptingMessages) throw new Error("Stream session is closed");
651
- const message = definition.contract.clientMessage.parse(rawMessage);
652
- messages = messages.then(async () => {
653
- operationSignal.throwIfAborted();
654
- await session.onMessage?.(message);
655
- operationSignal.throwIfAborted();
656
- });
657
- return messages;
658
- }
659
- };
660
- } });
661
- });
662
- return {
663
- get: (name) => streams.get(name),
664
- names: [...streams.keys()]
665
- };
666
- };
667
- //#endregion
668
- //#region src/internal/server/runtime/create-runtime.ts
669
- const createRuntime = async ({ config, debug = false, notifications = { send: async () => void 0 }, signal }) => {
670
- const lifecycle = createRuntimeLifecycle(signal);
671
- const instance = createRuntimeInstance(config.instanceId);
672
- try {
673
- const resources = createRuntimeResources({
674
- definitions: config.server.resources ?? {},
675
- instance: instance.context,
676
- lifecycle
677
- });
678
- const context = resources.context;
679
- const operations = createRuntimeOperations({
680
- context,
681
- definitions: config.server.operations ?? {},
682
- notifications
683
- });
684
- const streams = createRuntimeStreams({
685
- context,
686
- definitions: config.server.streams ?? {},
687
- lifecycle
688
- });
689
- lifecycle.signal.throwIfAborted();
690
- lifecycle.completePreparation();
691
- const manifest = {
692
- debug,
693
- operations: [...operations.names],
694
- protocolVersion: 10,
695
- resources: [...resources.names],
696
- streams: [...streams.names]
697
- };
698
- return {
699
- instance: instance.context,
700
- establishInstance: instance.establish,
701
- manifest,
702
- dispose: lifecycle.dispose,
703
- getOperation: operations.get,
704
- getResource: resources.get,
705
- getStream: streams.get
706
- };
707
- } catch (cause) {
708
- await lifecycle.dispose(cause).catch(() => void 0);
709
- throw cause;
710
- }
711
- };
712
- //#endregion
713
- //#region src/internal/server/check-overmux.ts
714
- const loadCompilerOptions = (configPath) => {
715
- const tsconfig = ts.findConfigFile(dirname(configPath), ts.sys.fileExists);
716
- if (!tsconfig) return {
717
- allowImportingTsExtensions: true,
718
- allowJs: false,
719
- esModuleInterop: true,
720
- jsx: ts.JsxEmit.ReactJSX,
721
- module: ts.ModuleKind.ESNext,
722
- moduleResolution: ts.ModuleResolutionKind.Bundler,
723
- noEmit: true,
724
- skipLibCheck: true,
725
- strict: true,
726
- target: ts.ScriptTarget.ES2022
727
- };
728
- const loaded = ts.readConfigFile(tsconfig, ts.sys.readFile);
729
- if (loaded.error) throw new Error(ts.flattenDiagnosticMessageText(loaded.error.messageText, "\n"));
730
- return {
731
- ...ts.parseJsonConfigFileContent(loaded.config, ts.sys, dirname(tsconfig)).options,
732
- allowImportingTsExtensions: true,
733
- noEmit: true
734
- };
735
- };
736
- const diagnosticOf = (diagnostic) => {
737
- const position = diagnostic.file && diagnostic.start !== void 0 ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start) : void 0;
738
- return {
739
- code: diagnostic.code,
740
- column: position ? position.character + 1 : void 0,
741
- file: diagnostic.file?.fileName,
742
- line: position ? position.line + 1 : void 0,
743
- message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
744
- phase: "types"
745
- };
746
- };
747
- const kindOf = (fileName, configPath) => {
748
- if (resolve(fileName) === resolve(configPath) || fileName.match(/\.server\.[cm]?[jt]sx?$/)) return "server";
749
- if (fileName.match(/\.ui\.[cm]?[jt]sx?$/)) return "client";
750
- if (fileName.match(/\.shared\.[cm]?[jt]sx?$/)) return "shared";
751
- };
752
- const kindOfSpecifier = (specifier) => {
753
- if (specifier === "overmux/client" || specifier.endsWith("/react")) return "client";
754
- if (specifier === "overmux/server" || specifier.endsWith("/server") || specifier.startsWith("node:")) return "server";
755
- if (specifier === "overmux" || specifier.endsWith("/shared")) return "shared";
756
- };
757
- const isTypeOnlyReference = (node) => {
758
- if (ts.isExportDeclaration(node)) return Boolean(node.isTypeOnly || node.exportClause && ts.isNamedExports(node.exportClause) && node.exportClause.elements.every((element) => element.isTypeOnly));
759
- const clause = node.importClause;
760
- if (!clause || clause.name) return Boolean(clause?.isTypeOnly);
761
- return Boolean(clause.isTypeOnly || clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.every((element) => element.isTypeOnly));
762
- };
763
- const resolvedImports = (program, source) => {
764
- const imports = [];
765
- source.forEachChild((node) => {
766
- if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || isTypeOnlyReference(node)) return;
767
- const specifier = node.moduleSpecifier.text;
768
- const resolved = ts.resolveModuleName(specifier, source.fileName, program.getCompilerOptions(), ts.sys).resolvedModule?.resolvedFileName;
769
- const target = resolved ? program.getSourceFile(resolved) : void 0;
770
- const kind = kindOfSpecifier(specifier);
771
- if (kind || target && !target.isDeclarationFile) imports.push({
772
- kind,
773
- node,
774
- source: target?.isDeclarationFile ? void 0 : target
775
- });
776
- });
777
- return imports;
778
- };
779
- const invalidImport = (source, target) => source === "server" && target === "client" || source === "client" && target === "server" || source === "shared" && target !== void 0 && target !== "shared";
780
- const visitImportGraph = ({ configPath, diagnostics, kind, program, seen, source }) => {
781
- const visitKey = `${kind}:${source.fileName}`;
782
- if (seen.has(visitKey)) return;
783
- seen.add(visitKey);
784
- resolvedImports(program, source).forEach(({ kind: importKind, node, source: target }) => {
785
- const declaredKind = importKind ?? (target ? kindOf(target.fileName, configPath) : void 0);
786
- if (invalidImport(kind, declaredKind)) {
787
- const position = source.getLineAndCharacterOfPosition(node.getStart());
788
- diagnostics.push({
789
- code: "OVERMUX_IMPORT_BOUNDARY",
790
- column: position.character + 1,
791
- file: source.fileName,
792
- line: position.line + 1,
793
- message: `${kind} modules may not import ${declaredKind} modules`,
794
- phase: "imports"
795
- });
796
- return;
797
- }
798
- if (target) visitImportGraph({
799
- configPath,
800
- diagnostics,
801
- kind: declaredKind === "shared" ? "shared" : kind,
802
- program,
803
- seen,
804
- source: target
805
- });
806
- });
807
- };
808
- const importBoundaryDiagnostics = (program, configPath) => {
809
- const diagnostics = [];
810
- const seen = /* @__PURE__ */ new Set();
811
- program.getSourceFiles().forEach((source) => {
812
- const kind = kindOf(source.fileName, configPath);
813
- if (kind && !source.isDeclarationFile) visitImportGraph({
814
- configPath,
815
- diagnostics,
816
- kind,
817
- program,
818
- seen,
819
- source
820
- });
821
- });
822
- return diagnostics;
823
- };
824
- const runtimeDiagnostics = (cause) => {
825
- if (cause instanceof ZodError) return cause.issues.map((issue) => ({
826
- message: issue.message,
827
- path: issue.path.length ? issue.path.join(".") : void 0,
828
- phase: "runtime"
829
- }));
830
- return [{
831
- message: cause instanceof Error ? cause.message : String(cause),
832
- phase: "runtime"
833
- }];
834
- };
835
- const typecheck = async ({ aliases = {}, configPath, typeAliases }) => {
836
- const options = loadCompilerOptions(configPath);
837
- const aliasesForTypes = typeAliases ?? Object.fromEntries(Object.entries(aliases).filter(([key]) => key === "overmux" || key.startsWith("overmux/")));
838
- const absoluteAliases = Object.fromEntries(Object.entries(aliasesForTypes).map(([key, value]) => [key, [isAbsolute(value) ? value : resolve(value)]]));
839
- const program = ts.createProgram({
840
- options: {
841
- ...options,
842
- baseUrl: options.baseUrl ?? "/",
843
- paths: {
844
- ...options.paths,
845
- ...absoluteAliases
846
- }
847
- },
848
- rootNames: [configPath]
849
- });
850
- return {
851
- diagnostics: [...ts.getPreEmitDiagnostics(program).map(diagnosticOf), ...importBoundaryDiagnostics(program, configPath)],
852
- files: program.getSourceFiles().filter((file) => !file.isDeclarationFile && !file.fileName.includes("node_modules")).length
853
- };
854
- };
855
- const checkOvermux = async (options) => {
856
- const configPath = resolve(options.configPath);
857
- let checked;
858
- try {
859
- checked = await typecheck({
860
- ...options,
861
- configPath
862
- });
863
- } catch (cause) {
864
- return {
865
- config: configPath,
866
- diagnostics: [{
867
- message: cause instanceof Error ? cause.message : String(cause),
868
- phase: "config"
869
- }],
870
- files: 0,
871
- ok: false
872
- };
873
- }
874
- if (checked.diagnostics.length || options.typesOnly) return {
875
- config: configPath,
876
- diagnostics: checked.diagnostics,
877
- files: checked.files,
878
- ok: checked.diagnostics.length === 0
879
- };
880
- let runtime;
881
- try {
882
- const { config } = await loadOvermuxConfig({
883
- aliases: options.aliases,
884
- configPath
885
- });
886
- runtime = await createRuntime({ config });
887
- return {
888
- operations: runtime.manifest.operations.length,
889
- config: configPath,
890
- diagnostics: [],
891
- files: checked.files,
892
- ok: true
893
- };
894
- } catch (cause) {
895
- return {
896
- config: configPath,
897
- diagnostics: runtimeDiagnostics(cause),
898
- files: checked.files,
899
- ok: false
900
- };
901
- } finally {
902
- await runtime?.dispose();
903
- }
904
- };
905
- //#endregion
906
- //#region src/internal/server/server-logger.ts
907
- const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
908
- //#endregion
909
- //#region src/internal/server/coordinator/ipc-protocol.ts
910
- const childOptionsSchema = z.object({
911
- configAliases: z.record(z.string(), z.string()).optional(),
912
- configPath: z.string(),
913
- debug: z.boolean().optional(),
914
- developmentWebTarget: z.url().optional(),
915
- host: z.string().optional(),
916
- logFile: z.string().optional(),
917
- port: z.number().int().min(0).max(65535).optional(),
918
- productionWebAssetsDir: z.string().optional(),
919
- watch: z.boolean().optional()
920
- });
921
- z.discriminatedUnion("type", [
922
- z.object({
923
- options: childOptionsSchema,
924
- type: z.literal("start")
925
- }),
926
- z.object({ type: z.literal("update-available") }),
927
- z.object({ type: z.literal("stop") })
928
- ]);
929
- const childMessageSchema = z.discriminatedUnion("type", [
930
- z.object({
931
- host: z.string(),
932
- port: z.number().int().min(1).max(65535),
933
- type: z.literal("ready"),
934
- url: z.string(),
935
- watch: z.boolean()
936
- }),
937
- z.object({ type: z.literal("restart-requested") }),
938
- z.object({
939
- message: z.string(),
940
- type: z.literal("startup-failed")
941
- })
942
- ]);
943
- const parseChildMessage = (value) => childMessageSchema.parse(value);
944
- //#endregion
945
- //#region src/internal/server/coordinator/update-watcher.ts
946
- const isRelevantUpdatePath = (path) => {
947
- if (path.split(/[\\/]/).some((segment) => segment.toLowerCase() === "node_modules")) return false;
948
- return /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts|json|css)$/i.test(path);
949
- };
950
- const startUpdateWatcher = ({ configPath, onUpdate }) => {
951
- let closed = false;
952
- let timeout;
953
- const watcher = watch(dirname(configPath), { recursive: true }, (_event, fileName) => {
954
- if (closed || !fileName || !isRelevantUpdatePath(fileName.toString())) return;
955
- if (timeout) clearTimeout(timeout);
956
- timeout = setTimeout(onUpdate, 100);
957
- });
958
- return { close: () => {
959
- if (closed) return;
960
- closed = true;
961
- if (timeout) clearTimeout(timeout);
962
- watcher.close();
963
- } };
964
- };
965
- //#endregion
966
- //#region src/internal/server/coordinator/server-coordinator.ts
967
- const send = (child, message, onError) => {
968
- if (!child.connected) {
969
- onError?.(/* @__PURE__ */ new Error("Server child IPC channel is closed"));
970
- return;
971
- }
972
- try {
973
- child.send(message, (cause) => {
974
- if (cause) onError?.(cause);
975
- });
976
- } catch (cause) {
977
- onError?.(cause instanceof Error ? cause : new Error(String(cause)));
978
- }
979
- };
980
- const stopChild = (child) => new Promise((resolve) => {
981
- let timeout;
982
- const finish = () => {
983
- child.off("exit", finish);
984
- clearTimeout(timeout);
985
- resolve();
986
- };
987
- child.once("exit", finish);
988
- if (child.exitCode !== null || child.signalCode !== null) {
989
- finish();
990
- return;
991
- }
992
- timeout = setTimeout(() => {
993
- if (!child.kill("SIGKILL")) finish();
994
- }, 5e3);
995
- send(child, { type: "stop" });
996
- });
997
- const serverChildPath = () => fileURLToPath(import.meta.resolve("#server-child"));
998
- const startChild = (options, onRestartRequested) => new Promise((resolve, reject) => {
999
- const child = fork(serverChildPath(), [], {
1000
- serialization: "json",
1001
- stdio: [
1002
- "inherit",
1003
- "inherit",
1004
- "inherit",
1005
- "ipc"
1006
- ]
1007
- });
1008
- let settled = false;
1009
- const fail = (cause) => {
1010
- if (settled) return;
1011
- settled = true;
1012
- if (child.exitCode === null && child.signalCode === null) child.kill();
1013
- reject(cause instanceof Error ? cause : new Error(String(cause)));
1014
- };
1015
- child.on("error", fail);
1016
- child.on("exit", (code, signal) => {
1017
- fail(/* @__PURE__ */ new Error(`Server child exited before readiness (${signal ?? code ?? "unknown"})`));
1018
- });
1019
- child.on("message", (raw) => {
1020
- let message;
1021
- try {
1022
- message = parseChildMessage(raw);
1023
- } catch (cause) {
1024
- fail(cause);
1025
- return;
1026
- }
1027
- if (message.type === "restart-requested") {
1028
- if (settled) onRestartRequested();
1029
- return;
1030
- }
1031
- if (message.type === "startup-failed") {
1032
- fail(new Error(message.message));
1033
- return;
1034
- }
1035
- if (!settled) {
1036
- settled = true;
1037
- resolve({
1038
- host: message.host,
1039
- port: message.port,
1040
- process: child,
1041
- url: message.url,
1042
- watch: message.watch
1043
- });
1044
- }
1045
- });
1046
- send(child, {
1047
- options,
1048
- type: "start"
1049
- }, fail);
1050
- });
1051
- const startServerCoordinator = async (initialOptions) => {
1052
- let child;
1053
- let closed = false;
1054
- let closePromise;
1055
- let restartPromise;
1056
- let updateDuringRestart = false;
1057
- let startupAddress;
1058
- let watcher;
1059
- const adoptChild = (next) => {
1060
- child = next;
1061
- next.process.once("exit", () => {
1062
- if (child?.process === next.process) child = void 0;
1063
- });
1064
- };
1065
- const restart = () => {
1066
- if (closed || restartPromise) return restartPromise ?? Promise.resolve();
1067
- restartPromise = (async () => {
1068
- const previous = child;
1069
- child = void 0;
1070
- if (previous) await stopChild(previous.process);
1071
- if (closed || !startupAddress) return;
1072
- try {
1073
- const replacement = await startChild({
1074
- ...initialOptions,
1075
- host: startupAddress.host,
1076
- port: startupAddress.port
1077
- }, () => void restart());
1078
- if (closed) {
1079
- await stopChild(replacement.process);
1080
- return;
1081
- }
1082
- adoptChild(replacement);
1083
- if (updateDuringRestart) {
1084
- updateDuringRestart = false;
1085
- send(replacement.process, { type: "update-available" });
1086
- }
1087
- } catch (cause) {
1088
- process.stderr.write(`[overmux] replacement child failed: ${errorMessage(cause)}\n`);
1089
- }
1090
- })().finally(() => {
1091
- const shouldRetry = updateDuringRestart && !child && !closed;
1092
- updateDuringRestart = false;
1093
- restartPromise = void 0;
1094
- if (shouldRetry) restart();
1095
- });
1096
- return restartPromise;
1097
- };
1098
- const onUpdate = () => {
1099
- if (closed) return;
1100
- if (restartPromise) {
1101
- updateDuringRestart = true;
1102
- return;
1103
- }
1104
- if (child) {
1105
- send(child.process, { type: "update-available" });
1106
- return;
1107
- }
1108
- restart();
1109
- };
1110
- const initial = await startChild(initialOptions, () => void restart());
1111
- startupAddress = {
1112
- host: initial.host,
1113
- port: initial.port
1114
- };
1115
- adoptChild(initial);
1116
- try {
1117
- if (initial.watch) watcher = startUpdateWatcher({
1118
- configPath: initialOptions.configPath,
1119
- onUpdate
1120
- });
1121
- } catch (cause) {
1122
- await stopChild(initial.process);
1123
- throw cause;
1124
- }
1125
- const close = () => {
1126
- if (closePromise) return closePromise;
1127
- closed = true;
1128
- closePromise = (async () => {
1129
- process.off("SIGINT", onSignal);
1130
- process.off("SIGTERM", onSignal);
1131
- watcher?.close();
1132
- const active = child;
1133
- child = void 0;
1134
- if (active) await stopChild(active.process);
1135
- await restartPromise;
1136
- })();
1137
- return closePromise;
1138
- };
1139
- const onSignal = () => {
1140
- close().catch((cause) => {
1141
- process.stderr.write(`[overmux] coordinator shutdown failed: ${errorMessage(cause)}\n`);
1142
- });
1143
- };
1144
- process.once("SIGINT", onSignal);
1145
- process.once("SIGTERM", onSignal);
1146
- return {
1147
- announceUpdateAvailable: onUpdate,
1148
- close,
1149
- port: initial.port,
1150
- url: initial.url
1151
- };
1152
- };
1153
- //#endregion
1154
- //#region src/internal/server/start-overmux-server.ts
1155
- const startOvermuxServer = async (options) => startServerCoordinator({
1156
- ...options,
1157
- configPath: resolve(options.configPath)
1158
- });
1159
- //#endregion
1160
- export { checkOvermux, getOvermuxPaths, startOvermuxServer };
108
+ export { getOvermuxPaths };
1161
109
 
1162
110
  //# sourceMappingURL=server.js.map