@cloudflare/sandbox 0.0.9 → 0.1.0
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 +10 -0
- package/Dockerfile +1 -14
- package/container_src/handler/exec.ts +337 -0
- package/container_src/handler/file.ts +844 -0
- package/container_src/handler/git.ts +182 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +640 -0
- package/container_src/index.ts +82 -2973
- package/container_src/types.ts +103 -0
- package/dist/chunk-6THNBO4S.js +46 -0
- package/dist/chunk-6THNBO4S.js.map +1 -0
- package/dist/chunk-6UAWTJ5S.js +85 -0
- package/dist/chunk-6UAWTJ5S.js.map +1 -0
- package/dist/chunk-G4XT4SP7.js +638 -0
- package/dist/chunk-G4XT4SP7.js.map +1 -0
- package/dist/chunk-ISFOIYQC.js +585 -0
- package/dist/chunk-ISFOIYQC.js.map +1 -0
- package/dist/chunk-NNGBXDMY.js +89 -0
- package/dist/chunk-NNGBXDMY.js.map +1 -0
- package/dist/client-Da-mLX4p.d.ts +210 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +3 -37
- package/dist/index.d.ts +3 -1
- package/dist/index.js +13 -3
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +4 -2
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +4 -2
- package/dist/security.d.ts +30 -0
- package/dist/security.js +13 -0
- package/dist/security.js.map +1 -0
- package/dist/sse-parser.d.ts +28 -0
- package/dist/sse-parser.js +11 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -7
- package/src/client.ts +235 -1286
- package/src/index.ts +6 -0
- package/src/request-handler.ts +69 -20
- package/src/sandbox.ts +463 -70
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +386 -0
- package/README.md +0 -65
- package/dist/chunk-4J5LQCCN.js +0 -1446
- package/dist/chunk-4J5LQCCN.js.map +0 -1
- package/dist/chunk-5SZ3RVJZ.js +0 -250
- package/dist/chunk-5SZ3RVJZ.js.map +0 -1
- package/dist/client-BuVjqV00.d.ts +0 -247
- 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 -929
package/container_src/index.ts
CHANGED
|
@@ -1,72 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
4
2
|
import { serve } from "bun";
|
|
5
|
-
|
|
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
|
-
interface ReadFileRequest {
|
|
34
|
-
path: string;
|
|
35
|
-
encoding?: string;
|
|
36
|
-
sessionId?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface DeleteFileRequest {
|
|
40
|
-
path: string;
|
|
41
|
-
sessionId?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface RenameFileRequest {
|
|
45
|
-
oldPath: string;
|
|
46
|
-
newPath: string;
|
|
47
|
-
sessionId?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface MoveFileRequest {
|
|
51
|
-
sourcePath: string;
|
|
52
|
-
destinationPath: string;
|
|
53
|
-
sessionId?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ExposePortRequest {
|
|
57
|
-
port: number;
|
|
58
|
-
name?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface UnexposePortRequest {
|
|
62
|
-
port: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface SessionData {
|
|
66
|
-
sessionId: string;
|
|
67
|
-
activeProcess: ChildProcess | null;
|
|
68
|
-
createdAt: Date;
|
|
69
|
-
}
|
|
3
|
+
import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
|
|
4
|
+
import {
|
|
5
|
+
handleDeleteFileRequest,
|
|
6
|
+
handleMkdirRequest,
|
|
7
|
+
handleMoveFileRequest,
|
|
8
|
+
handleReadFileRequest,
|
|
9
|
+
handleRenameFileRequest,
|
|
10
|
+
handleWriteFileRequest,
|
|
11
|
+
} from "./handler/file";
|
|
12
|
+
import { handleGitCheckoutRequest } from "./handler/git";
|
|
13
|
+
import {
|
|
14
|
+
handleExposePortRequest,
|
|
15
|
+
handleGetExposedPortsRequest,
|
|
16
|
+
handleProxyRequest,
|
|
17
|
+
handleUnexposePortRequest,
|
|
18
|
+
} from "./handler/ports";
|
|
19
|
+
import {
|
|
20
|
+
handleGetProcessLogsRequest,
|
|
21
|
+
handleGetProcessRequest,
|
|
22
|
+
handleKillAllProcessesRequest,
|
|
23
|
+
handleKillProcessRequest,
|
|
24
|
+
handleListProcessesRequest,
|
|
25
|
+
handleStartProcessRequest,
|
|
26
|
+
handleStreamProcessLogsRequest,
|
|
27
|
+
} from "./handler/process";
|
|
28
|
+
import type { ProcessRecord, SessionData } from "./types";
|
|
70
29
|
|
|
71
30
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
72
31
|
const sessions = new Map<string, SessionData>();
|
|
@@ -74,9 +33,12 @@ const sessions = new Map<string, SessionData>();
|
|
|
74
33
|
// In-memory storage for exposed ports
|
|
75
34
|
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
76
35
|
|
|
77
|
-
//
|
|
36
|
+
// In-memory process storage - cleared on container restart
|
|
37
|
+
const processes = new Map<string, ProcessRecord>();
|
|
38
|
+
|
|
39
|
+
// Generate a unique session ID using cryptographically secure randomness
|
|
78
40
|
function generateSessionId(): string {
|
|
79
|
-
return `session_${Date.now()}_${
|
|
41
|
+
return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
|
|
80
42
|
}
|
|
81
43
|
|
|
82
44
|
// Clean up old sessions (older than 1 hour)
|
|
@@ -181,13 +143,13 @@ const server = serve({
|
|
|
181
143
|
|
|
182
144
|
case "/api/execute":
|
|
183
145
|
if (req.method === "POST") {
|
|
184
|
-
return handleExecuteRequest(req, corsHeaders);
|
|
146
|
+
return handleExecuteRequest(sessions, req, corsHeaders);
|
|
185
147
|
}
|
|
186
148
|
break;
|
|
187
149
|
|
|
188
150
|
case "/api/execute/stream":
|
|
189
151
|
if (req.method === "POST") {
|
|
190
|
-
return handleStreamingExecuteRequest(req, corsHeaders);
|
|
152
|
+
return handleStreamingExecuteRequest(sessions, req, corsHeaders);
|
|
191
153
|
}
|
|
192
154
|
break;
|
|
193
155
|
|
|
@@ -242,25 +204,13 @@ const server = serve({
|
|
|
242
204
|
|
|
243
205
|
case "/api/git/checkout":
|
|
244
206
|
if (req.method === "POST") {
|
|
245
|
-
return handleGitCheckoutRequest(req, corsHeaders);
|
|
246
|
-
}
|
|
247
|
-
break;
|
|
248
|
-
|
|
249
|
-
case "/api/git/checkout/stream":
|
|
250
|
-
if (req.method === "POST") {
|
|
251
|
-
return handleStreamingGitCheckoutRequest(req, corsHeaders);
|
|
207
|
+
return handleGitCheckoutRequest(sessions, req, corsHeaders);
|
|
252
208
|
}
|
|
253
209
|
break;
|
|
254
210
|
|
|
255
211
|
case "/api/mkdir":
|
|
256
212
|
if (req.method === "POST") {
|
|
257
|
-
return handleMkdirRequest(req, corsHeaders);
|
|
258
|
-
}
|
|
259
|
-
break;
|
|
260
|
-
|
|
261
|
-
case "/api/mkdir/stream":
|
|
262
|
-
if (req.method === "POST") {
|
|
263
|
-
return handleStreamingMkdirRequest(req, corsHeaders);
|
|
213
|
+
return handleMkdirRequest(sessions, req, corsHeaders);
|
|
264
214
|
}
|
|
265
215
|
break;
|
|
266
216
|
|
|
@@ -270,82 +220,88 @@ const server = serve({
|
|
|
270
220
|
}
|
|
271
221
|
break;
|
|
272
222
|
|
|
273
|
-
case "/api/write/stream":
|
|
274
|
-
if (req.method === "POST") {
|
|
275
|
-
return handleStreamingWriteFileRequest(req, corsHeaders);
|
|
276
|
-
}
|
|
277
|
-
break;
|
|
278
|
-
|
|
279
223
|
case "/api/read":
|
|
280
224
|
if (req.method === "POST") {
|
|
281
225
|
return handleReadFileRequest(req, corsHeaders);
|
|
282
226
|
}
|
|
283
227
|
break;
|
|
284
228
|
|
|
285
|
-
case "/api/read/stream":
|
|
286
|
-
if (req.method === "POST") {
|
|
287
|
-
return handleStreamingReadFileRequest(req, corsHeaders);
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
|
|
291
229
|
case "/api/delete":
|
|
292
230
|
if (req.method === "POST") {
|
|
293
231
|
return handleDeleteFileRequest(req, corsHeaders);
|
|
294
232
|
}
|
|
295
233
|
break;
|
|
296
234
|
|
|
297
|
-
case "/api/
|
|
235
|
+
case "/api/rename":
|
|
298
236
|
if (req.method === "POST") {
|
|
299
|
-
return
|
|
237
|
+
return handleRenameFileRequest(req, corsHeaders);
|
|
300
238
|
}
|
|
301
239
|
break;
|
|
302
240
|
|
|
303
|
-
case "/api/
|
|
241
|
+
case "/api/move":
|
|
304
242
|
if (req.method === "POST") {
|
|
305
|
-
return
|
|
243
|
+
return handleMoveFileRequest(req, corsHeaders);
|
|
306
244
|
}
|
|
307
245
|
break;
|
|
308
246
|
|
|
309
|
-
case "/api/
|
|
247
|
+
case "/api/expose-port":
|
|
310
248
|
if (req.method === "POST") {
|
|
311
|
-
return
|
|
249
|
+
return handleExposePortRequest(exposedPorts, req, corsHeaders);
|
|
312
250
|
}
|
|
313
251
|
break;
|
|
314
252
|
|
|
315
|
-
case "/api/
|
|
316
|
-
if (req.method === "
|
|
317
|
-
return
|
|
253
|
+
case "/api/unexpose-port":
|
|
254
|
+
if (req.method === "DELETE") {
|
|
255
|
+
return handleUnexposePortRequest(exposedPorts, req, corsHeaders);
|
|
318
256
|
}
|
|
319
257
|
break;
|
|
320
258
|
|
|
321
|
-
case "/api/
|
|
322
|
-
if (req.method === "
|
|
323
|
-
return
|
|
259
|
+
case "/api/exposed-ports":
|
|
260
|
+
if (req.method === "GET") {
|
|
261
|
+
return handleGetExposedPortsRequest(exposedPorts, req, corsHeaders);
|
|
324
262
|
}
|
|
325
263
|
break;
|
|
326
264
|
|
|
327
|
-
case "/api/
|
|
265
|
+
case "/api/process/start":
|
|
328
266
|
if (req.method === "POST") {
|
|
329
|
-
return
|
|
267
|
+
return handleStartProcessRequest(processes, req, corsHeaders);
|
|
330
268
|
}
|
|
331
269
|
break;
|
|
332
270
|
|
|
333
|
-
case "/api/
|
|
334
|
-
if (req.method === "
|
|
335
|
-
return
|
|
271
|
+
case "/api/process/list":
|
|
272
|
+
if (req.method === "GET") {
|
|
273
|
+
return handleListProcessesRequest(processes, req, corsHeaders);
|
|
336
274
|
}
|
|
337
275
|
break;
|
|
338
276
|
|
|
339
|
-
case "/api/
|
|
340
|
-
if (req.method === "
|
|
341
|
-
return
|
|
277
|
+
case "/api/process/kill-all":
|
|
278
|
+
if (req.method === "DELETE") {
|
|
279
|
+
return handleKillAllProcessesRequest(processes, req, corsHeaders);
|
|
342
280
|
}
|
|
343
281
|
break;
|
|
344
282
|
|
|
345
283
|
default:
|
|
284
|
+
// Handle dynamic routes for individual processes
|
|
285
|
+
if (pathname.startsWith("/api/process/")) {
|
|
286
|
+
const segments = pathname.split('/');
|
|
287
|
+
if (segments.length >= 4) {
|
|
288
|
+
const processId = segments[3];
|
|
289
|
+
const action = segments[4]; // Optional: logs, stream, etc.
|
|
290
|
+
|
|
291
|
+
if (!action && req.method === "GET") {
|
|
292
|
+
return handleGetProcessRequest(processes, req, corsHeaders, processId);
|
|
293
|
+
} else if (!action && req.method === "DELETE") {
|
|
294
|
+
return handleKillProcessRequest(processes, req, corsHeaders, processId);
|
|
295
|
+
} else if (action === "logs" && req.method === "GET") {
|
|
296
|
+
return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
|
|
297
|
+
} else if (action === "stream" && req.method === "GET") {
|
|
298
|
+
return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
346
302
|
// Check if this is a proxy request for an exposed port
|
|
347
303
|
if (pathname.startsWith("/proxy/")) {
|
|
348
|
-
return handleProxyRequest(req, corsHeaders);
|
|
304
|
+
return handleProxyRequest(exposedPorts, req, corsHeaders);
|
|
349
305
|
}
|
|
350
306
|
|
|
351
307
|
console.log(`[Container] Route not found: ${pathname}`);
|
|
@@ -377,2851 +333,6 @@ const server = serve({
|
|
|
377
333
|
websocket: { async message() { } },
|
|
378
334
|
});
|
|
379
335
|
|
|
380
|
-
async function handleExecuteRequest(
|
|
381
|
-
req: Request,
|
|
382
|
-
corsHeaders: Record<string, string>
|
|
383
|
-
): Promise<Response> {
|
|
384
|
-
try {
|
|
385
|
-
const body = (await req.json()) as ExecuteRequest;
|
|
386
|
-
const { command, args = [], sessionId, background } = body;
|
|
387
|
-
|
|
388
|
-
if (!command || typeof command !== "string") {
|
|
389
|
-
return new Response(
|
|
390
|
-
JSON.stringify({
|
|
391
|
-
error: "Command is required and must be a string",
|
|
392
|
-
}),
|
|
393
|
-
{
|
|
394
|
-
headers: {
|
|
395
|
-
"Content-Type": "application/json",
|
|
396
|
-
...corsHeaders,
|
|
397
|
-
},
|
|
398
|
-
status: 400,
|
|
399
|
-
}
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Basic safety check - prevent dangerous commands
|
|
404
|
-
const dangerousCommands = [
|
|
405
|
-
"rm",
|
|
406
|
-
"rmdir",
|
|
407
|
-
"del",
|
|
408
|
-
"format",
|
|
409
|
-
"shutdown",
|
|
410
|
-
"reboot",
|
|
411
|
-
];
|
|
412
|
-
const lowerCommand = command.toLowerCase();
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
416
|
-
) {
|
|
417
|
-
return new Response(
|
|
418
|
-
JSON.stringify({
|
|
419
|
-
error: "Dangerous command not allowed",
|
|
420
|
-
}),
|
|
421
|
-
{
|
|
422
|
-
headers: {
|
|
423
|
-
"Content-Type": "application/json",
|
|
424
|
-
...corsHeaders,
|
|
425
|
-
},
|
|
426
|
-
status: 400,
|
|
427
|
-
}
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
|
|
432
|
-
|
|
433
|
-
const result = await executeCommand(command, args, sessionId, background);
|
|
434
|
-
|
|
435
|
-
return new Response(
|
|
436
|
-
JSON.stringify({
|
|
437
|
-
args,
|
|
438
|
-
command,
|
|
439
|
-
exitCode: result.exitCode,
|
|
440
|
-
stderr: result.stderr,
|
|
441
|
-
stdout: result.stdout,
|
|
442
|
-
success: result.success,
|
|
443
|
-
timestamp: new Date().toISOString(),
|
|
444
|
-
}),
|
|
445
|
-
{
|
|
446
|
-
headers: {
|
|
447
|
-
"Content-Type": "application/json",
|
|
448
|
-
...corsHeaders,
|
|
449
|
-
},
|
|
450
|
-
}
|
|
451
|
-
);
|
|
452
|
-
} catch (error) {
|
|
453
|
-
console.error("[Server] Error in handleExecuteRequest:", error);
|
|
454
|
-
return new Response(
|
|
455
|
-
JSON.stringify({
|
|
456
|
-
error: "Failed to execute command",
|
|
457
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
458
|
-
}),
|
|
459
|
-
{
|
|
460
|
-
headers: {
|
|
461
|
-
"Content-Type": "application/json",
|
|
462
|
-
...corsHeaders,
|
|
463
|
-
},
|
|
464
|
-
status: 500,
|
|
465
|
-
}
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function handleStreamingExecuteRequest(
|
|
471
|
-
req: Request,
|
|
472
|
-
corsHeaders: Record<string, string>
|
|
473
|
-
): Promise<Response> {
|
|
474
|
-
try {
|
|
475
|
-
const body = (await req.json()) as ExecuteRequest;
|
|
476
|
-
const { command, args = [], sessionId, background } = body;
|
|
477
|
-
|
|
478
|
-
if (!command || typeof command !== "string") {
|
|
479
|
-
return new Response(
|
|
480
|
-
JSON.stringify({
|
|
481
|
-
error: "Command is required and must be a string",
|
|
482
|
-
}),
|
|
483
|
-
{
|
|
484
|
-
headers: {
|
|
485
|
-
"Content-Type": "application/json",
|
|
486
|
-
...corsHeaders,
|
|
487
|
-
},
|
|
488
|
-
status: 400,
|
|
489
|
-
}
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Basic safety check - prevent dangerous commands
|
|
494
|
-
const dangerousCommands = [
|
|
495
|
-
"rm",
|
|
496
|
-
"rmdir",
|
|
497
|
-
"del",
|
|
498
|
-
"format",
|
|
499
|
-
"shutdown",
|
|
500
|
-
"reboot",
|
|
501
|
-
];
|
|
502
|
-
const lowerCommand = command.toLowerCase();
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
506
|
-
) {
|
|
507
|
-
return new Response(
|
|
508
|
-
JSON.stringify({
|
|
509
|
-
error: "Dangerous command not allowed",
|
|
510
|
-
}),
|
|
511
|
-
{
|
|
512
|
-
headers: {
|
|
513
|
-
"Content-Type": "application/json",
|
|
514
|
-
...corsHeaders,
|
|
515
|
-
},
|
|
516
|
-
status: 400,
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
console.log(
|
|
522
|
-
`[Server] Executing streaming command: ${command} ${args.join(" ")}`
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
const stream = new ReadableStream({
|
|
526
|
-
start(controller) {
|
|
527
|
-
const spawnOptions: SpawnOptions = {
|
|
528
|
-
shell: true,
|
|
529
|
-
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
530
|
-
detached: background || false,
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
const child = spawn(command, args, spawnOptions);
|
|
534
|
-
|
|
535
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
536
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
537
|
-
const session = sessions.get(sessionId)!;
|
|
538
|
-
session.activeProcess = child;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// For background processes, unref to prevent blocking
|
|
542
|
-
if (background) {
|
|
543
|
-
child.unref();
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
let stdout = "";
|
|
547
|
-
let stderr = "";
|
|
548
|
-
|
|
549
|
-
// Send command start event
|
|
550
|
-
controller.enqueue(
|
|
551
|
-
new TextEncoder().encode(
|
|
552
|
-
`data: ${JSON.stringify({
|
|
553
|
-
args,
|
|
554
|
-
command,
|
|
555
|
-
timestamp: new Date().toISOString(),
|
|
556
|
-
type: "command_start",
|
|
557
|
-
background: background || false,
|
|
558
|
-
})}\n\n`
|
|
559
|
-
)
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
child.stdout?.on("data", (data) => {
|
|
563
|
-
const output = data.toString();
|
|
564
|
-
stdout += output;
|
|
565
|
-
|
|
566
|
-
// Send real-time output
|
|
567
|
-
controller.enqueue(
|
|
568
|
-
new TextEncoder().encode(
|
|
569
|
-
`data: ${JSON.stringify({
|
|
570
|
-
command,
|
|
571
|
-
data: output,
|
|
572
|
-
stream: "stdout",
|
|
573
|
-
type: "output",
|
|
574
|
-
})}\n\n`
|
|
575
|
-
)
|
|
576
|
-
);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
child.stderr?.on("data", (data) => {
|
|
580
|
-
const output = data.toString();
|
|
581
|
-
stderr += output;
|
|
582
|
-
|
|
583
|
-
// Send real-time error output
|
|
584
|
-
controller.enqueue(
|
|
585
|
-
new TextEncoder().encode(
|
|
586
|
-
`data: ${JSON.stringify({
|
|
587
|
-
command,
|
|
588
|
-
data: output,
|
|
589
|
-
stream: "stderr",
|
|
590
|
-
type: "output",
|
|
591
|
-
})}\n\n`
|
|
592
|
-
)
|
|
593
|
-
);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
child.on("close", (code) => {
|
|
597
|
-
// Clear the active process reference
|
|
598
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
599
|
-
const session = sessions.get(sessionId)!;
|
|
600
|
-
session.activeProcess = null;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
console.log(
|
|
604
|
-
`[Server] Command completed: ${command}, Exit code: ${code}`
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
// Send command completion event
|
|
608
|
-
controller.enqueue(
|
|
609
|
-
new TextEncoder().encode(
|
|
610
|
-
`data: ${JSON.stringify({
|
|
611
|
-
args,
|
|
612
|
-
command,
|
|
613
|
-
exitCode: code,
|
|
614
|
-
stderr,
|
|
615
|
-
stdout,
|
|
616
|
-
success: code === 0,
|
|
617
|
-
timestamp: new Date().toISOString(),
|
|
618
|
-
type: "command_complete",
|
|
619
|
-
})}\n\n`
|
|
620
|
-
)
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
// For non-background processes, close the stream
|
|
624
|
-
// For background processes with streaming, the stream stays open
|
|
625
|
-
if (!background) {
|
|
626
|
-
controller.close();
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
child.on("error", (error) => {
|
|
631
|
-
// Clear the active process reference
|
|
632
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
633
|
-
const session = sessions.get(sessionId)!;
|
|
634
|
-
session.activeProcess = null;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
controller.enqueue(
|
|
638
|
-
new TextEncoder().encode(
|
|
639
|
-
`data: ${JSON.stringify({
|
|
640
|
-
args,
|
|
641
|
-
command,
|
|
642
|
-
error: error.message,
|
|
643
|
-
type: "error",
|
|
644
|
-
})}\n\n`
|
|
645
|
-
)
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
controller.close();
|
|
649
|
-
});
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
return new Response(stream, {
|
|
654
|
-
headers: {
|
|
655
|
-
"Cache-Control": "no-cache",
|
|
656
|
-
Connection: "keep-alive",
|
|
657
|
-
"Content-Type": "text/event-stream",
|
|
658
|
-
...corsHeaders,
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
} catch (error) {
|
|
662
|
-
console.error("[Server] Error in handleStreamingExecuteRequest:", error);
|
|
663
|
-
return new Response(
|
|
664
|
-
JSON.stringify({
|
|
665
|
-
error: "Failed to execute streaming command",
|
|
666
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
667
|
-
}),
|
|
668
|
-
{
|
|
669
|
-
headers: {
|
|
670
|
-
"Content-Type": "application/json",
|
|
671
|
-
...corsHeaders,
|
|
672
|
-
},
|
|
673
|
-
status: 500,
|
|
674
|
-
}
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function handleGitCheckoutRequest(
|
|
680
|
-
req: Request,
|
|
681
|
-
corsHeaders: Record<string, string>
|
|
682
|
-
): Promise<Response> {
|
|
683
|
-
try {
|
|
684
|
-
const body = (await req.json()) as GitCheckoutRequest;
|
|
685
|
-
const { repoUrl, branch = "main", targetDir, sessionId } = body;
|
|
686
|
-
|
|
687
|
-
if (!repoUrl || typeof repoUrl !== "string") {
|
|
688
|
-
return new Response(
|
|
689
|
-
JSON.stringify({
|
|
690
|
-
error: "Repository URL is required and must be a string",
|
|
691
|
-
}),
|
|
692
|
-
{
|
|
693
|
-
headers: {
|
|
694
|
-
"Content-Type": "application/json",
|
|
695
|
-
...corsHeaders,
|
|
696
|
-
},
|
|
697
|
-
status: 400,
|
|
698
|
-
}
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Validate repository URL format
|
|
703
|
-
const urlPattern =
|
|
704
|
-
/^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
|
|
705
|
-
if (!urlPattern.test(repoUrl)) {
|
|
706
|
-
return new Response(
|
|
707
|
-
JSON.stringify({
|
|
708
|
-
error: "Invalid repository URL format",
|
|
709
|
-
}),
|
|
710
|
-
{
|
|
711
|
-
headers: {
|
|
712
|
-
"Content-Type": "application/json",
|
|
713
|
-
...corsHeaders,
|
|
714
|
-
},
|
|
715
|
-
status: 400,
|
|
716
|
-
}
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Generate target directory if not provided
|
|
721
|
-
const checkoutDir =
|
|
722
|
-
targetDir ||
|
|
723
|
-
`repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
724
|
-
|
|
725
|
-
console.log(
|
|
726
|
-
`[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
|
|
727
|
-
);
|
|
728
|
-
|
|
729
|
-
const result = await executeGitCheckout(
|
|
730
|
-
repoUrl,
|
|
731
|
-
branch,
|
|
732
|
-
checkoutDir,
|
|
733
|
-
sessionId
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
return new Response(
|
|
737
|
-
JSON.stringify({
|
|
738
|
-
branch,
|
|
739
|
-
exitCode: result.exitCode,
|
|
740
|
-
repoUrl,
|
|
741
|
-
stderr: result.stderr,
|
|
742
|
-
stdout: result.stdout,
|
|
743
|
-
success: result.success,
|
|
744
|
-
targetDir: checkoutDir,
|
|
745
|
-
timestamp: new Date().toISOString(),
|
|
746
|
-
}),
|
|
747
|
-
{
|
|
748
|
-
headers: {
|
|
749
|
-
"Content-Type": "application/json",
|
|
750
|
-
...corsHeaders,
|
|
751
|
-
},
|
|
752
|
-
}
|
|
753
|
-
);
|
|
754
|
-
} catch (error) {
|
|
755
|
-
console.error("[Server] Error in handleGitCheckoutRequest:", error);
|
|
756
|
-
return new Response(
|
|
757
|
-
JSON.stringify({
|
|
758
|
-
error: "Failed to checkout repository",
|
|
759
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
760
|
-
}),
|
|
761
|
-
{
|
|
762
|
-
headers: {
|
|
763
|
-
"Content-Type": "application/json",
|
|
764
|
-
...corsHeaders,
|
|
765
|
-
},
|
|
766
|
-
status: 500,
|
|
767
|
-
}
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async function handleStreamingGitCheckoutRequest(
|
|
773
|
-
req: Request,
|
|
774
|
-
corsHeaders: Record<string, string>
|
|
775
|
-
): Promise<Response> {
|
|
776
|
-
try {
|
|
777
|
-
const body = (await req.json()) as GitCheckoutRequest;
|
|
778
|
-
const { repoUrl, branch = "main", targetDir, sessionId } = body;
|
|
779
|
-
|
|
780
|
-
if (!repoUrl || typeof repoUrl !== "string") {
|
|
781
|
-
return new Response(
|
|
782
|
-
JSON.stringify({
|
|
783
|
-
error: "Repository URL is required and must be a string",
|
|
784
|
-
}),
|
|
785
|
-
{
|
|
786
|
-
headers: {
|
|
787
|
-
"Content-Type": "application/json",
|
|
788
|
-
...corsHeaders,
|
|
789
|
-
},
|
|
790
|
-
status: 400,
|
|
791
|
-
}
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Validate repository URL format
|
|
796
|
-
const urlPattern =
|
|
797
|
-
/^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
|
|
798
|
-
if (!urlPattern.test(repoUrl)) {
|
|
799
|
-
return new Response(
|
|
800
|
-
JSON.stringify({
|
|
801
|
-
error: "Invalid repository URL format",
|
|
802
|
-
}),
|
|
803
|
-
{
|
|
804
|
-
headers: {
|
|
805
|
-
"Content-Type": "application/json",
|
|
806
|
-
...corsHeaders,
|
|
807
|
-
},
|
|
808
|
-
status: 400,
|
|
809
|
-
}
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Generate target directory if not provided
|
|
814
|
-
const checkoutDir =
|
|
815
|
-
targetDir ||
|
|
816
|
-
`repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
817
|
-
|
|
818
|
-
console.log(
|
|
819
|
-
`[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
const stream = new ReadableStream({
|
|
823
|
-
start(controller) {
|
|
824
|
-
const child = spawn(
|
|
825
|
-
"git",
|
|
826
|
-
["clone", "-b", branch, repoUrl, checkoutDir],
|
|
827
|
-
{
|
|
828
|
-
shell: true,
|
|
829
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
830
|
-
}
|
|
831
|
-
);
|
|
832
|
-
|
|
833
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
834
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
835
|
-
const session = sessions.get(sessionId)!;
|
|
836
|
-
session.activeProcess = child;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
let stdout = "";
|
|
840
|
-
let stderr = "";
|
|
841
|
-
|
|
842
|
-
// Send command start event
|
|
843
|
-
controller.enqueue(
|
|
844
|
-
new TextEncoder().encode(
|
|
845
|
-
`data: ${JSON.stringify({
|
|
846
|
-
args: [branch, repoUrl, checkoutDir],
|
|
847
|
-
command: "git clone",
|
|
848
|
-
timestamp: new Date().toISOString(),
|
|
849
|
-
type: "command_start",
|
|
850
|
-
})}\n\n`
|
|
851
|
-
)
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
child.stdout?.on("data", (data) => {
|
|
855
|
-
const output = data.toString();
|
|
856
|
-
stdout += output;
|
|
857
|
-
|
|
858
|
-
// Send real-time output
|
|
859
|
-
controller.enqueue(
|
|
860
|
-
new TextEncoder().encode(
|
|
861
|
-
`data: ${JSON.stringify({
|
|
862
|
-
command: "git clone",
|
|
863
|
-
data: output,
|
|
864
|
-
stream: "stdout",
|
|
865
|
-
type: "output",
|
|
866
|
-
})}\n\n`
|
|
867
|
-
)
|
|
868
|
-
);
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
child.stderr?.on("data", (data) => {
|
|
872
|
-
const output = data.toString();
|
|
873
|
-
stderr += output;
|
|
874
|
-
|
|
875
|
-
// Send real-time error output
|
|
876
|
-
controller.enqueue(
|
|
877
|
-
new TextEncoder().encode(
|
|
878
|
-
`data: ${JSON.stringify({
|
|
879
|
-
command: "git clone",
|
|
880
|
-
data: output,
|
|
881
|
-
stream: "stderr",
|
|
882
|
-
type: "output",
|
|
883
|
-
})}\n\n`
|
|
884
|
-
)
|
|
885
|
-
);
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
child.on("close", (code) => {
|
|
889
|
-
// Clear the active process reference
|
|
890
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
891
|
-
const session = sessions.get(sessionId)!;
|
|
892
|
-
session.activeProcess = null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
console.log(
|
|
896
|
-
`[Server] Command completed: git clone, Exit code: ${code}`
|
|
897
|
-
);
|
|
898
|
-
|
|
899
|
-
// Send command completion event
|
|
900
|
-
controller.enqueue(
|
|
901
|
-
new TextEncoder().encode(
|
|
902
|
-
`data: ${JSON.stringify({
|
|
903
|
-
args: [branch, repoUrl, checkoutDir],
|
|
904
|
-
command: "git clone",
|
|
905
|
-
exitCode: code,
|
|
906
|
-
stderr,
|
|
907
|
-
stdout,
|
|
908
|
-
success: code === 0,
|
|
909
|
-
timestamp: new Date().toISOString(),
|
|
910
|
-
type: "command_complete",
|
|
911
|
-
})}\n\n`
|
|
912
|
-
)
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
controller.close();
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
child.on("error", (error) => {
|
|
919
|
-
// Clear the active process reference
|
|
920
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
921
|
-
const session = sessions.get(sessionId)!;
|
|
922
|
-
session.activeProcess = null;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
controller.enqueue(
|
|
926
|
-
new TextEncoder().encode(
|
|
927
|
-
`data: ${JSON.stringify({
|
|
928
|
-
args: [branch, repoUrl, checkoutDir],
|
|
929
|
-
command: "git clone",
|
|
930
|
-
error: error.message,
|
|
931
|
-
type: "error",
|
|
932
|
-
})}\n\n`
|
|
933
|
-
)
|
|
934
|
-
);
|
|
935
|
-
|
|
936
|
-
controller.close();
|
|
937
|
-
});
|
|
938
|
-
},
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
return new Response(stream, {
|
|
942
|
-
headers: {
|
|
943
|
-
"Cache-Control": "no-cache",
|
|
944
|
-
Connection: "keep-alive",
|
|
945
|
-
"Content-Type": "text/event-stream",
|
|
946
|
-
...corsHeaders,
|
|
947
|
-
},
|
|
948
|
-
});
|
|
949
|
-
} catch (error) {
|
|
950
|
-
console.error(
|
|
951
|
-
"[Server] Error in handleStreamingGitCheckoutRequest:",
|
|
952
|
-
error
|
|
953
|
-
);
|
|
954
|
-
return new Response(
|
|
955
|
-
JSON.stringify({
|
|
956
|
-
error: "Failed to checkout repository",
|
|
957
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
958
|
-
}),
|
|
959
|
-
{
|
|
960
|
-
headers: {
|
|
961
|
-
"Content-Type": "application/json",
|
|
962
|
-
...corsHeaders,
|
|
963
|
-
},
|
|
964
|
-
status: 500,
|
|
965
|
-
}
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
async function handleMkdirRequest(
|
|
971
|
-
req: Request,
|
|
972
|
-
corsHeaders: Record<string, string>
|
|
973
|
-
): Promise<Response> {
|
|
974
|
-
try {
|
|
975
|
-
const body = (await req.json()) as MkdirRequest;
|
|
976
|
-
const { path, recursive = false, sessionId } = body;
|
|
977
|
-
|
|
978
|
-
if (!path || typeof path !== "string") {
|
|
979
|
-
return new Response(
|
|
980
|
-
JSON.stringify({
|
|
981
|
-
error: "Path is required and must be a string",
|
|
982
|
-
}),
|
|
983
|
-
{
|
|
984
|
-
headers: {
|
|
985
|
-
"Content-Type": "application/json",
|
|
986
|
-
...corsHeaders,
|
|
987
|
-
},
|
|
988
|
-
status: 400,
|
|
989
|
-
}
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Basic safety check - prevent dangerous paths
|
|
994
|
-
const dangerousPatterns = [
|
|
995
|
-
/^\/$/, // Root directory
|
|
996
|
-
/^\/etc/, // System directories
|
|
997
|
-
/^\/var/, // System directories
|
|
998
|
-
/^\/usr/, // System directories
|
|
999
|
-
/^\/bin/, // System directories
|
|
1000
|
-
/^\/sbin/, // System directories
|
|
1001
|
-
/^\/boot/, // System directories
|
|
1002
|
-
/^\/dev/, // System directories
|
|
1003
|
-
/^\/proc/, // System directories
|
|
1004
|
-
/^\/sys/, // System directories
|
|
1005
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1006
|
-
/\.\./, // Path traversal attempts
|
|
1007
|
-
];
|
|
1008
|
-
|
|
1009
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1010
|
-
return new Response(
|
|
1011
|
-
JSON.stringify({
|
|
1012
|
-
error: "Dangerous path not allowed",
|
|
1013
|
-
}),
|
|
1014
|
-
{
|
|
1015
|
-
headers: {
|
|
1016
|
-
"Content-Type": "application/json",
|
|
1017
|
-
...corsHeaders,
|
|
1018
|
-
},
|
|
1019
|
-
status: 400,
|
|
1020
|
-
}
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
console.log(
|
|
1025
|
-
`[Server] Creating directory: ${path} (recursive: ${recursive})`
|
|
1026
|
-
);
|
|
1027
|
-
|
|
1028
|
-
const result = await executeMkdir(path, recursive, sessionId);
|
|
1029
|
-
|
|
1030
|
-
return new Response(
|
|
1031
|
-
JSON.stringify({
|
|
1032
|
-
exitCode: result.exitCode,
|
|
1033
|
-
path,
|
|
1034
|
-
recursive,
|
|
1035
|
-
stderr: result.stderr,
|
|
1036
|
-
stdout: result.stdout,
|
|
1037
|
-
success: result.success,
|
|
1038
|
-
timestamp: new Date().toISOString(),
|
|
1039
|
-
}),
|
|
1040
|
-
{
|
|
1041
|
-
headers: {
|
|
1042
|
-
"Content-Type": "application/json",
|
|
1043
|
-
...corsHeaders,
|
|
1044
|
-
},
|
|
1045
|
-
}
|
|
1046
|
-
);
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
console.error("[Server] Error in handleMkdirRequest:", error);
|
|
1049
|
-
return new Response(
|
|
1050
|
-
JSON.stringify({
|
|
1051
|
-
error: "Failed to create directory",
|
|
1052
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1053
|
-
}),
|
|
1054
|
-
{
|
|
1055
|
-
headers: {
|
|
1056
|
-
"Content-Type": "application/json",
|
|
1057
|
-
...corsHeaders,
|
|
1058
|
-
},
|
|
1059
|
-
status: 500,
|
|
1060
|
-
}
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
async function handleStreamingMkdirRequest(
|
|
1066
|
-
req: Request,
|
|
1067
|
-
corsHeaders: Record<string, string>
|
|
1068
|
-
): Promise<Response> {
|
|
1069
|
-
try {
|
|
1070
|
-
const body = (await req.json()) as MkdirRequest;
|
|
1071
|
-
const { path, recursive = false, sessionId } = body;
|
|
1072
|
-
|
|
1073
|
-
if (!path || typeof path !== "string") {
|
|
1074
|
-
return new Response(
|
|
1075
|
-
JSON.stringify({
|
|
1076
|
-
error: "Path is required and must be a string",
|
|
1077
|
-
}),
|
|
1078
|
-
{
|
|
1079
|
-
headers: {
|
|
1080
|
-
"Content-Type": "application/json",
|
|
1081
|
-
...corsHeaders,
|
|
1082
|
-
},
|
|
1083
|
-
status: 400,
|
|
1084
|
-
}
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// Basic safety check - prevent dangerous paths
|
|
1089
|
-
const dangerousPatterns = [
|
|
1090
|
-
/^\/$/, // Root directory
|
|
1091
|
-
/^\/etc/, // System directories
|
|
1092
|
-
/^\/var/, // System directories
|
|
1093
|
-
/^\/usr/, // System directories
|
|
1094
|
-
/^\/bin/, // System directories
|
|
1095
|
-
/^\/sbin/, // System directories
|
|
1096
|
-
/^\/boot/, // System directories
|
|
1097
|
-
/^\/dev/, // System directories
|
|
1098
|
-
/^\/proc/, // System directories
|
|
1099
|
-
/^\/sys/, // System directories
|
|
1100
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1101
|
-
/\.\./, // Path traversal attempts
|
|
1102
|
-
];
|
|
1103
|
-
|
|
1104
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1105
|
-
return new Response(
|
|
1106
|
-
JSON.stringify({
|
|
1107
|
-
error: "Dangerous path not allowed",
|
|
1108
|
-
}),
|
|
1109
|
-
{
|
|
1110
|
-
headers: {
|
|
1111
|
-
"Content-Type": "application/json",
|
|
1112
|
-
...corsHeaders,
|
|
1113
|
-
},
|
|
1114
|
-
status: 400,
|
|
1115
|
-
}
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
console.log(
|
|
1120
|
-
`[Server] Creating directory: ${path} (recursive: ${recursive})`
|
|
1121
|
-
);
|
|
1122
|
-
|
|
1123
|
-
const stream = new ReadableStream({
|
|
1124
|
-
start(controller) {
|
|
1125
|
-
const args = recursive ? ["-p", path] : [path];
|
|
1126
|
-
const child = spawn("mkdir", args, {
|
|
1127
|
-
shell: true,
|
|
1128
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
1132
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1133
|
-
const session = sessions.get(sessionId)!;
|
|
1134
|
-
session.activeProcess = child;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
let stdout = "";
|
|
1138
|
-
let stderr = "";
|
|
1139
|
-
|
|
1140
|
-
// Send command start event
|
|
1141
|
-
controller.enqueue(
|
|
1142
|
-
new TextEncoder().encode(
|
|
1143
|
-
`data: ${JSON.stringify({
|
|
1144
|
-
args,
|
|
1145
|
-
command: "mkdir",
|
|
1146
|
-
timestamp: new Date().toISOString(),
|
|
1147
|
-
type: "command_start",
|
|
1148
|
-
})}\n\n`
|
|
1149
|
-
)
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
child.stdout?.on("data", (data) => {
|
|
1153
|
-
const output = data.toString();
|
|
1154
|
-
stdout += output;
|
|
1155
|
-
|
|
1156
|
-
// Send real-time output
|
|
1157
|
-
controller.enqueue(
|
|
1158
|
-
new TextEncoder().encode(
|
|
1159
|
-
`data: ${JSON.stringify({
|
|
1160
|
-
command: "mkdir",
|
|
1161
|
-
data: output,
|
|
1162
|
-
stream: "stdout",
|
|
1163
|
-
type: "output",
|
|
1164
|
-
})}\n\n`
|
|
1165
|
-
)
|
|
1166
|
-
);
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
child.stderr?.on("data", (data) => {
|
|
1170
|
-
const output = data.toString();
|
|
1171
|
-
stderr += output;
|
|
1172
|
-
|
|
1173
|
-
// Send real-time error output
|
|
1174
|
-
controller.enqueue(
|
|
1175
|
-
new TextEncoder().encode(
|
|
1176
|
-
`data: ${JSON.stringify({
|
|
1177
|
-
command: "mkdir",
|
|
1178
|
-
data: output,
|
|
1179
|
-
stream: "stderr",
|
|
1180
|
-
type: "output",
|
|
1181
|
-
})}\n\n`
|
|
1182
|
-
)
|
|
1183
|
-
);
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
child.on("close", (code) => {
|
|
1187
|
-
// Clear the active process reference
|
|
1188
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1189
|
-
const session = sessions.get(sessionId)!;
|
|
1190
|
-
session.activeProcess = null;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
console.log(`[Server] Command completed: mkdir, Exit code: ${code}`);
|
|
1194
|
-
|
|
1195
|
-
// Send command completion event
|
|
1196
|
-
controller.enqueue(
|
|
1197
|
-
new TextEncoder().encode(
|
|
1198
|
-
`data: ${JSON.stringify({
|
|
1199
|
-
args,
|
|
1200
|
-
command: "mkdir",
|
|
1201
|
-
exitCode: code,
|
|
1202
|
-
stderr,
|
|
1203
|
-
stdout,
|
|
1204
|
-
success: code === 0,
|
|
1205
|
-
timestamp: new Date().toISOString(),
|
|
1206
|
-
type: "command_complete",
|
|
1207
|
-
})}\n\n`
|
|
1208
|
-
)
|
|
1209
|
-
);
|
|
1210
|
-
|
|
1211
|
-
controller.close();
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
child.on("error", (error) => {
|
|
1215
|
-
// Clear the active process reference
|
|
1216
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
1217
|
-
const session = sessions.get(sessionId)!;
|
|
1218
|
-
session.activeProcess = null;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
controller.enqueue(
|
|
1222
|
-
new TextEncoder().encode(
|
|
1223
|
-
`data: ${JSON.stringify({
|
|
1224
|
-
args,
|
|
1225
|
-
command: "mkdir",
|
|
1226
|
-
error: error.message,
|
|
1227
|
-
type: "error",
|
|
1228
|
-
})}\n\n`
|
|
1229
|
-
)
|
|
1230
|
-
);
|
|
1231
|
-
|
|
1232
|
-
controller.close();
|
|
1233
|
-
});
|
|
1234
|
-
},
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
return new Response(stream, {
|
|
1238
|
-
headers: {
|
|
1239
|
-
"Cache-Control": "no-cache",
|
|
1240
|
-
Connection: "keep-alive",
|
|
1241
|
-
"Content-Type": "text/event-stream",
|
|
1242
|
-
...corsHeaders,
|
|
1243
|
-
},
|
|
1244
|
-
});
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
console.error("[Server] Error in handleStreamingMkdirRequest:", error);
|
|
1247
|
-
return new Response(
|
|
1248
|
-
JSON.stringify({
|
|
1249
|
-
error: "Failed to create directory",
|
|
1250
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1251
|
-
}),
|
|
1252
|
-
{
|
|
1253
|
-
headers: {
|
|
1254
|
-
"Content-Type": "application/json",
|
|
1255
|
-
...corsHeaders,
|
|
1256
|
-
},
|
|
1257
|
-
status: 500,
|
|
1258
|
-
}
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
async function handleWriteFileRequest(
|
|
1264
|
-
req: Request,
|
|
1265
|
-
corsHeaders: Record<string, string>
|
|
1266
|
-
): Promise<Response> {
|
|
1267
|
-
try {
|
|
1268
|
-
const body = (await req.json()) as WriteFileRequest;
|
|
1269
|
-
const { path, content, encoding = "utf-8", sessionId } = body;
|
|
1270
|
-
|
|
1271
|
-
if (!path || typeof path !== "string") {
|
|
1272
|
-
return new Response(
|
|
1273
|
-
JSON.stringify({
|
|
1274
|
-
error: "Path is required and must be a string",
|
|
1275
|
-
}),
|
|
1276
|
-
{
|
|
1277
|
-
headers: {
|
|
1278
|
-
"Content-Type": "application/json",
|
|
1279
|
-
...corsHeaders,
|
|
1280
|
-
},
|
|
1281
|
-
status: 400,
|
|
1282
|
-
}
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Basic safety check - prevent dangerous paths
|
|
1287
|
-
const dangerousPatterns = [
|
|
1288
|
-
/^\/$/, // Root directory
|
|
1289
|
-
/^\/etc/, // System directories
|
|
1290
|
-
/^\/var/, // System directories
|
|
1291
|
-
/^\/usr/, // System directories
|
|
1292
|
-
/^\/bin/, // System directories
|
|
1293
|
-
/^\/sbin/, // System directories
|
|
1294
|
-
/^\/boot/, // System directories
|
|
1295
|
-
/^\/dev/, // System directories
|
|
1296
|
-
/^\/proc/, // System directories
|
|
1297
|
-
/^\/sys/, // System directories
|
|
1298
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1299
|
-
/\.\./, // Path traversal attempts
|
|
1300
|
-
];
|
|
1301
|
-
|
|
1302
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1303
|
-
return new Response(
|
|
1304
|
-
JSON.stringify({
|
|
1305
|
-
error: "Dangerous path not allowed",
|
|
1306
|
-
}),
|
|
1307
|
-
{
|
|
1308
|
-
headers: {
|
|
1309
|
-
"Content-Type": "application/json",
|
|
1310
|
-
...corsHeaders,
|
|
1311
|
-
},
|
|
1312
|
-
status: 400,
|
|
1313
|
-
}
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
console.log(
|
|
1318
|
-
`[Server] Writing file: ${path} (content length: ${content.length})`
|
|
1319
|
-
);
|
|
1320
|
-
|
|
1321
|
-
const result = await executeWriteFile(path, content, encoding, sessionId);
|
|
1322
|
-
|
|
1323
|
-
return new Response(
|
|
1324
|
-
JSON.stringify({
|
|
1325
|
-
exitCode: result.exitCode,
|
|
1326
|
-
path,
|
|
1327
|
-
success: result.success,
|
|
1328
|
-
timestamp: new Date().toISOString(),
|
|
1329
|
-
}),
|
|
1330
|
-
{
|
|
1331
|
-
headers: {
|
|
1332
|
-
"Content-Type": "application/json",
|
|
1333
|
-
...corsHeaders,
|
|
1334
|
-
},
|
|
1335
|
-
}
|
|
1336
|
-
);
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
console.error("[Server] Error in handleWriteFileRequest:", error);
|
|
1339
|
-
return new Response(
|
|
1340
|
-
JSON.stringify({
|
|
1341
|
-
error: "Failed to write file",
|
|
1342
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1343
|
-
}),
|
|
1344
|
-
{
|
|
1345
|
-
headers: {
|
|
1346
|
-
"Content-Type": "application/json",
|
|
1347
|
-
...corsHeaders,
|
|
1348
|
-
},
|
|
1349
|
-
status: 500,
|
|
1350
|
-
}
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
async function handleStreamingWriteFileRequest(
|
|
1356
|
-
req: Request,
|
|
1357
|
-
corsHeaders: Record<string, string>
|
|
1358
|
-
): Promise<Response> {
|
|
1359
|
-
try {
|
|
1360
|
-
const body = (await req.json()) as WriteFileRequest;
|
|
1361
|
-
const { path, content, encoding = "utf-8", sessionId } = body;
|
|
1362
|
-
|
|
1363
|
-
if (!path || typeof path !== "string") {
|
|
1364
|
-
return new Response(
|
|
1365
|
-
JSON.stringify({
|
|
1366
|
-
error: "Path is required and must be a string",
|
|
1367
|
-
}),
|
|
1368
|
-
{
|
|
1369
|
-
headers: {
|
|
1370
|
-
"Content-Type": "application/json",
|
|
1371
|
-
...corsHeaders,
|
|
1372
|
-
},
|
|
1373
|
-
status: 400,
|
|
1374
|
-
}
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
// Basic safety check - prevent dangerous paths
|
|
1379
|
-
const dangerousPatterns = [
|
|
1380
|
-
/^\/$/, // Root directory
|
|
1381
|
-
/^\/etc/, // System directories
|
|
1382
|
-
/^\/var/, // System directories
|
|
1383
|
-
/^\/usr/, // System directories
|
|
1384
|
-
/^\/bin/, // System directories
|
|
1385
|
-
/^\/sbin/, // System directories
|
|
1386
|
-
/^\/boot/, // System directories
|
|
1387
|
-
/^\/dev/, // System directories
|
|
1388
|
-
/^\/proc/, // System directories
|
|
1389
|
-
/^\/sys/, // System directories
|
|
1390
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1391
|
-
/\.\./, // Path traversal attempts
|
|
1392
|
-
];
|
|
1393
|
-
|
|
1394
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1395
|
-
return new Response(
|
|
1396
|
-
JSON.stringify({
|
|
1397
|
-
error: "Dangerous path not allowed",
|
|
1398
|
-
}),
|
|
1399
|
-
{
|
|
1400
|
-
headers: {
|
|
1401
|
-
"Content-Type": "application/json",
|
|
1402
|
-
...corsHeaders,
|
|
1403
|
-
},
|
|
1404
|
-
status: 400,
|
|
1405
|
-
}
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
console.log(
|
|
1410
|
-
`[Server] Writing file (streaming): ${path} (content length: ${content.length})`
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
const stream = new ReadableStream({
|
|
1414
|
-
start(controller) {
|
|
1415
|
-
(async () => {
|
|
1416
|
-
try {
|
|
1417
|
-
// Send command start event
|
|
1418
|
-
controller.enqueue(
|
|
1419
|
-
new TextEncoder().encode(
|
|
1420
|
-
`data: ${JSON.stringify({
|
|
1421
|
-
path,
|
|
1422
|
-
timestamp: new Date().toISOString(),
|
|
1423
|
-
type: "command_start",
|
|
1424
|
-
})}\n\n`
|
|
1425
|
-
)
|
|
1426
|
-
);
|
|
1427
|
-
|
|
1428
|
-
// Ensure the directory exists
|
|
1429
|
-
const dir = dirname(path);
|
|
1430
|
-
if (dir !== ".") {
|
|
1431
|
-
await mkdir(dir, { recursive: true });
|
|
1432
|
-
|
|
1433
|
-
// Send directory creation event
|
|
1434
|
-
controller.enqueue(
|
|
1435
|
-
new TextEncoder().encode(
|
|
1436
|
-
`data: ${JSON.stringify({
|
|
1437
|
-
message: `Created directory: ${dir}`,
|
|
1438
|
-
type: "output",
|
|
1439
|
-
})}\n\n`
|
|
1440
|
-
)
|
|
1441
|
-
);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Write the file
|
|
1445
|
-
await writeFile(path, content, {
|
|
1446
|
-
encoding: encoding as BufferEncoding,
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
console.log(`[Server] File written successfully: ${path}`);
|
|
1450
|
-
|
|
1451
|
-
// Send command completion event
|
|
1452
|
-
controller.enqueue(
|
|
1453
|
-
new TextEncoder().encode(
|
|
1454
|
-
`data: ${JSON.stringify({
|
|
1455
|
-
path,
|
|
1456
|
-
success: true,
|
|
1457
|
-
timestamp: new Date().toISOString(),
|
|
1458
|
-
type: "command_complete",
|
|
1459
|
-
})}\n\n`
|
|
1460
|
-
)
|
|
1461
|
-
);
|
|
1462
|
-
|
|
1463
|
-
controller.close();
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
console.error(`[Server] Error writing file: ${path}`, error);
|
|
1466
|
-
|
|
1467
|
-
controller.enqueue(
|
|
1468
|
-
new TextEncoder().encode(
|
|
1469
|
-
`data: ${JSON.stringify({
|
|
1470
|
-
error:
|
|
1471
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1472
|
-
path,
|
|
1473
|
-
type: "error",
|
|
1474
|
-
})}\n\n`
|
|
1475
|
-
)
|
|
1476
|
-
);
|
|
1477
|
-
|
|
1478
|
-
controller.close();
|
|
1479
|
-
}
|
|
1480
|
-
})();
|
|
1481
|
-
},
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
return new Response(stream, {
|
|
1485
|
-
headers: {
|
|
1486
|
-
"Cache-Control": "no-cache",
|
|
1487
|
-
Connection: "keep-alive",
|
|
1488
|
-
"Content-Type": "text/event-stream",
|
|
1489
|
-
...corsHeaders,
|
|
1490
|
-
},
|
|
1491
|
-
});
|
|
1492
|
-
} catch (error) {
|
|
1493
|
-
console.error("[Server] Error in handleStreamingWriteFileRequest:", error);
|
|
1494
|
-
return new Response(
|
|
1495
|
-
JSON.stringify({
|
|
1496
|
-
error: "Failed to write file",
|
|
1497
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1498
|
-
}),
|
|
1499
|
-
{
|
|
1500
|
-
headers: {
|
|
1501
|
-
"Content-Type": "application/json",
|
|
1502
|
-
...corsHeaders,
|
|
1503
|
-
},
|
|
1504
|
-
status: 500,
|
|
1505
|
-
}
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
async function handleReadFileRequest(
|
|
1511
|
-
req: Request,
|
|
1512
|
-
corsHeaders: Record<string, string>
|
|
1513
|
-
): Promise<Response> {
|
|
1514
|
-
try {
|
|
1515
|
-
const body = (await req.json()) as ReadFileRequest;
|
|
1516
|
-
const { path, encoding = "utf-8", sessionId } = body;
|
|
1517
|
-
|
|
1518
|
-
if (!path || typeof path !== "string") {
|
|
1519
|
-
return new Response(
|
|
1520
|
-
JSON.stringify({
|
|
1521
|
-
error: "Path is required and must be a string",
|
|
1522
|
-
}),
|
|
1523
|
-
{
|
|
1524
|
-
headers: {
|
|
1525
|
-
"Content-Type": "application/json",
|
|
1526
|
-
...corsHeaders,
|
|
1527
|
-
},
|
|
1528
|
-
status: 400,
|
|
1529
|
-
}
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// Basic safety check - prevent dangerous paths
|
|
1534
|
-
const dangerousPatterns = [
|
|
1535
|
-
/^\/$/, // Root directory
|
|
1536
|
-
/^\/etc/, // System directories
|
|
1537
|
-
/^\/var/, // System directories
|
|
1538
|
-
/^\/usr/, // System directories
|
|
1539
|
-
/^\/bin/, // System directories
|
|
1540
|
-
/^\/sbin/, // System directories
|
|
1541
|
-
/^\/boot/, // System directories
|
|
1542
|
-
/^\/dev/, // System directories
|
|
1543
|
-
/^\/proc/, // System directories
|
|
1544
|
-
/^\/sys/, // System directories
|
|
1545
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1546
|
-
/\.\./, // Path traversal attempts
|
|
1547
|
-
];
|
|
1548
|
-
|
|
1549
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1550
|
-
return new Response(
|
|
1551
|
-
JSON.stringify({
|
|
1552
|
-
error: "Dangerous path not allowed",
|
|
1553
|
-
}),
|
|
1554
|
-
{
|
|
1555
|
-
headers: {
|
|
1556
|
-
"Content-Type": "application/json",
|
|
1557
|
-
...corsHeaders,
|
|
1558
|
-
},
|
|
1559
|
-
status: 400,
|
|
1560
|
-
}
|
|
1561
|
-
);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
console.log(`[Server] Reading file: ${path}`);
|
|
1565
|
-
|
|
1566
|
-
const result = await executeReadFile(path, encoding, sessionId);
|
|
1567
|
-
|
|
1568
|
-
return new Response(
|
|
1569
|
-
JSON.stringify({
|
|
1570
|
-
content: result.content,
|
|
1571
|
-
exitCode: result.exitCode,
|
|
1572
|
-
path,
|
|
1573
|
-
success: result.success,
|
|
1574
|
-
timestamp: new Date().toISOString(),
|
|
1575
|
-
}),
|
|
1576
|
-
{
|
|
1577
|
-
headers: {
|
|
1578
|
-
"Content-Type": "application/json",
|
|
1579
|
-
...corsHeaders,
|
|
1580
|
-
},
|
|
1581
|
-
}
|
|
1582
|
-
);
|
|
1583
|
-
} catch (error) {
|
|
1584
|
-
console.error("[Server] Error in handleReadFileRequest:", error);
|
|
1585
|
-
return new Response(
|
|
1586
|
-
JSON.stringify({
|
|
1587
|
-
error: "Failed to read file",
|
|
1588
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1589
|
-
}),
|
|
1590
|
-
{
|
|
1591
|
-
headers: {
|
|
1592
|
-
"Content-Type": "application/json",
|
|
1593
|
-
...corsHeaders,
|
|
1594
|
-
},
|
|
1595
|
-
status: 500,
|
|
1596
|
-
}
|
|
1597
|
-
);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
async function handleStreamingReadFileRequest(
|
|
1602
|
-
req: Request,
|
|
1603
|
-
corsHeaders: Record<string, string>
|
|
1604
|
-
): Promise<Response> {
|
|
1605
|
-
try {
|
|
1606
|
-
const body = (await req.json()) as ReadFileRequest;
|
|
1607
|
-
const { path, encoding = "utf-8", sessionId } = body;
|
|
1608
|
-
|
|
1609
|
-
if (!path || typeof path !== "string") {
|
|
1610
|
-
return new Response(
|
|
1611
|
-
JSON.stringify({
|
|
1612
|
-
error: "Path is required and must be a string",
|
|
1613
|
-
}),
|
|
1614
|
-
{
|
|
1615
|
-
headers: {
|
|
1616
|
-
"Content-Type": "application/json",
|
|
1617
|
-
...corsHeaders,
|
|
1618
|
-
},
|
|
1619
|
-
status: 400,
|
|
1620
|
-
}
|
|
1621
|
-
);
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Basic safety check - prevent dangerous paths
|
|
1625
|
-
const dangerousPatterns = [
|
|
1626
|
-
/^\/$/, // Root directory
|
|
1627
|
-
/^\/etc/, // System directories
|
|
1628
|
-
/^\/var/, // System directories
|
|
1629
|
-
/^\/usr/, // System directories
|
|
1630
|
-
/^\/bin/, // System directories
|
|
1631
|
-
/^\/sbin/, // System directories
|
|
1632
|
-
/^\/boot/, // System directories
|
|
1633
|
-
/^\/dev/, // System directories
|
|
1634
|
-
/^\/proc/, // System directories
|
|
1635
|
-
/^\/sys/, // System directories
|
|
1636
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1637
|
-
/\.\./, // Path traversal attempts
|
|
1638
|
-
];
|
|
1639
|
-
|
|
1640
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1641
|
-
return new Response(
|
|
1642
|
-
JSON.stringify({
|
|
1643
|
-
error: "Dangerous path not allowed",
|
|
1644
|
-
}),
|
|
1645
|
-
{
|
|
1646
|
-
headers: {
|
|
1647
|
-
"Content-Type": "application/json",
|
|
1648
|
-
...corsHeaders,
|
|
1649
|
-
},
|
|
1650
|
-
status: 400,
|
|
1651
|
-
}
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
console.log(`[Server] Reading file (streaming): ${path}`);
|
|
1656
|
-
|
|
1657
|
-
const stream = new ReadableStream({
|
|
1658
|
-
start(controller) {
|
|
1659
|
-
(async () => {
|
|
1660
|
-
try {
|
|
1661
|
-
// Send command start event
|
|
1662
|
-
controller.enqueue(
|
|
1663
|
-
new TextEncoder().encode(
|
|
1664
|
-
`data: ${JSON.stringify({
|
|
1665
|
-
path,
|
|
1666
|
-
timestamp: new Date().toISOString(),
|
|
1667
|
-
type: "command_start",
|
|
1668
|
-
})}\n\n`
|
|
1669
|
-
)
|
|
1670
|
-
);
|
|
1671
|
-
|
|
1672
|
-
// Read the file
|
|
1673
|
-
const content = await readFile(path, {
|
|
1674
|
-
encoding: encoding as BufferEncoding,
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
console.log(`[Server] File read successfully: ${path}`);
|
|
1678
|
-
|
|
1679
|
-
// Send command completion event
|
|
1680
|
-
controller.enqueue(
|
|
1681
|
-
new TextEncoder().encode(
|
|
1682
|
-
`data: ${JSON.stringify({
|
|
1683
|
-
content,
|
|
1684
|
-
path,
|
|
1685
|
-
success: true,
|
|
1686
|
-
timestamp: new Date().toISOString(),
|
|
1687
|
-
type: "command_complete",
|
|
1688
|
-
})}\n\n`
|
|
1689
|
-
)
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
controller.close();
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
console.error(`[Server] Error reading file: ${path}`, error);
|
|
1695
|
-
|
|
1696
|
-
controller.enqueue(
|
|
1697
|
-
new TextEncoder().encode(
|
|
1698
|
-
`data: ${JSON.stringify({
|
|
1699
|
-
error:
|
|
1700
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1701
|
-
path,
|
|
1702
|
-
type: "error",
|
|
1703
|
-
})}\n\n`
|
|
1704
|
-
)
|
|
1705
|
-
);
|
|
1706
|
-
|
|
1707
|
-
controller.close();
|
|
1708
|
-
}
|
|
1709
|
-
})();
|
|
1710
|
-
},
|
|
1711
|
-
});
|
|
1712
|
-
|
|
1713
|
-
return new Response(stream, {
|
|
1714
|
-
headers: {
|
|
1715
|
-
"Cache-Control": "no-cache",
|
|
1716
|
-
Connection: "keep-alive",
|
|
1717
|
-
"Content-Type": "text/event-stream",
|
|
1718
|
-
...corsHeaders,
|
|
1719
|
-
},
|
|
1720
|
-
});
|
|
1721
|
-
} catch (error) {
|
|
1722
|
-
console.error("[Server] Error in handleStreamingReadFileRequest:", error);
|
|
1723
|
-
return new Response(
|
|
1724
|
-
JSON.stringify({
|
|
1725
|
-
error: "Failed to read file",
|
|
1726
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1727
|
-
}),
|
|
1728
|
-
{
|
|
1729
|
-
headers: {
|
|
1730
|
-
"Content-Type": "application/json",
|
|
1731
|
-
...corsHeaders,
|
|
1732
|
-
},
|
|
1733
|
-
status: 500,
|
|
1734
|
-
}
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
async function handleDeleteFileRequest(
|
|
1740
|
-
req: Request,
|
|
1741
|
-
corsHeaders: Record<string, string>
|
|
1742
|
-
): Promise<Response> {
|
|
1743
|
-
try {
|
|
1744
|
-
const body = (await req.json()) as DeleteFileRequest;
|
|
1745
|
-
const { path, sessionId } = body;
|
|
1746
|
-
|
|
1747
|
-
if (!path || typeof path !== "string") {
|
|
1748
|
-
return new Response(
|
|
1749
|
-
JSON.stringify({
|
|
1750
|
-
error: "Path is required and must be a string",
|
|
1751
|
-
}),
|
|
1752
|
-
{
|
|
1753
|
-
headers: {
|
|
1754
|
-
"Content-Type": "application/json",
|
|
1755
|
-
...corsHeaders,
|
|
1756
|
-
},
|
|
1757
|
-
status: 400,
|
|
1758
|
-
}
|
|
1759
|
-
);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
// Basic safety check - prevent dangerous paths
|
|
1763
|
-
const dangerousPatterns = [
|
|
1764
|
-
/^\/$/, // Root directory
|
|
1765
|
-
/^\/etc/, // System directories
|
|
1766
|
-
/^\/var/, // System directories
|
|
1767
|
-
/^\/usr/, // System directories
|
|
1768
|
-
/^\/bin/, // System directories
|
|
1769
|
-
/^\/sbin/, // System directories
|
|
1770
|
-
/^\/boot/, // System directories
|
|
1771
|
-
/^\/dev/, // System directories
|
|
1772
|
-
/^\/proc/, // System directories
|
|
1773
|
-
/^\/sys/, // System directories
|
|
1774
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
1775
|
-
/\.\./, // Path traversal attempts
|
|
1776
|
-
];
|
|
1777
|
-
|
|
1778
|
-
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1779
|
-
return new Response(
|
|
1780
|
-
JSON.stringify({
|
|
1781
|
-
error: "Dangerous path not allowed",
|
|
1782
|
-
}),
|
|
1783
|
-
{
|
|
1784
|
-
headers: {
|
|
1785
|
-
"Content-Type": "application/json",
|
|
1786
|
-
...corsHeaders,
|
|
1787
|
-
},
|
|
1788
|
-
status: 400,
|
|
1789
|
-
}
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
console.log(`[Server] Deleting file: ${path}`);
|
|
1794
|
-
|
|
1795
|
-
const result = await executeDeleteFile(path, sessionId);
|
|
1796
|
-
|
|
1797
|
-
return new Response(
|
|
1798
|
-
JSON.stringify({
|
|
1799
|
-
exitCode: result.exitCode,
|
|
1800
|
-
path,
|
|
1801
|
-
success: result.success,
|
|
1802
|
-
timestamp: new Date().toISOString(),
|
|
1803
|
-
}),
|
|
1804
|
-
{
|
|
1805
|
-
headers: {
|
|
1806
|
-
"Content-Type": "application/json",
|
|
1807
|
-
...corsHeaders,
|
|
1808
|
-
},
|
|
1809
|
-
}
|
|
1810
|
-
);
|
|
1811
|
-
} catch (error) {
|
|
1812
|
-
console.error("[Server] Error in handleDeleteFileRequest:", error);
|
|
1813
|
-
return new Response(
|
|
1814
|
-
JSON.stringify({
|
|
1815
|
-
error: "Failed to delete file",
|
|
1816
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1817
|
-
}),
|
|
1818
|
-
{
|
|
1819
|
-
headers: {
|
|
1820
|
-
"Content-Type": "application/json",
|
|
1821
|
-
...corsHeaders,
|
|
1822
|
-
},
|
|
1823
|
-
status: 500,
|
|
1824
|
-
}
|
|
1825
|
-
);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
async function handleStreamingDeleteFileRequest(
|
|
1830
|
-
req: Request,
|
|
1831
|
-
corsHeaders: Record<string, string>
|
|
1832
|
-
): Promise<Response> {
|
|
1833
|
-
try {
|
|
1834
|
-
const body = (await req.json()) as DeleteFileRequest;
|
|
1835
|
-
const { path, sessionId } = body;
|
|
1836
|
-
|
|
1837
|
-
if (!path || typeof path !== "string") {
|
|
1838
|
-
return new Response(
|
|
1839
|
-
JSON.stringify({
|
|
1840
|
-
error: "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 (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1869
|
-
return new Response(
|
|
1870
|
-
JSON.stringify({
|
|
1871
|
-
error: "Dangerous path not allowed",
|
|
1872
|
-
}),
|
|
1873
|
-
{
|
|
1874
|
-
headers: {
|
|
1875
|
-
"Content-Type": "application/json",
|
|
1876
|
-
...corsHeaders,
|
|
1877
|
-
},
|
|
1878
|
-
status: 400,
|
|
1879
|
-
}
|
|
1880
|
-
);
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
console.log(`[Server] Deleting file (streaming): ${path}`);
|
|
1884
|
-
|
|
1885
|
-
const stream = new ReadableStream({
|
|
1886
|
-
start(controller) {
|
|
1887
|
-
(async () => {
|
|
1888
|
-
try {
|
|
1889
|
-
// Send command start event
|
|
1890
|
-
controller.enqueue(
|
|
1891
|
-
new TextEncoder().encode(
|
|
1892
|
-
`data: ${JSON.stringify({
|
|
1893
|
-
path,
|
|
1894
|
-
timestamp: new Date().toISOString(),
|
|
1895
|
-
type: "command_start",
|
|
1896
|
-
})}\n\n`
|
|
1897
|
-
)
|
|
1898
|
-
);
|
|
1899
|
-
|
|
1900
|
-
// Delete the file
|
|
1901
|
-
await executeDeleteFile(path, sessionId);
|
|
1902
|
-
|
|
1903
|
-
console.log(`[Server] File deleted successfully: ${path}`);
|
|
1904
|
-
|
|
1905
|
-
// Send command completion event
|
|
1906
|
-
controller.enqueue(
|
|
1907
|
-
new TextEncoder().encode(
|
|
1908
|
-
`data: ${JSON.stringify({
|
|
1909
|
-
path,
|
|
1910
|
-
success: true,
|
|
1911
|
-
timestamp: new Date().toISOString(),
|
|
1912
|
-
type: "command_complete",
|
|
1913
|
-
})}\n\n`
|
|
1914
|
-
)
|
|
1915
|
-
);
|
|
1916
|
-
|
|
1917
|
-
controller.close();
|
|
1918
|
-
} catch (error) {
|
|
1919
|
-
console.error(`[Server] Error deleting file: ${path}`, error);
|
|
1920
|
-
|
|
1921
|
-
controller.enqueue(
|
|
1922
|
-
new TextEncoder().encode(
|
|
1923
|
-
`data: ${JSON.stringify({
|
|
1924
|
-
error:
|
|
1925
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
1926
|
-
path,
|
|
1927
|
-
type: "error",
|
|
1928
|
-
})}\n\n`
|
|
1929
|
-
)
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
controller.close();
|
|
1933
|
-
}
|
|
1934
|
-
})();
|
|
1935
|
-
},
|
|
1936
|
-
});
|
|
1937
|
-
|
|
1938
|
-
return new Response(stream, {
|
|
1939
|
-
headers: {
|
|
1940
|
-
"Cache-Control": "no-cache",
|
|
1941
|
-
Connection: "keep-alive",
|
|
1942
|
-
"Content-Type": "text/event-stream",
|
|
1943
|
-
...corsHeaders,
|
|
1944
|
-
},
|
|
1945
|
-
});
|
|
1946
|
-
} catch (error) {
|
|
1947
|
-
console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
|
|
1948
|
-
return new Response(
|
|
1949
|
-
JSON.stringify({
|
|
1950
|
-
error: "Failed to delete file",
|
|
1951
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
1952
|
-
}),
|
|
1953
|
-
{
|
|
1954
|
-
headers: {
|
|
1955
|
-
"Content-Type": "application/json",
|
|
1956
|
-
...corsHeaders,
|
|
1957
|
-
},
|
|
1958
|
-
status: 500,
|
|
1959
|
-
}
|
|
1960
|
-
);
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
async function handleRenameFileRequest(
|
|
1965
|
-
req: Request,
|
|
1966
|
-
corsHeaders: Record<string, string>
|
|
1967
|
-
): Promise<Response> {
|
|
1968
|
-
try {
|
|
1969
|
-
const body = (await req.json()) as RenameFileRequest;
|
|
1970
|
-
const { oldPath, newPath, sessionId } = body;
|
|
1971
|
-
|
|
1972
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
1973
|
-
return new Response(
|
|
1974
|
-
JSON.stringify({
|
|
1975
|
-
error: "Old path is required and must be a string",
|
|
1976
|
-
}),
|
|
1977
|
-
{
|
|
1978
|
-
headers: {
|
|
1979
|
-
"Content-Type": "application/json",
|
|
1980
|
-
...corsHeaders,
|
|
1981
|
-
},
|
|
1982
|
-
status: 400,
|
|
1983
|
-
}
|
|
1984
|
-
);
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
if (!newPath || typeof newPath !== "string") {
|
|
1988
|
-
return new Response(
|
|
1989
|
-
JSON.stringify({
|
|
1990
|
-
error: "New path is required and must be a string",
|
|
1991
|
-
}),
|
|
1992
|
-
{
|
|
1993
|
-
headers: {
|
|
1994
|
-
"Content-Type": "application/json",
|
|
1995
|
-
...corsHeaders,
|
|
1996
|
-
},
|
|
1997
|
-
status: 400,
|
|
1998
|
-
}
|
|
1999
|
-
);
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
// Basic safety check - prevent dangerous paths
|
|
2003
|
-
const dangerousPatterns = [
|
|
2004
|
-
/^\/$/, // Root directory
|
|
2005
|
-
/^\/etc/, // System directories
|
|
2006
|
-
/^\/var/, // System directories
|
|
2007
|
-
/^\/usr/, // System directories
|
|
2008
|
-
/^\/bin/, // System directories
|
|
2009
|
-
/^\/sbin/, // System directories
|
|
2010
|
-
/^\/boot/, // System directories
|
|
2011
|
-
/^\/dev/, // System directories
|
|
2012
|
-
/^\/proc/, // System directories
|
|
2013
|
-
/^\/sys/, // System directories
|
|
2014
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2015
|
-
/\.\./, // Path traversal attempts
|
|
2016
|
-
];
|
|
2017
|
-
|
|
2018
|
-
if (
|
|
2019
|
-
dangerousPatterns.some(
|
|
2020
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
2021
|
-
)
|
|
2022
|
-
) {
|
|
2023
|
-
return new Response(
|
|
2024
|
-
JSON.stringify({
|
|
2025
|
-
error: "Dangerous path not allowed",
|
|
2026
|
-
}),
|
|
2027
|
-
{
|
|
2028
|
-
headers: {
|
|
2029
|
-
"Content-Type": "application/json",
|
|
2030
|
-
...corsHeaders,
|
|
2031
|
-
},
|
|
2032
|
-
status: 400,
|
|
2033
|
-
}
|
|
2034
|
-
);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
|
|
2038
|
-
|
|
2039
|
-
const result = await executeRenameFile(oldPath, newPath, sessionId);
|
|
2040
|
-
|
|
2041
|
-
return new Response(
|
|
2042
|
-
JSON.stringify({
|
|
2043
|
-
exitCode: result.exitCode,
|
|
2044
|
-
newPath,
|
|
2045
|
-
oldPath,
|
|
2046
|
-
success: result.success,
|
|
2047
|
-
timestamp: new Date().toISOString(),
|
|
2048
|
-
}),
|
|
2049
|
-
{
|
|
2050
|
-
headers: {
|
|
2051
|
-
"Content-Type": "application/json",
|
|
2052
|
-
...corsHeaders,
|
|
2053
|
-
},
|
|
2054
|
-
}
|
|
2055
|
-
);
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
console.error("[Server] Error in handleRenameFileRequest:", error);
|
|
2058
|
-
return new Response(
|
|
2059
|
-
JSON.stringify({
|
|
2060
|
-
error: "Failed to rename file",
|
|
2061
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2062
|
-
}),
|
|
2063
|
-
{
|
|
2064
|
-
headers: {
|
|
2065
|
-
"Content-Type": "application/json",
|
|
2066
|
-
...corsHeaders,
|
|
2067
|
-
},
|
|
2068
|
-
status: 500,
|
|
2069
|
-
}
|
|
2070
|
-
);
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
async function handleStreamingRenameFileRequest(
|
|
2075
|
-
req: Request,
|
|
2076
|
-
corsHeaders: Record<string, string>
|
|
2077
|
-
): Promise<Response> {
|
|
2078
|
-
try {
|
|
2079
|
-
const body = (await req.json()) as RenameFileRequest;
|
|
2080
|
-
const { oldPath, newPath, sessionId } = body;
|
|
2081
|
-
|
|
2082
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
2083
|
-
return new Response(
|
|
2084
|
-
JSON.stringify({
|
|
2085
|
-
error: "Old path is required and must be a string",
|
|
2086
|
-
}),
|
|
2087
|
-
{
|
|
2088
|
-
headers: {
|
|
2089
|
-
"Content-Type": "application/json",
|
|
2090
|
-
...corsHeaders,
|
|
2091
|
-
},
|
|
2092
|
-
status: 400,
|
|
2093
|
-
}
|
|
2094
|
-
);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (!newPath || typeof newPath !== "string") {
|
|
2098
|
-
return new Response(
|
|
2099
|
-
JSON.stringify({
|
|
2100
|
-
error: "New path is required and must be a string",
|
|
2101
|
-
}),
|
|
2102
|
-
{
|
|
2103
|
-
headers: {
|
|
2104
|
-
"Content-Type": "application/json",
|
|
2105
|
-
...corsHeaders,
|
|
2106
|
-
},
|
|
2107
|
-
status: 400,
|
|
2108
|
-
}
|
|
2109
|
-
);
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
// Basic safety check - prevent dangerous paths
|
|
2113
|
-
const dangerousPatterns = [
|
|
2114
|
-
/^\/$/, // Root directory
|
|
2115
|
-
/^\/etc/, // System directories
|
|
2116
|
-
/^\/var/, // System directories
|
|
2117
|
-
/^\/usr/, // System directories
|
|
2118
|
-
/^\/bin/, // System directories
|
|
2119
|
-
/^\/sbin/, // System directories
|
|
2120
|
-
/^\/boot/, // System directories
|
|
2121
|
-
/^\/dev/, // System directories
|
|
2122
|
-
/^\/proc/, // System directories
|
|
2123
|
-
/^\/sys/, // System directories
|
|
2124
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2125
|
-
/\.\./, // Path traversal attempts
|
|
2126
|
-
];
|
|
2127
|
-
|
|
2128
|
-
if (
|
|
2129
|
-
dangerousPatterns.some(
|
|
2130
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
2131
|
-
)
|
|
2132
|
-
) {
|
|
2133
|
-
return new Response(
|
|
2134
|
-
JSON.stringify({
|
|
2135
|
-
error: "Dangerous path not allowed",
|
|
2136
|
-
}),
|
|
2137
|
-
{
|
|
2138
|
-
headers: {
|
|
2139
|
-
"Content-Type": "application/json",
|
|
2140
|
-
...corsHeaders,
|
|
2141
|
-
},
|
|
2142
|
-
status: 400,
|
|
2143
|
-
}
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
|
|
2148
|
-
|
|
2149
|
-
const stream = new ReadableStream({
|
|
2150
|
-
start(controller) {
|
|
2151
|
-
(async () => {
|
|
2152
|
-
try {
|
|
2153
|
-
// Send command start event
|
|
2154
|
-
controller.enqueue(
|
|
2155
|
-
new TextEncoder().encode(
|
|
2156
|
-
`data: ${JSON.stringify({
|
|
2157
|
-
newPath,
|
|
2158
|
-
oldPath,
|
|
2159
|
-
timestamp: new Date().toISOString(),
|
|
2160
|
-
type: "command_start",
|
|
2161
|
-
})}\n\n`
|
|
2162
|
-
)
|
|
2163
|
-
);
|
|
2164
|
-
|
|
2165
|
-
// Rename the file
|
|
2166
|
-
await executeRenameFile(oldPath, newPath, sessionId);
|
|
2167
|
-
|
|
2168
|
-
console.log(
|
|
2169
|
-
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
2170
|
-
);
|
|
2171
|
-
|
|
2172
|
-
// Send command completion event
|
|
2173
|
-
controller.enqueue(
|
|
2174
|
-
new TextEncoder().encode(
|
|
2175
|
-
`data: ${JSON.stringify({
|
|
2176
|
-
newPath,
|
|
2177
|
-
oldPath,
|
|
2178
|
-
success: true,
|
|
2179
|
-
timestamp: new Date().toISOString(),
|
|
2180
|
-
type: "command_complete",
|
|
2181
|
-
})}\n\n`
|
|
2182
|
-
)
|
|
2183
|
-
);
|
|
2184
|
-
|
|
2185
|
-
controller.close();
|
|
2186
|
-
} catch (error) {
|
|
2187
|
-
console.error(
|
|
2188
|
-
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
2189
|
-
error
|
|
2190
|
-
);
|
|
2191
|
-
|
|
2192
|
-
controller.enqueue(
|
|
2193
|
-
new TextEncoder().encode(
|
|
2194
|
-
`data: ${JSON.stringify({
|
|
2195
|
-
error:
|
|
2196
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
2197
|
-
newPath,
|
|
2198
|
-
oldPath,
|
|
2199
|
-
type: "error",
|
|
2200
|
-
})}\n\n`
|
|
2201
|
-
)
|
|
2202
|
-
);
|
|
2203
|
-
|
|
2204
|
-
controller.close();
|
|
2205
|
-
}
|
|
2206
|
-
})();
|
|
2207
|
-
},
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
return new Response(stream, {
|
|
2211
|
-
headers: {
|
|
2212
|
-
"Cache-Control": "no-cache",
|
|
2213
|
-
Connection: "keep-alive",
|
|
2214
|
-
"Content-Type": "text/event-stream",
|
|
2215
|
-
...corsHeaders,
|
|
2216
|
-
},
|
|
2217
|
-
});
|
|
2218
|
-
} catch (error) {
|
|
2219
|
-
console.error("[Server] Error in handleStreamingRenameFileRequest:", error);
|
|
2220
|
-
return new Response(
|
|
2221
|
-
JSON.stringify({
|
|
2222
|
-
error: "Failed to rename file",
|
|
2223
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2224
|
-
}),
|
|
2225
|
-
{
|
|
2226
|
-
headers: {
|
|
2227
|
-
"Content-Type": "application/json",
|
|
2228
|
-
...corsHeaders,
|
|
2229
|
-
},
|
|
2230
|
-
status: 500,
|
|
2231
|
-
}
|
|
2232
|
-
);
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
async function handleMoveFileRequest(
|
|
2237
|
-
req: Request,
|
|
2238
|
-
corsHeaders: Record<string, string>
|
|
2239
|
-
): Promise<Response> {
|
|
2240
|
-
try {
|
|
2241
|
-
const body = (await req.json()) as MoveFileRequest;
|
|
2242
|
-
const { sourcePath, destinationPath, sessionId } = body;
|
|
2243
|
-
|
|
2244
|
-
if (!sourcePath || typeof sourcePath !== "string") {
|
|
2245
|
-
return new Response(
|
|
2246
|
-
JSON.stringify({
|
|
2247
|
-
error: "Source path is required and must be a string",
|
|
2248
|
-
}),
|
|
2249
|
-
{
|
|
2250
|
-
headers: {
|
|
2251
|
-
"Content-Type": "application/json",
|
|
2252
|
-
...corsHeaders,
|
|
2253
|
-
},
|
|
2254
|
-
status: 400,
|
|
2255
|
-
}
|
|
2256
|
-
);
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2260
|
-
return new Response(
|
|
2261
|
-
JSON.stringify({
|
|
2262
|
-
error: "Destination path is required and must be a string",
|
|
2263
|
-
}),
|
|
2264
|
-
{
|
|
2265
|
-
headers: {
|
|
2266
|
-
"Content-Type": "application/json",
|
|
2267
|
-
...corsHeaders,
|
|
2268
|
-
},
|
|
2269
|
-
status: 400,
|
|
2270
|
-
}
|
|
2271
|
-
);
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// Basic safety check - prevent dangerous paths
|
|
2275
|
-
const dangerousPatterns = [
|
|
2276
|
-
/^\/$/, // Root directory
|
|
2277
|
-
/^\/etc/, // System directories
|
|
2278
|
-
/^\/var/, // System directories
|
|
2279
|
-
/^\/usr/, // System directories
|
|
2280
|
-
/^\/bin/, // System directories
|
|
2281
|
-
/^\/sbin/, // System directories
|
|
2282
|
-
/^\/boot/, // System directories
|
|
2283
|
-
/^\/dev/, // System directories
|
|
2284
|
-
/^\/proc/, // System directories
|
|
2285
|
-
/^\/sys/, // System directories
|
|
2286
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2287
|
-
/\.\./, // Path traversal attempts
|
|
2288
|
-
];
|
|
2289
|
-
|
|
2290
|
-
if (
|
|
2291
|
-
dangerousPatterns.some(
|
|
2292
|
-
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2293
|
-
)
|
|
2294
|
-
) {
|
|
2295
|
-
return new Response(
|
|
2296
|
-
JSON.stringify({
|
|
2297
|
-
error: "Dangerous path not allowed",
|
|
2298
|
-
}),
|
|
2299
|
-
{
|
|
2300
|
-
headers: {
|
|
2301
|
-
"Content-Type": "application/json",
|
|
2302
|
-
...corsHeaders,
|
|
2303
|
-
},
|
|
2304
|
-
status: 400,
|
|
2305
|
-
}
|
|
2306
|
-
);
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
|
|
2310
|
-
|
|
2311
|
-
const result = await executeMoveFile(
|
|
2312
|
-
sourcePath,
|
|
2313
|
-
destinationPath,
|
|
2314
|
-
sessionId
|
|
2315
|
-
);
|
|
2316
|
-
|
|
2317
|
-
return new Response(
|
|
2318
|
-
JSON.stringify({
|
|
2319
|
-
destinationPath,
|
|
2320
|
-
exitCode: result.exitCode,
|
|
2321
|
-
sourcePath,
|
|
2322
|
-
success: result.success,
|
|
2323
|
-
timestamp: new Date().toISOString(),
|
|
2324
|
-
}),
|
|
2325
|
-
{
|
|
2326
|
-
headers: {
|
|
2327
|
-
"Content-Type": "application/json",
|
|
2328
|
-
...corsHeaders,
|
|
2329
|
-
},
|
|
2330
|
-
}
|
|
2331
|
-
);
|
|
2332
|
-
} catch (error) {
|
|
2333
|
-
console.error("[Server] Error in handleMoveFileRequest:", error);
|
|
2334
|
-
return new Response(
|
|
2335
|
-
JSON.stringify({
|
|
2336
|
-
error: "Failed to move file",
|
|
2337
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2338
|
-
}),
|
|
2339
|
-
{
|
|
2340
|
-
headers: {
|
|
2341
|
-
"Content-Type": "application/json",
|
|
2342
|
-
...corsHeaders,
|
|
2343
|
-
},
|
|
2344
|
-
status: 500,
|
|
2345
|
-
}
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
async function handleStreamingMoveFileRequest(
|
|
2351
|
-
req: Request,
|
|
2352
|
-
corsHeaders: Record<string, string>
|
|
2353
|
-
): Promise<Response> {
|
|
2354
|
-
try {
|
|
2355
|
-
const body = (await req.json()) as MoveFileRequest;
|
|
2356
|
-
const { sourcePath, destinationPath, sessionId } = body;
|
|
2357
|
-
|
|
2358
|
-
if (!sourcePath || typeof sourcePath !== "string") {
|
|
2359
|
-
return new Response(
|
|
2360
|
-
JSON.stringify({
|
|
2361
|
-
error: "Source path is required and must be a string",
|
|
2362
|
-
}),
|
|
2363
|
-
{
|
|
2364
|
-
headers: {
|
|
2365
|
-
"Content-Type": "application/json",
|
|
2366
|
-
...corsHeaders,
|
|
2367
|
-
},
|
|
2368
|
-
status: 400,
|
|
2369
|
-
}
|
|
2370
|
-
);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2374
|
-
return new Response(
|
|
2375
|
-
JSON.stringify({
|
|
2376
|
-
error: "Destination path is required and must be a string",
|
|
2377
|
-
}),
|
|
2378
|
-
{
|
|
2379
|
-
headers: {
|
|
2380
|
-
"Content-Type": "application/json",
|
|
2381
|
-
...corsHeaders,
|
|
2382
|
-
},
|
|
2383
|
-
status: 400,
|
|
2384
|
-
}
|
|
2385
|
-
);
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
// Basic safety check - prevent dangerous paths
|
|
2389
|
-
const dangerousPatterns = [
|
|
2390
|
-
/^\/$/, // Root directory
|
|
2391
|
-
/^\/etc/, // System directories
|
|
2392
|
-
/^\/var/, // System directories
|
|
2393
|
-
/^\/usr/, // System directories
|
|
2394
|
-
/^\/bin/, // System directories
|
|
2395
|
-
/^\/sbin/, // System directories
|
|
2396
|
-
/^\/boot/, // System directories
|
|
2397
|
-
/^\/dev/, // System directories
|
|
2398
|
-
/^\/proc/, // System directories
|
|
2399
|
-
/^\/sys/, // System directories
|
|
2400
|
-
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2401
|
-
/\.\./, // Path traversal attempts
|
|
2402
|
-
];
|
|
2403
|
-
|
|
2404
|
-
if (
|
|
2405
|
-
dangerousPatterns.some(
|
|
2406
|
-
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2407
|
-
)
|
|
2408
|
-
) {
|
|
2409
|
-
return new Response(
|
|
2410
|
-
JSON.stringify({
|
|
2411
|
-
error: "Dangerous path not allowed",
|
|
2412
|
-
}),
|
|
2413
|
-
{
|
|
2414
|
-
headers: {
|
|
2415
|
-
"Content-Type": "application/json",
|
|
2416
|
-
...corsHeaders,
|
|
2417
|
-
},
|
|
2418
|
-
status: 400,
|
|
2419
|
-
}
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
console.log(
|
|
2424
|
-
`[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
|
|
2425
|
-
);
|
|
2426
|
-
|
|
2427
|
-
const stream = new ReadableStream({
|
|
2428
|
-
start(controller) {
|
|
2429
|
-
(async () => {
|
|
2430
|
-
try {
|
|
2431
|
-
// Send command start event
|
|
2432
|
-
controller.enqueue(
|
|
2433
|
-
new TextEncoder().encode(
|
|
2434
|
-
`data: ${JSON.stringify({
|
|
2435
|
-
destinationPath,
|
|
2436
|
-
sourcePath,
|
|
2437
|
-
timestamp: new Date().toISOString(),
|
|
2438
|
-
type: "command_start",
|
|
2439
|
-
})}\n\n`
|
|
2440
|
-
)
|
|
2441
|
-
);
|
|
2442
|
-
|
|
2443
|
-
// Move the file
|
|
2444
|
-
await executeMoveFile(sourcePath, destinationPath, sessionId);
|
|
2445
|
-
|
|
2446
|
-
console.log(
|
|
2447
|
-
`[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
|
|
2448
|
-
);
|
|
2449
|
-
|
|
2450
|
-
// Send command completion event
|
|
2451
|
-
controller.enqueue(
|
|
2452
|
-
new TextEncoder().encode(
|
|
2453
|
-
`data: ${JSON.stringify({
|
|
2454
|
-
destinationPath,
|
|
2455
|
-
sourcePath,
|
|
2456
|
-
success: true,
|
|
2457
|
-
timestamp: new Date().toISOString(),
|
|
2458
|
-
type: "command_complete",
|
|
2459
|
-
})}\n\n`
|
|
2460
|
-
)
|
|
2461
|
-
);
|
|
2462
|
-
|
|
2463
|
-
controller.close();
|
|
2464
|
-
} catch (error) {
|
|
2465
|
-
console.error(
|
|
2466
|
-
`[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
|
|
2467
|
-
error
|
|
2468
|
-
);
|
|
2469
|
-
|
|
2470
|
-
controller.enqueue(
|
|
2471
|
-
new TextEncoder().encode(
|
|
2472
|
-
`data: ${JSON.stringify({
|
|
2473
|
-
destinationPath,
|
|
2474
|
-
error:
|
|
2475
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
2476
|
-
sourcePath,
|
|
2477
|
-
type: "error",
|
|
2478
|
-
})}\n\n`
|
|
2479
|
-
)
|
|
2480
|
-
);
|
|
2481
|
-
|
|
2482
|
-
controller.close();
|
|
2483
|
-
}
|
|
2484
|
-
})();
|
|
2485
|
-
},
|
|
2486
|
-
});
|
|
2487
|
-
|
|
2488
|
-
return new Response(stream, {
|
|
2489
|
-
headers: {
|
|
2490
|
-
"Cache-Control": "no-cache",
|
|
2491
|
-
Connection: "keep-alive",
|
|
2492
|
-
"Content-Type": "text/event-stream",
|
|
2493
|
-
...corsHeaders,
|
|
2494
|
-
},
|
|
2495
|
-
});
|
|
2496
|
-
} catch (error) {
|
|
2497
|
-
console.error("[Server] Error in handleStreamingMoveFileRequest:", error);
|
|
2498
|
-
return new Response(
|
|
2499
|
-
JSON.stringify({
|
|
2500
|
-
error: "Failed to move file",
|
|
2501
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2502
|
-
}),
|
|
2503
|
-
{
|
|
2504
|
-
headers: {
|
|
2505
|
-
"Content-Type": "application/json",
|
|
2506
|
-
...corsHeaders,
|
|
2507
|
-
},
|
|
2508
|
-
status: 500,
|
|
2509
|
-
}
|
|
2510
|
-
);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
function executeCommand(
|
|
2515
|
-
command: string,
|
|
2516
|
-
args: string[],
|
|
2517
|
-
sessionId?: string,
|
|
2518
|
-
background?: boolean
|
|
2519
|
-
): Promise<{
|
|
2520
|
-
success: boolean;
|
|
2521
|
-
stdout: string;
|
|
2522
|
-
stderr: string;
|
|
2523
|
-
exitCode: number;
|
|
2524
|
-
}> {
|
|
2525
|
-
return new Promise((resolve, reject) => {
|
|
2526
|
-
const spawnOptions: SpawnOptions = {
|
|
2527
|
-
shell: true,
|
|
2528
|
-
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
2529
|
-
detached: background || false,
|
|
2530
|
-
};
|
|
2531
|
-
|
|
2532
|
-
const child = spawn(command, args, spawnOptions);
|
|
2533
|
-
|
|
2534
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2535
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2536
|
-
const session = sessions.get(sessionId)!;
|
|
2537
|
-
session.activeProcess = child;
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
let stdout = "";
|
|
2541
|
-
let stderr = "";
|
|
2542
|
-
|
|
2543
|
-
child.stdout?.on("data", (data) => {
|
|
2544
|
-
stdout += data.toString();
|
|
2545
|
-
});
|
|
2546
|
-
|
|
2547
|
-
child.stderr?.on("data", (data) => {
|
|
2548
|
-
stderr += data.toString();
|
|
2549
|
-
});
|
|
2550
|
-
|
|
2551
|
-
if (background) {
|
|
2552
|
-
// For background processes, unref and return quickly
|
|
2553
|
-
child.unref();
|
|
2554
|
-
|
|
2555
|
-
// Collect initial output for 100ms then return
|
|
2556
|
-
setTimeout(() => {
|
|
2557
|
-
resolve({
|
|
2558
|
-
exitCode: 0, // Process is still running
|
|
2559
|
-
stderr,
|
|
2560
|
-
stdout,
|
|
2561
|
-
success: true,
|
|
2562
|
-
});
|
|
2563
|
-
}, 100);
|
|
2564
|
-
|
|
2565
|
-
// Still handle errors
|
|
2566
|
-
child.on("error", (error) => {
|
|
2567
|
-
console.error(`[Server] Background process error: ${command}`, error);
|
|
2568
|
-
// Don't reject since we might have already resolved
|
|
2569
|
-
});
|
|
2570
|
-
} else {
|
|
2571
|
-
// Normal synchronous execution
|
|
2572
|
-
child.on("close", (code) => {
|
|
2573
|
-
// Clear the active process reference
|
|
2574
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2575
|
-
const session = sessions.get(sessionId)!;
|
|
2576
|
-
session.activeProcess = null;
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
|
|
2580
|
-
|
|
2581
|
-
resolve({
|
|
2582
|
-
exitCode: code || 0,
|
|
2583
|
-
stderr,
|
|
2584
|
-
stdout,
|
|
2585
|
-
success: code === 0,
|
|
2586
|
-
});
|
|
2587
|
-
});
|
|
2588
|
-
|
|
2589
|
-
child.on("error", (error) => {
|
|
2590
|
-
// Clear the active process reference
|
|
2591
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2592
|
-
const session = sessions.get(sessionId)!;
|
|
2593
|
-
session.activeProcess = null;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
reject(error);
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
});
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
function executeGitCheckout(
|
|
2603
|
-
repoUrl: string,
|
|
2604
|
-
branch: string,
|
|
2605
|
-
targetDir: string,
|
|
2606
|
-
sessionId?: string
|
|
2607
|
-
): Promise<{
|
|
2608
|
-
success: boolean;
|
|
2609
|
-
stdout: string;
|
|
2610
|
-
stderr: string;
|
|
2611
|
-
exitCode: number;
|
|
2612
|
-
}> {
|
|
2613
|
-
return new Promise((resolve, reject) => {
|
|
2614
|
-
// First, clone the repository
|
|
2615
|
-
const cloneChild = spawn(
|
|
2616
|
-
"git",
|
|
2617
|
-
["clone", "-b", branch, repoUrl, targetDir],
|
|
2618
|
-
{
|
|
2619
|
-
shell: true,
|
|
2620
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2621
|
-
}
|
|
2622
|
-
);
|
|
2623
|
-
|
|
2624
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2625
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2626
|
-
const session = sessions.get(sessionId)!;
|
|
2627
|
-
session.activeProcess = cloneChild;
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
let stdout = "";
|
|
2631
|
-
let stderr = "";
|
|
2632
|
-
|
|
2633
|
-
cloneChild.stdout?.on("data", (data) => {
|
|
2634
|
-
stdout += data.toString();
|
|
2635
|
-
});
|
|
2636
|
-
|
|
2637
|
-
cloneChild.stderr?.on("data", (data) => {
|
|
2638
|
-
stderr += data.toString();
|
|
2639
|
-
});
|
|
2640
|
-
|
|
2641
|
-
cloneChild.on("close", (code) => {
|
|
2642
|
-
// Clear the active process reference
|
|
2643
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2644
|
-
const session = sessions.get(sessionId)!;
|
|
2645
|
-
session.activeProcess = null;
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
if (code === 0) {
|
|
2649
|
-
console.log(
|
|
2650
|
-
`[Server] Repository cloned successfully: ${repoUrl} to ${targetDir}`
|
|
2651
|
-
);
|
|
2652
|
-
resolve({
|
|
2653
|
-
exitCode: code || 0,
|
|
2654
|
-
stderr,
|
|
2655
|
-
stdout,
|
|
2656
|
-
success: true,
|
|
2657
|
-
});
|
|
2658
|
-
} else {
|
|
2659
|
-
console.error(
|
|
2660
|
-
`[Server] Failed to clone repository: ${repoUrl}, Exit code: ${code}`
|
|
2661
|
-
);
|
|
2662
|
-
resolve({
|
|
2663
|
-
exitCode: code || 1,
|
|
2664
|
-
stderr,
|
|
2665
|
-
stdout,
|
|
2666
|
-
success: false,
|
|
2667
|
-
});
|
|
2668
|
-
}
|
|
2669
|
-
});
|
|
2670
|
-
|
|
2671
|
-
cloneChild.on("error", (error) => {
|
|
2672
|
-
// Clear the active process reference
|
|
2673
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2674
|
-
const session = sessions.get(sessionId)!;
|
|
2675
|
-
session.activeProcess = null;
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
console.error(`[Server] Error cloning repository: ${repoUrl}`, error);
|
|
2679
|
-
reject(error);
|
|
2680
|
-
});
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
function executeMkdir(
|
|
2685
|
-
path: string,
|
|
2686
|
-
recursive: boolean,
|
|
2687
|
-
sessionId?: string
|
|
2688
|
-
): Promise<{
|
|
2689
|
-
success: boolean;
|
|
2690
|
-
stdout: string;
|
|
2691
|
-
stderr: string;
|
|
2692
|
-
exitCode: number;
|
|
2693
|
-
}> {
|
|
2694
|
-
return new Promise((resolve, reject) => {
|
|
2695
|
-
const args = recursive ? ["-p", path] : [path];
|
|
2696
|
-
const mkdirChild = spawn("mkdir", args, {
|
|
2697
|
-
shell: true,
|
|
2698
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2699
|
-
});
|
|
2700
|
-
|
|
2701
|
-
// Store the process reference for cleanup if sessionId is provided
|
|
2702
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2703
|
-
const session = sessions.get(sessionId)!;
|
|
2704
|
-
session.activeProcess = mkdirChild;
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
let stdout = "";
|
|
2708
|
-
let stderr = "";
|
|
2709
|
-
|
|
2710
|
-
mkdirChild.stdout?.on("data", (data) => {
|
|
2711
|
-
stdout += data.toString();
|
|
2712
|
-
});
|
|
2713
|
-
|
|
2714
|
-
mkdirChild.stderr?.on("data", (data) => {
|
|
2715
|
-
stderr += data.toString();
|
|
2716
|
-
});
|
|
2717
|
-
|
|
2718
|
-
mkdirChild.on("close", (code) => {
|
|
2719
|
-
// Clear the active process reference
|
|
2720
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2721
|
-
const session = sessions.get(sessionId)!;
|
|
2722
|
-
session.activeProcess = null;
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
if (code === 0) {
|
|
2726
|
-
console.log(`[Server] Directory created successfully: ${path}`);
|
|
2727
|
-
resolve({
|
|
2728
|
-
exitCode: code || 0,
|
|
2729
|
-
stderr,
|
|
2730
|
-
stdout,
|
|
2731
|
-
success: true,
|
|
2732
|
-
});
|
|
2733
|
-
} else {
|
|
2734
|
-
console.error(
|
|
2735
|
-
`[Server] Failed to create directory: ${path}, Exit code: ${code}`
|
|
2736
|
-
);
|
|
2737
|
-
resolve({
|
|
2738
|
-
exitCode: code || 1,
|
|
2739
|
-
stderr,
|
|
2740
|
-
stdout,
|
|
2741
|
-
success: false,
|
|
2742
|
-
});
|
|
2743
|
-
}
|
|
2744
|
-
});
|
|
2745
|
-
|
|
2746
|
-
mkdirChild.on("error", (error) => {
|
|
2747
|
-
// Clear the active process reference
|
|
2748
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2749
|
-
const session = sessions.get(sessionId)!;
|
|
2750
|
-
session.activeProcess = null;
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
console.error(`[Server] Error creating directory: ${path}`, error);
|
|
2754
|
-
reject(error);
|
|
2755
|
-
});
|
|
2756
|
-
});
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
function executeWriteFile(
|
|
2760
|
-
path: string,
|
|
2761
|
-
content: string,
|
|
2762
|
-
encoding: string,
|
|
2763
|
-
sessionId?: string
|
|
2764
|
-
): Promise<{
|
|
2765
|
-
success: boolean;
|
|
2766
|
-
exitCode: number;
|
|
2767
|
-
}> {
|
|
2768
|
-
return new Promise((resolve, reject) => {
|
|
2769
|
-
(async () => {
|
|
2770
|
-
try {
|
|
2771
|
-
// Ensure the directory exists
|
|
2772
|
-
const dir = dirname(path);
|
|
2773
|
-
if (dir !== ".") {
|
|
2774
|
-
await mkdir(dir, { recursive: true });
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
// Write the file
|
|
2778
|
-
await writeFile(path, content, {
|
|
2779
|
-
encoding: encoding as BufferEncoding,
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
console.log(`[Server] File written successfully: ${path}`);
|
|
2783
|
-
resolve({
|
|
2784
|
-
exitCode: 0,
|
|
2785
|
-
success: true,
|
|
2786
|
-
});
|
|
2787
|
-
} catch (error) {
|
|
2788
|
-
console.error(`[Server] Error writing file: ${path}`, error);
|
|
2789
|
-
reject(error);
|
|
2790
|
-
}
|
|
2791
|
-
})();
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
function executeReadFile(
|
|
2796
|
-
path: string,
|
|
2797
|
-
encoding: string,
|
|
2798
|
-
sessionId?: string
|
|
2799
|
-
): Promise<{
|
|
2800
|
-
success: boolean;
|
|
2801
|
-
exitCode: number;
|
|
2802
|
-
content: string;
|
|
2803
|
-
}> {
|
|
2804
|
-
return new Promise((resolve, reject) => {
|
|
2805
|
-
(async () => {
|
|
2806
|
-
try {
|
|
2807
|
-
// Read the file
|
|
2808
|
-
const content = await readFile(path, {
|
|
2809
|
-
encoding: encoding as BufferEncoding,
|
|
2810
|
-
});
|
|
2811
|
-
|
|
2812
|
-
console.log(`[Server] File read successfully: ${path}`);
|
|
2813
|
-
resolve({
|
|
2814
|
-
content,
|
|
2815
|
-
exitCode: 0,
|
|
2816
|
-
success: true,
|
|
2817
|
-
});
|
|
2818
|
-
} catch (error) {
|
|
2819
|
-
console.error(`[Server] Error reading file: ${path}`, error);
|
|
2820
|
-
reject(error);
|
|
2821
|
-
}
|
|
2822
|
-
})();
|
|
2823
|
-
});
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
function executeDeleteFile(
|
|
2827
|
-
path: string,
|
|
2828
|
-
sessionId?: string
|
|
2829
|
-
): Promise<{
|
|
2830
|
-
success: boolean;
|
|
2831
|
-
exitCode: number;
|
|
2832
|
-
}> {
|
|
2833
|
-
return new Promise((resolve, reject) => {
|
|
2834
|
-
(async () => {
|
|
2835
|
-
try {
|
|
2836
|
-
// Delete the file
|
|
2837
|
-
await unlink(path);
|
|
2838
|
-
|
|
2839
|
-
console.log(`[Server] File deleted successfully: ${path}`);
|
|
2840
|
-
resolve({
|
|
2841
|
-
exitCode: 0,
|
|
2842
|
-
success: true,
|
|
2843
|
-
});
|
|
2844
|
-
} catch (error) {
|
|
2845
|
-
console.error(`[Server] Error deleting file: ${path}`, error);
|
|
2846
|
-
reject(error);
|
|
2847
|
-
}
|
|
2848
|
-
})();
|
|
2849
|
-
});
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
function executeRenameFile(
|
|
2853
|
-
oldPath: string,
|
|
2854
|
-
newPath: string,
|
|
2855
|
-
sessionId?: string
|
|
2856
|
-
): Promise<{
|
|
2857
|
-
success: boolean;
|
|
2858
|
-
exitCode: number;
|
|
2859
|
-
}> {
|
|
2860
|
-
return new Promise((resolve, reject) => {
|
|
2861
|
-
(async () => {
|
|
2862
|
-
try {
|
|
2863
|
-
// Rename the file
|
|
2864
|
-
await rename(oldPath, newPath);
|
|
2865
|
-
|
|
2866
|
-
console.log(
|
|
2867
|
-
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
2868
|
-
);
|
|
2869
|
-
resolve({
|
|
2870
|
-
exitCode: 0,
|
|
2871
|
-
success: true,
|
|
2872
|
-
});
|
|
2873
|
-
} catch (error) {
|
|
2874
|
-
console.error(
|
|
2875
|
-
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
2876
|
-
error
|
|
2877
|
-
);
|
|
2878
|
-
reject(error);
|
|
2879
|
-
}
|
|
2880
|
-
})();
|
|
2881
|
-
});
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
function executeMoveFile(
|
|
2885
|
-
sourcePath: string,
|
|
2886
|
-
destinationPath: string,
|
|
2887
|
-
sessionId?: string
|
|
2888
|
-
): Promise<{
|
|
2889
|
-
success: boolean;
|
|
2890
|
-
exitCode: number;
|
|
2891
|
-
}> {
|
|
2892
|
-
return new Promise((resolve, reject) => {
|
|
2893
|
-
(async () => {
|
|
2894
|
-
try {
|
|
2895
|
-
// Move the file
|
|
2896
|
-
await rename(sourcePath, destinationPath);
|
|
2897
|
-
|
|
2898
|
-
console.log(
|
|
2899
|
-
`[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
|
|
2900
|
-
);
|
|
2901
|
-
resolve({
|
|
2902
|
-
exitCode: 0,
|
|
2903
|
-
success: true,
|
|
2904
|
-
});
|
|
2905
|
-
} catch (error) {
|
|
2906
|
-
console.error(
|
|
2907
|
-
`[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
|
|
2908
|
-
error
|
|
2909
|
-
);
|
|
2910
|
-
reject(error);
|
|
2911
|
-
}
|
|
2912
|
-
})();
|
|
2913
|
-
});
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
async function handleExposePortRequest(
|
|
2917
|
-
req: Request,
|
|
2918
|
-
corsHeaders: Record<string, string>
|
|
2919
|
-
): Promise<Response> {
|
|
2920
|
-
try {
|
|
2921
|
-
const body = (await req.json()) as ExposePortRequest;
|
|
2922
|
-
const { port, name } = body;
|
|
2923
|
-
|
|
2924
|
-
if (!port || typeof port !== "number") {
|
|
2925
|
-
return new Response(
|
|
2926
|
-
JSON.stringify({
|
|
2927
|
-
error: "Port is required and must be a number",
|
|
2928
|
-
}),
|
|
2929
|
-
{
|
|
2930
|
-
headers: {
|
|
2931
|
-
"Content-Type": "application/json",
|
|
2932
|
-
...corsHeaders,
|
|
2933
|
-
},
|
|
2934
|
-
status: 400,
|
|
2935
|
-
}
|
|
2936
|
-
);
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
// Validate port range
|
|
2940
|
-
if (port < 1 || port > 65535) {
|
|
2941
|
-
return new Response(
|
|
2942
|
-
JSON.stringify({
|
|
2943
|
-
error: "Port must be between 1 and 65535",
|
|
2944
|
-
}),
|
|
2945
|
-
{
|
|
2946
|
-
headers: {
|
|
2947
|
-
"Content-Type": "application/json",
|
|
2948
|
-
...corsHeaders,
|
|
2949
|
-
},
|
|
2950
|
-
status: 400,
|
|
2951
|
-
}
|
|
2952
|
-
);
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
// Store the exposed port
|
|
2956
|
-
exposedPorts.set(port, { name, exposedAt: new Date() });
|
|
2957
|
-
|
|
2958
|
-
console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
|
|
2959
|
-
|
|
2960
|
-
return new Response(
|
|
2961
|
-
JSON.stringify({
|
|
2962
|
-
port,
|
|
2963
|
-
name,
|
|
2964
|
-
exposedAt: new Date().toISOString(),
|
|
2965
|
-
success: true,
|
|
2966
|
-
timestamp: new Date().toISOString(),
|
|
2967
|
-
}),
|
|
2968
|
-
{
|
|
2969
|
-
headers: {
|
|
2970
|
-
"Content-Type": "application/json",
|
|
2971
|
-
...corsHeaders,
|
|
2972
|
-
},
|
|
2973
|
-
}
|
|
2974
|
-
);
|
|
2975
|
-
} catch (error) {
|
|
2976
|
-
console.error("[Server] Error in handleExposePortRequest:", error);
|
|
2977
|
-
return new Response(
|
|
2978
|
-
JSON.stringify({
|
|
2979
|
-
error: "Failed to expose port",
|
|
2980
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
2981
|
-
}),
|
|
2982
|
-
{
|
|
2983
|
-
headers: {
|
|
2984
|
-
"Content-Type": "application/json",
|
|
2985
|
-
...corsHeaders,
|
|
2986
|
-
},
|
|
2987
|
-
status: 500,
|
|
2988
|
-
}
|
|
2989
|
-
);
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
async function handleUnexposePortRequest(
|
|
2994
|
-
req: Request,
|
|
2995
|
-
corsHeaders: Record<string, string>
|
|
2996
|
-
): Promise<Response> {
|
|
2997
|
-
try {
|
|
2998
|
-
const body = (await req.json()) as UnexposePortRequest;
|
|
2999
|
-
const { port } = body;
|
|
3000
|
-
|
|
3001
|
-
if (!port || typeof port !== "number") {
|
|
3002
|
-
return new Response(
|
|
3003
|
-
JSON.stringify({
|
|
3004
|
-
error: "Port is required and must be a number",
|
|
3005
|
-
}),
|
|
3006
|
-
{
|
|
3007
|
-
headers: {
|
|
3008
|
-
"Content-Type": "application/json",
|
|
3009
|
-
...corsHeaders,
|
|
3010
|
-
},
|
|
3011
|
-
status: 400,
|
|
3012
|
-
}
|
|
3013
|
-
);
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
// Check if port is exposed
|
|
3017
|
-
if (!exposedPorts.has(port)) {
|
|
3018
|
-
return new Response(
|
|
3019
|
-
JSON.stringify({
|
|
3020
|
-
error: "Port is not exposed",
|
|
3021
|
-
}),
|
|
3022
|
-
{
|
|
3023
|
-
headers: {
|
|
3024
|
-
"Content-Type": "application/json",
|
|
3025
|
-
...corsHeaders,
|
|
3026
|
-
},
|
|
3027
|
-
status: 404,
|
|
3028
|
-
}
|
|
3029
|
-
);
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
// Remove the exposed port
|
|
3033
|
-
exposedPorts.delete(port);
|
|
3034
|
-
|
|
3035
|
-
console.log(`[Server] Unexposed port: ${port}`);
|
|
3036
|
-
|
|
3037
|
-
return new Response(
|
|
3038
|
-
JSON.stringify({
|
|
3039
|
-
port,
|
|
3040
|
-
success: true,
|
|
3041
|
-
timestamp: new Date().toISOString(),
|
|
3042
|
-
}),
|
|
3043
|
-
{
|
|
3044
|
-
headers: {
|
|
3045
|
-
"Content-Type": "application/json",
|
|
3046
|
-
...corsHeaders,
|
|
3047
|
-
},
|
|
3048
|
-
}
|
|
3049
|
-
);
|
|
3050
|
-
} catch (error) {
|
|
3051
|
-
console.error("[Server] Error in handleUnexposePortRequest:", error);
|
|
3052
|
-
return new Response(
|
|
3053
|
-
JSON.stringify({
|
|
3054
|
-
error: "Failed to unexpose port",
|
|
3055
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
3056
|
-
}),
|
|
3057
|
-
{
|
|
3058
|
-
headers: {
|
|
3059
|
-
"Content-Type": "application/json",
|
|
3060
|
-
...corsHeaders,
|
|
3061
|
-
},
|
|
3062
|
-
status: 500,
|
|
3063
|
-
}
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
|
-
async function handleGetExposedPortsRequest(
|
|
3069
|
-
req: Request,
|
|
3070
|
-
corsHeaders: Record<string, string>
|
|
3071
|
-
): Promise<Response> {
|
|
3072
|
-
try {
|
|
3073
|
-
const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
|
|
3074
|
-
port,
|
|
3075
|
-
name: info.name,
|
|
3076
|
-
exposedAt: info.exposedAt.toISOString(),
|
|
3077
|
-
}));
|
|
3078
|
-
|
|
3079
|
-
return new Response(
|
|
3080
|
-
JSON.stringify({
|
|
3081
|
-
ports,
|
|
3082
|
-
count: ports.length,
|
|
3083
|
-
timestamp: new Date().toISOString(),
|
|
3084
|
-
}),
|
|
3085
|
-
{
|
|
3086
|
-
headers: {
|
|
3087
|
-
"Content-Type": "application/json",
|
|
3088
|
-
...corsHeaders,
|
|
3089
|
-
},
|
|
3090
|
-
}
|
|
3091
|
-
);
|
|
3092
|
-
} catch (error) {
|
|
3093
|
-
console.error("[Server] Error in handleGetExposedPortsRequest:", error);
|
|
3094
|
-
return new Response(
|
|
3095
|
-
JSON.stringify({
|
|
3096
|
-
error: "Failed to get exposed ports",
|
|
3097
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
3098
|
-
}),
|
|
3099
|
-
{
|
|
3100
|
-
headers: {
|
|
3101
|
-
"Content-Type": "application/json",
|
|
3102
|
-
...corsHeaders,
|
|
3103
|
-
},
|
|
3104
|
-
status: 500,
|
|
3105
|
-
}
|
|
3106
|
-
);
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
|
-
async function handleProxyRequest(
|
|
3111
|
-
req: Request,
|
|
3112
|
-
corsHeaders: Record<string, string>
|
|
3113
|
-
): Promise<Response> {
|
|
3114
|
-
try {
|
|
3115
|
-
const url = new URL(req.url);
|
|
3116
|
-
const pathParts = url.pathname.split("/");
|
|
3117
|
-
|
|
3118
|
-
// Extract port from path like /proxy/3000/...
|
|
3119
|
-
if (pathParts.length < 3) {
|
|
3120
|
-
return new Response(
|
|
3121
|
-
JSON.stringify({
|
|
3122
|
-
error: "Invalid proxy path",
|
|
3123
|
-
}),
|
|
3124
|
-
{
|
|
3125
|
-
headers: {
|
|
3126
|
-
"Content-Type": "application/json",
|
|
3127
|
-
...corsHeaders,
|
|
3128
|
-
},
|
|
3129
|
-
status: 400,
|
|
3130
|
-
}
|
|
3131
|
-
);
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
const port = parseInt(pathParts[2]);
|
|
3135
|
-
if (!port || Number.isNaN(port)) {
|
|
3136
|
-
return new Response(
|
|
3137
|
-
JSON.stringify({
|
|
3138
|
-
error: "Invalid port in proxy path",
|
|
3139
|
-
}),
|
|
3140
|
-
{
|
|
3141
|
-
headers: {
|
|
3142
|
-
"Content-Type": "application/json",
|
|
3143
|
-
...corsHeaders,
|
|
3144
|
-
},
|
|
3145
|
-
status: 400,
|
|
3146
|
-
}
|
|
3147
|
-
);
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
// Check if port is exposed
|
|
3151
|
-
if (!exposedPorts.has(port)) {
|
|
3152
|
-
return new Response(
|
|
3153
|
-
JSON.stringify({
|
|
3154
|
-
error: `Port ${port} is not exposed`,
|
|
3155
|
-
}),
|
|
3156
|
-
{
|
|
3157
|
-
headers: {
|
|
3158
|
-
"Content-Type": "application/json",
|
|
3159
|
-
...corsHeaders,
|
|
3160
|
-
},
|
|
3161
|
-
status: 404,
|
|
3162
|
-
}
|
|
3163
|
-
);
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
// Construct the target URL
|
|
3167
|
-
const targetPath = `/${pathParts.slice(3).join("/")}`;
|
|
3168
|
-
// Use 127.0.0.1 instead of localhost for more reliable container networking
|
|
3169
|
-
const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
|
|
3170
|
-
|
|
3171
|
-
console.log(`[Server] Proxying request to: ${targetUrl}`);
|
|
3172
|
-
console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
|
|
3173
|
-
|
|
3174
|
-
try {
|
|
3175
|
-
// Forward the request to the target port
|
|
3176
|
-
const targetResponse = await fetch(targetUrl, {
|
|
3177
|
-
method: req.method,
|
|
3178
|
-
headers: req.headers,
|
|
3179
|
-
body: req.body,
|
|
3180
|
-
});
|
|
3181
|
-
|
|
3182
|
-
// Return the response from the target
|
|
3183
|
-
return new Response(targetResponse.body, {
|
|
3184
|
-
status: targetResponse.status,
|
|
3185
|
-
statusText: targetResponse.statusText,
|
|
3186
|
-
headers: {
|
|
3187
|
-
...Object.fromEntries(targetResponse.headers.entries()),
|
|
3188
|
-
...corsHeaders,
|
|
3189
|
-
},
|
|
3190
|
-
});
|
|
3191
|
-
} catch (fetchError) {
|
|
3192
|
-
console.error(`[Server] Error proxying to port ${port}:`, fetchError);
|
|
3193
|
-
return new Response(
|
|
3194
|
-
JSON.stringify({
|
|
3195
|
-
error: `Service on port ${port} is not responding`,
|
|
3196
|
-
message: fetchError instanceof Error ? fetchError.message : "Unknown error",
|
|
3197
|
-
}),
|
|
3198
|
-
{
|
|
3199
|
-
headers: {
|
|
3200
|
-
"Content-Type": "application/json",
|
|
3201
|
-
...corsHeaders,
|
|
3202
|
-
},
|
|
3203
|
-
status: 502,
|
|
3204
|
-
}
|
|
3205
|
-
);
|
|
3206
|
-
}
|
|
3207
|
-
} catch (error) {
|
|
3208
|
-
console.error("[Server] Error in handleProxyRequest:", error);
|
|
3209
|
-
return new Response(
|
|
3210
|
-
JSON.stringify({
|
|
3211
|
-
error: "Failed to proxy request",
|
|
3212
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
3213
|
-
}),
|
|
3214
|
-
{
|
|
3215
|
-
headers: {
|
|
3216
|
-
"Content-Type": "application/json",
|
|
3217
|
-
...corsHeaders,
|
|
3218
|
-
},
|
|
3219
|
-
status: 500,
|
|
3220
|
-
}
|
|
3221
|
-
);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
|
|
3225
336
|
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
3226
337
|
console.log(`📡 HTTP API endpoints available:`);
|
|
3227
338
|
console.log(` POST /api/session/create - Create a new session`);
|
|
@@ -3229,24 +340,22 @@ console.log(` GET /api/session/list - List all sessions`);
|
|
|
3229
340
|
console.log(` POST /api/execute - Execute a command (non-streaming)`);
|
|
3230
341
|
console.log(` POST /api/execute/stream - Execute a command (streaming)`);
|
|
3231
342
|
console.log(` POST /api/git/checkout - Checkout a git repository`);
|
|
3232
|
-
console.log(
|
|
3233
|
-
` POST /api/git/checkout/stream - Checkout a git repository (streaming)`
|
|
3234
|
-
);
|
|
3235
343
|
console.log(` POST /api/mkdir - Create a directory`);
|
|
3236
|
-
console.log(` POST /api/mkdir/stream - Create a directory (streaming)`);
|
|
3237
344
|
console.log(` POST /api/write - Write a file`);
|
|
3238
|
-
console.log(` POST /api/write/stream - Write a file (streaming)`);
|
|
3239
345
|
console.log(` POST /api/read - Read a file`);
|
|
3240
|
-
console.log(` POST /api/read/stream - Read a file (streaming)`);
|
|
3241
346
|
console.log(` POST /api/delete - Delete a file`);
|
|
3242
|
-
console.log(` POST /api/delete/stream - Delete a file (streaming)`);
|
|
3243
347
|
console.log(` POST /api/rename - Rename a file`);
|
|
3244
|
-
console.log(` POST /api/rename/stream - Rename a file (streaming)`);
|
|
3245
348
|
console.log(` POST /api/move - Move a file`);
|
|
3246
|
-
console.log(` POST /api/move/stream - Move a file (streaming)`);
|
|
3247
349
|
console.log(` POST /api/expose-port - Expose a port for external access`);
|
|
3248
350
|
console.log(` DELETE /api/unexpose-port - Unexpose a port`);
|
|
3249
351
|
console.log(` GET /api/exposed-ports - List exposed ports`);
|
|
352
|
+
console.log(` POST /api/process/start - Start a background process`);
|
|
353
|
+
console.log(` GET /api/process/list - List all processes`);
|
|
354
|
+
console.log(` GET /api/process/{id} - Get process status`);
|
|
355
|
+
console.log(` DELETE /api/process/{id} - Kill a process`);
|
|
356
|
+
console.log(` GET /api/process/{id}/logs - Get process logs`);
|
|
357
|
+
console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
|
|
358
|
+
console.log(` DELETE /api/process/kill-all - Kill all processes`);
|
|
3250
359
|
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
3251
360
|
console.log(` GET /api/ping - Health check`);
|
|
3252
361
|
console.log(` GET /api/commands - List available commands`);
|