overmux 0.0.2 → 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.
@@ -37,7 +37,7 @@ Once the CLI is installed, create your Overmux setup:
37
37
  overmux init
38
38
  ```
39
39
 
40
- This creates your setup in `$XDG_CONFIG_HOME/overmux`. Read more about the Overmux [project structure](../300-fundamentals/100-project-structure.md).
40
+ This validates your toolchain, creates a minimal application in `$XDG_CONFIG_HOME/overmux`, installs its dependencies, and checks the result. Existing scaffold files are left unchanged. Read more in the [`overmux init` reference](../400-reference/400-cli/050-init.md) and [project structure](../300-fundamentals/100-project-structure.md).
41
41
 
42
42
  ## Start the server
43
43
 
@@ -48,4 +48,3 @@ overmux serve
48
48
  Open Overmux in your browser to confirm it works.
49
49
 
50
50
  To configure hosts and ports, see [Configuration](../300-fundamentals/200-configuration.md).
51
-
@@ -1,3 +1,23 @@
1
1
  ---
2
2
  title: Project Structure
3
3
  ---
4
+
5
+ `overmux init` creates this minimal userland application:
6
+
7
+ ```text
8
+ .gitignore
9
+ mise.toml Mise installations only
10
+ package.json
11
+ pnpm-lock.yaml
12
+ overmux.config.ts authentication, server, Vite, and production settings
13
+ src/server/index.ts trusted resources, streams, and operations
14
+ src/ui/app.tsx browser application definition
15
+ src/ui/index.html browser document
16
+ src/ui/main.tsx React and Overmux host entry point
17
+ src/ui/styles.css application styles
18
+ vite.config.ts browser development and production build configuration
19
+ ```
20
+
21
+ The server and UI are separate trust boundaries. `src/server/index.ts` runs as trusted Node.js code. Files under `src/ui` run in the browser and communicate with the server through Overmux's public APIs.
22
+
23
+ The generated application has no shared directory. Add browser-safe shared schemas only when both sides need them.
@@ -1,3 +1,20 @@
1
1
  ---
2
2
  title: Client API
3
3
  ---
4
+
5
+ ## Clipboard
6
+
7
+ ```ts
8
+ import { readClipboardText, writeClipboardText } from "overmux/client";
9
+
10
+ await writeClipboardText("Text to copy");
11
+ const text = await readClipboardText();
12
+ ```
13
+
14
+ `readClipboardText(): Promise<string>` reads the system clipboard through `navigator.clipboard.readText()`. It never reads through the desktop bridge.
15
+
16
+ `writeClipboardText(text: string): Promise<void>` uses Overmux's version-1 desktop write bridge when present, otherwise `navigator.clipboard.writeText()`. Desktop writes are fire-and-forget: resolution confirms dispatch, not completion or acceptance by the host. Existing host origin and active-frame checks still apply.
17
+
18
+ Both reject on unavailable browser APIs, browser permission failures, or synchronous desktop dispatch failures. Browser secure-context, focus, permissions, and user-activation restrictions still apply. These APIs target the system clipboard, not the X11 primary selection. Only read clipboard data you need and trust the source of text you write.
19
+
20
+ For terminal-program access via OSC 52, use the opt-in factories from `@overmux/xterm/client` rather than wiring platform bridges in application code.
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: "overmux init"
3
+ ---
4
+
5
+ # `overmux init`
6
+
7
+ Create a minimal, runnable Overmux application without prompting.
8
+
9
+ ```sh
10
+ overmux init
11
+ ```
12
+
13
+ The application is created in `$XDG_CONFIG_HOME/overmux`.
14
+
15
+ The command validates the toolchain before writing files. When Mise is installed, it must be [activated in the shell](https://mise.jdx.dev/getting-started.html#activate-mise); the generated `mise.toml` pins Node 22, pnpm 10, and the latest published Overmux version. Without Mise, pnpm 10 or newer must already be available and `mise.toml` is omitted.
16
+
17
+ The initializer pins the exact latest published `overmux` version in `package.json`, installs dependencies, generates `pnpm-lock.yaml`, and runs `overmux check`. Existing scaffold files cause the command to fail without changing them. Unrelated files are preserved.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overmux",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "homepage": "https://github.com/richardgill/overmux",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,7 +52,7 @@
52
52
  "web-push": "3.6.7",
53
53
  "ws": "8.21.3",
54
54
  "zod": "4.4.3",
55
- "@overmux/keybindings": "0.0.2"
55
+ "@overmux/keybindings": "0.0.3"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@playwright/test": "1.58.0",
@@ -12,13 +12,15 @@ import { checkCommand } from "./commands/check";
12
12
  import { callCommand } from "./commands/call";
13
13
  import { desktopRoute } from "./commands/desktop";
14
14
  import { createDocsCommand, resolveInstalledDocsRoot } from "./commands/docs";
15
+ import { createInitCommand } from "./commands/init";
16
+ import { integrationCommand } from "./commands/integration";
15
17
  import { serveCommand } from "./commands/serve";
16
18
  import { detectCliEnvironment, type CliCommandContext } from "./environment";
17
19
 
18
20
  export const createCliApp = (process = globalThis.process) =>
19
21
  buildApplication<CliCommandContext>(
20
22
  buildRouteMap<string, CliCommandContext>({
21
- docs: { brief: "Serve, validate, or invoke Overmux operations" },
23
+ docs: { brief: "Initialize and manage Overmux applications" },
22
24
  routes: {
23
25
  ai: createAiRoute(process),
24
26
  auth: authRoute,
@@ -29,6 +31,8 @@ export const createCliApp = (process = globalThis.process) =>
29
31
  docsRoot: resolveInstalledDocsRoot(import.meta.url),
30
32
  output: process.stdout,
31
33
  }),
34
+ init: createInitCommand(process),
35
+ integration: integrationCommand,
32
36
  serve: serveCommand,
33
37
  },
34
38
  }),
@@ -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
+ });
@@ -0,0 +1,96 @@
1
+ // This hard-coded CLI seam delegates installation to the invoking project's integration.
2
+ // It intentionally resolves one known subpath rather than creating a plugin registry.
3
+ import { readFileSync } from "node:fs";
4
+ import { findPackageJSON } from "node:module";
5
+ import { dirname, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ import { buildCommand } from "@stricli/core";
9
+
10
+ type ZellijInstallModule = {
11
+ installZellij: (options: { session?: string }) => Promise<{
12
+ artifactPath: string;
13
+ pluginVersion: string;
14
+ protocolVersion: number;
15
+ session: string;
16
+ sha256: string;
17
+ }>;
18
+ };
19
+
20
+ type ZellijInstallDependencies = {
21
+ cwd: string;
22
+ importModule: (specifier: string) => Promise<ZellijInstallModule>;
23
+ resolveInstall: () => string;
24
+ };
25
+
26
+ const resolveZellijInstall = (cwd: string) => {
27
+ const projectUrl = pathToFileURL(resolve(cwd, "package.json"));
28
+ const packagePath = findPackageJSON("@overmux/zellij/install", projectUrl);
29
+ if (packagePath === undefined) {
30
+ throw new Error("package not found");
31
+ }
32
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8")) as {
33
+ exports?: { "./install"?: { import?: unknown } };
34
+ };
35
+ const installExport = packageJson.exports?.["./install"]?.import;
36
+ if (typeof installExport !== "string") {
37
+ throw new Error("package has no importable ./install export");
38
+ }
39
+ return resolve(dirname(packagePath), installExport);
40
+ };
41
+
42
+ const defaultDependencies = (): ZellijInstallDependencies => {
43
+ const cwd = process.cwd();
44
+ return {
45
+ cwd,
46
+ importModule: (specifier) =>
47
+ import(specifier) as Promise<ZellijInstallModule>,
48
+ resolveInstall: () => resolveZellijInstall(cwd),
49
+ };
50
+ };
51
+
52
+ export const executeZellijInstall = async (
53
+ { session }: { session?: string },
54
+ dependencies = defaultDependencies(),
55
+ ) => {
56
+ let modulePath: string;
57
+ try {
58
+ modulePath = dependencies.resolveInstall();
59
+ } catch {
60
+ throw new Error(
61
+ `Could not resolve @overmux/zellij/install from ${dependencies.cwd}. Add @overmux/zellij to this project, then run the command again.`,
62
+ );
63
+ }
64
+ const integration = await dependencies.importModule(
65
+ pathToFileURL(modulePath).href,
66
+ );
67
+ if (typeof integration.installZellij !== "function") {
68
+ throw new Error(
69
+ "@overmux/zellij/install does not export installZellij. Update @overmux/zellij and run the command again.",
70
+ );
71
+ }
72
+ return integration.installZellij({ session });
73
+ };
74
+
75
+ const runZellijInstall = async ({ session }: { session?: string }) => {
76
+ const result = await executeZellijInstall({ session });
77
+ console.log(
78
+ `Installed Zellij plugin ${result.pluginVersion} (protocol ${result.protocolVersion})\nSession: ${result.session}\nArtifact: ${result.artifactPath}\nSHA-256: ${result.sha256}`,
79
+ );
80
+ };
81
+
82
+ export const zellijInstallCommand = buildCommand({
83
+ docs: { brief: "Install and approve the @overmux/zellij plugin" },
84
+ func: runZellijInstall,
85
+ parameters: {
86
+ flags: {
87
+ session: {
88
+ brief: "Target an active Zellij session",
89
+ kind: "parsed",
90
+ optional: true,
91
+ parse: String,
92
+ placeholder: "name",
93
+ },
94
+ },
95
+ },
96
+ });
@@ -0,0 +1,22 @@
1
+ import "./host/desktop-host";
2
+
3
+ // Reads the system clipboard through the browser, never through the desktop host.
4
+ export const readClipboardText = async (): Promise<string> => {
5
+ if (!globalThis.navigator?.clipboard?.readText) {
6
+ throw new Error("Clipboard reading is unavailable in this browser context");
7
+ }
8
+ return navigator.clipboard.readText();
9
+ };
10
+
11
+ // Desktop writes are fire-and-forget: resolution confirms dispatch, not completion.
12
+ export const writeClipboardText = async (text: string): Promise<void> => {
13
+ const desktopClipboard = globalThis.window?.overmuxHost?.clipboard;
14
+ if (desktopClipboard?.version === 1) {
15
+ desktopClipboard.writeText(text);
16
+ return;
17
+ }
18
+ if (!globalThis.navigator?.clipboard?.writeText) {
19
+ throw new Error("Clipboard writing is unavailable in this browser context");
20
+ }
21
+ await navigator.clipboard.writeText(text);
22
+ };
@@ -0,0 +1,12 @@
1
+ type DesktopHost = {
2
+ clipboard?: { writeText: (text: string) => void; version: 1 };
3
+ notifications?: { show: (notification: unknown) => void; version: 1 };
4
+ };
5
+
6
+ declare global {
7
+ interface Window {
8
+ overmuxHost?: DesktopHost;
9
+ }
10
+ }
11
+
12
+ export {};