@cloudflare/sandbox 0.0.0-6d1b288 → 0.0.0-6f16c81

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.
@@ -1,11 +1,13 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
1
+ import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { serve } from "bun";
5
5
 
6
6
  interface ExecuteRequest {
7
7
  command: string;
8
8
  args?: string[];
9
+ sessionId?: string;
10
+ background?: boolean;
9
11
  }
10
12
 
11
13
  interface GitCheckoutRequest {
@@ -28,6 +30,12 @@ interface WriteFileRequest {
28
30
  sessionId?: string;
29
31
  }
30
32
 
33
+ interface ReadFileRequest {
34
+ path: string;
35
+ encoding?: string;
36
+ sessionId?: string;
37
+ }
38
+
31
39
  interface DeleteFileRequest {
32
40
  path: string;
33
41
  sessionId?: string;
@@ -45,15 +53,27 @@ interface MoveFileRequest {
45
53
  sessionId?: string;
46
54
  }
47
55
 
56
+ interface ExposePortRequest {
57
+ port: number;
58
+ name?: string;
59
+ }
60
+
61
+ interface UnexposePortRequest {
62
+ port: number;
63
+ }
64
+
48
65
  interface SessionData {
49
66
  sessionId: string;
50
- activeProcess: any | null;
67
+ activeProcess: ChildProcess | null;
51
68
  createdAt: Date;
52
69
  }
53
70
 
54
71
  // In-memory session storage (in production, you'd want to use a proper database)
55
72
  const sessions = new Map<string, SessionData>();
56
73
 
74
+ // In-memory storage for exposed ports
75
+ const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
76
+
57
77
  // Generate a unique session ID
58
78
  function generateSessionId(): string {
59
79
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -78,6 +98,8 @@ const server = serve({
78
98
  const url = new URL(req.url);
79
99
  const pathname = url.pathname;
80
100
 
101
+ console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
102
+
81
103
  // Handle CORS
82
104
  const corsHeaders = {
83
105
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
@@ -87,11 +109,13 @@ const server = serve({
87
109
 
88
110
  // Handle preflight requests
89
111
  if (req.method === "OPTIONS") {
112
+ console.log(`[Container] Handling CORS preflight for ${pathname}`);
90
113
  return new Response(null, { headers: corsHeaders, status: 200 });
91
114
  }
92
115
 
93
116
  try {
94
117
  // Handle different routes
118
+ console.log(`[Container] Processing ${req.method} ${pathname}`);
95
119
  switch (pathname) {
96
120
  case "/":
97
121
  return new Response("Hello from Bun server! 🚀", {
@@ -101,51 +125,6 @@ const server = serve({
101
125
  },
102
126
  });
103
127
 
104
- case "/api/hello":
105
- return new Response(
106
- JSON.stringify({
107
- message: "Hello from API!",
108
- timestamp: new Date().toISOString(),
109
- }),
110
- {
111
- headers: {
112
- "Content-Type": "application/json",
113
- ...corsHeaders,
114
- },
115
- }
116
- );
117
-
118
- case "/api/users":
119
- if (req.method === "GET") {
120
- return new Response(
121
- JSON.stringify([
122
- { id: 1, name: "Alice" },
123
- { id: 2, name: "Bob" },
124
- { id: 3, name: "Charlie" },
125
- ]),
126
- {
127
- headers: {
128
- "Content-Type": "application/json",
129
- ...corsHeaders,
130
- },
131
- }
132
- );
133
- } else if (req.method === "POST") {
134
- return new Response(
135
- JSON.stringify({
136
- message: "User created successfully",
137
- method: "POST",
138
- }),
139
- {
140
- headers: {
141
- "Content-Type": "application/json",
142
- ...corsHeaders,
143
- },
144
- }
145
- );
146
- }
147
- break;
148
-
149
128
  case "/api/session/create":
150
129
  if (req.method === "POST") {
151
130
  const sessionId = generateSessionId();
@@ -297,6 +276,18 @@ const server = serve({
297
276
  }
298
277
  break;
299
278
 
279
+ case "/api/read":
280
+ if (req.method === "POST") {
281
+ return handleReadFileRequest(req, corsHeaders);
282
+ }
283
+ break;
284
+
285
+ case "/api/read/stream":
286
+ if (req.method === "POST") {
287
+ return handleStreamingReadFileRequest(req, corsHeaders);
288
+ }
289
+ break;
290
+
300
291
  case "/api/delete":
301
292
  if (req.method === "POST") {
302
293
  return handleDeleteFileRequest(req, corsHeaders);
@@ -333,14 +324,38 @@ const server = serve({
333
324
  }
334
325
  break;
335
326
 
327
+ case "/api/expose-port":
328
+ if (req.method === "POST") {
329
+ return handleExposePortRequest(req, corsHeaders);
330
+ }
331
+ break;
332
+
333
+ case "/api/unexpose-port":
334
+ if (req.method === "DELETE") {
335
+ return handleUnexposePortRequest(req, corsHeaders);
336
+ }
337
+ break;
338
+
339
+ case "/api/exposed-ports":
340
+ if (req.method === "GET") {
341
+ return handleGetExposedPortsRequest(req, corsHeaders);
342
+ }
343
+ break;
344
+
336
345
  default:
346
+ // Check if this is a proxy request for an exposed port
347
+ if (pathname.startsWith("/proxy/")) {
348
+ return handleProxyRequest(req, corsHeaders);
349
+ }
350
+
351
+ console.log(`[Container] Route not found: ${pathname}`);
337
352
  return new Response("Not Found", {
338
353
  headers: corsHeaders,
339
354
  status: 404,
340
355
  });
341
356
  }
342
357
  } catch (error) {
343
- console.error("[Server] Error handling request:", error);
358
+ console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
344
359
  return new Response(
345
360
  JSON.stringify({
346
361
  error: "Internal server error",
@@ -356,16 +371,19 @@ const server = serve({
356
371
  );
357
372
  }
358
373
  },
374
+ hostname: "0.0.0.0",
359
375
  port: 3000,
360
- } as any);
376
+ // We don't need this, but typescript complains
377
+ websocket: { async message() { } },
378
+ });
361
379
 
362
380
  async function handleExecuteRequest(
363
381
  req: Request,
364
382
  corsHeaders: Record<string, string>
365
383
  ): Promise<Response> {
366
384
  try {
367
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
368
- const { command, args = [], sessionId } = body;
385
+ const body = (await req.json()) as ExecuteRequest;
386
+ const { command, args = [], sessionId, background } = body;
369
387
 
370
388
  if (!command || typeof command !== "string") {
371
389
  return new Response(
@@ -412,7 +430,7 @@ async function handleExecuteRequest(
412
430
 
413
431
  console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
414
432
 
415
- const result = await executeCommand(command, args, sessionId);
433
+ const result = await executeCommand(command, args, sessionId, background);
416
434
 
417
435
  return new Response(
418
436
  JSON.stringify({
@@ -454,8 +472,8 @@ async function handleStreamingExecuteRequest(
454
472
  corsHeaders: Record<string, string>
455
473
  ): Promise<Response> {
456
474
  try {
457
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
458
- const { command, args = [], sessionId } = body;
475
+ const body = (await req.json()) as ExecuteRequest;
476
+ const { command, args = [], sessionId, background } = body;
459
477
 
460
478
  if (!command || typeof command !== "string") {
461
479
  return new Response(
@@ -506,10 +524,13 @@ async function handleStreamingExecuteRequest(
506
524
 
507
525
  const stream = new ReadableStream({
508
526
  start(controller) {
509
- const child = spawn(command, args, {
527
+ const spawnOptions: SpawnOptions = {
510
528
  shell: true,
511
- stdio: ["pipe", "pipe", "pipe"],
512
- });
529
+ stdio: ["pipe", "pipe", "pipe"] as const,
530
+ detached: background || false,
531
+ };
532
+
533
+ const child = spawn(command, args, spawnOptions);
513
534
 
514
535
  // Store the process reference for cleanup if sessionId is provided
515
536
  if (sessionId && sessions.has(sessionId)) {
@@ -517,6 +538,11 @@ async function handleStreamingExecuteRequest(
517
538
  session.activeProcess = child;
518
539
  }
519
540
 
541
+ // For background processes, unref to prevent blocking
542
+ if (background) {
543
+ child.unref();
544
+ }
545
+
520
546
  let stdout = "";
521
547
  let stderr = "";
522
548
 
@@ -528,6 +554,7 @@ async function handleStreamingExecuteRequest(
528
554
  command,
529
555
  timestamp: new Date().toISOString(),
530
556
  type: "command_start",
557
+ background: background || false,
531
558
  })}\n\n`
532
559
  )
533
560
  );
@@ -593,7 +620,11 @@ async function handleStreamingExecuteRequest(
593
620
  )
594
621
  );
595
622
 
596
- controller.close();
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
+ }
597
628
  });
598
629
 
599
630
  child.on("error", (error) => {
@@ -1476,13 +1507,13 @@ async function handleStreamingWriteFileRequest(
1476
1507
  }
1477
1508
  }
1478
1509
 
1479
- async function handleDeleteFileRequest(
1510
+ async function handleReadFileRequest(
1480
1511
  req: Request,
1481
1512
  corsHeaders: Record<string, string>
1482
1513
  ): Promise<Response> {
1483
1514
  try {
1484
- const body = (await req.json()) as DeleteFileRequest;
1485
- const { path, sessionId } = body;
1515
+ const body = (await req.json()) as ReadFileRequest;
1516
+ const { path, encoding = "utf-8", sessionId } = body;
1486
1517
 
1487
1518
  if (!path || typeof path !== "string") {
1488
1519
  return new Response(
@@ -1530,12 +1561,13 @@ async function handleDeleteFileRequest(
1530
1561
  );
1531
1562
  }
1532
1563
 
1533
- console.log(`[Server] Deleting file: ${path}`);
1564
+ console.log(`[Server] Reading file: ${path}`);
1534
1565
 
1535
- const result = await executeDeleteFile(path, sessionId);
1566
+ const result = await executeReadFile(path, encoding, sessionId);
1536
1567
 
1537
1568
  return new Response(
1538
1569
  JSON.stringify({
1570
+ content: result.content,
1539
1571
  exitCode: result.exitCode,
1540
1572
  path,
1541
1573
  success: result.success,
@@ -1549,10 +1581,10 @@ async function handleDeleteFileRequest(
1549
1581
  }
1550
1582
  );
1551
1583
  } catch (error) {
1552
- console.error("[Server] Error in handleDeleteFileRequest:", error);
1584
+ console.error("[Server] Error in handleReadFileRequest:", error);
1553
1585
  return new Response(
1554
1586
  JSON.stringify({
1555
- error: "Failed to delete file",
1587
+ error: "Failed to read file",
1556
1588
  message: error instanceof Error ? error.message : "Unknown error",
1557
1589
  }),
1558
1590
  {
@@ -1566,13 +1598,13 @@ async function handleDeleteFileRequest(
1566
1598
  }
1567
1599
  }
1568
1600
 
1569
- async function handleStreamingDeleteFileRequest(
1601
+ async function handleStreamingReadFileRequest(
1570
1602
  req: Request,
1571
1603
  corsHeaders: Record<string, string>
1572
1604
  ): Promise<Response> {
1573
1605
  try {
1574
- const body = (await req.json()) as DeleteFileRequest;
1575
- const { path, sessionId } = body;
1606
+ const body = (await req.json()) as ReadFileRequest;
1607
+ const { path, encoding = "utf-8", sessionId } = body;
1576
1608
 
1577
1609
  if (!path || typeof path !== "string") {
1578
1610
  return new Response(
@@ -1620,7 +1652,7 @@ async function handleStreamingDeleteFileRequest(
1620
1652
  );
1621
1653
  }
1622
1654
 
1623
- console.log(`[Server] Deleting file (streaming): ${path}`);
1655
+ console.log(`[Server] Reading file (streaming): ${path}`);
1624
1656
 
1625
1657
  const stream = new ReadableStream({
1626
1658
  start(controller) {
@@ -1637,15 +1669,18 @@ async function handleStreamingDeleteFileRequest(
1637
1669
  )
1638
1670
  );
1639
1671
 
1640
- // Delete the file
1641
- await executeDeleteFile(path, sessionId);
1672
+ // Read the file
1673
+ const content = await readFile(path, {
1674
+ encoding: encoding as BufferEncoding,
1675
+ });
1642
1676
 
1643
- console.log(`[Server] File deleted successfully: ${path}`);
1677
+ console.log(`[Server] File read successfully: ${path}`);
1644
1678
 
1645
1679
  // Send command completion event
1646
1680
  controller.enqueue(
1647
1681
  new TextEncoder().encode(
1648
1682
  `data: ${JSON.stringify({
1683
+ content,
1649
1684
  path,
1650
1685
  success: true,
1651
1686
  timestamp: new Date().toISOString(),
@@ -1656,7 +1691,7 @@ async function handleStreamingDeleteFileRequest(
1656
1691
 
1657
1692
  controller.close();
1658
1693
  } catch (error) {
1659
- console.error(`[Server] Error deleting file: ${path}`, error);
1694
+ console.error(`[Server] Error reading file: ${path}`, error);
1660
1695
 
1661
1696
  controller.enqueue(
1662
1697
  new TextEncoder().encode(
@@ -1684,10 +1719,10 @@ async function handleStreamingDeleteFileRequest(
1684
1719
  },
1685
1720
  });
1686
1721
  } catch (error) {
1687
- console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
1722
+ console.error("[Server] Error in handleStreamingReadFileRequest:", error);
1688
1723
  return new Response(
1689
1724
  JSON.stringify({
1690
- error: "Failed to delete file",
1725
+ error: "Failed to read file",
1691
1726
  message: error instanceof Error ? error.message : "Unknown error",
1692
1727
  }),
1693
1728
  {
@@ -1701,33 +1736,18 @@ async function handleStreamingDeleteFileRequest(
1701
1736
  }
1702
1737
  }
1703
1738
 
1704
- async function handleRenameFileRequest(
1739
+ async function handleDeleteFileRequest(
1705
1740
  req: Request,
1706
1741
  corsHeaders: Record<string, string>
1707
1742
  ): Promise<Response> {
1708
1743
  try {
1709
- const body = (await req.json()) as RenameFileRequest;
1710
- const { oldPath, newPath, sessionId } = body;
1711
-
1712
- if (!oldPath || typeof oldPath !== "string") {
1713
- return new Response(
1714
- JSON.stringify({
1715
- error: "Old path is required and must be a string",
1716
- }),
1717
- {
1718
- headers: {
1719
- "Content-Type": "application/json",
1720
- ...corsHeaders,
1721
- },
1722
- status: 400,
1723
- }
1724
- );
1725
- }
1744
+ const body = (await req.json()) as DeleteFileRequest;
1745
+ const { path, sessionId } = body;
1726
1746
 
1727
- if (!newPath || typeof newPath !== "string") {
1747
+ if (!path || typeof path !== "string") {
1728
1748
  return new Response(
1729
1749
  JSON.stringify({
1730
- error: "New path is required and must be a string",
1750
+ error: "Path is required and must be a string",
1731
1751
  }),
1732
1752
  {
1733
1753
  headers: {
@@ -1755,11 +1775,7 @@ async function handleRenameFileRequest(
1755
1775
  /\.\./, // Path traversal attempts
1756
1776
  ];
1757
1777
 
1758
- if (
1759
- dangerousPatterns.some(
1760
- (pattern) => pattern.test(oldPath) || pattern.test(newPath)
1761
- )
1762
- ) {
1778
+ if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1763
1779
  return new Response(
1764
1780
  JSON.stringify({
1765
1781
  error: "Dangerous path not allowed",
@@ -1774,15 +1790,14 @@ async function handleRenameFileRequest(
1774
1790
  );
1775
1791
  }
1776
1792
 
1777
- console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
1793
+ console.log(`[Server] Deleting file: ${path}`);
1778
1794
 
1779
- const result = await executeRenameFile(oldPath, newPath, sessionId);
1795
+ const result = await executeDeleteFile(path, sessionId);
1780
1796
 
1781
1797
  return new Response(
1782
1798
  JSON.stringify({
1783
1799
  exitCode: result.exitCode,
1784
- newPath,
1785
- oldPath,
1800
+ path,
1786
1801
  success: result.success,
1787
1802
  timestamp: new Date().toISOString(),
1788
1803
  }),
@@ -1794,10 +1809,10 @@ async function handleRenameFileRequest(
1794
1809
  }
1795
1810
  );
1796
1811
  } catch (error) {
1797
- console.error("[Server] Error in handleRenameFileRequest:", error);
1812
+ console.error("[Server] Error in handleDeleteFileRequest:", error);
1798
1813
  return new Response(
1799
1814
  JSON.stringify({
1800
- error: "Failed to rename file",
1815
+ error: "Failed to delete file",
1801
1816
  message: error instanceof Error ? error.message : "Unknown error",
1802
1817
  }),
1803
1818
  {
@@ -1811,33 +1826,18 @@ async function handleRenameFileRequest(
1811
1826
  }
1812
1827
  }
1813
1828
 
1814
- async function handleStreamingRenameFileRequest(
1829
+ async function handleStreamingDeleteFileRequest(
1815
1830
  req: Request,
1816
1831
  corsHeaders: Record<string, string>
1817
1832
  ): Promise<Response> {
1818
1833
  try {
1819
- const body = (await req.json()) as RenameFileRequest;
1820
- const { oldPath, newPath, sessionId } = body;
1821
-
1822
- if (!oldPath || typeof oldPath !== "string") {
1823
- return new Response(
1824
- JSON.stringify({
1825
- error: "Old path is required and must be a string",
1826
- }),
1827
- {
1828
- headers: {
1829
- "Content-Type": "application/json",
1830
- ...corsHeaders,
1831
- },
1832
- status: 400,
1833
- }
1834
- );
1835
- }
1834
+ const body = (await req.json()) as DeleteFileRequest;
1835
+ const { path, sessionId } = body;
1836
1836
 
1837
- if (!newPath || typeof newPath !== "string") {
1837
+ if (!path || typeof path !== "string") {
1838
1838
  return new Response(
1839
1839
  JSON.stringify({
1840
- error: "New path is required and must be a string",
1840
+ error: "Path is required and must be a string",
1841
1841
  }),
1842
1842
  {
1843
1843
  headers: {
@@ -1865,11 +1865,7 @@ async function handleStreamingRenameFileRequest(
1865
1865
  /\.\./, // Path traversal attempts
1866
1866
  ];
1867
1867
 
1868
- if (
1869
- dangerousPatterns.some(
1870
- (pattern) => pattern.test(oldPath) || pattern.test(newPath)
1871
- )
1872
- ) {
1868
+ if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1873
1869
  return new Response(
1874
1870
  JSON.stringify({
1875
1871
  error: "Dangerous path not allowed",
@@ -1884,7 +1880,7 @@ async function handleStreamingRenameFileRequest(
1884
1880
  );
1885
1881
  }
1886
1882
 
1887
- console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
1883
+ console.log(`[Server] Deleting file (streaming): ${path}`);
1888
1884
 
1889
1885
  const stream = new ReadableStream({
1890
1886
  start(controller) {
@@ -1894,27 +1890,23 @@ async function handleStreamingRenameFileRequest(
1894
1890
  controller.enqueue(
1895
1891
  new TextEncoder().encode(
1896
1892
  `data: ${JSON.stringify({
1897
- newPath,
1898
- oldPath,
1893
+ path,
1899
1894
  timestamp: new Date().toISOString(),
1900
1895
  type: "command_start",
1901
1896
  })}\n\n`
1902
1897
  )
1903
1898
  );
1904
1899
 
1905
- // Rename the file
1906
- await executeRenameFile(oldPath, newPath, sessionId);
1900
+ // Delete the file
1901
+ await executeDeleteFile(path, sessionId);
1907
1902
 
1908
- console.log(
1909
- `[Server] File renamed successfully: ${oldPath} -> ${newPath}`
1910
- );
1903
+ console.log(`[Server] File deleted successfully: ${path}`);
1911
1904
 
1912
1905
  // Send command completion event
1913
1906
  controller.enqueue(
1914
1907
  new TextEncoder().encode(
1915
1908
  `data: ${JSON.stringify({
1916
- newPath,
1917
- oldPath,
1909
+ path,
1918
1910
  success: true,
1919
1911
  timestamp: new Date().toISOString(),
1920
1912
  type: "command_complete",
@@ -1924,18 +1916,14 @@ async function handleStreamingRenameFileRequest(
1924
1916
 
1925
1917
  controller.close();
1926
1918
  } catch (error) {
1927
- console.error(
1928
- `[Server] Error renaming file: ${oldPath} -> ${newPath}`,
1929
- error
1930
- );
1919
+ console.error(`[Server] Error deleting file: ${path}`, error);
1931
1920
 
1932
1921
  controller.enqueue(
1933
1922
  new TextEncoder().encode(
1934
1923
  `data: ${JSON.stringify({
1935
1924
  error:
1936
1925
  error instanceof Error ? error.message : "Unknown error",
1937
- newPath,
1938
- oldPath,
1926
+ path,
1939
1927
  type: "error",
1940
1928
  })}\n\n`
1941
1929
  )
@@ -1956,10 +1944,10 @@ async function handleStreamingRenameFileRequest(
1956
1944
  },
1957
1945
  });
1958
1946
  } catch (error) {
1959
- console.error("[Server] Error in handleStreamingRenameFileRequest:", error);
1947
+ console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
1960
1948
  return new Response(
1961
1949
  JSON.stringify({
1962
- error: "Failed to rename file",
1950
+ error: "Failed to delete file",
1963
1951
  message: error instanceof Error ? error.message : "Unknown error",
1964
1952
  }),
1965
1953
  {
@@ -1973,18 +1961,18 @@ async function handleStreamingRenameFileRequest(
1973
1961
  }
1974
1962
  }
1975
1963
 
1976
- async function handleMoveFileRequest(
1964
+ async function handleRenameFileRequest(
1977
1965
  req: Request,
1978
1966
  corsHeaders: Record<string, string>
1979
1967
  ): Promise<Response> {
1980
1968
  try {
1981
- const body = (await req.json()) as MoveFileRequest;
1982
- const { sourcePath, destinationPath, sessionId } = body;
1969
+ const body = (await req.json()) as RenameFileRequest;
1970
+ const { oldPath, newPath, sessionId } = body;
1983
1971
 
1984
- if (!sourcePath || typeof sourcePath !== "string") {
1972
+ if (!oldPath || typeof oldPath !== "string") {
1985
1973
  return new Response(
1986
1974
  JSON.stringify({
1987
- error: "Source path is required and must be a string",
1975
+ error: "Old path is required and must be a string",
1988
1976
  }),
1989
1977
  {
1990
1978
  headers: {
@@ -1996,10 +1984,10 @@ async function handleMoveFileRequest(
1996
1984
  );
1997
1985
  }
1998
1986
 
1999
- if (!destinationPath || typeof destinationPath !== "string") {
1987
+ if (!newPath || typeof newPath !== "string") {
2000
1988
  return new Response(
2001
1989
  JSON.stringify({
2002
- error: "Destination path is required and must be a string",
1990
+ error: "New path is required and must be a string",
2003
1991
  }),
2004
1992
  {
2005
1993
  headers: {
@@ -2029,7 +2017,7 @@ async function handleMoveFileRequest(
2029
2017
 
2030
2018
  if (
2031
2019
  dangerousPatterns.some(
2032
- (pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
2020
+ (pattern) => pattern.test(oldPath) || pattern.test(newPath)
2033
2021
  )
2034
2022
  ) {
2035
2023
  return new Response(
@@ -2046,19 +2034,15 @@ async function handleMoveFileRequest(
2046
2034
  );
2047
2035
  }
2048
2036
 
2049
- console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
2037
+ console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
2050
2038
 
2051
- const result = await executeMoveFile(
2052
- sourcePath,
2053
- destinationPath,
2054
- sessionId
2055
- );
2039
+ const result = await executeRenameFile(oldPath, newPath, sessionId);
2056
2040
 
2057
2041
  return new Response(
2058
2042
  JSON.stringify({
2059
- destinationPath,
2060
2043
  exitCode: result.exitCode,
2061
- sourcePath,
2044
+ newPath,
2045
+ oldPath,
2062
2046
  success: result.success,
2063
2047
  timestamp: new Date().toISOString(),
2064
2048
  }),
@@ -2070,10 +2054,10 @@ async function handleMoveFileRequest(
2070
2054
  }
2071
2055
  );
2072
2056
  } catch (error) {
2073
- console.error("[Server] Error in handleMoveFileRequest:", error);
2057
+ console.error("[Server] Error in handleRenameFileRequest:", error);
2074
2058
  return new Response(
2075
2059
  JSON.stringify({
2076
- error: "Failed to move file",
2060
+ error: "Failed to rename file",
2077
2061
  message: error instanceof Error ? error.message : "Unknown error",
2078
2062
  }),
2079
2063
  {
@@ -2087,18 +2071,18 @@ async function handleMoveFileRequest(
2087
2071
  }
2088
2072
  }
2089
2073
 
2090
- async function handleStreamingMoveFileRequest(
2074
+ async function handleStreamingRenameFileRequest(
2091
2075
  req: Request,
2092
2076
  corsHeaders: Record<string, string>
2093
2077
  ): Promise<Response> {
2094
2078
  try {
2095
- const body = (await req.json()) as MoveFileRequest;
2096
- const { sourcePath, destinationPath, sessionId } = body;
2079
+ const body = (await req.json()) as RenameFileRequest;
2080
+ const { oldPath, newPath, sessionId } = body;
2097
2081
 
2098
- if (!sourcePath || typeof sourcePath !== "string") {
2082
+ if (!oldPath || typeof oldPath !== "string") {
2099
2083
  return new Response(
2100
2084
  JSON.stringify({
2101
- error: "Source path is required and must be a string",
2085
+ error: "Old path is required and must be a string",
2102
2086
  }),
2103
2087
  {
2104
2088
  headers: {
@@ -2110,10 +2094,10 @@ async function handleStreamingMoveFileRequest(
2110
2094
  );
2111
2095
  }
2112
2096
 
2113
- if (!destinationPath || typeof destinationPath !== "string") {
2097
+ if (!newPath || typeof newPath !== "string") {
2114
2098
  return new Response(
2115
2099
  JSON.stringify({
2116
- error: "Destination path is required and must be a string",
2100
+ error: "New path is required and must be a string",
2117
2101
  }),
2118
2102
  {
2119
2103
  headers: {
@@ -2143,7 +2127,7 @@ async function handleStreamingMoveFileRequest(
2143
2127
 
2144
2128
  if (
2145
2129
  dangerousPatterns.some(
2146
- (pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
2130
+ (pattern) => pattern.test(oldPath) || pattern.test(newPath)
2147
2131
  )
2148
2132
  ) {
2149
2133
  return new Response(
@@ -2160,9 +2144,7 @@ async function handleStreamingMoveFileRequest(
2160
2144
  );
2161
2145
  }
2162
2146
 
2163
- console.log(
2164
- `[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
2165
- );
2147
+ console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
2166
2148
 
2167
2149
  const stream = new ReadableStream({
2168
2150
  start(controller) {
@@ -2172,27 +2154,27 @@ async function handleStreamingMoveFileRequest(
2172
2154
  controller.enqueue(
2173
2155
  new TextEncoder().encode(
2174
2156
  `data: ${JSON.stringify({
2175
- destinationPath,
2176
- sourcePath,
2157
+ newPath,
2158
+ oldPath,
2177
2159
  timestamp: new Date().toISOString(),
2178
2160
  type: "command_start",
2179
2161
  })}\n\n`
2180
2162
  )
2181
2163
  );
2182
2164
 
2183
- // Move the file
2184
- await executeMoveFile(sourcePath, destinationPath, sessionId);
2165
+ // Rename the file
2166
+ await executeRenameFile(oldPath, newPath, sessionId);
2185
2167
 
2186
2168
  console.log(
2187
- `[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
2169
+ `[Server] File renamed successfully: ${oldPath} -> ${newPath}`
2188
2170
  );
2189
2171
 
2190
2172
  // Send command completion event
2191
2173
  controller.enqueue(
2192
2174
  new TextEncoder().encode(
2193
2175
  `data: ${JSON.stringify({
2194
- destinationPath,
2195
- sourcePath,
2176
+ newPath,
2177
+ oldPath,
2196
2178
  success: true,
2197
2179
  timestamp: new Date().toISOString(),
2198
2180
  type: "command_complete",
@@ -2203,14 +2185,292 @@ async function handleStreamingMoveFileRequest(
2203
2185
  controller.close();
2204
2186
  } catch (error) {
2205
2187
  console.error(
2206
- `[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
2188
+ `[Server] Error renaming file: ${oldPath} -> ${newPath}`,
2207
2189
  error
2208
2190
  );
2209
2191
 
2210
2192
  controller.enqueue(
2211
2193
  new TextEncoder().encode(
2212
2194
  `data: ${JSON.stringify({
2213
- destinationPath,
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,
2214
2474
  error:
2215
2475
  error instanceof Error ? error.message : "Unknown error",
2216
2476
  sourcePath,
@@ -2254,7 +2514,8 @@ async function handleStreamingMoveFileRequest(
2254
2514
  function executeCommand(
2255
2515
  command: string,
2256
2516
  args: string[],
2257
- sessionId?: string
2517
+ sessionId?: string,
2518
+ background?: boolean
2258
2519
  ): Promise<{
2259
2520
  success: boolean;
2260
2521
  stdout: string;
@@ -2262,10 +2523,13 @@ function executeCommand(
2262
2523
  exitCode: number;
2263
2524
  }> {
2264
2525
  return new Promise((resolve, reject) => {
2265
- const child = spawn(command, args, {
2526
+ const spawnOptions: SpawnOptions = {
2266
2527
  shell: true,
2267
- stdio: ["pipe", "pipe", "pipe"],
2268
- });
2528
+ stdio: ["pipe", "pipe", "pipe"] as const,
2529
+ detached: background || false,
2530
+ };
2531
+
2532
+ const child = spawn(command, args, spawnOptions);
2269
2533
 
2270
2534
  // Store the process reference for cleanup if sessionId is provided
2271
2535
  if (sessionId && sessions.has(sessionId)) {
@@ -2284,32 +2548,54 @@ function executeCommand(
2284
2548
  stderr += data.toString();
2285
2549
  });
2286
2550
 
2287
- child.on("close", (code) => {
2288
- // Clear the active process reference
2289
- if (sessionId && sessions.has(sessionId)) {
2290
- const session = sessions.get(sessionId)!;
2291
- session.activeProcess = null;
2292
- }
2551
+ if (background) {
2552
+ // For background processes, unref and return quickly
2553
+ child.unref();
2293
2554
 
2294
- console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
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);
2295
2564
 
2296
- resolve({
2297
- exitCode: code || 0,
2298
- stderr,
2299
- stdout,
2300
- success: code === 0,
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
2301
2569
  });
2302
- });
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
+ }
2303
2578
 
2304
- child.on("error", (error) => {
2305
- // Clear the active process reference
2306
- if (sessionId && sessions.has(sessionId)) {
2307
- const session = sessions.get(sessionId)!;
2308
- session.activeProcess = null;
2309
- }
2579
+ console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2310
2580
 
2311
- reject(error);
2312
- });
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
+ }
2313
2599
  });
2314
2600
  }
2315
2601
 
@@ -2506,6 +2792,37 @@ function executeWriteFile(
2506
2792
  });
2507
2793
  }
2508
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
+
2509
2826
  function executeDeleteFile(
2510
2827
  path: string,
2511
2828
  sessionId?: string
@@ -2596,7 +2913,316 @@ function executeMoveFile(
2596
2913
  });
2597
2914
  }
2598
2915
 
2599
- console.log(`🚀 Bun server running on http://localhost:${server.port}`);
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
+ console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
2600
3226
  console.log(`📡 HTTP API endpoints available:`);
2601
3227
  console.log(` POST /api/session/create - Create a new session`);
2602
3228
  console.log(` GET /api/session/list - List all sessions`);
@@ -2610,11 +3236,17 @@ console.log(` POST /api/mkdir - Create a directory`);
2610
3236
  console.log(` POST /api/mkdir/stream - Create a directory (streaming)`);
2611
3237
  console.log(` POST /api/write - Write a file`);
2612
3238
  console.log(` POST /api/write/stream - Write a file (streaming)`);
3239
+ console.log(` POST /api/read - Read a file`);
3240
+ console.log(` POST /api/read/stream - Read a file (streaming)`);
2613
3241
  console.log(` POST /api/delete - Delete a file`);
2614
3242
  console.log(` POST /api/delete/stream - Delete a file (streaming)`);
2615
3243
  console.log(` POST /api/rename - Rename a file`);
2616
3244
  console.log(` POST /api/rename/stream - Rename a file (streaming)`);
2617
3245
  console.log(` POST /api/move - Move a file`);
2618
3246
  console.log(` POST /api/move/stream - Move a file (streaming)`);
3247
+ console.log(` POST /api/expose-port - Expose a port for external access`);
3248
+ console.log(` DELETE /api/unexpose-port - Unexpose a port`);
3249
+ console.log(` GET /api/exposed-ports - List exposed ports`);
3250
+ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
2619
3251
  console.log(` GET /api/ping - Health check`);
2620
3252
  console.log(` GET /api/commands - List available commands`);