@cloudflare/sandbox 0.0.0-d1c7c99 → 0.0.0-d670ba2

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 (43) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/Dockerfile +97 -10
  3. package/README.md +858 -24
  4. package/container_src/bun.lock +76 -0
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +185 -0
  8. package/container_src/handler/file.ts +457 -0
  9. package/container_src/handler/git.ts +130 -0
  10. package/container_src/handler/ports.ts +314 -0
  11. package/container_src/handler/process.ts +568 -0
  12. package/container_src/handler/session.ts +92 -0
  13. package/container_src/index.ts +433 -2738
  14. package/container_src/interpreter-service.ts +276 -0
  15. package/container_src/isolation.ts +1213 -0
  16. package/container_src/mime-processor.ts +255 -0
  17. package/container_src/package.json +9 -0
  18. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  19. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  20. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  21. package/container_src/runtime/process-pool.ts +464 -0
  22. package/container_src/shell-escape.ts +42 -0
  23. package/container_src/startup.sh +11 -0
  24. package/container_src/types.ts +131 -0
  25. package/package.json +5 -9
  26. package/src/client.ts +448 -1360
  27. package/src/errors.ts +219 -0
  28. package/src/file-stream.ts +162 -0
  29. package/src/index.ts +78 -133
  30. package/src/interpreter-client.ts +352 -0
  31. package/src/interpreter-types.ts +390 -0
  32. package/src/interpreter.ts +150 -0
  33. package/src/request-handler.ts +144 -0
  34. package/src/sandbox.ts +756 -0
  35. package/src/security.ts +113 -0
  36. package/src/sse-parser.ts +147 -0
  37. package/src/types.ts +571 -0
  38. package/tsconfig.json +1 -1
  39. package/tests/client.example.ts +0 -308
  40. package/tests/connection-test.ts +0 -81
  41. package/tests/simple-test.ts +0 -81
  42. package/tests/test1.ts +0 -281
  43. package/tests/test2.ts +0 -929
package/src/client.ts CHANGED
@@ -1,36 +1,23 @@
1
- import type { DurableObject } from "cloudflare:workers";
1
+ import type { ExecuteRequest } from "../container_src/types";
2
2
  import type { Sandbox } from "./index";
3
+ import type {
4
+ BaseExecOptions,
5
+ DeleteFileResponse,
6
+ ExecuteResponse,
7
+ GetProcessLogsResponse,
8
+ GetProcessResponse,
9
+ GitCheckoutResponse,
10
+ ListFilesResponse,
11
+ ListProcessesResponse,
12
+ MkdirResponse,
13
+ MoveFileResponse,
14
+ ReadFileResponse,
15
+ RenameFileResponse,
16
+ StartProcessRequest,
17
+ StartProcessResponse,
18
+ WriteFileResponse,
19
+ } from "./types";
3
20
 
4
- interface ExecuteRequest {
5
- command: string;
6
- args?: string[];
7
- }
8
-
9
- export interface ExecuteResponse {
10
- success: boolean;
11
- stdout: string;
12
- stderr: string;
13
- exitCode: number;
14
- command: string;
15
- args: string[];
16
- timestamp: string;
17
- }
18
-
19
- interface SessionResponse {
20
- sessionId: string;
21
- message: string;
22
- timestamp: string;
23
- }
24
-
25
- interface SessionListResponse {
26
- sessions: Array<{
27
- sessionId: string;
28
- hasActiveProcess: boolean;
29
- createdAt: string;
30
- }>;
31
- count: number;
32
- timestamp: string;
33
- }
34
21
 
