@jhebe/web-terminal 0.2.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.
@@ -0,0 +1,352 @@
1
+ import { FitAddon as e } from "@xterm/addon-fit";
2
+ import { Terminal as t } from "@xterm/xterm";
3
+ //#region src/protocol.ts
4
+ function n(e) {
5
+ let t = r(e);
6
+ if (!(!t || typeof t.type != "string")) switch (t.type) {
7
+ case "sessions": return Array.isArray(t.sessions) && t.sessions.every(i) ? {
8
+ type: "sessions",
9
+ sessions: t.sessions
10
+ } : void 0;
11
+ case "opened": return a(t) && i(t.session) ? {
12
+ type: "opened",
13
+ requestId: t.requestId,
14
+ session: t.session
15
+ } : void 0;
16
+ case "closed":
17
+ case "written":
18
+ case "unobserved": return a(t) && typeof t.sessionId == "string" ? {
19
+ type: t.type,
20
+ requestId: t.requestId,
21
+ sessionId: t.sessionId
22
+ } : void 0;
23
+ case "observing": return a(t) && i(t.session) && typeof t.history == "string" ? {
24
+ type: "observing",
25
+ requestId: t.requestId,
26
+ session: t.session,
27
+ history: t.history
28
+ } : void 0;
29
+ case "attached": return a(t) && i(t.session) && typeof t.history == "string" ? {
30
+ type: "attached",
31
+ requestId: t.requestId,
32
+ session: t.session,
33
+ history: t.history
34
+ } : void 0;
35
+ case "detached": return a(t) && typeof t.sessionId == "string" ? {
36
+ type: "detached",
37
+ requestId: t.requestId,
38
+ sessionId: t.sessionId
39
+ } : void 0;
40
+ case "output": return typeof t.sessionId == "string" && typeof t.data == "string" ? {
41
+ type: "output",
42
+ sessionId: t.sessionId,
43
+ data: t.data
44
+ } : void 0;
45
+ case "exit": return typeof t.sessionId == "string" && typeof t.exitCode == "number" && (t.signal === void 0 || typeof t.signal == "number") ? {
46
+ type: "exit",
47
+ sessionId: t.sessionId,
48
+ exitCode: t.exitCode,
49
+ ...t.signal === void 0 ? {} : { signal: t.signal }
50
+ } : void 0;
51
+ case "error": return typeof t.message == "string" && (t.requestId === void 0 || typeof t.requestId == "string") ? {
52
+ type: "error",
53
+ message: t.message,
54
+ ...t.requestId === void 0 ? {} : { requestId: t.requestId }
55
+ } : void 0;
56
+ default: return;
57
+ }
58
+ }
59
+ function r(e) {
60
+ if (typeof e == "string") try {
61
+ let t = JSON.parse(e);
62
+ return o(t) ? t : void 0;
63
+ } catch {
64
+ return;
65
+ }
66
+ }
67
+ function i(e) {
68
+ return o(e) && typeof e.id == "string" && typeof e.name == "string" && (e.state === "running" || e.state === "exited") && typeof e.attached == "boolean" && typeof e.observers == "number" && typeof e.createdAt == "string" && typeof e.lastActiveAt == "string" && (e.exitCode === void 0 || typeof e.exitCode == "number");
69
+ }
70
+ function a(e) {
71
+ return typeof e.requestId == "string";
72
+ }
73
+ function o(e) {
74
+ return typeof e == "object" && !!e;
75
+ }
76
+ //#endregion
77
+ //#region src/terminal-view.ts
78
+ var s = class {
79
+ terminal;
80
+ fitAddon = new e();
81
+ target;
82
+ controller;
83
+ resizeObserver;
84
+ disposed = !1;
85
+ constructor(e) {
86
+ this.target = e.target, this.controller = e.controller, this.terminal = new t({
87
+ cursorBlink: !0,
88
+ convertEol: !0,
89
+ fontFamily: "\"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, monospace",
90
+ fontSize: 14,
91
+ theme: {
92
+ background: "#111318",
93
+ foreground: "#e7e9ee",
94
+ cursor: "#8bd5ca",
95
+ selectionBackground: "#3b4252"
96
+ },
97
+ ...e.terminalOptions
98
+ }), this.target.classList.add("web-terminal"), this.terminal.loadAddon(this.fitAddon), this.terminal.open(this.target), this.terminal.onData((e) => this.controller.input(e)), this.terminal.onResize(({ cols: e, rows: t }) => this.controller.resize(e, t)), this.resizeObserver = new ResizeObserver(() => this.fit()), this.resizeObserver.observe(this.target), this.fit();
99
+ }
100
+ write(e) {
101
+ this.disposed || this.terminal.write(e);
102
+ }
103
+ focus() {
104
+ this.assertActive(), this.terminal.focus();
105
+ }
106
+ fit() {
107
+ this.disposed || this.fitAddon.fit();
108
+ }
109
+ dispose() {
110
+ this.disposed || (this.disposeVisuals(), this.controller.release(this));
111
+ }
112
+ disposeVisuals() {
113
+ this.disposed || (this.disposed = !0, this.resizeObserver.disconnect(), this.terminal.dispose(), this.target.classList.remove("web-terminal"));
114
+ }
115
+ assertActive() {
116
+ if (this.disposed) throw Error("This terminal view has been disposed.");
117
+ }
118
+ }, c = class {
119
+ options;
120
+ sessionListeners = /* @__PURE__ */ new Set();
121
+ eventListeners = /* @__PURE__ */ new Set();
122
+ pending = /* @__PURE__ */ new Map();
123
+ socket;
124
+ connectPromise;
125
+ sessionSnapshot = [];
126
+ attachedSessionId;
127
+ view;
128
+ viewSessionId;
129
+ bufferedOutput = [];
130
+ constructor(e) {
131
+ this.options = e, e.autoConnect !== !1 && this.connect().catch((e) => this.reportError(d(e)));
132
+ }
133
+ get sessions() {
134
+ return this.sessionSnapshot;
135
+ }
136
+ get connected() {
137
+ return this.socket?.readyState === WebSocket.OPEN;
138
+ }
139
+ get attachedSession() {
140
+ return this.attachedSessionId;
141
+ }
142
+ connect() {
143
+ return this.connected ? Promise.resolve() : (this.connectPromise ||= new Promise((e, t) => {
144
+ let r = typeof this.options.socketUrl == "function" ? this.options.socketUrl() : this.options.socketUrl, i = new WebSocket(r);
145
+ this.socket = i, i.addEventListener("open", () => {
146
+ this.connectPromise = void 0, this.emitEvent({ type: "connected" }), e();
147
+ }, { once: !0 }), i.addEventListener("message", (e) => {
148
+ this.handleMessage(n(e.data));
149
+ }), i.addEventListener("error", () => {
150
+ let e = /* @__PURE__ */ Error("Terminal WebSocket connection failed.");
151
+ this.connectPromise && (this.connectPromise = void 0, t(e)), this.reportError(e);
152
+ }), i.addEventListener("close", () => {
153
+ this.socket === i && (this.socket = void 0, this.connectPromise = void 0, this.attachedSessionId = void 0, this.rejectPending(/* @__PURE__ */ Error("Terminal connection closed before the operation completed.")), this.emitEvent({ type: "disconnected" }));
154
+ });
155
+ }), this.connectPromise);
156
+ }
157
+ disconnect() {
158
+ this.socket?.close(1e3, "Client disconnected");
159
+ }
160
+ async open(e = {}) {
161
+ let t = await this.request({
162
+ type: "open",
163
+ requestId: u(),
164
+ ...e.name === void 0 ? {} : { name: e.name }
165
+ });
166
+ if (t.type !== "opened") throw Error("Unexpected response while opening a terminal session.");
167
+ return this.emitEvent({
168
+ type: "opened",
169
+ sessionId: t.session.id
170
+ }), t.session;
171
+ }
172
+ async close(e) {
173
+ if ((await this.request({
174
+ type: "close",
175
+ requestId: u(),
176
+ sessionId: e
177
+ })).type !== "closed") throw Error("Unexpected response while closing a terminal session.");
178
+ this.attachedSessionId === e && (this.attachedSessionId = void 0, this.disposeView()), this.emitEvent({
179
+ type: "closed",
180
+ sessionId: e
181
+ });
182
+ }
183
+ async write(e, t) {
184
+ if ((await this.request({
185
+ type: "write",
186
+ requestId: u(),
187
+ sessionId: e,
188
+ data: t
189
+ })).type !== "written") throw Error("Unexpected response while writing to a terminal session.");
190
+ }
191
+ async attach(e) {
192
+ let t = await this.request({
193
+ type: "attach",
194
+ requestId: u(),
195
+ sessionId: e
196
+ });
197
+ if (t.type !== "attached") throw Error("Unexpected response while attaching a terminal session.");
198
+ return this.attachedSessionId = e, this.bufferedOutput = [], this.emitEvent({
199
+ type: "attached",
200
+ sessionId: e
201
+ }), {
202
+ session: t.session,
203
+ history: t.history
204
+ };
205
+ }
206
+ async detach() {
207
+ let e = this.attachedSessionId;
208
+ if (!e) {
209
+ this.disposeView();
210
+ return;
211
+ }
212
+ await this.detachSession(e);
213
+ }
214
+ async detachSession(e) {
215
+ if ((await this.request({
216
+ type: "detach",
217
+ requestId: u(),
218
+ sessionId: e
219
+ })).type !== "detached") throw Error("Unexpected response while detaching a terminal session.");
220
+ this.attachedSessionId === e && (this.attachedSessionId = void 0), this.viewSessionId === e && this.disposeView(), this.emitEvent({
221
+ type: "detached",
222
+ sessionId: e
223
+ });
224
+ }
225
+ async display(e, t, n = {}) {
226
+ let r = await this.attach(e);
227
+ this.disposeView();
228
+ let i = new s({
229
+ target: t,
230
+ controller: {
231
+ input: (e) => this.input(e),
232
+ resize: (e, t) => this.resize(e, t),
233
+ release: (e) => this.releaseView(e)
234
+ },
235
+ ...n.terminalOptions === void 0 ? {} : { terminalOptions: n.terminalOptions }
236
+ });
237
+ this.view = i, this.viewSessionId = e, i.write(r.history);
238
+ for (let e of this.bufferedOutput) i.write(e);
239
+ return this.bufferedOutput = [], i.focus(), i;
240
+ }
241
+ subscribeSessions(e) {
242
+ return this.sessionListeners.add(e), e(this.sessionSnapshot), () => this.sessionListeners.delete(e);
243
+ }
244
+ subscribeEvents(e) {
245
+ return this.eventListeners.add(e), () => this.eventListeners.delete(e);
246
+ }
247
+ dispose() {
248
+ this.disposeView(), this.disconnect(), this.sessionListeners.clear(), this.eventListeners.clear();
249
+ }
250
+ input(e) {
251
+ this.attachedSessionId && this.send({
252
+ type: "input",
253
+ sessionId: this.attachedSessionId,
254
+ data: e
255
+ });
256
+ }
257
+ resize(e, t) {
258
+ this.attachedSessionId && this.send({
259
+ type: "resize",
260
+ sessionId: this.attachedSessionId,
261
+ cols: e,
262
+ rows: t
263
+ });
264
+ }
265
+ releaseView(e) {
266
+ if (this.view !== e) return;
267
+ let t = this.viewSessionId;
268
+ this.view = void 0, this.viewSessionId = void 0, t && this.detachSession(t).catch((e) => this.reportError(d(e)));
269
+ }
270
+ disposeView() {
271
+ let e = this.view;
272
+ this.view = void 0, this.viewSessionId = void 0, e?.disposeVisuals();
273
+ }
274
+ async request(e) {
275
+ if (!("requestId" in e)) throw Error("Request messages require an identity.");
276
+ return await this.connect(), new Promise((t, n) => {
277
+ this.pending.set(e.requestId, {
278
+ resolve: t,
279
+ reject: n
280
+ });
281
+ try {
282
+ this.send(e);
283
+ } catch (t) {
284
+ this.pending.delete(e.requestId), n(d(t));
285
+ }
286
+ });
287
+ }
288
+ send(e) {
289
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) throw Error("Terminal client is not connected.");
290
+ this.socket.send(JSON.stringify(e));
291
+ }
292
+ handleMessage(e) {
293
+ if (!e) {
294
+ this.reportError(/* @__PURE__ */ Error("Received an invalid terminal server message."));
295
+ return;
296
+ }
297
+ if (e.type === "sessions") {
298
+ this.sessionSnapshot = Object.freeze(e.sessions.map((e) => Object.freeze({ ...e })));
299
+ let t = this.attachedSessionId;
300
+ t && !this.sessionSnapshot.some((e) => e.id === t) && (this.attachedSessionId = void 0, this.disposeView(), this.emitEvent({
301
+ type: "detached",
302
+ sessionId: t
303
+ }));
304
+ for (let e of this.sessionListeners) e(this.sessionSnapshot);
305
+ return;
306
+ }
307
+ if (e.type === "output") {
308
+ if (e.sessionId !== this.attachedSessionId) return;
309
+ this.view ? this.view.write(e.data) : this.bufferedOutput.push(e.data);
310
+ return;
311
+ }
312
+ if (e.type === "exit") {
313
+ this.emitEvent({
314
+ type: "exit",
315
+ sessionId: e.sessionId,
316
+ exitCode: e.exitCode
317
+ });
318
+ return;
319
+ }
320
+ if (e.type === "error") {
321
+ let t = Error(e.message);
322
+ if (e.requestId) {
323
+ let n = this.pending.get(e.requestId);
324
+ this.pending.delete(e.requestId), n?.reject(t);
325
+ } else this.reportError(t);
326
+ return;
327
+ }
328
+ let t = this.pending.get(e.requestId);
329
+ t && (this.pending.delete(e.requestId), t.resolve(e));
330
+ }
331
+ rejectPending(e) {
332
+ for (let t of this.pending.values()) t.reject(e);
333
+ this.pending.clear();
334
+ }
335
+ emitEvent(e) {
336
+ for (let t of this.eventListeners) t(e);
337
+ }
338
+ reportError(e) {
339
+ this.options.onError ? this.options.onError(e) : console.error(e);
340
+ }
341
+ };
342
+ function l(e) {
343
+ return new c(e);
344
+ }
345
+ function u() {
346
+ return crypto.randomUUID();
347
+ }
348
+ function d(e) {
349
+ return e instanceof Error ? e : Error(String(e));
350
+ }
351
+ //#endregion
352
+ export { c as TerminalClient, s as TerminalView, l as createTerminalClient };
@@ -0,0 +1,8 @@
1
+ export declare class HistoryBuffer {
2
+ private readonly limitBytes;
3
+ private chunks;
4
+ private totalBytes;
5
+ constructor(limitBytes: number);
6
+ append(data: string): void;
7
+ read(): string;
8
+ }
@@ -0,0 +1,59 @@
1
+ export class HistoryBuffer {
2
+ limitBytes;
3
+ chunks = [];
4
+ totalBytes = 0;
5
+ constructor(limitBytes) {
6
+ this.limitBytes = limitBytes;
7
+ }
8
+ append(data) {
9
+ if (this.limitBytes === 0 || data.length === 0) {
10
+ return;
11
+ }
12
+ let normalized = data;
13
+ let bytes = Buffer.byteLength(normalized);
14
+ if (bytes > this.limitBytes) {
15
+ normalized = trimToLastBytes(normalized, this.limitBytes);
16
+ bytes = Buffer.byteLength(normalized);
17
+ this.chunks = [];
18
+ this.totalBytes = 0;
19
+ }
20
+ this.chunks.push({ data: normalized, bytes });
21
+ this.totalBytes += bytes;
22
+ while (this.totalBytes > this.limitBytes &&
23
+ this.chunks.length > 1) {
24
+ const removed = this.chunks.shift();
25
+ if (removed) {
26
+ this.totalBytes -= removed.bytes;
27
+ }
28
+ }
29
+ const first = this.chunks[0];
30
+ if (first && this.totalBytes > this.limitBytes) {
31
+ const excess = this.totalBytes - this.limitBytes;
32
+ first.data = trimToLastBytes(first.data, first.bytes - excess);
33
+ const nextBytes = Buffer.byteLength(first.data);
34
+ this.totalBytes += nextBytes - first.bytes;
35
+ first.bytes = nextBytes;
36
+ }
37
+ }
38
+ read() {
39
+ return this.chunks.map(({ data }) => data).join('');
40
+ }
41
+ }
42
+ function trimToLastBytes(value, limitBytes) {
43
+ const characters = Array.from(value);
44
+ let bytes = 0;
45
+ let start = characters.length;
46
+ while (start > 0) {
47
+ const character = characters[start - 1];
48
+ if (character === undefined) {
49
+ break;
50
+ }
51
+ const characterBytes = Buffer.byteLength(character);
52
+ if (bytes + characterBytes > limitBytes) {
53
+ break;
54
+ }
55
+ bytes += characterBytes;
56
+ start -= 1;
57
+ }
58
+ return characters.slice(start).join('');
59
+ }
@@ -0,0 +1,2 @@
1
+ export { createTerminalSessionManager, type OpenSessionOptions, type SessionAttachment, type SessionEvent, type SessionObservation, type SessionSink, type TerminalSessionManager, type TerminalSessionManagerOptions, } from './session-manager.js';
2
+ export { attachTerminalServer, type TerminalAction, type TerminalServer, type TerminalServerOptions, } from './terminal-server.js';
@@ -0,0 +1,2 @@
1
+ export { createTerminalSessionManager, } from './session-manager.js';
2
+ export { attachTerminalServer, } from './terminal-server.js';
@@ -0,0 +1,51 @@
1
+ import type { SessionSummary } from '../src/protocol.js';
2
+ export interface TerminalSessionManagerOptions {
3
+ shell?: string;
4
+ shellArgs?: string[];
5
+ cwd?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ historyLimitBytes?: number;
8
+ idleTimeoutMs?: number;
9
+ maxSessions?: number;
10
+ maxObserversPerSession?: number;
11
+ maxWriteBytes?: number;
12
+ maxInputBytes?: number;
13
+ }
14
+ export interface OpenSessionOptions {
15
+ name?: string;
16
+ shell?: string;
17
+ shellArgs?: string[];
18
+ cwd?: string;
19
+ env?: NodeJS.ProcessEnv;
20
+ }
21
+ export interface SessionSink {
22
+ output(data: string): void;
23
+ exit(exitCode: number, signal?: number): void;
24
+ }
25
+ export interface SessionAttachment {
26
+ readonly session: SessionSummary;
27
+ readonly history: string;
28
+ }
29
+ export interface SessionObservation {
30
+ readonly session: SessionSummary;
31
+ readonly history: string;
32
+ }
33
+ export interface SessionEvent {
34
+ readonly type: 'opened' | 'attached' | 'detached' | 'exited' | 'closed' | 'expired';
35
+ readonly session: SessionSummary;
36
+ }
37
+ export interface TerminalSessionManager {
38
+ readonly sessions: readonly SessionSummary[];
39
+ open(options?: OpenSessionOptions): SessionSummary;
40
+ close(sessionId: string): void;
41
+ write(sessionId: string, data: string): void;
42
+ observe(sessionId: string, observerId: string, sink: SessionSink): SessionObservation;
43
+ unobserve(sessionId: string, observerId: string): void;
44
+ attach(sessionId: string, attachmentId: string, sink: SessionSink): SessionAttachment;
45
+ detach(sessionId: string, attachmentId: string): void;
46
+ input(sessionId: string, attachmentId: string, data: string): void;
47
+ resize(sessionId: string, attachmentId: string, cols: number, rows: number): void;
48
+ subscribe(listener: (event: SessionEvent) => void): () => void;
49
+ shutdown(): void;
50
+ }
51
+ export declare function createTerminalSessionManager(options?: TerminalSessionManagerOptions): TerminalSessionManager;