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,190 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { Readable, Transform } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
|
|
6
|
+
export type DesktopArchitecture = "arm64" | "x64";
|
|
7
|
+
|
|
8
|
+
type GitHubAsset = {
|
|
9
|
+
browser_download_url: string;
|
|
10
|
+
name: string;
|
|
11
|
+
size: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type GitHubRelease = {
|
|
15
|
+
assets: GitHubAsset[];
|
|
16
|
+
draft: boolean;
|
|
17
|
+
prerelease: boolean;
|
|
18
|
+
published_at: string | null;
|
|
19
|
+
tag_name: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type DesktopRelease = {
|
|
23
|
+
artifact: GitHubAsset;
|
|
24
|
+
checksums: GitHubAsset;
|
|
25
|
+
tag: string;
|
|
26
|
+
version: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const releasesUrl =
|
|
30
|
+
"https://api.github.com/repos/richardgill/overmux/releases?per_page=100";
|
|
31
|
+
const downloadRoot =
|
|
32
|
+
"https://github.com/richardgill/overmux/releases/download/";
|
|
33
|
+
|
|
34
|
+
export const getMacDesktopArtifactName = (
|
|
35
|
+
version: string,
|
|
36
|
+
architecture: DesktopArchitecture,
|
|
37
|
+
) => `Overmux-Desktop-${version}-mac-${architecture}.zip`;
|
|
38
|
+
|
|
39
|
+
const assertOfficialAsset = (asset: GitHubAsset, tag: string) => {
|
|
40
|
+
const expected = `${downloadRoot}${tag}/${asset.name}`;
|
|
41
|
+
if (asset.browser_download_url !== expected) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Refusing unofficial desktop asset URL: ${asset.browser_download_url}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isSafeInteger(asset.size) || asset.size <= 0) {
|
|
47
|
+
throw new Error(`Desktop release asset ${asset.name} has an invalid size`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const selectDesktopRelease = (
|
|
52
|
+
releases: readonly GitHubRelease[],
|
|
53
|
+
architecture: DesktopArchitecture,
|
|
54
|
+
): DesktopRelease => {
|
|
55
|
+
const candidates = releases
|
|
56
|
+
.filter(
|
|
57
|
+
(release) =>
|
|
58
|
+
!release.draft &&
|
|
59
|
+
!release.prerelease &&
|
|
60
|
+
/^desktop-v[^/]+$/.test(release.tag_name),
|
|
61
|
+
)
|
|
62
|
+
.sort((left, right) =>
|
|
63
|
+
(right.published_at ?? "").localeCompare(left.published_at ?? ""),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const selected = candidates.find((release) => {
|
|
67
|
+
const version = release.tag_name.slice("desktop-v".length);
|
|
68
|
+
return release.assets.some(
|
|
69
|
+
(asset) =>
|
|
70
|
+
asset.name === getMacDesktopArtifactName(version, architecture),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
if (!selected) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`No stable Overmux Desktop release supports macOS ${architecture}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const version = selected.tag_name.slice("desktop-v".length);
|
|
80
|
+
const artifactName = getMacDesktopArtifactName(version, architecture);
|
|
81
|
+
const artifact = selected.assets.find((asset) => asset.name === artifactName);
|
|
82
|
+
const checksums = selected.assets.find(
|
|
83
|
+
(asset) => asset.name === "SHA256SUMS",
|
|
84
|
+
);
|
|
85
|
+
if (!artifact || !checksums) {
|
|
86
|
+
throw new Error(`Desktop release ${selected.tag_name} is incomplete`);
|
|
87
|
+
}
|
|
88
|
+
assertOfficialAsset(artifact, selected.tag_name);
|
|
89
|
+
assertOfficialAsset(checksums, selected.tag_name);
|
|
90
|
+
return { artifact, checksums, tag: selected.tag_name, version };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const fetchOk = async (url: string, fetch: typeof globalThis.fetch) => {
|
|
94
|
+
if (new URL(url).protocol !== "https:") {
|
|
95
|
+
throw new Error(`Refusing insecure desktop release URL: ${url}`);
|
|
96
|
+
}
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`GitHub request failed (${response.status}) for ${url}`);
|
|
102
|
+
}
|
|
103
|
+
if (response.url && new URL(response.url).protocol !== "https:") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Refusing insecure desktop release redirect: ${response.url}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return response;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const findDesktopRelease = async (
|
|
112
|
+
architecture: DesktopArchitecture,
|
|
113
|
+
fetch = globalThis.fetch,
|
|
114
|
+
) => {
|
|
115
|
+
const response = await fetchOk(releasesUrl, fetch);
|
|
116
|
+
return selectDesktopRelease(
|
|
117
|
+
(await response.json()) as GitHubRelease[],
|
|
118
|
+
architecture,
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const parseSha256Sums = (contents: string) => {
|
|
123
|
+
const checksums = new Map<string, string>();
|
|
124
|
+
contents.split(/\r?\n/).forEach((line) => {
|
|
125
|
+
if (!line) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const match = /^([a-f0-9]{64}) ([^/\\]+)$/.exec(line);
|
|
129
|
+
if (!match || checksums.has(match[2])) {
|
|
130
|
+
throw new Error("Desktop release has an invalid SHA256SUMS file");
|
|
131
|
+
}
|
|
132
|
+
checksums.set(match[2], match[1]);
|
|
133
|
+
});
|
|
134
|
+
return checksums;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const downloadTextAsset = async (
|
|
138
|
+
asset: GitHubAsset,
|
|
139
|
+
fetch = globalThis.fetch,
|
|
140
|
+
) => {
|
|
141
|
+
const response = await fetchOk(asset.browser_download_url, fetch);
|
|
142
|
+
const contents = await response.text();
|
|
143
|
+
if (Buffer.byteLength(contents) !== asset.size) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Downloaded ${asset.name} size does not match GitHub metadata`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return contents;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const downloadVerifiedAsset = async ({
|
|
152
|
+
asset,
|
|
153
|
+
destination,
|
|
154
|
+
expectedSha256,
|
|
155
|
+
fetch = globalThis.fetch,
|
|
156
|
+
}: {
|
|
157
|
+
asset: GitHubAsset;
|
|
158
|
+
destination: string;
|
|
159
|
+
expectedSha256: string;
|
|
160
|
+
fetch?: typeof globalThis.fetch;
|
|
161
|
+
}) => {
|
|
162
|
+
const response = await fetchOk(asset.browser_download_url, fetch);
|
|
163
|
+
if (!response.body) {
|
|
164
|
+
throw new Error(`Downloaded ${asset.name} has no body`);
|
|
165
|
+
}
|
|
166
|
+
const hash = createHash("sha256");
|
|
167
|
+
let size = 0;
|
|
168
|
+
const verification = new Transform({
|
|
169
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
170
|
+
size += chunk.byteLength;
|
|
171
|
+
hash.update(chunk);
|
|
172
|
+
callback(null, chunk);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
await pipeline(
|
|
176
|
+
Readable.fromWeb(
|
|
177
|
+
response.body as unknown as import("node:stream/web").ReadableStream,
|
|
178
|
+
),
|
|
179
|
+
verification,
|
|
180
|
+
createWriteStream(destination),
|
|
181
|
+
);
|
|
182
|
+
if (size !== asset.size) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Downloaded ${asset.name} size does not match GitHub metadata`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (hash.digest("hex") !== expectedSha256) {
|
|
188
|
+
throw new Error(`Downloaded ${asset.name} failed SHA-256 verification`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildCommand, buildRouteMap } from "@stricli/core";
|
|
2
|
+
|
|
3
|
+
import type { CliCommandContext } from "../environment";
|
|
4
|
+
import { installDesktop } from "./desktop-installer";
|
|
5
|
+
|
|
6
|
+
type InstallFlags = { yes?: boolean };
|
|
7
|
+
|
|
8
|
+
function runInstallCommand(this: CliCommandContext, flags: InstallFlags) {
|
|
9
|
+
return installDesktop(
|
|
10
|
+
{ action: "install", yes: flags.yes },
|
|
11
|
+
{},
|
|
12
|
+
this.process,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runUpgradeCommand(this: CliCommandContext) {
|
|
17
|
+
return installDesktop({ action: "upgrade" }, {}, this.process);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const desktopInstallCommand = buildCommand({
|
|
21
|
+
func: runInstallCommand,
|
|
22
|
+
parameters: {
|
|
23
|
+
flags: {
|
|
24
|
+
yes: {
|
|
25
|
+
brief: "Accept the unsigned application risk without prompting",
|
|
26
|
+
kind: "boolean",
|
|
27
|
+
optional: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
positional: { kind: "tuple", parameters: [] },
|
|
31
|
+
},
|
|
32
|
+
docs: { brief: "Install the experimental macOS desktop application" },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const desktopUpgradeCommand = buildCommand({
|
|
36
|
+
func: runUpgradeCommand,
|
|
37
|
+
parameters: { positional: { kind: "tuple", parameters: [] } },
|
|
38
|
+
docs: { brief: "Upgrade the installed macOS desktop application" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const desktopRoute = buildRouteMap({
|
|
42
|
+
docs: { brief: "Install or upgrade Overmux Desktop on macOS" },
|
|
43
|
+
routes: {
|
|
44
|
+
install: desktopInstallCommand,
|
|
45
|
+
upgrade: desktopUpgradeCommand,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildCommand } from "@stricli/core";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
type DocsOutput = { write: (text: string) => unknown };
|
|
7
|
+
|
|
8
|
+
export const resolveInstalledDocsRoot = (moduleUrl: string) => {
|
|
9
|
+
const candidates = ["../docs", "./docs", "../../../docs"].map((path) =>
|
|
10
|
+
fileURLToPath(new URL(path, moduleUrl)),
|
|
11
|
+
);
|
|
12
|
+
const docsRoot = candidates.find((candidate) => existsSync(candidate));
|
|
13
|
+
if (!docsRoot) {
|
|
14
|
+
throw new Error("Installed Overmux documentation is missing");
|
|
15
|
+
}
|
|
16
|
+
return resolve(docsRoot);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const createDocsCommand = ({
|
|
20
|
+
docsRoot,
|
|
21
|
+
output,
|
|
22
|
+
}: {
|
|
23
|
+
docsRoot: string;
|
|
24
|
+
output: DocsOutput;
|
|
25
|
+
}) =>
|
|
26
|
+
buildCommand({
|
|
27
|
+
func: () => {
|
|
28
|
+
output.write(`${resolve(docsRoot)}\n`);
|
|
29
|
+
},
|
|
30
|
+
parameters: {},
|
|
31
|
+
docs: {
|
|
32
|
+
brief: "Print the installed documentation directory",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { loadOvermuxConfig } from "@overmux/shared/node";
|
|
2
|
+
import { buildCommand } from "@stricli/core";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
import type { CliCommandContext, CliEnvironment } from "../environment";
|
|
9
|
+
import { issueLoginGrant, printLoginFallback, printLoginGrant } from "../login";
|
|
10
|
+
import {
|
|
11
|
+
loadViteRoot,
|
|
12
|
+
startManagedVite,
|
|
13
|
+
watchTrustedFiles,
|
|
14
|
+
type ViteApi,
|
|
15
|
+
} from "../managed-vite";
|
|
16
|
+
import { parsePort } from "../parse-port";
|
|
17
|
+
import { startOvermuxServer } from "../../server/index";
|
|
18
|
+
import { getDefaultConfigPath } from "../../server/paths";
|
|
19
|
+
|
|
20
|
+
export type ServeFlags = {
|
|
21
|
+
config: string;
|
|
22
|
+
host?: string;
|
|
23
|
+
login?: boolean;
|
|
24
|
+
noBuild?: boolean;
|
|
25
|
+
noLogin?: boolean;
|
|
26
|
+
port?: number;
|
|
27
|
+
production?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RunningServe = {
|
|
31
|
+
close: () => Promise<void>;
|
|
32
|
+
port: number;
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const loadApplicationVite = async (
|
|
37
|
+
viteConfigPath: string,
|
|
38
|
+
): Promise<ViteApi> => {
|
|
39
|
+
const require = createRequire(viteConfigPath);
|
|
40
|
+
const viteEntry = require.resolve("vite");
|
|
41
|
+
return import(pathToFileURL(viteEntry).href) as Promise<ViteApi>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Register before startup resolves so signals still clean up partially started servers.
|
|
45
|
+
const registerShutdown = (
|
|
46
|
+
running: Promise<RunningServe>,
|
|
47
|
+
process: NodeJS.Process,
|
|
48
|
+
) => {
|
|
49
|
+
let shuttingDown = false;
|
|
50
|
+
const shutdown = async () => {
|
|
51
|
+
if (shuttingDown) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
shuttingDown = true;
|
|
55
|
+
try {
|
|
56
|
+
const server = await running;
|
|
57
|
+
await server.close();
|
|
58
|
+
process.exitCode = 0;
|
|
59
|
+
} catch {
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const onSignal = () => void shutdown();
|
|
64
|
+
process.once("SIGINT", onSignal);
|
|
65
|
+
process.once("SIGTERM", onSignal);
|
|
66
|
+
return () => {
|
|
67
|
+
process.off("SIGINT", onSignal);
|
|
68
|
+
process.off("SIGTERM", onSignal);
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const startDevelopmentServer = async ({
|
|
73
|
+
configPath,
|
|
74
|
+
host,
|
|
75
|
+
port,
|
|
76
|
+
shouldWatch,
|
|
77
|
+
vite,
|
|
78
|
+
viteConfigPath,
|
|
79
|
+
}: {
|
|
80
|
+
configPath: string;
|
|
81
|
+
host?: string;
|
|
82
|
+
port?: number;
|
|
83
|
+
shouldWatch: boolean;
|
|
84
|
+
vite: ViteApi;
|
|
85
|
+
viteConfigPath: string;
|
|
86
|
+
}): Promise<RunningServe> => {
|
|
87
|
+
const managedVite = await startManagedVite({
|
|
88
|
+
api: vite,
|
|
89
|
+
configPath: viteConfigPath,
|
|
90
|
+
});
|
|
91
|
+
let server: Awaited<ReturnType<typeof startOvermuxServer>> | undefined;
|
|
92
|
+
let stopWatching: () => void = () => undefined;
|
|
93
|
+
try {
|
|
94
|
+
if (shouldWatch) {
|
|
95
|
+
stopWatching = await watchTrustedFiles({
|
|
96
|
+
configPath,
|
|
97
|
+
onUpdate: () => server?.announceUpdateAvailable(),
|
|
98
|
+
server: managedVite.server,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
server = await startOvermuxServer({
|
|
102
|
+
configPath,
|
|
103
|
+
developmentWebTarget: managedVite.target,
|
|
104
|
+
...(host === undefined ? {} : { host }),
|
|
105
|
+
...(port === undefined ? {} : { port }),
|
|
106
|
+
watch: false,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
close: async () => {
|
|
110
|
+
stopWatching();
|
|
111
|
+
await Promise.all([server?.close(), managedVite.close()]);
|
|
112
|
+
},
|
|
113
|
+
port: server.port,
|
|
114
|
+
url: server.url,
|
|
115
|
+
};
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
stopWatching();
|
|
118
|
+
await Promise.all([server?.close(), managedVite.close()]);
|
|
119
|
+
throw cause;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const startProductionServer = async ({
|
|
124
|
+
configPath,
|
|
125
|
+
host,
|
|
126
|
+
noBuild,
|
|
127
|
+
port,
|
|
128
|
+
productionWebAssetsDir,
|
|
129
|
+
vite,
|
|
130
|
+
viteConfigPath,
|
|
131
|
+
}: {
|
|
132
|
+
configPath: string;
|
|
133
|
+
host?: string;
|
|
134
|
+
noBuild: boolean;
|
|
135
|
+
port?: number;
|
|
136
|
+
productionWebAssetsDir: string;
|
|
137
|
+
vite: ViteApi;
|
|
138
|
+
viteConfigPath: string;
|
|
139
|
+
}): Promise<RunningServe> => {
|
|
140
|
+
if (noBuild) {
|
|
141
|
+
const entryPath = join(productionWebAssetsDir, "index.html");
|
|
142
|
+
try {
|
|
143
|
+
await access(entryPath);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`Production entry does not exist: ${entryPath}`);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
const { root } = await loadViteRoot({
|
|
149
|
+
api: vite,
|
|
150
|
+
command: "build",
|
|
151
|
+
configPath: viteConfigPath,
|
|
152
|
+
});
|
|
153
|
+
await vite.build({ configFile: viteConfigPath, root });
|
|
154
|
+
}
|
|
155
|
+
const server = await startOvermuxServer({
|
|
156
|
+
configPath,
|
|
157
|
+
...(host === undefined ? {} : { host }),
|
|
158
|
+
...(port === undefined ? {} : { port }),
|
|
159
|
+
});
|
|
160
|
+
return { close: server.close, port: server.port, url: server.url };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const resolveServeLogin = (
|
|
164
|
+
flags: Pick<ServeFlags, "login" | "noLogin">,
|
|
165
|
+
environment: Pick<CliEnvironment, "isHumanUser">,
|
|
166
|
+
) => {
|
|
167
|
+
if (flags.login && flags.noLogin) {
|
|
168
|
+
throw new Error("Only set one of: --login and --no-login");
|
|
169
|
+
}
|
|
170
|
+
return flags.login || (!flags.noLogin && environment.isHumanUser);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const announceServeLogin = async ({
|
|
174
|
+
createLogin,
|
|
175
|
+
port,
|
|
176
|
+
process,
|
|
177
|
+
}: {
|
|
178
|
+
createLogin: boolean;
|
|
179
|
+
port: number;
|
|
180
|
+
process: NodeJS.Process;
|
|
181
|
+
}) => {
|
|
182
|
+
if (!createLogin) {
|
|
183
|
+
printLoginFallback(port, process);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
printLoginGrant({ ...(await issueLoginGrant(port)), output: process });
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const runServe = async ({
|
|
190
|
+
environment,
|
|
191
|
+
flags,
|
|
192
|
+
process,
|
|
193
|
+
}: {
|
|
194
|
+
environment: Pick<CliEnvironment, "isHumanUser">;
|
|
195
|
+
flags: ServeFlags;
|
|
196
|
+
process: NodeJS.Process;
|
|
197
|
+
}) => {
|
|
198
|
+
if (flags.noBuild && !flags.production) {
|
|
199
|
+
throw new Error("--no-build requires --production");
|
|
200
|
+
}
|
|
201
|
+
const createLogin = resolveServeLogin(flags, environment);
|
|
202
|
+
const configPath = resolve(flags.config);
|
|
203
|
+
const { config } = await loadOvermuxConfig({ configPath });
|
|
204
|
+
if (!config.vite) {
|
|
205
|
+
throw new Error("Overmux config must define a Vite config path in `vite`");
|
|
206
|
+
}
|
|
207
|
+
const viteConfigPath = resolve(dirname(configPath), config.vite);
|
|
208
|
+
const vite = await loadApplicationVite(viteConfigPath);
|
|
209
|
+
let runningPromise: Promise<RunningServe>;
|
|
210
|
+
if (flags.production) {
|
|
211
|
+
if (!config.productionWebAssetsDir) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"Overmux config must define `productionWebAssetsDir` for production serving",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
runningPromise = startProductionServer({
|
|
217
|
+
configPath,
|
|
218
|
+
host: flags.host,
|
|
219
|
+
noBuild: flags.noBuild ?? false,
|
|
220
|
+
port: flags.port,
|
|
221
|
+
productionWebAssetsDir: resolve(
|
|
222
|
+
dirname(configPath),
|
|
223
|
+
config.productionWebAssetsDir,
|
|
224
|
+
),
|
|
225
|
+
vite,
|
|
226
|
+
viteConfigPath,
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
runningPromise = startDevelopmentServer({
|
|
230
|
+
configPath,
|
|
231
|
+
host: flags.host,
|
|
232
|
+
port: flags.port,
|
|
233
|
+
shouldWatch: config.watch ?? true,
|
|
234
|
+
vite,
|
|
235
|
+
viteConfigPath,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const unregisterShutdown = registerShutdown(runningPromise, process);
|
|
239
|
+
let running: RunningServe | undefined;
|
|
240
|
+
try {
|
|
241
|
+
running = await runningPromise;
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
`Overmux listening on ${config.auth.origins?.[0] ?? running.url}\n`,
|
|
244
|
+
);
|
|
245
|
+
await announceServeLogin({ createLogin, port: running.port, process });
|
|
246
|
+
} catch (cause) {
|
|
247
|
+
unregisterShutdown();
|
|
248
|
+
await running?.close().catch(() => undefined);
|
|
249
|
+
throw cause;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function runServeCommand(this: CliCommandContext, flags: ServeFlags) {
|
|
254
|
+
return runServe({
|
|
255
|
+
environment: this.environment,
|
|
256
|
+
flags,
|
|
257
|
+
process: this.process,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const serveCommand = buildCommand({
|
|
262
|
+
func: runServeCommand,
|
|
263
|
+
parameters: {
|
|
264
|
+
aliases: { c: "config" },
|
|
265
|
+
flags: {
|
|
266
|
+
config: {
|
|
267
|
+
brief: "Configuration file",
|
|
268
|
+
default: getDefaultConfigPath(),
|
|
269
|
+
kind: "parsed",
|
|
270
|
+
parse: String,
|
|
271
|
+
placeholder: "path",
|
|
272
|
+
},
|
|
273
|
+
host: {
|
|
274
|
+
brief: "Override the public listener host",
|
|
275
|
+
kind: "parsed",
|
|
276
|
+
optional: true,
|
|
277
|
+
parse: String,
|
|
278
|
+
placeholder: "host",
|
|
279
|
+
},
|
|
280
|
+
login: {
|
|
281
|
+
brief: "Create a browser login grant after startup",
|
|
282
|
+
kind: "boolean",
|
|
283
|
+
optional: true,
|
|
284
|
+
withNegated: false,
|
|
285
|
+
},
|
|
286
|
+
noBuild: {
|
|
287
|
+
brief: "Serve existing production assets without building",
|
|
288
|
+
kind: "boolean",
|
|
289
|
+
optional: true,
|
|
290
|
+
},
|
|
291
|
+
noLogin: {
|
|
292
|
+
brief: "Do not create a browser login grant after startup",
|
|
293
|
+
kind: "boolean",
|
|
294
|
+
optional: true,
|
|
295
|
+
withNegated: false,
|
|
296
|
+
},
|
|
297
|
+
port: {
|
|
298
|
+
brief: "Override the public listener port",
|
|
299
|
+
kind: "parsed",
|
|
300
|
+
optional: true,
|
|
301
|
+
parse: (value) => {
|
|
302
|
+
const port = parsePort(value);
|
|
303
|
+
if (port instanceof Error) {
|
|
304
|
+
throw port;
|
|
305
|
+
}
|
|
306
|
+
return port;
|
|
307
|
+
},
|
|
308
|
+
placeholder: "port",
|
|
309
|
+
},
|
|
310
|
+
production: {
|
|
311
|
+
brief: "Build and serve the production web application",
|
|
312
|
+
kind: "boolean",
|
|
313
|
+
optional: true,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
docs: {
|
|
318
|
+
brief: "Serve the Overmux web application",
|
|
319
|
+
fullDescription:
|
|
320
|
+
"Starts private Vite behind the authenticated Overmux gateway, or builds and serves production assets with --production.",
|
|
321
|
+
},
|
|
322
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { determineAgent } from "@vercel/detect-agent";
|
|
3
|
+
import * as ciInfo from "ci-info";
|
|
4
|
+
|
|
5
|
+
export type CliEnvironment = {
|
|
6
|
+
readonly agent?: string;
|
|
7
|
+
readonly isAgent: boolean;
|
|
8
|
+
readonly isCI: boolean;
|
|
9
|
+
readonly isHumanUser: boolean;
|
|
10
|
+
readonly isInteractiveTerminal: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CliCommandContext = CommandContext & {
|
|
14
|
+
readonly environment: CliEnvironment;
|
|
15
|
+
readonly process: NodeJS.Process;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CliEnvironmentSignals = {
|
|
19
|
+
readonly agent?: string;
|
|
20
|
+
readonly isAgent: boolean;
|
|
21
|
+
readonly isCI: boolean;
|
|
22
|
+
readonly stdinIsTTY?: boolean;
|
|
23
|
+
readonly stdoutIsTTY?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const classifyCliEnvironment = ({
|
|
27
|
+
agent,
|
|
28
|
+
isAgent,
|
|
29
|
+
isCI,
|
|
30
|
+
stdinIsTTY,
|
|
31
|
+
stdoutIsTTY,
|
|
32
|
+
}: CliEnvironmentSignals): CliEnvironment => {
|
|
33
|
+
const isInteractiveTerminal = stdinIsTTY === true && stdoutIsTTY === true;
|
|
34
|
+
return {
|
|
35
|
+
agent,
|
|
36
|
+
isAgent,
|
|
37
|
+
isCI,
|
|
38
|
+
isHumanUser: isInteractiveTerminal && !isCI && !isAgent,
|
|
39
|
+
isInteractiveTerminal,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const detectCliEnvironment = async (
|
|
44
|
+
process: Pick<NodeJS.Process, "stdin" | "stdout"> = globalThis.process,
|
|
45
|
+
): Promise<CliEnvironment> => {
|
|
46
|
+
const detectedAgent = await determineAgent();
|
|
47
|
+
return classifyCliEnvironment({
|
|
48
|
+
agent: detectedAgent.agent?.name,
|
|
49
|
+
isAgent: detectedAgent.isAgent,
|
|
50
|
+
isCI: ciInfo.isCI,
|
|
51
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
52
|
+
stdoutIsTTY: process.stdout.isTTY,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LoginGrant } from "../server/auth/auth-service";
|
|
2
|
+
import {
|
|
3
|
+
discoverInstance,
|
|
4
|
+
sendControlRequest,
|
|
5
|
+
} from "../server/auth/instance-control";
|
|
6
|
+
|
|
7
|
+
type CliOutput = Pick<NodeJS.Process, "stderr" | "stdout">;
|
|
8
|
+
|
|
9
|
+
export const issueLoginGrant = async (port?: number) => {
|
|
10
|
+
const instance = await discoverInstance(port);
|
|
11
|
+
const response = await sendControlRequest(instance, { type: "create-login" });
|
|
12
|
+
return { login: response.login, port: instance.port };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const printLoginFallback = (port: number, { stdout }: CliOutput) => {
|
|
16
|
+
stdout.write(`Login with \`overmux auth login --port ${port}\`\n`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const printLoginGrant = ({
|
|
20
|
+
login,
|
|
21
|
+
output,
|
|
22
|
+
port,
|
|
23
|
+
}: {
|
|
24
|
+
login: LoginGrant;
|
|
25
|
+
output: CliOutput;
|
|
26
|
+
port: number;
|
|
27
|
+
}) => {
|
|
28
|
+
const expiresIn = Math.max(
|
|
29
|
+
0,
|
|
30
|
+
Math.ceil((Date.parse(login.expiresAt) - Date.now()) / 60_000),
|
|
31
|
+
);
|
|
32
|
+
output.stdout.write(
|
|
33
|
+
[
|
|
34
|
+
`Code: ${login.code}`,
|
|
35
|
+
...login.urls.map((url) => `Open: ${url}`),
|
|
36
|
+
"This single-use grant authenticates one origin. Create another grant to authenticate another origin.",
|
|
37
|
+
"Enter the code on the login page shown by an unauthenticated Overmux origin.",
|
|
38
|
+
`Expires at ${login.expiresAt} (in ${expiresIn} minutes).`,
|
|
39
|
+
`Need another grant? Run \`overmux auth login --port ${port}\`.`,
|
|
40
|
+
"",
|
|
41
|
+
].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
output.stderr.write(
|
|
44
|
+
"The login URL is a temporary credential; avoid sharing terminal output or screenshots.\n",
|
|
45
|
+
);
|
|
46
|
+
};
|