mstro-app 0.1.47
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/LICENSE +21 -0
- package/README.md +177 -0
- package/bin/commands/config.js +145 -0
- package/bin/commands/login.js +313 -0
- package/bin/commands/logout.js +75 -0
- package/bin/commands/status.js +197 -0
- package/bin/commands/whoami.js +161 -0
- package/bin/configure-claude.js +298 -0
- package/bin/mstro.js +581 -0
- package/bin/postinstall.js +45 -0
- package/bin/release.sh +110 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker.js +311 -0
- package/dist/server/cli/headless/claude-invoker.js.map +1 -0
- package/dist/server/cli/headless/index.d.ts +13 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -0
- package/dist/server/cli/headless/index.js +10 -0
- package/dist/server/cli/headless/index.js.map +1 -0
- package/dist/server/cli/headless/mcp-config.d.ts +11 -0
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
- package/dist/server/cli/headless/mcp-config.js +76 -0
- package/dist/server/cli/headless/mcp-config.js.map +1 -0
- package/dist/server/cli/headless/output-utils.d.ts +33 -0
- package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/output-utils.js +101 -0
- package/dist/server/cli/headless/output-utils.js.map +1 -0
- package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/prompt-utils.js +84 -0
- package/dist/server/cli/headless/prompt-utils.js.map +1 -0
- package/dist/server/cli/headless/runner.d.ts +24 -0
- package/dist/server/cli/headless/runner.d.ts.map +1 -0
- package/dist/server/cli/headless/runner.js +99 -0
- package/dist/server/cli/headless/runner.js.map +1 -0
- package/dist/server/cli/headless/types.d.ts +106 -0
- package/dist/server/cli/headless/types.d.ts.map +1 -0
- package/dist/server/cli/headless/types.js +4 -0
- package/dist/server/cli/headless/types.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
- package/dist/server/cli/improvisation-session-manager.js +415 -0
- package/dist/server/cli/improvisation-session-manager.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +386 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp/bouncer-cli.d.ts +3 -0
- package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-cli.js +99 -0
- package/dist/server/mcp/bouncer-cli.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +36 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-integration.js +301 -0
- package/dist/server/mcp/bouncer-integration.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +52 -0
- package/dist/server/mcp/security-audit.d.ts.map +1 -0
- package/dist/server/mcp/security-audit.js +118 -0
- package/dist/server/mcp/security-audit.js.map +1 -0
- package/dist/server/mcp/security-patterns.d.ts +73 -0
- package/dist/server/mcp/security-patterns.d.ts.map +1 -0
- package/dist/server/mcp/security-patterns.js +247 -0
- package/dist/server/mcp/security-patterns.js.map +1 -0
- package/dist/server/mcp/server.d.ts +3 -0
- package/dist/server/mcp/server.d.ts.map +1 -0
- package/dist/server/mcp/server.js +146 -0
- package/dist/server/mcp/server.js.map +1 -0
- package/dist/server/routes/files.d.ts +9 -0
- package/dist/server/routes/files.d.ts.map +1 -0
- package/dist/server/routes/files.js +24 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/improvise.d.ts +3 -0
- package/dist/server/routes/improvise.d.ts.map +1 -0
- package/dist/server/routes/improvise.js +72 -0
- package/dist/server/routes/improvise.js.map +1 -0
- package/dist/server/routes/index.d.ts +10 -0
- package/dist/server/routes/index.d.ts.map +1 -0
- package/dist/server/routes/index.js +12 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/routes/instances.d.ts +10 -0
- package/dist/server/routes/instances.d.ts.map +1 -0
- package/dist/server/routes/instances.js +47 -0
- package/dist/server/routes/instances.js.map +1 -0
- package/dist/server/routes/notifications.d.ts +3 -0
- package/dist/server/routes/notifications.d.ts.map +1 -0
- package/dist/server/routes/notifications.js +136 -0
- package/dist/server/routes/notifications.js.map +1 -0
- package/dist/server/services/analytics.d.ts +56 -0
- package/dist/server/services/analytics.d.ts.map +1 -0
- package/dist/server/services/analytics.js +240 -0
- package/dist/server/services/analytics.js.map +1 -0
- package/dist/server/services/auth.d.ts +26 -0
- package/dist/server/services/auth.d.ts.map +1 -0
- package/dist/server/services/auth.js +71 -0
- package/dist/server/services/auth.js.map +1 -0
- package/dist/server/services/client-id.d.ts +10 -0
- package/dist/server/services/client-id.d.ts.map +1 -0
- package/dist/server/services/client-id.js +61 -0
- package/dist/server/services/client-id.js.map +1 -0
- package/dist/server/services/credentials.d.ts +39 -0
- package/dist/server/services/credentials.d.ts.map +1 -0
- package/dist/server/services/credentials.js +110 -0
- package/dist/server/services/credentials.js.map +1 -0
- package/dist/server/services/files.d.ts +119 -0
- package/dist/server/services/files.d.ts.map +1 -0
- package/dist/server/services/files.js +560 -0
- package/dist/server/services/files.js.map +1 -0
- package/dist/server/services/instances.d.ts +52 -0
- package/dist/server/services/instances.d.ts.map +1 -0
- package/dist/server/services/instances.js +241 -0
- package/dist/server/services/instances.js.map +1 -0
- package/dist/server/services/pathUtils.d.ts +47 -0
- package/dist/server/services/pathUtils.d.ts.map +1 -0
- package/dist/server/services/pathUtils.js +124 -0
- package/dist/server/services/pathUtils.js.map +1 -0
- package/dist/server/services/platform.d.ts +72 -0
- package/dist/server/services/platform.d.ts.map +1 -0
- package/dist/server/services/platform.js +368 -0
- package/dist/server/services/platform.js.map +1 -0
- package/dist/server/services/sentry.d.ts +5 -0
- package/dist/server/services/sentry.d.ts.map +1 -0
- package/dist/server/services/sentry.js +71 -0
- package/dist/server/services/sentry.js.map +1 -0
- package/dist/server/services/terminal/pty-manager.d.ts +149 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-manager.js +377 -0
- package/dist/server/services/terminal/pty-manager.js.map +1 -0
- package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
- package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/tmux-manager.js +352 -0
- package/dist/server/services/terminal/tmux-manager.js.map +1 -0
- package/dist/server/services/websocket/autocomplete.d.ts +50 -0
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
- package/dist/server/services/websocket/autocomplete.js +361 -0
- package/dist/server/services/websocket/autocomplete.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +44 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/file-utils.js +272 -0
- package/dist/server/services/websocket/file-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +246 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -0
- package/dist/server/services/websocket/handler.js +1771 -0
- package/dist/server/services/websocket/handler.js.map +1 -0
- package/dist/server/services/websocket/index.d.ts +11 -0
- package/dist/server/services/websocket/index.d.ts.map +1 -0
- package/dist/server/services/websocket/index.js +14 -0
- package/dist/server/services/websocket/index.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +214 -0
- package/dist/server/services/websocket/types.d.ts.map +1 -0
- package/dist/server/services/websocket/types.js +4 -0
- package/dist/server/services/websocket/types.js.map +1 -0
- package/dist/server/utils/agent-manager.d.ts +69 -0
- package/dist/server/utils/agent-manager.d.ts.map +1 -0
- package/dist/server/utils/agent-manager.js +269 -0
- package/dist/server/utils/agent-manager.js.map +1 -0
- package/dist/server/utils/paths.d.ts +25 -0
- package/dist/server/utils/paths.d.ts.map +1 -0
- package/dist/server/utils/paths.js +38 -0
- package/dist/server/utils/paths.js.map +1 -0
- package/dist/server/utils/port-manager.d.ts +10 -0
- package/dist/server/utils/port-manager.d.ts.map +1 -0
- package/dist/server/utils/port-manager.js +60 -0
- package/dist/server/utils/port-manager.js.map +1 -0
- package/dist/server/utils/port.d.ts +26 -0
- package/dist/server/utils/port.d.ts.map +1 -0
- package/dist/server/utils/port.js +83 -0
- package/dist/server/utils/port.js.map +1 -0
- package/hooks/bouncer.sh +138 -0
- package/package.json +74 -0
- package/server/README.md +191 -0
- package/server/cli/headless/claude-invoker.ts +415 -0
- package/server/cli/headless/index.ts +39 -0
- package/server/cli/headless/mcp-config.ts +87 -0
- package/server/cli/headless/output-utils.ts +109 -0
- package/server/cli/headless/prompt-utils.ts +108 -0
- package/server/cli/headless/runner.ts +133 -0
- package/server/cli/headless/types.ts +118 -0
- package/server/cli/improvisation-session-manager.ts +531 -0
- package/server/index.ts +456 -0
- package/server/mcp/README.md +122 -0
- package/server/mcp/bouncer-cli.ts +127 -0
- package/server/mcp/bouncer-integration.ts +430 -0
- package/server/mcp/security-audit.ts +180 -0
- package/server/mcp/security-patterns.ts +290 -0
- package/server/mcp/server.ts +174 -0
- package/server/routes/files.ts +29 -0
- package/server/routes/improvise.ts +82 -0
- package/server/routes/index.ts +13 -0
- package/server/routes/instances.ts +54 -0
- package/server/routes/notifications.ts +158 -0
- package/server/services/analytics.ts +277 -0
- package/server/services/auth.ts +80 -0
- package/server/services/client-id.ts +68 -0
- package/server/services/credentials.ts +134 -0
- package/server/services/files.ts +710 -0
- package/server/services/instances.ts +275 -0
- package/server/services/pathUtils.ts +158 -0
- package/server/services/platform.test.ts +1314 -0
- package/server/services/platform.ts +435 -0
- package/server/services/sentry.ts +81 -0
- package/server/services/terminal/pty-manager.ts +464 -0
- package/server/services/terminal/tmux-manager.ts +426 -0
- package/server/services/websocket/autocomplete.ts +438 -0
- package/server/services/websocket/file-utils.ts +305 -0
- package/server/services/websocket/handler.test.ts +20 -0
- package/server/services/websocket/handler.ts +2047 -0
- package/server/services/websocket/index.ts +40 -0
- package/server/services/websocket/types.ts +339 -0
- package/server/tsconfig.json +19 -0
- package/server/utils/agent-manager.ts +323 -0
- package/server/utils/paths.ts +45 -0
- package/server/utils/port-manager.ts +70 -0
- package/server/utils/port.ts +102 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PTY Manager - Manages pseudo-terminal sessions for shell access
|
|
6
|
+
*
|
|
7
|
+
* Provides terminal emulation for running shell commands on the local machine.
|
|
8
|
+
* Each terminal session is managed independently with its own PTY process.
|
|
9
|
+
*
|
|
10
|
+
* Supports session persistence:
|
|
11
|
+
* - Sessions survive WebSocket disconnections
|
|
12
|
+
* - Scrollback buffer is maintained for replay on reconnect
|
|
13
|
+
* - Sessions can be reattached without losing running processes
|
|
14
|
+
*
|
|
15
|
+
* Also supports tmux-backed persistence for sessions that survive server restarts.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: node-pty is an optional dependency requiring native compilation.
|
|
18
|
+
* Terminal features gracefully degrade when node-pty is not available.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { EventEmitter } from 'node:events';
|
|
22
|
+
import { homedir, platform } from 'node:os';
|
|
23
|
+
import { getTmuxManager, isTmuxAvailable, type TmuxSession } from './tmux-manager.js';
|
|
24
|
+
|
|
25
|
+
// Try to load node-pty (optional native dependency)
|
|
26
|
+
let pty: typeof import('node-pty') | null = null;
|
|
27
|
+
let _ptyLoadError: string | null = null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
pty = await import('node-pty');
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
_ptyLoadError = error.message || 'Failed to load node-pty';
|
|
33
|
+
console.warn('[PTYManager] node-pty not available - terminal features disabled');
|
|
34
|
+
console.warn('[PTYManager] To enable terminals, run: mstro setup-terminal');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if node-pty is available
|
|
39
|
+
*/
|
|
40
|
+
export function isPtyAvailable(): boolean {
|
|
41
|
+
return pty !== null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get installation instructions for node-pty based on platform
|
|
46
|
+
*/
|
|
47
|
+
export function getPtyInstallInstructions(): string {
|
|
48
|
+
const os = platform();
|
|
49
|
+
|
|
50
|
+
let instructions = `Terminal feature requires native compilation of node-pty.\n\n`;
|
|
51
|
+
instructions += `To enable this feature:\n\n`;
|
|
52
|
+
|
|
53
|
+
if (os === 'darwin') {
|
|
54
|
+
instructions += `1. Install Xcode Command Line Tools:\n`;
|
|
55
|
+
instructions += ` xcode-select --install\n\n`;
|
|
56
|
+
} else if (os === 'win32') {
|
|
57
|
+
instructions += `1. Install Windows Build Tools:\n`;
|
|
58
|
+
instructions += ` npm install -g windows-build-tools\n\n`;
|
|
59
|
+
} else {
|
|
60
|
+
// Linux
|
|
61
|
+
instructions += `1. Install build tools:\n`;
|
|
62
|
+
instructions += ` # Debian/Ubuntu:\n`;
|
|
63
|
+
instructions += ` sudo apt install build-essential python3\n\n`;
|
|
64
|
+
instructions += ` # Fedora/RHEL:\n`;
|
|
65
|
+
instructions += ` sudo dnf install gcc-c++ make python3\n\n`;
|
|
66
|
+
instructions += ` # Arch:\n`;
|
|
67
|
+
instructions += ` sudo pacman -S base-devel python\n\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
instructions += `2. Rebuild native modules:\n`;
|
|
71
|
+
instructions += ` npm rebuild node-pty\n\n`;
|
|
72
|
+
instructions += `3. Restart mstro\n`;
|
|
73
|
+
|
|
74
|
+
return instructions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Maximum lines to store in scrollback buffer per terminal
|
|
78
|
+
const MAX_SCROLLBACK_LINES = 5000;
|
|
79
|
+
// Maximum characters per line to prevent memory bloat
|
|
80
|
+
const MAX_LINE_LENGTH = 2000;
|
|
81
|
+
|
|
82
|
+
// Import type separately for type-checking (doesn't require the module to load)
|
|
83
|
+
type IPty = import('node-pty').IPty;
|
|
84
|
+
|
|
85
|
+
export interface PTYSession {
|
|
86
|
+
id: string;
|
|
87
|
+
pty: IPty;
|
|
88
|
+
shell: string;
|
|
89
|
+
cwd: string;
|
|
90
|
+
// Scrollback buffer for replay on reconnect
|
|
91
|
+
scrollback: string[];
|
|
92
|
+
// Timestamp when session was created
|
|
93
|
+
createdAt: number;
|
|
94
|
+
// Last activity timestamp
|
|
95
|
+
lastActivityAt: number;
|
|
96
|
+
// Current dimensions
|
|
97
|
+
cols: number;
|
|
98
|
+
rows: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Detect the user's default shell
|
|
103
|
+
*/
|
|
104
|
+
function detectShell(): string {
|
|
105
|
+
const shell = process.env.SHELL;
|
|
106
|
+
|
|
107
|
+
if (shell) {
|
|
108
|
+
return shell;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Platform-specific defaults
|
|
112
|
+
if (platform() === 'win32') {
|
|
113
|
+
return process.env.COMSPEC || 'powershell.exe';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return '/bin/bash';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get shell name from path
|
|
121
|
+
*/
|
|
122
|
+
function getShellName(shellPath: string): string {
|
|
123
|
+
const parts = shellPath.split(/[/\\]/);
|
|
124
|
+
return parts[parts.length - 1] || 'shell';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class PTYManager extends EventEmitter {
|
|
128
|
+
private terminals: Map<string, PTYSession> = new Map();
|
|
129
|
+
|
|
130
|
+
constructor() {
|
|
131
|
+
super();
|
|
132
|
+
// Each terminal adds 3 listeners (output, exit, error) to this singleton.
|
|
133
|
+
// With multiple terminals, the default limit of 10 is easily exceeded.
|
|
134
|
+
this.setMaxListeners(50);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a terminal session exists and is still running
|
|
139
|
+
*/
|
|
140
|
+
exists(terminalId: string): boolean {
|
|
141
|
+
return this.terminals.has(terminalId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get session info for reconnection
|
|
146
|
+
* Returns null if session doesn't exist
|
|
147
|
+
*/
|
|
148
|
+
getSessionInfo(terminalId: string): { shell: string; cwd: string; cols: number; rows: number } | null {
|
|
149
|
+
const session = this.terminals.get(terminalId);
|
|
150
|
+
if (!session) return null;
|
|
151
|
+
return {
|
|
152
|
+
shell: session.shell,
|
|
153
|
+
cwd: session.cwd,
|
|
154
|
+
cols: session.cols,
|
|
155
|
+
rows: session.rows,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get scrollback buffer for replay on reconnect
|
|
161
|
+
* Returns the stored output history
|
|
162
|
+
*/
|
|
163
|
+
getScrollback(terminalId: string): string[] {
|
|
164
|
+
const session = this.terminals.get(terminalId);
|
|
165
|
+
if (!session) return [];
|
|
166
|
+
return [...session.scrollback];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Add data to scrollback buffer
|
|
171
|
+
* Maintains a rolling buffer of recent terminal output
|
|
172
|
+
*/
|
|
173
|
+
private addToScrollback(session: PTYSession, data: string): void {
|
|
174
|
+
// Split data into lines
|
|
175
|
+
const lines = data.split(/\r?\n/);
|
|
176
|
+
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
// Truncate very long lines to prevent memory issues
|
|
179
|
+
const truncatedLine = line.length > MAX_LINE_LENGTH
|
|
180
|
+
? `${line.slice(0, MAX_LINE_LENGTH)}...`
|
|
181
|
+
: line;
|
|
182
|
+
|
|
183
|
+
session.scrollback.push(truncatedLine);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Trim buffer if it exceeds max size
|
|
187
|
+
if (session.scrollback.length > MAX_SCROLLBACK_LINES) {
|
|
188
|
+
session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK_LINES);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
session.lastActivityAt = Date.now();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if PTY functionality is available
|
|
196
|
+
*/
|
|
197
|
+
isPtyAvailable(): boolean {
|
|
198
|
+
return isPtyAvailable();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get installation instructions if PTY is not available
|
|
203
|
+
*/
|
|
204
|
+
getPtyInstallInstructions(): string {
|
|
205
|
+
return getPtyInstallInstructions();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a new terminal session
|
|
210
|
+
*/
|
|
211
|
+
create(
|
|
212
|
+
terminalId: string,
|
|
213
|
+
workingDir: string,
|
|
214
|
+
cols: number = 80,
|
|
215
|
+
rows: number = 24,
|
|
216
|
+
requestedShell?: string
|
|
217
|
+
): { shell: string; cwd: string; isReconnect: boolean } {
|
|
218
|
+
// Check if node-pty is available
|
|
219
|
+
if (!pty) {
|
|
220
|
+
throw new Error(`PTY_NOT_AVAILABLE:${getPtyInstallInstructions()}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if session already exists - if so, this is a reconnection
|
|
224
|
+
if (this.terminals.has(terminalId)) {
|
|
225
|
+
const existingSession = this.terminals.get(terminalId)!;
|
|
226
|
+
|
|
227
|
+
// Always resize on reconnect to trigger SIGWINCH, which causes the
|
|
228
|
+
// shell to redraw its prompt line for the reconnected client
|
|
229
|
+
existingSession.pty.resize(cols, rows);
|
|
230
|
+
existingSession.cols = cols;
|
|
231
|
+
existingSession.rows = rows;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
shell: existingSession.shell,
|
|
235
|
+
cwd: existingSession.cwd,
|
|
236
|
+
isReconnect: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const shell = requestedShell || detectShell();
|
|
241
|
+
const cwd = workingDir || homedir();
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Spawn the PTY process
|
|
246
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
247
|
+
name: 'xterm-256color',
|
|
248
|
+
cols,
|
|
249
|
+
rows,
|
|
250
|
+
cwd,
|
|
251
|
+
env: {
|
|
252
|
+
...process.env,
|
|
253
|
+
TERM: 'xterm-256color',
|
|
254
|
+
COLORTERM: 'truecolor',
|
|
255
|
+
// Ensure home directory is set
|
|
256
|
+
HOME: homedir(),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Store the session with scrollback buffer
|
|
261
|
+
const session: PTYSession = {
|
|
262
|
+
id: terminalId,
|
|
263
|
+
pty: ptyProcess,
|
|
264
|
+
shell: getShellName(shell),
|
|
265
|
+
cwd,
|
|
266
|
+
scrollback: [],
|
|
267
|
+
createdAt: Date.now(),
|
|
268
|
+
lastActivityAt: Date.now(),
|
|
269
|
+
cols,
|
|
270
|
+
rows,
|
|
271
|
+
};
|
|
272
|
+
this.terminals.set(terminalId, session);
|
|
273
|
+
|
|
274
|
+
// Handle data output - store in scrollback and emit
|
|
275
|
+
ptyProcess.onData((data: string) => {
|
|
276
|
+
this.addToScrollback(session, data);
|
|
277
|
+
this.emit('output', terminalId, data);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Handle exit
|
|
281
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
282
|
+
this.emit('exit', terminalId, exitCode);
|
|
283
|
+
this.terminals.delete(terminalId);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return { shell: session.shell, cwd, isReconnect: false };
|
|
287
|
+
} catch (error: any) {
|
|
288
|
+
console.error(`[PTYManager] Failed to create terminal ${terminalId}:`, error);
|
|
289
|
+
this.emit('error', terminalId, error.message || 'Failed to create terminal');
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Write input data to terminal
|
|
296
|
+
*/
|
|
297
|
+
write(terminalId: string, data: string): boolean {
|
|
298
|
+
const session = this.terminals.get(terminalId);
|
|
299
|
+
if (!session) {
|
|
300
|
+
console.warn(`[PTYManager] Terminal ${terminalId} not found for write`);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
session.pty.write(data);
|
|
306
|
+
return true;
|
|
307
|
+
} catch (error: any) {
|
|
308
|
+
console.error(`[PTYManager] Error writing to terminal ${terminalId}:`, error);
|
|
309
|
+
this.emit('error', terminalId, error.message || 'Write failed');
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resize terminal
|
|
316
|
+
*/
|
|
317
|
+
resize(terminalId: string, cols: number, rows: number): boolean {
|
|
318
|
+
const session = this.terminals.get(terminalId);
|
|
319
|
+
if (!session) {
|
|
320
|
+
console.warn(`[PTYManager] Terminal ${terminalId} not found for resize`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
session.pty.resize(cols, rows);
|
|
326
|
+
return true;
|
|
327
|
+
} catch (error: any) {
|
|
328
|
+
console.error(`[PTYManager] Error resizing terminal ${terminalId}:`, error);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Close terminal session
|
|
335
|
+
*/
|
|
336
|
+
close(terminalId: string): boolean {
|
|
337
|
+
const session = this.terminals.get(terminalId);
|
|
338
|
+
if (!session) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
session.pty.kill();
|
|
345
|
+
this.terminals.delete(terminalId);
|
|
346
|
+
return true;
|
|
347
|
+
} catch (error: any) {
|
|
348
|
+
console.error(`[PTYManager] Error closing terminal ${terminalId}:`, error);
|
|
349
|
+
this.terminals.delete(terminalId);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get terminal session info
|
|
356
|
+
*/
|
|
357
|
+
getSession(terminalId: string): PTYSession | undefined {
|
|
358
|
+
return this.terminals.get(terminalId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check if terminal exists
|
|
363
|
+
*/
|
|
364
|
+
has(terminalId: string): boolean {
|
|
365
|
+
return this.terminals.has(terminalId);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get all active terminal IDs
|
|
370
|
+
*/
|
|
371
|
+
getActiveTerminals(): string[] {
|
|
372
|
+
return Array.from(this.terminals.keys());
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Close all terminals
|
|
377
|
+
*/
|
|
378
|
+
closeAll(): void {
|
|
379
|
+
for (const terminalId of this.terminals.keys()) {
|
|
380
|
+
this.close(terminalId);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if tmux persistence is available
|
|
386
|
+
*/
|
|
387
|
+
isTmuxAvailable(): boolean {
|
|
388
|
+
return isTmuxAvailable();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get list of persistent tmux sessions that can be restored
|
|
393
|
+
* These are sessions that survived a server restart
|
|
394
|
+
*/
|
|
395
|
+
getPersistentSessions(): TmuxSession[] {
|
|
396
|
+
const tmux = getTmuxManager();
|
|
397
|
+
return tmux.getActiveSessions();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Create a persistent (tmux-backed) terminal session
|
|
402
|
+
* These sessions survive server restarts
|
|
403
|
+
*/
|
|
404
|
+
createPersistent(
|
|
405
|
+
terminalId: string,
|
|
406
|
+
workingDir: string,
|
|
407
|
+
cols: number = 80,
|
|
408
|
+
rows: number = 24,
|
|
409
|
+
requestedShell?: string
|
|
410
|
+
): { shell: string; cwd: string; isReconnect: boolean; persistent: true } {
|
|
411
|
+
const tmux = getTmuxManager();
|
|
412
|
+
|
|
413
|
+
if (!tmux.isAvailable()) {
|
|
414
|
+
throw new Error('tmux is not available for persistent sessions');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = tmux.create(terminalId, workingDir, cols, rows, requestedShell);
|
|
418
|
+
return { ...result, persistent: true };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Attach to a persistent (tmux) session
|
|
423
|
+
* Returns handlers for write, resize, and detach
|
|
424
|
+
*/
|
|
425
|
+
attachPersistent(
|
|
426
|
+
terminalId: string,
|
|
427
|
+
onOutput: (data: string) => void,
|
|
428
|
+
onExit: (code: number) => void
|
|
429
|
+
): { write: (data: string) => void; resize: (cols: number, rows: number) => void; detach: () => void } | null {
|
|
430
|
+
const tmux = getTmuxManager();
|
|
431
|
+
|
|
432
|
+
if (!tmux.exists(terminalId)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return tmux.attach(terminalId, onOutput, onExit);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get scrollback from a persistent (tmux) session
|
|
441
|
+
*/
|
|
442
|
+
getPersistentScrollback(terminalId: string): string[] {
|
|
443
|
+
const tmux = getTmuxManager();
|
|
444
|
+
return tmux.getScrollback(terminalId);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Close a persistent (tmux) session
|
|
449
|
+
*/
|
|
450
|
+
closePersistent(terminalId: string): boolean {
|
|
451
|
+
const tmux = getTmuxManager();
|
|
452
|
+
return tmux.close(terminalId);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Singleton instance
|
|
457
|
+
let ptyManagerInstance: PTYManager | null = null;
|
|
458
|
+
|
|
459
|
+
export function getPTYManager(): PTYManager {
|
|
460
|
+
if (!ptyManagerInstance) {
|
|
461
|
+
ptyManagerInstance = new PTYManager();
|
|
462
|
+
}
|
|
463
|
+
return ptyManagerInstance;
|
|
464
|
+
}
|