@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/Dockerfile +1 -14
  3. package/container_src/handler/exec.ts +337 -0
  4. package/container_src/handler/file.ts +844 -0
  5. package/container_src/handler/git.ts +182 -0
  6. package/container_src/handler/ports.ts +314 -0
  7. package/container_src/handler/process.ts +640 -0
  8. package/container_src/index.ts +82 -2973
  9. package/container_src/types.ts +103 -0
  10. package/dist/chunk-6THNBO4S.js +46 -0
  11. package/dist/chunk-6THNBO4S.js.map +1 -0
  12. package/dist/chunk-6UAWTJ5S.js +85 -0
  13. package/dist/chunk-6UAWTJ5S.js.map +1 -0
  14. package/dist/chunk-G4XT4SP7.js +638 -0
  15. package/dist/chunk-G4XT4SP7.js.map +1 -0
  16. package/dist/chunk-ISFOIYQC.js +585 -0
  17. package/dist/chunk-ISFOIYQC.js.map +1 -0
  18. package/dist/chunk-NNGBXDMY.js +89 -0
  19. package/dist/chunk-NNGBXDMY.js.map +1 -0
  20. package/dist/client-Da-mLX4p.d.ts +210 -0
  21. package/dist/client.d.ts +2 -1
  22. package/dist/client.js +3 -37
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.js +13 -3
  25. package/dist/request-handler.d.ts +2 -1
  26. package/dist/request-handler.js +4 -2
  27. package/dist/sandbox.d.ts +2 -1
  28. package/dist/sandbox.js +4 -2
  29. package/dist/security.d.ts +30 -0
  30. package/dist/security.js +13 -0
  31. package/dist/security.js.map +1 -0
  32. package/dist/sse-parser.d.ts +28 -0
  33. package/dist/sse-parser.js +11 -0
  34. package/dist/sse-parser.js.map +1 -0
  35. package/dist/types.d.ts +284 -0
  36. package/dist/types.js +19 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +2 -7
  39. package/src/client.ts +235 -1286
  40. package/src/index.ts +6 -0
  41. package/src/request-handler.ts +69 -20
  42. package/src/sandbox.ts +463 -70
  43. package/src/security.ts +113 -0
  44. package/src/sse-parser.ts +147 -0
  45. package/src/types.ts +386 -0
  46. package/README.md +0 -65
  47. package/dist/chunk-4J5LQCCN.js +0 -1446
  48. package/dist/chunk-4J5LQCCN.js.map +0 -1
  49. package/dist/chunk-5SZ3RVJZ.js +0 -250
  50. package/dist/chunk-5SZ3RVJZ.js.map +0 -1
  51. package/dist/client-BuVjqV00.d.ts +0 -247
  52. package/tests/client.example.ts +0 -308
  53. package/tests/connection-test.ts +0 -81
  54. package/tests/simple-test.ts +0 -81
  55. package/tests/test1.ts +0 -281
  56. package/tests/test2.ts +0 -929
@@ -1,72 +1,31 @@
1
- import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
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
- interface ExecuteRequest {
7
- command: string;
8
- args?: string[];
9
- sessionId?: string;
10
- background?: boolean;
11
- }
12
-
13
- interface GitCheckoutRequest {
14
- repoUrl: string;
15
- branch?: string;
16
- targetDir?: string;
17
- sessionId?: string;
18
- }
19
-
20
- interface MkdirRequest {
21
- path: string;
22
- recursive?: boolean;
23
- sessionId?: string;
24
- }
25
-
26
- interface WriteFileRequest {
27
- path: string;
28
- content: string;
29
- encoding?: string;
30
- sessionId?: string;
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
- // Generate a unique session ID
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()}_${Math.random().toString(36).substr(2, 9)}`;
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/delete/stream":
235
+ case "/api/rename":
298
236
  if (req.method === "POST") {
299
- return handleStreamingDeleteFileRequest(req, corsHeaders);
237
+ return handleRenameFileRequest(req, corsHeaders);
300
238
  }
301
239
  break;
302
240
 
303
- case "/api/rename":
241
+ case "/api/move":
304
242
  if (req.method === "POST") {
305
- return handleRenameFileRequest(req, corsHeaders);
243
+ return handleMoveFileRequest(req, corsHeaders);
306
244
  }
307
245
  break;
308
246
 
309
- case "/api/rename/stream":
247
+ case "/api/expose-port":
310
248
  if (req.method === "POST") {
311
- return handleStreamingRenameFileRequest(req, corsHeaders);
249
+ return handleExposePortRequest(exposedPorts, req, corsHeaders);
312
250
  }
313
251
  break;
314
252
 
315
- case "/api/move":
316
- if (req.method === "POST") {
317
- return handleMoveFileRequest(req, corsHeaders);
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/move/stream":
322
- if (req.method === "POST") {
323
- return handleStreamingMoveFileRequest(req, corsHeaders);
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/expose-port":
265
+ case "/api/process/start":
328
266
  if (req.method === "POST") {
329
- return handleExposePortRequest(req, corsHeaders);
267
+ return handleStartProcessRequest(processes, req, corsHeaders);
330
268
  }
331
269
  break;
332
270
 
333
- case "/api/unexpose-port":
334
- if (req.method === "DELETE") {
335
- return handleUnexposePortRequest(req, corsHeaders);
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/exposed-ports":
340
- if (req.method === "GET") {
341
- return handleGetExposedPortsRequest(req, corsHeaders);
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`);