35
22
  interface CommandsResponse {
36
23
  availableCommands: string[];
@@ -41,135 +28,103 @@ interface GitCheckoutRequest {
41
28
  repoUrl: string;
42
29
  branch?: string;
43
30
  targetDir?: string;
44
- sessionId?: string;
31
+ sessionId: string;
45
32
  }
46
33
 
47
- export interface GitCheckoutResponse {
48
- success: boolean;
49
- stdout: string;
50
- stderr: string;
51
- exitCode: number;
52
- repoUrl: string;
53
- branch: string;
54
- targetDir: string;
55
- timestamp: string;
56
- }
57
34
 
58
35
  interface MkdirRequest {
59
36
  path: string;
60
37
  recursive?: boolean;
61
- sessionId?: string;
38
+ sessionId: string;
62
39
  }
63
40
 
64
- export interface MkdirResponse {
65
- success: boolean;
66
- stdout: string;
67
- stderr: string;
68
- exitCode: number;
69
- path: string;
70
- recursive: boolean;
71
- timestamp: string;
72
- }
73
41
 
74
42
  interface WriteFileRequest {
75
43
  path: string;
76
44
  content: string;
77
45
  encoding?: string;
78
- sessionId?: string;
46
+ sessionId: string;
79
47
  }
80
48
 
81
- export interface WriteFileResponse {
82
- success: boolean;
83
- exitCode: number;
84
- path: string;
85
- timestamp: string;
86
- }
87
49
 
88
50
  interface ReadFileRequest {
89
51
  path: string;
90
52
  encoding?: string;
91
- sessionId?: string;
53
+ sessionId: string;
92
54
  }
93
55
 
94
- export interface ReadFileResponse {
95
- success: boolean;
96
- exitCode: number;
97
- path: string;
98
- content: string;
99
- timestamp: string;
100
- }
101
56
 
102
57
  interface DeleteFileRequest {
103
58
  path: string;
104
- sessionId?: string;
59
+ sessionId: string;
105
60
  }
106
61
 
107
- export interface DeleteFileResponse {
108
- success: boolean;
109
- exitCode: number;
110
- path: string;
111
- timestamp: string;
112
- }
113
62
 
114
63
  interface RenameFileRequest {
115
64
  oldPath: string;
116
65
  newPath: string;
117
- sessionId?: string;
66
+ sessionId: string;
118
67
  }
119
68
 
120
- export interface RenameFileResponse {
121
- success: boolean;
122
- exitCode: number;
123
- oldPath: string;
124
- newPath: string;
125
- timestamp: string;
126
- }
127
69
 
128
70
  interface MoveFileRequest {
129
71
  sourcePath: string;
130
72
  destinationPath: string;
131
- sessionId?: string;
73
+ sessionId: string;
74
+ }
75
+
76
+
77
+ interface ListFilesRequest {
78
+ path: string;
79
+ options?: {
80
+ recursive?: boolean;
81
+ includeHidden?: boolean;
82
+ };
83
+ sessionId: string;
84
+ }
85
+
86
+
87
+ interface PreviewInfo {
88
+ url: string;
89
+ port: number;
90
+ name?: string;
91
+ }
92
+
93
+ interface ExposedPort extends PreviewInfo {
94
+ exposedAt: string;
95
+ timestamp: string;
132
96
  }
133
97
 
134
- export interface MoveFileResponse {
98
+ interface ExposePortResponse {
135
99
  success: boolean;
136
- exitCode: number;
137
- sourcePath: string;
138
- destinationPath: string;
100
+ port: number;
101
+ name?: string;
102
+ exposedAt: string;
139
103
  timestamp: string;
140
104
  }
141
105
 
142
- interface PingResponse {
143
- message: string;
106
+ interface UnexposePortResponse {
107
+ success: boolean;
108
+ port: number;
109
+ timestamp: string;
110
+ }
111
+
112
+ interface GetExposedPortsResponse {
113
+ ports: ExposedPort[];
114
+ count: number;
144
115
  timestamp: string;
145
116
  }
146
117
 
147
- interface StreamEvent {
148
- type: "command_start" | "output" | "command_complete" | "error";
149
- command?: string;
150
- args?: string[];
151
- stream?: "stdout" | "stderr";
152
- data?: string;
153
- message?: string;
154
- path?: string;
155
- oldPath?: string;
156
- newPath?: string;
157
- sourcePath?: string;
158
- destinationPath?: string;
159
- content?: string;
160
- success?: boolean;
161
- exitCode?: number;
162
- stdout?: string;
163
- stderr?: string;
164
- error?: string;
165
- timestamp?: string;
118
+ interface PingResponse {
119
+ message: string;
120
+ timestamp: string;
166
121
  }
167
122
 
168
123
  interface HttpClientOptions {
169
124
  stub?: Sandbox;
170
125
  baseUrl?: string;
171
126
  port?: number;
172
- onCommandStart?: (command: string, args: string[]) => void;
127
+ onCommandStart?: (command: string) => void;
173
128
  onOutput?: (
174
129
  stream: "stdout" | "stderr",
175
130
  data: string,
@@ -180,17 +135,14 @@ interface HttpClientOptions {
180
135
  exitCode: number,
181
136
  stdout: string,
182
137
  stderr: string,
183
- command: string,
184
- args: string[]
138
+ command: string
185
139
  ) => void;
186
- onError?: (error: string, command?: string, args?: string[]) => void;
187
- onStreamEvent?: (event: StreamEvent) => void;
140
+ onError?: (error: string, command?: string) => void;
188
141
  }
189
142
 
190
143
  export class HttpClient {
191
144
  private baseUrl: string;
192
145
  private options: HttpClientOptions;
193
- private sessionId: string | null = null;
194
146
 
195
147
  constructor(options: HttpClientOptions = {}) {
196
148
  this.options = {
@@ -199,7 +151,7 @@ export class HttpClient {
199
151
  this.baseUrl = this.options.baseUrl!;
200
152
  }
201
153
 
202
- private async doFetch(
154
+ protected async doFetch(
203
155
  path: string,
204
156
  options?: RequestInit
205
157
  ): Promise<Response> {
@@ -239,121 +191,53 @@ export class HttpClient {
239
191
  throw error;
240
192
  }
241
193
  }
242
- // Public methods to set event handlers
243
- setOnOutput(
244
- handler: (
245
- stream: "stdout" | "stderr",
246
- data: string,
247
- command: string
248
- ) => void
249
- ): void {
250
- this.options.onOutput = handler;
251
- }
252
-
253
- setOnCommandComplete(
254
- handler: (
255
- success: boolean,
256
- exitCode: number,
257
- stdout: string,
258
- stderr: string,
259
- command: string,
260
- args: string[]
261
- ) => void
262
- ): void {
263
- this.options.onCommandComplete = handler;
264
- }
265
-
266
- setOnStreamEvent(handler: (event: StreamEvent) => void): void {
267
- this.options.onStreamEvent = handler;
268
- }
269
-
270
- // Public getter methods
271
- getOnOutput():
272
- | ((stream: "stdout" | "stderr", data: string, command: string) => void)
273
- | undefined {
274
- return this.options.onOutput;
275
- }
276
-
277
- getOnCommandComplete():
278
- | ((
279
- success: boolean,
280
- exitCode: number,
281
- stdout: string,
282
- stderr: string,
283
- command: string,
284
- args: string[]
285
- ) => void)
286
- | undefined {
287
- return this.options.onCommandComplete;
288
- }
289
-
290
- getOnStreamEvent(): ((event: StreamEvent) => void) | undefined {
291
- return this.options.onStreamEvent;
292
- }
293
194
 
294
- async createSession(): Promise<string> {
195
+ async createSession(options: {
196
+ id: string;
197
+ env?: Record<string, string>;
198
+ cwd?: string;
199
+ isolation?: boolean;
200
+ }): Promise<{ success: boolean; id: string; message: string }> {
295
201
  try {
296
202
  const response = await this.doFetch(`/api/session/create`, {
297
- headers: {
298
- "Content-Type": "application/json",
299
- },
300
203
  method: "POST",
301
- });
302
-
303
- if (!response.ok) {
304
- throw new Error(`HTTP error! status: ${response.status}`);
305
- }
306
-
307
- const data: SessionResponse = await response.json();
308
- this.sessionId = data.sessionId;
309
- console.log(`[HTTP Client] Created session: ${this.sessionId}`);
310
- return this.sessionId;
311
- } catch (error) {
312
- console.error("[HTTP Client] Error creating session:", error);
313
- throw error;
314
- }
315
- }
316
-
317
- async listSessions(): Promise<SessionListResponse> {
318
- try {
319
- const response = await this.doFetch(`/api/session/list`, {
320
204
  headers: {
321
205
  "Content-Type": "application/json",
322
206
  },
323
- method: "GET",
207
+ body: JSON.stringify(options),
324
208
  });
325
209
 
326
210
  if (!response.ok) {
327
- throw new Error(`HTTP error! status: ${response.status}`);
211
+ const errorData = (await response.json().catch(() => ({}))) as {
212
+ error?: string;
213
+ };
214
+ throw new Error(
215
+ errorData.error || `Failed to create session: ${response.status}`
216
+ );
328
217
  }
329
218
 
330
- const data: SessionListResponse = await response.json();
331
- console.log(`[HTTP Client] Listed ${data.count} sessions`);
219
+ const data = await response.json() as { success: boolean; id: string; message: string };
220
+ console.log(`[HTTP Client] Session created: ${options.id}`);
332
221
  return data;
333
222
  } catch (error) {
334
- console.error("[HTTP Client] Error listing sessions:", error);
223
+ console.error("[HTTP Client] Error creating session:", error);
335
224
  throw error;
336
225
  }
337
226
  }
338
227
 
339
- async execute(
228
+ async exec(
229
+ sessionId: string,
340
230
  command: string,
341
- args: string[] = [],
342
- sessionId?: string
231
+ options?: Pick<BaseExecOptions, "cwd" | "env">
343
232
  ): Promise<ExecuteResponse> {
344
233
  try {
345
- const targetSessionId = sessionId || this.sessionId;
346
-
234
+ // Always use session-specific endpoint
347
235
  const response = await this.doFetch(`/api/execute`, {
348
- body: JSON.stringify({
349
- args,
350
- command,
351
- sessionId: targetSessionId,
352
- }),
236
+ method: "POST",
353
237
  headers: {
354
238
  "Content-Type": "application/json",
355
239
  },
356
- method: "POST",
240
+ body: JSON.stringify({ id: sessionId, command }),
357
241
  });
358
242
 
359
243
  if (!response.ok) {
@@ -361,55 +245,57 @@ export class HttpClient {
361
245
  error?: string;
362
246
  };
363
247
  throw new Error(
364
- errorData.error || `HTTP error! status: ${response.status}`
248
+ errorData.error || `Failed to execute in session: ${response.status}`
365
249
  );
366
250
  }
367
251
 
368
- const data: ExecuteResponse = await response.json();
252
+ const data = await response.json() as { stdout: string; stderr: string; exitCode: number; success: boolean };
369
253
  console.log(
370
- `[HTTP Client] Command executed: ${command}, Success: ${data.success}`
254
+ `[HTTP Client] Command executed in session ${sessionId}: ${command}`
371
255
  );
256
+
257
+ // Convert to ExecuteResponse format for consistency
258
+ const executeResponse: ExecuteResponse = {
259
+ ...data,
260
+ command,
261
+ timestamp: new Date().toISOString()
262
+ };
372
263
 
373
264
  // Call the callback if provided
374
265
  this.options.onCommandComplete?.(
375
- data.success,
376
- data.exitCode,
377
- data.stdout,
378
- data.stderr,
379
- data.command,
380
- data.args
266
+ executeResponse.success,
267
+ executeResponse.exitCode,
268
+ executeResponse.stdout,
269
+ executeResponse.stderr,
270
+ executeResponse.command
381
271
  );
382
272
 
383
- return data;
273
+ return executeResponse;
384
274
  } catch (error) {
385
- console.error("[HTTP Client] Error executing command:", error);
275
+ console.error("[HTTP Client] Error executing in session:", error);
386
276
  this.options.onError?.(
387
277
  error instanceof Error ? error.message : "Unknown error",
388
- command,
389
- args
278
+ command
390
279
  );
391
280
  throw error;
392
281
  }
393
282
  }
394
283
 
395
- async executeStream(
396
- command: string,
397
- args: string[] = [],
398
- sessionId?: string
399
- ): Promise<void> {
284
+ async execStream(
285
+ sessionId: string,
286
+ command: string
287
+ ): Promise<ReadableStream<Uint8Array>> {
400
288
  try {
401
- const targetSessionId = sessionId || this.sessionId;
402
-
289
+ // Always use session-specific streaming endpoint
403
290
  const response = await this.doFetch(`/api/execute/stream`, {
404
- body: JSON.stringify({
405
- args,
406
- command,
407
- sessionId: targetSessionId,
408
- }),
291
+ method: "POST",
409
292
  headers: {
410
293
  "Content-Type": "application/json",
411
294
  },
412
- method: "POST",
295
+ body: JSON.stringify({
296
+ id: sessionId,
297
+ command
298
+ }),
413
299
  });
414
300
 
415
301
  if (!response.ok) {
@@ -417,123 +303,38 @@ export class HttpClient {
417
303
  error?: string;
418
304
  };
419
305
  throw new Error(
420
- errorData.error || `HTTP error! status: ${response.status}`
306
+ errorData.error || `Failed to stream execute in session: ${response.status}`
421
307
  );
422
308
  }
423
309
 
424
310
  if (!response.body) {
425
- throw new Error("No response body for streaming request");
311
+ throw new Error("No response body for streaming execution");
426
312
  }
427
313
 
428
- const reader = response.body.getReader();
429
- const decoder = new TextDecoder();
430
-
431
- try {
432
- while (true) {
433
- const { done, value } = await reader.read();
434
-
435
- if (done) {
436
- break;
437
- }
438
-
439
- const chunk = decoder.decode(value, { stream: true });
440
- const lines = chunk.split("\n");
441
-
442
- for (const line of lines) {
443
- if (line.startsWith("data: ")) {
444
- try {
445
- const eventData = line.slice(6); // Remove 'data: ' prefix
446
- const event: StreamEvent = JSON.parse(eventData);
447
-
448
- console.log(`[HTTP Client] Stream event: ${event.type}`);
449
- this.options.onStreamEvent?.(event);
450
-
451
- switch (event.type) {
452
- case "command_start":
453
- console.log(
454
- `[HTTP Client] Command started: ${
455
- event.command
456
- } ${event.args?.join(" ")}`
457
- );
458
- this.options.onCommandStart?.(
459
- event.command!,
460
- event.args || []
461
- );
462
- break;
463
-
464
- case "output":
465
- console.log(`[${event.stream}] ${event.data}`);
466
- this.options.onOutput?.(
467
- event.stream!,
468
- event.data!,
469
- event.command!
470
- );
471
- break;
472
-
473
- case "command_complete":
474
- console.log(
475
- `[HTTP Client] Command completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
476
- );
477
- this.options.onCommandComplete?.(
478
- event.success!,
479
- event.exitCode!,
480
- event.stdout!,
481
- event.stderr!,
482
- event.command!,
483
- event.args || []
484
- );
485
- break;
486
-
487
- case "error":
488
- console.error(
489
- `[HTTP Client] Command error: ${event.error}`
490
- );
491
- this.options.onError?.(
492
- event.error!,
493
- event.command,
494
- event.args
495
- );
496
- break;
497
- }
498
- } catch (parseError) {
499
- console.warn(
500
- "[HTTP Client] Failed to parse stream event:",
501
- parseError
502
- );
503
- }
504
- }
505
- }
506
- }
507
- } finally {
508
- reader.releaseLock();
509
- }
510
- } catch (error) {
511
- console.error("[HTTP Client] Error in streaming execution:", error);
512
- this.options.onError?.(
513
- error instanceof Error ? error.message : "Unknown error",
514
- command,
515
- args
314
+ console.log(
315
+ `[HTTP Client] Started streaming command in session ${sessionId}: ${command}`
516
316
  );
317
+ return response.body;
318
+ } catch (error) {
319
+ console.error("[HTTP Client] Error streaming execute in session:", error);
517
320
  throw error;
518
321
  }
519
322
  }
520
323
 
521
324
  async gitCheckout(
522
325
  repoUrl: string,
326
+ sessionId: string,
523
327
  branch: string = "main",
524
- targetDir?: string,
525
- sessionId?: string
328
+ targetDir?: string
526
329
  ): Promise<GitCheckoutResponse> {
527
330
  try {
528
- const targetSessionId = sessionId || this.sessionId;
529
-
530
331
  const response = await this.doFetch(`/api/git/checkout`, {
531
332
  body: JSON.stringify({
532
333
  branch,
533
334
  repoUrl,
534
- sessionId: targetSessionId,
535
335
  targetDir,
536
- }),
336
+ sessionId,
337
+ } as GitCheckoutRequest),
537
338
  headers: {
538
339
  "Content-Type": "application/json",
539
340
  },
@@ -561,150 +362,18 @@ export class HttpClient {
561
362
  }
562
363
  }
563
364
 
564
- async gitCheckoutStream(
565
- repoUrl: string,
566
- branch: string = "main",
567
- targetDir?: string,
568
- sessionId?: string
569
- ): Promise<void> {
570
- try {
571
- const targetSessionId = sessionId || this.sessionId;
572
-
573
- const response = await this.doFetch(`/api/git/checkout/stream`, {
574
- body: JSON.stringify({
575
- branch,
576
- repoUrl,
577
- sessionId: targetSessionId,
578
- targetDir,
579
- }),
580
- headers: {
581
- "Content-Type": "application/json",
582
- },
583
- method: "POST",
584
- });
585
-
586
- if (!response.ok) {
587
- const errorData = (await response.json().catch(() => ({}))) as {
588
- error?: string;
589
- };
590
- throw new Error(
591
- errorData.error || `HTTP error! status: ${response.status}`
592
- );
593
- }
594
-
595
- if (!response.body) {
596
- throw new Error("No response body for streaming request");
597
- }
598
-
599
- const reader = response.body.getReader();
600
- const decoder = new TextDecoder();
601
-
602
- try {
603
- while (true) {
604
- const { done, value } = await reader.read();
605
-
606
- if (done) {
607
- break;
608
- }
609
-
610
- const chunk = decoder.decode(value, { stream: true });
611
- const lines = chunk.split("\n");
612
-
613
- for (const line of lines) {
614
- if (line.startsWith("data: ")) {
615
- try {
616
- const eventData = line.slice(6); // Remove 'data: ' prefix
617
- const event: StreamEvent = JSON.parse(eventData);
618
-
619
- console.log(
620
- `[HTTP Client] Git checkout stream event: ${event.type}`
621
- );
622
- this.options.onStreamEvent?.(event);
623
-
624
- switch (event.type) {
625
- case "command_start":
626
- console.log(
627
- `[HTTP Client] Git checkout started: ${
628
- event.command
629
- } ${event.args?.join(" ")}`
630
- );
631
- this.options.onCommandStart?.(
632
- event.command!,
633
- event.args || []
634
- );
635
- break;
636
-
637
- case "output":
638
- console.log(`[${event.stream}] ${event.data}`);
639
- this.options.onOutput?.(
640
- event.stream!,
641
- event.data!,
642
- event.command!
643
- );
644
- break;
645
-
646
- case "command_complete":
647
- console.log(
648
- `[HTTP Client] Git checkout completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
649
- );
650
- this.options.onCommandComplete?.(
651
- event.success!,
652
- event.exitCode!,
653
- event.stdout!,
654
- event.stderr!,
655
- event.command!,
656
- event.args || []
657
- );
658
- break;
659
-
660
- case "error":
661
- console.error(
662
- `[HTTP Client] Git checkout error: ${event.error}`
663
- );
664
- this.options.onError?.(
665
- event.error!,
666
- event.command,
667
- event.args
668
- );
669
- break;
670
- }
671
- } catch (parseError) {
672
- console.warn(
673
- "[HTTP Client] Failed to parse git checkout stream event:",
674
- parseError
675
- );
676
- }
677
- }
678
- }
679
- }
680
- } finally {
681
- reader.releaseLock();
682
- }
683
- } catch (error) {
684
- console.error("[HTTP Client] Error in streaming git checkout:", error);
685
- this.options.onError?.(
686
- error instanceof Error ? error.message : "Unknown error",
687
- "git clone",
688
- [branch, repoUrl, targetDir || ""]
689
- );
690
- throw error;
691
- }
692
- }
693
-
694
365
  async mkdir(
695
366
  path: string,
696
367
  recursive: boolean = false,
697
- sessionId?: string
368
+ sessionId: string
698
369
  ): Promise<MkdirResponse> {
699
370
  try {
700
- const targetSessionId = sessionId || this.sessionId;
701
-
702
371
  const response = await this.doFetch(`/api/mkdir`, {
703
372
  body: JSON.stringify({
704
373
  path,
705
374
  recursive,
706
- sessionId: targetSessionId,
707
- }),
375
+ sessionId,
376
+ } as MkdirRequest),
708
377
  headers: {
709
378
  "Content-Type": "application/json",
710
379
  },
@@ -722,7 +391,7 @@ export class HttpClient {
722
391
 
723
392
  const data: MkdirResponse = await response.json();
724
393
  console.log(
725
- `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}`
394
+ `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}${sessionId ? ` in session: ${sessionId}` : ''}`
726
395
  );
727
396
 
728
397
  return data;
@@ -732,20 +401,20 @@ export class HttpClient {
732
401
  }
733
402
  }
734
403
 
735
- async mkdirStream(
404
+ async writeFile(
736
405
  path: string,
737
- recursive: boolean = false,
738
- sessionId?: string
739
- ): Promise<void> {
406
+ content: string,
407
+ encoding: string = "utf-8",
408
+ sessionId: string
409
+ ): Promise<WriteFileResponse> {
740
410
  try {
741
- const targetSessionId = sessionId || this.sessionId;
742
-
743
- const response = await this.doFetch(`/api/mkdir/stream`, {
411
+ const response = await this.doFetch(`/api/write`, {
744
412
  body: JSON.stringify({
413
+ content,
414
+ encoding,
745
415
  path,
746
- recursive,
747
- sessionId: targetSessionId,
748
- }),
416
+ sessionId,
417
+ } as WriteFileRequest),
749
418
  headers: {
750
419
  "Content-Type": "application/json",
751
420
  },
@@ -761,117 +430,30 @@ export class HttpClient {
761
430
  );
762
431
  }
763
432
 
764
- if (!response.body) {
765
- throw new Error("No response body for streaming request");
766
- }
433
+ const data: WriteFileResponse = await response.json();
434
+ console.log(
435
+ `[HTTP Client] File written: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
436
+ );
767
437
 
768
- const reader = response.body.getReader();
769
- const decoder = new TextDecoder();
770
-
771
- try {
772
- while (true) {
773
- const { done, value } = await reader.read();
774
-
775
- if (done) {
776
- break;
777
- }
778
-
779
- const chunk = decoder.decode(value, { stream: true });
780
- const lines = chunk.split("\n");
781
-
782
- for (const line of lines) {
783
- if (line.startsWith("data: ")) {
784
- try {
785
- const eventData = line.slice(6); // Remove 'data: ' prefix
786
- const event: StreamEvent = JSON.parse(eventData);
787
-
788
- console.log(`[HTTP Client] Mkdir stream event: ${event.type}`);
789
- this.options.onStreamEvent?.(event);
790
-
791
- switch (event.type) {
792
- case "command_start":
793
- console.log(
794
- `[HTTP Client] Mkdir started: ${
795
- event.command
796
- } ${event.args?.join(" ")}`
797
- );
798
- this.options.onCommandStart?.(
799
- event.command!,
800
- event.args || []
801
- );
802
- break;
803
-
804
- case "output":
805
- console.log(`[${event.stream}] ${event.data}`);
806
- this.options.onOutput?.(
807
- event.stream!,
808
- event.data!,
809
- event.command!
810
- );
811
- break;
812
-
813
- case "command_complete":
814
- console.log(
815
- `[HTTP Client] Mkdir completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
816
- );
817
- this.options.onCommandComplete?.(
818
- event.success!,
819
- event.exitCode!,
820
- event.stdout!,
821
- event.stderr!,
822
- event.command!,
823
- event.args || []
824
- );
825
- break;
826
-
827
- case "error":
828
- console.error(`[HTTP Client] Mkdir error: ${event.error}`);
829
- this.options.onError?.(
830
- event.error!,
831
- event.command,
832
- event.args
833
- );
834
- break;
835
- }
836
- } catch (parseError) {
837
- console.warn(
838
- "[HTTP Client] Failed to parse mkdir stream event:",
839
- parseError
840
- );
841
- }
842
- }
843
- }
844
- }
845
- } finally {
846
- reader.releaseLock();
847
- }
438
+ return data;
848
439
  } catch (error) {
849
- console.error("[HTTP Client] Error in streaming mkdir:", error);
850
- this.options.onError?.(
851
- error instanceof Error ? error.message : "Unknown error",
852
- "mkdir",
853
- recursive ? ["-p", path] : [path]
854
- );
440
+ console.error("[HTTP Client] Error writing file:", error);
855
441
  throw error;
856
442
  }
857
443
  }
858
444
 
859
- async writeFile(
445
+ async readFile(
860
446
  path: string,
861
- content: string,
862
447
  encoding: string = "utf-8",
863
- sessionId?: string
864
- ): Promise<WriteFileResponse> {
448
+ sessionId: string
449
+ ): Promise<ReadFileResponse> {
865
450
  try {
866
- const targetSessionId = sessionId || this.sessionId;
867
-
868
- const response = await this.doFetch(`/api/write`, {
451
+ const response = await this.doFetch(`/api/read`, {
869
452
  body: JSON.stringify({
870
- content,
871
453
  encoding,
872
454
  path,
873
- sessionId: targetSessionId,
874
- }),
455
+ sessionId,
456
+ } as ReadFileRequest),
875
457
  headers: {
876
458
  "Content-Type": "application/json",
877
459
  },
@@ -887,38 +469,32 @@ export class HttpClient {
887
469
  );
888
470
  }
889
471
 
890
- const data: WriteFileResponse = await response.json();
472
+ const data: ReadFileResponse = await response.json();
891
473
  console.log(
892
- `[HTTP Client] File written: ${path}, Success: ${data.success}`
474
+ `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}${sessionId ? ` in session: ${sessionId}` : ''}`
893
475
  );
894
476
 
895
477
  return data;
896
478
  } catch (error) {
897
- console.error("[HTTP Client] Error writing file:", error);
479
+ console.error("[HTTP Client] Error reading file:", error);
898
480
  throw error;
899
481
  }
900
482
  }
901
483
 
902
- async writeFileStream(
484
+ async readFileStream(
903
485
  path: string,
904
- content: string,
905
- encoding: string = "utf-8",
906
- sessionId?: string
907
- ): Promise<void> {
486
+ sessionId: string
487
+ ): Promise<ReadableStream<Uint8Array>> {
908
488
  try {
909
- const targetSessionId = sessionId || this.sessionId;
910
-
911
- const response = await this.doFetch(`/api/write/stream`, {
912
- body: JSON.stringify({
913
- content,
914
- encoding,
915
- path,
916
- sessionId: targetSessionId,
917
- }),
489
+ const response = await this.doFetch(`/api/read/stream`, {
490
+ method: "POST",
918
491
  headers: {
919
492
  "Content-Type": "application/json",
920
493
  },
921
- method: "POST",
494
+ body: JSON.stringify({
495
+ path,
496
+ sessionId,
497
+ } as ReadFileRequest),
922
498
  });
923
499
 
924
500
  if (!response.ok) {
@@ -931,113 +507,29 @@ export class HttpClient {
931
507
  }
932
508
 
933
509
  if (!response.body) {
934
- throw new Error("No response body for streaming request");
510
+ throw new Error("No response body for file streaming");
935
511
  }
936
512
 
937
- const reader = response.body.getReader();
938
- const decoder = new TextDecoder();
939
-
940
- try {
941
- while (true) {
942
- const { done, value } = await reader.read();
943
-
944
- if (done) {
945
- break;
946
- }
947
-
948
- const chunk = decoder.decode(value, { stream: true });
949
- const lines = chunk.split("\n");
950
-
951
- for (const line of lines) {
952
- if (line.startsWith("data: ")) {
953
- try {
954
- const eventData = line.slice(6); // Remove 'data: ' prefix
955
- const event: StreamEvent = JSON.parse(eventData);
956
-
957
- console.log(
958
- `[HTTP Client] Write file stream event: ${event.type}`
959
- );
960
- this.options.onStreamEvent?.(event);
961
-
962
- switch (event.type) {
963
- case "command_start":
964
- console.log(
965
- `[HTTP Client] Write file started: ${event.path}`
966
- );
967
- this.options.onCommandStart?.("write", [
968
- path,
969
- content,
970
- encoding,
971
- ]);
972
- break;
973
-
974
- case "output":
975
- console.log(`[output] ${event.message}`);
976
- this.options.onOutput?.("stdout", event.message!, "write");
977
- break;
978
-
979
- case "command_complete":
980
- console.log(
981
- `[HTTP Client] Write file completed: ${event.path}, Success: ${event.success}`
982
- );
983
- this.options.onCommandComplete?.(
984
- event.success!,
985
- 0,
986
- "",
987
- "",
988
- "write",
989
- [path, content, encoding]
990
- );
991
- break;
992
-
993
- case "error":
994
- console.error(
995
- `[HTTP Client] Write file error: ${event.error}`
996
- );
997
- this.options.onError?.(event.error!, "write", [
998
- path,
999
- content,
1000
- encoding,
1001
- ]);
1002
- break;
1003
- }
1004
- } catch (parseError) {
1005
- console.warn(
1006
- "[HTTP Client] Failed to parse write file stream event:",
1007
- parseError
1008
- );
1009
- }
1010
- }
1011
- }
1012
- }
1013
- } finally {
1014
- reader.releaseLock();
1015
- }
1016
- } catch (error) {
1017
- console.error("[HTTP Client] Error in streaming write file:", error);
1018
- this.options.onError?.(
1019
- error instanceof Error ? error.message : "Unknown error",
1020
- "write",
1021
- [path, content, encoding]
513
+ console.log(
514
+ `[HTTP Client] Started streaming file: ${path}${sessionId ? ` in session: ${sessionId}` : ''}`
1022
515
  );
516
+ return response.body;
517
+ } catch (error) {
518
+ console.error("[HTTP Client] Error streaming file:", error);
1023
519
  throw error;
1024
520
  }
1025
521
  }
1026
522
 
1027
- async readFile(
523
+ async deleteFile(
1028
524
  path: string,
1029
- encoding: string = "utf-8",
1030
- sessionId?: string
1031
- ): Promise<ReadFileResponse> {
525
+ sessionId: string
526
+ ): Promise<DeleteFileResponse> {
1032
527
  try {
1033
- const targetSessionId = sessionId || this.sessionId;
1034
-
1035
- const response = await this.doFetch(`/api/read`, {
528
+ const response = await this.doFetch(`/api/delete`, {
1036
529
  body: JSON.stringify({
1037
- encoding,
1038
530
  path,
1039
- sessionId: targetSessionId,
1040
- }),
531
+ sessionId,
532
+ } as DeleteFileRequest),
1041
533
  headers: {
1042
534
  "Content-Type": "application/json",
1043
535
  },
@@ -1053,32 +545,30 @@ export class HttpClient {
1053
545
  );
1054
546
  }
1055
547
 
1056
- const data: ReadFileResponse = await response.json();
548
+ const data: DeleteFileResponse = await response.json();
1057
549
  console.log(
1058
- `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}`
550
+ `[HTTP Client] File deleted: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1059
551
  );
1060
552
 
1061
553
  return data;
1062
554
  } catch (error) {
1063
- console.error("[HTTP Client] Error reading file:", error);
555
+ console.error("[HTTP Client] Error deleting file:", error);
1064
556
  throw error;
1065
557
  }
1066
558
  }
1067
559
 
1068
- async readFileStream(
1069
- path: string,
1070
- encoding: string = "utf-8",
1071
- sessionId?: string
1072
- ): Promise<void> {
560
+ async renameFile(
561
+ oldPath: string,
562
+ newPath: string,
563
+ sessionId: string
564
+ ): Promise<RenameFileResponse> {
1073
565
  try {
1074
- const targetSessionId = sessionId || this.sessionId;
1075
-
1076
- const response = await this.doFetch(`/api/read/stream`, {
566
+ const response = await this.doFetch(`/api/rename`, {
1077
567
  body: JSON.stringify({
1078
- encoding,
1079
- path,
1080
- sessionId: targetSessionId,
1081
- }),
568
+ newPath,
569
+ oldPath,
570
+ sessionId,
571
+ } as RenameFileRequest),
1082
572
  headers: {
1083
573
  "Content-Type": "application/json",
1084
574
  },
@@ -1094,106 +584,30 @@ export class HttpClient {
1094
584
  );
1095
585
  }
1096
586
 
1097
- if (!response.body) {
1098
- throw new Error("No response body for streaming request");
1099
- }
587
+ const data: RenameFileResponse = await response.json();
588
+ console.log(
589
+ `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
590
+ );
1100
591
 
1101
- const reader = response.body.getReader();
1102
- const decoder = new TextDecoder();
1103
-
1104
- try {
1105
- while (true) {
1106
- const { done, value } = await reader.read();
1107
-
1108
- if (done) {
1109
- break;
1110
- }
1111
-
1112
- const chunk = decoder.decode(value, { stream: true });
1113
- const lines = chunk.split("\n");
1114
-
1115
- for (const line of lines) {
1116
- if (line.startsWith("data: ")) {
1117
- try {
1118
- const eventData = line.slice(6); // Remove 'data: ' prefix
1119
- const event: StreamEvent = JSON.parse(eventData);
1120
-
1121
- console.log(
1122
- `[HTTP Client] Read file stream event: ${event.type}`
1123
- );
1124
- this.options.onStreamEvent?.(event);
1125
-
1126
- switch (event.type) {
1127
- case "command_start":
1128
- console.log(
1129
- `[HTTP Client] Read file started: ${event.path}`
1130
- );
1131
- this.options.onCommandStart?.("read", [path, encoding]);
1132
- break;
1133
-
1134
- case "command_complete":
1135
- console.log(
1136
- `[HTTP Client] Read file completed: ${
1137
- event.path
1138
- }, Success: ${event.success}, Content length: ${
1139
- event.content?.length || 0
1140
- }`
1141
- );
1142
- this.options.onCommandComplete?.(
1143
- event.success!,
1144
- 0,
1145
- event.content || "",
1146
- "",
1147
- "read",
1148
- [path, encoding]
1149
- );
1150
- break;
1151
-
1152
- case "error":
1153
- console.error(
1154
- `[HTTP Client] Read file error: ${event.error}`
1155
- );
1156
- this.options.onError?.(event.error!, "read", [
1157
- path,
1158
- encoding,
1159
- ]);
1160
- break;
1161
- }
1162
- } catch (parseError) {
1163
- console.warn(
1164
- "[HTTP Client] Failed to parse read file stream event:",
1165
- parseError
1166
- );
1167
- }
1168
- }
1169
- }
1170
- }
1171
- } finally {
1172
- reader.releaseLock();
1173
- }
592
+ return data;
1174
593
  } catch (error) {
1175
- console.error("[HTTP Client] Error in streaming read file:", error);
1176
- this.options.onError?.(
1177
- error instanceof Error ? error.message : "Unknown error",
1178
- "read",
1179
- [path, encoding]
1180
- );
594
+ console.error("[HTTP Client] Error renaming file:", error);
1181
595
  throw error;
1182
596
  }
1183
597
  }
1184
598
 
1185
- async deleteFile(
1186
- path: string,
1187
- sessionId?: string
1188
- ): Promise<DeleteFileResponse> {
599
+ async moveFile(
600
+ sourcePath: string,
601
+ destinationPath: string,
602
+ sessionId: string
603
+ ): Promise<MoveFileResponse> {
1189
604
  try {
1190
- const targetSessionId = sessionId || this.sessionId;
1191
-
1192
- const response = await this.doFetch(`/api/delete`, {
605
+ const response = await this.doFetch(`/api/move`, {
1193
606
  body: JSON.stringify({
1194
- path,
1195
- sessionId: targetSessionId,
1196
- }),
607
+ destinationPath,
608
+ sourcePath,
609
+ sessionId,
610
+ } as MoveFileRequest),
1197
611
  headers: {
1198
612
  "Content-Type": "application/json",
1199
613
  },
@@ -1209,27 +623,33 @@ export class HttpClient {
1209
623
  );
1210
624
  }
1211
625
 
1212
- const data: DeleteFileResponse = await response.json();
626
+ const data: MoveFileResponse = await response.json();
1213
627
  console.log(
1214
- `[HTTP Client] File deleted: ${path}, Success: ${data.success}`
628
+ `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1215
629
  );
1216
630
 
1217
631
  return data;
1218
632
  } catch (error) {
1219
- console.error("[HTTP Client] Error deleting file:", error);
633
+ console.error("[HTTP Client] Error moving file:", error);
1220
634
  throw error;
1221
635
  }
1222
636
  }
1223
637
 
1224
- async deleteFileStream(path: string, sessionId?: string): Promise<void> {
638
+ async listFiles(
639
+ path: string,
640
+ sessionId: string,
641
+ options?: {
642
+ recursive?: boolean;
643
+ includeHidden?: boolean;
644
+ }
645
+ ): Promise<ListFilesResponse> {
1225
646
  try {
1226
- const targetSessionId = sessionId || this.sessionId;
1227
-
1228
- const response = await this.doFetch(`/api/delete/stream`, {
647
+ const response = await this.doFetch(`/api/list-files`, {
1229
648
  body: JSON.stringify({
1230
649
  path,
1231
- sessionId: targetSessionId,
1232
- }),
650
+ options,
651
+ sessionId,
652
+ } as ListFilesRequest),
1233
653
  headers: {
1234
654
  "Content-Type": "application/json",
1235
655
  },
@@ -1245,100 +665,24 @@ export class HttpClient {
1245
665
  );
1246
666
  }
1247
667
 
1248
- if (!response.body) {
1249
- throw new Error("No response body for streaming request");
1250
- }
668
+ const data: ListFilesResponse = await response.json();
669
+ console.log(
670
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
671
+ );
1251
672
 
1252
- const reader = response.body.getReader();
1253
- const decoder = new TextDecoder();
1254
-
1255
- try {
1256
- while (true) {
1257
- const { done, value } = await reader.read();
1258
-
1259
- if (done) {
1260
- break;
1261
- }
1262
-
1263
- const chunk = decoder.decode(value, { stream: true });
1264
- const lines = chunk.split("\n");
1265
-
1266
- for (const line of lines) {
1267
- if (line.startsWith("data: ")) {
1268
- try {
1269
- const eventData = line.slice(6); // Remove 'data: ' prefix
1270
- const event: StreamEvent = JSON.parse(eventData);
1271
-
1272
- console.log(
1273
- `[HTTP Client] Delete file stream event: ${event.type}`
1274
- );
1275
- this.options.onStreamEvent?.(event);
1276
-
1277
- switch (event.type) {
1278
- case "command_start":
1279
- console.log(
1280
- `[HTTP Client] Delete file started: ${event.path}`
1281
- );
1282
- this.options.onCommandStart?.("delete", [path]);
1283
- break;
1284
-
1285
- case "command_complete":
1286
- console.log(
1287
- `[HTTP Client] Delete file completed: ${event.path}, Success: ${event.success}`
1288
- );
1289
- this.options.onCommandComplete?.(
1290
- event.success!,
1291
- 0,
1292
- "",
1293
- "",
1294
- "delete",
1295
- [path]
1296
- );
1297
- break;
1298
-
1299
- case "error":
1300
- console.error(
1301
- `[HTTP Client] Delete file error: ${event.error}`
1302
- );
1303
- this.options.onError?.(event.error!, "delete", [path]);
1304
- break;
1305
- }
1306
- } catch (parseError) {
1307
- console.warn(
1308
- "[HTTP Client] Failed to parse delete file stream event:",
1309
- parseError
1310
- );
1311
- }
1312
- }
1313
- }
1314
- }
1315
- } finally {
1316
- reader.releaseLock();
1317
- }
673
+ return data;
1318
674
  } catch (error) {
1319
- console.error("[HTTP Client] Error in streaming delete file:", error);
1320
- this.options.onError?.(
1321
- error instanceof Error ? error.message : "Unknown error",
1322
- "delete",
1323
- [path]
1324
- );
675
+ console.error("[HTTP Client] Error listing files:", error);
1325
676
  throw error;
1326
677
  }
1327
678
  }
1328
679
 
1329
- async renameFile(
1330
- oldPath: string,
1331
- newPath: string,
1332
- sessionId?: string
1333
- ): Promise<RenameFileResponse> {
680
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1334
681
  try {
1335
- const targetSessionId = sessionId || this.sessionId;
1336
-
1337
- const response = await this.doFetch(`/api/rename`, {
682
+ const response = await this.doFetch(`/api/expose-port`, {
1338
683
  body: JSON.stringify({
1339
- newPath,
1340
- oldPath,
1341
- sessionId: targetSessionId,
684
+ port,
685
+ name,
1342
686
  }),
1343
687
  headers: {
1344
688
  "Content-Type": "application/json",
@@ -1350,41 +694,36 @@ export class HttpClient {
1350
694
  const errorData = (await response.json().catch(() => ({}))) as {
1351
695
  error?: string;
1352
696
  };
697
+ console.log(errorData);
1353
698
  throw new Error(
1354
699
  errorData.error || `HTTP error! status: ${response.status}`
1355
700
  );
1356
701
  }
1357
702
 
1358
- const data: RenameFileResponse = await response.json();
703
+ const data: ExposePortResponse = await response.json();
1359
704
  console.log(
1360
- `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}`
705
+ `[HTTP Client] Port exposed: ${port}${
706
+ name ? ` (${name})` : ""
707
+ }, Success: ${data.success}`
1361
708
  );
1362
709
 
1363
710
  return data;
1364
711
  } catch (error) {
1365
- console.error("[HTTP Client] Error renaming file:", error);
712
+ console.error("[HTTP Client] Error exposing port:", error);
1366
713
  throw error;
1367
714
  }
1368
715
  }
1369
716
 
1370
- async renameFileStream(
1371
- oldPath: string,
1372
- newPath: string,
1373
- sessionId?: string
1374
- ): Promise<void> {
717
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1375
718
  try {
1376
- const targetSessionId = sessionId || this.sessionId;
1377
-
1378
- const response = await this.doFetch(`/api/rename/stream`, {
719
+ const response = await this.doFetch(`/api/unexpose-port`, {
1379
720
  body: JSON.stringify({
1380
- newPath,
1381
- oldPath,
1382
- sessionId: targetSessionId,
721
+ port,
1383
722
  }),
1384
723
  headers: {
1385
724
  "Content-Type": "application/json",
1386
725
  },
1387
- method: "POST",
726
+ method: "DELETE",
1388
727
  });
1389
728
 
1390
729
  if (!response.ok) {
@@ -1396,108 +735,25 @@ export class HttpClient {
1396
735
  );
1397
736
  }
1398
737
 
1399
- if (!response.body) {
1400
- throw new Error("No response body for streaming request");
1401
- }
738
+ const data: UnexposePortResponse = await response.json();
739
+ console.log(
740
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
741
+ );
1402
742
 
1403
- const reader = response.body.getReader();
1404
- const decoder = new TextDecoder();
1405
-
1406
- try {
1407
- while (true) {
1408
- const { done, value } = await reader.read();
1409
-
1410
- if (done) {
1411
- break;
1412
- }
1413
-
1414
- const chunk = decoder.decode(value, { stream: true });
1415
- const lines = chunk.split("\n");
1416
-
1417
- for (const line of lines) {
1418
- if (line.startsWith("data: ")) {
1419
- try {
1420
- const eventData = line.slice(6); // Remove 'data: ' prefix
1421
- const event: StreamEvent = JSON.parse(eventData);
1422
-
1423
- console.log(
1424
- `[HTTP Client] Rename file stream event: ${event.type}`
1425
- );
1426
- this.options.onStreamEvent?.(event);
1427
-
1428
- switch (event.type) {
1429
- case "command_start":
1430
- console.log(
1431
- `[HTTP Client] Rename file started: ${event.oldPath} -> ${event.newPath}`
1432
- );
1433
- this.options.onCommandStart?.("rename", [oldPath, newPath]);
1434
- break;
1435
-
1436
- case "command_complete":
1437
- console.log(
1438
- `[HTTP Client] Rename file completed: ${event.oldPath} -> ${event.newPath}, Success: ${event.success}`
1439
- );
1440
- this.options.onCommandComplete?.(
1441
- event.success!,
1442
- 0,
1443
- "",
1444
- "",
1445
- "rename",
1446
- [oldPath, newPath]
1447
- );
1448
- break;
1449
-
1450
- case "error":
1451
- console.error(
1452
- `[HTTP Client] Rename file error: ${event.error}`
1453
- );
1454
- this.options.onError?.(event.error!, "rename", [
1455
- oldPath,
1456
- newPath,
1457
- ]);
1458
- break;
1459
- }
1460
- } catch (parseError) {
1461
- console.warn(
1462
- "[HTTP Client] Failed to parse rename file stream event:",
1463
- parseError
1464
- );
1465
- }
1466
- }
1467
- }
1468
- }
1469
- } finally {
1470
- reader.releaseLock();
1471
- }
743
+ return data;
1472
744
  } catch (error) {
1473
- console.error("[HTTP Client] Error in streaming rename file:", error);
1474
- this.options.onError?.(
1475
- error instanceof Error ? error.message : "Unknown error",
1476
- "rename",
1477
- [oldPath, newPath]
1478
- );
745
+ console.error("[HTTP Client] Error unexposing port:", error);
1479
746
  throw error;
1480
747
  }
1481
748
  }
1482
749
 
1483
- async moveFile(
1484
- sourcePath: string,
1485
- destinationPath: string,
1486
- sessionId?: string
1487
- ): Promise<MoveFileResponse> {
750
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
1488
751
  try {
1489
- const targetSessionId = sessionId || this.sessionId;
1490
-
1491
- const response = await this.doFetch(`/api/move`, {
1492
- body: JSON.stringify({
1493
- destinationPath,
1494
- sessionId: targetSessionId,
1495
- sourcePath,
1496
- }),
752
+ const response = await this.doFetch(`/api/exposed-ports`, {
1497
753
  headers: {
1498
754
  "Content-Type": "application/json",
1499
755
  },
1500
- method: "POST",
756
+ method: "GET",
1501
757
  });
1502
758
 
1503
759
  if (!response.ok) {
@@ -1509,32 +765,59 @@ export class HttpClient {
1509
765
  );
1510
766
  }
1511
767
 
1512
- const data: MoveFileResponse = await response.json();
1513
- console.log(
1514
- `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}`
1515
- );
768
+ const data: GetExposedPortsResponse = await response.json();
769
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
1516
770
 
1517
771
  return data;
1518
772
  } catch (error) {
1519
- console.error("[HTTP Client] Error moving file:", error);
773
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1520
774
  throw error;
1521
775
  }
1522
776
  }
1523
777
 
1524
- async moveFileStream(
1525
- sourcePath: string,
1526
- destinationPath: string,
1527
- sessionId?: string
1528
- ): Promise<void> {
778
+ async ping(): Promise<string> {
1529
779
  try {
1530
- const targetSessionId = sessionId || this.sessionId;
780
+ const response = await this.doFetch(`/api/ping`, {
781
+ headers: {
782
+ "Content-Type": "application/json",
783
+ },
784
+ method: "GET",
785
+ });
1531
786
 
1532
- const response = await this.doFetch(`/api/move/stream`, {
787
+ if (!response.ok) {
788
+ throw new Error(`HTTP error! status: ${response.status}`);
789
+ }
790
+
791
+ const data: PingResponse = await response.json();
792
+ console.log(`[HTTP Client] Ping response: ${data.message}`);
793
+ return data.timestamp;
794
+ } catch (error) {
795
+ console.error("[HTTP Client] Error pinging server:", error);
796
+ throw error;
797
+ }
798
+ }
799
+
800
+
801
+ // Process management methods
802
+ async startProcess(
803
+ command: string,
804
+ sessionId: string,
805
+ options?: {
806
+ processId?: string;
807
+ timeout?: number;
808
+ env?: Record<string, string>;
809
+ cwd?: string;
810
+ encoding?: string;
811
+ autoCleanup?: boolean;
812
+ }
813
+ ): Promise<StartProcessResponse> {
814
+ try {
815
+ const response = await this.doFetch("/api/process/start", {
1533
816
  body: JSON.stringify({
1534
- destinationPath,
1535
- sessionId: targetSessionId,
1536
- sourcePath,
1537
- }),
817
+ command,
818
+ sessionId,
819
+ options,
820
+ } as StartProcessRequest),
1538
821
  headers: {
1539
822
  "Content-Type": "application/json",
1540
823
  },
@@ -1550,96 +833,24 @@ export class HttpClient {
1550
833
  );
1551
834
  }
1552
835
 
1553
- if (!response.body) {
1554
- throw new Error("No response body for streaming request");
1555
- }
836
+ const data: StartProcessResponse = await response.json();
837
+ console.log(
838
+ `[HTTP Client] Process started: ${command}, ID: ${data.process.id}`
839
+ );
1556
840
 
1557
- const reader = response.body.getReader();
1558
- const decoder = new TextDecoder();
1559
-
1560
- try {
1561
- while (true) {
1562
- const { done, value } = await reader.read();
1563
-
1564
- if (done) {
1565
- break;
1566
- }
1567
-
1568
- const chunk = decoder.decode(value, { stream: true });
1569
- const lines = chunk.split("\n");
1570
-
1571
- for (const line of lines) {
1572
- if (line.startsWith("data: ")) {
1573
- try {
1574
- const eventData = line.slice(6); // Remove 'data: ' prefix
1575
- const event: StreamEvent = JSON.parse(eventData);
1576
-
1577
- console.log(
1578
- `[HTTP Client] Move file stream event: ${event.type}`
1579
- );
1580
- this.options.onStreamEvent?.(event);
1581
-
1582
- switch (event.type) {
1583
- case "command_start":
1584
- console.log(
1585
- `[HTTP Client] Move file started: ${event.sourcePath} -> ${event.destinationPath}`
1586
- );
1587
- this.options.onCommandStart?.("move", [
1588
- sourcePath,
1589
- destinationPath,
1590
- ]);
1591
- break;
1592
-
1593
- case "command_complete":
1594
- console.log(
1595
- `[HTTP Client] Move file completed: ${event.sourcePath} -> ${event.destinationPath}, Success: ${event.success}`
1596
- );
1597
- this.options.onCommandComplete?.(
1598
- event.success!,
1599
- 0,
1600
- "",
1601
- "",
1602
- "move",
1603
- [sourcePath, destinationPath]
1604
- );
1605
- break;
1606
-
1607
- case "error":
1608
- console.error(
1609
- `[HTTP Client] Move file error: ${event.error}`
1610
- );
1611
- this.options.onError?.(event.error!, "move", [
1612
- sourcePath,
1613
- destinationPath,
1614
- ]);
1615
- break;
1616
- }
1617
- } catch (parseError) {
1618
- console.warn(
1619
- "[HTTP Client] Failed to parse move file stream event:",
1620
- parseError
1621
- );
1622
- }
1623
- }
1624
- }
1625
- }
1626
- } finally {
1627
- reader.releaseLock();
1628
- }
841
+ return data;
1629
842
  } catch (error) {
1630
- console.error("[HTTP Client] Error in streaming move file:", error);
1631
- this.options.onError?.(
1632
- error instanceof Error ? error.message : "Unknown error",
1633
- "move",
1634
- [sourcePath, destinationPath]
1635
- );
843
+ console.error("[HTTP Client] Error starting process:", error);
1636
844
  throw error;
1637
845
  }
1638
846
  }
1639
847
 
1640
- async ping(): Promise<string> {
848
+ async listProcesses(sessionId?: string): Promise<ListProcessesResponse> {
1641
849
  try {
1642
- const response = await this.doFetch(`/api/ping`, {
850
+ const url = sessionId
851
+ ? `/api/process/list?session=${encodeURIComponent(sessionId)}`
852
+ : "/api/process/list";
853
+ const response = await this.doFetch(url, {
1643
854
  headers: {
1644
855
  "Content-Type": "application/json",
1645
856
  },
@@ -1647,21 +858,27 @@ export class HttpClient {
1647
858
  });
1648
859
 
1649
860
  if (!response.ok) {
1650
- throw new Error(`HTTP error! status: ${response.status}`);
861
+ const errorData = (await response.json().catch(() => ({}))) as {
862
+ error?: string;
863
+ };
864
+ throw new Error(
865
+ errorData.error || `HTTP error! status: ${response.status}`
866
+ );
1651
867
  }
1652
868
 
1653
- const data: PingResponse = await response.json();
1654
- console.log(`[HTTP Client] Ping response: ${data.message}`);
1655
- return data.timestamp;
869
+ const data: ListProcessesResponse = await response.json();
870
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
871
+
872
+ return data;
1656
873
  } catch (error) {
1657
- console.error("[HTTP Client] Error pinging server:", error);
874
+ console.error("[HTTP Client] Error listing processes:", error);
1658
875
  throw error;
1659
876
  }
1660
877
  }
1661
878
 
1662
- async getCommands(): Promise<string[]> {
879
+ async getProcess(processId: string): Promise<GetProcessResponse> {
1663
880
  try {
1664
- const response = await fetch(`${this.baseUrl}/api/commands`, {
881
+ const response = await this.doFetch(`/api/process/${processId}`, {
1665
882
  headers: {
1666
883
  "Content-Type": "application/json",
1667
884
  },
@@ -1669,292 +886,163 @@ export class HttpClient {
1669
886
  });
1670
887
 
1671
888
  if (!response.ok) {
1672
- throw new Error(`HTTP error! status: ${response.status}`);
889
+ const errorData = (await response.json().catch(() => ({}))) as {
890
+ error?: string;
891
+ };
892
+ throw new Error(
893
+ errorData.error || `HTTP error! status: ${response.status}`
894
+ );
1673
895
  }
1674
896
 
1675
- const data: CommandsResponse = await response.json();
897
+ const data: GetProcessResponse = await response.json();
1676
898
  console.log(
1677
- `[HTTP Client] Available commands: ${data.availableCommands.length}`
899
+ `[HTTP Client] Got process ${processId}: ${
900
+ data.process?.status || "not found"
901
+ }`
1678
902
  );
1679
- return data.availableCommands;
903
+
904
+ return data;
1680
905
  } catch (error) {
1681
- console.error("[HTTP Client] Error getting commands:", error);
906
+ console.error("[HTTP Client] Error getting process:", error);
1682
907
  throw error;
1683
908
  }
1684
909
  }
1685
910
 
1686
- getSessionId(): string | null {
1687
- return this.sessionId;
1688
- }
1689
-
1690
- setSessionId(sessionId: string): void {
1691
- this.sessionId = sessionId;
1692
- }
1693
-
1694
- clearSession(): void {
1695
- this.sessionId = null;
1696
- }
1697
- }
1698
-
1699
- // Example usage and utility functions
1700
- export function createClient(options?: HttpClientOptions): HttpClient {
1701
- return new HttpClient(options);
1702
- }
911
+ async killProcess(
912
+ processId: string
913
+ ): Promise<{ success: boolean; message: string }> {
914
+ try {
915
+ const response = await this.doFetch(`/api/process/${processId}`, {
916
+ headers: {
917
+ "Content-Type": "application/json",
918
+ },
919
+ method: "DELETE",
920
+ });
1703
921
 
1704
- // Convenience function for quick command execution
1705
- export async function quickExecute(
1706
- command: string,
1707
- args: string[] = [],
1708
- options?: HttpClientOptions
1709
- ): Promise<ExecuteResponse> {
1710
- const client = createClient(options);
1711
- await client.createSession();
1712
-
1713
- try {
1714
- return await client.execute(command, args);
1715
- } finally {
1716
- client.clearSession();
1717
- }
1718
- }
922
+ if (!response.ok) {
923
+ const errorData = (await response.json().catch(() => ({}))) as {
924
+ error?: string;
925
+ };
926
+ throw new Error(
927
+ errorData.error || `HTTP error! status: ${response.status}`
928
+ );
929
+ }
1719
930
 
1720
- // Convenience function for quick streaming command execution
1721
- export async function quickExecuteStream(
1722
- command: string,
1723
- args: string[] = [],
1724
- options?: HttpClientOptions
1725
- ): Promise<void> {
1726
- const client = createClient(options);
1727
- await client.createSession();
1728
-
1729
- try {
1730
- await client.executeStream(command, args);
1731
- } finally {
1732
- client.clearSession();
1733
- }
1734
- }
931
+ const data = (await response.json()) as {
932
+ success: boolean;
933
+ message: string;
934
+ };
935
+ console.log(`[HTTP Client] Killed process ${processId}`);
1735
936
 
1736
- // Convenience function for quick git checkout
1737
- export async function quickGitCheckout(
1738
- repoUrl: string,
1739
- branch: string = "main",
1740
- targetDir?: string,
1741
- options?: HttpClientOptions
1742
- ): Promise<GitCheckoutResponse> {
1743
- const client = createClient(options);
1744
- await client.createSession();
1745
-
1746
- try {
1747
- return await client.gitCheckout(repoUrl, branch, targetDir);
1748
- } finally {
1749
- client.clearSession();
937
+ return data;
938
+ } catch (error) {
939
+ console.error("[HTTP Client] Error killing process:", error);
940
+ throw error;
941
+ }
1750
942
  }
1751
- }
1752
943
 
1753
- // Convenience function for quick directory creation
1754
- export async function quickMkdir(
1755
- path: string,
1756
- recursive: boolean = false,
1757
- options?: HttpClientOptions
1758
- ): Promise<MkdirResponse> {
1759
- const client = createClient(options);
1760
- await client.createSession();
1761
-
1762
- try {
1763
- return await client.mkdir(path, recursive);
1764
- } finally {
1765
- client.clearSession();
1766
- }
1767
- }
944
+ async killAllProcesses(sessionId?: string): Promise<{
945
+ success: boolean;
946
+ killedCount: number;
947
+ message: string;
948
+ }> {
949
+ try {
950
+ const url = sessionId
951
+ ? `/api/process/kill-all?session=${encodeURIComponent(sessionId)}`
952
+ : "/api/process/kill-all";
953
+ const response = await this.doFetch(url, {
954
+ headers: {
955
+ "Content-Type": "application/json",
956
+ },
957
+ method: "DELETE",
958
+ });
1768
959
 
1769
- // Convenience function for quick streaming git checkout
1770
- export async function quickGitCheckoutStream(
1771
- repoUrl: string,
1772
- branch: string = "main",
1773
- targetDir?: string,
1774
- options?: HttpClientOptions
1775
- ): Promise<void> {
1776
- const client = createClient(options);
1777
- await client.createSession();
1778
-
1779
- try {
1780
- await client.gitCheckoutStream(repoUrl, branch, targetDir);
1781
- } finally {
1782
- client.clearSession();
1783
- }
1784
- }
960
+ if (!response.ok) {
961
+ const errorData = (await response.json().catch(() => ({}))) as {
962
+ error?: string;
963
+ };
964
+ throw new Error(
965
+ errorData.error || `HTTP error! status: ${response.status}`
966
+ );
967
+ }
1785
968
 
1786
- // Convenience function for quick streaming directory creation
1787
- export async function quickMkdirStream(
1788
- path: string,
1789
- recursive: boolean = false,
1790
- options?: HttpClientOptions
1791
- ): Promise<void> {
1792
- const client = createClient(options);
1793
- await client.createSession();
1794
-
1795
- try {
1796
- await client.mkdirStream(path, recursive);
1797
- } finally {
1798
- client.clearSession();
1799
- }
1800
- }
969
+ const data = (await response.json()) as {
970
+ success: boolean;
971
+ killedCount: number;
972
+ message: string;
973
+ };
974
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
1801
975
 
1802
- // Convenience function for quick file writing
1803
- export async function quickWriteFile(
1804
- path: string,
1805
- content: string,
1806
- encoding: string = "utf-8",
1807
- options?: HttpClientOptions
1808
- ): Promise<WriteFileResponse> {
1809
- const client = createClient(options);
1810
- await client.createSession();
1811
-
1812
- try {
1813
- return await client.writeFile(path, content, encoding);
1814
- } finally {
1815
- client.clearSession();
976
+ return data;
977
+ } catch (error) {
978
+ console.error("[HTTP Client] Error killing all processes:", error);
979
+ throw error;
980
+ }
1816
981
  }
1817
- }
1818
982
 
1819
- // Convenience function for quick streaming file writing
1820
- export async function quickWriteFileStream(
1821
- path: string,
1822
- content: string,
1823
- encoding: string = "utf-8",
1824
- options?: HttpClientOptions
1825
- ): Promise<void> {
1826
- const client = createClient(options);
1827
- await client.createSession();
1828
-
1829
- try {
1830
- await client.writeFileStream(path, content, encoding);
1831
- } finally {
1832
- client.clearSession();
1833
- }
1834
- }
983
+ async getProcessLogs(processId: string): Promise<GetProcessLogsResponse> {
984
+ try {
985
+ const response = await this.doFetch(`/api/process/${processId}/logs`, {
986
+ headers: {
987
+ "Content-Type": "application/json",
988
+ },
989
+ method: "GET",
990
+ });
1835
991
 
1836
- // Convenience function for quick file reading
1837
- export async function quickReadFile(
1838
- path: string,
1839
- encoding: string = "utf-8",
1840
- options?: HttpClientOptions
1841
- ): Promise<ReadFileResponse> {
1842
- const client = createClient(options);
1843
- await client.createSession();
1844
-
1845
- try {
1846
- return await client.readFile(path, encoding);
1847
- } finally {
1848
- client.clearSession();
1849
- }
1850
- }
992
+ if (!response.ok) {
993
+ const errorData = (await response.json().catch(() => ({}))) as {
994
+ error?: string;
995
+ };
996
+ throw new Error(
997
+ errorData.error || `HTTP error! status: ${response.status}`
998
+ );
999
+ }
1851
1000
 
1852
- // Convenience function for quick streaming file reading
1853
- export async function quickReadFileStream(
1854
- path: string,
1855
- encoding: string = "utf-8",
1856
- options?: HttpClientOptions
1857
- ): Promise<void> {
1858
- const client = createClient(options);
1859
- await client.createSession();
1860
-
1861
- try {
1862
- await client.readFileStream(path, encoding);
1863
- } finally {
1864
- client.clearSession();
1865
- }
1866
- }
1001
+ const data: GetProcessLogsResponse = await response.json();
1002
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
1867
1003
 
1868
- // Convenience function for quick file deletion
1869
- export async function quickDeleteFile(
1870
- path: string,
1871
- options?: HttpClientOptions
1872
- ): Promise<DeleteFileResponse> {
1873
- const client = createClient(options);
1874
- await client.createSession();
1875
-
1876
- try {
1877
- return await client.deleteFile(path);
1878
- } finally {
1879
- client.clearSession();
1004
+ return data;
1005
+ } catch (error) {
1006
+ console.error("[HTTP Client] Error getting process logs:", error);
1007
+ throw error;
1008
+ }
1880
1009
  }
1881
- }
1882
1010
 
1883
- // Convenience function for quick streaming file deletion
1884
- export async function quickDeleteFileStream(
1885
- path: string,
1886
- options?: HttpClientOptions
1887
- ): Promise<void> {
1888
- const client = createClient(options);
1889
- await client.createSession();
1890
-
1891
- try {
1892
- await client.deleteFileStream(path);
1893
- } finally {
1894
- client.clearSession();
1895
- }
1896
- }
1011
+ async streamProcessLogs(
1012
+ processId: string,
1013
+ options?: { signal?: AbortSignal }
1014
+ ): Promise<ReadableStream<Uint8Array>> {
1015
+ try {
1016
+ const response = await this.doFetch(`/api/process/${processId}/stream`, {
1017
+ headers: {
1018
+ Accept: "text/event-stream",
1019
+ "Cache-Control": "no-cache",
1020
+ },
1021
+ method: "GET",
1022
+ signal: options?.signal,
1023
+ });
1897
1024
 
1898
- // Convenience function for quick file renaming
1899
- export async function quickRenameFile(
1900
- oldPath: string,
1901
- newPath: string,
1902
- options?: HttpClientOptions
1903
- ): Promise<RenameFileResponse> {
1904
- const client = createClient(options);
1905
- await client.createSession();
1906
-
1907
- try {
1908
- return await client.renameFile(oldPath, newPath);
1909
- } finally {
1910
- client.clearSession();
1911
- }
1912
- }
1025
+ if (!response.ok) {
1026
+ const errorData = (await response.json().catch(() => ({}))) as {
1027
+ error?: string;
1028
+ };
1029
+ throw new Error(
1030
+ errorData.error || `HTTP error! status: ${response.status}`
1031
+ );
1032
+ }
1913
1033
 
1914
- // Convenience function for quick streaming file renaming
1915
- export async function quickRenameFileStream(
1916
- oldPath: string,
1917
- newPath: string,
1918
- options?: HttpClientOptions
1919
- ): Promise<void> {
1920
- const client = createClient(options);
1921
- await client.createSession();
1922
-
1923
- try {
1924
- await client.renameFileStream(oldPath, newPath);
1925
- } finally {
1926
- client.clearSession();
1927
- }
1928
- }
1034
+ if (!response.body) {
1035
+ throw new Error("No response body for streaming request");
1036
+ }
1929
1037
 
1930
- // Convenience function for quick file moving
1931
- export async function quickMoveFile(
1932
- sourcePath: string,
1933
- destinationPath: string,
1934
- options?: HttpClientOptions
1935
- ): Promise<MoveFileResponse> {
1936
- const client = createClient(options);
1937
- await client.createSession();
1938
-
1939
- try {
1940
- return await client.moveFile(sourcePath, destinationPath);
1941
- } finally {
1942
- client.clearSession();
1943
- }
1944
- }
1038
+ console.log(
1039
+ `[HTTP Client] Started streaming logs for process ${processId}`
1040
+ );
1945
1041
 
1946
- // Convenience function for quick streaming file moving
1947
- export async function quickMoveFileStream(
1948
- sourcePath: string,
1949
- destinationPath: string,
1950
- options?: HttpClientOptions
1951
- ): Promise<void> {
1952
- const client = createClient(options);
1953
- await client.createSession();
1954
-
1955
- try {
1956
- await client.moveFileStream(sourcePath, destinationPath);
1957
- } finally {
1958
- client.clearSession();
1042
+ return response.body;
1043
+ } catch (error) {
1044
+ console.error("[HTTP Client] Error streaming process logs:", error);
1045
+ throw error;
1046
+ }
1959
1047
  }
1960
1048
  }