@aitty/server 0.1.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/dist/cli-token.d.ts +5 -0
- package/dist/cli-token.js +14 -0
- package/dist/frontend/browser-shell.d.ts +34 -0
- package/dist/frontend/browser-shell.html +218 -0
- package/dist/frontend/browser-shell.js +291 -0
- package/dist/frontend/terminal-app.js +4780 -0
- package/dist/frontend/terminal.css +268 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/logging.d.ts +17 -0
- package/dist/logging.js +34 -0
- package/dist/network-policy.d.ts +10 -0
- package/dist/network-policy.js +88 -0
- package/dist/runtime/dependencies.d.ts +16 -0
- package/dist/runtime/dependencies.js +16 -0
- package/dist/runtime/output-buffer.d.ts +11 -0
- package/dist/runtime/output-buffer.js +202 -0
- package/dist/runtime/pty-session.d.ts +59 -0
- package/dist/runtime/pty-session.js +247 -0
- package/dist/runtime/websocket-transport.d.ts +32 -0
- package/dist/runtime/websocket-transport.js +465 -0
- package/dist/server.d.ts +51 -0
- package/dist/server.js +234 -0
- package/dist/theme-source.d.ts +2 -0
- package/dist/theme-source.js +2 -0
- package/package.json +73 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//#region src/runtime/output-buffer.ts
|
|
2
|
+
const DEFAULT_OUTPUT_BUFFER_LIMIT_BYTES = 256 * 1024;
|
|
3
|
+
const MIN_OUTPUT_BUFFER_LIMIT_BYTES = 1024;
|
|
4
|
+
const MAX_OUTPUT_BUFFER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
5
|
+
const ESCAPE_BYTE = 27;
|
|
6
|
+
const OSC_TERMINATOR_BYTE = 7;
|
|
7
|
+
const STRING_TERMINATOR_BYTE = 92;
|
|
8
|
+
const INITIAL_REPLAY_BOUNDARY_STATE = {
|
|
9
|
+
ansiMode: "ground",
|
|
10
|
+
utf8Remaining: 0
|
|
11
|
+
};
|
|
12
|
+
function createOutputBuffer(limitBytes = DEFAULT_OUTPUT_BUFFER_LIMIT_BYTES) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let byteLength = 0;
|
|
15
|
+
let leadingState = cloneReplayBoundaryState(INITIAL_REPLAY_BOUNDARY_STATE);
|
|
16
|
+
return {
|
|
17
|
+
append(chunk) {
|
|
18
|
+
const normalizedChunk = Buffer.from(chunk);
|
|
19
|
+
chunks.push(normalizedChunk);
|
|
20
|
+
byteLength += normalizedChunk.length;
|
|
21
|
+
while (byteLength > limitBytes && chunks.length > 0) {
|
|
22
|
+
const oldestChunk = chunks[0];
|
|
23
|
+
if (!oldestChunk) break;
|
|
24
|
+
const overflow = byteLength - limitBytes;
|
|
25
|
+
if (oldestChunk.length <= overflow) {
|
|
26
|
+
leadingState = advanceReplayBoundaryState(leadingState, oldestChunk);
|
|
27
|
+
chunks.shift();
|
|
28
|
+
byteLength -= oldestChunk.length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
leadingState = advanceReplayBoundaryState(leadingState, oldestChunk.subarray(0, overflow));
|
|
32
|
+
chunks[0] = oldestChunk.subarray(overflow);
|
|
33
|
+
byteLength -= overflow;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
snapshot() {
|
|
37
|
+
if (chunks.length === 0) return null;
|
|
38
|
+
return sanitizeWrappedSnapshot(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks), leadingState);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sanitizeWrappedSnapshot(snapshot, leadingState) {
|
|
43
|
+
const safeOffset = findSafeSnapshotOffset(snapshot, leadingState);
|
|
44
|
+
if (safeOffset >= snapshot.length) return null;
|
|
45
|
+
return safeOffset === 0 ? snapshot : snapshot.subarray(safeOffset);
|
|
46
|
+
}
|
|
47
|
+
function findSafeSnapshotOffset(snapshot, leadingState) {
|
|
48
|
+
const state = cloneReplayBoundaryState(leadingState);
|
|
49
|
+
if (isSafeReplayBoundary(state)) return 0;
|
|
50
|
+
for (let offset = 0; offset < snapshot.length; offset += 1) {
|
|
51
|
+
const byte = snapshot[offset];
|
|
52
|
+
if (canResynchronizeBeforeByte(state, byte)) return offset;
|
|
53
|
+
advanceReplayBoundaryStateByte(state, byte);
|
|
54
|
+
if (isSafeReplayBoundary(state)) return offset + 1;
|
|
55
|
+
}
|
|
56
|
+
return snapshot.length;
|
|
57
|
+
}
|
|
58
|
+
function advanceReplayBoundaryState(state, chunk) {
|
|
59
|
+
const nextState = cloneReplayBoundaryState(state);
|
|
60
|
+
for (let index = 0; index < chunk.length; index += 1) advanceReplayBoundaryStateByte(nextState, chunk[index]);
|
|
61
|
+
return nextState;
|
|
62
|
+
}
|
|
63
|
+
function advanceReplayBoundaryStateByte(state, byte) {
|
|
64
|
+
switch (state.ansiMode) {
|
|
65
|
+
case "ground":
|
|
66
|
+
advanceGroundReplayBoundaryState(state, byte);
|
|
67
|
+
return;
|
|
68
|
+
case "escape":
|
|
69
|
+
advanceEscapeReplayBoundaryState(state, byte);
|
|
70
|
+
return;
|
|
71
|
+
case "escape-intermediate":
|
|
72
|
+
if (byte === ESCAPE_BYTE) {
|
|
73
|
+
state.ansiMode = "escape";
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (byte >= 32 && byte <= 47) return;
|
|
77
|
+
state.ansiMode = "ground";
|
|
78
|
+
return;
|
|
79
|
+
case "csi":
|
|
80
|
+
if (byte === ESCAPE_BYTE) {
|
|
81
|
+
state.ansiMode = "escape";
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (byte >= 64 && byte <= 126) state.ansiMode = "ground";
|
|
85
|
+
return;
|
|
86
|
+
case "osc":
|
|
87
|
+
if (byte === OSC_TERMINATOR_BYTE) {
|
|
88
|
+
state.ansiMode = "ground";
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (byte === ESCAPE_BYTE) state.ansiMode = "osc-escape";
|
|
92
|
+
return;
|
|
93
|
+
case "osc-escape":
|
|
94
|
+
if (byte === STRING_TERMINATOR_BYTE) {
|
|
95
|
+
state.ansiMode = "ground";
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
state.ansiMode = byte === ESCAPE_BYTE ? "osc-escape" : "osc";
|
|
99
|
+
return;
|
|
100
|
+
case "dcs":
|
|
101
|
+
if (byte === ESCAPE_BYTE) state.ansiMode = "dcs-escape";
|
|
102
|
+
return;
|
|
103
|
+
case "dcs-escape":
|
|
104
|
+
if (byte === STRING_TERMINATOR_BYTE) {
|
|
105
|
+
state.ansiMode = "ground";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
state.ansiMode = byte === ESCAPE_BYTE ? "dcs-escape" : "dcs";
|
|
109
|
+
return;
|
|
110
|
+
case "control-string":
|
|
111
|
+
if (byte === ESCAPE_BYTE) state.ansiMode = "control-string-escape";
|
|
112
|
+
return;
|
|
113
|
+
case "control-string-escape":
|
|
114
|
+
if (byte === STRING_TERMINATOR_BYTE) {
|
|
115
|
+
state.ansiMode = "ground";
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
state.ansiMode = byte === ESCAPE_BYTE ? "control-string-escape" : "control-string";
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function advanceGroundReplayBoundaryState(state, byte) {
|
|
123
|
+
if (state.utf8Remaining > 0) {
|
|
124
|
+
if (isUtf8ContinuationByte(byte)) {
|
|
125
|
+
state.utf8Remaining -= 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
state.utf8Remaining = 0;
|
|
129
|
+
}
|
|
130
|
+
if (byte === ESCAPE_BYTE) {
|
|
131
|
+
state.ansiMode = "escape";
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (byte === 155) {
|
|
135
|
+
state.ansiMode = "csi";
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (byte === 157) {
|
|
139
|
+
state.ansiMode = "osc";
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (byte === 144) {
|
|
143
|
+
state.ansiMode = "dcs";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (byte === 152 || byte === 158 || byte === 159) {
|
|
147
|
+
state.ansiMode = "control-string";
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
state.utf8Remaining = getUtf8ContinuationCount(byte);
|
|
151
|
+
}
|
|
152
|
+
function advanceEscapeReplayBoundaryState(state, byte) {
|
|
153
|
+
if (byte === ESCAPE_BYTE) return;
|
|
154
|
+
if (byte === 91) {
|
|
155
|
+
state.ansiMode = "csi";
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (byte === 93) {
|
|
159
|
+
state.ansiMode = "osc";
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (byte === 80) {
|
|
163
|
+
state.ansiMode = "dcs";
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (byte === 88 || byte === 94 || byte === 95) {
|
|
167
|
+
state.ansiMode = "control-string";
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (byte >= 32 && byte <= 47) {
|
|
171
|
+
state.ansiMode = "escape-intermediate";
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
state.ansiMode = "ground";
|
|
175
|
+
}
|
|
176
|
+
function cloneReplayBoundaryState(state) {
|
|
177
|
+
return {
|
|
178
|
+
ansiMode: state.ansiMode,
|
|
179
|
+
utf8Remaining: state.utf8Remaining
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function isSafeReplayBoundary(state) {
|
|
183
|
+
return state.ansiMode === "ground" && state.utf8Remaining === 0;
|
|
184
|
+
}
|
|
185
|
+
function canResynchronizeBeforeByte(state, byte) {
|
|
186
|
+
if (state.ansiMode === "ground") return state.utf8Remaining > 0 && !isUtf8ContinuationByte(byte);
|
|
187
|
+
return (state.ansiMode === "escape" || state.ansiMode === "escape-intermediate" || state.ansiMode === "csi") && isAnsiSequenceStartByte(byte);
|
|
188
|
+
}
|
|
189
|
+
function isAnsiSequenceStartByte(byte) {
|
|
190
|
+
return byte === ESCAPE_BYTE || byte === 144 || byte === 152 || byte === 155 || byte === 157 || byte === 158 || byte === 159;
|
|
191
|
+
}
|
|
192
|
+
function isUtf8ContinuationByte(byte) {
|
|
193
|
+
return byte >= 128 && byte <= 191;
|
|
194
|
+
}
|
|
195
|
+
function getUtf8ContinuationCount(byte) {
|
|
196
|
+
if (byte >= 194 && byte <= 223) return 1;
|
|
197
|
+
if (byte >= 224 && byte <= 239) return 2;
|
|
198
|
+
if (byte >= 240 && byte <= 244) return 3;
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { DEFAULT_OUTPUT_BUFFER_LIMIT_BYTES, MAX_OUTPUT_BUFFER_LIMIT_BYTES, MIN_OUTPUT_BUFFER_LIMIT_BYTES, createOutputBuffer };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/runtime/pty-session.d.ts
|
|
2
|
+
declare const DEFAULT_TERM = "xterm-256color";
|
|
3
|
+
declare const DEFAULT_COLUMNS = 80;
|
|
4
|
+
declare const DEFAULT_ROWS = 24;
|
|
5
|
+
interface Disposable {
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
interface PtyExitEvent {
|
|
9
|
+
exitCode: number;
|
|
10
|
+
signal?: number;
|
|
11
|
+
}
|
|
12
|
+
interface PtyProcess {
|
|
13
|
+
pid: number;
|
|
14
|
+
kill(signal?: string): void;
|
|
15
|
+
onData(listener: (data: Buffer | string) => void): Disposable;
|
|
16
|
+
onExit(listener: (event: PtyExitEvent) => void): Disposable;
|
|
17
|
+
resize(columns: number, rows: number): void;
|
|
18
|
+
write(data: string | Buffer): void;
|
|
19
|
+
}
|
|
20
|
+
interface PtyModule {
|
|
21
|
+
spawn(file: string, args: string[], options: PtySpawnOptions): PtyProcess;
|
|
22
|
+
}
|
|
23
|
+
interface PtySessionOptions {
|
|
24
|
+
args?: string[];
|
|
25
|
+
bufferSize?: number;
|
|
26
|
+
cols?: number;
|
|
27
|
+
cwd: string;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
file: string;
|
|
30
|
+
rows?: number;
|
|
31
|
+
term?: string;
|
|
32
|
+
}
|
|
33
|
+
interface PtySpawnOptions {
|
|
34
|
+
cols: number;
|
|
35
|
+
cwd: string;
|
|
36
|
+
encoding: null;
|
|
37
|
+
env: Record<string, string>;
|
|
38
|
+
name: string;
|
|
39
|
+
rows: number;
|
|
40
|
+
}
|
|
41
|
+
interface PtySession {
|
|
42
|
+
readonly closed: Promise<PtyExitEvent>;
|
|
43
|
+
readonly cols: number;
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
readonly file: string;
|
|
46
|
+
readonly pid: number;
|
|
47
|
+
readonly rows: number;
|
|
48
|
+
readonly term: string;
|
|
49
|
+
close(signal?: NodeJS.Signals): Promise<PtyExitEvent>;
|
|
50
|
+
getBufferedOutput(): Buffer | null;
|
|
51
|
+
kill(signal?: string): void;
|
|
52
|
+
onData(listener: (data: Buffer) => void): Disposable;
|
|
53
|
+
onExit(listener: (event: PtyExitEvent) => void): Disposable;
|
|
54
|
+
resize(columns: number, rows: number): void;
|
|
55
|
+
write(data: string | Buffer): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
declare function createPtySession(options: PtySessionOptions, nodePty: PtyModule): PtySession;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { DEFAULT_COLUMNS, DEFAULT_ROWS, DEFAULT_TERM, Disposable, PtyExitEvent, PtyModule, PtyProcess, PtySession, PtySessionOptions, PtySpawnOptions, createPtySession };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createOutputBuffer } from "./output-buffer.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
//#region src/runtime/pty-session.ts
|
|
4
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 1e3;
|
|
5
|
+
const BULK_PASTE_THRESHOLD_BYTES = 1024;
|
|
6
|
+
const BULK_PASTE_SETTLE_MS = 75;
|
|
7
|
+
const LARGE_WRITE_CHUNK_BYTES = 512;
|
|
8
|
+
const DEFAULT_TERM = "xterm-256color";
|
|
9
|
+
const DEFAULT_COLUMNS = 80;
|
|
10
|
+
const DEFAULT_ROWS = 24;
|
|
11
|
+
function createPtySession(options, nodePty) {
|
|
12
|
+
const cols = options.cols ?? 80;
|
|
13
|
+
const rows = options.rows ?? 24;
|
|
14
|
+
const term = resolveTerm(options.term, options.env?.TERM);
|
|
15
|
+
const ptyProcess = nodePty.spawn(options.file, options.args ?? [], {
|
|
16
|
+
cols,
|
|
17
|
+
cwd: options.cwd,
|
|
18
|
+
encoding: null,
|
|
19
|
+
env: toPtyEnvironment(options.env, term),
|
|
20
|
+
name: term,
|
|
21
|
+
rows
|
|
22
|
+
});
|
|
23
|
+
const ttyPath = resolvePtyPath(ptyProcess);
|
|
24
|
+
let currentCols = cols;
|
|
25
|
+
let currentRows = rows;
|
|
26
|
+
const dataListeners = /* @__PURE__ */ new Set();
|
|
27
|
+
const outputBuffer = createOutputBuffer(options.bufferSize);
|
|
28
|
+
let settled = false;
|
|
29
|
+
let resolvedExit;
|
|
30
|
+
let resolveClosed;
|
|
31
|
+
const closed = new Promise((resolve) => {
|
|
32
|
+
resolveClosed = resolve;
|
|
33
|
+
});
|
|
34
|
+
const settleExit = (event) => {
|
|
35
|
+
if (settled) return resolvedExit ?? event;
|
|
36
|
+
settled = true;
|
|
37
|
+
resolvedExit = event;
|
|
38
|
+
resolveClosed(resolvedExit);
|
|
39
|
+
return resolvedExit;
|
|
40
|
+
};
|
|
41
|
+
const exitSubscription = ptyProcess.onExit((event) => {
|
|
42
|
+
settleExit({
|
|
43
|
+
exitCode: event.exitCode,
|
|
44
|
+
signal: event.signal
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
const dataSubscription = ptyProcess.onData((data) => {
|
|
48
|
+
const chunk = normalizeChunk(data);
|
|
49
|
+
outputBuffer.append(chunk);
|
|
50
|
+
for (const listener of dataListeners) listener(chunk);
|
|
51
|
+
});
|
|
52
|
+
closed.finally(() => {
|
|
53
|
+
dataSubscription.dispose();
|
|
54
|
+
exitSubscription.dispose();
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
closed,
|
|
58
|
+
get cols() {
|
|
59
|
+
return currentCols;
|
|
60
|
+
},
|
|
61
|
+
cwd: options.cwd,
|
|
62
|
+
file: options.file,
|
|
63
|
+
getBufferedOutput() {
|
|
64
|
+
return outputBuffer.snapshot();
|
|
65
|
+
},
|
|
66
|
+
pid: ptyProcess.pid,
|
|
67
|
+
get rows() {
|
|
68
|
+
return currentRows;
|
|
69
|
+
},
|
|
70
|
+
term,
|
|
71
|
+
async close(signal = "SIGTERM") {
|
|
72
|
+
if (settled && resolvedExit) return resolvedExit;
|
|
73
|
+
sendSignal(ptyProcess, signal);
|
|
74
|
+
const gracefulExit = await waitForClosed(closed, DEFAULT_CLOSE_TIMEOUT_MS, resolvedExit);
|
|
75
|
+
if (gracefulExit) return gracefulExit;
|
|
76
|
+
if (!isProcessRunning(ptyProcess.pid)) return settleExit({ exitCode: 0 });
|
|
77
|
+
sendSignal(ptyProcess, "SIGKILL");
|
|
78
|
+
const forcedExit = await waitForClosed(closed, DEFAULT_CLOSE_TIMEOUT_MS, resolvedExit);
|
|
79
|
+
if (forcedExit) return forcedExit;
|
|
80
|
+
if (!isProcessRunning(ptyProcess.pid)) return settleExit({ exitCode: 0 });
|
|
81
|
+
throw new Error("PTY process did not exit after SIGKILL");
|
|
82
|
+
},
|
|
83
|
+
kill(signal) {
|
|
84
|
+
sendSignal(ptyProcess, signal);
|
|
85
|
+
},
|
|
86
|
+
onData(listener) {
|
|
87
|
+
dataListeners.add(listener);
|
|
88
|
+
return { dispose() {
|
|
89
|
+
dataListeners.delete(listener);
|
|
90
|
+
} };
|
|
91
|
+
},
|
|
92
|
+
onExit(listener) {
|
|
93
|
+
return ptyProcess.onExit(listener);
|
|
94
|
+
},
|
|
95
|
+
resize(columns, nextRows) {
|
|
96
|
+
if (!isValidResizeDimension(columns) || !isValidResizeDimension(nextRows)) return;
|
|
97
|
+
if (columns === currentCols && nextRows === currentRows) return;
|
|
98
|
+
currentCols = columns;
|
|
99
|
+
currentRows = nextRows;
|
|
100
|
+
ptyProcess.resize(columns, nextRows);
|
|
101
|
+
},
|
|
102
|
+
async write(data) {
|
|
103
|
+
const chunk = normalizeChunk(data);
|
|
104
|
+
if (!ttyPath || !shouldUseBulkPasteMode(chunk)) {
|
|
105
|
+
await writeChunked(ptyProcess, chunk);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const ttyState = await captureBulkPasteTtyState(ttyPath);
|
|
109
|
+
if (!ttyState?.canonical) {
|
|
110
|
+
await writeChunked(ptyProcess, chunk);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await runSttyCommand(ttyPath, [
|
|
115
|
+
"-icanon",
|
|
116
|
+
"min",
|
|
117
|
+
"1",
|
|
118
|
+
"time",
|
|
119
|
+
"0"
|
|
120
|
+
]);
|
|
121
|
+
await writeChunked(ptyProcess, chunk);
|
|
122
|
+
await delay(BULK_PASTE_SETTLE_MS);
|
|
123
|
+
} finally {
|
|
124
|
+
await runSttyCommand(ttyPath, [ttyState.serialized]).catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function resolveTerm(explicitTerm, environmentTerm) {
|
|
130
|
+
const candidate = explicitTerm?.trim() || environmentTerm?.trim();
|
|
131
|
+
if (!candidate || candidate.toLowerCase() === "dumb") return DEFAULT_TERM;
|
|
132
|
+
return candidate;
|
|
133
|
+
}
|
|
134
|
+
function toPtyEnvironment(environment, term) {
|
|
135
|
+
const normalizedEnvironment = Object.fromEntries(Object.entries(environment ?? process.env).filter((entry) => typeof entry[1] === "string"));
|
|
136
|
+
normalizedEnvironment.TERM = term;
|
|
137
|
+
return normalizedEnvironment;
|
|
138
|
+
}
|
|
139
|
+
function sendSignal(ptyProcess, signal) {
|
|
140
|
+
try {
|
|
141
|
+
ptyProcess.kill(signal);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!isMissingProcessError(error)) throw error;
|
|
144
|
+
}
|
|
145
|
+
if (!signal) return;
|
|
146
|
+
try {
|
|
147
|
+
process.kill(ptyProcess.pid, signal);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!isMissingProcessError(error)) throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function waitForClosed(closed, timeout, resolvedExit) {
|
|
153
|
+
if (resolvedExit) return resolvedExit;
|
|
154
|
+
const result = await Promise.race([closed, delay(timeout).then(() => void 0)]);
|
|
155
|
+
if (result) return result;
|
|
156
|
+
}
|
|
157
|
+
function delay(milliseconds) {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
setTimeout(resolve, milliseconds);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function waitForImmediate() {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
setImmediate(resolve);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function isMissingProcessError(error) {
|
|
168
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
169
|
+
}
|
|
170
|
+
function isProcessRunning(pid) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0);
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (isMissingProcessError(error)) return false;
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function normalizeChunk(data) {
|
|
180
|
+
if (Buffer.isBuffer(data)) return data;
|
|
181
|
+
return Buffer.from(data, "utf8");
|
|
182
|
+
}
|
|
183
|
+
async function writeChunked(ptyProcess, chunk) {
|
|
184
|
+
if (chunk.length <= LARGE_WRITE_CHUNK_BYTES) {
|
|
185
|
+
ptyProcess.write(chunk);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (let offset = 0; offset < chunk.length; offset += LARGE_WRITE_CHUNK_BYTES) {
|
|
189
|
+
ptyProcess.write(chunk.subarray(offset, offset + LARGE_WRITE_CHUNK_BYTES));
|
|
190
|
+
if (offset + LARGE_WRITE_CHUNK_BYTES < chunk.length) await waitForImmediate();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function isValidResizeDimension(value) {
|
|
194
|
+
return Number.isInteger(value) && value > 0;
|
|
195
|
+
}
|
|
196
|
+
function resolvePtyPath(ptyProcess) {
|
|
197
|
+
const ttyPath = ptyProcess._pty;
|
|
198
|
+
return typeof ttyPath === "string" && ttyPath.length > 0 ? ttyPath : null;
|
|
199
|
+
}
|
|
200
|
+
function shouldUseBulkPasteMode(chunk) {
|
|
201
|
+
return chunk.length >= BULK_PASTE_THRESHOLD_BYTES;
|
|
202
|
+
}
|
|
203
|
+
async function captureBulkPasteTtyState(ttyPath) {
|
|
204
|
+
try {
|
|
205
|
+
const [serialized, modes] = await Promise.all([runSttyCommand(ttyPath, ["-g"]), runSttyCommand(ttyPath, ["-a"])]);
|
|
206
|
+
return {
|
|
207
|
+
canonical: /(^|[\s;])(-?icanon)(?=[\s;]|$)/.exec(modes)?.[2] === "icanon",
|
|
208
|
+
serialized
|
|
209
|
+
};
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function runSttyCommand(ttyPath, args) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const child = spawn("stty", [
|
|
217
|
+
getSttyPathFlag(),
|
|
218
|
+
ttyPath,
|
|
219
|
+
...args
|
|
220
|
+
], { stdio: [
|
|
221
|
+
"ignore",
|
|
222
|
+
"pipe",
|
|
223
|
+
"pipe"
|
|
224
|
+
] });
|
|
225
|
+
let stdout = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
child.stdout.on("data", (chunk) => {
|
|
228
|
+
stdout += chunk.toString("utf8");
|
|
229
|
+
});
|
|
230
|
+
child.stderr.on("data", (chunk) => {
|
|
231
|
+
stderr += chunk.toString("utf8");
|
|
232
|
+
});
|
|
233
|
+
child.once("error", reject);
|
|
234
|
+
child.once("close", (code) => {
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve(stdout.trim());
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
reject(new Error(stderr.trim() || `stty exited with code ${code ?? "unknown"}`));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function getSttyPathFlag() {
|
|
244
|
+
return process.platform === "linux" ? "-F" : "-f";
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
export { DEFAULT_COLUMNS, DEFAULT_ROWS, DEFAULT_TERM, createPtySession };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NetworkPolicy } from "../network-policy.js";
|
|
2
|
+
import { PtyExitEvent, PtySession } from "./pty-session.js";
|
|
3
|
+
import { Server } from "node:http";
|
|
4
|
+
import { AittyThemeSource } from "@aitty/protocol";
|
|
5
|
+
import * as _$ws from "ws";
|
|
6
|
+
|
|
7
|
+
//#region src/runtime/websocket-transport.d.ts
|
|
8
|
+
interface TransportLogWriter {
|
|
9
|
+
write(chunk: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
interface WebSocketTransportOptions {
|
|
12
|
+
WebSocketServer: typeof _$ws.WebSocketServer;
|
|
13
|
+
networkPolicy?: NetworkPolicy;
|
|
14
|
+
server: Server;
|
|
15
|
+
session: PtySession;
|
|
16
|
+
stderr: TransportLogWriter;
|
|
17
|
+
themeSource?: AittyThemeSource;
|
|
18
|
+
token: string;
|
|
19
|
+
maxBufferedAmountBytes?: number;
|
|
20
|
+
maxQueuedBytes?: number;
|
|
21
|
+
drainWaitMs?: number;
|
|
22
|
+
keepaliveInitialDelayMs?: number;
|
|
23
|
+
keepaliveIntervalMs?: number;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface WebSocketTransport {
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
notifyExit(event: PtyExitEvent): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
declare function createWebSocketTransport(options: WebSocketTransportOptions): WebSocketTransport;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { TransportLogWriter, WebSocketTransport, WebSocketTransportOptions, createWebSocketTransport };
|