cmux 0.1.0 → 0.1.2

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.
@@ -0,0 +1,167 @@
1
+ export type Json = null | boolean | number | string | Json[] | {
2
+ [key: string]: Json;
3
+ };
4
+ export type JsonObject = {
5
+ [key: string]: Json;
6
+ };
7
+ export interface IdentifyResult {
8
+ app: string;
9
+ version: string;
10
+ protocol: number;
11
+ session: string;
12
+ pid: number;
13
+ }
14
+ export interface EmptyResult {
15
+ }
16
+ export interface SurfaceResult {
17
+ surface: number;
18
+ }
19
+ export interface ReadScreenResult {
20
+ text: string;
21
+ }
22
+ export interface VtStateResult {
23
+ cols: number;
24
+ rows: number;
25
+ data: string;
26
+ }
27
+ export interface Size {
28
+ cols: number;
29
+ rows: number;
30
+ }
31
+ export type Layout = {
32
+ type: "leaf";
33
+ pane: number;
34
+ } | {
35
+ type: "split";
36
+ dir: "right" | "down";
37
+ ratio: number;
38
+ a: Layout;
39
+ b: Layout;
40
+ };
41
+ export type Pane = {
42
+ id: number;
43
+ name: string | null;
44
+ active_tab: number;
45
+ tabs: Tab[];
46
+ dead?: false;
47
+ } | {
48
+ id: number;
49
+ dead: true;
50
+ name?: null;
51
+ active_tab?: 0;
52
+ tabs?: [];
53
+ };
54
+ export interface Tab {
55
+ surface: number;
56
+ kind: "pty" | "browser" | string;
57
+ browser_source: "external" | "launched" | null | string;
58
+ name: string | null;
59
+ title: string;
60
+ size: Size | null;
61
+ dead: boolean;
62
+ }
63
+ export interface Screen {
64
+ id: number;
65
+ name: string | null;
66
+ active: boolean;
67
+ active_pane: number;
68
+ layout: Layout;
69
+ panes: Pane[];
70
+ }
71
+ export interface Workspace {
72
+ id: number;
73
+ name: string;
74
+ active: boolean;
75
+ screens: Screen[];
76
+ }
77
+ export interface Tree {
78
+ workspaces: Workspace[];
79
+ }
80
+ export type SubscribeEvent = {
81
+ event: "tree-changed";
82
+ } | {
83
+ event: "surface-output";
84
+ surface: number;
85
+ } | {
86
+ event: "surface-resized";
87
+ surface: number;
88
+ cols: number;
89
+ rows: number;
90
+ } | {
91
+ event: "surface-exited";
92
+ surface: number;
93
+ } | {
94
+ event: "title-changed";
95
+ surface: number;
96
+ } | {
97
+ event: "bell";
98
+ surface: number;
99
+ } | {
100
+ event: "empty";
101
+ } | UnknownEvent;
102
+ export type AttachEvent = {
103
+ event: "vt-state";
104
+ surface: number;
105
+ cols: number;
106
+ rows: number;
107
+ data: string;
108
+ } | {
109
+ event: "output";
110
+ surface: number;
111
+ data: string;
112
+ } | {
113
+ event: "resized";
114
+ surface: number;
115
+ cols: number;
116
+ rows: number;
117
+ replay: string;
118
+ } | {
119
+ event: "detached";
120
+ surface: number;
121
+ } | UnknownEvent;
122
+ export interface UnknownEvent {
123
+ event: string;
124
+ [key: string]: unknown;
125
+ }
126
+ export interface ClientOptions {
127
+ socketPath?: string;
128
+ session?: string;
129
+ timeoutMs?: number;
130
+ allowProtocolV6Attach?: boolean;
131
+ }
132
+ export interface NewTabOptions {
133
+ pane?: number;
134
+ cwd?: string;
135
+ cols?: number;
136
+ rows?: number;
137
+ }
138
+ export interface NewBrowserTabOptions {
139
+ pane?: number;
140
+ cols?: number;
141
+ rows?: number;
142
+ }
143
+ export interface NewWorkspaceOptions {
144
+ name?: string;
145
+ cols?: number;
146
+ rows?: number;
147
+ }
148
+ export interface NewScreenOptions {
149
+ workspace?: number;
150
+ cols?: number;
151
+ rows?: number;
152
+ }
153
+ export interface SplitOptions {
154
+ cols?: number;
155
+ rows?: number;
156
+ }
157
+ export interface SendOptions {
158
+ text?: string;
159
+ bytes?: string | Uint8Array;
160
+ }
161
+ export interface SelectOptions {
162
+ index?: number;
163
+ delta?: number;
164
+ }
165
+ export interface SelectTabOptions extends SelectOptions {
166
+ pane?: number;
167
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/e2e/e2e.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { CmuxClient, CmuxCommandError, CmuxTimeoutError, Tree } from "../src/index.js";
2
+
3
+ async function main(): Promise<void> {
4
+ const socketPath = process.env.CMUX_MUX_SOCKET;
5
+ if (!socketPath) throw new Error("CMUX_MUX_SOCKET is required");
6
+
7
+ const marker = `CMUX_TS_E2E_${process.pid}_${Date.now()}`;
8
+ const later = `${marker}_ATTACH`;
9
+ const client = new CmuxClient({ socketPath, timeoutMs: 5000 });
10
+ try {
11
+ const identify = await client.identify();
12
+ assert(identify.app === "cmux-mux", `unexpected app ${identify.app}`);
13
+ assert(identify.protocol >= 5 && identify.protocol <= 6, `unsupported protocol ${identify.protocol}`);
14
+
15
+ const created = await client.newWorkspace({ name: marker, cols: 80, rows: 24 });
16
+ await client.send(created.surface, { text: `printf '${marker}\\n'\r` });
17
+ await waitForMarker(client, created.surface, marker);
18
+ const screen = await client.readScreen(created.surface);
19
+ assert(screen.text.includes(marker), "marker missing from read-screen");
20
+
21
+ const tree = await client.listWorkspaces();
22
+ const workspaceId = findWorkspaceForSurface(tree, created.surface);
23
+ assert(workspaceId !== undefined, "new workspace not found");
24
+
25
+ await client.renameSurface(created.surface, `${marker}-renamed`);
26
+ const events = await client.subscribe();
27
+ await client.resizeSurface(created.surface, 100, 31);
28
+ const resized = await nextSurfaceResized(events, created.surface, 1000);
29
+ assert(resized.cols === 100 && resized.rows === 31, `bad resize event ${JSON.stringify(resized)}`);
30
+ await client.resizeSurface(created.surface, 100, 31);
31
+ const duplicate = await nextSurfaceResized(events, created.surface, 500).catch((err) => {
32
+ if (err instanceof CmuxTimeoutError) return null;
33
+ throw err;
34
+ });
35
+ assert(duplicate === null, "same-size resize emitted surface-resized");
36
+ events.close();
37
+
38
+ const attach = await client.attachSurface(created.surface);
39
+ const first = await attach.next(1000);
40
+ assert(first.event === "vt-state", `first attach event was ${first.event}`);
41
+ await client.send(created.surface, { text: `printf '${later}\\n'\r` });
42
+ const output = await nextAttachOutput(attach, 3000);
43
+ assert(output.event === "output" || output.event === "resized", "attach did not produce output/resized after vt-state");
44
+ attach.close();
45
+
46
+ await client.closeWorkspace(workspaceId!);
47
+ const afterClose = await client.listWorkspaces();
48
+ assert(findWorkspaceForSurface(afterClose, created.surface) === undefined, "closed workspace still present");
49
+ try {
50
+ await client.readScreen(created.surface);
51
+ throw new Error("read-screen on closed surface unexpectedly succeeded");
52
+ } catch (err) {
53
+ assert(err instanceof CmuxCommandError, `closed surface error was not command error: ${err}`);
54
+ assert(String(err.message).length > 0, "command error did not preserve server message");
55
+ }
56
+ } finally {
57
+ await client.close().catch(() => undefined);
58
+ }
59
+ }
60
+
61
+ async function waitForMarker(client: CmuxClient, surface: number, marker: string): Promise<void> {
62
+ const deadline = Date.now() + 5000;
63
+ let last = "";
64
+ while (Date.now() < deadline) {
65
+ last = (await client.readScreen(surface)).text;
66
+ if (last.includes(marker)) return;
67
+ await new Promise((resolve) => setTimeout(resolve, 50));
68
+ }
69
+ throw new Error(`marker not found; last screen: ${JSON.stringify(last)}`);
70
+ }
71
+
72
+ async function nextSurfaceResized(events: Awaited<ReturnType<CmuxClient["subscribe"]>>, surface: number, timeoutMs: number) {
73
+ const deadline = Date.now() + timeoutMs;
74
+ for (;;) {
75
+ const remaining = deadline - Date.now();
76
+ if (remaining <= 0) throw new CmuxTimeoutError("surface-resized not observed");
77
+ const event = await events.next(remaining);
78
+ if (event.event === "surface-resized" && event.surface === surface) return event;
79
+ }
80
+ }
81
+
82
+ async function nextAttachOutput(attach: Awaited<ReturnType<CmuxClient["attachSurface"]>>, timeoutMs: number) {
83
+ const deadline = Date.now() + timeoutMs;
84
+ for (;;) {
85
+ const remaining = deadline - Date.now();
86
+ if (remaining <= 0) throw new CmuxTimeoutError("attach output not observed");
87
+ const event = await attach.next(remaining);
88
+ if (event.event === "output" || event.event === "resized") return event;
89
+ }
90
+ }
91
+
92
+ function findWorkspaceForSurface(tree: Tree, surface: number): number | undefined {
93
+ for (const workspace of tree.workspaces) {
94
+ for (const screen of workspace.screens) {
95
+ for (const pane of screen.panes) {
96
+ if ("tabs" in pane && pane.tabs?.some((tab) => tab.surface === surface)) return workspace.id;
97
+ }
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ function assert(condition: unknown, message: string): asserts condition {
104
+ if (!condition) throw new Error(message);
105
+ }
106
+
107
+ main().catch((err) => {
108
+ console.error(err);
109
+ process.exit(1);
110
+ });
package/package.json CHANGED
@@ -1,16 +1,24 @@
1
1
  {
2
2
  "name": "cmux",
3
- "version": "0.1.0",
4
- "description": "A multiplexer tool",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": ["cmux", "multiplexer"],
10
- "author": "Lawrence Chen",
11
- "license": "MIT",
3
+ "version": "0.1.2",
4
+ "description": "Node.js client for the cmux-mux JSON-lines protocol",
12
5
  "repository": {
13
6
  "type": "git",
14
- "url": "https://github.com/lawrencechen/cmux"
7
+ "url": "git+https://github.com/manaflow-ai/cmux.git",
8
+ "directory": "mux/bindings/typescript"
9
+ },
10
+ "homepage": "https://github.com/manaflow-ai/cmux/tree/main/mux/bindings/typescript",
11
+ "main": "dist/src/index.js",
12
+ "types": "dist/src/index.d.ts",
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "e2e": "tsc && node dist/e2e/e2e.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "20.19.25",
22
+ "typescript": "5.9.3"
15
23
  }
16
- }
24
+ }
package/src/client.ts ADDED
@@ -0,0 +1,318 @@
1
+ import { Buffer } from "node:buffer";
2
+ import * as net from "node:net";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import {
6
+ AttachEvent,
7
+ ClientOptions,
8
+ EmptyResult,
9
+ IdentifyResult,
10
+ JsonObject,
11
+ NewBrowserTabOptions,
12
+ NewScreenOptions,
13
+ NewTabOptions,
14
+ NewWorkspaceOptions,
15
+ ReadScreenResult,
16
+ SelectOptions,
17
+ SelectTabOptions,
18
+ SendOptions,
19
+ SplitOptions,
20
+ SubscribeEvent,
21
+ SurfaceResult,
22
+ Tree,
23
+ VtStateResult,
24
+ } from "./types.js";
25
+ import { CmuxCommandError, CmuxConnectionError, CmuxProtocolError, CmuxTimeoutError } from "./errors.js";
26
+
27
+ type ResponseEnvelope = { id?: unknown; ok: true; data: unknown } | { id?: unknown; ok: false; error: string };
28
+ type EventObject = { event: string; [key: string]: unknown };
29
+
30
+ export function defaultSocketPath(session = "main"): string {
31
+ const base = process.env.TMPDIR || os.tmpdir();
32
+ return path.join(base, `cmux-mux-${process.getuid?.() ?? 0}`, `${session}.sock`);
33
+ }
34
+
35
+ class JsonLineConnection {
36
+ private socket: net.Socket;
37
+ private buffer = "";
38
+ private lines: string[] = [];
39
+ private waiters: Array<{
40
+ active: boolean;
41
+ resolve: (line: string) => void;
42
+ reject: (error: Error) => void;
43
+ }> = [];
44
+ private closedError: Error | null = null;
45
+
46
+ private constructor(socket: net.Socket) {
47
+ this.socket = socket;
48
+ socket.setEncoding("utf8");
49
+ socket.on("data", (chunk: string) => this.onData(chunk));
50
+ socket.on("error", (err: Error) => this.closeWith(new CmuxConnectionError(`socket error: ${err.message}`)));
51
+ socket.on("close", () => this.closeWith(new CmuxConnectionError("session socket closed")));
52
+ }
53
+
54
+ static connect(socketPath: string): Promise<JsonLineConnection> {
55
+ return new Promise((resolve, reject) => {
56
+ const socket = net.createConnection({ path: socketPath });
57
+ socket.once("connect", () => resolve(new JsonLineConnection(socket)));
58
+ socket.once("error", (err: Error) => reject(new CmuxConnectionError(`cannot connect to session socket ${socketPath}: ${err.message}`)));
59
+ });
60
+ }
61
+
62
+ send(value: JsonObject): Promise<void> {
63
+ const line = `${JSON.stringify(value)}\n`;
64
+ return new Promise((resolve, reject) => {
65
+ this.socket.write(line, "utf8", (err?: Error | null) => {
66
+ if (err) reject(new CmuxConnectionError(`socket write failed: ${err.message}`));
67
+ else resolve();
68
+ });
69
+ });
70
+ }
71
+
72
+ async recv(timeoutMs: number): Promise<JsonObject> {
73
+ const pending = this.nextLine();
74
+ const line = await withTimeout(pending.promise, timeoutMs, "session did not respond", pending.cancel);
75
+ try {
76
+ const value = JSON.parse(line) as unknown;
77
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
78
+ throw new CmuxProtocolError("server sent non-object JSON line");
79
+ }
80
+ return value as JsonObject;
81
+ } catch (err) {
82
+ if (err instanceof CmuxProtocolError) throw err;
83
+ throw new CmuxProtocolError(`bad JSON from server: ${(err as Error).message}`);
84
+ }
85
+ }
86
+
87
+ close(): void {
88
+ this.socket.destroy();
89
+ }
90
+
91
+ private nextLine(): { promise: Promise<string>; cancel: () => void } {
92
+ if (this.lines.length > 0) {
93
+ return { promise: Promise.resolve(this.lines.shift()!), cancel: () => undefined };
94
+ }
95
+ if (this.closedError) {
96
+ return { promise: Promise.reject(this.closedError), cancel: () => undefined };
97
+ }
98
+ const waiter: {
99
+ active: boolean;
100
+ resolve: (line: string) => void;
101
+ reject: (error: Error) => void;
102
+ } = {
103
+ active: true,
104
+ resolve: (_line: string) => {},
105
+ reject: (_error: Error) => {},
106
+ };
107
+ const promise = new Promise<string>((resolve, reject) => {
108
+ waiter.resolve = resolve;
109
+ waiter.reject = reject;
110
+ });
111
+ this.waiters.push(waiter);
112
+ return {
113
+ promise,
114
+ cancel: () => {
115
+ waiter.active = false;
116
+ },
117
+ };
118
+ }
119
+
120
+ private onData(chunk: string): void {
121
+ this.buffer += chunk;
122
+ for (;;) {
123
+ const index = this.buffer.indexOf("\n");
124
+ if (index < 0) break;
125
+ const line = this.buffer.slice(0, index);
126
+ this.buffer = this.buffer.slice(index + 1);
127
+ if (line.trim() === "") continue;
128
+ let delivered = false;
129
+ while (this.waiters.length > 0) {
130
+ const waiter = this.waiters.shift()!;
131
+ if (!waiter.active) continue;
132
+ waiter.resolve(line);
133
+ delivered = true;
134
+ break;
135
+ }
136
+ if (!delivered) this.lines.push(line);
137
+ }
138
+ }
139
+
140
+ private closeWith(error: Error): void {
141
+ if (this.closedError) return;
142
+ this.closedError = error;
143
+ while (this.waiters.length > 0) {
144
+ const waiter = this.waiters.shift()!;
145
+ if (waiter.active) waiter.reject(error);
146
+ }
147
+ }
148
+ }
149
+
150
+ export class CmuxStream<T extends EventObject> implements AsyncIterable<T> {
151
+ private readonly buffered: T[] = [];
152
+ private closed = false;
153
+
154
+ private constructor(
155
+ private readonly conn: JsonLineConnection,
156
+ private readonly timeoutMs: number,
157
+ buffered: T[],
158
+ ) {
159
+ this.buffered = buffered;
160
+ }
161
+
162
+ static async open<T extends EventObject>(
163
+ socketPath: string,
164
+ timeoutMs: number,
165
+ request: JsonObject,
166
+ ): Promise<CmuxStream<T>> {
167
+ const conn = await JsonLineConnection.connect(socketPath);
168
+ await conn.send(request);
169
+ const requestId = request.id;
170
+ const buffered: T[] = [];
171
+ for (;;) {
172
+ const value = await conn.recv(timeoutMs);
173
+ if (typeof value.event === "string") {
174
+ buffered.push(value as T);
175
+ continue;
176
+ }
177
+ if (value.id !== requestId) continue;
178
+ const response = value as ResponseEnvelope;
179
+ if (response.ok === true) return new CmuxStream(conn, timeoutMs, buffered);
180
+ throw new CmuxCommandError(response.error || "unknown error", response.id, response);
181
+ }
182
+ }
183
+
184
+ async next(timeoutMs = this.timeoutMs): Promise<T> {
185
+ if (this.closed) throw new CmuxConnectionError("stream is closed");
186
+ if (this.buffered.length > 0) return this.buffered.shift()!;
187
+ for (;;) {
188
+ const value = await this.conn.recv(timeoutMs);
189
+ if (typeof value.event !== "string") continue;
190
+ const event = value as T;
191
+ if (event.event === "detached") this.close();
192
+ return event;
193
+ }
194
+ }
195
+
196
+ close(): void {
197
+ if (!this.closed) {
198
+ this.closed = true;
199
+ this.conn.close();
200
+ }
201
+ }
202
+
203
+ async *[Symbol.asyncIterator](): AsyncIterator<T> {
204
+ while (!this.closed) {
205
+ yield await this.next();
206
+ }
207
+ }
208
+ }
209
+
210
+ export class CmuxClient {
211
+ readonly socketPath: string;
212
+ readonly timeoutMs: number;
213
+ readonly allowProtocolV6Attach: boolean;
214
+ private connPromise: Promise<JsonLineConnection>;
215
+ private nextRequestId = 1;
216
+ private protocol: number | null = null;
217
+
218
+ constructor(options: ClientOptions = {}) {
219
+ this.socketPath = options.socketPath ?? defaultSocketPath(options.session ?? "main");
220
+ this.timeoutMs = options.timeoutMs ?? 10_000;
221
+ this.allowProtocolV6Attach = options.allowProtocolV6Attach ?? true;
222
+ this.connPromise = JsonLineConnection.connect(this.socketPath);
223
+ }
224
+
225
+ async close(): Promise<void> {
226
+ const conn = await this.connPromise;
227
+ conn.close();
228
+ }
229
+
230
+ async sendRaw(obj: JsonObject): Promise<ResponseEnvelope> {
231
+ const payload = { ...obj };
232
+ if (!("id" in payload)) payload.id = this.nextId();
233
+ const requestId = payload.id;
234
+ const conn = await this.connPromise;
235
+ await conn.send(payload);
236
+ for (;;) {
237
+ const response = await conn.recv(this.timeoutMs);
238
+ if (typeof response.event === "string") continue;
239
+ if (response.id !== requestId && response.id !== undefined) continue;
240
+ return response as ResponseEnvelope;
241
+ }
242
+ }
243
+
244
+ async request(cmd: string, params: JsonObject = {}): Promise<unknown> {
245
+ const response = await this.sendRaw({ id: this.nextId(), cmd, ...dropUndefined(params) });
246
+ if (response.ok === true) return response.data;
247
+ throw new CmuxCommandError(response.error || "unknown error", response.id, response);
248
+ }
249
+
250
+ async identify(): Promise<IdentifyResult> {
251
+ const result = await this.request("identify") as IdentifyResult;
252
+ this.protocol = result.protocol;
253
+ return result;
254
+ }
255
+
256
+ async listWorkspaces(): Promise<Tree> { return this.request("list-workspaces") as Promise<Tree>; }
257
+ async send(surface: number, options: SendOptions = {}): Promise<EmptyResult> {
258
+ const bytes = options.bytes instanceof Uint8Array ? Buffer.from(options.bytes).toString("base64") : options.bytes;
259
+ await this.request("send", dropUndefined({ surface, text: options.text, bytes }));
260
+ return {};
261
+ }
262
+ async readScreen(surface: number): Promise<ReadScreenResult> { return this.request("read-screen", { surface }) as Promise<ReadScreenResult>; }
263
+ async vtState(surface: number): Promise<VtStateResult> { return this.request("vt-state", { surface }) as Promise<VtStateResult>; }
264
+ async newTab(options: NewTabOptions = {}): Promise<SurfaceResult> { return this.request("new-tab", options as JsonObject) as Promise<SurfaceResult>; }
265
+ async newBrowserTab(url: string, options: NewBrowserTabOptions = {}): Promise<SurfaceResult> { return this.request("new-browser-tab", dropUndefined({ url, ...options })) as Promise<SurfaceResult>; }
266
+ async newWorkspace(options: NewWorkspaceOptions = {}): Promise<SurfaceResult> { return this.request("new-workspace", options as JsonObject) as Promise<SurfaceResult>; }
267
+ async newScreen(options: NewScreenOptions = {}): Promise<SurfaceResult> { return this.request("new-screen", options as JsonObject) as Promise<SurfaceResult>; }
268
+ async split(pane: number, dir: "right" | "down", options: SplitOptions = {}): Promise<SurfaceResult> { return this.request("split", dropUndefined({ pane, dir, ...options })) as Promise<SurfaceResult>; }
269
+ async setRatio(pane: number, dir: "right" | "down", ratio: number): Promise<EmptyResult> { await this.request("set-ratio", { pane, dir, ratio }); return {}; }
270
+ async setDefaultColors(fg?: string, bg?: string): Promise<EmptyResult> { await this.request("set-default-colors", dropUndefined({ fg, bg })); return {}; }
271
+ async closeSurface(surface: number): Promise<EmptyResult> { await this.request("close-surface", { surface }); return {}; }
272
+ async closePane(pane: number): Promise<EmptyResult> { await this.request("close-pane", { pane }); return {}; }
273
+ async closeScreen(screen: number): Promise<EmptyResult> { await this.request("close-screen", { screen }); return {}; }
274
+ async closeWorkspace(workspace: number): Promise<EmptyResult> { await this.request("close-workspace", { workspace }); return {}; }
275
+ async renamePane(pane: number, name: string): Promise<EmptyResult> { await this.request("rename-pane", { pane, name }); return {}; }
276
+ async renameSurface(surface: number, name: string): Promise<EmptyResult> { await this.request("rename-surface", { surface, name }); return {}; }
277
+ async renameScreen(screen: number, name: string): Promise<EmptyResult> { await this.request("rename-screen", { screen, name }); return {}; }
278
+ async renameWorkspace(workspace: number, name: string): Promise<EmptyResult> { await this.request("rename-workspace", { workspace, name }); return {}; }
279
+ async resizeSurface(surface: number, cols: number, rows: number): Promise<EmptyResult> { await this.request("resize-surface", { surface, cols, rows }); return {}; }
280
+ async focusPane(pane: number): Promise<EmptyResult> { await this.request("focus-pane", { pane }); return {}; }
281
+ async selectTab(options: SelectTabOptions = {}): Promise<EmptyResult> { await this.request("select-tab", options as JsonObject); return {}; }
282
+ async selectScreen(options: SelectOptions = {}): Promise<EmptyResult> { await this.request("select-screen", options as JsonObject); return {}; }
283
+ async selectWorkspace(options: SelectOptions = {}): Promise<EmptyResult> { await this.request("select-workspace", options as JsonObject); return {}; }
284
+ async moveTab(surface: number, pane: number, index: number): Promise<EmptyResult> { await this.request("move-tab", { surface, pane, index }); return {}; }
285
+ async moveWorkspace(workspace: number, index: number): Promise<EmptyResult> { await this.request("move-workspace", { workspace, index }); return {}; }
286
+ async scrollSurface(surface: number, delta: number): Promise<EmptyResult> { await this.request("scroll-surface", { surface, delta }); return {}; }
287
+
288
+ async subscribe(): Promise<CmuxStream<SubscribeEvent>> {
289
+ return CmuxStream.open<SubscribeEvent>(this.socketPath, this.timeoutMs, { id: this.nextId(), cmd: "subscribe" });
290
+ }
291
+
292
+ async attachSurface(surface: number): Promise<CmuxStream<AttachEvent>> {
293
+ const protocol = this.protocol ?? (await this.identify()).protocol;
294
+ if (protocol > 6 || (protocol > 5 && !this.allowProtocolV6Attach)) {
295
+ throw new CmuxProtocolError(`unsupported attach protocol ${protocol}`);
296
+ }
297
+ return CmuxStream.open<AttachEvent>(this.socketPath, this.timeoutMs, { id: this.nextId(), cmd: "attach-surface", surface });
298
+ }
299
+
300
+ private nextId(): number {
301
+ return this.nextRequestId++;
302
+ }
303
+ }
304
+
305
+ function dropUndefined(value: Record<string, unknown>): JsonObject {
306
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as JsonObject;
307
+ }
308
+
309
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string, onTimeout?: () => void): Promise<T> {
310
+ let timer: NodeJS.Timeout;
311
+ const timeout = new Promise<never>((_, reject) => {
312
+ timer = setTimeout(() => {
313
+ onTimeout?.();
314
+ reject(new CmuxTimeoutError(message));
315
+ }, timeoutMs);
316
+ });
317
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));
318
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,21 @@
1
+ export class CmuxError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+
8
+ export class CmuxCommandError extends CmuxError {
9
+ readonly commandId: unknown;
10
+ readonly response: unknown;
11
+
12
+ constructor(message: string, commandId?: unknown, response?: unknown) {
13
+ super(message);
14
+ this.commandId = commandId;
15
+ this.response = response;
16
+ }
17
+ }
18
+
19
+ export class CmuxConnectionError extends CmuxError {}
20
+ export class CmuxProtocolError extends CmuxError {}
21
+ export class CmuxTimeoutError extends CmuxError {}
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./client.js";
2
+ export * from "./errors.js";
3
+ export * from "./types.js";