overmux 0.0.1 → 0.0.2

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 (193) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +7 -0
  3. package/dist/auth/.vite/manifest.json +11 -0
  4. package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
  5. package/dist/auth/assets/index-CHIE7qYj.js +72 -0
  6. package/dist/auth/index.html +14 -0
  7. package/dist/bin.js +4504 -7870
  8. package/dist/bin.js.map +1 -0
  9. package/dist/docs/000-index.md +21 -0
  10. package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
  11. package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
  12. package/dist/docs/100-introduction/300-why-overmux.md +22 -0
  13. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  14. package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  15. package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  16. package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  17. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  18. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  19. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  20. package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
  21. package/dist/docs/300-fundamentals/200-configuration.md +3 -0
  22. package/dist/docs/300-fundamentals/300-theming.md +3 -0
  23. package/dist/docs/300-fundamentals/400-server.md +3 -0
  24. package/dist/docs/300-fundamentals/500-client.md +3 -0
  25. package/dist/docs/300-fundamentals/600-operations.md +3 -0
  26. package/dist/docs/300-fundamentals/700-resources.md +3 -0
  27. package/dist/docs/300-fundamentals/800-streams.md +3 -0
  28. package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  29. package/dist/docs/400-reference/100-configuration.md +3 -0
  30. package/dist/docs/400-reference/200-server-api.md +3 -0
  31. package/dist/docs/400-reference/300-client-api.md +3 -0
  32. package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
  33. package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
  34. package/dist/docs/400-reference/400-cli/300-call.md +28 -0
  35. package/dist/docs/400-reference/400-cli/400-check.md +20 -0
  36. package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
  37. package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
  38. package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
  39. package/dist/docs/500-hosted-pages.md +31 -0
  40. package/dist/exports/client.d.ts +237 -0
  41. package/dist/exports/client.d.ts.map +1 -0
  42. package/dist/exports/client.js +1643 -0
  43. package/dist/exports/client.js.map +1 -0
  44. package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
  45. package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
  46. package/dist/exports/index.d.ts +2 -0
  47. package/dist/exports/index.js +49 -0
  48. package/dist/exports/index.js.map +1 -0
  49. package/dist/exports/notifications-kDp16bU_.js +56 -0
  50. package/dist/exports/notifications-kDp16bU_.js.map +1 -0
  51. package/dist/exports/server.d.ts +63 -0
  52. package/dist/exports/server.d.ts.map +1 -0
  53. package/dist/exports/server.js +1120 -0
  54. package/dist/exports/server.js.map +1 -0
  55. package/dist/exports/style.css +368 -0
  56. package/dist/internal/server/coordinator/server-child.d.ts +1 -0
  57. package/dist/internal/server/coordinator/server-child.js +3299 -0
  58. package/dist/internal/server/coordinator/server-child.js.map +1 -0
  59. package/docs/000-index.md +21 -0
  60. package/docs/100-introduction/100-what-is-overmux.md +7 -0
  61. package/docs/100-introduction/200-how-overmux-works.md +7 -0
  62. package/docs/100-introduction/300-why-overmux.md +22 -0
  63. package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  64. package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  65. package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  66. package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  67. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  68. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  69. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  70. package/docs/300-fundamentals/100-project-structure.md +3 -0
  71. package/docs/300-fundamentals/200-configuration.md +3 -0
  72. package/docs/300-fundamentals/300-theming.md +3 -0
  73. package/docs/300-fundamentals/400-server.md +3 -0
  74. package/docs/300-fundamentals/500-client.md +3 -0
  75. package/docs/300-fundamentals/600-operations.md +3 -0
  76. package/docs/300-fundamentals/700-resources.md +3 -0
  77. package/docs/300-fundamentals/800-streams.md +3 -0
  78. package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  79. package/docs/400-reference/100-configuration.md +3 -0
  80. package/docs/400-reference/200-server-api.md +3 -0
  81. package/docs/400-reference/300-client-api.md +3 -0
  82. package/docs/400-reference/400-cli/100-serve.md +33 -0
  83. package/docs/400-reference/400-cli/200-auth.md +52 -0
  84. package/docs/400-reference/400-cli/300-call.md +28 -0
  85. package/docs/400-reference/400-cli/400-check.md +20 -0
  86. package/docs/400-reference/400-cli/500-ai-context.md +102 -0
  87. package/docs/400-reference/400-cli/600-docs.md +23 -0
  88. package/docs/400-reference/400-cli/700-desktop.md +35 -0
  89. package/docs/500-hosted-pages.md +31 -0
  90. package/package.json +59 -18
  91. package/src/internal/cli/app.ts +72 -0
  92. package/src/internal/cli/bin.ts +3 -0
  93. package/src/internal/cli/commands/ai.ts +44 -0
  94. package/src/internal/cli/commands/auth.ts +195 -0
  95. package/src/internal/cli/commands/call.ts +128 -0
  96. package/src/internal/cli/commands/check.ts +48 -0
  97. package/src/internal/cli/commands/desktop-installer.ts +446 -0
  98. package/src/internal/cli/commands/desktop-release.ts +190 -0
  99. package/src/internal/cli/commands/desktop.ts +47 -0
  100. package/src/internal/cli/commands/docs.ts +34 -0
  101. package/src/internal/cli/commands/serve.ts +322 -0
  102. package/src/internal/cli/environment.ts +54 -0
  103. package/src/internal/cli/login.ts +46 -0
  104. package/src/internal/cli/managed-vite.ts +267 -0
  105. package/src/internal/cli/parse-port.ts +8 -0
  106. package/src/internal/client/auth/auth-api.ts +53 -0
  107. package/src/internal/client/auth/auth-shell.css +74 -0
  108. package/src/internal/client/auth/auth-shell.tsx +167 -0
  109. package/src/internal/client/auth/entry.tsx +26 -0
  110. package/src/internal/client/auth/index.html +13 -0
  111. package/src/internal/client/background-notifications.ts +194 -0
  112. package/src/internal/client/browser-api.ts +97 -0
  113. package/src/internal/client/client-definition.ts +184 -0
  114. package/src/internal/client/commands.tsx +260 -0
  115. package/src/internal/client/host/browser-log-forwarding.ts +149 -0
  116. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
  117. package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
  118. package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
  119. package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
  120. package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
  121. package/src/internal/client/host/notification-forwarding.ts +22 -0
  122. package/src/internal/client/host/overmux-host.tsx +200 -0
  123. package/src/internal/client/host/recovery-screen.css +75 -0
  124. package/src/internal/client/host/recovery-screen.tsx +17 -0
  125. package/src/internal/client/host/update-popover.css +82 -0
  126. package/src/internal/client/host/update-popover.tsx +166 -0
  127. package/src/internal/client/index.ts +45 -0
  128. package/src/internal/client/overmux-react.ts +283 -0
  129. package/src/internal/client/shortcuts.tsx +373 -0
  130. package/src/internal/client/theme-scope.css +114 -0
  131. package/src/internal/client/theme-scope.tsx +107 -0
  132. package/src/internal/client/transport.ts +69 -0
  133. package/src/internal/client/websocket.ts +441 -0
  134. package/src/internal/server/auth/auth-http.ts +414 -0
  135. package/src/internal/server/auth/auth-service.ts +627 -0
  136. package/src/internal/server/auth/auth-shell-http.ts +90 -0
  137. package/src/internal/server/auth/instance-control.ts +381 -0
  138. package/src/internal/server/check-overmux.ts +364 -0
  139. package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
  140. package/src/internal/server/coordinator/server-child.ts +108 -0
  141. package/src/internal/server/coordinator/server-coordinator.ts +262 -0
  142. package/src/internal/server/coordinator/update-watcher.ts +54 -0
  143. package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
  144. package/src/internal/server/http/background-notification-handlers.ts +83 -0
  145. package/src/internal/server/http/control-handlers.ts +44 -0
  146. package/src/internal/server/http/create-http-app.ts +53 -0
  147. package/src/internal/server/http/development-web-backend.ts +83 -0
  148. package/src/internal/server/http/operation-handler.ts +93 -0
  149. package/src/internal/server/http/request-logging.ts +34 -0
  150. package/src/internal/server/http/routes.ts +241 -0
  151. package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
  152. package/src/internal/server/index.ts +16 -0
  153. package/src/internal/server/notifications/background-notification-payload.ts +74 -0
  154. package/src/internal/server/notifications/background-notification-service.ts +139 -0
  155. package/src/internal/server/notifications/background-notification-store.ts +162 -0
  156. package/src/internal/server/notifications/notification-service.ts +70 -0
  157. package/src/internal/server/notifications/private-json-file.ts +84 -0
  158. package/src/internal/server/paths.ts +13 -0
  159. package/src/internal/server/rate-limiter.ts +58 -0
  160. package/src/internal/server/runtime/create-runtime.ts +87 -0
  161. package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
  162. package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
  163. package/src/internal/server/runtime/runtime-operations.ts +78 -0
  164. package/src/internal/server/runtime/runtime-resources.ts +201 -0
  165. package/src/internal/server/runtime/runtime-streams.ts +130 -0
  166. package/src/internal/server/server-logger.ts +110 -0
  167. package/src/internal/server/server-startup-options.ts +51 -0
  168. package/src/internal/server/start-application-server.ts +412 -0
  169. package/src/internal/server/start-overmux-server.ts +26 -0
  170. package/src/internal/server/test-port.ts +23 -0
  171. package/src/internal/server/vite-websocket-proxy.ts +201 -0
  172. package/src/internal/server/websocket-connection.ts +586 -0
  173. package/src/internal/server/websocket-limits.ts +2 -0
  174. package/src/internal/server/websocket-server.ts +311 -0
  175. package/src/internal/shared/background-notifications.ts +37 -0
  176. package/src/internal/shared/index.ts +2 -0
  177. package/src/internal/shared/protocol.ts +317 -0
  178. package/src/internal/shared/routes.ts +3 -0
  179. package/src/public/ai-context.ts +44 -0
  180. package/src/public/client.ts +53 -0
  181. package/src/public/config.ts +287 -0
  182. package/src/public/contracts.ts +55 -0
  183. package/src/public/index.ts +57 -0
  184. package/src/public/notifications.ts +10 -0
  185. package/src/public/server.ts +16 -0
  186. package/dist/public/core.ts +0 -119
  187. package/dist/public/protocol.ts +0 -51
  188. package/dist/public/terminal.ts +0 -23
  189. package/dist/web/assets/index-DC___IUw.css +0 -2
  190. package/dist/web/assets/index-DF9Cptse.js +0 -74
  191. package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
  192. package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
  193. package/dist/web/index.html +0 -14
