@ebowwa/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.
- package/dist/client.d.ts +15 -0
- package/dist/client.js +45 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.js +12 -0
- package/dist/exec.d.ts +47 -0
- package/dist/exec.js +107 -0
- package/dist/files.d.ts +124 -0
- package/dist/files.js +436 -0
- package/dist/fingerprint.d.ts +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/pool.d.ts +143 -0
- package/dist/pool.js +554 -0
- package/dist/pty.d.ts +59 -0
- package/dist/scp.d.ts +30 -0
- package/dist/scp.js +74 -0
- package/dist/sessions.d.ts +98 -0
- package/dist/tmux-exec.d.ts +50 -0
- package/dist/tmux.d.ts +213 -0
- package/dist/tmux.js +528 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +5 -0
- package/ebowwa-terminal-0.2.0.tgz +0 -0
- package/mcp/README.md +181 -0
- package/mcp/package.json +34 -0
- package/mcp/test-fix.sh +273 -0
- package/package.json +118 -0
- package/src/api.ts +752 -0
- package/src/client.ts +55 -0
- package/src/config.ts +489 -0
- package/src/error.ts +13 -0
- package/src/exec.ts +128 -0
- package/src/files.ts +636 -0
- package/src/fingerprint.ts +263 -0
- package/src/index.ts +144 -0
- package/src/manager.ts +319 -0
- package/src/mcp/index.ts +467 -0
- package/src/mcp/stdio.ts +708 -0
- package/src/network-error-detector.ts +121 -0
- package/src/pool.ts +662 -0
- package/src/pty.ts +285 -0
- package/src/scp.ts +109 -0
- package/src/sessions.ts +861 -0
- package/src/tmux-exec.ts +96 -0
- package/src/tmux-local.ts +839 -0
- package/src/tmux-manager.ts +962 -0
- package/src/tmux.ts +711 -0
- package/src/types.ts +19 -0
package/src/pty.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH PTY session manager for interactive terminal sessions
|
|
3
|
+
* Handles bidirectional communication with remote shells
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SSHOptions } from "./types.js";
|
|
7
|
+
|
|
8
|
+
interface PTYSession {
|
|
9
|
+
id: string;
|
|
10
|
+
host: string;
|
|
11
|
+
user: string;
|
|
12
|
+
proc: any;
|
|
13
|
+
stdin: WritableStream<Uint8Array>;
|
|
14
|
+
stdout: ReadableStream<Uint8Array>;
|
|
15
|
+
stderr: ReadableStream<Uint8Array>;
|
|
16
|
+
rows: number;
|
|
17
|
+
cols: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Active PTY sessions storage
|
|
22
|
+
const activeSessions = new Map<string, PTYSession>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a unique session ID
|
|
26
|
+
*/
|
|
27
|
+
function generateSessionId(): string {
|
|
28
|
+
return `pty-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a new SSH PTY session
|
|
33
|
+
* Uses script or expect to wrap SSH with PTY allocation
|
|
34
|
+
*/
|
|
35
|
+
export async function createPTYSession(
|
|
36
|
+
host: string,
|
|
37
|
+
user: string = "root",
|
|
38
|
+
options: {
|
|
39
|
+
rows?: number;
|
|
40
|
+
cols?: number;
|
|
41
|
+
port?: number;
|
|
42
|
+
keyPath?: string;
|
|
43
|
+
} = {},
|
|
44
|
+
): Promise<{ sessionId: string; initialOutput: string }> {
|
|
45
|
+
const { rows = 24, cols = 80, port = 22, keyPath } = options;
|
|
46
|
+
const sessionId = generateSessionId();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Build SSH command with explicit PTY request
|
|
50
|
+
const sshArgs = [
|
|
51
|
+
"ssh",
|
|
52
|
+
"-F",
|
|
53
|
+
"/dev/null", // Skip user SSH config to avoid conflicts
|
|
54
|
+
"-o",
|
|
55
|
+
"StrictHostKeyChecking=no",
|
|
56
|
+
"-o",
|
|
57
|
+
"UserKnownHostsFile=/dev/null",
|
|
58
|
+
"-o",
|
|
59
|
+
"ServerAliveInterval=30",
|
|
60
|
+
"-o",
|
|
61
|
+
"ServerAliveCountMax=3",
|
|
62
|
+
"-p",
|
|
63
|
+
port.toString(),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
if (keyPath) {
|
|
67
|
+
// Ensure keyPath is properly escaped for SSH
|
|
68
|
+
// Bun.spawn() passes args directly without shell, but SSH -i needs proper path handling
|
|
69
|
+
// When keyPath contains spaces, we need to ensure it's a single argument
|
|
70
|
+
sshArgs.push("-i", String(keyPath));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Use script command to ensure PTY allocation
|
|
74
|
+
// This creates a proper terminal session with full VT100 support
|
|
75
|
+
sshArgs.push("-t", "-t");
|
|
76
|
+
sshArgs.push(`${user}@${host}`);
|
|
77
|
+
sshArgs.push("TERM=xterm-256color");
|
|
78
|
+
sshArgs.push("script", "-q", "/dev/null", "/bin/bash");
|
|
79
|
+
|
|
80
|
+
const proc = Bun.spawn(sshArgs, {
|
|
81
|
+
stdin: "pipe",
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "pipe",
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
COLUMNS: cols.toString(),
|
|
87
|
+
LINES: rows.toString(),
|
|
88
|
+
TERM: "xterm-256color",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Set up streams
|
|
93
|
+
const stdin = proc.stdin as unknown as WritableStream<Uint8Array>;
|
|
94
|
+
const stdout = proc.stdout as ReadableStream<Uint8Array>;
|
|
95
|
+
const stderr = proc.stderr as ReadableStream<Uint8Array>;
|
|
96
|
+
|
|
97
|
+
// Wait a moment for connection and initial output
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
99
|
+
|
|
100
|
+
let initialOutput = "";
|
|
101
|
+
|
|
102
|
+
// Read initial output (non-blocking)
|
|
103
|
+
try {
|
|
104
|
+
const reader = stdout.getReader();
|
|
105
|
+
const { value, done } = await reader.read();
|
|
106
|
+
if (value && !done) {
|
|
107
|
+
initialOutput = new TextDecoder().decode(value);
|
|
108
|
+
}
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
} catch {
|
|
111
|
+
// Ignore initial read errors
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Store session
|
|
115
|
+
const session: PTYSession = {
|
|
116
|
+
id: sessionId,
|
|
117
|
+
host,
|
|
118
|
+
user,
|
|
119
|
+
proc,
|
|
120
|
+
stdin,
|
|
121
|
+
stdout,
|
|
122
|
+
stderr,
|
|
123
|
+
rows,
|
|
124
|
+
cols,
|
|
125
|
+
createdAt: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
activeSessions.set(sessionId, session);
|
|
129
|
+
|
|
130
|
+
// Set up cleanup on process exit
|
|
131
|
+
proc.exited.then(() => {
|
|
132
|
+
activeSessions.delete(sessionId);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { sessionId, initialOutput };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`Failed to create PTY session: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write data to PTY session stdin
|
|
143
|
+
*/
|
|
144
|
+
export async function writeToPTY(
|
|
145
|
+
sessionId: string,
|
|
146
|
+
data: string,
|
|
147
|
+
): Promise<boolean> {
|
|
148
|
+
const session = activeSessions.get(sessionId);
|
|
149
|
+
if (!session) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const writer = session.stdin.getWriter();
|
|
155
|
+
await writer.write(new TextEncoder().encode(data));
|
|
156
|
+
writer.releaseLock();
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set PTY size (rows and columns)
|
|
165
|
+
*/
|
|
166
|
+
export async function setPTYSize(
|
|
167
|
+
sessionId: string,
|
|
168
|
+
rows: number,
|
|
169
|
+
cols: number,
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
const session = activeSessions.get(sessionId);
|
|
172
|
+
if (!session) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Send SIGWINCH to resize the terminal
|
|
178
|
+
// We need to use stty or similar command on remote side
|
|
179
|
+
const resizeCmd = `\u001B[8;${rows};${cols}t`;
|
|
180
|
+
const writer = session.stdin.getWriter();
|
|
181
|
+
await writer.write(new TextEncoder().encode(resizeCmd));
|
|
182
|
+
writer.releaseLock();
|
|
183
|
+
|
|
184
|
+
session.rows = rows;
|
|
185
|
+
session.cols = cols;
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read from PTY session stdout (non-blocking)
|
|
194
|
+
*/
|
|
195
|
+
export async function readFromPTY(
|
|
196
|
+
sessionId: string,
|
|
197
|
+
timeout: number = 100,
|
|
198
|
+
): Promise<string | null> {
|
|
199
|
+
const session = activeSessions.get(sessionId);
|
|
200
|
+
if (!session) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const reader = session.stdout.getReader();
|
|
206
|
+
const { value, done } = await Promise.race([
|
|
207
|
+
reader.read(),
|
|
208
|
+
new Promise<{ value: Uint8Array | undefined; done: boolean }>((resolve) =>
|
|
209
|
+
setTimeout(() => resolve({ value: undefined, done: false }), timeout),
|
|
210
|
+
),
|
|
211
|
+
]);
|
|
212
|
+
reader.releaseLock();
|
|
213
|
+
|
|
214
|
+
if (done) {
|
|
215
|
+
return null; // Session closed
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (value) {
|
|
219
|
+
return new TextDecoder().decode(value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return "";
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Close PTY session
|
|
230
|
+
*/
|
|
231
|
+
export async function closePTYSession(sessionId: string): Promise<boolean> {
|
|
232
|
+
const session = activeSessions.get(sessionId);
|
|
233
|
+
if (!session) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Send exit sequence to gracefully close
|
|
239
|
+
const writer = session.stdin.getWriter();
|
|
240
|
+
await writer.write(new TextEncoder().encode("exit\r\n"));
|
|
241
|
+
writer.releaseLock();
|
|
242
|
+
|
|
243
|
+
// Give it a moment to close gracefully
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
245
|
+
|
|
246
|
+
// Kill the process if still running
|
|
247
|
+
try {
|
|
248
|
+
session.proc.kill();
|
|
249
|
+
} catch {
|
|
250
|
+
// Process already terminated
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
activeSessions.delete(sessionId);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
activeSessions.delete(sessionId);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get session info
|
|
263
|
+
*/
|
|
264
|
+
export function getPTYSession(sessionId: string): PTYSession | undefined {
|
|
265
|
+
return activeSessions.get(sessionId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get all active sessions
|
|
270
|
+
*/
|
|
271
|
+
export function getActivePTYSessions(): PTYSession[] {
|
|
272
|
+
return Array.from(activeSessions.values());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Clean up stale sessions (older than specified milliseconds)
|
|
277
|
+
*/
|
|
278
|
+
export function cleanupStaleSessions(maxAge: number = 3600000): void {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
for (const [id, session] of activeSessions.entries()) {
|
|
281
|
+
if (now - session.createdAt > maxAge) {
|
|
282
|
+
closePTYSession(id);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/scp.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCP/SFTP file transfer operations
|
|
3
|
+
* Uses SSH connection pool and SFTP for efficient transfers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import type { SCPOptions } from "./types.js";
|
|
8
|
+
import { SSHError } from "./error.js";
|
|
9
|
+
import { SCPOptionsSchema } from '@ebowwa/codespaces-types/runtime/ssh'
|
|
10
|
+
import { getSSHPool } from './pool.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Upload a file to remote server via SFTP
|
|
14
|
+
* @param options - SCP options including source and destination
|
|
15
|
+
* @returns True if successful
|
|
16
|
+
*/
|
|
17
|
+
export async function scpUpload(options: SCPOptions): Promise<boolean> {
|
|
18
|
+
// Validate inputs with Zod
|
|
19
|
+
const validated = SCPOptionsSchema.safeParse(options)
|
|
20
|
+
if (!validated.success) {
|
|
21
|
+
throw new Error(`Invalid SCP options: ${validated.error.issues.map(i => i.message).join(', ')}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
host,
|
|
26
|
+
user = "root",
|
|
27
|
+
timeout = 30,
|
|
28
|
+
port = 22,
|
|
29
|
+
keyPath,
|
|
30
|
+
source,
|
|
31
|
+
destination,
|
|
32
|
+
recursive = false,
|
|
33
|
+
preserve = false,
|
|
34
|
+
} = validated.data;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const pool = getSSHPool()
|
|
38
|
+
const ssh = await pool.getConnection({ host, user, port, keyPath, timeout })
|
|
39
|
+
|
|
40
|
+
// Use SFTP for file transfer
|
|
41
|
+
await ssh.putFile(source, destination, null, {
|
|
42
|
+
mode: recursive ? 'recursive' : undefined,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return true
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new SSHError(`SFTP upload failed: ${source} -> ${destination}`, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Download a file from remote server via SFTP
|
|
53
|
+
* @param options - SCP options including source (remote) and destination (local)
|
|
54
|
+
* @returns True if successful
|
|
55
|
+
*/
|
|
56
|
+
export async function scpDownload(options: SCPOptions): Promise<boolean> {
|
|
57
|
+
// Validate inputs with Zod
|
|
58
|
+
const validated = SCPOptionsSchema.safeParse(options)
|
|
59
|
+
if (!validated.success) {
|
|
60
|
+
throw new Error(`Invalid SCP options: ${validated.error.issues.map(i => i.message).join(', ')}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
host,
|
|
65
|
+
user = "root",
|
|
66
|
+
timeout = 30,
|
|
67
|
+
port = 22,
|
|
68
|
+
keyPath,
|
|
69
|
+
source,
|
|
70
|
+
destination,
|
|
71
|
+
recursive = false,
|
|
72
|
+
preserve = false,
|
|
73
|
+
} = validated.data;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const pool = getSSHPool()
|
|
77
|
+
const ssh = await pool.getConnection({ host, user, port, keyPath, timeout })
|
|
78
|
+
|
|
79
|
+
// Use SFTP for file transfer
|
|
80
|
+
await ssh.getFile(destination, source, null, {
|
|
81
|
+
mode: recursive ? 'recursive' : undefined,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return true
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new SSHError(
|
|
87
|
+
`SFTP download failed: ${source} -> ${destination}`,
|
|
88
|
+
error,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Upload a directory to remote server via SFTP
|
|
95
|
+
* @param options - SCP options with source directory
|
|
96
|
+
* @returns True if successful
|
|
97
|
+
*/
|
|
98
|
+
export async function scpUploadDirectory(options: SCPOptions): Promise<boolean> {
|
|
99
|
+
return scpUpload({ ...options, recursive: true })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Download a directory from remote server via SFTP
|
|
104
|
+
* @param options - SCP options with source directory
|
|
105
|
+
* @returns True if successful
|
|
106
|
+
*/
|
|
107
|
+
export async function scpDownloadDirectory(options: SCPOptions): Promise<boolean> {
|
|
108
|
+
return scpDownload({ ...options, recursive: true })
|
|
109
|
+
}
|