@cloudflare/sandbox 0.0.0-db09b4d → 0.0.0-dcf36ef
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/CHANGELOG.md +235 -0
- package/Dockerfile +103 -10
- package/README.md +859 -22
- package/container_src/bun.lock +76 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +185 -0
- package/container_src/handler/file.ts +457 -0
- package/container_src/handler/git.ts +130 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +568 -0
- package/container_src/handler/session.ts +92 -0
- package/container_src/index.ts +447 -2466
- package/container_src/interpreter-service.ts +276 -0
- package/container_src/isolation.ts +1213 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
- package/container_src/runtime/executors/python/ipython_executor.py +338 -0
- package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
- package/container_src/runtime/process-pool.ts +464 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/startup.sh +11 -0
- package/container_src/types.ts +131 -0
- package/package.json +6 -8
- package/src/client.ts +510 -1186
- package/src/errors.ts +219 -0
- package/src/file-stream.ts +162 -0
- package/src/index.ts +78 -76
- package/src/interpreter-client.ts +352 -0
- package/src/interpreter-types.ts +390 -0
- package/src/interpreter.ts +150 -0
- package/src/request-handler.ts +144 -0
- package/src/sandbox.ts +756 -0
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +571 -0
- package/tsconfig.json +1 -1
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- package/tests/test2.ts +0 -710
package/container_src/index.ts
CHANGED
|
@@ -1,83 +1,99 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
1
|
import { serve } from "bun";
|
|
2
|
+
import {
|
|
3
|
+
handleExecuteRequest,
|
|
4
|
+
handleStreamingExecuteRequest,
|
|
5
|
+
} from "./handler/exec";
|
|
6
|
+
import {
|
|
7
|
+
handleDeleteFileRequest,
|
|
8
|
+
handleListFilesRequest,
|
|
9
|
+
handleMkdirRequest,
|
|
10
|
+
handleMoveFileRequest,
|
|
11
|
+
handleReadFileRequest,
|
|
12
|
+
handleReadFileStreamRequest,
|
|
13
|
+
handleRenameFileRequest,
|
|
14
|
+
handleWriteFileRequest,
|
|
15
|
+
} from "./handler/file";
|
|
16
|
+
import { handleGitCheckoutRequest } from "./handler/git";
|
|
17
|
+
import {
|
|
18
|
+
handleExposePortRequest,
|
|
19
|
+
handleGetExposedPortsRequest,
|
|
20
|
+
handleProxyRequest,
|
|
21
|
+
handleUnexposePortRequest,
|
|
22
|
+
} from "./handler/ports";
|
|
23
|
+
import {
|
|
24
|
+
handleGetProcessLogsRequest,
|
|
25
|
+
handleGetProcessRequest,
|
|
26
|
+
handleKillAllProcessesRequest,
|
|
27
|
+
handleKillProcessRequest,
|
|
28
|
+
handleListProcessesRequest,
|
|
29
|
+
handleStartProcessRequest,
|
|
30
|
+
handleStreamProcessLogsRequest,
|
|
31
|
+
} from "./handler/process";
|
|
32
|
+
import { handleCreateSession, handleListSessions } from "./handler/session";
|
|
33
|
+
import type { CreateContextRequest } from "./interpreter-service";
|
|
34
|
+
import {
|
|
35
|
+
InterpreterNotReadyError,
|
|
36
|
+
InterpreterService,
|
|
37
|
+
} from "./interpreter-service";
|
|
38
|
+
import { hasNamespaceSupport, SessionManager } from "./isolation";
|
|
39
|
+
|
|
40
|
+
// In-memory storage for exposed ports
|
|
41
|
+
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
42
|
+
|
|
43
|
+
// Check isolation capabilities on startup
|
|
44
|
+
const isolationAvailable = hasNamespaceSupport();
|
|
45
|
+
console.log(
|
|
46
|
+
`[Container] Process isolation: ${
|
|
47
|
+
isolationAvailable
|
|
48
|
+
? "ENABLED (production mode)"
|
|
49
|
+
: "DISABLED (development mode)"
|
|
50
|
+
}`
|
|
51
|
+
);
|
|
5
52
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
interface MoveFileRequest {
|
|
43
|
-
sourcePath: string;
|
|
44
|
-
destinationPath: string;
|
|
45
|
-
sessionId?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface SessionData {
|
|
49
|
-
sessionId: string;
|
|
50
|
-
activeProcess: any | null;
|
|
51
|
-
createdAt: Date;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// In-memory session storage (in production, you'd want to use a proper database)
|
|
55
|
-
const sessions = new Map<string, SessionData>();
|
|
56
|
-
|
|
57
|
-
// Generate a unique session ID
|
|
58
|
-
function generateSessionId(): string {
|
|
59
|
-
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Clean up old sessions (older than 1 hour)
|
|
63
|
-
function cleanupOldSessions() {
|
|
64
|
-
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
|
65
|
-
for (const [sessionId, session] of sessions.entries()) {
|
|
66
|
-
if (session.createdAt < oneHourAgo && !session.activeProcess) {
|
|
67
|
-
sessions.delete(sessionId);
|
|
68
|
-
console.log(`[Server] Cleaned up old session: ${sessionId}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Run cleanup every 10 minutes
|
|
74
|
-
setInterval(cleanupOldSessions, 10 * 60 * 1000);
|
|
53
|
+
// Session manager for secure execution with isolation
|
|
54
|
+
const sessionManager = new SessionManager();
|
|
55
|
+
|
|
56
|
+
// Graceful shutdown handler
|
|
57
|
+
const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
|
|
58
|
+
|
|
59
|
+
process.on("SIGTERM", async () => {
|
|
60
|
+
console.log("[Container] SIGTERM received, cleaning up sessions...");
|
|
61
|
+
await sessionManager.destroyAll();
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
process.on("SIGINT", async () => {
|
|
68
|
+
console.log("[Container] SIGINT received, cleaning up sessions...");
|
|
69
|
+
await sessionManager.destroyAll();
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Cleanup on uncaught exceptions (log but still exit)
|
|
76
|
+
process.on("uncaughtException", async (error) => {
|
|
77
|
+
console.error("[Container] Uncaught exception:", error);
|
|
78
|
+
await sessionManager.destroyAll();
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Initialize interpreter service
|
|
83
|
+
const interpreterService = new InterpreterService();
|
|
84
|
+
|
|
85
|
+
// No initialization needed - service is ready immediately!
|
|
86
|
+
console.log("[Container] Interpreter service ready - no cold start!");
|
|
87
|
+
console.log("[Container] All API endpoints available immediately");
|
|
75
88
|
|
|
76
89
|
const server = serve({
|
|
77
|
-
|
|
90
|
+
idleTimeout: 255,
|
|
91
|
+
async fetch(req: Request) {
|
|
78
92
|
const url = new URL(req.url);
|
|
79
93
|
const pathname = url.pathname;
|
|
80
94
|
|
|
95
|
+
console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
|
|
96
|
+
|
|
81
97
|
// Handle CORS
|
|
82
98
|
const corsHeaders = {
|
|
83
99
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
@@ -87,11 +103,13 @@ const server = serve({
|
|
|
87
103
|
|
|
88
104
|
// Handle preflight requests
|
|
89
105
|
if (req.method === "OPTIONS") {
|
|
106
|
+
console.log(`[Container] Handling CORS preflight for ${pathname}`);
|
|
90
107
|
return new Response(null, { headers: corsHeaders, status: 200 });
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
try {
|
|
94
111
|
// Handle different routes
|
|
112
|
+
console.log(`[Container] Processing ${req.method} ${pathname}`);
|
|
95
113
|
switch (pathname) {
|
|
96
114
|
case "/":
|
|
97
115
|
return new Response("Hello from Bun server! 🚀", {
|
|
@@ -101,123 +119,44 @@ const server = serve({
|
|
|
101
119
|
},
|
|
102
120
|
});
|
|
103
121
|
|
|
104
|
-
case "/api/hello":
|
|
105
|
-
return new Response(
|
|
106
|
-
JSON.stringify({
|
|
107
|
-
message: "Hello from API!",
|
|
108
|
-
timestamp: new Date().toISOString(),
|
|
109
|
-
}),
|
|
110
|
-
{
|
|
111
|
-
headers: {
|
|
112
|
-
"Content-Type": "application/json",
|
|
113
|
-
...corsHeaders,
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
case "/api/users":
|
|
119
|
-
if (req.method === "GET") {
|
|
120
|
-
return new Response(
|
|
121
|
-
JSON.stringify([
|
|
122
|
-
{ id: 1, name: "Alice" },
|
|
123
|
-
{ id: 2, name: "Bob" },
|
|
124
|
-
{ id: 3, name: "Charlie" },
|
|
125
|
-
]),
|
|
126
|
-
{
|
|
127
|
-
headers: {
|
|
128
|
-
"Content-Type": "application/json",
|
|
129
|
-
...corsHeaders,
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
} else if (req.method === "POST") {
|
|
134
|
-
return new Response(
|
|
135
|
-
JSON.stringify({
|
|
136
|
-
message: "User created successfully",
|
|
137
|
-
method: "POST",
|
|
138
|
-
}),
|
|
139
|
-
{
|
|
140
|
-
headers: {
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
...corsHeaders,
|
|
143
|
-
},
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
|
|
149
122
|
case "/api/session/create":
|
|
150
123
|
if (req.method === "POST") {
|
|
151
|
-
|
|
152
|
-
const sessionData: SessionData = {
|
|
153
|
-
activeProcess: null,
|
|
154
|
-
createdAt: new Date(),
|
|
155
|
-
sessionId,
|
|
156
|
-
};
|
|
157
|
-
sessions.set(sessionId, sessionData);
|
|
158
|
-
|
|
159
|
-
console.log(`[Server] Created new session: ${sessionId}`);
|
|
160
|
-
|
|
161
|
-
return new Response(
|
|
162
|
-
JSON.stringify({
|
|
163
|
-
message: "Session created successfully",
|
|
164
|
-
sessionId,
|
|
165
|
-
timestamp: new Date().toISOString(),
|
|
166
|
-
}),
|
|
167
|
-
{
|
|
168
|
-
headers: {
|
|
169
|
-
"Content-Type": "application/json",
|
|
170
|
-
...corsHeaders,
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
);
|
|
124
|
+
return handleCreateSession(req, corsHeaders, sessionManager);
|
|
174
125
|
}
|
|
175
126
|
break;
|
|
176
127
|
|
|
177
128
|
case "/api/session/list":
|
|
178
129
|
if (req.method === "GET") {
|
|
179
|
-
|
|
180
|
-
(session) => ({
|
|
181
|
-
createdAt: session.createdAt.toISOString(),
|
|
182
|
-
hasActiveProcess: !!session.activeProcess,
|
|
183
|
-
sessionId: session.sessionId,
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return new Response(
|
|
188
|
-
JSON.stringify({
|
|
189
|
-
count: sessionList.length,
|
|
190
|
-
sessions: sessionList,
|
|
191
|
-
timestamp: new Date().toISOString(),
|
|
192
|
-
}),
|
|
193
|
-
{
|
|
194
|
-
headers: {
|
|
195
|
-
"Content-Type": "application/json",
|
|
196
|
-
...corsHeaders,
|
|
197
|
-
},
|
|
198
|
-
}
|
|
199
|
-
);
|
|
130
|
+
return handleListSessions(corsHeaders, sessionManager);
|
|
200
131
|
}
|
|
201
132
|
break;
|
|
202
133
|
|
|
203
134
|
case "/api/execute":
|
|
204
135
|
if (req.method === "POST") {
|
|
205
|
-
return handleExecuteRequest(req, corsHeaders);
|
|
136
|
+
return handleExecuteRequest(req, corsHeaders, sessionManager);
|
|
206
137
|
}
|
|
207
138
|
break;
|
|
208
139
|
|
|
209
140
|
case "/api/execute/stream":
|
|
210
141
|
if (req.method === "POST") {
|
|
211
|
-
return handleStreamingExecuteRequest(
|
|
142
|
+
return handleStreamingExecuteRequest(
|
|
143
|
+
req,
|
|
144
|
+
sessionManager,
|
|
145
|
+
corsHeaders
|
|
146
|
+
);
|
|
212
147
|
}
|
|
213
148
|
break;
|
|
214
149
|
|
|
215
150
|
case "/api/ping":
|
|
216
151
|
if (req.method === "GET") {
|
|
152
|
+
const health = await interpreterService.getHealthStatus();
|
|
217
153
|
return new Response(
|
|
218
154
|
JSON.stringify({
|
|
219
155
|
message: "pong",
|
|
220
156
|
timestamp: new Date().toISOString(),
|
|
157
|
+
system: "interpreter (70x faster)",
|
|
158
|
+
status: health.ready ? "ready" : "initializing",
|
|
159
|
+
progress: health.progress,
|
|
221
160
|
}),
|
|
222
161
|
{
|
|
223
162
|
headers: {
|
|
@@ -229,118 +168,384 @@ const server = serve({
|
|
|
229
168
|
}
|
|
230
169
|
break;
|
|
231
170
|
|
|
232
|
-
case "/api/
|
|
233
|
-
if (req.method === "
|
|
234
|
-
return
|
|
235
|
-
JSON.stringify({
|
|
236
|
-
availableCommands: [
|
|
237
|
-
"ls",
|
|
238
|
-
"pwd",
|
|
239
|
-
"echo",
|
|
240
|
-
"cat",
|
|
241
|
-
"grep",
|
|
242
|
-
"find",
|
|
243
|
-
"whoami",
|
|
244
|
-
"date",
|
|
245
|
-
"uptime",
|
|
246
|
-
"ps",
|
|
247
|
-
"top",
|
|
248
|
-
"df",
|
|
249
|
-
"du",
|
|
250
|
-
"free",
|
|
251
|
-
],
|
|
252
|
-
timestamp: new Date().toISOString(),
|
|
253
|
-
}),
|
|
254
|
-
{
|
|
255
|
-
headers: {
|
|
256
|
-
"Content-Type": "application/json",
|
|
257
|
-
...corsHeaders,
|
|
258
|
-
},
|
|
259
|
-
}
|
|
260
|
-
);
|
|
171
|
+
case "/api/git/checkout":
|
|
172
|
+
if (req.method === "POST") {
|
|
173
|
+
return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
|
|
261
174
|
}
|
|
262
175
|
break;
|
|
263
176
|
|
|
264
|
-
case "/api/
|
|
177
|
+
case "/api/mkdir":
|
|
265
178
|
if (req.method === "POST") {
|
|
266
|
-
return
|
|
179
|
+
return handleMkdirRequest(req, corsHeaders, sessionManager);
|
|
267
180
|
}
|
|
268
181
|
break;
|
|
269
182
|
|
|
270
|
-
case "/api/
|
|
183
|
+
case "/api/write":
|
|
271
184
|
if (req.method === "POST") {
|
|
272
|
-
return
|
|
185
|
+
return handleWriteFileRequest(req, corsHeaders, sessionManager);
|
|
273
186
|
}
|
|
274
187
|
break;
|
|
275
188
|
|
|
276
|
-
case "/api/
|
|
189
|
+
case "/api/read":
|
|
277
190
|
if (req.method === "POST") {
|
|
278
|
-
return
|
|
191
|
+
return handleReadFileRequest(req, corsHeaders, sessionManager);
|
|
279
192
|
}
|
|
280
193
|
break;
|
|
281
194
|
|
|
282
|
-
case "/api/
|
|
195
|
+
case "/api/read/stream":
|
|
283
196
|
if (req.method === "POST") {
|
|
284
|
-
return
|
|
197
|
+
return handleReadFileStreamRequest(req, corsHeaders, sessionManager);
|
|
285
198
|
}
|
|
286
199
|
break;
|
|
287
200
|
|
|
288
|
-
case "/api/
|
|
201
|
+
case "/api/delete":
|
|
289
202
|
if (req.method === "POST") {
|
|
290
|
-
return
|
|
203
|
+
return handleDeleteFileRequest(req, corsHeaders, sessionManager);
|
|
291
204
|
}
|
|
292
205
|
break;
|
|
293
206
|
|
|
294
|
-
case "/api/
|
|
207
|
+
case "/api/rename":
|
|
295
208
|
if (req.method === "POST") {
|
|
296
|
-
return
|
|
209
|
+
return handleRenameFileRequest(req, corsHeaders, sessionManager);
|
|
297
210
|
}
|
|
298
211
|
break;
|
|
299
212
|
|
|
300
|
-
case "/api/
|
|
213
|
+
case "/api/move":
|
|
301
214
|
if (req.method === "POST") {
|
|
302
|
-
return
|
|
215
|
+
return handleMoveFileRequest(req, corsHeaders, sessionManager);
|
|
303
216
|
}
|
|
304
217
|
break;
|
|
305
218
|
|
|
306
|
-
case "/api/
|
|
219
|
+
case "/api/list-files":
|
|
307
220
|
if (req.method === "POST") {
|
|
308
|
-
return
|
|
221
|
+
return handleListFilesRequest(req, corsHeaders, sessionManager);
|
|
309
222
|
}
|
|
310
223
|
break;
|
|
311
224
|
|
|
312
|
-
case "/api/
|
|
225
|
+
case "/api/expose-port":
|
|
313
226
|
if (req.method === "POST") {
|
|
314
|
-
return
|
|
227
|
+
return handleExposePortRequest(exposedPorts, req, corsHeaders);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case "/api/unexpose-port":
|
|
232
|
+
if (req.method === "DELETE") {
|
|
233
|
+
return handleUnexposePortRequest(exposedPorts, req, corsHeaders);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "/api/exposed-ports":
|
|
238
|
+
if (req.method === "GET") {
|
|
239
|
+
return handleGetExposedPortsRequest(exposedPorts, req, corsHeaders);
|
|
315
240
|
}
|
|
316
241
|
break;
|
|
317
242
|
|
|
318
|
-
case "/api/
|
|
243
|
+
case "/api/process/start":
|
|
319
244
|
if (req.method === "POST") {
|
|
320
|
-
return
|
|
245
|
+
return handleStartProcessRequest(req, corsHeaders, sessionManager);
|
|
321
246
|
}
|
|
322
247
|
break;
|
|
323
248
|
|
|
324
|
-
case "/api/
|
|
249
|
+
case "/api/process/list":
|
|
250
|
+
if (req.method === "GET") {
|
|
251
|
+
return handleListProcessesRequest(req, corsHeaders, sessionManager);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case "/api/process/kill-all":
|
|
256
|
+
if (req.method === "DELETE") {
|
|
257
|
+
return handleKillAllProcessesRequest(
|
|
258
|
+
req,
|
|
259
|
+
corsHeaders,
|
|
260
|
+
sessionManager
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case "/api/contexts":
|
|
325
266
|
if (req.method === "POST") {
|
|
326
|
-
|
|
267
|
+
try {
|
|
268
|
+
const body = (await req.json()) as CreateContextRequest;
|
|
269
|
+
const context = await interpreterService.createContext(body);
|
|
270
|
+
return new Response(
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
id: context.id,
|
|
273
|
+
language: context.language,
|
|
274
|
+
cwd: context.cwd,
|
|
275
|
+
createdAt: context.createdAt,
|
|
276
|
+
lastUsed: context.lastUsed,
|
|
277
|
+
}),
|
|
278
|
+
{
|
|
279
|
+
headers: {
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
...corsHeaders,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof InterpreterNotReadyError) {
|
|
287
|
+
console.log(
|
|
288
|
+
`[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
|
|
289
|
+
);
|
|
290
|
+
return new Response(
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
error: error.message,
|
|
293
|
+
status: "initializing",
|
|
294
|
+
progress: error.progress,
|
|
295
|
+
}),
|
|
296
|
+
{
|
|
297
|
+
status: 503,
|
|
298
|
+
headers: {
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
"Retry-After": String(error.retryAfter),
|
|
301
|
+
...corsHeaders,
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check if it's a circuit breaker error
|
|
308
|
+
if (
|
|
309
|
+
error instanceof Error &&
|
|
310
|
+
error.message.includes("Circuit breaker is open")
|
|
311
|
+
) {
|
|
312
|
+
console.log(
|
|
313
|
+
"[Container] Circuit breaker is open:",
|
|
314
|
+
error.message
|
|
315
|
+
);
|
|
316
|
+
return new Response(
|
|
317
|
+
JSON.stringify({
|
|
318
|
+
error:
|
|
319
|
+
"Service temporarily unavailable due to high error rate. Please try again later.",
|
|
320
|
+
status: "circuit_open",
|
|
321
|
+
details: error.message,
|
|
322
|
+
}),
|
|
323
|
+
{
|
|
324
|
+
status: 503,
|
|
325
|
+
headers: {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
"Retry-After": "60",
|
|
328
|
+
...corsHeaders,
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Only log actual errors with stack traces
|
|
335
|
+
console.error("[Container] Error creating context:", error);
|
|
336
|
+
return new Response(
|
|
337
|
+
JSON.stringify({
|
|
338
|
+
error:
|
|
339
|
+
error instanceof Error
|
|
340
|
+
? error.message
|
|
341
|
+
: "Failed to create context",
|
|
342
|
+
}),
|
|
343
|
+
{
|
|
344
|
+
status: 500,
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
...corsHeaders,
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
} else if (req.method === "GET") {
|
|
353
|
+
const contexts = await interpreterService.listContexts();
|
|
354
|
+
return new Response(JSON.stringify({ contexts }), {
|
|
355
|
+
headers: {
|
|
356
|
+
"Content-Type": "application/json",
|
|
357
|
+
...corsHeaders,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
327
360
|
}
|
|
328
361
|
break;
|
|
329
362
|
|
|
330
|
-
case "/api/
|
|
363
|
+
case "/api/execute/code":
|
|
331
364
|
if (req.method === "POST") {
|
|
332
|
-
|
|
365
|
+
try {
|
|
366
|
+
const body = (await req.json()) as {
|
|
367
|
+
context_id: string;
|
|
368
|
+
code: string;
|
|
369
|
+
language?: string;
|
|
370
|
+
};
|
|
371
|
+
return await interpreterService.executeCode(
|
|
372
|
+
body.context_id,
|
|
373
|
+
body.code,
|
|
374
|
+
body.language
|
|
375
|
+
);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// Check if it's a circuit breaker error
|
|
378
|
+
if (
|
|
379
|
+
error instanceof Error &&
|
|
380
|
+
error.message.includes("Circuit breaker is open")
|
|
381
|
+
) {
|
|
382
|
+
console.log(
|
|
383
|
+
"[Container] Circuit breaker is open for code execution:",
|
|
384
|
+
error.message
|
|
385
|
+
);
|
|
386
|
+
return new Response(
|
|
387
|
+
JSON.stringify({
|
|
388
|
+
error:
|
|
389
|
+
"Service temporarily unavailable due to high error rate. Please try again later.",
|
|
390
|
+
status: "circuit_open",
|
|
391
|
+
details: error.message,
|
|
392
|
+
}),
|
|
393
|
+
{
|
|
394
|
+
status: 503,
|
|
395
|
+
headers: {
|
|
396
|
+
"Content-Type": "application/json",
|
|
397
|
+
"Retry-After": "30",
|
|
398
|
+
...corsHeaders,
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Don't log stack traces for expected initialization state
|
|
405
|
+
if (
|
|
406
|
+
error instanceof Error &&
|
|
407
|
+
error.message.includes("initializing")
|
|
408
|
+
) {
|
|
409
|
+
console.log(
|
|
410
|
+
"[Container] Code execution deferred - service still initializing"
|
|
411
|
+
);
|
|
412
|
+
} else {
|
|
413
|
+
console.error("[Container] Error executing code:", error);
|
|
414
|
+
}
|
|
415
|
+
// Error response is already handled by service.executeCode for not ready state
|
|
416
|
+
return new Response(
|
|
417
|
+
JSON.stringify({
|
|
418
|
+
error:
|
|
419
|
+
error instanceof Error
|
|
420
|
+
? error.message
|
|
421
|
+
: "Failed to execute code",
|
|
422
|
+
}),
|
|
423
|
+
{
|
|
424
|
+
status: 500,
|
|
425
|
+
headers: {
|
|
426
|
+
"Content-Type": "application/json",
|
|
427
|
+
...corsHeaders,
|
|
428
|
+
},
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
}
|
|
333
432
|
}
|
|
334
433
|
break;
|
|
335
434
|
|
|
336
435
|
default:
|
|
436
|
+
// Handle dynamic routes for contexts
|
|
437
|
+
if (
|
|
438
|
+
pathname.startsWith("/api/contexts/") &&
|
|
439
|
+
pathname.split("/").length === 4
|
|
440
|
+
) {
|
|
441
|
+
const contextId = pathname.split("/")[3];
|
|
442
|
+
if (req.method === "DELETE") {
|
|
443
|
+
try {
|
|
444
|
+
await interpreterService.deleteContext(contextId);
|
|
445
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
446
|
+
headers: {
|
|
447
|
+
"Content-Type": "application/json",
|
|
448
|
+
...corsHeaders,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof InterpreterNotReadyError) {
|
|
453
|
+
console.log(
|
|
454
|
+
`[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
|
|
455
|
+
);
|
|
456
|
+
return new Response(
|
|
457
|
+
JSON.stringify({
|
|
458
|
+
error: error.message,
|
|
459
|
+
status: "initializing",
|
|
460
|
+
progress: error.progress,
|
|
461
|
+
}),
|
|
462
|
+
{
|
|
463
|
+
status: 503,
|
|
464
|
+
headers: {
|
|
465
|
+
"Content-Type": "application/json",
|
|
466
|
+
"Retry-After": "5",
|
|
467
|
+
...corsHeaders,
|
|
468
|
+
},
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return new Response(
|
|
473
|
+
JSON.stringify({
|
|
474
|
+
error:
|
|
475
|
+
error instanceof Error
|
|
476
|
+
? error.message
|
|
477
|
+
: "Failed to delete context",
|
|
478
|
+
}),
|
|
479
|
+
{
|
|
480
|
+
status:
|
|
481
|
+
error instanceof Error &&
|
|
482
|
+
error.message.includes("not found")
|
|
483
|
+
? 404
|
|
484
|
+
: 500,
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
...corsHeaders,
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Handle dynamic routes for individual processes
|
|
496
|
+
if (pathname.startsWith("/api/process/")) {
|
|
497
|
+
const segments = pathname.split("/");
|
|
498
|
+
if (segments.length >= 4) {
|
|
499
|
+
const processId = segments[3];
|
|
500
|
+
const action = segments[4]; // Optional: logs, stream, etc.
|
|
501
|
+
|
|
502
|
+
if (!action && req.method === "GET") {
|
|
503
|
+
return handleGetProcessRequest(
|
|
504
|
+
req,
|
|
505
|
+
corsHeaders,
|
|
506
|
+
processId,
|
|
507
|
+
sessionManager
|
|
508
|
+
);
|
|
509
|
+
} else if (!action && req.method === "DELETE") {
|
|
510
|
+
return handleKillProcessRequest(
|
|
511
|
+
req,
|
|
512
|
+
corsHeaders,
|
|
513
|
+
processId,
|
|
514
|
+
sessionManager
|
|
515
|
+
);
|
|
516
|
+
} else if (action === "logs" && req.method === "GET") {
|
|
517
|
+
return handleGetProcessLogsRequest(
|
|
518
|
+
req,
|
|
519
|
+
corsHeaders,
|
|
520
|
+
processId,
|
|
521
|
+
sessionManager
|
|
522
|
+
);
|
|
523
|
+
} else if (action === "stream" && req.method === "GET") {
|
|
524
|
+
return handleStreamProcessLogsRequest(
|
|
525
|
+
req,
|
|
526
|
+
corsHeaders,
|
|
527
|
+
processId,
|
|
528
|
+
sessionManager
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Check if this is a proxy request for an exposed port
|
|
534
|
+
if (pathname.startsWith("/proxy/")) {
|
|
535
|
+
return handleProxyRequest(exposedPorts, req, corsHeaders);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log(`[Container] Route not found: ${pathname}`);
|
|
337
539
|
return new Response("Not Found", {
|
|
338
540
|
headers: corsHeaders,
|
|
339
541
|
status: 404,
|
|
340
542
|
});
|
|
341
543
|
}
|
|
342
544
|
} catch (error) {
|
|
343
|
-
console.error(
|
|
545
|
+
console.error(
|
|
546
|
+
`[Container] Error handling ${req.method} ${pathname}:`,
|
|
547
|
+
error
|
|
548
|
+
);
|
|
344
549
|
return new Response(
|
|
345
550
|
JSON.stringify({
|
|
346
551
|
error: "Internal server error",
|
|
@@ -356,2265 +561,41 @@ const server = serve({
|
|
|
356
561
|
);
|
|
357
562
|
}
|
|
358
563
|
},
|
|
564
|
+
hostname: "0.0.0.0",
|
|
359
565
|
port: 3000,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
req: Request,
|
|
364
|
-
corsHeaders: Record<string, string>
|
|
365
|
-
): Promise<Response> {
|
|
366
|
-
try {
|
|
367
|
-
const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
|
|
368
|
-
const { command, args = [], sessionId } = body;
|
|
369
|
-
|
|
370
|
-
if (!command || typeof command !== "string") {
|
|
371
|
-
return new Response(
|
|
372
|
-
JSON.stringify({
|
|
373
|
-
error: "Command is required and must be a string",
|
|
374
|
-
}),
|
|
375
|
-
{
|
|
376
|
-
headers: {
|
|
377
|
-
"Content-Type": "application/json",
|
|
378
|
-
...corsHeaders,
|
|
379
|
-
},
|
|
380
|
-
status: 400,
|
|
381
|
-
}
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Basic safety check - prevent dangerous commands
|
|
386
|
-
const dangerousCommands = [
|
|
387
|
-
"rm",
|
|
388
|
-
"rmdir",
|
|
389
|
-
"del",
|
|
390
|
-
"format",
|
|
391
|
-
"shutdown",
|
|
392
|
-
"reboot",
|
|
393
|
-
];
|
|
394
|
-
const lowerCommand = command.toLowerCase();
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
398
|
-
) {
|
|
399
|
-
return new Response(
|
|
400
|
-
JSON.stringify({
|
|
401
|
-
error: "Dangerous command not allowed",
|
|
402
|
-
}),
|
|
403
|
-
{
|
|
404
|
-
headers: {
|
|
405
|
-
"Content-Type": "application/json",
|
|
406
|
-
...corsHeaders,
|
|
407
|
-
},
|
|
408
|
-
status: 400,
|
|
409
|
-
}
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
|
|
414
|
-
|
|
415
|
-
const result = await executeCommand(command, args, sessionId);
|
|
416
|
-
|
|
417
|
-
return new Response(
|
|
418
|
-
JSON.stringify({
|
|
419
|
-
args,
|
|
420
|
-
command,
|
|
421
|
-
exitCode: result.exitCode,
|
|
422
|
-
stderr: result.stderr,
|
|
423
|
-
stdout: result.stdout,
|
|
424
|
-
success: result.success,
|
|
425
|
-
timestamp: new Date().toISOString(),
|
|
426
|
-
}),
|
|
427
|
-
{
|
|
428
|
-
headers: {
|
|
429
|
-
"Content-Type": "application/json",
|
|
430
|
-
...corsHeaders,
|
|
431
|
-
},
|
|
432
|
-
}
|
|
433
|
-
);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
console.error("[Server] Error in handleExecuteRequest:", error);
|
|
436
|
-
return new Response(
|
|
437
|
-
JSON.stringify({
|
|
438
|
-
error: "Failed to execute command",
|
|
439
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
440
|
-
}),
|
|
441
|
-
{
|
|
442
|
-
headers: {
|
|
443
|
-
"Content-Type": "application/json",
|
|
444
|
-
...corsHeaders,
|
|
445
|
-
},
|
|
446
|
-
status: 500,
|
|
447
|
-
}
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function handleStreamingExecuteRequest(
|
|
453
|
-
req: Request,
|
|
454
|
-
corsHeaders: Record<string, string>
|
|
455
|
-
): Promise<Response> {
|
|
456
|
-
try {
|
|
457
|
-
const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
|
|
458
|
-
const { command, args = [], sessionId } = body;
|
|
459
|
-
|
|
460
|
-
if (!command || typeof command !== "string") {
|
|
461
|
-
return new Response(
|
|
462
|
-
JSON.stringify({
|
|
463
|
-
error: "Command is required and must be a string",
|
|
464
|
-
}),
|
|
465
|
-
{
|
|
466
|
-
headers: {
|
|
467
|
-
"Content-Type": "application/json",
|
|
468
|
-
...corsHeaders,
|
|
469
|
-
},
|
|
470
|
-
status: 400,
|
|
471
|
-
}
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Basic safety check - prevent dangerous commands
|
|
476
|
-
const dangerousCommands = [
|
|
477
|
-
"rm",
|
|
478
|
-
"rmdir",
|
|
479
|
-
"del",
|
|
480
|
-
"format",
|
|
481
|
-
"shutdown",
|
|
482
|
-
"reboot",
|
|
483
|
-
];
|
|
484
|
-
const lowerCommand = command.toLowerCase();
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
488
|
-
) {
|
|
489
|
-
return new Response(
|
|
490
|
-
JSON.stringify({
|
|
491
|
-
error: "Dangerous command not allowed",
|
|
492
|
-
}),
|
|
493
|
-
{
|
|
494
|
-
headers: {
|
|
495
|
-
"Content-Type": "application/json",
|
|
496
|
-
...corsHeaders,
|
|
497
|
-
},
|
|
498
|
-
status: 400,
|
|
499
|
-
}
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
console.log(
|
|
504
|
-
`[Server] Executing streaming command: ${command} ${args.join(" ")}`
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
const stream = new ReadableStream({
|
|
508
|
-
start(controller) {
|
|
509
|
-
const child = spawn(command, args, {
|
|
510
|
-
shell: true,
|
|
511
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
515
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
516
|
-
const session = sessions.get(sessionId)!;
|
|
517
|
-
session.activeProcess = child;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
let stdout = "";
|
|
521
|
-
let stderr = "";
|
|
522
|
-
|
|
523
|
-
// Send command start event
|
|
524
|
-
controller.enqueue(
|
|
525
|
-
new TextEncoder().encode(
|
|
526
|
-
`data: ${JSON.stringify({
|
|
527
|
-
args,
|
|
528
|
-
command,
|
|
529
|
-
timestamp: new Date().toISOString(),
|
|
530
|
-
type: "command_start",
|
|
531
|
-
})}\n\n`
|
|
532
|
-
)
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
child.stdout?.on("data", (data) => {
|
|
536
|
-
const output = data.toString();
|
|
537
|
-
stdout += output;
|
|
538
|
-
|
|
539
|
-
// Send real-time output
|
|
540
|
-
controller.enqueue(
|
|
541
|
-
new TextEncoder().encode(
|
|
542
|
-
`data: ${JSON.stringify({
|
|
543
|
-
command,
|
|
544
|
-
data: output,
|
|
545
|
-
stream: "stdout",
|
|
546
|
-
type: "output",
|
|
547
|
-
})}\n\n`
|
|
548
|
-
)
|
|
549
|
-
);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
child.stderr?.on("data", (data) => {
|
|
553
|
-
const output = data.toString();
|
|
554
|
-
stderr += output;
|
|
555
|
-
|
|
556
|
-
// Send real-time error output
|
|
557
|
-
controller.enqueue(
|
|
558
|
-
new TextEncoder().encode(
|
|
559
|
-
`data: ${JSON.stringify({
|
|
560
|
-
command,
|
|
561
|
-
data: output,
|
|
562
|
-
stream: "stderr",
|
|
563
|
-
type: "output",
|
|
564
|
-
})}\n\n`
|
|
565
|
-
)
|
|
566
|
-
);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
child.on("close", (code) => {
|
|
570
|
-
// Clear the active process reference
|
|
571
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
572
|
-
const session = sessions.get(sessionId)!;
|
|
573
|
-
session.activeProcess = null;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
console.log(
|
|
577
|
-
`[Server] Command completed: ${command}, Exit code: ${code}`
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
// Send command completion event
|
|
581
|
-
controller.enqueue(
|
|
582
|
-
new TextEncoder().encode(
|
|
583
|
-
`data: ${JSON.stringify({
|
|
584
|
-
args,
|
|
585
|
-
command,
|
|
586
|
-
exitCode: code,
|
|
587
|
-
stderr,
|
|
588
|
-
stdout,
|
|
589
|
-
success: code === 0,
|
|
590
|
-
timestamp: new Date().toISOString(),
|
|
591
|
-
type: "command_complete",
|
|
592
|
-
})}\n\n`
|
|
593
|
-
)
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
controller.close();
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
child.on("error", (error) => {
|
|
600
|
-
// Clear the active process reference
|
|
601
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
602
|
-
const session = sessions.get(sessionId)!;
|
|
603
|
-
session.activeProcess = null;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
controller.enqueue(
|
|
607
|
-
new TextEncoder().encode(
|
|
608
|
-
`data: ${JSON.stringify({
|
|
609
|
-
args,
|
|
610
|
-
command,
|
|
611
|
-
error: error.message,
|
|
612
|
-
type: "error",
|
|
613
|
-
})}\n\n`
|
|
614
|
-
)
|
|
615
|
-
);
|
|
616
|
-
|
|
617
|
-
controller.close();
|
|
618
|
-
});
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
return new Response(stream, {
|
|
623
|
-
headers: {
|
|
624
|
-
"Cache-Control": "no-cache",
|
|
625
|
-
Connection: "keep-alive",
|
|
626
|
-
"Content-Type": "text/event-stream",
|
|
627
|
-
...corsHeaders,
|
|
628
|
-
},
|
|
629
|
-
});
|
|
630
|
-
} catch (error) {
|
|
631
|
-
console.error("[Server] Error in handleStreamingExecuteRequest:", error);
|
|
632
|
-
return new Response(
|
|
633
|
-
JSON.stringify({
|
|
634
|
-
error: "Failed to execute streaming command",
|
|
635
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
636
|
-
}),
|
|
637
|
-
{
|
|
638
|
-
headers: {
|
|
639
|
-
"Content-Type": "application/json",
|
|
640
|
-
...corsHeaders,
|
|
641
|
-
},
|
|
642
|
-
status: 500,
|
|
643
|
-
}
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
async function handleGitCheckoutRequest(
|
|
649
|
-
req: Request,
|
|
650
|
-
corsHeaders: Record<string, string>
|
|
651
|
-
): Promise<Response> {
|
|
652
|
-
try {
|
|
653
|
-
const body = (await req.json()) as GitCheckoutRequest;
|
|
654
|
-
const { repoUrl, branch = "main", targetDir, sessionId } = body;
|
|
655
|
-
|
|
656
|
-
if (!repoUrl || typeof repoUrl !== "string") {
|
|
657
|
-
return new Response(
|
|
658
|
-
JSON.stringify({
|
|
659
|
-
error: "Repository URL is required and must be a string",
|
|
660
|
-
}),
|
|
661
|
-
{
|
|
662
|
-
headers: {
|
|
663
|
-
"Content-Type": "application/json",
|
|
664
|
-
...corsHeaders,
|
|
665
|
-
},
|
|
666
|
-
status: 400,
|
|
667
|
-
}
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Validate repository URL format
|
|
672
|
-
const urlPattern =
|
|
673
|
-
/^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
|
|
674
|
-
if (!urlPattern.test(repoUrl)) {
|
|
675
|
-
return new Response(
|
|
676
|
-
JSON.stringify({
|
|
677
|
-
error: "Invalid repository URL format",
|
|
678
|
-
}),
|
|
679
|
-
{
|
|
680
|
-
headers: {
|
|
681
|
-
"Content-Type": "application/json",
|
|
682
|
-
...corsHeaders,
|
|
683
|
-
},
|
|
684
|
-
status: 400,
|
|
685
|
-
}
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Generate target directory if not provided
|
|
690
|
-
const checkoutDir =
|
|
691
|
-
targetDir ||
|
|
692
|
-
`repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
693
|
-
|
|
694
|
-
console.log(
|
|
695
|
-
`[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
|
|
696
|
-
);
|
|
697
|
-
|
|
698
|
-
const result = await executeGitCheckout(
|
|
699
|
-
repoUrl,
|
|
700
|
-
branch,
|
|
701
|
-
checkoutDir,
|
|
702
|
-
sessionId
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
return new Response(
|
|
706
|
-
JSON.stringify({
|
|
707
|
-
branch,
|
|
708
|
-
exitCode: result.exitCode,
|
|
709
|
-
repoUrl,
|
|
710
|
-
stderr: result.stderr,
|
|
711
|
-
stdout: result.stdout,
|
|
712
|
-
success: result.success,
|
|
713
|
-
targetDir: checkoutDir,
|
|
714
|
-
timestamp: new Date().toISOString(),
|
|
715
|
-
}),
|
|
716
|
-
{
|
|
717
|
-
headers: {
|
|
718
|
-
"Content-Type": "application/json",
|
|
719
|
-
...corsHeaders,
|
|
720
|
-
},
|
|
721
|
-
}
|
|
722
|
-
);
|
|
723
|
-
} catch (error) {
|
|
724
|
-
console.error("[Server] Error in handleGitCheckoutRequest:", error);
|
|
725
|
-
return new Response(
|
|
726
|
-
JSON.stringify({
|
|
727
|
-
error: "Failed to checkout repository",
|
|
728
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
729
|
-
}),
|
|
730
|
-
{
|
|
731
|
-
headers: {
|
|
732
|
-
"Content-Type": "application/json",
|
|
733
|
-
...corsHeaders,
|
|
734
|
-
},
|
|
735
|
-
status: 500,
|
|
736
|
-
}
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
566
|
+
// We don't need this, but typescript complains
|
|
567
|
+
websocket: { async message() {} },
|
|
568
|
+
});
|
|
740
569
|
|
|
741
|
-
|
|
742
|
-
req: Request,
|
|
743
|
-
corsHeaders: Record<string, string>
|
|
744
|
-
): Promise<Response> {
|
|
745
|
-
try {
|
|
746
|
-
const body = (await req.json()) as GitCheckoutRequest;
|
|
747
|
-
const { repoUrl, branch = "main", targetDir, sessionId } = body;
|
|
748
|
-
|
|
749
|
-
if (!repoUrl || typeof repoUrl !== "string") {
|
|
750
|
-
return new Response(
|
|
751
|
-
JSON.stringify({
|
|
752
|
-
error: "Repository URL is required and must be a string",
|
|
753
|
-
}),
|
|
754
|
-
{
|
|
755
|
-
headers: {
|
|
756
|
-
"Content-Type": "application/json",
|
|
757
|
-
...corsHeaders,
|
|
758
|
-
},
|
|
759
|
-
status: 400,
|
|
760
|
-
}
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Validate repository URL format
|
|
765
|
-
const urlPattern =
|
|
766
|
-
/^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
|
|
767
|
-
if (!urlPattern.test(repoUrl)) {
|
|
768
|
-
return new Response(
|
|
769
|
-
JSON.stringify({
|
|
770
|
-
error: "Invalid repository URL format",
|
|
771
|
-
}),
|
|
772
|
-
{
|
|
773
|
-
headers: {
|
|
774
|
-
"Content-Type": "application/json",
|
|
775
|
-
...corsHeaders,
|
|
776
|
-
},
|
|
777
|
-
status: 400,
|
|
778
|
-
}
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Generate target directory if not provided
|
|
783
|
-
const checkoutDir =
|
|
784
|
-
targetDir ||
|
|
785
|
-
`repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
786
|
-
|
|
787
|
-
console.log(
|
|
788
|
-
`[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
const stream = new ReadableStream({
|
|
792
|
-
start(controller) {
|
|
793
|
-
const child = spawn(
|
|
794
|
-
"git",
|
|
795
|
-
["clone", "-b", branch, repoUrl, checkoutDir],
|
|
796
|
-
{
|
|
797
|
-
shell: true,
|
|
798
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
799
|
-
}
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
803
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
804
|
-
const session = sessions.get(sessionId)!;
|
|
805
|
-
session.activeProcess = child;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
let stdout = "";
|
|
809
|
-
let stderr = "";
|
|
810
|
-
|
|
811
|
-
// Send command start event
|
|
812
|
-
controller.enqueue(
|
|
813
|
-
new TextEncoder().encode(
|
|
814
|
-
`data: ${JSON.stringify({
|
|
815
|
-
args: [branch, repoUrl, checkoutDir],
|
|
816
|
-
command: "git clone",
|
|
817
|
-
timestamp: new Date().toISOString(),
|
|
818
|
-
type: "command_start",
|
|
819
|
-
})}\n\n`
|
|
820
|
-
)
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
child.stdout?.on("data", (data) => {
|
|
824
|
-
const output = data.toString();
|
|
825
|
-
stdout += output;
|
|
826
|
-
|
|
827
|
-
// Send real-time output
|
|
828
|
-
controller.enqueue(
|
|
829
|
-
new TextEncoder().encode(
|
|
830
|
-
`data: ${JSON.stringify({
|
|
831
|
-
command: "git clone",
|
|
832
|
-
data: output,
|
|
833
|
-
stream: "stdout",
|
|
834
|
-
type: "output",
|
|
835
|
-
})}\n\n`
|
|
836
|
-
)
|
|
837
|
-
);
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
child.stderr?.on("data", (data) => {
|
|
841
|
-
const output = data.toString();
|
|
842
|
-
stderr += output;
|
|
843
|
-
|
|
844
|
-
// Send real-time error output
|
|
845
|
-
controller.enqueue(
|
|
846
|
-
new TextEncoder().encode(
|
|
847
|
-
`data: ${JSON.stringify({
|
|
848
|
-
command: "git clone",
|
|
849
|
-
data: output,
|
|
850
|
-
stream: "stderr",
|
|
851
|
-
type: "output",
|
|
852
|
-
})}\n\n`
|
|
853
|
-
)
|
|
854
|
-
);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
child.on("close", (code) => {
|
|
858
|
-
// Clear the active process reference
|
|
859
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
860
|
-
const session = sessions.get(sessionId)!;
|
|
861
|
-
session.activeProcess = null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
console.log(
|
|
865
|
-
`[Server] Command completed: git clone, Exit code: ${code}`
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
// Send command completion event
|
|
869
|
-
controller.enqueue(
|
|
870
|
-
new TextEncoder().encode(
|
|
871
|
-
`data: ${JSON.stringify({
|
|
872
|
-
args: [branch, repoUrl, checkoutDir],
|
|
873
|
-
command: "git clone",
|
|
874
|
-
exitCode: code,
|
|
875
|
-
stderr,
|
|
876
|
-
stdout,
|
|
877
|
-
success: code === 0,
|
|
878
|
-
timestamp: new Date().toISOString(),
|
|
879
|
-
type: "command_complete",
|
|
880
|
-
})}\n\n`
|
|
881
|
-
)
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
controller.close();
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
child.on("error", (error) => {
|
|
888
|
-
// Clear the active process reference
|
|
889
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
890
|
-
const session = sessions.get(sessionId)!;
|
|
891
|
-
session.activeProcess = null;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
controller.enqueue(
|
|
895
|
-
new TextEncoder().encode(
|
|
896
|
-
`data: ${JSON.stringify({
|
|
897
|
-
args: [branch, repoUrl, checkoutDir],
|
|
898
|
-
command: "git clone",
|
|
899
|
-
error: error.message,
|
|
900
|
-
type: "error",
|
|
901
|
-
})}\n\n`
|
|
902
|
-
)
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
controller.close();
|
|
906
|
-
});
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
return new Response(stream, {
|
|
911
|
-
headers: {
|
|
912
|
-
"Cache-Control": "no-cache",
|
|
913
|
-
Connection: "keep-alive",
|
|
914
|
-
"Content-Type": "text/event-stream",
|
|
915
|
-
...corsHeaders,
|
|
916
|
-
},
|
|
917
|
-
});
|
|
918
|
-
} catch (error) {
|
|
919
|
-
console.error(
|
|
920
|
-
"[Server] Error in handleStreamingGitCheckoutRequest:",
|
|
921
|
-
error
|
|
922
|
-
);
|
|
923
|
-
return new Response(
|
|
924
|
-
JSON.stringify({
|
|
925
|
-
error: "Failed to checkout repository",
|
|
926
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
927
|
-
}),
|
|
928
|
-
{
|
|
929
|
-
headers: {
|
|
930
|
-
"Content-Type": "application/json",
|
|
931
|
-
...corsHeaders,
|
|
932
|
-
},
|
|
933
|
-
status: 500,
|
|
934
|
-
}
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
async function handleMkdirRequest(
|
|
940
|
-
req: Request,
|
|
941
|
-
corsHeaders: Record<string, string>
|
|
942
|
-
): Promise<Response> {
|
|
943
|
-
try {
|
|
944
|
-
const body = (await req.json()) as MkdirRequest;
|
|
945
|
-
const { path, recursive = false, sessionId } = body;
|
|
946
|
-
|
|
947
|
-
if (!path || typeof path !== "string") {
|
|
948
|
-
return new Response(
|
|
949
|
-
JSON.stringify({
|
|
950
|
-
error: "Path is required and must be a string",
|
|
951
|
-
}),
|
|
952
|
-
{
|
|
953
|
-
headers: {
|
|
954
|
-
"Content-Type": "application/json",
|
|
955
|
-
...corsHeaders,
|
|
956
|
-
},
|
|
957
|
-
status: 400,
|
|
958
|
-
}
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Basic safety check - prevent dangerous paths
|
|
963
|
-
const dangerousPatterns = [
|
|
964
|
-
/^\/$/, // Root directory
|
|
965
|
-
/^\/etc/, // System directories
|
|
966
|
-
/^\/var/, // System directories
|
|
967
|
-
/^\/usr/, // System directories
|
|
968
|
-
/^\/bin/, // System directories
|
|
969
|
-
/^\/sbin/, // System directories
|
|
970
|
-
/^\/boot/, // System directories
|
|
971
|
-
/^\/dev/, // System directories
|
|
972
|
-
/^\/proc/, // System directories
|
|
973
|
-
/^\/sys/, // System directories
|
|
974
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
975
|
-
/\.\./, // Path traversal attempts
|
|
976
|
-
];
|
|
977
|
-
|
|
978
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
979
|
-
return new Response(
|
|
980
|
-
JSON.stringify({
|
|
981
|
-
error: "Dangerous path not allowed",
|
|
982
|
-
}),
|
|
983
|
-
{
|
|
984
|
-
headers: {
|
|
985
|
-
"Content-Type": "application/json",
|
|
986
|
-
...corsHeaders,
|
|
987
|
-
},
|
|
988
|
-
status: 400,
|
|
989
|
-
}
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
console.log(
|
|
994
|
-
`[Server] Creating directory: ${path} (recursive: ${recursive})`
|
|
995
|
-
);
|
|
996
|
-
|
|
997
|
-
const result = await executeMkdir(path, recursive, sessionId);
|
|
998
|
-
|
|
999
|
-
return new Response(
|
|
1000
|
-
JSON.stringify({
|
|
1001
|
-
exitCode: result.exitCode,
|
|
1002
|
-
path,
|
|
1003
|
-
recursive,
|
|
1004
|
-
stderr: result.stderr,
|
|
1005
|
-
stdout: result.stdout,
|
|
1006
|
-
success: result.success,
|
|
1007
|
-
timestamp: new Date().toISOString(),
|
|
1008
|
-
}),
|
|
1009
|
-
{
|
|
1010
|
-
headers: {
|
|
1011
|
-
"Content-Type": "application/json",
|
|
1012
|
-
...corsHeaders,
|
|
1013
|
-
},
|
|
1014
|
-
}
|
|
1015
|
-
);
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
console.error("[Server] Error in handleMkdirRequest:", error);
|
|
1018
|
-
return new Response(
|
|
1019
|
-
JSON.stringify({
|
|
1020
|
-
error: "Failed to create directory",
|
|
1021
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1022
|
-
}),
|
|
1023
|
-
{
|
|
1024
|
-
headers: {
|
|
1025
|
-
"Content-Type": "application/json",
|
|
1026
|
-
...corsHeaders,
|
|
1027
|
-
},
|
|
1028
|
-
status: 500,
|
|
1029
|
-
}
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
async function handleStreamingMkdirRequest(
|
|
1035
|
-
req: Request,
|
|
1036
|
-
corsHeaders: Record<string, string>
|
|
1037
|
-
): Promise<Response> {
|
|
1038
|
-
try {
|
|
1039
|
-
const body = (await req.json()) as MkdirRequest;
|
|
1040
|
-
const { path, recursive = false, sessionId } = body;
|
|
1041
|
-
|
|
1042
|
-
if (!path || typeof path !== "string") {
|
|
1043
|
-
return new Response(
|
|
1044
|
-
JSON.stringify({
|
|
1045
|
-
error: "Path is required and must be a string",
|
|
1046
|
-
}),
|
|
1047
|
-
{
|
|
1048
|
-
headers: {
|
|
1049
|
-
"Content-Type": "application/json",
|
|
1050
|
-
...corsHeaders,
|
|
1051
|
-
},
|
|
1052
|
-
status: 400,
|
|
1053
|
-
}
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Basic safety check - prevent dangerous paths
|
|
1058
|
-
const dangerousPatterns = [
|
|
1059
|
-
/^\/$/, // Root directory
|
|
1060
|
-
/^\/etc/, // System directories
|
|
1061
|
-
/^\/var/, // System directories
|
|
1062
|
-
/^\/usr/, // System directories
|
|
1063
|
-
/^\/bin/, // System directories
|
|
1064
|
-
/^\/sbin/, // System directories
|
|
1065
|
-
/^\/boot/, // System directories
|
|
1066
|
-
/^\/dev/, // System directories
|
|
1067
|
-
/^\/proc/, // System directories
|
|
1068
|
-
/^\/sys/, // System directories
|
|
1069
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1070
|
-
/\.\./, // Path traversal attempts
|
|
1071
|
-
];
|
|
1072
|
-
|
|
1073
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1074
|
-
return new Response(
|
|
1075
|
-
JSON.stringify({
|
|
1076
|
-
error: "Dangerous path not allowed",
|
|
1077
|
-
}),
|
|
1078
|
-
{
|
|
1079
|
-
headers: {
|
|
1080
|
-
"Content-Type": "application/json",
|
|
1081
|
-
...corsHeaders,
|
|
1082
|
-
},
|
|
1083
|
-
status: 400,
|
|
1084
|
-
}
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
console.log(
|
|
1089
|
-
`[Server] Creating directory: ${path} (recursive: ${recursive})`
|
|
1090
|
-
);
|
|
1091
|
-
|
|
1092
|
-
const stream = new ReadableStream({
|
|
1093
|
-
start(controller) {
|
|
1094
|
-
const args = recursive ? ["-p", path] : [path];
|
|
1095
|
-
const child = spawn("mkdir", args, {
|
|
1096
|
-
shell: true,
|
|
1097
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
1101
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1102
|
-
const session = sessions.get(sessionId)!;
|
|
1103
|
-
session.activeProcess = child;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
let stdout = "";
|
|
1107
|
-
let stderr = "";
|
|
1108
|
-
|
|
1109
|
-
// Send command start event
|
|
1110
|
-
controller.enqueue(
|
|
1111
|
-
new TextEncoder().encode(
|
|
1112
|
-
`data: ${JSON.stringify({
|
|
1113
|
-
args,
|
|
1114
|
-
command: "mkdir",
|
|
1115
|
-
timestamp: new Date().toISOString(),
|
|
1116
|
-
type: "command_start",
|
|
1117
|
-
})}\n\n`
|
|
1118
|
-
)
|
|
1119
|
-
);
|
|
1120
|
-
|
|
1121
|
-
child.stdout?.on("data", (data) => {
|
|
1122
|
-
const output = data.toString();
|
|
1123
|
-
stdout += output;
|
|
1124
|
-
|
|
1125
|
-
// Send real-time output
|
|
1126
|
-
controller.enqueue(
|
|
1127
|
-
new TextEncoder().encode(
|
|
1128
|
-
`data: ${JSON.stringify({
|
|
1129
|
-
command: "mkdir",
|
|
1130
|
-
data: output,
|
|
1131
|
-
stream: "stdout",
|
|
1132
|
-
type: "output",
|
|
1133
|
-
})}\n\n`
|
|
1134
|
-
)
|
|
1135
|
-
);
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
child.stderr?.on("data", (data) => {
|
|
1139
|
-
const output = data.toString();
|
|
1140
|
-
stderr += output;
|
|
1141
|
-
|
|
1142
|
-
// Send real-time error output
|
|
1143
|
-
controller.enqueue(
|
|
1144
|
-
new TextEncoder().encode(
|
|
1145
|
-
`data: ${JSON.stringify({
|
|
1146
|
-
command: "mkdir",
|
|
1147
|
-
data: output,
|
|
1148
|
-
stream: "stderr",
|
|
1149
|
-
type: "output",
|
|
1150
|
-
})}\n\n`
|
|
1151
|
-
)
|
|
1152
|
-
);
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
child.on("close", (code) => {
|
|
1156
|
-
// Clear the active process reference
|
|
1157
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1158
|
-
const session = sessions.get(sessionId)!;
|
|
1159
|
-
session.activeProcess = null;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
console.log(`[Server] Command completed: mkdir, Exit code: ${code}`);
|
|
1163
|
-
|
|
1164
|
-
// Send command completion event
|
|
1165
|
-
controller.enqueue(
|
|
1166
|
-
new TextEncoder().encode(
|
|
1167
|
-
`data: ${JSON.stringify({
|
|
1168
|
-
args,
|
|
1169
|
-
command: "mkdir",
|
|
1170
|
-
exitCode: code,
|
|
1171
|
-
stderr,
|
|
1172
|
-
stdout,
|
|
1173
|
-
success: code === 0,
|
|
1174
|
-
timestamp: new Date().toISOString(),
|
|
1175
|
-
type: "command_complete",
|
|
1176
|
-
})}\n\n`
|
|
1177
|
-
)
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
controller.close();
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
child.on("error", (error) => {
|
|
1184
|
-
// Clear the active process reference
|
|
1185
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1186
|
-
const session = sessions.get(sessionId)!;
|
|
1187
|
-
session.activeProcess = null;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
controller.enqueue(
|
|
1191
|
-
new TextEncoder().encode(
|
|
1192
|
-
`data: ${JSON.stringify({
|
|
1193
|
-
args,
|
|
1194
|
-
command: "mkdir",
|
|
1195
|
-
error: error.message,
|
|
1196
|
-
type: "error",
|
|
1197
|
-
})}\n\n`
|
|
1198
|
-
)
|
|
1199
|
-
);
|
|
1200
|
-
|
|
1201
|
-
controller.close();
|
|
1202
|
-
});
|
|
1203
|
-
},
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
return new Response(stream, {
|
|
1207
|
-
headers: {
|
|
1208
|
-
"Cache-Control": "no-cache",
|
|
1209
|
-
Connection: "keep-alive",
|
|
1210
|
-
"Content-Type": "text/event-stream",
|
|
1211
|
-
...corsHeaders,
|
|
1212
|
-
},
|
|
1213
|
-
});
|
|
1214
|
-
} catch (error) {
|
|
1215
|
-
console.error("[Server] Error in handleStreamingMkdirRequest:", error);
|
|
1216
|
-
return new Response(
|
|
1217
|
-
JSON.stringify({
|
|
1218
|
-
error: "Failed to create directory",
|
|
1219
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1220
|
-
}),
|
|
1221
|
-
{
|
|
1222
|
-
headers: {
|
|
1223
|
-
"Content-Type": "application/json",
|
|
1224
|
-
...corsHeaders,
|
|
1225
|
-
},
|
|
1226
|
-
status: 500,
|
|
1227
|
-
}
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
async function handleWriteFileRequest(
|
|
1233
|
-
req: Request,
|
|
1234
|
-
corsHeaders: Record<string, string>
|
|
1235
|
-
): Promise<Response> {
|
|
1236
|
-
try {
|
|
1237
|
-
const body = (await req.json()) as WriteFileRequest;
|
|
1238
|
-
const { path, content, encoding = "utf-8", sessionId } = body;
|
|
1239
|
-
|
|
1240
|
-
if (!path || typeof path !== "string") {
|
|
1241
|
-
return new Response(
|
|
1242
|
-
JSON.stringify({
|
|
1243
|
-
error: "Path is required and must be a string",
|
|
1244
|
-
}),
|
|
1245
|
-
{
|
|
1246
|
-
headers: {
|
|
1247
|
-
"Content-Type": "application/json",
|
|
1248
|
-
...corsHeaders,
|
|
1249
|
-
},
|
|
1250
|
-
status: 400,
|
|
1251
|
-
}
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// Basic safety check - prevent dangerous paths
|
|
1256
|
-
const dangerousPatterns = [
|
|
1257
|
-
/^\/$/, // Root directory
|
|
1258
|
-
/^\/etc/, // System directories
|
|
1259
|
-
/^\/var/, // System directories
|
|
1260
|
-
/^\/usr/, // System directories
|
|
1261
|
-
/^\/bin/, // System directories
|
|
1262
|
-
/^\/sbin/, // System directories
|
|
1263
|
-
/^\/boot/, // System directories
|
|
1264
|
-
/^\/dev/, // System directories
|
|
1265
|
-
/^\/proc/, // System directories
|
|
1266
|
-
/^\/sys/, // System directories
|
|
1267
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1268
|
-
/\.\./, // Path traversal attempts
|
|
1269
|
-
];
|
|
1270
|
-
|
|
1271
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1272
|
-
return new Response(
|
|
1273
|
-
JSON.stringify({
|
|
1274
|
-
error: "Dangerous path not allowed",
|
|
1275
|
-
}),
|
|
1276
|
-
{
|
|
1277
|
-
headers: {
|
|
1278
|
-
"Content-Type": "application/json",
|
|
1279
|
-
...corsHeaders,
|
|
1280
|
-
},
|
|
1281
|
-
status: 400,
|
|
1282
|
-
}
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
console.log(
|
|
1287
|
-
`[Server] Writing file: ${path} (content length: ${content.length})`
|
|
1288
|
-
);
|
|
1289
|
-
|
|
1290
|
-
const result = await executeWriteFile(path, content, encoding, sessionId);
|
|
1291
|
-
|
|
1292
|
-
return new Response(
|
|
1293
|
-
JSON.stringify({
|
|
1294
|
-
exitCode: result.exitCode,
|
|
1295
|
-
path,
|
|
1296
|
-
success: result.success,
|
|
1297
|
-
timestamp: new Date().toISOString(),
|
|
1298
|
-
}),
|
|
1299
|
-
{
|
|
1300
|
-
headers: {
|
|
1301
|
-
"Content-Type": "application/json",
|
|
1302
|
-
...corsHeaders,
|
|
1303
|
-
},
|
|
1304
|
-
}
|
|
1305
|
-
);
|
|
1306
|
-
} catch (error) {
|
|
1307
|
-
console.error("[Server] Error in handleWriteFileRequest:", error);
|
|
1308
|
-
return new Response(
|
|
1309
|
-
JSON.stringify({
|
|
1310
|
-
error: "Failed to write file",
|
|
1311
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1312
|
-
}),
|
|
1313
|
-
{
|
|
1314
|
-
headers: {
|
|
1315
|
-
"Content-Type": "application/json",
|
|
1316
|
-
...corsHeaders,
|
|
1317
|
-
},
|
|
1318
|
-
status: 500,
|
|
1319
|
-
}
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
async function handleStreamingWriteFileRequest(
|
|
1325
|
-
req: Request,
|
|
1326
|
-
corsHeaders: Record<string, string>
|
|
1327
|
-
): Promise<Response> {
|
|
1328
|
-
try {
|
|
1329
|
-
const body = (await req.json()) as WriteFileRequest;
|
|
1330
|
-
const { path, content, encoding = "utf-8", sessionId } = body;
|
|
1331
|
-
|
|
1332
|
-
if (!path || typeof path !== "string") {
|
|
1333
|
-
return new Response(
|
|
1334
|
-
JSON.stringify({
|
|
1335
|
-
error: "Path is required and must be a string",
|
|
1336
|
-
}),
|
|
1337
|
-
{
|
|
1338
|
-
headers: {
|
|
1339
|
-
"Content-Type": "application/json",
|
|
1340
|
-
...corsHeaders,
|
|
1341
|
-
},
|
|
1342
|
-
status: 400,
|
|
1343
|
-
}
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// Basic safety check - prevent dangerous paths
|
|
1348
|
-
const dangerousPatterns = [
|
|
1349
|
-
/^\/$/, // Root directory
|
|
1350
|
-
/^\/etc/, // System directories
|
|
1351
|
-
/^\/var/, // System directories
|
|
1352
|
-
/^\/usr/, // System directories
|
|
1353
|
-
/^\/bin/, // System directories
|
|
1354
|
-
/^\/sbin/, // System directories
|
|
1355
|
-
/^\/boot/, // System directories
|
|
1356
|
-
/^\/dev/, // System directories
|
|
1357
|
-
/^\/proc/, // System directories
|
|
1358
|
-
/^\/sys/, // System directories
|
|
1359
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1360
|
-
/\.\./, // Path traversal attempts
|
|
1361
|
-
];
|
|
1362
|
-
|
|
1363
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1364
|
-
return new Response(
|
|
1365
|
-
JSON.stringify({
|
|
1366
|
-
error: "Dangerous path not allowed",
|
|
1367
|
-
}),
|
|
1368
|
-
{
|
|
1369
|
-
headers: {
|
|
1370
|
-
"Content-Type": "application/json",
|
|
1371
|
-
...corsHeaders,
|
|
1372
|
-
},
|
|
1373
|
-
status: 400,
|
|
1374
|
-
}
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
console.log(
|
|
1379
|
-
`[Server] Writing file (streaming): ${path} (content length: ${content.length})`
|
|
1380
|
-
);
|
|
1381
|
-
|
|
1382
|
-
const stream = new ReadableStream({
|
|
1383
|
-
start(controller) {
|
|
1384
|
-
(async () => {
|
|
1385
|
-
try {
|
|
1386
|
-
// Send command start event
|
|
1387
|
-
controller.enqueue(
|
|
1388
|
-
new TextEncoder().encode(
|
|
1389
|
-
`data: ${JSON.stringify({
|
|
1390
|
-
path,
|
|
1391
|
-
timestamp: new Date().toISOString(),
|
|
1392
|
-
type: "command_start",
|
|
1393
|
-
})}\n\n`
|
|
1394
|
-
)
|
|
1395
|
-
);
|
|
1396
|
-
|
|
1397
|
-
// Ensure the directory exists
|
|
1398
|
-
const dir = dirname(path);
|
|
1399
|
-
if (dir !== ".") {
|
|
1400
|
-
await mkdir(dir, { recursive: true });
|
|
1401
|
-
|
|
1402
|
-
// Send directory creation event
|
|
1403
|
-
controller.enqueue(
|
|
1404
|
-
new TextEncoder().encode(
|
|
1405
|
-
`data: ${JSON.stringify({
|
|
1406
|
-
message: `Created directory: ${dir}`,
|
|
1407
|
-
type: "output",
|
|
1408
|
-
})}\n\n`
|
|
1409
|
-
)
|
|
1410
|
-
);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// Write the file
|
|
1414
|
-
await writeFile(path, content, {
|
|
1415
|
-
encoding: encoding as BufferEncoding,
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
console.log(`[Server] File written successfully: ${path}`);
|
|
1419
|
-
|
|
1420
|
-
// Send command completion event
|
|
1421
|
-
controller.enqueue(
|
|
1422
|
-
new TextEncoder().encode(
|
|
1423
|
-
`data: ${JSON.stringify({
|
|
1424
|
-
path,
|
|
1425
|
-
success: true,
|
|
1426
|
-
timestamp: new Date().toISOString(),
|
|
1427
|
-
type: "command_complete",
|
|
1428
|
-
})}\n\n`
|
|
1429
|
-
)
|
|
1430
|
-
);
|
|
1431
|
-
|
|
1432
|
-
controller.close();
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
console.error(`[Server] Error writing file: ${path}`, error);
|
|
1435
|
-
|
|
1436
|
-
controller.enqueue(
|
|
1437
|
-
new TextEncoder().encode(
|
|
1438
|
-
`data: ${JSON.stringify({
|
|
1439
|
-
error:
|
|
1440
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1441
|
-
path,
|
|
1442
|
-
type: "error",
|
|
1443
|
-
})}\n\n`
|
|
1444
|
-
)
|
|
1445
|
-
);
|
|
1446
|
-
|
|
1447
|
-
controller.close();
|
|
1448
|
-
}
|
|
1449
|
-
})();
|
|
1450
|
-
},
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
return new Response(stream, {
|
|
1454
|
-
headers: {
|
|
1455
|
-
"Cache-Control": "no-cache",
|
|
1456
|
-
Connection: "keep-alive",
|
|
1457
|
-
"Content-Type": "text/event-stream",
|
|
1458
|
-
...corsHeaders,
|
|
1459
|
-
},
|
|
1460
|
-
});
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
console.error("[Server] Error in handleStreamingWriteFileRequest:", error);
|
|
1463
|
-
return new Response(
|
|
1464
|
-
JSON.stringify({
|
|
1465
|
-
error: "Failed to write file",
|
|
1466
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1467
|
-
}),
|
|
1468
|
-
{
|
|
1469
|
-
headers: {
|
|
1470
|
-
"Content-Type": "application/json",
|
|
1471
|
-
...corsHeaders,
|
|
1472
|
-
},
|
|
1473
|
-
status: 500,
|
|
1474
|
-
}
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
async function handleDeleteFileRequest(
|
|
1480
|
-
req: Request,
|
|
1481
|
-
corsHeaders: Record<string, string>
|
|
1482
|
-
): Promise<Response> {
|
|
1483
|
-
try {
|
|
1484
|
-
const body = (await req.json()) as DeleteFileRequest;
|
|
1485
|
-
const { path, sessionId } = body;
|
|
1486
|
-
|
|
1487
|
-
if (!path || typeof path !== "string") {
|
|
1488
|
-
return new Response(
|
|
1489
|
-
JSON.stringify({
|
|
1490
|
-
error: "Path is required and must be a string",
|
|
1491
|
-
}),
|
|
1492
|
-
{
|
|
1493
|
-
headers: {
|
|
1494
|
-
"Content-Type": "application/json",
|
|
1495
|
-
...corsHeaders,
|
|
1496
|
-
},
|
|
1497
|
-
status: 400,
|
|
1498
|
-
}
|
|
1499
|
-
);
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Basic safety check - prevent dangerous paths
|
|
1503
|
-
const dangerousPatterns = [
|
|
1504
|
-
/^\/$/, // Root directory
|
|
1505
|
-
/^\/etc/, // System directories
|
|
1506
|
-
/^\/var/, // System directories
|
|
1507
|
-
/^\/usr/, // System directories
|
|
1508
|
-
/^\/bin/, // System directories
|
|
1509
|
-
/^\/sbin/, // System directories
|
|
1510
|
-
/^\/boot/, // System directories
|
|
1511
|
-
/^\/dev/, // System directories
|
|
1512
|
-
/^\/proc/, // System directories
|
|
1513
|
-
/^\/sys/, // System directories
|
|
1514
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1515
|
-
/\.\./, // Path traversal attempts
|
|
1516
|
-
];
|
|
1517
|
-
|
|
1518
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1519
|
-
return new Response(
|
|
1520
|
-
JSON.stringify({
|
|
1521
|
-
error: "Dangerous path not allowed",
|
|
1522
|
-
}),
|
|
1523
|
-
{
|
|
1524
|
-
headers: {
|
|
1525
|
-
"Content-Type": "application/json",
|
|
1526
|
-
...corsHeaders,
|
|
1527
|
-
},
|
|
1528
|
-
status: 400,
|
|
1529
|
-
}
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
console.log(`[Server] Deleting file: ${path}`);
|
|
1534
|
-
|
|
1535
|
-
const result = await executeDeleteFile(path, sessionId);
|
|
1536
|
-
|
|
1537
|
-
return new Response(
|
|
1538
|
-
JSON.stringify({
|
|
1539
|
-
exitCode: result.exitCode,
|
|
1540
|
-
path,
|
|
1541
|
-
success: result.success,
|
|
1542
|
-
timestamp: new Date().toISOString(),
|
|
1543
|
-
}),
|
|
1544
|
-
{
|
|
1545
|
-
headers: {
|
|
1546
|
-
"Content-Type": "application/json",
|
|
1547
|
-
...corsHeaders,
|
|
1548
|
-
},
|
|
1549
|
-
}
|
|
1550
|
-
);
|
|
1551
|
-
} catch (error) {
|
|
1552
|
-
console.error("[Server] Error in handleDeleteFileRequest:", error);
|
|
1553
|
-
return new Response(
|
|
1554
|
-
JSON.stringify({
|
|
1555
|
-
error: "Failed to delete file",
|
|
1556
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1557
|
-
}),
|
|
1558
|
-
{
|
|
1559
|
-
headers: {
|
|
1560
|
-
"Content-Type": "application/json",
|
|
1561
|
-
...corsHeaders,
|
|
1562
|
-
},
|
|
1563
|
-
status: 500,
|
|
1564
|
-
}
|
|
1565
|
-
);
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
async function handleStreamingDeleteFileRequest(
|
|
1570
|
-
req: Request,
|
|
1571
|
-
corsHeaders: Record<string, string>
|
|
1572
|
-
): Promise<Response> {
|
|
1573
|
-
try {
|
|
1574
|
-
const body = (await req.json()) as DeleteFileRequest;
|
|
1575
|
-
const { path, sessionId } = body;
|
|
1576
|
-
|
|
1577
|
-
if (!path || typeof path !== "string") {
|
|
1578
|
-
return new Response(
|
|
1579
|
-
JSON.stringify({
|
|
1580
|
-
error: "Path is required and must be a string",
|
|
1581
|
-
}),
|
|
1582
|
-
{
|
|
1583
|
-
headers: {
|
|
1584
|
-
"Content-Type": "application/json",
|
|
1585
|
-
...corsHeaders,
|
|
1586
|
-
},
|
|
1587
|
-
status: 400,
|
|
1588
|
-
}
|
|
1589
|
-
);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// Basic safety check - prevent dangerous paths
|
|
1593
|
-
const dangerousPatterns = [
|
|
1594
|
-
/^\/$/, // Root directory
|
|
1595
|
-
/^\/etc/, // System directories
|
|
1596
|
-
/^\/var/, // System directories
|
|
1597
|
-
/^\/usr/, // System directories
|
|
1598
|
-
/^\/bin/, // System directories
|
|
1599
|
-
/^\/sbin/, // System directories
|
|
1600
|
-
/^\/boot/, // System directories
|
|
1601
|
-
/^\/dev/, // System directories
|
|
1602
|
-
/^\/proc/, // System directories
|
|
1603
|
-
/^\/sys/, // System directories
|
|
1604
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1605
|
-
/\.\./, // Path traversal attempts
|
|
1606
|
-
];
|
|
1607
|
-
|
|
1608
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1609
|
-
return new Response(
|
|
1610
|
-
JSON.stringify({
|
|
1611
|
-
error: "Dangerous path not allowed",
|
|
1612
|
-
}),
|
|
1613
|
-
{
|
|
1614
|
-
headers: {
|
|
1615
|
-
"Content-Type": "application/json",
|
|
1616
|
-
...corsHeaders,
|
|
1617
|
-
},
|
|
1618
|
-
status: 400,
|
|
1619
|
-
}
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
console.log(`[Server] Deleting file (streaming): ${path}`);
|
|
1624
|
-
|
|
1625
|
-
const stream = new ReadableStream({
|
|
1626
|
-
start(controller) {
|
|
1627
|
-
(async () => {
|
|
1628
|
-
try {
|
|
1629
|
-
// Send command start event
|
|
1630
|
-
controller.enqueue(
|
|
1631
|
-
new TextEncoder().encode(
|
|
1632
|
-
`data: ${JSON.stringify({
|
|
1633
|
-
path,
|
|
1634
|
-
timestamp: new Date().toISOString(),
|
|
1635
|
-
type: "command_start",
|
|
1636
|
-
})}\n\n`
|
|
1637
|
-
)
|
|
1638
|
-
);
|
|
1639
|
-
|
|
1640
|
-
// Delete the file
|
|
1641
|
-
await executeDeleteFile(path, sessionId);
|
|
1642
|
-
|
|
1643
|
-
console.log(`[Server] File deleted successfully: ${path}`);
|
|
1644
|
-
|
|
1645
|
-
// Send command completion event
|
|
1646
|
-
controller.enqueue(
|
|
1647
|
-
new TextEncoder().encode(
|
|
1648
|
-
`data: ${JSON.stringify({
|
|
1649
|
-
path,
|
|
1650
|
-
success: true,
|
|
1651
|
-
timestamp: new Date().toISOString(),
|
|
1652
|
-
type: "command_complete",
|
|
1653
|
-
})}\n\n`
|
|
1654
|
-
)
|
|
1655
|
-
);
|
|
1656
|
-
|
|
1657
|
-
controller.close();
|
|
1658
|
-
} catch (error) {
|
|
1659
|
-
console.error(`[Server] Error deleting file: ${path}`, error);
|
|
1660
|
-
|
|
1661
|
-
controller.enqueue(
|
|
1662
|
-
new TextEncoder().encode(
|
|
1663
|
-
`data: ${JSON.stringify({
|
|
1664
|
-
error:
|
|
1665
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1666
|
-
path,
|
|
1667
|
-
type: "error",
|
|
1668
|
-
})}\n\n`
|
|
1669
|
-
)
|
|
1670
|
-
);
|
|
1671
|
-
|
|
1672
|
-
controller.close();
|
|
1673
|
-
}
|
|
1674
|
-
})();
|
|
1675
|
-
},
|
|
1676
|
-
});
|
|
1677
|
-
|
|
1678
|
-
return new Response(stream, {
|
|
1679
|
-
headers: {
|
|
1680
|
-
"Cache-Control": "no-cache",
|
|
1681
|
-
Connection: "keep-alive",
|
|
1682
|
-
"Content-Type": "text/event-stream",
|
|
1683
|
-
...corsHeaders,
|
|
1684
|
-
},
|
|
1685
|
-
});
|
|
1686
|
-
} catch (error) {
|
|
1687
|
-
console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
|
|
1688
|
-
return new Response(
|
|
1689
|
-
JSON.stringify({
|
|
1690
|
-
error: "Failed to delete file",
|
|
1691
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1692
|
-
}),
|
|
1693
|
-
{
|
|
1694
|
-
headers: {
|
|
1695
|
-
"Content-Type": "application/json",
|
|
1696
|
-
...corsHeaders,
|
|
1697
|
-
},
|
|
1698
|
-
status: 500,
|
|
1699
|
-
}
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
async function handleRenameFileRequest(
|
|
1705
|
-
req: Request,
|
|
1706
|
-
corsHeaders: Record<string, string>
|
|
1707
|
-
): Promise<Response> {
|
|
1708
|
-
try {
|
|
1709
|
-
const body = (await req.json()) as RenameFileRequest;
|
|
1710
|
-
const { oldPath, newPath, sessionId } = body;
|
|
1711
|
-
|
|
1712
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
1713
|
-
return new Response(
|
|
1714
|
-
JSON.stringify({
|
|
1715
|
-
error: "Old path is required and must be a string",
|
|
1716
|
-
}),
|
|
1717
|
-
{
|
|
1718
|
-
headers: {
|
|
1719
|
-
"Content-Type": "application/json",
|
|
1720
|
-
...corsHeaders,
|
|
1721
|
-
},
|
|
1722
|
-
status: 400,
|
|
1723
|
-
}
|
|
1724
|
-
);
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
if (!newPath || typeof newPath !== "string") {
|
|
1728
|
-
return new Response(
|
|
1729
|
-
JSON.stringify({
|
|
1730
|
-
error: "New path is required and must be a string",
|
|
1731
|
-
}),
|
|
1732
|
-
{
|
|
1733
|
-
headers: {
|
|
1734
|
-
"Content-Type": "application/json",
|
|
1735
|
-
...corsHeaders,
|
|
1736
|
-
},
|
|
1737
|
-
status: 400,
|
|
1738
|
-
}
|
|
1739
|
-
);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
// Basic safety check - prevent dangerous paths
|
|
1743
|
-
const dangerousPatterns = [
|
|
1744
|
-
/^\/$/, // Root directory
|
|
1745
|
-
/^\/etc/, // System directories
|
|
1746
|
-
/^\/var/, // System directories
|
|
1747
|
-
/^\/usr/, // System directories
|
|
1748
|
-
/^\/bin/, // System directories
|
|
1749
|
-
/^\/sbin/, // System directories
|
|
1750
|
-
/^\/boot/, // System directories
|
|
1751
|
-
/^\/dev/, // System directories
|
|
1752
|
-
/^\/proc/, // System directories
|
|
1753
|
-
/^\/sys/, // System directories
|
|
1754
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1755
|
-
/\.\./, // Path traversal attempts
|
|
1756
|
-
];
|
|
1757
|
-
|
|
1758
|
-
if (
|
|
1759
|
-
dangerousPatterns.some(
|
|
1760
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
1761
|
-
)
|
|
1762
|
-
) {
|
|
1763
|
-
return new Response(
|
|
1764
|
-
JSON.stringify({
|
|
1765
|
-
error: "Dangerous path not allowed",
|
|
1766
|
-
}),
|
|
1767
|
-
{
|
|
1768
|
-
headers: {
|
|
1769
|
-
"Content-Type": "application/json",
|
|
1770
|
-
...corsHeaders,
|
|
1771
|
-
},
|
|
1772
|
-
status: 400,
|
|
1773
|
-
}
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
|
|
1778
|
-
|
|
1779
|
-
const result = await executeRenameFile(oldPath, newPath, sessionId);
|
|
1780
|
-
|
|
1781
|
-
return new Response(
|
|
1782
|
-
JSON.stringify({
|
|
1783
|
-
exitCode: result.exitCode,
|
|
1784
|
-
newPath,
|
|
1785
|
-
oldPath,
|
|
1786
|
-
success: result.success,
|
|
1787
|
-
timestamp: new Date().toISOString(),
|
|
1788
|
-
}),
|
|
1789
|
-
{
|
|
1790
|
-
headers: {
|
|
1791
|
-
"Content-Type": "application/json",
|
|
1792
|
-
...corsHeaders,
|
|
1793
|
-
},
|
|
1794
|
-
}
|
|
1795
|
-
);
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
console.error("[Server] Error in handleRenameFileRequest:", error);
|
|
1798
|
-
return new Response(
|
|
1799
|
-
JSON.stringify({
|
|
1800
|
-
error: "Failed to rename file",
|
|
1801
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1802
|
-
}),
|
|
1803
|
-
{
|
|
1804
|
-
headers: {
|
|
1805
|
-
"Content-Type": "application/json",
|
|
1806
|
-
...corsHeaders,
|
|
1807
|
-
},
|
|
1808
|
-
status: 500,
|
|
1809
|
-
}
|
|
1810
|
-
);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
async function handleStreamingRenameFileRequest(
|
|
1815
|
-
req: Request,
|
|
1816
|
-
corsHeaders: Record<string, string>
|
|
1817
|
-
): Promise<Response> {
|
|
1818
|
-
try {
|
|
1819
|
-
const body = (await req.json()) as RenameFileRequest;
|
|
1820
|
-
const { oldPath, newPath, sessionId } = body;
|
|
1821
|
-
|
|
1822
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
1823
|
-
return new Response(
|
|
1824
|
-
JSON.stringify({
|
|
1825
|
-
error: "Old path is required and must be a string",
|
|
1826
|
-
}),
|
|
1827
|
-
{
|
|
1828
|
-
headers: {
|
|
1829
|
-
"Content-Type": "application/json",
|
|
1830
|
-
...corsHeaders,
|
|
1831
|
-
},
|
|
1832
|
-
status: 400,
|
|
1833
|
-
}
|
|
1834
|
-
);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
if (!newPath || typeof newPath !== "string") {
|
|
1838
|
-
return new Response(
|
|
1839
|
-
JSON.stringify({
|
|
1840
|
-
error: "New path is required and must be a string",
|
|
1841
|
-
}),
|
|
1842
|
-
{
|
|
1843
|
-
headers: {
|
|
1844
|
-
"Content-Type": "application/json",
|
|
1845
|
-
...corsHeaders,
|
|
1846
|
-
},
|
|
1847
|
-
status: 400,
|
|
1848
|
-
}
|
|
1849
|
-
);
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// Basic safety check - prevent dangerous paths
|
|
1853
|
-
const dangerousPatterns = [
|
|
1854
|
-
/^\/$/, // Root directory
|
|
1855
|
-
/^\/etc/, // System directories
|
|
1856
|
-
/^\/var/, // System directories
|
|
1857
|
-
/^\/usr/, // System directories
|
|
1858
|
-
/^\/bin/, // System directories
|
|
1859
|
-
/^\/sbin/, // System directories
|
|
1860
|
-
/^\/boot/, // System directories
|
|
1861
|
-
/^\/dev/, // System directories
|
|
1862
|
-
/^\/proc/, // System directories
|
|
1863
|
-
/^\/sys/, // System directories
|
|
1864
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1865
|
-
/\.\./, // Path traversal attempts
|
|
1866
|
-
];
|
|
1867
|
-
|
|
1868
|
-
if (
|
|
1869
|
-
dangerousPatterns.some(
|
|
1870
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
1871
|
-
)
|
|
1872
|
-
) {
|
|
1873
|
-
return new Response(
|
|
1874
|
-
JSON.stringify({
|
|
1875
|
-
error: "Dangerous path not allowed",
|
|
1876
|
-
}),
|
|
1877
|
-
{
|
|
1878
|
-
headers: {
|
|
1879
|
-
"Content-Type": "application/json",
|
|
1880
|
-
...corsHeaders,
|
|
1881
|
-
},
|
|
1882
|
-
status: 400,
|
|
1883
|
-
}
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
|
|
1888
|
-
|
|
1889
|
-
const stream = new ReadableStream({
|
|
1890
|
-
start(controller) {
|
|
1891
|
-
(async () => {
|
|
1892
|
-
try {
|
|
1893
|
-
// Send command start event
|
|
1894
|
-
controller.enqueue(
|
|
1895
|
-
new TextEncoder().encode(
|
|
1896
|
-
`data: ${JSON.stringify({
|
|
1897
|
-
newPath,
|
|
1898
|
-
oldPath,
|
|
1899
|
-
timestamp: new Date().toISOString(),
|
|
1900
|
-
type: "command_start",
|
|
1901
|
-
})}\n\n`
|
|
1902
|
-
)
|
|
1903
|
-
);
|
|
1904
|
-
|
|
1905
|
-
// Rename the file
|
|
1906
|
-
await executeRenameFile(oldPath, newPath, sessionId);
|
|
1907
|
-
|
|
1908
|
-
console.log(
|
|
1909
|
-
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
1910
|
-
);
|
|
1911
|
-
|
|
1912
|
-
// Send command completion event
|
|
1913
|
-
controller.enqueue(
|
|
1914
|
-
new TextEncoder().encode(
|
|
1915
|
-
`data: ${JSON.stringify({
|
|
1916
|
-
newPath,
|
|
1917
|
-
oldPath,
|
|
1918
|
-
success: true,
|
|
1919
|
-
timestamp: new Date().toISOString(),
|
|
1920
|
-
type: "command_complete",
|
|
1921
|
-
})}\n\n`
|
|
1922
|
-
)
|
|
1923
|
-
);
|
|
1924
|
-
|
|
1925
|
-
controller.close();
|
|
1926
|
-
} catch (error) {
|
|
1927
|
-
console.error(
|
|
1928
|
-
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
1929
|
-
error
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
controller.enqueue(
|
|
1933
|
-
new TextEncoder().encode(
|
|
1934
|
-
`data: ${JSON.stringify({
|
|
1935
|
-
error:
|
|
1936
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1937
|
-
newPath,
|
|
1938
|
-
oldPath,
|
|
1939
|
-
type: "error",
|
|
1940
|
-
})}\n\n`
|
|
1941
|
-
)
|
|
1942
|
-
);
|
|
1943
|
-
|
|
1944
|
-
controller.close();
|
|
1945
|
-
}
|
|
1946
|
-
})();
|
|
1947
|
-
},
|
|
1948
|
-
});
|
|
1949
|
-
|
|
1950
|
-
return new Response(stream, {
|
|
1951
|
-
headers: {
|
|
1952
|
-
"Cache-Control": "no-cache",
|
|
1953
|
-
Connection: "keep-alive",
|
|
1954
|
-
"Content-Type": "text/event-stream",
|
|
1955
|
-
...corsHeaders,
|
|
1956
|
-
},
|
|
1957
|
-
});
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
console.error("[Server] Error in handleStreamingRenameFileRequest:", error);
|
|
1960
|
-
return new Response(
|
|
1961
|
-
JSON.stringify({
|
|
1962
|
-
error: "Failed to rename file",
|
|
1963
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1964
|
-
}),
|
|
1965
|
-
{
|
|
1966
|
-
headers: {
|
|
1967
|
-
"Content-Type": "application/json",
|
|
1968
|
-
...corsHeaders,
|
|
1969
|
-
},
|
|
1970
|
-
status: 500,
|
|
1971
|
-
}
|
|
1972
|
-
);
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
async function handleMoveFileRequest(
|
|
1977
|
-
req: Request,
|
|
1978
|
-
corsHeaders: Record<string, string>
|
|
1979
|
-
): Promise<Response> {
|
|
1980
|
-
try {
|
|
1981
|
-
const body = (await req.json()) as MoveFileRequest;
|
|
1982
|
-
const { sourcePath, destinationPath, sessionId } = body;
|
|
1983
|
-
|
|
1984
|
-
if (!sourcePath || typeof sourcePath !== "string") {
|
|
1985
|
-
return new Response(
|
|
1986
|
-
JSON.stringify({
|
|
1987
|
-
error: "Source path is required and must be a string",
|
|
1988
|
-
}),
|
|
1989
|
-
{
|
|
1990
|
-
headers: {
|
|
1991
|
-
"Content-Type": "application/json",
|
|
1992
|
-
...corsHeaders,
|
|
1993
|
-
},
|
|
1994
|
-
status: 400,
|
|
1995
|
-
}
|
|
1996
|
-
);
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2000
|
-
return new Response(
|
|
2001
|
-
JSON.stringify({
|
|
2002
|
-
error: "Destination path is required and must be a string",
|
|
2003
|
-
}),
|
|
2004
|
-
{
|
|
2005
|
-
headers: {
|
|
2006
|
-
"Content-Type": "application/json",
|
|
2007
|
-
...corsHeaders,
|
|
2008
|
-
},
|
|
2009
|
-
status: 400,
|
|
2010
|
-
}
|
|
2011
|
-
);
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
// Basic safety check - prevent dangerous paths
|
|
2015
|
-
const dangerousPatterns = [
|
|
2016
|
-
/^\/$/, // Root directory
|
|
2017
|
-
/^\/etc/, // System directories
|
|
2018
|
-
/^\/var/, // System directories
|
|
2019
|
-
/^\/usr/, // System directories
|
|
2020
|
-
/^\/bin/, // System directories
|
|
2021
|
-
/^\/sbin/, // System directories
|
|
2022
|
-
/^\/boot/, // System directories
|
|
2023
|
-
/^\/dev/, // System directories
|
|
2024
|
-
/^\/proc/, // System directories
|
|
2025
|
-
/^\/sys/, // System directories
|
|
2026
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2027
|
-
/\.\./, // Path traversal attempts
|
|
2028
|
-
];
|
|
2029
|
-
|
|
2030
|
-
if (
|
|
2031
|
-
dangerousPatterns.some(
|
|
2032
|
-
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2033
|
-
)
|
|
2034
|
-
) {
|
|
2035
|
-
return new Response(
|
|
2036
|
-
JSON.stringify({
|
|
2037
|
-
error: "Dangerous path not allowed",
|
|
2038
|
-
}),
|
|
2039
|
-
{
|
|
2040
|
-
headers: {
|
|
2041
|
-
"Content-Type": "application/json",
|
|
2042
|
-
...corsHeaders,
|
|
2043
|
-
},
|
|
2044
|
-
status: 400,
|
|
2045
|
-
}
|
|
2046
|
-
);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
|
|
2050
|
-
|
|
2051
|
-
const result = await executeMoveFile(
|
|
2052
|
-
sourcePath,
|
|
2053
|
-
destinationPath,
|
|
2054
|
-
sessionId
|
|
2055
|
-
);
|
|
2056
|
-
|
|
2057
|
-
return new Response(
|
|
2058
|
-
JSON.stringify({
|
|
2059
|
-
destinationPath,
|
|
2060
|
-
exitCode: result.exitCode,
|
|
2061
|
-
sourcePath,
|
|
2062
|
-
success: result.success,
|
|
2063
|
-
timestamp: new Date().toISOString(),
|
|
2064
|
-
}),
|
|
2065
|
-
{
|
|
2066
|
-
headers: {
|
|
2067
|
-
"Content-Type": "application/json",
|
|
2068
|
-
...corsHeaders,
|
|
2069
|
-
},
|
|
2070
|
-
}
|
|
2071
|
-
);
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
console.error("[Server] Error in handleMoveFileRequest:", error);
|
|
2074
|
-
return new Response(
|
|
2075
|
-
JSON.stringify({
|
|
2076
|
-
error: "Failed to move file",
|
|
2077
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2078
|
-
}),
|
|
2079
|
-
{
|
|
2080
|
-
headers: {
|
|
2081
|
-
"Content-Type": "application/json",
|
|
2082
|
-
...corsHeaders,
|
|
2083
|
-
},
|
|
2084
|
-
status: 500,
|
|
2085
|
-
}
|
|
2086
|
-
);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
async function handleStreamingMoveFileRequest(
|
|
2091
|
-
req: Request,
|
|
2092
|
-
corsHeaders: Record<string, string>
|
|
2093
|
-
): Promise<Response> {
|
|
2094
|
-
try {
|
|
2095
|
-
const body = (await req.json()) as MoveFileRequest;
|
|
2096
|
-
const { sourcePath, destinationPath, sessionId } = body;
|
|
2097
|
-
|
|
2098
|
-
if (!sourcePath || typeof sourcePath !== "string") {
|
|
2099
|
-
return new Response(
|
|
2100
|
-
JSON.stringify({
|
|
2101
|
-
error: "Source path is required and must be a string",
|
|
2102
|
-
}),
|
|
2103
|
-
{
|
|
2104
|
-
headers: {
|
|
2105
|
-
"Content-Type": "application/json",
|
|
2106
|
-
...corsHeaders,
|
|
2107
|
-
},
|
|
2108
|
-
status: 400,
|
|
2109
|
-
}
|
|
2110
|
-
);
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2114
|
-
return new Response(
|
|
2115
|
-
JSON.stringify({
|
|
2116
|
-
error: "Destination path is required and must be a string",
|
|
2117
|
-
}),
|
|
2118
|
-
{
|
|
2119
|
-
headers: {
|
|
2120
|
-
"Content-Type": "application/json",
|
|
2121
|
-
...corsHeaders,
|
|
2122
|
-
},
|
|
2123
|
-
status: 400,
|
|
2124
|
-
}
|
|
2125
|
-
);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// Basic safety check - prevent dangerous paths
|
|
2129
|
-
const dangerousPatterns = [
|
|
2130
|
-
/^\/$/, // Root directory
|
|
2131
|
-
/^\/etc/, // System directories
|
|
2132
|
-
/^\/var/, // System directories
|
|
2133
|
-
/^\/usr/, // System directories
|
|
2134
|
-
/^\/bin/, // System directories
|
|
2135
|
-
/^\/sbin/, // System directories
|
|
2136
|
-
/^\/boot/, // System directories
|
|
2137
|
-
/^\/dev/, // System directories
|
|
2138
|
-
/^\/proc/, // System directories
|
|
2139
|
-
/^\/sys/, // System directories
|
|
2140
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2141
|
-
/\.\./, // Path traversal attempts
|
|
2142
|
-
];
|
|
2143
|
-
|
|
2144
|
-
if (
|
|
2145
|
-
dangerousPatterns.some(
|
|
2146
|
-
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2147
|
-
)
|
|
2148
|
-
) {
|
|
2149
|
-
return new Response(
|
|
2150
|
-
JSON.stringify({
|
|
2151
|
-
error: "Dangerous path not allowed",
|
|
2152
|
-
}),
|
|
2153
|
-
{
|
|
2154
|
-
headers: {
|
|
2155
|
-
"Content-Type": "application/json",
|
|
2156
|
-
...corsHeaders,
|
|
2157
|
-
},
|
|
2158
|
-
status: 400,
|
|
2159
|
-
}
|
|
2160
|
-
);
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
console.log(
|
|
2164
|
-
`[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
|
|
2165
|
-
);
|
|
2166
|
-
|
|
2167
|
-
const stream = new ReadableStream({
|
|
2168
|
-
start(controller) {
|
|
2169
|
-
(async () => {
|
|
2170
|
-
try {
|
|
2171
|
-
// Send command start event
|
|
2172
|
-
controller.enqueue(
|
|
2173
|
-
new TextEncoder().encode(
|
|
2174
|
-
`data: ${JSON.stringify({
|
|
2175
|
-
destinationPath,
|
|
2176
|
-
sourcePath,
|
|
2177
|
-
timestamp: new Date().toISOString(),
|
|
2178
|
-
type: "command_start",
|
|
2179
|
-
})}\n\n`
|
|
2180
|
-
)
|
|
2181
|
-
);
|
|
2182
|
-
|
|
2183
|
-
// Move the file
|
|
2184
|
-
await executeMoveFile(sourcePath, destinationPath, sessionId);
|
|
2185
|
-
|
|
2186
|
-
console.log(
|
|
2187
|
-
`[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
|
|
2188
|
-
);
|
|
2189
|
-
|
|
2190
|
-
// Send command completion event
|
|
2191
|
-
controller.enqueue(
|
|
2192
|
-
new TextEncoder().encode(
|
|
2193
|
-
`data: ${JSON.stringify({
|
|
2194
|
-
destinationPath,
|
|
2195
|
-
sourcePath,
|
|
2196
|
-
success: true,
|
|
2197
|
-
timestamp: new Date().toISOString(),
|
|
2198
|
-
type: "command_complete",
|
|
2199
|
-
})}\n\n`
|
|
2200
|
-
)
|
|
2201
|
-
);
|
|
2202
|
-
|
|
2203
|
-
controller.close();
|
|
2204
|
-
} catch (error) {
|
|
2205
|
-
console.error(
|
|
2206
|
-
`[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
|
|
2207
|
-
error
|
|
2208
|
-
);
|
|
2209
|
-
|
|
2210
|
-
controller.enqueue(
|
|
2211
|
-
new TextEncoder().encode(
|
|
2212
|
-
`data: ${JSON.stringify({
|
|
2213
|
-
destinationPath,
|
|
2214
|
-
error:
|
|
2215
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
2216
|
-
sourcePath,
|
|
2217
|
-
type: "error",
|
|
2218
|
-
})}\n\n`
|
|
2219
|
-
)
|
|
2220
|
-
);
|
|
2221
|
-
|
|
2222
|
-
controller.close();
|
|
2223
|
-
}
|
|
2224
|
-
})();
|
|
2225
|
-
},
|
|
2226
|
-
});
|
|
2227
|
-
|
|
2228
|
-
return new Response(stream, {
|
|
2229
|
-
headers: {
|
|
2230
|
-
"Cache-Control": "no-cache",
|
|
2231
|
-
Connection: "keep-alive",
|
|
2232
|
-
"Content-Type": "text/event-stream",
|
|
2233
|
-
...corsHeaders,
|
|
2234
|
-
},
|
|
2235
|
-
});
|
|
2236
|
-
} catch (error) {
|
|
2237
|
-
console.error("[Server] Error in handleStreamingMoveFileRequest:", error);
|
|
2238
|
-
return new Response(
|
|
2239
|
-
JSON.stringify({
|
|
2240
|
-
error: "Failed to move file",
|
|
2241
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2242
|
-
}),
|
|
2243
|
-
{
|
|
2244
|
-
headers: {
|
|
2245
|
-
"Content-Type": "application/json",
|
|
2246
|
-
...corsHeaders,
|
|
2247
|
-
},
|
|
2248
|
-
status: 500,
|
|
2249
|
-
}
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function executeCommand(
|
|
2255
|
-
command: string,
|
|
2256
|
-
args: string[],
|
|
2257
|
-
sessionId?: string
|
|
2258
|
-
): Promise<{
|
|
2259
|
-
success: boolean;
|
|
2260
|
-
stdout: string;
|
|
2261
|
-
stderr: string;
|
|
2262
|
-
exitCode: number;
|
|
2263
|
-
}> {
|
|
2264
|
-
return new Promise((resolve, reject) => {
|
|
2265
|
-
const child = spawn(command, args, {
|
|
2266
|
-
shell: true,
|
|
2267
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2268
|
-
});
|
|
2269
|
-
|
|
2270
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2271
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2272
|
-
const session = sessions.get(sessionId)!;
|
|
2273
|
-
session.activeProcess = child;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
let stdout = "";
|
|
2277
|
-
let stderr = "";
|
|
2278
|
-
|
|
2279
|
-
child.stdout?.on("data", (data) => {
|
|
2280
|
-
stdout += data.toString();
|
|
2281
|
-
});
|
|
2282
|
-
|
|
2283
|
-
child.stderr?.on("data", (data) => {
|
|
2284
|
-
stderr += data.toString();
|
|
2285
|
-
});
|
|
2286
|
-
|
|
2287
|
-
child.on("close", (code) => {
|
|
2288
|
-
// Clear the active process reference
|
|
2289
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2290
|
-
const session = sessions.get(sessionId)!;
|
|
2291
|
-
session.activeProcess = null;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
|
|
2295
|
-
|
|
2296
|
-
resolve({
|
|
2297
|
-
exitCode: code || 0,
|
|
2298
|
-
stderr,
|
|
2299
|
-
stdout,
|
|
2300
|
-
success: code === 0,
|
|
2301
|
-
});
|
|
2302
|
-
});
|
|
2303
|
-
|
|
2304
|
-
child.on("error", (error) => {
|
|
2305
|
-
// Clear the active process reference
|
|
2306
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2307
|
-
const session = sessions.get(sessionId)!;
|
|
2308
|
-
session.activeProcess = null;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
reject(error);
|
|
2312
|
-
});
|
|
2313
|
-
});
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
function executeGitCheckout(
|
|
2317
|
-
repoUrl: string,
|
|
2318
|
-
branch: string,
|
|
2319
|
-
targetDir: string,
|
|
2320
|
-
sessionId?: string
|
|
2321
|
-
): Promise<{
|
|
2322
|
-
success: boolean;
|
|
2323
|
-
stdout: string;
|
|
2324
|
-
stderr: string;
|
|
2325
|
-
exitCode: number;
|
|
2326
|
-
}> {
|
|
2327
|
-
return new Promise((resolve, reject) => {
|
|
2328
|
-
// First, clone the repository
|
|
2329
|
-
const cloneChild = spawn(
|
|
2330
|
-
"git",
|
|
2331
|
-
["clone", "-b", branch, repoUrl, targetDir],
|
|
2332
|
-
{
|
|
2333
|
-
shell: true,
|
|
2334
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2335
|
-
}
|
|
2336
|
-
);
|
|
2337
|
-
|
|
2338
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2339
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2340
|
-
const session = sessions.get(sessionId)!;
|
|
2341
|
-
session.activeProcess = cloneChild;
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
let stdout = "";
|
|
2345
|
-
let stderr = "";
|
|
2346
|
-
|
|
2347
|
-
cloneChild.stdout?.on("data", (data) => {
|
|
2348
|
-
stdout += data.toString();
|
|
2349
|
-
});
|
|
2350
|
-
|
|
2351
|
-
cloneChild.stderr?.on("data", (data) => {
|
|
2352
|
-
stderr += data.toString();
|
|
2353
|
-
});
|
|
2354
|
-
|
|
2355
|
-
cloneChild.on("close", (code) => {
|
|
2356
|
-
// Clear the active process reference
|
|
2357
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2358
|
-
const session = sessions.get(sessionId)!;
|
|
2359
|
-
session.activeProcess = null;
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
if (code === 0) {
|
|
2363
|
-
console.log(
|
|
2364
|
-
`[Server] Repository cloned successfully: ${repoUrl} to ${targetDir}`
|
|
2365
|
-
);
|
|
2366
|
-
resolve({
|
|
2367
|
-
exitCode: code || 0,
|
|
2368
|
-
stderr,
|
|
2369
|
-
stdout,
|
|
2370
|
-
success: true,
|
|
2371
|
-
});
|
|
2372
|
-
} else {
|
|
2373
|
-
console.error(
|
|
2374
|
-
`[Server] Failed to clone repository: ${repoUrl}, Exit code: ${code}`
|
|
2375
|
-
);
|
|
2376
|
-
resolve({
|
|
2377
|
-
exitCode: code || 1,
|
|
2378
|
-
stderr,
|
|
2379
|
-
stdout,
|
|
2380
|
-
success: false,
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
});
|
|
2384
|
-
|
|
2385
|
-
cloneChild.on("error", (error) => {
|
|
2386
|
-
// Clear the active process reference
|
|
2387
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2388
|
-
const session = sessions.get(sessionId)!;
|
|
2389
|
-
session.activeProcess = null;
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
console.error(`[Server] Error cloning repository: ${repoUrl}`, error);
|
|
2393
|
-
reject(error);
|
|
2394
|
-
});
|
|
2395
|
-
});
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
function executeMkdir(
|
|
2399
|
-
path: string,
|
|
2400
|
-
recursive: boolean,
|
|
2401
|
-
sessionId?: string
|
|
2402
|
-
): Promise<{
|
|
2403
|
-
success: boolean;
|
|
2404
|
-
stdout: string;
|
|
2405
|
-
stderr: string;
|
|
2406
|
-
exitCode: number;
|
|
2407
|
-
}> {
|
|
2408
|
-
return new Promise((resolve, reject) => {
|
|
2409
|
-
const args = recursive ? ["-p", path] : [path];
|
|
2410
|
-
const mkdirChild = spawn("mkdir", args, {
|
|
2411
|
-
shell: true,
|
|
2412
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2413
|
-
});
|
|
2414
|
-
|
|
2415
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2416
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2417
|
-
const session = sessions.get(sessionId)!;
|
|
2418
|
-
session.activeProcess = mkdirChild;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
let stdout = "";
|
|
2422
|
-
let stderr = "";
|
|
2423
|
-
|
|
2424
|
-
mkdirChild.stdout?.on("data", (data) => {
|
|
2425
|
-
stdout += data.toString();
|
|
2426
|
-
});
|
|
2427
|
-
|
|
2428
|
-
mkdirChild.stderr?.on("data", (data) => {
|
|
2429
|
-
stderr += data.toString();
|
|
2430
|
-
});
|
|
2431
|
-
|
|
2432
|
-
mkdirChild.on("close", (code) => {
|
|
2433
|
-
// Clear the active process reference
|
|
2434
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2435
|
-
const session = sessions.get(sessionId)!;
|
|
2436
|
-
session.activeProcess = null;
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
if (code === 0) {
|
|
2440
|
-
console.log(`[Server] Directory created successfully: ${path}`);
|
|
2441
|
-
resolve({
|
|
2442
|
-
exitCode: code || 0,
|
|
2443
|
-
stderr,
|
|
2444
|
-
stdout,
|
|
2445
|
-
success: true,
|
|
2446
|
-
});
|
|
2447
|
-
} else {
|
|
2448
|
-
console.error(
|
|
2449
|
-
`[Server] Failed to create directory: ${path}, Exit code: ${code}`
|
|
2450
|
-
);
|
|
2451
|
-
resolve({
|
|
2452
|
-
exitCode: code || 1,
|
|
2453
|
-
stderr,
|
|
2454
|
-
stdout,
|
|
2455
|
-
success: false,
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
});
|
|
2459
|
-
|
|
2460
|
-
mkdirChild.on("error", (error) => {
|
|
2461
|
-
// Clear the active process reference
|
|
2462
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2463
|
-
const session = sessions.get(sessionId)!;
|
|
2464
|
-
session.activeProcess = null;
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
console.error(`[Server] Error creating directory: ${path}`, error);
|
|
2468
|
-
reject(error);
|
|
2469
|
-
});
|
|
2470
|
-
});
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
function executeWriteFile(
|
|
2474
|
-
path: string,
|
|
2475
|
-
content: string,
|
|
2476
|
-
encoding: string,
|
|
2477
|
-
sessionId?: string
|
|
2478
|
-
): Promise<{
|
|
2479
|
-
success: boolean;
|
|
2480
|
-
exitCode: number;
|
|
2481
|
-
}> {
|
|
2482
|
-
return new Promise((resolve, reject) => {
|
|
2483
|
-
(async () => {
|
|
2484
|
-
try {
|
|
2485
|
-
// Ensure the directory exists
|
|
2486
|
-
const dir = dirname(path);
|
|
2487
|
-
if (dir !== ".") {
|
|
2488
|
-
await mkdir(dir, { recursive: true });
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
// Write the file
|
|
2492
|
-
await writeFile(path, content, {
|
|
2493
|
-
encoding: encoding as BufferEncoding,
|
|
2494
|
-
});
|
|
2495
|
-
|
|
2496
|
-
console.log(`[Server] File written successfully: ${path}`);
|
|
2497
|
-
resolve({
|
|
2498
|
-
exitCode: 0,
|
|
2499
|
-
success: true,
|
|
2500
|
-
});
|
|
2501
|
-
} catch (error) {
|
|
2502
|
-
console.error(`[Server] Error writing file: ${path}`, error);
|
|
2503
|
-
reject(error);
|
|
2504
|
-
}
|
|
2505
|
-
})();
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
function executeDeleteFile(
|
|
2510
|
-
path: string,
|
|
2511
|
-
sessionId?: string
|
|
2512
|
-
): Promise<{
|
|
2513
|
-
success: boolean;
|
|
2514
|
-
exitCode: number;
|
|
2515
|
-
}> {
|
|
2516
|
-
return new Promise((resolve, reject) => {
|
|
2517
|
-
(async () => {
|
|
2518
|
-
try {
|
|
2519
|
-
// Delete the file
|
|
2520
|
-
await unlink(path);
|
|
2521
|
-
|
|
2522
|
-
console.log(`[Server] File deleted successfully: ${path}`);
|
|
2523
|
-
resolve({
|
|
2524
|
-
exitCode: 0,
|
|
2525
|
-
success: true,
|
|
2526
|
-
});
|
|
2527
|
-
} catch (error) {
|
|
2528
|
-
console.error(`[Server] Error deleting file: ${path}`, error);
|
|
2529
|
-
reject(error);
|
|
2530
|
-
}
|
|
2531
|
-
})();
|
|
2532
|
-
});
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
function executeRenameFile(
|
|
2536
|
-
oldPath: string,
|
|
2537
|
-
newPath: string,
|
|
2538
|
-
sessionId?: string
|
|
2539
|
-
): Promise<{
|
|
2540
|
-
success: boolean;
|
|
2541
|
-
exitCode: number;
|
|
2542
|
-
}> {
|
|
2543
|
-
return new Promise((resolve, reject) => {
|
|
2544
|
-
(async () => {
|
|
2545
|
-
try {
|
|
2546
|
-
// Rename the file
|
|
2547
|
-
await rename(oldPath, newPath);
|
|
2548
|
-
|
|
2549
|
-
console.log(
|
|
2550
|
-
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
2551
|
-
);
|
|
2552
|
-
resolve({
|
|
2553
|
-
exitCode: 0,
|
|
2554
|
-
success: true,
|
|
2555
|
-
});
|
|
2556
|
-
} catch (error) {
|
|
2557
|
-
console.error(
|
|
2558
|
-
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
2559
|
-
error
|
|
2560
|
-
);
|
|
2561
|
-
reject(error);
|
|
2562
|
-
}
|
|
2563
|
-
})();
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
function executeMoveFile(
|
|
2568
|
-
sourcePath: string,
|
|
2569
|
-
destinationPath: string,
|
|
2570
|
-
sessionId?: string
|
|
2571
|
-
): Promise<{
|
|
2572
|
-
success: boolean;
|
|
2573
|
-
exitCode: number;
|
|
2574
|
-
}> {
|
|
2575
|
-
return new Promise((resolve, reject) => {
|
|
2576
|
-
(async () => {
|
|
2577
|
-
try {
|
|
2578
|
-
// Move the file
|
|
2579
|
-
await rename(sourcePath, destinationPath);
|
|
2580
|
-
|
|
2581
|
-
console.log(
|
|
2582
|
-
`[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
|
|
2583
|
-
);
|
|
2584
|
-
resolve({
|
|
2585
|
-
exitCode: 0,
|
|
2586
|
-
success: true,
|
|
2587
|
-
});
|
|
2588
|
-
} catch (error) {
|
|
2589
|
-
console.error(
|
|
2590
|
-
`[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
|
|
2591
|
-
error
|
|
2592
|
-
);
|
|
2593
|
-
reject(error);
|
|
2594
|
-
}
|
|
2595
|
-
})();
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
console.log(`🚀 Bun server running on http://localhost:${server.port}`);
|
|
570
|
+
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
2600
571
|
console.log(`📡 HTTP API endpoints available:`);
|
|
2601
572
|
console.log(` POST /api/session/create - Create a new session`);
|
|
2602
573
|
console.log(` GET /api/session/list - List all sessions`);
|
|
2603
574
|
console.log(` POST /api/execute - Execute a command (non-streaming)`);
|
|
2604
575
|
console.log(` POST /api/execute/stream - Execute a command (streaming)`);
|
|
2605
576
|
console.log(` POST /api/git/checkout - Checkout a git repository`);
|
|
2606
|
-
console.log(
|
|
2607
|
-
` POST /api/git/checkout/stream - Checkout a git repository (streaming)`
|
|
2608
|
-
);
|
|
2609
577
|
console.log(` POST /api/mkdir - Create a directory`);
|
|
2610
|
-
console.log(` POST /api/mkdir/stream - Create a directory (streaming)`);
|
|
2611
578
|
console.log(` POST /api/write - Write a file`);
|
|
2612
|
-
console.log(` POST /api/
|
|
579
|
+
console.log(` POST /api/read - Read a file`);
|
|
580
|
+
console.log(` POST /api/read/stream - Stream a file (SSE)`);
|
|
2613
581
|
console.log(` POST /api/delete - Delete a file`);
|
|
2614
|
-
console.log(` POST /api/delete/stream - Delete a file (streaming)`);
|
|
2615
582
|
console.log(` POST /api/rename - Rename a file`);
|
|
2616
|
-
console.log(` POST /api/rename/stream - Rename a file (streaming)`);
|
|
2617
583
|
console.log(` POST /api/move - Move a file`);
|
|
2618
|
-
console.log(` POST /api/
|
|
584
|
+
console.log(` POST /api/expose-port - Expose a port for external access`);
|
|
585
|
+
console.log(` DELETE /api/unexpose-port - Unexpose a port`);
|
|
586
|
+
console.log(` GET /api/exposed-ports - List exposed ports`);
|
|
587
|
+
console.log(` POST /api/process/start - Start a background process`);
|
|
588
|
+
console.log(` GET /api/process/list - List all processes`);
|
|
589
|
+
console.log(` GET /api/process/{id} - Get process status`);
|
|
590
|
+
console.log(` DELETE /api/process/{id} - Kill a process`);
|
|
591
|
+
console.log(` GET /api/process/{id}/logs - Get process logs`);
|
|
592
|
+
console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
|
|
593
|
+
console.log(` DELETE /api/process/kill-all - Kill all processes`);
|
|
594
|
+
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
595
|
+
console.log(` POST /api/contexts - Create a code execution context`);
|
|
596
|
+
console.log(` GET /api/contexts - List all contexts`);
|
|
597
|
+
console.log(` DELETE /api/contexts/{id} - Delete a context`);
|
|
598
|
+
console.log(
|
|
599
|
+
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
600
|
+
);
|
|
2619
601
|
console.log(` GET /api/ping - Health check`);
|
|
2620
|
-
console.log(` GET /api/commands - List available commands`);
|