@@ -0,0 +1,381 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmod, mkdir, readdir, rm } from "node:fs/promises";
3
+ import { createConnection, createServer, type Socket } from "node:net";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { z } from "zod";
7
+
8
+ import { getOvermuxPaths } from "../paths";
9
+ import {
10
+ readPrivateJson,
11
+ replacePrivateJsonAtomically,
12
+ } from "../notifications/private-json-file";
13
+ import type { AuthService } from "./auth-service";
14
+
15
+ const maxControlMessageBytes = 16_384;
16
+
17
+ const controlRequestSchema = z.discriminatedUnion("type", [
18
+ z.object({ type: z.literal("ping") }).strict(),
19
+ z.object({ type: z.literal("create-login") }).strict(),
20
+ z.object({ type: z.literal("issue-bearer") }).strict(),
21
+ z.object({ type: z.literal("list-sessions") }).strict(),
22
+ z.object({ id: z.string(), type: z.literal("revoke-session") }).strict(),
23
+ z.object({ type: z.literal("revoke-all") }).strict(),
24
+ ]);
25
+
26
+ const loginGrantSchema = z
27
+ .object({
28
+ code: z.string(),
29
+ expiresAt: z.string(),
30
+ id: z.string(),
31
+ urls: z.array(z.string()),
32
+ })
33
+ .strict();
34
+
35
+ const sessionSchema = z
36
+ .object({
37
+ createdAt: z.string(),
38
+ expiresAt: z.string().optional(),
39
+ id: z.string(),
40
+ lastSeenAt: z.string(),
41
+ origin: z.string().optional(),
42
+ revokedAt: z.string().optional(),
43
+ })
44
+ .strict();
45
+
46
+ const controlResponseSchemas = {
47
+ "create-login": z
48
+ .object({ login: loginGrantSchema, ok: z.literal(true) })
49
+ .strict(),
50
+ "issue-bearer": z
51
+ .object({
52
+ bearer: z.object({ expiresAt: z.string(), token: z.string() }).strict(),
53
+ ok: z.literal(true),
54
+ })
55
+ .strict(),
56
+ "list-sessions": z
57
+ .object({ ok: z.literal(true), sessions: z.array(sessionSchema) })
58
+ .strict(),
59
+ ping: z.object({ ok: z.literal(true) }).strict(),
60
+ "revoke-all": z
61
+ .object({ count: z.number().int().nonnegative(), ok: z.literal(true) })
62
+ .strict(),
63
+ "revoke-session": z
64
+ .object({ ok: z.literal(true), revoked: z.boolean() })
65
+ .strict(),
66
+ };
67
+
68
+ const controlResponseEnvelopeSchema = z
69
+ .object({ error: z.unknown().optional(), ok: z.unknown() })
70
+ .passthrough();
71
+
72
+ const registrationSchema = z.object({
73
+ apiUrl: z.string().url(),
74
+ controlSocket: z.string(),
75
+ id: z.string(),
76
+ pid: z.number().int().positive(),
77
+ port: z.number().int().min(1).max(65_535),
78
+ url: z.string().url(),
79
+ });
80
+
81
+ export type InstanceRegistration = z.infer<typeof registrationSchema>;
82
+ export type ControlRequest = z.infer<typeof controlRequestSchema>;
83
+ export type InstanceControl = { close: () => Promise<void> };
84
+
85
+ type ControlResponseByType = {
86
+ [Type in keyof typeof controlResponseSchemas]: z.infer<
87
+ (typeof controlResponseSchemas)[Type]
88
+ >;
89
+ };
90
+
91
+ const runtimeDirectory = () => {
92
+ const paths = getOvermuxPaths();
93
+ const preferred = paths.runtimeDir ?? join(paths.stateDir, "runtime");
94
+ if (Buffer.byteLength(preferred) <= 80) {
95
+ return preferred;
96
+ }
97
+ const suffix = createHash("sha256")
98
+ .update(preferred)
99
+ .digest("hex")
100
+ .slice(0, 12);
101
+ return join(tmpdir(), `overmux-${process.getuid?.() ?? "user"}-${suffix}`);
102
+ };
103
+
104
+ const readNewlineFrame = ({
105
+ maximumBytes,
106
+ socket,
107
+ timeout,
108
+ }: {
109
+ maximumBytes: number;
110
+ socket: Socket;
111
+ timeout?: number;
112
+ }): Promise<Buffer> =>
113
+ new Promise((resolve, reject) => {
114
+ let buffer = Buffer.alloc(0);
115
+ let timer: NodeJS.Timeout | undefined;
116
+
117
+ const cleanup = () => {
118
+ socket.off("data", onData);
119
+ socket.off("end", onEnd);
120
+ socket.off("error", onError);
121
+ if (timer) {
122
+ clearTimeout(timer);
123
+ }
124
+ };
125
+ const fail = (cause: Error) => {
126
+ cleanup();
127
+ reject(cause);
128
+ };
129
+ const onData = (chunk: Buffer) => {
130
+ buffer = Buffer.concat([buffer, chunk]);
131
+ if (buffer.length > maximumBytes) {
132
+ fail(new Error("Control message is too large"));
133
+ return;
134
+ }
135
+ const newline = buffer.indexOf(0x0a);
136
+ if (newline < 0) {
137
+ return;
138
+ }
139
+ cleanup();
140
+ resolve(buffer.subarray(0, newline));
141
+ };
142
+ const onEnd = () =>
143
+ fail(new Error("Control socket ended before a complete frame"));
144
+ const onError = (cause: Error) => fail(cause);
145
+
146
+ if (socket.errored) {
147
+ fail(socket.errored);
148
+ } else if (socket.readableEnded || socket.destroyed) {
149
+ fail(new Error("Control socket is already closed"));
150
+ } else {
151
+ socket.on("data", onData);
152
+ socket.once("end", onEnd);
153
+ socket.once("error", onError);
154
+ if (timeout !== undefined) {
155
+ timer = setTimeout(() => {
156
+ socket.destroy();
157
+ fail(new Error("Control request timed out"));
158
+ }, timeout);
159
+ }
160
+ }
161
+ });
162
+
163
+ const writeResponse = (socket: Socket, response: unknown) => {
164
+ socket.end(`${JSON.stringify(response)}\n`);
165
+ };
166
+
167
+ const handleRequest = async (auth: AuthService, request: ControlRequest) => {
168
+ if (request.type === "ping") {
169
+ return { ok: true };
170
+ }
171
+ if (request.type === "create-login") {
172
+ return { login: await auth.createLoginGrant(), ok: true };
173
+ }
174
+ if (request.type === "issue-bearer") {
175
+ return { bearer: await auth.issueLocalBearer(), ok: true };
176
+ }
177
+ if (request.type === "list-sessions") {
178
+ return { ok: true, sessions: await auth.listSessions() };
179
+ }
180
+ if (request.type === "revoke-session") {
181
+ return { ok: true, revoked: await auth.revokeSession(request.id) };
182
+ }
183
+ return { count: await auth.revokeAllSessions(), ok: true };
184
+ };
185
+
186
+ const handleConnection = async ({
187
+ auth,
188
+ socket,
189
+ }: {
190
+ auth: AuthService;
191
+ socket: Socket;
192
+ }) => {
193
+ try {
194
+ const frame = await readNewlineFrame({
195
+ maximumBytes: maxControlMessageBytes,
196
+ socket,
197
+ });
198
+ const request = controlRequestSchema.parse(JSON.parse(frame.toString()));
199
+ writeResponse(socket, await handleRequest(auth, request));
200
+ } catch (cause) {
201
+ writeResponse(socket, {
202
+ error: cause instanceof Error ? cause.message : "Invalid control request",
203
+ ok: false,
204
+ });
205
+ }
206
+ };
207
+
208
+ // Instance control is the private, same-user administration channel for a running
209
+ // server. It bootstraps browser login grants and short-lived CLI bearers without
210
+ // exposing those privileged operations over HTTP. The owner-only runtime
211
+ // directory, registration file, and Unix socket make OS permissions the root of
212
+ // trust. This boundary intentionally does not defend against another process
213
+ // already running as the same OS user.
214
+ export const startInstanceControl = async ({
215
+ auth,
216
+ apiUrl,
217
+ port,
218
+ url,
219
+ }: {
220
+ auth: AuthService;
221
+ apiUrl: string;
222
+ port: number;
223
+ url: string;
224
+ }): Promise<InstanceControl> => {
225
+ const directory = runtimeDirectory();
226
+ await mkdir(directory, { mode: 0o700, recursive: true });
227
+ await chmod(directory, 0o700);
228
+ const id = randomBytes(8).toString("hex");
229
+ const socketPath = join(directory, `${id}.sock`);
230
+ const registrationPath = join(directory, `${id}.json`);
231
+ const sockets = new Set<Socket>();
232
+ const server = createServer((socket) => {
233
+ sockets.add(socket);
234
+ socket.once("close", () => sockets.delete(socket));
235
+ void handleConnection({ auth, socket });
236
+ });
237
+ const close = async () => {
238
+ sockets.forEach((socket) => socket.destroy());
239
+ try {
240
+ await new Promise<void>((resolve, reject) =>
241
+ server.close((cause) => (cause ? reject(cause) : resolve())),
242
+ );
243
+ } finally {
244
+ await Promise.all([
245
+ rm(socketPath, { force: true }),
246
+ rm(registrationPath, { force: true }),
247
+ ]);
248
+ }
249
+ };
250
+
251
+ await new Promise<void>((resolve, reject) => {
252
+ server.once("error", reject);
253
+ server.listen(socketPath, () => {
254
+ server.off("error", reject);
255
+ resolve();
256
+ });
257
+ });
258
+ try {
259
+ await chmod(socketPath, 0o600);
260
+ await replacePrivateJsonAtomically(registrationPath, {
261
+ apiUrl,
262
+ controlSocket: socketPath,
263
+ id,
264
+ pid: process.pid,
265
+ port,
266
+ url,
267
+ });
268
+ } catch (cause) {
269
+ await close().catch(() => undefined);
270
+ throw cause;
271
+ }
272
+
273
+ return { close };
274
+ };
275
+
276
+ const processIsLive = (pid: number) => {
277
+ try {
278
+ process.kill(pid, 0);
279
+ return true;
280
+ } catch (cause) {
281
+ return (cause as NodeJS.ErrnoException).code === "EPERM";
282
+ }
283
+ };
284
+
285
+ // Finds the sole responsive local server, optionally filtered by port.
286
+ export const discoverInstance = async (
287
+ port?: number,
288
+ ): Promise<InstanceRegistration> => {
289
+ const directory = runtimeDirectory();
290
+ let names: string[];
291
+ try {
292
+ names = await readdir(directory);
293
+ } catch (cause) {
294
+ if ((cause as NodeJS.ErrnoException).code === "ENOENT") {
295
+ throw new Error("No running Overmux server was found");
296
+ }
297
+ throw cause;
298
+ }
299
+ const candidates = (
300
+ await Promise.all(
301
+ names
302
+ .filter((name) => name.endsWith(".json"))
303
+ .map((name) =>
304
+ readPrivateJson(join(directory, name), registrationSchema).catch(
305
+ () => undefined,
306
+ ),
307
+ ),
308
+ )
309
+ ).filter((registration): registration is InstanceRegistration =>
310
+ Boolean(
311
+ registration &&
312
+ processIsLive(registration.pid) &&
313
+ (port === undefined || registration.port === port),
314
+ ),
315
+ );
316
+ const registrations = (
317
+ await Promise.all(
318
+ candidates.map(async (registration) => {
319
+ try {
320
+ await sendControlRequest(registration, { type: "ping" });
321
+ return registration;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ }),
326
+ )
327
+ ).filter(
328
+ (registration): registration is InstanceRegistration =>
329
+ registration !== undefined,
330
+ );
331
+ if (registrations.length === 0) {
332
+ throw new Error(
333
+ port === undefined
334
+ ? "No running Overmux server was found"
335
+ : `No running Overmux server was found on port ${port}`,
336
+ );
337
+ }
338
+ if (registrations.length > 1) {
339
+ throw new Error(
340
+ "Multiple Overmux servers are running; use --port to select one",
341
+ );
342
+ }
343
+ return registrations[0]!;
344
+ };
345
+
346
+ export const sendControlRequest = async <Request extends ControlRequest>(
347
+ registration: InstanceRegistration,
348
+ request: Request,
349
+ ): Promise<ControlResponseByType[Request["type"]]> => {
350
+ const socket = createConnection(registration.controlSocket);
351
+ let frame: Buffer;
352
+ try {
353
+ const response = readNewlineFrame({
354
+ maximumBytes: maxControlMessageBytes,
355
+ socket,
356
+ timeout: 1_000,
357
+ });
358
+ socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
359
+ frame = await response;
360
+ } finally {
361
+ socket.destroy();
362
+ }
363
+ const parsed = controlResponseEnvelopeSchema.safeParse(
364
+ JSON.parse(frame.toString()),
365
+ );
366
+ if (!parsed.success) {
367
+ throw new Error("Invalid control response");
368
+ }
369
+ if (parsed.data.ok !== true) {
370
+ throw new Error(
371
+ typeof parsed.data.error === "string"
372
+ ? parsed.data.error
373
+ : "Control request failed",
374
+ );
375
+ }
376
+ const response = controlResponseSchemas[request.type].safeParse(parsed.data);
377
+ if (!response.success) {
378
+ throw new Error("Invalid control response");
379
+ }
380
+ return response.data as ControlResponseByType[Request["type"]];
381
+ };