cmux 0.3.6 → 0.4.0
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.
- package/README.md +97 -51
- package/dist/src/base64.d.ts +4 -0
- package/dist/src/base64.js +38 -0
- package/dist/src/browser.d.ts +6 -0
- package/dist/src/browser.js +6 -0
- package/dist/src/client.d.ts +202 -0
- package/dist/src/client.js +799 -0
- package/dist/src/errors.d.ts +18 -0
- package/dist/src/errors.js +29 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +8 -0
- package/dist/src/node-client.d.ts +18 -0
- package/dist/src/node-client.js +18 -0
- package/dist/src/node-transport.d.ts +27 -0
- package/dist/src/node-transport.js +99 -0
- package/dist/src/protocol/commands.d.ts +770 -0
- package/dist/src/protocol/commands.js +1 -0
- package/dist/src/protocol/common.d.ts +74 -0
- package/dist/src/protocol/common.js +1 -0
- package/dist/src/protocol/events.d.ts +349 -0
- package/dist/src/protocol/events.js +1 -0
- package/dist/src/protocol/index.d.ts +5 -0
- package/dist/src/protocol/index.js +5 -0
- package/dist/src/protocol/render.d.ts +49 -0
- package/dist/src/protocol/render.js +1 -0
- package/dist/src/protocol/tree.d.ts +95 -0
- package/dist/src/protocol/tree.js +1 -0
- package/dist/src/transport.d.ts +15 -0
- package/dist/src/transport.js +1 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.js +2 -0
- package/dist/src/websocket-transport.d.ts +75 -0
- package/dist/src/websocket-transport.js +168 -0
- package/package.json +39 -31
- package/AGENTS.md +0 -87
- package/bin/cmux +0 -97
- package/lib/postinstall.js +0 -29
- package/llms.txt +0 -70
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { decodeBase64, encodeBase64 } from "./base64.js";
|
|
2
|
+
import { CmuxCommandError, CmuxConnectionError, CmuxError, CmuxProtocolError, CmuxTimeoutError, } from "./errors.js";
|
|
3
|
+
export const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
4
|
+
export const DEFAULT_MAX_ATTACH_ENCODED_CHARS = 16 * 1024 * 1024;
|
|
5
|
+
export const TERMINAL_KEY_TEXT_MAX_BYTES = 4 * 1024;
|
|
6
|
+
function validateViewportPaneWidth(width) {
|
|
7
|
+
if (typeof width !== "number"
|
|
8
|
+
|| !Number.isFinite(width)
|
|
9
|
+
|| width < 0.1
|
|
10
|
+
|| width > 1.0) {
|
|
11
|
+
throw new CmuxProtocolError("viewport pane width must be between 0.1 and 1.0");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function layoutUndoUint(value, field) {
|
|
15
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
16
|
+
throw new CmuxProtocolError(`layout undo ${field} is not a nonnegative integer`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function decodeLayoutUndoResult(value) {
|
|
21
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
22
|
+
throw new CmuxProtocolError("layout undo result is not an object");
|
|
23
|
+
}
|
|
24
|
+
const result = value;
|
|
25
|
+
const screen = layoutUndoUint(result.screen, "screen");
|
|
26
|
+
const revision = layoutUndoUint(result.revision, "revision");
|
|
27
|
+
if (result.undone === true
|
|
28
|
+
&& (result.confirmation_required === undefined
|
|
29
|
+
|| result.confirmation_required === false)) {
|
|
30
|
+
return { undone: true, screen, revision };
|
|
31
|
+
}
|
|
32
|
+
if (result.undone === false && result.confirmation_required === true) {
|
|
33
|
+
if (!Array.isArray(result.closes_panes)) {
|
|
34
|
+
throw new CmuxProtocolError("layout undo closes_panes is not an array");
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
undone: false,
|
|
38
|
+
confirmation_required: true,
|
|
39
|
+
screen,
|
|
40
|
+
revision,
|
|
41
|
+
closes_panes: result.closes_panes.map((pane) => layoutUndoUint(pane, "pane ID")),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
throw new CmuxProtocolError("layout undo result does not contain exactly one valid outcome");
|
|
45
|
+
}
|
|
46
|
+
function decodeResponseData(command, value) {
|
|
47
|
+
if (command === "undo-layout")
|
|
48
|
+
return decodeLayoutUndoResult(value);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function workspaceMutationResult(result) {
|
|
52
|
+
if ("workspace" in result
|
|
53
|
+
&& "key" in result
|
|
54
|
+
&& "workspace_revision" in result
|
|
55
|
+
&& typeof result.workspace === "number"
|
|
56
|
+
&& typeof result.key === "string"
|
|
57
|
+
&& typeof result.workspace_revision === "number") {
|
|
58
|
+
return {
|
|
59
|
+
workspace: result.workspace,
|
|
60
|
+
key: result.key,
|
|
61
|
+
workspace_revision: result.workspace_revision,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw new CmuxProtocolError("server returned an invalid workspace registry mutation");
|
|
65
|
+
}
|
|
66
|
+
function normalizeClientSizing(clients) {
|
|
67
|
+
return clients.map((client) => {
|
|
68
|
+
const fallback = client.size_participating ?? true;
|
|
69
|
+
if (client.sizes.every((size) => size.size_participating !== undefined))
|
|
70
|
+
return client;
|
|
71
|
+
return {
|
|
72
|
+
...client,
|
|
73
|
+
sizes: client.sizes.map((size) => (size.size_participating === undefined
|
|
74
|
+
? { ...size, size_participating: fallback }
|
|
75
|
+
: size)),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
class MessageRouter {
|
|
80
|
+
transport;
|
|
81
|
+
pending = new Map();
|
|
82
|
+
eventHandlers = new Set();
|
|
83
|
+
terminalHandlers = new Set();
|
|
84
|
+
terminalError = null;
|
|
85
|
+
constructor(transport) {
|
|
86
|
+
this.transport = transport;
|
|
87
|
+
transport.onMessage((json) => this.receive(json));
|
|
88
|
+
transport.onError((error) => this.terminate(this.connectionError(error)));
|
|
89
|
+
transport.onClose(() => this.terminate(new CmuxConnectionError("session transport closed")));
|
|
90
|
+
}
|
|
91
|
+
send(request, timeoutMs) {
|
|
92
|
+
const key = this.idKey(request.id);
|
|
93
|
+
if (this.terminalError)
|
|
94
|
+
return Promise.reject(this.terminalError);
|
|
95
|
+
if (this.pending.has(key))
|
|
96
|
+
return Promise.reject(new CmuxProtocolError(`duplicate request id ${key}`));
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
this.pending.delete(key);
|
|
100
|
+
reject(new CmuxTimeoutError("session did not respond"));
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
this.pending.set(key, { resolve, reject, timer });
|
|
103
|
+
try {
|
|
104
|
+
this.transport.send(JSON.stringify(request));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
this.pending.delete(key);
|
|
109
|
+
reject(this.connectionError(error));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
onEvent(handler) {
|
|
114
|
+
this.eventHandlers.add(handler);
|
|
115
|
+
return () => this.eventHandlers.delete(handler);
|
|
116
|
+
}
|
|
117
|
+
onTerminal(handler) {
|
|
118
|
+
this.terminalHandlers.add(handler);
|
|
119
|
+
if (this.terminalError)
|
|
120
|
+
queueMicrotask(() => handler(this.terminalError));
|
|
121
|
+
return () => this.terminalHandlers.delete(handler);
|
|
122
|
+
}
|
|
123
|
+
receive(json) {
|
|
124
|
+
let value;
|
|
125
|
+
try {
|
|
126
|
+
value = JSON.parse(json);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.terminate(new CmuxProtocolError(`bad JSON from server: ${error.message}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
133
|
+
this.terminate(new CmuxProtocolError("server sent non-object JSON message"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const object = value;
|
|
137
|
+
if (typeof object.event === "string") {
|
|
138
|
+
for (const handler of this.eventHandlers)
|
|
139
|
+
handler(object);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const key = object.id === undefined ? this.pending.keys().next().value : this.idKey(object.id);
|
|
143
|
+
if (key === undefined)
|
|
144
|
+
return;
|
|
145
|
+
const pending = this.pending.get(key);
|
|
146
|
+
if (!pending)
|
|
147
|
+
return;
|
|
148
|
+
clearTimeout(pending.timer);
|
|
149
|
+
this.pending.delete(key);
|
|
150
|
+
pending.resolve(object);
|
|
151
|
+
}
|
|
152
|
+
terminate(error) {
|
|
153
|
+
if (this.terminalError)
|
|
154
|
+
return;
|
|
155
|
+
this.terminalError = error;
|
|
156
|
+
for (const pending of this.pending.values()) {
|
|
157
|
+
clearTimeout(pending.timer);
|
|
158
|
+
pending.reject(error);
|
|
159
|
+
}
|
|
160
|
+
this.pending.clear();
|
|
161
|
+
for (const handler of this.terminalHandlers)
|
|
162
|
+
handler(error);
|
|
163
|
+
}
|
|
164
|
+
idKey(id) {
|
|
165
|
+
return id === undefined ? "undefined" : JSON.stringify(id);
|
|
166
|
+
}
|
|
167
|
+
connectionError(error) {
|
|
168
|
+
if (error instanceof CmuxError)
|
|
169
|
+
return error;
|
|
170
|
+
return new CmuxConnectionError(error instanceof Error ? error.message : String(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** A closeable async event stream with optional per-read timeouts. */
|
|
174
|
+
export class CmuxStream {
|
|
175
|
+
timeoutMs;
|
|
176
|
+
cleanup;
|
|
177
|
+
maxBufferedEvents;
|
|
178
|
+
maxBufferedBytes;
|
|
179
|
+
retainedBytes;
|
|
180
|
+
buffered = [];
|
|
181
|
+
bufferedBytes = 0;
|
|
182
|
+
waiters = [];
|
|
183
|
+
closed = false;
|
|
184
|
+
endsAfterDrain = false;
|
|
185
|
+
terminalError = null;
|
|
186
|
+
constructor(timeoutMs, cleanup, maxBufferedEvents = DEFAULT_MAX_BUFFERED_EVENTS, maxBufferedBytes = Number.POSITIVE_INFINITY, retainedBytes = () => 0) {
|
|
187
|
+
this.timeoutMs = timeoutMs;
|
|
188
|
+
this.cleanup = cleanup;
|
|
189
|
+
this.maxBufferedEvents = maxBufferedEvents;
|
|
190
|
+
this.maxBufferedBytes = maxBufferedBytes;
|
|
191
|
+
this.retainedBytes = retainedBytes;
|
|
192
|
+
}
|
|
193
|
+
async next(timeoutMs = this.timeoutMs) {
|
|
194
|
+
if (this.buffered.length > 0) {
|
|
195
|
+
const event = this.buffered.shift();
|
|
196
|
+
this.bufferedBytes = Math.max(0, this.bufferedBytes - this.retainedBytes(event));
|
|
197
|
+
if (this.endsAfterDrain && this.buffered.length === 0)
|
|
198
|
+
this.finish();
|
|
199
|
+
return event;
|
|
200
|
+
}
|
|
201
|
+
if (this.terminalError)
|
|
202
|
+
throw this.terminalError;
|
|
203
|
+
if (this.closed)
|
|
204
|
+
throw new CmuxConnectionError("stream is closed");
|
|
205
|
+
const waiter = {
|
|
206
|
+
active: true,
|
|
207
|
+
resolve: () => undefined,
|
|
208
|
+
reject: () => undefined,
|
|
209
|
+
};
|
|
210
|
+
const event = await new Promise((resolve, reject) => {
|
|
211
|
+
const timer = setTimeout(() => {
|
|
212
|
+
waiter.active = false;
|
|
213
|
+
const index = this.waiters.indexOf(waiter);
|
|
214
|
+
if (index >= 0)
|
|
215
|
+
this.waiters.splice(index, 1);
|
|
216
|
+
reject(new CmuxTimeoutError("stream did not produce an event"));
|
|
217
|
+
}, timeoutMs);
|
|
218
|
+
waiter.resolve = (value) => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
resolve(value);
|
|
221
|
+
};
|
|
222
|
+
waiter.reject = (error) => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
reject(error);
|
|
225
|
+
};
|
|
226
|
+
this.waiters.push(waiter);
|
|
227
|
+
});
|
|
228
|
+
if (this.endsAfterDrain && this.buffered.length === 0)
|
|
229
|
+
this.finish();
|
|
230
|
+
return event;
|
|
231
|
+
}
|
|
232
|
+
close() {
|
|
233
|
+
if (this.closed)
|
|
234
|
+
return;
|
|
235
|
+
this.buffered.length = 0;
|
|
236
|
+
this.bufferedBytes = 0;
|
|
237
|
+
this.finish();
|
|
238
|
+
this.rejectWaiters(new CmuxConnectionError("stream is closed"));
|
|
239
|
+
}
|
|
240
|
+
push(event, terminal = false) {
|
|
241
|
+
if (this.closed)
|
|
242
|
+
return;
|
|
243
|
+
let delivered = false;
|
|
244
|
+
while (this.waiters.length > 0) {
|
|
245
|
+
const waiter = this.waiters.shift();
|
|
246
|
+
if (!waiter.active)
|
|
247
|
+
continue;
|
|
248
|
+
waiter.resolve(event);
|
|
249
|
+
delivered = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (!delivered) {
|
|
253
|
+
if (this.buffered.length >= this.maxBufferedEvents) {
|
|
254
|
+
this.fail(new CmuxProtocolError("stream event buffer overflow"));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const retainedBytes = this.retainedBytes(event);
|
|
258
|
+
if (retainedBytes > this.maxBufferedBytes - this.bufferedBytes) {
|
|
259
|
+
this.fail(new CmuxProtocolError(`stream buffered data exceeds ${this.maxBufferedBytes} bytes`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.buffered.push(event);
|
|
263
|
+
this.bufferedBytes += retainedBytes;
|
|
264
|
+
}
|
|
265
|
+
if (terminal)
|
|
266
|
+
this.endsAfterDrain = true;
|
|
267
|
+
}
|
|
268
|
+
fail(error) {
|
|
269
|
+
if (this.closed)
|
|
270
|
+
return;
|
|
271
|
+
this.terminalError = error;
|
|
272
|
+
this.buffered.length = 0;
|
|
273
|
+
this.bufferedBytes = 0;
|
|
274
|
+
this.finish();
|
|
275
|
+
this.rejectWaiters(error);
|
|
276
|
+
}
|
|
277
|
+
get error() {
|
|
278
|
+
return this.terminalError;
|
|
279
|
+
}
|
|
280
|
+
async *[Symbol.asyncIterator]() {
|
|
281
|
+
try {
|
|
282
|
+
while (true) {
|
|
283
|
+
if (this.terminalError)
|
|
284
|
+
throw this.terminalError;
|
|
285
|
+
if (this.closed)
|
|
286
|
+
return;
|
|
287
|
+
yield await this.next();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
this.close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
finish() {
|
|
295
|
+
if (this.closed)
|
|
296
|
+
return;
|
|
297
|
+
this.closed = true;
|
|
298
|
+
this.cleanup();
|
|
299
|
+
}
|
|
300
|
+
rejectWaiters(error) {
|
|
301
|
+
while (this.waiters.length > 0) {
|
|
302
|
+
const waiter = this.waiters.shift();
|
|
303
|
+
if (waiter.active)
|
|
304
|
+
waiter.reject(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Promise-based typed client for any cmux JSON transport. */
|
|
309
|
+
export class CmuxClient {
|
|
310
|
+
timeoutMs;
|
|
311
|
+
allowProtocolV6Attach;
|
|
312
|
+
maxBufferedEvents;
|
|
313
|
+
maxAttachEncodedChars;
|
|
314
|
+
transport;
|
|
315
|
+
router;
|
|
316
|
+
streamTransportFactory;
|
|
317
|
+
nextRequestId = 1;
|
|
318
|
+
identifiedProtocol = null;
|
|
319
|
+
identifiedCapabilities = new Set();
|
|
320
|
+
sharedSubscriptionActive = false;
|
|
321
|
+
constructor(options) {
|
|
322
|
+
this.transport = options.transport;
|
|
323
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
324
|
+
this.allowProtocolV6Attach = options.allowProtocolV6Attach ?? true;
|
|
325
|
+
this.maxBufferedEvents = this.securityLimit("maxBufferedEvents", options.maxBufferedEvents, DEFAULT_MAX_BUFFERED_EVENTS);
|
|
326
|
+
this.maxAttachEncodedChars = this.securityLimit("maxAttachEncodedChars", options.maxAttachEncodedChars, DEFAULT_MAX_ATTACH_ENCODED_CHARS);
|
|
327
|
+
this.streamTransportFactory = options.streamTransportFactory;
|
|
328
|
+
this.router = new MessageRouter(this.transport);
|
|
329
|
+
}
|
|
330
|
+
async close() {
|
|
331
|
+
this.transport.close();
|
|
332
|
+
}
|
|
333
|
+
async sendRaw(obj) {
|
|
334
|
+
const payload = this.dropUndefined({ ...obj });
|
|
335
|
+
if (!("id" in payload))
|
|
336
|
+
payload.id = this.nextId();
|
|
337
|
+
return this.router.send(payload, this.timeoutMs);
|
|
338
|
+
}
|
|
339
|
+
async request(requestOrCommand, params) {
|
|
340
|
+
const request = typeof requestOrCommand === "string"
|
|
341
|
+
? { cmd: requestOrCommand, ...(params ?? {}) }
|
|
342
|
+
: requestOrCommand;
|
|
343
|
+
const response = await this.sendRaw(request);
|
|
344
|
+
if (response.ok) {
|
|
345
|
+
return decodeResponseData(request.cmd, response.data);
|
|
346
|
+
}
|
|
347
|
+
throw new CmuxCommandError(response.error || "unknown error", response.id, response, response.error_code, response.error_delivery);
|
|
348
|
+
}
|
|
349
|
+
async identify() {
|
|
350
|
+
const result = await this.request("identify");
|
|
351
|
+
this.identifiedProtocol = result.protocol;
|
|
352
|
+
this.identifiedCapabilities = new Set(result.capabilities ?? []);
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
/** The protocol reported by the latest `identify()`, or null before identification. */
|
|
356
|
+
get protocol() { return this.identifiedProtocol; }
|
|
357
|
+
ping() { return this.request("ping"); }
|
|
358
|
+
setClientInfo(name, kind) {
|
|
359
|
+
return this.request("set-client-info", { name, kind });
|
|
360
|
+
}
|
|
361
|
+
async listClients() {
|
|
362
|
+
return normalizeClientSizing(await this.request("list-clients"));
|
|
363
|
+
}
|
|
364
|
+
detachClient(client) { return this.request("detach-client", { client }); }
|
|
365
|
+
async setClientSizing(surface, client, enabled) {
|
|
366
|
+
await this.requireProtocol(10, "set-client-sizing");
|
|
367
|
+
return this.request("set-client-sizing", { surface, client, enabled });
|
|
368
|
+
}
|
|
369
|
+
async useOnlyClientSizing(surface, client) {
|
|
370
|
+
await this.requireProtocol(10, "set-client-sizing");
|
|
371
|
+
return this.request("set-client-sizing", { surface, client, enabled: true, exclusive: true });
|
|
372
|
+
}
|
|
373
|
+
async useAllClientSizing(surface) {
|
|
374
|
+
await this.requireProtocol(10, "set-client-sizing");
|
|
375
|
+
return this.request("set-client-sizing", { surface, enabled: true });
|
|
376
|
+
}
|
|
377
|
+
reloadConfig() { return this.request("reload-config"); }
|
|
378
|
+
setWindowTitle(title) { return this.request("set-window-title", { title }); }
|
|
379
|
+
clearWindowTitle() { return this.request("clear-window-title"); }
|
|
380
|
+
listWorkspaces() { return this.request("list-workspaces"); }
|
|
381
|
+
listTerminals() { return this.request("list-terminals"); }
|
|
382
|
+
terminalEvents(afterRevision = 0) {
|
|
383
|
+
return this.request("terminal-events", { after_revision: afterRevision });
|
|
384
|
+
}
|
|
385
|
+
exportLayout(screen) { return this.request("export-layout", { screen }); }
|
|
386
|
+
applyLayout(layout, options = {}) {
|
|
387
|
+
return this.request("apply-layout", { ...options, layout });
|
|
388
|
+
}
|
|
389
|
+
async send(surface, options = {}) {
|
|
390
|
+
const legacyBytes = options.bytes instanceof Uint8Array ? encodeBase64(options.bytes) : options.bytes;
|
|
391
|
+
const bytes = "base64" in options ? options.base64 : legacyBytes;
|
|
392
|
+
return this.request("send", { surface, text: options.text, bytes, paste: options.paste });
|
|
393
|
+
}
|
|
394
|
+
async clearHistory(surface, fallbackKey) {
|
|
395
|
+
await this.requireCapability("clear-history-v1", "clear-history");
|
|
396
|
+
if (fallbackKey !== undefined) {
|
|
397
|
+
await this.requireCapability("clear-history-key-v1", "clear-history key fallback");
|
|
398
|
+
if (new TextEncoder().encode(fallbackKey.utf8).byteLength > TERMINAL_KEY_TEXT_MAX_BYTES) {
|
|
399
|
+
throw new TypeError("terminal key text exceeds the 4 KiB protocol limit");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return this.request("clear-history", { surface, fallback_key: fallbackKey });
|
|
403
|
+
}
|
|
404
|
+
readScreen(surface) { return this.request("read-screen", { surface }); }
|
|
405
|
+
readScrollback(surface, start, count) {
|
|
406
|
+
return this.request("read-scrollback", { surface, start, count });
|
|
407
|
+
}
|
|
408
|
+
sidebarPlugin(cols, rows, relaunch) {
|
|
409
|
+
return this.request("sidebar-plugin", { cols, rows, relaunch });
|
|
410
|
+
}
|
|
411
|
+
vtState(surface) { return this.request("vt-state", { surface }); }
|
|
412
|
+
resolveTerminal(terminalId) {
|
|
413
|
+
return this.request("resolve-terminal", { terminal_id: terminalId });
|
|
414
|
+
}
|
|
415
|
+
closeTerminal(terminalId, terminalIncarnation, options = {}) {
|
|
416
|
+
return this.request("close-terminal", {
|
|
417
|
+
...options,
|
|
418
|
+
terminal_id: terminalId,
|
|
419
|
+
terminal_incarnation: terminalIncarnation,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
newTab(options = {}) { return this.request("new-tab", options); }
|
|
423
|
+
newBrowserTab(url, options = {}) {
|
|
424
|
+
return this.request("new-browser-tab", { url, ...options });
|
|
425
|
+
}
|
|
426
|
+
newWorkspace(options = {}) { return this.request("new-workspace", options); }
|
|
427
|
+
async createWorkspace(options = {}) {
|
|
428
|
+
await this.requireCapability("workspace-registry-v1", "workspace registry");
|
|
429
|
+
return this.request("create-workspace", options);
|
|
430
|
+
}
|
|
431
|
+
async createTerminal(options) {
|
|
432
|
+
await this.requireCapability("workspace-registry-v1", "workspace registry");
|
|
433
|
+
return this.request("create-terminal", options);
|
|
434
|
+
}
|
|
435
|
+
moveTerminal(terminalId, workspaceKey, options = {}) {
|
|
436
|
+
return this.request("move-terminal", {
|
|
437
|
+
...options,
|
|
438
|
+
terminal_id: terminalId,
|
|
439
|
+
workspace_key: workspaceKey,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
newScreen(options = {}) { return this.request("new-screen", options); }
|
|
443
|
+
async newPane(pane, options = {}) {
|
|
444
|
+
await this.requireProtocol(9, "new-pane");
|
|
445
|
+
return this.request("new-pane", { pane, ...options });
|
|
446
|
+
}
|
|
447
|
+
async newPaneRight(pane, options = {}) {
|
|
448
|
+
if (options.width !== undefined && options.width !== null) {
|
|
449
|
+
validateViewportPaneWidth(options.width);
|
|
450
|
+
}
|
|
451
|
+
await this.requireCapability("viewport-splits-v1", "viewport panes");
|
|
452
|
+
return this.request("new-pane-right", { pane, ...options });
|
|
453
|
+
}
|
|
454
|
+
split(pane, dir, options = {}) {
|
|
455
|
+
return this.request("split", { pane, dir, ...options });
|
|
456
|
+
}
|
|
457
|
+
setRatio(pane, dir, ratio) {
|
|
458
|
+
return this.request("set-ratio", { pane, dir, ratio });
|
|
459
|
+
}
|
|
460
|
+
async setSplitRatio(split, ratio, options = {}) {
|
|
461
|
+
await this.requireProtocol(8, "set-split-ratio");
|
|
462
|
+
return this.request("set-split-ratio", { split, ratio, ...options });
|
|
463
|
+
}
|
|
464
|
+
async setViewportPaneWidth(pane, width, options = {}) {
|
|
465
|
+
validateViewportPaneWidth(width);
|
|
466
|
+
await this.requireCapability("viewport-column-resize-v1", "viewport pane resizing");
|
|
467
|
+
return this.request("set-viewport-pane-width", { pane, width, ...options });
|
|
468
|
+
}
|
|
469
|
+
async undoLayout(pane, confirmationRevision) {
|
|
470
|
+
await this.requireCapability("layout-undo-v1", "layout undo");
|
|
471
|
+
if (confirmationRevision === undefined) {
|
|
472
|
+
return this.request("undo-layout", { pane });
|
|
473
|
+
}
|
|
474
|
+
return this.request("undo-layout", {
|
|
475
|
+
pane,
|
|
476
|
+
revision: confirmationRevision,
|
|
477
|
+
confirm_close: true,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
paneNeighbor(pane, dir) {
|
|
481
|
+
return this.request("pane-neighbor", { pane, dir });
|
|
482
|
+
}
|
|
483
|
+
focusDirection(dir, pane) {
|
|
484
|
+
return this.request("focus-direction", { pane, dir });
|
|
485
|
+
}
|
|
486
|
+
swapPane(params) { return this.request("swap-pane", params); }
|
|
487
|
+
zoomPane(params = {}) { return this.request("zoom-pane", params); }
|
|
488
|
+
processInfo(surface) { return this.request("process-info", { surface }); }
|
|
489
|
+
setDefaultColors(fg, bg) {
|
|
490
|
+
return this.request("set-default-colors", { fg, bg });
|
|
491
|
+
}
|
|
492
|
+
closeSurface(surface) { return this.request("close-surface", { surface }); }
|
|
493
|
+
closePane(pane) { return this.request("close-pane", { pane }); }
|
|
494
|
+
closeScreen(screen) { return this.request("close-screen", { screen }); }
|
|
495
|
+
async closeWorkspace(workspace) {
|
|
496
|
+
await this.request("close-workspace", { workspace });
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
async closeWorkspaceRegistry(options) {
|
|
500
|
+
await this.requireCapability("workspace-registry-v1", "workspace registry");
|
|
501
|
+
return workspaceMutationResult(await this.request("close-workspace", options));
|
|
502
|
+
}
|
|
503
|
+
renamePane(pane, name) { return this.request("rename-pane", { pane, name }); }
|
|
504
|
+
renameSurface(surface, name) { return this.request("rename-surface", { surface, name }); }
|
|
505
|
+
renameScreen(screen, name) { return this.request("rename-screen", { screen, name }); }
|
|
506
|
+
async renameWorkspace(workspace, name) {
|
|
507
|
+
await this.request("rename-workspace", { workspace, name });
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
async renameWorkspaceRegistry(options) {
|
|
511
|
+
await this.requireCapability("workspace-registry-v1", "workspace registry");
|
|
512
|
+
return workspaceMutationResult(await this.request("rename-workspace", options));
|
|
513
|
+
}
|
|
514
|
+
async resizeSurface(surface, cols, rows) {
|
|
515
|
+
const result = await this.request("resize-surface", { surface, cols, rows });
|
|
516
|
+
return { ...result, accepted: result.accepted ?? true };
|
|
517
|
+
}
|
|
518
|
+
releaseSurfaceSize(surface) {
|
|
519
|
+
return this.request("release-surface-size", { surface });
|
|
520
|
+
}
|
|
521
|
+
focusPane(pane) { return this.request("focus-pane", { pane }); }
|
|
522
|
+
selectTab(options = {}) { return this.request("select-tab", options); }
|
|
523
|
+
selectScreen(options = {}) { return this.request("select-screen", options); }
|
|
524
|
+
selectWorkspace(options = {}) { return this.request("select-workspace", options); }
|
|
525
|
+
moveTab(surface, pane, index) { return this.request("move-tab", { surface, pane, index }); }
|
|
526
|
+
async moveWorkspace(workspace, index) {
|
|
527
|
+
await this.request("move-workspace", { workspace, index });
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
async moveWorkspaceRegistry(options) {
|
|
531
|
+
await this.requireCapability("workspace-registry-v1", "workspace registry");
|
|
532
|
+
return workspaceMutationResult(await this.request("move-workspace", options));
|
|
533
|
+
}
|
|
534
|
+
scrollSurface(surface, delta) { return this.request("scroll-surface", { surface, delta }); }
|
|
535
|
+
async subscribe(options = {}) {
|
|
536
|
+
return this.openStream({ cmd: "subscribe", tree_events: options.treeEvents }, (event) => event, (event, dedicated) => dedicated
|
|
537
|
+
|| (!this.attachOnlyEvent(event.event) && !this.isSurfaceOverflow(event)), (event) => event.event === "overflow" && !this.isSurfaceOverflow(event), true);
|
|
538
|
+
}
|
|
539
|
+
async attachSurface(surface, options = {}) {
|
|
540
|
+
if ((options.cols === undefined) !== (options.rows === undefined)) {
|
|
541
|
+
throw new CmuxProtocolError("attach-surface cols and rows must be supplied together");
|
|
542
|
+
}
|
|
543
|
+
const mode = options.mode ?? "bytes";
|
|
544
|
+
const protocol = this.identifiedProtocol ?? (await this.identify()).protocol;
|
|
545
|
+
if (mode === "render" && protocol < 7) {
|
|
546
|
+
throw new CmuxProtocolError(`render attach requires protocol 7 or newer; server reported protocol ${protocol}`);
|
|
547
|
+
}
|
|
548
|
+
if (mode === "bytes" && protocol > 5 && !this.allowProtocolV6Attach) {
|
|
549
|
+
throw new CmuxProtocolError(`byte attach for protocol ${protocol} is disabled`);
|
|
550
|
+
}
|
|
551
|
+
if ((options.cols !== undefined || options.rows !== undefined)
|
|
552
|
+
&& !this.identifiedCapabilities.has("attach-initial-size")) {
|
|
553
|
+
throw new CmuxProtocolError("initial attach sizing is not supported by this server");
|
|
554
|
+
}
|
|
555
|
+
const request = {
|
|
556
|
+
cmd: "attach-surface",
|
|
557
|
+
surface,
|
|
558
|
+
...(options.mode === undefined ? {} : { mode }),
|
|
559
|
+
...(options.cols === undefined ? {} : { cols: options.cols }),
|
|
560
|
+
...(options.rows === undefined ? {} : { rows: options.rows }),
|
|
561
|
+
};
|
|
562
|
+
if (mode === "render") {
|
|
563
|
+
return this.openStream(request, (event) => event, (event, dedicated) => dedicated || this.matchesAttachEvent(event, surface, mode), (event) => event.event === "detached" || this.isSurfaceOverflow(event, surface));
|
|
564
|
+
}
|
|
565
|
+
return this.openStream(request, (event) => this.decodeAttachEvent(event), (event, dedicated) => dedicated || this.matchesAttachEvent(event, surface, mode), (event) => event.event === "detached" || this.isSurfaceOverflow(event, surface), false, {
|
|
566
|
+
maxBytes: this.maxAttachEncodedChars,
|
|
567
|
+
retainedBytes: (event) => this.attachEventRetainedBytes(event),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
async requireCapability(capability, feature) {
|
|
571
|
+
if (this.identifiedProtocol === null) {
|
|
572
|
+
await this.identify();
|
|
573
|
+
}
|
|
574
|
+
if (!this.identifiedCapabilities.has(capability)) {
|
|
575
|
+
throw new CmuxProtocolError(`${feature} is not supported by this server`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async requireProtocol(minimum, feature) {
|
|
579
|
+
const protocol = this.protocol ?? (await this.identify()).protocol;
|
|
580
|
+
if (protocol < minimum) {
|
|
581
|
+
throw new CmuxProtocolError(`${feature} requires protocol ${minimum}; server uses protocol ${protocol}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
waitFor(surface, pattern, timeoutMs) {
|
|
585
|
+
return this.request("wait-for", { surface, pattern, timeout_ms: timeoutMs });
|
|
586
|
+
}
|
|
587
|
+
run(options) { return this.request("run", options); }
|
|
588
|
+
sendKey(surface, keys) { return this.request("send-key", { surface, keys }); }
|
|
589
|
+
copy(surface, mode) { return this.request("copy", { surface, mode }); }
|
|
590
|
+
ids(kind) { return this.request("ids", { kind }); }
|
|
591
|
+
notify(title, body, options = {}) {
|
|
592
|
+
return this.request("notify", { title, body, ...options });
|
|
593
|
+
}
|
|
594
|
+
listAgents(options = {}) {
|
|
595
|
+
return this.request("list-agents", options);
|
|
596
|
+
}
|
|
597
|
+
reportAgent(surface, state, source, session) {
|
|
598
|
+
return this.request("report-agent", { surface, state, source, session });
|
|
599
|
+
}
|
|
600
|
+
async openStream(request, map, accept, terminal = () => false, exclusiveSharedSubscription = false, buffering) {
|
|
601
|
+
const dedicated = this.streamTransportFactory !== undefined;
|
|
602
|
+
if (exclusiveSharedSubscription && !dedicated) {
|
|
603
|
+
if (this.sharedSubscriptionActive) {
|
|
604
|
+
throw new CmuxProtocolError("concurrent subscriptions require streamTransportFactory");
|
|
605
|
+
}
|
|
606
|
+
this.sharedSubscriptionActive = true;
|
|
607
|
+
}
|
|
608
|
+
const transport = this.streamTransportFactory?.() ?? this.transport;
|
|
609
|
+
const router = dedicated ? new MessageRouter(transport) : this.router;
|
|
610
|
+
let eventSubscription = () => undefined;
|
|
611
|
+
let terminalSubscription = () => undefined;
|
|
612
|
+
let streamError = null;
|
|
613
|
+
const stream = new CmuxStream(this.timeoutMs, () => {
|
|
614
|
+
eventSubscription();
|
|
615
|
+
terminalSubscription();
|
|
616
|
+
if (exclusiveSharedSubscription && !dedicated) {
|
|
617
|
+
this.sharedSubscriptionActive = false;
|
|
618
|
+
}
|
|
619
|
+
if (dedicated)
|
|
620
|
+
transport.close();
|
|
621
|
+
}, this.maxBufferedEvents, buffering?.maxBytes, buffering?.retainedBytes);
|
|
622
|
+
eventSubscription = router.onEvent((event) => {
|
|
623
|
+
if (!accept(event, dedicated))
|
|
624
|
+
return;
|
|
625
|
+
try {
|
|
626
|
+
const mapped = map(event);
|
|
627
|
+
stream.push(mapped, terminal(mapped));
|
|
628
|
+
streamError ??= stream.error;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
streamError = error instanceof CmuxProtocolError
|
|
632
|
+
? error
|
|
633
|
+
: new CmuxProtocolError(`invalid stream event: ${error.message}`);
|
|
634
|
+
stream.fail(streamError);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
terminalSubscription = router.onTerminal((error) => stream.fail(error));
|
|
638
|
+
const payload = this.dropUndefined({ id: this.nextId(), ...request });
|
|
639
|
+
const response = await router.send(payload, this.timeoutMs).catch((error) => {
|
|
640
|
+
stream.fail(error);
|
|
641
|
+
throw streamError ?? error;
|
|
642
|
+
});
|
|
643
|
+
if (!response.ok) {
|
|
644
|
+
stream.close();
|
|
645
|
+
throw new CmuxCommandError(response.error || "unknown error", response.id, response, response.error_code, response.error_delivery);
|
|
646
|
+
}
|
|
647
|
+
const terminalError = streamError ?? stream.error;
|
|
648
|
+
if (terminalError)
|
|
649
|
+
throw terminalError;
|
|
650
|
+
return stream;
|
|
651
|
+
}
|
|
652
|
+
decodeAttachEvent(event) {
|
|
653
|
+
switch (event.event) {
|
|
654
|
+
case "vt-state": {
|
|
655
|
+
return { ...event, data: this.decodeAttachData(event.data, "vt-state") };
|
|
656
|
+
}
|
|
657
|
+
case "output": {
|
|
658
|
+
return { ...event, data: this.decodeAttachData(event.data, "output") };
|
|
659
|
+
}
|
|
660
|
+
case "resized": {
|
|
661
|
+
const encoded = typeof event.data === "string" ? event.data : event.replay;
|
|
662
|
+
const data = this.decodeAttachData(encoded, "resized");
|
|
663
|
+
return { ...event, data, replay: data };
|
|
664
|
+
}
|
|
665
|
+
case "frame": {
|
|
666
|
+
return {
|
|
667
|
+
...event,
|
|
668
|
+
...this.decodeBrowserFrame(event, "frame"),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
case "browser-state": {
|
|
672
|
+
const frame = event.frame;
|
|
673
|
+
if (frame !== undefined && frame !== null) {
|
|
674
|
+
return {
|
|
675
|
+
...event,
|
|
676
|
+
frame: this.decodeBrowserFrame(frame, "browser-state frame"),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return event;
|
|
680
|
+
}
|
|
681
|
+
default: return event;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
decodeAttachData(value, eventName) {
|
|
685
|
+
return decodeBase64(this.validateAttachEncodedData(value, eventName));
|
|
686
|
+
}
|
|
687
|
+
decodeBrowserFrame(value, eventName) {
|
|
688
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
689
|
+
throw new CmuxProtocolError(`${eventName} is not an object`);
|
|
690
|
+
}
|
|
691
|
+
const frame = value;
|
|
692
|
+
const seq = this.validateFrameSequence(frame.seq, eventName);
|
|
693
|
+
const width = this.validateFrameCssDimension(frame.width, eventName, "width");
|
|
694
|
+
const height = this.validateFrameCssDimension(frame.height, eventName, "height");
|
|
695
|
+
const imageWidth = frame.image_width === undefined
|
|
696
|
+
? width
|
|
697
|
+
: this.validateFrameDimension(frame.image_width, eventName, "image_width");
|
|
698
|
+
const imageHeight = frame.image_height === undefined
|
|
699
|
+
? height
|
|
700
|
+
: this.validateFrameDimension(frame.image_height, eventName, "image_height");
|
|
701
|
+
const data = this.validateAttachEncodedData(frame.data, eventName);
|
|
702
|
+
return {
|
|
703
|
+
...frame,
|
|
704
|
+
seq,
|
|
705
|
+
width,
|
|
706
|
+
height,
|
|
707
|
+
image_width: imageWidth,
|
|
708
|
+
image_height: imageHeight,
|
|
709
|
+
data,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
validateFrameSequence(value, eventName) {
|
|
713
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
714
|
+
throw new CmuxProtocolError(`${eventName} seq is not a nonnegative integer`);
|
|
715
|
+
}
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
validateFrameCssDimension(value, eventName, field) {
|
|
719
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
720
|
+
throw new CmuxProtocolError(`${eventName} ${field} is not a nonnegative integer`);
|
|
721
|
+
}
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
validateFrameDimension(value, eventName, field) {
|
|
725
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
726
|
+
throw new CmuxProtocolError(`${eventName} ${field} is not a positive integer`);
|
|
727
|
+
}
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
validateAttachEncodedData(value, eventName) {
|
|
731
|
+
if (typeof value !== "string") {
|
|
732
|
+
throw new CmuxProtocolError(`${eventName} data is not base64 text`);
|
|
733
|
+
}
|
|
734
|
+
if (value.length > this.maxAttachEncodedChars) {
|
|
735
|
+
throw new CmuxProtocolError(`${eventName} data exceeds ${this.maxAttachEncodedChars} encoded characters`);
|
|
736
|
+
}
|
|
737
|
+
return value;
|
|
738
|
+
}
|
|
739
|
+
attachEventRetainedBytes(event) {
|
|
740
|
+
switch (event.event) {
|
|
741
|
+
case "vt-state":
|
|
742
|
+
case "output":
|
|
743
|
+
case "resized":
|
|
744
|
+
return event.data instanceof Uint8Array ? event.data.byteLength : 0;
|
|
745
|
+
case "frame":
|
|
746
|
+
return typeof event.data === "string" ? event.data.length : 0;
|
|
747
|
+
case "browser-state":
|
|
748
|
+
return new TextEncoder().encode(JSON.stringify(event)).byteLength;
|
|
749
|
+
default:
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
matchesAttachEvent(event, surface, mode) {
|
|
754
|
+
// colors-changed is scoped by its attach connection and intentionally has
|
|
755
|
+
// no surface field in protocol v6. Protocol v7 includes the surface id.
|
|
756
|
+
if (event.event === "colors-changed") {
|
|
757
|
+
return mode === "bytes" && (!("surface" in event) || event.surface === surface);
|
|
758
|
+
}
|
|
759
|
+
if (!("surface" in event) || event.surface !== surface)
|
|
760
|
+
return false;
|
|
761
|
+
if (event.event === "detached" || event.event === "scroll-changed"
|
|
762
|
+
|| this.isSurfaceOverflow(event, surface))
|
|
763
|
+
return true;
|
|
764
|
+
return mode === "render"
|
|
765
|
+
? event.event === "render-state" || event.event === "render-delta"
|
|
766
|
+
: event.event === "vt-state" || event.event === "output" || event.event === "resized"
|
|
767
|
+
|| event.event === "frame" || event.event === "browser-state";
|
|
768
|
+
}
|
|
769
|
+
isSurfaceOverflow(event, surface) {
|
|
770
|
+
return event.event === "overflow"
|
|
771
|
+
&& event.scope === "surface"
|
|
772
|
+
&& "surface" in event
|
|
773
|
+
&& (surface === undefined || event.surface === surface);
|
|
774
|
+
}
|
|
775
|
+
attachOnlyEvent(event) {
|
|
776
|
+
return event === "vt-state"
|
|
777
|
+
|| event === "output"
|
|
778
|
+
|| event === "resized"
|
|
779
|
+
|| event === "frame"
|
|
780
|
+
|| event === "browser-state"
|
|
781
|
+
|| event === "colors-changed"
|
|
782
|
+
|| event === "render-state"
|
|
783
|
+
|| event === "render-delta"
|
|
784
|
+
|| event === "detached";
|
|
785
|
+
}
|
|
786
|
+
dropUndefined(value) {
|
|
787
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
788
|
+
}
|
|
789
|
+
securityLimit(name, value, maximum) {
|
|
790
|
+
const limit = value ?? maximum;
|
|
791
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > maximum) {
|
|
792
|
+
throw new RangeError(`${name} must be an integer from 1 through ${maximum}`);
|
|
793
|
+
}
|
|
794
|
+
return limit;
|
|
795
|
+
}
|
|
796
|
+
nextId() {
|
|
797
|
+
return this.nextRequestId++;
|
|
798
|
+
}
|
|
799
|
+
}
|