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,426 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tmux Session Manager - Provides persistent terminal sessions via tmux
|
|
6
|
+
*
|
|
7
|
+
* When enabled, terminals are run inside tmux sessions, which allows:
|
|
8
|
+
* - Sessions to survive client restarts
|
|
9
|
+
* - True process persistence across browser disconnections
|
|
10
|
+
* - Session restoration even after server restart
|
|
11
|
+
*
|
|
12
|
+
* Tmux sessions are named with a prefix to identify them as mstro-managed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const MSTRO_TMUX_PREFIX = 'mstro-terminal-';
|
|
22
|
+
const SESSION_REGISTRY_PATH = join(homedir(), '.mstro', 'terminal-sessions.json');
|
|
23
|
+
|
|
24
|
+
export interface TmuxSession {
|
|
25
|
+
terminalId: string;
|
|
26
|
+
tmuxSessionName: string;
|
|
27
|
+
shell: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
lastAttachedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SessionRegistry {
|
|
34
|
+
sessions: TmuxSession[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if tmux is available on the system
|
|
39
|
+
*/
|
|
40
|
+
export function isTmuxAvailable(): boolean {
|
|
41
|
+
try {
|
|
42
|
+
const result = spawnSync('which', ['tmux'], { encoding: 'utf-8' });
|
|
43
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* TmuxManager - Manages tmux-backed terminal sessions
|
|
51
|
+
*/
|
|
52
|
+
export class TmuxManager extends EventEmitter {
|
|
53
|
+
private sessions: Map<string, TmuxSession> = new Map();
|
|
54
|
+
private outputHandlers: Map<string, NodeJS.Timeout> = new Map();
|
|
55
|
+
private tmuxAvailable: boolean;
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
super();
|
|
59
|
+
this.tmuxAvailable = isTmuxAvailable();
|
|
60
|
+
|
|
61
|
+
if (this.tmuxAvailable) {
|
|
62
|
+
this.loadRegistry();
|
|
63
|
+
this.syncWithTmux();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if tmux persistence is available
|
|
69
|
+
*/
|
|
70
|
+
isAvailable(): boolean {
|
|
71
|
+
return this.tmuxAvailable;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load session registry from disk
|
|
76
|
+
*/
|
|
77
|
+
private loadRegistry(): void {
|
|
78
|
+
try {
|
|
79
|
+
if (existsSync(SESSION_REGISTRY_PATH)) {
|
|
80
|
+
const data = readFileSync(SESSION_REGISTRY_PATH, 'utf-8');
|
|
81
|
+
const registry: SessionRegistry = JSON.parse(data);
|
|
82
|
+
for (const session of registry.sessions) {
|
|
83
|
+
this.sessions.set(session.terminalId, session);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('[TmuxManager] Failed to load registry:', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Save session registry to disk
|
|
93
|
+
*/
|
|
94
|
+
private saveRegistry(): void {
|
|
95
|
+
try {
|
|
96
|
+
const dir = join(homedir(), '.mstro');
|
|
97
|
+
if (!existsSync(dir)) {
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const registry: SessionRegistry = {
|
|
102
|
+
sessions: Array.from(this.sessions.values()),
|
|
103
|
+
};
|
|
104
|
+
writeFileSync(SESSION_REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('[TmuxManager] Failed to save registry:', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sync our registry with actual tmux sessions
|
|
112
|
+
* Remove sessions that no longer exist in tmux
|
|
113
|
+
*/
|
|
114
|
+
private syncWithTmux(): void {
|
|
115
|
+
if (!this.tmuxAvailable) return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// List all tmux sessions
|
|
119
|
+
const result = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], {
|
|
120
|
+
encoding: 'utf-8',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (result.status !== 0) {
|
|
124
|
+
// No tmux server running - clear all sessions
|
|
125
|
+
this.sessions.clear();
|
|
126
|
+
this.saveRegistry();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const existingSessions = new Set(
|
|
131
|
+
result.stdout
|
|
132
|
+
.trim()
|
|
133
|
+
.split('\n')
|
|
134
|
+
.filter((name) => name.startsWith(MSTRO_TMUX_PREFIX))
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Remove sessions that no longer exist
|
|
138
|
+
for (const [terminalId, session] of this.sessions) {
|
|
139
|
+
if (!existingSessions.has(session.tmuxSessionName)) {
|
|
140
|
+
this.sessions.delete(terminalId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.saveRegistry();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('[TmuxManager] Failed to sync with tmux:', error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if a session exists
|
|
152
|
+
*/
|
|
153
|
+
exists(terminalId: string): boolean {
|
|
154
|
+
const session = this.sessions.get(terminalId);
|
|
155
|
+
if (!session) return false;
|
|
156
|
+
|
|
157
|
+
// Verify it still exists in tmux
|
|
158
|
+
try {
|
|
159
|
+
const result = spawnSync('tmux', ['has-session', '-t', session.tmuxSessionName], {
|
|
160
|
+
encoding: 'utf-8',
|
|
161
|
+
});
|
|
162
|
+
return result.status === 0;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get session info
|
|
170
|
+
*/
|
|
171
|
+
getSessionInfo(terminalId: string): TmuxSession | null {
|
|
172
|
+
return this.sessions.get(terminalId) || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all active sessions
|
|
177
|
+
*/
|
|
178
|
+
getActiveSessions(): TmuxSession[] {
|
|
179
|
+
this.syncWithTmux();
|
|
180
|
+
return Array.from(this.sessions.values());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a new tmux-backed terminal session
|
|
185
|
+
*/
|
|
186
|
+
create(
|
|
187
|
+
terminalId: string,
|
|
188
|
+
workingDir: string,
|
|
189
|
+
cols: number = 80,
|
|
190
|
+
rows: number = 24,
|
|
191
|
+
shell?: string
|
|
192
|
+
): { shell: string; cwd: string; isReconnect: boolean } {
|
|
193
|
+
if (!this.tmuxAvailable) {
|
|
194
|
+
throw new Error('tmux is not available');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if session already exists
|
|
198
|
+
if (this.exists(terminalId)) {
|
|
199
|
+
const session = this.sessions.get(terminalId)!;
|
|
200
|
+
session.lastAttachedAt = Date.now();
|
|
201
|
+
this.saveRegistry();
|
|
202
|
+
return {
|
|
203
|
+
shell: session.shell,
|
|
204
|
+
cwd: session.cwd,
|
|
205
|
+
isReconnect: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tmuxSessionName = `${MSTRO_TMUX_PREFIX}${terminalId}`;
|
|
210
|
+
const cwd = workingDir || homedir();
|
|
211
|
+
const shellPath = shell || process.env.SHELL || '/bin/bash';
|
|
212
|
+
const shellName = shellPath.split('/').pop() || 'shell';
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// Create new tmux session
|
|
217
|
+
const result = spawnSync(
|
|
218
|
+
'tmux',
|
|
219
|
+
[
|
|
220
|
+
'new-session',
|
|
221
|
+
'-d', // Detached
|
|
222
|
+
'-s', tmuxSessionName,
|
|
223
|
+
'-x', cols.toString(),
|
|
224
|
+
'-y', rows.toString(),
|
|
225
|
+
'-c', cwd,
|
|
226
|
+
shellPath,
|
|
227
|
+
],
|
|
228
|
+
{
|
|
229
|
+
encoding: 'utf-8',
|
|
230
|
+
cwd,
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (result.status !== 0) {
|
|
235
|
+
throw new Error(`Failed to create tmux session: ${result.stderr}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Store session info
|
|
239
|
+
const session: TmuxSession = {
|
|
240
|
+
terminalId,
|
|
241
|
+
tmuxSessionName,
|
|
242
|
+
shell: shellName,
|
|
243
|
+
cwd,
|
|
244
|
+
createdAt: Date.now(),
|
|
245
|
+
lastAttachedAt: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
this.sessions.set(terminalId, session);
|
|
248
|
+
this.saveRegistry();
|
|
249
|
+
|
|
250
|
+
return { shell: shellName, cwd, isReconnect: false };
|
|
251
|
+
} catch (error: any) {
|
|
252
|
+
console.error(`[TmuxManager] Failed to create session:`, error);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Attach to a tmux session and stream output
|
|
259
|
+
* Returns a function to detach
|
|
260
|
+
*/
|
|
261
|
+
attach(
|
|
262
|
+
terminalId: string,
|
|
263
|
+
onOutput: (data: string) => void,
|
|
264
|
+
onExit: (code: number) => void
|
|
265
|
+
): { write: (data: string) => void; resize: (cols: number, rows: number) => void; detach: () => void } {
|
|
266
|
+
const session = this.sessions.get(terminalId);
|
|
267
|
+
if (!session) {
|
|
268
|
+
throw new Error(`Session not found: ${terminalId}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update last attached time
|
|
272
|
+
session.lastAttachedAt = Date.now();
|
|
273
|
+
this.saveRegistry();
|
|
274
|
+
|
|
275
|
+
// Create a pipe process to capture tmux output
|
|
276
|
+
// We use 'tmux pipe-pane' to capture output and 'tmux send-keys' for input
|
|
277
|
+
const _pipePath = `/tmp/mstro-tmux-pipe-${terminalId}`;
|
|
278
|
+
|
|
279
|
+
// Start capturing output using tmux's capture-pane in a loop
|
|
280
|
+
let capturing = true;
|
|
281
|
+
let lastCaptureLength = 0;
|
|
282
|
+
|
|
283
|
+
const captureLoop = setInterval(() => {
|
|
284
|
+
if (!capturing) {
|
|
285
|
+
clearInterval(captureLoop);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
// Capture the current pane content
|
|
291
|
+
const result = spawnSync(
|
|
292
|
+
'tmux',
|
|
293
|
+
['capture-pane', '-t', session.tmuxSessionName, '-p', '-S', '-100'],
|
|
294
|
+
{ encoding: 'utf-8' }
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (result.status === 0) {
|
|
298
|
+
const output = result.stdout;
|
|
299
|
+
// Only emit new content
|
|
300
|
+
if (output.length > lastCaptureLength) {
|
|
301
|
+
const newContent = output.slice(lastCaptureLength);
|
|
302
|
+
if (newContent.trim()) {
|
|
303
|
+
onOutput(newContent);
|
|
304
|
+
}
|
|
305
|
+
lastCaptureLength = output.length;
|
|
306
|
+
}
|
|
307
|
+
} else if (result.stderr?.includes('no server running')) {
|
|
308
|
+
// Session ended
|
|
309
|
+
capturing = false;
|
|
310
|
+
clearInterval(captureLoop);
|
|
311
|
+
onExit(0);
|
|
312
|
+
this.sessions.delete(terminalId);
|
|
313
|
+
this.saveRegistry();
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('[TmuxManager] Capture error:', error);
|
|
317
|
+
}
|
|
318
|
+
}, 100); // Poll every 100ms
|
|
319
|
+
|
|
320
|
+
this.outputHandlers.set(terminalId, captureLoop);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
write: (data: string) => {
|
|
324
|
+
try {
|
|
325
|
+
// Send keys to tmux session
|
|
326
|
+
spawnSync('tmux', ['send-keys', '-t', session.tmuxSessionName, '-l', data], {
|
|
327
|
+
encoding: 'utf-8',
|
|
328
|
+
});
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('[TmuxManager] Write error:', error);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
resize: (cols: number, rows: number) => {
|
|
335
|
+
try {
|
|
336
|
+
spawnSync(
|
|
337
|
+
'tmux',
|
|
338
|
+
['resize-window', '-t', session.tmuxSessionName, '-x', cols.toString(), '-y', rows.toString()],
|
|
339
|
+
{ encoding: 'utf-8' }
|
|
340
|
+
);
|
|
341
|
+
} catch (_error) {
|
|
342
|
+
// Resize errors are not critical
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
detach: () => {
|
|
347
|
+
capturing = false;
|
|
348
|
+
const handler = this.outputHandlers.get(terminalId);
|
|
349
|
+
if (handler) {
|
|
350
|
+
clearInterval(handler);
|
|
351
|
+
this.outputHandlers.delete(terminalId);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get scrollback/history from tmux session
|
|
359
|
+
*/
|
|
360
|
+
getScrollback(terminalId: string, lines: number = 5000): string[] {
|
|
361
|
+
const session = this.sessions.get(terminalId);
|
|
362
|
+
if (!session) return [];
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const result = spawnSync(
|
|
366
|
+
'tmux',
|
|
367
|
+
['capture-pane', '-t', session.tmuxSessionName, '-p', '-S', `-${lines}`],
|
|
368
|
+
{ encoding: 'utf-8' }
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (result.status === 0) {
|
|
372
|
+
return result.stdout.split('\n');
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('[TmuxManager] Failed to get scrollback:', error);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Close/kill a tmux session
|
|
383
|
+
*/
|
|
384
|
+
close(terminalId: string): boolean {
|
|
385
|
+
const session = this.sessions.get(terminalId);
|
|
386
|
+
if (!session) return false;
|
|
387
|
+
|
|
388
|
+
// Stop output capture if running
|
|
389
|
+
const handler = this.outputHandlers.get(terminalId);
|
|
390
|
+
if (handler) {
|
|
391
|
+
clearInterval(handler);
|
|
392
|
+
this.outputHandlers.delete(terminalId);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
spawnSync('tmux', ['kill-session', '-t', session.tmuxSessionName], {
|
|
397
|
+
encoding: 'utf-8',
|
|
398
|
+
});
|
|
399
|
+
this.sessions.delete(terminalId);
|
|
400
|
+
this.saveRegistry();
|
|
401
|
+
return true;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('[TmuxManager] Failed to close session:', error);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Close all mstro-managed tmux sessions
|
|
410
|
+
*/
|
|
411
|
+
closeAll(): void {
|
|
412
|
+
for (const terminalId of this.sessions.keys()) {
|
|
413
|
+
this.close(terminalId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Singleton instance
|
|
419
|
+
let tmuxManagerInstance: TmuxManager | null = null;
|
|
420
|
+
|
|
421
|
+
export function getTmuxManager(): TmuxManager {
|
|
422
|
+
if (!tmuxManagerInstance) {
|
|
423
|
+
tmuxManagerInstance = new TmuxManager();
|
|
424
|
+
}
|
|
425
|
+
return tmuxManagerInstance;
|
|
426
|
+
}
|