overmux 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/auth/.vite/manifest.json +2 -2
  3. package/dist/auth/assets/{index-Bx8JhwVi.css → index-BM9gETvx.css} +1 -1
  4. package/dist/auth/assets/{index-CHIE7qYj.js → index-DabAfcs1.js} +1 -1
  5. package/dist/auth/index.html +2 -2
  6. package/dist/bin.js +430 -18
  7. package/dist/bin.js.map +1 -1
  8. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +1 -2
  9. package/dist/docs/300-fundamentals/100-project-structure.md +20 -0
  10. package/dist/docs/300-fundamentals/300-theming.md +51 -0
  11. package/dist/docs/400-reference/100-configuration.md +20 -0
  12. package/dist/docs/400-reference/200-server-api.md +18 -0
  13. package/dist/docs/400-reference/300-client-api.md +36 -0
  14. package/dist/docs/400-reference/400-cli/050-init.md +17 -0
  15. package/dist/docs/400-reference/400-cli/350-instance.md +24 -0
  16. package/dist/exports/client.d.ts +33 -3
  17. package/dist/exports/client.d.ts.map +1 -1
  18. package/dist/exports/client.js +123 -14
  19. package/dist/exports/client.js.map +1 -1
  20. package/dist/exports/{index-D3_Dpy_T.d.ts → index-DS70rKzo.d.ts} +14 -2
  21. package/dist/exports/index-DS70rKzo.d.ts.map +1 -0
  22. package/dist/exports/index.d.ts +2 -2
  23. package/dist/exports/index.js +1 -1
  24. package/dist/exports/index.js.map +1 -1
  25. package/dist/exports/{notifications-kDp16bU_.js → notifications-av0FK0yZ.js} +7 -2
  26. package/dist/exports/{notifications-kDp16bU_.js.map → notifications-av0FK0yZ.js.map} +1 -1
  27. package/dist/exports/server.d.ts.map +1 -1
  28. package/dist/exports/server.js +52 -10
  29. package/dist/exports/server.js.map +1 -1
  30. package/dist/internal/server/coordinator/server-child.js +110 -46
  31. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  32. package/docs/200-getting-started/100-install-and-run-overmux.md +1 -2
  33. package/docs/300-fundamentals/100-project-structure.md +20 -0
  34. package/docs/300-fundamentals/300-theming.md +51 -0
  35. package/docs/400-reference/100-configuration.md +20 -0
  36. package/docs/400-reference/200-server-api.md +18 -0
  37. package/docs/400-reference/300-client-api.md +36 -0
  38. package/docs/400-reference/400-cli/050-init.md +17 -0
  39. package/docs/400-reference/400-cli/350-instance.md +24 -0
  40. package/package.json +2 -2
  41. package/src/internal/cli/app.ts +7 -1
  42. package/src/internal/cli/commands/init-template.ts +146 -0
  43. package/src/internal/cli/commands/init.ts +267 -0
  44. package/src/internal/cli/commands/instance.ts +58 -0
  45. package/src/internal/cli/commands/integration.ts +15 -0
  46. package/src/internal/cli/commands/zellij-install.ts +96 -0
  47. package/src/internal/cli/parse-port.ts +2 -1
  48. package/src/internal/client/auth/auth-shell.css +17 -1
  49. package/src/internal/client/auth/auth-shell.tsx +2 -11
  50. package/src/internal/client/auth/login-code-input.tsx +140 -0
  51. package/src/internal/client/browser-api.ts +17 -3
  52. package/src/internal/client/clipboard.ts +22 -0
  53. package/src/internal/client/host/desktop-host.ts +15 -0
  54. package/src/internal/client/host/environment.ts +67 -0
  55. package/src/internal/client/host/instance-forwarding.ts +21 -0
  56. package/src/internal/client/host/notification-forwarding.ts +1 -10
  57. package/src/internal/client/host/overmux-host.tsx +8 -0
  58. package/src/internal/client/overmux-react.ts +24 -2
  59. package/src/internal/client/websocket.ts +48 -20
  60. package/src/internal/server/auth/instance-control.ts +24 -3
  61. package/src/internal/server/runtime/create-runtime.ts +12 -1
  62. package/src/internal/server/runtime/runtime-instance.ts +46 -0
  63. package/src/internal/server/runtime/runtime-resources.ts +3 -0
  64. package/src/internal/server/server-startup-options.ts +9 -4
  65. package/src/internal/server/start-application-server.ts +20 -12
  66. package/src/internal/server/websocket-server.ts +7 -0
  67. package/src/internal/shared/index.ts +2 -0
  68. package/src/internal/shared/protocol.ts +10 -1
  69. package/src/public/client.ts +5 -0
  70. package/src/public/config.ts +7 -0
  71. package/src/public/index.ts +2 -0
  72. package/dist/exports/index-D3_Dpy_T.d.ts.map +0 -1
