@myrialabs/clopen 0.0.6 → 0.0.8
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/.env.example +7 -1
- package/backend/index.ts +17 -32
- package/backend/lib/shared/env.ts +9 -2
- package/backend/lib/shared/port-utils.ts +10 -0
- package/backend/lib/terminal/stream-manager.ts +6 -3
- package/backend/ws/terminal/session.ts +48 -0
- package/bun.lock +31 -23
- package/frontend/lib/services/terminal/project.service.ts +65 -10
- package/frontend/lib/services/terminal/terminal.service.ts +19 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +10 -0
- package/package.json +6 -4
- package/scripts/dev.ts +45 -0
- package/vite.config.ts +17 -0
- package/backend/lib/vite-dev.ts +0 -295
package/.env.example
CHANGED
package/backend/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { loggerMiddleware } from './middleware/logger';
|
|
|
11
11
|
import { initializeDatabase, closeDatabase } from './lib/database';
|
|
12
12
|
import { disposeAllEngines } from './lib/engine';
|
|
13
13
|
import { debug } from '$shared/utils/logger';
|
|
14
|
-
import {
|
|
14
|
+
import { findAvailablePort } from './lib/shared/port-utils';
|
|
15
15
|
import { networkInterfaces } from 'os';
|
|
16
16
|
import { resolve } from 'node:path';
|
|
17
17
|
import { statSync } from 'node:fs';
|
|
@@ -22,9 +22,8 @@ import { wsRouter } from './ws';
|
|
|
22
22
|
/**
|
|
23
23
|
* Clopen - Elysia Backend Server
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Production: Serves frontend static files + API
|
|
25
|
+
* Development: Elysia runs on port 9151, Vite dev server proxies /api and /ws from port 9141
|
|
26
|
+
* Production: Elysia runs on port 9141, serves static files from dist/ + API + WebSocket
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
function getLocalIps(): string[] {
|
|
@@ -48,8 +47,8 @@ const app = new Elysia()
|
|
|
48
47
|
.use(errorHandlerMiddleware)
|
|
49
48
|
.use(loggerMiddleware)
|
|
50
49
|
|
|
51
|
-
//
|
|
52
|
-
.get('/health', () => ({
|
|
50
|
+
// API routes
|
|
51
|
+
.get('/api/health', () => ({
|
|
53
52
|
status: 'ok',
|
|
54
53
|
timestamp: new Date().toISOString(),
|
|
55
54
|
environment: SERVER_ENV.NODE_ENV
|
|
@@ -58,20 +57,11 @@ const app = new Elysia()
|
|
|
58
57
|
// Mount WebSocket router (all functionality now via WebSocket)
|
|
59
58
|
.use(wsRouter.asPlugin('/ws'));
|
|
60
59
|
|
|
61
|
-
if (isDevelopment) {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const vite = await initViteDev();
|
|
67
|
-
|
|
68
|
-
app.all('/*', async ({ request }) => handleDevRequest(vite, request));
|
|
69
|
-
|
|
70
|
-
// Store cleanup function for graceful shutdown
|
|
71
|
-
(globalThis as any).__closeViteDev = closeViteDev;
|
|
72
|
-
} else {
|
|
73
|
-
// Production: Read index.html once at startup as a string so Content-Length
|
|
74
|
-
// is always correct. Bun.file() streaming can hang on some platforms.
|
|
60
|
+
if (!isDevelopment) {
|
|
61
|
+
// Production: serve static files manually instead of @elysiajs/static.
|
|
62
|
+
// The static plugin tries to serve directories (like /) as files via Bun.file(),
|
|
63
|
+
// which hangs on some devices/platforms. Using statSync to verify the path is
|
|
64
|
+
// an actual file before serving avoids this issue.
|
|
75
65
|
const distDir = resolve(process.cwd(), 'dist');
|
|
76
66
|
const indexHtml = await Bun.file(resolve(distDir, 'index.html')).text();
|
|
77
67
|
|
|
@@ -93,15 +83,14 @@ if (isDevelopment) {
|
|
|
93
83
|
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
94
84
|
});
|
|
95
85
|
});
|
|
96
|
-
|
|
97
86
|
}
|
|
98
87
|
|
|
99
88
|
// Start server with proper initialization sequence
|
|
100
89
|
async function startServer() {
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
// Find available port — auto-increment if desired port is in use
|
|
91
|
+
const actualPort = await findAvailablePort(PORT);
|
|
92
|
+
if (actualPort !== PORT) {
|
|
93
|
+
debug.log('server', `⚠️ Port ${PORT} in use, using ${actualPort} instead`);
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
// Initialize database first before accepting connections
|
|
@@ -114,14 +103,14 @@ async function startServer() {
|
|
|
114
103
|
|
|
115
104
|
// Start listening after database is ready
|
|
116
105
|
app.listen({
|
|
117
|
-
port:
|
|
106
|
+
port: actualPort,
|
|
118
107
|
hostname: HOST
|
|
119
108
|
}, () => {
|
|
120
|
-
console.log(`🚀 Clopen running at http://localhost:${
|
|
109
|
+
console.log(`🚀 Clopen running at http://localhost:${actualPort}`);
|
|
121
110
|
if (HOST === '0.0.0.0') {
|
|
122
111
|
const ips = getLocalIps();
|
|
123
112
|
for (const ip of ips) {
|
|
124
|
-
console.log(`🌐 Network access: http://${ip}:${
|
|
113
|
+
console.log(`🌐 Network access: http://${ip}:${actualPort}`);
|
|
125
114
|
}
|
|
126
115
|
}
|
|
127
116
|
});
|
|
@@ -136,10 +125,6 @@ startServer().catch((error) => {
|
|
|
136
125
|
async function gracefulShutdown() {
|
|
137
126
|
console.log('\n🛑 Shutting down server...');
|
|
138
127
|
try {
|
|
139
|
-
// Close Vite dev server if running
|
|
140
|
-
if (isDevelopment && (globalThis as any).__closeViteDev) {
|
|
141
|
-
await (globalThis as any).__closeViteDev();
|
|
142
|
-
}
|
|
143
128
|
// Dispose all AI engines
|
|
144
129
|
await disposeAllEngines();
|
|
145
130
|
// Stop accepting new connections
|
|
@@ -18,11 +18,18 @@ import { readFileSync } from 'fs';
|
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
|
|
20
20
|
// ── Server configuration (read once at import time) ─────────────────
|
|
21
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
22
|
+
|
|
21
23
|
export const SERVER_ENV = {
|
|
22
24
|
NODE_ENV: (process.env.NODE_ENV || 'development') as string,
|
|
23
|
-
|
|
25
|
+
/** Backend port — dev: PORT_BACKEND (default 9151), prod: PORT (default 9141) */
|
|
26
|
+
PORT: isDev
|
|
27
|
+
? (process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) : 9151)
|
|
28
|
+
: (process.env.PORT ? parseInt(process.env.PORT) : 9141),
|
|
29
|
+
/** Frontend port — only used in dev for Vite proxy coordination */
|
|
30
|
+
PORT_FRONTEND: process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9141,
|
|
24
31
|
HOST: (process.env.HOST || 'localhost') as string,
|
|
25
|
-
isDevelopment:
|
|
32
|
+
isDevelopment: isDev,
|
|
26
33
|
} as const;
|
|
27
34
|
|
|
28
35
|
// ── .env parsing ────────────────────────────────────────────────────
|
|
@@ -23,3 +23,13 @@ export async function isPortInUse(port: number): Promise<boolean> {
|
|
|
23
23
|
return false; // Connection refused = port is free
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/** Find an available port starting from the given port, incrementing on collision */
|
|
28
|
+
export async function findAvailablePort(startPort: number, maxAttempts = 8): Promise<number> {
|
|
29
|
+
let port = startPort;
|
|
30
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
31
|
+
if (!(await isPortInUse(port))) return port;
|
|
32
|
+
port++;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`No available port found starting from ${startPort} (tried ${maxAttempts} ports)`);
|
|
35
|
+
}
|
|
@@ -49,17 +49,20 @@ class TerminalStreamManager {
|
|
|
49
49
|
): string {
|
|
50
50
|
// Check if there's already a stream for this session
|
|
51
51
|
const existingStreamId = this.sessionToStream.get(sessionId);
|
|
52
|
+
let preservedOutput: string[] = [];
|
|
52
53
|
if (existingStreamId) {
|
|
53
54
|
const existingStream = this.streams.get(existingStreamId);
|
|
54
55
|
if (existingStream) {
|
|
55
|
-
// Clean up existing stream first
|
|
56
56
|
if (existingStream.pty && existingStream.pty !== pty) {
|
|
57
|
-
//
|
|
57
|
+
// Different PTY, kill the old one
|
|
58
58
|
try {
|
|
59
59
|
existingStream.pty.kill();
|
|
60
60
|
} catch (error) {
|
|
61
61
|
// Ignore error if PTY already killed
|
|
62
62
|
}
|
|
63
|
+
} else if (existingStream.pty === pty) {
|
|
64
|
+
// Same PTY (reconnection after browser refresh) - preserve output buffer
|
|
65
|
+
preservedOutput = [...existingStream.output];
|
|
63
66
|
}
|
|
64
67
|
// Remove the old stream
|
|
65
68
|
this.streams.delete(existingStreamId);
|
|
@@ -79,7 +82,7 @@ class TerminalStreamManager {
|
|
|
79
82
|
workingDirectory,
|
|
80
83
|
projectPath,
|
|
81
84
|
projectId,
|
|
82
|
-
output:
|
|
85
|
+
output: preservedOutput,
|
|
83
86
|
processId: pty.pid,
|
|
84
87
|
outputStartIndex: outputStartIndex || 0
|
|
85
88
|
};
|
|
@@ -179,6 +179,22 @@ export const sessionHandler = createRouter()
|
|
|
179
179
|
|
|
180
180
|
debug.log('terminal', `✅ Added fresh listeners to PTY session ${sessionId}`);
|
|
181
181
|
|
|
182
|
+
// Replay historical output for reconnection (e.g., after browser refresh)
|
|
183
|
+
// The stream preserves output from the old stream when reconnecting to the same PTY.
|
|
184
|
+
// Replay from outputStartIndex so frontend receives all output it doesn't have yet.
|
|
185
|
+
const historicalOutput = terminalStreamManager.getOutput(registeredStreamId, outputStartIndex);
|
|
186
|
+
if (historicalOutput.length > 0) {
|
|
187
|
+
debug.log('terminal', `📜 Replaying ${historicalOutput.length} historical output entries for session ${sessionId}`);
|
|
188
|
+
for (const output of historicalOutput) {
|
|
189
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
190
|
+
sessionId,
|
|
191
|
+
content: output,
|
|
192
|
+
projectId,
|
|
193
|
+
timestamp: new Date().toISOString()
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
182
198
|
// Broadcast terminal tab created to all project users
|
|
183
199
|
ws.emit.project(projectId, 'terminal:tab-created', {
|
|
184
200
|
sessionId,
|
|
@@ -379,4 +395,36 @@ export const sessionHandler = createRouter()
|
|
|
379
395
|
message: 'PTY not found'
|
|
380
396
|
};
|
|
381
397
|
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// List active PTY sessions for a project
|
|
401
|
+
// Used after browser refresh to discover existing sessions
|
|
402
|
+
.http('terminal:list-sessions', {
|
|
403
|
+
data: t.Object({
|
|
404
|
+
projectId: t.String()
|
|
405
|
+
}),
|
|
406
|
+
response: t.Object({
|
|
407
|
+
sessions: t.Array(t.Object({
|
|
408
|
+
sessionId: t.String(),
|
|
409
|
+
pid: t.Number(),
|
|
410
|
+
cwd: t.String(),
|
|
411
|
+
createdAt: t.String(),
|
|
412
|
+
lastActivityAt: t.String()
|
|
413
|
+
}))
|
|
414
|
+
})
|
|
415
|
+
}, async ({ data }) => {
|
|
416
|
+
const { projectId } = data;
|
|
417
|
+
|
|
418
|
+
const allSessions = ptySessionManager.getAllSessions();
|
|
419
|
+
const projectSessions = allSessions
|
|
420
|
+
.filter(session => session.projectId === projectId)
|
|
421
|
+
.map(session => ({
|
|
422
|
+
sessionId: session.sessionId,
|
|
423
|
+
pid: session.pty.pid,
|
|
424
|
+
cwd: session.cwd,
|
|
425
|
+
createdAt: session.createdAt.toISOString(),
|
|
426
|
+
lastActivityAt: session.lastActivityAt.toISOString()
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
return { sessions: projectSessions };
|
|
382
430
|
});
|
package/bun.lock
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
|
9
9
|
"@anthropic-ai/sdk": "^0.62.0",
|
|
10
10
|
"@elysiajs/cors": "^1.4.0",
|
|
11
|
-
"@elysiajs/static": "^1.4.7",
|
|
12
11
|
"@iconify-json/lucide": "^1.2.57",
|
|
13
12
|
"@iconify-json/material-icon-theme": "^1.2.16",
|
|
14
13
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -45,6 +44,7 @@
|
|
|
45
44
|
"@types/bun": "^1.2.18",
|
|
46
45
|
"@types/qrcode": "^1.5.6",
|
|
47
46
|
"@types/xterm": "^3.0.0",
|
|
47
|
+
"concurrently": "^9.2.1",
|
|
48
48
|
"eslint": "^9.31.0",
|
|
49
49
|
"eslint-plugin-svelte": "^3.10.1",
|
|
50
50
|
"globals": "^16.3.0",
|
|
@@ -76,8 +76,6 @@
|
|
|
76
76
|
|
|
77
77
|
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
|
|
78
78
|
|
|
79
|
-
"@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="],
|
|
80
|
-
|
|
81
79
|
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
|
82
80
|
|
|
83
81
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="],
|
|
@@ -488,7 +486,7 @@
|
|
|
488
486
|
|
|
489
487
|
"chromium-bidi": ["chromium-bidi@11.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw=="],
|
|
490
488
|
|
|
491
|
-
"cliui": ["cliui@
|
|
489
|
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
492
490
|
|
|
493
491
|
"cloudflared": ["cloudflared@0.7.1", "", { "bin": { "cloudflared": "lib/cloudflared.js" } }, "sha512-jJn1Gu9Tf4qnIu8tfiHZ25Hs8rNcRYSVf8zAd97wvYdOCzftm1CTs1S/RPhijjGi8gUT1p9yzfDi9zYlU/0RwA=="],
|
|
494
492
|
|
|
@@ -506,6 +504,8 @@
|
|
|
506
504
|
|
|
507
505
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
|
508
506
|
|
|
507
|
+
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
|
508
|
+
|
|
509
509
|
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
|
510
510
|
|
|
511
511
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
|
@@ -1008,6 +1008,8 @@
|
|
|
1008
1008
|
|
|
1009
1009
|
"rx.mini": ["rx.mini@1.4.0", "", {}, "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA=="],
|
|
1010
1010
|
|
|
1011
|
+
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
|
1012
|
+
|
|
1011
1013
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
|
1012
1014
|
|
|
1013
1015
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
@@ -1030,6 +1032,8 @@
|
|
|
1030
1032
|
|
|
1031
1033
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
|
1032
1034
|
|
|
1035
|
+
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
|
1036
|
+
|
|
1033
1037
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
|
1034
1038
|
|
|
1035
1039
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
|
@@ -1070,7 +1074,7 @@
|
|
|
1070
1074
|
|
|
1071
1075
|
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
|
1072
1076
|
|
|
1073
|
-
"supports-color": ["supports-color@
|
|
1077
|
+
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
|
1074
1078
|
|
|
1075
1079
|
"svelte": ["svelte@5.36.12", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-c3mWT+b0yBLl3gPGSHiy4pdSQCsPNTjLC0tVoOhrGJ6PPfCzD/RQpAmAfJtQZ304CAae2ph+L3C4aqds3R3seQ=="],
|
|
1076
1080
|
|
|
@@ -1102,6 +1106,8 @@
|
|
|
1102
1106
|
|
|
1103
1107
|
"token-types": ["token-types@6.0.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw=="],
|
|
1104
1108
|
|
|
1109
|
+
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
|
1110
|
+
|
|
1105
1111
|
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
|
1106
1112
|
|
|
1107
1113
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
@@ -1162,7 +1168,7 @@
|
|
|
1162
1168
|
|
|
1163
1169
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
|
1164
1170
|
|
|
1165
|
-
"wrap-ansi": ["wrap-ansi@
|
|
1171
|
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
|
1166
1172
|
|
|
1167
1173
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
1168
1174
|
|
|
@@ -1170,15 +1176,15 @@
|
|
|
1170
1176
|
|
|
1171
1177
|
"xterm": ["xterm@5.3.0", "", {}, "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="],
|
|
1172
1178
|
|
|
1173
|
-
"y18n": ["y18n@
|
|
1179
|
+
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
|
1174
1180
|
|
|
1175
1181
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
|
1176
1182
|
|
|
1177
1183
|
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
|
1178
1184
|
|
|
1179
|
-
"yargs": ["yargs@
|
|
1185
|
+
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
|
1180
1186
|
|
|
1181
|
-
"yargs-parser": ["yargs-parser@
|
|
1187
|
+
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
1182
1188
|
|
|
1183
1189
|
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
|
1184
1190
|
|
|
@@ -1218,8 +1224,6 @@
|
|
|
1218
1224
|
|
|
1219
1225
|
"@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
|
|
1220
1226
|
|
|
1221
|
-
"@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
|
1222
|
-
|
|
1223
1227
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
|
1224
1228
|
|
|
1225
1229
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
|
@@ -1250,6 +1254,8 @@
|
|
|
1250
1254
|
|
|
1251
1255
|
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
1252
1256
|
|
|
1257
|
+
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
1258
|
+
|
|
1253
1259
|
"eslint/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
|
1254
1260
|
|
|
1255
1261
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
@@ -1280,6 +1286,8 @@
|
|
|
1280
1286
|
|
|
1281
1287
|
"puppeteer-core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
1282
1288
|
|
|
1289
|
+
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
|
1290
|
+
|
|
1283
1291
|
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
|
1284
1292
|
|
|
1285
1293
|
"router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
@@ -1322,32 +1330,32 @@
|
|
|
1322
1330
|
|
|
1323
1331
|
"werift-ice/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
1324
1332
|
|
|
1325
|
-
"yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
|
1326
|
-
|
|
1327
1333
|
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
|
1328
1334
|
|
|
1329
1335
|
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
1330
1336
|
|
|
1331
1337
|
"@puppeteer/browsers/tar-fs/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
|
1332
1338
|
|
|
1333
|
-
"@puppeteer/browsers/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
1334
|
-
|
|
1335
|
-
"@puppeteer/browsers/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
|
1336
|
-
|
|
1337
|
-
"@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
1338
|
-
|
|
1339
1339
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
|
1340
1340
|
|
|
1341
1341
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
|
1342
1342
|
|
|
1343
1343
|
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
1344
1344
|
|
|
1345
|
-
"yargs/
|
|
1345
|
+
"qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
|
1346
|
+
|
|
1347
|
+
"qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
|
1348
|
+
|
|
1349
|
+
"qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
|
1350
|
+
|
|
1351
|
+
"qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
|
1352
|
+
|
|
1353
|
+
"qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
|
1346
1354
|
|
|
1347
|
-
"
|
|
1355
|
+
"qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
|
1348
1356
|
|
|
1349
|
-
"yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
|
1357
|
+
"qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
|
1350
1358
|
|
|
1351
|
-
"yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
|
1359
|
+
"qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
|
1352
1360
|
}
|
|
1353
1361
|
}
|
|
@@ -98,28 +98,83 @@ class TerminalProjectManager {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Create initial terminal sessions for a project
|
|
101
|
+
* First checks backend for existing PTY sessions (e.g., after browser refresh)
|
|
101
102
|
*/
|
|
102
103
|
private async createProjectTerminalSessions(projectId: string, projectPath: string): Promise<void> {
|
|
103
|
-
// Creating terminal session for project
|
|
104
|
-
|
|
105
104
|
const context = this.getOrCreateProjectContext(projectId, projectPath);
|
|
106
|
-
|
|
107
|
-
//
|
|
105
|
+
|
|
106
|
+
// Check backend for existing PTY sessions (survives browser refresh)
|
|
107
|
+
const existingBackendSessions = await terminalService.listProjectSessions(projectId);
|
|
108
|
+
|
|
109
|
+
if (existingBackendSessions.length > 0) {
|
|
110
|
+
debug.log('terminal', `Found ${existingBackendSessions.length} existing PTY sessions for project ${projectId}`);
|
|
111
|
+
|
|
112
|
+
// Sort by sessionId to maintain consistent order (terminal-1, terminal-2, etc.)
|
|
113
|
+
existingBackendSessions.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
114
|
+
|
|
115
|
+
// Restore all existing sessions as tabs
|
|
116
|
+
for (const backendSession of existingBackendSessions) {
|
|
117
|
+
const sessionParts = backendSession.sessionId.split('-');
|
|
118
|
+
const terminalNumber = sessionParts[sessionParts.length - 1] || '1';
|
|
119
|
+
|
|
120
|
+
const terminalSession: TerminalSession = {
|
|
121
|
+
id: backendSession.sessionId,
|
|
122
|
+
name: `Terminal ${terminalNumber}`,
|
|
123
|
+
directory: backendSession.cwd || projectPath,
|
|
124
|
+
lines: [],
|
|
125
|
+
commandHistory: [],
|
|
126
|
+
isActive: false,
|
|
127
|
+
createdAt: new Date(backendSession.createdAt),
|
|
128
|
+
lastUsedAt: new Date(backendSession.lastActivityAt),
|
|
129
|
+
shellType: 'Unknown',
|
|
130
|
+
terminalBuffer: undefined,
|
|
131
|
+
projectId: projectId,
|
|
132
|
+
projectPath: projectPath
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
terminalStore.addSession(terminalSession);
|
|
136
|
+
terminalSessionManager.createSession(backendSession.sessionId, projectId, projectPath, backendSession.cwd || projectPath);
|
|
137
|
+
context.sessionIds.push(backendSession.sessionId);
|
|
138
|
+
|
|
139
|
+
// Update nextSessionId to avoid ID conflicts
|
|
140
|
+
const match = backendSession.sessionId.match(/terminal-(\d+)/);
|
|
141
|
+
if (match) {
|
|
142
|
+
terminalStore.updateNextSessionId(parseInt(match[1], 10) + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Restore previously active session from sessionStorage, or default to first
|
|
147
|
+
let activeSessionId = existingBackendSessions[0].sessionId;
|
|
148
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
149
|
+
try {
|
|
150
|
+
const savedActiveId = sessionStorage.getItem(`terminal-active-session-${projectId}`);
|
|
151
|
+
if (savedActiveId && context.sessionIds.includes(savedActiveId)) {
|
|
152
|
+
activeSessionId = savedActiveId;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// sessionStorage not available
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
context.activeSessionId = activeSessionId;
|
|
159
|
+
terminalStore.switchToSession(context.activeSessionId);
|
|
160
|
+
this.persistContexts();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// No existing backend sessions, create 1 new terminal session
|
|
108
165
|
const sessionId = terminalStore.createNewSession(projectPath, projectPath, projectId);
|
|
109
|
-
|
|
110
|
-
// Update the session's directory to ensure it's correct
|
|
166
|
+
|
|
111
167
|
const session = terminalStore.getSession(sessionId);
|
|
112
168
|
if (session) {
|
|
113
169
|
session.directory = projectPath;
|
|
114
170
|
}
|
|
115
|
-
|
|
116
|
-
// Create a fresh session in terminalSessionManager with correct project association
|
|
171
|
+
|
|
117
172
|
terminalSessionManager.createSession(sessionId, projectId, projectPath, projectPath);
|
|
118
|
-
|
|
173
|
+
|
|
119
174
|
context.sessionIds.push(sessionId);
|
|
120
175
|
context.activeSessionId = sessionId;
|
|
121
176
|
terminalStore.switchToSession(sessionId);
|
|
122
|
-
|
|
177
|
+
|
|
123
178
|
this.persistContexts();
|
|
124
179
|
}
|
|
125
180
|
|
|
@@ -339,6 +339,25 @@ export class TerminalService {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* List active PTY sessions for a project on the backend
|
|
344
|
+
* Used after browser refresh to discover existing sessions
|
|
345
|
+
*/
|
|
346
|
+
async listProjectSessions(projectId: string): Promise<Array<{
|
|
347
|
+
sessionId: string;
|
|
348
|
+
pid: number;
|
|
349
|
+
cwd: string;
|
|
350
|
+
createdAt: string;
|
|
351
|
+
lastActivityAt: string;
|
|
352
|
+
}>> {
|
|
353
|
+
try {
|
|
354
|
+
const data = await ws.http('terminal:list-sessions', { projectId }, 5000);
|
|
355
|
+
return data.sessions || [];
|
|
356
|
+
} catch {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
342
361
|
/**
|
|
343
362
|
* Cleanup listeners for a session
|
|
344
363
|
*/
|
|
@@ -138,6 +138,16 @@ export const terminalStore = {
|
|
|
138
138
|
}));
|
|
139
139
|
|
|
140
140
|
terminalState.activeSessionId = sessionId;
|
|
141
|
+
|
|
142
|
+
// Persist active session ID for restoration after browser refresh
|
|
143
|
+
const session = terminalState.sessions.find(s => s.id === sessionId);
|
|
144
|
+
if (session?.projectId && typeof sessionStorage !== 'undefined') {
|
|
145
|
+
try {
|
|
146
|
+
sessionStorage.setItem(`terminal-active-session-${session.projectId}`, sessionId);
|
|
147
|
+
} catch {
|
|
148
|
+
// sessionStorage not available
|
|
149
|
+
}
|
|
150
|
+
}
|
|
141
151
|
},
|
|
142
152
|
|
|
143
153
|
async closeSession(sessionId: string): Promise<boolean> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
|
|
5
5
|
"author": "Myria Labs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
"bun": ">=1.2.12"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"dev": "bun
|
|
48
|
+
"dev": "bun scripts/dev.ts",
|
|
49
|
+
"dev:backend": "bun --watch backend/index.ts",
|
|
50
|
+
"dev:frontend": "bunx vite dev",
|
|
49
51
|
"build": "vite build",
|
|
50
|
-
"start": "bun backend/index.ts",
|
|
52
|
+
"start": "NODE_ENV=production bun backend/index.ts",
|
|
51
53
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
52
54
|
"lint": "eslint .",
|
|
53
55
|
"lint:fix": "eslint . --fix",
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
"@types/bun": "^1.2.18",
|
|
61
63
|
"@types/qrcode": "^1.5.6",
|
|
62
64
|
"@types/xterm": "^3.0.0",
|
|
65
|
+
"concurrently": "^9.2.1",
|
|
63
66
|
"eslint": "^9.31.0",
|
|
64
67
|
"eslint-plugin-svelte": "^3.10.1",
|
|
65
68
|
"globals": "^16.3.0",
|
|
@@ -74,7 +77,6 @@
|
|
|
74
77
|
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
|
75
78
|
"@anthropic-ai/sdk": "^0.62.0",
|
|
76
79
|
"@elysiajs/cors": "^1.4.0",
|
|
77
|
-
"@elysiajs/static": "^1.4.7",
|
|
78
80
|
"@iconify-json/lucide": "^1.2.57",
|
|
79
81
|
"@iconify-json/material-icon-theme": "^1.2.16",
|
|
80
82
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
package/scripts/dev.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Development script — runs backend and frontend concurrently.
|
|
5
|
+
* Resolves available ports before spawning, then passes them via env
|
|
6
|
+
* so Vite proxy and Elysia backend use the same coordinated ports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import concurrently from 'concurrently';
|
|
10
|
+
import { findAvailablePort } from '../backend/lib/shared/port-utils';
|
|
11
|
+
|
|
12
|
+
const desiredBackend = process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) : 9151;
|
|
13
|
+
const desiredFrontend = process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9141;
|
|
14
|
+
|
|
15
|
+
// Resolve available ports
|
|
16
|
+
const backendPort = await findAvailablePort(desiredBackend);
|
|
17
|
+
let frontendPort = await findAvailablePort(desiredFrontend);
|
|
18
|
+
|
|
19
|
+
// Ensure they don't collide
|
|
20
|
+
if (frontendPort === backendPort) {
|
|
21
|
+
frontendPort = await findAvailablePort(frontendPort + 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (backendPort !== desiredBackend) {
|
|
25
|
+
console.log(`⚠️ Backend port ${desiredBackend} in use, using ${backendPort}`);
|
|
26
|
+
}
|
|
27
|
+
if (frontendPort !== desiredFrontend) {
|
|
28
|
+
console.log(`⚠️ Frontend port ${desiredFrontend} in use, using ${frontendPort}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Backend: http://localhost:${backendPort}`);
|
|
32
|
+
console.log(`Frontend: http://localhost:${frontendPort}`);
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
const portEnv = {
|
|
36
|
+
PORT_BACKEND: String(backendPort),
|
|
37
|
+
PORT_FRONTEND: String(frontendPort),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
concurrently([
|
|
41
|
+
{ command: 'bun --watch backend/index.ts', name: 'backend', prefixColor: 'blue', env: portEnv },
|
|
42
|
+
{ command: 'bunx vite dev', name: 'frontend', prefixColor: 'green', env: portEnv },
|
|
43
|
+
], {
|
|
44
|
+
killOthersOn: ['failure'],
|
|
45
|
+
});
|
package/vite.config.ts
CHANGED
|
@@ -3,9 +3,26 @@ import { defineConfig } from 'vite';
|
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
|
|
6
|
+
const frontendPort = parseInt(process.env.PORT_FRONTEND || '9141');
|
|
7
|
+
const backendPort = parseInt(process.env.PORT_BACKEND || '9151');
|
|
8
|
+
|
|
6
9
|
export default defineConfig({
|
|
7
10
|
plugins: [tailwindcss(), svelte()],
|
|
8
11
|
publicDir: 'static',
|
|
12
|
+
server: {
|
|
13
|
+
port: frontendPort,
|
|
14
|
+
strictPort: false,
|
|
15
|
+
proxy: {
|
|
16
|
+
'/api': {
|
|
17
|
+
target: `http://localhost:${backendPort}`,
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
20
|
+
'/ws': {
|
|
21
|
+
target: `ws://localhost:${backendPort}`,
|
|
22
|
+
ws: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
9
26
|
build: {
|
|
10
27
|
outDir: 'dist',
|
|
11
28
|
emptyOutDir: true,
|
package/backend/lib/vite-dev.ts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vite Dev Server Integration
|
|
3
|
-
*
|
|
4
|
-
* Embeds Vite as middleware inside the Elysia/Bun server.
|
|
5
|
-
* Uses Vite's direct APIs (transformIndexHtml, transformRequest) for speed,
|
|
6
|
-
* with Node.js compat middleware adapter only for Vite internals (HMR, pre-bundled deps).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createServer as createViteServer, type ViteDevServer, type Connect } from 'vite';
|
|
10
|
-
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
|
-
import { resolve } from 'node:path';
|
|
12
|
-
import { statSync } from 'node:fs';
|
|
13
|
-
|
|
14
|
-
let vite: ViteDevServer | null = null;
|
|
15
|
-
|
|
16
|
-
const PUBLIC_DIR = resolve(process.cwd(), 'static');
|
|
17
|
-
const INDEX_PATH = resolve(process.cwd(), 'index.html');
|
|
18
|
-
|
|
19
|
-
// Safety timeout for the Node.js compat middleware adapter (ms).
|
|
20
|
-
// Prevents hanging if Vite middleware never calls res.end() or next().
|
|
21
|
-
const MIDDLEWARE_TIMEOUT = 10000;
|
|
22
|
-
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Lifecycle
|
|
25
|
-
// ============================================================================
|
|
26
|
-
|
|
27
|
-
export async function initViteDev(): Promise<ViteDevServer> {
|
|
28
|
-
vite = await createViteServer({
|
|
29
|
-
configFile: './vite.config.ts',
|
|
30
|
-
server: { middlewareMode: true },
|
|
31
|
-
appType: 'spa'
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
return vite;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getViteDev(): ViteDevServer | null {
|
|
38
|
-
return vite;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function closeViteDev(): Promise<void> {
|
|
42
|
-
if (vite) {
|
|
43
|
-
await vite.close();
|
|
44
|
-
vite = null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ============================================================================
|
|
49
|
-
// Main Request Handler
|
|
50
|
-
// ============================================================================
|
|
51
|
-
|
|
52
|
-
export async function handleDevRequest(viteServer: ViteDevServer, request: Request): Promise<Response> {
|
|
53
|
-
const url = new URL(request.url);
|
|
54
|
-
const pathname = url.pathname;
|
|
55
|
-
|
|
56
|
-
// 1. Static public files
|
|
57
|
-
const publicFile = servePublicFile(pathname);
|
|
58
|
-
if (publicFile) return publicFile;
|
|
59
|
-
|
|
60
|
-
// 2. HTML / SPA routes
|
|
61
|
-
if (isHtmlRequest(pathname)) {
|
|
62
|
-
return serveHtml(viteServer, pathname);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 3. Module requests (skip Vite internals like /__vite_hmr)
|
|
66
|
-
if (!pathname.startsWith('/__')) {
|
|
67
|
-
const moduleResponse = await serveModule(viteServer, pathname + url.search, request);
|
|
68
|
-
if (moduleResponse) return moduleResponse;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 4. Fallback: Vite connect middleware (for HMR, pre-bundled deps, etc.)
|
|
72
|
-
const middlewareResponse = await pipeViteMiddleware(viteServer.middlewares, request);
|
|
73
|
-
if (middlewareResponse) return middlewareResponse;
|
|
74
|
-
|
|
75
|
-
return new Response('Not Found', { status: 404 });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// Static Public Files
|
|
80
|
-
// ============================================================================
|
|
81
|
-
|
|
82
|
-
function servePublicFile(pathname: string): Response | null {
|
|
83
|
-
if (pathname === '/' || pathname.includes('..')) return null;
|
|
84
|
-
|
|
85
|
-
const filePath = resolve(PUBLIC_DIR, pathname.slice(1));
|
|
86
|
-
if (!filePath.startsWith(PUBLIC_DIR)) return null;
|
|
87
|
-
|
|
88
|
-
// Use statSync to verify the path is a regular file, not a directory.
|
|
89
|
-
// Bun.file().exists() returns inconsistent results for directories across
|
|
90
|
-
// platforms (Linux/macOS/Windows) and Bun versions, which can cause
|
|
91
|
-
// Response(Bun.file(directory)) to hang indefinitely.
|
|
92
|
-
try {
|
|
93
|
-
if (!statSync(filePath).isFile()) return null;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const file = Bun.file(filePath);
|
|
99
|
-
return new Response(file, {
|
|
100
|
-
headers: {
|
|
101
|
-
'Content-Type': file.type || 'application/octet-stream',
|
|
102
|
-
'Cache-Control': 'public, max-age=3600'
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// HTML / SPA
|
|
109
|
-
// ============================================================================
|
|
110
|
-
|
|
111
|
-
function isHtmlRequest(pathname: string): boolean {
|
|
112
|
-
if (pathname === '/') return true;
|
|
113
|
-
if (pathname.startsWith('/@') || pathname.startsWith('/__')) return false;
|
|
114
|
-
if (pathname.startsWith('/node_modules/')) return false;
|
|
115
|
-
const lastSegment = pathname.split('/').pop() || '';
|
|
116
|
-
return !lastSegment.includes('.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<Response> {
|
|
120
|
-
const rawHtml = await Bun.file(INDEX_PATH).text();
|
|
121
|
-
const html = await viteServer.transformIndexHtml(pathname, rawHtml);
|
|
122
|
-
return new Response(html, {
|
|
123
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ============================================================================
|
|
128
|
-
// Module Requests
|
|
129
|
-
// ============================================================================
|
|
130
|
-
|
|
131
|
-
function getModuleContentType(url: string): string {
|
|
132
|
-
const pathname = url.split('?')[0];
|
|
133
|
-
// Raw CSS only when explicitly requested with ?direct (e.g. <link> tags).
|
|
134
|
-
// CSS imported via JS is transformed to a JS module by Vite (for HMR).
|
|
135
|
-
if (pathname.endsWith('.css') && url.includes('direct')) {
|
|
136
|
-
return 'text/css';
|
|
137
|
-
}
|
|
138
|
-
return 'application/javascript';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function serveModule(viteServer: ViteDevServer, url: string, request: Request): Promise<Response | null> {
|
|
142
|
-
try {
|
|
143
|
-
const result = await viteServer.transformRequest(url);
|
|
144
|
-
if (!result) return null;
|
|
145
|
-
|
|
146
|
-
if (result.etag) {
|
|
147
|
-
const ifNoneMatch = request.headers.get('if-none-match');
|
|
148
|
-
if (ifNoneMatch === result.etag) {
|
|
149
|
-
return new Response(null, { status: 304 });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const headers: Record<string, string> = {
|
|
154
|
-
'Content-Type': getModuleContentType(url),
|
|
155
|
-
};
|
|
156
|
-
if (result.etag) {
|
|
157
|
-
headers['ETag'] = result.etag;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return new Response(result.code, { headers });
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ============================================================================
|
|
167
|
-
// Vite Connect Middleware Adapter
|
|
168
|
-
// ============================================================================
|
|
169
|
-
|
|
170
|
-
const MOCK_SOCKET = {
|
|
171
|
-
remoteAddress: '127.0.0.1',
|
|
172
|
-
remotePort: 0,
|
|
173
|
-
remoteFamily: 'IPv4',
|
|
174
|
-
encrypted: false,
|
|
175
|
-
writable: true,
|
|
176
|
-
readable: true,
|
|
177
|
-
destroy() {},
|
|
178
|
-
end() {},
|
|
179
|
-
write() { return true; },
|
|
180
|
-
on() { return this; },
|
|
181
|
-
once() { return this; },
|
|
182
|
-
off() { return this; },
|
|
183
|
-
emit() { return false; },
|
|
184
|
-
addListener() { return this; },
|
|
185
|
-
removeListener() { return this; },
|
|
186
|
-
setTimeout() { return this; },
|
|
187
|
-
setNoDelay() { return this; },
|
|
188
|
-
setKeepAlive() { return this; },
|
|
189
|
-
ref() { return this; },
|
|
190
|
-
unref() { return this; },
|
|
191
|
-
address() { return { address: '127.0.0.1', family: 'IPv4', port: 0 }; }
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Bridges Web API Request → Node.js IncomingMessage/ServerResponse → Web API Response.
|
|
196
|
-
* Required because Vite's connect middleware uses the Node.js HTTP API.
|
|
197
|
-
*/
|
|
198
|
-
function pipeViteMiddleware(
|
|
199
|
-
middleware: Connect.Server,
|
|
200
|
-
request: Request
|
|
201
|
-
): Promise<Response | null> {
|
|
202
|
-
return new Promise((resolve) => {
|
|
203
|
-
const url = new URL(request.url);
|
|
204
|
-
|
|
205
|
-
const req = new IncomingMessage(MOCK_SOCKET as any);
|
|
206
|
-
req.method = request.method;
|
|
207
|
-
req.url = url.pathname + url.search;
|
|
208
|
-
req.headers = request.headers.toJSON();
|
|
209
|
-
req.push(null);
|
|
210
|
-
|
|
211
|
-
const res = new ServerResponse(req);
|
|
212
|
-
let ended = false;
|
|
213
|
-
const chunks: Uint8Array[] = [];
|
|
214
|
-
|
|
215
|
-
const safetyTimer = setTimeout(() => {
|
|
216
|
-
if (!ended) {
|
|
217
|
-
ended = true;
|
|
218
|
-
resolve(null);
|
|
219
|
-
}
|
|
220
|
-
}, MIDDLEWARE_TIMEOUT);
|
|
221
|
-
|
|
222
|
-
function finalize() {
|
|
223
|
-
if (ended) return;
|
|
224
|
-
ended = true;
|
|
225
|
-
clearTimeout(safetyTimer);
|
|
226
|
-
|
|
227
|
-
const nodeHeaders = res.getHeaders();
|
|
228
|
-
const h = new Headers();
|
|
229
|
-
for (const key in nodeHeaders) {
|
|
230
|
-
// Skip hop-by-hop headers — Bun sets correct Content-Length/Transfer-Encoding
|
|
231
|
-
// based on the actual body. Forwarding stale values from ServerResponse
|
|
232
|
-
// causes browser to wait for data that never arrives.
|
|
233
|
-
const lower = key.toLowerCase();
|
|
234
|
-
if (lower === 'transfer-encoding' || lower === 'content-length' ||
|
|
235
|
-
lower === 'connection' || lower === 'keep-alive') continue;
|
|
236
|
-
|
|
237
|
-
const v = nodeHeaders[key];
|
|
238
|
-
if (v === undefined) continue;
|
|
239
|
-
if (Array.isArray(v)) {
|
|
240
|
-
for (let i = 0; i < v.length; i++) h.append(key, v[i]);
|
|
241
|
-
} else {
|
|
242
|
-
h.set(key, String(v));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const body = chunks.length === 0
|
|
247
|
-
? null
|
|
248
|
-
: Buffer.concat(chunks);
|
|
249
|
-
|
|
250
|
-
resolve(new Response(body, { status: res.statusCode, headers: h }));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
res.write = function (chunk: any, encodingOrCb?: any, cb?: any): boolean {
|
|
254
|
-
if (chunk != null) {
|
|
255
|
-
chunks.push(
|
|
256
|
-
typeof chunk === 'string'
|
|
257
|
-
? Buffer.from(chunk, typeof encodingOrCb === 'string' ? encodingOrCb as BufferEncoding : 'utf-8')
|
|
258
|
-
: chunk instanceof Uint8Array ? chunk : Buffer.from(chunk)
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
|
|
262
|
-
if (typeof callback === 'function') callback();
|
|
263
|
-
return true;
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
res.end = function (data?: any, encodingOrCb?: any, cb?: any): ServerResponse {
|
|
267
|
-
if (ended) return res;
|
|
268
|
-
|
|
269
|
-
if (typeof data === 'function') {
|
|
270
|
-
data();
|
|
271
|
-
} else {
|
|
272
|
-
if (data != null) {
|
|
273
|
-
chunks.push(
|
|
274
|
-
typeof data === 'string'
|
|
275
|
-
? Buffer.from(data, typeof encodingOrCb === 'string' ? encodingOrCb as BufferEncoding : 'utf-8')
|
|
276
|
-
: data instanceof Uint8Array ? data : Buffer.from(data)
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
|
|
280
|
-
if (typeof callback === 'function') callback();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
finalize();
|
|
284
|
-
return res;
|
|
285
|
-
} as any;
|
|
286
|
-
|
|
287
|
-
middleware(req as any, res as any, () => {
|
|
288
|
-
if (!ended) {
|
|
289
|
-
ended = true;
|
|
290
|
-
clearTimeout(safetyTimer);
|
|
291
|
-
resolve(null);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
}
|