overmux 0.0.1 → 0.0.3
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 +27 -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 +4863 -7897
- 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 +50 -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 +23 -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 +20 -0
- package/dist/docs/400-reference/400-cli/050-init.md +17 -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 +254 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +1665 -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 +50 -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 +23 -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 +20 -0
- package/docs/400-reference/400-cli/050-init.md +17 -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 +76 -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/init-template.ts +146 -0
- package/src/internal/cli/commands/init.ts +267 -0
- package/src/internal/cli/commands/integration.ts +15 -0
- package/src/internal/cli/commands/serve.ts +322 -0
- package/src/internal/cli/commands/zellij-install.ts +96 -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/clipboard.ts +22 -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/desktop-host.ts +12 -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 +13 -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 +449 -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 +57 -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,146 @@
|
|
|
1
|
+
export type InitToolchain = "mise" | "pnpm";
|
|
2
|
+
|
|
3
|
+
export const initScaffoldPaths = [
|
|
4
|
+
".gitignore",
|
|
5
|
+
"AGENTS.md",
|
|
6
|
+
"CLAUDE.md",
|
|
7
|
+
"mise.toml",
|
|
8
|
+
"package.json",
|
|
9
|
+
"pnpm-lock.yaml",
|
|
10
|
+
"overmux.config.ts",
|
|
11
|
+
"src/server/index.ts",
|
|
12
|
+
"src/ui/app.tsx",
|
|
13
|
+
"src/ui/index.html",
|
|
14
|
+
"src/ui/main.tsx",
|
|
15
|
+
"src/ui/styles.css",
|
|
16
|
+
"vite.config.ts",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
const packageJson = (overmuxVersion: string) =>
|
|
20
|
+
`${JSON.stringify(
|
|
21
|
+
{
|
|
22
|
+
name: "my-overmux",
|
|
23
|
+
private: true,
|
|
24
|
+
type: "module",
|
|
25
|
+
scripts: {
|
|
26
|
+
build: "vite build",
|
|
27
|
+
check: "overmux check --config ./overmux.config.ts",
|
|
28
|
+
dev: "overmux serve --config ./overmux.config.ts",
|
|
29
|
+
},
|
|
30
|
+
dependencies: {
|
|
31
|
+
overmux: overmuxVersion,
|
|
32
|
+
react: "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0",
|
|
34
|
+
},
|
|
35
|
+
devDependencies: {
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
39
|
+
typescript: "^5.9.0",
|
|
40
|
+
vite: "^8.0.0",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
2,
|
|
45
|
+
)}\n`;
|
|
46
|
+
|
|
47
|
+
export const createInitScaffold = ({
|
|
48
|
+
overmuxVersion,
|
|
49
|
+
toolchain,
|
|
50
|
+
}: {
|
|
51
|
+
overmuxVersion: string;
|
|
52
|
+
toolchain: InitToolchain;
|
|
53
|
+
}): Readonly<Record<string, string>> => ({
|
|
54
|
+
".gitignore": "node_modules\ndist\n",
|
|
55
|
+
"AGENTS.md":
|
|
56
|
+
"Immediately run `overmux ai context` for documentation about Overmux.\n",
|
|
57
|
+
"CLAUDE.md": "@AGENTS.md\n",
|
|
58
|
+
...(toolchain === "mise"
|
|
59
|
+
? {
|
|
60
|
+
"mise.toml": `[tools]
|
|
61
|
+
node = "22"
|
|
62
|
+
pnpm = "10"
|
|
63
|
+
"npm:overmux" = "${overmuxVersion}"
|
|
64
|
+
`,
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
"package.json": packageJson(overmuxVersion),
|
|
68
|
+
"overmux.config.ts": `import { defineOvermuxConfig } from "overmux";
|
|
69
|
+
|
|
70
|
+
import server from "./src/server/index";
|
|
71
|
+
|
|
72
|
+
export default defineOvermuxConfig({
|
|
73
|
+
auth: { mode: "cli-login" },
|
|
74
|
+
productionWebAssetsDir: "./dist",
|
|
75
|
+
server,
|
|
76
|
+
vite: "./vite.config.ts",
|
|
77
|
+
});
|
|
78
|
+
`,
|
|
79
|
+
"src/server/index.ts": `import { defineOvermuxServer } from "overmux";
|
|
80
|
+
|
|
81
|
+
export default defineOvermuxServer({ resources: {} });
|
|
82
|
+
`,
|
|
83
|
+
"src/ui/app.tsx": `import { defineOvermuxClient } from "overmux/client";
|
|
84
|
+
|
|
85
|
+
const App = () => <main>Overmux is running.</main>;
|
|
86
|
+
|
|
87
|
+
export default defineOvermuxClient({
|
|
88
|
+
commands: {},
|
|
89
|
+
component: App,
|
|
90
|
+
});
|
|
91
|
+
`,
|
|
92
|
+
"src/ui/index.html": `<!doctype html>
|
|
93
|
+
<html lang="en">
|
|
94
|
+
<head>
|
|
95
|
+
<meta charset="UTF-8" />
|
|
96
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
97
|
+
<title>Overmux</title>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div id="root">
|
|
101
|
+
<p>Loading Overmux…</p>
|
|
102
|
+
<button type="button" onclick="location.reload()">Retry</button>
|
|
103
|
+
</div>
|
|
104
|
+
<script type="module" src="/main.tsx"></script>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
`,
|
|
108
|
+
"src/ui/main.tsx": `import { OvermuxHost } from "overmux/client";
|
|
109
|
+
import { createRoot } from "react-dom/client";
|
|
110
|
+
|
|
111
|
+
import definition from "./app";
|
|
112
|
+
import "./styles.css";
|
|
113
|
+
|
|
114
|
+
const root = document.querySelector("#root");
|
|
115
|
+
if (!root) {
|
|
116
|
+
throw new Error("Missing root element");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
createRoot(root).render(<OvermuxHost definition={definition} />);
|
|
120
|
+
`,
|
|
121
|
+
"src/ui/styles.css": `:root {
|
|
122
|
+
color: #f5f5f5;
|
|
123
|
+
background: #111;
|
|
124
|
+
font-family: system-ui, sans-serif;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
body {
|
|
128
|
+
margin: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main {
|
|
132
|
+
display: grid;
|
|
133
|
+
min-height: 100vh;
|
|
134
|
+
place-items: center;
|
|
135
|
+
}
|
|
136
|
+
`,
|
|
137
|
+
"vite.config.ts": `import viteReact from "@vitejs/plugin-react";
|
|
138
|
+
import { defineConfig } from "vite";
|
|
139
|
+
|
|
140
|
+
export default defineConfig({
|
|
141
|
+
build: { emptyOutDir: true, outDir: "../../dist" },
|
|
142
|
+
plugins: [viteReact()],
|
|
143
|
+
root: "src/ui",
|
|
144
|
+
});
|
|
145
|
+
`,
|
|
146
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { buildCommand } from "@stricli/core";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { getOvermuxPaths } from "../../server/paths";
|
|
16
|
+
import {
|
|
17
|
+
createInitScaffold,
|
|
18
|
+
initScaffoldPaths,
|
|
19
|
+
type InitToolchain,
|
|
20
|
+
} from "./init-template";
|
|
21
|
+
|
|
22
|
+
const miseActivationDocs =
|
|
23
|
+
"https://mise.jdx.dev/getting-started.html#activate-mise";
|
|
24
|
+
type CommandResult = {
|
|
25
|
+
error?: NodeJS.ErrnoException;
|
|
26
|
+
status: number | null;
|
|
27
|
+
stderr: string;
|
|
28
|
+
stdout: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type InitDependencies = {
|
|
32
|
+
directory: string;
|
|
33
|
+
getLatestOvermuxVersion: () => Promise<string>;
|
|
34
|
+
runCommand: (
|
|
35
|
+
command: string,
|
|
36
|
+
args: readonly string[],
|
|
37
|
+
options?: { cwd?: string },
|
|
38
|
+
) => CommandResult;
|
|
39
|
+
stderr: { write: (text: string) => unknown };
|
|
40
|
+
stdout: { write: (text: string) => unknown };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const runCommand: InitDependencies["runCommand"] = (command, args, options) => {
|
|
44
|
+
const result = spawnSync(command, args, {
|
|
45
|
+
cwd: options?.cwd,
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
error: result.error,
|
|
51
|
+
status: result.status,
|
|
52
|
+
stderr: result.stderr ?? "",
|
|
53
|
+
stdout: result.stdout ?? "",
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getLatestOvermuxVersion = async () => {
|
|
58
|
+
const response = await fetch("https://registry.npmjs.org/overmux/latest", {
|
|
59
|
+
headers: { accept: "application/json" },
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Could not resolve the latest Overmux version: npm returned ${response.status}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const body: unknown = await response.json();
|
|
67
|
+
const version =
|
|
68
|
+
typeof body === "object" && body !== null && "version" in body
|
|
69
|
+
? body.version
|
|
70
|
+
: undefined;
|
|
71
|
+
if (typeof version !== "string") {
|
|
72
|
+
throw new Error("Could not resolve the latest Overmux version from npm");
|
|
73
|
+
}
|
|
74
|
+
return version;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const commandExists = (result: CommandResult) =>
|
|
78
|
+
result.error?.code !== "ENOENT";
|
|
79
|
+
|
|
80
|
+
const isMiseActivated = (dependencies: InitDependencies) => {
|
|
81
|
+
const doctor = dependencies.runCommand("mise", ["doctor", "--json"]);
|
|
82
|
+
if (doctor.status !== 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const report: unknown = JSON.parse(doctor.stdout);
|
|
87
|
+
return (
|
|
88
|
+
typeof report === "object" &&
|
|
89
|
+
report !== null &&
|
|
90
|
+
"activated" in report &&
|
|
91
|
+
report.activated === true
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const detectToolchain = (dependencies: InitDependencies): InitToolchain => {
|
|
99
|
+
const mise = dependencies.runCommand("mise", ["--version"]);
|
|
100
|
+
if (commandExists(mise)) {
|
|
101
|
+
if (mise.status !== 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Mise could not be validated. Activate mise and try again. ${miseActivationDocs}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!isMiseActivated(dependencies)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Mise must be activated in your shell before running overmux init. ${miseActivationDocs}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return "mise";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pnpm = dependencies.runCommand("pnpm", ["--version"]);
|
|
115
|
+
const major = Number.parseInt(pnpm.stdout.trim().split(".")[0] ?? "", 10);
|
|
116
|
+
if (!commandExists(pnpm) || pnpm.status !== 0 || !Number.isFinite(major)) {
|
|
117
|
+
throw new Error("pnpm 10 or newer is required when mise is not installed");
|
|
118
|
+
}
|
|
119
|
+
if (major < 10) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`pnpm 10 or newer is required when mise is not installed (found ${pnpm.stdout.trim()})`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return "pnpm";
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const findCollisions = (directory: string) => {
|
|
128
|
+
if (existsSync(directory) && !statSync(directory).isDirectory()) {
|
|
129
|
+
return [directory];
|
|
130
|
+
}
|
|
131
|
+
return initScaffoldPaths.filter((path) => existsSync(join(directory, path)));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const writeScaffold = (
|
|
135
|
+
directory: string,
|
|
136
|
+
files: Readonly<Record<string, string>>,
|
|
137
|
+
) => {
|
|
138
|
+
Object.entries(files).forEach(([path, content]) => {
|
|
139
|
+
const destination = join(directory, path);
|
|
140
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
141
|
+
writeFileSync(destination, content);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const runProjectCommand = ({
|
|
146
|
+
args,
|
|
147
|
+
command,
|
|
148
|
+
cwd,
|
|
149
|
+
dependencies,
|
|
150
|
+
}: {
|
|
151
|
+
args: readonly string[];
|
|
152
|
+
command: string;
|
|
153
|
+
cwd: string;
|
|
154
|
+
dependencies: InitDependencies;
|
|
155
|
+
}) => {
|
|
156
|
+
const result = dependencies.runCommand(command, args, { cwd });
|
|
157
|
+
dependencies.stdout.write(result.stdout);
|
|
158
|
+
dependencies.stderr.write(result.stderr);
|
|
159
|
+
if (result.error || result.status !== 0) {
|
|
160
|
+
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const pnpmCommands = [
|
|
165
|
+
["install", "--ignore-workspace"],
|
|
166
|
+
["exec", "overmux", "check", "--config", "./overmux.config.ts"],
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
const installAndCheck = ({
|
|
170
|
+
directory,
|
|
171
|
+
dependencies,
|
|
172
|
+
toolchain,
|
|
173
|
+
}: {
|
|
174
|
+
directory: string;
|
|
175
|
+
dependencies: InitDependencies;
|
|
176
|
+
toolchain: InitToolchain;
|
|
177
|
+
}) => {
|
|
178
|
+
const context = { cwd: directory, dependencies };
|
|
179
|
+
if (toolchain === "mise") {
|
|
180
|
+
runProjectCommand({
|
|
181
|
+
...context,
|
|
182
|
+
command: "mise",
|
|
183
|
+
args: ["install", "node@22", "pnpm@10", "--yes"],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
pnpmCommands.forEach((args) =>
|
|
187
|
+
runProjectCommand({
|
|
188
|
+
...context,
|
|
189
|
+
command: toolchain,
|
|
190
|
+
args:
|
|
191
|
+
toolchain === "mise"
|
|
192
|
+
? ["exec", "node@22", "pnpm@10", "--", "pnpm", ...args]
|
|
193
|
+
: args,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mergeScaffold = (source: string, target: string) => {
|
|
199
|
+
if (!existsSync(target)) {
|
|
200
|
+
renameSync(source, target);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
cpSync(source, target, {
|
|
204
|
+
errorOnExist: true,
|
|
205
|
+
force: false,
|
|
206
|
+
recursive: true,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const initializeOvermux = async (dependencies: InitDependencies) => {
|
|
211
|
+
const directory = resolve(dependencies.directory);
|
|
212
|
+
const collisions = findCollisions(directory);
|
|
213
|
+
if (collisions.length > 0) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Refusing to overwrite existing files:\n${collisions.map((path) => `- ${path}`).join("\n")}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const toolchain = detectToolchain(dependencies);
|
|
220
|
+
const overmuxVersion = await dependencies.getLatestOvermuxVersion();
|
|
221
|
+
const files = createInitScaffold({ overmuxVersion, toolchain });
|
|
222
|
+
const parent = dirname(directory);
|
|
223
|
+
mkdirSync(parent, { recursive: true });
|
|
224
|
+
const stagingDirectory = mkdtempSync(join(parent, ".overmux-init-"));
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Defer the npm tool entry so dependency installation uses the project-local Overmux binary.
|
|
228
|
+
const installFiles = Object.fromEntries(
|
|
229
|
+
Object.entries(files).filter(([path]) => path !== "mise.toml"),
|
|
230
|
+
);
|
|
231
|
+
writeScaffold(stagingDirectory, installFiles);
|
|
232
|
+
installAndCheck({
|
|
233
|
+
directory: stagingDirectory,
|
|
234
|
+
dependencies,
|
|
235
|
+
toolchain,
|
|
236
|
+
});
|
|
237
|
+
if (!existsSync(join(stagingDirectory, "pnpm-lock.yaml"))) {
|
|
238
|
+
throw new Error("pnpm install did not generate pnpm-lock.yaml");
|
|
239
|
+
}
|
|
240
|
+
if (files["mise.toml"]) {
|
|
241
|
+
writeFileSync(join(stagingDirectory, "mise.toml"), files["mise.toml"]);
|
|
242
|
+
}
|
|
243
|
+
mergeScaffold(stagingDirectory, directory);
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(stagingDirectory, { force: true, recursive: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
dependencies.stdout.write(
|
|
249
|
+
`Initialized Overmux in ${directory}\nNext: overmux serve\n`,
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const createInitCommand = (process: NodeJS.Process) =>
|
|
254
|
+
buildCommand({
|
|
255
|
+
func: () =>
|
|
256
|
+
initializeOvermux({
|
|
257
|
+
directory: getOvermuxPaths({ environment: process.env }).configDir,
|
|
258
|
+
getLatestOvermuxVersion,
|
|
259
|
+
runCommand,
|
|
260
|
+
stderr: process.stderr,
|
|
261
|
+
stdout: process.stdout,
|
|
262
|
+
}),
|
|
263
|
+
parameters: { flags: {} },
|
|
264
|
+
docs: {
|
|
265
|
+
brief: "Create a minimal Overmux application",
|
|
266
|
+
},
|
|
267
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// This namespace groups deliberate integration lifecycle commands by verb.
|
|
2
|
+
// Only install zellij exists now; the parser shape leaves later lifecycle verbs explicit.
|
|
3
|
+
import { buildRouteMap } from "@stricli/core";
|
|
4
|
+
|
|
5
|
+
import { zellijInstallCommand } from "./zellij-install";
|
|
6
|
+
|
|
7
|
+
const installIntegrationCommand = buildRouteMap({
|
|
8
|
+
docs: { brief: "Install an Overmux integration" },
|
|
9
|
+
routes: { zellij: zellijInstallCommand },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const integrationCommand = buildRouteMap({
|
|
13
|
+
docs: { brief: "Manage Overmux integrations" },
|
|
14
|
+
routes: { install: installIntegrationCommand },
|
|
15
|
+
});
|