@@ -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,54 @@
1
1
  ---
2
2
  title: Theming
3
3
  ---
4
+
5
+ ## Host and platform selectors
6
+
7
+ Overmux sets two independent attributes on the hosted application's `<html>` element before React mounts:
8
+
9
+ ```html
10
+ <html data-om-host="desktop" data-om-platform="macos">
11
+ ```
12
+
13
+ Use these public CSS selectors to adapt your application to its environment:
14
+
15
+ ```css
16
+ /* Hide browser-only installation guidance in native and installed apps. */
17
+ html:not([data-om-host="browser"]) .install-prompt {
18
+ display: none;
19
+ }
20
+
21
+ /* OS-specific styling also works when using Overmux in a browser. */
22
+ html[data-om-platform="macos"] .shortcut-hint {
23
+ font-family: system-ui;
24
+ }
25
+
26
+ /* Combine the independent host and platform selectors. */
27
+ html[data-om-host="desktop"][data-om-platform="windows"] .app-toolbar {
28
+ padding-inline: 1rem;
29
+ }
30
+ ```
31
+
32
+ ### Host values
33
+
34
+ | `data-om-host` | Meaning |
35
+ | --- | --- |
36
+ | `desktop` | Running inside the native Overmux desktop host, regardless of window size. |
37
+ | `browser` | Running in a regular browser tab or window. |
38
+ | `pwa` | Running as an installed web app in `standalone`, `minimal-ui`, or `window-controls-overlay` display mode, including Safari's installed standalone mode. |
39
+
40
+ A progressive web app (PWA) is a website that can run as an installed application. Visiting an installable website does not make the host `pwa`. Ordinary browser fullscreen does not qualify either. Native desktop detection takes precedence over display mode. Browser/PWA selectors update when the supported display modes change.
41
+
42
+ `desktop` describes the host, not a desktop-sized layout. Use CSS media or container queries for responsive layout. The native host's implementation technology is not a CSS value.
43
+
44
+ ### Platform values
45
+
46
+ `data-om-platform` is always one of `macos`, `windows`, `linux`, `android`, `ios`, or `unknown`.
47
+
48
+ The native desktop host supplies its OS identity through versioned preload metadata. Browsers use best-effort detection from browser-provided platform and user-agent information, including touch-capable iPads presenting a Mac identity. Missing, obscured, or unrecognized information can yield `unknown`; do not use these selectors for security decisions or feature detection.
49
+
50
+ ### Scope
51
+
52
+ These attributes belong to the hosted application document, independently of `OvermuxThemeScope`. They do not change theme tokens, color scheme, contrast, nested theme scopes, or portal inheritance. Selectors anchored at `html` also match portal content in the same document, including caller-owned external portal containers.
53
+
54
+ This API does not theme native window controls or the desktop connection screen. It requires no public JavaScript API; use the attributes directly in your application's CSS.
@@ -1,3 +1,23 @@
1
1
  ---
2
2
  title: Configuration
3
3
  ---
4
+
5
+ ## Instance identity
6
+
7
+ ```ts
8
+ import { defineOvermuxConfig } from "overmux";
9
+ import { server } from "./server";
10
+
11
+ export default defineOvermuxConfig({
12
+ server,
13
+ instanceId: ({ port }) => `rich-work-${port}`,
14
+ });
15
+ ```
16
+
17
+ `instanceId` accepts a fixed string (for example `instanceId: "rich-work"`) or a function of `{ port }`. It is resolved once at server startup using the actual listening port. Every hostname serving this running instance reports the same ID. Changing the port intentionally changes a port-dependent ID.
18
+
19
+ `port` in `overmux.config.ts` must be an integer from `1` through `65535`. Internal listeners and tests may use `0` to ask the OS for a free port; this is not a user configuration value.
20
+
21
+ The default is the machine hostname followed by `-<port>`. IDs are canonical lowercase ASCII: 1-253 characters, start and end with a letter or digit, and may contain dots or hyphens internally. Explicit IDs are validated, never silently normalized. Set an explicit ID if the machine hostname does not meet these rules.
22
+
23
+ Distinct instances require distinct IDs. IDs are names, not credentials; authentication is still required. Deep links use `overmux://<instance-id>/<application-route>`. Their authority is only a lookup key, never a network address. Use the live runtime helpers or `overmux instance --json` to produce a prefix rather than guessing it from a browser URL.
@@ -1,3 +1,21 @@
1
1
  ---
2
2
  title: Server API
3
3
  ---
