treesap 0.1.8 → 0.1.10
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/components/Sidebar.d.ts +8 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.js +6 -0
- package/dist/components/Sidebar.js.map +1 -0
- package/dist/components/SimpleLivePreview.js +1 -1
- package/dist/components/SimpleLivePreview.js.map +1 -1
- package/dist/layouts/Layout.js +1 -1
- package/dist/layouts/Layout.js.map +1 -1
- package/dist/pages/Code.d.ts.map +1 -1
- package/dist/pages/Code.js +2 -2
- package/dist/pages/Code.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +81 -11
- package/dist/server.js.map +1 -1
- package/dist/services/terminal.d.ts +25 -1
- package/dist/services/terminal.d.ts.map +1 -1
- package/dist/services/terminal.js +135 -6
- package/dist/services/terminal.js.map +1 -1
- package/dist/services/websocket.d.ts +45 -0
- package/dist/services/websocket.d.ts.map +1 -0
- package/dist/services/websocket.js +306 -0
- package/dist/services/websocket.js.map +1 -0
- package/dist/static/components/Sidebar.js +225 -0
- package/dist/static/components/SimpleLivePreview.js +73 -53
- package/dist/static/components/Terminal.js +141 -61
- package/dist/static/signals/SidebarSignal.js +123 -0
- package/dist/static/signals/TerminalSignal.js +137 -2
- package/dist/static/styles/main.css +111 -25
- package/package.json +6 -2
- package/src/components/Sidebar.tsx +92 -0
- package/src/components/SimpleLivePreview.tsx +4 -4
- package/src/layouts/Layout.tsx +1 -1
- package/src/pages/Code.tsx +18 -145
- package/src/server.tsx +97 -12
- package/src/services/terminal.ts +164 -6
- package/src/services/websocket.ts +374 -0
- package/src/static/components/Sidebar.js +225 -0
- package/src/static/components/SimpleLivePreview.js +73 -53
- package/src/static/components/Terminal.js +141 -61
- package/src/static/signals/SidebarSignal.js +123 -0
- package/src/static/signals/TerminalSignal.js +137 -2
- package/src/static/styles/main.css +111 -25
- package/tailwind.config.ts +10 -0
package/src/pages/Code.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Layout from "../layouts/Layout.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Sidebar } from "../components/Sidebar.js";
|
|
3
3
|
import { SimpleLivePreview } from "../components/SimpleLivePreview.js";
|
|
4
4
|
|
|
5
5
|
interface TerminalProps {
|
|
@@ -10,152 +10,25 @@ interface TerminalProps {
|
|
|
10
10
|
export function Code({ previewPort = 1234, workingDirectory }: TerminalProps) {
|
|
11
11
|
return (
|
|
12
12
|
<Layout title="Code Editor">
|
|
13
|
-
<div id="code-container" class="h-screen flex bg-[#1e1e1e]">
|
|
14
|
-
{/*
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<button
|
|
27
|
-
type="button"
|
|
28
|
-
id="live-preview-refresh-btn"
|
|
29
|
-
class="p-2 hover:bg-[#3c3c3c] rounded-md transition-colors flex-shrink-0 text-[#cccccc] hover:text-white"
|
|
30
|
-
title="Reload"
|
|
31
|
-
>
|
|
32
|
-
<iconify-icon icon="tabler:refresh" width="16" height="16"></iconify-icon>
|
|
33
|
-
</button>
|
|
34
|
-
<div class="flex-1 flex items-center bg-[#1e1e1e] border border-[#3c3c3c] rounded px-3 py-2 hover:border-[#0e639c] focus-within:border-[#0e639c] transition-all">
|
|
35
|
-
<iconify-icon icon="tabler:world" width="16" height="16" class="text-[#cccccc] mr-2"></iconify-icon>
|
|
36
|
-
<span class="text-[#cccccc] text-sm">localhost:{previewPort}/</span>
|
|
37
|
-
<input
|
|
38
|
-
id="live-preview-url-input"
|
|
39
|
-
type="text"
|
|
40
|
-
placeholder="path"
|
|
41
|
-
defaultValue=""
|
|
42
|
-
class="flex-1 bg-transparent text-sm focus:outline-none text-[#cccccc] ml-1"
|
|
43
|
-
/>
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
id="live-preview-load-btn"
|
|
47
|
-
class="ml-2 p-1 hover:bg-[#3c3c3c] rounded transition-colors flex-shrink-0 text-[#cccccc] hover:text-white"
|
|
48
|
-
title="Go"
|
|
49
|
-
>
|
|
50
|
-
<iconify-icon icon="tabler:chevron-right" width="16" height="16"></iconify-icon>
|
|
51
|
-
</button>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
{/* Tab Headers */}
|
|
57
|
-
<div class="flex py-2">
|
|
58
|
-
<button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
|
|
59
|
-
<iconify-icon icon="tabler:folder" width="20" height="20"></iconify-icon>
|
|
60
|
-
</button>
|
|
61
|
-
<button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
|
|
62
|
-
<iconify-icon icon="tabler:search" width="20" height="20"></iconify-icon>
|
|
63
|
-
</button>
|
|
64
|
-
<button class="px-3 py-2 text-white flex items-center justify-center">
|
|
65
|
-
<iconify-icon icon="tabler:terminal" width="20" height="20"></iconify-icon>
|
|
66
|
-
</button>
|
|
67
|
-
<button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
|
|
68
|
-
<iconify-icon icon="tabler:git-merge" width="20" height="20"></iconify-icon>
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{/* Tab Content */}
|
|
73
|
-
<div class="flex-1 overflow-hidden bg-[#1e1e1e]">
|
|
74
|
-
{/* Terminal Tab Content (Active) */}
|
|
75
|
-
<div class="h-full flex flex-col">
|
|
76
|
-
|
|
77
|
-
{/* Terminal Tabs Bar */}
|
|
78
|
-
<div class="border-b border-[#3c3c3c] bg-[#252526] px-3 py-1">
|
|
79
|
-
<div class="flex items-center gap-1">
|
|
80
|
-
{/* Terminal Tab 1 */}
|
|
81
|
-
<button
|
|
82
|
-
id="terminal-tab-1"
|
|
83
|
-
class="terminal-tab flex items-center px-3 py-1 text-sm text-white bg-[#1e1e1e] border-t-2 border-[#0e639c] rounded-t-sm hover:bg-[#2d2d30] transition-colors"
|
|
84
|
-
data-terminal-index="1"
|
|
85
|
-
>
|
|
86
|
-
<iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
|
|
87
|
-
Terminal 1
|
|
88
|
-
<button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="1" style="display: none;">
|
|
89
|
-
<iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
|
|
90
|
-
</button>
|
|
91
|
-
</button>
|
|
92
|
-
|
|
93
|
-
{/* Terminal Tab 2 */}
|
|
94
|
-
<button
|
|
95
|
-
id="terminal-tab-2"
|
|
96
|
-
class="terminal-tab flex items-center px-3 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded-t-sm"
|
|
97
|
-
data-terminal-index="2"
|
|
98
|
-
style="display: none;"
|
|
99
|
-
>
|
|
100
|
-
<iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
|
|
101
|
-
Terminal 2
|
|
102
|
-
<button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="2">
|
|
103
|
-
<iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
|
|
104
|
-
</button>
|
|
105
|
-
</button>
|
|
106
|
-
|
|
107
|
-
{/* Terminal Tab 3 */}
|
|
108
|
-
<button
|
|
109
|
-
id="terminal-tab-3"
|
|
110
|
-
class="terminal-tab flex items-center px-3 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded-t-sm"
|
|
111
|
-
data-terminal-index="3"
|
|
112
|
-
style="display: none;"
|
|
113
|
-
>
|
|
114
|
-
<iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
|
|
115
|
-
Terminal 3
|
|
116
|
-
<button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="3">
|
|
117
|
-
<iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
|
|
118
|
-
</button>
|
|
119
|
-
</button>
|
|
120
|
-
|
|
121
|
-
{/* Add Terminal Button */}
|
|
122
|
-
<button
|
|
123
|
-
id="add-terminal-btn"
|
|
124
|
-
class="flex items-center px-2 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded"
|
|
125
|
-
title="New Terminal"
|
|
126
|
-
>
|
|
127
|
-
<iconify-icon icon="tabler:plus" width="14" height="14"></iconify-icon>
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
{/* Terminal Content Area */}
|
|
133
|
-
<div class="flex-1 overflow-hidden relative">
|
|
134
|
-
{/* Terminal 1 Container */}
|
|
135
|
-
<div id="terminal-container-1" class="h-full p-4 terminal-container" data-terminal-index="1">
|
|
136
|
-
<TerminalComponent index={1} />
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
{/* Terminal 2 Container */}
|
|
140
|
-
<div id="terminal-container-2" class="h-full p-4 terminal-container" data-terminal-index="2" style="display: none;">
|
|
141
|
-
<TerminalComponent index={2} />
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{/* Terminal 3 Container */}
|
|
145
|
-
<div id="terminal-container-3" class="h-full p-4 terminal-container" data-terminal-index="3" style="display: none;">
|
|
146
|
-
<TerminalComponent index={3} />
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
13
|
+
<div id="code-container" class="h-screen flex bg-[#1e1e1e] relative">
|
|
14
|
+
{/* Mobile toggle button */}
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
id="mobile-sidebar-toggle"
|
|
18
|
+
class="fixed top-4 left-4 z-60 p-3 bg-[#2d2d30] border border-[#3c3c3c] rounded-lg shadow-xl hover:bg-[#3c3c3c] transition-all md:hidden"
|
|
19
|
+
title="Toggle Sidebar"
|
|
20
|
+
>
|
|
21
|
+
<iconify-icon icon="tabler:menu-2" width="20" height="20" class="text-[#cccccc]"></iconify-icon>
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
{/* Sidebar */}
|
|
25
|
+
<Sidebar id="sidebar" previewPort={previewPort} workingDirectory={workingDirectory} />
|
|
152
26
|
|
|
153
|
-
{/*
|
|
154
|
-
<
|
|
27
|
+
{/* Main Content - Live Preview */}
|
|
28
|
+
<div class="flex-1 md:flex-1">
|
|
29
|
+
<SimpleLivePreview id="live-preview" previewPort={previewPort} />
|
|
30
|
+
</div>
|
|
155
31
|
</div>
|
|
156
|
-
|
|
157
|
-
{/* Terminal Tabs Management Script */}
|
|
158
|
-
<script type="module" src="/components/TerminalTabs.js"></script>
|
|
159
32
|
</Layout>
|
|
160
33
|
);
|
|
161
34
|
}
|
package/src/server.tsx
CHANGED
|
@@ -6,9 +6,11 @@ import { Welcome } from "./pages/Welcome.js";
|
|
|
6
6
|
import { Code } from "./pages/Code.js";
|
|
7
7
|
import { DevServerManager } from "./services/dev-server.js";
|
|
8
8
|
import { TerminalService } from "./services/terminal.js";
|
|
9
|
+
import { WebSocketTerminalService } from "./services/websocket.js";
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
import process from "node:process";
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import type { Server } from 'http';
|
|
12
14
|
|
|
13
15
|
export interface TreesapConfig {
|
|
14
16
|
port?: number;
|
|
@@ -144,15 +146,90 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
|
|
|
144
146
|
return c.json({ logs });
|
|
145
147
|
});
|
|
146
148
|
|
|
147
|
-
// List active terminal sessions
|
|
149
|
+
// List active terminal sessions with WebSocket client info
|
|
148
150
|
app.get("/api/terminal/sessions", (c: Context) => {
|
|
149
151
|
const sessions = TerminalService.getAllSessions();
|
|
152
|
+
const wsActiveSessions = WebSocketTerminalService.getActiveSessions();
|
|
153
|
+
|
|
154
|
+
return c.json({
|
|
155
|
+
sessions: sessions.map(session => {
|
|
156
|
+
const wsInfo = wsActiveSessions.find(ws => ws.sessionId === session.id);
|
|
157
|
+
return {
|
|
158
|
+
id: session.id,
|
|
159
|
+
createdAt: session.createdAt,
|
|
160
|
+
lastActivity: session.lastActivity,
|
|
161
|
+
connectedClients: wsInfo ? wsInfo.clientCount : 0
|
|
162
|
+
};
|
|
163
|
+
}),
|
|
164
|
+
totalConnectedClients: WebSocketTerminalService.getConnectedClients()
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Get specific terminal session status
|
|
169
|
+
app.get("/api/terminal/sessions/:sessionId/status", (c: Context) => {
|
|
170
|
+
const sessionId = c.req.param('sessionId');
|
|
171
|
+
const session = TerminalService.getSession(sessionId);
|
|
172
|
+
|
|
173
|
+
if (!session) {
|
|
174
|
+
return c.json({ error: "Session not found" }, 404);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const clients = WebSocketTerminalService.getSessionClients(sessionId);
|
|
178
|
+
|
|
179
|
+
return c.json({
|
|
180
|
+
id: session.id,
|
|
181
|
+
createdAt: session.createdAt,
|
|
182
|
+
lastActivity: session.lastActivity,
|
|
183
|
+
connectedClients: clients.length,
|
|
184
|
+
clientIds: clients
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Send command to terminal via API
|
|
189
|
+
app.post("/api/terminal/sessions/:sessionId/command", async (c: Context) => {
|
|
190
|
+
const sessionId = c.req.param('sessionId');
|
|
191
|
+
const body = await c.req.json();
|
|
192
|
+
const { command } = body;
|
|
193
|
+
|
|
194
|
+
if (!command) {
|
|
195
|
+
return c.json({ error: "Command is required" }, 400);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Get or create terminal session
|
|
199
|
+
let session = TerminalService.getSession(sessionId);
|
|
200
|
+
if (!session) {
|
|
201
|
+
session = TerminalService.createSession(sessionId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const success = WebSocketTerminalService.sendCommandToSession(sessionId, command);
|
|
205
|
+
|
|
206
|
+
if (success) {
|
|
207
|
+
return c.json({
|
|
208
|
+
success: true,
|
|
209
|
+
message: `Command sent to session ${sessionId}`,
|
|
210
|
+
connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
return c.json({ error: "Failed to send command to terminal" }, 500);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Get recent output from terminal session (for API clients)
|
|
218
|
+
app.get("/api/terminal/sessions/:sessionId/output", async (c: Context) => {
|
|
219
|
+
const sessionId = c.req.param('sessionId');
|
|
220
|
+
const session = TerminalService.getSession(sessionId);
|
|
221
|
+
|
|
222
|
+
if (!session) {
|
|
223
|
+
return c.json({ error: "Session not found" }, 404);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Note: This is a basic implementation. For production, you'd want to
|
|
227
|
+
// store recent output history in the TerminalService
|
|
150
228
|
return c.json({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}))
|
|
229
|
+
sessionId,
|
|
230
|
+
message: "Output streaming available via WebSocket connection",
|
|
231
|
+
connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length,
|
|
232
|
+
websocketUrl: `ws://${c.req.header('host')}/terminal/ws`
|
|
156
233
|
});
|
|
157
234
|
});
|
|
158
235
|
|
|
@@ -371,11 +448,23 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
|
|
|
371
448
|
|
|
372
449
|
const { serve } = await import('@hono/node-server');
|
|
373
450
|
|
|
451
|
+
// Start the server and initialize WebSocket
|
|
452
|
+
const server = serve({
|
|
453
|
+
fetch: app.fetch,
|
|
454
|
+
port,
|
|
455
|
+
}) as Server;
|
|
456
|
+
|
|
457
|
+
// Initialize WebSocket service
|
|
458
|
+
WebSocketTerminalService.initialize(server);
|
|
459
|
+
|
|
374
460
|
// Setup global graceful shutdown for all subprocess managers
|
|
375
461
|
const setupGlobalShutdown = () => {
|
|
376
462
|
const cleanup = async () => {
|
|
377
463
|
console.log('\n🛑 Shutting down server and all subprocesses...');
|
|
378
464
|
|
|
465
|
+
// Clean up WebSocket connections
|
|
466
|
+
WebSocketTerminalService.cleanup();
|
|
467
|
+
|
|
379
468
|
// Stop Claude Code process if running
|
|
380
469
|
if (claudeCodeManager) {
|
|
381
470
|
try {
|
|
@@ -425,10 +514,6 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
|
|
|
425
514
|
// Initialize terminal service cleanup
|
|
426
515
|
TerminalService.setupGlobalCleanup();
|
|
427
516
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
port,
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
console.log(`\n🌳 Treesap server running at http://localhost:${port}\n`);
|
|
517
|
+
console.log(`\n🌳 Treesap server running at http://localhost:${port}`);
|
|
518
|
+
console.log(`🔌 WebSocket terminal server available at ws://localhost:${port}/terminal/ws\n`);
|
|
434
519
|
}
|
package/src/services/terminal.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { spawn, ChildProcess } from 'child_process';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import * as pty from 'node-pty';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import * as os from 'node:os';
|
|
4
7
|
|
|
5
8
|
export interface TerminalSession {
|
|
6
9
|
id: string;
|
|
@@ -8,13 +11,29 @@ export interface TerminalSession {
|
|
|
8
11
|
eventEmitter: EventEmitter;
|
|
9
12
|
createdAt: Date;
|
|
10
13
|
lastActivity: Date;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
cols?: number;
|
|
17
|
+
rows?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PersistedSessionData {
|
|
21
|
+
id: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
lastActivity: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
cols: number;
|
|
27
|
+
rows: number;
|
|
11
28
|
}
|
|
12
29
|
|
|
13
30
|
export class TerminalService {
|
|
14
31
|
private static sessions = new Map<string, TerminalSession>();
|
|
15
32
|
private static readonly SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
|
33
|
+
private static readonly PERSISTENCE_DIR = path.join(os.tmpdir(), '.treesap-terminals');
|
|
34
|
+
private static readonly SESSIONS_FILE = path.join(this.PERSISTENCE_DIR, 'sessions.json');
|
|
16
35
|
|
|
17
|
-
static createSession(sessionId: string): TerminalSession {
|
|
36
|
+
static createSession(sessionId: string, options?: { cwd?: string; cols?: number; rows?: number }): TerminalSession {
|
|
18
37
|
// Clean up any existing session with the same ID
|
|
19
38
|
this.destroySession(sessionId);
|
|
20
39
|
|
|
@@ -22,13 +41,26 @@ export class TerminalService {
|
|
|
22
41
|
// Increase max listeners to handle multiple terminal tabs and connections
|
|
23
42
|
eventEmitter.setMaxListeners(20);
|
|
24
43
|
|
|
44
|
+
// Use provided options or defaults
|
|
45
|
+
const cwd = options?.cwd || process.cwd();
|
|
46
|
+
const cols = options?.cols || 80;
|
|
47
|
+
const rows = options?.rows || 24;
|
|
48
|
+
const env: Record<string, string> = {};
|
|
49
|
+
|
|
50
|
+
// Filter out undefined values from process.env
|
|
51
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
52
|
+
if (value !== undefined) {
|
|
53
|
+
env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
// Create a PTY process for proper terminal behavior
|
|
26
58
|
const ptyProcess = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : process.env.SHELL || '/bin/bash', [], {
|
|
27
59
|
name: 'xterm-256color',
|
|
28
|
-
cols
|
|
29
|
-
rows
|
|
30
|
-
cwd
|
|
31
|
-
env
|
|
60
|
+
cols,
|
|
61
|
+
rows,
|
|
62
|
+
cwd,
|
|
63
|
+
env
|
|
32
64
|
});
|
|
33
65
|
|
|
34
66
|
const session: TerminalSession = {
|
|
@@ -36,7 +68,11 @@ export class TerminalService {
|
|
|
36
68
|
process: ptyProcess,
|
|
37
69
|
eventEmitter,
|
|
38
70
|
createdAt: new Date(),
|
|
39
|
-
lastActivity: new Date()
|
|
71
|
+
lastActivity: new Date(),
|
|
72
|
+
cwd,
|
|
73
|
+
env,
|
|
74
|
+
cols,
|
|
75
|
+
rows
|
|
40
76
|
};
|
|
41
77
|
|
|
42
78
|
// Handle process output
|
|
@@ -60,6 +96,9 @@ export class TerminalService {
|
|
|
60
96
|
|
|
61
97
|
// Set up session cleanup
|
|
62
98
|
this.scheduleSessionCleanup(sessionId);
|
|
99
|
+
|
|
100
|
+
// Persist session data
|
|
101
|
+
this.persistSessionData(session);
|
|
63
102
|
|
|
64
103
|
return session;
|
|
65
104
|
}
|
|
@@ -100,6 +139,9 @@ export class TerminalService {
|
|
|
100
139
|
// Remove from sessions map
|
|
101
140
|
this.sessions.delete(sessionId);
|
|
102
141
|
|
|
142
|
+
// Remove from persistent storage
|
|
143
|
+
this.removePersistedSession(sessionId);
|
|
144
|
+
|
|
103
145
|
return true;
|
|
104
146
|
} catch (error) {
|
|
105
147
|
console.error(`Error destroying session ${sessionId}:`, error);
|
|
@@ -144,6 +186,9 @@ export class TerminalService {
|
|
|
144
186
|
}
|
|
145
187
|
|
|
146
188
|
static setupGlobalCleanup(): void {
|
|
189
|
+
// Load persisted sessions on startup
|
|
190
|
+
this.loadPersistedSessions();
|
|
191
|
+
|
|
147
192
|
// Cleanup all sessions on process exit
|
|
148
193
|
const cleanup = () => {
|
|
149
194
|
console.log('Cleaning up all terminal sessions...');
|
|
@@ -164,4 +209,117 @@ export class TerminalService {
|
|
|
164
209
|
}
|
|
165
210
|
}, 5 * 60 * 1000); // Every 5 minutes
|
|
166
211
|
}
|
|
212
|
+
|
|
213
|
+
// Persistence methods
|
|
214
|
+
private static ensurePersistenceDir(): void {
|
|
215
|
+
try {
|
|
216
|
+
if (!fs.existsSync(this.PERSISTENCE_DIR)) {
|
|
217
|
+
fs.mkdirSync(this.PERSISTENCE_DIR, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Error creating persistence directory:', error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private static persistSessionData(session: TerminalSession): void {
|
|
225
|
+
try {
|
|
226
|
+
this.ensurePersistenceDir();
|
|
227
|
+
|
|
228
|
+
const sessionData: PersistedSessionData = {
|
|
229
|
+
id: session.id,
|
|
230
|
+
createdAt: session.createdAt.toISOString(),
|
|
231
|
+
lastActivity: session.lastActivity.toISOString(),
|
|
232
|
+
cwd: session.cwd || process.cwd(),
|
|
233
|
+
env: session.env || (() => {
|
|
234
|
+
const env: Record<string, string> = {};
|
|
235
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
236
|
+
if (value !== undefined) {
|
|
237
|
+
env[key] = value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return env;
|
|
241
|
+
})(),
|
|
242
|
+
cols: session.cols || 80,
|
|
243
|
+
rows: session.rows || 24
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let existingData: PersistedSessionData[] = [];
|
|
247
|
+
if (fs.existsSync(this.SESSIONS_FILE)) {
|
|
248
|
+
const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
|
|
249
|
+
if (content.trim()) {
|
|
250
|
+
existingData = JSON.parse(content);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Remove existing session data if present
|
|
255
|
+
existingData = existingData.filter(s => s.id !== session.id);
|
|
256
|
+
|
|
257
|
+
// Add new session data
|
|
258
|
+
existingData.push(sessionData);
|
|
259
|
+
|
|
260
|
+
fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Error persisting session data:', error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static removePersistedSession(sessionId: string): void {
|
|
267
|
+
try {
|
|
268
|
+
if (!fs.existsSync(this.SESSIONS_FILE)) return;
|
|
269
|
+
|
|
270
|
+
const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
|
|
271
|
+
if (!content.trim()) return;
|
|
272
|
+
|
|
273
|
+
let existingData: PersistedSessionData[] = JSON.parse(content);
|
|
274
|
+
existingData = existingData.filter(s => s.id !== sessionId);
|
|
275
|
+
|
|
276
|
+
fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2));
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('Error removing persisted session:', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private static loadPersistedSessions(): void {
|
|
283
|
+
try {
|
|
284
|
+
if (!fs.existsSync(this.SESSIONS_FILE)) return;
|
|
285
|
+
|
|
286
|
+
const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
|
|
287
|
+
if (!content.trim()) return;
|
|
288
|
+
|
|
289
|
+
const persistedSessions: PersistedSessionData[] = JSON.parse(content);
|
|
290
|
+
|
|
291
|
+
console.log(`Found ${persistedSessions.length} persisted terminal session(s)`);
|
|
292
|
+
|
|
293
|
+
for (const sessionData of persistedSessions) {
|
|
294
|
+
// Check if session is not too old
|
|
295
|
+
const lastActivity = new Date(sessionData.lastActivity);
|
|
296
|
+
const timeSinceLastActivity = Date.now() - lastActivity.getTime();
|
|
297
|
+
|
|
298
|
+
if (timeSinceLastActivity < this.SESSION_TIMEOUT) {
|
|
299
|
+
console.log(`Restoring terminal session: ${sessionData.id}`);
|
|
300
|
+
|
|
301
|
+
// Create new session with the persisted options
|
|
302
|
+
this.createSession(sessionData.id, {
|
|
303
|
+
cwd: sessionData.cwd,
|
|
304
|
+
cols: sessionData.cols,
|
|
305
|
+
rows: sessionData.rows
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
console.log(`Skipping expired session: ${sessionData.id}`);
|
|
309
|
+
this.removePersistedSession(sessionData.id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Error loading persisted sessions:', error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Update session activity and persist
|
|
318
|
+
static updateSessionActivity(sessionId: string): void {
|
|
319
|
+
const session = this.getSession(sessionId);
|
|
320
|
+
if (session) {
|
|
321
|
+
session.lastActivity = new Date();
|
|
322
|
+
this.persistSessionData(session);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
167
325
|
}
|