overmux 0.0.3 → 0.0.5

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 (159) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +2 -2
  3. package/dist/auth/.vite/manifest.json +2 -2
  4. package/dist/auth/assets/{index-Bx8JhwVi.css → index-BM9gETvx.css} +1 -1
  5. package/dist/auth/assets/{index-CHIE7qYj.js → index-DabAfcs1.js} +1 -1
  6. package/dist/auth/index.html +2 -2
  7. package/dist/bin.js +219 -126
  8. package/dist/bin.js.map +1 -1
  9. package/dist/docs/000-index.md +2 -4
  10. package/dist/docs/100-introduction/200-how-overmux-works.md +126 -2
  11. package/{docs/100-introduction/300-why-overmux.md → dist/docs/100-introduction/300-why-i-built-overmux.md} +1 -1
  12. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  13. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  14. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  15. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  16. package/dist/docs/400-reference/100-project-structure.md +83 -0
  17. package/dist/docs/400-reference/200-configuration.md +257 -0
  18. package/dist/docs/400-reference/300-storage-locations.md +46 -0
  19. package/dist/docs/400-reference/400-authentication-and-security.md +37 -0
  20. package/dist/docs/400-reference/500-server/000-index.md +13 -0
  21. package/dist/docs/400-reference/500-server/100-resources.md +191 -0
  22. package/dist/docs/400-reference/500-server/200-operations.md +111 -0
  23. package/dist/docs/400-reference/500-server/300-streams.md +140 -0
  24. package/dist/docs/400-reference/500-server/400-notifications.md +32 -0
  25. package/dist/docs/400-reference/500-server/500-api.md +118 -0
  26. package/dist/docs/400-reference/600-client/000-index.md +9 -0
  27. package/dist/docs/400-reference/600-client/100-api.md +370 -0
  28. package/dist/docs/400-reference/600-client/200-theming.md +94 -0
  29. package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  30. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/050-init.md +2 -4
  31. package/dist/docs/400-reference/{400-cli → 700-cli}/100-serve.md +9 -4
  32. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/200-auth.md +1 -4
  33. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/300-call.md +1 -4
  34. package/dist/docs/400-reference/700-cli/350-instance.md +21 -0
  35. package/dist/docs/400-reference/700-cli/600-docs.md +128 -0
  36. package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/700-desktop.md +1 -3
  37. package/dist/docs/500-hosted-pages.md +0 -1
  38. package/dist/exports/client.d.ts +19 -15
  39. package/dist/exports/client.d.ts.map +1 -1
  40. package/dist/exports/client.js +240 -58
  41. package/dist/exports/client.js.map +1 -1
  42. package/dist/exports/{index-D3_Dpy_T.d.ts → index-Cz3xkCa4.d.ts} +62 -36
  43. package/dist/exports/index-Cz3xkCa4.d.ts.map +1 -0
  44. package/dist/exports/index.d.ts +2 -2
  45. package/dist/exports/index.js +19 -5
  46. package/dist/exports/index.js.map +1 -1
  47. package/dist/exports/{notifications-kDp16bU_.js → notifications-BFAD3QQl.js} +34 -4
  48. package/dist/exports/notifications-BFAD3QQl.js.map +1 -0
  49. package/dist/exports/server.d.ts +1 -44
  50. package/dist/exports/server.d.ts.map +1 -1
  51. package/dist/exports/server.js +8 -1018
  52. package/dist/exports/server.js.map +1 -1
  53. package/dist/internal/server/coordinator/server-child.js +153 -85
  54. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  55. package/docs/000-index.md +2 -4
  56. package/docs/100-introduction/200-how-overmux-works.md +126 -2
  57. package/{dist/docs/100-introduction/300-why-overmux.md → docs/100-introduction/300-why-i-built-overmux.md} +1 -1
  58. package/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
  59. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
  60. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
  61. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
  62. package/docs/400-reference/100-project-structure.md +83 -0
  63. package/docs/400-reference/200-configuration.md +257 -0
  64. package/docs/400-reference/300-storage-locations.md +46 -0
  65. package/docs/400-reference/400-authentication-and-security.md +37 -0
  66. package/docs/400-reference/500-server/000-index.md +13 -0
  67. package/docs/400-reference/500-server/100-resources.md +191 -0
  68. package/docs/400-reference/500-server/200-operations.md +111 -0
  69. package/docs/400-reference/500-server/300-streams.md +140 -0
  70. package/docs/400-reference/500-server/400-notifications.md +32 -0
  71. package/docs/400-reference/500-server/500-api.md +118 -0
  72. package/docs/400-reference/600-client/000-index.md +9 -0
  73. package/docs/400-reference/600-client/100-api.md +370 -0
  74. package/docs/400-reference/600-client/200-theming.md +94 -0
  75. package/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
  76. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/050-init.md +2 -4
  77. package/docs/400-reference/{400-cli → 700-cli}/100-serve.md +9 -4
  78. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/200-auth.md +1 -4
  79. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/300-call.md +1 -4
  80. package/docs/400-reference/700-cli/350-instance.md +21 -0
  81. package/docs/400-reference/700-cli/600-docs.md +128 -0
  82. package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/700-desktop.md +1 -3
  83. package/docs/500-hosted-pages.md +0 -1
  84. package/package.json +3 -2
  85. package/src/internal/cli/app.ts +5 -5
  86. package/src/internal/cli/commands/docs-ai-context.ts +33 -0
  87. package/src/internal/cli/commands/docs.ts +19 -2
  88. package/src/internal/cli/commands/init-template.ts +1 -1
  89. package/src/internal/cli/commands/instance.ts +58 -0
  90. package/src/internal/cli/commands/serve.ts +3 -0
  91. package/src/internal/cli/login.ts +9 -9
  92. package/src/internal/cli/parse-port.ts +2 -1
  93. package/src/internal/client/auth/auth-shell.css +17 -1
  94. package/src/internal/client/auth/auth-shell.tsx +2 -11
  95. package/src/internal/client/auth/login-code-input.tsx +140 -0
  96. package/src/internal/client/browser-api.ts +17 -3
  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/desktop-host.ts +3 -0
  100. package/src/internal/client/host/environment.ts +67 -0
  101. package/src/internal/client/host/instance-forwarding.ts +21 -0
  102. package/src/internal/client/host/overmux-host.tsx +17 -0
  103. package/src/internal/client/index.ts +0 -6
  104. package/src/internal/client/overmux-react.ts +95 -42
  105. package/src/internal/client/websocket.ts +32 -12
  106. package/src/internal/server/auth/auth-service.ts +2 -2
  107. package/src/internal/server/auth/instance-control.ts +89 -17
  108. package/src/internal/server/coordinator/ipc-protocol.ts +0 -1
  109. package/src/internal/server/runtime/create-runtime.ts +15 -2
  110. package/src/internal/server/runtime/runtime-instance.ts +47 -0
  111. package/src/internal/server/runtime/runtime-operations.ts +9 -3
  112. package/src/internal/server/runtime/runtime-resources.ts +15 -32
  113. package/src/internal/server/runtime/runtime-streams.ts +5 -6
  114. package/src/internal/server/server-logger.ts +5 -10
  115. package/src/internal/server/server-startup-options.ts +14 -14
  116. package/src/internal/server/start-application-server.ts +24 -19
  117. package/src/internal/server/websocket-server.ts +7 -0
  118. package/src/internal/shared/index.ts +2 -0
  119. package/src/internal/shared/protocol.ts +10 -1
  120. package/src/public/ai-context.ts +7 -29
  121. package/src/public/client.ts +1 -6
  122. package/src/public/config.ts +163 -31
  123. package/src/public/index.ts +2 -0
  124. package/src/public/server.ts +1 -16
  125. package/dist/docs/100-introduction/100-what-is-overmux.md +0 -7
  126. package/dist/docs/300-fundamentals/100-project-structure.md +0 -23
  127. package/dist/docs/300-fundamentals/200-configuration.md +0 -3
  128. package/dist/docs/300-fundamentals/300-theming.md +0 -3
  129. package/dist/docs/300-fundamentals/400-server.md +0 -3
  130. package/dist/docs/300-fundamentals/500-client.md +0 -3
  131. package/dist/docs/300-fundamentals/600-operations.md +0 -3
  132. package/dist/docs/300-fundamentals/700-resources.md +0 -3
  133. package/dist/docs/300-fundamentals/800-streams.md +0 -3
  134. package/dist/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  135. package/dist/docs/400-reference/100-configuration.md +0 -3
  136. package/dist/docs/400-reference/200-server-api.md +0 -3
  137. package/dist/docs/400-reference/300-client-api.md +0 -20
  138. package/dist/docs/400-reference/400-cli/400-check.md +0 -20
  139. package/dist/docs/400-reference/400-cli/500-ai-context.md +0 -102
  140. package/dist/docs/400-reference/400-cli/600-docs.md +0 -23
  141. package/dist/exports/index-D3_Dpy_T.d.ts.map +0 -1
  142. package/dist/exports/notifications-kDp16bU_.js.map +0 -1
  143. package/docs/100-introduction/100-what-is-overmux.md +0 -7
  144. package/docs/300-fundamentals/100-project-structure.md +0 -23
  145. package/docs/300-fundamentals/200-configuration.md +0 -3
  146. package/docs/300-fundamentals/300-theming.md +0 -3
  147. package/docs/300-fundamentals/400-server.md +0 -3
  148. package/docs/300-fundamentals/500-client.md +0 -3
  149. package/docs/300-fundamentals/600-operations.md +0 -3
  150. package/docs/300-fundamentals/700-resources.md +0 -3
  151. package/docs/300-fundamentals/800-streams.md +0 -3
  152. package/docs/300-fundamentals/900-authentication-and-security.md +0 -3
  153. package/docs/400-reference/100-configuration.md +0 -3
  154. package/docs/400-reference/200-server-api.md +0 -3
  155. package/docs/400-reference/300-client-api.md +0 -20
  156. package/docs/400-reference/400-cli/400-check.md +0 -20
  157. package/docs/400-reference/400-cli/500-ai-context.md +0 -102
  158. package/docs/400-reference/400-cli/600-docs.md +0 -23
  159. 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.3",
