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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/bin/commands/config.js +145 -0
  4. package/bin/commands/login.js +313 -0
  5. package/bin/commands/logout.js +75 -0
  6. package/bin/commands/status.js +197 -0
  7. package/bin/commands/whoami.js +161 -0
  8. package/bin/configure-claude.js +298 -0
  9. package/bin/mstro.js +581 -0
  10. package/bin/postinstall.js +45 -0
  11. package/bin/release.sh +110 -0
  12. package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
  13. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker.js +311 -0
  15. package/dist/server/cli/headless/claude-invoker.js.map +1 -0
  16. package/dist/server/cli/headless/index.d.ts +13 -0
  17. package/dist/server/cli/headless/index.d.ts.map +1 -0
  18. package/dist/server/cli/headless/index.js +10 -0
  19. package/dist/server/cli/headless/index.js.map +1 -0
  20. package/dist/server/cli/headless/mcp-config.d.ts +11 -0
  21. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
  22. package/dist/server/cli/headless/mcp-config.js +76 -0
  23. package/dist/server/cli/headless/mcp-config.js.map +1 -0
  24. package/dist/server/cli/headless/output-utils.d.ts +33 -0
  25. package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
  26. package/dist/server/cli/headless/output-utils.js +101 -0
  27. package/dist/server/cli/headless/output-utils.js.map +1 -0
  28. package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
  29. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
  30. package/dist/server/cli/headless/prompt-utils.js +84 -0
  31. package/dist/server/cli/headless/prompt-utils.js.map +1 -0
  32. package/dist/server/cli/headless/runner.d.ts +24 -0
  33. package/dist/server/cli/headless/runner.d.ts.map +1 -0
  34. package/dist/server/cli/headless/runner.js +99 -0
  35. package/dist/server/cli/headless/runner.js.map +1 -0
  36. package/dist/server/cli/headless/types.d.ts +106 -0
  37. package/dist/server/cli/headless/types.d.ts.map +1 -0
  38. package/dist/server/cli/headless/types.js +4 -0
  39. package/dist/server/cli/headless/types.js.map +1 -0
  40. package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
  41. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-session-manager.js +415 -0
  43. package/dist/server/cli/improvisation-session-manager.js.map +1 -0
  44. package/dist/server/index.d.ts +2 -0
  45. package/dist/server/index.d.ts.map +1 -0
  46. package/dist/server/index.js +386 -0
  47. package/dist/server/index.js.map +1 -0
  48. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  49. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  50. package/dist/server/mcp/bouncer-cli.js +99 -0
  51. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  52. package/dist/server/mcp/bouncer-integration.d.ts +36 -0
  53. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
  54. package/dist/server/mcp/bouncer-integration.js +301 -0
  55. package/dist/server/mcp/bouncer-integration.js.map +1 -0
  56. package/dist/server/mcp/security-audit.d.ts +52 -0
  57. package/dist/server/mcp/security-audit.d.ts.map +1 -0
  58. package/dist/server/mcp/security-audit.js +118 -0
  59. package/dist/server/mcp/security-audit.js.map +1 -0
  60. package/dist/server/mcp/security-patterns.d.ts +73 -0
  61. package/dist/server/mcp/security-patterns.d.ts.map +1 -0
  62. package/dist/server/mcp/security-patterns.js +247 -0
  63. package/dist/server/mcp/security-patterns.js.map +1 -0
  64. package/dist/server/mcp/server.d.ts +3 -0
  65. package/dist/server/mcp/server.d.ts.map +1 -0
  66. package/dist/server/mcp/server.js +146 -0
  67. package/dist/server/mcp/server.js.map +1 -0
  68. package/dist/server/routes/files.d.ts +9 -0
  69. package/dist/server/routes/files.d.ts.map +1 -0
  70. package/dist/server/routes/files.js +24 -0
  71. package/dist/server/routes/files.js.map +1 -0
  72. package/dist/server/routes/improvise.d.ts +3 -0
  73. package/dist/server/routes/improvise.d.ts.map +1 -0
  74. package/dist/server/routes/improvise.js +72 -0
  75. package/dist/server/routes/improvise.js.map +1 -0
  76. package/dist/server/routes/index.d.ts +10 -0
  77. package/dist/server/routes/index.d.ts.map +1 -0
  78. package/dist/server/routes/index.js +12 -0
  79. package/dist/server/routes/index.js.map +1 -0
  80. package/dist/server/routes/instances.d.ts +10 -0
  81. package/dist/server/routes/instances.d.ts.map +1 -0
  82. package/dist/server/routes/instances.js +47 -0
  83. package/dist/server/routes/instances.js.map +1 -0
  84. package/dist/server/routes/notifications.d.ts +3 -0
  85. package/dist/server/routes/notifications.d.ts.map +1 -0
  86. package/dist/server/routes/notifications.js +136 -0
  87. package/dist/server/routes/notifications.js.map +1 -0
  88. package/dist/server/services/analytics.d.ts +56 -0
  89. package/dist/server/services/analytics.d.ts.map +1 -0
  90. package/dist/server/services/analytics.js +240 -0
  91. package/dist/server/services/analytics.js.map +1 -0
  92. package/dist/server/services/auth.d.ts +26 -0
  93. package/dist/server/services/auth.d.ts.map +1 -0
  94. package/dist/server/services/auth.js +71 -0
  95. package/dist/server/services/auth.js.map +1 -0
  96. package/dist/server/services/client-id.d.ts +10 -0
  97. package/dist/server/services/client-id.d.ts.map +1 -0
  98. package/dist/server/services/client-id.js +61 -0
  99. package/dist/server/services/client-id.js.map +1 -0
  100. package/dist/server/services/credentials.d.ts +39 -0
  101. package/dist/server/services/credentials.d.ts.map +1 -0
  102. package/dist/server/services/credentials.js +110 -0
  103. package/dist/server/services/credentials.js.map +1 -0
  104. package/dist/server/services/files.d.ts +119 -0
  105. package/dist/server/services/files.d.ts.map +1 -0
  106. package/dist/server/services/files.js +560 -0
  107. package/dist/server/services/files.js.map +1 -0
  108. package/dist/server/services/instances.d.ts +52 -0
  109. package/dist/server/services/instances.d.ts.map +1 -0
  110. package/dist/server/services/instances.js +241 -0
  111. package/dist/server/services/instances.js.map +1 -0
  112. package/dist/server/services/pathUtils.d.ts +47 -0
  113. package/dist/server/services/pathUtils.d.ts.map +1 -0
  114. package/dist/server/services/pathUtils.js +124 -0
  115. package/dist/server/services/pathUtils.js.map +1 -0
  116. package/dist/server/services/platform.d.ts +72 -0
  117. package/dist/server/services/platform.d.ts.map +1 -0
  118. package/dist/server/services/platform.js +368 -0
  119. package/dist/server/services/platform.js.map +1 -0
  120. package/dist/server/services/sentry.d.ts +5 -0
  121. package/dist/server/services/sentry.d.ts.map +1 -0
  122. package/dist/server/services/sentry.js +71 -0
  123. package/dist/server/services/sentry.js.map +1 -0
  124. package/dist/server/services/terminal/pty-manager.d.ts +149 -0
  125. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
  126. package/dist/server/services/terminal/pty-manager.js +377 -0
  127. package/dist/server/services/terminal/pty-manager.js.map +1 -0
  128. package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
  129. package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
  130. package/dist/server/services/terminal/tmux-manager.js +352 -0
  131. package/dist/server/services/terminal/tmux-manager.js.map +1 -0
  132. package/dist/server/services/websocket/autocomplete.d.ts +50 -0
  133. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
  134. package/dist/server/services/websocket/autocomplete.js +361 -0
  135. package/dist/server/services/websocket/autocomplete.js.map +1 -0
  136. package/dist/server/services/websocket/file-utils.d.ts +44 -0
  137. package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-utils.js +272 -0
  139. package/dist/server/services/websocket/file-utils.js.map +1 -0
  140. package/dist/server/services/websocket/handler.d.ts +246 -0
  141. package/dist/server/services/websocket/handler.d.ts.map +1 -0
  142. package/dist/server/services/websocket/handler.js +1771 -0
  143. package/dist/server/services/websocket/handler.js.map +1 -0
  144. package/dist/server/services/websocket/index.d.ts +11 -0
  145. package/dist/server/services/websocket/index.d.ts.map +1 -0
  146. package/dist/server/services/websocket/index.js +14 -0
  147. package/dist/server/services/websocket/index.js.map +1 -0
  148. package/dist/server/services/websocket/types.d.ts +214 -0
  149. package/dist/server/services/websocket/types.d.ts.map +1 -0
  150. package/dist/server/services/websocket/types.js +4 -0
  151. package/dist/server/services/websocket/types.js.map +1 -0
  152. package/dist/server/utils/agent-manager.d.ts +69 -0
  153. package/dist/server/utils/agent-manager.d.ts.map +1 -0
  154. package/dist/server/utils/agent-manager.js +269 -0
  155. package/dist/server/utils/agent-manager.js.map +1 -0
  156. package/dist/server/utils/paths.d.ts +25 -0
  157. package/dist/server/utils/paths.d.ts.map +1 -0
  158. package/dist/server/utils/paths.js +38 -0
  159. package/dist/server/utils/paths.js.map +1 -0
  160. package/dist/server/utils/port-manager.d.ts +10 -0
  161. package/dist/server/utils/port-manager.d.ts.map +1 -0
  162. package/dist/server/utils/port-manager.js +60 -0
  163. package/dist/server/utils/port-manager.js.map +1 -0
  164. package/dist/server/utils/port.d.ts +26 -0
  165. package/dist/server/utils/port.d.ts.map +1 -0
  166. package/dist/server/utils/port.js +83 -0
  167. package/dist/server/utils/port.js.map +1 -0
  168. package/hooks/bouncer.sh +138 -0
  169. package/package.json +74 -0
  170. package/server/README.md +191 -0
  171. package/server/cli/headless/claude-invoker.ts +415 -0
  172. package/server/cli/headless/index.ts +39 -0
  173. package/server/cli/headless/mcp-config.ts +87 -0
  174. package/server/cli/headless/output-utils.ts +109 -0
  175. package/server/cli/headless/prompt-utils.ts +108 -0
  176. package/server/cli/headless/runner.ts +133 -0
  177. package/server/cli/headless/types.ts +118 -0
  178. package/server/cli/improvisation-session-manager.ts +531 -0
  179. package/server/index.ts +456 -0
  180. package/server/mcp/README.md +122 -0
  181. package/server/mcp/bouncer-cli.ts +127 -0
  182. package/server/mcp/bouncer-integration.ts +430 -0
  183. package/server/mcp/security-audit.ts +180 -0
  184. package/server/mcp/security-patterns.ts +290 -0
  185. package/server/mcp/server.ts +174 -0
  186. package/server/routes/files.ts +29 -0
  187. package/server/routes/improvise.ts +82 -0
  188. package/server/routes/index.ts +13 -0
  189. package/server/routes/instances.ts +54 -0
  190. package/server/routes/notifications.ts +158 -0
  191. package/server/services/analytics.ts +277 -0
  192. package/server/services/auth.ts +80 -0
  193. package/server/services/client-id.ts +68 -0
  194. package/server/services/credentials.ts +134 -0
  195. package/server/services/files.ts +710 -0
  196. package/server/services/instances.ts +275 -0
  197. package/server/services/pathUtils.ts +158 -0
  198. package/server/services/platform.test.ts +1314 -0
  199. package/server/services/platform.ts +435 -0
  200. package/server/services/sentry.ts +81 -0
  201. package/server/services/terminal/pty-manager.ts +464 -0
  202. package/server/services/terminal/tmux-manager.ts +426 -0
  203. package/server/services/websocket/autocomplete.ts +438 -0
  204. package/server/services/websocket/file-utils.ts +305 -0
  205. package/server/services/websocket/handler.test.ts +20 -0
  206. package/server/services/websocket/handler.ts +2047 -0
  207. package/server/services/websocket/index.ts +40 -0
  208. package/server/services/websocket/types.ts +339 -0
  209. package/server/tsconfig.json +19 -0
  210. package/server/utils/agent-manager.ts +323 -0
  211. package/server/utils/paths.ts +45 -0
  212. package/server/utils/port-manager.ts +70 -0
  213. 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
+ }