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.
- package/CHANGELOG.md +16 -0
- package/README.md +7 -0
- package/dist/auth/.vite/manifest.json +11 -0
- package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
- package/dist/auth/assets/index-CHIE7qYj.js +72 -0
- package/dist/auth/index.html +14 -0
- package/dist/bin.js +4504 -7870
- package/dist/bin.js.map +1 -0
- package/dist/docs/000-index.md +21 -0
- package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/dist/docs/100-introduction/300-why-overmux.md +22 -0
- package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
- package/dist/docs/300-fundamentals/200-configuration.md +3 -0
- package/dist/docs/300-fundamentals/300-theming.md +3 -0
- package/dist/docs/300-fundamentals/400-server.md +3 -0
- package/dist/docs/300-fundamentals/500-client.md +3 -0
- package/dist/docs/300-fundamentals/600-operations.md +3 -0
- package/dist/docs/300-fundamentals/700-resources.md +3 -0
- package/dist/docs/300-fundamentals/800-streams.md +3 -0
- package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/dist/docs/400-reference/100-configuration.md +3 -0
- package/dist/docs/400-reference/200-server-api.md +3 -0
- package/dist/docs/400-reference/300-client-api.md +3 -0
- package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
- package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
- package/dist/docs/400-reference/400-cli/300-call.md +28 -0
- package/dist/docs/400-reference/400-cli/400-check.md +20 -0
- package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
- package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/dist/docs/500-hosted-pages.md +31 -0
- package/dist/exports/client.d.ts +237 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +1643 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
- package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.js +49 -0
- package/dist/exports/index.js.map +1 -0
- package/dist/exports/notifications-kDp16bU_.js +56 -0
- package/dist/exports/notifications-kDp16bU_.js.map +1 -0
- package/dist/exports/server.d.ts +63 -0
- package/dist/exports/server.d.ts.map +1 -0
- package/dist/exports/server.js +1120 -0
- package/dist/exports/server.js.map +1 -0
- package/dist/exports/style.css +368 -0
- package/dist/internal/server/coordinator/server-child.d.ts +1 -0
- package/dist/internal/server/coordinator/server-child.js +3299 -0
- package/dist/internal/server/coordinator/server-child.js.map +1 -0
- package/docs/000-index.md +21 -0
- package/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/docs/100-introduction/300-why-overmux.md +22 -0
- package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/docs/300-fundamentals/100-project-structure.md +3 -0
- package/docs/300-fundamentals/200-configuration.md +3 -0
- package/docs/300-fundamentals/300-theming.md +3 -0
- package/docs/300-fundamentals/400-server.md +3 -0
- package/docs/300-fundamentals/500-client.md +3 -0
- package/docs/300-fundamentals/600-operations.md +3 -0
- package/docs/300-fundamentals/700-resources.md +3 -0
- package/docs/300-fundamentals/800-streams.md +3 -0
- package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/docs/400-reference/100-configuration.md +3 -0
- package/docs/400-reference/200-server-api.md +3 -0
- package/docs/400-reference/300-client-api.md +3 -0
- package/docs/400-reference/400-cli/100-serve.md +33 -0
- package/docs/400-reference/400-cli/200-auth.md +52 -0
- package/docs/400-reference/400-cli/300-call.md +28 -0
- package/docs/400-reference/400-cli/400-check.md +20 -0
- package/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/docs/400-reference/400-cli/600-docs.md +23 -0
- package/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/docs/500-hosted-pages.md +31 -0
- package/package.json +59 -18
- package/src/internal/cli/app.ts +72 -0
- package/src/internal/cli/bin.ts +3 -0
- package/src/internal/cli/commands/ai.ts +44 -0
- package/src/internal/cli/commands/auth.ts +195 -0
- package/src/internal/cli/commands/call.ts +128 -0
- package/src/internal/cli/commands/check.ts +48 -0
- package/src/internal/cli/commands/desktop-installer.ts +446 -0
- package/src/internal/cli/commands/desktop-release.ts +190 -0
- package/src/internal/cli/commands/desktop.ts +47 -0
- package/src/internal/cli/commands/docs.ts +34 -0
- package/src/internal/cli/commands/serve.ts +322 -0
- package/src/internal/cli/environment.ts +54 -0
- package/src/internal/cli/login.ts +46 -0
- package/src/internal/cli/managed-vite.ts +267 -0
- package/src/internal/cli/parse-port.ts +8 -0
- package/src/internal/client/auth/auth-api.ts +53 -0
- package/src/internal/client/auth/auth-shell.css +74 -0
- package/src/internal/client/auth/auth-shell.tsx +167 -0
- package/src/internal/client/auth/entry.tsx +26 -0
- package/src/internal/client/auth/index.html +13 -0
- package/src/internal/client/background-notifications.ts +194 -0
- package/src/internal/client/browser-api.ts +97 -0
- package/src/internal/client/client-definition.ts +184 -0
- package/src/internal/client/commands.tsx +260 -0
- package/src/internal/client/host/browser-log-forwarding.ts +149 -0
- package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
- package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
- package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
- package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
- package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
- package/src/internal/client/host/notification-forwarding.ts +22 -0
- package/src/internal/client/host/overmux-host.tsx +200 -0
- package/src/internal/client/host/recovery-screen.css +75 -0
- package/src/internal/client/host/recovery-screen.tsx +17 -0
- package/src/internal/client/host/update-popover.css +82 -0
- package/src/internal/client/host/update-popover.tsx +166 -0
- package/src/internal/client/index.ts +45 -0
- package/src/internal/client/overmux-react.ts +283 -0
- package/src/internal/client/shortcuts.tsx +373 -0
- package/src/internal/client/theme-scope.css +114 -0
- package/src/internal/client/theme-scope.tsx +107 -0
- package/src/internal/client/transport.ts +69 -0
- package/src/internal/client/websocket.ts +441 -0
- package/src/internal/server/auth/auth-http.ts +414 -0
- package/src/internal/server/auth/auth-service.ts +627 -0
- package/src/internal/server/auth/auth-shell-http.ts +90 -0
- package/src/internal/server/auth/instance-control.ts +381 -0
- package/src/internal/server/check-overmux.ts +364 -0
- package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
- package/src/internal/server/coordinator/server-child.ts +108 -0
- package/src/internal/server/coordinator/server-coordinator.ts +262 -0
- package/src/internal/server/coordinator/update-watcher.ts +54 -0
- package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
- package/src/internal/server/http/background-notification-handlers.ts +83 -0
- package/src/internal/server/http/control-handlers.ts +44 -0
- package/src/internal/server/http/create-http-app.ts +53 -0
- package/src/internal/server/http/development-web-backend.ts +83 -0
- package/src/internal/server/http/operation-handler.ts +93 -0
- package/src/internal/server/http/request-logging.ts +34 -0
- package/src/internal/server/http/routes.ts +241 -0
- package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
- package/src/internal/server/index.ts +16 -0
- package/src/internal/server/notifications/background-notification-payload.ts +74 -0
- package/src/internal/server/notifications/background-notification-service.ts +139 -0
- package/src/internal/server/notifications/background-notification-store.ts +162 -0
- package/src/internal/server/notifications/notification-service.ts +70 -0
- package/src/internal/server/notifications/private-json-file.ts +84 -0
- package/src/internal/server/paths.ts +13 -0
- package/src/internal/server/rate-limiter.ts +58 -0
- package/src/internal/server/runtime/create-runtime.ts +87 -0
- package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
- package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
- package/src/internal/server/runtime/runtime-operations.ts +78 -0
- package/src/internal/server/runtime/runtime-resources.ts +201 -0
- package/src/internal/server/runtime/runtime-streams.ts +130 -0
- package/src/internal/server/server-logger.ts +110 -0
- package/src/internal/server/server-startup-options.ts +51 -0
- package/src/internal/server/start-application-server.ts +412 -0
- package/src/internal/server/start-overmux-server.ts +26 -0
- package/src/internal/server/test-port.ts +23 -0
- package/src/internal/server/vite-websocket-proxy.ts +201 -0
- package/src/internal/server/websocket-connection.ts +586 -0
- package/src/internal/server/websocket-limits.ts +2 -0
- package/src/internal/server/websocket-server.ts +311 -0
- package/src/internal/shared/background-notifications.ts +37 -0
- package/src/internal/shared/index.ts +2 -0
- package/src/internal/shared/protocol.ts +317 -0
- package/src/internal/shared/routes.ts +3 -0
- package/src/public/ai-context.ts +44 -0
- package/src/public/client.ts +53 -0
- package/src/public/config.ts +287 -0
- package/src/public/contracts.ts +55 -0
- package/src/public/index.ts +57 -0
- package/src/public/notifications.ts +10 -0
- package/src/public/server.ts +16 -0
- package/dist/public/core.ts +0 -119
- package/dist/public/protocol.ts +0 -51
- package/dist/public/terminal.ts +0 -23
- package/dist/web/assets/index-DC___IUw.css +0 -2
- package/dist/web/assets/index-DF9Cptse.js +0 -74
- package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
- package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
- package/dist/web/index.html +0 -14
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { RuntimeManifest } from "../shared/index";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
type RefObject,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ClientAppDefinition,
|
|
13
|
+
CommandHandle,
|
|
14
|
+
ShortcutBinding,
|
|
15
|
+
} from "./client-definition";
|
|
16
|
+
import {
|
|
17
|
+
activeCommandBindings,
|
|
18
|
+
configuredCommandBindings,
|
|
19
|
+
} from "./client-definition";
|
|
20
|
+
import type { RuntimeOvermuxServerApi } from "./browser-api";
|
|
21
|
+
|
|
22
|
+
type CommandRunResult = Promise<unknown> | unknown;
|
|
23
|
+
type LocalCommandRun = () => CommandRunResult;
|
|
24
|
+
type ParamsSchema<TCommand> = TCommand extends {
|
|
25
|
+
params: infer TParams extends z.ZodType;
|
|
26
|
+
}
|
|
27
|
+
? TParams
|
|
28
|
+
: undefined;
|
|
29
|
+
type RegistrationParams<TCommand> =
|
|
30
|
+
ParamsSchema<TCommand> extends z.ZodType
|
|
31
|
+
? { params: z.input<ParamsSchema<TCommand>> }
|
|
32
|
+
: { params?: never };
|
|
33
|
+
type RegistrationRun<TCommand> = TCommand extends {
|
|
34
|
+
run: (...args: never[]) => unknown;
|
|
35
|
+
}
|
|
36
|
+
? { run?: LocalCommandRun }
|
|
37
|
+
: { run: LocalCommandRun };
|
|
38
|
+
type CommandRegistration<TCommand> = RegistrationParams<TCommand> &
|
|
39
|
+
RegistrationRun<TCommand> & {
|
|
40
|
+
element?: RefObject<HTMLElement | null>;
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
};
|
|
43
|
+
type UseCommandRegistration<TCommand> =
|
|
44
|
+
ParamsSchema<TCommand> extends z.ZodType
|
|
45
|
+
? CommandRegistration<TCommand> | SkipToken
|
|
46
|
+
: CommandRegistration<TCommand>;
|
|
47
|
+
|
|
48
|
+
export const skipToken = Symbol("overmux command registration");
|
|
49
|
+
export type SkipToken = typeof skipToken;
|
|
50
|
+
|
|
51
|
+
export type RegisteredCommand = {
|
|
52
|
+
command: CommandHandle;
|
|
53
|
+
element: () => HTMLElement | null | undefined;
|
|
54
|
+
enabled: () => boolean;
|
|
55
|
+
execute: () => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type RuntimeContextValue = {
|
|
59
|
+
manifest: RuntimeManifest;
|
|
60
|
+
overmuxServerApi: RuntimeOvermuxServerApi;
|
|
61
|
+
refreshCommands: () => void;
|
|
62
|
+
registerCommand: (registration: RegisteredCommand) => () => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const RuntimeContext = createContext<RuntimeContextValue | undefined>(
|
|
66
|
+
undefined,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const useRuntime = () => {
|
|
70
|
+
const runtime = useContext(RuntimeContext);
|
|
71
|
+
if (!runtime) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Overmux hooks must be used inside an active Overmux client runtime",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return runtime;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type RuntimeCommandRegistration = {
|
|
80
|
+
element?: RefObject<HTMLElement | null>;
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
params?: unknown;
|
|
83
|
+
run?: LocalCommandRun;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const executeRegistration = async (
|
|
87
|
+
command: CommandHandle,
|
|
88
|
+
registration: RuntimeCommandRegistration,
|
|
89
|
+
overmuxServerApi: RuntimeOvermuxServerApi,
|
|
90
|
+
) => {
|
|
91
|
+
const params = command.params
|
|
92
|
+
? command.params.parse(registration.params)
|
|
93
|
+
: undefined;
|
|
94
|
+
if (registration.run) {
|
|
95
|
+
await registration.run();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof command.run !== "function") {
|
|
99
|
+
throw new Error(`Command has no implementation: ${command.id}`);
|
|
100
|
+
}
|
|
101
|
+
await command.run({ overmuxServerApi, params });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const createRegisteredCommand = ({
|
|
105
|
+
command,
|
|
106
|
+
getRegistration,
|
|
107
|
+
overmuxServerApi,
|
|
108
|
+
}: {
|
|
109
|
+
command: CommandHandle;
|
|
110
|
+
getRegistration: () => RuntimeCommandRegistration | SkipToken;
|
|
111
|
+
overmuxServerApi: RuntimeOvermuxServerApi;
|
|
112
|
+
}): RegisteredCommand => ({
|
|
113
|
+
command,
|
|
114
|
+
element: () => {
|
|
115
|
+
const registration = getRegistration();
|
|
116
|
+
return registration === skipToken
|
|
117
|
+
? undefined
|
|
118
|
+
: registration.element?.current;
|
|
119
|
+
},
|
|
120
|
+
enabled: () => {
|
|
121
|
+
const registration = getRegistration();
|
|
122
|
+
return registration === skipToken ? false : (registration.enabled ?? true);
|
|
123
|
+
},
|
|
124
|
+
execute: () => {
|
|
125
|
+
const registration = getRegistration();
|
|
126
|
+
return registration === skipToken
|
|
127
|
+
? Promise.resolve()
|
|
128
|
+
: executeRegistration(command, registration, overmuxServerApi);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const useCommand = <TCommand extends CommandHandle>(
|
|
133
|
+
command: TCommand,
|
|
134
|
+
registration: UseCommandRegistration<TCommand>,
|
|
135
|
+
) => {
|
|
136
|
+
const { overmuxServerApi, refreshCommands, registerCommand } = useRuntime();
|
|
137
|
+
const registrationRef = useRef(registration);
|
|
138
|
+
registrationRef.current = registration;
|
|
139
|
+
const skipped = registration === skipToken;
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (skipped) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
return registerCommand(
|
|
145
|
+
createRegisteredCommand({
|
|
146
|
+
command,
|
|
147
|
+
getRegistration: () =>
|
|
148
|
+
registrationRef.current as RuntimeCommandRegistration | SkipToken,
|
|
149
|
+
overmuxServerApi,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}, [command, overmuxServerApi, registerCommand, skipped]);
|
|
153
|
+
const enabled = skipped ? false : (registration.enabled ?? true);
|
|
154
|
+
const element = skipped ? undefined : registration.element;
|
|
155
|
+
useEffect(refreshCommands, [element, enabled, refreshCommands, skipped]);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const elementDepth = (element: HTMLElement) => {
|
|
159
|
+
let depth = 0;
|
|
160
|
+
for (
|
|
161
|
+
let current: HTMLElement | null = element;
|
|
162
|
+
current;
|
|
163
|
+
current = current.parentElement
|
|
164
|
+
) {
|
|
165
|
+
depth += 1;
|
|
166
|
+
}
|
|
167
|
+
return depth;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const selectCommandRegistration = (
|
|
171
|
+
command: CommandHandle,
|
|
172
|
+
registrations: ReadonlySet<RegisteredCommand>,
|
|
173
|
+
activeElement: Element | null,
|
|
174
|
+
) => {
|
|
175
|
+
const candidates = [...registrations].filter(
|
|
176
|
+
(registration) => registration.command === command,
|
|
177
|
+
);
|
|
178
|
+
const focused = candidates.filter((registration) => {
|
|
179
|
+
const element = registration.element();
|
|
180
|
+
return Boolean(element && activeElement && element.contains(activeElement));
|
|
181
|
+
});
|
|
182
|
+
return (
|
|
183
|
+
focused.reduce<RegisteredCommand | undefined>((selected, candidate) => {
|
|
184
|
+
const selectedElement = selected?.element();
|
|
185
|
+
const candidateElement = candidate.element();
|
|
186
|
+
if (!selectedElement || !candidateElement) {
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
return elementDepth(candidateElement) >= elementDepth(selectedElement)
|
|
190
|
+
? candidate
|
|
191
|
+
: selected;
|
|
192
|
+
}, undefined) ?? candidates.at(-1)
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const currentActiveElement = () => document.activeElement;
|
|
197
|
+
|
|
198
|
+
const selectedRegistration = (
|
|
199
|
+
command: CommandHandle,
|
|
200
|
+
registrations: ReadonlySet<RegisteredCommand>,
|
|
201
|
+
getActiveElement: () => Element | null = currentActiveElement,
|
|
202
|
+
) => selectCommandRegistration(command, registrations, getActiveElement());
|
|
203
|
+
|
|
204
|
+
export const executeCommand = async (
|
|
205
|
+
command: CommandHandle,
|
|
206
|
+
registrations: ReadonlySet<RegisteredCommand>,
|
|
207
|
+
getActiveElement: () => Element | null = currentActiveElement,
|
|
208
|
+
) => {
|
|
209
|
+
const selected = selectedRegistration(
|
|
210
|
+
command,
|
|
211
|
+
registrations,
|
|
212
|
+
getActiveElement,
|
|
213
|
+
);
|
|
214
|
+
if (selected?.enabled()) {
|
|
215
|
+
await selected.execute();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type CommandEntry = {
|
|
220
|
+
bindings: readonly ShortcutBinding[];
|
|
221
|
+
disabledReason?: string;
|
|
222
|
+
enabled: boolean;
|
|
223
|
+
execute: () => Promise<void>;
|
|
224
|
+
id: string;
|
|
225
|
+
title: string;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const createCommandEntries = (
|
|
229
|
+
definition: ClientAppDefinition,
|
|
230
|
+
registrations: ReadonlySet<RegisteredCommand>,
|
|
231
|
+
getActiveElement: () => Element | null = currentActiveElement,
|
|
232
|
+
matchesMedia: (media: string) => boolean = (media) =>
|
|
233
|
+
typeof window !== "undefined" && window.matchMedia(media).matches,
|
|
234
|
+
): CommandEntry[] =>
|
|
235
|
+
Object.values(definition.commands).flatMap((command) => {
|
|
236
|
+
const selected = selectedRegistration(
|
|
237
|
+
command,
|
|
238
|
+
registrations,
|
|
239
|
+
getActiveElement,
|
|
240
|
+
);
|
|
241
|
+
return selected
|
|
242
|
+
? [
|
|
243
|
+
{
|
|
244
|
+
bindings: activeCommandBindings(
|
|
245
|
+
configuredCommandBindings(definition, command),
|
|
246
|
+
matchesMedia,
|
|
247
|
+
),
|
|
248
|
+
enabled: selected.enabled(),
|
|
249
|
+
execute: () =>
|
|
250
|
+
executeCommand(command, registrations, getActiveElement),
|
|
251
|
+
id: command.id,
|
|
252
|
+
title: command.title,
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: [];
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export const CommandsContext = createContext<readonly CommandEntry[]>([]);
|
|
259
|
+
|
|
260
|
+
export const useCommands = () => useContext(CommandsContext);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ClientDiagnosticEvent } from "../../shared/index";
|
|
2
|
+
|
|
3
|
+
const maxStringLength = 2_000;
|
|
4
|
+
const truncate = (value: string, length = maxStringLength) =>
|
|
5
|
+
value.length <= length ? value : `${value.slice(0, length)}…[truncated]`;
|
|
6
|
+
|
|
7
|
+
const serializeValue = (
|
|
8
|
+
value: unknown,
|
|
9
|
+
seen: WeakSet<object>,
|
|
10
|
+
depth = 0,
|
|
11
|
+
): unknown => {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return truncate(value);
|
|
14
|
+
}
|
|
15
|
+
if (
|
|
16
|
+
value === null ||
|
|
17
|
+
typeof value === "boolean" ||
|
|
18
|
+
typeof value === "number"
|
|
19
|
+
) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "bigint") {
|
|
23
|
+
return `${value}n`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "undefined" || typeof value === "symbol") {
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "function") {
|
|
29
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
30
|
+
}
|
|
31
|
+
if (depth >= 4) {
|
|
32
|
+
return "[Max depth]";
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== "object") {
|
|
35
|
+
return truncate(String(value));
|
|
36
|
+
}
|
|
37
|
+
if (seen.has(value)) {
|
|
38
|
+
return "[Circular]";
|
|
39
|
+
}
|
|
40
|
+
seen.add(value);
|
|
41
|
+
if (value instanceof Error) {
|
|
42
|
+
return {
|
|
43
|
+
message: truncate(value.message),
|
|
44
|
+
name: value.name,
|
|
45
|
+
...(value.stack ? { stack: truncate(value.stack, 8_000) } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value
|
|
50
|
+
.slice(0, 25)
|
|
51
|
+
.map((item) => serializeValue(item, seen, depth + 1));
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return Object.fromEntries(
|
|
55
|
+
Object.entries(value)
|
|
56
|
+
.slice(0, 25)
|
|
57
|
+
.map(([key, item]) => [key, serializeValue(item, seen, depth + 1)]),
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return `[Unserializable ${Object.prototype.toString.call(value)}]`;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const serializeBrowserLogArguments = (values: unknown[]) => {
|
|
65
|
+
const seen = new WeakSet<object>();
|
|
66
|
+
const serialized = values.map((value) => serializeValue(value, seen));
|
|
67
|
+
const json = JSON.stringify(serialized);
|
|
68
|
+
return truncate(json, 16_000);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const diagnosticUrl = () => {
|
|
72
|
+
try {
|
|
73
|
+
return `${location.origin}${location.pathname}`;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const stackOf = (values: unknown[]) => {
|
|
80
|
+
const error = values.find((value) => value instanceof Error);
|
|
81
|
+
return error instanceof Error && error.stack
|
|
82
|
+
? truncate(error.stack, 8_000)
|
|
83
|
+
: undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const installBrowserLogForwarding = (
|
|
87
|
+
report: (event: ClientDiagnosticEvent) => void,
|
|
88
|
+
) => {
|
|
89
|
+
const methods = ["log", "info", "warn", "error", "debug"] as const;
|
|
90
|
+
const originals = Object.fromEntries(
|
|
91
|
+
methods.map((level) => [level, console[level].bind(console)]),
|
|
92
|
+
) as Record<(typeof methods)[number], (...values: unknown[]) => void>;
|
|
93
|
+
let reporting = false;
|
|
94
|
+
const emit = (
|
|
95
|
+
level: ClientDiagnosticEvent["level"],
|
|
96
|
+
values: unknown[],
|
|
97
|
+
stack?: string,
|
|
98
|
+
) => {
|
|
99
|
+
if (reporting) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
reporting = true;
|
|
103
|
+
try {
|
|
104
|
+
const serializedArguments = serializeBrowserLogArguments(values);
|
|
105
|
+
report({
|
|
106
|
+
arguments: serializedArguments,
|
|
107
|
+
level,
|
|
108
|
+
message: truncate(
|
|
109
|
+
values
|
|
110
|
+
.map((value) =>
|
|
111
|
+
typeof value === "string" ? value : serializedArguments,
|
|
112
|
+
)
|
|
113
|
+
.join(" "),
|
|
114
|
+
4_000,
|
|
115
|
+
),
|
|
116
|
+
...(stack ? { stack: truncate(stack, 8_000) } : {}),
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
type: "client-diagnostic",
|
|
119
|
+
...(diagnosticUrl() ? { url: diagnosticUrl() } : {}),
|
|
120
|
+
});
|
|
121
|
+
} finally {
|
|
122
|
+
reporting = false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
methods.forEach((level) => {
|
|
126
|
+
console[level] = (...values: unknown[]) => {
|
|
127
|
+
originals[level](...values);
|
|
128
|
+
emit(level, values, stackOf(values));
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
const onError = (event: ErrorEvent) =>
|
|
132
|
+
emit("error", [event.message], event.error?.stack);
|
|
133
|
+
const onRejection = (event: PromiseRejectionEvent) =>
|
|
134
|
+
emit(
|
|
135
|
+
"error",
|
|
136
|
+
["Unhandled rejection", event.reason],
|
|
137
|
+
stackOf([event.reason]),
|
|
138
|
+
);
|
|
139
|
+
window.addEventListener("error", onError);
|
|
140
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
methods.forEach((level) => {
|
|
144
|
+
console[level] = originals[level];
|
|
145
|
+
});
|
|
146
|
+
window.removeEventListener("error", onError);
|
|
147
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
148
|
+
};
|
|
149
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
disableBackgroundNotifications,
|
|
5
|
+
enableBackgroundNotifications,
|
|
6
|
+
reconciledStatus,
|
|
7
|
+
type BackgroundNotificationStatus,
|
|
8
|
+
} from "../../background-notifications";
|
|
9
|
+
|
|
10
|
+
const statusMessage = (status: BackgroundNotificationStatus) => {
|
|
11
|
+
const messages: Record<BackgroundNotificationStatus, string> = {
|
|
12
|
+
checking: "Checking background notification status…",
|
|
13
|
+
denied:
|
|
14
|
+
"Background notifications are blocked in this browser's site settings.",
|
|
15
|
+
disabled: "Background notifications are disabled for this browser.",
|
|
16
|
+
enabled: "Background notifications are enabled for this browser.",
|
|
17
|
+
error: "Background notification settings could not be updated. Try again.",
|
|
18
|
+
"ios-home-screen":
|
|
19
|
+
"On iPhone or iPad, add Overmux to the Home Screen before enabling background notifications.",
|
|
20
|
+
unsupported: "Background notifications are not supported in this browser.",
|
|
21
|
+
};
|
|
22
|
+
return messages[status];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const BrowserNotificationSettings = () => {
|
|
26
|
+
const [status, setStatus] =
|
|
27
|
+
useState<BackgroundNotificationStatus>("checking");
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let active = true;
|
|
31
|
+
const reconcile = () => {
|
|
32
|
+
void reconciledStatus()
|
|
33
|
+
.then((next) => {
|
|
34
|
+
if (active) {
|
|
35
|
+
setStatus(next);
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.catch(() => {
|
|
39
|
+
if (active) {
|
|
40
|
+
setStatus("error");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const onVisibilityChange = () => {
|
|
45
|
+
if (document.visibilityState === "visible") {
|
|
46
|
+
reconcile();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
reconcile();
|
|
50
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
51
|
+
navigator.serviceWorker?.addEventListener("controllerchange", reconcile);
|
|
52
|
+
return () => {
|
|
53
|
+
active = false;
|
|
54
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
55
|
+
navigator.serviceWorker?.removeEventListener(
|
|
56
|
+
"controllerchange",
|
|
57
|
+
reconcile,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const enable = useCallback(async () => {
|
|
63
|
+
try {
|
|
64
|
+
setStatus("checking");
|
|
65
|
+
const enabled = await enableBackgroundNotifications();
|
|
66
|
+
setStatus(
|
|
67
|
+
enabled
|
|
68
|
+
? "enabled"
|
|
69
|
+
: Notification.permission === "denied"
|
|
70
|
+
? "denied"
|
|
71
|
+
: "disabled",
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
setStatus("error");
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const disable = useCallback(async () => {
|
|
79
|
+
try {
|
|
80
|
+
setStatus("checking");
|
|
81
|
+
await disableBackgroundNotifications();
|
|
82
|
+
setStatus("disabled");
|
|
83
|
+
} catch {
|
|
84
|
+
setStatus("error");
|
|
85
|
+
}
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const canEnable = status === "disabled" || status === "error";
|
|
89
|
+
const canDisable = status === "enabled" || status === "error";
|
|
90
|
+
return (
|
|
91
|
+
<div data-om-browser-notifications="">
|
|
92
|
+
<h3>Browser notifications</h3>
|
|
93
|
+
<p aria-live="polite" data-om-background-notifications-status="">
|
|
94
|
+
{statusMessage(status)}
|
|
95
|
+
</p>
|
|
96
|
+
<div data-om-hosted-actions="">
|
|
97
|
+
{canEnable ? (
|
|
98
|
+
<button
|
|
99
|
+
data-om-background-notifications-enable=""
|
|
100
|
+
onClick={enable}
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
Enable
|
|
104
|
+
</button>
|
|
105
|
+
) : null}
|
|
106
|
+
{canDisable ? (
|
|
107
|
+
<button
|
|
108
|
+
data-om-background-notifications-disable=""
|
|
109
|
+
onClick={disable}
|
|
110
|
+
type="button"
|
|
111
|
+
>
|
|
112
|
+
Disable
|
|
113
|
+
</button>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
@layer om.components {
|
|
2
|
+
@scope ([data-om-hosted-page]) {
|
|
3
|
+
:where(:scope) {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
overflow: auto;
|
|
8
|
+
padding: clamp(1rem, 5vw, 4rem);
|
|
9
|
+
color: var(--om-color-fg, var(--_om-color-fg));
|
|
10
|
+
background: var(--om-color-canvas, var(--_om-color-canvas));
|
|
11
|
+
}
|
|
12
|
+
:where([data-om-hosted-panel]) {
|
|
13
|
+
inline-size: min(100%, 44rem);
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
margin-inline: auto;
|
|
16
|
+
padding: clamp(1.25rem, 4vw, 2.5rem);
|
|
17
|
+
border: 1px solid var(--om-color-border, var(--_om-color-border));
|
|
18
|
+
border-radius: var(--om-radius, 0.5rem);
|
|
19
|
+
background: var(--om-color-surface, var(--_om-color-surface));
|
|
20
|
+
box-shadow: var(--om-elevation, 0 1rem 2rem rgb(0 0 0 / 20%));
|
|
21
|
+
}
|
|
22
|
+
:where(h1, h2, h3, p) {
|
|
23
|
+
margin-block-start: 0;
|
|
24
|
+
}
|
|
25
|
+
:where(h1) {
|
|
26
|
+
font-size: clamp(1.5rem, 4vw, 2rem);
|
|
27
|
+
}
|
|
28
|
+
:where(h2) {
|
|
29
|
+
font-size: 1.125rem;
|
|
30
|
+
}
|
|
31
|
+
:where(h3) {
|
|
32
|
+
font-size: 1rem;
|
|
33
|
+
}
|
|
34
|
+
:where(p) {
|
|
35
|
+
color: var(--om-color-muted, var(--_om-color-muted));
|
|
36
|
+
line-height: 1.55;
|
|
37
|
+
}
|
|
38
|
+
:where([data-om-hosted-header]) {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: baseline;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
gap: 1rem;
|
|
43
|
+
}
|
|
44
|
+
:where([data-om-hosted-section]) {
|
|
45
|
+
margin-block-start: 1.5rem;
|
|
46
|
+
padding-block-start: 1.5rem;
|
|
47
|
+
border-block-start: 1px solid
|
|
48
|
+
var(--om-color-border, var(--_om-color-border));
|
|
49
|
+
}
|
|
50
|
+
:where([data-om-hosted-actions]) {
|
|
51
|
+
display: flex;
|
|
52
|
+
gap: 0.5rem;
|
|
53
|
+
}
|
|
54
|
+
:where(
|
|
55
|
+
[data-om-background-notifications-enable],
|
|
56
|
+
[data-om-background-notifications-disable],
|
|
57
|
+
[data-om-hosted-retry],
|
|
58
|
+
[data-om-hosted-button]
|
|
59
|
+
) {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
min-block-size: 2.75rem;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
align-items: center;
|
|
64
|
+
padding-inline: 0.85rem;
|
|
65
|
+
border: 1px solid var(--om-color-border, var(--_om-color-border));
|
|
66
|
+
border-radius: var(--om-radius, 0.5rem);
|
|
67
|
+
color: inherit;
|
|
68
|
+
background: var(--om-color-panel, var(--_om-color-panel));
|
|
69
|
+
font: inherit;
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
:where(a) {
|
|
74
|
+
color: var(--om-color-accent, var(--_om-color-accent));
|
|
75
|
+
}
|
|
76
|
+
:where(a, button):focus-visible {
|
|
77
|
+
outline: 2px solid var(--om-color-accent, var(--_om-color-accent));
|
|
78
|
+
outline-offset: 2px;
|
|
79
|
+
}
|
|
80
|
+
@media (max-width: 30rem) {
|
|
81
|
+
:where([data-om-hosted-header]) {
|
|
82
|
+
align-items: flex-start;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { HostedPage } from "./hosted-page";
|
|
4
|
+
|
|
5
|
+
const logoutRequests = new WeakMap<Document, Promise<void>>();
|
|
6
|
+
const minimumLogoutDuration = 3_000;
|
|
7
|
+
|
|
8
|
+
const requestLogout = () => {
|
|
9
|
+
const existing = logoutRequests.get(document);
|
|
10
|
+
if (existing) {
|
|
11
|
+
return existing;
|
|
12
|
+
}
|
|
13
|
+
const request = fetch("/api/auth/logout", { method: "POST" }).then(
|
|
14
|
+
(response) => {
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Logout failed: ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
logoutRequests.set(document, request);
|
|
21
|
+
return request;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const minimumDelay = () =>
|
|
25
|
+
new Promise<void>((resolve) => setTimeout(resolve, minimumLogoutDuration));
|
|
26
|
+
|
|
27
|
+
export const HostedLogoutPage = () => {
|
|
28
|
+
const [attempt, setAttempt] = useState(0);
|
|
29
|
+
const [error, setError] = useState(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let active = true;
|
|
33
|
+
setError(false);
|
|
34
|
+
void Promise.allSettled([requestLogout(), minimumDelay()]).then(
|
|
35
|
+
([logout]) => {
|
|
36
|
+
if (!active) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (logout?.status === "fulfilled") {
|
|
40
|
+
location.replace("/login");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setError(true);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
return () => {
|
|
47
|
+
active = false;
|
|
48
|
+
};
|
|
49
|
+
}, [attempt]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<HostedPage>
|
|
53
|
+
<h1>{error ? "Log out failed" : "Logging out…"}</h1>
|
|
54
|
+
{error ? (
|
|
55
|
+
<>
|
|
56
|
+
<p role="alert">
|
|
57
|
+
Overmux could not log you out. Check your connection and try again.
|
|
58
|
+
</p>
|
|
59
|
+
<button
|
|
60
|
+
data-om-hosted-retry=""
|
|
61
|
+
onClick={() => {
|
|
62
|
+
logoutRequests.delete(document);
|
|
63
|
+
setAttempt((current) => current + 1);
|
|
64
|
+
}}
|
|
65
|
+
type="button"
|
|
66
|
+
>
|
|
67
|
+
Retry
|
|
68
|
+
</button>
|
|
69
|
+
</>
|
|
70
|
+
) : (
|
|
71
|
+
<p>Please wait while Overmux ends your session.</p>
|
|
72
|
+
)}
|
|
73
|
+
</HostedPage>
|
|
74
|
+
);
|
|
75
|
+
};
|