4
+
5
+ ## Instance identity in handlers
6
+
7
+ Operation, resource, and stream handler contexts expose the running instance:
8
+
9
+ ```ts
10
+ import { defineOperation } from "overmux";
11
+ import { z } from "zod";
12
+
13
+ export const paneLink = defineOperation({
14
+ input: z.object({ paneId: z.string() }),
15
+ output: z.string(),
16
+ handle: ({ paneId }, context) =>
17
+ `${context.instance.getDeepLinkPrefix()}/panes/${encodeURIComponent(paneId)}`,
18
+ });
19
+ ```
20
+
21
+ `context.instance.getInstanceId(): string` and `context.instance.getDeepLinkPrefix(): string` are synchronous and ready before handlers execute. They belong to this server runtime, not process-global state. The prefix is exactly `overmux://<instance-id>` without a trailing slash. Encode route segments once, not the whole assembled URL.
@@ -1,3 +1,39 @@
1
1
  ---
2
2
  title: Client API
3
3
  ---
4
+
5
+ ## Instance identity
6
+
7
+ ```tsx
8
+ import { createOvermuxHooks } from "overmux/client";
9
+ import type { serverConfig } from "./server";
10
+
11
+ const { useInstance } = createOvermuxHooks<typeof serverConfig>();
12
+
13
+ const PaneLink = ({ paneId }: { paneId: string }) => {
14
+ const instance = useInstance();
15
+ if (!instance) return null;
16
+ return <a href={`${instance.deepLinkPrefix}/panes/${encodeURIComponent(paneId)}`}>Open pane in desktop</a>;
17
+ };
18
+ ```
19
+
20
+ Use these hooks under the existing Overmux provider. `useInstance()` returns `{ instanceId, deepLinkPrefix } | undefined` and rerenders when authenticated discovery arrives or changes after reconnecting.
21
+
22
+ The same scoped `overmuxServerApi` object passed to command handlers for operations also provides `overmuxServerApi.getInstanceId()` and `overmuxServerApi.getDeepLinkPrefix()`. Both return `undefined` until the authenticated server announces its identity. The prefix is exactly `overmux://<instance-id>`, without a trailing slash. Browser addresses are not instance identities. Desktop hosts receive discovery through a narrow identity bridge automatically; application code does not need to forward it.
23
+
24
+ ## Clipboard
25
+
26
+ ```ts
27
+ import { readClipboardText, writeClipboardText } from "overmux/client";
28
+
29
+ await writeClipboardText("Text to copy");
30
+ const text = await readClipboardText();
31
+ ```
32
+
33
+ `readClipboardText(): Promise<string>` reads the system clipboard through `navigator.clipboard.readText()`. It never reads through the desktop bridge.
34
+
35
+ `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.
36
+
37
+ 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.
38
+
39
+ 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.
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: "overmux instance"
3
+ description: "Read the identity of a running Overmux server"
4
+ ---
5
+
6
+ # `overmux instance`
7
+
8
+ Print the live server's `instanceId` and `deepLinkPrefix`. Like `overmux call`, this discovers a local running server through its private same-user control channel. It queries the identity resolved by that running server, never reloads local configuration, and does not verify HTTP or WebSocket reachability.
9
+
10
+ ```sh
11
+ overmux instance
12
+ overmux instance --port 4242 --json
13
+ # {"instanceId":"rich-work-4242","deepLinkPrefix":"overmux://rich-work-4242"}
14
+
15
+ prefix=$(overmux instance --json | jq -r .deepLinkPrefix)
16
+ printf '%s/tmux/$71/@647/%%25647\n' "$prefix"
17
+ ```
18
+
19
+ | Flag | Description |
20
+ | --- | --- |
21
+ | `--port <value>, -p <value>` | Select a running local server when several are available |
22
+ | `--json` | Print only a JSON object with `instanceId` and `deepLinkPrefix` |
23
+
24
+ The prefix has no trailing slash. Append your application route, encoding each path segment once: pane ID `%647` becomes `%25647`. An instance ID is a lookup key, not a hostname, network address, or credential. The desktop associates it with URLs learned from authenticated server connections.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overmux",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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,16 @@ 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";
17
+ import { createInstanceCommand } from "./commands/instance";
15
18
  import { serveCommand } from "./commands/serve";
16
19
  import { detectCliEnvironment, type CliCommandContext } from "./environment";
17
20
 
18
21
  export const createCliApp = (process = globalThis.process) =>
19
22
  buildApplication<CliCommandContext>(
20
23
  buildRouteMap<string, CliCommandContext>({
21
- docs: { brief: "Serve, validate, or invoke Overmux operations" },
24
+ docs: { brief: "Initialize and manage Overmux applications" },
22
25
  routes: {
23
26
  ai: createAiRoute(process),
24
27
  auth: authRoute,
@@ -29,6 +32,9 @@ export const createCliApp = (process = globalThis.process) =>
29
32
  docsRoot: resolveInstalledDocsRoot(import.meta.url),
30
33
  output: process.stdout,
31
34
  }),
35
+ init: createInitCommand(process),
36
+ integration: integrationCommand,
37
+ instance: createInstanceCommand(process.stdout),
32
38
  serve: serveCommand,
33
39
  },
34
40
  }),
@@ -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
+ });