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,194 @@
1
+ // Provides the browser API and controls for background notifications.
2
+ // Browser PushManager details remain inside this delivery boundary.
3
+ import {
4
+ backgroundNotificationPublicKeyResponseSchema,
5
+ backgroundNotificationSubscriptionSchema,
6
+ } from "../shared/index";
7
+
8
+ const enabledStorageKey = "overmux.backgroundNotificationsEnabled";
9
+
10
+ export type BackgroundNotificationStatus =
11
+ | "checking"
12
+ | "denied"
13
+ | "disabled"
14
+ | "enabled"
15
+ | "error"
16
+ | "ios-home-screen"
17
+ | "unsupported";
18
+
19
+ const isIos = () =>
20
+ /iPad|iPhone|iPod/u.test(navigator.userAgent) ||
21
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
22
+ const isStandalone = () =>
23
+ window.matchMedia("(display-mode: standalone)").matches ||
24
+ Boolean((navigator as Navigator & { standalone?: boolean }).standalone);
25
+
26
+ const supportStatus = (): BackgroundNotificationStatus | undefined => {
27
+ if (window.overmuxHost?.notifications) {
28
+ return "unsupported";
29
+ }
30
+ if (isIos() && !isStandalone()) {
31
+ return "ios-home-screen";
32
+ }
33
+ if (
34
+ !window.isSecureContext ||
35
+ !("Notification" in window) ||
36
+ !("serviceWorker" in navigator) ||
37
+ !("PushManager" in window)
38
+ ) {
39
+ return "unsupported";
40
+ }
41
+ return undefined;
42
+ };
43
+
44
+ const decodeApplicationServerKey = (value: string) => {
45
+ const padded = `${value}${"=".repeat((4 - (value.length % 4)) % 4)}`;
46
+ return Uint8Array.from(
47
+ atob(padded.replaceAll("-", "+").replaceAll("_", "/")),
48
+ (character) => character.charCodeAt(0),
49
+ );
50
+ };
51
+
52
+ const encodeApplicationServerKey = (value: ArrayBuffer | null) => {
53
+ if (!value) {
54
+ return undefined;
55
+ }
56
+ const binary = String.fromCharCode(...new Uint8Array(value));
57
+ return btoa(binary)
58
+ .replaceAll("+", "-")
59
+ .replaceAll("/", "_")
60
+ .replace(/=+$/u, "");
61
+ };
62
+
63
+ const request = async (path: string, init?: RequestInit) => {
64
+ const response = await fetch(path, { cache: "no-store", ...init });
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `Background notification request failed: ${response.status}`,
68
+ );
69
+ }
70
+ return response;
71
+ };
72
+
73
+ const getPublicKey = async () => {
74
+ const response = await request("/api/background-notifications/public-key");
75
+ return backgroundNotificationPublicKeyResponseSchema.parse(
76
+ await response.json(),
77
+ ).publicKey;
78
+ };
79
+
80
+ const serializeSubscription = (subscription: PushSubscription) => {
81
+ const json = subscription.toJSON();
82
+ return backgroundNotificationSubscriptionSchema.parse({
83
+ endpoint: subscription.endpoint,
84
+ expirationTime: subscription.expirationTime ?? null,
85
+ keys: json.keys,
86
+ });
87
+ };
88
+
89
+ const enableSubscription = async (subscription: PushSubscription) => {
90
+ await request("/api/background-notifications/subscription", {
91
+ body: JSON.stringify(serializeSubscription(subscription)),
92
+ headers: { "Content-Type": "application/json" },
93
+ method: "PUT",
94
+ });
95
+ };
96
+
97
+ const disableSubscription = async (subscription: PushSubscription) => {
98
+ await request("/api/background-notifications/subscription", {
99
+ body: JSON.stringify({ endpoint: subscription.endpoint }),
100
+ headers: { "Content-Type": "application/json" },
101
+ method: "DELETE",
102
+ });
103
+ };
104
+
105
+ const subscribe = async (
106
+ registration: ServiceWorkerRegistration,
107
+ publicKey: string,
108
+ ) =>
109
+ registration.pushManager.subscribe({
110
+ applicationServerKey: decodeApplicationServerKey(publicKey),
111
+ userVisibleOnly: true,
112
+ });
113
+
114
+ const canManageSubscription = () =>
115
+ window.isSecureContext &&
116
+ "serviceWorker" in navigator &&
117
+ "PushManager" in window;
118
+
119
+ const removeCurrentSubscription = async () => {
120
+ if (!canManageSubscription()) {
121
+ return;
122
+ }
123
+ const registration = await navigator.serviceWorker.getRegistration();
124
+ const subscription = await registration?.pushManager.getSubscription();
125
+ if (!subscription) {
126
+ return;
127
+ }
128
+ await disableSubscription(subscription);
129
+ if (!(await subscription.unsubscribe())) {
130
+ throw new Error("The background subscription could not be removed");
131
+ }
132
+ };
133
+
134
+ const reconcileBackgroundNotifications = async () => {
135
+ const registration = await navigator.serviceWorker.ready;
136
+ const publicKey = await getPublicKey();
137
+ let subscription = await registration.pushManager.getSubscription();
138
+ const subscriptionKey = subscription
139
+ ? encodeApplicationServerKey(subscription.options.applicationServerKey)
140
+ : undefined;
141
+ if (subscription && subscriptionKey !== publicKey) {
142
+ await disableSubscription(subscription);
143
+ if (!(await subscription.unsubscribe())) {
144
+ throw new Error("The previous background subscription is still active");
145
+ }
146
+ subscription = null;
147
+ }
148
+ if (!subscription) {
149
+ subscription = await subscribe(registration, publicKey);
150
+ }
151
+ await enableSubscription(subscription);
152
+ };
153
+
154
+ export const enableBackgroundNotifications = async () => {
155
+ if (supportStatus()) {
156
+ localStorage.removeItem(enabledStorageKey);
157
+ return false;
158
+ }
159
+ const permission =
160
+ Notification.permission === "default"
161
+ ? await Notification.requestPermission()
162
+ : Notification.permission;
163
+ if (permission !== "granted") {
164
+ localStorage.removeItem(enabledStorageKey);
165
+ return false;
166
+ }
167
+ await reconcileBackgroundNotifications();
168
+ localStorage.setItem(enabledStorageKey, "true");
169
+ return true;
170
+ };
171
+
172
+ export const disableBackgroundNotifications = async () => {
173
+ localStorage.removeItem(enabledStorageKey);
174
+ await removeCurrentSubscription();
175
+ };
176
+
177
+ export const reconciledStatus =
178
+ async (): Promise<BackgroundNotificationStatus> => {
179
+ const unavailable = supportStatus();
180
+ if (unavailable) {
181
+ await disableBackgroundNotifications().catch(() => undefined);
182
+ return unavailable;
183
+ }
184
+ if (Notification.permission !== "granted") {
185
+ await disableBackgroundNotifications().catch(() => undefined);
186
+ return Notification.permission === "denied" ? "denied" : "disabled";
187
+ }
188
+ if (localStorage.getItem(enabledStorageKey) !== "true") {
189
+ await disableBackgroundNotifications();
190
+ return "disabled";
191
+ }
192
+ await reconcileBackgroundNotifications();
193
+ return "enabled";
194
+ };
@@ -0,0 +1,97 @@
1
+ import type { RuntimeManifest } from "../shared/index";
2
+ import type { ContractInputArguments } from "../../public/index";
3
+ import type { z } from "zod";
4
+
5
+ import type { ClientTransport } from "./transport";
6
+
7
+ type Operations<TConfig> = TConfig extends { operations?: infer TOperations }
8
+ ? NonNullable<TOperations>
9
+ : never;
10
+ type InputOf<T> = T extends { input: infer TInput extends z.ZodType }
11
+ ? TInput
12
+ : never;
13
+ type OutputOf<T> = T extends { output: infer TOutput extends z.ZodType }
14
+ ? z.output<TOutput>
15
+ : never;
16
+ type OperationId<TConfig> = Extract<keyof Operations<TConfig>, string>;
17
+ type OperationInput<TConfig, TId extends OperationId<TConfig>> = InputOf<
18
+ Operations<TConfig>[TId]
19
+ >;
20
+ type OperationOutput<TConfig, TId extends OperationId<TConfig>> = OutputOf<
21
+ Operations<TConfig>[TId]
22
+ >;
23
+ type OperationArguments<TInput extends z.ZodType> = [
24
+ ...ContractInputArguments<TInput>,
25
+ signal?: AbortSignal,
26
+ ];
27
+
28
+ export type OvermuxServerApi<TServerConfig> = {
29
+ executeOperation: <TId extends OperationId<TServerConfig>>(
30
+ id: TId,
31
+ ...args: OperationArguments<OperationInput<TServerConfig, TId>>
32
+ ) => Promise<OperationOutput<TServerConfig, TId>>;
33
+ };
34
+
35
+ export type RuntimeOvermuxServerApi = {
36
+ executeOperation: (
37
+ id: string,
38
+ input?: unknown,
39
+ signal?: AbortSignal,
40
+ ) => Promise<unknown>;
41
+ openStream: (input: {
42
+ id: string;
43
+ input?: unknown;
44
+ onClose: () => void;
45
+ onError: (error: Error) => void;
46
+ onMessage: (message: unknown) => void;
47
+ onOpen: () => void;
48
+ }) => { close: () => void; send: (message: unknown) => boolean };
49
+ readResource: (input: { id: string; input?: unknown }) => Promise<unknown>;
50
+ subscribeResource: (input: {
51
+ id: string;
52
+ input?: unknown;
53
+ onError: (error: Error) => void;
54
+ onInvalidate: () => void;
55
+ }) => () => void;
56
+ };
57
+
58
+ const availableName = (
59
+ names: readonly string[],
60
+ kind: "Resource" | "Stream",
61
+ name: string,
62
+ ) => {
63
+ if (!names.includes(name)) {
64
+ throw new Error(`${kind} is not available: ${name}`);
65
+ }
66
+ return name;
67
+ };
68
+
69
+ export const createOvermuxServerApi = ({
70
+ manifest,
71
+ transport,
72
+ }: {
73
+ manifest: RuntimeManifest;
74
+ transport: ClientTransport;
75
+ }): RuntimeOvermuxServerApi => ({
76
+ executeOperation: (id, input, signal) => {
77
+ if (!manifest.operations.includes(id)) {
78
+ throw new Error(`Operation is not available: ${id}`);
79
+ }
80
+ return transport.invokeOperation({ input, name: id, signal });
81
+ },
82
+ openStream: ({ id, ...input }) =>
83
+ transport.openStream({
84
+ ...input,
85
+ streamName: availableName(manifest.streams, "Stream", id),
86
+ }),
87
+ readResource: ({ id, input }) =>
88
+ transport.readResource({
89
+ input,
90
+ resourceName: availableName(manifest.resources, "Resource", id),
91
+ }),
92
+ subscribeResource: ({ id, ...input }) =>
93
+ transport.subscribeResource({
94
+ ...input,
95
+ resourceName: availableName(manifest.resources, "Resource", id),
96
+ }),
97
+ });
@@ -0,0 +1,184 @@
1
+ import type { OvermuxContrast, OvermuxScheme } from "./theme-scope";
2
+ import type { ComponentType } from "react";
3
+ import {
4
+ formatKeyBinding,
5
+ keyBindingSchema,
6
+ matchesKeyBinding,
7
+ type KeyBinding,
8
+ } from "@overmux/keybindings";
9
+ export {
10
+ formatKeyBinding,
11
+ keyBindingSchema,
12
+ matchesKeyBinding,
13
+ } from "@overmux/keybindings";
14
+ export type { KeyBinding } from "@overmux/keybindings";
15
+ import { z } from "zod";
16
+
17
+ import type { OvermuxServerApi } from "./browser-api";
18
+
19
+ export type KeySequence = readonly [KeyBinding, KeyBinding, ...KeyBinding[]];
20
+ export type ShortcutBinding = KeyBinding | KeySequence;
21
+ export type ConditionalShortcutBinding = {
22
+ binding: ShortcutBinding;
23
+ when: { media: string };
24
+ };
25
+ export type CommandBinding = ShortcutBinding | ConditionalShortcutBinding;
26
+ export type ChordPrefix = {
27
+ binding: KeyBinding;
28
+ unmatched: "replay-to-focused-input";
29
+ };
30
+
31
+ export const shortcutBindingSchema = z.union([
32
+ keyBindingSchema,
33
+ z.array(keyBindingSchema).min(2),
34
+ ]);
35
+ export const commandBindingSchema = z.union([
36
+ shortcutBindingSchema,
37
+ z.object({
38
+ binding: shortcutBindingSchema,
39
+ when: z.object({ media: z.string().trim().min(1) }),
40
+ }),
41
+ ]);
42
+ export const chordPrefixSchema = z.object({
43
+ binding: keyBindingSchema,
44
+ unmatched: z.literal("replay-to-focused-input"),
45
+ });
46
+
47
+ export const formatShortcutBinding = (binding: ShortcutBinding) =>
48
+ typeof binding === "string"
49
+ ? formatKeyBinding(binding)
50
+ : binding.map(formatKeyBinding).join(" ");
51
+
52
+ type CommandRunResult = Promise<unknown> | unknown;
53
+ type CommandRun<TServerConfig, TParams> = (input: {
54
+ overmuxServerApi: OvermuxServerApi<TServerConfig>;
55
+ params: TParams;
56
+ }) => CommandRunResult;
57
+ type CommandDefinitionSource = {
58
+ defaultBindings?: readonly CommandBinding[];
59
+ params?: z.ZodType;
60
+ run?: unknown;
61
+ title: string;
62
+ };
63
+ type ParamsSchema<TCommand> = TCommand extends {
64
+ params: infer TParams extends z.ZodType;
65
+ }
66
+ ? TParams
67
+ : undefined;
68
+ type ParamsOutput<TCommand> =
69
+ ParamsSchema<TCommand> extends z.ZodType
70
+ ? z.output<ParamsSchema<TCommand>>
71
+ : undefined;
72
+ type CheckedCommand<TServerConfig, TCommand extends CommandDefinitionSource> = {
73
+ [TKey in keyof TCommand]: TKey extends "run"
74
+ ? CommandRun<TServerConfig, ParamsOutput<TCommand>>
75
+ : TCommand[TKey];
76
+ } & {
77
+ defaultBindings?: readonly CommandBinding[];
78
+ params?: z.ZodType;
79
+ title: string;
80
+ };
81
+ type CheckedCommands<
82
+ TServerConfig,
83
+ TCommands extends Record<string, CommandDefinitionSource>,
84
+ > = {
85
+ [TId in keyof TCommands]: CheckedCommand<TServerConfig, TCommands[TId]>;
86
+ };
87
+
88
+ export type CommandHandle<TId extends string = string> = {
89
+ defaultBindings?: readonly CommandBinding[];
90
+ id: TId;
91
+ params?: z.ZodType;
92
+ run?: unknown;
93
+ title: string;
94
+ };
95
+
96
+ type DefinedCommands<
97
+ TServerConfig,
98
+ TCommands extends Record<string, CommandDefinitionSource>,
99
+ > = {
100
+ readonly [TId in keyof TCommands]: CheckedCommand<
101
+ TServerConfig,
102
+ TCommands[TId]
103
+ > & { readonly id: Extract<TId, string> };
104
+ };
105
+
106
+ const validateBindings = (bindings: readonly CommandBinding[] | undefined) =>
107
+ bindings?.forEach((binding) => commandBindingSchema.parse(binding));
108
+
109
+ export const defineCommandRegistry =
110
+ <TServerConfig>() =>
111
+ <const TCommands extends Record<string, CommandDefinitionSource>>(
112
+ commands: CheckedCommands<TServerConfig, TCommands>,
113
+ ): DefinedCommands<TServerConfig, TCommands> =>
114
+ Object.fromEntries(
115
+ Object.entries(commands).map(([id, command]) => {
116
+ validateBindings(command.defaultBindings);
117
+ return [id, { ...command, id }];
118
+ }),
119
+ ) as DefinedCommands<TServerConfig, TCommands>;
120
+
121
+ export type CommandRegistry = Readonly<Record<string, CommandHandle>>;
122
+
123
+ export type OvermuxClientAppearance = {
124
+ contrast?: OvermuxContrast;
125
+ scheme?: OvermuxScheme;
126
+ };
127
+
128
+ export type ClientAppDefinition<
129
+ TCommands extends CommandRegistry = CommandRegistry,
130
+ > = {
131
+ appearance?: OvermuxClientAppearance;
132
+ chordPrefixes?: readonly ChordPrefix[];
133
+ commands: TCommands;
134
+ component: ComponentType;
135
+ shortcutOverrides?: Partial<
136
+ Record<Extract<keyof TCommands, string>, readonly CommandBinding[]>
137
+ >;
138
+ };
139
+
140
+ export const defineOvermuxClient = <const TCommands extends CommandRegistry>(
141
+ definition: ClientAppDefinition<TCommands>,
142
+ ): ClientAppDefinition<TCommands> => {
143
+ definition.chordPrefixes?.forEach((prefix) =>
144
+ chordPrefixSchema.parse(prefix),
145
+ );
146
+ Object.values(definition.shortcutOverrides ?? {}).forEach(validateBindings);
147
+ return definition;
148
+ };
149
+
150
+ export const isConditionalShortcutBinding = (
151
+ binding: CommandBinding,
152
+ ): binding is ConditionalShortcutBinding =>
153
+ typeof binding !== "string" && !Array.isArray(binding);
154
+
155
+ export const resolveShortcutBinding = (
156
+ binding: CommandBinding,
157
+ ): ShortcutBinding =>
158
+ isConditionalShortcutBinding(binding) ? binding.binding : binding;
159
+
160
+ export const configuredCommandBindings = (
161
+ definition: ClientAppDefinition,
162
+ command: CommandHandle,
163
+ ) =>
164
+ definition.shortcutOverrides?.[command.id] ?? command.defaultBindings ?? [];
165
+
166
+ export const activeCommandBindings = (
167
+ bindings: readonly CommandBinding[],
168
+ matchesMedia: (media: string) => boolean,
169
+ ) =>
170
+ bindings.flatMap((binding) =>
171
+ !isConditionalShortcutBinding(binding) || matchesMedia(binding.when.media)
172
+ ? [resolveShortcutBinding(binding)]
173
+ : [],
174
+ );
175
+
176
+ export const shortcutMediaQueries = (definition: ClientAppDefinition) => [
177
+ ...new Set(
178
+ Object.values(definition.commands).flatMap((command) =>
179
+ configuredCommandBindings(definition, command).flatMap((binding) =>
180
+ isConditionalShortcutBinding(binding) ? [binding.when.media] : [],
181
+ ),
182
+ ),
183
+ ),
184
+ ];