@cloudflare/sandbox 0.0.0-444d2da → 0.0.0-46eb4e6

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