overmux 0.0.4 → 0.0.6

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 (151) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +16 -2
  3. package/dist/bin.js +140 -127
  4. package/dist/bin.js.map +1 -1
  5. package/dist/docs/000-index.md +2 -4
  6. package/dist/docs/100-introduction/200-how-overmux-works.md +126 -2
  7. package/dist/docs/100-introduction/{300-why-overmux.md → 300-why-i-built-overmux.md} +1 -1
  8. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  9. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  10. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  11. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  12. package/dist/docs/200-getting-started/500-set-up-overmux-with-packages.md +42 -0
  13. package/dist/docs/400-reference/100-project-structure.md +83 -0
  14. package/dist/docs/400-reference/200-configuration.md +257 -0
  15. package/dist/docs/400-reference/300-storage-locations.md +46 -0
  16. package/dist/docs/400-reference/400-authentication-and-security.md +37 -0
  17. package/dist/docs/400-reference/500-server/000-index.md +13 -0
  18. package/dist/docs/400-reference/500-server/100-resources.md +191 -0
  19. package/dist/docs/400-reference/500-server/200-operations.md +111 -0
  20. package/dist/docs/400-reference/500-server/300-streams.md +140 -0
  21. package/dist/docs/400-reference/500-server/400-notifications.md +32 -0
  22. package/dist/docs/400-reference/500-server/500-api.md +118 -0
  23. package/dist/docs/400-reference/600-client/000-index.md +9 -0
  24. package/dist/docs/400-reference/600-client/005-setting-up-your-ui.md +137 -0
  25. package/dist/docs/400-reference/600-client/007-deep-links.md +87 -0
  26. package/dist/docs/400-reference/600-client/010-commands.md +189 -0
  27. package/dist/docs/400-reference/600-client/020-shortcuts.md +171 -0
  28. package/dist/docs/400-reference/600-client/100-api.md +370 -0
  29. package/dist/docs/400-reference/600-client/200-theming.md +209 -0
  30. package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  31. package/dist/docs/400-reference/{400-cli → 700-cli}/050-init.md +2 -4
  32. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/100-serve.md +9 -4
  33. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/200-auth.md +1 -4
  34. package/dist/docs/400-reference/{400-cli → 700-cli}/300-call.md +1 -4
  35. package/dist/docs/400-reference/{400-cli → 700-cli}/350-instance.md +1 -4
  36. package/dist/docs/400-reference/700-cli/600-docs.md +128 -0
  37. package/dist/docs/400-reference/{400-cli → 700-cli}/700-desktop.md +1 -3
  38. package/dist/docs/500-hosted-pages.md +0 -1
  39. package/dist/exports/client.d.ts +5 -14
  40. package/dist/exports/client.d.ts.map +1 -1
  41. package/dist/exports/client.js +141 -46
  42. package/dist/exports/client.js.map +1 -1
  43. package/dist/exports/{index-DS70rKzo.d.ts → index-Cz3xkCa4.d.ts} +50 -36
  44. package/dist/exports/index-Cz3xkCa4.d.ts.map +1 -0
  45. package/dist/exports/index.d.ts +1 -1
  46. package/dist/exports/index.js +19 -5
  47. package/dist/exports/index.js.map +1 -1
  48. package/dist/exports/{notifications-av0FK0yZ.js → notifications-BFAD3QQl.js} +29 -4
  49. package/dist/exports/notifications-BFAD3QQl.js.map +1 -0
  50. package/dist/exports/server.d.ts +1 -44
  51. package/dist/exports/server.d.ts.map +1 -1
  52. package/dist/exports/server.js +9 -1061
  53. package/dist/exports/server.js.map +1 -1
  54. package/dist/internal/server/coordinator/server-child.js +47 -43
  55. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  56. package/docs/000-index.md +2 -4
  57. package/docs/100-introduction/200-how-overmux-works.md +126 -2
  58. package/docs/100-introduction/{300-why-overmux.md → 300-why-i-built-overmux.md} +1 -1
  59. package/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  60. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  61. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  62. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  63. package/docs/200-getting-started/500-set-up-overmux-with-packages.md +42 -0
  64. package/docs/400-reference/100-project-structure.md +83 -0
  65. package/docs/400-reference/200-configuration.md +257 -0
  66. package/docs/400-reference/300-storage-locations.md +46 -0
  67. package/docs/400-reference/400-authentication-and-security.md +37 -0
  68. package/docs/400-reference/500-server/000-index.md +13 -0
  69. package/docs/400-reference/500-server/100-resources.md +191 -0
  70. package/docs/400-reference/500-server/200-operations.md +111 -0
  71. package/docs/400-reference/500-server/300-streams.md +140 -0
  72. package/docs/400-reference/500-server/400-notifications.md +32 -0
  73. package/docs/400-reference/500-server/500-api.md +118 -0
  74. package/docs/400-reference/600-client/000-index.md +9 -0
  75. package/docs/400-reference/600-client/005-setting-up-your-ui.md +137 -0
  76. package/docs/400-reference/600-client/007-deep-links.md +87 -0
  77. package/docs/400-reference/600-client/010-commands.md +189 -0
  78. package/docs/400-reference/600-client/020-shortcuts.md +171 -0
  79. package/docs/400-reference/600-client/100-api.md +370 -0
  80. package/docs/400-reference/600-client/200-theming.md +209 -0
  81. package/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  82. package/docs/400-reference/{400-cli → 700-cli}/050-init.md +2 -4
  83. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/100-serve.md +9 -4
  84. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/200-auth.md +1 -4
  85. package/docs/400-reference/{400-cli → 700-cli}/300-call.md +1 -4
  86. package/docs/400-reference/{400-cli → 700-cli}/350-instance.md +1 -4
  87. package/docs/400-reference/700-cli/600-docs.md +128 -0
  88. package/docs/400-reference/{400-cli → 700-cli}/700-desktop.md +1 -3
  89. package/docs/500-hosted-pages.md +0 -1
  90. package/package.json +4 -3
  91. package/src/internal/cli/app.ts +3 -5
  92. package/src/internal/cli/commands/docs-ai-context.ts +33 -0
  93. package/src/internal/cli/commands/docs.ts +19 -2
  94. package/src/internal/cli/commands/init-template.ts +1 -1
  95. package/src/internal/cli/commands/serve.ts +3 -0
  96. package/src/internal/cli/login.ts +9 -9
  97. package/src/internal/client/client-definition.ts +6 -8
  98. package/src/internal/client/host/deep-link-navigation.ts +75 -0
  99. package/src/internal/client/host/overmux-host.tsx +9 -0
  100. package/src/internal/client/index.ts +0 -6
  101. package/src/internal/client/overmux-react.ts +71 -40
  102. package/src/internal/server/auth/auth-service.ts +2 -2
  103. package/src/internal/server/auth/instance-control.ts +65 -14
  104. package/src/internal/server/coordinator/ipc-protocol.ts +0 -1
  105. package/src/internal/server/runtime/create-runtime.ts +5 -3
  106. package/src/internal/server/runtime/runtime-instance.ts +5 -4
  107. package/src/internal/server/runtime/runtime-operations.ts +9 -3
  108. package/src/internal/server/runtime/runtime-resources.ts +12 -32
  109. package/src/internal/server/runtime/runtime-streams.ts +5 -6
  110. package/src/internal/server/server-logger.ts +5 -10
  111. package/src/internal/server/server-startup-options.ts +5 -10
  112. package/src/internal/server/start-application-server.ts +4 -7
  113. package/src/public/ai-context.ts +7 -29
  114. package/src/public/client.ts +0 -6
  115. package/src/public/config.ts +156 -31
  116. package/src/public/server.ts +1 -16
  117. package/dist/docs/100-introduction/100-what-is-overmux.md +0 -7
  118. package/dist/docs/300-fundamentals/100-project-structure.md +0 -23
  119. package/dist/docs/300-fundamentals/200-configuration.md +0 -3
  120. package/dist/docs/300-fundamentals/300-theming.md +0 -54
  121. package/dist/docs/300-fundamentals/400-server.md +0 -3
  122. package/dist/docs/300-fundamentals/500-client.md +0 -3
  123. package/dist/docs/300-fundamentals/600-operations.md +0 -3
  124. package/dist/docs/300-fundamentals/700-resources.md +0 -3
  125. package/dist/docs/300-fundamentals/800-streams.md +0 -3
  126. package/dist/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  127. package/dist/docs/400-reference/100-configuration.md +0 -23
  128. package/dist/docs/400-reference/200-server-api.md +0 -21
  129. package/dist/docs/400-reference/300-client-api.md +0 -39
  130. package/dist/docs/400-reference/400-cli/400-check.md +0 -20
  131. package/dist/docs/400-reference/400-cli/500-ai-context.md +0 -102
  132. package/dist/docs/400-reference/400-cli/600-docs.md +0 -23
  133. package/dist/exports/index-DS70rKzo.d.ts.map +0 -1
  134. package/dist/exports/notifications-av0FK0yZ.js.map +0 -1
  135. package/docs/100-introduction/100-what-is-overmux.md +0 -7
  136. package/docs/300-fundamentals/100-project-structure.md +0 -23
  137. package/docs/300-fundamentals/200-configuration.md +0 -3
  138. package/docs/300-fundamentals/300-theming.md +0 -54
  139. package/docs/300-fundamentals/400-server.md +0 -3
  140. package/docs/300-fundamentals/500-client.md +0 -3
  141. package/docs/300-fundamentals/600-operations.md +0 -3
  142. package/docs/300-fundamentals/700-resources.md +0 -3
  143. package/docs/300-fundamentals/800-streams.md +0 -3
  144. package/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  145. package/docs/400-reference/100-configuration.md +0 -23
  146. package/docs/400-reference/200-server-api.md +0 -21
  147. package/docs/400-reference/300-client-api.md +0 -39
  148. package/docs/400-reference/400-cli/400-check.md +0 -20
  149. package/docs/400-reference/400-cli/500-ai-context.md +0 -102
  150. package/docs/400-reference/400-cli/600-docs.md +0 -23
  151. package/src/internal/cli/commands/ai.ts +0 -44
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: "`overmux docs`"
3
+ ---
4
+
5
+ Overmux documentation and AI context commands
6
+
7
+ ## `overmux docs path`
8
+
9
+ `overmux docs path` prints the installed Overmux CLI’s absolute documentation path.
10
+
11
+ ```console
12
+ $ overmux docs path
13
+ /path/to/somewhere/node_modules/overmux/docs
14
+ ```
15
+
16
+ ## `overmux docs ai-context`
17
+
18
+ `overmux docs ai-context` prints `CLAUDE.md`/`AGENTS.md` guidance for coding agents.
19
+
20
+ ### Usage
21
+
22
+ ```text
23
+ overmux docs ai-context
24
+ ```
25
+
26
+ Prints the selected snippets shown in [available snippets](#available-snippets).
27
+
28
+ ### Set up your coding agent
29
+
30
+ Tell your agent how to load Overmux guidance using `overmux docs ai-context`.
31
+
32
+ <CodeBlockTabs defaultValue="agents">
33
+ <CodeBlockTabsList>
34
+ <CodeBlockTabsTrigger value="agents">AGENTS.md</CodeBlockTabsTrigger>
35
+ <CodeBlockTabsTrigger value="claude">CLAUDE.md</CodeBlockTabsTrigger>
36
+ </CodeBlockTabsList>
37
+ <CodeBlockTab value="agents">
38
+
39
+ ```bash
40
+ echo 'Run `overmux docs ai-context` for help configuring Overmux.' >> AGENTS.md
41
+ ```
42
+
43
+ </CodeBlockTab>
44
+ <CodeBlockTab value="claude">
45
+
46
+ ```bash
47
+ echo 'Run `overmux docs ai-context` for help configuring Overmux.' >> CLAUDE.md
48
+ ```
49
+
50
+ </CodeBlockTab>
51
+ </CodeBlockTabs>
52
+
53
+ ### Configure context snippets
54
+
55
+ Use `aiContextSnippets` in `overmux.config.ts` to select which snippets `overmux docs ai-context` prints.
56
+
57
+ ```ts
58
+ import {
59
+ coreAiContextSnippets,
60
+ defineOvermuxConfig,
61
+ } from "overmux";
62
+
63
+ export default defineOvermuxConfig({
64
+ aiContextSnippets: coreAiContextSnippets,
65
+ // ...
66
+ });
67
+ ```
68
+
69
+ When `aiContextSnippets` is omitted, Overmux uses `defaultAiContextSnippets`. Set it to an empty array to emit no context.
70
+
71
+ You can compose an explicit selection from the individual exports:
72
+
73
+ ```ts
74
+ import {
75
+ defineOvermuxConfig,
76
+ packageSourceSnippet,
77
+ techStackRecommendationsSnippet,
78
+ } from "overmux";
79
+
80
+ export default defineOvermuxConfig({
81
+ aiContextSnippets: [
82
+ packageSourceSnippet,
83
+ techStackRecommendationsSnippet,
84
+ ],
85
+ // ...
86
+ });
87
+ ```
88
+
89
+ ### Options
90
+
91
+ | Flag | Description | Default |
92
+ | --- | --- | --- |
93
+ | `--config <path>, -c <path>` | Configuration file | `$XDG_CONFIG_HOME/overmux/overmux.config.ts` |
94
+
95
+ ### Snippet presets
96
+
97
+ [//]: # (BEGIN GENERATED AI CONTEXT PRESETS)
98
+
99
+ | Export | Included snippets |
100
+ | --- | --- |
101
+ | `defaultAiContextSnippets` | `package-source`, `tech-stack-recommendations` |
102
+ | `coreAiContextSnippets` | `package-source` |
103
+
104
+ [//]: # (END GENERATED AI CONTEXT PRESETS)
105
+
106
+ ### Available snippets
107
+
108
+ [//]: # (BEGIN GENERATED AI CONTEXT)
109
+
110
+ #### `package-source`
111
+
112
+ ```text
113
+ Overmux ships with documentation and TypeScript source.
114
+
115
+ Run `overmux docs path` to find the installed documentation directory for the Overmux CLI. This prints a documentation directory, not source paths.
116
+
117
+ Inspect TypeScript source in installed packages, e.g. `node_modules/overmux/src` and `node_modules/@overmux/xterm/src`.
118
+
119
+ Inspect these files so guidance matches the versions used by the application.
120
+ ```
121
+
122
+ #### `tech-stack-recommendations`
123
+
124
+ ```text
125
+ Prefer pnpm as the package manager, TanStack Router for routing, shadcn/ui for UI components, and Zod for schemas and runtime validation. Follow the application's established stack when it already differs.
126
+ ```
127
+
128
+ [//]: # (END GENERATED AI CONTEXT)
@@ -1,9 +1,7 @@
1
1
  ---
2
- title: "overmux desktop"
2
+ title: "`overmux desktop`"
3
3
  ---
4
4
 
5
- # `overmux desktop`
6
-
7
5
  Install or upgrade the experimental macOS desktop preview. The desktop version is independent from the `overmux` npm package version.
8
6
 
9
7
  ## Install
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Hosted pages
3
- description: Link to Overmux-owned settings and logout pages.
4
3
  ---
5
4
 
6
5
  # Hosted pages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overmux",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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.3"
55
+ "@overmux/keybindings": "0.0.4"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@playwright/test": "1.58.0",
@@ -65,6 +65,7 @@
65
65
  "react-dom": "19.2.3",
66
66
  "vite": "8.2.2",
67
67
  "vitest": "4.1.10",
68
+ "@overmux/ai-context": "0.0.1",
68
69
  "@overmux/lib": "0.0.1",
69
70
  "@overmux/shared": "0.0.1",
70
71
  "@overmux/tsconfig": "0.0.1"
@@ -78,7 +79,7 @@
78
79
  "access": "public"
79
80
  },
80
81
  "scripts": {
81
- "build": "rm -rf dist && vite build && vite build --config vite.auth.config.ts && vp pack && node scripts/clean-declarations.ts && node scripts/add-client-styles.mjs && cp -R docs dist/docs",
82
+ "build": "node ../../../scripts/generate-ai-context-docs.ts && rm -rf dist && vite build && vite build --config vite.auth.config.ts && vp pack && node scripts/clean-declarations.ts && node scripts/add-client-styles.mjs && cp -R docs dist/docs",
82
83
  "dev": "pnpm run dev:cli",
83
84
  "dev:cli": "node --import jiti/register src/internal/cli/bin.ts serve",
84
85
  "serve": "node dist/bin.js serve",
@@ -6,12 +6,11 @@ import {
6
6
  text_en,
7
7
  } from "@stricli/core";
8
8
 
9
- import { createAiRoute } from "./commands/ai";
10
9
  import { authRoute } from "./commands/auth";
11
10
  import { checkCommand } from "./commands/check";
12
11
  import { callCommand } from "./commands/call";
13
12
  import { desktopRoute } from "./commands/desktop";
14
- import { createDocsCommand, resolveInstalledDocsRoot } from "./commands/docs";
13
+ import { createDocsRoute, resolveInstalledDocsRoot } from "./commands/docs";
15
14
  import { createInitCommand } from "./commands/init";
16
15
  import { integrationCommand } from "./commands/integration";
17
16
  import { createInstanceCommand } from "./commands/instance";
@@ -23,14 +22,13 @@ export const createCliApp = (process = globalThis.process) =>
23
22
  buildRouteMap<string, CliCommandContext>({
24
23
  docs: { brief: "Initialize and manage Overmux applications" },
25
24
  routes: {
26
- ai: createAiRoute(process),
27
25
  auth: authRoute,
28
26
  check: checkCommand,
29
27
  call: callCommand,
30
28
  desktop: desktopRoute,
31
- docs: createDocsCommand({
29
+ docs: createDocsRoute({
32
30
  docsRoot: resolveInstalledDocsRoot(import.meta.url),
33
- output: process.stdout,
31
+ process,
34
32
  }),
35
33
  init: createInitCommand(process),
36
34
  integration: integrationCommand,
@@ -0,0 +1,33 @@
1
+ import { loadOvermuxConfig } from "@overmux/shared/node";
2
+ import { buildCommand } from "@stricli/core";
3
+
4
+ import {
5
+ composeAiContext,
6
+ defaultAiContextSnippets,
7
+ } from "../../../public/ai-context";
8
+ import { getDefaultConfigPath } from "../../server/paths";
9
+
10
+ type AiContextFlags = { config: string };
11
+
12
+ export const createAiContextCommand = (process: NodeJS.Process) =>
13
+ buildCommand({
14
+ func: async (flags: AiContextFlags) => {
15
+ const { config } = await loadOvermuxConfig({ configPath: flags.config });
16
+ process.stdout.write(
17
+ composeAiContext(config.aiContextSnippets ?? defaultAiContextSnippets),
18
+ );
19
+ },
20
+ parameters: {
21
+ aliases: { c: "config" },
22
+ flags: {
23
+ config: {
24
+ brief: "Configuration file",
25
+ default: getDefaultConfigPath(),
26
+ kind: "parsed",
27
+ parse: String,
28
+ placeholder: "path",
29
+ },
30
+ },
31
+ },
32
+ docs: { brief: "Print the configured Overmux AI context" },
33
+ });
@@ -1,8 +1,10 @@
1
- import { buildCommand } from "@stricli/core";
1
+ import { buildCommand, buildRouteMap } from "@stricli/core";
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
+ import { createAiContextCommand } from "./docs-ai-context";
7
+
6
8
  type DocsOutput = { write: (text: string) => unknown };
7
9
 
8
10
  export const resolveInstalledDocsRoot = (moduleUrl: string) => {
@@ -16,7 +18,7 @@ export const resolveInstalledDocsRoot = (moduleUrl: string) => {
16
18
  return resolve(docsRoot);
17
19
  };
18
20
 
19
- export const createDocsCommand = ({
21
+ const createDocsPathCommand = ({
20
22
  docsRoot,
21
23
  output,
22
24
  }: {
@@ -32,3 +34,18 @@ export const createDocsCommand = ({
32
34
  brief: "Print the installed documentation directory",
33
35
  },
34
36
  });
37
+
38
+ export const createDocsRoute = ({
39
+ docsRoot,
40
+ process,
41
+ }: {
42
+ docsRoot: string;
43
+ process: NodeJS.Process;
44
+ }) =>
45
+ buildRouteMap({
46
+ docs: { brief: "Find documentation and print coding-agent guidance" },
47
+ routes: {
48
+ path: createDocsPathCommand({ docsRoot, output: process.stdout }),
49
+ "ai-context": createAiContextCommand(process),
50
+ },
51
+ });
@@ -53,7 +53,7 @@ export const createInitScaffold = ({
53
53
  }): Readonly<Record<string, string>> => ({
54
54
  ".gitignore": "node_modules\ndist\n",
55
55
  "AGENTS.md":
56
- "Immediately run `overmux ai context` for documentation about Overmux.\n",
56
+ "Immediately run `overmux docs ai-context` for documentation about Overmux.\n",
57
57
  "CLAUDE.md": "@AGENTS.md\n",
58
58
  ...(toolchain === "mise"
59
59
  ? {
@@ -184,6 +184,9 @@ const announceServeLogin = async ({
184
184
  return;
185
185
  }
186
186
  printLoginGrant({ ...(await issueLoginGrant(port)), output: process });
187
+ process.stdout.write(
188
+ `\nNeed another grant? Run \`overmux auth login --port ${port}\`.\n`,
189
+ );
187
190
  };
188
191
 
189
192
  export const runServe = async ({
@@ -19,11 +19,9 @@ export const printLoginFallback = (port: number, { stdout }: CliOutput) => {
19
19
  export const printLoginGrant = ({
20
20
  login,
21
21
  output,
22
- port,
23
22
  }: {
24
23
  login: LoginGrant;
25
24
  output: CliOutput;
26
- port: number;
27
25
  }) => {
28
26
  const expiresIn = Math.max(
29
27
  0,
@@ -31,16 +29,18 @@ export const printLoginGrant = ({
31
29
  );
32
30
  output.stdout.write(
33
31
  [
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}\`.`,
32
+ `Code: ${login.code} (enter on the login page)`,
33
+ "",
34
+ ...(login.urls.length === 1
35
+ ? [`Login URL: ${login.urls[0]}`]
36
+ : ["Login URLs:", ...login.urls.map((url) => ` ${url}`)]),
37
+ "",
38
+ `Expiry in ${expiresIn} minutes (at ${login.expiresAt}).`,
39
+ "",
40
40
  "",
41
41
  ].join("\n"),
42
42
  );
43
43
  output.stderr.write(
44
- "The login URL is a temporary credential; avoid sharing terminal output or screenshots.\n",
44
+ `Keep the code and link${login.urls.length === 1 ? "" : "s"} private.\n`,
45
45
  );
46
46
  };
@@ -6,11 +6,6 @@ import {
6
6
  matchesKeyBinding,
7
7
  type KeyBinding,
8
8
  } from "@overmux/keybindings";
9
- export {
10
- formatKeyBinding,
11
- keyBindingSchema,
12
- matchesKeyBinding,
13
- } from "@overmux/keybindings";
14
9
  export type { KeyBinding } from "@overmux/keybindings";
15
10
  import { z } from "zod";
16
11
 
@@ -28,18 +23,18 @@ export type ChordPrefix = {
28
23
  unmatched: "replay-to-focused-input";
29
24
  };
30
25
 
31
- export const shortcutBindingSchema = z.union([
26
+ const shortcutBindingSchema = z.union([
32
27
  keyBindingSchema,
33
28
  z.array(keyBindingSchema).min(2),
34
29
  ]);
35
- export const commandBindingSchema = z.union([
30
+ const commandBindingSchema = z.union([
36
31
  shortcutBindingSchema,
37
32
  z.object({
38
33
  binding: shortcutBindingSchema,
39
34
  when: z.object({ media: z.string().trim().min(1) }),
40
35
  }),
41
36
  ]);
42
- export const chordPrefixSchema = z.object({
37
+ const chordPrefixSchema = z.object({
43
38
  binding: keyBindingSchema,
44
39
  unmatched: z.literal("replay-to-focused-input"),
45
40
  });
@@ -132,6 +127,9 @@ export type ClientAppDefinition<
132
127
  chordPrefixes?: readonly ChordPrefix[];
133
128
  commands: TCommands;
134
129
  component: ComponentType;
130
+ // Receives validated same-instance deep-link routes, including query and fragment.
131
+ // Defaults to document navigation on the current origin; supply your router to avoid a reload.
132
+ navigate?: (route: string) => unknown;
135
133
  shortcutOverrides?: Partial<
136
134
  Record<Extract<keyof TCommands, string>, readonly CommandBinding[]>
137
135
  >;
@@ -0,0 +1,75 @@
1
+ import { parseDeepLink } from "@overmux/shared";
2
+ import type { ClientTransport } from "../transport";
3
+ import type {} from "./desktop-host";
4
+
5
+ const defaultNavigate = (route: string) => window.location.assign(route);
6
+
7
+ /**
8
+ * Intercepts overmux:// anchor activation inside the running app, including the
9
+ * temporary anchors created by xterm's default link handler. Without this,
10
+ * browser/PWA clicks dispatch the custom protocol to the OS, which fails in
11
+ * Android PWAs where it is not possible to install an Overmux protocol handler.
12
+ *
13
+ * Validates the original URL and compares its instance ID with authenticated
14
+ * runtime discovery. For same-instance links, preventDefault stops OS dispatch
15
+ * and navigate receives only the local path, query, and fragment. By default,
16
+ * this loads that route on the current origin; a supplied router callback avoids
17
+ * a document reload. Instance IDs are never treated as network hostnames.
18
+ *
19
+ * Valid cross-instance links remain Desktop's responsibility. Browsers/PWAs
20
+ * block unknown or different instances; all hosts block invalid links. Ordinary
21
+ * links and clicks already cancelled by application code are left untouched.
22
+ * Returns cleanup for the document listeners when the runtime unmounts.
23
+ */
24
+ export const installDeepLinkNavigation = ({
25
+ getInstance,
26
+ navigate = defaultNavigate,
27
+ }: {
28
+ getInstance: ClientTransport["getInstance"];
29
+ navigate?: (route: string) => unknown;
30
+ }) => {
31
+ const onClick = (event: MouseEvent) => {
32
+ if (event.defaultPrevented || event.button > 1) {
33
+ return;
34
+ }
35
+ const anchor = event
36
+ .composedPath()
37
+ .find(
38
+ (target): target is HTMLAnchorElement =>
39
+ target instanceof HTMLAnchorElement,
40
+ );
41
+ if (anchor?.protocol !== "overmux:") {
42
+ return;
43
+ }
44
+ let link;
45
+ try {
46
+ // Inspect the original attribute before URL normalization can hide unsafe text.
47
+ link = parseDeepLink(anchor.getAttribute("href") ?? "");
48
+ } catch {
49
+ event.preventDefault();
50
+ console.warn("Cannot open an invalid Overmux deep link.");
51
+ return;
52
+ }
53
+ if (link.instanceId !== getInstance()?.instanceId) {
54
+ // Desktop owns registry lookup and cross-instance confirmation. Browsers must
55
+ // never interpret an instance ID as a hostname or assume it names this server.
56
+ if (window.overmuxHost?.version !== 1) {
57
+ event.preventDefault();
58
+ console.warn(
59
+ "Cannot open an Overmux link to an unknown or different instance in this browser.",
60
+ );
61
+ }
62
+ return;
63
+ }
64
+ event.preventDefault();
65
+ navigate(link.route);
66
+ };
67
+ // Bubble after application handlers so preventDefault remains an explicit opt-out.
68
+ // Read identity on activation, not installation: discovery and reconnects can change it.
69
+ document.addEventListener("click", onClick);
70
+ document.addEventListener("auxclick", onClick);
71
+ return () => {
72
+ document.removeEventListener("click", onClick);
73
+ document.removeEventListener("auxclick", onClick);
74
+ };
75
+ };
@@ -22,6 +22,7 @@ import { createOvermuxServerApi } from "../browser-api";
22
22
  import { installBrowserLogForwarding } from "./browser-log-forwarding";
23
23
  import { installNotificationForwarding } from "./notification-forwarding";
24
24
  import { installInstanceForwarding } from "./instance-forwarding";
25
+ import { installDeepLinkNavigation } from "./deep-link-navigation";
25
26
  import type { ClientAppDefinition } from "../client-definition";
26
27
  import { type RegisteredCommand, RuntimeContext } from "../commands";
27
28
  import { ShortcutHost } from "../shortcuts";
@@ -137,6 +138,14 @@ const OvermuxRuntime = ({
137
138
  );
138
139
  useEffect(() => installNotificationForwarding(transport), [transport]);
139
140
  useEffect(() => installInstanceForwarding(transport), [transport]);
141
+ useEffect(
142
+ () =>
143
+ installDeepLinkNavigation({
144
+ getInstance: transport.getInstance,
145
+ navigate: definition.navigate,
146
+ }),
147
+ [transport, definition.navigate],
148
+ );
140
149
  const refreshCommands = useCallback(
141
150
  () => setRegistrationRevision((revision) => revision + 1),
142
151
  [],
@@ -19,15 +19,9 @@ export type {
19
19
  OvermuxThemeToken,
20
20
  } from "./theme-scope";
21
21
  export {
22
- chordPrefixSchema,
23
- commandBindingSchema,
24
22
  defineCommandRegistry,
25
23
  defineOvermuxClient,
26
- formatKeyBinding,
27
24
  formatShortcutBinding,
28
- keyBindingSchema,
29
- matchesKeyBinding,
30
- shortcutBindingSchema,
31
25
  } from "./client-definition";
32
26
  export { skipToken, useCommand, useCommands } from "./commands";
33
27
  export { useShortcutInputTarget } from "./shortcuts";
@@ -99,6 +99,10 @@ export type ResourceResult<T> =
99
99
  };
100
100
 
101
101
  export type StreamResult<TClientMessage, TServerMessage> = {
102
+ // Identifies one server-confirmed stream lifetime, not this hook or its stable methods.
103
+ // Undefined until stream-opened; reconnects and effect restarts receive a fresh identity.
104
+ // Replacement renders report opening, never readiness inherited from the old stream.
105
+ connectionId: symbol | undefined;
102
106
  close: () => void;
103
107
  error?: Error;
104
108
  send: (message: TClientMessage) => boolean;
@@ -200,67 +204,94 @@ const useStreamById = <TClientMessage, TServerMessage>(
200
204
  ): StreamResult<TClientMessage, TServerMessage> => {
201
205
  const { manifest, overmuxServerApi } = useRuntime();
202
206
  const streamAvailable = manifest.streams.includes(id);
207
+ const canonicalInput = useCanonicalInput(input);
208
+ // This scope owns the stream-opening effect. Fast Refresh invalidates the memo
209
+ // and restarts that effect; the previous scope's open state is never exposed as
210
+ // readiness for the replacement, even before its passive effect has run.
211
+ const scope = useMemo(
212
+ () => ({ canonicalInput, id, overmuxServerApi, streamAvailable }),
213
+ [canonicalInput, id, overmuxServerApi, streamAvailable],
214
+ );
203
215
  const [state, setState] = useState<{
216
+ scope: typeof scope;
217
+ connectionId?: symbol;
204
218
  error?: Error;
205
219
  status: "closed" | "open" | "opening";
206
- }>({ status: "opening" });
207
- const listeners = useMemo(
220
+ }>({ scope, status: "opening" });
221
+ // Unlike memoized callbacks, state survives Fast Refresh. Existing consumers
222
+ // keep their subscriptions and methods while the real stream lifetime changes.
223
+ const [listeners] = useState(
208
224
  () => new Set<(message: TServerMessage) => void>(),
209
- [],
210
225
  );
211
- const canonicalInput = useCanonicalInput(input);
212
226
  const streamRef = useRef<
213
227
  ReturnType<typeof overmuxServerApi.openStream> | undefined
214
228
  >(undefined);
215
229
  useEffect(() => {
216
230
  streamRef.current?.close();
217
231
  streamRef.current = undefined;
218
- if (!streamAvailable) {
219
- setState({ status: "closed" });
232
+ if (!scope.streamAvailable) {
233
+ setState({ scope, status: "closed" });
220
234
  return;
221
235
  }
222
- setState({ status: "opening" });
223
- const stream = overmuxServerApi.openStream({
224
- id,
225
- input: canonicalInput,
226
- onClose: () => setState({ status: "closed" }),
227
- onError: (error) => setState({ error, status: "closed" }),
228
- onOpen: () => setState({ status: "open" }),
236
+ setState({ scope, status: "opening" });
237
+ let active = true;
238
+ const stream = scope.overmuxServerApi.openStream({
239
+ id: scope.id,
240
+ input: scope.canonicalInput,
241
+ onClose: () => active && setState({ scope, status: "closed" }),
242
+ onError: (error) =>
243
+ active && setState({ scope, error, status: "closed" }),
244
+ // The transport reuses its handle on reconnect, but acknowledges a new server stream.
245
+ onOpen: () =>
246
+ active &&
247
+ setState({ scope, connectionId: Symbol(scope.id), status: "open" }),
229
248
  onMessage: (message) =>
249
+ active &&
230
250
  listeners.forEach((listener) => listener(message as TServerMessage)),
231
251
  });
232
- streamRef.current = stream;
233
- return () => {
234
- stream.close();
235
- if (streamRef.current === stream) {
236
- streamRef.current = undefined;
237
- }
252
+ const connection = {
253
+ close: () => {
254
+ if (!active) {
255
+ return;
256
+ }
257
+ // Old callbacks must not overwrite the stream created by an effect restart (including HMR).
258
+ active = false;
259
+ if (streamRef.current === connection) {
260
+ streamRef.current = undefined;
261
+ }
262
+ setState({ scope, status: "closed" });
263
+ stream.close();
264
+ },
265
+ send: stream.send,
238
266
  };
239
- }, [canonicalInput, id, listeners, overmuxServerApi, streamAvailable]);
240
- const close = useCallback(() => {
241
- streamRef.current?.close();
242
- streamRef.current = undefined;
243
- setState({ status: "closed" });
244
- }, []);
245
- const send = useCallback(
246
- (message: TClientMessage) => streamRef.current?.send(message) ?? false,
247
- [],
248
- );
249
- const subscribe = useCallback(
250
- (listener: (message: TServerMessage) => void) => {
267
+ streamRef.current = connection;
268
+ return connection.close;
269
+ }, [listeners, scope]);
270
+ const [methods] = useState(() => ({
271
+ close: () => streamRef.current?.close(),
272
+ send: (message: TClientMessage) =>
273
+ streamRef.current?.send(message) ?? false,
274
+ subscribe: (listener: (message: TServerMessage) => void) => {
251
275
  listeners.add(listener);
252
276
  return () => listeners.delete(listener);
253
277
  },
254
- [listeners],
255
- );
278
+ }));
279
+ if (!streamAvailable) {
280
+ return {
281
+ ...methods,
282
+ connectionId: undefined,
283
+ error: new Error(`Stream is not available: ${id}`),
284
+ status: "closed",
285
+ };
286
+ }
287
+ if (state.scope !== scope) {
288
+ return { ...methods, connectionId: undefined, status: "opening" };
289
+ }
256
290
  return {
257
- close,
258
- error: streamAvailable
259
- ? state.error
260
- : new Error(`Stream is not available: ${id}`),
261
- send,
262
- status: streamAvailable ? state.status : "closed",
263
- subscribe,
291
+ ...methods,
292
+ connectionId: state.connectionId,
293
+ error: state.error,
294
+ status: state.status,
264
295
  };
265
296
  };
266
297
 
@@ -129,7 +129,7 @@ const randomCode = () => {
129
129
  const durationMilliseconds = (
130
130
  duration: Exclude<
131
131
  AuthConfigDefinition["sessionLifetime"],
132
- "never" | undefined
132
+ "forever" | undefined
133
133
  >,
134
134
  ) => {
135
135
  const milliseconds = durationToMilliseconds(
@@ -357,7 +357,7 @@ export const createAuthService = ({
357
357
  const token = randomToken();
358
358
  const createdAt = new Date(now()).toISOString();
359
359
  const configuredLifetime =
360
- config.sessionLifetime && config.sessionLifetime !== "never"
360
+ config.sessionLifetime && config.sessionLifetime !== "forever"
361
361
  ? durationMilliseconds(config.sessionLifetime)
362
362
  : undefined;
363
363
  const expiresIn = lifetimeMs ?? configuredLifetime;