3
+ "version": "0.0.5",
4
4
  "homepage": "https://github.com/richardgill/overmux",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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,14 +6,14 @@ 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";
16
+ import { createInstanceCommand } from "./commands/instance";
17
17
  import { serveCommand } from "./commands/serve";
18
18
  import { detectCliEnvironment, type CliCommandContext } from "./environment";
19
19
 
@@ -22,17 +22,17 @@ export const createCliApp = (process = globalThis.process) =>
22
22
  buildRouteMap<string, CliCommandContext>({
23
23
  docs: { brief: "Initialize and manage Overmux applications" },
24
24
  routes: {
25
- ai: createAiRoute(process),
26
25
  auth: authRoute,
27
26
  check: checkCommand,
28
27
  call: callCommand,
29
28
  desktop: desktopRoute,
30
- docs: createDocsCommand({
29
+ docs: createDocsRoute({
31
30
  docsRoot: resolveInstalledDocsRoot(import.meta.url),
32
- output: process.stdout,
31
+ process,
33
32
  }),
34
33
  init: createInitCommand(process),
35
34
  integration: integrationCommand,
35
+ instance: createInstanceCommand(process.stdout),
36
36
  serve: serveCommand,
37
37
  },
38
38
  }),
@@ -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
  ? {
@@ -0,0 +1,58 @@
1
+ import { buildCommand } from "@stricli/core";
2
+
3
+ import {
4
+ discoverInstance,
5
+ sendControlRequest,
6
+ } from "../../server/auth/instance-control";
7
+ import { createInstanceIdentity } from "../../shared/index";
8
+ import { parsePort } from "../parse-port";
9
+
10
+ type InstanceFlags = { json?: boolean; port?: string };
11
+
12
+ // Read the already-resolved identity through the private same-user control socket.
13
+ // Config may have changed since startup, and re-evaluating it can repeat side effects.
14
+ // The running server resolved its identity once using the actual bound port.
15
+ const runInstance = async (
16
+ flags: InstanceFlags,
17
+ output: Pick<NodeJS.WriteStream, "write">,
18
+ ) => {
19
+ const port = flags.port === undefined ? undefined : parsePort(flags.port);
20
+ if (port instanceof Error) {
21
+ throw port;
22
+ }
23
+ const instance = await discoverInstance(port);
24
+ const response = await sendControlRequest(instance, {
25
+ type: "get-instance",
26
+ });
27
+ const identity = createInstanceIdentity(response.instanceId);
28
+ output.write(
29
+ flags.json
30
+ ? `${JSON.stringify(identity)}\n`
31
+ : `Instance ID: ${identity.instanceId}\nDeep-link prefix: ${identity.deepLinkPrefix}\n`,
32
+ );
33
+ };
34
+
35
+ export const createInstanceCommand = (
36
+ output: Pick<NodeJS.WriteStream, "write">,
37
+ ) =>
38
+ buildCommand({
39
+ func: (flags: InstanceFlags) => runInstance(flags, output),
40
+ parameters: {
41
+ aliases: { p: "port" },
42
+ flags: {
43
+ json: {
44
+ brief: "Print machine-readable JSON",
45
+ kind: "boolean",
46
+ optional: true,
47
+ },
48
+ port: {
49
+ brief: "Running local server port",
50
+ kind: "parsed",
51
+ optional: true,
52
+ parse: String,
53
+ },
54
+ },
55
+ positional: { kind: "tuple", parameters: [] },
56
+ },
57
+ docs: { brief: "Print the identity of a running Overmux server" },
58
+ });
@@ -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
  };
@@ -2,7 +2,8 @@ import { serverConfigRuntimeSchema } from "@overmux/shared/node";
2
2
 
3
3
  export const parsePort = (value: string): Error | number => {
4
4
  const port = Number(value);
5
- return serverConfigRuntimeSchema.shape.port.safeParse(port).success
5
+ return port > 0 &&
6
+ serverConfigRuntimeSchema.shape.port.safeParse(port).success
6
7
  ? port
7
8
  : new Error(`Invalid port: ${value}`);
8
9
  };
@@ -46,10 +46,26 @@ body {
46
46
  border: 1px solid #666;
47
47
  background: #111;
48
48
  color: inherit;
49
- letter-spacing: 0.08em;
49
+ font-family: ui-monospace, monospace;
50
+ font-size: 1.25rem;
51
+ letter-spacing: 0.12em;
50
52
  text-transform: uppercase;
51
53
  }
52
54
 
55
+ [data-om-auth-shell] input[aria-invalid="true"] {
56
+ border-color: #ff9b9b;
57
+ }
58
+
59
+ #auth-code-error {
60
+ margin: 0;
61
+ color: #ff9b9b;
62
+ font-size: 0.875rem;
63
+ }
64
+
65
+ #auth-code-error:not(:empty) {
66
+ margin-top: 0.5rem;
67
+ }
68
+
53
69
  [data-om-auth-shell] button {
54
70
  margin-top: 1rem;
55
71
  border: 0;
@@ -15,6 +15,7 @@ import {
15
15
  // React Query is intentionally excluded to keep the public pre-authentication shell lightweight.
16
16
  // Its mutations may still run concurrently, so it would not replace latest-request cancellation.
17
17
  import { getAuthState, login, type LoginInput } from "./auth-api";
18
+ import { LoginCodeInput } from "./login-code-input";
18
19
  import "./auth-shell.css";
19
20
 
20
21
  type AuthStatus =
@@ -147,17 +148,7 @@ export const AuthShell = ({ ticket }: AuthShellProps) => {
147
148
  </p>
148
149
  <form aria-busy={pending} onSubmit={submitCode}>
149
150
  <label htmlFor="auth-code">Login code</label>
150
- <input
151
- aria-describedby="auth-status"
152
- autoCapitalize="characters"
153
- autoComplete="one-time-code"
154
- disabled={pending}
155
- id="auth-code"
156
- name="code"
157
- pattern="[A-Za-z0-9]{4}-[A-Za-z0-9]{4}"
158
- required
159
- spellCheck={false}
160
- />
151
+ <LoginCodeInput disabled={pending} />
161
152
  <button disabled={pending} type="submit">
162
153
  Unlock Overmux
163
154
  </button>
@@ -0,0 +1,140 @@
1
+ import { useState, type ClipboardEvent, type KeyboardEvent } from "react";
2
+
3
+ type LoginCodeInputProps = {
4
+ disabled: boolean;
5
+ };
6
+
7
+ const maxCodeLength = 8;
8
+ const codeError = "Enter 8 letters or numbers, with no spaces inside the code.";
9
+
10
+ const formatCode = (value: string) => {
11
+ const trimmed = value.trim();
12
+ const ungrouped = trimmed.replace("-", "");
13
+ if (!/^[A-Za-z0-9]*$/.test(ungrouped)) {
14
+ return trimmed;
15
+ }
16
+ const characters = ungrouped.slice(0, maxCodeLength).toUpperCase();
17
+ return characters.length < 4
18
+ ? characters
19
+ : `${characters.slice(0, 4)}-${characters.slice(4)}`;
20
+ };
21
+
22
+ const formatInput = (input: HTMLInputElement) => {
23
+ const { value, selectionStart, selectionEnd, selectionDirection } = input;
24
+ const formatted = formatCode(value);
25
+ if (formatted === value) {
26
+ return;
27
+ }
28
+ // Formatting the prefix maps the native selection past an inserted dash without
29
+ // moving edits in the middle of the code to the end of the field.
30
+ const start = formatCode(
31
+ value.slice(0, selectionStart ?? value.length),
32
+ ).length;
33
+ const end = formatCode(value.slice(0, selectionEnd ?? value.length)).length;
34
+ input.value = formatted;
35
+ input.setSelectionRange(start, end, selectionDirection ?? undefined);
36
+ };
37
+
38
+ const pasteCode = (event: ClipboardEvent<HTMLInputElement>) => {
39
+ event.preventDefault();
40
+ const input = event.currentTarget;
41
+ // Text inputs silently remove line breaks; preserve internal ones as invalid
42
+ // spaces rather than accidentally accepting a code with embedded whitespace.
43
+ const pasted = event.clipboardData
44
+ .getData("text/plain")
45
+ .trim()
46
+ .replace(/[\r\n]/g, " ");
47
+ // A whole code replaces both groups regardless of the caret. Reject oversized
48
+ // pastes before changing the field, including partial pastes that would overflow.
49
+ const replaceWholeCode = pasted.length >= maxCodeLength;
50
+ const start = replaceWholeCode ? 0 : (input.selectionStart ?? 0);
51
+ const end = replaceWholeCode ? input.value.length : (input.selectionEnd ?? 0);
52
+ const nextValue =
53
+ input.value.slice(0, start) + pasted + input.value.slice(end);
54
+ if (nextValue.trim().replace("-", "").length > maxCodeLength) {
55
+ input.setCustomValidity(codeError);
56
+ return false;
57
+ }
58
+ input.setCustomValidity("");
59
+ input.setRangeText(pasted, start, end, "end");
60
+ formatInput(input);
61
+ return true;
62
+ };
63
+
64
+ const editSeparator = (event: KeyboardEvent<HTMLInputElement>) => {
65
+ const input = event.currentTarget;
66
+ const caret = input.selectionStart;
67
+ if (
68
+ event.ctrlKey ||
69
+ event.metaKey ||
70
+ event.altKey ||
71
+ caret !== input.selectionEnd ||
72
+ input.value[4] !== "-"
73
+ ) {
74
+ return;
75
+ }
76
+ if (event.key === "-" && (caret === 4 || caret === 5)) {
77
+ event.preventDefault();
78
+ input.setSelectionRange(5, 5);
79
+ }
80
+ // Treat the separator as formatting: deletion at its boundary removes the
81
+ // adjacent character rather than restoring the dash without making progress.
82
+ if (event.key === "Backspace" && caret === 5) {
83
+ event.preventDefault();
84
+ input.setRangeText("", 3, 5, "end");
85
+ formatInput(input);
86
+ }
87
+ if (event.key === "Delete" && caret === 4) {
88
+ event.preventDefault();
89
+ input.setRangeText("", 4, Math.min(6, input.value.length), "end");
90
+ formatInput(input);
91
+ }
92
+ };
93
+
94
+ export const LoginCodeInput = ({ disabled }: LoginCodeInputProps) => {
95
+ const [invalid, setInvalid] = useState(false);
96
+
97
+ return (
98
+ <>
99
+ <input
100
+ aria-describedby="auth-code-error auth-status"
101
+ aria-invalid={invalid || undefined}
102
+ autoCapitalize="characters"
103
+ autoComplete="one-time-code"
104
+ disabled={disabled}
105
+ id="auth-code"
106
+ maxLength={maxCodeLength + 1}
107
+ name="code"
108
+ onBlur={(event) => formatInput(event.currentTarget)}
109
+ onInput={(event) => {
110
+ event.currentTarget.setCustomValidity("");
111
+ // Do not discard a typed space before learning whether it is internal.
112
+ if (!/\s$/.test(event.currentTarget.value)) {
113
+ formatInput(event.currentTarget);
114
+ }
115
+ setInvalid(false);
116
+ }}
117
+ onInvalid={() => setInvalid(true)}
118
+ onKeyDown={(event) => {
119
+ if (event.key === "Enter") {
120
+ formatInput(event.currentTarget);
121
+ }
122
+ editSeparator(event);
123
+ if (event.defaultPrevented) {
124
+ event.currentTarget.setCustomValidity("");
125
+ setInvalid(false);
126
+ }
127
+ }}
128
+ onPaste={(event) => setInvalid(!pasteCode(event))}
129
+ pattern="[A-Za-z0-9]{4}-[A-Za-z0-9]{4}"
130
+ placeholder="XXXX-XXXX"
131
+ required
132
+ spellCheck={false}
133
+ type="text"
134
+ />
135
+ <p aria-live="polite" id="auth-code-error">
136
+ {invalid ? codeError : undefined}
137
+ </p>
138
+ </>
139
+ );
140
+ };
@@ -1,5 +1,8 @@
1
1
  import type { RuntimeManifest } from "../shared/index";
2
- import type { ContractInputArguments } from "../../public/index";
2
+ import type {
3
+ ContractInputArguments,
4
+ InstanceIdentity,
5
+ } from "../../public/index";
3
6
  import type { z } from "zod";
4
7
 
5
8
  import type { ClientTransport } from "./transport";
@@ -25,14 +28,21 @@ type OperationArguments<TInput extends z.ZodType> = [
25
28
  signal?: AbortSignal,
26
29
  ];
27
30
 
28
- export type OvermuxServerApi<TServerConfig> = {
31
+ type InstanceGetters = {
32
+ getInstanceId: () => string | undefined;
33
+ getDeepLinkPrefix: () => string | undefined;
34
+ };
35
+
36
+ export type OvermuxServerApi<TServerConfig> = InstanceGetters & {
29
37
  executeOperation: <TId extends OperationId<TServerConfig>>(
30
38
  id: TId,
31
39
  ...args: OperationArguments<OperationInput<TServerConfig, TId>>
32
40
  ) => Promise<OperationOutput<TServerConfig, TId>>;
33
41
  };
34
42
 
35
- export type RuntimeOvermuxServerApi = {
43
+ export type RuntimeOvermuxServerApi = InstanceGetters & {
44
+ getInstance: () => InstanceIdentity | undefined;
45
+ subscribeInstance: (listener: () => void) => () => void;
36
46
  executeOperation: (
37
47
  id: string,
38
48
  input?: unknown,
@@ -73,6 +83,10 @@ export const createOvermuxServerApi = ({
73
83
  manifest: RuntimeManifest;
74
84
  transport: ClientTransport;
75
85
  }): RuntimeOvermuxServerApi => ({
86
+ getInstance: transport.getInstance,
87
+ subscribeInstance: transport.subscribeInstance,
88
+ getInstanceId: () => transport.getInstance()?.instanceId,
89
+ getDeepLinkPrefix: () => transport.getInstance()?.deepLinkPrefix,
76
90
  executeOperation: (id, input, signal) => {
77
91
  if (!manifest.operations.includes(id)) {
78
92
  throw new Error(`Operation is not available: ${id}`);